@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.
- package/README.md +180 -177
- package/dist/auth/provider.js +2 -14
- package/dist/auth/storage.js +22 -0
- package/dist/client/http.js +10 -0
- package/dist/config.d.ts +0 -13
- package/dist/config.js +0 -14
- package/dist/http.js +35 -3
- package/dist/instructions.d.ts +5 -0
- package/dist/instructions.js +440 -0
- package/dist/prompts/fan-engagement-setup.d.ts +107 -0
- package/dist/prompts/fan-engagement-setup.js +492 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.js +60 -273
- package/dist/tools/achievement/handlers.d.ts +117 -0
- package/dist/tools/achievement/handlers.js +161 -0
- package/dist/tools/achievement/index.d.ts +479 -0
- package/dist/tools/achievement/index.js +74 -0
- package/dist/tools/achievement/schemas.d.ts +433 -0
- package/dist/tools/achievement/schemas.js +142 -0
- package/dist/tools/achievement.d.ts +141 -121
- package/dist/tools/achievement.js +60 -24
- package/dist/tools/admin.d.ts +6 -6
- package/dist/tools/admin.js +12 -12
- package/dist/tools/analytics.d.ts +11 -11
- package/dist/tools/analytics.js +30 -29
- package/dist/tools/apikey.d.ts +3 -3
- package/dist/tools/apikey.js +6 -6
- package/dist/tools/audit.d.ts +2 -2
- package/dist/tools/audit.js +4 -4
- package/dist/tools/badge.d.ts +6 -6
- package/dist/tools/badge.js +23 -18
- package/dist/tools/campaign/handlers.d.ts +42 -0
- package/dist/tools/campaign/handlers.js +223 -0
- package/dist/tools/campaign/index.d.ts +783 -0
- package/dist/tools/campaign/index.js +117 -0
- package/dist/tools/campaign/member-handlers.d.ts +60 -0
- package/dist/tools/campaign/member-handlers.js +159 -0
- package/dist/tools/campaign/schemas.d.ts +704 -0
- package/dist/tools/campaign/schemas.js +259 -0
- package/dist/tools/campaign/types.d.ts +161 -0
- package/dist/tools/campaign/types.js +2 -0
- package/dist/tools/custom-event.d.ts +315 -0
- package/dist/tools/custom-event.js +270 -0
- package/dist/tools/export.d.ts +4 -4
- package/dist/tools/export.js +12 -12
- package/dist/tools/import.d.ts +3 -3
- package/dist/tools/import.js +23 -15
- package/dist/tools/index.js +13 -5
- package/dist/tools/member/handlers.d.ts +111 -0
- package/dist/tools/member/handlers.js +206 -0
- package/dist/tools/member/index.d.ts +169 -0
- package/dist/tools/member/index.js +92 -0
- package/dist/tools/member/schemas.d.ts +89 -0
- package/dist/tools/member/schemas.js +65 -0
- package/dist/tools/points.d.ts +7 -6
- package/dist/tools/points.js +21 -20
- package/dist/tools/referral/handlers.d.ts +47 -0
- package/dist/tools/referral/handlers.js +115 -0
- package/dist/tools/referral/index.d.ts +44 -0
- package/dist/tools/referral/index.js +44 -0
- package/dist/tools/referral/schemas.d.ts +34 -0
- package/dist/tools/referral/schemas.js +52 -0
- package/dist/tools/reward/handlers.d.ts +110 -0
- package/dist/tools/reward/handlers.js +289 -0
- package/dist/tools/reward/index.d.ts +177 -0
- package/dist/tools/reward/index.js +93 -0
- package/dist/tools/reward/schemas.d.ts +116 -0
- package/dist/tools/reward/schemas.js +92 -0
- package/dist/tools/role.d.ts +6 -6
- package/dist/tools/role.js +12 -12
- package/dist/tools/segment/handlers.d.ts +87 -0
- package/dist/tools/segment/handlers.js +174 -0
- package/dist/tools/segment/index.d.ts +395 -0
- package/dist/tools/segment/index.js +88 -0
- package/dist/tools/segment/schemas.d.ts +337 -0
- package/dist/tools/segment/schemas.js +79 -0
- package/dist/tools/segment.d.ts +10 -10
- package/dist/tools/segment.js +55 -31
- package/dist/tools/store.d.ts +4 -4
- package/dist/tools/store.js +8 -8
- package/dist/tools/tierset.d.ts +10 -10
- package/dist/tools/tierset.js +69 -37
- package/dist/tools/transaction.d.ts +4 -4
- package/dist/tools/transaction.js +12 -12
- package/dist/tools/wallet-type.d.ts +221 -16
- package/dist/tools/wallet-type.js +248 -17
- package/dist/tools/webhook.d.ts +6 -6
- package/dist/tools/webhook.js +90 -31
- package/dist/types/schemas/achievement.d.ts +18 -18
- package/dist/types/schemas/campaign.d.ts +64 -184
- package/dist/types/schemas/campaign.js +2 -7
- package/dist/types/schemas/common.d.ts +5 -0
- package/dist/types/schemas/common.js +5 -0
- package/dist/types/schemas/member.d.ts +2 -2
- package/dist/types/schemas/reward.d.ts +94 -18
- package/dist/types/schemas/reward.js +8 -3
- package/dist/types/schemas/wallet-type.d.ts +306 -8
- package/dist/types/schemas/wallet-type.js +82 -1
- package/dist/utils/errors.js +32 -5
- package/dist/workflows/app-login-streak.d.ts +39 -0
- package/dist/workflows/app-login-streak.js +298 -0
- package/dist/workflows/early-arrival.d.ts +33 -0
- package/dist/workflows/early-arrival.js +148 -0
- package/dist/workflows/index.d.ts +101 -0
- package/dist/workflows/index.js +208 -0
- package/dist/workflows/match-attendance.d.ts +45 -0
- package/dist/workflows/match-attendance.js +308 -0
- package/dist/workflows/sportsbar-visit.d.ts +41 -0
- package/dist/workflows/sportsbar-visit.js +284 -0
- package/dist/workflows/vod-watching.d.ts +43 -0
- package/dist/workflows/vod-watching.js +326 -0
- package/package.json +8 -2
|
@@ -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(),
|
|
@@ -10,8 +10,89 @@ export const WalletTypeSchema = z.object({
|
|
|
10
10
|
createdAt: z.string().optional(),
|
|
11
11
|
limits: z.record(z.unknown()).optional(),
|
|
12
12
|
allowNegativeBalance: z.boolean().optional(),
|
|
13
|
+
translations: z.record(z.unknown()).optional(),
|
|
14
|
+
unitDaysExpiryAfter: z.string().optional(),
|
|
15
|
+
unitDaysActiveCount: z.number().optional(),
|
|
16
|
+
unitYearsActiveCount: z.number().optional(),
|
|
17
|
+
unitDaysLocked: z.number().optional(),
|
|
18
|
+
allTimeNotLocked: z.boolean().optional(),
|
|
19
|
+
unitExpiryDate: z.string().optional(),
|
|
13
20
|
});
|
|
14
21
|
// API returns { items: [...] } not just an array
|
|
15
22
|
export const WalletTypeListResponseSchema = z.object({
|
|
16
23
|
items: z.array(WalletTypeSchema),
|
|
17
24
|
});
|
|
25
|
+
// Create response schema - API returns { walletTypeId: "..." }
|
|
26
|
+
export const WalletTypeCreateResponseSchema = z.object({
|
|
27
|
+
walletTypeId: z.string(),
|
|
28
|
+
});
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// INPUT SCHEMAS (for MCP tool definitions)
|
|
31
|
+
// =============================================================================
|
|
32
|
+
export const WalletTypeListInputSchema = {
|
|
33
|
+
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."),
|
|
34
|
+
};
|
|
35
|
+
export const WalletTypeGetInputSchema = {
|
|
36
|
+
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."),
|
|
37
|
+
walletTypeId: z.string().describe("The wallet type ID (UUID) to retrieve."),
|
|
38
|
+
};
|
|
39
|
+
// Translations schema for wallet types
|
|
40
|
+
const WalletTypeTranslationsInputSchema = z.object({
|
|
41
|
+
en: z.object({
|
|
42
|
+
name: z.string().describe("Wallet type name in English (REQUIRED)."),
|
|
43
|
+
description: z.string().optional().describe("Description in English (optional)."),
|
|
44
|
+
}),
|
|
45
|
+
}).passthrough().describe("Translations object with at least 'en' key containing { name }.");
|
|
46
|
+
// Limits interval schema
|
|
47
|
+
const LimitsIntervalSchema = z.object({
|
|
48
|
+
type: z.enum(["calendarHours", "calendarDays", "calendarWeeks", "calendarMonths", "calendarYears"]).describe("Interval type. IMPORTANT: Use 'calendarDays' NOT 'days', 'calendarMonths' NOT 'months', etc."),
|
|
49
|
+
value: z.number().int().positive().describe("Interval value (e.g., 1 for 'every 1 calendarMonth')."),
|
|
50
|
+
});
|
|
51
|
+
// Points limit schema
|
|
52
|
+
const PointsLimitSchema = z.object({
|
|
53
|
+
interval: LimitsIntervalSchema.optional().describe("Interval for limit reset. Omit for lifetime/forever limits."),
|
|
54
|
+
value: z.number().int().nonnegative().describe("Maximum points value for this limit."),
|
|
55
|
+
}).optional();
|
|
56
|
+
// Limits schema
|
|
57
|
+
export const WalletTypeLimitsInputSchema = z.object({
|
|
58
|
+
points: PointsLimitSchema.describe("Global points limit across all members. Example: { interval: { type: 'calendarYears', value: 1 }, value: 100000 }"),
|
|
59
|
+
pointsPerMember: PointsLimitSchema.describe("Per-member points limit. Example: { interval: { type: 'calendarYears', value: 1 }, value: 10000 }"),
|
|
60
|
+
}).optional();
|
|
61
|
+
export const WalletTypeCreateInputSchema = {
|
|
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
|
+
translations: WalletTypeTranslationsInputSchema.describe("Translations with at least 'en' key containing { name }. REQUIRED."),
|
|
64
|
+
unitSingularName: z.string().describe("Singular unit name (e.g., 'point', 'coin', 'star'). REQUIRED."),
|
|
65
|
+
unitPluralName: z.string().describe("Plural unit name (e.g., 'points', 'coins', 'stars'). REQUIRED."),
|
|
66
|
+
unitDaysExpiryAfter: z.string().describe("⚠️ REQUIRED. Days after earning before points expire. Use 'all_time_active' for never-expiring points, " +
|
|
67
|
+
"or a number string like '365' for points that expire 365 days after earning."),
|
|
68
|
+
code: z.string().optional().describe("Unique code for the wallet type (e.g., 'bonus_points', 'store_credit'). " +
|
|
69
|
+
"Use lowercase with underscores. If not provided, a code will be auto-generated. " +
|
|
70
|
+
"IMPORTANT: Cannot be changed after creation."),
|
|
71
|
+
allowNegativeBalance: z.boolean().optional().describe("Whether to allow negative balances (default: false). " +
|
|
72
|
+
"Set to true for credit-like systems where members can owe points."),
|
|
73
|
+
unitExpiryDate: z.string().optional().describe("Annual expiry date in 'MM-DD' format (e.g., '12-31' for end of year). " +
|
|
74
|
+
"Points earned during the year expire on this date. " +
|
|
75
|
+
"IMPORTANT: Use 'MM-DD' format, NOT 'YYYY-MM-DD'."),
|
|
76
|
+
unitDaysActiveCount: z.number().int().nonnegative().optional().describe("Number of days points remain active after earning. Use with unitDaysExpiryAfter."),
|
|
77
|
+
unitYearsActiveCount: z.number().int().nonnegative().optional().describe("Number of years points remain active after earning."),
|
|
78
|
+
unitDaysLocked: z.number().int().nonnegative().optional().describe("Number of days points are locked after earning before becoming spendable. " +
|
|
79
|
+
"Use 0 or omit for immediately available points."),
|
|
80
|
+
allTimeNotLocked: z.boolean().optional().describe("If true, points are never locked and immediately available. Overrides unitDaysLocked."),
|
|
81
|
+
};
|
|
82
|
+
export const WalletTypeUpdateInputSchema = {
|
|
83
|
+
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."),
|
|
84
|
+
walletTypeId: z.string().describe("The wallet type ID (UUID) to update."),
|
|
85
|
+
name: z.string().optional().describe("Update the wallet type name (updates translations.en.name)."),
|
|
86
|
+
description: z.string().optional().describe("Update the description (updates translations.en.description)."),
|
|
87
|
+
unitSingularName: z.string().optional().describe("Update the singular unit name."),
|
|
88
|
+
unitPluralName: z.string().optional().describe("Update the plural unit name."),
|
|
89
|
+
active: z.boolean().optional().describe("Update active status."),
|
|
90
|
+
allowNegativeBalance: z.boolean().optional().describe("Update negative balance setting."),
|
|
91
|
+
limits: WalletTypeLimitsInputSchema.describe("Update points limits."),
|
|
92
|
+
unitExpiryDate: z.string().optional().describe("Update annual expiry date ('MM-DD' format)."),
|
|
93
|
+
unitDaysExpiryAfter: z.string().optional().describe("Update days until expiry or 'all_time_active'."),
|
|
94
|
+
unitDaysActiveCount: z.number().int().nonnegative().optional().describe("Update days active count."),
|
|
95
|
+
unitYearsActiveCount: z.number().int().nonnegative().optional().describe("Update years active count."),
|
|
96
|
+
unitDaysLocked: z.number().int().nonnegative().optional().describe("Update locked days."),
|
|
97
|
+
allTimeNotLocked: z.boolean().optional().describe("Update not-locked setting."),
|
|
98
|
+
};
|
package/dist/utils/errors.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import axios from "axios";
|
|
2
2
|
export class OpenLoyaltyError extends Error {
|
|
3
3
|
code;
|
|
4
4
|
hint;
|
|
@@ -15,7 +15,7 @@ export function formatApiError(error, relatedTool) {
|
|
|
15
15
|
if (error instanceof OpenLoyaltyError) {
|
|
16
16
|
return error;
|
|
17
17
|
}
|
|
18
|
-
if (error
|
|
18
|
+
if (axios.isAxiosError(error)) {
|
|
19
19
|
const status = error.response?.status;
|
|
20
20
|
const data = error.response?.data;
|
|
21
21
|
if (status === 404) {
|
|
@@ -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:
|
|
42
|
-
? `${String(data.message)}${errorDetails ? ` (${errorDetails})` : ''}`
|
|
43
|
-
: "Invalid request data",
|
|
70
|
+
message: fullMessage,
|
|
44
71
|
hint,
|
|
45
72
|
relatedTool,
|
|
46
73
|
});
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
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
|
+
import { campaignCreate } from "../tools/campaign/handlers.js";
|
|
8
|
+
import { formatOLDate, DEFAULTS } from "../prompts/fan-engagement-setup.js";
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Workflow Implementation
|
|
11
|
+
// ============================================================================
|
|
12
|
+
/**
|
|
13
|
+
* Execute the early arrival workflow
|
|
14
|
+
*/
|
|
15
|
+
export async function executeEarlyArrivalWorkflow(config = {}) {
|
|
16
|
+
const result = {
|
|
17
|
+
success: false,
|
|
18
|
+
errors: [],
|
|
19
|
+
summary: "",
|
|
20
|
+
};
|
|
21
|
+
// Merge with defaults
|
|
22
|
+
const cfg = {
|
|
23
|
+
minutesBefore: config.minutesBefore ?? DEFAULTS.earlyArrival.minutesBefore,
|
|
24
|
+
coinsPerArrival: config.coinsPerArrival ?? DEFAULTS.earlyArrival.coinsPerArrival,
|
|
25
|
+
limitPerMatch: config.limitPerMatch ?? DEFAULTS.earlyArrival.limitPerMatch,
|
|
26
|
+
seasonStart: config.seasonStart ?? DEFAULTS.seasonDates.start,
|
|
27
|
+
seasonEnd: config.seasonEnd ?? DEFAULTS.seasonDates.end,
|
|
28
|
+
};
|
|
29
|
+
try {
|
|
30
|
+
// Create the early arrival campaign with condition
|
|
31
|
+
const campaignResult = await createEarlyArrivalCampaign(cfg);
|
|
32
|
+
if (campaignResult.error) {
|
|
33
|
+
result.errors.push(campaignResult.error);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
result.campaignId = campaignResult.campaignId;
|
|
37
|
+
result.success = true;
|
|
38
|
+
}
|
|
39
|
+
// Build summary
|
|
40
|
+
result.summary = buildSummary(cfg, result);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
result.errors.push(`Workflow error: ${error instanceof Error ? error.message : String(error)}`);
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// Helper Functions
|
|
49
|
+
// ============================================================================
|
|
50
|
+
async function createEarlyArrivalCampaign(cfg) {
|
|
51
|
+
try {
|
|
52
|
+
/**
|
|
53
|
+
* The early_arrival custom event expects:
|
|
54
|
+
* {
|
|
55
|
+
* "event": "early_arrival",
|
|
56
|
+
* "attributes": {
|
|
57
|
+
* "minutes_before": 75 // how many minutes before kickoff they arrived
|
|
58
|
+
* }
|
|
59
|
+
* }
|
|
60
|
+
*
|
|
61
|
+
* The campaign condition checks if minutes_before >= threshold
|
|
62
|
+
*/
|
|
63
|
+
const response = await campaignCreate({
|
|
64
|
+
type: "direct",
|
|
65
|
+
trigger: "custom_event",
|
|
66
|
+
event: "early_arrival",
|
|
67
|
+
translations: {
|
|
68
|
+
en: {
|
|
69
|
+
name: "Early Arrival Bonus",
|
|
70
|
+
description: `Earn ${cfg.coinsPerArrival} bonus coins for arriving at least ${cfg.minutesBefore} minutes before kickoff`,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
activity: {
|
|
74
|
+
startsAt: formatOLDate(cfg.seasonStart),
|
|
75
|
+
endsAt: formatOLDate(cfg.seasonEnd),
|
|
76
|
+
},
|
|
77
|
+
rules: [
|
|
78
|
+
{
|
|
79
|
+
name: "Award coins for early arrival",
|
|
80
|
+
effects: [
|
|
81
|
+
{
|
|
82
|
+
effect: "give_points",
|
|
83
|
+
pointsRule: { fixedValue: cfg.coinsPerArrival },
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
conditions: [
|
|
87
|
+
{
|
|
88
|
+
operator: "gte",
|
|
89
|
+
attribute: "event.minutes_before",
|
|
90
|
+
data: { value: cfg.minutesBefore },
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
limits: {
|
|
96
|
+
executionsPerMember: {
|
|
97
|
+
value: cfg.limitPerMatch,
|
|
98
|
+
interval: { type: "days", value: 1 },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
active: true,
|
|
102
|
+
});
|
|
103
|
+
return { campaignId: response.campaignId };
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
return {
|
|
107
|
+
error: `Failed to create early arrival campaign: ${error instanceof Error ? error.message : String(error)}`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function buildSummary(cfg, result) {
|
|
112
|
+
const lines = [];
|
|
113
|
+
if (result.success && result.campaignId) {
|
|
114
|
+
lines.push(`Early arrival campaign created successfully!`);
|
|
115
|
+
lines.push(`\nReward: ${cfg.coinsPerArrival} coins`);
|
|
116
|
+
lines.push(`Requirement: Arrive at least ${cfg.minutesBefore} minutes before kickoff`);
|
|
117
|
+
lines.push(`Limit: ${cfg.limitPerMatch} bonus per match day`);
|
|
118
|
+
lines.push(`\nSeason: ${cfg.seasonStart} to ${cfg.seasonEnd}`);
|
|
119
|
+
lines.push(`\nCustom event to trigger reward:`);
|
|
120
|
+
lines.push(` Event: early_arrival`);
|
|
121
|
+
lines.push(` Required attribute: minutes_before (number)`);
|
|
122
|
+
lines.push(`\nExample event payload:`);
|
|
123
|
+
lines.push(`{`);
|
|
124
|
+
lines.push(` "event": "early_arrival",`);
|
|
125
|
+
lines.push(` "attributes": {`);
|
|
126
|
+
lines.push(` "minutes_before": 75`);
|
|
127
|
+
lines.push(` }`);
|
|
128
|
+
lines.push(`}`);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
lines.push(`Early arrival campaign setup failed.`);
|
|
132
|
+
}
|
|
133
|
+
if (result.errors.length > 0) {
|
|
134
|
+
lines.push(`\nErrors:`);
|
|
135
|
+
for (const error of result.errors) {
|
|
136
|
+
lines.push(`- ${error}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return lines.join("\n");
|
|
140
|
+
}
|
|
141
|
+
// ============================================================================
|
|
142
|
+
// Exports
|
|
143
|
+
// ============================================================================
|
|
144
|
+
export const earlyArrivalWorkflow = {
|
|
145
|
+
id: "early-arrival",
|
|
146
|
+
name: "Early Arrival Campaign",
|
|
147
|
+
execute: executeEarlyArrivalWorkflow,
|
|
148
|
+
};
|