@open-loyalty/mcp-server 1.3.3 → 1.3.5

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.
@@ -100,15 +100,15 @@ export declare const campaignToolDefinitions: readonly [{
100
100
  conditions: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodObject<{
101
101
  operator: import("zod").ZodString;
102
102
  attribute: import("zod").ZodString;
103
- data: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>>;
103
+ data: import("zod").ZodUnknown;
104
104
  }, "strip", import("zod").ZodTypeAny, {
105
105
  attribute: string;
106
106
  operator: string;
107
- data?: Record<string, unknown> | undefined;
107
+ data?: unknown;
108
108
  }, {
109
109
  attribute: string;
110
110
  operator: string;
111
- data?: Record<string, unknown> | undefined;
111
+ data?: unknown;
112
112
  }>, "many">>;
113
113
  }, "strip", import("zod").ZodTypeAny, {
114
114
  name: string;
@@ -124,7 +124,7 @@ export declare const campaignToolDefinitions: readonly [{
124
124
  conditions?: {
125
125
  attribute: string;
126
126
  operator: string;
127
- data?: Record<string, unknown> | undefined;
127
+ data?: unknown;
128
128
  }[] | undefined;
129
129
  target?: "self" | "referrer" | undefined;
130
130
  }, {
@@ -141,7 +141,7 @@ export declare const campaignToolDefinitions: readonly [{
141
141
  conditions?: {
142
142
  attribute: string;
143
143
  operator: string;
144
- data?: Record<string, unknown> | undefined;
144
+ data?: unknown;
145
145
  }[] | undefined;
146
146
  target?: "self" | "referrer" | undefined;
147
147
  }>, "many">;
@@ -27,8 +27,10 @@ export const campaignToolDefinitions = [
27
27
  "3. translations: { en: { name: 'Campaign Name' } }, " +
28
28
  "4. activity: { startsAt: '2026-01-01 00:00+00:00' }, " +
29
29
  "5. rules: [{ name: 'Rule Name', effects: [{ effect: 'give_points', pointsRule: 'transaction.grossValue * 10' }] }]. " +
30
- "pointsRule is a STRING expression, not an object. Examples: '100' (fixed), 'transaction.grossValue * 10' (dynamic). " +
31
- "To target ALL members, OMIT the audience parameter entirely - do not set audience.target='all'.",
30
+ "pointsRule is a STRING expression. Examples: '100' (fixed), 'transaction.grossValue * 10' (dynamic). " +
31
+ "CONDITIONS: Use operator='is_greater_or_equal' (NOT 'gte'), attribute='transaction.grossValue', data=100. " +
32
+ "For labels: operator='has_at_least_one_label', attribute='transaction.labels', data=[{key:'name',value:'val'}]. " +
33
+ "To target ALL members, OMIT the audience parameter entirely.",
32
34
  readOnly: false,
33
35
  inputSchema: CampaignCreateInputSchema,
34
36
  handler: campaignCreate,
@@ -495,15 +495,15 @@ export declare const CampaignCreateInputSchema: {
495
495
  conditions: z.ZodOptional<z.ZodArray<z.ZodObject<{
496
496
  operator: z.ZodString;
497
497
  attribute: z.ZodString;
498
- data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
498
+ data: z.ZodUnknown;
499
499
  }, "strip", z.ZodTypeAny, {
500
500
  attribute: string;
501
501
  operator: string;
502
- data?: Record<string, unknown> | undefined;
502
+ data?: unknown;
503
503
  }, {
504
504
  attribute: string;
505
505
  operator: string;
506
- data?: Record<string, unknown> | undefined;
506
+ data?: unknown;
507
507
  }>, "many">>;
508
508
  }, "strip", z.ZodTypeAny, {
509
509
  name: string;
@@ -519,7 +519,7 @@ export declare const CampaignCreateInputSchema: {
519
519
  conditions?: {
520
520
  attribute: string;
521
521
  operator: string;
522
- data?: Record<string, unknown> | undefined;
522
+ data?: unknown;
523
523
  }[] | undefined;
524
524
  target?: "self" | "referrer" | undefined;
525
525
  }, {
@@ -536,7 +536,7 @@ export declare const CampaignCreateInputSchema: {
536
536
  conditions?: {
537
537
  attribute: string;
538
538
  operator: string;
539
- data?: Record<string, unknown> | undefined;
539
+ data?: unknown;
540
540
  }[] | undefined;
541
541
  target?: "self" | "referrer" | undefined;
542
542
  }>, "many">;
@@ -25,10 +25,23 @@ const CampaignEffectInputSchema = z.object({
25
25
  customAttributeValueRule: z.string().optional().describe("Custom attribute value rule."),
26
26
  });
27
27
  // Condition input schema
28
+ // Valid operators: is_equal, is_not_equal, is_greater, is_greater_or_equal, is_less, is_less_or_equal,
29
+ // contains, not_contains, contains_one_of, not_contains_one_of, has_at_least_one_label,
30
+ // is_one_of, is_not_one_of, starts_with, ends_with, matches_regex,
31
+ // is_day_of_week, is_month_of_year, is_day_of_month, is_between, is_not_between, is_after, is_before
28
32
  const CampaignConditionInputSchema = z.object({
29
- operator: z.string().describe("Condition operator (is_equal, gte, lte, etc.)."),
30
- attribute: z.string().describe("Attribute to check."),
31
- data: z.record(z.unknown()).optional().describe("Condition-specific data."),
33
+ operator: z.string().describe("Condition operator. Valid values: " +
34
+ "is_equal, is_not_equal, is_greater, is_greater_or_equal, is_less, is_less_or_equal, " +
35
+ "contains, not_contains, has_at_least_one_label, is_one_of, is_not_one_of, " +
36
+ "starts_with, ends_with, matches_regex, is_between, is_not_between, is_after, is_before. " +
37
+ "⚠️ Do NOT use 'gte', 'eq' - use full names like 'is_greater_or_equal', 'is_equal'."),
38
+ attribute: z.string().describe("Attribute path to check (e.g., 'transaction.grossValue', 'transaction.labels', 'customer.email'). " +
39
+ "Use full path with context prefix."),
40
+ data: z.unknown().describe("Condition value. Format depends on operator: " +
41
+ "• Number for is_greater/is_less/etc: 100 " +
42
+ "• String for contains/starts_with/etc: 'value' " +
43
+ "• Array for is_one_of/contains_one_of: ['a', 'b'] " +
44
+ "• Label array for has_at_least_one_label: [{key: 'name', value: 'val'}]"),
32
45
  });
33
46
  // Rule input schema
34
47
  const CampaignRuleInputSchema = z.object({
@@ -80,9 +80,17 @@ export async function pointsSpend(input) {
80
80
  return { transferId: response.transferId };
81
81
  }
82
82
  catch (error) {
83
- // Check for insufficient points error
84
- const errorMessage = error instanceof Error ? error.message : String(error);
85
- if (errorMessage.includes("NotEnoughPoints") || errorMessage.includes("insufficient")) {
83
+ // Check for insufficient points error - API returns various formats:
84
+ // "NotEnoughPoints", "insufficient", "not enough points" (lowercase)
85
+ // Must check error.message, data.message, AND data.errors array
86
+ const axiosError = error;
87
+ const apiErrors = axiosError.response?.data?.errors || [];
88
+ const allMessages = [
89
+ error instanceof Error ? error.message : String(error),
90
+ axiosError.response?.data?.message || "",
91
+ ...apiErrors.map(e => e.message)
92
+ ].join(" ").toLowerCase();
93
+ if (allMessages.includes("notenoughpoints") || allMessages.includes("insufficient") || allMessages.includes("not enough points")) {
86
94
  throw new OpenLoyaltyError({
87
95
  code: "INSUFFICIENT_BALANCE",
88
96
  message: "Member does not have enough points to complete this operation",
@@ -110,8 +118,16 @@ export async function pointsTransfer(input) {
110
118
  return { transferId: response.transferId };
111
119
  }
112
120
  catch (error) {
113
- const errorMessage = error instanceof Error ? error.message : String(error);
114
- if (errorMessage.includes("NotEnoughPoints") || errorMessage.includes("insufficient")) {
121
+ // Check for insufficient points error - API returns various formats
122
+ // Must check error.message, data.message, AND data.errors array
123
+ const axiosError = error;
124
+ const apiErrors = axiosError.response?.data?.errors || [];
125
+ const allMessages = [
126
+ error instanceof Error ? error.message : String(error),
127
+ axiosError.response?.data?.message || "",
128
+ ...apiErrors.map(e => e.message)
129
+ ].join(" ").toLowerCase();
130
+ if (allMessages.includes("notenoughpoints") || allMessages.includes("insufficient") || allMessages.includes("not enough points")) {
115
131
  throw new OpenLoyaltyError({
116
132
  code: "INSUFFICIENT_BALANCE",
117
133
  message: "Sender does not have enough points to transfer",
@@ -35,9 +35,8 @@ export declare function rewardCreate(input: {
35
35
  from: string;
36
36
  to: string;
37
37
  };
38
- usageLimit: {
38
+ usageLimit?: {
39
39
  perUser: number;
40
- general?: number;
41
40
  };
42
41
  costInPoints?: number;
43
42
  usageInstruction?: string;
@@ -46,7 +46,8 @@ export async function rewardList(input) {
46
46
  }
47
47
  export async function rewardCreate(input) {
48
48
  const storeCode = getStoreCode(input.storeCode);
49
- // API requires: translations (name only), reward (type), activity, visibility, usageLimit.perUser
49
+ // API requires: translations (name only), reward (type), activity, visibility
50
+ // NOTE: usageLimit only accepts { perUser: N } - API rejects 'general' as extra field
50
51
  // NOTE: description is NOT supported by the API at creation time
51
52
  const payload = omitUndefined({
52
53
  translations: input.translations,
@@ -93,12 +94,12 @@ export async function rewardUpdate(input) {
93
94
  const existing = await apiGet(`/${storeCode}/reward/${input.rewardId}`);
94
95
  // Extract only the fields that PUT accepts (API is strict about extra fields)
95
96
  // Note: GET returns name at root level, but PUT expects it in translations.en.name
97
+ // Note: usageLimit is NOT supported by the API - it rejects it as "extra fields"
96
98
  const existingName = existing.name;
97
99
  const existingActivity = existing.activity;
98
100
  const existingVisibility = existing.visibility;
99
- const existingUsageLimit = existing.usageLimit;
100
101
  // Build payload with only accepted fields - NEVER include undefined values
101
- // Only include activity/visibility/usageLimit if they exist in GET response
102
+ // Only include activity/visibility if they exist in GET response
102
103
  const payload = {
103
104
  // translations - only include name (GET returns name at root level)
104
105
  translations: {
@@ -132,11 +133,6 @@ export async function rewardUpdate(input) {
132
133
  visibilityPayload.to = existingVisibility.to;
133
134
  payload.visibility = visibilityPayload;
134
135
  }
135
- // usageLimit: Required field, but only include perUser (API rejects extra fields)
136
- // The GET response may include derived fields that PUT doesn't accept
137
- payload.usageLimit = {
138
- perUser: existingUsageLimit?.perUser ?? 1,
139
- };
140
136
  // Coupon-specific fields (required for static_coupon, dynamic_coupon, conversion_coupon)
141
137
  if (existing.couponValue !== undefined)
142
138
  payload.couponValue = existing.couponValue;
@@ -212,8 +208,17 @@ export async function rewardBuy(input) {
212
208
  };
213
209
  }
214
210
  catch (error) {
215
- const errorMessage = error instanceof Error ? error.message : String(error);
216
- if (errorMessage.includes("NotEnoughPoints") || errorMessage.includes("insufficient")) {
211
+ // Check for insufficient points error - API returns various formats:
212
+ // "NotEnoughPoints", "insufficient", "not enough points" (lowercase)
213
+ // Must check error.message, data.message, AND data.errors array
214
+ const axiosError = error;
215
+ const apiErrors = axiosError.response?.data?.errors || [];
216
+ const allMessages = [
217
+ error instanceof Error ? error.message : String(error),
218
+ axiosError.response?.data?.message || "",
219
+ ...apiErrors.map(e => e.message)
220
+ ].join(" ").toLowerCase();
221
+ if (allMessages.includes("notenoughpoints") || allMessages.includes("insufficient") || allMessages.includes("not enough points")) {
217
222
  throw new OpenLoyaltyError({
218
223
  code: "INSUFFICIENT_BALANCE",
219
224
  message: "Member does not have enough points to purchase this reward",
@@ -68,16 +68,13 @@ export declare const rewardToolDefinitions: readonly [{
68
68
  from: string;
69
69
  to: string;
70
70
  }>;
71
- usageLimit: import("zod").ZodObject<{
71
+ usageLimit: import("zod").ZodOptional<import("zod").ZodObject<{
72
72
  perUser: import("zod").ZodNumber;
73
- general: import("zod").ZodNumber;
74
73
  }, "strip", import("zod").ZodTypeAny, {
75
74
  perUser: number;
76
- general: number;
77
75
  }, {
78
76
  perUser: number;
79
- general: number;
80
- }>;
77
+ }>>;
81
78
  costInPoints: import("zod").ZodOptional<import("zod").ZodNumber>;
82
79
  usageInstruction: import("zod").ZodOptional<import("zod").ZodString>;
83
80
  active: import("zod").ZodOptional<import("zod").ZodBoolean>;
@@ -24,9 +24,9 @@ export const rewardToolDefinitions = [
24
24
  "1. translations: { en: { name: 'Reward Name' } }, " +
25
25
  "2. reward: 'material' or 'static_coupon' or 'dynamic_coupon', " +
26
26
  "3. activity: { from: '2026-01-01 00:00', to: '2027-12-31 23:59' }, " +
27
- "4. visibility: { from: '2026-01-01 00:00', to: '2027-12-31 23:59' }, " +
28
- "5. usageLimit: { perUser: 5, general: 1000 } - BOTH perUser AND general are REQUIRED. " +
27
+ "4. visibility: { from: '2026-01-01 00:00', to: '2027-12-31 23:59' }. " +
29
28
  "Types: material (physical goods), static_coupon (fixed discount), dynamic_coupon (variable value). " +
29
+ "For coupons: add couponValue, couponValueType ('money' or 'percentage'), daysValid. " +
30
30
  "For tier targeting: set target='level' and levels=['tier-uuid-1', 'tier-uuid-2'].",
31
31
  readOnly: false,
32
32
  inputSchema: RewardCreateInputSchema,
@@ -55,16 +55,13 @@ export declare const RewardCreateInputSchema: {
55
55
  from: string;
56
56
  to: string;
57
57
  }>;
58
- usageLimit: z.ZodObject<{
58
+ usageLimit: z.ZodOptional<z.ZodObject<{
59
59
  perUser: z.ZodNumber;
60
- general: z.ZodNumber;
61
60
  }, "strip", z.ZodTypeAny, {
62
61
  perUser: number;
63
- general: number;
64
62
  }, {
65
63
  perUser: number;
66
- general: number;
67
- }>;
64
+ }>>;
68
65
  costInPoints: z.ZodOptional<z.ZodNumber>;
69
66
  usageInstruction: z.ZodOptional<z.ZodString>;
70
67
  active: z.ZodOptional<z.ZodBoolean>;
@@ -26,13 +26,10 @@ const RewardVisibilityInputSchema = z.object({
26
26
  from: z.string().describe("Visibility start datetime (format: 'YYYY-MM-DD HH:mm'). REQUIRED."),
27
27
  to: z.string().describe("Visibility end datetime (format: 'YYYY-MM-DD HH:mm'). REQUIRED."),
28
28
  });
29
- // Usage limit schema (REQUIRED by API)
30
- // NOTE: API uses 'general' (not 'total') for total redemptions limit
31
- // BOTH perUser and general are REQUIRED by the API
29
+ // Usage limit schema - API only accepts perUser (NOT general)
32
30
  const RewardUsageLimitInputSchema = z.object({
33
- perUser: z.number().describe("Maximum redemptions per member. REQUIRED."),
34
- general: z.number().describe("Total redemptions across all members. REQUIRED. Use a large number (e.g., 10000) if unlimited."),
35
- });
31
+ perUser: z.number().describe("Maximum redemptions per member."),
32
+ }).describe("Usage limits. ⚠️ Only 'perUser' is supported - API rejects 'general' as extra field.");
36
33
  export const RewardCreateInputSchema = {
37
34
  storeCode: z.string().optional().describe("Store code for multi-tenant routing. DO NOT pass this parameter - the configured default will be used automatically. Only provide a value if the user explicitly asks to work with a different store."),
38
35
  translations: RewardTranslationsInputSchema.describe("Reward name translations. At least 'en' key with { name } is REQUIRED."),
@@ -40,7 +37,7 @@ export const RewardCreateInputSchema = {
40
37
  "conversion_coupon (converts points to coupon), material (physical goods), fortune_wheel (gamified)."),
41
38
  activity: RewardActivityInputSchema.describe("Activity period when reward can be purchased. Use datetime format 'YYYY-MM-DD HH:mm'. REQUIRED."),
42
39
  visibility: RewardVisibilityInputSchema.describe("Visibility period when reward is shown. Use datetime format 'YYYY-MM-DD HH:mm'. REQUIRED."),
43
- usageLimit: RewardUsageLimitInputSchema.describe("Usage limits. ⚠️ BOTH perUser AND general are REQUIRED by the API."),
40
+ usageLimit: RewardUsageLimitInputSchema.optional().describe("Usage limits. Only perUser is supported by API."),
44
41
  costInPoints: z.number().optional().describe("Points required to redeem this reward."),
45
42
  usageInstruction: z.string().optional().describe("Instructions for using the reward."),
46
43
  active: z.boolean().optional().describe("Whether reward is active (default: false)."),
@@ -19,17 +19,6 @@ export declare const TierSetCreateInputSchema: {
19
19
  attribute: "activeUnits" | "totalEarnedUnits" | "totalSpending" | "monthsSinceJoiningProgram" | "cumulatedEarnedUnits";
20
20
  walletType?: string | undefined;
21
21
  }>, "many">;
22
- downgrade: z.ZodOptional<z.ZodObject<{
23
- mode: z.ZodEnum<["none", "automatic", "x_days"]>;
24
- days: z.ZodOptional<z.ZodNumber>;
25
- }, "strip", z.ZodTypeAny, {
26
- mode: "none" | "automatic" | "x_days";
27
- days?: number | undefined;
28
- }, {
29
- mode: "none" | "automatic" | "x_days";
30
- days?: number | undefined;
31
- }>>;
32
- active: z.ZodOptional<z.ZodBoolean>;
33
22
  };
34
23
  export declare const TierSetGetInputSchema: {
35
24
  storeCode: z.ZodOptional<z.ZodString>;
@@ -107,11 +96,6 @@ type TierSetCreateInput = {
107
96
  attribute: string;
108
97
  walletType?: string;
109
98
  }[];
110
- downgrade?: {
111
- mode: string;
112
- days?: number;
113
- };
114
- active?: boolean;
115
99
  };
116
100
  type TierSetGetInput = {
117
101
  storeCode?: string;
@@ -196,17 +180,6 @@ export declare const tiersetToolDefinitions: readonly [{
196
180
  attribute: "activeUnits" | "totalEarnedUnits" | "totalSpending" | "monthsSinceJoiningProgram" | "cumulatedEarnedUnits";
197
181
  walletType?: string | undefined;
198
182
  }>, "many">;
199
- downgrade: z.ZodOptional<z.ZodObject<{
200
- mode: z.ZodEnum<["none", "automatic", "x_days"]>;
201
- days: z.ZodOptional<z.ZodNumber>;
202
- }, "strip", z.ZodTypeAny, {
203
- mode: "none" | "automatic" | "x_days";
204
- days?: number | undefined;
205
- }, {
206
- mode: "none" | "automatic" | "x_days";
207
- days?: number | undefined;
208
- }>>;
209
- active: z.ZodOptional<z.ZodBoolean>;
210
183
  };
211
184
  readonly handler: typeof tiersetCreate;
212
185
  }, {
@@ -20,11 +20,7 @@ export const TierSetCreateInputSchema = {
20
20
  walletType: z.string().optional().describe("Wallet type CODE (not UUID). Required for unit-based attributes (activeUnits, totalEarnedUnits, cumulatedEarnedUnits). " +
21
21
  "Use wallet_type_list to find walletType.code (e.g., 'default')."),
22
22
  })).describe("Array of conditions that define tier progression criteria. IMPORTANT: Use 'totalEarnedUnits' for lifetime points (NOT 'earnedUnits')."),
23
- downgrade: z.object({
24
- mode: DowngradeModeEnum.describe("Downgrade mode."),
25
- days: z.number().optional().describe("Number of days for x_days mode (required if mode is x_days)."),
26
- }).optional().describe("Downgrade configuration for the tier set."),
27
- active: z.boolean().optional().describe("Whether the tier set is active. Defaults to true."),
23
+ // NOTE: downgrade and active are NOT supported at creation time - use tierset_update after creation
28
24
  };
29
25
  export const TierSetGetInputSchema = {
30
26
  storeCode: z.string().optional().describe("Store code for multi-tenant routing. DO NOT pass this parameter - the configured default will be used automatically. Only provide a value if the user explicitly asks to work with a different store."),
@@ -87,25 +83,34 @@ export async function tiersetList(input) {
87
83
  }
88
84
  export async function tiersetCreate(input) {
89
85
  const storeCode = getStoreCode(input.storeCode);
90
- // API requires tierSet wrapper with translations (at least en is required)
91
- // Also include empty tiers array - some API versions require it
92
- const payload = {
93
- tierSet: {
94
- translations: {
95
- en: {
96
- name: input.name,
97
- description: input.description || "",
98
- },
86
+ // API QUIRK: Only accepts specific fields at creation time.
87
+ // Fields NOT supported at creation: tiers, active, downgrade
88
+ // These must be set via tierset_update or tierset_update_tiers after creation.
89
+ //
90
+ // REQUIRED fields:
91
+ // - translations.en.name (string)
92
+ // - conditions (array with attribute, optionally walletType)
93
+ const tierSetPayload = {
94
+ translations: {
95
+ en: {
96
+ name: input.name,
97
+ description: input.description || "",
99
98
  },
100
- conditions: input.conditions,
101
- tiers: [], // Required by API - tiers are added separately via tierset_update_tiers
102
- downgrade: input.downgrade,
103
- active: input.active ?? true,
104
99
  },
100
+ conditions: input.conditions,
105
101
  };
102
+ const payload = { tierSet: tierSetPayload };
106
103
  try {
107
104
  const response = await apiPost(`/${storeCode}/tierSet`, payload);
108
- const validated = TierSetSchema.parse(response);
105
+ // API QUIRK: Create endpoint only returns { tierSetId: string }
106
+ // We need to fetch the full tier set to get the conditionId values
107
+ const createResponse = response;
108
+ if (!createResponse.tierSetId) {
109
+ throw new Error(`Unexpected response format: ${JSON.stringify(response)}`);
110
+ }
111
+ // Fetch the created tier set to get condition IDs
112
+ const tierSetResponse = await apiGet(`/${storeCode}/tierSet/${createResponse.tierSetId}`);
113
+ const validated = TierSetSchema.parse(tierSetResponse);
109
114
  return {
110
115
  tierSetId: validated.tierSetId,
111
116
  conditions: validated.conditions.map((c) => ({
@@ -228,7 +233,9 @@ export const tiersetToolDefinitions = [
228
233
  description: "Create a new tier set (loyalty program structure). " +
229
234
  "⚠️ LIMIT: Maximum 3 ACTIVE tier sets per store. ALWAYS call tierset_list FIRST to check existing tier sets - if one exists that matches your needs, REUSE IT instead of creating a new one. " +
230
235
  "WORKFLOW: 1) tierset_list (check existing) → 2) tierset_create (only if needed) → 3) tierset_get (to get conditionId) → 4) tierset_update_tiers (to define thresholds). " +
231
- "Valid attributes: 'totalEarnedUnits' (lifetime points), 'activeUnits' (current balance), 'totalSpending', 'monthsSinceJoiningProgram'. " +
236
+ "⚠️ API LIMITATION: Only 'name', 'description', and 'conditions' are accepted at creation time. " +
237
+ "Fields like 'active', 'downgrade', 'tiers' are NOT supported at creation - use tierset_update after creation to set these. " +
238
+ "Valid condition attributes: 'totalEarnedUnits' (lifetime points), 'activeUnits' (current balance), 'totalSpending', 'monthsSinceJoiningProgram'. " +
232
239
  "COMMON MISTAKE: Use 'totalEarnedUnits' NOT 'earnedUnits' for lifetime points. " +
233
240
  "For unit-based attributes, set walletType to 'default'.",
234
241
  readOnly: false,
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { apiGet, apiPost } from "../client/http.js";
3
- import { formatApiError } from "../utils/errors.js";
3
+ import { formatApiError, OpenLoyaltyError } from "../utils/errors.js";
4
4
  import { getStoreCode } from "../config.js";
5
5
  import { buildPaginationParams, normalizeDateToISO } from "../utils/pagination.js";
6
6
  // Input Schemas
@@ -95,6 +95,19 @@ export async function transactionCreate(input) {
95
95
  };
96
96
  }
97
97
  catch (error) {
98
+ // Check for duplicate document number error
99
+ // Must check axios response data, not just error.message
100
+ const axiosError = error;
101
+ const apiErrors = axiosError.response?.data?.errors || [];
102
+ const allErrorMessages = apiErrors.map(e => e.message).join(" ").toLowerCase();
103
+ if (allErrorMessages.includes("document number") && allErrorMessages.includes("unique")) {
104
+ throw new OpenLoyaltyError({
105
+ code: "DUPLICATE_DOCUMENT",
106
+ message: `Transaction with document number '${input.header.documentNumber}' already exists`,
107
+ hint: `Each transaction must have a unique documentNumber. Use transaction_list(documentNumber: "${input.header.documentNumber}") to check if it exists, or generate a new unique document number (e.g., add timestamp suffix).`,
108
+ relatedTool: "ol_transaction_create",
109
+ });
110
+ }
98
111
  throw formatApiError(error, "ol_transaction_create");
99
112
  }
100
113
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-loyalty/mcp-server",
3
- "version": "1.3.3",
3
+ "version": "1.3.5",
4
4
  "type": "module",
5
5
  "description": "MCP server for Open Loyalty API - enables AI agents to manage loyalty programs, members, points, rewards, and transactions",
6
6
  "author": "Marcin Dyguda <md@openloyalty.io>",