@open-loyalty/mcp-server 1.0.3 → 1.3.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 (142) 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.d.ts +5 -0
  5. package/dist/client/http.js +62 -3
  6. package/dist/config.d.ts +6 -5
  7. package/dist/config.js +15 -11
  8. package/dist/http.js +170 -65
  9. package/dist/instructions.d.ts +5 -0
  10. package/dist/instructions.js +420 -0
  11. package/dist/prompts/fan-engagement-setup.d.ts +107 -0
  12. package/dist/prompts/fan-engagement-setup.js +492 -0
  13. package/dist/server.d.ts +1 -1
  14. package/dist/server.js +68 -278
  15. package/dist/tools/achievement/handlers.d.ts +117 -0
  16. package/dist/tools/achievement/handlers.js +161 -0
  17. package/dist/tools/achievement/index.d.ts +479 -0
  18. package/dist/tools/achievement/index.js +74 -0
  19. package/dist/tools/achievement/schemas.d.ts +433 -0
  20. package/dist/tools/achievement/schemas.js +142 -0
  21. package/dist/tools/achievement.d.ts +155 -121
  22. package/dist/tools/achievement.js +82 -39
  23. package/dist/tools/admin.d.ts +18 -6
  24. package/dist/tools/admin.js +24 -12
  25. package/dist/tools/analytics.d.ts +29 -11
  26. package/dist/tools/analytics.js +58 -48
  27. package/dist/tools/apikey.d.ts +10 -3
  28. package/dist/tools/apikey.js +13 -6
  29. package/dist/tools/audit.d.ts +6 -2
  30. package/dist/tools/audit.js +8 -4
  31. package/dist/tools/badge.d.ts +14 -6
  32. package/dist/tools/badge.js +36 -27
  33. package/dist/tools/campaign/handlers.d.ts +42 -0
  34. package/dist/tools/campaign/handlers.js +223 -0
  35. package/dist/tools/campaign/index.d.ts +783 -0
  36. package/dist/tools/campaign/index.js +112 -0
  37. package/dist/tools/campaign/member-handlers.d.ts +60 -0
  38. package/dist/tools/campaign/member-handlers.js +159 -0
  39. package/dist/tools/campaign/schemas.d.ts +704 -0
  40. package/dist/tools/campaign/schemas.js +259 -0
  41. package/dist/tools/campaign/types.d.ts +161 -0
  42. package/dist/tools/campaign/types.js +2 -0
  43. package/dist/tools/campaign.d.ts +41 -16
  44. package/dist/tools/campaign.js +38 -25
  45. package/dist/tools/custom-event.d.ts +315 -0
  46. package/dist/tools/custom-event.js +270 -0
  47. package/dist/tools/export.d.ts +12 -4
  48. package/dist/tools/export.js +25 -20
  49. package/dist/tools/import.d.ts +9 -3
  50. package/dist/tools/import.js +33 -21
  51. package/dist/tools/index.d.ts +3 -11
  52. package/dist/tools/index.js +17 -475
  53. package/dist/tools/member/handlers.d.ts +111 -0
  54. package/dist/tools/member/handlers.js +206 -0
  55. package/dist/tools/member/index.d.ts +169 -0
  56. package/dist/tools/member/index.js +92 -0
  57. package/dist/tools/member/schemas.d.ts +89 -0
  58. package/dist/tools/member/schemas.js +65 -0
  59. package/dist/tools/member.d.ts +21 -0
  60. package/dist/tools/member.js +56 -62
  61. package/dist/tools/points.d.ts +19 -6
  62. package/dist/tools/points.js +51 -49
  63. package/dist/tools/referral/handlers.d.ts +47 -0
  64. package/dist/tools/referral/handlers.js +115 -0
  65. package/dist/tools/referral/index.d.ts +44 -0
  66. package/dist/tools/referral/index.js +44 -0
  67. package/dist/tools/referral/schemas.d.ts +34 -0
  68. package/dist/tools/referral/schemas.js +52 -0
  69. package/dist/tools/reward/handlers.d.ts +110 -0
  70. package/dist/tools/reward/handlers.js +289 -0
  71. package/dist/tools/reward/index.d.ts +177 -0
  72. package/dist/tools/reward/index.js +90 -0
  73. package/dist/tools/reward/schemas.d.ts +116 -0
  74. package/dist/tools/reward/schemas.js +91 -0
  75. package/dist/tools/reward.d.ts +18 -0
  76. package/dist/tools/reward.js +56 -66
  77. package/dist/tools/role.d.ts +26 -7
  78. package/dist/tools/role.js +25 -12
  79. package/dist/tools/segment/handlers.d.ts +87 -0
  80. package/dist/tools/segment/handlers.js +174 -0
  81. package/dist/tools/segment/index.d.ts +395 -0
  82. package/dist/tools/segment/index.js +87 -0
  83. package/dist/tools/segment/schemas.d.ts +337 -0
  84. package/dist/tools/segment/schemas.js +79 -0
  85. package/dist/tools/segment.d.ts +29 -10
  86. package/dist/tools/segment.js +84 -50
  87. package/dist/tools/store.d.ts +12 -4
  88. package/dist/tools/store.js +16 -8
  89. package/dist/tools/tierset.d.ts +19 -7
  90. package/dist/tools/tierset.js +44 -35
  91. package/dist/tools/transaction.d.ts +16 -8
  92. package/dist/tools/transaction.js +25 -21
  93. package/dist/tools/wallet-type.d.ts +7 -3
  94. package/dist/tools/wallet-type.js +14 -12
  95. package/dist/tools/webhook.d.ts +23 -10
  96. package/dist/tools/webhook.js +135 -33
  97. package/dist/types/schemas/achievement.d.ts +12 -309
  98. package/dist/types/schemas/achievement.js +0 -13
  99. package/dist/types/schemas/admin.d.ts +10 -97
  100. package/dist/types/schemas/admin.js +0 -38
  101. package/dist/types/schemas/badge.d.ts +0 -37
  102. package/dist/types/schemas/badge.js +0 -11
  103. package/dist/types/schemas/campaign.d.ts +64 -832
  104. package/dist/types/schemas/campaign.js +2 -25
  105. package/dist/types/schemas/common.d.ts +5 -0
  106. package/dist/types/schemas/common.js +5 -0
  107. package/dist/types/schemas/export.d.ts +0 -17
  108. package/dist/types/schemas/export.js +0 -7
  109. package/dist/types/schemas/member.d.ts +37 -176
  110. package/dist/types/schemas/member.js +0 -27
  111. package/dist/types/schemas/points.d.ts +0 -63
  112. package/dist/types/schemas/points.js +0 -22
  113. package/dist/types/schemas/reward.d.ts +71 -68
  114. package/dist/types/schemas/reward.js +8 -28
  115. package/dist/types/schemas/role.d.ts +0 -100
  116. package/dist/types/schemas/role.js +0 -29
  117. package/dist/types/schemas/segment.d.ts +0 -58
  118. package/dist/types/schemas/segment.js +0 -17
  119. package/dist/types/schemas/tierset.d.ts +0 -176
  120. package/dist/types/schemas/tierset.js +0 -27
  121. package/dist/types/schemas/transaction.d.ts +23 -254
  122. package/dist/types/schemas/transaction.js +0 -7
  123. package/dist/types/schemas/wallet-type.d.ts +8 -8
  124. package/dist/types/schemas/wallet-type.js +1 -1
  125. package/dist/types/schemas/webhook.d.ts +0 -58
  126. package/dist/types/schemas/webhook.js +0 -12
  127. package/dist/utils/errors.js +30 -3
  128. package/dist/utils/payload.d.ts +12 -0
  129. package/dist/utils/payload.js +14 -0
  130. package/dist/workflows/app-login-streak.d.ts +39 -0
  131. package/dist/workflows/app-login-streak.js +298 -0
  132. package/dist/workflows/early-arrival.d.ts +33 -0
  133. package/dist/workflows/early-arrival.js +148 -0
  134. package/dist/workflows/index.d.ts +101 -0
  135. package/dist/workflows/index.js +208 -0
  136. package/dist/workflows/match-attendance.d.ts +45 -0
  137. package/dist/workflows/match-attendance.js +308 -0
  138. package/dist/workflows/sportsbar-visit.d.ts +41 -0
  139. package/dist/workflows/sportsbar-visit.js +284 -0
  140. package/dist/workflows/vod-watching.d.ts +43 -0
  141. package/dist/workflows/vod-watching.js +326 -0
  142. package/package.json +10 -2
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  export declare const WalletTypeSchema: z.ZodObject<{
3
3
  walletTypeId: z.ZodString;
4
4
  code: z.ZodString;
5
- name: z.ZodString;
5
+ name: z.ZodOptional<z.ZodString>;
6
6
  unitSingularName: z.ZodOptional<z.ZodString>;
7
7
  unitPluralName: z.ZodOptional<z.ZodString>;
8
8
  active: z.ZodBoolean;
@@ -13,9 +13,9 @@ export declare const WalletTypeSchema: z.ZodObject<{
13
13
  }, "strip", z.ZodTypeAny, {
14
14
  code: string;
15
15
  walletTypeId: string;
16
- name: string;
17
16
  active: boolean;
18
17
  isDefault: boolean;
18
+ name?: string | undefined;
19
19
  unitSingularName?: string | undefined;
20
20
  unitPluralName?: string | undefined;
21
21
  createdAt?: string | undefined;
@@ -24,9 +24,9 @@ export declare const WalletTypeSchema: z.ZodObject<{
24
24
  }, {
25
25
  code: string;
26
26
  walletTypeId: string;
27
- name: string;
28
27
  active: boolean;
29
28
  isDefault: boolean;
29
+ name?: string | undefined;
30
30
  unitSingularName?: string | undefined;
31
31
  unitPluralName?: string | undefined;
32
32
  createdAt?: string | undefined;
@@ -37,7 +37,7 @@ export declare const WalletTypeListResponseSchema: z.ZodObject<{
37
37
  items: z.ZodArray<z.ZodObject<{
38
38
  walletTypeId: z.ZodString;
39
39
  code: z.ZodString;
40
- name: z.ZodString;
40
+ name: z.ZodOptional<z.ZodString>;
41
41
  unitSingularName: z.ZodOptional<z.ZodString>;
42
42
  unitPluralName: z.ZodOptional<z.ZodString>;
43
43
  active: z.ZodBoolean;
@@ -48,9 +48,9 @@ export declare const WalletTypeListResponseSchema: z.ZodObject<{
48
48
  }, "strip", z.ZodTypeAny, {
49
49
  code: string;
50
50
  walletTypeId: string;
51
- name: string;
52
51
  active: boolean;
53
52
  isDefault: boolean;
53
+ name?: string | undefined;
54
54
  unitSingularName?: string | undefined;
55
55
  unitPluralName?: string | undefined;
56
56
  createdAt?: string | undefined;
@@ -59,9 +59,9 @@ export declare const WalletTypeListResponseSchema: z.ZodObject<{
59
59
  }, {
60
60
  code: string;
61
61
  walletTypeId: string;
62
- name: string;
63
62
  active: boolean;
64
63
  isDefault: boolean;
64
+ name?: string | undefined;
65
65
  unitSingularName?: string | undefined;
66
66
  unitPluralName?: string | undefined;
67
67
  createdAt?: string | undefined;
@@ -72,9 +72,9 @@ export declare const WalletTypeListResponseSchema: z.ZodObject<{
72
72
  items: {
73
73
  code: string;
74
74
  walletTypeId: string;
75
- name: string;
76
75
  active: boolean;
77
76
  isDefault: boolean;
77
+ name?: string | undefined;
78
78
  unitSingularName?: string | undefined;
79
79
  unitPluralName?: string | undefined;
80
80
  createdAt?: string | undefined;
@@ -85,9 +85,9 @@ export declare const WalletTypeListResponseSchema: z.ZodObject<{
85
85
  items: {
86
86
  code: string;
87
87
  walletTypeId: string;
88
- name: string;
89
88
  active: boolean;
90
89
  isDefault: boolean;
90
+ name?: string | undefined;
91
91
  unitSingularName?: string | undefined;
92
92
  unitPluralName?: string | undefined;
93
93
  createdAt?: string | undefined;
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  export const WalletTypeSchema = z.object({
3
3
  walletTypeId: z.string(),
4
4
  code: z.string(),
5
- name: z.string(),
5
+ name: z.string().optional(), // Optional: single wallet type endpoint may not return name
6
6
  unitSingularName: z.string().optional(),
7
7
  unitPluralName: z.string().optional(),
8
8
  active: z.boolean(),
@@ -45,64 +45,6 @@ export declare const WebhookSubscriptionSchema: z.ZodObject<{
45
45
  createdAt?: string | undefined;
46
46
  }>;
47
47
  export type WebhookSubscription = z.infer<typeof WebhookSubscriptionSchema>;
48
- export declare const WebhookSubscriptionCreateInputSchema: z.ZodObject<{
49
- eventName: z.ZodString;
50
- url: z.ZodString;
51
- headers: z.ZodOptional<z.ZodArray<z.ZodObject<{
52
- headerName: z.ZodString;
53
- headerValue: z.ZodString;
54
- }, "strip", z.ZodTypeAny, {
55
- headerName: string;
56
- headerValue: string;
57
- }, {
58
- headerName: string;
59
- headerValue: string;
60
- }>, "many">>;
61
- }, "strip", z.ZodTypeAny, {
62
- url: string;
63
- eventName: string;
64
- headers?: {
65
- headerName: string;
66
- headerValue: string;
67
- }[] | undefined;
68
- }, {
69
- url: string;
70
- eventName: string;
71
- headers?: {
72
- headerName: string;
73
- headerValue: string;
74
- }[] | undefined;
75
- }>;
76
- export type WebhookSubscriptionCreateInput = z.infer<typeof WebhookSubscriptionCreateInputSchema>;
77
- export declare const WebhookSubscriptionUpdateInputSchema: z.ZodObject<{
78
- eventName: z.ZodOptional<z.ZodString>;
79
- url: z.ZodOptional<z.ZodString>;
80
- headers: z.ZodOptional<z.ZodArray<z.ZodObject<{
81
- headerName: z.ZodString;
82
- headerValue: z.ZodString;
83
- }, "strip", z.ZodTypeAny, {
84
- headerName: string;
85
- headerValue: string;
86
- }, {
87
- headerName: string;
88
- headerValue: string;
89
- }>, "many">>;
90
- }, "strip", z.ZodTypeAny, {
91
- headers?: {
92
- headerName: string;
93
- headerValue: string;
94
- }[] | undefined;
95
- url?: string | undefined;
96
- eventName?: string | undefined;
97
- }, {
98
- headers?: {
99
- headerName: string;
100
- headerValue: string;
101
- }[] | undefined;
102
- url?: string | undefined;
103
- eventName?: string | undefined;
104
- }>;
105
- export type WebhookSubscriptionUpdateInput = z.infer<typeof WebhookSubscriptionUpdateInputSchema>;
106
48
  export declare const WebhookEventTypesSchema: z.ZodObject<{
107
49
  items: z.ZodArray<z.ZodString, "many">;
108
50
  }, "strip", z.ZodTypeAny, {
@@ -12,18 +12,6 @@ export const WebhookSubscriptionSchema = z.object({
12
12
  headers: z.array(WebhookHeaderSchema).optional().describe("Custom headers to include in webhook requests."),
13
13
  createdAt: z.string().optional().describe("Creation timestamp (ISO format)."),
14
14
  });
15
- // Input schema for creating a webhook subscription
16
- export const WebhookSubscriptionCreateInputSchema = z.object({
17
- eventName: z.string().describe("Event name to subscribe to. Use webhook_events to discover available events."),
18
- url: z.string().describe("URL to receive webhook events."),
19
- headers: z.array(WebhookHeaderSchema).optional().describe("Custom headers to include in webhook requests."),
20
- });
21
- // Input schema for updating a webhook subscription
22
- export const WebhookSubscriptionUpdateInputSchema = z.object({
23
- eventName: z.string().optional().describe("Event name to subscribe to."),
24
- url: z.string().optional().describe("URL to receive webhook events."),
25
- headers: z.array(WebhookHeaderSchema).optional().describe("Custom headers to include in webhook requests."),
26
- });
27
15
  // Response schema for webhook event types list
28
16
  export const WebhookEventTypesSchema = z.object({
29
17
  items: z.array(z.string()).describe("List of available webhook event type names."),
@@ -31,6 +31,35 @@ export function formatApiError(error, relatedTool) {
31
31
  if (status === 400) {
32
32
  const errors = data?.errors;
33
33
  const errorDetails = errors?.map(e => `${e.path || ''}: ${e.message}`).join('; ') || '';
34
+ const fullMessage = data?.message
35
+ ? `${String(data.message)}${errorDetails ? ` (${errorDetails})` : ''}`
36
+ : "Invalid request data";
37
+ // Parse "extra fields" errors - usually means a field shouldn't be at top level
38
+ if (fullMessage.includes("extra fields") || fullMessage.includes("should not contain extra fields")) {
39
+ return new OpenLoyaltyError({
40
+ code: "VALIDATION_ERROR",
41
+ message: fullMessage,
42
+ hint: "The API rejected unexpected fields in your request. Common causes: " +
43
+ "1. A field name is misspelled, " +
44
+ "2. A field should be nested differently (e.g., badgeTypeId may not be supported at creation time), " +
45
+ "3. The field isn't supported for this endpoint. " +
46
+ "Try removing optional fields and adding them back one at a time to identify the problematic field.",
47
+ relatedTool,
48
+ });
49
+ }
50
+ // Parse "selected choice is invalid" errors - enum value mismatch
51
+ if (fullMessage.includes("selected choice is invalid") || fullMessage.includes("The selected choice is invalid")) {
52
+ return new OpenLoyaltyError({
53
+ code: "VALIDATION_ERROR",
54
+ message: fullMessage,
55
+ hint: "An enum value is invalid. Common fixes: " +
56
+ "1. For interval types, use 'calendarDays' (not 'days'), 'calendarWeeks', 'calendarMonths', 'calendarYears'. " +
57
+ "2. For period types, use 'day' (not 'days'), 'week', 'month', 'year'. " +
58
+ "3. For trigger types, check the tool description for valid options. " +
59
+ "4. Omit interval entirely for lifetime/forever limits.",
60
+ relatedTool,
61
+ });
62
+ }
34
63
  // If API returns "Validation failed" with no details, it's likely an MCP implementation bug
35
64
  const hasNoDetails = !errorDetails && data?.message === "Validation failed";
36
65
  const hint = hasNoDetails
@@ -38,9 +67,7 @@ export function formatApiError(error, relatedTool) {
38
67
  : "Check the input parameters match the expected format";
39
68
  return new OpenLoyaltyError({
40
69
  code: "VALIDATION_ERROR",
41
- message: data?.message
42
- ? `${String(data.message)}${errorDetails ? ` (${errorDetails})` : ''}`
43
- : "Invalid request data",
70
+ message: fullMessage,
44
71
  hint,
45
72
  relatedTool,
46
73
  });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Removes undefined values from an object.
3
+ * Useful for building API payloads where undefined fields should be omitted.
4
+ *
5
+ * Note: Only filters top-level undefined values. Nested objects are passed through as-is.
6
+ * Falsy values (false, 0, '', null) are preserved - only undefined is removed.
7
+ *
8
+ * @example
9
+ * omitUndefined({ a: 1, b: undefined, c: false, d: '' })
10
+ * // Returns: { a: 1, c: false, d: '' }
11
+ */
12
+ export declare function omitUndefined<T extends Record<string, unknown>>(obj: T): Partial<T>;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Removes undefined values from an object.
3
+ * Useful for building API payloads where undefined fields should be omitted.
4
+ *
5
+ * Note: Only filters top-level undefined values. Nested objects are passed through as-is.
6
+ * Falsy values (false, 0, '', null) are preserved - only undefined is removed.
7
+ *
8
+ * @example
9
+ * omitUndefined({ a: 1, b: undefined, c: false, d: '' })
10
+ * // Returns: { a: 1, c: false, d: '' }
11
+ */
12
+ export function omitUndefined(obj) {
13
+ return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
14
+ }
@@ -0,0 +1,39 @@
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
+ };
@@ -0,0 +1,298 @@
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
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Early Arrival Workflow
3
+ *
4
+ * Creates a campaign to reward fans who arrive early to matches.
5
+ * Uses a custom event with minutes_before attribute for conditional rewards.
6
+ */
7
+ export interface EarlyArrivalConfig {
8
+ /** How early (in minutes) counts as "early arrival" */
9
+ minutesBefore: number;
10
+ /** Coins awarded for early arrival */
11
+ coinsPerArrival: number;
12
+ /** Maximum early arrival bonuses per match (typically 1) */
13
+ limitPerMatch: number;
14
+ /** Season start date (ISO format) */
15
+ seasonStart: string;
16
+ /** Season end date (ISO format) */
17
+ seasonEnd: string;
18
+ }
19
+ export interface EarlyArrivalResult {
20
+ success: boolean;
21
+ campaignId?: string;
22
+ errors: string[];
23
+ summary: string;
24
+ }
25
+ /**
26
+ * Execute the early arrival workflow
27
+ */
28
+ export declare function executeEarlyArrivalWorkflow(config?: Partial<EarlyArrivalConfig>): Promise<EarlyArrivalResult>;
29
+ export declare const earlyArrivalWorkflow: {
30
+ id: string;
31
+ name: string;
32
+ execute: typeof executeEarlyArrivalWorkflow;
33
+ };