@fire-spark/core 0.0.1 → 0.0.2

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +416 -46
  2. package/dist/index.js +570 -49
  3. package/package.json +2 -3
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AndConditions, CheckoutMode, CheckoutResolved, Condition, CustomerIdentifyInput, CustomerRegisterInput, CustomerUpdateInput, FulfillmentListInput, GroupDisplayType, KioskDeviceConfig, KioskDeviceOverrides, KioskHealthBatchInput, KioskHealthBatchResult, KioskHeartbeatInput, KioskHeartbeatResult, KioskRealtimeToken, MatchResult, MatchableRule, Menu, Menu as Menu$1, MenuLookup, MenuLookup as MenuLookup$1, ModifierControlType, ModifierGroup, OrConditions, Order as Order$1, Order as Order$2, OrderInsertInput, OrderLine, OrderListInput, OrderReorderInput, OrderTotals, OrderUpdateInput, OrderValidateInput, OrderValidateOutput, PaymentIntentInput, PaymentMethod, PaymentProvider, PricingModifier, Product, Product as Product$1, Recommendation, RecurringDateCronResult, RecurringDateCronValues, RulesEngineOrderLine, RulesEngineParams, RulesEngineRuntime, Schedule, StoreListInput, StoreNearbyInput, StorefrontAvailability, StorefrontMenuScope, StorefrontMenuStreamOptions, StorefrontPaymentsListInput } from "@fire-spark/types";
1
+ import { AndConditions, CheckoutMode, CheckoutResolved, Condition, CustomerIdentifyInput, CustomerRegisterInput, CustomerUpdateInput, FulfillmentListInput, GroupDisplayType, KioskDeviceConfig, KioskDeviceOverrides, KioskHealthBatchInput, KioskHealthBatchResult, KioskHeartbeatInput, KioskHeartbeatResult, KioskRealtimeToken, MatchResult, MatchableRule, Menu, Menu as Menu$1, MenuLookup, MenuLookup as MenuLookup$1, ModifierControlType, ModifierGroup, OrConditions, Order as Order$1, Order as Order$2, OrderInsertInput, OrderLine, OrderListInput, OrderRedeem, OrderRedemptionStep, OrderReorderInput, OrderTotals, OrderUpdateInput, OrderValidateInput, OrderValidateOutput, PaymentIntentInput, PaymentMethod, PaymentProvider, PricingModifier, Product, Product as Product$1, Recommendation, RecurringDateCronResult, RecurringDateCronValues, RulesEngineOrderLine, RulesEngineParams, RulesEngineRuntime, Schedule, StoreListInput, StoreNearbyInput, StorefrontAvailability, StorefrontMenuScope, StorefrontMenuStreamOptions, StorefrontPaymentsListInput } from "@fire-spark/types";
2
2
  //#region src/productCustomization/types.d.ts
3
3
  type SelectionStatus = "IDLE" | "INCOMPLETE" | "VALID" | "INVALID";
4
4
  type CustomizationPath = string;
@@ -208,9 +208,11 @@ declare const listOrders: (input?: OrderListInput, options?: ServiceHttpOptions)
208
208
  included_quantity: number;
209
209
  }[];
210
210
  redeem: {
211
- id: string;
212
- amount: number;
213
- value: number;
211
+ reward_id: string;
212
+ quantity: number;
213
+ discount_rate?: number | undefined;
214
+ discount_amount?: number | undefined;
215
+ expires_at?: string | undefined;
214
216
  }[];
215
217
  quantity: number;
216
218
  tax_rate: number | null;
@@ -233,9 +235,44 @@ declare const listOrders: (input?: OrderListInput, options?: ServiceHttpOptions)
233
235
  group_total: number;
234
236
  }[];
235
237
  redeem: {
236
- id: string;
237
- amount: number;
238
- value: number;
238
+ reward_id: string;
239
+ quantity: number;
240
+ discount_rate?: number | undefined;
241
+ discount_amount?: number | undefined;
242
+ expires_at?: string | undefined;
243
+ }[];
244
+ redemption_steps: {
245
+ reward_id: string;
246
+ reward_quantity: number;
247
+ target: "LINE" | "ORDER";
248
+ target_id?: string | undefined;
249
+ action: "CREATE" | "DELETE" | "READ" | "UPDATE";
250
+ discount_amount: number;
251
+ before: {
252
+ value: number;
253
+ quantity: number;
254
+ discount_amount: number;
255
+ discount_rate: number;
256
+ };
257
+ after: {
258
+ value: number;
259
+ quantity: number;
260
+ discount_amount: number;
261
+ discount_rate: number;
262
+ };
263
+ target_before?: {
264
+ value: number;
265
+ quantity: number;
266
+ discount_amount: number;
267
+ discount_rate: number;
268
+ } | undefined;
269
+ target_after?: {
270
+ value: number;
271
+ quantity: number;
272
+ discount_amount: number;
273
+ discount_rate: number;
274
+ } | undefined;
275
+ error?: string | undefined;
239
276
  }[];
