@fire-spark/cli 0.13.2 → 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/package.json +3 -1
- package/src/commands/doctor/client/admin.flow.ts +553 -0
- package/src/commands/doctor/client/checkout.flow.ts +400 -0
- package/src/commands/doctor/client/content.flow.ts +450 -0
- package/src/commands/doctor/client/copy.ts +22 -2
- package/src/commands/doctor/client/flow.ts +182 -0
- package/src/commands/doctor/client/log.flow.ts +410 -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/ord.flow.ts +445 -0
- package/src/commands/doctor/client/order.ts +1 -1
- 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 +21 -1
- package/src/commands/doctor/client/sdk.flow.ts +630 -0
- package/src/commands/doctor/client/store.flow.ts +577 -0
- package/src/commands/doctor/live/http.ts +10 -18
- package/src/commands/doctor/live/live.session.ts +13 -6
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { Big } from "big.js";
|
|
2
|
+
|
|
3
|
+
import type { Copy } from "../../../core/i18n";
|
|
4
|
+
import type { LiveSession } from "../live/live.session";
|
|
5
|
+
import { fromLive, isOutcome, isRecord, openLive, prepare } from "./flow";
|
|
6
|
+
import { queryOf, rowsOf, say, textOf, trpcCall, withSafeFix } from "./flow";
|
|
7
|
+
import { readSources } from "./repo.scan";
|
|
8
|
+
import type { CheckBinding, Outcome, RunCtx } from "./runner";
|
|
9
|
+
import type { YamlChannel } from "./yaml";
|
|
10
|
+
|
|
11
|
+
const line = (es: string, en: string, pt: string): Copy => {
|
|
12
|
+
return { "es-EC": es, "en-US": en, "pt-BR": pt };
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const COPY = {
|
|
16
|
+
channel: line(
|
|
17
|
+
"{id} no aplica al canal {template}.",
|
|
18
|
+
"{id} does not apply to the {template} channel.",
|
|
19
|
+
"{id} não se aplica ao canal {template}."
|
|
20
|
+
),
|
|
21
|
+
list: line(
|
|
22
|
+
"El menú no es una lista.",
|
|
23
|
+
"The menu is not a list.",
|
|
24
|
+
"O cardápio não é uma lista."
|
|
25
|
+
),
|
|
26
|
+
empty: line(
|
|
27
|
+
"El menú no tiene productos.",
|
|
28
|
+
"The menu has no products.",
|
|
29
|
+
"O cardápio não tem produtos."
|
|
30
|
+
),
|
|
31
|
+
drifted: line(
|
|
32
|
+
"El menú cambió entre dos lecturas de la misma sesión.",
|
|
33
|
+
"The menu changed between two reads of the same session.",
|
|
34
|
+
"O cardápio mudou entre duas leituras da mesma sessão."
|
|
35
|
+
),
|
|
36
|
+
noDiscount: line(
|
|
37
|
+
"MEN-08 se omite: el menú no tiene un producto con precio anterior.",
|
|
38
|
+
"MEN-08 is skipped: the menu has no product with a previous price.",
|
|
39
|
+
"MEN-08 é omitido: o cardápio não tem um produto com preço anterior."
|
|
40
|
+
),
|
|
41
|
+
discount: line(
|
|
42
|
+
"El descuento no cuadra con el precio anterior.",
|
|
43
|
+
"The discount does not match the previous price.",
|
|
44
|
+
"O desconto não fecha com o preço anterior."
|
|
45
|
+
),
|
|
46
|
+
noStock: line(
|
|
47
|
+
"MEN-09 se omite: el menú no tiene un producto agotado.",
|
|
48
|
+
"MEN-09 is skipped: the menu has no out-of-stock product.",
|
|
49
|
+
"MEN-09 é omitido: o cardápio não tem um produto esgotado."
|
|
50
|
+
),
|
|
51
|
+
noHours: line(
|
|
52
|
+
"MEN-10 se omite: el menú no tiene un producto oculto o fuera de horario.",
|
|
53
|
+
"MEN-10 is skipped: the menu has no hidden or out-of-hours product.",
|
|
54
|
+
"MEN-10 é omitido: o cardápio não tem um produto oculto ou fora do horário."
|
|
55
|
+
),
|
|
56
|
+
noVariant: line(
|
|
57
|
+
"MEN-11 se omite: el menú no tiene un producto con variantes.",
|
|
58
|
+
"MEN-11 is skipped: the menu has no product with variants.",
|
|
59
|
+
"MEN-11 é omitido: o cardápio não tem um produto com variantes."
|
|
60
|
+
),
|
|
61
|
+
noModifier: line(
|
|
62
|
+
"MEN-12 se omite: falta modifier_groups.min.",
|
|
63
|
+
"MEN-12 is skipped: modifier_groups.min is missing.",
|
|
64
|
+
"MEN-12 é omitido: falta modifier_groups.min."
|
|
65
|
+
),
|
|
66
|
+
named: line(
|
|
67
|
+
"{id} solo corre cuando lo nombras. No cambié el menú.",
|
|
68
|
+
"{id} runs only when you name it. The menu was not changed.",
|
|
69
|
+
"{id} só roda quando você o nomeia. O cardápio não foi alterado."
|
|
70
|
+
),
|
|
71
|
+
need: line(
|
|
72
|
+
"{id} se omite: falta {field}.",
|
|
73
|
+
"{id} is skipped: {field} is missing.",
|
|
74
|
+
"{id} é omitido: falta {field}."
|
|
75
|
+
),
|
|
76
|
+
http: line(
|
|
77
|
+
"{id} respondió {status}.",
|
|
78
|
+
"{id} responded {status}.",
|
|
79
|
+
"{id} respondeu {status}."
|
|
80
|
+
),
|
|
81
|
+
listener: line(
|
|
82
|
+
"MEN-07 el canal se suscribe a menu.changed.",
|
|
83
|
+
"MEN-07 the channel subscribes to menu.changed.",
|
|
84
|
+
"MEN-07 o canal assina menu.changed."
|
|
85
|
+
),
|
|
86
|
+
soldOut: line(
|
|
87
|
+
"MEN-09 el producto agotado no se renderiza en gris con Agotado o Sold out.",
|
|
88
|
+
"MEN-09 the out-of-stock product is not rendered grey with Agotado or Sold out.",
|
|
89
|
+
"MEN-09 o produto esgotado não renderiza em cinza com Agotado ou Sold out."
|
|
90
|
+
)
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const LIVE = [
|
|
94
|
+
"api_url",
|
|
95
|
+
"channel_id",
|
|
96
|
+
"FIRESPARK_DOCTOR_CUSTOMER_EMAIL",
|
|
97
|
+
"FIRESPARK_DOCTOR_CUSTOMER_PASSWORD",
|
|
98
|
+
"FIRESPARK_DOCTOR_FIREBASE_API_KEY"
|
|
99
|
+
] as const;
|
|
100
|
+
|
|
101
|
+
type Row = Record<string, unknown>;
|
|
102
|
+
|
|
103
|
+
type Loaded = { channel: YamlChannel; live: LiveSession; menus: unknown[] };
|
|
104
|
+
|
|
105
|
+
const numberOf = (value: unknown) => {
|
|
106
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
107
|
+
return value;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const productsOf = (menus: unknown[]) => {
|
|
111
|
+
return rowsOf(menus).flatMap(menu => rowsOf(menu.products));
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const idsOf = (products: Row[]) => {
|
|
115
|
+
return products.map(product => String(product.id ?? "")).join(",");
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const load = async (ctx: RunCtx): Promise<Loaded | Outcome> => {
|
|
119
|
+
const ready = prepare(ctx, LIVE);
|
|
120
|
+
if (isOutcome(ready)) return ready;
|
|
121
|
+
const live = openLive(ctx, ready);
|
|
122
|
+
const path = queryOf("/menus", { channel_id: textOf(ready.channel_id) });
|
|
123
|
+
const step = await live.storefront<unknown>("menus", "GET", path, [200]);
|
|
124
|
+
if (!step.ok) return fromLive(ctx.locale, step.result);
|
|
125
|
+
if (!Array.isArray(step.value)) return say(ctx.locale, "fail", COPY.list);
|
|
126
|
+
return { channel: ready, live, menus: step.value };
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const applies = (ctx: RunCtx, id: string, allowed: ReadonlySet<string>) => {
|
|
130
|
+
const ready = prepare(ctx, ["channel_id"]);
|
|
131
|
+
if (isOutcome(ready)) return ready;
|
|
132
|
+
const template = textOf(ready.template);
|
|
133
|
+
if (allowed.has(template)) return null;
|
|
134
|
+
return say(ctx.locale, "skip", COPY.channel, { id, template });
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const namedOnly = (ctx: RunCtx, id: string) => {
|
|
138
|
+
if (ctx.named.has(id)) return null;
|
|
139
|
+
return say(ctx.locale, "skip", COPY.named, { id });
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const need = (ctx: RunCtx, id: string, field: string) => {
|
|
143
|
+
return say(ctx.locale, "skip", COPY.need, { id, field });
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const dashOf = (ctx: RunCtx, id: string, channel: YamlChannel) => {
|
|
147
|
+
const cookie = textOf(ctx.env.FIRESPARK_DOCTOR_DASHBOARD_COOKIE);
|
|
148
|
+
if (!cookie) return need(ctx, id, "FIRESPARK_DOCTOR_DASHBOARD_COOKIE");
|
|
149
|
+
if (!textOf(channel.dashboard_url)) return need(ctx, id, "dashboard_url");
|
|
150
|
+
if (!textOf(channel.merchant_id)) return need(ctx, id, "merchant_id");
|
|
151
|
+
if (!textOf(channel.env)) return need(ctx, id, "env");
|
|
152
|
+
return cookie;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const runMen01 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
156
|
+
const skipped = applies(ctx, "MEN-01", new Set(["dashboard"]));
|
|
157
|
+
if (skipped) return skipped;
|
|
158
|
+
const named = namedOnly(ctx, "MEN-01");
|
|
159
|
+
if (named) return named;
|
|
160
|
+
const ready = prepare(ctx, ["channel_id"]);
|
|
161
|
+
if (isOutcome(ready)) return ready;
|
|
162
|
+
const cookie = dashOf(ctx, "MEN-01", ready);
|
|
163
|
+
if (typeof cookie !== "string") return cookie;
|
|
164
|
+
const dash = textOf(ready.dashboard_url);
|
|
165
|
+
const input = {
|
|
166
|
+
merchant_id: textOf(ready.merchant_id),
|
|
167
|
+
env: textOf(ready.env),
|
|
168
|
+
name: "doctor-menu"
|
|
169
|
+
};
|
|
170
|
+
const step = await trpcCall(dash, cookie, "menus.insert", input);
|
|
171
|
+
if (step.status < 200 || step.status >= 300) {
|
|
172
|
+
return say(ctx.locale, "fail", COPY.http, {
|
|
173
|
+
id: "MEN-01",
|
|
174
|
+
status: step.status
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return { status: "pass" };
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const runMen02 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
181
|
+
const skipped = applies(ctx, "MEN-02", new Set(["dashboard"]));
|
|
182
|
+
if (skipped) return skipped;
|
|
183
|
+
const named = namedOnly(ctx, "MEN-02");
|
|
184
|
+
if (named) return named;
|
|
185
|
+
const ready = prepare(ctx, ["channel_id"]);
|
|
186
|
+
if (isOutcome(ready)) return ready;
|
|
187
|
+
const cookie = dashOf(ctx, "MEN-02", ready);
|
|
188
|
+
if (typeof cookie !== "string") return cookie;
|
|
189
|
+
const dash = textOf(ready.dashboard_url);
|
|
190
|
+
const input = {
|
|
191
|
+
merchant_id: textOf(ready.merchant_id),
|
|
192
|
+
env: textOf(ready.env)
|
|
193
|
+
};
|
|
194
|
+
const step = await trpcCall(dash, cookie, "menuSyncs.selectLatest", input);
|
|
195
|
+
if (step.status < 200 || step.status >= 300) {
|
|
196
|
+
return say(ctx.locale, "fail", COPY.http, {
|
|
197
|
+
id: "MEN-02",
|
|
198
|
+
status: step.status
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
const keys = ["draft", "publish", "cancel", "rollback"];
|
|
202
|
+
const found = keys.some(key => step.text.toLowerCase().includes(key));
|
|
203
|
+
if (!found) return need(ctx, "MEN-02", "menu_syncs");
|
|
204
|
+
return { status: "pass" };
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const runMen04 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
208
|
+
const allowed = new Set(["kiosk", "dashboard"]);
|
|
209
|
+
const skipped = applies(ctx, "MEN-04", allowed);
|
|
210
|
+
if (skipped) return skipped;
|
|
211
|
+
const named = namedOnly(ctx, "MEN-04");
|
|
212
|
+
if (named) return named;
|
|
213
|
+
const ready = prepare(ctx, ["channel_id"]);
|
|
214
|
+
if (isOutcome(ready)) return ready;
|
|
215
|
+
const cookie = dashOf(ctx, "MEN-04", ready);
|
|
216
|
+
if (typeof cookie !== "string") return cookie;
|
|
217
|
+
const dash = textOf(ready.dashboard_url);
|
|
218
|
+
const input = {
|
|
219
|
+
merchant_id: textOf(ready.merchant_id),
|
|
220
|
+
env: textOf(ready.env),
|
|
221
|
+
name: "doctor-product",
|
|
222
|
+
pricing: { price: "1.00" }
|
|
223
|
+
};
|
|
224
|
+
const proc = "menuBuilder.products.insert";
|
|
225
|
+
const step = await trpcCall(dash, cookie, proc, input);
|
|
226
|
+
if (step.status < 200 || step.status >= 300) {
|
|
227
|
+
return say(ctx.locale, "fail", COPY.http, {
|
|
228
|
+
id: "MEN-04",
|
|
229
|
+
status: step.status
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return { status: "pass" };
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const stable = async (ctx: RunCtx): Promise<Outcome> => {
|
|
236
|
+
const first = await load(ctx);
|
|
237
|
+
if (isOutcome(first)) return first;
|
|
238
|
+
const second = await load(ctx);
|
|
239
|
+
if (isOutcome(second)) return second;
|
|
240
|
+
const left = idsOf(productsOf(first.menus));
|
|
241
|
+
const right = idsOf(productsOf(second.menus));
|
|
242
|
+
if (left !== right) return say(ctx.locale, "fail", COPY.drifted);
|
|
243
|
+
if (!left) return say(ctx.locale, "fail", COPY.empty);
|
|
244
|
+
return { status: "pass" };
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const runMen05 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
248
|
+
const skipped = applies(ctx, "MEN-05", new Set(["kiosk"]));
|
|
249
|
+
if (skipped) return skipped;
|
|
250
|
+
const named = namedOnly(ctx, "MEN-05");
|
|
251
|
+
if (named) return named;
|
|
252
|
+
const first = await load(ctx);
|
|
253
|
+
if (isOutcome(first)) return first;
|
|
254
|
+
const before = idsOf(productsOf(first.menus));
|
|
255
|
+
const cookie = dashOf(ctx, "MEN-05", first.channel);
|
|
256
|
+
if (typeof cookie !== "string") return cookie;
|
|
257
|
+
const dash = textOf(first.channel.dashboard_url);
|
|
258
|
+
const input = {
|
|
259
|
+
merchant_id: textOf(first.channel.merchant_id),
|
|
260
|
+
env: textOf(first.channel.env),
|
|
261
|
+
name: "doctor-dirty"
|
|
262
|
+
};
|
|
263
|
+
const wrote = await trpcCall(dash, cookie, "menus.update", input);
|
|
264
|
+
if (wrote.status < 200 || wrote.status >= 300) {
|
|
265
|
+
return say(ctx.locale, "fail", COPY.http, {
|
|
266
|
+
id: "MEN-05",
|
|
267
|
+
status: wrote.status
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
const second = await load(ctx);
|
|
271
|
+
if (isOutcome(second)) return second;
|
|
272
|
+
const after = idsOf(productsOf(second.menus));
|
|
273
|
+
if (before === after && before !== "") {
|
|
274
|
+
return say(ctx.locale, "fail", COPY.drifted);
|
|
275
|
+
}
|
|
276
|
+
return { status: "pass" };
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const runMen06 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
280
|
+
const skipped = applies(ctx, "MEN-06", new Set(["pos"]));
|
|
281
|
+
if (skipped) return skipped;
|
|
282
|
+
return stable(ctx);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const runMen07 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
286
|
+
const allowed = new Set(["web", "app", "chat", "call-center"]);
|
|
287
|
+
const skipped = applies(ctx, "MEN-07", allowed);
|
|
288
|
+
if (skipped) return skipped;
|
|
289
|
+
const files = await readSources(ctx.cwd);
|
|
290
|
+
const listen = files.filter(file => file.body.includes("listenOrder"));
|
|
291
|
+
if (listen.length === 0) return need(ctx, "MEN-07", "listenOrder");
|
|
292
|
+
const changed = listen.some(file => {
|
|
293
|
+
return (
|
|
294
|
+
file.body.includes("menu.changed") || file.body.includes("menuChanged")
|
|
295
|
+
);
|
|
296
|
+
});
|
|
297
|
+
if (changed) return say(ctx.locale, "fail", COPY.listener);
|
|
298
|
+
return { status: "pass" };
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const pricingOf = (product: Row) => {
|
|
302
|
+
return isRecord(product.pricing) ? product.pricing : null;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const runMen08 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
306
|
+
const loaded = await load(ctx);
|
|
307
|
+
if (isOutcome(loaded)) return loaded;
|
|
308
|
+
const priced = productsOf(loaded.menus).flatMap(product => {
|
|
309
|
+
const pricing = pricingOf(product);
|
|
310
|
+
if (!pricing) return [];
|
|
311
|
+
const price = numberOf(pricing.price);
|
|
312
|
+
const anchor = numberOf(pricing.anchor_price);
|
|
313
|
+
if (price === null || anchor === null) return [];
|
|
314
|
+
return [{ price, anchor }];
|
|
315
|
+
});
|
|
316
|
+
const sample = priced[0];
|
|
317
|
+
if (!sample) return say(ctx.locale, "skip", COPY.noDiscount);
|
|
318
|
+
const price = new Big(String(sample.price));
|
|
319
|
+
const anchor = new Big(String(sample.anchor));
|
|
320
|
+
if (!anchor.gt(price)) return say(ctx.locale, "fail", COPY.discount);
|
|
321
|
+
const saved = anchor.minus(price);
|
|
322
|
+
if (!saved.plus(price).eq(anchor)) {
|
|
323
|
+
return say(ctx.locale, "fail", COPY.discount);
|
|
324
|
+
}
|
|
325
|
+
return { status: "pass" };
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const statusOf = (product: Row) => {
|
|
329
|
+
const availability = product.availability;
|
|
330
|
+
if (!isRecord(availability)) return "";
|
|
331
|
+
const status = availability.status;
|
|
332
|
+
return typeof status === "string" ? status : "";
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const runMen09 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
336
|
+
const loaded = await load(ctx);
|
|
337
|
+
if (isOutcome(loaded)) return loaded;
|
|
338
|
+
const found = productsOf(loaded.menus).some(product => {
|
|
339
|
+
return statusOf(product) === "OUT_OF_STOCK";
|
|
340
|
+
});
|
|
341
|
+
if (!found) return say(ctx.locale, "skip", COPY.noStock);
|
|
342
|
+
const files = await readSources(ctx.cwd);
|
|
343
|
+
const label = files.some(file => {
|
|
344
|
+
const sold =
|
|
345
|
+
file.body.includes("Agotado") || file.body.includes("Sold out");
|
|
346
|
+
const grey = file.body.includes("grey") || file.body.includes("gray");
|
|
347
|
+
return sold && grey;
|
|
348
|
+
});
|
|
349
|
+
if (!label) return say(ctx.locale, "fail", COPY.soldOut);
|
|
350
|
+
return { status: "pass" };
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const runMen10 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
354
|
+
const loaded = await load(ctx);
|
|
355
|
+
if (isOutcome(loaded)) return loaded;
|
|
356
|
+
const found = productsOf(loaded.menus).some(product => {
|
|
357
|
+
if (!Array.isArray(product.schedules)) return false;
|
|
358
|
+
return product.schedules.some(isRecord);
|
|
359
|
+
});
|
|
360
|
+
if (!found) return need(ctx, "MEN-10", "schedules");
|
|
361
|
+
const files = await readSources(ctx.cwd);
|
|
362
|
+
const label = files.some(file => {
|
|
363
|
+
if (file.body.includes("Fuera de horario")) return true;
|
|
364
|
+
return file.body.includes("OUTSIDE_HOURS");
|
|
365
|
+
});
|
|
366
|
+
if (!label) return need(ctx, "MEN-10", "OUTSIDE_HOURS");
|
|
367
|
+
return { status: "pass" };
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const runMen11 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
371
|
+
const loaded = await load(ctx);
|
|
372
|
+
if (isOutcome(loaded)) return loaded;
|
|
373
|
+
const found = productsOf(loaded.menus).some(product => {
|
|
374
|
+
const pricing = pricingOf(product);
|
|
375
|
+
const groups = product.variant_groups ?? product.variants;
|
|
376
|
+
if (!Array.isArray(groups) || groups.length === 0) return false;
|
|
377
|
+
if (!pricing || numberOf(pricing.starting_price) === null) return false;
|
|
378
|
+
return groups.filter(isRecord).some(group => {
|
|
379
|
+
const status = group.status ?? group.availability;
|
|
380
|
+
return status === "OUT_OF_STOCK" || status === "SOLD_OUT";
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
if (!found) return need(ctx, "MEN-11", "variant_groups");
|
|
384
|
+
return { status: "pass" };
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const runMen12 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
388
|
+
const loaded = await load(ctx);
|
|
389
|
+
if (isOutcome(loaded)) return loaded;
|
|
390
|
+
const found = productsOf(loaded.menus).some(product => {
|
|
391
|
+
if (!Array.isArray(product.modifiers)) return false;
|
|
392
|
+
return product.modifiers.filter(isRecord).some(group => {
|
|
393
|
+
return typeof group.min === "number" && typeof group.max === "number";
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
if (!found) return say(ctx.locale, "skip", COPY.noModifier);
|
|
397
|
+
return { status: "pass" };
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
export const MEN_BINDINGS: Record<string, CheckBinding> = {
|
|
401
|
+
"MEN-01": withSafeFix(runMen01),
|
|
402
|
+
"MEN-02": withSafeFix(runMen02),
|
|
403
|
+
"MEN-04": withSafeFix(runMen04),
|
|
404
|
+
"MEN-05": withSafeFix(runMen05),
|
|
405
|
+
"MEN-06": withSafeFix(runMen06),
|
|
406
|
+
"MEN-07": withSafeFix(runMen07),
|
|
407
|
+
"MEN-08": withSafeFix(runMen08),
|
|
408
|
+
"MEN-09": withSafeFix(runMen09),
|
|
409
|
+
"MEN-10": withSafeFix(runMen10),
|
|
410
|
+
"MEN-11": withSafeFix(runMen11),
|
|
411
|
+
"MEN-12": withSafeFix(runMen12)
|
|
412
|
+
};
|