@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
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sports Bar Visit Workflow
|
|
3
|
+
*
|
|
4
|
+
* Creates a campaign to reward fans for visiting the sports bar,
|
|
5
|
+
* with optional achievement for visiting multiple times.
|
|
6
|
+
*/
|
|
7
|
+
export interface SportsbarVisitConfig {
|
|
8
|
+
/** Coins awarded per visit */
|
|
9
|
+
coinsPerVisit: number;
|
|
10
|
+
/** Whether to create achievement for multiple visits */
|
|
11
|
+
createAchievement: boolean;
|
|
12
|
+
/** Number of visits for achievement milestone */
|
|
13
|
+
visitMilestone: number;
|
|
14
|
+
/** Bonus coins for completing the achievement */
|
|
15
|
+
milestoneBonus: number;
|
|
16
|
+
/** Maximum visits counted per day (anti-abuse) */
|
|
17
|
+
limitPerDay: number;
|
|
18
|
+
/** Badge name for achievement (optional) */
|
|
19
|
+
badgeName?: string;
|
|
20
|
+
/** Season start date (ISO format) */
|
|
21
|
+
seasonStart: string;
|
|
22
|
+
/** Season end date (ISO format) */
|
|
23
|
+
seasonEnd: string;
|
|
24
|
+
}
|
|
25
|
+
export interface SportsbarVisitResult {
|
|
26
|
+
success: boolean;
|
|
27
|
+
visitCampaignId?: string;
|
|
28
|
+
achievementId?: string;
|
|
29
|
+
bonusCampaignId?: string;
|
|
30
|
+
errors: string[];
|
|
31
|
+
summary: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Execute the sports bar visit workflow
|
|
35
|
+
*/
|
|
36
|
+
export declare function executeSportsbarVisitWorkflow(config?: Partial<SportsbarVisitConfig>): Promise<SportsbarVisitResult>;
|
|
37
|
+
export declare const sportsbarVisitWorkflow: {
|
|
38
|
+
id: string;
|
|
39
|
+
name: string;
|
|
40
|
+
execute: typeof executeSportsbarVisitWorkflow;
|
|
41
|
+
};
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sports Bar Visit Workflow
|
|
3
|
+
*
|
|
4
|
+
* Creates a campaign to reward fans for visiting the sports bar,
|
|
5
|
+
* with optional achievement for visiting multiple times.
|
|
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 sports bar visit workflow
|
|
16
|
+
*/
|
|
17
|
+
export async function executeSportsbarVisitWorkflow(config = {}) {
|
|
18
|
+
const result = {
|
|
19
|
+
success: false,
|
|
20
|
+
errors: [],
|
|
21
|
+
summary: "",
|
|
22
|
+
};
|
|
23
|
+
// Merge with defaults
|
|
24
|
+
const cfg = {
|
|
25
|
+
coinsPerVisit: config.coinsPerVisit ?? DEFAULTS.sportsbarVisit.coinsPerVisit,
|
|
26
|
+
createAchievement: config.createAchievement ?? true,
|
|
27
|
+
visitMilestone: config.visitMilestone ?? DEFAULTS.sportsbarVisit.visitMilestone,
|
|
28
|
+
milestoneBonus: config.milestoneBonus ?? DEFAULTS.sportsbarVisit.milestoneBonus,
|
|
29
|
+
limitPerDay: config.limitPerDay ?? DEFAULTS.sportsbarVisit.limitPerDay,
|
|
30
|
+
badgeName: config.badgeName ?? "Sports Bar Regular",
|
|
31
|
+
seasonStart: config.seasonStart ?? DEFAULTS.seasonDates.start,
|
|
32
|
+
seasonEnd: config.seasonEnd ?? DEFAULTS.seasonDates.end,
|
|
33
|
+
};
|
|
34
|
+
try {
|
|
35
|
+
// Step 1: Create base visit campaign
|
|
36
|
+
const visitCampaignResult = await createVisitCampaign(cfg);
|
|
37
|
+
if (visitCampaignResult.error) {
|
|
38
|
+
result.errors.push(visitCampaignResult.error);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
result.visitCampaignId = visitCampaignResult.campaignId;
|
|
42
|
+
}
|
|
43
|
+
// Step 2: Create achievement and bonus campaign if enabled
|
|
44
|
+
if (cfg.createAchievement) {
|
|
45
|
+
// Get available badges
|
|
46
|
+
const badges = await getAvailableBadges();
|
|
47
|
+
const achievementResult = await createVisitAchievement(cfg, badges);
|
|
48
|
+
if (achievementResult.error) {
|
|
49
|
+
result.errors.push(achievementResult.error);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
result.achievementId = achievementResult.achievementId;
|
|
53
|
+
// Create bonus campaign for achievement
|
|
54
|
+
if (achievementResult.achievementId) {
|
|
55
|
+
const bonusCampaignResult = await createAchievementBonusCampaign(achievementResult.achievementId, cfg);
|
|
56
|
+
if (bonusCampaignResult.error) {
|
|
57
|
+
result.errors.push(bonusCampaignResult.error);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
result.bonusCampaignId = bonusCampaignResult.campaignId;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Step 3: Verify setup
|
|
66
|
+
const verification = await verifySetup(result);
|
|
67
|
+
if (verification.warnings.length > 0) {
|
|
68
|
+
result.errors.push(...verification.warnings);
|
|
69
|
+
}
|
|
70
|
+
// Determine success
|
|
71
|
+
result.success = result.visitCampaignId !== undefined && result.errors.length === 0;
|
|
72
|
+
// Build summary
|
|
73
|
+
result.summary = buildSummary(cfg, result);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
result.errors.push(`Workflow error: ${error instanceof Error ? error.message : String(error)}`);
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// Helper Functions
|
|
82
|
+
// ============================================================================
|
|
83
|
+
async function createVisitCampaign(cfg) {
|
|
84
|
+
try {
|
|
85
|
+
const response = await campaignCreate({
|
|
86
|
+
type: "direct",
|
|
87
|
+
trigger: "custom_event",
|
|
88
|
+
event: "sportsbar_visit",
|
|
89
|
+
translations: {
|
|
90
|
+
en: {
|
|
91
|
+
name: "Sports Bar Visit Reward",
|
|
92
|
+
description: `Earn ${cfg.coinsPerVisit} coins for each visit to the sports bar`,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
activity: {
|
|
96
|
+
startsAt: formatOLDate(cfg.seasonStart),
|
|
97
|
+
endsAt: formatOLDate(cfg.seasonEnd),
|
|
98
|
+
},
|
|
99
|
+
rules: [
|
|
100
|
+
{
|
|
101
|
+
name: "Award coins for sports bar visit",
|
|
102
|
+
effects: [
|
|
103
|
+
{
|
|
104
|
+
effect: "give_points",
|
|
105
|
+
pointsRule: { fixedValue: cfg.coinsPerVisit },
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
limits: {
|
|
111
|
+
executionsPerMember: {
|
|
112
|
+
value: cfg.limitPerDay,
|
|
113
|
+
interval: { type: "days", value: 1 },
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
active: true,
|
|
117
|
+
});
|
|
118
|
+
return { campaignId: response.campaignId };
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
return {
|
|
122
|
+
error: `Failed to create visit campaign: ${error instanceof Error ? error.message : String(error)}`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function getAvailableBadges() {
|
|
127
|
+
try {
|
|
128
|
+
const response = await badgeList({});
|
|
129
|
+
const badgeMap = new Map();
|
|
130
|
+
for (const badge of response.badges) {
|
|
131
|
+
if (badge.name && badge.badgeTypeId) {
|
|
132
|
+
badgeMap.set(badge.name.toLowerCase(), badge.badgeTypeId);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return badgeMap;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return new Map();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function createVisitAchievement(cfg, badges) {
|
|
142
|
+
try {
|
|
143
|
+
const badgeTypeId = cfg.badgeName
|
|
144
|
+
? badges.get(cfg.badgeName.toLowerCase())
|
|
145
|
+
: undefined;
|
|
146
|
+
const achievementPayload = {
|
|
147
|
+
translations: {
|
|
148
|
+
en: {
|
|
149
|
+
name: `${cfg.visitMilestone} Sports Bar Visits`,
|
|
150
|
+
description: `Visit the sports bar ${cfg.visitMilestone} times this season`,
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
active: true,
|
|
154
|
+
activity: {
|
|
155
|
+
startsAt: formatOLDate(cfg.seasonStart),
|
|
156
|
+
endsAt: formatOLDate(cfg.seasonEnd),
|
|
157
|
+
},
|
|
158
|
+
rules: [
|
|
159
|
+
{
|
|
160
|
+
trigger: "custom_event",
|
|
161
|
+
event: "sportsbar_visit",
|
|
162
|
+
completeRule: {
|
|
163
|
+
periodGoal: cfg.visitMilestone,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
};
|
|
168
|
+
if (badgeTypeId) {
|
|
169
|
+
achievementPayload.badgeTypeId = badgeTypeId;
|
|
170
|
+
}
|
|
171
|
+
const response = await achievementCreate(achievementPayload);
|
|
172
|
+
return { achievementId: response.achievementId };
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
return {
|
|
176
|
+
error: `Failed to create visit achievement: ${error instanceof Error ? error.message : String(error)}`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function createAchievementBonusCampaign(achievementId, cfg) {
|
|
181
|
+
try {
|
|
182
|
+
const response = await campaignCreate({
|
|
183
|
+
type: "direct",
|
|
184
|
+
trigger: "achievement",
|
|
185
|
+
translations: {
|
|
186
|
+
en: {
|
|
187
|
+
name: `Sports Bar ${cfg.visitMilestone} Visits Bonus`,
|
|
188
|
+
description: `Bonus ${cfg.milestoneBonus} coins for visiting the sports bar ${cfg.visitMilestone} times`,
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
activity: {
|
|
192
|
+
startsAt: formatOLDate(cfg.seasonStart),
|
|
193
|
+
endsAt: formatOLDate(cfg.seasonEnd),
|
|
194
|
+
},
|
|
195
|
+
rules: [
|
|
196
|
+
{
|
|
197
|
+
name: `Bonus for ${cfg.visitMilestone} visits`,
|
|
198
|
+
effects: [
|
|
199
|
+
{
|
|
200
|
+
effect: "give_points",
|
|
201
|
+
pointsRule: { fixedValue: cfg.milestoneBonus },
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
conditions: [
|
|
205
|
+
{
|
|
206
|
+
operator: "is_equal",
|
|
207
|
+
attribute: "achievement.achievementId",
|
|
208
|
+
data: { value: achievementId },
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
active: true,
|
|
214
|
+
});
|
|
215
|
+
return { campaignId: response.campaignId };
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
return {
|
|
219
|
+
error: `Failed to create bonus campaign: ${error instanceof Error ? error.message : String(error)}`,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async function verifySetup(result) {
|
|
224
|
+
const warnings = [];
|
|
225
|
+
try {
|
|
226
|
+
if (result.visitCampaignId) {
|
|
227
|
+
const campaigns = await campaignList({ active: true });
|
|
228
|
+
const found = campaigns.campaigns.some((c) => c.campaignId === result.visitCampaignId);
|
|
229
|
+
if (!found) {
|
|
230
|
+
warnings.push("Visit campaign created but not found in active campaigns list");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (result.achievementId) {
|
|
234
|
+
const achievements = await achievementList({ active: true });
|
|
235
|
+
const found = achievements.achievements.some((a) => a.achievementId === result.achievementId);
|
|
236
|
+
if (!found) {
|
|
237
|
+
warnings.push("Achievement created but not found in active achievements list");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
warnings.push(`Verification error: ${error instanceof Error ? error.message : String(error)}`);
|
|
243
|
+
}
|
|
244
|
+
return { warnings };
|
|
245
|
+
}
|
|
246
|
+
function buildSummary(cfg, result) {
|
|
247
|
+
const lines = [];
|
|
248
|
+
if (result.visitCampaignId) {
|
|
249
|
+
lines.push(`Sports bar visit campaign created!`);
|
|
250
|
+
lines.push(`\nPer-visit reward: ${cfg.coinsPerVisit} coins`);
|
|
251
|
+
lines.push(`Daily limit: ${cfg.limitPerDay} visit${cfg.limitPerDay > 1 ? "s" : ""}`);
|
|
252
|
+
}
|
|
253
|
+
if (result.achievementId) {
|
|
254
|
+
lines.push(`\nAchievement: Visit ${cfg.visitMilestone} times`);
|
|
255
|
+
if (cfg.badgeName) {
|
|
256
|
+
lines.push(`Badge: ${cfg.badgeName}`);
|
|
257
|
+
}
|
|
258
|
+
if (result.bonusCampaignId) {
|
|
259
|
+
lines.push(`Completion bonus: ${cfg.milestoneBonus} coins`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
lines.push(`\nSeason: ${cfg.seasonStart} to ${cfg.seasonEnd}`);
|
|
263
|
+
lines.push(`\nCustom event to trigger reward:`);
|
|
264
|
+
lines.push(` Event: sportsbar_visit`);
|
|
265
|
+
lines.push(`\nExample event payload:`);
|
|
266
|
+
lines.push(`{`);
|
|
267
|
+
lines.push(` "event": "sportsbar_visit"`);
|
|
268
|
+
lines.push(`}`);
|
|
269
|
+
if (result.errors.length > 0) {
|
|
270
|
+
lines.push(`\nWarnings/Errors:`);
|
|
271
|
+
for (const error of result.errors) {
|
|
272
|
+
lines.push(`- ${error}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return lines.join("\n");
|
|
276
|
+
}
|
|
277
|
+
// ============================================================================
|
|
278
|
+
// Exports
|
|
279
|
+
// ============================================================================
|
|
280
|
+
export const sportsbarVisitWorkflow = {
|
|
281
|
+
id: "sportsbar-visit",
|
|
282
|
+
name: "Sports Bar Visit Campaign",
|
|
283
|
+
execute: executeSportsbarVisitWorkflow,
|
|
284
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VOD Watching Workflow
|
|
3
|
+
*
|
|
4
|
+
* Creates a campaign to reward fans for watching video content,
|
|
5
|
+
* with achievements for total watch time using attribute aggregation.
|
|
6
|
+
*/
|
|
7
|
+
export interface VodWatchingConfig {
|
|
8
|
+
/** Track by "views" or "minutes" */
|
|
9
|
+
trackBy: "views" | "minutes";
|
|
10
|
+
/** Coins awarded per unit (view or unitSize minutes) */
|
|
11
|
+
coinsPerUnit: number;
|
|
12
|
+
/** For minutes tracking: how many minutes per reward unit */
|
|
13
|
+
unitSize: number;
|
|
14
|
+
/** Whether to create achievement for total watching */
|
|
15
|
+
createAchievement: boolean;
|
|
16
|
+
/** Target for achievement (minutes or views) */
|
|
17
|
+
achievementTarget: number;
|
|
18
|
+
/** Bonus coins for completing the achievement */
|
|
19
|
+
achievementBonus: number;
|
|
20
|
+
/** Badge name for achievement (optional) */
|
|
21
|
+
badgeName?: string;
|
|
22
|
+
/** Season start date (ISO format) */
|
|
23
|
+
seasonStart: string;
|
|
24
|
+
/** Season end date (ISO format) */
|
|
25
|
+
seasonEnd: string;
|
|
26
|
+
}
|
|
27
|
+
export interface VodWatchingResult {
|
|
28
|
+
success: boolean;
|
|
29
|
+
watchCampaignId?: string;
|
|
30
|
+
achievementId?: string;
|
|
31
|
+
bonusCampaignId?: string;
|
|
32
|
+
errors: string[];
|
|
33
|
+
summary: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Execute the VOD watching workflow
|
|
37
|
+
*/
|
|
38
|
+
export declare function executeVodWatchingWorkflow(config?: Partial<VodWatchingConfig>): Promise<VodWatchingResult>;
|
|
39
|
+
export declare const vodWatchingWorkflow: {
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
execute: typeof executeVodWatchingWorkflow;
|
|
43
|
+
};
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VOD Watching Workflow
|
|
3
|
+
*
|
|
4
|
+
* Creates a campaign to reward fans for watching video content,
|
|
5
|
+
* with achievements for total watch time using attribute aggregation.
|
|
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 VOD watching workflow
|
|
16
|
+
*/
|
|
17
|
+
export async function executeVodWatchingWorkflow(config = {}) {
|
|
18
|
+
const result = {
|
|
19
|
+
success: false,
|
|
20
|
+
errors: [],
|
|
21
|
+
summary: "",
|
|
22
|
+
};
|
|
23
|
+
// Merge with defaults
|
|
24
|
+
const cfg = {
|
|
25
|
+
trackBy: config.trackBy ?? DEFAULTS.vodWatching.trackBy,
|
|
26
|
+
coinsPerUnit: config.coinsPerUnit ?? DEFAULTS.vodWatching.coinsPerUnit,
|
|
27
|
+
unitSize: config.unitSize ?? DEFAULTS.vodWatching.unitSize,
|
|
28
|
+
createAchievement: config.createAchievement ?? true,
|
|
29
|
+
achievementTarget: config.achievementTarget ?? DEFAULTS.vodWatching.achievementMinutes,
|
|
30
|
+
achievementBonus: config.achievementBonus ?? DEFAULTS.vodWatching.achievementBonus,
|
|
31
|
+
badgeName: config.badgeName ?? "Content Enthusiast",
|
|
32
|
+
seasonStart: config.seasonStart ?? DEFAULTS.seasonDates.start,
|
|
33
|
+
seasonEnd: config.seasonEnd ?? DEFAULTS.seasonDates.end,
|
|
34
|
+
};
|
|
35
|
+
try {
|
|
36
|
+
// Step 1: Create base VOD watching campaign
|
|
37
|
+
const watchCampaignResult = await createWatchCampaign(cfg);
|
|
38
|
+
if (watchCampaignResult.error) {
|
|
39
|
+
result.errors.push(watchCampaignResult.error);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
result.watchCampaignId = watchCampaignResult.campaignId;
|
|
43
|
+
}
|
|
44
|
+
// Step 2: Create achievement and bonus campaign if enabled
|
|
45
|
+
if (cfg.createAchievement) {
|
|
46
|
+
const badges = await getAvailableBadges();
|
|
47
|
+
const achievementResult = await createWatchAchievement(cfg, badges);
|
|
48
|
+
if (achievementResult.error) {
|
|
49
|
+
result.errors.push(achievementResult.error);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
result.achievementId = achievementResult.achievementId;
|
|
53
|
+
// Create bonus campaign for achievement
|
|
54
|
+
if (achievementResult.achievementId) {
|
|
55
|
+
const bonusCampaignResult = await createAchievementBonusCampaign(achievementResult.achievementId, cfg);
|
|
56
|
+
if (bonusCampaignResult.error) {
|
|
57
|
+
result.errors.push(bonusCampaignResult.error);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
result.bonusCampaignId = bonusCampaignResult.campaignId;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Step 3: Verify setup
|
|
66
|
+
const verification = await verifySetup(result);
|
|
67
|
+
if (verification.warnings.length > 0) {
|
|
68
|
+
result.errors.push(...verification.warnings);
|
|
69
|
+
}
|
|
70
|
+
// Determine success
|
|
71
|
+
result.success = result.watchCampaignId !== undefined && result.errors.length === 0;
|
|
72
|
+
// Build summary
|
|
73
|
+
result.summary = buildSummary(cfg, result);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
result.errors.push(`Workflow error: ${error instanceof Error ? error.message : String(error)}`);
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// Helper Functions
|
|
82
|
+
// ============================================================================
|
|
83
|
+
async function createWatchCampaign(cfg) {
|
|
84
|
+
try {
|
|
85
|
+
const isMinutesBased = cfg.trackBy === "minutes";
|
|
86
|
+
// For minutes-based tracking, we use an expression to calculate points
|
|
87
|
+
// based on the minutes_watched attribute divided by unitSize
|
|
88
|
+
const pointsRule = isMinutesBased
|
|
89
|
+
? {
|
|
90
|
+
expression: `Math.floor(event.minutes_watched / ${cfg.unitSize}) * ${cfg.coinsPerUnit}`,
|
|
91
|
+
}
|
|
92
|
+
: {
|
|
93
|
+
fixedValue: cfg.coinsPerUnit,
|
|
94
|
+
};
|
|
95
|
+
const description = isMinutesBased
|
|
96
|
+
? `Earn ${cfg.coinsPerUnit} coins for every ${cfg.unitSize} minutes of video watched`
|
|
97
|
+
: `Earn ${cfg.coinsPerUnit} coins for each video viewed`;
|
|
98
|
+
const response = await campaignCreate({
|
|
99
|
+
type: "direct",
|
|
100
|
+
trigger: "custom_event",
|
|
101
|
+
event: "vod_watch",
|
|
102
|
+
translations: {
|
|
103
|
+
en: {
|
|
104
|
+
name: "Video Content Reward",
|
|
105
|
+
description,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
activity: {
|
|
109
|
+
startsAt: formatOLDate(cfg.seasonStart),
|
|
110
|
+
endsAt: formatOLDate(cfg.seasonEnd),
|
|
111
|
+
},
|
|
112
|
+
rules: [
|
|
113
|
+
{
|
|
114
|
+
name: "Award coins for watching content",
|
|
115
|
+
effects: [
|
|
116
|
+
{
|
|
117
|
+
effect: "give_points",
|
|
118
|
+
pointsRule,
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
active: true,
|
|
124
|
+
});
|
|
125
|
+
return { campaignId: response.campaignId };
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
return {
|
|
129
|
+
error: `Failed to create VOD watch campaign: ${error instanceof Error ? error.message : String(error)}`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function getAvailableBadges() {
|
|
134
|
+
try {
|
|
135
|
+
const response = await badgeList({});
|
|
136
|
+
const badgeMap = new Map();
|
|
137
|
+
for (const badge of response.badges) {
|
|
138
|
+
if (badge.name && badge.badgeTypeId) {
|
|
139
|
+
badgeMap.set(badge.name.toLowerCase(), badge.badgeTypeId);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return badgeMap;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return new Map();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async function createWatchAchievement(cfg, badges) {
|
|
149
|
+
try {
|
|
150
|
+
const badgeTypeId = cfg.badgeName
|
|
151
|
+
? badges.get(cfg.badgeName.toLowerCase())
|
|
152
|
+
: undefined;
|
|
153
|
+
const isMinutesBased = cfg.trackBy === "minutes";
|
|
154
|
+
const targetLabel = isMinutesBased ? "minutes" : "videos";
|
|
155
|
+
/**
|
|
156
|
+
* For attribute-sum achievements, we use aggregation rule "sum" with
|
|
157
|
+
* the attribute to aggregate (e.g., "minutes_watched").
|
|
158
|
+
* The periodGoal is the total to reach.
|
|
159
|
+
*/
|
|
160
|
+
const achievementPayload = {
|
|
161
|
+
translations: {
|
|
162
|
+
en: {
|
|
163
|
+
name: `Watch ${cfg.achievementTarget} ${targetLabel}`,
|
|
164
|
+
description: isMinutesBased
|
|
165
|
+
? `Watch ${cfg.achievementTarget} total minutes of video content`
|
|
166
|
+
: `Watch ${cfg.achievementTarget} videos`,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
active: true,
|
|
170
|
+
activity: {
|
|
171
|
+
startsAt: formatOLDate(cfg.seasonStart),
|
|
172
|
+
endsAt: formatOLDate(cfg.seasonEnd),
|
|
173
|
+
},
|
|
174
|
+
rules: [
|
|
175
|
+
{
|
|
176
|
+
trigger: "custom_event",
|
|
177
|
+
event: "vod_watch",
|
|
178
|
+
completeRule: {
|
|
179
|
+
periodGoal: cfg.achievementTarget,
|
|
180
|
+
},
|
|
181
|
+
// For minutes tracking, aggregate the minutes_watched attribute
|
|
182
|
+
...(isMinutesBased && {
|
|
183
|
+
aggregation: {
|
|
184
|
+
rule: "sum",
|
|
185
|
+
},
|
|
186
|
+
}),
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
if (badgeTypeId) {
|
|
191
|
+
achievementPayload.badgeTypeId = badgeTypeId;
|
|
192
|
+
}
|
|
193
|
+
const response = await achievementCreate(achievementPayload);
|
|
194
|
+
return { achievementId: response.achievementId };
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
return {
|
|
198
|
+
error: `Failed to create watch achievement: ${error instanceof Error ? error.message : String(error)}`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async function createAchievementBonusCampaign(achievementId, cfg) {
|
|
203
|
+
try {
|
|
204
|
+
const targetLabel = cfg.trackBy === "minutes" ? "minutes" : "videos";
|
|
205
|
+
const response = await campaignCreate({
|
|
206
|
+
type: "direct",
|
|
207
|
+
trigger: "achievement",
|
|
208
|
+
translations: {
|
|
209
|
+
en: {
|
|
210
|
+
name: `Watch ${cfg.achievementTarget} ${targetLabel} Bonus`,
|
|
211
|
+
description: `Bonus ${cfg.achievementBonus} coins for watching ${cfg.achievementTarget} ${targetLabel}`,
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
activity: {
|
|
215
|
+
startsAt: formatOLDate(cfg.seasonStart),
|
|
216
|
+
endsAt: formatOLDate(cfg.seasonEnd),
|
|
217
|
+
},
|
|
218
|
+
rules: [
|
|
219
|
+
{
|
|
220
|
+
name: `Bonus for watching ${cfg.achievementTarget} ${targetLabel}`,
|
|
221
|
+
effects: [
|
|
222
|
+
{
|
|
223
|
+
effect: "give_points",
|
|
224
|
+
pointsRule: { fixedValue: cfg.achievementBonus },
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
conditions: [
|
|
228
|
+
{
|
|
229
|
+
operator: "is_equal",
|
|
230
|
+
attribute: "achievement.achievementId",
|
|
231
|
+
data: { value: achievementId },
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
active: true,
|
|
237
|
+
});
|
|
238
|
+
return { campaignId: response.campaignId };
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
return {
|
|
242
|
+
error: `Failed to create bonus campaign: ${error instanceof Error ? error.message : String(error)}`,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
async function verifySetup(result) {
|
|
247
|
+
const warnings = [];
|
|
248
|
+
try {
|
|
249
|
+
if (result.watchCampaignId) {
|
|
250
|
+
const campaigns = await campaignList({ active: true });
|
|
251
|
+
const found = campaigns.campaigns.some((c) => c.campaignId === result.watchCampaignId);
|
|
252
|
+
if (!found) {
|
|
253
|
+
warnings.push("Watch campaign created but not found in active campaigns list");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (result.achievementId) {
|
|
257
|
+
const achievements = await achievementList({ active: true });
|
|
258
|
+
const found = achievements.achievements.some((a) => a.achievementId === result.achievementId);
|
|
259
|
+
if (!found) {
|
|
260
|
+
warnings.push("Achievement created but not found in active achievements list");
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
warnings.push(`Verification error: ${error instanceof Error ? error.message : String(error)}`);
|
|
266
|
+
}
|
|
267
|
+
return { warnings };
|
|
268
|
+
}
|
|
269
|
+
function buildSummary(cfg, result) {
|
|
270
|
+
const lines = [];
|
|
271
|
+
const isMinutesBased = cfg.trackBy === "minutes";
|
|
272
|
+
const targetLabel = isMinutesBased ? "minutes" : "videos";
|
|
273
|
+
if (result.watchCampaignId) {
|
|
274
|
+
lines.push(`VOD watching campaign created!`);
|
|
275
|
+
lines.push(`\nTracking by: ${cfg.trackBy}`);
|
|
276
|
+
if (isMinutesBased) {
|
|
277
|
+
lines.push(`Reward: ${cfg.coinsPerUnit} coins per ${cfg.unitSize} minutes watched`);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
lines.push(`Reward: ${cfg.coinsPerUnit} coins per video viewed`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (result.achievementId) {
|
|
284
|
+
lines.push(`\nAchievement: Watch ${cfg.achievementTarget} ${targetLabel}`);
|
|
285
|
+
if (cfg.badgeName) {
|
|
286
|
+
lines.push(`Badge: ${cfg.badgeName}`);
|
|
287
|
+
}
|
|
288
|
+
if (result.bonusCampaignId) {
|
|
289
|
+
lines.push(`Completion bonus: ${cfg.achievementBonus} coins`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
lines.push(`\nSeason: ${cfg.seasonStart} to ${cfg.seasonEnd}`);
|
|
293
|
+
lines.push(`\nCustom event to trigger reward:`);
|
|
294
|
+
lines.push(` Event: vod_watch`);
|
|
295
|
+
if (isMinutesBased) {
|
|
296
|
+
lines.push(` Required attribute: minutes_watched (number)`);
|
|
297
|
+
lines.push(`\nExample event payload:`);
|
|
298
|
+
lines.push(`{`);
|
|
299
|
+
lines.push(` "event": "vod_watch",`);
|
|
300
|
+
lines.push(` "attributes": {`);
|
|
301
|
+
lines.push(` "minutes_watched": 15`);
|
|
302
|
+
lines.push(` }`);
|
|
303
|
+
lines.push(`}`);
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
lines.push(`\nExample event payload:`);
|
|
307
|
+
lines.push(`{`);
|
|
308
|
+
lines.push(` "event": "vod_watch"`);
|
|
309
|
+
lines.push(`}`);
|
|
310
|
+
}
|
|
311
|
+
if (result.errors.length > 0) {
|
|
312
|
+
lines.push(`\nWarnings/Errors:`);
|
|
313
|
+
for (const error of result.errors) {
|
|
314
|
+
lines.push(`- ${error}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return lines.join("\n");
|
|
318
|
+
}
|
|
319
|
+
// ============================================================================
|
|
320
|
+
// Exports
|
|
321
|
+
// ============================================================================
|
|
322
|
+
export const vodWatchingWorkflow = {
|
|
323
|
+
id: "vod-watching",
|
|
324
|
+
name: "VOD Watching Campaign",
|
|
325
|
+
execute: executeVodWatchingWorkflow,
|
|
326
|
+
};
|