@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,400 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import type { Copy } from "../../../core/i18n";
|
|
4
|
+
import type { Menu } from "../live/live.session";
|
|
5
|
+
import { buildOrderBody, doctorOrderId, pickProduct } from "../live/order";
|
|
6
|
+
import { paymentOf } from "./config";
|
|
7
|
+
import { fromLive, isOutcome, isRecord, namedOrderSkip } from "./flow";
|
|
8
|
+
import { openLive, prepare, queryOf, rowsOf, say, textOf } from "./flow";
|
|
9
|
+
import { trpcCall, withSafeFix } from "./flow";
|
|
10
|
+
import { liveConfigOf } from "./order";
|
|
11
|
+
import type { CheckBinding, Outcome, RunCtx } from "./runner";
|
|
12
|
+
import type { YamlChannel } from "./yaml";
|
|
13
|
+
|
|
14
|
+
const line = (es: string, en: string, pt: string): Copy => {
|
|
15
|
+
return { "es-EC": es, "en-US": en, "pt-BR": pt };
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const COPY = {
|
|
19
|
+
channel: line(
|
|
20
|
+
"{id} no aplica al canal {template}.",
|
|
21
|
+
"{id} does not apply to the {template} channel.",
|
|
22
|
+
"{id} não se aplica ao canal {template}."
|
|
23
|
+
),
|
|
24
|
+
list: line(
|
|
25
|
+
"El menú no es una lista.",
|
|
26
|
+
"The menu is not a list.",
|
|
27
|
+
"O cardápio não é uma lista."
|
|
28
|
+
),
|
|
29
|
+
search: line(
|
|
30
|
+
"La búsqueda del menú no devolvió el producto ni el estado vacío.",
|
|
31
|
+
"Menu search did not return the product or the empty state.",
|
|
32
|
+
"A busca do cardápio não devolveu o produto nem o estado vazio."
|
|
33
|
+
),
|
|
34
|
+
language: line(
|
|
35
|
+
"MEN-14 se omite: el menú no tiene textos en más de un idioma.",
|
|
36
|
+
"MEN-14 is skipped: the menu has no text in more than one language.",
|
|
37
|
+
"MEN-14 é omitido: o cardápio não tem textos em mais de um idioma."
|
|
38
|
+
),
|
|
39
|
+
modifiers: line(
|
|
40
|
+
"MEN-15 se omite: falta menus[].products[].modifiers.",
|
|
41
|
+
"MEN-15 is skipped: menus[].products[].modifiers is missing.",
|
|
42
|
+
"MEN-15 é omitido: falta menus[].products[].modifiers."
|
|
43
|
+
),
|
|
44
|
+
scope: line(
|
|
45
|
+
"MEN-16 se omite: falta menus[].scope.",
|
|
46
|
+
"MEN-16 is skipped: menus[].scope is missing.",
|
|
47
|
+
"MEN-16 é omitido: falta menus[].scope."
|
|
48
|
+
),
|
|
49
|
+
recommendations: line(
|
|
50
|
+
"MEN-17 se omite: ningún producto tiene recomendaciones.",
|
|
51
|
+
"MEN-17 is skipped: no product has recommendations.",
|
|
52
|
+
"MEN-17 é omitido: nenhum produto tem recomendações."
|
|
53
|
+
),
|
|
54
|
+
rules: line(
|
|
55
|
+
"CHK-03 se omite: no hay reglas de pedido distintas del default.",
|
|
56
|
+
"CHK-03 is skipped: there are no order rules beyond the default.",
|
|
57
|
+
"CHK-03 é omitido: não há regras de pedido além do padrão."
|
|
58
|
+
),
|
|
59
|
+
orderId: line(
|
|
60
|
+
"Falta FIRESPARK_DOCTOR_ORDER_ID.",
|
|
61
|
+
"FIRESPARK_DOCTOR_ORDER_ID is missing.",
|
|
62
|
+
"Falta FIRESPARK_DOCTOR_ORDER_ID."
|
|
63
|
+
),
|
|
64
|
+
anonymous: line(
|
|
65
|
+
"CHK-04 se omite: este canal no acepta pedidos anónimos.",
|
|
66
|
+
"CHK-04 is skipped: this channel does not accept anonymous orders.",
|
|
67
|
+
"CHK-04 é omitido: este canal não aceita pedidos anônimos."
|
|
68
|
+
),
|
|
69
|
+
validate: line(
|
|
70
|
+
"La validación del carrito no respondió ok.",
|
|
71
|
+
"Cart validation did not respond ok.",
|
|
72
|
+
"A validação do carrinho não respondeu ok."
|
|
73
|
+
),
|
|
74
|
+
notCash: line(
|
|
75
|
+
"El pago del canal no es cash ni sandbox. No hice place.",
|
|
76
|
+
"The channel payment is not cash or sandbox. Place was not called.",
|
|
77
|
+
"O pagamento do canal não é cash nem sandbox. Place não foi chamado."
|
|
78
|
+
),
|
|
79
|
+
placeStatus: line(
|
|
80
|
+
"Place respondió {status}. Esperaba ACCEPTED o IN_PROGRESS.",
|
|
81
|
+
"Place returned {status}. Expected ACCEPTED or IN_PROGRESS.",
|
|
82
|
+
"Place respondeu {status}. Esperava ACCEPTED ou IN_PROGRESS."
|
|
83
|
+
)
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const LIVE = [
|
|
87
|
+
"api_url",
|
|
88
|
+
"channel_id",
|
|
89
|
+
"FIRESPARK_DOCTOR_CUSTOMER_EMAIL",
|
|
90
|
+
"FIRESPARK_DOCTOR_CUSTOMER_PASSWORD",
|
|
91
|
+
"FIRESPARK_DOCTOR_FIREBASE_API_KEY"
|
|
92
|
+
] as const;
|
|
93
|
+
|
|
94
|
+
const PLACE = new Set(["ACCEPTED", "IN_PROGRESS"]);
|
|
95
|
+
const DEFAULT_MAX_ITEMS = 99;
|
|
96
|
+
const DEFAULT_MAX_TOTAL = 999999;
|
|
97
|
+
|
|
98
|
+
const applies = (ctx: RunCtx, id: string, allowed: ReadonlySet<string>) => {
|
|
99
|
+
const ready = prepare(ctx, ["channel_id"]);
|
|
100
|
+
if (isOutcome(ready)) return ready;
|
|
101
|
+
const template = textOf(ready.template);
|
|
102
|
+
if (allowed.has(template)) return null;
|
|
103
|
+
return say(ctx.locale, "skip", COPY.channel, { id, template });
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const menusOf = async (ctx: RunCtx, channel: YamlChannel) => {
|
|
107
|
+
const live = openLive(ctx, channel);
|
|
108
|
+
const path = queryOf("/menus", { channel_id: textOf(channel.channel_id) });
|
|
109
|
+
const step = await live.storefront<unknown>("menus", "GET", path, [200]);
|
|
110
|
+
if (!step.ok) return fromLive(ctx.locale, step.result);
|
|
111
|
+
if (!Array.isArray(step.value)) return say(ctx.locale, "fail", COPY.list);
|
|
112
|
+
return { live, menus: step.value };
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const productsOf = (menus: unknown[]) => {
|
|
116
|
+
return rowsOf(menus).flatMap(menu => rowsOf(menu.products));
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const namesOf = (menus: unknown[]) => {
|
|
120
|
+
return productsOf(menus)
|
|
121
|
+
.map(item => String(item.name ?? ""))
|
|
122
|
+
.filter(Boolean);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const searchMenus = async (ctx: RunCtx, channel: YamlChannel, q: string) => {
|
|
126
|
+
const live = openLive(ctx, channel);
|
|
127
|
+
const path = queryOf("/menus", {
|
|
128
|
+
channel_id: textOf(channel.channel_id),
|
|
129
|
+
q
|
|
130
|
+
});
|
|
131
|
+
const step = await live.storefront<unknown>("search", "GET", path, [200]);
|
|
132
|
+
if (!step.ok) return fromLive(ctx.locale, step.result);
|
|
133
|
+
if (!Array.isArray(step.value)) return say(ctx.locale, "fail", COPY.list);
|
|
134
|
+
return namesOf(step.value);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const runMen13 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
138
|
+
const ready = prepare(ctx, LIVE);
|
|
139
|
+
if (isOutcome(ready)) return ready;
|
|
140
|
+
const loaded = await menusOf(ctx, ready);
|
|
141
|
+
if (isOutcome(loaded)) return loaded;
|
|
142
|
+
const name = namesOf(loaded.menus)[0] ?? "";
|
|
143
|
+
if (!name) return say(ctx.locale, "fail", COPY.search);
|
|
144
|
+
const hit = await searchMenus(ctx, ready, name);
|
|
145
|
+
if (!Array.isArray(hit)) return hit;
|
|
146
|
+
const miss = await searchMenus(ctx, ready, "zzzz-doctor-miss");
|
|
147
|
+
if (!Array.isArray(miss)) return miss;
|
|
148
|
+
if (!hit.includes(name) || miss.length !== 0) {
|
|
149
|
+
return say(ctx.locale, "fail", COPY.search);
|
|
150
|
+
}
|
|
151
|
+
return { status: "pass" };
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const runMen14 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
155
|
+
const ready = prepare(ctx, LIVE);
|
|
156
|
+
if (isOutcome(ready)) return ready;
|
|
157
|
+
const loaded = await menusOf(ctx, ready);
|
|
158
|
+
if (isOutcome(loaded)) return loaded;
|
|
159
|
+
const multi = productsOf(loaded.menus).some(item => {
|
|
160
|
+
const bag = item.translations ?? item.names;
|
|
161
|
+
if (!isRecord(bag)) return false;
|
|
162
|
+
const locales = Object.keys(bag).filter(key => {
|
|
163
|
+
const value = bag[key];
|
|
164
|
+
return typeof value === "string" && value.trim() !== "";
|
|
165
|
+
});
|
|
166
|
+
return locales.length >= 2;
|
|
167
|
+
});
|
|
168
|
+
if (!multi) return say(ctx.locale, "skip", COPY.language);
|
|
169
|
+
return { status: "pass" };
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const runMen15 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
173
|
+
const skipped = applies(ctx, "MEN-15", new Set(["dashboard"]));
|
|
174
|
+
if (skipped) return skipped;
|
|
175
|
+
const ready = prepare(ctx, LIVE);
|
|
176
|
+
if (isOutcome(ready)) return ready;
|
|
177
|
+
const cookie = textOf(ctx.env.FIRESPARK_DOCTOR_DASHBOARD_COOKIE);
|
|
178
|
+
if (!cookie) return say(ctx.locale, "skip", COPY.modifiers);
|
|
179
|
+
const dash = textOf(ready.dashboard_url);
|
|
180
|
+
if (!dash) return say(ctx.locale, "skip", COPY.modifiers);
|
|
181
|
+
const input = {
|
|
182
|
+
merchant_id: textOf(ready.merchant_id),
|
|
183
|
+
env: textOf(ready.env)
|
|
184
|
+
};
|
|
185
|
+
const productsProc = "menuBuilder.products.selectByRange";
|
|
186
|
+
const products = await trpcCall(dash, cookie, productsProc, input);
|
|
187
|
+
const groupsProc = "menuBuilder.modifierGroups.selectByRange";
|
|
188
|
+
const groups = await trpcCall(dash, cookie, groupsProc, input);
|
|
189
|
+
if (products.status >= 400 || groups.status >= 400) {
|
|
190
|
+
return say(ctx.locale, "fail", COPY.list);
|
|
191
|
+
}
|
|
192
|
+
if (!products.text.includes("id") || !groups.text.includes("id")) {
|
|
193
|
+
return say(ctx.locale, "skip", COPY.modifiers);
|
|
194
|
+
}
|
|
195
|
+
return { status: "pass" };
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const runMen16 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
199
|
+
const skipped = applies(ctx, "MEN-16", new Set(["dashboard"]));
|
|
200
|
+
if (skipped) return skipped;
|
|
201
|
+
const ready = prepare(ctx, LIVE);
|
|
202
|
+
if (isOutcome(ready)) return ready;
|
|
203
|
+
const loaded = await menusOf(ctx, ready);
|
|
204
|
+
if (isOutcome(loaded)) return loaded;
|
|
205
|
+
const found = rowsOf(loaded.menus).some(menu => {
|
|
206
|
+
const golden = typeof menu.golden_menu_id === "string";
|
|
207
|
+
const stale = Array.isArray(menu.outdated_entities) || menu.stale === true;
|
|
208
|
+
return golden && stale;
|
|
209
|
+
});
|
|
210
|
+
if (!found) return say(ctx.locale, "skip", COPY.scope);
|
|
211
|
+
return { status: "pass" };
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const runMen17 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
215
|
+
const ready = prepare(ctx, LIVE);
|
|
216
|
+
if (isOutcome(ready)) return ready;
|
|
217
|
+
const loaded = await menusOf(ctx, ready);
|
|
218
|
+
if (isOutcome(loaded)) return loaded;
|
|
219
|
+
const types = new Set<string>();
|
|
220
|
+
for (const item of productsOf(loaded.menus)) {
|
|
221
|
+
const recs = item.recommendations ?? item.recommendation_types;
|
|
222
|
+
if (!Array.isArray(recs)) continue;
|
|
223
|
+
for (const rec of recs) {
|
|
224
|
+
if (isRecord(rec) && typeof rec.type === "string") types.add(rec.type);
|
|
225
|
+
if (typeof rec === "string") types.add(rec);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (types.size === 0) return say(ctx.locale, "skip", COPY.recommendations);
|
|
229
|
+
return { status: "pass" };
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const rulesAreDefault = (value: unknown) => {
|
|
233
|
+
if (!isRecord(value)) return true;
|
|
234
|
+
const rules = value.order_rules;
|
|
235
|
+
if (!isRecord(rules) || !isRecord(rules.order)) return true;
|
|
236
|
+
const order = rules.order;
|
|
237
|
+
const minItems = order.min_items;
|
|
238
|
+
const minTotal = order.min_total;
|
|
239
|
+
const maxItems = order.max_items;
|
|
240
|
+
const maxTotal = order.max_total;
|
|
241
|
+
if (typeof minItems === "number" && minItems !== 0) return false;
|
|
242
|
+
if (typeof minTotal === "number" && minTotal !== 0) return false;
|
|
243
|
+
if (typeof maxItems === "number" && maxItems !== DEFAULT_MAX_ITEMS) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
if (typeof maxTotal === "number" && maxTotal !== DEFAULT_MAX_TOTAL) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
return true;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const preferencesOf = async (ctx: RunCtx, channel: YamlChannel) => {
|
|
253
|
+
const live = openLive(ctx, channel);
|
|
254
|
+
const codes = [200];
|
|
255
|
+
const args = ["preferences", "GET", "/preferences", codes] as const;
|
|
256
|
+
const step = await live.storefront<unknown>(...args);
|
|
257
|
+
if (!step.ok) return fromLive(ctx.locale, step.result);
|
|
258
|
+
return step.value;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const CODES = new Set([
|
|
262
|
+
"TOTAL_BELOW_MINIMUM",
|
|
263
|
+
"TOTAL_ABOVE_MAXIMUM",
|
|
264
|
+
"ITEMS_BELOW_MINIMUM",
|
|
265
|
+
"ITEMS_ABOVE_MAXIMUM",
|
|
266
|
+
"DAILY_ORDER_LIMIT_REACHED"
|
|
267
|
+
]);
|
|
268
|
+
|
|
269
|
+
const runChk03 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
270
|
+
const ready = prepare(ctx, LIVE);
|
|
271
|
+
if (isOutcome(ready)) return ready;
|
|
272
|
+
const prefs = await preferencesOf(ctx, ready);
|
|
273
|
+
if (isOutcome(prefs)) return prefs;
|
|
274
|
+
if (rulesAreDefault(prefs)) return say(ctx.locale, "skip", COPY.rules);
|
|
275
|
+
const live = openLive(ctx, ready);
|
|
276
|
+
const body = { items: [], total: "0" };
|
|
277
|
+
const codes = [200, 400, 422];
|
|
278
|
+
const args = ["rules", "POST", "/orders/validate", codes, body] as const;
|
|
279
|
+
const step = await live.probe(...args);
|
|
280
|
+
if (!step.ok) return fromLive(ctx.locale, step.result);
|
|
281
|
+
const raw = JSON.stringify(step.value.json ?? "");
|
|
282
|
+
const hit = [...CODES].some(code => raw.includes(code));
|
|
283
|
+
if (!hit) return say(ctx.locale, "fail", COPY.validate);
|
|
284
|
+
return { status: "pass" };
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const runChk04 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
288
|
+
const ready = prepare(ctx, LIVE);
|
|
289
|
+
if (isOutcome(ready)) return ready;
|
|
290
|
+
const prefs = await preferencesOf(ctx, ready);
|
|
291
|
+
if (isOutcome(prefs)) return prefs;
|
|
292
|
+
const fields = guestFields(prefs);
|
|
293
|
+
if (!fields) return say(ctx.locale, "skip", COPY.anonymous);
|
|
294
|
+
const modes = Object.values(fields);
|
|
295
|
+
if (modes.length === 0) return say(ctx.locale, "skip", COPY.anonymous);
|
|
296
|
+
const known = new Set(["required", "optional", "hidden"]);
|
|
297
|
+
const ok = modes.every(mode => typeof mode === "string" && known.has(mode));
|
|
298
|
+
if (!ok) return say(ctx.locale, "fail", COPY.anonymous);
|
|
299
|
+
return { status: "pass" };
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const runChk05 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
303
|
+
const skipped = applies(ctx, "CHK-05", new Set(["kiosk"]));
|
|
304
|
+
if (skipped) return skipped;
|
|
305
|
+
const named = namedOrderSkip(ctx, "CHK-05");
|
|
306
|
+
if (named) return named;
|
|
307
|
+
const id = textOf(ctx.env.FIRESPARK_DOCTOR_ORDER_ID);
|
|
308
|
+
if (!id) return say(ctx.locale, "fail", COPY.orderId);
|
|
309
|
+
const ready = prepare(ctx, LIVE);
|
|
310
|
+
if (isOutcome(ready)) return ready;
|
|
311
|
+
const live = openLive(ctx, ready);
|
|
312
|
+
const codes = [200];
|
|
313
|
+
const args = ["cancel", "POST", `/orders/${id}/cancel`, codes, {}] as const;
|
|
314
|
+
const step = await live.storefront<unknown>(...args);
|
|
315
|
+
if (!step.ok) return fromLive(ctx.locale, step.result);
|
|
316
|
+
return { status: "pass" };
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const guestFields = (value: unknown) => {
|
|
320
|
+
if (!isRecord(value) || !isRecord(value.checkout)) return null;
|
|
321
|
+
const guest = value.checkout.anonymous_customer;
|
|
322
|
+
if (!isRecord(guest) || !isRecord(guest.fields)) return null;
|
|
323
|
+
return guest.fields;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const runChk02 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
327
|
+
const fields = [...LIVE, "store_id", "fulfillment_id"] as const;
|
|
328
|
+
const ready = prepare(ctx, fields);
|
|
329
|
+
if (isOutcome(ready)) return ready;
|
|
330
|
+
const loaded = await menusOf(ctx, ready);
|
|
331
|
+
if (isOutcome(loaded)) return loaded;
|
|
332
|
+
const menus = loaded.menus as Menu[];
|
|
333
|
+
const wanted = ctx.env.FIRESPARK_DOCTOR_PRODUCT_ID;
|
|
334
|
+
const product = pickProduct(menus, wanted);
|
|
335
|
+
if (!product?.id) return say(ctx.locale, "fail", COPY.list);
|
|
336
|
+
const config = liveConfigOf(ready, ctx.env);
|
|
337
|
+
const lineUid = randomUUID();
|
|
338
|
+
const id = "validate-only";
|
|
339
|
+
const b1 = { id, config, product, lineUid };
|
|
340
|
+
const built = buildOrderBody(b1);
|
|
341
|
+
const store_id = textOf(ready.store_id);
|
|
342
|
+
const channel_id = textOf(ready.channel_id);
|
|
343
|
+
const fulfillment_id = textOf(ready.fulfillment_id);
|
|
344
|
+
const scope = { store_id, channel_id, fulfillment_id };
|
|
345
|
+
const body = { ...scope, lines: built.lines, totals: built.totals };
|
|
346
|
+
const codes = [200];
|
|
347
|
+
const args = ["validate", "POST", "/orders/validate", codes, body] as const;
|
|
348
|
+
const step = await loaded.live.storefront<{ ok?: boolean }>(...args);
|
|
349
|
+
if (!step.ok) return fromLive(ctx.locale, step.result);
|
|
350
|
+
if (step.value.ok !== true) return say(ctx.locale, "fail", COPY.validate);
|
|
351
|
+
return { status: "pass" };
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const runChk01 = async (ctx: RunCtx): Promise<Outcome> => {
|
|
355
|
+
const skipped = namedOrderSkip(ctx, "CHK-01");
|
|
356
|
+
if (skipped) return skipped;
|
|
357
|
+
const fields = [...LIVE, "store_id", "fulfillment_id", "payment"] as const;
|
|
358
|
+
const ready = prepare(ctx, fields);
|
|
359
|
+
if (isOutcome(ready)) return ready;
|
|
360
|
+
if (!paymentOf(textOf(ready.payment))) {
|
|
361
|
+
return say(ctx.locale, "fail", COPY.notCash);
|
|
362
|
+
}
|
|
363
|
+
const loaded = await menusOf(ctx, ready);
|
|
364
|
+
if (isOutcome(loaded)) return loaded;
|
|
365
|
+
const product = pickProduct(loaded.menus as Menu[]);
|
|
366
|
+
if (!product?.id) return say(ctx.locale, "fail", COPY.list);
|
|
367
|
+
const id = doctorOrderId();
|
|
368
|
+
const config = liveConfigOf(ready, ctx.env);
|
|
369
|
+
const lineUid = randomUUID();
|
|
370
|
+
const built = buildOrderBody({ id, config, product, lineUid });
|
|
371
|
+
const codes = [201, 202];
|
|
372
|
+
const create = ["order-create", "POST", "/orders", codes, built] as const;
|
|
373
|
+
const created = await loaded.live.storefront<{ id?: string }>(...create);
|
|
374
|
+
if (!created.ok) return fromLive(ctx.locale, created.result);
|
|
375
|
+
const payment = { type: "CASH" };
|
|
376
|
+
const placePath = `/orders/${encodeURIComponent(id)}/place`;
|
|
377
|
+
const p1 = ["order-place", "POST", placePath] as const;
|
|
378
|
+
const placeArgs = [...p1, codes, { payment }] as const;
|
|
379
|
+
const place = loaded.live.storefront<{ status?: string }>;
|
|
380
|
+
const placed = await place(...placeArgs);
|
|
381
|
+
if (!placed.ok) return fromLive(ctx.locale, placed.result);
|
|
382
|
+
const status = placed.value.status ?? "";
|
|
383
|
+
if (!PLACE.has(status)) {
|
|
384
|
+
return say(ctx.locale, "fail", COPY.placeStatus, { status: status || "—" });
|
|
385
|
+
}
|
|
386
|
+
return { status: "pass" };
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
export const CHECKOUT_BINDINGS: Record<string, CheckBinding> = {
|
|
390
|
+
"MEN-13": withSafeFix(runMen13),
|
|
391
|
+
"MEN-14": withSafeFix(runMen14),
|
|
392
|
+
"MEN-15": withSafeFix(runMen15),
|
|
393
|
+
"MEN-16": withSafeFix(runMen16),
|
|
394
|
+
"MEN-17": withSafeFix(runMen17),
|
|
395
|
+
"CHK-01": withSafeFix(runChk01),
|
|
396
|
+
"CHK-02": withSafeFix(runChk02),
|
|
397
|
+
"CHK-03": withSafeFix(runChk03),
|
|
398
|
+
"CHK-04": withSafeFix(runChk04),
|
|
399
|
+
"CHK-05": withSafeFix(runChk05)
|
|
400
|
+
};
|