diff --git a/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/model/Result.java b/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/model/Result.java index 0d3f684..468f023 100644 --- a/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/model/Result.java +++ b/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/model/Result.java @@ -25,4 +25,10 @@ public class Result { public void instruction(String instruction) { this.agentInstruction = instruction; } + + public static Result failure(String errorMessage) { + Result result = of(null); + result.instruction("VALIDATION_ERROR: " + errorMessage); + return result; + } } diff --git a/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/reward/client/PoolDefinitionClient.java b/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/reward/client/PoolDefinitionClient.java index 5dd5cf6..1a4bb19 100644 --- a/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/reward/client/PoolDefinitionClient.java +++ b/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/reward/client/PoolDefinitionClient.java @@ -15,4 +15,7 @@ public interface PoolDefinitionClient { @GetExchange("/api/pool-definition/list-by-ids") List findActiveByIds(@RequestParam("idList") List idList); + + @GetExchange("/api/pool-definition/csr/list") + List getAll(dev.sonpx.loyalty.mcp.reward.criteria.PoolDefinitionCriteria criteria, org.springframework.data.domain.Pageable pageable); } diff --git a/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/reward/criteria/CampaignCriteria.java b/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/reward/criteria/CampaignCriteria.java index b31e576..8123cce 100644 --- a/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/reward/criteria/CampaignCriteria.java +++ b/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/reward/criteria/CampaignCriteria.java @@ -5,7 +5,6 @@ package dev.sonpx.loyalty.mcp.reward.criteria; import dev.sonpx.loyalty.mcp.criteria.AbstractFwCriteria; import dev.sonpx.loyalty.mcp.filter.StringFilter; import lombok.Data; -import com.fasterxml.jackson.annotation.JsonPropertyDescription; /** * Created by SonPhung on Monday, 20-Jul-2026 @@ -13,18 +12,13 @@ import com.fasterxml.jackson.annotation.JsonPropertyDescription; @Data public class CampaignCriteria extends AbstractFwCriteria { - @JsonPropertyDescription("Bộ lọc theo Mã chiến dịch (bằng chính xác, chứa từ khóa, v.v.)") private StringFilter campaignId; - @JsonPropertyDescription("Bộ lọc theo Tên người/phòng ban sở hữu chiến dịch") private StringFilter ownerName; - @JsonPropertyDescription("Bộ lọc theo Tên chiến dịch") private StringFilter name; - @JsonPropertyDescription("Bộ lọc theo Mô tả của chiến dịch") private StringFilter description; - @JsonPropertyDescription("Bộ lọc theo Loại chiến dịch (ví dụ: POINT, VOUCHER, v.v.)") private StringFilter campaignType; } diff --git a/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/service/PoolDefinitionService.java b/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/service/PoolDefinitionService.java index 4c3ee29..5edc1ff 100644 --- a/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/service/PoolDefinitionService.java +++ b/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/service/PoolDefinitionService.java @@ -35,4 +35,12 @@ public class PoolDefinitionService { Không dùng markdown table. """); } + + public Result> getAll(dev.sonpx.loyalty.mcp.reward.criteria.PoolDefinitionCriteria criteria) { + List pools = poolDefinitionClient.getAll(criteria, org.springframework.data.domain.Pageable.ofSize(10)); + return Result.of(pools, """ + Trả lời tự nhiên bằng tiếng Việt. Hiển thị danh sách các Pool, mỗi pool ghi: Tên, Mã, Loại Pool. + Không dùng markdown table. + """); + } } diff --git a/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/tool/CampaignRuleTools.java b/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/tool/CampaignRuleTools.java index ed56589..dacb755 100644 --- a/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/tool/CampaignRuleTools.java +++ b/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/tool/CampaignRuleTools.java @@ -23,13 +23,58 @@ public class CampaignRuleTools { @McpTool(description = """ TÌM KIẾM THỂ LỆ / RULE của chiến dịch. Trả về: tên rule, loại rule, chiến dịch gốc, công thức thưởng. - - Param `search` (String): từ khóa tìm kiếm theo tên hoặc mã rule cụ thể. - - QUAN TRỌNG: Khi người dùng hỏi lấy danh sách chung ("danh sách rule", "xem thể lệ"), KHÔNG truyền param `search` (để null hoặc rỗng ""). Tuyệt đối KHÔNG truyền các từ chung như "rule", "thể lệ", "danh sách" vào `search`. + - Param `request`: đối tượng chứa các điều kiện lọc (có thể để null nếu không cần lọc). + - QUAN TRỌNG: Khi người dùng hỏi lấy danh sách chung ("danh sách rule", "xem thể lệ"), KHÔNG truyền điều kiện nào. - KHÔNG dùng tool này khi người dùng chỉ hỏi về thông tin cơ bản chiến dịch (tên, owner, loại) → hãy dùng tool searchCampaigns. Hiển thị tối đa 10 bản ghi.""") - public Result> searchRules(String search) { + public Result> searchRules(dev.sonpx.loyalty.mcp.tool.request.CampaignRuleSearchRequest request) { CampaignRuleCriteria criteria = new CampaignRuleCriteria(); - criteria.setSearch(SearchUtils.sanitizeSearch(search)); + if (request != null) { + if (request.campaignId() != null) { + dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter(); + filter.setEquals(request.campaignId()); + criteria.setCampaignId(filter); + } + if (request.ruleId() != null) { + dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter(); + filter.setEquals(request.ruleId()); + criteria.setRuleId(filter); + } + if (request.ruleName() != null) { + dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter(); + filter.setContains(request.ruleName()); + criteria.setRuleName(filter); + } + if (request.description() != null) { + dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter(); + filter.setContains(request.description()); + criteria.setDescription(filter); + } + if (request.ruleType() != null) { + dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter(); + filter.setEquals(request.ruleType()); + criteria.setRuleType(filter); + } + if (request.poolId() != null) { + dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter(); + filter.setEquals(request.poolId()); + criteria.setPoolId(filter); + } + try { + if (request.effectiveFrom() != null) { + dev.sonpx.loyalty.mcp.filter.LocalDateTimeFilter filter = new dev.sonpx.loyalty.mcp.filter.LocalDateTimeFilter(); + filter.setGreaterThanOrEqual(java.time.LocalDateTime.parse(request.effectiveFrom())); + criteria.setEffectiveFrom(filter); + } + if (request.effectiveTo() != null) { + dev.sonpx.loyalty.mcp.filter.LocalDateTimeFilter filter = new dev.sonpx.loyalty.mcp.filter.LocalDateTimeFilter(); + filter.setLessThanOrEqual(java.time.LocalDateTime.parse(request.effectiveTo())); + criteria.setEffectiveTo(filter); + } + } catch (java.time.format.DateTimeParseException e) { + return Result.failure("Invalid date format. Please use ISO-8601 format (e.g. '2026-07-27T10:00:00')."); + } + } return campaignRuleService.getAll(criteria); } diff --git a/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/tool/CampaignTools.java b/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/tool/CampaignTools.java index c3f8b77..28cd45d 100644 --- a/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/tool/CampaignTools.java +++ b/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/tool/CampaignTools.java @@ -23,12 +23,38 @@ public class CampaignTools { @McpTool(description = """ TÌM KIẾM CHIẾN DỊCH (Campaign). Trả về thông tin CƠ BẢN của chiến dịch: tên, mã, owner, loại, thời gian. - - Param `search` (String): từ khóa tìm kiếm theo tên hoặc mã chiến dịch cụ thể. - - QUAN TRỌNG: Khi người dùng hỏi lấy danh sách chung ("danh sách chiến dịch", "xem các chiến dịch"), KHÔNG truyền param `search` (để null hoặc rỗng ""). Tuyệt đối KHÔNG truyền các từ chung như "chiến dịch", "danh sách", "campaign" vào `search`. + - Param `request`: đối tượng chứa các điều kiện lọc (có thể để null nếu không cần lọc). + - QUAN TRỌNG: Khi người dùng hỏi lấy danh sách chung ("danh sách chiến dịch", "xem các chiến dịch"), KHÔNG truyền filter nào. - KHÔNG dùng tool này khi người dùng hỏi về: rule, thể lệ, quy tắc, điều kiện, công thức thưởng → hãy dùng tool searchRules.""") - public Result> searchCampaigns(String search) { + public Result> searchCampaigns(dev.sonpx.loyalty.mcp.tool.request.CampaignSearchRequest request) { CampaignCriteria criteria = new CampaignCriteria(); - criteria.setSearch(SearchUtils.sanitizeSearch(search)); + if (request != null) { + if (request.campaignId() != null) { + dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter(); + filter.setContains(request.campaignId()); + criteria.setCampaignId(filter); + } + if (request.ownerName() != null) { + dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter(); + filter.setContains(request.ownerName()); + criteria.setOwnerName(filter); + } + if (request.name() != null) { + dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter(); + filter.setContains(request.name()); + criteria.setName(filter); + } + if (request.description() != null) { + dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter(); + filter.setContains(request.description()); + criteria.setDescription(filter); + } + if (request.campaignType() != null) { + dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter(); + filter.setEquals(request.campaignType()); + criteria.setCampaignType(filter); + } + } return campaignService.getAll(criteria); } diff --git a/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/tool/PoolDefinitionTools.java b/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/tool/PoolDefinitionTools.java index e0ff8d0..03275cb 100644 --- a/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/tool/PoolDefinitionTools.java +++ b/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/tool/PoolDefinitionTools.java @@ -23,4 +23,42 @@ public class PoolDefinitionTools { public Result> getPoolDefinitionsByIds(String ids) { return poolDefinitionService.findActiveByIds(ids); } + + @McpTool(description = "TÌM KIẾM Point Pool. Trả về thông tin của pool. Param `request`: đối tượng chứa các điều kiện lọc (có thể để null nếu không cần lọc).") + public Result> searchPools(dev.sonpx.loyalty.mcp.tool.request.PoolDefinitionSearchRequest request) { + dev.sonpx.loyalty.mcp.reward.criteria.PoolDefinitionCriteria criteria = new dev.sonpx.loyalty.mcp.reward.criteria.PoolDefinitionCriteria(); + if (request != null) { + if (request.poolId() != null) { + dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter(); + filter.setEquals(request.poolId()); + criteria.setPoolId(filter); + } + if (request.poolName() != null) { + dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter(); + filter.setContains(request.poolName()); + criteria.setPoolName(filter); + } + if (request.poolType() != null) { + dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter(); + filter.setEquals(request.poolType()); + criteria.setPoolType(filter); + } + if (request.entityLevel() != null) { + dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter(); + filter.setEquals(request.entityLevel()); + criteria.setEntityLevel(filter); + } + if (request.expiryPolicy() != null) { + dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter(); + filter.setEquals(request.expiryPolicy()); + criteria.setExpiryPolicy(filter); + } + if (request.atgGroupId() != null) { + dev.sonpx.loyalty.mcp.filter.StringFilter filter = new dev.sonpx.loyalty.mcp.filter.StringFilter(); + filter.setEquals(request.atgGroupId()); + criteria.setAtgGroupId(filter); + } + } + return poolDefinitionService.getAll(criteria); + } } diff --git a/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/tool/request/CampaignRuleSearchRequest.java b/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/tool/request/CampaignRuleSearchRequest.java new file mode 100644 index 0000000..4ea17d5 --- /dev/null +++ b/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/tool/request/CampaignRuleSearchRequest.java @@ -0,0 +1,29 @@ +package dev.sonpx.loyalty.mcp.tool.request; + +import com.fasterxml.jackson.annotation.JsonPropertyDescription; + +public record CampaignRuleSearchRequest( + @JsonPropertyDescription("Search by Campaign ID (exact match). Leave null if not filtering by Campaign ID.") + String campaignId, + + @JsonPropertyDescription("Search by Rule ID (exact match). Leave null if not filtering by Rule ID.") + String ruleId, + + @JsonPropertyDescription("Search by Rule Name (partial or exact match). Leave null if not filtering by Rule Name.") + String ruleName, + + @JsonPropertyDescription("Search by Rule Description (partial or exact match). Leave null if not filtering by Description.") + String description, + + @JsonPropertyDescription("Filter rules effective from this date/time (ISO-8601 format, e.g., '2026-07-27T10:00:00'). Leave null if not filtering by effective from.") + String effectiveFrom, + + @JsonPropertyDescription("Filter rules effective to this date/time (ISO-8601 format, e.g., '2026-07-27T10:00:00'). Leave null if not filtering by effective to.") + String effectiveTo, + + @JsonPropertyDescription("Search by Rule Type (exact match). Leave null if not filtering by Rule Type.") + String ruleType, + + @JsonPropertyDescription("Search by Pool ID (exact match). Leave null if not filtering by Pool ID.") + String poolId +) {} diff --git a/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/tool/request/CampaignSearchRequest.java b/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/tool/request/CampaignSearchRequest.java new file mode 100644 index 0000000..0bf9bad --- /dev/null +++ b/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/tool/request/CampaignSearchRequest.java @@ -0,0 +1,20 @@ +package dev.sonpx.loyalty.mcp.tool.request; + +import com.fasterxml.jackson.annotation.JsonPropertyDescription; + +public record CampaignSearchRequest( + @JsonPropertyDescription("Search by Campaign ID (partial or exact match). Leave null if not filtering by ID.") + String campaignId, + + @JsonPropertyDescription("Search by Owner Name/Department (partial or exact match). Leave null if not filtering by owner.") + String ownerName, + + @JsonPropertyDescription("Search by Campaign Name (partial or exact match). Leave null if not filtering by name.") + String name, + + @JsonPropertyDescription("Search by Campaign Description (partial or exact match). Leave null if not filtering by description.") + String description, + + @JsonPropertyDescription("Search by Campaign Type (exact match, e.g. POINT, VOUCHER). Leave null if not filtering by type.") + String campaignType +) {} diff --git a/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/tool/request/PoolDefinitionSearchRequest.java b/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/tool/request/PoolDefinitionSearchRequest.java new file mode 100644 index 0000000..0d85039 --- /dev/null +++ b/loyalty-mcp-server/src/main/java/dev/sonpx/loyalty/mcp/tool/request/PoolDefinitionSearchRequest.java @@ -0,0 +1,23 @@ +package dev.sonpx.loyalty.mcp.tool.request; + +import com.fasterxml.jackson.annotation.JsonPropertyDescription; + +public record PoolDefinitionSearchRequest( + @JsonPropertyDescription("Search by Pool ID (exact match). Leave null if not filtering by Pool ID.") + String poolId, + + @JsonPropertyDescription("Search by Pool Name (partial or exact match). Leave null if not filtering by Pool Name.") + String poolName, + + @JsonPropertyDescription("Search by Pool Type (exact match). Leave null if not filtering by Pool Type.") + String poolType, + + @JsonPropertyDescription("Search by Entity Level (exact match). Leave null if not filtering by Entity Level.") + String entityLevel, + + @JsonPropertyDescription("Search by Expiry Policy (exact match). Leave null if not filtering by Expiry Policy.") + String expiryPolicy, + + @JsonPropertyDescription("Search by ATG Group ID (exact match). Leave null if not filtering by ATG Group ID.") + String atgGroupId +) {} diff --git a/start-all.ps1 b/start-all.ps1 index 6c13f59..3760eba 100644 --- a/start-all.ps1 +++ b/start-all.ps1 @@ -6,6 +6,8 @@ if (-not (Test-Path $LogDir)) { New-Item -ItemType Directory -Force -Path $LogDir | Out-Null } +$env:JAVA_TOOL_OPTIONS = "-Duser.timezone=Asia/Ho_Chi_Minh" + if (Test-Path (Join-Path $ProjectRoot ".env")) { Write-Host "Loading environment variables from .env" Get-Content (Join-Path $ProjectRoot ".env") | Where-Object { $_ -match "^[^#]" -and $_ -match "=" } | ForEach-Object {