240
277
  customer: {
241
278
  customer_info: {
@@ -343,9 +380,11 @@ declare const createOrder: (body: OrderInsertInput, options?: ServiceHttpOptions
343
380
  included_quantity: number;
344
381
  }[];
345
382
  redeem: {
346
- id: string;
347
- amount: number;
348
- value: number;
383
+ reward_id: string;
384
+ quantity: number;
385
+ discount_rate?: number | undefined;
386
+ discount_amount?: number | undefined;
387
+ expires_at?: string | undefined;
349
388
  }[];
350
389
  quantity: number;
351
390
  tax_rate: number | null;
@@ -368,9 +407,44 @@ declare const createOrder: (body: OrderInsertInput, options?: ServiceHttpOptions
368
407
  group_total: number;
369
408
  }[];
370
409
  redeem: {
371
- id: string;
372
- amount: number;
373
- value: number;
410
+ reward_id: string;
411
+ quantity: number;
412
+ discount_rate?: number | undefined;
413
+ discount_amount?: number | undefined;
414
+ expires_at?: string | undefined;
415
+ }[];
416
+ redemption_steps: {
417
+ reward_id: string;
418
+ reward_quantity: number;
419
+ target: "LINE" | "ORDER";
420
+ target_id?: string | undefined;
421
+ action: "CREATE" | "DELETE" | "READ" | "UPDATE";
422
+ discount_amount: number;
423
+ before: {
424
+ value: number;
425
+ quantity: number;
426
+ discount_amount: number;
427
+ discount_rate: number;
428
+ };
429
+ after: {
430
+ value: number;
431
+ quantity: number;
432
+ discount_amount: number;
433
+ discount_rate: number;
434
+ };
435
+ target_before?: {
436
+ value: number;
437
+ quantity: number;
438
+ discount_amount: number;
439
+ discount_rate: number;
440
+ } | undefined;
441
+ target_after?: {
442
+ value: number;
443
+ quantity: number;
444
+ discount_amount: number;
445
+ discount_rate: number;
446
+ } | undefined;
447
+ error?: string | undefined;
374
448
  }[];
375
449
  customer: {
376
450
  customer_info: {
@@ -478,9 +552,11 @@ declare const getOrder: (id: string, options?: ServiceHttpOptions) => Promise<{
478
552
  included_quantity: number;
479
553
  }[];
480
554
  redeem: {
481
- id: string;
482
- amount: number;
483
- value: number;
555
+ reward_id: string;
556
+ quantity: number;
557
+ discount_rate?: number | undefined;
558
+ discount_amount?: number | undefined;
559
+ expires_at?: string | undefined;
484
560
  }[];
485
561
  quantity: number;
486
562
  tax_rate: number | null;
@@ -503,9 +579,44 @@ declare const getOrder: (id: string, options?: ServiceHttpOptions) => Promise<{
503
579
  group_total: number;
504
580
  }[];
505
581
  redeem: {
506
- id: string;
507
- amount: number;
508
- value: number;
582
+ reward_id: string;
583
+ quantity: number;
584
+ discount_rate?: number | undefined;
585
+ discount_amount?: number | undefined;
586
+ expires_at?: string | undefined;
587
+ }[];
588
+ redemption_steps: {
589
+ reward_id: string;
590
+ reward_quantity: number;
591
+ target: "LINE" | "ORDER";
592
+ target_id?: string | undefined;
593
+ action: "CREATE" | "DELETE" | "READ" | "UPDATE";
594
+ discount_amount: number;
595
+ before: {
596
+ value: number;
597
+ quantity: number;
598
+ discount_amount: number;
599
+ discount_rate: number;
600
+ };
601
+ after: {
602
+ value: number;
603
+ quantity: number;
604
+ discount_amount: number;
605
+ discount_rate: number;
606
+ };
607
+ target_before?: {
608
+ value: number;
609
+ quantity: number;
610
+ discount_amount: number;
611
+ discount_rate: number;
612
+ } | undefined;
613
+ target_after?: {
614
+ value: number;
615
+ quantity: number;
616
+ discount_amount: number;
617
+ discount_rate: number;
618
+ } | undefined;
619
+ error?: string | undefined;
509
620
  }[];
510
621
  customer: {
511
622
  customer_info: {
@@ -1051,9 +1162,11 @@ declare const updateOrder: (id: string, body: OrderUpdateInput, options?: Servic
1051
1162
  included_quantity: number;
1052
1163
  }[];
1053
1164
  redeem: {
1054
- id: string;
1055
- amount: number;
1056
- value: number;
1165
+ reward_id: string;
1166
+ quantity: number;
1167
+ discount_rate?: number | undefined;
1168
+ discount_amount?: number | undefined;
1169
+ expires_at?: string | undefined;
1057
1170
  }[];
1058
1171
  quantity: number;
1059
1172
  tax_rate: number | null;
@@ -1076,9 +1189,44 @@ declare const updateOrder: (id: string, body: OrderUpdateInput, options?: Servic
1076
1189
  group_total: number;
1077
1190
  }[];
1078
1191
  redeem: {
1079
- id: string;
1080
- amount: number;
1081
- value: number;
1192
+ reward_id: string;
1193
+ quantity: number;
1194
+ discount_rate?: number | undefined;
1195
+ discount_amount?: number | undefined;
1196
+ expires_at?: string | undefined;
1197
+ }[];
1198
+ redemption_steps: {
1199
+ reward_id: string;
1200
+ reward_quantity: number;
1201
+ target: "LINE" | "ORDER";
1202
+ target_id?: string | undefined;
1203
+ action: "CREATE" | "DELETE" | "READ" | "UPDATE";
1204
+ discount_amount: number;
1205
+ before: {
1206
+ value: number;
1207
+ quantity: number;
1208
+ discount_amount: number;
1209
+ discount_rate: number;
1210
+ };
1211
+ after: {
1212
+ value: number;
1213
+ quantity: number;
1214
+ discount_amount: number;
1215
+ discount_rate: number;
1216
+ };
1217
+ target_before?: {
1218
+ value: number;
1219
+ quantity: number;
1220
+ discount_amount: number;
1221
+ discount_rate: number;
1222
+ } | undefined;
1223
+ target_after?: {
1224
+ value: number;
1225
+ quantity: number;
1226
+ discount_amount: number;
1227
+ discount_rate: number;
1228
+ } | undefined;
1229
+ error?: string | undefined;
1082
1230
  }[];
1083
1231
  customer: {
1084
1232
  customer_info: {
@@ -1186,9 +1334,11 @@ declare const cancelOrder: (id: string, options?: ServiceHttpOptions) => Promise
1186
1334
  included_quantity: number;
1187
1335
  }[];
1188
1336
  redeem: {
1189
- id: string;
1190
- amount: number;
1191
- value: number;
1337
+ reward_id: string;
1338
+ quantity: number;
1339
+ discount_rate?: number | undefined;
1340
+ discount_amount?: number | undefined;
1341
+ expires_at?: string | undefined;
1192
1342
  }[];
1193
1343
  quantity: number;
1194
1344
  tax_rate: number | null;
@@ -1211,9 +1361,44 @@ declare const cancelOrder: (id: string, options?: ServiceHttpOptions) => Promise
1211
1361
  group_total: number;
1212
1362
  }[];
1213
1363
  redeem: {
1214
- id: string;
1215
- amount: number;
1216
- value: number;
1364
+ reward_id: string;
1365
+ quantity: number;
1366
+ discount_rate?: number | undefined;
1367
+ discount_amount?: number | undefined;
1368
+ expires_at?: string | undefined;
1369
+ }[];
1370
+ redemption_steps: {
1371
+ reward_id: string;
1372
+ reward_quantity: number;
1373
+ target: "LINE" | "ORDER";
1374
+ target_id?: string | undefined;
1375
+ action: "CREATE" | "DELETE" | "READ" | "UPDATE";
1376
+ discount_amount: number;
1377
+ before: {
1378
+ value: number;
1379
+ quantity: number;
1380
+ discount_amount: number;
1381
+ discount_rate: number;
1382
+ };
1383
+ after: {
1384
+ value: number;
1385
+ quantity: number;
1386
+ discount_amount: number;
1387
+ discount_rate: number;
1388
+ };
1389
+ target_before?: {
1390
+ value: number;
1391
+ quantity: number;
1392
+ discount_amount: number;
1393
+ discount_rate: number;
1394
+ } | undefined;
1395
+ target_after?: {
1396
+ value: number;
1397
+ quantity: number;
1398
+ discount_amount: number;
1399
+ discount_rate: number;
1400
+ } | undefined;
1401
+ error?: string | undefined;
1217
1402
  }[];
1218
1403
  customer: {
1219
1404
  customer_info: {
@@ -1321,9 +1506,11 @@ declare const refundOrder: (id: string, options?: ServiceHttpOptions) => Promise
1321
1506
  included_quantity: number;
1322
1507
  }[];
1323
1508
  redeem: {
1324
- id: string;
1325
- amount: number;
1326
- value: number;
1509
+ reward_id: string;
1510
+ quantity: number;
1511
+ discount_rate?: number | undefined;
1512
+ discount_amount?: number | undefined;
1513
+ expires_at?: string | undefined;
1327
1514
  }[];
1328
1515
  quantity: number;
1329
1516
  tax_rate: number | null;
@@ -1346,9 +1533,44 @@ declare const refundOrder: (id: string, options?: ServiceHttpOptions) => Promise
1346
1533
  group_total: number;
1347
1534
  }[];
1348
1535
  redeem: {
1349
- id: string;
1350
- amount: number;
1351
- value: number;
1536
+ reward_id: string;
1537
+ quantity: number;
1538
+ discount_rate?: number | undefined;
1539
+ discount_amount?: number | undefined;
1540
+ expires_at?: string | undefined;
1541
+ }[];
1542
+ redemption_steps: {
1543
+ reward_id: string;
1544
+ reward_quantity: number;
1545
+ target: "LINE" | "ORDER";
1546
+ target_id?: string | undefined;
1547
+ action: "CREATE" | "DELETE" | "READ" | "UPDATE";
1548
+ discount_amount: number;
1549
+ before: {
1550
+ value: number;
1551
+ quantity: number;
1552
+ discount_amount: number;
1553
+ discount_rate: number;
1554
+ };
1555
+ after: {
1556
+ value: number;
1557
+ quantity: number;
1558
+ discount_amount: number;
1559
+ discount_rate: number;
1560
+ };
1561
+ target_before?: {
1562
+ value: number;
1563
+ quantity: number;
1564
+ discount_amount: number;
1565
+ discount_rate: number;
1566
+ } | undefined;
1567
+ target_after?: {
1568
+ value: number;
1569
+ quantity: number;
1570
+ discount_amount: number;
1571
+ discount_rate: number;
1572
+ } | undefined;
1573
+ error?: string | undefined;
1352
1574
  }[];
1353
1575
  customer: {
1354
1576
  customer_info: {
@@ -1456,9 +1678,11 @@ declare const reorderOrder: (id: string, body?: OrderReorderInput, options?: Ser
1456
1678
  included_quantity: number;
1457
1679
  }[];
1458
1680
  redeem: {
1459
- id: string;
1460
- amount: number;
1461
- value: number;
1681
+ reward_id: string;
1682
+ quantity: number;
1683
+ discount_rate?: number | undefined;
1684
+ discount_amount?: number | undefined;
1685
+ expires_at?: string | undefined;
1462
1686
  }[];
1463
1687
  quantity: number;
1464
1688
  tax_rate: number | null;
@@ -1481,9 +1705,44 @@ declare const reorderOrder: (id: string, body?: OrderReorderInput, options?: Ser
1481
1705
  group_total: number;
1482
1706
  }[];
1483
1707
  redeem: {
1484
- id: string;
1485
- amount: number;
1486
- value: number;
1708
+ reward_id: string;
1709
+ quantity: number;
1710
+ discount_rate?: number | undefined;
1711
+ discount_amount?: number | undefined;
1712
+ expires_at?: string | undefined;
1713
+ }[];
1714
+ redemption_steps: {
1715
+ reward_id: string;
1716
+ reward_quantity: number;
1717
+ target: "LINE" | "ORDER";
1718
+ target_id?: string | undefined;
1719
+ action: "CREATE" | "DELETE" | "READ" | "UPDATE";
1720
+ discount_amount: number;
1721
+ before: {
1722
+ value: number;
1723
+ quantity: number;
1724
+ discount_amount: number;
1725
+ discount_rate: number;
1726
+ };
1727
+ after: {
1728
+ value: number;
1729
+ quantity: number;
1730
+ discount_amount: number;
1731
+ discount_rate: number;
1732
+ };
1733
+ target_before?: {
1734
+ value: number;
1735
+ quantity: number;
1736
+ discount_amount: number;
1737
+ discount_rate: number;
1738
+ } | undefined;
1739
+ target_after?: {
1740
+ value: number;
1741
+ quantity: number;
1742
+ discount_amount: number;
1743
+ discount_rate: number;
1744
+ } | undefined;
1745
+ error?: string | undefined;
1487
1746
  }[];
1488
1747
  customer: {
1489
1748
  customer_info: {
@@ -1594,6 +1853,26 @@ type ReorderOrderOptions = CreateOrderOptions & {
1594
1853
  strategy?: "replace" | "merge";
1595
1854
  };
1596
1855
  type OrderDraft = Order$2;
1856
+ type ApplyRedeemInput = {
1857
+ reward_id: string;
1858
+ /** Reward units to redeem (integer ≥ 1). */
1859
+ quantity: number;
1860
+ /** 0–1, compounded per unit over the remaining value. */
1861
+ discount_rate?: number;
1862
+ /** > 0, per unit; caps the rate discount when both are set. */
1863
+ discount_amount?: number;
1864
+ /** Root line to discount; omit to discount the whole order. */
1865
+ line_uid?: string;
1866
+ /** ISO datetime with offset; validation fails once it has passed. */
1867
+ expires_at?: string;
1868
+ };
1869
+ type RemoveRedeemInput = {
1870
+ reward_id: string;
1871
+ /** Reward units to remove (integer ≥ 1), latest applied first. */
1872
+ quantity: number;
1873
+ /** Root line holding the redeem; omit for order-level redeems. */
1874
+ line_uid?: string;
1875
+ };
1597
1876
  //#endregion
1598
1877
  //#region src/order/Order.d.ts
1599
1878
  declare class Order {
@@ -1650,6 +1929,16 @@ declare class Order {
1650
1929
  removeLine(lineUid: string): void;
1651
1930
  replaceLine(lineUid: string, lines: OrderLine | OrderLine[]): void;
1652
1931
  emptyOrder(): void;
1932
+ /**
1933
+ * Appends a redeem to `line_uid` (root line) or to the order. Steps keep
1934
+ * the order in which redeems were applied.
1935
+ */
1936
+ applyRedeem(input: ApplyRedeemInput): void;
1937
+ /**
1938
+ * Removes `quantity` units of `reward_id` from `line_uid` or the order,
1939
+ * latest applied first. Removing more units than applied clears them all.
1940
+ */
1941
+ removeRedeem(input: RemoveRedeemInput): void;
1653
1942
  setScope(scope: Partial<OrderScope>): void;
1654
1943
  /** Shallow-merges into order metadata; `null` values are kept as-is. */
1655
1944
  setMetadata(patch: Record<string, unknown>): void;
@@ -1661,6 +1950,8 @@ declare class Order {
1661
1950
  refundOrder(): Promise<void>;
1662
1951
  validateOrder(): Promise<void>;
1663
1952
  placeOrder(input?: PlaceOrderInput): Promise<void>;
1953
+ /** Applies lines/redeem, replaying redemption steps and totals. */
1954
+ private commitLines;
1664
1955
  private scheduleAfterMutate;
1665
1956
  private notify;
1666
1957
  }
@@ -1725,8 +2016,8 @@ declare const milestoneTrack: (workflow: TrackWorkflow) => MilestoneTrackItem[];
1725
2016
  declare const milestoneSteps: (workflow: TrackWorkflow, currentNodeIds: string[]) => MilestoneSteps | null;
1726
2017
  //#endregion
1727
2018
  //#region src/order/validate.d.ts
1728
- type OrderLineIssueCode = "PRODUCT_NOT_FOUND" | "HIDDEN" | "OUT_OF_STOCK" | "OUTSIDE_HOURS" | "MODIFIER_NOT_FOUND" | "MODIFIER_RULES_INVALID" | "PRICE_MISMATCH" | "TAX_MISMATCH" | "DISCOUNT_MISMATCH" | "QUANTITY_MISMATCH" | "QUANTITY_BELOW_MINIMUM" | "QUANTITY_ABOVE_MAXIMUM" | "STRUCTURE_MISMATCH" | "FEE_MISMATCH";
1729
- type OrderIssueCode = "NO_MENU" | "MENU_CLOSED" | "SCOPE_INACTIVE" | "TOTALS_MISMATCH" | "EMPTY_ORDER" | "ITEMS_BELOW_MINIMUM" | "ITEMS_ABOVE_MAXIMUM" | "TOTAL_BELOW_MINIMUM" | "TOTAL_ABOVE_MAXIMUM" | "REDEEMS_ABOVE_MAXIMUM" | "REDEEMS_TOTAL_ABOVE_MAXIMUM" | "SHIPPING_REQUIRED" | "BILLING_REQUIRED" | "SHIPPING_FIELD_REQUIRED" | "BILLING_FIELD_REQUIRED";
2019
+ type OrderLineIssueCode = "PRODUCT_NOT_FOUND" | "HIDDEN" | "OUT_OF_STOCK" | "OUTSIDE_HOURS" | "MODIFIER_NOT_FOUND" | "MODIFIER_RULES_INVALID" | "PRICE_MISMATCH" | "TAX_MISMATCH" | "DISCOUNT_MISMATCH" | "QUANTITY_MISMATCH" | "QUANTITY_BELOW_MINIMUM" | "QUANTITY_ABOVE_MAXIMUM" | "STRUCTURE_MISMATCH" | "FEE_MISMATCH" | "REDEEM_EXPIRED" | "REDEMPTION_STEPS_MISMATCH";
2020
+ type OrderIssueCode = "NO_MENU" | "MENU_CLOSED" | "SCOPE_INACTIVE" | "TOTALS_MISMATCH" | "REDEEM_EXPIRED" | "REDEMPTION_STEPS_MISMATCH" | "REDEMPTION_MISMATCH" | "EMPTY_ORDER" | "ITEMS_BELOW_MINIMUM" | "ITEMS_ABOVE_MAXIMUM" | "TOTAL_BELOW_MINIMUM" | "TOTAL_ABOVE_MAXIMUM" | "REDEEMS_ABOVE_MAXIMUM" | "REDEEMS_TOTAL_ABOVE_MAXIMUM" | "SHIPPING_REQUIRED" | "BILLING_REQUIRED" | "SHIPPING_FIELD_REQUIRED" | "BILLING_FIELD_REQUIRED";
1730
2021
  type OrderLineIssue = {
1731
2022
  code: OrderLineIssueCode;
1732
2023
  field?: string;
@@ -1753,6 +2044,10 @@ type ValidateOrderAgainstMenuInput = {
1753
2044
  menuActive: boolean;
1754
2045
  locale?: string;
1755
2046
  expectedFeeLines?: OrderLine[];
2047
+ redeem?: OrderRedeem[];
2048
+ redemptionSteps?: OrderRedemptionStep[];
2049
+ /** Epoch ms used for `expires_at` checks. */
2050
+ now?: number;
1756
2051
  };
1757
2052
  /** Compare cart lines/totals against a fresh menu (exact Big.js equality). */
1758
2053
  declare const validateOrderAgainstMenu: (input: ValidateOrderAgainstMenuInput) => OrderValidationResult;
@@ -1778,6 +2073,58 @@ declare class OrderValidationError extends Error {
1778
2073
  }
1779
2074
  declare const isOrderValidationError: (error: unknown) => error is OrderValidationError;
1780
2075
  //#endregion
2076
+ //#region src/order/redemptions.d.ts
2077
+ /** Steps + totals for a set of lines and redeems, keeping step order. */
2078
+ declare const recomputeRedemptions: (input: {
2079
+ lines: OrderLine[];
2080
+ redeem: OrderRedeem[];
2081
+ redemption_steps: OrderRedemptionStep[];
2082
+ currency: string;
2083
+ }) => {
2084
+ redemption_steps: {
2085
+ reward_id: string;
2086
+ reward_quantity: number;
2087
+ target: "LINE" | "ORDER";
2088
+ target_id?: string | undefined;
2089
+ action: "CREATE" | "DELETE" | "READ" | "UPDATE";
2090
+ discount_amount: number;
2091
+ before: {
2092
+ value: number;
2093
+ quantity: number;
2094
+ discount_amount: number;
2095
+ discount_rate: number;
2096
+ };
2097
+ after: {
2098
+ value: number;
2099
+ quantity: number;
2100
+ discount_amount: number;
2101
+ discount_rate: number;
2102
+ };
2103
+ target_before?: {
2104
+ value: number;
2105
+ quantity: number;
2106
+ discount_amount: number;
2107
+ discount_rate: number;
2108
+ } | undefined;
2109
+ target_after?: {
2110
+ value: number;
2111
+ quantity: number;
2112
+ discount_amount: number;
2113
+ discount_rate: number;
2114
+ } | undefined;
2115
+ error?: string | undefined;
2116
+ }[];
2117
+ totals: {
2118
+ currency: string;
2119
+ subtotal: number;
2120
+ discount_rate: number;
2121
+ discount_total: number;
2122
+ tax_rate: number;
2123
+ tax_total: number;
2124
+ total: number;
2125
+ };
2126
+ };
2127
+ //#endregion
1781
2128
  //#region src/order/totals.d.ts
1782
2129
  /** Sum root line group totals into order totals (Big.js). */
1783
2130
  declare const recomputeTotals: (lines: OrderLine[], currency?: string) => OrderTotals;
@@ -3276,6 +3623,29 @@ type ScheduleWindow = {
3276
3623
  declare const getScheduleWindow: (schedule: Schedule | null | undefined, now?: Date) => ScheduleWindow;
3277
3624
  declare const isOnSchedule: (schedule: Schedule | null | undefined, now?: Date) => boolean;
3278
3625
  //#endregion
3626
+ //#region src/shared/genId.d.ts
3627
+ type IdBuilder = {
3628
+ /**
3629
+ * Keeps only `[A-Za-z0-9]`, cut to 32 characters. `fallback` is used only
3630
+ * when `value` is empty after trim; throws if nothing alphanumeric is left.
3631
+ */
3632
+ literal: (value: string | null | undefined, fallback?: string) => IdBuilder;
3633
+ /** Appends `-`, the only separator allowed. */
3634
+ separator: () => IdBuilder;
3635
+ /** Cryptographically random `[0-9a-z]`; at least 12 characters. */
3636
+ hash: (length?: number) => IdBuilder;
3637
+ /** New id on every call; throws when the shape is unsafe. */
3638
+ build: () => string;
3639
+ };
3640
+ /**
3641
+ * Safe id builder: `genId().literal("ord").separator().hash(12).build()`.
3642
+ * Literals are cleaned to `[A-Za-z0-9]`; `-` is the only separator and a
3643
+ * random hash of at least 12 characters is required.
3644
+ */
3645
+ declare const genId: () => IdBuilder;
3646
+ /** `ord-<store>-<hash>`; `nostore` when the store id is blank. */
3647
+ declare const genOrderId: (storeId?: string | null) => string;
3648
+ //#endregion
3279
3649
  //#region src/rules/RulesEngine.d.ts
3280
3650
  declare class RulesEngine {
3281
3651
  order?: RulesEngineParams["order"];
@@ -3396,4 +3766,4 @@ declare const paymentMatchesClientConditions: (conditions: OrConditions | null |
3396
3766
  now?: string;
3397
3767
  }) => boolean;
3398
3768
  //#endregion
3399
- export { type AuthConfigureOptions, type CreateOrderOptions, type CreateSessionOptions, type CustomizationPath, type CustomizationSession, DEFAULT_RECOMMENDATION_TYPES, type ExchangeOptions, type FlatMenuCategory, type GroupDisplayType, type ListMenusStreamOptions, type Menu, type MenuCategoryNode, type MenuLookup, type MilestoneSteps, type MilestoneTrackItem, type ModifierControlType, type ModifierCtx, type ModifierGroupCtx, type NestedMenuCategory, Order, type OrderCustomerSource, type OrderDraft, type OrderIssue, type OrderIssueCode, type OrderLineDiff, type OrderLineIssue, type OrderLineIssueCode, type OrderScope, OrderSyncPendingError, OrderValidationError, type OrderValidationResult, type PricingModifier, type Product, type ProductCtx, type QuantitiesByPath, type RecommendationType, RecurringDateCron, type ReorderOrderOptions, type ResetOptions, RulesEngine, type ScheduleWindow, type SelectionStatus, type ServiceHttpOptions, type Snapshot, type StorefrontAccessToken, StorefrontHttpError, type TrackWorkflow, type ValidateOptions, type ValidateOrderAgainstMenuInput, type VisitedState, addLines, auth, buildCustomerSnapshot, collectOrderLineDiffs, countCartProducts, createSession, currentNodes, defaultConditionError, flattenMenuCategories, fromOrderLines, getScheduleWindow, indexMenu, isCheckoutFieldFilled, isDeviceNotLinked, isKioskUnauthorized, isMilestoneStepDone, isOnSchedule, isOrderValidationError, isStorefrontNotFound, listMenusStream, matchAbsoluteDateOperator, matchBooleanOperator, matchCurrencyOperator, matchEnumOperator, matchNumberOperator, matchRecurringDateOperator, matchRelativeDateOperator, matchStringOperator, milestoneSteps, milestoneTrack, nestMenuCategories, paymentMatchesClientConditions, recomputeTotals, resolveCheckout, services, storefrontJson, toOrderLines, validateOrderAgainstMenu, visitedStates };
3769
+ export { type ApplyRedeemInput, type AuthConfigureOptions, type CreateOrderOptions, type CreateSessionOptions, type CustomizationPath, type CustomizationSession, DEFAULT_RECOMMENDATION_TYPES, type ExchangeOptions, type FlatMenuCategory, type GroupDisplayType, type IdBuilder, type ListMenusStreamOptions, type Menu, type MenuCategoryNode, type MenuLookup, type MilestoneSteps, type MilestoneTrackItem, type ModifierControlType, type ModifierCtx, type ModifierGroupCtx, type NestedMenuCategory, Order, type OrderCustomerSource, type OrderDraft, type OrderIssue, type OrderIssueCode, type OrderLineDiff, type OrderLineIssue, type OrderLineIssueCode, type OrderScope, OrderSyncPendingError, OrderValidationError, type OrderValidationResult, type PricingModifier, type Product, type ProductCtx, type QuantitiesByPath, type RecommendationType, RecurringDateCron, type RemoveRedeemInput, type ReorderOrderOptions, type ResetOptions, RulesEngine, type ScheduleWindow, type SelectionStatus, type ServiceHttpOptions, type Snapshot, type StorefrontAccessToken, StorefrontHttpError, type TrackWorkflow, type ValidateOptions, type ValidateOrderAgainstMenuInput, type VisitedState, addLines, auth, buildCustomerSnapshot, collectOrderLineDiffs, countCartProducts, createSession, currentNodes, defaultConditionError, flattenMenuCategories, fromOrderLines, genId, genOrderId, getScheduleWindow, indexMenu, isCheckoutFieldFilled, isDeviceNotLinked, isKioskUnauthorized, isMilestoneStepDone, isOnSchedule, isOrderValidationError, isStorefrontNotFound, listMenusStream, matchAbsoluteDateOperator, matchBooleanOperator, matchCurrencyOperator, matchEnumOperator, matchNumberOperator, matchRecurringDateOperator, matchRelativeDateOperator, matchStringOperator, milestoneSteps, milestoneTrack, nestMenuCategories, paymentMatchesClientConditions, recomputeRedemptions, recomputeTotals, resolveCheckout, services, storefrontJson, toOrderLines, validateOrderAgainstMenu, visitedStates };
package/dist/index.js CHANGED
@@ -1978,6 +1978,83 @@ const services = {
1978
1978
  isKioskUnauthorized
1979
1979
  }
1980
1980
  };
1981
+ const DISALLOWED = /[^A-Za-z0-9]/g;
1982
+ const ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz";
1983
+ const BYTE_LIMIT = 252;
1984
+ const toLiteral = (value, fallback) => {
1985
+ const trimmed = (value ?? "").trim();
1986
+ const source = trimmed === "" ? fallback : trimmed;
1987
+ if (source === void 0) throw new Error("Id literal must not be empty");
1988
+ const cleaned = source.replace(DISALLOWED, "");
1989
+ if (cleaned === "") throw new Error(`Id literal has no alphanumeric characters: "${source}"`);
1990
+ return cleaned.slice(0, 32);
1991
+ };
1992
+ const randomChars = (length) => {
1993
+ const c = globalThis.crypto;
1994
+ if (typeof c?.getRandomValues !== "function") throw new Error("crypto.getRandomValues is required");
1995
+ let out = "";
1996
+ const bytes = new Uint8Array(length * 2);
1997
+ while (out.length < length) {
1998
+ c.getRandomValues(bytes);
1999
+ for (const byte of bytes) {
2000
+ if (byte >= BYTE_LIMIT) continue;
2001
+ out += ALPHABET[byte % 36];
2002
+ if (out.length === length) break;
2003
+ }
2004
+ }
2005
+ return out;
2006
+ };
2007
+ const assertShape = (parts) => {
2008
+ if (!parts.some((part) => part.kind === "hash")) throw new Error("Id requires a random hash part");
2009
+ const first = parts[0];
2010
+ const last = parts[parts.length - 1];
2011
+ if (first?.kind === "separator" || last?.kind === "separator") throw new Error("Id must not start or end with a separator");
2012
+ parts.forEach((part, index) => {
2013
+ if (part.kind !== "separator") return;
2014
+ if (parts[index - 1]?.kind !== "separator") return;
2015
+ throw new Error("Id must not contain consecutive separators");
2016
+ });
2017
+ };
2018
+ const render = (part) => {
2019
+ if (part.kind === "literal") return part.value;
2020
+ if (part.kind === "separator") return "-";
2021
+ return randomChars(part.length);
2022
+ };
2023
+ const builder = (parts) => {
2024
+ return {
2025
+ literal: (value, fallback) => {
2026
+ const literal = {
2027
+ kind: "literal",
2028
+ value: toLiteral(value, fallback)
2029
+ };
2030
+ return builder([...parts, literal]);
2031
+ },
2032
+ separator: () => builder([...parts, { kind: "separator" }]),
2033
+ hash: (length = 12) => {
2034
+ if (!Number.isInteger(length) || length < 12) throw new Error(`Id hash must be an integer ≥ 12, got ${length}`);
2035
+ return builder([...parts, {
2036
+ kind: "hash",
2037
+ length
2038
+ }]);
2039
+ },
2040
+ build: () => {
2041
+ assertShape(parts);
2042
+ const id = parts.map(render).join("");
2043
+ if (id.length > 64) throw new Error(`Id must be ≤ 64 characters`);
2044
+ return id;
2045
+ }
2046
+ };
2047
+ };
2048
+ /**
2049
+ * Safe id builder: `genId().literal("ord").separator().hash(12).build()`.
2050
+ * Literals are cleaned to `[A-Za-z0-9]`; `-` is the only separator and a
2051
+ * random hash of at least 12 characters is required.
2052
+ */
2053
+ const genId = () => builder([]);
2054
+ /** `ord-<store>-<hash>`; `nostore` when the store id is blank. */
2055
+ const genOrderId = (storeId) => {
2056
+ return genId().literal("ord").separator().literal(storeId, "nostore").separator().hash().build();
2057
+ };
1981
2058
  //#endregion
1982
2059
  //#region src/order/retry.ts
1983
2060
  const defaultSleep = (ms) => {
@@ -2037,6 +2114,8 @@ const createFlushController = (runtime) => {
2037
2114
  const b5 = { customer: order.customer };
2038
2115
  const b6 = { payment_intents: order.payment_intents };
2039
2116
  const b7 = { payment_status: order.payment_status };
2117
+ const b8 = { redeem: order.redeem };
2118
+ const b9 = { redemption_steps: order.redemption_steps };
2040
2119
  return {
2041
2120
  ...b1,
2042
2121
  ...b2,
@@ -2044,7 +2123,9 @@ const createFlushController = (runtime) => {
2044
2123
  ...b4,
2045
2124
  ...b5,
2046
2125
  ...b6,
2047
- ...b7
2126
+ ...b7,
2127
+ ...b8,
2128
+ ...b9
2048
2129
  };
2049
2130
  };
2050
2131
  const insertBody = (order) => {
@@ -2068,6 +2149,8 @@ const createFlushController = (runtime) => {
2068
2149
  };
2069
2150
  const b7 = { payment_intents: order.payment_intents };
2070
2151
  const b8 = { payment_status: order.payment_status };
2152
+ const b9 = { redeem: order.redeem };
2153
+ const b10 = { redemption_steps: order.redemption_steps };
2071
2154
  return {
2072
2155
  ...b1,
2073
2156
  ...b2,
@@ -2076,7 +2159,9 @@ const createFlushController = (runtime) => {
2076
2159
  ...b5,
2077
2160
  ...b6,
2078
2161
  ...b7,
2079
- ...b8
2162
+ ...b8,
2163
+ ...b9,
2164
+ ...b10
2080
2165
  };
2081
2166
  };
2082
2167
  const flushNow = () => {
@@ -2564,6 +2649,227 @@ const emptyTotals = (currency = "USD") => {
2564
2649
  };
2565
2650
  };
2566
2651
  //#endregion
2652
+ //#region src/order/redemptions.ts
2653
+ const ORDER_KEY = "ORDER";
2654
+ /** Same rules as `vOrders.redeem()`; throws on invalid input. */
2655
+ const buildRedeem = (input) => {
2656
+ const { reward_id, quantity, discount_rate, discount_amount } = input;
2657
+ const { expires_at } = input;
2658
+ if (!reward_id.trim()) throw new Error("Redeem requires reward_id");
2659
+ if (!Number.isInteger(quantity) || quantity < 1) throw new Error("Redeem quantity must be an integer ≥ 1");
2660
+ if (discount_rate === void 0 && discount_amount === void 0) throw new Error("Redeem requires discount_rate or discount_amount");
2661
+ const rate = discount_rate ?? 0;
2662
+ if (!(rate >= 0 && rate <= 1)) throw new Error("Redeem discount_rate must be between 0 and 1");
2663
+ if (discount_amount !== void 0 && !(discount_amount > 0)) throw new Error("Redeem discount_amount must be > 0");
2664
+ if (expires_at !== void 0 && Number.isNaN(Date.parse(expires_at))) throw new Error("Redeem expires_at must be an ISO datetime");
2665
+ const redeem = {
2666
+ reward_id,
2667
+ quantity
2668
+ };
2669
+ if (discount_rate !== void 0) redeem.discount_rate = discount_rate;
2670
+ if (discount_amount !== void 0) redeem.discount_amount = discount_amount;
2671
+ if (expires_at !== void 0) redeem.expires_at = expires_at;
2672
+ return redeem;
2673
+ };
2674
+ /**
2675
+ * Takes `quantity` units of `reward_id` off the latest entries first, so the
2676
+ * earlier redeems keep their step position. Entries that reach 0 are removed.
2677
+ */
2678
+ const subtractRedeem = (items, reward_id, quantity) => {
2679
+ if (!Number.isInteger(quantity) || quantity < 1) throw new Error("Redeem quantity must be an integer ≥ 1");
2680
+ let pending = quantity;
2681
+ const kept = [];
2682
+ for (let i = items.length - 1; i >= 0; i--) {
2683
+ const item = items[i];
2684
+ if (pending === 0 || item.reward_id !== reward_id) {
2685
+ kept.unshift(item);
2686
+ continue;
2687
+ }
2688
+ const taken = Math.min(item.quantity, pending);
2689
+ pending -= taken;
2690
+ const rest = item.quantity - taken;
2691
+ if (rest > 0) kept.unshift({
2692
+ ...item,
2693
+ quantity: rest
2694
+ });
2695
+ }
2696
+ return kept;
2697
+ };
2698
+ const redemptionTargetKey = (target) => {
2699
+ if (target.target === "ORDER") return ORDER_KEY;
2700
+ return `LINE:${target.target_id ?? ""}`;
2701
+ };
2702
+ /** Only root lines own redeems; child-line redeems are never applied. */
2703
+ const redeemPools = (lines, redeem) => {
2704
+ const pools = /* @__PURE__ */ new Map();
2705
+ for (const line of lines) {
2706
+ const items = line.redeem ?? [];
2707
+ if (line.parent_uid != null || items.length === 0) continue;
2708
+ const target = {
2709
+ target: "LINE",
2710
+ target_id: line.uid
2711
+ };
2712
+ const pool = items.map((item) => ({
2713
+ ...target,
2714
+ redeem: item
2715
+ }));
2716
+ pools.set(redemptionTargetKey(target), pool);
2717
+ }
2718
+ const order = { target: "ORDER" };
2719
+ const pool = redeem.map((item) => ({
2720
+ ...order,
2721
+ redeem: item
2722
+ }));
2723
+ pools.set(ORDER_KEY, pool);
2724
+ return pools;
2725
+ };
2726
+ /**
2727
+ * Pairs the k-th step of a target with the k-th redeem of that target, in
2728
+ * step order. Steps without a matching redeem are dropped; redeems without a
2729
+ * step are appended (lines first, then order).
2730
+ */
2731
+ const sequenceRedeems = (lines, redeem, steps) => {
2732
+ const pools = redeemPools(lines, redeem);
2733
+ const cursors = /* @__PURE__ */ new Map();
2734
+ const sequence = [];
2735
+ for (const step of steps) {
2736
+ const key = redemptionTargetKey(step);
2737
+ const at = cursors.get(key) ?? 0;
2738
+ const next = pools.get(key)?.[at];
2739
+ if (!next || next.redeem.reward_id !== step.reward_id) continue;
2740
+ cursors.set(key, at + 1);
2741
+ sequence.push(next);
2742
+ }
2743
+ for (const [key, pool] of pools) sequence.push(...pool.slice(cursors.get(key) ?? 0));
2744
+ return sequence;
2745
+ };
2746
+ const minBig = (a, b) => {
2747
+ return a.lt(b) ? a : b;
2748
+ };
2749
+ const left = (run) => {
2750
+ return run.base.minus(run.discount);
2751
+ };
2752
+ const rateOf = (discount, base) => {
2753
+ if (base.lte(0)) return 0;
2754
+ return minBig(discount.div(base), new Big(1)).toNumber();
2755
+ };
2756
+ const snapshot = (run) => {
2757
+ const b1 = {
2758
+ value: left(run).toNumber(),
2759
+ quantity: run.quantity
2760
+ };
2761
+ const b2 = { discount_amount: run.discount.toNumber() };
2762
+ const b3 = { discount_rate: rateOf(run.discount, run.base) };
2763
+ return {
2764
+ ...b1,
2765
+ ...b2,
2766
+ ...b3
2767
+ };
2768
+ };
2769
+ /**
2770
+ * Rate compounds once per reward unit over the remaining value; amount is
2771
+ * per unit. With both, the rate discount is capped at `amount × quantity`.
2772
+ */
2773
+ const redeemDiscount = (redeem, remaining) => {
2774
+ const { quantity, discount_rate, discount_amount } = redeem;
2775
+ let cap = null;
2776
+ if (discount_amount !== void 0) cap = new Big(discount_amount).times(quantity);
2777
+ if (discount_rate === void 0) return cap ?? new Big(0);
2778
+ const kept = new Big(1).minus(discount_rate).pow(quantity);
2779
+ const byRate = remaining.times(new Big(1).minus(kept));
2780
+ return cap ? minBig(byRate, cap) : byRate;
2781
+ };
2782
+ const runningFor = (base, quantity) => {
2783
+ return {
2784
+ base: new Big(base),
2785
+ discount: new Big(0),
2786
+ quantity
2787
+ };
2788
+ };
2789
+ /**
2790
+ * Replays every redeem in persisted step order over the root lines' group
2791
+ * totals. A discount never exceeds what is left on its target or the order.
2792
+ */
2793
+ const computeRedemptionSteps = (lines, redeem, steps) => {
2794
+ const sequence = sequenceRedeems(lines, redeem, steps);
2795
+ if (sequence.length === 0) return [];
2796
+ const roots = lines.filter((line) => line.parent_uid == null);
2797
+ let orderBase = new Big(0);
2798
+ const lineRuns = /* @__PURE__ */ new Map();
2799
+ for (const root of roots) {
2800
+ orderBase = orderBase.plus(root.group_total);
2801
+ lineRuns.set(root.uid, runningFor(root.group_total, root.quantity));
2802
+ }
2803
+ const order = runningFor(orderBase, 1);
2804
+ return sequence.map((item) => {
2805
+ const line = item.target_id ? lineRuns.get(item.target_id) : void 0;
2806
+ const before = snapshot(order);
2807
+ const targetBefore = line ? snapshot(line) : void 0;
2808
+ const orderLeft = left(order);
2809
+ const targetLeft = line ? left(line) : orderLeft;
2810
+ const raw = redeemDiscount(item.redeem, targetLeft);
2811
+ const discount = minBig(minBig(raw, targetLeft), orderLeft);
2812
+ order.discount = order.discount.plus(discount);
2813
+ if (line) line.discount = line.discount.plus(discount);
2814
+ const action = discount.gt(0) ? "UPDATE" : "READ";
2815
+ const { reward_id, quantity } = item.redeem;
2816
+ const b1 = {
2817
+ reward_id,
2818
+ reward_quantity: quantity,
2819
+ target: item.target
2820
+ };
2821
+ const b2 = item.target_id ? { target_id: item.target_id } : {};
2822
+ const b3 = {
2823
+ action,
2824
+ discount_amount: discount.toNumber()
2825
+ };
2826
+ const b4 = {
2827
+ before,
2828
+ after: snapshot(order)
2829
+ };
2830
+ let b5 = {};
2831
+ if (line) b5 = {
2832
+ target_before: targetBefore,
2833
+ target_after: snapshot(line)
2834
+ };
2835
+ return {
2836
+ ...b1,
2837
+ ...b2,
2838
+ ...b3,
2839
+ ...b4,
2840
+ ...b5
2841
+ };
2842
+ });
2843
+ };
2844
+ /** Subtract redemption discounts from line-based order totals. */
2845
+ const applyRedemptionTotals = (totals, steps) => {
2846
+ if (steps.length === 0) return totals;
2847
+ let redeemed = new Big(0);
2848
+ for (const step of steps) redeemed = redeemed.plus(step.discount_amount);
2849
+ const discountTotal = new Big(totals.discount_total).plus(redeemed);
2850
+ const subtotal = new Big(totals.subtotal);
2851
+ const discount_rate = rateOf(discountTotal, subtotal);
2852
+ const discount_total = discountTotal.toNumber();
2853
+ const total = new Big(totals.total).minus(redeemed).toNumber();
2854
+ return {
2855
+ ...totals,
2856
+ discount_rate,
2857
+ discount_total,
2858
+ total
2859
+ };
2860
+ };
2861
+ /** Steps + totals for a set of lines and redeems, keeping step order. */
2862
+ const recomputeRedemptions = (input) => {
2863
+ const { lines, redeem, currency } = input;
2864
+ const steps = input.redemption_steps;
2865
+ const redemption_steps = computeRedemptionSteps(lines, redeem, steps);
2866
+ const lineTotals = recomputeTotals(lines, currency);
2867
+ return {
2868
+ redemption_steps,
2869
+ totals: applyRedemptionTotals(lineTotals, redemption_steps)
2870
+ };
2871
+ };
2872
+ //#endregion
2567
2873
  //#region src/order/Order.ts
2568
2874
  const cloneOrder = (order) => {
2569
2875
  return structuredClone(order);
@@ -2585,7 +2891,7 @@ const optionalBrandId = (value) => {
2585
2891
  const buildInsertFromOptions = (options) => {
2586
2892
  if (!options.scope) throw new Error("Order requires scope when fromOrderId is omitted");
2587
2893
  if (!options.customer) throw new Error("Order requires customer when fromOrderId is omitted");
2588
- const orderId = options.orderId ?? `ord-${randomUUID().slice(0, 8)}`;
2894
+ const orderId = options.orderId ?? genOrderId(options.scope.store_id);
2589
2895
  const brand_id = optionalBrandId(options.scope.brand_id);
2590
2896
  const customer = buildCustomerSnapshot(options.customer);
2591
2897
  const is_anonymous = options.isAnonymous ?? false;
@@ -2606,7 +2912,8 @@ const buildInsertFromOptions = (options) => {
2606
2912
  };
2607
2913
  const b5 = {
2608
2914
  totals: emptyTotals(),
2609
- metadata: {}
2915
+ metadata: {},
2916
+ redemption_steps: []
2610
2917
  };
2611
2918
  return {
2612
2919
  ...b1,
@@ -2617,8 +2924,8 @@ const buildInsertFromOptions = (options) => {
2617
2924
  };
2618
2925
  };
2619
2926
  const buildInsertFromSource = (options, source) => {
2620
- const orderId = options.orderId ?? `ord-${randomUUID().slice(0, 8)}`;
2621
2927
  const store_id = options.scope?.store_id ?? source.store_id;
2928
+ const orderId = options.orderId ?? genOrderId(store_id);
2622
2929
  const channel_id = options.scope?.channel_id ?? source.channel_id;
2623
2930
  const fulfillment_id = options.scope?.fulfillment_id ?? source.fulfillment_id;
2624
2931
  let brand_id = optionalBrandId(source.brand_id);
@@ -2627,7 +2934,13 @@ const buildInsertFromSource = (options, source) => {
2627
2934
  if (options.customer) customer = buildCustomerSnapshot(options.customer);
2628
2935
  const lines = structuredClone(source.lines);
2629
2936
  const is_anonymous = options.isAnonymous ?? source.is_anonymous;
2630
- const totals = recomputeTotals(lines, source.totals.currency);
2937
+ const redeem = structuredClone(source.redeem ?? []);
2938
+ const { redemption_steps, totals } = recomputeRedemptions({
2939
+ lines,
2940
+ redeem,
2941
+ redemption_steps: source.redemption_steps ?? [],
2942
+ currency: source.totals.currency
2943
+ });
2631
2944
  const b1 = {
2632
2945
  id: orderId,
2633
2946
  store_id,
@@ -2638,7 +2951,6 @@ const buildInsertFromSource = (options, source) => {
2638
2951
  brand_id,
2639
2952
  is_anonymous
2640
2953
  };
2641
- const redeem = source.redeem ?? [];
2642
2954
  const b3 = {
2643
2955
  customer,
2644
2956
  lines,
@@ -2649,13 +2961,14 @@ const buildInsertFromSource = (options, source) => {
2649
2961
  return {
2650
2962
  ...b1,
2651
2963
  ...b2,
2652
- ...b3
2964
+ ...b3,
2965
+ redemption_steps
2653
2966
  };
2654
2967
  };
2655
2968
  const toRedeem = (items) => {
2656
2969
  return (items ?? []).map((item) => ({
2657
2970
  ...item,
2658
- amount: item.amount ?? 1
2971
+ quantity: item.quantity ?? 1
2659
2972
  }));
2660
2973
  };
2661
2974
  /** Local draft until the first mutating flush POSTs to the API. */
@@ -2708,7 +3021,10 @@ const buildLocalDraft = (insert) => {
2708
3021
  totals,
2709
3022
  metadata
2710
3023
  };
2711
- const b6 = { payment_status };
3024
+ const b6 = {
3025
+ payment_status,
3026
+ redemption_steps: insert.redemption_steps ?? []
3027
+ };
2712
3028
  const b7 = {
2713
3029
  fulfillment_status: "IDLE",
2714
3030
  status: "OPEN"
@@ -2924,54 +3240,89 @@ var Order = class Order {
2924
3240
  }
2925
3241
  addLine(lines) {
2926
3242
  if (this.deleted) return;
2927
- const nextLines = addLines(this.draft.lines, lines);
2928
- this.draft = {
2929
- ...this.draft,
2930
- lines: nextLines,
2931
- totals: recomputeTotals(nextLines, this.draft.totals.currency)
2932
- };
2933
- this.scheduleAfterMutate();
3243
+ this.commitLines({ lines: addLines(this.draft.lines, lines) });
2934
3244
  }
2935
3245
  subtractLine(lineUid, quantity = 1) {
2936
3246
  if (this.deleted) return;
2937
- const nextLines = subtractLine(this.draft.lines, lineUid, quantity);
2938
- this.draft = {
2939
- ...this.draft,
2940
- lines: nextLines,
2941
- totals: recomputeTotals(nextLines, this.draft.totals.currency)
2942
- };
2943
- this.scheduleAfterMutate();
3247
+ const p = [
3248
+ this.draft.lines,
3249
+ lineUid,
3250
+ quantity
3251
+ ];
3252
+ this.commitLines({ lines: subtractLine(...p) });
2944
3253
  }
2945
3254
  removeLine(lineUid) {
2946
3255
  if (this.deleted) return;
2947
- const nextLines = removeLine(this.draft.lines, lineUid);
2948
- this.draft = {
2949
- ...this.draft,
2950
- lines: nextLines,
2951
- totals: recomputeTotals(nextLines, this.draft.totals.currency)
2952
- };
2953
- this.scheduleAfterMutate();
3256
+ this.commitLines({ lines: removeLine(this.draft.lines, lineUid) });
2954
3257
  }
2955
3258
  replaceLine(lineUid, lines) {
2956
3259
  if (this.deleted) return;
2957
- const nextLines = replaceLine(this.draft.lines, lineUid, lines);
2958
- this.draft = {
2959
- ...this.draft,
2960
- lines: nextLines,
2961
- totals: recomputeTotals(nextLines, this.draft.totals.currency)
2962
- };
2963
- this.scheduleAfterMutate();
3260
+ const p = [
3261
+ this.draft.lines,
3262
+ lineUid,
3263
+ lines
3264
+ ];
3265
+ this.commitLines({ lines: replaceLine(...p) });
2964
3266
  }
2965
3267
  emptyOrder() {
2966
3268
  if (this.deleted) return;
2967
- const nextLines = emptyLines();
2968
- this.draft = {
2969
- ...this.draft,
2970
- lines: nextLines,
2971
- redeem: [],
2972
- totals: recomputeTotals(nextLines, this.draft.totals.currency)
2973
- };
2974
- this.scheduleAfterMutate();
3269
+ this.commitLines({
3270
+ lines: emptyLines(),
3271
+ redeem: []
3272
+ });
3273
+ }
3274
+ /**
3275
+ * Appends a redeem to `line_uid` (root line) or to the order. Steps keep
3276
+ * the order in which redeems were applied.
3277
+ */
3278
+ applyRedeem(input) {
3279
+ if (this.deleted) return;
3280
+ const { line_uid, ...rest } = input;
3281
+ const redeem = buildRedeem(rest);
3282
+ if (line_uid === void 0) {
3283
+ this.commitLines({ redeem: [...this.draft.redeem ?? [], redeem] });
3284
+ return;
3285
+ }
3286
+ const line = this.draft.lines.find((item) => item.uid === line_uid);
3287
+ if (!line || line.parent_uid != null || line.type === "FEE") throw new Error(`Redeem requires a root product line, got ${line_uid}`);
3288
+ const lines = this.draft.lines.map((item) => {
3289
+ if (item.uid !== line_uid) return item;
3290
+ return {
3291
+ ...item,
3292
+ redeem: [...item.redeem ?? [], redeem]
3293
+ };
3294
+ });
3295
+ this.commitLines({ lines });
3296
+ }
3297
+ /**
3298
+ * Removes `quantity` units of `reward_id` from `line_uid` or the order,
3299
+ * latest applied first. Removing more units than applied clears them all.
3300
+ */
3301
+ removeRedeem(input) {
3302
+ if (this.deleted) return;
3303
+ const { reward_id, quantity, line_uid } = input;
3304
+ if (line_uid === void 0) {
3305
+ const p = [
3306
+ this.draft.redeem ?? [],
3307
+ reward_id,
3308
+ quantity
3309
+ ];
3310
+ this.commitLines({ redeem: subtractRedeem(...p) });
3311
+ return;
3312
+ }
3313
+ const lines = this.draft.lines.map((item) => {
3314
+ if (item.uid !== line_uid) return item;
3315
+ const p = [
3316
+ item.redeem ?? [],
3317
+ reward_id,
3318
+ quantity
3319
+ ];
3320
+ return {
3321
+ ...item,
3322
+ redeem: subtractRedeem(...p)
3323
+ };
3324
+ });
3325
+ this.commitLines({ lines });
2975
3326
  }
2976
3327
  setScope(scope) {
2977
3328
  if (this.deleted) return;
@@ -3068,7 +3419,11 @@ var Order = class Order {
3068
3419
  lines: draft.lines,
3069
3420
  redeem: draft.redeem ?? []
3070
3421
  };
3071
- const b5 = { totals: draft.totals };
3422
+ const steps = draft.redemption_steps ?? [];
3423
+ const b5 = {
3424
+ totals: draft.totals,
3425
+ redemption_steps: steps
3426
+ };
3072
3427
  const body = {
3073
3428
  ...b1,
3074
3429
  ...b2,
@@ -3092,6 +3447,25 @@ var Order = class Order {
3092
3447
  this.synced = cloneOrder(next);
3093
3448
  this.notify();
3094
3449
  }
3450
+ /** Applies lines/redeem, replaying redemption steps and totals. */
3451
+ commitLines(patch) {
3452
+ const lines = patch.lines ?? this.draft.lines;
3453
+ const redeem = patch.redeem ?? this.draft.redeem ?? [];
3454
+ const { redemption_steps, totals } = recomputeRedemptions({
3455
+ lines,
3456
+ redeem,
3457
+ redemption_steps: this.draft.redemption_steps ?? [],
3458
+ currency: this.draft.totals.currency
3459
+ });
3460
+ this.draft = {
3461
+ ...this.draft,
3462
+ lines,
3463
+ redeem,
3464
+ redemption_steps,
3465
+ totals
3466
+ };
3467
+ this.scheduleAfterMutate();
3468
+ }
3095
3469
  scheduleAfterMutate() {
3096
3470
  this.flush.markDirtyDuringFlight();
3097
3471
  this.notify();
@@ -3611,6 +3985,152 @@ const validateProductRoot = (root, lines, lookup, locale, out) => {
3611
3985
  }
3612
3986
  compareTrees(lines, expected, root.uid, expectedRoot.uid, out);
3613
3987
  };
3988
+ const isExpired = (redeem, now) => {
3989
+ if (redeem.expires_at === void 0) return false;
3990
+ return Date.parse(redeem.expires_at) <= now;
3991
+ };
3992
+ const expiredIssues = (redeem, now) => {
3993
+ const issues = [];
3994
+ redeem.forEach((item, index) => {
3995
+ if (!isExpired(item, now)) return;
3996
+ const field = `redeem.${index}.expires_at`;
3997
+ issues.push({
3998
+ code: "REDEEM_EXPIRED",
3999
+ field,
4000
+ actual: item.expires_at
4001
+ });
4002
+ });
4003
+ return issues;
4004
+ };
4005
+ const redeemSignature = (items) => {
4006
+ return items.map((item) => `${item.reward_id}:${item.quantity}`).join(",");
4007
+ };
4008
+ const stepSignature = (steps) => {
4009
+ return steps.map((step) => `${step.reward_id}:${step.reward_quantity}`).join(",");
4010
+ };
4011
+ /** Every target's redeems must have one step each, in the same order. */
4012
+ const checkRedemptionTargets = (lines, redeem, steps, issues, lineIssues) => {
4013
+ const byTarget = /* @__PURE__ */ new Map();
4014
+ for (const step of steps) {
4015
+ const key = redemptionTargetKey(step);
4016
+ byTarget.set(key, [...byTarget.get(key) ?? [], step]);
4017
+ }
4018
+ const check = (key, items) => {
4019
+ const targetSteps = byTarget.get(key) ?? [];
4020
+ byTarget.delete(key);
4021
+ const expected = redeemSignature(items);
4022
+ const actual = stepSignature(targetSteps);
4023
+ if (expected === actual) return null;
4024
+ return {
4025
+ code: "REDEMPTION_STEPS_MISMATCH",
4026
+ field: "redemption_steps",
4027
+ expected,
4028
+ actual
4029
+ };
4030
+ };
4031
+ const orderIssue = check(redemptionTargetKey({ target: "ORDER" }), redeem);
4032
+ if (orderIssue) issues.push(orderIssue);
4033
+ for (const line of lines) {
4034
+ const lineIssue = check(redemptionTargetKey({
4035
+ target: "LINE",
4036
+ target_id: line.uid
4037
+ }), line.redeem ?? []);
4038
+ if (lineIssue) pushLineIssue(lineIssues, line.uid, lineIssue);
4039
+ }
4040
+ for (const orphan of byTarget.values()) {
4041
+ const b1 = { code: "REDEMPTION_STEPS_MISMATCH" };
4042
+ const b2 = {
4043
+ field: "redemption_steps.target_id",
4044
+ expected: ""
4045
+ };
4046
+ issues.push({
4047
+ ...b1,
4048
+ ...b2,
4049
+ actual: stepSignature(orphan)
4050
+ });
4051
+ }
4052
+ };
4053
+ const STATE_FIELDS = [
4054
+ "value",
4055
+ "quantity",
4056
+ "discount_amount",
4057
+ "discount_rate"
4058
+ ];
4059
+ const STATE_KEYS = [
4060
+ "before",
4061
+ "after",
4062
+ "target_before",
4063
+ "target_after"
4064
+ ];
4065
+ const flattenState = (prefix, state, out) => {
4066
+ for (const field of STATE_FIELDS) out[`${prefix}.${field}`] = state ? bigStr(state[field]) : "";
4067
+ };
4068
+ const flattenStep = (step) => {
4069
+ const b1 = { reward_id: step.reward_id };
4070
+ const b2 = { reward_quantity: String(step.reward_quantity) };
4071
+ const b3 = {
4072
+ target: step.target,
4073
+ target_id: step.target_id ?? ""
4074
+ };
4075
+ const b4 = { action: step.action };
4076
+ const b5 = { discount_amount: bigStr(step.discount_amount) };
4077
+ const out = {
4078
+ ...b1,
4079
+ ...b2,
4080
+ ...b3,
4081
+ ...b4,
4082
+ ...b5
4083
+ };
4084
+ for (const key of STATE_KEYS) flattenState(key, step[key], out);
4085
+ return out;
4086
+ };
4087
+ /** Replayed steps must equal the submitted ones field by field. */
4088
+ const compareRedemptionSteps = (submitted, expected, issues) => {
4089
+ if (submitted.length !== expected.length) {
4090
+ const b1 = { code: "REDEMPTION_MISMATCH" };
4091
+ const b2 = { field: "redemption_steps" };
4092
+ const b3 = { expected: String(expected.length) };
4093
+ issues.push({
4094
+ ...b1,
4095
+ ...b2,
4096
+ ...b3,
4097
+ actual: String(submitted.length)
4098
+ });
4099
+ return;
4100
+ }
4101
+ submitted.forEach((step, index) => {
4102
+ const actual = flattenStep(step);
4103
+ const wanted = flattenStep(expected[index]);
4104
+ for (const field of Object.keys(wanted)) {
4105
+ if (actual[field] === wanted[field]) continue;
4106
+ const b1 = { code: "REDEMPTION_MISMATCH" };
4107
+ const b2 = { field: `redemption_steps.${index}.${field}` };
4108
+ const b3 = {
4109
+ expected: wanted[field],
4110
+ actual: actual[field]
4111
+ };
4112
+ issues.push({
4113
+ ...b1,
4114
+ ...b2,
4115
+ ...b3
4116
+ });
4117
+ }
4118
+ });
4119
+ };
4120
+ const validateRedemptions = (input, issues, lineIssues) => {
4121
+ const redeem = input.redeem ?? [];
4122
+ const steps = input.redemptionSteps ?? [];
4123
+ const now = input.now ?? Date.now();
4124
+ issues.push(...expiredIssues(redeem, now));
4125
+ for (const line of input.lines) {
4126
+ const expired = expiredIssues(line.redeem ?? [], now);
4127
+ for (const issue of expired) pushLineIssue(lineIssues, line.uid, issue);
4128
+ }
4129
+ checkRedemptionTargets(input.lines, redeem, steps, issues, lineIssues);
4130
+ const expected = computeRedemptionSteps(input.lines, redeem, steps);
4131
+ compareRedemptionSteps(steps, expected, issues);
4132
+ return expected;
4133
+ };
3614
4134
  /** Compare cart lines/totals against a fresh menu (exact Big.js equality). */
3615
4135
  const validateOrderAgainstMenu = (input) => {
3616
4136
  const locale = input.locale ?? "en_us";
@@ -3641,7 +4161,8 @@ const validateOrderAgainstMenu = (input) => {
3641
4161
  }
3642
4162
  checkRootProductQuantities(roots, lookup, lineIssues);
3643
4163
  if (input.expectedFeeLines) compareExpectedFeeLines(input.lines, input.expectedFeeLines, lineIssues);
3644
- const expectedTotals = recomputeTotals(input.lines, input.totals.currency);
4164
+ const expectedSteps = validateRedemptions(input, issues, lineIssues);
4165
+ const expectedTotals = applyRedemptionTotals(recomputeTotals(input.lines, input.totals.currency), expectedSteps);
3645
4166
  for (const field of TOTALS_FIELDS) {
3646
4167
  if (bigEq(input.totals[field], expectedTotals[field])) continue;
3647
4168
  issues.push({
@@ -5018,4 +5539,4 @@ const paymentMatchesClientConditions = (conditions, order, options) => {
5018
5539
  return Boolean(engine.matchOrConditions(groups).match);
5019
5540
  };
5020
5541
  //#endregion
5021
- export { DEFAULT_RECOMMENDATION_TYPES, Order, OrderSyncPendingError, OrderValidationError, RecurringDateCron, RulesEngine, StorefrontHttpError, addLines, auth, buildCustomerSnapshot, collectOrderLineDiffs, countCartProducts, createSession, currentNodes, defaultConditionError, flattenMenuCategories, fromOrderLines, getScheduleWindow, indexMenu, isCheckoutFieldFilled, isDeviceNotLinked, isKioskUnauthorized, isMilestoneStepDone, isOnSchedule, isOrderValidationError, isStorefrontNotFound, listMenusStream, matchAbsoluteDateOperator, matchBooleanOperator, matchCurrencyOperator, matchEnumOperator, matchNumberOperator, matchRecurringDateOperator, matchRelativeDateOperator, matchStringOperator, milestoneSteps, milestoneTrack, nestMenuCategories, paymentMatchesClientConditions, recomputeTotals, resolveCheckout, services, storefrontJson, toOrderLines, validateOrderAgainstMenu, visitedStates };
5542
+ export { DEFAULT_RECOMMENDATION_TYPES, Order, OrderSyncPendingError, OrderValidationError, RecurringDateCron, RulesEngine, StorefrontHttpError, addLines, auth, buildCustomerSnapshot, collectOrderLineDiffs, countCartProducts, createSession, currentNodes, defaultConditionError, flattenMenuCategories, fromOrderLines, genId, genOrderId, getScheduleWindow, indexMenu, isCheckoutFieldFilled, isDeviceNotLinked, isKioskUnauthorized, isMilestoneStepDone, isOnSchedule, isOrderValidationError, isStorefrontNotFound, listMenusStream, matchAbsoluteDateOperator, matchBooleanOperator, matchCurrencyOperator, matchEnumOperator, matchNumberOperator, matchRecurringDateOperator, matchRelativeDateOperator, matchStringOperator, milestoneSteps, milestoneTrack, nestMenuCategories, paymentMatchesClientConditions, recomputeRedemptions, recomputeTotals, resolveCheckout, services, storefrontJson, toOrderLines, validateOrderAgainstMenu, visitedStates };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fire-spark/core",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Fire Spark headless SDK",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -11,7 +11,6 @@
11
11
  "types": "./dist/index.d.ts",
12
12
  "exports": {
13
13
  ".": {
14
- "development": "./src/index.ts",
15
14
  "types": "./dist/index.d.ts",
16
15
  "import": "./dist/index.js",
17
16
  "default": "./dist/index.js"
@@ -20,7 +19,7 @@
20
19
  "dependencies": {
21
20
  "big.js": "6.2.1",
22
21
  "dayjs": "1.11.9",
23
- "@fire-spark/types": "0.0.1"
22
+ "@fire-spark/types": "0.0.2"
24
23
  },
25
24
  "devDependencies": {
26
25
  "@types/big.js": "6.2.2",