@fire-spark/core 0.8.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -280,8 +280,15 @@ declare const reorderOrder: (id: string, body?: OrderReorderInput, options?: Ser
280
280
  * unpaid order, cancels it (`VOIDED`) and returns the new `OPEN` order with
281
281
  * the same content. Rejects with `StorefrontHttpError` 400
282
282
  * (`details.code: "ORDER_PAYMENT_CAPTURED"`) when a payment was captured.
283
+ * Optional `body.order_id` must be an id `genId` can produce (`isGenId`;
284
+ * `genOrderId` is one option, including `BB2192100000001`) and becomes
285
+ * the new order id.
286
+ * `400` `ORDER_ID_INVALID` when the format is wrong. `409`
287
+ * `ORDER_ID_EXISTS` when that id is already taken.
283
288
  */
284
- declare const reopenOrder: (id: string, options?: ServiceHttpOptions) => Promise<Order$1>;
289
+ declare const reopenOrder: (id: string, options?: ServiceHttpOptions, body?: {
290
+ order_id?: string;
291
+ }) => Promise<Order$1>;
285
292
  declare const validateOrder: (body: OrderValidateInput, options?: ServiceHttpOptions) => Promise<OrderValidateOutput>;
286
293
  //#endregion
287
294
  //#region src/order/payments/orchestrator.d.ts
@@ -382,6 +389,11 @@ type PayIntentResult = {
382
389
  };
383
390
  type ReverseAllOptions = {
384
391
  onProgress?: PaymentProgressListener;
392
+ /**
393
+ * Only these tenders (e.g. `device_reversal.pending` of a failed
394
+ * checkout); omitted = every charged tender. An empty list voids none.
395
+ */
396
+ tenderIds?: readonly string[];
385
397
  };
386
398
  /** Settled tenders plus PENDING `confirm_after_inject`, order currency. */
387
399
  declare const coveredAmount: (intents: Intents) => number;
@@ -561,11 +573,19 @@ declare class Order {
561
573
  * `OPEN` order with the same content. This instance is closed afterwards;
562
574
  * replace it with the returned `Order` (same HTTP options and listener).
563
575
  *
576
+ * Pass `orderId` from `genId` (or `genOrderId`) to set the new order id.
577
+ * The request body field is `order_id`. Any id `isGenId` accepts is
578
+ * valid, including a literal glued to the hash
579
+ * (`genId().literal("BB21921").hash(8, "numeric").build()`). Omit it
580
+ * and the server generates one.
581
+ * `400` `ORDER_ID_INVALID` when the format is wrong. `409`
582
+ * `ORDER_ID_EXISTS` when that id is already taken.
583
+ *
564
584
  * Rejects with `StorefrontHttpError` 400 and
565
585
  * `details.code: "ORDER_PAYMENT_CAPTURED"` when a payment was already
566
586
  * captured; nothing changes then and this instance stays usable.
567
587
  */
568
- reopen(): Promise<Order>;
588
+ reopen(orderId?: string): Promise<Order>;
569
589
  refundOrder(): Promise<void>;
570
590
  validateOrder(): Promise<void>;
571
591
  /**
@@ -1295,6 +1315,12 @@ interface VerifyWebhookSignatureInput {
1295
1315
  declare const verifyWebhookSignature: (input: VerifyWebhookSignatureInput) => Promise<boolean>;
1296
1316
  //#endregion
1297
1317
  //#region src/shared/genId.d.ts
1318
+ declare const ID_HASH_FORMATS: readonly ["alphanumeric", "numeric", "alphabetic"];
1319
+ type IdHashFormat = (typeof ID_HASH_FORMATS)[number];
1320
+ interface GenOrderIdOptions {
1321
+ length?: number;
1322
+ format?: IdHashFormat;
1323
+ }
1298
1324
  type IdBuilder = {
1299
1325
  /**
1300
1326
  * Keeps only `[A-Za-z0-9]`, cut to 32 characters. `fallback` is used only
@@ -1303,19 +1329,35 @@ type IdBuilder = {
1303
1329
  literal: (value: string | null | undefined, fallback?: string) => IdBuilder;
1304
1330
  /** Appends `-`, the only separator allowed. */
1305
1331
  separator: () => IdBuilder;
1306
- /** Cryptographically random `[0-9a-z]`; at least 12 characters. */
1307
- hash: (length?: number) => IdBuilder;
1332
+ /**
1333
+ * Cryptographically random hash of `length` characters (at least 8,
1334
+ * default 12). `format` is `alphanumeric` (`0-9a-z`, default),
1335
+ * `numeric` (`0-9`), or `alphabetic` (`a-z`).
1336
+ */
1337
+ hash: (length?: number, format?: IdHashFormat) => IdBuilder;
1308
1338
  /** New id on every call; throws when the shape is unsafe. */
1309
1339
  build: () => string;
1310
1340
  };
1311
1341
  /**
1312
- * Safe id builder: `genId().literal("ord").separator().hash(12).build()`.
1313
- * Literals are cleaned to `[A-Za-z0-9]`; `-` is the only separator and a
1314
- * random hash of at least 12 characters is required.
1342
+ * Safe id builder: `genId().literal("ord").separator().hash().build()`.
1343
+ * Literals are cleaned to `[A-Za-z0-9]`; `-` is the only separator. A
1344
+ * random hash of at least 8 characters is required (default 12). Parts
1345
+ * join with `""`, so a literal can sit directly against the hash.
1315
1346
  */
1316
1347
  declare const genId: () => IdBuilder;
1317
- /** `ord-<store>-<hash>`; `nostore` when the store id is blank. */
1318
- declare const genOrderId: (storeId?: string | null) => string;
1348
+ /**
1349
+ * Always `ord-<store>-<hash>`; `nostore` when the store id is blank.
1350
+ * Omitted length and format stay at 12 lowercase alphanumeric characters.
1351
+ * `length` may be as short as 8.
1352
+ */
1353
+ declare const genOrderId: (storeId?: string | null, options?: GenOrderIdOptions) => string;
1354
+ /**
1355
+ * True for an id {@link genId} can produce. The whole value is
1356
+ * `[A-Za-z0-9-]`, at most 64 characters, with no leading, trailing, or
1357
+ * doubled `-`, and it contains a run of at least 8 `[0-9a-z]`. A literal
1358
+ * may sit directly against that run (`BB2192100000001`).
1359
+ */
1360
+ declare const isGenId: (value: string) => boolean;
1319
1361
  //#endregion
1320
1362
  //#region src/rules/RulesEngine.d.ts
1321
1363
  declare class RulesEngine {
@@ -1512,4 +1554,4 @@ declare const validateCardholderName: (value: string) => CardholderNameError | n
1512
1554
  */
1513
1555
  declare const cardPreviewGroups: (value: string) => CardPreviewSlot[][];
1514
1556
  //#endregion
1515
- export { type ApplyRedeemInput, type AuthConfigureOptions, CARD_BRANDS, type CardBrand, type CardBrandSpec, type CardCvcError, type CardExpiry, type CardExpiryError, type CardNumberError, type CardPreviewSlot, type CardholderNameError, type CreateOrderOptions, type CreateSessionOptions, type CustomizationPath, type CustomizationSession, DEFAULT_RECOMMENDATION_TYPES, type ExchangeOptions, type FailedCharge, type FlatMenuCategory, type GroupDisplayType, type IdBuilder, type ListMenusStreamOptions, type ListPaymentsOptions, type Menu, type MenuCategoryNode, type MenuLookup, type MilestoneSteps, type MilestoneTrackItem, type ModifierControlType, type ModifierCtx, type ModifierGroupCtx, type NestedMenuCategory, Order, type OrderCustomerSource, type OrderDeviceCredential, type OrderDraft, type OrderIssue, type OrderIssueCode, type OrderLineDiff, type OrderLineIssue, type OrderLineIssueCode, OrderPaymentRequiredError, type OrderScope, OrderSyncPendingError, OrderSyncRejectedError, OrderValidationError, type OrderValidationResult, type PayIntentOptions, type PayIntentResult, type PayOptions, type PaymentCardBrand, type PaymentProgressEvent, type PaymentProgressListener, type PaymentRunOptions, type PaymentSessionOptions, type PricingModifier, type Product, type ProductCtx, type QuantitiesByPath, type RecommendationType, RecurringDateCron, type RedeemPaymentRow, type RemoveRedeemInput, type ReorderOrderOptions, type ResetOptions, type ReversalOutcome, type ReverseAllOptions, RulesEngine, type ScheduleWindow, type SelectionStatus, type SelectionsByPath, type SerializedError, type ServiceHttpOptions, type SimpleTenderPayment, type Snapshot, type StorefrontAccessToken, type StorefrontErrorDetails, StorefrontHttpError, type TrackWorkflow, type ValidateOptions, type ValidateOrderAgainstMenuInput, type Variant, type VariantGroup, type VariantGroupCtx, type VariantOptionCtx, type VariantSelection, type VerifyWebhookSignatureInput, type VisitedState, addLines, auth, buildCustomerSnapshot, capRedeems, cappedRedeems, cardBrandSpec, cardDigits, cardNumberGaps, cardNumberMaxLength, cardPreviewGroups, caretAfterDigits, collectOrderLineDiffs, computeStartingPrice, countCartProducts, coveredAmount, createSession, currentNodes, cvcMaxLength, defaultConditionError, detectCardBrand, fitRedeems, flattenMenuCategories, flushLogs, formatCardNumber, formatCvc, formatExpiry, fromOrderLines, genId, genOrderId, getLogContext, getScheduleWindow, indexMenu, isCardNumberComplete, isCardNumberDefinitelyInvalid, isCheckoutFieldFilled, isCvcComplete, isDeviceCharged, isDeviceNotLinked, isExpiryComplete, isKioskUnauthorized, isMilestoneStepDone, isOnSchedule, isOrderPaymentRequiredError, isOrderValidationError, isPosDeviceNotLinked, isPosUnauthorized, isRealPaymentMethod, isStorefrontNotFound, isVariantBase, linePayableTotal, listMenusStream, localizePosError, log, logError, luhnCheck, matchAbsoluteDateOperator, matchBooleanOperator, matchCurrencyOperator, matchEnumOperator, matchNumberOperator, matchRecurringDateOperator, matchRelativeDateOperator, matchStringOperator, milestoneSteps, milestoneTrack, nestMenuCategories, normalizeCardholderName, orderPayableTotal, orderRedeemedTotal, orderRedeems, parseExpiry, paymentMatchesClientConditions, rebuildRootGroups, recomputeTotals, redeemedTotal, redemptionCap, redemptionSteps, remainingAmount, removeLine, replaceLine, resolveCheckout, resolveModifierImageUrl, reversePayment, runPaymentAttempt, selectablePayments, serializeError, services, setLineQuantity, storefrontJson, toOrderLines, toPaymentCardBrand, validateCardNumber, validateCardholderName, validateCvc, validateExpiry, validateOrderAgainstMenu, verifyWebhookSignature, visitedStates };
1557
+ export { type ApplyRedeemInput, type AuthConfigureOptions, CARD_BRANDS, type CardBrand, type CardBrandSpec, type CardCvcError, type CardExpiry, type CardExpiryError, type CardNumberError, type CardPreviewSlot, type CardholderNameError, type CreateOrderOptions, type CreateSessionOptions, type CustomizationPath, type CustomizationSession, DEFAULT_RECOMMENDATION_TYPES, type ExchangeOptions, type FailedCharge, type FlatMenuCategory, type GenOrderIdOptions, type GroupDisplayType, ID_HASH_FORMATS, type IdBuilder, type IdHashFormat, type ListMenusStreamOptions, type ListPaymentsOptions, type Menu, type MenuCategoryNode, type MenuLookup, type MilestoneSteps, type MilestoneTrackItem, type ModifierControlType, type ModifierCtx, type ModifierGroupCtx, type NestedMenuCategory, Order, type OrderCustomerSource, type OrderDeviceCredential, type OrderDraft, type OrderIssue, type OrderIssueCode, type OrderLineDiff, type OrderLineIssue, type OrderLineIssueCode, OrderPaymentRequiredError, type OrderScope, OrderSyncPendingError, OrderSyncRejectedError, OrderValidationError, type OrderValidationResult, type PayIntentOptions, type PayIntentResult, type PayOptions, type PaymentCardBrand, type PaymentProgressEvent, type PaymentProgressListener, type PaymentRunOptions, type PaymentSessionOptions, type PricingModifier, type Product, type ProductCtx, type QuantitiesByPath, type RecommendationType, RecurringDateCron, type RedeemPaymentRow, type RemoveRedeemInput, type ReorderOrderOptions, type ResetOptions, type ReversalOutcome, type ReverseAllOptions, RulesEngine, type ScheduleWindow, type SelectionStatus, type SelectionsByPath, type SerializedError, type ServiceHttpOptions, type SimpleTenderPayment, type Snapshot, type StorefrontAccessToken, type StorefrontErrorDetails, StorefrontHttpError, type TrackWorkflow, type ValidateOptions, type ValidateOrderAgainstMenuInput, type Variant, type VariantGroup, type VariantGroupCtx, type VariantOptionCtx, type VariantSelection, type VerifyWebhookSignatureInput, type VisitedState, addLines, auth, buildCustomerSnapshot, capRedeems, cappedRedeems, cardBrandSpec, cardDigits, cardNumberGaps, cardNumberMaxLength, cardPreviewGroups, caretAfterDigits, collectOrderLineDiffs, computeStartingPrice, countCartProducts, coveredAmount, createSession, currentNodes, cvcMaxLength, defaultConditionError, detectCardBrand, fitRedeems, flattenMenuCategories, flushLogs, formatCardNumber, formatCvc, formatExpiry, fromOrderLines, genId, genOrderId, getLogContext, getScheduleWindow, indexMenu, isCardNumberComplete, isCardNumberDefinitelyInvalid, isCheckoutFieldFilled, isCvcComplete, isDeviceCharged, isDeviceNotLinked, isExpiryComplete, isGenId, isKioskUnauthorized, isMilestoneStepDone, isOnSchedule, isOrderPaymentRequiredError, isOrderValidationError, isPosDeviceNotLinked, isPosUnauthorized, isRealPaymentMethod, isStorefrontNotFound, isVariantBase, linePayableTotal, listMenusStream, localizePosError, log, logError, luhnCheck, matchAbsoluteDateOperator, matchBooleanOperator, matchCurrencyOperator, matchEnumOperator, matchNumberOperator, matchRecurringDateOperator, matchRelativeDateOperator, matchStringOperator, milestoneSteps, milestoneTrack, nestMenuCategories, normalizeCardholderName, orderPayableTotal, orderRedeemedTotal, orderRedeems, parseExpiry, paymentMatchesClientConditions, rebuildRootGroups, recomputeTotals, redeemedTotal, redemptionCap, redemptionSteps, remainingAmount, removeLine, replaceLine, resolveCheckout, resolveModifierImageUrl, reversePayment, runPaymentAttempt, selectablePayments, serializeError, services, setLineQuantity, storefrontJson, toOrderLines, toPaymentCardBrand, validateCardNumber, validateCardholderName, validateCvc, validateExpiry, validateOrderAgainstMenu, verifyWebhookSignature, visitedStates };
package/dist/index.js CHANGED
@@ -2514,10 +2514,15 @@ const listOrders = (input = {}, options) => {
2514
2514
  return storefrontJson(listQuery(input), {}, options);
2515
2515
  };
2516
2516
  const createOrder = (body, options) => {
2517
- return storefrontJson(orderPath(), {
2517
+ const init = {
2518
2518
  method: "POST",
2519
2519
  body: JSON.stringify(body)
2520
- }, options);
2520
+ };
2521
+ return storefrontJson(...[
2522
+ orderPath(),
2523
+ init,
2524
+ options
2525
+ ]);
2521
2526
  };
2522
2527
  const getOrder = (id, options) => {
2523
2528
  return storefrontJson(orderPath(id), {}, options);
@@ -2602,17 +2607,23 @@ const listOrderFulfillments = async (ids, options) => {
2602
2607
  const unique = [...new Set(ids)];
2603
2608
  const pages = [];
2604
2609
  for (let i = 0; i < unique.length; i += LIST_FULFILLMENT_LIMIT) pages.push(unique.slice(i, i + LIST_FULFILLMENT_LIMIT));
2605
- return (await Promise.all(pages.map((page) => {
2610
+ const load = (page) => {
2606
2611
  const params = new URLSearchParams();
2607
2612
  params.set("ids", page.join(","));
2608
2613
  return storefrontJson(`${orderPath()}/fulfillments?${params.toString()}`, {}, options);
2609
- }))).flat();
2614
+ };
2615
+ return (await Promise.all(pages.map(load))).flat();
2610
2616
  };
2611
2617
  const updateOrder = (id, body, options) => {
2612
- return storefrontJson(orderPath(id), {
2618
+ const init = {
2613
2619
  method: "PATCH",
2614
2620
  body: JSON.stringify(body)
2615
- }, options);
2621
+ };
2622
+ return storefrontJson(...[
2623
+ orderPath(id),
2624
+ init,
2625
+ options
2626
+ ]);
2616
2627
  };
2617
2628
  const cancelOrder = (id, options) => {
2618
2629
  return storefrontJson(`${orderPath(id)}/cancel`, { method: "POST" }, options);
@@ -2631,9 +2642,21 @@ const reorderOrder = (id, body = {}, options) => {
2631
2642
  * unpaid order, cancels it (`VOIDED`) and returns the new `OPEN` order with
2632
2643
  * the same content. Rejects with `StorefrontHttpError` 400
2633
2644
  * (`details.code: "ORDER_PAYMENT_CAPTURED"`) when a payment was captured.
2645
+ * Optional `body.order_id` must be an id `genId` can produce (`isGenId`;
2646
+ * `genOrderId` is one option, including `BB2192100000001`) and becomes
2647
+ * the new order id.
2648
+ * `400` `ORDER_ID_INVALID` when the format is wrong. `409`
2649
+ * `ORDER_ID_EXISTS` when that id is already taken.
2634
2650
  */
2635
- const reopenOrder = (id, options) => {
2636
- return storefrontJson(`${orderPath(id)}/reopen`, { method: "POST" }, options);
2651
+ const reopenOrder = (id, options, body) => {
2652
+ const path = `${orderPath(id)}/reopen`;
2653
+ const method = "POST";
2654
+ const orderId = body?.order_id;
2655
+ if (orderId === void 0) return storefrontJson(path, { method }, options);
2656
+ return storefrontJson(path, {
2657
+ method,
2658
+ body: JSON.stringify({ order_id: orderId })
2659
+ }, options);
2637
2660
  };
2638
2661
  const parseValidationDetails = (body) => {
2639
2662
  if (!body || typeof body !== "object") return null;
@@ -2647,10 +2670,15 @@ const parseValidationDetails = (body) => {
2647
2670
  };
2648
2671
  };
2649
2672
  const validateOrder = async (body, options) => {
2650
- const response = await storefrontRequest(orderPath("validate"), {
2673
+ const init = {
2651
2674
  method: "POST",
2652
2675
  body: JSON.stringify(body)
2653
- }, options);
2676
+ };
2677
+ const response = await storefrontRequest(...[
2678
+ orderPath("validate"),
2679
+ init,
2680
+ options
2681
+ ]);
2654
2682
  if (response.ok) {
2655
2683
  const json = await response.json();
2656
2684
  if (json.data === void 0) throw new Error("Storefront response missing data");
@@ -3118,9 +3146,44 @@ const services = {
3118
3146
  localizeError: localizePosError
3119
3147
  }
3120
3148
  };
3149
+ const DEFAULT_ID_HASH_LENGTH = 12;
3150
+ const ID_HASH_FORMATS = [
3151
+ "alphanumeric",
3152
+ "numeric",
3153
+ "alphabetic"
3154
+ ];
3121
3155
  const DISALLOWED = /[^A-Za-z0-9]/g;
3122
- const ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz";
3123
- const BYTE_LIMIT = 252;
3156
+ const alnum = "0123456789abcdefghijklmnopqrstuvwxyz";
3157
+ const digits = "0123456789";
3158
+ const letters = "abcdefghijklmnopqrstuvwxyz";
3159
+ const b1 = {
3160
+ alphanumeric: alnum,
3161
+ numeric: digits
3162
+ };
3163
+ const b2 = { alphabetic: letters };
3164
+ const ALPHABETS = {
3165
+ ...b1,
3166
+ ...b2
3167
+ };
3168
+ const ORDER_ID_PREFIX = "ord";
3169
+ const ORDER_ID_STORE_FALLBACK = "nostore";
3170
+ const NUMERIC_WARN_LENGTH = 16;
3171
+ /** Largest multiple of `size` that is still below 256. */
3172
+ const byteLimit = (size) => Math.floor(255 / size) * size;
3173
+ const hashFormat = (value) => {
3174
+ const known = ID_HASH_FORMATS.find((item) => item === value);
3175
+ if (known) return known;
3176
+ throw new Error(`Unknown id hash format: ${value}`);
3177
+ };
3178
+ const warnShortHash = (length, format) => {
3179
+ if (length < DEFAULT_ID_HASH_LENGTH) {
3180
+ const head = `Id hash is ${length} characters`;
3181
+ console.warn(`${head}; ${`hashes shorter than ${DEFAULT_ID_HASH_LENGTH} are easier to guess`}`);
3182
+ }
3183
+ if (format !== "numeric" || length >= NUMERIC_WARN_LENGTH) return;
3184
+ const head = `Numeric id hash is ${length} digits`;
3185
+ console.warn(`${head}; ${`hashes shorter than ${NUMERIC_WARN_LENGTH} are easier to guess`}`);
3186
+ };
3124
3187
  const toLiteral = (value, fallback) => {
3125
3188
  const trimmed = (value ?? "").trim();
3126
3189
  const source = trimmed === "" ? fallback : trimmed;
@@ -3129,16 +3192,17 @@ const toLiteral = (value, fallback) => {
3129
3192
  if (cleaned === "") throw new Error(`Id literal has no alphanumeric characters: "${source}"`);
3130
3193
  return cleaned.slice(0, 32);
3131
3194
  };
3132
- const randomChars = (length) => {
3195
+ const randomChars = (length, alphabet) => {
3133
3196
  const c = globalThis.crypto;
3134
3197
  if (typeof c?.getRandomValues !== "function") throw new Error("crypto.getRandomValues is required");
3198
+ const limit = byteLimit(alphabet.length);
3135
3199
  let out = "";
3136
3200
  const bytes = new Uint8Array(length * 2);
3137
3201
  while (out.length < length) {
3138
3202
  c.getRandomValues(bytes);
3139
3203
  for (const byte of bytes) {
3140
- if (byte >= BYTE_LIMIT) continue;
3141
- out += ALPHABET[byte % 36];
3204
+ if (byte >= limit) continue;
3205
+ out += alphabet[byte % alphabet.length];
3142
3206
  if (out.length === length) break;
3143
3207
  }
3144
3208
  }
@@ -3158,7 +3222,7 @@ const assertShape = (parts) => {
3158
3222
  const render = (part) => {
3159
3223
  if (part.kind === "literal") return part.value;
3160
3224
  if (part.kind === "separator") return "-";
3161
- return randomChars(part.length);
3225
+ return randomChars(part.length, ALPHABETS[part.format]);
3162
3226
  };
3163
3227
  const builder = (parts) => {
3164
3228
  return {
@@ -3170,12 +3234,16 @@ const builder = (parts) => {
3170
3234
  return builder([...parts, literal]);
3171
3235
  },
3172
3236
  separator: () => builder([...parts, { kind: "separator" }]),
3173
- hash: (length = 12) => {
3174
- if (!Number.isInteger(length) || length < 12) throw new Error(`Id hash must be an integer ≥ 12, got ${length}`);
3175
- return builder([...parts, {
3237
+ hash: (length = DEFAULT_ID_HASH_LENGTH, format = "alphanumeric") => {
3238
+ if (!Number.isInteger(length) || length < 8) throw new Error(`Id hash must be an integer ≥ 8, got ${length}`);
3239
+ const kind = hashFormat(format);
3240
+ warnShortHash(length, kind);
3241
+ const part = {
3176
3242
  kind: "hash",
3177
- length
3178
- }]);
3243
+ length,
3244
+ format: kind
3245
+ };
3246
+ return builder([...parts, part]);
3179
3247
  },
3180
3248
  build: () => {
3181
3249
  assertShape(parts);
@@ -3186,14 +3254,37 @@ const builder = (parts) => {
3186
3254
  };
3187
3255
  };
3188
3256
  /**
3189
- * Safe id builder: `genId().literal("ord").separator().hash(12).build()`.
3190
- * Literals are cleaned to `[A-Za-z0-9]`; `-` is the only separator and a
3191
- * random hash of at least 12 characters is required.
3257
+ * Safe id builder: `genId().literal("ord").separator().hash().build()`.
3258
+ * Literals are cleaned to `[A-Za-z0-9]`; `-` is the only separator. A
3259
+ * random hash of at least 8 characters is required (default 12). Parts
3260
+ * join with `""`, so a literal can sit directly against the hash.
3192
3261
  */
3193
3262
  const genId = () => builder([]);
3194
- /** `ord-<store>-<hash>`; `nostore` when the store id is blank. */
3195
- const genOrderId = (storeId) => {
3196
- return genId().literal("ord").separator().literal(storeId, "nostore").separator().hash().build();
3263
+ /**
3264
+ * Always `ord-<store>-<hash>`; `nostore` when the store id is blank.
3265
+ * Omitted length and format stay at 12 lowercase alphanumeric characters.
3266
+ * `length` may be as short as 8.
3267
+ */
3268
+ const genOrderId = (storeId, options) => {
3269
+ return genId().literal(ORDER_ID_PREFIX).separator().literal(storeId, ORDER_ID_STORE_FALLBACK).separator().hash(options?.length, options?.format).build();
3270
+ };
3271
+ const ID_BODY = /^[A-Za-z0-9-]+$/;
3272
+ const HASH_RUN = new RegExp(`[0-9a-z]{8,}`);
3273
+ /**
3274
+ * True for an id {@link genId} can produce. The whole value is
3275
+ * `[A-Za-z0-9-]`, at most 64 characters, with no leading, trailing, or
3276
+ * doubled `-`, and it contains a run of at least 8 `[0-9a-z]`. A literal
3277
+ * may sit directly against that run (`BB2192100000001`).
3278
+ */
3279
+ const isGenId = (value) => {
3280
+ if (value.length < 8) return false;
3281
+ if (value.length > 64) return false;
3282
+ if (!ID_BODY.test(value)) return false;
3283
+ const doubled = `--`;
3284
+ const starts = value.startsWith("-");
3285
+ const ends = value.endsWith("-");
3286
+ if (starts || ends || value.includes(doubled)) return false;
3287
+ return HASH_RUN.test(value);
3197
3288
  };
3198
3289
  //#endregion
3199
3290
  //#region src/order/OrderPaymentRequiredError.ts
@@ -4634,7 +4725,9 @@ var PaymentSession = class {
4634
4725
  async reverseAll(options = {}) {
4635
4726
  const intents = this.deps.getOrder().payment_intents;
4636
4727
  const outcomes = [];
4728
+ const only = options.tenderIds ? new Set(options.tenderIds) : null;
4637
4729
  for (const tender of chargedTenders(intents)) {
4730
+ if (only && !only.has(tender.id)) continue;
4638
4731
  if (tender.status === "IDLE") continue;
4639
4732
  const found = await this.contextFor(tender);
4640
4733
  if (!found) continue;
@@ -5335,34 +5428,16 @@ const buildLocalDraft = (insert) => {
5335
5428
  issued_at: null,
5336
5429
  resolved_at: null
5337
5430
  };
5338
- const p12 = {
5431
+ return {
5339
5432
  ...b1,
5340
- ...b2
5341
- };
5342
- const p34 = {
5433
+ ...b2,
5343
5434
  ...b3,
5344
- ...b4
5345
- };
5346
- const p56 = {
5435
+ ...b4,
5347
5436
  ...b5,
5348
- ...b6
5349
- };
5350
- const p78 = {
5437
+ ...b6,
5351
5438
  ...b7,
5352
5439
  ...b8
5353
5440
  };
5354
- const p1234 = {
5355
- ...p12,
5356
- ...p34
5357
- };
5358
- const p5678 = {
5359
- ...p56,
5360
- ...p78
5361
- };
5362
- return {
5363
- ...p1234,
5364
- ...p5678
5365
- };
5366
5441
  };
5367
5442
  const sameBrand = (left, right) => {
5368
5443
  return (optionalBrandId(left) ?? null) === (optionalBrandId(right) ?? null);
@@ -5370,14 +5445,22 @@ const sameBrand = (left, right) => {
5370
5445
  /** Newest OPEN order for customer + scope, if any. */
5371
5446
  const findOpenOrder = async (scope, http) => {
5372
5447
  const brandId = optionalBrandId(scope.brand_id);
5373
- return (await services.orders.list({
5448
+ const b1 = {
5374
5449
  store_id: scope.store_id,
5375
- channel_id: scope.channel_id,
5450
+ channel_id: scope.channel_id
5451
+ };
5452
+ const b2 = {
5376
5453
  fulfillment_id: scope.fulfillment_id,
5377
- ...brandId ? { brand_id: brandId } : {},
5378
- status: "OPEN",
5454
+ status: "OPEN"
5455
+ };
5456
+ const brand = brandId ? { brand_id: brandId } : {};
5457
+ const query = {
5458
+ ...b1,
5459
+ ...b2,
5460
+ ...brand,
5379
5461
  limit: 20
5380
- }, http)).find((order) => sameBrand(order.brand_id, brandId)) ?? null;
5462
+ };
5463
+ return (await services.orders.list(query, http)).find((order) => sameBrand(order.brand_id, brandId)) ?? null;
5381
5464
  };
5382
5465
  var Order = class Order {
5383
5466
  draft;
@@ -5409,32 +5492,59 @@ var Order = class Order {
5409
5492
  this.debounceMs = config.debounceMs;
5410
5493
  this.device = config.device;
5411
5494
  this.externalNotify = notify;
5412
- this.flush = createFlushController({
5495
+ const b1 = {
5413
5496
  getDraft: () => this.draft,
5414
- setDraft: (next) => {
5415
- this.replaceDraft(next);
5416
- },
5417
- getSynced: () => this.synced,
5418
- setSynced: (next) => {
5419
- this.synced = next;
5420
- },
5421
- isDeleted: () => this.deleted,
5422
- isPersisted: () => this.persisted,
5423
- setPersisted: (next) => {
5424
- this.persisted = next;
5425
- },
5497
+ getSynced: () => this.synced
5498
+ };
5499
+ const setDraft = (next) => {
5500
+ this.replaceDraft(next);
5501
+ };
5502
+ const setSynced = (next) => {
5503
+ this.synced = next;
5504
+ };
5505
+ const b2 = { setDraft };
5506
+ const b3 = { setSynced };
5507
+ const b4 = { isDeleted: () => this.deleted };
5508
+ const b5 = { isPersisted: () => this.persisted };
5509
+ const setPersisted = (next) => {
5510
+ this.persisted = next;
5511
+ };
5512
+ const b6 = { setPersisted };
5513
+ const b7 = {
5426
5514
  notify: () => this.notify(),
5427
- http: this.http,
5428
- debounceMs: config.debounceMs,
5515
+ http: this.http
5516
+ };
5517
+ const b8 = { debounceMs: config.debounceMs };
5518
+ const b9 = {
5429
5519
  fresh: config.fresh ?? false,
5430
5520
  sleep: config.sleep
5431
- });
5432
- this.payments = new PaymentSession({
5521
+ };
5522
+ const flush = {
5523
+ ...b1,
5524
+ ...b2,
5525
+ ...b3,
5526
+ ...b4,
5527
+ ...b5,
5528
+ ...b6,
5529
+ ...b7,
5530
+ ...b8,
5531
+ ...b9
5532
+ };
5533
+ this.flush = createFlushController(flush);
5534
+ const c1 = {
5433
5535
  getOrder: () => this.draft,
5434
- placeOrder: (input) => this.placeOrder(input),
5435
- applyOrder: (next) => this.applyServerOrder(next),
5436
5536
  http: this.http
5437
- });
5537
+ };
5538
+ const place = (input) => this.placeOrder(input);
5539
+ const apply = (next) => this.applyServerOrder(next);
5540
+ const c2 = { placeOrder: place };
5541
+ const c3 = { applyOrder: apply };
5542
+ const pay = {
5543
+ ...c1,
5544
+ ...c2,
5545
+ ...c3
5546
+ };
5547
+ this.payments = new PaymentSession(pay);
5438
5548
  }
5439
5549
  /**
5440
5550
  * Opens a cart session: reuses an existing OPEN order for the scope, or a
@@ -5472,13 +5582,19 @@ var Order = class Order {
5472
5582
  persisted: true,
5473
5583
  device: options.device
5474
5584
  }, notify);
5475
- return new Order({
5585
+ const d1 = {
5476
5586
  order: buildLocalDraft(body),
5477
5587
  http,
5478
5588
  debounceMs,
5479
- persisted: false,
5589
+ persisted: false
5590
+ };
5591
+ const d2 = {
5480
5592
  fresh: !reuse,
5481
5593
  device: options.device
5594
+ };
5595
+ return new Order({
5596
+ ...d1,
5597
+ ...d2
5482
5598
  }, notify);
5483
5599
  }
5484
5600
  static async reorder(orderId, options = {}, notify) {
@@ -5501,12 +5617,18 @@ var Order = class Order {
5501
5617
  }
5502
5618
  /** @internal test hook — create without POST */
5503
5619
  static fromExisting(config, notify) {
5504
- return new Order({
5620
+ const debounceMs = config.debounceMs ?? 300;
5621
+ const persisted = config.persisted ?? true;
5622
+ const d1 = {
5505
5623
  order: config.order,
5506
5624
  http: config.http,
5507
- debounceMs: config.debounceMs ?? 300,
5508
- persisted: config.persisted ?? true,
5509
- sleep: config.sleep
5625
+ debounceMs,
5626
+ persisted
5627
+ };
5628
+ const d2 = { sleep: config.sleep };
5629
+ return new Order({
5630
+ ...d1,
5631
+ ...d2
5510
5632
  }, notify);
5511
5633
  }
5512
5634
  /** Whether this session has a server row yet. */
@@ -5666,12 +5788,22 @@ var Order = class Order {
5666
5788
  }
5667
5789
  setScope(scope) {
5668
5790
  if (this.deleted) return;
5669
- this.draft = {
5791
+ const store_id = scope.store_id ?? this.draft.store_id;
5792
+ const channel_id = scope.channel_id ?? this.draft.channel_id;
5793
+ const prevFulfillment = this.draft.fulfillment_id;
5794
+ const fulfillment_id = scope.fulfillment_id ?? prevFulfillment;
5795
+ const kept = scope.brand_id === void 0;
5796
+ const nextBrand = optionalBrandId(scope.brand_id);
5797
+ const brand_id = kept ? this.draft.brand_id : nextBrand;
5798
+ const b1 = {
5670
5799
  ...this.draft,
5671
- store_id: scope.store_id ?? this.draft.store_id,
5672
- channel_id: scope.channel_id ?? this.draft.channel_id,
5673
- fulfillment_id: scope.fulfillment_id ?? this.draft.fulfillment_id,
5674
- brand_id: scope.brand_id === void 0 ? this.draft.brand_id : optionalBrandId(scope.brand_id)
5800
+ store_id,
5801
+ channel_id,
5802
+ fulfillment_id
5803
+ };
5804
+ this.draft = {
5805
+ ...b1,
5806
+ brand_id
5675
5807
  };
5676
5808
  this.rememberLogScope();
5677
5809
  this.scheduleAfterMutate();
@@ -5741,15 +5873,24 @@ var Order = class Order {
5741
5873
  * `OPEN` order with the same content. This instance is closed afterwards;
5742
5874
  * replace it with the returned `Order` (same HTTP options and listener).
5743
5875
  *
5876
+ * Pass `orderId` from `genId` (or `genOrderId`) to set the new order id.
5877
+ * The request body field is `order_id`. Any id `isGenId` accepts is
5878
+ * valid, including a literal glued to the hash
5879
+ * (`genId().literal("BB21921").hash(8, "numeric").build()`). Omit it
5880
+ * and the server generates one.
5881
+ * `400` `ORDER_ID_INVALID` when the format is wrong. `409`
5882
+ * `ORDER_ID_EXISTS` when that id is already taken.
5883
+ *
5744
5884
  * Rejects with `StorefrontHttpError` 400 and
5745
5885
  * `details.code: "ORDER_PAYMENT_CAPTURED"` when a payment was already
5746
5886
  * captured; nothing changes then and this instance stays usable.
5747
5887
  */
5748
- async reopen() {
5888
+ async reopen(orderId) {
5749
5889
  if (!this.persisted) throw new Error("Only a placed order is reopened");
5750
5890
  this.flush.cancel();
5891
+ const body = orderId !== void 0 ? { order_id: orderId } : void 0;
5751
5892
  const c1 = {
5752
- order: await services.orders.reopen(this.draft.id, this.http),
5893
+ order: await services.orders.reopen(this.draft.id, this.http, body),
5753
5894
  http: this.http,
5754
5895
  debounceMs: this.debounceMs
5755
5896
  };
@@ -5959,8 +6100,13 @@ var Order = class Order {
5959
6100
  idempotency_key,
5960
6101
  reason
5961
6102
  } : { idempotency_key };
5962
- const id = this.draft.id;
5963
- const next = await services.orders.voidTender(id, tenderId, body, this.http);
6103
+ const p1 = [
6104
+ this.draft.id,
6105
+ tenderId,
6106
+ body,
6107
+ this.http
6108
+ ];
6109
+ const next = await services.orders.voidTender(...p1);
5964
6110
  this.applyServerOrder(next);
5965
6111
  return this.getOrder();
5966
6112
  }
@@ -8791,4 +8937,4 @@ const cardPreviewGroups = (value) => {
8791
8937
  return groups;
8792
8938
  };
8793
8939
  //#endregion
8794
- export { CARD_BRANDS, DEFAULT_RECOMMENDATION_TYPES, Order, OrderPaymentRequiredError, OrderSyncPendingError, OrderSyncRejectedError, OrderValidationError, RecurringDateCron, RulesEngine, StorefrontHttpError, addLines, auth, buildCustomerSnapshot, capRedeems, cappedRedeems, cardBrandSpec, cardDigits, cardNumberGaps, cardNumberMaxLength, cardPreviewGroups, caretAfterDigits, collectOrderLineDiffs, computeStartingPrice, countCartProducts, coveredAmount, createSession, currentNodes, cvcMaxLength, defaultConditionError, detectCardBrand, fitRedeems, flattenMenuCategories, flushLogs, formatCardNumber, formatCvc, formatExpiry, fromOrderLines, genId, genOrderId, getLogContext, getScheduleWindow, indexMenu, isCardNumberComplete, isCardNumberDefinitelyInvalid, isCheckoutFieldFilled, isCvcComplete, isDeviceCharged, isDeviceNotLinked, isExpiryComplete, isKioskUnauthorized, isMilestoneStepDone, isOnSchedule, isOrderPaymentRequiredError, isOrderValidationError, isPosDeviceNotLinked, isPosUnauthorized, isRealPaymentMethod, isStorefrontNotFound, isVariantBase, linePayableTotal, listMenusStream, localizePosError, log, logError, luhnCheck, matchAbsoluteDateOperator, matchBooleanOperator, matchCurrencyOperator, matchEnumOperator, matchNumberOperator, matchRecurringDateOperator, matchRelativeDateOperator, matchStringOperator, milestoneSteps, milestoneTrack, nestMenuCategories, normalizeCardholderName, orderPayableTotal, orderRedeemedTotal, orderRedeems, parseExpiry, paymentMatchesClientConditions, rebuildRootGroups, recomputeTotals, redeemedTotal, redemptionCap, redemptionSteps, remainingAmount, removeLine, replaceLine, resolveCheckout, resolveModifierImageUrl, reversePayment, runPaymentAttempt, selectablePayments, serializeError, services, setLineQuantity, storefrontJson, toOrderLines, toPaymentCardBrand, validateCardNumber, validateCardholderName, validateCvc, validateExpiry, validateOrderAgainstMenu, verifyWebhookSignature, visitedStates };
8940
+ export { CARD_BRANDS, DEFAULT_RECOMMENDATION_TYPES, ID_HASH_FORMATS, Order, OrderPaymentRequiredError, OrderSyncPendingError, OrderSyncRejectedError, OrderValidationError, RecurringDateCron, RulesEngine, StorefrontHttpError, addLines, auth, buildCustomerSnapshot, capRedeems, cappedRedeems, cardBrandSpec, cardDigits, cardNumberGaps, cardNumberMaxLength, cardPreviewGroups, caretAfterDigits, collectOrderLineDiffs, computeStartingPrice, countCartProducts, coveredAmount, createSession, currentNodes, cvcMaxLength, defaultConditionError, detectCardBrand, fitRedeems, flattenMenuCategories, flushLogs, formatCardNumber, formatCvc, formatExpiry, fromOrderLines, genId, genOrderId, getLogContext, getScheduleWindow, indexMenu, isCardNumberComplete, isCardNumberDefinitelyInvalid, isCheckoutFieldFilled, isCvcComplete, isDeviceCharged, isDeviceNotLinked, isExpiryComplete, isGenId, isKioskUnauthorized, isMilestoneStepDone, isOnSchedule, isOrderPaymentRequiredError, isOrderValidationError, isPosDeviceNotLinked, isPosUnauthorized, isRealPaymentMethod, isStorefrontNotFound, isVariantBase, linePayableTotal, listMenusStream, localizePosError, log, logError, luhnCheck, matchAbsoluteDateOperator, matchBooleanOperator, matchCurrencyOperator, matchEnumOperator, matchNumberOperator, matchRecurringDateOperator, matchRelativeDateOperator, matchStringOperator, milestoneSteps, milestoneTrack, nestMenuCategories, normalizeCardholderName, orderPayableTotal, orderRedeemedTotal, orderRedeems, parseExpiry, paymentMatchesClientConditions, rebuildRootGroups, recomputeTotals, redeemedTotal, redemptionCap, redemptionSteps, remainingAmount, removeLine, replaceLine, resolveCheckout, resolveModifierImageUrl, reversePayment, runPaymentAttempt, selectablePayments, serializeError, services, setLineQuantity, storefrontJson, toOrderLines, toPaymentCardBrand, validateCardNumber, validateCardholderName, validateCvc, validateExpiry, validateOrderAgainstMenu, verifyWebhookSignature, visitedStates };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fire-spark/core",
3
- "version": "0.8.3",
3
+ "version": "0.9.0",
4
4
  "description": "Fire Spark headless SDK",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -20,8 +20,8 @@
20
20
  "big.js": "6.2.1",
21
21
  "dayjs": "1.11.9",
22
22
  "@types/big.js": "6.2.2",
23
- "@fire-spark/payments": "^0.8.3",
24
- "@fire-spark/types": "^0.8.3"
23
+ "@fire-spark/payments": "^0.9.0",
24
+ "@fire-spark/types": "^0.9.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "tsdown": "^0.22.14",