@fire-spark/cli 0.0.0-stage → 0.13.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/bin/firespark.mjs +14 -0
- package/package.json +39 -3
- package/src/commands/doctor/client/admin.flow.ts +553 -0
- package/src/commands/doctor/client/catalog.json +1586 -0
- package/src/commands/doctor/client/catalog.ts +179 -0
- package/src/commands/doctor/client/checkout.flow.ts +400 -0
- package/src/commands/doctor/client/command.ts +246 -0
- package/src/commands/doctor/client/config.ts +346 -0
- package/src/commands/doctor/client/content.flow.ts +450 -0
- package/src/commands/doctor/client/copy.ts +248 -0
- package/src/commands/doctor/client/flow.ts +182 -0
- package/src/commands/doctor/client/log.flow.ts +410 -0
- package/src/commands/doctor/client/manifests.ts +161 -0
- package/src/commands/doctor/client/men.flow.ts +412 -0
- package/src/commands/doctor/client/ops.flow.ts +312 -0
- package/src/commands/doctor/client/options.ts +201 -0
- package/src/commands/doctor/client/ord.flow.ts +445 -0
- package/src/commands/doctor/client/order.ts +261 -0
- package/src/commands/doctor/client/pay.flow.ts +772 -0
- package/src/commands/doctor/client/repo.scan.ts +142 -0
- package/src/commands/doctor/client/runner.ts +182 -0
- package/src/commands/doctor/client/sdk.flow.ts +630 -0
- package/src/commands/doctor/client/store.flow.ts +577 -0
- package/src/commands/doctor/client/yaml.ts +179 -0
- package/src/commands/doctor/live/http.ts +81 -0
- package/src/commands/doctor/live/live.session.ts +250 -0
- package/src/commands/doctor/live/order.ts +95 -0
- package/src/commands/doctor/live/realtime.ts +161 -0
- package/src/commands/doctor/live/result.ts +76 -0
- package/src/commands/index.ts +8 -0
- package/src/core/command.types.ts +25 -0
- package/src/core/help.ts +85 -0
- package/src/core/i18n.ts +49 -0
- package/src/core/options.ts +131 -0
- package/src/core/output.ts +47 -0
- package/src/core/redact.ts +71 -0
- package/src/index.ts +4 -0
- package/src/main.ts +113 -0
- package/README.md +0 -3
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { register } from "tsx/esm/api";
|
|
3
|
+
|
|
4
|
+
// Piping into `head` closes stdout early; that is not an error.
|
|
5
|
+
process.stdout.on("error", error => {
|
|
6
|
+
if (error.code === "EPIPE") process.exit(process.exitCode ?? 0);
|
|
7
|
+
throw error;
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
register();
|
|
11
|
+
|
|
12
|
+
const { run } = await import("../src/main.ts");
|
|
13
|
+
|
|
14
|
+
process.exitCode = await run(process.argv.slice(2));
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fire-spark/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.13.3",
|
|
4
|
+
"description": "Fire Spark command line: doctor and future commands",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"firespark": "./bin/firespark.mjs"
|
|
8
|
+
},
|
|
9
|
+
"main": "./src/index.ts",
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"src",
|
|
14
|
+
"!src/**/*.test.ts"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=22"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"big.js": "6.2.1",
|
|
21
|
+
"tsx": "^4.23.15"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/big.js": "6.2.2",
|
|
25
|
+
"vitest": "^2.1.8"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"exports": {
|
|
31
|
+
".": "./src/index.ts"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"start": "node ./bin/firespark.mjs",
|
|
35
|
+
"doctor": "node ./bin/firespark.mjs doctor",
|
|
36
|
+
"check-types": "tsc --noEmit",
|
|
37
|
+
"test:utils": "vitest run",
|
|
38
|
+
"lint": "oxlint ./src --max-warnings=0",
|
|
39
|
+
"format": "oxfmt ./src",
|
|
40
|
+
"check-format": "oxfmt --check ./src"
|
|
41
|
+
}
|
|
6
42
|
}
|
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import type { Copy } from "../../../core/i18n";
|
|
4
|
+
import { fromLive, isOutcome, isRecord, openLive, prepare } from "./flow";
|
|
5
|
+
import { isActive, queryOf, rowsOf, say, textOf, trpcCall } from "./flow";
|
|
6
|
+
import { withSafeFix } from "./flow";
|
|
7
|
+
import type { CheckBinding, Outcome, RunCtx } from "./runner";
|
|
8
|
+
import type { YamlChannel } from "./yaml";
|
|
9
|
+
|
|
10
|
+
const line = (es: string, en: string, pt: string): Copy => {
|
|
11
|
+
return { "es-EC": es, "en-US": en, "pt-BR": pt };
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const COPY = {
|
|
15
|
+
channel: line(
|
|
16
|
+
"{id} no aplica al canal {template}.",
|
|
17
|
+
"{id} does not apply to the {template} channel.",
|
|
18
|
+
"{id} não se aplica ao canal {template}."
|
|
19
|
+
),
|
|
20
|
+
need: line(
|
|
21
|
+
"{id} se omite: falta {field}.",
|
|
22
|
+
"{id} is skipped: {field} is missing.",
|
|
23
|
+
"{id} é omitido: falta {field}."
|
|
24
|
+
),
|
|
25
|
+
redeem: line(
|
|
26
|
+
"MKT-04 se omite: no hay un payment REDEMPTION activo.",
|
|
27
|
+
"MKT-04 is skipped: there is no active REDEMPTION payment.",
|
|
28
|
+
"MKT-04 é omitido: não há um payment REDEMPTION ativo."
|
|
29
|
+
),
|
|
30
|
+
analytics: line(
|
|
31
|
+
"MKT-05 se omite: preferences no trae analytics.",
|
|
32
|
+
"MKT-05 is skipped: preferences has no analytics.",
|
|
33
|
+
"MKT-05 é omitido: preferences não traz analytics."
|
|
34
|
+
),
|
|
35
|
+
named: line(
|
|
36
|
+
"{id} solo corre cuando lo nombras. No envié nada.",
|
|
37
|
+
"{id} runs only when you name it. Nothing was sent.",
|
|
38
|
+
"{id} só roda quando você o nomeia. Nada foi enviado."
|
|
39
|
+
),
|
|
40
|
+
masivo: line(
|
|
41
|
+
"MKT-03 se omite: Masivo no está habilitado.",
|
|
42
|
+
"MKT-03 is skipped: Masivo is not enabled.",
|
|
43
|
+
"MKT-03 é omitido: o Masivo não está habilitado."
|
|
44
|
+
),
|
|
45
|
+
receiver: line(
|
|
46
|
+
"ADM-02 el receiver no respondió.",
|
|
47
|
+
"ADM-02 the receiver did not respond.",
|
|
48
|
+
"ADM-02 o receiver não respondeu."
|
|
49
|
+
),
|
|
50
|
+
floor: line(
|
|
51
|
+
"MES-03 la respuesta de /pos/floor no es JSON.",
|
|
52
|
+
"MES-03 /pos/floor did not return JSON.",
|
|
53
|
+
"MES-03 /pos/floor não devolveu JSON."
|
|
54
|
+
)
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const LIVE = [
|
|
58
|
+
"api_url",
|
|
59
|
+
"channel_id",
|
|
60
|
+
"FIRESPARK_DOCTOR_CUSTOMER_EMAIL",
|
|
61
|
+
"FIRESPARK_DOCTOR_CUSTOMER_PASSWORD",
|
|
62
|
+
"FIRESPARK_DOCTOR_FIREBASE_API_KEY"
|
|
63
|
+
] as const;
|
|
64
|
+
|
|
65
|
+
const NAMES = ["menu_viewed", "product_viewed", "add_to_cart", "purchase"];
|
|
66
|
+
|
|
67
|
+
const applies = (ctx: RunCtx, id: string, allowed: ReadonlySet<string>) => {
|
|
68
|
+
const ready = prepare(ctx, ["channel_id"]);
|
|
69
|
+
if (isOutcome(ready)) return ready;
|
|
70
|
+
const template = textOf(ready.template);
|
|
71
|
+
if (allowed.has(template)) return null;
|
|
72
|
+
return say(ctx.locale, "skip", COPY.channel, { id, template });
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const need = (ctx: RunCtx, id: string, field: string) => {
|
|
76
|
+
return say(ctx.locale, "skip", COPY.need, { id, field });
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const onlyNamed = (ctx: RunCtx, id: string) => {
|
|
80
|
+
if (ctx.named.has(id)) return null;
|
|
81
|
+
return say(ctx.locale, "skip", COPY.named, { id });
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const methodOf = (row: Record<string, unknown>) => {
|
|
85
|
+
if (!isRecord(row.method)) return "";
|
|
86
|
+
return typeof row.method.method === "string" ? row.method.method : "";
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const preferencesOf = async (ctx: RunCtx, channel: YamlChannel) => {
|
|
90
|
+
const live = openLive(ctx, channel);
|
|
91
|
+
const codes = [200];
|
|
92
|
+
const args = ["preferences", "GET", "/preferences", codes] as const;
|
|
93
|
+
const step = await live.storefront<unknown>(...args);
|
|
94
|
+
if (!step.ok) return fromLive(ctx.locale, step.result);
|
|
95
|
+
return { live, value: step.value };
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const runMkt04 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
99
|
+
const skipped = applies(ctx, "MKT-04", new Set(["kiosk", "web", "app"]));
|
|
100
|
+
if (skipped) return skipped;
|
|
101
|
+
const ready = prepare(ctx, LIVE);
|
|
102
|
+
if (isOutcome(ready)) return ready;
|
|
103
|
+
const loaded = await preferencesOf(ctx, ready);
|
|
104
|
+
if (isOutcome(loaded)) return loaded;
|
|
105
|
+
const path = queryOf("/payments", { channel_id: textOf(ready.channel_id) });
|
|
106
|
+
const codes = [200];
|
|
107
|
+
const args = ["payments", "GET", path, codes] as const;
|
|
108
|
+
const step = await loaded.live.storefront<unknown>(...args);
|
|
109
|
+
if (!step.ok) return fromLive(ctx.locale, step.result);
|
|
110
|
+
const found = rowsOf(step.value).some(row => {
|
|
111
|
+
return isActive(row) && methodOf(row) === "REDEMPTION";
|
|
112
|
+
});
|
|
113
|
+
if (!found) return say(ctx.locale, "skip", COPY.redeem);
|
|
114
|
+
return need(ctx, "MKT-04", "orders.total");
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const runMkt05 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
118
|
+
const allowed = new Set([
|
|
119
|
+
"kiosk",
|
|
120
|
+
"web",
|
|
121
|
+
"app",
|
|
122
|
+
"chat",
|
|
123
|
+
"call-center",
|
|
124
|
+
"dashboard"
|
|
125
|
+
]);
|
|
126
|
+
const skipped = applies(ctx, "MKT-05", allowed);
|
|
127
|
+
if (skipped) return skipped;
|
|
128
|
+
const ready = prepare(ctx, LIVE);
|
|
129
|
+
if (isOutcome(ready)) return ready;
|
|
130
|
+
const loaded = await preferencesOf(ctx, ready);
|
|
131
|
+
if (isOutcome(loaded)) return loaded;
|
|
132
|
+
const analytics = isRecord(loaded.value) ? loaded.value.analytics : undefined;
|
|
133
|
+
if (!isRecord(analytics)) return say(ctx.locale, "skip", COPY.analytics);
|
|
134
|
+
const plans = analytics.plans ?? analytics.tracking_plans;
|
|
135
|
+
if (!Array.isArray(plans) || plans.length === 0) {
|
|
136
|
+
return need(ctx, "MKT-05", "trackingPlans");
|
|
137
|
+
}
|
|
138
|
+
const destinations = analytics.destinations;
|
|
139
|
+
if (!Array.isArray(destinations) || destinations.length === 0) {
|
|
140
|
+
return need(ctx, "MKT-05", "analytics.destinations");
|
|
141
|
+
}
|
|
142
|
+
return { status: "pass" };
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const eventOf = (channel: YamlChannel, name: string) => {
|
|
146
|
+
const id = randomUUID();
|
|
147
|
+
const session_id = randomUUID();
|
|
148
|
+
const head = { id, session_id, is_anonymous: false, name };
|
|
149
|
+
const when = { timestamp: new Date().toISOString(), platform: "web" };
|
|
150
|
+
const channel_id = textOf(channel.channel_id);
|
|
151
|
+
const store_id = textOf(channel.store_id);
|
|
152
|
+
const scope = { channel_id, store_id };
|
|
153
|
+
const properties = name === "menu_viewed" ? undefined : { product_id: "p1" };
|
|
154
|
+
if (name === "purchase") {
|
|
155
|
+
const order = { order_id: "o1" };
|
|
156
|
+
return { ...head, ...when, ...scope, properties: order };
|
|
157
|
+
}
|
|
158
|
+
if (name === "add_to_cart") {
|
|
159
|
+
const cart = { product_id: "p1", quantity: 1 };
|
|
160
|
+
return { ...head, ...when, ...scope, properties: cart };
|
|
161
|
+
}
|
|
162
|
+
if (!properties) return { ...head, ...when, ...scope };
|
|
163
|
+
return { ...head, ...when, ...scope, properties };
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const runMkt07 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
167
|
+
const allowed = new Set([
|
|
168
|
+
"kiosk",
|
|
169
|
+
"web",
|
|
170
|
+
"app",
|
|
171
|
+
"chat",
|
|
172
|
+
"call-center",
|
|
173
|
+
"dashboard"
|
|
174
|
+
]);
|
|
175
|
+
const skipped = applies(ctx, "MKT-07", allowed);
|
|
176
|
+
if (skipped) return skipped;
|
|
177
|
+
const named = onlyNamed(ctx, "MKT-07");
|
|
178
|
+
if (named) return named;
|
|
179
|
+
const ready = prepare(ctx, LIVE);
|
|
180
|
+
if (isOutcome(ready)) return ready;
|
|
181
|
+
const loaded = await preferencesOf(ctx, ready);
|
|
182
|
+
if (isOutcome(loaded)) return loaded;
|
|
183
|
+
const events = NAMES.map(name => eventOf(ready, name));
|
|
184
|
+
const codes = [200, 201, 202];
|
|
185
|
+
const body = { events };
|
|
186
|
+
const args = ["analytics", "POST", "/analytics/events", codes, body] as const;
|
|
187
|
+
const step = await loaded.live.storefront<unknown>(...args);
|
|
188
|
+
if (!step.ok) return fromLive(ctx.locale, step.result);
|
|
189
|
+
return { status: "pass" };
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const runMkt03 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
193
|
+
const skipped = applies(ctx, "MKT-03", new Set(["dashboard"]));
|
|
194
|
+
if (skipped) return skipped;
|
|
195
|
+
const ready = prepare(ctx, LIVE);
|
|
196
|
+
if (isOutcome(ready)) return ready;
|
|
197
|
+
const loaded = await preferencesOf(ctx, ready);
|
|
198
|
+
if (isOutcome(loaded)) return loaded;
|
|
199
|
+
const masivo = isRecord(loaded.value) ? loaded.value.masivo : null;
|
|
200
|
+
const coupons = isRecord(masivo) && masivo.coupons_enabled;
|
|
201
|
+
const loyalty = isRecord(masivo) && masivo.loyalty_enabled;
|
|
202
|
+
const enabled = coupons || loyalty;
|
|
203
|
+
if (!enabled) return say(ctx.locale, "skip", COPY.masivo);
|
|
204
|
+
const path = queryOf("/payments", { channel_id: textOf(ready.channel_id) });
|
|
205
|
+
const codes = [200];
|
|
206
|
+
const args = ["redemptions", "GET", path, codes] as const;
|
|
207
|
+
const step = await loaded.live.storefront<unknown>(...args);
|
|
208
|
+
if (!step.ok) return fromLive(ctx.locale, step.result);
|
|
209
|
+
const reflected = rowsOf(step.value).some(row => {
|
|
210
|
+
return methodOf(row) === "REDEMPTION" && typeof row.id === "string";
|
|
211
|
+
});
|
|
212
|
+
if (!reflected) return need(ctx, "MKT-03", "masivo.redemption");
|
|
213
|
+
return { status: "pass" };
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const runAdm06 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
217
|
+
const allowed = new Set([
|
|
218
|
+
"kiosk",
|
|
219
|
+
"pos",
|
|
220
|
+
"web",
|
|
221
|
+
"app",
|
|
222
|
+
"chat",
|
|
223
|
+
"call-center",
|
|
224
|
+
"dashboard"
|
|
225
|
+
]);
|
|
226
|
+
const skipped = applies(ctx, "ADM-06", allowed);
|
|
227
|
+
if (skipped) return skipped;
|
|
228
|
+
const named = onlyNamed(ctx, "ADM-06");
|
|
229
|
+
if (named) return named;
|
|
230
|
+
const ready = prepare(ctx, LIVE);
|
|
231
|
+
if (isOutcome(ready)) return ready;
|
|
232
|
+
const loaded = await preferencesOf(ctx, ready);
|
|
233
|
+
if (isOutcome(loaded)) return loaded;
|
|
234
|
+
const entry = {
|
|
235
|
+
level: "info",
|
|
236
|
+
message: "firespark doctor",
|
|
237
|
+
channel_id: textOf(ready.channel_id)
|
|
238
|
+
};
|
|
239
|
+
const codes = [200, 201, 202];
|
|
240
|
+
const body = { logs: [entry] };
|
|
241
|
+
const args = ["logs", "POST", "/logs", codes, body] as const;
|
|
242
|
+
const step = await loaded.live.storefront<unknown>(...args);
|
|
243
|
+
if (!step.ok) return fromLive(ctx.locale, step.result);
|
|
244
|
+
return { status: "pass" };
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const customerOf = async (ctx: RunCtx, channel: YamlChannel, id: string) => {
|
|
248
|
+
const live = openLive(ctx, channel);
|
|
249
|
+
const codes = [200];
|
|
250
|
+
const args = ["customer", "GET", "/customers/me", codes] as const;
|
|
251
|
+
const step = await live.storefront<unknown>(...args);
|
|
252
|
+
if (!step.ok) return fromLive(ctx.locale, step.result);
|
|
253
|
+
if (!isRecord(step.value)) return need(ctx, id, "customers.me");
|
|
254
|
+
return step.value;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const runMkt01 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
258
|
+
const channels = new Set(["kiosk", "pos", "dashboard"]);
|
|
259
|
+
const skipped = applies(ctx, "MKT-01", channels);
|
|
260
|
+
if (skipped) return skipped;
|
|
261
|
+
const ready = prepare(ctx, LIVE);
|
|
262
|
+
if (isOutcome(ready)) return ready;
|
|
263
|
+
const live = openLive(ctx, ready);
|
|
264
|
+
const path = queryOf("/menus", { channel_id: textOf(ready.channel_id) });
|
|
265
|
+
const codes = [200];
|
|
266
|
+
const args = ["menus", "GET", path, codes] as const;
|
|
267
|
+
const step = await live.storefront<unknown>(...args);
|
|
268
|
+
if (!step.ok) return fromLive(ctx.locale, step.result);
|
|
269
|
+
const rows = rowsOf(step.value);
|
|
270
|
+
const types = new Set<string>();
|
|
271
|
+
for (const row of rows) {
|
|
272
|
+
const products = Array.isArray(row.products) ? row.products : [];
|
|
273
|
+
for (const product of products.filter(isRecord)) {
|
|
274
|
+
const recs = product.recommendations;
|
|
275
|
+
if (!Array.isArray(recs)) continue;
|
|
276
|
+
for (const rec of recs) {
|
|
277
|
+
if (isRecord(rec) && typeof rec.type === "string") types.add(rec.type);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (types.size === 0) return need(ctx, "MKT-01", "recommendations");
|
|
282
|
+
const known = ["UPSELL", "CROSS_SELL", "DOWNSELL"];
|
|
283
|
+
if (!known.some(type => types.has(type))) {
|
|
284
|
+
return need(ctx, "MKT-01", "recommendations.type");
|
|
285
|
+
}
|
|
286
|
+
return { status: "pass" };
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const runMkt02 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
290
|
+
const skipped = applies(ctx, "MKT-02", new Set(["dashboard"]));
|
|
291
|
+
if (skipped) return skipped;
|
|
292
|
+
const ready = prepare(ctx, LIVE);
|
|
293
|
+
if (isOutcome(ready)) return ready;
|
|
294
|
+
const live = openLive(ctx, ready);
|
|
295
|
+
const codes = [200];
|
|
296
|
+
const args = ["preferences", "GET", "/preferences", codes] as const;
|
|
297
|
+
const step = await live.storefront<unknown>(...args);
|
|
298
|
+
if (!step.ok) return fromLive(ctx.locale, step.result);
|
|
299
|
+
const masivo = isRecord(step.value) ? step.value.masivo : undefined;
|
|
300
|
+
const coupons = isRecord(masivo) && masivo.coupons_enabled === true;
|
|
301
|
+
const loyalty = isRecord(masivo) && masivo.loyalty_enabled === true;
|
|
302
|
+
if (!coupons && !loyalty)
|
|
303
|
+
return need(ctx, "MKT-02", "masivo.coupons_enabled");
|
|
304
|
+
const cookie = textOf(ctx.env.FIRESPARK_DOCTOR_DASHBOARD_COOKIE);
|
|
305
|
+
if (!cookie) return need(ctx, "MKT-02", "FIRESPARK_DOCTOR_DASHBOARD_COOKIE");
|
|
306
|
+
const dash = textOf(ready.dashboard_url);
|
|
307
|
+
if (!dash) return need(ctx, "MKT-02", "dashboard_url");
|
|
308
|
+
const root = dash.replace(/\/$/, "");
|
|
309
|
+
const products = coupons ? ["coupons"] : [];
|
|
310
|
+
if (loyalty) products.push("loyalty");
|
|
311
|
+
for (const product of products) {
|
|
312
|
+
const url = `${root}/masivo?product=${product}`;
|
|
313
|
+
try {
|
|
314
|
+
const page = await fetch(url, { headers: { cookie } });
|
|
315
|
+
if (page.status < 200 || page.status >= 300) {
|
|
316
|
+
return need(ctx, "MKT-02", `masivo.product=${product}`);
|
|
317
|
+
}
|
|
318
|
+
} catch {
|
|
319
|
+
return need(ctx, "MKT-02", `masivo.product=${product}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return { status: "pass" };
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const runMkt08 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
326
|
+
const allowed = new Set(["kiosk", "web", "app", "chat", "call-center"]);
|
|
327
|
+
const skipped = applies(ctx, "MKT-08", allowed);
|
|
328
|
+
if (skipped) return skipped;
|
|
329
|
+
const ready = prepare(ctx, LIVE);
|
|
330
|
+
if (isOutcome(ready)) return ready;
|
|
331
|
+
const customer = await customerOf(ctx, ready, "MKT-08");
|
|
332
|
+
if (isOutcome(customer)) return customer;
|
|
333
|
+
const consent = isRecord(customer.consent) ? customer.consent : undefined;
|
|
334
|
+
const notes = customer.notification_preferences;
|
|
335
|
+
if (!isRecord(consent) || typeof consent.marketing !== "boolean") {
|
|
336
|
+
return need(ctx, "MKT-08", "customers.consent.marketing");
|
|
337
|
+
}
|
|
338
|
+
if (!isRecord(notes) || typeof notes.email !== "boolean") {
|
|
339
|
+
return need(ctx, "MKT-08", "notification_preferences.email");
|
|
340
|
+
}
|
|
341
|
+
if (typeof notes.push !== "boolean") {
|
|
342
|
+
return need(ctx, "MKT-08", "notification_preferences.push");
|
|
343
|
+
}
|
|
344
|
+
return { status: "pass" };
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const dashboardRead = async (ctx: RunCtx, id: string, procedure: string) => {
|
|
348
|
+
const ready = prepare(ctx, LIVE);
|
|
349
|
+
if (isOutcome(ready)) return ready;
|
|
350
|
+
const cookie = textOf(ctx.env.FIRESPARK_DOCTOR_DASHBOARD_COOKIE);
|
|
351
|
+
if (!cookie) return need(ctx, id, "FIRESPARK_DOCTOR_DASHBOARD_COOKIE");
|
|
352
|
+
const dash = textOf(ready.dashboard_url);
|
|
353
|
+
if (!dash) return need(ctx, id, "dashboard_url");
|
|
354
|
+
const input = {
|
|
355
|
+
merchant_id: textOf(ready.merchant_id),
|
|
356
|
+
env: textOf(ready.env)
|
|
357
|
+
};
|
|
358
|
+
const step = await trpcCall(dash, cookie, procedure, input);
|
|
359
|
+
if (step.status < 200 || step.status >= 300) {
|
|
360
|
+
return say(ctx.locale, "fail", COPY.receiver);
|
|
361
|
+
}
|
|
362
|
+
return { status: "pass" as const };
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const runAdm01 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
366
|
+
const skipped = applies(ctx, "ADM-01", new Set(["dashboard"]));
|
|
367
|
+
if (skipped) return skipped;
|
|
368
|
+
const procedures = [
|
|
369
|
+
"merchants.selectByRange",
|
|
370
|
+
"organizations.selectByRange",
|
|
371
|
+
"environments.selectByRange",
|
|
372
|
+
"stores.selectByRange",
|
|
373
|
+
"brands.selectByRange",
|
|
374
|
+
"channels.selectByRange",
|
|
375
|
+
"entityLabels.selectByRange"
|
|
376
|
+
];
|
|
377
|
+
for (const procedure of procedures) {
|
|
378
|
+
const step = await dashboardRead(ctx, "ADM-01", procedure);
|
|
379
|
+
if (step.status !== "pass") return step;
|
|
380
|
+
}
|
|
381
|
+
return { status: "pass" };
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const runAdm02 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
385
|
+
const skipped = applies(ctx, "ADM-02", new Set(["server", "dashboard"]));
|
|
386
|
+
if (skipped) return skipped;
|
|
387
|
+
const ready = prepare(ctx, ["receiver"]);
|
|
388
|
+
if (isOutcome(ready)) return ready;
|
|
389
|
+
const body = JSON.stringify({ event: "order.fulfilled" });
|
|
390
|
+
const headers: Record<string, string> = {
|
|
391
|
+
"content-type": "application/json"
|
|
392
|
+
};
|
|
393
|
+
const secret = textOf(ctx.env.FIRESPARK_DOCTOR_WEBHOOK_SECRET);
|
|
394
|
+
if (secret) headers["x-firespark-signature"] = secret;
|
|
395
|
+
try {
|
|
396
|
+
const signal = AbortSignal.timeout(15000);
|
|
397
|
+
const url = textOf(ready.receiver);
|
|
398
|
+
const init = { method: "POST", headers, body, signal };
|
|
399
|
+
const page = await fetch(url, init);
|
|
400
|
+
if (page.status < 200 || page.status >= 300) {
|
|
401
|
+
return say(ctx.locale, "fail", COPY.receiver);
|
|
402
|
+
}
|
|
403
|
+
return { status: "pass" };
|
|
404
|
+
} catch {
|
|
405
|
+
return say(ctx.locale, "fail", COPY.receiver);
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const runAdm03 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
410
|
+
const skipped = applies(ctx, "ADM-03", new Set(["dashboard"]));
|
|
411
|
+
if (skipped) return skipped;
|
|
412
|
+
const procedures = [
|
|
413
|
+
"orderMigrations.selectByRange",
|
|
414
|
+
"customerMigrations.selectByRange",
|
|
415
|
+
"exports.selectByRange",
|
|
416
|
+
"auditLogs.selectByRange"
|
|
417
|
+
];
|
|
418
|
+
for (const procedure of procedures) {
|
|
419
|
+
const step = await dashboardRead(ctx, "ADM-03", procedure);
|
|
420
|
+
if (step.status !== "pass") return step;
|
|
421
|
+
}
|
|
422
|
+
return { status: "pass" };
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const runAdm04 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
426
|
+
const skipped = applies(ctx, "ADM-04", new Set(["dashboard"]));
|
|
427
|
+
if (skipped) return skipped;
|
|
428
|
+
const named = onlyNamed(ctx, "ADM-04");
|
|
429
|
+
if (named) return named;
|
|
430
|
+
const ready = prepare(ctx, LIVE);
|
|
431
|
+
if (isOutcome(ready)) return ready;
|
|
432
|
+
const cookie = textOf(ctx.env.FIRESPARK_DOCTOR_DASHBOARD_COOKIE);
|
|
433
|
+
if (!cookie) return need(ctx, "ADM-04", "FIRESPARK_DOCTOR_DASHBOARD_COOKIE");
|
|
434
|
+
const template = textOf(ready.template);
|
|
435
|
+
if (!template) return need(ctx, "ADM-04", "template");
|
|
436
|
+
const dash = textOf(ready.dashboard_url);
|
|
437
|
+
if (!dash) return need(ctx, "ADM-04", "dashboard_url");
|
|
438
|
+
const input = {
|
|
439
|
+
merchant_id: textOf(ready.merchant_id),
|
|
440
|
+
env: textOf(ready.env),
|
|
441
|
+
template
|
|
442
|
+
};
|
|
443
|
+
const step = await trpcCall(dash, cookie, "builder.insert", input);
|
|
444
|
+
if (step.status < 200 || step.status >= 300) {
|
|
445
|
+
return say(ctx.locale, "fail", COPY.receiver);
|
|
446
|
+
}
|
|
447
|
+
return { status: "pass" };
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const runAdm05 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
451
|
+
const allowed = new Set([
|
|
452
|
+
"kiosk",
|
|
453
|
+
"web",
|
|
454
|
+
"app",
|
|
455
|
+
"chat",
|
|
456
|
+
"call-center",
|
|
457
|
+
"dashboard"
|
|
458
|
+
]);
|
|
459
|
+
const skipped = applies(ctx, "ADM-05", allowed);
|
|
460
|
+
if (skipped) return skipped;
|
|
461
|
+
const ready = prepare(ctx, LIVE);
|
|
462
|
+
if (isOutcome(ready)) return ready;
|
|
463
|
+
const customer = await customerOf(ctx, ready, "ADM-05");
|
|
464
|
+
if (isOutcome(customer)) return customer;
|
|
465
|
+
const email = typeof customer.email === "string" ? customer.email : "";
|
|
466
|
+
if (!email.includes("@")) return need(ctx, "ADM-05", "customers.email");
|
|
467
|
+
const cookie = textOf(ctx.env.FIRESPARK_DOCTOR_DASHBOARD_COOKIE);
|
|
468
|
+
if (!cookie) return need(ctx, "ADM-05", "FIRESPARK_DOCTOR_DASHBOARD_COOKIE");
|
|
469
|
+
const dash = textOf(ready.dashboard_url);
|
|
470
|
+
if (!dash) return need(ctx, "ADM-05", "dashboard_url");
|
|
471
|
+
const input = {
|
|
472
|
+
merchant_id: textOf(ready.merchant_id),
|
|
473
|
+
env: textOf(ready.env),
|
|
474
|
+
q: email
|
|
475
|
+
};
|
|
476
|
+
const step = await trpcCall(dash, cookie, "customers.selectByRange", input);
|
|
477
|
+
if (step.status < 200 || step.status >= 300) {
|
|
478
|
+
return say(ctx.locale, "fail", COPY.receiver);
|
|
479
|
+
}
|
|
480
|
+
if (!step.text.includes(email)) return need(ctx, "ADM-05", "customers.email");
|
|
481
|
+
return { status: "pass" };
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
const floorOf = async (ctx: RunCtx, id: string) => {
|
|
485
|
+
const ready = prepare(ctx, LIVE);
|
|
486
|
+
if (isOutcome(ready)) return ready;
|
|
487
|
+
const uid = textOf(ctx.env.FIRESPARK_DOCTOR_DEVICE_UID);
|
|
488
|
+
const secret = textOf(ctx.env.FIRESPARK_DOCTOR_DEVICE_SECRET);
|
|
489
|
+
if (!uid) return need(ctx, id, "FIRESPARK_DOCTOR_DEVICE_UID");
|
|
490
|
+
if (!secret) return need(ctx, id, "FIRESPARK_DOCTOR_DEVICE_SECRET");
|
|
491
|
+
const live = openLive(ctx, ready);
|
|
492
|
+
const body = { device_uid: uid, device_secret: secret };
|
|
493
|
+
const codes = [200];
|
|
494
|
+
const args = ["floor", "POST", "/pos/floor", codes, body] as const;
|
|
495
|
+
const step = await live.probe(...args);
|
|
496
|
+
if (!step.ok) return fromLive(ctx.locale, step.result);
|
|
497
|
+
return step.value.text;
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const runMes01 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
501
|
+
const skipped = applies(ctx, "MES-01", new Set(["pos", "dashboard"]));
|
|
502
|
+
if (skipped) return skipped;
|
|
503
|
+
const body = await floorOf(ctx, "MES-01");
|
|
504
|
+
if (isOutcome(body)) return body;
|
|
505
|
+
return { status: "pass" };
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
const runMes02 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
509
|
+
const skipped = applies(ctx, "MES-02", new Set(["pos"]));
|
|
510
|
+
if (skipped) return skipped;
|
|
511
|
+
const body = await floorOf(ctx, "MES-02");
|
|
512
|
+
if (isOutcome(body)) return body;
|
|
513
|
+
return { status: "pass" };
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const runMes03 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
517
|
+
const skipped = applies(ctx, "MES-03", new Set(["pos", "dashboard"]));
|
|
518
|
+
if (skipped) return skipped;
|
|
519
|
+
const body = await floorOf(ctx, "MES-03");
|
|
520
|
+
if (isOutcome(body)) return body;
|
|
521
|
+
let parsed: unknown;
|
|
522
|
+
try {
|
|
523
|
+
parsed = JSON.parse(body);
|
|
524
|
+
} catch {
|
|
525
|
+
return say(ctx.locale, "fail", COPY.floor);
|
|
526
|
+
}
|
|
527
|
+
const data = isRecord(parsed) && isRecord(parsed.data) ? parsed.data : parsed;
|
|
528
|
+
const occupancy = isRecord(data) ? data.occupancy : undefined;
|
|
529
|
+
const rows = isRecord(occupancy) ? occupancy.reservations : undefined;
|
|
530
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
531
|
+
return need(ctx, "MES-03", "occupancy.reservations");
|
|
532
|
+
}
|
|
533
|
+
return { status: "pass" };
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
export const ADMIN_BINDINGS: Record<string, CheckBinding> = {
|
|
537
|
+
"MKT-01": withSafeFix(runMkt01),
|
|
538
|
+
"MKT-02": withSafeFix(runMkt02),
|
|
539
|
+
"MKT-03": withSafeFix(runMkt03),
|
|
540
|
+
"MKT-04": withSafeFix(runMkt04),
|
|
541
|
+
"MKT-05": withSafeFix(runMkt05),
|
|
542
|
+
"MKT-07": withSafeFix(runMkt07),
|
|
543
|
+
"MKT-08": withSafeFix(runMkt08),
|
|
544
|
+
"ADM-01": withSafeFix(runAdm01),
|
|
545
|
+
"ADM-02": withSafeFix(runAdm02),
|
|
546
|
+
"ADM-03": withSafeFix(runAdm03),
|
|
547
|
+
"ADM-04": withSafeFix(runAdm04),
|
|
548
|
+
"ADM-05": withSafeFix(runAdm05),
|
|
549
|
+
"ADM-06": withSafeFix(runAdm06),
|
|
550
|
+
"MES-01": withSafeFix(runMes01),
|
|
551
|
+
"MES-02": withSafeFix(runMes02),
|
|
552
|
+
"MES-03": withSafeFix(runMes03)
|
|
553
|
+
};
|