@open-loyalty/mcp-server 1.3.6 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/instructions.d.ts +1 -1
  2. package/dist/instructions.js +18 -5
  3. package/dist/tools/achievement/index.js +10 -10
  4. package/dist/tools/achievement/schemas.js +16 -8
  5. package/dist/tools/reward/handlers.d.ts +3 -3
  6. package/dist/tools/reward/handlers.js +54 -8
  7. package/dist/tools/reward/index.d.ts +4 -8
  8. package/dist/tools/reward/index.js +13 -5
  9. package/dist/tools/reward/schemas.d.ts +3 -7
  10. package/dist/tools/reward/schemas.js +16 -8
  11. package/dist/tools/tierset.d.ts +1 -1
  12. package/dist/tools/tierset.js +49 -25
  13. package/dist/tools/transaction.js +5 -2
  14. package/dist/tools/wallet-type.js +27 -16
  15. package/dist/types/schemas/admin.d.ts +6 -6
  16. package/dist/types/schemas/role.d.ts +4 -4
  17. package/dist/types/schemas/wallet-type.js +7 -5
  18. package/package.json +1 -1
  19. package/dist/prompts/fan-engagement-setup.d.ts +0 -107
  20. package/dist/prompts/fan-engagement-setup.js +0 -492
  21. package/dist/tools/achievement.d.ts +0 -1017
  22. package/dist/tools/achievement.js +0 -354
  23. package/dist/tools/campaign.d.ts +0 -1800
  24. package/dist/tools/campaign.js +0 -737
  25. package/dist/tools/member.d.ts +0 -366
  26. package/dist/tools/member.js +0 -352
  27. package/dist/tools/reward.d.ts +0 -279
  28. package/dist/tools/reward.js +0 -361
  29. package/dist/tools/segment.d.ts +0 -816
  30. package/dist/tools/segment.js +0 -333
  31. package/dist/workflows/app-login-streak.d.ts +0 -39
  32. package/dist/workflows/app-login-streak.js +0 -298
  33. package/dist/workflows/early-arrival.d.ts +0 -33
  34. package/dist/workflows/early-arrival.js +0 -148
  35. package/dist/workflows/index.d.ts +0 -101
  36. package/dist/workflows/index.js +0 -208
  37. package/dist/workflows/match-attendance.d.ts +0 -45
  38. package/dist/workflows/match-attendance.js +0 -308
  39. package/dist/workflows/sportsbar-visit.d.ts +0 -41
  40. package/dist/workflows/sportsbar-visit.js +0 -284
  41. package/dist/workflows/vod-watching.d.ts +0 -43
  42. package/dist/workflows/vod-watching.js +0 -326
