@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,577 @@
|
|
|
1
|
+
import { Big } from "big.js";
|
|
2
|
+
|
|
3
|
+
import type { Copy } from "../../../core/i18n";
|
|
4
|
+
import { fromLive, isOutcome, isRecord, openLive, prepare } from "./flow";
|
|
5
|
+
import { say, textOf, trpcCall, withSafeFix } from "./flow";
|
|
6
|
+
import { readSources } from "./repo.scan";
|
|
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
|
+
hours: line(
|
|
21
|
+
"AVA-01 se omite: preferences no trae availability.",
|
|
22
|
+
"AVA-01 is skipped: preferences has no availability.",
|
|
23
|
+
"AVA-01 é omitido: preferences não traz availability."
|
|
24
|
+
),
|
|
25
|
+
need: line(
|
|
26
|
+
"{id} se omite: falta {field}.",
|
|
27
|
+
"{id} is skipped: {field} is missing.",
|
|
28
|
+
"{id} é omitido: falta {field}."
|
|
29
|
+
),
|
|
30
|
+
checkout: line(
|
|
31
|
+
"AVA-03 se omite: preferences no trae shipping ni billing.",
|
|
32
|
+
"AVA-03 is skipped: preferences has no shipping or billing.",
|
|
33
|
+
"AVA-03 é omitido: preferences não traz shipping nem billing."
|
|
34
|
+
),
|
|
35
|
+
fees: line(
|
|
36
|
+
"AVA-04 se omite: no hay reglas de fee para calcular.",
|
|
37
|
+
"AVA-04 is skipped: there are no fee rules to calculate.",
|
|
38
|
+
"AVA-04 é omitido: não há regras de fee para calcular."
|
|
39
|
+
),
|
|
40
|
+
closed: line(
|
|
41
|
+
"AVA-05 se omite: la regla de no disponible no está activa.",
|
|
42
|
+
"AVA-05 is skipped: the unavailable rule is not active.",
|
|
43
|
+
"AVA-05 é omitido: a regra de indisponível não está ativa."
|
|
44
|
+
),
|
|
45
|
+
version: line(
|
|
46
|
+
"AVA-06 se omite: este canal no publicó min_version.",
|
|
47
|
+
"AVA-06 is skipped: this channel did not publish min_version.",
|
|
48
|
+
"AVA-06 é omitido: este canal não publicou min_version."
|
|
49
|
+
),
|
|
50
|
+
outside: line(
|
|
51
|
+
"AVA-02 el punto está fuera de las coverage zones.",
|
|
52
|
+
"AVA-02 the point is outside the coverage zones.",
|
|
53
|
+
"AVA-02 o ponto está fora das coverage zones."
|
|
54
|
+
),
|
|
55
|
+
drift: line(
|
|
56
|
+
"{id} el menú cambió entre dos lecturas.",
|
|
57
|
+
"{id} the menu changed between two reads.",
|
|
58
|
+
"{id} o cardápio mudou entre duas leituras."
|
|
59
|
+
),
|
|
60
|
+
command: line(
|
|
61
|
+
"{id} el comando {type} respondió {status}.",
|
|
62
|
+
"{id} command {type} responded {status}.",
|
|
63
|
+
"{id} o comando {type} respondeu {status}."
|
|
64
|
+
),
|
|
65
|
+
named: line(
|
|
66
|
+
"{id} solo corre cuando lo nombras. No mandé el comando.",
|
|
67
|
+
"{id} runs only when you name it. The command was not sent.",
|
|
68
|
+
"{id} só roda quando você o nomeia. O comando não foi enviado."
|
|
69
|
+
),
|
|
70
|
+
fee: line(
|
|
71
|
+
"AVA-04 el fee calculado {expected} no coincide con el validate {actual}.",
|
|
72
|
+
"AVA-04 the computed fee {expected} does not match validate {actual}.",
|
|
73
|
+
"AVA-04 a taxa calculada {expected} não coincide com o validate {actual}."
|
|
74
|
+
),
|
|
75
|
+
gate: line(
|
|
76
|
+
"AVA-05 la regla activa no trae título y descripción.",
|
|
77
|
+
"AVA-05 the active rule is missing a title and description.",
|
|
78
|
+
"AVA-05 a regra ativa não traz título e descrição."
|
|
79
|
+
),
|
|
80
|
+
reset: line(
|
|
81
|
+
"RT-03 se omite: falta resetKioskCache en el repo del kiosk.",
|
|
82
|
+
"RT-03 is skipped: resetKioskCache is missing from the kiosk repo.",
|
|
83
|
+
"RT-03 é omitido: falta resetKioskCache no repo do kiosk."
|
|
84
|
+
),
|
|
85
|
+
checklist: line(
|
|
86
|
+
"{id} el checklist no está completo: falta {field}.",
|
|
87
|
+
"{id} the checklist is incomplete: {field} is missing.",
|
|
88
|
+
"{id} o checklist não está completo: falta {field}."
|
|
89
|
+
)
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const LIVE = [
|
|
93
|
+
"api_url",
|
|
94
|
+
"channel_id",
|
|
95
|
+
"FIRESPARK_DOCTOR_CUSTOMER_EMAIL",
|
|
96
|
+
"FIRESPARK_DOCTOR_CUSTOMER_PASSWORD",
|
|
97
|
+
"FIRESPARK_DOCTOR_FIREBASE_API_KEY"
|
|
98
|
+
] as const;
|
|
99
|
+
|
|
100
|
+
const BASE = "10.00";
|
|
101
|
+
|
|
102
|
+
const applies = (ctx: RunCtx, id: string, allowed: ReadonlySet<string>) => {
|
|
103
|
+
const ready = prepare(ctx, ["channel_id"]);
|
|
104
|
+
if (isOutcome(ready)) return ready;
|
|
105
|
+
const template = textOf(ready.template);
|
|
106
|
+
if (allowed.has(template)) return null;
|
|
107
|
+
return say(ctx.locale, "skip", COPY.channel, { id, template });
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const need = (ctx: RunCtx, id: string, field: string) => {
|
|
111
|
+
return say(ctx.locale, "skip", COPY.need, { id, field });
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const finite = (value: unknown) => {
|
|
115
|
+
const number = Number(value);
|
|
116
|
+
return Number.isFinite(number) ? number : null;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const haversine = (lat1: number, lng1: number, lat2: number, lng2: number) => {
|
|
120
|
+
const radius = 6371000;
|
|
121
|
+
const p1 = (lat1 * Math.PI) / 180;
|
|
122
|
+
const p2 = (lat2 * Math.PI) / 180;
|
|
123
|
+
const dLat = ((lat2 - lat1) * Math.PI) / 180;
|
|
124
|
+
const dLng = ((lng2 - lng1) * Math.PI) / 180;
|
|
125
|
+
const a = Math.sin(dLat / 2) ** 2;
|
|
126
|
+
const b = Math.cos(p1) * Math.cos(p2) * Math.sin(dLng / 2) ** 2;
|
|
127
|
+
return 2 * radius * Math.asin(Math.sqrt(a + b));
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const insidePolygon = (
|
|
131
|
+
lat: number,
|
|
132
|
+
lng: number,
|
|
133
|
+
points: { lat: number; lng: number }[]
|
|
134
|
+
) => {
|
|
135
|
+
let inside = false;
|
|
136
|
+
for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
|
|
137
|
+
const yi = points[i]?.lat ?? 0;
|
|
138
|
+
const xi = points[i]?.lng ?? 0;
|
|
139
|
+
const yj = points[j]?.lat ?? 0;
|
|
140
|
+
const xj = points[j]?.lng ?? 0;
|
|
141
|
+
const crosses = yi > lat !== yj > lat;
|
|
142
|
+
const edge = ((xj - xi) * (lat - yi)) / (yj - yi) + xi;
|
|
143
|
+
if (crosses && lng < edge) inside = !inside;
|
|
144
|
+
}
|
|
145
|
+
return inside;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const zoneContains = (
|
|
149
|
+
lat: number,
|
|
150
|
+
lng: number,
|
|
151
|
+
zone: Record<string, unknown>
|
|
152
|
+
) => {
|
|
153
|
+
if (zone.type === "RADIUS" && isRecord(zone.radius_info)) {
|
|
154
|
+
const zlat = finite(zone.radius_info.latitude);
|
|
155
|
+
const zlng = finite(zone.radius_info.longitude);
|
|
156
|
+
const meters = finite(zone.radius_info.radius);
|
|
157
|
+
if (zlat === null || zlng === null || meters === null) return null;
|
|
158
|
+
return haversine(lat, lng, zlat, zlng) <= meters;
|
|
159
|
+
}
|
|
160
|
+
if (zone.type !== "POLYGON" || !isRecord(zone.polygon_info)) return null;
|
|
161
|
+
const polygon = zone.polygon_info.polygon;
|
|
162
|
+
if (!Array.isArray(polygon) || polygon.length < 3) return null;
|
|
163
|
+
const points: { lat: number; lng: number }[] = [];
|
|
164
|
+
for (const point of polygon) {
|
|
165
|
+
if (!isRecord(point)) return null;
|
|
166
|
+
const plat = finite(point.latitude);
|
|
167
|
+
const plng = finite(point.longitude);
|
|
168
|
+
if (plat === null || plng === null) return null;
|
|
169
|
+
points.push({ lat: plat, lng: plng });
|
|
170
|
+
}
|
|
171
|
+
return insidePolygon(lat, lng, points);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const preferencesOf = async (ctx: RunCtx, channel: YamlChannel) => {
|
|
175
|
+
const live = openLive(ctx, channel);
|
|
176
|
+
const codes = [200];
|
|
177
|
+
const args = ["preferences", "GET", "/preferences", codes] as const;
|
|
178
|
+
const step = await live.storefront<unknown>(...args);
|
|
179
|
+
if (!step.ok) return fromLive(ctx.locale, step.result);
|
|
180
|
+
return step.value;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const availabilityOf = (value: unknown) => {
|
|
184
|
+
if (!isRecord(value) || !isRecord(value.availability)) return null;
|
|
185
|
+
return value.availability;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const runAva01 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
189
|
+
const allowed = new Set([
|
|
190
|
+
"kiosk",
|
|
191
|
+
"web",
|
|
192
|
+
"app",
|
|
193
|
+
"chat",
|
|
194
|
+
"call-center",
|
|
195
|
+
"dashboard"
|
|
196
|
+
]);
|
|
197
|
+
const skipped = applies(ctx, "AVA-01", allowed);
|
|
198
|
+
if (skipped) return skipped;
|
|
199
|
+
if (!ctx.named.has("AVA-01")) return need(ctx, "AVA-01", "named");
|
|
200
|
+
const ready = prepare(ctx, LIVE);
|
|
201
|
+
if (isOutcome(ready)) return ready;
|
|
202
|
+
const prefs = await preferencesOf(ctx, ready);
|
|
203
|
+
if (isOutcome(prefs)) return prefs;
|
|
204
|
+
const availability = availabilityOf(prefs);
|
|
205
|
+
if (!availability) return need(ctx, "AVA-01", "preferences.availability");
|
|
206
|
+
const cookie = textOf(ctx.env.FIRESPARK_DOCTOR_DASHBOARD_COOKIE);
|
|
207
|
+
if (!cookie) return need(ctx, "AVA-01", "FIRESPARK_DOCTOR_DASHBOARD_COOKIE");
|
|
208
|
+
const dash = textOf(ready.dashboard_url);
|
|
209
|
+
if (!dash) return need(ctx, "AVA-01", "dashboard_url");
|
|
210
|
+
const merchant = textOf(ready.merchant_id);
|
|
211
|
+
if (!merchant) return need(ctx, "AVA-01", "merchant_id");
|
|
212
|
+
const env = textOf(ready.env);
|
|
213
|
+
if (!env) return need(ctx, "AVA-01", "env");
|
|
214
|
+
let current = "AVAILABLE";
|
|
215
|
+
if (availability.status === "UNAVAILABLE") current = "UNAVAILABLE";
|
|
216
|
+
const next = current === "AVAILABLE" ? "UNAVAILABLE" : "AVAILABLE";
|
|
217
|
+
const input = {
|
|
218
|
+
merchant_id: merchant,
|
|
219
|
+
env,
|
|
220
|
+
uid: textOf(ready.channel_id),
|
|
221
|
+
availability: { status: next }
|
|
222
|
+
};
|
|
223
|
+
const proc = "channels.updateAvailability";
|
|
224
|
+
const wrote = await trpcCall(dash, cookie, proc, input);
|
|
225
|
+
if (wrote.status < 200 || wrote.status >= 300) {
|
|
226
|
+
return say(ctx.locale, "fail", COPY.command, {
|
|
227
|
+
id: "AVA-01",
|
|
228
|
+
type: "availability.changed",
|
|
229
|
+
status: wrote.status
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
const again = await preferencesOf(ctx, ready);
|
|
233
|
+
if (isOutcome(again)) return again;
|
|
234
|
+
const seen = availabilityOf(again);
|
|
235
|
+
const restore = {
|
|
236
|
+
merchant_id: merchant,
|
|
237
|
+
env,
|
|
238
|
+
uid: textOf(ready.channel_id),
|
|
239
|
+
availability: { status: current }
|
|
240
|
+
};
|
|
241
|
+
await trpcCall(dash, cookie, "channels.updateAvailability", restore);
|
|
242
|
+
if (!seen || seen.status !== next) {
|
|
243
|
+
return say(ctx.locale, "fail", COPY.command, {
|
|
244
|
+
id: "AVA-01",
|
|
245
|
+
type: "availability.changed",
|
|
246
|
+
status: 200
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return { status: "pass" };
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const MODES = new Set(["required", "optional", "hidden"]);
|
|
253
|
+
|
|
254
|
+
const fieldModes = (value: unknown) => {
|
|
255
|
+
if (!isRecord(value) || !isRecord(value.fields)) return null;
|
|
256
|
+
const modes = Object.values(value.fields);
|
|
257
|
+
if (modes.length === 0) return null;
|
|
258
|
+
const ok = modes.every(mode => typeof mode === "string" && MODES.has(mode));
|
|
259
|
+
return ok ? modes : null;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const runAva03 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
263
|
+
const allowed = new Set(["kiosk", "web", "app", "chat", "call-center"]);
|
|
264
|
+
const skipped = applies(ctx, "AVA-03", allowed);
|
|
265
|
+
if (skipped) return skipped;
|
|
266
|
+
const ready = prepare(ctx, LIVE);
|
|
267
|
+
if (isOutcome(ready)) return ready;
|
|
268
|
+
const prefs = await preferencesOf(ctx, ready);
|
|
269
|
+
if (isOutcome(prefs)) return prefs;
|
|
270
|
+
if (!isRecord(prefs) || !isRecord(prefs.checkout)) {
|
|
271
|
+
return say(ctx.locale, "skip", COPY.checkout);
|
|
272
|
+
}
|
|
273
|
+
const shipping = fieldModes(prefs.checkout.shipping);
|
|
274
|
+
if (!shipping) return need(ctx, "AVA-03", "checkout.shipping.fields");
|
|
275
|
+
const billing = fieldModes(prefs.checkout.billing);
|
|
276
|
+
if (!billing) return need(ctx, "AVA-03", "checkout.billing.fields");
|
|
277
|
+
return { status: "pass" };
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const feeRows = (value: unknown) => {
|
|
281
|
+
if (!isRecord(value) || !Array.isArray(value.fees)) return [];
|
|
282
|
+
return value.fees.filter(isRecord);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const percentageFee = (row: Record<string, unknown>) => {
|
|
286
|
+
const based = row.based_on;
|
|
287
|
+
if (based !== "ORDER_TOTAL" && based !== "ORDER_SUBTOTAL") return "";
|
|
288
|
+
if (typeof row.percentage !== "number") return "";
|
|
289
|
+
if (!Number.isFinite(row.percentage)) return "";
|
|
290
|
+
const due = new Big(BASE).times(row.percentage).div(100);
|
|
291
|
+
return due.toFixed(2);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const feeAmount = (value: unknown) => {
|
|
295
|
+
if (!isRecord(value)) return "";
|
|
296
|
+
const fees = value.fees;
|
|
297
|
+
const row = Array.isArray(fees) ? fees.find(isRecord) : undefined;
|
|
298
|
+
if (!row) return "";
|
|
299
|
+
const amount = row.amount;
|
|
300
|
+
if (typeof amount === "number" && Number.isFinite(amount)) {
|
|
301
|
+
return new Big(String(amount)).toFixed(2);
|
|
302
|
+
}
|
|
303
|
+
if (typeof amount === "string" && amount.trim()) {
|
|
304
|
+
try {
|
|
305
|
+
return new Big(amount).toFixed(2);
|
|
306
|
+
} catch {
|
|
307
|
+
return "";
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return "";
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const runAva04 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
314
|
+
const allowed = new Set([
|
|
315
|
+
"kiosk",
|
|
316
|
+
"web",
|
|
317
|
+
"app",
|
|
318
|
+
"chat",
|
|
319
|
+
"call-center",
|
|
320
|
+
"dashboard"
|
|
321
|
+
]);
|
|
322
|
+
const skipped = applies(ctx, "AVA-04", allowed);
|
|
323
|
+
if (skipped) return skipped;
|
|
324
|
+
const ready = prepare(ctx, LIVE);
|
|
325
|
+
if (isOutcome(ready)) return ready;
|
|
326
|
+
const prefs = await preferencesOf(ctx, ready);
|
|
327
|
+
if (isOutcome(prefs)) return prefs;
|
|
328
|
+
const rows = feeRows(prefs);
|
|
329
|
+
const expected = rows.map(percentageFee).find(Boolean) ?? "";
|
|
330
|
+
const fixed = rows.find(row => {
|
|
331
|
+
return row.based_on === "FIXED" && typeof row.amount === "number";
|
|
332
|
+
});
|
|
333
|
+
const fixedDue = fixed ? new Big(String(fixed.amount)).toFixed(2) : "";
|
|
334
|
+
const due = expected || fixedDue;
|
|
335
|
+
if (!due) return need(ctx, "AVA-04", "fees.based_on");
|
|
336
|
+
const live = openLive(ctx, ready);
|
|
337
|
+
const body = { subtotal: BASE };
|
|
338
|
+
const codes = [200];
|
|
339
|
+
const args = ["fees", "POST", "/orders/validate", codes, body] as const;
|
|
340
|
+
const step = await live.storefront<unknown>(...args);
|
|
341
|
+
if (!step.ok) return fromLive(ctx.locale, step.result);
|
|
342
|
+
const actual = feeAmount(step.value);
|
|
343
|
+
if (!actual) return need(ctx, "AVA-04", "orders.validate.fees");
|
|
344
|
+
if (actual !== due) {
|
|
345
|
+
return say(ctx.locale, "fail", COPY.fee, { expected: due, actual });
|
|
346
|
+
}
|
|
347
|
+
return { status: "pass" };
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const runAva05 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
351
|
+
const allowed = new Set([
|
|
352
|
+
"kiosk",
|
|
353
|
+
"web",
|
|
354
|
+
"app",
|
|
355
|
+
"chat",
|
|
356
|
+
"call-center",
|
|
357
|
+
"dashboard"
|
|
358
|
+
]);
|
|
359
|
+
const skipped = applies(ctx, "AVA-05", allowed);
|
|
360
|
+
if (skipped) return skipped;
|
|
361
|
+
const ready = prepare(ctx, LIVE);
|
|
362
|
+
if (isOutcome(ready)) return ready;
|
|
363
|
+
const prefs = await preferencesOf(ctx, ready);
|
|
364
|
+
if (isOutcome(prefs)) return prefs;
|
|
365
|
+
const availability = availabilityOf(prefs);
|
|
366
|
+
if (!availability || availability.status !== "UNAVAILABLE") {
|
|
367
|
+
return say(ctx.locale, "skip", COPY.closed);
|
|
368
|
+
}
|
|
369
|
+
const rule = availability.rule;
|
|
370
|
+
if (!isRecord(rule)) return say(ctx.locale, "fail", COPY.gate);
|
|
371
|
+
const title = typeof rule.title === "string" ? rule.title.trim() : "";
|
|
372
|
+
const body = rule.description ?? rule.body;
|
|
373
|
+
const text = typeof body === "string" ? body.trim() : "";
|
|
374
|
+
if (!title || !text) return say(ctx.locale, "fail", COPY.gate);
|
|
375
|
+
return { status: "pass" };
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const runAva06 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
379
|
+
const allowed = new Set(["kiosk", "app", "dashboard"]);
|
|
380
|
+
const skipped = applies(ctx, "AVA-06", allowed);
|
|
381
|
+
if (skipped) return skipped;
|
|
382
|
+
const ready = prepare(ctx, LIVE);
|
|
383
|
+
if (isOutcome(ready)) return ready;
|
|
384
|
+
const prefs = await preferencesOf(ctx, ready);
|
|
385
|
+
if (isOutcome(prefs)) return prefs;
|
|
386
|
+
if (!isRecord(prefs) || !isRecord(prefs.release)) {
|
|
387
|
+
return need(ctx, "AVA-06", "release.min_version");
|
|
388
|
+
}
|
|
389
|
+
const min = prefs.release.min_version;
|
|
390
|
+
if (typeof min !== "string" || !min.trim()) {
|
|
391
|
+
return need(ctx, "AVA-06", "release.min_version");
|
|
392
|
+
}
|
|
393
|
+
const installed = textOf(ready.template_version);
|
|
394
|
+
if (!installed) return need(ctx, "AVA-06", "template_version");
|
|
395
|
+
if (min <= installed) return { status: "pass" };
|
|
396
|
+
return need(ctx, "AVA-06", "update gate");
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const storeOf = async (ctx: RunCtx, channel: YamlChannel) => {
|
|
400
|
+
const live = openLive(ctx, channel);
|
|
401
|
+
const id = textOf(channel.store_id);
|
|
402
|
+
const codes = [200];
|
|
403
|
+
const args = ["store", "GET", `/stores/${id}`, codes] as const;
|
|
404
|
+
const step = await live.storefront<unknown>(...args);
|
|
405
|
+
if (!step.ok) return fromLive(ctx.locale, step.result);
|
|
406
|
+
return step.value;
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const runAva02 = async (ctx: RunCtx) => {
|
|
410
|
+
const allowed = new Set(["web", "app", "chat", "call-center", "dashboard"]);
|
|
411
|
+
const skipped = applies(ctx, "AVA-02", allowed);
|
|
412
|
+
if (skipped) return skipped;
|
|
413
|
+
const ready = prepare(ctx, [...LIVE, "store_id"]);
|
|
414
|
+
if (isOutcome(ready)) return ready;
|
|
415
|
+
const store = await storeOf(ctx, ready);
|
|
416
|
+
if (isOutcome(store)) return store;
|
|
417
|
+
const zones = isRecord(store) ? store.coverage_zones : undefined;
|
|
418
|
+
if (!Array.isArray(zones) || zones.length === 0) {
|
|
419
|
+
return need(ctx, "AVA-02", "stores.coverage_zones");
|
|
420
|
+
}
|
|
421
|
+
const lat = textOf(ctx.env.FIRESPARK_DOCTOR_LATITUDE);
|
|
422
|
+
const lng = textOf(ctx.env.FIRESPARK_DOCTOR_LONGITUDE);
|
|
423
|
+
if (!lat) return need(ctx, "AVA-02", "FIRESPARK_DOCTOR_LATITUDE");
|
|
424
|
+
if (!lng) return need(ctx, "AVA-02", "FIRESPARK_DOCTOR_LONGITUDE");
|
|
425
|
+
if (!Number.isFinite(Number(lat)) || !Number.isFinite(Number(lng))) {
|
|
426
|
+
return need(ctx, "AVA-02", "FIRESPARK_DOCTOR_LATITUDE");
|
|
427
|
+
}
|
|
428
|
+
let evaluated = false;
|
|
429
|
+
for (const zone of zones) {
|
|
430
|
+
if (!isRecord(zone)) continue;
|
|
431
|
+
const hit = zoneContains(Number(lat), Number(lng), zone);
|
|
432
|
+
if (hit === null) continue;
|
|
433
|
+
evaluated = true;
|
|
434
|
+
if (hit) return { status: "pass" as const };
|
|
435
|
+
}
|
|
436
|
+
if (!evaluated) return need(ctx, "AVA-02", "coverage_zones.radius_info");
|
|
437
|
+
return say(ctx.locale, "fail", COPY.outside);
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const overrideRead = async (ctx: RunCtx, id: string) => {
|
|
441
|
+
const skipped = applies(ctx, id, new Set(["dashboard"]));
|
|
442
|
+
if (skipped) return skipped;
|
|
443
|
+
const ready = prepare(ctx, LIVE);
|
|
444
|
+
if (isOutcome(ready)) return ready;
|
|
445
|
+
const cookie = textOf(ctx.env.FIRESPARK_DOCTOR_DASHBOARD_COOKIE);
|
|
446
|
+
if (!cookie) return need(ctx, id, "FIRESPARK_DOCTOR_DASHBOARD_COOKIE");
|
|
447
|
+
const dash = textOf(ready.dashboard_url);
|
|
448
|
+
if (!dash) return need(ctx, id, "dashboard_url");
|
|
449
|
+
const merchant = textOf(ready.merchant_id);
|
|
450
|
+
if (!merchant) return need(ctx, id, "merchant_id");
|
|
451
|
+
const env = textOf(ready.env);
|
|
452
|
+
if (!env) return need(ctx, id, "env");
|
|
453
|
+
const input = {
|
|
454
|
+
merchant_id: merchant,
|
|
455
|
+
env,
|
|
456
|
+
channel_id: textOf(ready.channel_id),
|
|
457
|
+
store_id: textOf(ready.store_id),
|
|
458
|
+
fulfillment_id: textOf(ready.fulfillment_id)
|
|
459
|
+
};
|
|
460
|
+
const proc = "menusOverrides.selectForScope";
|
|
461
|
+
const step = await trpcCall(dash, cookie, proc, input);
|
|
462
|
+
if (step.status < 200 || step.status >= 300) {
|
|
463
|
+
return say(ctx.locale, "fail", COPY.command, {
|
|
464
|
+
id,
|
|
465
|
+
type: "menusOverrides.selectForScope",
|
|
466
|
+
status: step.status
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
return { status: "pass" as const, text: step.text };
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const runOvr01 = async (ctx: RunCtx) => {
|
|
473
|
+
const read = await overrideRead(ctx, "OVR-01");
|
|
474
|
+
if (!read || !("text" in read)) return read;
|
|
475
|
+
const ops = ["REORDER", "DELETE", "INSERT", "UPDATE"];
|
|
476
|
+
const found = ops.some(op => read.text.includes(op));
|
|
477
|
+
if (!found) return need(ctx, "OVR-01", "menus_overrides");
|
|
478
|
+
return { status: "pass" as const };
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const runOvr02 = async (ctx: RunCtx) => {
|
|
482
|
+
if (!ctx.named.has("OVR-02")) {
|
|
483
|
+
return say(ctx.locale, "skip", COPY.named, { id: "OVR-02" });
|
|
484
|
+
}
|
|
485
|
+
const read = await overrideRead(ctx, "OVR-02");
|
|
486
|
+
if (!read || !("text" in read)) return read;
|
|
487
|
+
if (!read.text.includes("REORDER") && !read.text.includes("overrides")) {
|
|
488
|
+
return need(ctx, "OVR-02", "menus_overrides.preview");
|
|
489
|
+
}
|
|
490
|
+
return { status: "pass" as const };
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const sendCommand = async (
|
|
494
|
+
ctx: RunCtx,
|
|
495
|
+
ready: YamlChannel,
|
|
496
|
+
id: string,
|
|
497
|
+
type: string
|
|
498
|
+
) => {
|
|
499
|
+
if (!ctx.named.has(id)) return say(ctx.locale, "skip", COPY.named, { id });
|
|
500
|
+
const cookie = textOf(ctx.env.FIRESPARK_DOCTOR_DASHBOARD_COOKIE);
|
|
501
|
+
if (!cookie) return need(ctx, id, "FIRESPARK_DOCTOR_DASHBOARD_COOKIE");
|
|
502
|
+
const device = textOf(ctx.env.FIRESPARK_DOCTOR_DEVICE_ID);
|
|
503
|
+
if (!device) return need(ctx, id, "FIRESPARK_DOCTOR_DEVICE_ID");
|
|
504
|
+
const dash = textOf(ready.dashboard_url);
|
|
505
|
+
if (!dash) return need(ctx, id, "dashboard_url");
|
|
506
|
+
const merchant = textOf(ready.merchant_id);
|
|
507
|
+
if (!merchant) return need(ctx, id, "merchant_id");
|
|
508
|
+
const env = textOf(ready.env);
|
|
509
|
+
if (!env) return need(ctx, id, "env");
|
|
510
|
+
const input = { merchant_id: merchant, env, id: device, type };
|
|
511
|
+
const step = await trpcCall(dash, cookie, "kioskDevices.sendCommand", input);
|
|
512
|
+
if (step.status >= 400 || step.status === 0) {
|
|
513
|
+
return say(ctx.locale, "fail", COPY.command, {
|
|
514
|
+
id,
|
|
515
|
+
type,
|
|
516
|
+
status: step.status
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
return { status: "pass" as const };
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
const runCommand = (id: string, allowed: readonly string[], type: string) => {
|
|
523
|
+
return async (ctx: RunCtx) => {
|
|
524
|
+
const skipped = applies(ctx, id, new Set(allowed));
|
|
525
|
+
if (skipped) return skipped;
|
|
526
|
+
const ready = prepare(ctx, ["channel_id"]);
|
|
527
|
+
if (isOutcome(ready)) return ready;
|
|
528
|
+
return sendCommand(ctx, ready, id, type);
|
|
529
|
+
};
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
const runRt02 = runCommand("RT-02", ["kiosk", "dashboard"], "refresh_cache");
|
|
533
|
+
|
|
534
|
+
const runRt03 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
535
|
+
const skipped = applies(ctx, "RT-03", new Set(["kiosk"]));
|
|
536
|
+
if (skipped) return skipped;
|
|
537
|
+
if (!ctx.named.has("RT-03")) {
|
|
538
|
+
return say(ctx.locale, "skip", COPY.named, { id: "RT-03" });
|
|
539
|
+
}
|
|
540
|
+
const files = await readSources(ctx.cwd);
|
|
541
|
+
const file = files.find(item => item.body.includes("resetKioskCache"));
|
|
542
|
+
if (!file) return say(ctx.locale, "skip", COPY.reset);
|
|
543
|
+
const clears = file.body.includes("clearImageCaches");
|
|
544
|
+
const queries = file.body.includes("removeQueries");
|
|
545
|
+
if (!clears || !queries) return say(ctx.locale, "fail", COPY.reset);
|
|
546
|
+
return { status: "pass" as const };
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
const runRt04 = runCommand("RT-04", ["kiosk", "dashboard"], "reload");
|
|
550
|
+
|
|
551
|
+
const runRt05 = async (ctx: RunCtx) => {
|
|
552
|
+
const skipped = applies(ctx, "RT-05", new Set(["kiosk", "dashboard"]));
|
|
553
|
+
if (skipped) return skipped;
|
|
554
|
+
if (!ctx.named.has("RT-05")) {
|
|
555
|
+
return say(ctx.locale, "skip", COPY.named, { id: "RT-05" });
|
|
556
|
+
}
|
|
557
|
+
const ready = prepare(ctx, ["channel_id"]);
|
|
558
|
+
if (isOutcome(ready)) return ready;
|
|
559
|
+
const update = textOf(ctx.env.FIRESPARK_DOCTOR_OTA_UPDATE_ID);
|
|
560
|
+
if (!update) return need(ctx, "RT-05", "FIRESPARK_DOCTOR_OTA_UPDATE_ID");
|
|
561
|
+
return sendCommand(ctx, ready, "RT-05", "check_ota");
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
export const STORE_BINDINGS: Record<string, CheckBinding> = {
|
|
565
|
+
"AVA-01": withSafeFix(runAva01),
|
|
566
|
+
"AVA-02": withSafeFix(runAva02),
|
|
567
|
+
"AVA-03": withSafeFix(runAva03),
|
|
568
|
+
"AVA-04": withSafeFix(runAva04),
|
|
569
|
+
"AVA-05": withSafeFix(runAva05),
|
|
570
|
+
"AVA-06": withSafeFix(runAva06),
|
|
571
|
+
"OVR-01": withSafeFix(runOvr01),
|
|
572
|
+
"OVR-02": withSafeFix(runOvr02),
|
|
573
|
+
"RT-02": withSafeFix(runRt02),
|
|
574
|
+
"RT-03": withSafeFix(runRt03),
|
|
575
|
+
"RT-04": withSafeFix(runRt04),
|
|
576
|
+
"RT-05": withSafeFix(runRt05)
|
|
577
|
+
};
|