@open-loyalty/mcp-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +654 -0
  3. package/dist/client/http.d.ts +8 -0
  4. package/dist/client/http.js +69 -0
  5. package/dist/config.d.ts +17 -0
  6. package/dist/config.js +40 -0
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.js +20 -0
  9. package/dist/server.d.ts +4 -0
  10. package/dist/server.js +334 -0
  11. package/dist/tools/achievement.d.ts +983 -0
  12. package/dist/tools/achievement.js +311 -0
  13. package/dist/tools/admin.d.ts +153 -0
  14. package/dist/tools/admin.js +193 -0
  15. package/dist/tools/analytics.d.ts +162 -0
  16. package/dist/tools/analytics.js +245 -0
  17. package/dist/tools/apikey.d.ts +72 -0
  18. package/dist/tools/apikey.js +78 -0
  19. package/dist/tools/audit.d.ts +107 -0
  20. package/dist/tools/audit.js +90 -0
  21. package/dist/tools/badge.d.ts +135 -0
  22. package/dist/tools/badge.js +165 -0
  23. package/dist/tools/campaign.d.ts +1775 -0
  24. package/dist/tools/campaign.js +724 -0
  25. package/dist/tools/export.d.ts +110 -0
  26. package/dist/tools/export.js +147 -0
  27. package/dist/tools/import.d.ts +110 -0
  28. package/dist/tools/import.js +126 -0
  29. package/dist/tools/index.d.ts +22 -0
  30. package/dist/tools/index.js +527 -0
  31. package/dist/tools/member.d.ts +345 -0
  32. package/dist/tools/member.js +358 -0
  33. package/dist/tools/member.test.d.ts +1 -0
  34. package/dist/tools/member.test.js +213 -0
  35. package/dist/tools/points.d.ts +188 -0
  36. package/dist/tools/points.js +306 -0
  37. package/dist/tools/points.test.d.ts +1 -0
  38. package/dist/tools/points.test.js +292 -0
  39. package/dist/tools/reward.d.ts +261 -0
  40. package/dist/tools/reward.js +371 -0
  41. package/dist/tools/reward.test.d.ts +1 -0
  42. package/dist/tools/reward.test.js +240 -0
  43. package/dist/tools/role.d.ts +161 -0
  44. package/dist/tools/role.js +160 -0
  45. package/dist/tools/segment.d.ts +797 -0
  46. package/dist/tools/segment.js +299 -0
  47. package/dist/tools/store.d.ts +101 -0
  48. package/dist/tools/store.js +117 -0
  49. package/dist/tools/tierset.d.ts +288 -0
  50. package/dist/tools/tierset.js +244 -0
  51. package/dist/tools/transaction.d.ts +357 -0
  52. package/dist/tools/transaction.js +242 -0
  53. package/dist/tools/transaction.test.d.ts +1 -0
  54. package/dist/tools/transaction.test.js +235 -0
  55. package/dist/tools/wallet-type.d.ts +32 -0
  56. package/dist/tools/wallet-type.js +58 -0
  57. package/dist/tools/webhook.d.ts +179 -0
  58. package/dist/tools/webhook.js +171 -0
  59. package/dist/types/schemas/achievement.d.ts +1116 -0
  60. package/dist/types/schemas/achievement.js +172 -0
  61. package/dist/types/schemas/admin.d.ts +263 -0
  62. package/dist/types/schemas/admin.js +99 -0
  63. package/dist/types/schemas/analytics.d.ts +542 -0
  64. package/dist/types/schemas/analytics.js +130 -0
  65. package/dist/types/schemas/badge.d.ts +131 -0
  66. package/dist/types/schemas/badge.js +48 -0
  67. package/dist/types/schemas/campaign.d.ts +2005 -0
  68. package/dist/types/schemas/campaign.js +189 -0
  69. package/dist/types/schemas/common.d.ts +52 -0
  70. package/dist/types/schemas/common.js +26 -0
  71. package/dist/types/schemas/export.d.ts +127 -0
  72. package/dist/types/schemas/export.js +43 -0
  73. package/dist/types/schemas/import.d.ts +344 -0
  74. package/dist/types/schemas/import.js +68 -0
  75. package/dist/types/schemas/member.d.ts +443 -0
  76. package/dist/types/schemas/member.js +92 -0
  77. package/dist/types/schemas/points.d.ts +188 -0
  78. package/dist/types/schemas/points.js +54 -0
  79. package/dist/types/schemas/reward.d.ts +278 -0
  80. package/dist/types/schemas/reward.js +69 -0
  81. package/dist/types/schemas/role.d.ts +260 -0
  82. package/dist/types/schemas/role.js +75 -0
  83. package/dist/types/schemas/segment.d.ts +592 -0
  84. package/dist/types/schemas/segment.js +114 -0
  85. package/dist/types/schemas/tierset.d.ts +552 -0
  86. package/dist/types/schemas/tierset.js +87 -0
  87. package/dist/types/schemas/transaction.d.ts +1022 -0
  88. package/dist/types/schemas/transaction.js +63 -0
  89. package/dist/types/schemas/wallet-type.d.ts +99 -0
  90. package/dist/types/schemas/wallet-type.js +17 -0
  91. package/dist/types/schemas/webhook.d.ts +195 -0
  92. package/dist/types/schemas/webhook.js +39 -0
  93. package/dist/utils/cursor.d.ts +84 -0
  94. package/dist/utils/cursor.js +117 -0
  95. package/dist/utils/errors.d.ts +12 -0
  96. package/dist/utils/errors.js +69 -0
  97. package/dist/utils/pagination.d.ts +39 -0
  98. package/dist/utils/pagination.js +77 -0
  99. package/package.json +65 -0