@@ -1,333 +0,0 @@
1
- import { z } from "zod";
2
- import { apiGet, apiPost, apiPut, apiDelete } from "../client/http.js";
3
- import { formatApiError } from "../utils/errors.js";
4
- import { getStoreCode } from "../config.js";
5
- // Input Schemas
6
- export const SegmentListInputSchema = {
7
- 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."),
8
- page: z.number().optional().describe("Page number (default: 1)."),
9
- perPage: z.number().optional().describe("Items per page (default: 10)."),
10
- active: z.boolean().optional().describe("Filter by active status."),
11
- name: z.string().optional().describe("Filter by segment name."),
12
- };
13
- // Criterion input schema - flexible to support all criterion types
14
- // NOTE: Only certain criterion types are supported by the API.
15
- // WORKING: transaction_count (requires both min AND max; MCP will auto-fill max if omitted)
16
- // NOT WORKING: tier, points_balance (rejected by API despite being documented)
17
- const SegmentCriterionInputSchema = z.object({
18
- type: z.string().describe("Criterion type. WORKING: 'transaction_count' (requires both min AND max; MCP auto-fills max if omitted). NOT WORKING: 'tier', 'points_balance' are rejected by the API."),
19
- criterionId: z.string().optional().describe("Criterion ID (optional, generated if not provided)."),
20
- // Common criterion data fields
21
- days: z.number().optional().describe("Days for time-based criteria."),
22
- fromDate: z.string().optional().describe("Start date (ISO format)."),
23
- toDate: z.string().optional().describe("End date (ISO format)."),
24
- min: z.number().optional().describe("Minimum value."),
25
- max: z.number().optional().describe("Maximum value."),
26
- posIds: z.array(z.string()).optional().describe("POS IDs."),
27
- skus: z.array(z.string()).optional().describe("SKU codes."),
28
- makers: z.array(z.string()).optional().describe("Maker/brand names."),
29
- labels: z.array(z.object({ key: z.string(), value: z.string() })).optional().describe("Label key-value pairs."),
30
- tierIds: z.array(z.string()).optional().describe("Tier level IDs."),
31
- campaignId: z.string().optional().describe("Campaign ID for campaign_completion criterion."),
32
- countries: z.array(z.string()).optional().describe("Country codes."),
33
- anniversaryType: z.string().optional().describe("Anniversary type."),
34
- customAttributeKey: z.string().optional().describe("Custom attribute key."),
35
- customAttributeValue: z.string().optional().describe("Custom attribute value."),
36
- }).passthrough();
37
- // Part input schema
38
- const SegmentPartInputSchema = z.object({
39
- segmentPartId: z.string().optional().describe("Part ID (optional)."),
40
- criteria: z.array(SegmentCriterionInputSchema).describe("Criteria for this part (AND logic)."),
41
- });
42
- export const SegmentCreateInputSchema = {
43
- 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."),
44
- name: z.string().describe("Segment name (required)."),
45
- description: z.string().optional().describe("Segment description."),
46
- active: z.boolean().optional().describe("Whether segment is active (default: false)."),
47
- parts: z.array(SegmentPartInputSchema).describe("Segment parts. Parts use OR logic (ANY part matches), criteria within parts use AND logic (ALL criteria must match)."),
48
- };
49
- export const SegmentGetInputSchema = {
50
- 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."),
51
- segmentId: z.string().describe("The segment ID (UUID) to retrieve."),
52
- };
53
- export const SegmentUpdateInputSchema = {
54
- 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."),
55
- segmentId: z.string().describe("The segment ID (UUID) to update."),
56
- name: z.string().describe("Segment name."),
57
- description: z.string().optional().describe("Segment description."),
58
- active: z.boolean().optional().describe("Whether segment is active."),
59
- parts: z.array(SegmentPartInputSchema).describe("Segment parts."),
60
- };
61
- export const SegmentDeleteInputSchema = {
62
- 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."),
63
- segmentId: z.string().describe("The segment ID (UUID) to delete."),
64
- };
65
- export const SegmentGetMembersInputSchema = {
66
- 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."),
67
- segmentId: z.string().describe("The segment ID (UUID) to get members for."),
68
- page: z.number().optional().describe("Page number (default: 1)."),
69
- perPage: z.number().optional().describe("Items per page (default: 25)."),
70
- };
71
- export const SegmentActivateInputSchema = {
72
- 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."),
73
- segmentId: z.string().describe("The segment ID (UUID) to activate."),
74
- };
75
- export const SegmentDeactivateInputSchema = {
76
- 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."),
77
- segmentId: z.string().describe("The segment ID (UUID) to deactivate."),
78
- };
79
- export const SegmentGetResourcesInputSchema = {
80
- 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."),
81
- segmentId: z.string().describe("The segment ID (UUID) to get associated resources for."),
82
- };
83
- // Handler functions
84
- export async function segmentList(input) {
85
- const storeCode = getStoreCode(input.storeCode);
86
- const params = new URLSearchParams();
87
- if (input.page)
88
- params.append("_page", String(input.page));
89
- if (input.perPage)
90
- params.append("_itemsOnPage", String(input.perPage));
91
- if (input.active !== undefined)
92
- params.append("active", String(input.active));
93
- if (input.name)
94
- params.append("name", input.name);
95
- const queryString = params.toString();
96
- const url = `/${storeCode}/segment${queryString ? `?${queryString}` : ""}`;
97
- try {
98
- const response = await apiGet(url);
99
- const segments = (response.items || []).map((item) => ({
100
- segmentId: item.segmentId,
101
- name: item.name,
102
- description: item.description,
103
- active: item.active,
104
- customersCount: item.customersCount,
105
- createdAt: item.createdAt,
106
- }));
107
- const total = response.total || {};
108
- return {
109
- segments,
110
- total: {
111
- all: typeof total.all === "number" ? total.all : undefined,
112
- filtered: typeof total.filtered === "number" ? total.filtered : undefined,
113
- },
114
- };
115
- }
116
- catch (error) {
117
- throw formatApiError(error, "ol_segment_list");
118
- }
119
- }
120
- const DEFAULT_TRANSACTION_COUNT_MAX = 999999;
121
- function normalizeSegmentParts(parts) {
122
- return parts.map((part) => ({
123
- ...part,
124
- criteria: part.criteria.map((criterion) => {
125
- if (!criterion || typeof criterion !== "object") {
126
- return criterion;
127
- }
128
- const normalized = { ...criterion };
129
- if (normalized.type === "transaction_count" &&
130
- normalized.min !== undefined &&
131
- normalized.max === undefined) {
132
- normalized.max = DEFAULT_TRANSACTION_COUNT_MAX;
133
- }
134
- return normalized;
135
- }),
136
- }));
137
- }
138
- export async function segmentCreate(input) {
139
- const storeCode = getStoreCode(input.storeCode);
140
- const segmentPayload = {
141
- name: input.name,
142
- parts: normalizeSegmentParts(input.parts),
143
- };
144
- if (input.description)
145
- segmentPayload.description = input.description;
146
- if (input.active !== undefined)
147
- segmentPayload.active = input.active;
148
- try {
149
- // CRITICAL: Wrap body as { segment: {...} }
150
- const response = await apiPost(`/${storeCode}/segment`, { segment: segmentPayload });
151
- return { segmentId: response.segmentId };
152
- }
153
- catch (error) {
154
- throw formatApiError(error, "ol_segment_create");
155
- }
156
- }
157
- export async function segmentGet(input) {
158
- const storeCode = getStoreCode(input.storeCode);
159
- try {
160
- const response = await apiGet(`/${storeCode}/segment/${input.segmentId}`);
161
- return response;
162
- }
163
- catch (error) {
164
- throw formatApiError(error, "ol_segment_get");
165
- }
166
- }
167
- export async function segmentUpdate(input) {
168
- const storeCode = getStoreCode(input.storeCode);
169
- const segmentPayload = {
170
- name: input.name,
171
- parts: normalizeSegmentParts(input.parts),
172
- };
173
- if (input.description !== undefined)
174
- segmentPayload.description = input.description;
175
- if (input.active !== undefined)
176
- segmentPayload.active = input.active;
177
- try {
178
- // CRITICAL: Wrap body as { segment: {...} }
179
- await apiPut(`/${storeCode}/segment/${input.segmentId}`, { segment: segmentPayload });
180
- }
181
- catch (error) {
182
- throw formatApiError(error, "ol_segment_update");
183
- }
184
- }
185
- export async function segmentDelete(input) {
186
- const storeCode = getStoreCode(input.storeCode);
187
- try {
188
- await apiDelete(`/${storeCode}/segment/${input.segmentId}`);
189
- }
190
- catch (error) {
191
- throw formatApiError(error, "ol_segment_delete");
192
- }
193
- }
194
- export async function segmentGetMembers(input) {
195
- const storeCode = getStoreCode(input.storeCode);
196
- const params = new URLSearchParams();
197
- if (input.page)
198
- params.append("_page", String(input.page));
199
- if (input.perPage)
200
- params.append("_itemsOnPage", String(input.perPage));
201
- const queryString = params.toString();
202
- const url = `/${storeCode}/segment/${input.segmentId}/members${queryString ? `?${queryString}` : ""}`;
203
- try {
204
- const response = await apiGet(url);
205
- const members = (response.items || []).map((item) => ({
206
- customerId: item.customerId,
207
- firstName: item.firstName,
208
- lastName: item.lastName,
209
- email: item.email,
210
- loyaltyCardNumber: item.loyaltyCardNumber,
211
- active: item.active,
212
- }));
213
- const total = response.total || {};
214
- return {
215
- members,
216
- total: {
217
- all: typeof total.all === "number" ? total.all : undefined,
218
- filtered: typeof total.filtered === "number" ? total.filtered : undefined,
219
- },
220
- };
221
- }
222
- catch (error) {
223
- throw formatApiError(error, "ol_segment_get_members");
224
- }
225
- }
226
- export async function segmentActivate(input) {
227
- const storeCode = getStoreCode(input.storeCode);
228
- try {
229
- await apiPost(`/${storeCode}/segment/${input.segmentId}/activate`, {});
230
- }
231
- catch (error) {
232
- throw formatApiError(error, "ol_segment_activate");
233
- }
234
- }
235
- export async function segmentDeactivate(input) {
236
- const storeCode = getStoreCode(input.storeCode);
237
- try {
238
- await apiPost(`/${storeCode}/segment/${input.segmentId}/deactivate`, {});
239
- }
240
- catch (error) {
241
- throw formatApiError(error, "ol_segment_deactivate");
242
- }
243
- }
244
- export async function segmentGetResources(input) {
245
- const storeCode = getStoreCode(input.storeCode);
246
- try {
247
- const response = await apiGet(`/${storeCode}/segment/${input.segmentId}/resources`);
248
- const resources = response.items || response.resources || [];
249
- return { resources };
250
- }
251
- catch (error) {
252
- throw formatApiError(error, "ol_segment_get_resources");
253
- }
254
- }
255
- // Tool definitions
256
- export const segmentToolDefinitions = [
257
- {
258
- name: "ol_segment_list",
259
- title: "List Segments",
260
- description: "List customer segments. Segments group members by criteria (purchase behavior, tier, location, etc). Use for campaign targeting and analytics.",
261
- readOnly: true,
262
- inputSchema: SegmentListInputSchema,
263
- handler: segmentList,
264
- },
265
- {
266
- name: "ol_segment_create",
267
- title: "Create Segment",
268
- description: "Create segment to group members. Parts use OR logic (member matches if ANY part matches). Criteria within parts use AND logic (must match ALL criteria in that part). " +
269
- "WORKING criterion types: 'transaction_count' (requires BOTH min AND max). " +
270
- "NOT WORKING: 'tier', 'points_balance' are rejected by the API. " +
271
- "Example - High activity segment: parts: [{ criteria: [{ type: 'transaction_count', min: 5, max: 999999 }] }]",
272
- readOnly: false,
273
- inputSchema: SegmentCreateInputSchema,
274
- handler: segmentCreate,
275
- },
276
- {
277
- name: "ol_segment_get",
278
- title: "Get Segment Details",
279
- description: "Get full segment details including all parts and criteria configurations.",
280
- readOnly: true,
281
- inputSchema: SegmentGetInputSchema,
282
- handler: segmentGet,
283
- },
284
- {
285
- name: "ol_segment_update",
286
- title: "Update Segment",
287
- description: "Update segment configuration. Requires full segment definition (name, parts with criteria). Use segment_get first to retrieve current configuration.",
288
- readOnly: false,
289
- inputSchema: SegmentUpdateInputSchema,
290
- handler: segmentUpdate,
291
- },
292
- {
293
- name: "ol_segment_delete",
294
- title: "Delete Segment (Permanent)",
295
- description: "Permanently delete a segment. Cannot be undone. Check segment_get_resources first to see what uses this segment.",
296
- readOnly: false,
297
- destructive: true,
298
- inputSchema: SegmentDeleteInputSchema,
299
- handler: segmentDelete,
300
- },
301
- {
302
- name: "ol_segment_get_members",
303
- title: "Get Segment Members",
304
- description: "Get members belonging to a segment. Returns paginated list of member details. Use for verifying segment criteria or exporting member lists.",
305
- readOnly: true,
306
- inputSchema: SegmentGetMembersInputSchema,
307
- handler: segmentGetMembers,
308
- },
309
- {
310
- name: "ol_segment_activate",
311
- title: "Activate Segment",
312
- description: "Activate a segment. Active segments are used for campaign targeting and can be queried for members.",
313
- readOnly: false,
314
- inputSchema: SegmentActivateInputSchema,
315
- handler: segmentActivate,
316
- },
317
- {
318
- name: "ol_segment_deactivate",
319
- title: "Deactivate Segment",
320
- description: "Deactivate a segment. Deactivated segments are not used for campaign targeting but retain their configuration.",
321
- readOnly: false,
322
- inputSchema: SegmentDeactivateInputSchema,
323
- handler: segmentDeactivate,
324
- },
325
- {
326
- name: "ol_segment_get_resources",
327
- title: "Get Segment Resources",
328
- description: "Get resources (campaigns, rewards, etc.) that use this segment for targeting. Check before deleting a segment.",
329
- readOnly: true,
330
- inputSchema: SegmentGetResourcesInputSchema,
331
- handler: segmentGetResources,
332
- },
333
- ];
@@ -1,39 +0,0 @@
1
- /**
2
- * App Login Streak Workflow
3
- *
4
- * Creates a campaign to reward fans for daily app logins,
5
- * with achievements for consecutive login streaks.
6
- */
7
- export interface AppLoginStreakConfig {
8
- /** Coins awarded per daily login */
9
- coinsPerLogin: number;
10
- /** Whether to create streak bonus achievement */
11
- createStreakBonus: boolean;
12
- /** Number of consecutive days for streak achievement */
13
- streakDays: number;
14
- /** Bonus coins for completing the streak */
15
- streakBonus: number;
16
- /** Badge name for streak achievement (optional) */
17
- badgeName?: string;
18
- /** Season start date (ISO format) */
19
- seasonStart: string;
20
- /** Season end date (ISO format) */
21
- seasonEnd: string;
22
- }
23
- export interface AppLoginStreakResult {
24
- success: boolean;
25
- loginCampaignId?: string;
26
- streakAchievementId?: string;
27
- streakBonusCampaignId?: string;
28
- errors: string[];
29
- summary: string;
30
- }
31
- /**
32
- * Execute the app login streak workflow
33
- */
34
- export declare function executeAppLoginStreakWorkflow(config?: Partial<AppLoginStreakConfig>): Promise<AppLoginStreakResult>;
35
- export declare const appLoginStreakWorkflow: {
36
- id: string;
37
- name: string;
38
- execute: typeof executeAppLoginStreakWorkflow;
39
- };
@@ -1,298 +0,0 @@
1
- /**
2
- * App Login Streak Workflow
3
- *
4
- * Creates a campaign to reward fans for daily app logins,
5
- * with achievements for consecutive login streaks.
6
- */
7
- import { campaignCreate, campaignList } from "../tools/campaign/handlers.js";
8
- import { achievementCreate, achievementList } from "../tools/achievement.js";
9
- import { badgeList } from "../tools/badge.js";
10
- import { formatOLDate, DEFAULTS } from "../prompts/fan-engagement-setup.js";
11
- // ============================================================================
12
- // Workflow Implementation
13
- // ============================================================================
14
- /**
15
- * Execute the app login streak workflow
16
- */
17
- export async function executeAppLoginStreakWorkflow(config = {}) {
18
- const result = {
19
- success: false,
20
- errors: [],
21
- summary: "",
22
- };
23
- // Merge with defaults
24
- const cfg = {
25
- coinsPerLogin: config.coinsPerLogin ?? DEFAULTS.appLoginStreak.coinsPerLogin,
26
- createStreakBonus: config.createStreakBonus ?? true,
27
- streakDays: config.streakDays ?? DEFAULTS.appLoginStreak.streakDays,
28
- streakBonus: config.streakBonus ?? DEFAULTS.appLoginStreak.streakBonus,
29
- badgeName: config.badgeName ?? "Dedicated Fan",
30
- seasonStart: config.seasonStart ?? DEFAULTS.seasonDates.start,
31
- seasonEnd: config.seasonEnd ?? DEFAULTS.seasonDates.end,
32
- };
33
- try {
34
- // Step 1: Create daily login campaign (limited to 1 per day)
35
- const loginCampaignResult = await createLoginCampaign(cfg);
36
- if (loginCampaignResult.error) {
37
- result.errors.push(loginCampaignResult.error);
38
- }
39
- else {
40
- result.loginCampaignId = loginCampaignResult.campaignId;
41
- }
42
- // Step 2: Create streak achievement and bonus campaign if enabled
43
- if (cfg.createStreakBonus) {
44
- const badges = await getAvailableBadges();
45
- const achievementResult = await createStreakAchievement(cfg, badges);
46
- if (achievementResult.error) {
47
- result.errors.push(achievementResult.error);
48
- }
49
- else {
50
- result.streakAchievementId = achievementResult.achievementId;
51
- // Create bonus campaign for streak achievement
52
- if (achievementResult.achievementId) {
53
- const bonusCampaignResult = await createStreakBonusCampaign(achievementResult.achievementId, cfg);
54
- if (bonusCampaignResult.error) {
55
- result.errors.push(bonusCampaignResult.error);
56
- }
57
- else {
58
- result.streakBonusCampaignId = bonusCampaignResult.campaignId;
59
- }
60
- }
61
- }
62
- }
63
- // Step 3: Verify setup
64
- const verification = await verifySetup(result);
65
- if (verification.warnings.length > 0) {
66
- result.errors.push(...verification.warnings);
67
- }
68
- // Determine success
69
- result.success = result.loginCampaignId !== undefined && result.errors.length === 0;
70
- // Build summary
71
- result.summary = buildSummary(cfg, result);
72
- }
73
- catch (error) {
74
- result.errors.push(`Workflow error: ${error instanceof Error ? error.message : String(error)}`);
75
- }
76
- return result;
77
- }
78
- // ============================================================================
79
- // Helper Functions
80
- // ============================================================================
81
- async function createLoginCampaign(cfg) {
82
- try {
83
- const response = await campaignCreate({
84
- type: "direct",
85
- trigger: "custom_event",
86
- event: "app_login",
87
- translations: {
88
- en: {
89
- name: "Daily App Login Reward",
90
- description: `Earn ${cfg.coinsPerLogin} coins for logging into the app daily`,
91
- },
92
- },
93
- activity: {
94
- startsAt: formatOLDate(cfg.seasonStart),
95
- endsAt: formatOLDate(cfg.seasonEnd),
96
- },
97
- rules: [
98
- {
99
- name: "Award coins for daily login",
100
- effects: [
101
- {
102
- effect: "give_points",
103
- pointsRule: { fixedValue: cfg.coinsPerLogin },
104
- },
105
- ],
106
- },
107
- ],
108
- limits: {
109
- // Strictly one login reward per day
110
- executionsPerMember: {
111
- value: 1,
112
- interval: { type: "days", value: 1 },
113
- },
114
- },
115
- active: true,
116
- });
117
- return { campaignId: response.campaignId };
118
- }
119
- catch (error) {
120
- return {
121
- error: `Failed to create login campaign: ${error instanceof Error ? error.message : String(error)}`,
122
- };
123
- }
124
- }
125
- async function getAvailableBadges() {
126
- try {
127
- const response = await badgeList({});
128
- const badgeMap = new Map();
129
- for (const badge of response.badges) {
130
- if (badge.name && badge.badgeTypeId) {
131
- badgeMap.set(badge.name.toLowerCase(), badge.badgeTypeId);
132
- }
133
- }
134
- return badgeMap;
135
- }
136
- catch {
137
- return new Map();
138
- }
139
- }
140
- async function createStreakAchievement(cfg, badges) {
141
- try {
142
- const badgeTypeId = cfg.badgeName
143
- ? badges.get(cfg.badgeName.toLowerCase())
144
- : undefined;
145
- /**
146
- * Streak achievements in Open Loyalty use:
147
- * - periodGoal: 1 (one login per period)
148
- * - period: { consecutive: X, value: 1 } where X is streak days
149
- *
150
- * This tracks consecutive periods (days) where the goal is met.
151
- */
152
- const achievementPayload = {
153
- translations: {
154
- en: {
155
- name: `${cfg.streakDays}-Day Login Streak`,
156
- description: `Log in ${cfg.streakDays} consecutive days to complete this achievement`,
157
- },
158
- },
159
- active: true,
160
- activity: {
161
- startsAt: formatOLDate(cfg.seasonStart),
162
- endsAt: formatOLDate(cfg.seasonEnd),
163
- },
164
- rules: [
165
- {
166
- trigger: "custom_event",
167
- event: "app_login",
168
- completeRule: {
169
- periodGoal: 1,
170
- period: {
171
- consecutive: cfg.streakDays,
172
- value: 1, // 1 day period
173
- },
174
- },
175
- },
176
- ],
177
- };
178
- if (badgeTypeId) {
179
- achievementPayload.badgeTypeId = badgeTypeId;
180
- }
181
- const response = await achievementCreate(achievementPayload);
182
- return { achievementId: response.achievementId };
183
- }
184
- catch (error) {
185
- return {
186
- error: `Failed to create streak achievement: ${error instanceof Error ? error.message : String(error)}`,
187
- };
188
- }
189
- }
190
- async function createStreakBonusCampaign(achievementId, cfg) {
191
- try {
192
- const response = await campaignCreate({
193
- type: "direct",
194
- trigger: "achievement",
195
- translations: {
196
- en: {
197
- name: `${cfg.streakDays}-Day Login Streak Bonus`,
198
- description: `Bonus ${cfg.streakBonus} coins for maintaining a ${cfg.streakDays}-day login streak`,
199
- },
200
- },
201
- activity: {
202
- startsAt: formatOLDate(cfg.seasonStart),
203
- endsAt: formatOLDate(cfg.seasonEnd),
204
- },
205
- rules: [
206
- {
207
- name: `Bonus for ${cfg.streakDays}-day streak`,
208
- effects: [
209
- {
210
- effect: "give_points",
211
- pointsRule: { fixedValue: cfg.streakBonus },
212
- },
213
- ],
214
- conditions: [
215
- {
216
- operator: "is_equal",
217
- attribute: "achievement.achievementId",
218
- data: { value: achievementId },
219
- },
220
- ],
221
- },
222
- ],
223
- active: true,
224
- });
225
- return { campaignId: response.campaignId };
226
- }
227
- catch (error) {
228
- return {
229
- error: `Failed to create streak bonus campaign: ${error instanceof Error ? error.message : String(error)}`,
230
- };
231
- }
232
- }
233
- async function verifySetup(result) {
234
- const warnings = [];
235
- try {
236
- if (result.loginCampaignId) {
237
- const campaigns = await campaignList({ active: true });
238
- const found = campaigns.campaigns.some((c) => c.campaignId === result.loginCampaignId);
239
- if (!found) {
240
- warnings.push("Login campaign created but not found in active campaigns list");
241
- }
242
- }
243
- if (result.streakAchievementId) {
244
- const achievements = await achievementList({ active: true });
245
- const found = achievements.achievements.some((a) => a.achievementId === result.streakAchievementId);
246
- if (!found) {
247
- warnings.push("Streak achievement created but not found in active achievements list");
248
- }
249
- }
250
- }
251
- catch (error) {
252
- warnings.push(`Verification error: ${error instanceof Error ? error.message : String(error)}`);
253
- }
254
- return { warnings };
255
- }
256
- function buildSummary(cfg, result) {
257
- const lines = [];
258
- if (result.loginCampaignId) {
259
- lines.push(`Daily login campaign created!`);
260
- lines.push(`\nDaily reward: ${cfg.coinsPerLogin} coins`);
261
- lines.push(`Limit: 1 reward per day (prevents multiple login scans)`);
262
- }
263
- if (result.streakAchievementId) {
264
- lines.push(`\nStreak Achievement: ${cfg.streakDays} consecutive days`);
265
- if (cfg.badgeName) {
266
- lines.push(`Badge: ${cfg.badgeName}`);
267
- }
268
- if (result.streakBonusCampaignId) {
269
- lines.push(`Streak bonus: ${cfg.streakBonus} coins`);
270
- }
271
- }
272
- lines.push(`\nSeason: ${cfg.seasonStart} to ${cfg.seasonEnd}`);
273
- lines.push(`\nCustom event to trigger reward:`);
274
- lines.push(` Event: app_login`);
275
- lines.push(`\nExample event payload:`);
276
- lines.push(`{`);
277
- lines.push(` "event": "app_login"`);
278
- lines.push(`}`);
279
- lines.push(`\nStreak mechanics:`);
280
- lines.push(`- Fan must log in at least once each day`);
281
- lines.push(`- Missing a day resets the streak counter`);
282
- lines.push(`- Achievement completes when ${cfg.streakDays} consecutive days are reached`);
283
- if (result.errors.length > 0) {
284
- lines.push(`\nWarnings/Errors:`);
285
- for (const error of result.errors) {
286
- lines.push(`- ${error}`);
287
- }
288
- }
289
- return lines.join("\n");
290
- }
291
- // ============================================================================
292
- // Exports
293
- // ============================================================================
294
- export const appLoginStreakWorkflow = {
295
- id: "app-login-streak",
296
- name: "App Login Streak Campaign",
297
- execute: executeAppLoginStreakWorkflow,
298
- };