@open-loyalty/mcp-server 1.1.0 → 1.3.3

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 (112) hide show
  1. package/README.md +180 -177
  2. package/dist/auth/provider.js +2 -14
  3. package/dist/auth/storage.js +22 -0
  4. package/dist/client/http.js +10 -0
  5. package/dist/config.d.ts +0 -13
  6. package/dist/config.js +0 -14
  7. package/dist/http.js +35 -3
  8. package/dist/instructions.d.ts +5 -0
  9. package/dist/instructions.js +440 -0
  10. package/dist/prompts/fan-engagement-setup.d.ts +107 -0
  11. package/dist/prompts/fan-engagement-setup.js +492 -0
  12. package/dist/server.d.ts +1 -1
  13. package/dist/server.js +60 -273
  14. package/dist/tools/achievement/handlers.d.ts +117 -0
  15. package/dist/tools/achievement/handlers.js +161 -0
  16. package/dist/tools/achievement/index.d.ts +479 -0
  17. package/dist/tools/achievement/index.js +74 -0
  18. package/dist/tools/achievement/schemas.d.ts +433 -0
  19. package/dist/tools/achievement/schemas.js +142 -0
  20. package/dist/tools/achievement.d.ts +141 -121
  21. package/dist/tools/achievement.js +60 -24
  22. package/dist/tools/admin.d.ts +6 -6
  23. package/dist/tools/admin.js +12 -12
  24. package/dist/tools/analytics.d.ts +11 -11
  25. package/dist/tools/analytics.js +30 -29
  26. package/dist/tools/apikey.d.ts +3 -3
  27. package/dist/tools/apikey.js +6 -6
  28. package/dist/tools/audit.d.ts +2 -2
  29. package/dist/tools/audit.js +4 -4
  30. package/dist/tools/badge.d.ts +6 -6
  31. package/dist/tools/badge.js +23 -18
  32. package/dist/tools/campaign/handlers.d.ts +42 -0
  33. package/dist/tools/campaign/handlers.js +223 -0
  34. package/dist/tools/campaign/index.d.ts +783 -0
  35. package/dist/tools/campaign/index.js +117 -0
  36. package/dist/tools/campaign/member-handlers.d.ts +60 -0
  37. package/dist/tools/campaign/member-handlers.js +159 -0
  38. package/dist/tools/campaign/schemas.d.ts +704 -0
  39. package/dist/tools/campaign/schemas.js +259 -0
  40. package/dist/tools/campaign/types.d.ts +161 -0
  41. package/dist/tools/campaign/types.js +2 -0
  42. package/dist/tools/custom-event.d.ts +315 -0
  43. package/dist/tools/custom-event.js +270 -0
  44. package/dist/tools/export.d.ts +4 -4
  45. package/dist/tools/export.js +12 -12
  46. package/dist/tools/import.d.ts +3 -3
  47. package/dist/tools/import.js +23 -15
  48. package/dist/tools/index.js +13 -5
  49. package/dist/tools/member/handlers.d.ts +111 -0
  50. package/dist/tools/member/handlers.js +206 -0
  51. package/dist/tools/member/index.d.ts +169 -0
  52. package/dist/tools/member/index.js +92 -0
  53. package/dist/tools/member/schemas.d.ts +89 -0
  54. package/dist/tools/member/schemas.js +65 -0
  55. package/dist/tools/points.d.ts +7 -6
  56. package/dist/tools/points.js +21 -20
  57. package/dist/tools/referral/handlers.d.ts +47 -0
  58. package/dist/tools/referral/handlers.js +115 -0
  59. package/dist/tools/referral/index.d.ts +44 -0
  60. package/dist/tools/referral/index.js +44 -0
  61. package/dist/tools/referral/schemas.d.ts +34 -0
  62. package/dist/tools/referral/schemas.js +52 -0
  63. package/dist/tools/reward/handlers.d.ts +110 -0
  64. package/dist/tools/reward/handlers.js +289 -0
  65. package/dist/tools/reward/index.d.ts +177 -0
  66. package/dist/tools/reward/index.js +93 -0
  67. package/dist/tools/reward/schemas.d.ts +116 -0
  68. package/dist/tools/reward/schemas.js +92 -0
  69. package/dist/tools/role.d.ts +6 -6
  70. package/dist/tools/role.js +12 -12
  71. package/dist/tools/segment/handlers.d.ts +87 -0
  72. package/dist/tools/segment/handlers.js +174 -0
  73. package/dist/tools/segment/index.d.ts +395 -0
  74. package/dist/tools/segment/index.js +88 -0
  75. package/dist/tools/segment/schemas.d.ts +337 -0
  76. package/dist/tools/segment/schemas.js +79 -0
  77. package/dist/tools/segment.d.ts +10 -10
  78. package/dist/tools/segment.js +55 -31
  79. package/dist/tools/store.d.ts +4 -4
  80. package/dist/tools/store.js +8 -8
  81. package/dist/tools/tierset.d.ts +10 -10
  82. package/dist/tools/tierset.js +69 -37
  83. package/dist/tools/transaction.d.ts +4 -4
  84. package/dist/tools/transaction.js +12 -12
  85. package/dist/tools/wallet-type.d.ts +221 -16
  86. package/dist/tools/wallet-type.js +248 -17
  87. package/dist/tools/webhook.d.ts +6 -6
  88. package/dist/tools/webhook.js +90 -31
  89. package/dist/types/schemas/achievement.d.ts +18 -18
  90. package/dist/types/schemas/campaign.d.ts +64 -184
  91. package/dist/types/schemas/campaign.js +2 -7
  92. package/dist/types/schemas/common.d.ts +5 -0
  93. package/dist/types/schemas/common.js +5 -0
  94. package/dist/types/schemas/member.d.ts +2 -2
  95. package/dist/types/schemas/reward.d.ts +94 -18
  96. package/dist/types/schemas/reward.js +8 -3
  97. package/dist/types/schemas/wallet-type.d.ts +306 -8
  98. package/dist/types/schemas/wallet-type.js +82 -1
  99. package/dist/utils/errors.js +32 -5
  100. package/dist/workflows/app-login-streak.d.ts +39 -0
  101. package/dist/workflows/app-login-streak.js +298 -0
  102. package/dist/workflows/early-arrival.d.ts +33 -0
  103. package/dist/workflows/early-arrival.js +148 -0
  104. package/dist/workflows/index.d.ts +101 -0
  105. package/dist/workflows/index.js +208 -0
  106. package/dist/workflows/match-attendance.d.ts +45 -0
  107. package/dist/workflows/match-attendance.js +308 -0
  108. package/dist/workflows/sportsbar-visit.d.ts +41 -0
  109. package/dist/workflows/sportsbar-visit.js +284 -0
  110. package/dist/workflows/vod-watching.d.ts +43 -0
  111. package/dist/workflows/vod-watching.js +326 -0
  112. package/package.json +8 -2
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Fan Engagement Workflows
3
+ *
4
+ * Orchestrated workflows for setting up complete fan engagement campaigns.
5
+ * These combine multiple Open Loyalty tools into guided, step-by-step flows.
6
+ */
7
+ import { matchAttendanceWorkflow, executeMatchAttendanceWorkflow, type MatchAttendanceConfig, type MatchAttendanceResult } from "./match-attendance.js";
8
+ import { earlyArrivalWorkflow, executeEarlyArrivalWorkflow, type EarlyArrivalConfig, type EarlyArrivalResult } from "./early-arrival.js";
9
+ import { sportsbarVisitWorkflow, executeSportsbarVisitWorkflow, type SportsbarVisitConfig, type SportsbarVisitResult } from "./sportsbar-visit.js";
10
+ import { vodWatchingWorkflow, executeVodWatchingWorkflow, type VodWatchingConfig, type VodWatchingResult } from "./vod-watching.js";
11
+ import { appLoginStreakWorkflow, executeAppLoginStreakWorkflow, type AppLoginStreakConfig, type AppLoginStreakResult } from "./app-login-streak.js";
12
+ export { matchAttendanceWorkflow, executeMatchAttendanceWorkflow, type MatchAttendanceConfig, type MatchAttendanceResult, };
13
+ export { earlyArrivalWorkflow, executeEarlyArrivalWorkflow, type EarlyArrivalConfig, type EarlyArrivalResult, };
14
+ export { sportsbarVisitWorkflow, executeSportsbarVisitWorkflow, type SportsbarVisitConfig, type SportsbarVisitResult, };
15
+ export { vodWatchingWorkflow, executeVodWatchingWorkflow, type VodWatchingConfig, type VodWatchingResult, };
16
+ export { appLoginStreakWorkflow, executeAppLoginStreakWorkflow, type AppLoginStreakConfig, type AppLoginStreakResult, };
17
+ export { DEFAULTS, ALL_WORKFLOWS, findMatchingWorkflow, getWorkflowById, parseCommaSeparated, parseMilestones, formatOLDate, AGENT_PROMPTS, type WorkflowDefinition, type WorkflowQuestion, type WorkflowStep, } from "../prompts/fan-engagement-setup.js";
18
+ export interface FanEngagementConfig {
19
+ /** Match attendance configuration */
20
+ matchAttendance?: Partial<MatchAttendanceConfig>;
21
+ /** Early arrival configuration */
22
+ earlyArrival?: Partial<EarlyArrivalConfig>;
23
+ /** Sports bar visit configuration */
24
+ sportsbarVisit?: Partial<SportsbarVisitConfig>;
25
+ /** VOD watching configuration */
26
+ vodWatching?: Partial<VodWatchingConfig>;
27
+ /** App login streak configuration */
28
+ appLoginStreak?: Partial<AppLoginStreakConfig>;
29
+ /** Which workflows to execute (default: all) */
30
+ enabledWorkflows?: ("match-attendance" | "early-arrival" | "sportsbar-visit" | "vod-watching" | "app-login-streak")[];
31
+ }
32
+ export interface FanEngagementResult {
33
+ success: boolean;
34
+ results: {
35
+ matchAttendance?: MatchAttendanceResult;
36
+ earlyArrival?: EarlyArrivalResult;
37
+ sportsbarVisit?: SportsbarVisitResult;
38
+ vodWatching?: VodWatchingResult;
39
+ appLoginStreak?: AppLoginStreakResult;
40
+ };
41
+ summary: string;
42
+ totalCampaigns: number;
43
+ totalAchievements: number;
44
+ errors: string[];
45
+ }
46
+ /**
47
+ * Execute all fan engagement workflows
48
+ *
49
+ * This is the master workflow that orchestrates all individual workflows
50
+ * based on the provided configuration.
51
+ */
52
+ export declare function executeAllFanEngagementWorkflows(config?: FanEngagementConfig): Promise<FanEngagementResult>;
53
+ export declare const allWorkflows: readonly [{
54
+ id: string;
55
+ name: string;
56
+ execute: typeof executeMatchAttendanceWorkflow;
57
+ }, {
58
+ id: string;
59
+ name: string;
60
+ execute: typeof executeEarlyArrivalWorkflow;
61
+ }, {
62
+ id: string;
63
+ name: string;
64
+ execute: typeof executeSportsbarVisitWorkflow;
65
+ }, {
66
+ id: string;
67
+ name: string;
68
+ execute: typeof executeVodWatchingWorkflow;
69
+ }, {
70
+ id: string;
71
+ name: string;
72
+ execute: typeof executeAppLoginStreakWorkflow;
73
+ }];
74
+ /**
75
+ * Get workflow by ID
76
+ */
77
+ export declare function getWorkflow(id: string): {
78
+ id: string;
79
+ name: string;
80
+ execute: typeof executeAppLoginStreakWorkflow;
81
+ } | {
82
+ id: string;
83
+ name: string;
84
+ execute: typeof executeEarlyArrivalWorkflow;
85
+ } | {
86
+ id: string;
87
+ name: string;
88
+ execute: typeof executeMatchAttendanceWorkflow;
89
+ } | {
90
+ id: string;
91
+ name: string;
92
+ execute: typeof executeSportsbarVisitWorkflow;
93
+ } | {
94
+ id: string;
95
+ name: string;
96
+ execute: typeof executeVodWatchingWorkflow;
97
+ } | undefined;
98
+ /**
99
+ * Execute a workflow by ID with given config
100
+ */
101
+ export declare function executeWorkflowById(id: string, config?: Record<string, unknown>): Promise<unknown>;
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Fan Engagement Workflows
3
+ *
4
+ * Orchestrated workflows for setting up complete fan engagement campaigns.
5
+ * These combine multiple Open Loyalty tools into guided, step-by-step flows.
6
+ */
7
+ // Import execute functions for internal use
8
+ import { matchAttendanceWorkflow, executeMatchAttendanceWorkflow, } from "./match-attendance.js";
9
+ import { earlyArrivalWorkflow, executeEarlyArrivalWorkflow, } from "./early-arrival.js";
10
+ import { sportsbarVisitWorkflow, executeSportsbarVisitWorkflow, } from "./sportsbar-visit.js";
11
+ import { vodWatchingWorkflow, executeVodWatchingWorkflow, } from "./vod-watching.js";
12
+ import { appLoginStreakWorkflow, executeAppLoginStreakWorkflow, } from "./app-login-streak.js";
13
+ // Re-export all workflow components
14
+ export { matchAttendanceWorkflow, executeMatchAttendanceWorkflow, };
15
+ export { earlyArrivalWorkflow, executeEarlyArrivalWorkflow, };
16
+ export { sportsbarVisitWorkflow, executeSportsbarVisitWorkflow, };
17
+ export { vodWatchingWorkflow, executeVodWatchingWorkflow, };
18
+ export { appLoginStreakWorkflow, executeAppLoginStreakWorkflow, };
19
+ // Re-export prompt utilities
20
+ export { DEFAULTS, ALL_WORKFLOWS, findMatchingWorkflow, getWorkflowById, parseCommaSeparated, parseMilestones, formatOLDate, AGENT_PROMPTS, } from "../prompts/fan-engagement-setup.js";
21
+ // ============================================================================
22
+ // Master Workflow
23
+ // ============================================================================
24
+ /**
25
+ * Execute all fan engagement workflows
26
+ *
27
+ * This is the master workflow that orchestrates all individual workflows
28
+ * based on the provided configuration.
29
+ */
30
+ export async function executeAllFanEngagementWorkflows(config = {}) {
31
+ const enabledWorkflows = config.enabledWorkflows || [
32
+ "match-attendance",
33
+ "early-arrival",
34
+ "sportsbar-visit",
35
+ "vod-watching",
36
+ "app-login-streak",
37
+ ];
38
+ const result = {
39
+ success: false,
40
+ results: {},
41
+ summary: "",
42
+ totalCampaigns: 0,
43
+ totalAchievements: 0,
44
+ errors: [],
45
+ };
46
+ try {
47
+ // Execute each enabled workflow
48
+ for (const workflowId of enabledWorkflows) {
49
+ switch (workflowId) {
50
+ case "match-attendance": {
51
+ const maResult = await executeMatchAttendanceWorkflow(config.matchAttendance);
52
+ result.results.matchAttendance = maResult;
53
+ if (maResult.baseCampaignId)
54
+ result.totalCampaigns++;
55
+ result.totalCampaigns += maResult.achievements.filter((a) => a.bonusCampaignId).length;
56
+ result.totalAchievements += maResult.achievements.length;
57
+ result.errors.push(...maResult.errors);
58
+ break;
59
+ }
60
+ case "early-arrival": {
61
+ const eaResult = await executeEarlyArrivalWorkflow(config.earlyArrival);
62
+ result.results.earlyArrival = eaResult;
63
+ if (eaResult.campaignId)
64
+ result.totalCampaigns++;
65
+ result.errors.push(...eaResult.errors);
66
+ break;
67
+ }
68
+ case "sportsbar-visit": {
69
+ const sbResult = await executeSportsbarVisitWorkflow(config.sportsbarVisit);
70
+ result.results.sportsbarVisit = sbResult;
71
+ if (sbResult.visitCampaignId)
72
+ result.totalCampaigns++;
73
+ if (sbResult.bonusCampaignId)
74
+ result.totalCampaigns++;
75
+ if (sbResult.achievementId)
76
+ result.totalAchievements++;
77
+ result.errors.push(...sbResult.errors);
78
+ break;
79
+ }
80
+ case "vod-watching": {
81
+ const vodResult = await executeVodWatchingWorkflow(config.vodWatching);
82
+ result.results.vodWatching = vodResult;
83
+ if (vodResult.watchCampaignId)
84
+ result.totalCampaigns++;
85
+ if (vodResult.bonusCampaignId)
86
+ result.totalCampaigns++;
87
+ if (vodResult.achievementId)
88
+ result.totalAchievements++;
89
+ result.errors.push(...vodResult.errors);
90
+ break;
91
+ }
92
+ case "app-login-streak": {
93
+ const loginResult = await executeAppLoginStreakWorkflow(config.appLoginStreak);
94
+ result.results.appLoginStreak = loginResult;
95
+ if (loginResult.loginCampaignId)
96
+ result.totalCampaigns++;
97
+ if (loginResult.streakBonusCampaignId)
98
+ result.totalCampaigns++;
99
+ if (loginResult.streakAchievementId)
100
+ result.totalAchievements++;
101
+ result.errors.push(...loginResult.errors);
102
+ break;
103
+ }
104
+ }
105
+ }
106
+ // Determine overall success
107
+ result.success = result.errors.length === 0 && result.totalCampaigns > 0;
108
+ // Build summary
109
+ result.summary = buildMasterSummary(result);
110
+ }
111
+ catch (error) {
112
+ result.errors.push(`Master workflow error: ${error instanceof Error ? error.message : String(error)}`);
113
+ }
114
+ return result;
115
+ }
116
+ // ============================================================================
117
+ // Helper Functions
118
+ // ============================================================================
119
+ function buildMasterSummary(result) {
120
+ const lines = [];
121
+ lines.push(`Fan Engagement Program Setup Complete!`);
122
+ lines.push(`========================================`);
123
+ lines.push(`\nCreated: ${result.totalCampaigns} campaigns, ${result.totalAchievements} achievements`);
124
+ // Match Attendance
125
+ if (result.results.matchAttendance) {
126
+ const ma = result.results.matchAttendance;
127
+ if (ma.baseCampaignId) {
128
+ lines.push(`\nMatch Attendance:`);
129
+ lines.push(` - Base campaign: ${ma.baseCampaignId}`);
130
+ if (ma.achievements.length > 0) {
131
+ lines.push(` - Achievements: ${ma.achievements.map((a) => a.milestone).join(", ")} matches`);
132
+ }
133
+ }
134
+ }
135
+ // Early Arrival
136
+ if (result.results.earlyArrival?.campaignId) {
137
+ lines.push(`\nEarly Arrival:`);
138
+ lines.push(` - Campaign: ${result.results.earlyArrival.campaignId}`);
139
+ }
140
+ // Sports Bar
141
+ if (result.results.sportsbarVisit?.visitCampaignId) {
142
+ lines.push(`\nSports Bar Visits:`);
143
+ lines.push(` - Visit campaign: ${result.results.sportsbarVisit.visitCampaignId}`);
144
+ if (result.results.sportsbarVisit.achievementId) {
145
+ lines.push(` - Achievement: ${result.results.sportsbarVisit.achievementId}`);
146
+ }
147
+ }
148
+ // VOD Watching
149
+ if (result.results.vodWatching?.watchCampaignId) {
150
+ lines.push(`\nVOD Watching:`);
151
+ lines.push(` - Watch campaign: ${result.results.vodWatching.watchCampaignId}`);
152
+ if (result.results.vodWatching.achievementId) {
153
+ lines.push(` - Achievement: ${result.results.vodWatching.achievementId}`);
154
+ }
155
+ }
156
+ // App Login
157
+ if (result.results.appLoginStreak?.loginCampaignId) {
158
+ lines.push(`\nApp Login Streak:`);
159
+ lines.push(` - Login campaign: ${result.results.appLoginStreak.loginCampaignId}`);
160
+ if (result.results.appLoginStreak.streakAchievementId) {
161
+ lines.push(` - Streak achievement: ${result.results.appLoginStreak.streakAchievementId}`);
162
+ }
163
+ }
164
+ // Custom events reference
165
+ lines.push(`\nCustom Events Reference:`);
166
+ lines.push(` - match_attendance: Triggered when fan scans at stadium entry`);
167
+ lines.push(` - early_arrival: Triggered when fan arrives early (needs minutes_before attribute)`);
168
+ lines.push(` - sportsbar_visit: Triggered when fan visits the sports bar`);
169
+ lines.push(` - vod_watch: Triggered when fan watches video (needs minutes_watched attribute)`);
170
+ lines.push(` - app_login: Triggered on daily app login`);
171
+ // Errors
172
+ if (result.errors.length > 0) {
173
+ lines.push(`\nWarnings/Errors (${result.errors.length}):`);
174
+ for (const error of result.errors.slice(0, 5)) {
175
+ lines.push(` - ${error}`);
176
+ }
177
+ if (result.errors.length > 5) {
178
+ lines.push(` ... and ${result.errors.length - 5} more`);
179
+ }
180
+ }
181
+ return lines.join("\n");
182
+ }
183
+ // ============================================================================
184
+ // All Workflows Registry
185
+ // ============================================================================
186
+ export const allWorkflows = [
187
+ matchAttendanceWorkflow,
188
+ earlyArrivalWorkflow,
189
+ sportsbarVisitWorkflow,
190
+ vodWatchingWorkflow,
191
+ appLoginStreakWorkflow,
192
+ ];
193
+ /**
194
+ * Get workflow by ID
195
+ */
196
+ export function getWorkflow(id) {
197
+ return allWorkflows.find((w) => w.id === id);
198
+ }
199
+ /**
200
+ * Execute a workflow by ID with given config
201
+ */
202
+ export async function executeWorkflowById(id, config) {
203
+ const workflow = getWorkflow(id);
204
+ if (!workflow) {
205
+ throw new Error(`Workflow not found: ${id}`);
206
+ }
207
+ return workflow.execute(config);
208
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Match Attendance Workflow
3
+ *
4
+ * Creates a complete match attendance reward program including:
5
+ * - Base campaign for per-match coin rewards
6
+ * - Achievements for attendance milestones (6, 12 matches, etc.)
7
+ * - Bonus campaigns triggered by achievement completion
8
+ */
9
+ export interface MatchAttendanceConfig {
10
+ /** Coins awarded per match attendance */
11
+ coinsPerMatch: number;
12
+ /** Attendance milestones for achievements (e.g., [6, 12] or [6, 12, "80%"]) */
13
+ milestones: (number | string)[];
14
+ /** Badge names for each milestone */
15
+ badgeNames: string[];
16
+ /** Bonus coins for each milestone */
17
+ milestoneBonuses: number[];
18
+ /** Maximum attendances counted per day (anti-fraud) */
19
+ limitPerDay: number;
20
+ /** Season start date (ISO format) */
21
+ seasonStart: string;
22
+ /** Season end date (ISO format) */
23
+ seasonEnd: string;
24
+ }
25
+ export interface MatchAttendanceResult {
26
+ success: boolean;
27
+ baseCampaignId?: string;
28
+ achievements: Array<{
29
+ achievementId: string;
30
+ milestone: number | string;
31
+ badgeName: string;
32
+ bonusCampaignId?: string;
33
+ }>;
34
+ errors: string[];
35
+ summary: string;
36
+ }
37
+ /**
38
+ * Execute the match attendance workflow
39
+ */
40
+ export declare function executeMatchAttendanceWorkflow(config?: Partial<MatchAttendanceConfig>): Promise<MatchAttendanceResult>;
41
+ export declare const matchAttendanceWorkflow: {
42
+ id: string;
43
+ name: string;
44
+ execute: typeof executeMatchAttendanceWorkflow;
45
+ };
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Match Attendance Workflow
3
+ *
4
+ * Creates a complete match attendance reward program including:
5
+ * - Base campaign for per-match coin rewards
6
+ * - Achievements for attendance milestones (6, 12 matches, etc.)
7
+ * - Bonus campaigns triggered by achievement completion
8
+ */
9
+ import { campaignCreate, campaignList } from "../tools/campaign/handlers.js";
10
+ import { achievementCreate, achievementList } from "../tools/achievement.js";
11
+ import { badgeList } from "../tools/badge.js";
12
+ import { formatOLDate, DEFAULTS } from "../prompts/fan-engagement-setup.js";
13
+ // ============================================================================
14
+ // Workflow Implementation
15
+ // ============================================================================
16
+ /**
17
+ * Execute the match attendance workflow
18
+ */
19
+ export async function executeMatchAttendanceWorkflow(config = {}) {
20
+ const result = {
21
+ success: false,
22
+ achievements: [],
23
+ errors: [],
24
+ summary: "",
25
+ };
26
+ // Merge with defaults
27
+ const cfg = {
28
+ coinsPerMatch: config.coinsPerMatch ?? DEFAULTS.matchAttendance.coinsPerMatch,
29
+ milestones: config.milestones ?? DEFAULTS.matchAttendance.milestones,
30
+ badgeNames: config.badgeNames ?? Object.values(DEFAULTS.matchAttendance.badges),
31
+ milestoneBonuses: config.milestoneBonuses ?? Object.values(DEFAULTS.matchAttendance.milestoneBonuses),
32
+ limitPerDay: config.limitPerDay ?? DEFAULTS.matchAttendance.limitPerDay,
33
+ seasonStart: config.seasonStart ?? DEFAULTS.seasonDates.start,
34
+ seasonEnd: config.seasonEnd ?? DEFAULTS.seasonDates.end,
35
+ };
36
+ try {
37
+ // Step 1: Create base match attendance campaign
38
+ const baseCampaignResult = await createBaseCampaign(cfg);
39
+ if (baseCampaignResult.error) {
40
+ result.errors.push(baseCampaignResult.error);
41
+ }
42
+ else {
43
+ result.baseCampaignId = baseCampaignResult.campaignId;
44
+ }
45
+ // Step 2: Get available badges for achievements
46
+ const badges = await getAvailableBadges();
47
+ // Step 3: Create achievements and bonus campaigns for each milestone
48
+ for (let i = 0; i < cfg.milestones.length; i++) {
49
+ const milestone = cfg.milestones[i];
50
+ const badgeName = cfg.badgeNames[i] || `Milestone ${milestone}`;
51
+ const bonus = cfg.milestoneBonuses[i] || 500;
52
+ const achievementResult = await createMilestoneAchievement(milestone, badgeName, badges, cfg.seasonStart, cfg.seasonEnd);
53
+ if (achievementResult.error) {
54
+ result.errors.push(achievementResult.error);
55
+ continue;
56
+ }
57
+ const achievementEntry = {
58
+ achievementId: achievementResult.achievementId,
59
+ milestone,
60
+ badgeName,
61
+ };
62
+ // Create bonus campaign for this achievement
63
+ if (achievementResult.achievementId) {
64
+ const bonusCampaignResult = await createAchievementBonusCampaign(achievementResult.achievementId, milestone, bonus, cfg.seasonStart, cfg.seasonEnd);
65
+ if (bonusCampaignResult.error) {
66
+ result.errors.push(bonusCampaignResult.error);
67
+ }
68
+ else {
69
+ achievementEntry.bonusCampaignId = bonusCampaignResult.campaignId;
70
+ }
71
+ }
72
+ result.achievements.push(achievementEntry);
73
+ }
74
+ // Step 4: Verify setup
75
+ const verification = await verifySetup(result);
76
+ if (verification.warnings.length > 0) {
77
+ result.errors.push(...verification.warnings);
78
+ }
79
+ // Build summary
80
+ result.success = result.baseCampaignId !== undefined && result.errors.length === 0;
81
+ result.summary = buildSummary(cfg, result);
82
+ }
83
+ catch (error) {
84
+ result.errors.push(`Workflow error: ${error instanceof Error ? error.message : String(error)}`);
85
+ }
86
+ return result;
87
+ }
88
+ // ============================================================================
89
+ // Helper Functions
90
+ // ============================================================================
91
+ async function createBaseCampaign(cfg) {
92
+ try {
93
+ const response = await campaignCreate({
94
+ type: "direct",
95
+ trigger: "custom_event",
96
+ event: "match_attendance",
97
+ translations: {
98
+ en: {
99
+ name: "Match Attendance Reward",
100
+ description: `Earn ${cfg.coinsPerMatch} coins for each match you attend`,
101
+ },
102
+ },
103
+ activity: {
104
+ startsAt: formatOLDate(cfg.seasonStart),
105
+ endsAt: formatOLDate(cfg.seasonEnd),
106
+ },
107
+ rules: [
108
+ {
109
+ name: "Award coins for match attendance",
110
+ effects: [
111
+ {
112
+ effect: "give_points",
113
+ pointsRule: { fixedValue: cfg.coinsPerMatch },
114
+ },
115
+ ],
116
+ },
117
+ ],
118
+ limits: {
119
+ executionsPerMember: {
120
+ value: cfg.limitPerDay,
121
+ interval: { type: "days", value: 1 },
122
+ },
123
+ },
124
+ active: true,
125
+ });
126
+ return { campaignId: response.campaignId };
127
+ }
128
+ catch (error) {
129
+ return {
130
+ error: `Failed to create base campaign: ${error instanceof Error ? error.message : String(error)}`,
131
+ };
132
+ }
133
+ }
134
+ async function getAvailableBadges() {
135
+ try {
136
+ const response = await badgeList({});
137
+ const badgeMap = new Map();
138
+ for (const badge of response.badges) {
139
+ if (badge.name && badge.badgeTypeId) {
140
+ badgeMap.set(badge.name.toLowerCase(), badge.badgeTypeId);
141
+ }
142
+ }
143
+ return badgeMap;
144
+ }
145
+ catch {
146
+ // Return empty map if badges can't be fetched - achievements will be created without badges
147
+ return new Map();
148
+ }
149
+ }
150
+ async function createMilestoneAchievement(milestone, badgeName, badges, seasonStart, seasonEnd) {
151
+ try {
152
+ // Find badge ID by name (case-insensitive)
153
+ const badgeTypeId = badges.get(badgeName.toLowerCase());
154
+ // Determine the goal value (for percentage, calculate based on assumed total matches)
155
+ let periodGoal;
156
+ let achievementName;
157
+ let achievementDescription;
158
+ if (typeof milestone === "string" && milestone.includes("%")) {
159
+ // Percentage milestone (e.g., "80%")
160
+ const percentage = parseInt(milestone.replace("%", ""), 10);
161
+ // Assume ~19 home matches per season (typical for major football leagues)
162
+ periodGoal = Math.ceil(19 * (percentage / 100));
163
+ achievementName = `${milestone} Match Attendance`;
164
+ achievementDescription = `Attend ${milestone} of all home matches this season`;
165
+ }
166
+ else {
167
+ periodGoal = typeof milestone === "number" ? milestone : parseInt(milestone, 10);
168
+ achievementName = `Attend ${periodGoal} Matches`;
169
+ achievementDescription = `Be at ${periodGoal} home games this season`;
170
+ }
171
+ const achievementPayload = {
172
+ translations: {
173
+ en: {
174
+ name: achievementName,
175
+ description: achievementDescription,
176
+ },
177
+ },
178
+ active: true,
179
+ activity: {
180
+ startsAt: formatOLDate(seasonStart),
181
+ endsAt: formatOLDate(seasonEnd),
182
+ },
183
+ rules: [
184
+ {
185
+ trigger: "custom_event",
186
+ event: "match_attendance",
187
+ completeRule: {
188
+ periodGoal,
189
+ },
190
+ },
191
+ ],
192
+ };
193
+ // Add badge if found
194
+ if (badgeTypeId) {
195
+ achievementPayload.badgeTypeId = badgeTypeId;
196
+ }
197
+ const response = await achievementCreate(achievementPayload);
198
+ return { achievementId: response.achievementId };
199
+ }
200
+ catch (error) {
201
+ return {
202
+ error: `Failed to create achievement for ${milestone}: ${error instanceof Error ? error.message : String(error)}`,
203
+ };
204
+ }
205
+ }
206
+ async function createAchievementBonusCampaign(achievementId, milestone, bonusCoins, seasonStart, seasonEnd) {
207
+ try {
208
+ const milestoneLabel = typeof milestone === "string" ? milestone : `${milestone} Matches`;
209
+ const response = await campaignCreate({
210
+ type: "direct",
211
+ trigger: "achievement",
212
+ translations: {
213
+ en: {
214
+ name: `${milestoneLabel} Achievement Bonus`,
215
+ description: `Bonus ${bonusCoins} coins for completing the ${milestoneLabel} achievement`,
216
+ },
217
+ },
218
+ activity: {
219
+ startsAt: formatOLDate(seasonStart),
220
+ endsAt: formatOLDate(seasonEnd),
221
+ },
222
+ rules: [
223
+ {
224
+ name: `Bonus for completing ${milestoneLabel}`,
225
+ effects: [
226
+ {
227
+ effect: "give_points",
228
+ pointsRule: { fixedValue: bonusCoins },
229
+ },
230
+ ],
231
+ conditions: [
232
+ {
233
+ operator: "is_equal",
234
+ attribute: "achievement.achievementId",
235
+ data: { value: achievementId },
236
+ },
237
+ ],
238
+ },
239
+ ],
240
+ active: true,
241
+ });
242
+ return { campaignId: response.campaignId };
243
+ }
244
+ catch (error) {
245
+ return {
246
+ error: `Failed to create bonus campaign for ${milestone}: ${error instanceof Error ? error.message : String(error)}`,
247
+ };
248
+ }
249
+ }
250
+ async function verifySetup(result) {
251
+ const warnings = [];
252
+ try {
253
+ // Verify campaigns exist
254
+ if (result.baseCampaignId) {
255
+ const campaigns = await campaignList({ active: true });
256
+ const found = campaigns.campaigns.some((c) => c.campaignId === result.baseCampaignId);
257
+ if (!found) {
258
+ warnings.push("Base campaign created but not found in active campaigns list");
259
+ }
260
+ }
261
+ // Verify achievements exist
262
+ if (result.achievements.length > 0) {
263
+ const achievements = await achievementList({ active: true });
264
+ for (const ach of result.achievements) {
265
+ const found = achievements.achievements.some((a) => a.achievementId === ach.achievementId);
266
+ if (!found) {
267
+ warnings.push(`Achievement ${ach.milestone} created but not found in active achievements list`);
268
+ }
269
+ }
270
+ }
271
+ }
272
+ catch (error) {
273
+ warnings.push(`Verification error: ${error instanceof Error ? error.message : String(error)}`);
274
+ }
275
+ return { warnings };
276
+ }
277
+ function buildSummary(cfg, result) {
278
+ const lines = [];
279
+ if (result.baseCampaignId) {
280
+ lines.push(`Fans will earn ${cfg.coinsPerMatch} coins per match attended`);
281
+ }
282
+ if (result.achievements.length > 0) {
283
+ lines.push(`\nMilestone achievements created:`);
284
+ for (const ach of result.achievements) {
285
+ const bonusText = ach.bonusCampaignId
286
+ ? ` (+ ${cfg.milestoneBonuses[result.achievements.indexOf(ach)] || 500} coin bonus)`
287
+ : "";
288
+ lines.push(`- ${ach.milestone} matches: ${ach.badgeName} badge${bonusText}`);
289
+ }
290
+ }
291
+ lines.push(`\nSeason: ${cfg.seasonStart} to ${cfg.seasonEnd}`);
292
+ lines.push(`Anti-fraud: Max ${cfg.limitPerDay} attendance${cfg.limitPerDay > 1 ? "s" : ""} per day`);
293
+ if (result.errors.length > 0) {
294
+ lines.push(`\nWarnings/Errors:`);
295
+ for (const error of result.errors) {
296
+ lines.push(`- ${error}`);
297
+ }
298
+ }
299
+ return lines.join("\n");
300
+ }
301
+ // ============================================================================
302
+ // Exports
303
+ // ============================================================================
304
+ export const matchAttendanceWorkflow = {
305
+ id: "match-attendance",
306
+ name: "Match Attendance Campaign",
307
+ execute: executeMatchAttendanceWorkflow,
308
+ };