@open-loyalty/mcp-server 1.13.0 → 1.14.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.
package/dist/config.d.ts CHANGED
@@ -14,7 +14,22 @@ declare const ConfigSchema: z.ZodObject<{
14
14
  }>;
15
15
  export type Config = z.infer<typeof ConfigSchema>;
16
16
  /**
17
- * Gets the store code, falling back to default from config if not provided.
17
+ * Resets the cached config. For testing only.
18
+ */
19
+ export declare function resetConfig(): void;
20
+ /**
21
+ * Returns true if a default store code is configured and locked.
22
+ * When locked, getStoreCode() always returns the default — ignoring any override.
23
+ */
24
+ export declare function isStoreCodeLocked(): boolean;
25
+ /**
26
+ * Gets the store code to use for API calls.
27
+ *
28
+ * HARD LOCK: When OPENLOYALTY_DEFAULT_STORE_CODE is configured, it ALWAYS wins.
29
+ * Any storeCode parameter passed by the agent is silently ignored.
30
+ * This prevents multi-tenant API keys from accidentally operating on the wrong store.
31
+ *
32
+ * When no default is configured, falls back to the provided storeCode parameter.
18
33
  * Throws a clear error if no store code is available from either source.
19
34
  */
20
35
  export declare function getStoreCode(storeCode?: string): string;
package/dist/config.js CHANGED
@@ -6,11 +6,31 @@ const ConfigSchema = z.object({
6
6
  });
7
7
  let config = null;
8
8
  /**
9
- * Gets the store code, falling back to default from config if not provided.
9
+ * Resets the cached config. For testing only.
10
+ */
11
+ export function resetConfig() {
12
+ config = null;
13
+ }
14
+ /**
15
+ * Returns true if a default store code is configured and locked.
16
+ * When locked, getStoreCode() always returns the default — ignoring any override.
17
+ */
18
+ export function isStoreCodeLocked() {
19
+ return !!getConfig().defaultStoreCode;
20
+ }
21
+ /**
22
+ * Gets the store code to use for API calls.
23
+ *
24
+ * HARD LOCK: When OPENLOYALTY_DEFAULT_STORE_CODE is configured, it ALWAYS wins.
25
+ * Any storeCode parameter passed by the agent is silently ignored.
26
+ * This prevents multi-tenant API keys from accidentally operating on the wrong store.
27
+ *
28
+ * When no default is configured, falls back to the provided storeCode parameter.
10
29
  * Throws a clear error if no store code is available from either source.
11
30
  */