@@ -0,0 +1,724 @@
1
+ import { z } from "zod";
2
+ import { apiGet, apiPost, apiPut, apiPatch, apiDelete } from "../client/http.js";
3
+ import { CampaignTypeEnum, CampaignTriggerEnum, } from "../types/schemas/campaign.js";
4
+ import { formatApiError } from "../utils/errors.js";
5
+ import { getConfig } from "../config.js";
6
+ // Input Schemas
7
+ export const CampaignListInputSchema = {
8
+ storeCode: z.string().optional().describe("Store code. If not provided, uses the default store code from configuration."),
9
+ page: z.number().optional().describe("Page number (default: 1)."),
10
+ perPage: z.number().optional().describe("Items per page (default: 10)."),
11
+ active: z.boolean().optional().describe("Filter by active status."),
12
+ type: CampaignTypeEnum.optional().describe("Filter by campaign type: direct or referral."),
13
+ trigger: CampaignTriggerEnum.optional().describe("Filter by trigger type: transaction, custom_event, time, etc."),
14
+ };
15
+ // Translations schema for campaign input
16
+ const TranslationsInputSchema = z.object({
17
+ en: z.object({
18
+ name: z.string().describe("Campaign name in English."),
19
+ description: z.string().optional().describe("Campaign description in English."),
20
+ }),
21
+ }).passthrough().describe("Translations object with at least 'en' key. Other languages (pl, de, etc.) are optional.");
22
+ // Effect input schema
23
+ const CampaignEffectInputSchema = z.object({
24
+ effect: z.enum(["give_points", "give_reward", "deduct_unit", "assign_member_custom_attribute", "remove_member_custom_attribute"]).describe("Effect type: give_points, give_reward, deduct_unit, etc."),
25
+ pointsRule: z.object({
26
+ expression: z.string().optional().describe("Expression to calculate points (e.g., 'transaction.grossValue * 10')."),
27
+ multiplier: z.number().optional().describe("Points multiplier."),
28
+ fixedValue: z.number().optional().describe("Fixed points value."),
29
+ }).optional().describe("Points calculation rule (required for give_points effect)."),
30
+ walletCode: z.string().optional().describe("Wallet code for points (default wallet if not specified)."),
31
+ rewardId: z.string().optional().describe("Reward ID (required for give_reward effect)."),
32
+ customAttributeKey: z.string().optional().describe("Custom attribute key (for assign/remove member custom attribute effects)."),
33
+ customAttributeValueRule: z.string().optional().describe("Custom attribute value rule."),
34
+ });
35
+ // Condition input schema
36
+ const CampaignConditionInputSchema = z.object({
37
+ operator: z.string().describe("Condition operator (is_equal, gte, lte, etc.)."),
38
+ attribute: z.string().describe("Attribute to check."),
39
+ data: z.record(z.unknown()).optional().describe("Condition-specific data."),
40
+ });
41
+ // Rule input schema
42
+ const CampaignRuleInputSchema = z.object({
43
+ name: z.string().describe("Rule name."),
44
+ description: z.string().optional().describe("Rule description."),
45
+ target: z.enum(["self", "referrer"]).optional().describe("Target for rule effects: self (default) or referrer."),
46
+ effects: z.array(CampaignEffectInputSchema).describe("Array of effects to apply when rule matches."),
47
+ conditions: z.array(CampaignConditionInputSchema).optional().describe("Array of conditions that must match for rule to apply."),
48
+ });
49
+ // Activity input schema
50
+ const CampaignActivityInputSchema = z.object({
51
+ startsAt: z.string().describe("Campaign start date/time (ISO format: YYYY-MM-DD HH:mm+TZ)."),
52
+ endsAt: z.string().optional().describe("Campaign end date/time (ISO format). If not specified, campaign runs indefinitely."),
53
+ });
54
+ // Visibility input schema
55
+ const CampaignVisibilityInputSchema = z.object({
56
+ target: z.enum(["all", "tier", "segment"]).describe("Who can see the campaign: all, tier (specific tiers), or segment (specific segments)."),
57
+ tiers: z.array(z.string()).optional().describe("Array of tier level IDs (required if target is 'tier')."),
58
+ segments: z.array(z.string()).optional().describe("Array of segment IDs (required if target is 'segment')."),
59
+ }).optional();
60
+ // Audience input schema
61
+ const CampaignAudienceInputSchema = z.object({
62
+ target: z.enum(["all", "tier", "segment"]).describe("Who can participate: all, tier (specific tiers), or segment (specific segments)."),
63
+ tiers: z.array(z.string()).optional().describe("Array of tier level IDs."),
64
+ segments: z.array(z.string()).optional().describe("Array of segment IDs."),
65
+ }).optional();
66
+ // Limit value input schema
67
+ const CampaignLimitValueInputSchema = z.object({
68
+ value: z.number().describe("Limit value."),
69
+ interval: z.object({
70
+ type: z.enum(["days", "weeks", "months", "years", "forever"]).describe("Interval type."),
71
+ value: z.number().optional().describe("Interval value (not needed for 'forever')."),
72
+ }).optional().describe("Time interval for the limit."),
73
+ });
74
+ // Limits input schema
75
+ const CampaignLimitsInputSchema = z.object({
76
+ points: CampaignLimitValueInputSchema.optional().describe("Total points limit for the campaign."),
77
+ pointsPerMember: CampaignLimitValueInputSchema.optional().describe("Points limit per member."),
78
+ executionsPerMember: CampaignLimitValueInputSchema.optional().describe("Execution limit per member."),
79
+ }).optional();
80
+ // Label input schema
81
+ const CampaignLabelInputSchema = z.object({
82
+ key: z.string().describe("Label key."),
83
+ value: z.string().describe("Label value."),
84
+ });
85
+ export const CampaignGetInputSchema = {
86
+ storeCode: z.string().optional().describe("Store code. If not provided, uses the default store code from configuration."),
87
+ campaignId: z.string().describe("The campaign ID (UUID) to retrieve."),
88
+ };
89
+ export const CampaignUpdateInputSchema = {
90
+ storeCode: z.string().optional().describe("Store code. If not provided, uses the default store code from configuration."),
91
+ campaignId: z.string().describe("The campaign ID (UUID) to update."),
92
+ type: CampaignTypeEnum.describe("Campaign type: direct or referral."),
93
+ trigger: CampaignTriggerEnum.describe("What triggers the campaign."),
94
+ translations: z.object({
95
+ en: z.object({
96
+ name: z.string(),
97
+ description: z.string().optional(),
98
+ }),
99
+ }).passthrough().describe("Translations object with at least 'en' key."),
100
+ activity: z.object({
101
+ startsAt: z.string(),
102
+ endsAt: z.string().optional(),
103
+ }).describe("Campaign active period."),
104
+ rules: z.array(z.object({
105
+ name: z.string(),
106
+ description: z.string().optional(),
107
+ target: z.enum(["self", "referrer"]).optional(),
108
+ effects: z.array(z.object({
109
+ effect: z.string(),
110
+ pointsRule: z.record(z.unknown()).optional(),
111
+ walletCode: z.string().optional(),
112
+ rewardId: z.string().optional(),
113
+ })),
114
+ conditions: z.array(z.object({
115
+ operator: z.string(),
116
+ attribute: z.string(),
117
+ data: z.record(z.unknown()).optional(),
118
+ })).optional(),
119
+ })).describe("Campaign rules."),
120
+ visibility: z.object({
121
+ target: z.enum(["all", "tier", "segment"]),
122
+ tiers: z.array(z.string()).optional(),
123
+ segments: z.array(z.string()).optional(),
124
+ }).optional(),
125
+ audience: z.object({
126
+ target: z.enum(["all", "tier", "segment"]),
127
+ tiers: z.array(z.string()).optional(),
128
+ segments: z.array(z.string()).optional(),
129
+ }).optional(),
130
+ limits: z.object({
131
+ points: z.object({ value: z.number(), interval: z.record(z.unknown()).optional() }).optional(),
132
+ pointsPerMember: z.object({ value: z.number(), interval: z.record(z.unknown()).optional() }).optional(),
133
+ executionsPerMember: z.object({ value: z.number(), interval: z.record(z.unknown()).optional() }).optional(),
134
+ }).optional(),
135
+ active: z.boolean().optional(),
136
+ displayOrder: z.number().optional(),
137
+ labels: z.array(z.object({ key: z.string(), value: z.string() })).optional(),
138
+ event: z.string().optional(),
139
+ };
140
+ export const CampaignPatchInputSchema = {
141
+ storeCode: z.string().optional().describe("Store code. If not provided, uses the default store code from configuration."),
142
+ campaignId: z.string().describe("The campaign ID (UUID) to patch."),
143
+ active: z.boolean().optional().describe("Set campaign active status."),
144
+ displayOrder: z.number().optional().describe("Set campaign display order."),
145
+ };
146
+ export const CampaignDeleteInputSchema = {
147
+ storeCode: z.string().optional().describe("Store code. If not provided, uses the default store code from configuration."),
148
+ campaignId: z.string().describe("The campaign ID (UUID) to delete."),
149
+ };
150
+ // Transaction item for simulation
151
+ const SimulateTransactionItemSchema = z.object({
152
+ sku: z.string().describe("Product SKU."),
153
+ name: z.string().describe("Product name."),
154
+ qty: z.union([z.string(), z.number()]).optional().describe("Item quantity."),
155
+ grossValue: z.union([z.string(), z.number()]).describe("Item gross value."),
156
+ category: z.string().optional().describe("Product category."),
157
+ maker: z.string().optional().describe("Product maker/brand."),
158
+ labels: z.array(z.object({
159
+ key: z.string(),
160
+ value: z.string(),
161
+ })).optional().describe("Custom labels."),
162
+ });
163
+ // Transaction for simulation
164
+ const SimulateTransactionSchema = z.object({
165
+ grossValue: z.number().optional().describe("Total transaction gross value (alternative to items)."),
166
+ documentNumber: z.string().optional().describe("Document number."),
167
+ purchasedAt: z.string().optional().describe("Purchase date/time (ISO format)."),
168
+ documentType: z.enum(["sell", "return"]).optional().describe("Document type."),
169
+ purchasePlace: z.string().optional().describe("Purchase location."),
170
+ items: z.array(SimulateTransactionItemSchema).optional().describe("Transaction items."),
171
+ labels: z.array(z.object({
172
+ key: z.string(),
173
+ value: z.string(),
174
+ })).optional().describe("Custom labels."),
175
+ }).optional();
176
+ // Custom event for simulation
177
+ const SimulateCustomEventSchema = z.object({
178
+ eventCode: z.string().describe("Custom event code (must match campaign trigger event)."),
179
+ attributes: z.record(z.unknown()).optional().describe("Custom event attributes."),
180
+ }).optional();
181
+ // Customer for simulation
182
+ const SimulateCustomerSchema = z.object({
183
+ customerId: z.string().optional().describe("Member ID (UUID) for explicit member matching."),
184
+ id: z.string().optional().describe("Member ID (UUID) - alias for customerId."),
185
+ email: z.string().optional().describe("Email for member matching."),
186
+ loyaltyCardNumber: z.string().optional().describe("Loyalty card for member matching."),
187
+ phone: z.string().optional().describe("Phone for member matching."),
188
+ firstName: z.string().optional().describe("Customer first name."),
189
+ lastName: z.string().optional().describe("Customer last name."),
190
+ });
191
+ // Referrer for simulation (referral campaigns)
192
+ const SimulateReferrerSchema = z.object({
193
+ id: z.string().optional().describe("Referrer member ID."),
194
+ email: z.string().optional().describe("Referrer email."),
195
+ loyaltyCardNumber: z.string().optional().describe("Referrer loyalty card number."),
196
+ }).optional();
197
+ export const CampaignSimulateInputSchema = {
198
+ storeCode: z.string().optional().describe("Store code. If not provided, uses the default store code from configuration."),
199
+ trigger: CampaignTriggerEnum.describe("Campaign trigger type to simulate: transaction, custom_event, etc."),
200
+ transaction: SimulateTransactionSchema.describe("Transaction data (required for transaction trigger)."),
201
+ customEvent: SimulateCustomEventSchema.describe("Custom event data (required for custom_event trigger)."),
202
+ customer: SimulateCustomerSchema.describe("Customer/member data for simulation."),
203
+ referrer: SimulateReferrerSchema.describe("Referrer data for referral campaigns."),
204
+ };
205
+ export const CampaignGenerateCodesInputSchema = {
206
+ storeCode: z.string().optional().describe("Store code. If not provided, uses the default store code from configuration."),
207
+ campaignId: z.string().describe("The campaign ID (UUID) to generate codes for."),
208
+ quantity: z.number().min(1).describe("Number of codes to generate."),
209
+ };
210
+ export const CampaignListCodesInputSchema = {
211
+ storeCode: z.string().optional().describe("Store code. If not provided, uses the default store code from configuration."),
212
+ campaignId: z.string().describe("The campaign ID (UUID) to list codes for."),
213
+ page: z.number().optional().describe("Page number (default: 1)."),
214
+ perPage: z.number().optional().describe("Items per page (default: 25)."),
215
+ status: z.string().optional().describe("Filter by code status (e.g., 'active', 'used')."),
216
+ code: z.string().optional().describe("Filter by specific code."),
217
+ };
218
+ export const CampaignGetAvailableInputSchema = {
219
+ storeCode: z.string().optional().describe("Store code. If not provided, uses the default store code from configuration."),
220
+ memberId: z.string().describe("The member ID (UUID) to get available campaigns for."),
221
+ page: z.number().optional().describe("Page number (default: 1)."),
222
+ perPage: z.number().optional().describe("Items per page (default: 25)."),
223
+ type: CampaignTypeEnum.optional().describe("Filter by campaign type."),
224
+ trigger: CampaignTriggerEnum.optional().describe("Filter by campaign trigger."),
225
+ };
226
+ export const CampaignGetVisibleInputSchema = {
227
+ storeCode: z.string().optional().describe("Store code. If not provided, uses the default store code from configuration."),
228
+ memberId: z.string().describe("The member ID (UUID) to get visible campaigns for."),
229
+ page: z.number().optional().describe("Page number (default: 1)."),
230
+ perPage: z.number().optional().describe("Items per page (default: 25)."),
231
+ active: z.boolean().optional().describe("Filter by active status."),
232
+ };
233
+ export const CampaignGetLeaderboardInputSchema = {
234
+ storeCode: z.string().optional().describe("Store code. If not provided, uses the default store code from configuration."),
235
+ campaignId: z.string().describe("The campaign ID (UUID) to get leaderboard for."),
236
+ type: z.string().optional().describe("Cycle type for ranking (e.g., 'weekly', 'monthly')."),
237
+ };
238
+ export const CampaignCreateInputSchema = {
239
+ storeCode: z.string().optional().describe("Store code. If not provided, uses the default store code from configuration."),
240
+ type: CampaignTypeEnum.describe("Campaign type: direct (standard) or referral (for referral programs)."),
241
+ trigger: CampaignTriggerEnum.describe("What triggers the campaign: transaction, custom_event, time, achievement, etc."),
242
+ translations: TranslationsInputSchema,
243
+ activity: CampaignActivityInputSchema.describe("Campaign active period with startsAt and optional endsAt."),
244
+ rules: z.array(CampaignRuleInputSchema).describe("Array of rules defining when and what effects to apply."),
245
+ visibility: CampaignVisibilityInputSchema.describe("Who can see the campaign."),
246
+ audience: CampaignAudienceInputSchema.describe("Who can participate in the campaign."),
247
+ limits: CampaignLimitsInputSchema.describe("Campaign limits for points and executions."),
248
+ active: z.boolean().optional().describe("Whether campaign is active (default: false)."),
249
+ displayOrder: z.number().optional().describe("Display order for sorting campaigns."),
250
+ labels: z.array(CampaignLabelInputSchema).optional().describe("Custom labels/tags for the campaign."),
251
+ event: z.string().optional().describe("Custom event name (required for custom_event trigger)."),
252
+ };
253
+ // Handler functions
254
+ export async function campaignList(input) {
255
+ const config = getConfig();
256
+ const storeCode = input.storeCode || config.defaultStoreCode;
257
+ const params = new URLSearchParams();
258
+ if (input.page)
259
+ params.append("_page", String(input.page));
260
+ if (input.perPage)
261
+ params.append("_itemsOnPage", String(input.perPage));
262
+ if (input.active !== undefined)
263
+ params.append("active", String(input.active));
264
+ if (input.type)
265
+ params.append("type", input.type);
266
+ if (input.trigger)
267
+ params.append("trigger", input.trigger);
268
+ const queryString = params.toString();
269
+ const url = `/${storeCode}/campaign${queryString ? `?${queryString}` : ""}`;
270
+ try {
271
+ const response = await apiGet(url);
272
+ const campaigns = (response.items || []).map((item) => {
273
+ const campaign = item;
274
+ const translations = campaign.translations;
275
+ // Handle both object and array translations format
276
+ let name = "";
277
+ if (Array.isArray(translations)) {
278
+ const enTranslation = translations.find((t) => t.locale === "en" || t.locale?.startsWith("en"));
279
+ name = enTranslation?.name || translations[0]?.name || campaign.name || "";
280
+ }
281
+ else if (translations && typeof translations === "object") {
282
+ name = translations.en?.name || campaign.name || "";
283
+ }
284
+ else {
285
+ name = campaign.name || "";
286
+ }
287
+ return {
288
+ campaignId: campaign.campaignId,
289
+ name,
290
+ type: campaign.type,
291
+ trigger: campaign.trigger,
292
+ active: (campaign.active ?? false),
293
+ displayOrder: campaign.displayOrder,
294
+ };
295
+ });
296
+ const total = response.total || {};
297
+ return {
298
+ campaigns,
299
+ total: {
300
+ all: typeof total.all === "number" ? total.all : undefined,
301
+ filtered: typeof total.filtered === "number" ? total.filtered : undefined,
302
+ },
303
+ };
304
+ }
305
+ catch (error) {
306
+ throw formatApiError(error, "openloyalty_campaign_list");
307
+ }
308
+ }
309
+ export async function campaignGet(input) {
310
+ const config = getConfig();
311
+ const storeCode = input.storeCode || config.defaultStoreCode;
312
+ try {
313
+ const response = await apiGet(`/${storeCode}/campaign/${input.campaignId}`);
314
+ return response;
315
+ }
316
+ catch (error) {
317
+ throw formatApiError(error, "openloyalty_campaign_get");
318
+ }
319
+ }
320
+ export async function campaignUpdate(input) {
321
+ const config = getConfig();
322
+ const storeCode = input.storeCode || config.defaultStoreCode;
323
+ const campaignId = input.campaignId;
324
+ // Build campaign payload excluding storeCode and campaignId
325
+ const { storeCode: _sc, campaignId: _cid, ...campaignPayload } = input;
326
+ try {
327
+ // CRITICAL: Wrap body as { campaign: {...} }
328
+ await apiPut(`/${storeCode}/campaign/${campaignId}`, { campaign: campaignPayload });
329
+ }
330
+ catch (error) {
331
+ throw formatApiError(error, "openloyalty_campaign_update");
332
+ }
333
+ }
334
+ export async function campaignPatch(input) {
335
+ const config = getConfig();
336
+ const storeCode = input.storeCode || config.defaultStoreCode;
337
+ const patchPayload = {};
338
+ if (input.active !== undefined)
339
+ patchPayload.active = input.active;
340
+ if (input.displayOrder !== undefined)
341
+ patchPayload.displayOrder = input.displayOrder;
342
+ try {
343
+ await apiPatch(`/${storeCode}/campaign/${input.campaignId}`, { campaign: patchPayload });
344
+ }
345
+ catch (error) {
346
+ throw formatApiError(error, "openloyalty_campaign_patch");
347
+ }
348
+ }
349
+ export async function campaignDelete(input) {
350
+ const config = getConfig();
351
+ const storeCode = input.storeCode || config.defaultStoreCode;
352
+ try {
353
+ await apiDelete(`/${storeCode}/campaign/${input.campaignId}`);
354
+ }
355
+ catch (error) {
356
+ throw formatApiError(error, "openloyalty_campaign_delete");
357
+ }
358
+ }
359
+ export async function campaignSimulate(input) {
360
+ const config = getConfig();
361
+ const storeCode = input.storeCode || config.defaultStoreCode;
362
+ // Build the simulate payload
363
+ const simulatePayload = {
364
+ trigger: input.trigger,
365
+ customer: {},
366
+ };
367
+ // Build customer object
368
+ const customer = {};
369
+ if (input.customer.customerId)
370
+ customer.id = input.customer.customerId;
371
+ if (input.customer.id)
372
+ customer.id = input.customer.id;
373
+ if (input.customer.email)
374
+ customer.email = input.customer.email;
375
+ if (input.customer.loyaltyCardNumber)
376
+ customer.loyaltyCardNumber = input.customer.loyaltyCardNumber;
377
+ if (input.customer.phone)
378
+ customer.phone = input.customer.phone;
379
+ if (input.customer.firstName)
380
+ customer.firstName = input.customer.firstName;
381
+ if (input.customer.lastName)
382
+ customer.lastName = input.customer.lastName;
383
+ simulatePayload.customer = customer;
384
+ // Add transaction if provided
385
+ if (input.transaction) {
386
+ const transaction = {};
387
+ if (input.transaction.grossValue !== undefined)
388
+ transaction.grossValue = input.transaction.grossValue;
389
+ if (input.transaction.documentNumber)
390
+ transaction.documentNumber = input.transaction.documentNumber;
391
+ if (input.transaction.purchasedAt)
392
+ transaction.purchasedAt = input.transaction.purchasedAt;
393
+ if (input.transaction.documentType)
394
+ transaction.documentType = input.transaction.documentType;
395
+ if (input.transaction.purchasePlace)
396
+ transaction.purchasePlace = input.transaction.purchasePlace;
397
+ if (input.transaction.items)
398
+ transaction.items = input.transaction.items;
399
+ if (input.transaction.labels)
400
+ transaction.labels = input.transaction.labels;
401
+ simulatePayload.transaction = transaction;
402
+ }
403
+ // Add custom event if provided (for custom_event trigger)
404
+ if (input.customEvent) {
405
+ simulatePayload.event = input.customEvent.eventCode;
406
+ if (input.customEvent.attributes) {
407
+ simulatePayload.eventAttributes = input.customEvent.attributes;
408
+ }
409
+ }
410
+ // Add referrer if provided (for referral campaigns)
411
+ if (input.referrer) {
412
+ const referrer = {};
413
+ if (input.referrer.id)
414
+ referrer.id = input.referrer.id;
415
+ if (input.referrer.email)
416
+ referrer.email = input.referrer.email;
417
+ if (input.referrer.loyaltyCardNumber)
418
+ referrer.loyaltyCardNumber = input.referrer.loyaltyCardNumber;
419
+ simulatePayload.referrer = referrer;
420
+ }
421
+ try {
422
+ // CRITICAL: Wrap body as { simulate: {...} }
423
+ const response = await apiPost(`/${storeCode}/campaign/simulate`, { simulate: simulatePayload });
424
+ // Transform effects into simulated effects grouped by campaign
425
+ const effectsByCampaign = new Map();
426
+ for (const effect of response.effects || []) {
427
+ const campaignId = effect.campaign?.campaignId || "unknown";
428
+ const campaignName = effect.campaign?.name || undefined;
429
+ if (!effectsByCampaign.has(campaignId)) {
430
+ effectsByCampaign.set(campaignId, {
431
+ campaignId,
432
+ campaignName,
433
+ effects: [],
434
+ });
435
+ }
436
+ const campaignEffect = effectsByCampaign.get(campaignId);
437
+ campaignEffect.effects.push({
438
+ type: effect.type,
439
+ target: effect.target,
440
+ points: effect.points,
441
+ wallet: effect.wallet,
442
+ rewardId: effect.rewardId,
443
+ });
444
+ }
445
+ return {
446
+ simulatedEffects: Array.from(effectsByCampaign.values()),
447
+ evaluationWarnings: response.evaluationWarnings,
448
+ simulatedMember: response.simulatedMember,
449
+ };
450
+ }
451
+ catch (error) {
452
+ throw formatApiError(error, "openloyalty_campaign_simulate");
453
+ }
454
+ }
455
+ export async function campaignGenerateCodes(input) {
456
+ const config = getConfig();
457
+ const storeCode = input.storeCode || config.defaultStoreCode;
458
+ try {
459
+ // Body wrapped as { generate: { numberOfCodes } }
460
+ await apiPost(`/${storeCode}/campaign/${input.campaignId}/codes/generate`, { generate: { numberOfCodes: input.quantity } });
461
+ // API returns 204 No Content, so we return the requested quantity
462
+ return { codesGenerated: input.quantity };
463
+ }
464
+ catch (error) {
465
+ throw formatApiError(error, "openloyalty_campaign_generate_codes");
466
+ }
467
+ }
468
+ export async function campaignListCodes(input) {
469
+ const config = getConfig();
470
+ const storeCode = input.storeCode || config.defaultStoreCode;
471
+ const params = new URLSearchParams();
472
+ if (input.page)
473
+ params.append("_page", String(input.page));
474
+ if (input.perPage)
475
+ params.append("_itemsOnPage", String(input.perPage));
476
+ if (input.status)
477
+ params.append("status", input.status);
478
+ if (input.code)
479
+ params.append("code", input.code);
480
+ const queryString = params.toString();
481
+ const url = `/${storeCode}/campaign/${input.campaignId}/codes${queryString ? `?${queryString}` : ""}`;
482
+ try {
483
+ const response = await apiGet(url);
484
+ const codes = (response.items || []).map((item) => ({
485
+ codeId: item.codeId,
486
+ code: item.code,
487
+ status: item.status,
488
+ usedAt: item.usedAt,
489
+ createdAt: item.createdAt,
490
+ createdBy: item.createdBy,
491
+ }));
492
+ const total = response.total || {};
493
+ return {
494
+ codes,
495
+ total: {
496
+ all: typeof total.all === "number" ? total.all : undefined,
497
+ filtered: typeof total.filtered === "number" ? total.filtered : undefined,
498
+ },
499
+ };
500
+ }
501
+ catch (error) {
502
+ throw formatApiError(error, "openloyalty_campaign_list_codes");
503
+ }
504
+ }
505
+ export async function campaignGetAvailable(input) {
506
+ const config = getConfig();
507
+ const storeCode = input.storeCode || config.defaultStoreCode;
508
+ const params = new URLSearchParams();
509
+ if (input.page)
510
+ params.append("_page", String(input.page));
511
+ if (input.perPage)
512
+ params.append("_itemsOnPage", String(input.perPage));
513
+ if (input.type)
514
+ params.append("type", input.type);
515
+ if (input.trigger)
516
+ params.append("trigger", input.trigger);
517
+ const queryString = params.toString();
518
+ const url = `/${storeCode}/member/${input.memberId}/campaign${queryString ? `?${queryString}` : ""}`;
519
+ try {
520
+ const response = await apiGet(url);
521
+ const campaigns = (response.items || []).map((item) => ({
522
+ campaignId: item.campaignId,
523
+ name: item.name,
524
+ active: item.active,
525
+ type: item.type,
526
+ trigger: item.trigger,
527
+ limitReached: item.limitReached,
528
+ description: item.description,
529
+ }));
530
+ const total = response.total || {};
531
+ return {
532
+ campaigns,
533
+ total: {
534
+ all: typeof total.all === "number" ? total.all : undefined,
535
+ filtered: typeof total.filtered === "number" ? total.filtered : undefined,
536
+ },
537
+ };
538
+ }
539
+ catch (error) {
540
+ throw formatApiError(error, "openloyalty_campaign_get_available");
541
+ }
542
+ }
543
+ export async function campaignGetVisible(input) {
544
+ const config = getConfig();
545
+ const storeCode = input.storeCode || config.defaultStoreCode;
546
+ const params = new URLSearchParams();
547
+ if (input.page)
548
+ params.append("_page", String(input.page));
549
+ if (input.perPage)
550
+ params.append("_itemsOnPage", String(input.perPage));
551
+ if (input.active !== undefined)
552
+ params.append("active", String(input.active));
553
+ const queryString = params.toString();
554
+ const url = `/${storeCode}/member/${input.memberId}/campaign/visible${queryString ? `?${queryString}` : ""}`;
555
+ try {
556
+ const response = await apiGet(url);
557
+ const campaigns = (response.items || []).map((item) => ({
558
+ campaignId: item.campaignId,
559
+ name: item.name,
560
+ active: item.active,
561
+ type: item.type,
562
+ trigger: item.trigger,
563
+ description: item.description,
564
+ }));
565
+ const total = response.total || {};
566
+ return {
567
+ campaigns,
568
+ total: {
569
+ all: typeof total.all === "number" ? total.all : undefined,
570
+ filtered: typeof total.filtered === "number" ? total.filtered : undefined,
571
+ },
572
+ };
573
+ }
574
+ catch (error) {
575
+ throw formatApiError(error, "openloyalty_campaign_get_visible");
576
+ }
577
+ }
578
+ export async function campaignGetLeaderboard(input) {
579
+ const config = getConfig();
580
+ const storeCode = input.storeCode || config.defaultStoreCode;
581
+ const params = new URLSearchParams();
582
+ if (input.type)
583
+ params.append("type", input.type);
584
+ const queryString = params.toString();
585
+ const url = `/${storeCode}/campaign/${input.campaignId}/leaderboard${queryString ? `?${queryString}` : ""}`;
586
+ try {
587
+ const response = await apiGet(url);
588
+ const entries = (response.items || []).map((item) => ({
589
+ member: {
590
+ customerId: item.member.customerId,
591
+ firstName: item.member.firstName,
592
+ lastName: item.member.lastName,
593
+ email: item.member.email,
594
+ loyaltyCardNumber: item.member.loyaltyCardNumber,
595
+ },
596
+ ranking: {
597
+ score: item.ranking.score,
598
+ position: item.ranking.position,
599
+ recalculatedAt: item.ranking.recalculatedAt,
600
+ changedPositionAt: item.ranking.changedPositionAt,
601
+ },
602
+ }));
603
+ const total = response.total || {};
604
+ return {
605
+ entries,
606
+ total: {
607
+ all: typeof total.all === "number" ? total.all : undefined,
608
+ filtered: typeof total.filtered === "number" ? total.filtered : undefined,
609
+ },
610
+ };
611
+ }
612
+ catch (error) {
613
+ throw formatApiError(error, "openloyalty_campaign_get_leaderboard");
614
+ }
615
+ }
616
+ export async function campaignCreate(input) {
617
+ const config = getConfig();
618
+ const storeCode = input.storeCode || config.defaultStoreCode;
619
+ // Build the campaign payload
620
+ const campaignPayload = {
621
+ type: input.type,
622
+ trigger: input.trigger,
623
+ translations: input.translations,
624
+ activity: input.activity,
625
+ rules: input.rules,
626
+ };
627
+ if (input.visibility)
628
+ campaignPayload.visibility = input.visibility;
629
+ if (input.audience)
630
+ campaignPayload.audience = input.audience;
631
+ if (input.limits)
632
+ campaignPayload.limits = input.limits;
633
+ if (input.active !== undefined)
634
+ campaignPayload.active = input.active;
635
+ if (input.displayOrder !== undefined)
636
+ campaignPayload.displayOrder = input.displayOrder;
637
+ if (input.labels)
638
+ campaignPayload.labels = input.labels;
639
+ if (input.event)
640
+ campaignPayload.event = input.event;
641
+ try {
642
+ // CRITICAL: Wrap body as { campaign: {...} }
643
+ const response = await apiPost(`/${storeCode}/campaign`, { campaign: campaignPayload });
644
+ return { campaignId: response.campaignId };
645
+ }
646
+ catch (error) {
647
+ throw formatApiError(error, "openloyalty_campaign_create");
648
+ }
649
+ }
650
+ // Tool definitions
651
+ export const campaignToolDefinitions = [
652
+ {
653
+ name: "openloyalty_campaign_list",
654
+ description: "List all campaigns. Filter by type (direct/referral) or trigger (transaction/custom_event/time/etc). Use campaign_get for full configuration including rules and effects.",
655
+ inputSchema: CampaignListInputSchema,
656
+ handler: campaignList,
657
+ },
658
+ {
659
+ name: "openloyalty_campaign_create",
660
+ description: "Create campaign to automate engagement. Triggers: transaction (purchase-based), custom_event (for custom actions), time (scheduled). Effects: give_points, give_reward, deduct_unit. Rules define when/what happens. Example - Double points: trigger=transaction, effect=give_points with pointsRule containing multiplier:2.",
661
+ inputSchema: CampaignCreateInputSchema,
662
+ handler: campaignCreate,
663
+ },
664
+ {
665
+ name: "openloyalty_campaign_get",
666
+ description: "Get full campaign configuration including all rules, conditions, effects, and targeting.",
667
+ inputSchema: CampaignGetInputSchema,
668
+ handler: campaignGet,
669
+ },
670
+ {
671
+ name: "openloyalty_campaign_update",
672
+ description: "Full update of campaign configuration. Requires all campaign fields. Use campaign_get first to retrieve current configuration, modify it, then send the complete object.",
673
+ inputSchema: CampaignUpdateInputSchema,
674
+ handler: campaignUpdate,
675
+ },
676
+ {
677
+ name: "openloyalty_campaign_patch",
678
+ description: "Partial update of campaign - only active status and displayOrder can be patched. For full updates, use campaign_update.",
679
+ inputSchema: CampaignPatchInputSchema,
680
+ handler: campaignPatch,
681
+ },
682
+ {
683
+ name: "openloyalty_campaign_delete",
684
+ description: "Permanently delete a campaign. This cannot be undone. Consider deactivating instead if you may need the campaign later.",
685
+ inputSchema: CampaignDeleteInputSchema,
686
+ handler: campaignDelete,
687
+ },
688
+ {
689
+ name: "openloyalty_campaign_simulate",
690
+ description: "Simulate campaign effects without executing them. Use to preview what points/rewards a transaction would earn. Provide trigger type, transaction/event data, and customer. Returns simulated effects grouped by campaign.",
691
+ inputSchema: CampaignSimulateInputSchema,
692
+ handler: campaignSimulate,
693
+ },
694
+ {
695
+ name: "openloyalty_campaign_generate_codes",
696
+ description: "Generate unique redemption codes for a campaign. Use for promotions requiring unique codes. Codes are auto-generated and can be retrieved with campaign_list_codes.",
697
+ inputSchema: CampaignGenerateCodesInputSchema,
698
+ handler: campaignGenerateCodes,
699
+ },
700
+ {
701
+ name: "openloyalty_campaign_list_codes",
702
+ description: "List redemption codes for a campaign. Filter by status (active/used) or specific code. Returns code details including usage status.",
703
+ inputSchema: CampaignListCodesInputSchema,
704
+ handler: campaignListCodes,
705
+ },
706
+ {
707
+ name: "openloyalty_campaign_get_available",
708
+ description: "Get campaigns available to a specific member. Returns campaigns the member can participate in based on their tier, segments, and campaign targeting rules. Includes limitReached indicator.",
709
+ inputSchema: CampaignGetAvailableInputSchema,
710
+ handler: campaignGetAvailable,
711
+ },
712
+ {
713
+ name: "openloyalty_campaign_get_visible",
714
+ description: "Get campaigns visible to a specific member. May include campaigns not yet available (e.g., upcoming campaigns or those requiring certain conditions). Use get_available for campaigns the member can currently participate in.",
715
+ inputSchema: CampaignGetVisibleInputSchema,
716
+ handler: campaignGetVisible,
717
+ },
718
+ {
719
+ name: "openloyalty_campaign_get_leaderboard",
720
+ description: "Get leaderboard rankings for a campaign. Returns ranked list of members with scores and positions. Use for leaderboard-type campaigns to show competition standings.",
721
+ inputSchema: CampaignGetLeaderboardInputSchema,
722
+ handler: campaignGetLeaderboard,
723
+ },
724
+ ];