@open-loyalty/mcp-server 1.3.7 → 1.5.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/dist/config.d.ts +4 -3
- package/dist/config.js +9 -7
- package/dist/instructions.d.ts +1 -1
- package/dist/instructions.js +26 -8
- package/dist/tools/achievement/handlers.js +60 -1
- package/dist/tools/achievement/index.d.ts +8 -8
- package/dist/tools/achievement/index.js +10 -11
- package/dist/tools/achievement/schemas.d.ts +8 -8
- package/dist/tools/achievement/schemas.js +36 -32
- package/dist/tools/analytics.js +9 -9
- package/dist/tools/badge.js +4 -4
- package/dist/tools/campaign/handlers.js +35 -1
- package/dist/tools/campaign/index.d.ts +97 -34
- package/dist/tools/campaign/index.js +14 -12
- package/dist/tools/campaign/schemas.d.ts +96 -33
- package/dist/tools/campaign/schemas.js +89 -55
- package/dist/tools/custom-event.js +10 -9
- package/dist/tools/export.js +4 -4
- package/dist/tools/import.d.ts +1 -1
- package/dist/tools/import.js +11 -6
- package/dist/tools/index.js +4 -4
- package/dist/tools/member/handlers.js +48 -1
- package/dist/tools/member/index.d.ts +1 -1
- package/dist/tools/member/index.js +3 -1
- package/dist/tools/member/schemas.js +12 -12
- package/dist/tools/points/handlers.d.ts +75 -0
- package/dist/tools/{points.js → points/handlers.js} +21 -112
- package/dist/tools/points/index.d.ts +84 -0
- package/dist/tools/points/index.js +63 -0
- package/dist/tools/points/schemas.d.ts +45 -0
- package/dist/tools/points/schemas.js +47 -0
- package/dist/tools/referral/schemas.js +3 -3
- package/dist/tools/reward/handlers.d.ts +2 -0
- package/dist/tools/reward/handlers.js +79 -6
- package/dist/tools/reward/index.d.ts +2 -0
- package/dist/tools/reward/index.js +10 -10
- package/dist/tools/reward/schemas.d.ts +2 -0
- package/dist/tools/reward/schemas.js +42 -23
- package/dist/tools/segment/index.js +7 -5
- package/dist/tools/segment/schemas.js +11 -11
- package/dist/tools/tierset/handlers.d.ts +26 -0
- package/dist/tools/tierset/handlers.js +196 -0
- package/dist/tools/tierset/index.d.ts +124 -0
- package/dist/tools/tierset/index.js +88 -0
- package/dist/tools/tierset/schemas.d.ts +127 -0
- package/dist/tools/tierset/schemas.js +62 -0
- package/dist/tools/transaction/handlers.d.ts +89 -0
- package/dist/tools/transaction/handlers.js +159 -0
- package/dist/tools/transaction/index.d.ts +153 -0
- package/dist/tools/transaction/index.js +54 -0
- package/dist/tools/transaction/schemas.d.ts +126 -0
- package/dist/tools/transaction/schemas.js +60 -0
- package/dist/tools/wallet-type/handlers.d.ts +63 -0
- package/dist/tools/{wallet-type.js → wallet-type/handlers.js} +15 -78
- package/dist/tools/{wallet-type.d.ts → wallet-type/index.d.ts} +3 -64
- package/dist/tools/wallet-type/index.js +65 -0
- package/dist/tools/wallet-type/schemas.d.ts +1 -0
- package/dist/tools/wallet-type/schemas.js +1 -0
- package/dist/tools/webhook.js +6 -6
- package/dist/types/schemas/achievement.d.ts +48 -48
- package/dist/types/schemas/admin.d.ts +10 -10
- package/dist/types/schemas/campaign.d.ts +12 -12
- package/dist/types/schemas/common.js +1 -1
- package/dist/types/schemas/member.d.ts +18 -18
- package/dist/types/schemas/role.d.ts +4 -4
- package/dist/types/schemas/tierset.js +2 -1
- package/dist/types/schemas/transaction.d.ts +12 -12
- package/dist/types/schemas/wallet-type.js +12 -10
- package/dist/types/schemas/webhook.d.ts +6 -6
- package/dist/utils/errors.js +40 -0
- package/package.json +3 -2
- package/dist/prompts/fan-engagement-setup.d.ts +0 -107
- package/dist/prompts/fan-engagement-setup.js +0 -492
- package/dist/tools/achievement.d.ts +0 -1017
- package/dist/tools/achievement.js +0 -354
- package/dist/tools/campaign.d.ts +0 -1800
- package/dist/tools/campaign.js +0 -737
- package/dist/tools/member.d.ts +0 -366
- package/dist/tools/member.js +0 -352
- package/dist/tools/points.d.ts +0 -201
- package/dist/tools/reward.d.ts +0 -279
- package/dist/tools/reward.js +0 -361
- package/dist/tools/segment.d.ts +0 -816
- package/dist/tools/segment.js +0 -333
- package/dist/tools/tierset.d.ts +0 -273
- package/dist/tools/tierset.js +0 -289
- package/dist/tools/transaction.d.ts +0 -365
- package/dist/tools/transaction.js +0 -259
- package/dist/workflows/app-login-streak.d.ts +0 -39
- package/dist/workflows/app-login-streak.js +0 -298
- package/dist/workflows/early-arrival.d.ts +0 -33
- package/dist/workflows/early-arrival.js +0 -148
- package/dist/workflows/index.d.ts +0 -101
- package/dist/workflows/index.js +0 -208
- package/dist/workflows/match-attendance.d.ts +0 -45
- package/dist/workflows/match-attendance.js +0 -308
- package/dist/workflows/sportsbar-visit.d.ts +0 -41
- package/dist/workflows/sportsbar-visit.js +0 -284
- package/dist/workflows/vod-watching.d.ts +0 -43
- package/dist/workflows/vod-watching.js +0 -326
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { apiGet, apiPost } from "../client/http.js";
|
|
3
|
-
import { formatApiError, OpenLoyaltyError } from "../utils/errors.js";
|
|
4
|
-
import { getStoreCode } from "../config.js";
|
|
5
|
-
import { buildPaginationParams, normalizeDateToISO } from "../utils/pagination.js";
|
|
6
|
-
// Input Schemas
|
|
7
|
-
const LabelInputSchema = z.object({
|
|
8
|
-
key: z.string(),
|
|
9
|
-
value: z.string(),
|
|
10
|
-
});
|
|
11
|
-
const TransactionHeaderInputSchema = z.object({
|
|
12
|
-
documentNumber: z.string().describe("Unique document number (required, unique per store)."),
|
|
13
|
-
purchasedAt: z.string().describe("Purchase date/time in ISO format (required)."),
|
|
14
|
-
documentType: z.enum(["sell", "return"]).optional().describe("Document type: sell (default) or return."),
|
|
15
|
-
linkedDocumentNumber: z.string().optional().describe("Original document number (required for returns)."),
|
|
16
|
-
purchasePlace: z.string().optional().describe("Location/store where purchase was made."),
|
|
17
|
-
labels: z.array(LabelInputSchema).optional().describe("Custom key-value labels."),
|
|
18
|
-
});
|
|
19
|
-
const TransactionItemInputSchema = z.object({
|
|
20
|
-
sku: z.string().describe("Product SKU (required, max 255 chars)."),
|
|
21
|
-
name: z.string().describe("Product name (required, max 255 chars)."),
|
|
22
|
-
grossValue: z.number().describe("Item gross value (required)."),
|
|
23
|
-
category: z.string().describe("Product category (required, max 255 chars)."),
|
|
24
|
-
quantity: z.number().optional().describe("Item quantity (integer)."),
|
|
25
|
-
highPrecisionQuantity: z.number().optional().describe("High precision quantity (up to 3 decimals, recommended)."),
|
|
26
|
-
maker: z.string().optional().describe("Product brand/maker."),
|
|
27
|
-
labels: z.array(LabelInputSchema).optional().describe("Custom key-value labels."),
|
|
28
|
-
});
|
|
29
|
-
const CustomerDataInputSchema = z.object({
|
|
30
|
-
customerId: z.string().optional().describe("Member ID (UUID) for explicit assignment."),
|
|
31
|
-
email: z.string().optional().describe("Email for auto-matching to member."),
|
|
32
|
-
name: z.string().optional().describe("Customer name."),
|
|
33
|
-
phone: z.string().optional().describe("Phone for auto-matching to member."),
|
|
34
|
-
loyaltyCardNumber: z.string().optional().describe("Loyalty card for auto-matching."),
|
|
35
|
-
});
|
|
36
|
-
export const TransactionCreateInputSchema = {
|
|
37
|
-
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."),
|
|
38
|
-
header: TransactionHeaderInputSchema.describe("Transaction header with document info."),
|
|
39
|
-
items: z.array(TransactionItemInputSchema).describe("Array of purchased items (at least 1 required)."),
|
|
40
|
-
customerData: CustomerDataInputSchema.optional().describe("Customer data for auto-matching to member. If provided, triggers point campaigns."),
|
|
41
|
-
};
|
|
42
|
-
export const TransactionGetInputSchema = {
|
|
43
|
-
storeCode: z.string().optional().describe("Store code for multi-tenant routing. DO NOT pass this parameter - the configured default will be used automatically. Only provide a value if the user explicitly asks to work with a different store."),
|
|
44
|
-
transactionId: z.string().describe("The transaction ID (UUID) to retrieve."),
|
|
45
|
-
};
|
|
46
|
-
export const TransactionListInputSchema = {
|
|
47
|
-
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."),
|
|
48
|
-
cursor: z.string().optional().describe("Pagination cursor from previous response. If provided, page/perPage are ignored."),
|
|
49
|
-
page: z.number().optional().describe("Page number (default: 1)."),
|
|
50
|
-
perPage: z.number().max(50).optional().describe("Items per page (default: 10, max: 50). Use 50 for aggregation queries to reduce API calls."),
|
|
51
|
-
customerId: z.string().optional().describe("Filter by member ID."),
|
|
52
|
-
documentNumber: z.string().optional().describe("Filter by document number."),
|
|
53
|
-
documentType: z.enum(["sell", "return"]).optional().describe("Filter by document type."),
|
|
54
|
-
matched: z.boolean().optional().describe("Filter by matched status."),
|
|
55
|
-
purchasedAtFrom: z.string().optional().describe("Filter transactions purchased on or after this date (ISO format: YYYY-MM-DD or YYYY-MM-DDTHH:mm:ssZ)."),
|
|
56
|
-
purchasedAtTo: z.string().optional().describe("Filter transactions purchased on or before this date (ISO format: YYYY-MM-DD or YYYY-MM-DDTHH:mm:ssZ)."),
|
|
57
|
-
};
|
|
58
|
-
export const TransactionAssignMemberInputSchema = {
|
|
59
|
-
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."),
|
|
60
|
-
documentNumber: z.string().describe("Document number of the transaction to assign."),
|
|
61
|
-
customerId: z.string().optional().describe("Member ID (UUID) to assign."),
|
|
62
|
-
loyaltyCardNumber: z.string().optional().describe("Loyalty card number to match member."),
|
|
63
|
-
phone: z.string().optional().describe("Phone number to match member."),
|
|
64
|
-
};
|
|
65
|
-
// Handler functions
|
|
66
|
-
export async function transactionCreate(input) {
|
|
67
|
-
const storeCode = getStoreCode(input.storeCode);
|
|
68
|
-
const transactionPayload = {
|
|
69
|
-
header: {
|
|
70
|
-
documentNumber: input.header.documentNumber,
|
|
71
|
-
documentType: input.header.documentType || "sell",
|
|
72
|
-
purchasedAt: input.header.purchasedAt,
|
|
73
|
-
purchasePlace: input.header.purchasePlace,
|
|
74
|
-
linkedDocumentNumber: input.header.linkedDocumentNumber,
|
|
75
|
-
labels: input.header.labels,
|
|
76
|
-
},
|
|
77
|
-
items: input.items.map((item) => ({
|
|
78
|
-
sku: item.sku,
|
|
79
|
-
name: item.name,
|
|
80
|
-
grossValue: item.grossValue,
|
|
81
|
-
category: item.category,
|
|
82
|
-
highPrecisionQuantity: item.highPrecisionQuantity || item.quantity || 1,
|
|
83
|
-
maker: item.maker,
|
|
84
|
-
labels: item.labels,
|
|
85
|
-
})),
|
|
86
|
-
};
|
|
87
|
-
if (input.customerData) {
|
|
88
|
-
transactionPayload.customerData = input.customerData;
|
|
89
|
-
}
|
|
90
|
-
try {
|
|
91
|
-
const response = await apiPost(`/${storeCode}/transaction`, { transaction: transactionPayload });
|
|
92
|
-
return {
|
|
93
|
-
transactionId: response.transactionId,
|
|
94
|
-
pointsEarned: response.pointsEarned,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
catch (error) {
|
|
98
|
-
// Check for duplicate document number error
|
|
99
|
-
// Must check axios response data, not just error.message
|
|
100
|
-
const axiosError = error;
|
|
101
|
-
const apiErrors = axiosError.response?.data?.errors || [];
|
|
102
|
-
const allErrorMessages = apiErrors.map(e => e.message).join(" ").toLowerCase();
|
|
103
|
-
if (allErrorMessages.includes("document number") && allErrorMessages.includes("unique")) {
|
|
104
|
-
throw new OpenLoyaltyError({
|
|
105
|
-
code: "DUPLICATE_DOCUMENT",
|
|
106
|
-
message: `Transaction with document number '${input.header.documentNumber}' already exists`,
|
|
107
|
-
hint: `Each transaction must have a unique documentNumber. Use transaction_list(documentNumber: "${input.header.documentNumber}") to check if it exists, or generate a new unique document number (e.g., add timestamp suffix).`,
|
|
108
|
-
relatedTool: "ol_transaction_create",
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
throw formatApiError(error, "ol_transaction_create");
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
export async function transactionGet(input) {
|
|
115
|
-
const storeCode = getStoreCode(input.storeCode);
|
|
116
|
-
try {
|
|
117
|
-
const response = await apiGet(`/${storeCode}/transaction/${input.transactionId}`);
|
|
118
|
-
const header = response.header;
|
|
119
|
-
return {
|
|
120
|
-
transactionId: response.transactionId,
|
|
121
|
-
documentNumber: (header?.documentNumber || response.documentNumber),
|
|
122
|
-
documentType: (header?.documentType || response.documentType || "sell"),
|
|
123
|
-
items: (response.items || []),
|
|
124
|
-
customerData: response.customerData,
|
|
125
|
-
matched: (response.matched ?? response.customerId != null),
|
|
126
|
-
grossValue: (response.grossValue || 0),
|
|
127
|
-
pointsEarned: response.pointsEarned,
|
|
128
|
-
purchasedAt: (header?.purchasedAt || response.purchasedAt),
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
catch (error) {
|
|
132
|
-
throw formatApiError(error, "ol_transaction_get");
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
export async function transactionList(input) {
|
|
136
|
-
const storeCode = getStoreCode(input.storeCode);
|
|
137
|
-
const params = new URLSearchParams();
|
|
138
|
-
// Enforce max perPage of 50 (API limit)
|
|
139
|
-
const perPage = input.perPage ? Math.min(input.perPage, 50) : undefined;
|
|
140
|
-
// Use buildPaginationParams for cursor/page handling
|
|
141
|
-
buildPaginationParams({ cursor: input.cursor, page: input.page, perPage }, params);
|
|
142
|
-
if (input.customerId)
|
|
143
|
-
params.append("customerId", input.customerId);
|
|
144
|
-
if (input.documentNumber)
|
|
145
|
-
params.append("header:documentNumber", input.documentNumber);
|
|
146
|
-
if (input.documentType)
|
|
147
|
-
params.append("header:documentType", input.documentType);
|
|
148
|
-
if (input.matched !== undefined)
|
|
149
|
-
params.append("matched", String(input.matched));
|
|
150
|
-
if (input.purchasedAtFrom)
|
|
151
|
-
params.append("header:purchasedAt[gte]", normalizeDateToISO(input.purchasedAtFrom, "from"));
|
|
152
|
-
if (input.purchasedAtTo)
|
|
153
|
-
params.append("header:purchasedAt[lte]", normalizeDateToISO(input.purchasedAtTo, "to"));
|
|
154
|
-
const queryString = params.toString();
|
|
155
|
-
const url = `/${storeCode}/transaction${queryString ? `?${queryString}` : ""}`;
|
|
156
|
-
try {
|
|
157
|
-
const response = await apiGet(url);
|
|
158
|
-
const items = response.items || response.transactions || [];
|
|
159
|
-
const transactions = items.map((t) => {
|
|
160
|
-
const txn = t;
|
|
161
|
-
const header = txn.header;
|
|
162
|
-
const customerId = txn.customerId;
|
|
163
|
-
return {
|
|
164
|
-
transactionId: txn.transactionId,
|
|
165
|
-
documentNumber: (header?.documentNumber || txn.documentNumber),
|
|
166
|
-
grossValue: (txn.grossValue || 0),
|
|
167
|
-
matched: (txn.matched ?? customerId != null),
|
|
168
|
-
customerId,
|
|
169
|
-
pointsEarned: txn.pointsEarned,
|
|
170
|
-
purchasedAt: (header?.purchasedAt || txn.purchasedAt),
|
|
171
|
-
};
|
|
172
|
-
});
|
|
173
|
-
const total = response.total || {};
|
|
174
|
-
return {
|
|
175
|
-
transactions,
|
|
176
|
-
total: {
|
|
177
|
-
all: typeof total.all === 'number' ? total.all : undefined,
|
|
178
|
-
filtered: typeof total.filtered === 'number' ? total.filtered : undefined,
|
|
179
|
-
},
|
|
180
|
-
cursor: response.scroll,
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
catch (error) {
|
|
184
|
-
throw formatApiError(error, "ol_transaction_list");
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
export async function transactionAssignMember(input) {
|
|
188
|
-
const storeCode = getStoreCode(input.storeCode);
|
|
189
|
-
// At least one identifier required
|
|
190
|
-
if (!input.customerId && !input.loyaltyCardNumber && !input.phone) {
|
|
191
|
-
throw new Error("At least one member identifier required: customerId, loyaltyCardNumber, or phone.");
|
|
192
|
-
}
|
|
193
|
-
const assignPayload = {
|
|
194
|
-
transactionDocumentNumber: input.documentNumber,
|
|
195
|
-
};
|
|
196
|
-
if (input.customerId)
|
|
197
|
-
assignPayload.customerId = input.customerId;
|
|
198
|
-
if (input.loyaltyCardNumber)
|
|
199
|
-
assignPayload.customerLoyaltyCardNumber = input.loyaltyCardNumber;
|
|
200
|
-
if (input.phone)
|
|
201
|
-
assignPayload.customerPhoneNumber = input.phone;
|
|
202
|
-
try {
|
|
203
|
-
// API uses /transaction/assign with assign wrapper, not /transaction/customer/assign
|
|
204
|
-
const response = await apiPost(`/${storeCode}/transaction/assign`, { assign: assignPayload });
|
|
205
|
-
return {
|
|
206
|
-
transactionId: response.transactionId,
|
|
207
|
-
customerId: response.customerId,
|
|
208
|
-
pointsEarned: response.pointsEarned,
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
catch (error) {
|
|
212
|
-
throw formatApiError(error, "ol_transaction_assign_member");
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
// Tool definitions
|
|
216
|
-
export const transactionToolDefinitions = [
|
|
217
|
-
{
|
|
218
|
-
name: "ol_transaction_create",
|
|
219
|
-
title: "Record Purchase",
|
|
220
|
-
description: "Record a purchase transaction. If customerData provided, auto-matches to member and triggers point campaigns. " +
|
|
221
|
-
"For returns, set documentType='return' and provide linkedDocumentNumber referencing original sale. " +
|
|
222
|
-
"Returns transactionId and pointsEarned if campaigns triggered.",
|
|
223
|
-
readOnly: false,
|
|
224
|
-
inputSchema: TransactionCreateInputSchema,
|
|
225
|
-
handler: transactionCreate,
|
|
226
|
-
},
|
|
227
|
-
{
|
|
228
|
-
name: "ol_transaction_get",
|
|
229
|
-
title: "Get Transaction Details",
|
|
230
|
-
description: "Get transaction details including items, customerData, matched status, and pointsEarned.",
|
|
231
|
-
readOnly: true,
|
|
232
|
-
inputSchema: TransactionGetInputSchema,
|
|
233
|
-
handler: transactionGet,
|
|
234
|
-
},
|
|
235
|
-
{
|
|
236
|
-
name: "ol_transaction_list",
|
|
237
|
-
title: "Search Transactions",
|
|
238
|
-
description: "List transactions with optional filters. Filter by customerId, documentNumber, documentType, matched status, or date range (purchasedAtFrom/purchasedAtTo). " +
|
|
239
|
-
"Use ISO date format for date filters (e.g., '2025-07-01' or '2025-07-01T00:00:00Z'). " +
|
|
240
|
-
"Supports cursor pagination: provide 'cursor' from previous response to get next page. " +
|
|
241
|
-
"Returns customerId for matched transactions - use this for aggregation queries like 'top spenders'. " +
|
|
242
|
-
"IMPORTANT FOR AGGREGATION QUERIES: Always use cursor pagination and iterate through ALL pages, even for large datasets (1000+ transactions). " +
|
|
243
|
-
"Do NOT try to optimize or find shortcuts - the cursor approach is the correct and only way to get accurate results. " +
|
|
244
|
-
"Start with cursor='' (empty string), then use returned cursor for subsequent requests until no more data. " +
|
|
245
|
-
"Use perPage=50 to minimize API calls. Aggregate by customerId in your code, then use member_get for names.",
|
|
246
|
-
readOnly: true,
|
|
247
|
-
inputSchema: TransactionListInputSchema,
|
|
248
|
-
handler: transactionList,
|
|
249
|
-
},
|
|
250
|
-
{
|
|
251
|
-
name: "ol_transaction_assign_member",
|
|
252
|
-
title: "Link Transaction to Member",
|
|
253
|
-
description: "Assign unmatched transaction to member by ID, card number, or phone. Triggers point campaigns. " +
|
|
254
|
-
"Use this to link a transaction that wasn't matched at creation time. Returns transactionId, customerId, and pointsEarned.",
|
|
255
|
-
readOnly: false,
|
|
256
|
-
inputSchema: TransactionAssignMemberInputSchema,
|
|
257
|
-
handler: transactionAssignMember,
|
|
258
|
-
},
|
|
259
|
-
];
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* App Login Streak Workflow
|
|
3
|
-
*
|
|
4
|
-
* Creates a campaign to reward fans for daily app logins,
|
|
5
|
-
* with achievements for consecutive login streaks.
|
|
6
|
-
*/
|
|
7
|
-
export interface AppLoginStreakConfig {
|
|
8
|
-
/** Coins awarded per daily login */
|
|
9
|
-
coinsPerLogin: number;
|
|
10
|
-
/** Whether to create streak bonus achievement */
|
|
11
|
-
createStreakBonus: boolean;
|
|
12
|
-
/** Number of consecutive days for streak achievement */
|
|
13
|
-
streakDays: number;
|
|
14
|
-
/** Bonus coins for completing the streak */
|
|
15
|
-
streakBonus: number;
|
|
16
|
-
/** Badge name for streak achievement (optional) */
|
|
17
|
-
badgeName?: string;
|
|
18
|
-
/** Season start date (ISO format) */
|
|
19
|
-
seasonStart: string;
|
|
20
|
-
/** Season end date (ISO format) */
|
|
21
|
-
seasonEnd: string;
|
|
22
|
-
}
|
|
23
|
-
export interface AppLoginStreakResult {
|
|
24
|
-
success: boolean;
|
|
25
|
-
loginCampaignId?: string;
|
|
26
|
-
streakAchievementId?: string;
|
|
27
|
-
streakBonusCampaignId?: string;
|
|
28
|
-
errors: string[];
|
|
29
|
-
summary: string;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Execute the app login streak workflow
|
|
33
|
-
*/
|
|
34
|
-
export declare function executeAppLoginStreakWorkflow(config?: Partial<AppLoginStreakConfig>): Promise<AppLoginStreakResult>;
|
|
35
|
-
export declare const appLoginStreakWorkflow: {
|
|
36
|
-
id: string;
|
|
37
|
-
name: string;
|
|
38
|
-
execute: typeof executeAppLoginStreakWorkflow;
|
|
39
|
-
};
|
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* App Login Streak Workflow
|
|
3
|
-
*
|
|
4
|
-
* Creates a campaign to reward fans for daily app logins,
|
|
5
|
-
* with achievements for consecutive login streaks.
|
|
6
|
-
*/
|
|
7
|
-
import { campaignCreate, campaignList } from "../tools/campaign/handlers.js";
|
|
8
|
-
import { achievementCreate, achievementList } from "../tools/achievement.js";
|
|
9
|
-
import { badgeList } from "../tools/badge.js";
|
|
10
|
-
import { formatOLDate, DEFAULTS } from "../prompts/fan-engagement-setup.js";
|
|
11
|
-
// ============================================================================
|
|
12
|
-
// Workflow Implementation
|
|
13
|
-
// ============================================================================
|
|
14
|
-
/**
|
|
15
|
-
* Execute the app login streak workflow
|
|
16
|
-
*/
|
|
17
|
-
export async function executeAppLoginStreakWorkflow(config = {}) {
|
|
18
|
-
const result = {
|
|
19
|
-
success: false,
|
|
20
|
-
errors: [],
|
|
21
|
-
summary: "",
|
|
22
|
-
};
|
|
23
|
-
// Merge with defaults
|
|
24
|
-
const cfg = {
|
|
25
|
-
coinsPerLogin: config.coinsPerLogin ?? DEFAULTS.appLoginStreak.coinsPerLogin,
|
|
26
|
-
createStreakBonus: config.createStreakBonus ?? true,
|
|
27
|
-
streakDays: config.streakDays ?? DEFAULTS.appLoginStreak.streakDays,
|
|
28
|
-
streakBonus: config.streakBonus ?? DEFAULTS.appLoginStreak.streakBonus,
|
|
29
|
-
badgeName: config.badgeName ?? "Dedicated Fan",
|
|
30
|
-
seasonStart: config.seasonStart ?? DEFAULTS.seasonDates.start,
|
|
31
|
-
seasonEnd: config.seasonEnd ?? DEFAULTS.seasonDates.end,
|
|
32
|
-
};
|
|
33
|
-
try {
|
|
34
|
-
// Step 1: Create daily login campaign (limited to 1 per day)
|
|
35
|
-
const loginCampaignResult = await createLoginCampaign(cfg);
|
|
36
|
-
if (loginCampaignResult.error) {
|
|
37
|
-
result.errors.push(loginCampaignResult.error);
|
|
38
|
-
}
|
|
39
|
-
else {
|
|
40
|
-
result.loginCampaignId = loginCampaignResult.campaignId;
|
|
41
|
-
}
|
|
42
|
-
// Step 2: Create streak achievement and bonus campaign if enabled
|
|
43
|
-
if (cfg.createStreakBonus) {
|
|
44
|
-
const badges = await getAvailableBadges();
|
|
45
|
-
const achievementResult = await createStreakAchievement(cfg, badges);
|
|
46
|
-
if (achievementResult.error) {
|
|
47
|
-
result.errors.push(achievementResult.error);
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
result.streakAchievementId = achievementResult.achievementId;
|
|
51
|
-
// Create bonus campaign for streak achievement
|
|
52
|
-
if (achievementResult.achievementId) {
|
|
53
|
-
const bonusCampaignResult = await createStreakBonusCampaign(achievementResult.achievementId, cfg);
|
|
54
|
-
if (bonusCampaignResult.error) {
|
|
55
|
-
result.errors.push(bonusCampaignResult.error);
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
result.streakBonusCampaignId = bonusCampaignResult.campaignId;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
// Step 3: Verify setup
|
|
64
|
-
const verification = await verifySetup(result);
|
|
65
|
-
if (verification.warnings.length > 0) {
|
|
66
|
-
result.errors.push(...verification.warnings);
|
|
67
|
-
}
|
|
68
|
-
// Determine success
|
|
69
|
-
result.success = result.loginCampaignId !== undefined && result.errors.length === 0;
|
|
70
|
-
// Build summary
|
|
71
|
-
result.summary = buildSummary(cfg, result);
|
|
72
|
-
}
|
|
73
|
-
catch (error) {
|
|
74
|
-
result.errors.push(`Workflow error: ${error instanceof Error ? error.message : String(error)}`);
|
|
75
|
-
}
|
|
76
|
-
return result;
|
|
77
|
-
}
|
|
78
|
-
// ============================================================================
|
|
79
|
-
// Helper Functions
|
|
80
|
-
// ============================================================================
|
|
81
|
-
async function createLoginCampaign(cfg) {
|
|
82
|
-
try {
|
|
83
|
-
const response = await campaignCreate({
|
|
84
|
-
type: "direct",
|
|
85
|
-
trigger: "custom_event",
|
|
86
|
-
event: "app_login",
|
|
87
|
-
translations: {
|
|
88
|
-
en: {
|
|
89
|
-
name: "Daily App Login Reward",
|
|
90
|
-
description: `Earn ${cfg.coinsPerLogin} coins for logging into the app daily`,
|
|
91
|
-
},
|
|
92
|
-
},
|
|
93
|
-
activity: {
|
|
94
|
-
startsAt: formatOLDate(cfg.seasonStart),
|
|
95
|
-
endsAt: formatOLDate(cfg.seasonEnd),
|
|
96
|
-
},
|
|
97
|
-
rules: [
|
|
98
|
-
{
|
|
99
|
-
name: "Award coins for daily login",
|
|
100
|
-
effects: [
|
|
101
|
-
{
|
|
102
|
-
effect: "give_points",
|
|
103
|
-
pointsRule: { fixedValue: cfg.coinsPerLogin },
|
|
104
|
-
},
|
|
105
|
-
],
|
|
106
|
-
},
|
|
107
|
-
],
|
|
108
|
-
limits: {
|
|
109
|
-
// Strictly one login reward per day
|
|
110
|
-
executionsPerMember: {
|
|
111
|
-
value: 1,
|
|
112
|
-
interval: { type: "days", value: 1 },
|
|
113
|
-
},
|
|
114
|
-
},
|
|
115
|
-
active: true,
|
|
116
|
-
});
|
|
117
|
-
return { campaignId: response.campaignId };
|
|
118
|
-
}
|
|
119
|
-
catch (error) {
|
|
120
|
-
return {
|
|
121
|
-
error: `Failed to create login campaign: ${error instanceof Error ? error.message : String(error)}`,
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
async function getAvailableBadges() {
|
|
126
|
-
try {
|
|
127
|
-
const response = await badgeList({});
|
|
128
|
-
const badgeMap = new Map();
|
|
129
|
-
for (const badge of response.badges) {
|
|
130
|
-
if (badge.name && badge.badgeTypeId) {
|
|
131
|
-
badgeMap.set(badge.name.toLowerCase(), badge.badgeTypeId);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return badgeMap;
|
|
135
|
-
}
|
|
136
|
-
catch {
|
|
137
|
-
return new Map();
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
async function createStreakAchievement(cfg, badges) {
|
|
141
|
-
try {
|
|
142
|
-
const badgeTypeId = cfg.badgeName
|
|
143
|
-
? badges.get(cfg.badgeName.toLowerCase())
|
|
144
|
-
: undefined;
|
|
145
|
-
/**
|
|
146
|
-
* Streak achievements in Open Loyalty use:
|
|
147
|
-
* - periodGoal: 1 (one login per period)
|
|
148
|
-
* - period: { consecutive: X, value: 1 } where X is streak days
|
|
149
|
-
*
|
|
150
|
-
* This tracks consecutive periods (days) where the goal is met.
|
|
151
|
-
*/
|
|
152
|
-
const achievementPayload = {
|
|
153
|
-
translations: {
|
|
154
|
-
en: {
|
|
155
|
-
name: `${cfg.streakDays}-Day Login Streak`,
|
|
156
|
-
description: `Log in ${cfg.streakDays} consecutive days to complete this achievement`,
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
active: true,
|
|
160
|
-
activity: {
|
|
161
|
-
startsAt: formatOLDate(cfg.seasonStart),
|
|
162
|
-
endsAt: formatOLDate(cfg.seasonEnd),
|
|
163
|
-
},
|
|
164
|
-
rules: [
|
|
165
|
-
{
|
|
166
|
-
trigger: "custom_event",
|
|
167
|
-
event: "app_login",
|
|
168
|
-
completeRule: {
|
|
169
|
-
periodGoal: 1,
|
|
170
|
-
period: {
|
|
171
|
-
consecutive: cfg.streakDays,
|
|
172
|
-
value: 1, // 1 day period
|
|
173
|
-
},
|
|
174
|
-
},
|
|
175
|
-
},
|
|
176
|
-
],
|
|
177
|
-
};
|
|
178
|
-
if (badgeTypeId) {
|
|
179
|
-
achievementPayload.badgeTypeId = badgeTypeId;
|
|
180
|
-
}
|
|
181
|
-
const response = await achievementCreate(achievementPayload);
|
|
182
|
-
return { achievementId: response.achievementId };
|
|
183
|
-
}
|
|
184
|
-
catch (error) {
|
|
185
|
-
return {
|
|
186
|
-
error: `Failed to create streak achievement: ${error instanceof Error ? error.message : String(error)}`,
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
async function createStreakBonusCampaign(achievementId, cfg) {
|
|
191
|
-
try {
|
|
192
|
-
const response = await campaignCreate({
|
|
193
|
-
type: "direct",
|
|
194
|
-
trigger: "achievement",
|
|
195
|
-
translations: {
|
|
196
|
-
en: {
|
|
197
|
-
name: `${cfg.streakDays}-Day Login Streak Bonus`,
|
|
198
|
-
description: `Bonus ${cfg.streakBonus} coins for maintaining a ${cfg.streakDays}-day login streak`,
|
|
199
|
-
},
|
|
200
|
-
},
|
|
201
|
-
activity: {
|
|
202
|
-
startsAt: formatOLDate(cfg.seasonStart),
|
|
203
|
-
endsAt: formatOLDate(cfg.seasonEnd),
|
|
204
|
-
},
|
|
205
|
-
rules: [
|
|
206
|
-
{
|
|
207
|
-
name: `Bonus for ${cfg.streakDays}-day streak`,
|
|
208
|
-
effects: [
|
|
209
|
-
{
|
|
210
|
-
effect: "give_points",
|
|
211
|
-
pointsRule: { fixedValue: cfg.streakBonus },
|
|
212
|
-
},
|
|
213
|
-
],
|
|
214
|
-
conditions: [
|
|
215
|
-
{
|
|
216
|
-
operator: "is_equal",
|
|
217
|
-
attribute: "achievement.achievementId",
|
|
218
|
-
data: { value: achievementId },
|
|
219
|
-
},
|
|
220
|
-
],
|
|
221
|
-
},
|
|
222
|
-
],
|
|
223
|
-
active: true,
|
|
224
|
-
});
|
|
225
|
-
return { campaignId: response.campaignId };
|
|
226
|
-
}
|
|
227
|
-
catch (error) {
|
|
228
|
-
return {
|
|
229
|
-
error: `Failed to create streak bonus campaign: ${error instanceof Error ? error.message : String(error)}`,
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
async function verifySetup(result) {
|
|
234
|
-
const warnings = [];
|
|
235
|
-
try {
|
|
236
|
-
if (result.loginCampaignId) {
|
|
237
|
-
const campaigns = await campaignList({ active: true });
|
|
238
|
-
const found = campaigns.campaigns.some((c) => c.campaignId === result.loginCampaignId);
|
|
239
|
-
if (!found) {
|
|
240
|
-
warnings.push("Login campaign created but not found in active campaigns list");
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
if (result.streakAchievementId) {
|
|
244
|
-
const achievements = await achievementList({ active: true });
|
|
245
|
-
const found = achievements.achievements.some((a) => a.achievementId === result.streakAchievementId);
|
|
246
|
-
if (!found) {
|
|
247
|
-
warnings.push("Streak achievement created but not found in active achievements list");
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
catch (error) {
|
|
252
|
-
warnings.push(`Verification error: ${error instanceof Error ? error.message : String(error)}`);
|
|
253
|
-
}
|
|
254
|
-
return { warnings };
|
|
255
|
-
}
|
|
256
|
-
function buildSummary(cfg, result) {
|
|
257
|
-
const lines = [];
|
|
258
|
-
if (result.loginCampaignId) {
|
|
259
|
-
lines.push(`Daily login campaign created!`);
|
|
260
|
-
lines.push(`\nDaily reward: ${cfg.coinsPerLogin} coins`);
|
|
261
|
-
lines.push(`Limit: 1 reward per day (prevents multiple login scans)`);
|
|
262
|
-
}
|
|
263
|
-
if (result.streakAchievementId) {
|
|
264
|
-
lines.push(`\nStreak Achievement: ${cfg.streakDays} consecutive days`);
|
|
265
|
-
if (cfg.badgeName) {
|
|
266
|
-
lines.push(`Badge: ${cfg.badgeName}`);
|
|
267
|
-
}
|
|
268
|
-
if (result.streakBonusCampaignId) {
|
|
269
|
-
lines.push(`Streak bonus: ${cfg.streakBonus} coins`);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
lines.push(`\nSeason: ${cfg.seasonStart} to ${cfg.seasonEnd}`);
|
|
273
|
-
lines.push(`\nCustom event to trigger reward:`);
|
|
274
|
-
lines.push(` Event: app_login`);
|
|
275
|
-
lines.push(`\nExample event payload:`);
|
|
276
|
-
lines.push(`{`);
|
|
277
|
-
lines.push(` "event": "app_login"`);
|
|
278
|
-
lines.push(`}`);
|
|
279
|
-
lines.push(`\nStreak mechanics:`);
|
|
280
|
-
lines.push(`- Fan must log in at least once each day`);
|
|
281
|
-
lines.push(`- Missing a day resets the streak counter`);
|
|
282
|
-
lines.push(`- Achievement completes when ${cfg.streakDays} consecutive days are reached`);
|
|
283
|
-
if (result.errors.length > 0) {
|
|
284
|
-
lines.push(`\nWarnings/Errors:`);
|
|
285
|
-
for (const error of result.errors) {
|
|
286
|
-
lines.push(`- ${error}`);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
return lines.join("\n");
|
|
290
|
-
}
|
|
291
|
-
// ============================================================================
|
|
292
|
-
// Exports
|
|
293
|
-
// ============================================================================
|
|
294
|
-
export const appLoginStreakWorkflow = {
|
|
295
|
-
id: "app-login-streak",
|
|
296
|
-
name: "App Login Streak Campaign",
|
|
297
|
-
execute: executeAppLoginStreakWorkflow,
|
|
298
|
-
};
|
|
@@ -1,33 +0,0 @@
|
|
|
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
|
-
};
|