12
31
  export function getStoreCode(storeCode) {
13
- const code = storeCode || getConfig().defaultStoreCode;
32
+ const config = getConfig();
33
+ const code = config.defaultStoreCode || storeCode;
14
34
  if (!code) {
15
35
  throw new Error("No store code available. Either configure OPENLOYALTY_DEFAULT_STORE_CODE in server settings, " +
16
36
  "or pass storeCode parameter. Use ol_store_list to see available stores.");
@@ -2,4 +2,4 @@
2
2
  * MCP Server Instructions - provides context and guidance for AI agents
3
3
  * using the Open Loyalty MCP server.
4
4
  */
5
- export declare const SERVER_INSTRUCTIONS = "\nOpen Loyalty MCP Server - Complete Loyalty Program Management\n\n## Starting a Session\nCall ol_context_get FIRST at the start of every session.\nThis returns wallet types, tier sets (with tier levelIds), active segments, active campaigns, active rewards, and reward categories in a single call.\nIt surfaces IDs for targeting, prevents duplicate creation, and replaces 5+ individual discovery calls.\nExample: Use tierSets[].tiers[].levelId for reward tier targeting, activeSegments[].segmentId for campaign audience targeting.\n\n## Connection & Configuration\n- API URL and API Key are PRE-CONFIGURED via server settings. NEVER ask the user for these values.\n- Store code may be pre-configured. If it is, the storeCode parameter on tools is auto-filled \u2014 do NOT prompt for it.\n- If no default store code is configured, ask the user which store to use at the START of the conversation (use ol_store_list to show options), then pass storeCode on all subsequent tool calls.\n- If the user wants to switch stores mid-conversation, they will tell you explicitly \u2014 only then change the storeCode parameter.\n\n## Domain Model\n\nMember (loyalty program participant)\n \u2514\u2500\u2500 Points (wallet balance, transfers)\n \u2514\u2500\u2500 Tier (current level: Bronze, Silver, Gold)\n \u2514\u2500\u2500 Transactions (purchase history)\n \u2514\u2500\u2500 Rewards (redeemed coupons)\n \u2514\u2500\u2500 Achievements (gamification progress)\n \u2514\u2500\u2500 Badges (visual rewards from achievements)\n\nTierSet (loyalty program structure)\n \u2514\u2500\u2500 Conditions (criteria: activeUnits, totalSpending)\n \u2514\u2500\u2500 Tiers (levels with thresholds)\n\nWalletType (points currency configuration)\n\nReward (redeemable items)\n \u2514\u2500\u2500 Categories (reward groupings)\n\nCampaign (automated points/rewards rules)\n \u2514\u2500\u2500 Type (earning, spending, custom, instant_reward)\n \u2514\u2500\u2500 Trigger (transaction, custom_event, points_transfer)\n \u2514\u2500\u2500 Rules (SKU, category, transaction amount conditions)\n \u2514\u2500\u2500 Effects (give_points, multiply_points, percentage_discount)\n \u2514\u2500\u2500 Targeting (segments, tiers for audience)\n\nSegment (member audience grouping)\n \u2514\u2500\u2500 Parts (groups of criteria - OR logic between parts)\n \u2514\u2500\u2500 Criteria (conditions - AND logic within part)\n Types: transaction_count, average_transaction_value, transaction_amount,\n transaction_percent_in_period, purchase_in_period, bought_skus, bought_labels,\n bought_makers, customer_list, anniversary, last_purchase_n_days_before,\n customer_has_labels, customer_with_labels_values\n\nAchievement (gamification goals)\n \u2514\u2500\u2500 Trigger (transaction, custom_event, points_transfer, reward_redemption, referral, achievement, tier_change, profile_update)\n \u2514\u2500\u2500 CompleteRule (periodGoal, period type, unique counting)\n \u2514\u2500\u2500 Badge (visual reward when completed)\n\nBadge (visual recognition)\n \u2514\u2500\u2500 Linked to achievements via badgeTypeId\n \u2514\u2500\u2500 Auto-created when referenced by achievement\n\n## Key Workflows\n\n### 1. Create Multi-tier Loyalty Program:\nCRITICAL CONCEPT: A tier set is a CONTAINER that holds ALL tiers (Bronze/Silver/Gold/etc).\n- Create exactly ONE tier set\n- Add ALL tiers to it in ONE call to tierset_update_tiers\n- DO NOT create multiple tier sets for different tiers!\n\nRECOMMENDED WORKFLOW:\n1. wallet_type_list \u2192 Get wallet code (usually \"default\")\n2. tierset_list \u2192 Check if tier set already exists\n3. IF EXISTS: tierset_get \u2192 Get conditionId from existing tier set\n ELSE: tierset_create \u2192 Create ONE tier set, then tierset_get to get conditionId\n4. tierset_update_tiers \u2192 Define ALL tier thresholds in ONE call:\n tiers: [\n { name: \"Bronze\", conditions: [{ conditionId: \"xxx\", value: 0 }] },\n { name: \"Silver\", conditions: [{ conditionId: \"xxx\", value: 500 }] },\n { name: \"Gold\", conditions: [{ conditionId: \"xxx\", value: 1000 }] },\n { name: \"Platinum\", conditions: [{ conditionId: \"xxx\", value: 2000 }] }\n ]\n5. tierset_get_tiers \u2192 Verify tiers created correctly\n\nGOTCHA - Valid tier condition attributes:\n- activeUnits (current spendable balance)\n- totalEarnedUnits (lifetime points - NOT 'earnedUnits'!)\n- totalSpending (lifetime spend amount)\n- monthsSinceJoiningProgram (tenure in months)\n\nCOMMON MISTAKE: Using 'earnedUnits' will fail - use 'totalEarnedUnits' for lifetime points.\n\n### 2. Register and Reward Member:\nmember_create \u2192 points_add (welcome bonus) \u2192 member_get (verify)\n\n### 3. Record Purchase and Earn Points:\ntransaction_create (with customerData) \u2192 triggers campaigns \u2192 points auto-added\n\n### 4. Redeem Reward:\nreward_list \u2192 reward_buy (deducts points) \u2192 reward_redeem (mark coupon used)\n\n### 5. Assign Unmatched Transaction:\ntransaction_list (matched=false) \u2192 transaction_assign_member \u2192 points earned\n\n### 6. Check Member Tier Progress:\nmember_get_tier_progress \u2192 shows current tier, next tier, progress %\n\n### 7. Double Points for VIP Members:\nsegment_create (with transaction_count criteria) \u2192 campaign_create (targeting that segment with give_points effect and pointsRule: \"transaction.grossValue * 2\")\n\n### 8. Purchase Achievement with Badge:\nbadge_list (find or note badgeTypeId) \u2192 achievement_create (type+trigger+aggregation+period required, badgeTypeId)\n\n### 9. Create Targeted Promotion:\nsegment_create (define audience) \u2192 campaign_create (type: earning, target: segment, segmentIds: [segmentId])\n\n### 10. Track Member Gamification:\nachievement_list_member_achievements \u2192 badge_get_member_badges \u2192 achievement_get_member_progress\n\n## Discovery Paths:\n- Members: member_list \u2192 member_get \u2192 points_get_balance\n- Tiers: tierset_list \u2192 tierset_get \u2192 tierset_get_tiers\n- Rewards: reward_category_list \u2192 reward_list \u2192 reward_get\n- Transactions: transaction_list \u2192 transaction_get\n- Campaigns: campaign_list \u2192 campaign_get \u2192 campaign_simulate\n- Segments: segment_list \u2192 segment_get \u2192 segment_get_members \u2192 campaign_create (audience targeting)\n- Achievements: achievement_list \u2192 badge_list \u2192 achievement_create (with badge)\n- Member Gamification: achievement_list_member_achievements \u2192 badge_get_member_badges\n\n## Important Patterns:\n- TIER SET = CONTAINER: One tier set holds ALL tiers (Bronze/Silver/Gold/etc)\n - Create ONE tier set, add ALL tiers in ONE tierset_update_tiers call\n - DO NOT create multiple tier sets for different tiers!\n- TIER SETUP: conditionId REQUIRED for tier thresholds\n 1. tierset_create returns conditions but you need the id from tierset_get\n 2. The conditionId is in tierset_get response: conditions[].id (this IS the conditionId)\n 3. Use the SAME conditionId for ALL tiers in tierset_update_tiers\n- TIER ATTRIBUTES: Use 'totalEarnedUnits' (NOT 'earnedUnits') for lifetime points\n- walletType uses CODE (e.g., 'default'), not UUID\n- Points operations: check balance with points_get_balance before spending\n- Reward flow: reward_buy returns couponCode, use reward_redeem to mark used\n- Transactions auto-match to members via customerData (email, phone, loyaltyCardNumber)\n\n### Pagination:\n- Traditional pagination: use page + perPage params\n- Cursor pagination: provide cursor from previous response for efficient deep pagination\n- Cursor-enabled tools: member_list, transaction_list, points_get_history\n- First scroll request: cursor=\"\" (empty string)\n- Subsequent requests: use cursor value from previous response\n- When cursor provided, page param is ignored\n\n### Campaign Patterns:\n- Campaign types: direct (standard), referral (referral programs)\n- Triggers: transaction (purchase), custom_event, time, achievement, etc.\n- Effects: give_points, give_reward, deduct_unit\n- Target campaigns to segments or tiers using audience.target: \"segment\" and audience.segments array\n- campaign_create requires activity.startsAt, rules[].name, and effects[].effect (use key effect, not type). pointsRule is a STRING expression.\n\n### Points Expression (pointsRule):\npointsRule is a STRING expression, not an object. Examples:\n- Fixed: \"100\"\n- Per dollar: \"transaction.grossValue * 10\"\n- Category-based: \"transaction.category('electronics').grossValue * 5\"\n- Capped: \"(transaction.grossValue * 2 >= 100) ? 100 : transaction.grossValue * 2\"\n- Rounded: \"round_up(0.1 * transaction.grossValue)\"\n\n### Points Operations (Fraud & Corrections):\n- ol_points_block: Freeze points from a member's balance (fraud hold). Returns transferId.\n WORKFLOW: ol_points_get_balance \u2192 ol_points_block \u2192 ol_points_unblock (to release)\n- ol_points_unblock(transferId): Release a blocked transfer, returning points to active balance.\n Use ol_points_get_history(type: 'blocked') to find blocked transfer IDs.\n- ol_points_cancel(transferId): Cancel a points transfer (reverses the movement). Use for corrections.\n- ol_points_expire(transferId): Manually expire a points batch before its natural expiry.\n Use ol_points_get_history to find transferIds for cancel/expire operations.\n\n### Member GDPR Operations:\n- ol_member_anonymize: GDPR right-to-erasure. Replaces all PII with anonymous placeholders.\n PRESERVES transaction history and program stats. Distinct from ol_member_delete (hard delete).\n Use anonymize for GDPR requests; use delete only to purge all records entirely.\n\n### Reward Category Management:\n- ol_reward_category_create: Create a new category (name required; sortOrder controls display order).\n- ol_reward_category_update: Update category name, active status, or sort order.\n Use ol_reward_category_list to find categoryId values before updating.\n\n### Member Issued Rewards & Challenges:\n- ol_member_get_issued_rewards: GET /redemption?customerId=X \u2014 member's redeemed rewards (issuedRewardId, name, status, token, costInPoints, rewardType, redemptionDate). Filter by status ('pending','fulfilled','cancelled') or rewardType.\n- ol_member_get_challenge_progress: GET /member/{id}/challenge \u2014 member's challenge progress (milestones, earned rewards).\n\n### Store Settings:\n- ol_store_get_settings: Read program configuration (tier type, wallets, downgrade mode, timezone, etc.). Always call before updating settings.\n- ol_store_update_settings: PATCH settings (partial). Key: tierAssignType ('points'|'transactions'), tierWalletCode, levelDowngradeMode ('none'|'automatic'|'after_x_days'), accountActivationRequired, identificationMethod ('email'|'phone'|'loyaltyCardNumber'), timezone.\n\n### Segment Patterns:\n- Parts use OR logic (member matches ANY part)\n- Criteria within a part use AND logic (member must match ALL criteria in that part)\n- Valid criteria types: transaction_count (requires both min AND max), average_transaction_value,\n transaction_amount, transaction_percent_in_period, purchase_in_period, bought_skus,\n bought_labels, bought_makers, customer_list, anniversary, last_purchase_n_days_before,\n customer_has_labels, customer_with_labels_values\n- transaction_count example: { type: \"transaction_count\", min: 5, max: 999999 } (for \"5 or more purchases\" \u2014 BOTH min and max required; omitting max returns MISSING_MAX_VALUE error)\n\n### Achievement Patterns:\n- Triggers: transaction, custom_event, points_transfer, reward_redemption, referral, achievement, tier_change, profile_update\n- periodGoal sets the target (e.g., 5 purchases, 1000 points spent)\n- period.type defines timeframe: day, week, month, year, last_day, calendarDays, calendarWeeks, calendarMonths, calendarYears\n- aggregation.type defines counting method: quantity (count events only)\n- Link to badge via badgeTypeId - badge auto-created if doesn't exist\n- uniqueAttribute for counting distinct values (e.g., unique products)\n\n### Achievement Period Types:\n- \"day\": Count all-time when consecutive=1\n- \"week\": Count per week (use consecutive=1 for all-time weekly tracking)\n- \"month\": Count per month\n- \"year\": Count per year\n- \"last_day\": Rolling window of N days (e.g., value: 7 for last 7 days)\n- \"calendarDays\": Reset daily at midnight\n- \"calendarWeeks\": Reset weekly (Monday)\n- \"calendarMonths\": Reset monthly (1st of month)\n- \"calendarYears\": Reset yearly (January 1st)\n\n### Streak Achievement Example (7-day login streak):\n{\n \"rules\": [{\n \"trigger\": \"custom_event\",\n \"event\": \"app_login\",\n \"type\": \"direct\",\n \"aggregation\": {\"type\": \"quantity\"},\n \"completeRule\": {\n \"periodGoal\": 1,\n \"period\": {\"type\": \"last_day\", \"value\": 7, \"consecutive\": 1}\n },\n \"limit\": {\n \"interval\": {\"type\": \"calendarDays\", \"value\": 1},\n \"value\": 1\n }\n }]\n}\n\n### Custom Event Workflows:\n\n#### Create Custom Event Campaign:\n1. custom_event_schema_list \u2192 Check if event type exists\n2. custom_event_schema_create (if needed) \u2192 Define event type and fields\n3. campaign_create with trigger: \"custom_event\", event: \"{event_type}\"\n4. Send events via custom_event_send to trigger campaign\n\n#### Create Achievement with Custom Event:\n1. custom_event_schema_create \u2192 Define event (e.g., \"location_visit\")\n2. achievement_create with:\n - rules[].trigger: \"custom_event\"\n - rules[].event: \"{event_type}\"\n - rules[].type: \"direct\"\n - rules[].aggregation.type: \"quantity\"\n - rules[].completeRule.periodGoal: {count}\n - rules[].completeRule.period: { type: \"day\", consecutive: 1 } (all-time)\n\n#### Create Campaign Triggered by Achievement:\n1. achievement_create \u2192 Get achievementId from response\n2. campaign_create with:\n - trigger: \"achievement\"\n - achievementId: \"{achievement_id}\"\n - rules with effects (give_points, give_reward)\n\n### Condition Operators (for campaigns/achievements):\n- is_equal, is_not_equal\n- is_greater_or_equal, is_greater, is_less_or_equal, is_less\n- is_between\n- is_after, is_before\n- contains, not_contains\n- has_at_least_one_label\n- is_one_of\n- starts_with, ends_with\n- matches_regex\n\n## Common Campaign Patterns (Industry-Agnostic)\n\nThese patterns work for any loyalty program - retail, QSR, aviation, fan engagement, hospitality, etc.\n\n### Pattern 1: Count Events Toward Goal (Achievements)\nUse case: Track member actions (visits, purchases, logins, check-ins) toward a milestone.\n1. custom_event_schema_create \u2192 Define your action type (e.g., \"store_visit\", \"app_login\")\n2. achievement_create with:\n - trigger: \"custom_event\"\n - event: \"{your_event_code}\"\n - aggregation: { type: \"quantity\" }\n - completeRule: { periodGoal: {count}, period: { type: \"day\", consecutive: 1 } }\n3. custom_event_send to log each action\n\n### Pattern 2: Points Per Action (Instant Rewards)\nUse case: Award instant points for each qualifying action.\n1. custom_event_schema_create (if not using transaction)\n2. campaign_create with:\n - trigger: \"custom_event\" (or \"transaction\")\n - event: \"{event_code}\" (for custom events)\n - rules: [{ effects: [{ effect: \"give_points\", pointsRule: \"50\" }] }]\n\n### Pattern 3: Conditional Rewards (Bonus Conditions)\nUse case: Award points only when a condition is met (e.g., early arrival, large purchase).\nUse ternary expressions in pointsRule instead of conditions array:\n- pointsRule: \"event.body.minutes_before >= 60 ? 25 : 0\"\n- pointsRule: \"transaction.grossValue >= 100 ? 50 : 0\"\n\n### Pattern 4: Execution Limits (Anti-Fraud)\nUse case: Limit rewards to prevent abuse (once per day, once per week).\nCampaign limits structure:\n{\n \"limits\": {\n \"executionsPerMember\": {\n \"value\": 1,\n \"interval\": { \"type\": \"calendarDays\", \"value\": 1 }\n }\n }\n}\nNote: Use \"calendarDays\" (not \"days\"), \"calendarWeeks\", \"calendarMonths\", \"calendarYears\".\nOmit interval entirely for lifetime/forever limit.\n\n### Pattern 5: Achievement-Triggered Bonus\nUse case: Award bonus points/rewards when member completes an achievement.\n1. achievement_create \u2192 Get achievementId from response\n2. campaign_create with:\n - trigger: \"achievement\"\n - achievementId: \"{achievement_id}\"\n - rules with give_points or give_reward effect\n\n### Pattern 6: Tiered Milestones\nUse case: Multiple achievement levels (bronze/silver/gold, 5/10/25 visits).\nCreate multiple achievements with increasing periodGoal values,\neach linked to a different bonus campaign with increasing rewards.\n\n### Pattern 7: Time-Limited Promotion\nUse case: Seasonal or promotional campaigns.\n{\n \"activity\": {\n \"startsAt\": \"2026-01-01 00:00+00:00\",\n \"endsAt\": \"2026-03-31 23:59+00:00\"\n }\n}\n\n### Pattern 8: Streak Tracking\nUse case: Reward consecutive daily/weekly actions (login streaks, workout streaks).\nAchievement with:\n- completeRule.period: { type: \"last_day\", value: 7 } (rolling 7-day window)\n- limit: { value: 1, interval: { type: \"calendarDays\", value: 1 } } (max 1 per day)\n\n## Phase 3: Analytics, Admin, Roles & Stores\n\n### Domain Model (Extended)\n\nAdmin (system user)\n \u2514\u2500\u2500 Roles (permission sets)\n \u2514\u2500\u2500 Permissions (resource + access level)\n \u2514\u2500\u2500 API Keys (programmatic access)\n\nStore (multi-tenant container)\n \u2514\u2500\u2500 Members, Campaigns, Rewards (isolated per store)\n\nAudit Log (compliance tracking)\n \u2514\u2500\u2500 eventType, entityType, entityId, username, timestamp\n\n### Analytics Workflows:\n\n#### 11. Get Program Overview:\nanalytics_dashboard \u2192 analytics_tiers \u2192 analytics_members\n\n#### 12. Analyze Points Economy:\nanalytics_points \u2192 analytics_units (per wallet) \u2192 points_get_histogram\n\n#### 13. Measure Campaign Performance:\nanalytics_campaigns \u2192 analytics_campaign_detail (specific campaign)\n\n#### 14. Track Transactions:\nanalytics_transactions \u2192 transaction_list (details)\n\n### Aggregation Queries (Top Spenders, Purchase Analysis):\n\nIMPORTANT: For queries like \"top 5 spenders in July 2025\", use this approach:\n\n#### 15. Find Top Spenders by Date Range:\n1. transaction_list(purchasedAtFrom, purchasedAtTo, perPage=50, cursor='') - returns customerId with each transaction\n2. Use cursor pagination to fetch ALL pages - even if there are 1000+ transactions\n3. Aggregate grossValue by customerId in your code\n4. Sort by total spent, take top N\n5. member_get for each top spender to get names/details\n\nCRITICAL: ALWAYS iterate ALL cursor pages \u2014 DO NOT skip or sample data.\n- Start cursor='' (empty), use returned cursor each page until exhausted\n- Aggregate grossValue by customerId in your code; 1000+ transactions is normal \u2014 just keep paginating\n- First request example: transaction_list(purchasedAtFrom: \"2025-07-01\", purchasedAtTo: \"2025-07-31\", perPage: 50, cursor: \"\")\n\n### Admin & Security Workflows:\n\n#### 15. Create Admin with Limited Role:\nacl_get_resources \u2192 role_create \u2192 admin_create (with roleId)\n\n#### 16. Generate API Key for Integration:\nadmin_get \u2192 apikey_create \u2192 SAVE TOKEN IMMEDIATELY (shown once!)\n\n#### 17. Audit User Actions:\naudit_list (filter by username/entity) \u2192 audit_export (compliance report)\n\n### Store Multi-Tenancy:\n\n#### 18. Create New Store:\nstore_create \u2192 configure campaigns/tiers \u2192 member_create (with storeCode)\n\n### Analytics Query Patterns:\n- All analytics tools support dateFrom/dateTo ISO date filters\n- analytics_dashboard: high-level program metrics\n- analytics_units: wallet-specific metrics (requires walletTypeCode)\n- analytics_campaign_detail: detailed metrics for single campaign\n\n### Admin \u2192 Role \u2192 Permission Model:\n\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Admin \u2502\u2500\u2500\u2500\u2500\u25B6\u2502 Role \u2502\u2500\u2500\u2500\u2500\u25B6\u2502 Permission \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502 \u2502\n \u2502 resource + access\n \u2502 (VIEW, MODIFY, etc.)\n \u25BC\n\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 API Key \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n\n### Store Multi-Tenancy:\n- Each store is isolated: members, campaigns, rewards, transactions\n- storeCode is auto-configured from server settings \u2014 NEVER ask the user for it\n- Only pass storeCode if the user explicitly requests a different store\n\n## Phase 4: Webhooks, Import & Export\n\n### Domain Model (Extended)\n\nWebhook Subscription (event notification)\n \u2514\u2500\u2500 eventName (event to subscribe to)\n \u2514\u2500\u2500 url (callback endpoint)\n \u2514\u2500\u2500 headers (custom HTTP headers)\n\nImport (bulk data upload)\n \u2514\u2500\u2500 type: member, groupValue, segmentMembers, unitTransferAdding, etc.\n \u2514\u2500\u2500 status: pending, processing, succeed, failed\n \u2514\u2500\u2500 items (individual row results)\n\nExport (bulk data download)\n \u2514\u2500\u2500 type: campaignCode, member, memberTier, memberSegment, rewardFulfillment\n \u2514\u2500\u2500 status: pending, done, failed, error\n \u2514\u2500\u2500 CSV file (when status='done')\n\n### Webhook Workflows:\n\n#### 19. Subscribe to Member Events for CRM Sync:\nwebhook_events \u2192 webhook_create (eventName: 'member.created', url: 'https://crm.example.com/webhook')\n\n#### 20. List and Manage Subscriptions:\nwebhook_list \u2192 webhook_get \u2192 webhook_update or webhook_delete\n\n### Import Workflows:\n\n#### 21. Bulk Import Members from CSV:\nimport_create (type: 'member', fileContent: CSV data) \u2192 import_list \u2192 import_get (check status)\n\n#### 22. Bulk Add Points to Members:\nimport_create (type: 'unitTransferAdding', fileContent: CSV) \u2192 poll import_get until complete\n\n### Export Workflows:\n\n#### 23. Export Campaign Codes:\nexport_create (type: 'campaignCode', filters: { campaignId }) \u2192 poll export_get (until status='done') \u2192 export_download\n\n#### 24. Export Member Data:\nexport_create (type: 'member') \u2192 export_get \u2192 export_download (returns CSV)\n\n### Webhook Patterns:\n- Use webhook_events to discover available event types before subscribing\n- API uses wrapper: { webhookSubscription: { eventName, url, headers? } }\n- Common events: member.created, member.updated, transaction.created, reward.purchased\n\n### Import Patterns:\n- Import is async: create returns importId, poll status with import_get\n- CSV format required - provide plain text, not base64\n- Types: member, groupValue, segmentMembers, unitTransferAdding, unitTransferSpending, transaction, campaignCode, rewardCoupon\n\n### Export Patterns:\n- Export is async: create returns exportId, poll status until 'done'\n- API body wrapper varies by type: { campaignCode: { filters... } }\n- Only call export_download when status='done'\n- Types: campaignCode, member, memberTier, memberSegment, rewardFulfillment\n\n### Channel, Language & Config Patterns:\n- ol_channel_*: Manage sales channels (Mobile App, POS, Web). identifier must be unique per store.\n- ol_language_*: Manage languages. GLOBAL \u2014 no storeCode. Cannot delete default language ('en').\n- ol_group_of_values_*: Manage label value groups and their values. No DELETE for groups (use active: false). Use ol_group_value_add/update/delete for individual values.\n- ol_reward_photo_upload/delete: Attach/remove images from rewards. Provide photoUrl \u2014 handler fetches and uploads. Use ol_reward_get to find existing photoIds for deletion.\n";
5
+ export declare const SERVER_INSTRUCTIONS = "\nOpen Loyalty MCP Server - Complete Loyalty Program Management\n\n## Starting a Session\nCall ol_context_get FIRST at the start of every session.\nThis returns wallet types, tier sets (with tier levelIds), active segments, active campaigns, active rewards, and reward categories in a single call.\nIt surfaces IDs for targeting, prevents duplicate creation, and replaces 5+ individual discovery calls.\nExample: Use tierSets[].tiers[].levelId for reward tier targeting, activeSegments[].segmentId for campaign audience targeting.\n\n## Connection & Configuration\n- API URL and API Key are PRE-CONFIGURED via server settings. NEVER ask the user for these values.\n- STORE CODE LOCK: When OPENLOYALTY_DEFAULT_STORE_CODE is configured, the store code is HARD-LOCKED. All API calls will use the configured default \u2014 any storeCode parameter you pass is silently ignored. Do NOT attempt to use a different store code. To switch stores, the user must change the OPENLOYALTY_DEFAULT_STORE_CODE environment variable and restart the server.\n- If no default store code is configured, ask the user which store to use at the START of the conversation (use ol_store_list to show options), then pass storeCode on all subsequent tool calls.\n\n## Domain Model\n\nMember (loyalty program participant)\n \u2514\u2500\u2500 Points (wallet balance, transfers)\n \u2514\u2500\u2500 Tier (current level: Bronze, Silver, Gold)\n \u2514\u2500\u2500 Transactions (purchase history)\n \u2514\u2500\u2500 Rewards (redeemed coupons)\n \u2514\u2500\u2500 Achievements (gamification progress)\n \u2514\u2500\u2500 Badges (visual rewards from achievements)\n\nTierSet (loyalty program structure)\n \u2514\u2500\u2500 Conditions (criteria: activeUnits, totalSpending)\n \u2514\u2500\u2500 Tiers (levels with thresholds)\n\nWalletType (points currency configuration)\n\nReward (redeemable items)\n \u2514\u2500\u2500 Categories (reward groupings)\n\nCampaign (automated points/rewards rules)\n \u2514\u2500\u2500 Type (earning, spending, custom, instant_reward)\n \u2514\u2500\u2500 Trigger (transaction, custom_event, points_transfer)\n \u2514\u2500\u2500 Rules (SKU, category, transaction amount conditions)\n \u2514\u2500\u2500 Effects (give_points, multiply_points, percentage_discount)\n \u2514\u2500\u2500 Targeting (segments, tiers for audience)\n\nSegment (member audience grouping)\n \u2514\u2500\u2500 Parts (groups of criteria - OR logic between parts)\n \u2514\u2500\u2500 Criteria (conditions - AND logic within part)\n Types: transaction_count, average_transaction_value, transaction_amount,\n transaction_percent_in_period, purchase_in_period, bought_skus, bought_labels,\n bought_makers, customer_list, anniversary, last_purchase_n_days_before,\n customer_has_labels, customer_with_labels_values\n\nAchievement (gamification goals)\n \u2514\u2500\u2500 Trigger (transaction, custom_event, points_transfer, reward_redemption, referral, achievement, tier_change, profile_update)\n \u2514\u2500\u2500 CompleteRule (periodGoal, period type, unique counting)\n \u2514\u2500\u2500 Badge (visual reward when completed)\n\nBadge (visual recognition)\n \u2514\u2500\u2500 Linked to achievements via badgeTypeId\n \u2514\u2500\u2500 Auto-created when referenced by achievement\n\n## Key Workflows\n\n### 1. Create Multi-tier Loyalty Program:\nCRITICAL CONCEPT: A tier set is a CONTAINER that holds ALL tiers (Bronze/Silver/Gold/etc).\n- Create exactly ONE tier set\n- Add ALL tiers to it in ONE call to tierset_update_tiers\n- DO NOT create multiple tier sets for different tiers!\n\nRECOMMENDED WORKFLOW:\n1. wallet_type_list \u2192 Get wallet code (usually \"default\")\n2. tierset_list \u2192 Check if tier set already exists\n3. IF EXISTS: tierset_get \u2192 Get conditionId from existing tier set\n ELSE: tierset_create \u2192 Create ONE tier set, then tierset_get to get conditionId\n4. tierset_update_tiers \u2192 Define ALL tier thresholds in ONE call:\n tiers: [\n { name: \"Bronze\", conditions: [{ conditionId: \"xxx\", value: 0 }] },\n { name: \"Silver\", conditions: [{ conditionId: \"xxx\", value: 500 }] },\n { name: \"Gold\", conditions: [{ conditionId: \"xxx\", value: 1000 }] },\n { name: \"Platinum\", conditions: [{ conditionId: \"xxx\", value: 2000 }] }\n ]\n5. tierset_get_tiers \u2192 Verify tiers created correctly\n\nGOTCHA - Valid tier condition attributes:\n- activeUnits (current spendable balance)\n- totalEarnedUnits (lifetime points - NOT 'earnedUnits'!)\n- totalSpending (lifetime spend amount)\n- monthsSinceJoiningProgram (tenure in months)\n\nCOMMON MISTAKE: Using 'earnedUnits' will fail - use 'totalEarnedUnits' for lifetime points.\n\n### 2. Register and Reward Member:\nmember_create \u2192 points_add (welcome bonus) \u2192 member_get (verify)\n\n### 3. Record Purchase and Earn Points:\ntransaction_create (with customerData) \u2192 triggers campaigns \u2192 points auto-added\n\n### 4. Redeem Reward:\nreward_list \u2192 reward_buy (deducts points) \u2192 reward_redeem (mark coupon used)\n\n### 5. Assign Unmatched Transaction:\ntransaction_list (matched=false) \u2192 transaction_assign_member \u2192 points earned\n\n### 6. Check Member Tier Progress:\nmember_get_tier_progress \u2192 shows current tier, next tier, progress %\n\n### 7. Double Points for VIP Members:\nsegment_create (with transaction_count criteria) \u2192 campaign_create (targeting that segment with give_points effect and pointsRule: \"transaction.grossValue * 2\")\n\n### 8. Purchase Achievement with Badge:\nbadge_list (find or note badgeTypeId) \u2192 achievement_create (type+trigger+aggregation+period required, badgeTypeId)\n\n### 9. Create Targeted Promotion:\nsegment_create (define audience) \u2192 campaign_create (type: earning, target: segment, segmentIds: [segmentId])\n\n### 10. Track Member Gamification:\nachievement_list_member_achievements \u2192 badge_get_member_badges \u2192 achievement_get_member_progress\n\n## Discovery Paths:\n- Members: member_list \u2192 member_get \u2192 points_get_balance\n- Tiers: tierset_list \u2192 tierset_get \u2192 tierset_get_tiers\n- Rewards: reward_category_list \u2192 reward_list \u2192 reward_get\n- Transactions: transaction_list \u2192 transaction_get\n- Campaigns: campaign_list \u2192 campaign_get \u2192 campaign_simulate\n- Segments: segment_list \u2192 segment_get \u2192 segment_get_members \u2192 campaign_create (audience targeting)\n- Achievements: achievement_list \u2192 badge_list \u2192 achievement_create (with badge)\n- Member Gamification: achievement_list_member_achievements \u2192 badge_get_member_badges\n\n## Important Patterns:\n- TIER SET = CONTAINER: One tier set holds ALL tiers (Bronze/Silver/Gold/etc)\n - Create ONE tier set, add ALL tiers in ONE tierset_update_tiers call\n - DO NOT create multiple tier sets for different tiers!\n- TIER SETUP: conditionId REQUIRED for tier thresholds\n 1. tierset_create returns conditions but you need the id from tierset_get\n 2. The conditionId is in tierset_get response: conditions[].id (this IS the conditionId)\n 3. Use the SAME conditionId for ALL tiers in tierset_update_tiers\n- TIER ATTRIBUTES: Use 'totalEarnedUnits' (NOT 'earnedUnits') for lifetime points\n- walletType uses CODE (e.g., 'default'), not UUID\n- Points operations: check balance with points_get_balance before spending\n- Reward flow: reward_buy returns couponCode, use reward_redeem to mark used\n- Transactions auto-match to members via customerData (email, phone, loyaltyCardNumber)\n\n### Pagination:\n- Traditional pagination: use page + perPage params\n- Cursor pagination: provide cursor from previous response for efficient deep pagination\n- Cursor-enabled tools: member_list, transaction_list, points_get_history\n- First scroll request: cursor=\"\" (empty string)\n- Subsequent requests: use cursor value from previous response\n- When cursor provided, page param is ignored\n\n### Campaign Patterns:\n- Campaign types: direct (standard), referral (referral programs)\n- Triggers: transaction (purchase), custom_event, time, achievement, etc.\n- Effects: give_points, give_reward, deduct_unit\n- Target campaigns to segments or tiers using audience.target: \"segment\" and audience.segments array\n- campaign_create requires activity.startsAt, rules[].name, and effects[].effect (use key effect, not type). pointsRule is a STRING expression.\n\n### Points Expression (pointsRule):\npointsRule is a STRING expression, not an object. Examples:\n- Fixed: \"100\"\n- Per dollar: \"transaction.grossValue * 10\"\n- Category-based: \"transaction.category('electronics').grossValue * 5\"\n- Capped: \"(transaction.grossValue * 2 >= 100) ? 100 : transaction.grossValue * 2\"\n- Rounded: \"round_up(0.1 * transaction.grossValue)\"\n\n### Points Operations (Fraud & Corrections):\n- ol_points_block: Freeze points from a member's balance (fraud hold). Returns transferId.\n WORKFLOW: ol_points_get_balance \u2192 ol_points_block \u2192 ol_points_unblock (to release)\n- ol_points_unblock(transferId): Release a blocked transfer, returning points to active balance.\n Use ol_points_get_history(type: 'blocked') to find blocked transfer IDs.\n- ol_points_cancel(transferId): Cancel a points transfer (reverses the movement). Use for corrections.\n- ol_points_expire(transferId): Manually expire a points batch before its natural expiry.\n Use ol_points_get_history to find transferIds for cancel/expire operations.\n\n### Member GDPR Operations:\n- ol_member_anonymize: GDPR right-to-erasure. Replaces all PII with anonymous placeholders.\n PRESERVES transaction history and program stats. Distinct from ol_member_delete (hard delete).\n Use anonymize for GDPR requests; use delete only to purge all records entirely.\n\n### Reward Category Management:\n- ol_reward_category_create: Create a new category (name required; sortOrder controls display order).\n- ol_reward_category_update: Update category name, active status, or sort order.\n Use ol_reward_category_list to find categoryId values before updating.\n\n### Member Issued Rewards & Challenges:\n- ol_member_get_issued_rewards: GET /redemption?customerId=X \u2014 member's redeemed rewards (issuedRewardId, name, status, token, costInPoints, rewardType, redemptionDate). Filter by status ('pending','fulfilled','cancelled') or rewardType.\n- ol_member_get_challenge_progress: GET /member/{id}/challenge \u2014 member's challenge progress (milestones, earned rewards).\n\n### Store Settings:\n- ol_store_get_settings: Read program configuration (tier type, wallets, downgrade mode, timezone, etc.). Always call before updating settings.\n- ol_store_update_settings: PATCH settings (partial). Key: tierAssignType ('points'|'transactions'), tierWalletCode, levelDowngradeMode ('none'|'automatic'|'after_x_days'), accountActivationRequired, identificationMethod ('email'|'phone'|'loyaltyCardNumber'), timezone.\n\n### Segment Patterns:\n- Parts use OR logic (member matches ANY part)\n- Criteria within a part use AND logic (member must match ALL criteria in that part)\n- Valid criteria types: transaction_count (requires both min AND max), average_transaction_value,\n transaction_amount, transaction_percent_in_period, purchase_in_period, bought_skus,\n bought_labels, bought_makers, customer_list, anniversary, last_purchase_n_days_before,\n customer_has_labels, customer_with_labels_values\n- transaction_count example: { type: \"transaction_count\", min: 5, max: 999999 } (for \"5 or more purchases\" \u2014 BOTH min and max required; omitting max returns MISSING_MAX_VALUE error)\n\n### Achievement Patterns:\n- Triggers: transaction, custom_event, points_transfer, reward_redemption, referral, achievement, tier_change, profile_update\n- periodGoal sets the target (e.g., 5 purchases, 1000 points spent)\n- period.type defines timeframe: day, week, month, year, last_day, calendarDays, calendarWeeks, calendarMonths, calendarYears\n- aggregation.type defines counting method: quantity (count events only)\n- Link to badge via badgeTypeId - badge auto-created if doesn't exist\n- uniqueAttribute for counting distinct values (e.g., unique products)\n\n### Achievement Period Types:\n- \"day\": Count all-time when consecutive=1\n- \"week\": Count per week (use consecutive=1 for all-time weekly tracking)\n- \"month\": Count per month\n- \"year\": Count per year\n- \"last_day\": Rolling window of N days (e.g., value: 7 for last 7 days)\n- \"calendarDays\": Reset daily at midnight\n- \"calendarWeeks\": Reset weekly (Monday)\n- \"calendarMonths\": Reset monthly (1st of month)\n- \"calendarYears\": Reset yearly (January 1st)\n\n### Streak Achievement Example (7-day login streak):\n{\n \"rules\": [{\n \"trigger\": \"custom_event\",\n \"event\": \"app_login\",\n \"type\": \"direct\",\n \"aggregation\": {\"type\": \"quantity\"},\n \"completeRule\": {\n \"periodGoal\": 1,\n \"period\": {\"type\": \"last_day\", \"value\": 7, \"consecutive\": 1}\n },\n \"limit\": {\n \"interval\": {\"type\": \"calendarDays\", \"value\": 1},\n \"value\": 1\n }\n }]\n}\n\n### Custom Event Workflows:\n\n#### Create Custom Event Campaign:\n1. custom_event_schema_list \u2192 Check if event type exists\n2. custom_event_schema_create (if needed) \u2192 Define event type and fields\n3. campaign_create with trigger: \"custom_event\", event: \"{event_type}\"\n4. Send events via custom_event_send to trigger campaign\n\n#### Create Achievement with Custom Event:\n1. custom_event_schema_create \u2192 Define event (e.g., \"location_visit\")\n2. achievement_create with:\n - rules[].trigger: \"custom_event\"\n - rules[].event: \"{event_type}\"\n - rules[].type: \"direct\"\n - rules[].aggregation.type: \"quantity\"\n - rules[].completeRule.periodGoal: {count}\n - rules[].completeRule.period: { type: \"day\", consecutive: 1 } (all-time)\n\n#### Create Campaign Triggered by Achievement:\n1. achievement_create \u2192 Get achievementId from response\n2. campaign_create with:\n - trigger: \"achievement\"\n - achievementId: \"{achievement_id}\"\n - rules with effects (give_points, give_reward)\n\n### Condition Operators (for campaigns/achievements):\n- is_equal, is_not_equal\n- is_greater_or_equal, is_greater, is_less_or_equal, is_less\n- is_between\n- is_after, is_before\n- contains, not_contains\n- has_at_least_one_label\n- is_one_of\n- starts_with, ends_with\n- matches_regex\n\n## Common Campaign Patterns (Industry-Agnostic)\n\nThese patterns work for any loyalty program - retail, QSR, aviation, fan engagement, hospitality, etc.\n\n### Pattern 1: Count Events Toward Goal (Achievements)\nUse case: Track member actions (visits, purchases, logins, check-ins) toward a milestone.\n1. custom_event_schema_create \u2192 Define your action type (e.g., \"store_visit\", \"app_login\")\n2. achievement_create with:\n - trigger: \"custom_event\"\n - event: \"{your_event_code}\"\n - aggregation: { type: \"quantity\" }\n - completeRule: { periodGoal: {count}, period: { type: \"day\", consecutive: 1 } }\n3. custom_event_send to log each action\n\n### Pattern 2: Points Per Action (Instant Rewards)\nUse case: Award instant points for each qualifying action.\n1. custom_event_schema_create (if not using transaction)\n2. campaign_create with:\n - trigger: \"custom_event\" (or \"transaction\")\n - event: \"{event_code}\" (for custom events)\n - rules: [{ effects: [{ effect: \"give_points\", pointsRule: \"50\" }] }]\n\n### Pattern 3: Conditional Rewards (Bonus Conditions)\nUse case: Award points only when a condition is met (e.g., early arrival, large purchase).\nUse ternary expressions in pointsRule instead of conditions array:\n- pointsRule: \"event.body.minutes_before >= 60 ? 25 : 0\"\n- pointsRule: \"transaction.grossValue >= 100 ? 50 : 0\"\n\n### Pattern 4: Execution Limits (Anti-Fraud)\nUse case: Limit rewards to prevent abuse (once per day, once per week).\nCampaign limits structure:\n{\n \"limits\": {\n \"executionsPerMember\": {\n \"value\": 1,\n \"interval\": { \"type\": \"calendarDays\", \"value\": 1 }\n }\n }\n}\nNote: Use \"calendarDays\" (not \"days\"), \"calendarWeeks\", \"calendarMonths\", \"calendarYears\".\nOmit interval entirely for lifetime/forever limit.\n\n### Pattern 5: Achievement-Triggered Bonus\nUse case: Award bonus points/rewards when member completes an achievement.\n1. achievement_create \u2192 Get achievementId from response\n2. campaign_create with:\n - trigger: \"achievement\"\n - achievementId: \"{achievement_id}\"\n - rules with give_points or give_reward effect\n\n### Pattern 6: Tiered Milestones\nUse case: Multiple achievement levels (bronze/silver/gold, 5/10/25 visits).\nCreate multiple achievements with increasing periodGoal values,\neach linked to a different bonus campaign with increasing rewards.\n\n### Pattern 7: Time-Limited Promotion\nUse case: Seasonal or promotional campaigns.\n{\n \"activity\": {\n \"startsAt\": \"2026-01-01 00:00+00:00\",\n \"endsAt\": \"2026-03-31 23:59+00:00\"\n }\n}\n\n### Pattern 8: Streak Tracking\nUse case: Reward consecutive daily/weekly actions (login streaks, workout streaks).\nAchievement with:\n- completeRule.period: { type: \"last_day\", value: 7 } (rolling 7-day window)\n- limit: { value: 1, interval: { type: \"calendarDays\", value: 1 } } (max 1 per day)\n\n## Phase 3: Analytics, Admin, Roles & Stores\n\n### Domain Model (Extended)\n\nAdmin (system user)\n \u2514\u2500\u2500 Roles (permission sets)\n \u2514\u2500\u2500 Permissions (resource + access level)\n \u2514\u2500\u2500 API Keys (programmatic access)\n\nStore (multi-tenant container)\n \u2514\u2500\u2500 Members, Campaigns, Rewards (isolated per store)\n\nAudit Log (compliance tracking)\n \u2514\u2500\u2500 eventType, entityType, entityId, username, timestamp\n\n### Analytics Workflows:\n\n#### 11. Get Program Overview:\nanalytics_dashboard \u2192 analytics_tiers \u2192 analytics_members\n\n#### 12. Analyze Points Economy:\nanalytics_points \u2192 analytics_units (per wallet) \u2192 points_get_histogram\n\n#### 13. Measure Campaign Performance:\nanalytics_campaigns \u2192 analytics_campaign_detail (specific campaign)\n\n#### 14. Track Transactions:\nanalytics_transactions \u2192 transaction_list (details)\n\n### Aggregation Queries (Top Spenders, Purchase Analysis):\n\nIMPORTANT: For queries like \"top 5 spenders in July 2025\", use this approach:\n\n#### 15. Find Top Spenders by Date Range:\n1. transaction_list(purchasedAtFrom, purchasedAtTo, perPage=50, cursor='') - returns customerId with each transaction\n2. Use cursor pagination to fetch ALL pages - even if there are 1000+ transactions\n3. Aggregate grossValue by customerId in your code\n4. Sort by total spent, take top N\n5. member_get for each top spender to get names/details\n\nCRITICAL: ALWAYS iterate ALL cursor pages \u2014 DO NOT skip or sample data.\n- Start cursor='' (empty), use returned cursor each page until exhausted\n- Aggregate grossValue by customerId in your code; 1000+ transactions is normal \u2014 just keep paginating\n- First request example: transaction_list(purchasedAtFrom: \"2025-07-01\", purchasedAtTo: \"2025-07-31\", perPage: 50, cursor: \"\")\n\n### Admin & Security Workflows:\n\n#### 15. Create Admin with Limited Role:\nacl_get_resources \u2192 role_create \u2192 admin_create (with roleId)\n\n#### 16. Generate API Key for Integration:\nadmin_get \u2192 apikey_create \u2192 SAVE TOKEN IMMEDIATELY (shown once!)\n\n#### 17. Audit User Actions:\naudit_list (filter by username/entity) \u2192 audit_export (compliance report)\n\n### Store Multi-Tenancy:\n\n#### 18. Create New Store:\nstore_create \u2192 configure campaigns/tiers \u2192 member_create (with storeCode)\n\n### Analytics Query Patterns:\n- All analytics tools support dateFrom/dateTo ISO date filters\n- analytics_dashboard: high-level program metrics\n- analytics_units: wallet-specific metrics (requires walletTypeCode)\n- analytics_campaign_detail: detailed metrics for single campaign\n\n### Admin \u2192 Role \u2192 Permission Model:\n\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Admin \u2502\u2500\u2500\u2500\u2500\u25B6\u2502 Role \u2502\u2500\u2500\u2500\u2500\u25B6\u2502 Permission \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502 \u2502\n \u2502 resource + access\n \u2502 (VIEW, MODIFY, etc.)\n \u25BC\n\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 API Key \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n\n### Store Multi-Tenancy:\n- Each store is isolated: members, campaigns, rewards, transactions\n- When OPENLOYALTY_DEFAULT_STORE_CODE is configured, the store code is HARD-LOCKED \u2014 all tools use the configured default regardless of any storeCode parameter you pass\n- Do NOT attempt to switch stores. To use a different store, the user must change the environment variable and restart the server\n- If no default is configured, ask the user which store to use at session start\n\n## Phase 4: Webhooks, Import & Export\n\n### Domain Model (Extended)\n\nWebhook Subscription (event notification)\n \u2514\u2500\u2500 eventName (event to subscribe to)\n \u2514\u2500\u2500 url (callback endpoint)\n \u2514\u2500\u2500 headers (custom HTTP headers)\n\nImport (bulk data upload)\n \u2514\u2500\u2500 type: member, groupValue, segmentMembers, unitTransferAdding, etc.\n \u2514\u2500\u2500 status: pending, processing, succeed, failed\n \u2514\u2500\u2500 items (individual row results)\n\nExport (bulk data download)\n \u2514\u2500\u2500 type: campaignCode, member, memberTier, memberSegment, rewardFulfillment\n \u2514\u2500\u2500 status: pending, done, failed, error\n \u2514\u2500\u2500 CSV file (when status='done')\n\n### Webhook Workflows:\n\n#### 19. Subscribe to Member Events for CRM Sync:\nwebhook_events \u2192 webhook_create (eventName: 'member.created', url: 'https://crm.example.com/webhook')\n\n#### 20. List and Manage Subscriptions:\nwebhook_list \u2192 webhook_get \u2192 webhook_update or webhook_delete\n\n### Import Workflows:\n\n#### 21. Bulk Import Members from CSV:\nimport_create (type: 'member', fileContent: CSV data) \u2192 import_list \u2192 import_get (check status)\n\n#### 22. Bulk Add Points to Members:\nimport_create (type: 'unitTransferAdding', fileContent: CSV) \u2192 poll import_get until complete\n\n### Export Workflows:\n\n#### 23. Export Campaign Codes:\nexport_create (type: 'campaignCode', filters: { campaignId }) \u2192 poll export_get (until status='done') \u2192 export_download\n\n#### 24. Export Member Data:\nexport_create (type: 'member') \u2192 export_get \u2192 export_download (returns CSV)\n\n### Webhook Patterns:\n- Use webhook_events to discover available event types before subscribing\n- API uses wrapper: { webhookSubscription: { eventName, url, headers? } }\n- Common events: member.created, member.updated, transaction.created, reward.purchased\n\n### Import Patterns:\n- Import is async: create returns importId, poll status with import_get\n- CSV format required - provide plain text, not base64\n- Types: member, groupValue, segmentMembers, unitTransferAdding, unitTransferSpending, transaction, campaignCode, rewardCoupon\n\n### Export Patterns:\n- Export is async: create returns exportId, poll status until 'done'\n- API body wrapper varies by type: { campaignCode: { filters... } }\n- Only call export_download when status='done'\n- Types: campaignCode, member, memberTier, memberSegment, rewardFulfillment\n\n### Channel, Language & Config Patterns:\n- ol_channel_*: Manage sales channels (Mobile App, POS, Web). identifier must be unique per store.\n- ol_language_*: Manage languages. GLOBAL \u2014 no storeCode. Cannot delete default language ('en').\n- ol_group_of_values_*: Manage label value groups and their values. No DELETE for groups (use active: false). Use ol_group_value_add/update/delete for individual values.\n- ol_reward_photo_upload/delete: Attach/remove images from rewards. Provide photoUrl \u2014 handler fetches and uploads. Use ol_reward_get to find existing photoIds for deletion.\n";
@@ -13,9 +13,8 @@ Example: Use tierSets[].tiers[].levelId for reward tier targeting, activeSegment
13
13
 
14
14
  ## Connection & Configuration
15
15
  - API URL and API Key are PRE-CONFIGURED via server settings. NEVER ask the user for these values.
16
- - Store code may be pre-configured. If it is, the storeCode parameter on tools is auto-filled do NOT prompt for it.
16
+ - STORE CODE LOCK: When OPENLOYALTY_DEFAULT_STORE_CODE is configured, the store code is HARD-LOCKED. All API calls will use the configured default — any storeCode parameter you pass is silently ignored. Do NOT attempt to use a different store code. To switch stores, the user must change the OPENLOYALTY_DEFAULT_STORE_CODE environment variable and restart the server.
17
17
  - If no default store code is configured, ask the user which store to use at the START of the conversation (use ol_store_list to show options), then pass storeCode on all subsequent tool calls.
18
- - If the user wants to switch stores mid-conversation, they will tell you explicitly — only then change the storeCode parameter.
19
18
 
20
19
  ## Domain Model
21
20
 
@@ -424,8 +423,9 @@ store_create → configure campaigns/tiers → member_create (with storeCode)
424
423
 
425
424
  ### Store Multi-Tenancy:
426
425
  - Each store is isolated: members, campaigns, rewards, transactions
427
- - storeCode is auto-configured from server settingsNEVER ask the user for it
428
- - Only pass storeCode if the user explicitly requests a different store
426
+ - When OPENLOYALTY_DEFAULT_STORE_CODE is configured, the store code is HARD-LOCKED all tools use the configured default regardless of any storeCode parameter you pass
427
+ - Do NOT attempt to switch stores. To use a different store, the user must change the environment variable and restart the server
428
+ - If no default is configured, ask the user which store to use at session start
429
429
 
430
430
  ## Phase 4: Webhooks, Import & Export
431
431
 
@@ -69,14 +69,14 @@ const AchievementRuleInputSchema = z.object({
69
69
  uniqueReferee: z.boolean().optional().describe("Whether referee must be unique (for referral achievements)."),
70
70
  });
71
71
  export const AchievementListInputSchema = {
72
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
72
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
73
73
  page: z.number().optional().describe("Page number (default: 1)."),
74
74
  perPage: z.number().optional().describe("Items per page (default: 10)."),
75
75
  active: z.boolean().optional().describe("Filter by active status."),
76
76
  name: z.string().optional().describe("Filter by achievement name."),
77
77
  };
78
78
  export const AchievementCreateInputSchema = {
79
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
79
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
80
80
  translations: z.record(z.string(), z.object({
81
81
  name: z.string(),
82
82
  description: z.string().optional(),
@@ -102,11 +102,11 @@ export const AchievementCreateInputSchema = {
102
102
  "If creation fails, try without badgeTypeId - the system will assign a default badge."),
103
103
  };
104
104
  export const AchievementGetInputSchema = {
105
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
105
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
106
106
  achievementId: z.string().describe("The achievement ID (UUID) to retrieve."),
107
107
  };
108
108
  export const AchievementUpdateInputSchema = {
109
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
109
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
110
110
  achievementId: z.string().describe("The achievement ID (UUID) to update."),
111
111
  translations: z.record(z.string(), z.object({
112
112
  name: z.string(),
@@ -132,7 +132,7 @@ export const AchievementUpdateInputSchema = {
132
132
  "May cause 'extra fields' errors in some API versions - try without if creation fails."),
133
133
  };
134
134
  export const AchievementPatchInputSchema = {
135
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
135
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
136
136
  achievementId: z.string().describe("The achievement ID (UUID) to patch."),
137
137
  active: z.boolean().optional().describe("Whether achievement is active."),
138
138
  translations: z.record(z.string(), z.object({
@@ -141,12 +141,12 @@ export const AchievementPatchInputSchema = {
141
141
  })).optional().describe("Partial translation updates."),
142
142
  };
143
143
  export const AchievementGetMemberProgressInputSchema = {
144
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
144
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
145
145
  memberId: z.string().describe("The member ID (UUID)."),
146
146
  achievementId: z.string().describe("The achievement ID (UUID)."),
147
147
  };
148
148
  export const AchievementListMemberAchievementsInputSchema = {
149
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
149
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
150
150
  memberId: z.string().describe("The member ID (UUID)."),
151
151
  page: z.number().optional().describe("Page number (default: 1)."),
152
152
  perPage: z.number().optional().describe("Items per page (default: 25)."),
@@ -1,28 +1,28 @@
1
1
  import { z } from "zod";
2
2
  export const AnalyticsTiersInputSchema = {
3
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
3
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
4
4
  };
5
5
  export const AnalyticsMembersInputSchema = {
6
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
6
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
7
7
  withTransaction: z.boolean().optional().describe("Filter: true for members with transactions, false for members without, omit for all."),
8
8
  };
9
9
  export const AnalyticsPointsInputSchema = {
10
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
10
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
11
11
  };
12
12
  export const AnalyticsTransactionsInputSchema = {
13
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
13
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
14
14
  };
15
15
  export const AnalyticsReferralsInputSchema = {
16
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
16
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
17
17
  };
18
18
  export const AnalyticsCampaignsInputSchema = {
19
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
19
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
20
20
  page: z.number().optional().describe("Page number (default: 1)."),
21
21
  perPage: z.number().optional().describe("Items per page (default: 25)."),
22
22
  executedAt: z.string().optional().describe("Filter by execution date (ISO format)."),
23
23
  };
24
24
  export const AnalyticsDashboardInputSchema = {
25
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
25
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
26
26
  dataType: z.enum([
27
27
  "registeredMembers", "activeMembers", "revenue", "avgSpending",
28
28
  "transactions", "avgTransactionValue", "avgNumberOfTransactions"
@@ -32,7 +32,7 @@ export const AnalyticsDashboardInputSchema = {
32
32
  intervalEndDate: z.string().optional().describe("End date (YYYY-MM-DD format)."),
33
33
  };
34
34
  export const AnalyticsUnitsInputSchema = {
35
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
35
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
36
36
  walletTypeCode: z.string().describe("Wallet type code (e.g., 'default' or 'points')."),
37
37
  dataType: z.enum([
38
38
  "unitsIssued", "unitsSpent", "unitsExpired", "unitsPending", "unitsActive"
@@ -42,6 +42,6 @@ export const AnalyticsUnitsInputSchema = {
42
42
  intervalEndDate: z.string().optional().describe("End date (YYYY-MM-DD format)."),
43
43
  };
44
44
  export const AnalyticsCampaignDetailInputSchema = {
45
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
45
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
46
46
  campaignId: z.string().describe("The campaign ID to get analytics for."),
47
47
  };
@@ -1,17 +1,17 @@
1
1
  import { z } from "zod";
2
2
  export const BadgeListInputSchema = {
3
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
3
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
4
4
  page: z.number().optional().describe("Page number (default: 1)."),
5
5
  perPage: z.number().optional().describe("Items per page (default: 10)."),
6
6
  name: z.string().optional().describe("Filter by badge name."),
7
7
  badgeTypeId: z.string().optional().describe("Filter by specific badge type ID."),
8
8
  };
9
9
  export const BadgeGetInputSchema = {
10
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
10
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
11
11
  badgeTypeId: z.string().describe("The badge type ID (UUID) to retrieve."),
12
12
  };
13
13
  export const BadgeUpdateInputSchema = {
14
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
14
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
15
15
  badgeTypeId: z.string().describe("The badge type ID (UUID) to update."),
16
16
  name: z.string().describe("Badge name (REQUIRED). API requires name as a direct field."),
17
17
  translations: z.record(z.string(), z.object({
@@ -22,7 +22,7 @@ export const BadgeUpdateInputSchema = {
22
22
  active: z.boolean().optional().describe("Whether badge type is active."),
23
23
  };
24
24
  export const BadgeGetMemberBadgesInputSchema = {
25
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
25
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
26
26
  memberId: z.string().describe("The member ID (UUID)."),
27
27
  page: z.number().optional().describe("Page number (default: 1)."),
28
28
  perPage: z.number().optional().describe("Items per page (default: 10)."),
@@ -141,7 +141,7 @@ const SimulateReferrerSchema = z.object({
141
141
  }).optional();
142
142
  // Exported Input Schemas
143
143
  export const CampaignListInputSchema = {
144
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
144
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
145
145
  page: z.number().optional().describe("Page number (default: 1)."),
146
146
  perPage: z.number().optional().describe("Items per page (default: 10)."),
147
147
  active: z.boolean().optional().describe("Filter by active status."),
@@ -149,11 +149,11 @@ export const CampaignListInputSchema = {
149
149
  trigger: CampaignTriggerEnum.optional().describe("Filter by trigger type: transaction, custom_event, time, etc."),
150
150
  };
151
151
  export const CampaignGetInputSchema = {
152
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
152
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
153
153
  campaignId: z.string().describe("The campaign ID (UUID) to retrieve."),
154
154
  };
155
155
  export const CampaignUpdateInputSchema = {
156
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
156
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
157
157
  campaignId: z.string().describe("The campaign ID (UUID) to update."),
158
158
  type: CampaignTypeEnum.describe("Campaign type: direct or referral."),
159
159
  trigger: CampaignTriggerEnum.describe("What triggers the campaign. Valid values: transaction, custom_event, time, achievement, " +
@@ -233,17 +233,17 @@ export const CampaignUpdateInputSchema = {
233
233
  event: z.string().optional().describe("Custom event name (required for custom_event trigger). Use ol_custom_event_schema_list to find valid event codes."),
234
234
  };
235
235
  export const CampaignPatchInputSchema = {
236
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
236
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
237
237
  campaignId: z.string().describe("The campaign ID (UUID) to patch."),
238
238
  active: z.boolean().optional().describe("Set campaign active status."),
239
239
  displayOrder: z.number().optional().describe("Set campaign display order."),
240
240
  };
241
241
  export const CampaignDeleteInputSchema = {
242
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
242
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
243
243
  campaignId: z.string().describe("The campaign ID (UUID) to delete."),
244
244
  };
245
245
  export const CampaignSimulateInputSchema = {
246
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
246
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
247
247
  trigger: CampaignTriggerEnum.describe("Campaign trigger type to simulate: transaction, custom_event, etc."),
248
248
  transaction: SimulateTransactionSchema.describe("Transaction data (required for transaction trigger)."),
249
249
  customEvent: SimulateCustomEventSchema.describe("Custom event data (required for custom_event trigger)."),
@@ -251,12 +251,12 @@ export const CampaignSimulateInputSchema = {
251
251
  referrer: SimulateReferrerSchema.describe("Referrer data for referral campaigns."),
252
252
  };
253
253
  export const CampaignGenerateCodesInputSchema = {
254
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
254
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
255
255
  campaignId: z.string().describe("The campaign ID (UUID) to generate codes for."),
256
256
  quantity: z.number().min(1).describe("Number of codes to generate."),
257
257
  };
258
258
  export const CampaignListCodesInputSchema = {
259
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
259
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
260
260
  campaignId: z.string().describe("The campaign ID (UUID) to list codes for."),
261
261
  page: z.number().optional().describe("Page number (default: 1)."),
262
262
  perPage: z.number().optional().describe("Items per page (default: 25)."),
@@ -264,7 +264,7 @@ export const CampaignListCodesInputSchema = {
264
264
  code: z.string().optional().describe("Filter by specific code."),
265
265
  };
266
266
  export const CampaignGetAvailableInputSchema = {
267
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
267
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
268
268
  memberId: z.string().describe("The member ID (UUID) to get available campaigns for."),
269
269
  page: z.number().optional().describe("Page number (default: 1)."),
270
270
  perPage: z.number().optional().describe("Items per page (default: 25)."),
@@ -272,19 +272,19 @@ export const CampaignGetAvailableInputSchema = {
272
272
  trigger: CampaignTriggerEnum.optional().describe("Filter by campaign trigger."),
273
273
  };
274
274
  export const CampaignGetVisibleInputSchema = {
275
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
275
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
276
276
  memberId: z.string().describe("The member ID (UUID) to get visible campaigns for."),
277
277
  page: z.number().optional().describe("Page number (default: 1)."),
278
278
  perPage: z.number().optional().describe("Items per page (default: 25)."),
279
279
  active: z.boolean().optional().describe("Filter by active status."),
280
280
  };
281
281
  export const CampaignGetLeaderboardInputSchema = {
282
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
282
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
283
283
  campaignId: z.string().describe("The campaign ID (UUID) to get leaderboard for."),
284
284
  type: z.string().optional().describe("Cycle type for ranking (e.g., 'weekly', 'monthly')."),
285
285
  };
286
286
  export const CampaignCreateInputSchema = {
287
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
287
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
288
288
  type: CampaignTypeEnum.describe("Campaign type: direct (standard) or referral (for referral programs)."),
289
289
  trigger: CampaignTriggerEnum.describe("What triggers the campaign. Valid values: transaction, custom_event, time, achievement, " +
290
290
  "custom_event_unique_code, leaderboard, internal_event, return_transaction, challenge, fortune_wheel."),
@@ -307,7 +307,7 @@ export const CampaignCreateInputSchema = {
307
307
  "Use achievement_create first, then pass the returned achievementId here to award points/rewards when members complete that achievement."),
308
308
  };
309
309
  export const MemberGetChallengeProgressInputSchema = {
310
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
310
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
311
311
  memberId: z.string().describe("Member ID (UUID) to retrieve challenge progress for."),
312
312
  page: z.number().optional().describe("Page number (default: 1)."),
313
313
  perPage: z.number().optional().describe("Items per page (default: 10)."),
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- const STORE_CODE = z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store.");
2
+ const STORE_CODE = z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured.");
3
3
  export const ChannelListInputSchema = {
4
4
  storeCode: STORE_CODE,
5
5
  page: z.number().optional().describe("Page number (default: 1)."),
@@ -12,6 +12,7 @@ interface TierSetSummary {
12
12
  }
13
13
  interface ContextResult {
14
14
  storeCode: string;
15
+ storeCodeLocked: boolean;
15
16
  store?: {
16
17
  code: string;
17
18
  name: string;
@@ -1,5 +1,5 @@
1
1
  import { apiGet } from "../../client/http.js";
2
- import { getStoreCode } from "../../config.js";
2
+ import { getStoreCode, isStoreCodeLocked } from "../../config.js";
3
3
  function extractCampaignName(campaign) {
4
4
  const translations = campaign.translations;
5
5
  if (Array.isArray(translations)) {
@@ -120,6 +120,7 @@ export async function contextGet(input) {
120
120
  }
121
121
  return {
122
122
  storeCode,
123
+ storeCodeLocked: isStoreCodeLocked(),
123
124
  store,
124
125
  walletTypes,
125
126
  tierSets,
@@ -1,4 +1,4 @@
1
1
  import { z } from "zod";
2
2
  export const ContextGetInputSchema = {
3
- storeCode: z.string().optional().describe("Store code to get context for. Uses configured default store if not provided."),
3
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set — any value passed here is silently ignored. Only used as fallback when no default is configured."),
4
4
  };
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  export const CustomEventSchemaListInputSchema = {
3
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
3
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
4
4
  page: z.number().optional().describe("Page number (default: 1)."),
5
5
  perPage: z.number().optional().describe("Items per page (default: 25)."),
6
6
  eventType: z.string().optional().describe("Filter by event type code."),
@@ -8,7 +8,7 @@ export const CustomEventSchemaListInputSchema = {
8
8
  active: z.boolean().optional().describe("Filter by active status."),
9
9
  };
10
10
  export const CustomEventSchemaGetInputSchema = {
11
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
11
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
12
12
  eventType: z.string().describe("The custom event type code to retrieve."),
13
13
  };
14
14
  const CustomEventSchemaFieldSchema = z.object({
@@ -17,7 +17,7 @@ const CustomEventSchemaFieldSchema = z.object({
17
17
  description: z.string().optional().describe("Field description."),
18
18
  });
19
19
  export const CustomEventSchemaCreateInputSchema = {
20
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
20
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
21
21
  eventType: z.string().describe("Unique event type code (e.g., 'location_visit', 'app_login', 'video_watch'). " +
22
22
  "Use snake_case. This is referenced in campaigns and achievements."),
23
23
  name: z.string().describe("Human-readable name for the event schema."),
@@ -25,19 +25,19 @@ export const CustomEventSchemaCreateInputSchema = {
25
25
  active: z.boolean().optional().describe("Whether the schema is active (default: true)."),
26
26
  };
27
27
  export const CustomEventSchemaUpdateInputSchema = {
28
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
28
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
29
29
  eventType: z.string().describe("The custom event type code to update."),
30
30
  name: z.string().describe("Human-readable name for the event schema."),
31
31
  fields: z.array(CustomEventSchemaFieldSchema).describe("Array of fields the event can contain."),
32
32
  active: z.boolean().optional().describe("Whether the schema is active."),
33
33
  };
34
34
  export const CustomEventSchemaActivateInputSchema = {
35
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
35
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
36
36
  eventType: z.string().describe("The custom event type code to activate/deactivate."),
37
37
  active: z.boolean().describe("Set to true to activate, false to deactivate."),
38
38
  };
39
39
  export const CustomEventSendInputSchema = {
40
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
40
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
41
41
  eventType: z.string().describe("The custom event type code (must match an existing schema). Use ol_custom_event_schema_list to find valid event codes."),
42
42
  customerData: z.object({
43
43
  customerId: z.string().optional().describe("Member ID (UUID) for identification."),
@@ -50,7 +50,7 @@ export const CustomEventSendInputSchema = {
50
50
  eventId: z.string().optional().describe("Unique event identifier for deduplication (optional, max 255 chars)."),
51
51
  };
52
52
  export const CustomEventListInputSchema = {
53
- storeCode: z.string().optional().describe("INTERNAL: Auto-configured from server settings. NEVER ask the user for this value. Only set if the user explicitly requests a different store."),
53
+ storeCode: z.string().optional().describe("Store code. LOCKED to the configured default when OPENLOYALTY_DEFAULT_STORE_CODE is set any value passed here is silently ignored. Only used as fallback when no default is configured."),
54
54
  page: z.number().optional().describe("Page number (default: 1)."),
55
55
  perPage: z.number().optional().describe("Items per page (default: 25)."),
56
56
  type: z.string().optional().describe("Filter by event type."),