@aglyn/plugins-commerce 1.0.0-beta.232 → 1.0.0-beta.234

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 (228) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/cart-extras.d.ts +6 -0
  3. package/src/lib/components/cart-extras.js +4 -1
  4. package/src/lib/components/cart-extras.js.map +1 -1
  5. package/src/lib/components/cart-fulfillment.d.ts +117 -0
  6. package/src/lib/components/cart-fulfillment.js +367 -0
  7. package/src/lib/components/cart-fulfillment.js.map +1 -0
  8. package/src/lib/components/cart.js +52 -10
  9. package/src/lib/components/cart.js.map +1 -1
  10. package/src/lib/components/commerce-console-page.js +42 -1
  11. package/src/lib/components/commerce-console-page.js.map +1 -1
  12. package/src/lib/components/console/host-orders-card.component.js +38 -10
  13. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  14. package/src/lib/components/console/local-delivery-settings-card.component.d.ts +37 -0
  15. package/src/lib/components/console/local-delivery-settings-card.component.js +542 -0
  16. package/src/lib/components/console/local-delivery-settings-card.component.js.map +1 -0
  17. package/src/lib/components/console/locations-card.component.js +177 -3
  18. package/src/lib/components/console/locations-card.component.js.map +1 -1
  19. package/src/lib/components/console/order-detail-dialog.component.js +10 -0
  20. package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
  21. package/src/lib/components/console/order-local-fulfillment-panel.component.d.ts +44 -0
  22. package/src/lib/components/console/order-local-fulfillment-panel.component.js +265 -0
  23. package/src/lib/components/console/order-local-fulfillment-panel.component.js.map +1 -0
  24. package/src/lib/components/console/order-offline-notice.component.d.ts +12 -0
  25. package/src/lib/components/console/order-offline-notice.component.js +53 -0
  26. package/src/lib/components/console/order-offline-notice.component.js.map +1 -0
  27. package/src/lib/components/console/pickup-delivery-queue-card.component.d.ts +35 -0
  28. package/src/lib/components/console/pickup-delivery-queue-card.component.js +222 -0
  29. package/src/lib/components/console/pickup-delivery-queue-card.component.js.map +1 -0
  30. package/src/lib/components/console/pos/pos-api.d.ts +24 -1
  31. package/src/lib/components/console/pos/pos-api.js +13 -0
  32. package/src/lib/components/console/pos/pos-api.js.map +1 -1
  33. package/src/lib/components/console/pos/pos-item-dialog.component.d.ts +12 -0
  34. package/src/lib/components/console/pos/pos-item-dialog.component.js +10 -8
  35. package/src/lib/components/console/pos/pos-item-dialog.component.js.map +1 -1
  36. package/src/lib/components/console/pos/pos-kiosk-queue.component.d.ts +22 -0
  37. package/src/lib/components/console/pos/pos-kiosk-queue.component.js +168 -0
  38. package/src/lib/components/console/pos/pos-kiosk-queue.component.js.map +1 -0
  39. package/src/lib/components/console/pos/pos-tender-dialogs.component.d.ts +18 -0
  40. package/src/lib/components/console/pos/pos-tender-dialogs.component.js +116 -1
  41. package/src/lib/components/console/pos/pos-tender-dialogs.component.js.map +1 -1
  42. package/src/lib/components/console/pos/pos-tender-panel.component.d.ts +8 -0
  43. package/src/lib/components/console/pos/pos-tender-panel.component.js +77 -18
  44. package/src/lib/components/console/pos/pos-tender-panel.component.js.map +1 -1
  45. package/src/lib/components/console/pos/use-pos-display.d.ts +5 -2
  46. package/src/lib/components/console/pos/use-pos-display.js +3 -2
  47. package/src/lib/components/console/pos/use-pos-display.js.map +1 -1
  48. package/src/lib/components/console/pos-devices-card.component.js +2 -2
  49. package/src/lib/components/console/pos-devices-card.component.js.map +1 -1
  50. package/src/lib/components/console/pos-display/pairing-screen.d.ts +5 -2
  51. package/src/lib/components/console/pos-display/pairing-screen.js +7 -5
  52. package/src/lib/components/console/pos-display/pairing-screen.js.map +1 -1
  53. package/src/lib/components/console/pos-display/pos-display-api.d.ts +7 -1
  54. package/src/lib/components/console/pos-display/pos-display-api.js +7 -2
  55. package/src/lib/components/console/pos-display/pos-display-api.js.map +1 -1
  56. package/src/lib/components/console/pos-kiosk/kiosk-checkout.d.ts +41 -0
  57. package/src/lib/components/console/pos-kiosk/kiosk-checkout.js +275 -0
  58. package/src/lib/components/console/pos-kiosk/kiosk-checkout.js.map +1 -0
  59. package/src/lib/components/console/pos-kiosk/kiosk-menu.d.ts +32 -0
  60. package/src/lib/components/console/pos-kiosk/kiosk-menu.js +344 -0
  61. package/src/lib/components/console/pos-kiosk/kiosk-menu.js.map +1 -0
  62. package/src/lib/components/console/pos-kiosk/kiosk-staff.d.ts +12 -0
  63. package/src/lib/components/console/pos-kiosk/kiosk-staff.js +257 -0
  64. package/src/lib/components/console/pos-kiosk/kiosk-staff.js.map +1 -0
  65. package/src/lib/components/console/pos-kiosk/pos-kiosk-api.d.ts +114 -0
  66. package/src/lib/components/console/pos-kiosk/pos-kiosk-api.js +189 -0
  67. package/src/lib/components/console/pos-kiosk/pos-kiosk-api.js.map +1 -0
  68. package/src/lib/components/console/pos-kiosk/use-kiosk-idle.d.ts +16 -0
  69. package/src/lib/components/console/pos-kiosk/use-kiosk-idle.js +80 -0
  70. package/src/lib/components/console/pos-kiosk/use-kiosk-idle.js.map +1 -0
  71. package/src/lib/components/console/pos-kiosk-page.component.d.ts +28 -0
  72. package/src/lib/components/console/pos-kiosk-page.component.js +791 -0
  73. package/src/lib/components/console/pos-kiosk-page.component.js.map +1 -0
  74. package/src/lib/components/console/pos-offline/pos-offline-api.d.ts +44 -0
  75. package/src/lib/components/console/pos-offline/pos-offline-api.js +69 -0
  76. package/src/lib/components/console/pos-offline/pos-offline-api.js.map +1 -0
  77. package/src/lib/components/console/pos-offline/pos-offline-banner.component.d.ts +7 -0
  78. package/src/lib/components/console/pos-offline/pos-offline-banner.component.js +135 -0
  79. package/src/lib/components/console/pos-offline/pos-offline-banner.component.js.map +1 -0
  80. package/src/lib/components/console/pos-offline/pos-offline-checkout.component.d.ts +32 -0
  81. package/src/lib/components/console/pos-offline/pos-offline-checkout.component.js +287 -0
  82. package/src/lib/components/console/pos-offline/pos-offline-checkout.component.js.map +1 -0
  83. package/src/lib/components/console/pos-offline/pos-offline-store.d.ts +63 -0
  84. package/src/lib/components/console/pos-offline/pos-offline-store.js +145 -0
  85. package/src/lib/components/console/pos-offline/pos-offline-store.js.map +1 -0
  86. package/src/lib/components/console/pos-offline/register-offline.d.ts +24 -0
  87. package/src/lib/components/console/pos-offline/register-offline.js +24 -0
  88. package/src/lib/components/console/pos-offline/register-offline.js.map +1 -0
  89. package/src/lib/components/console/pos-offline/use-pos-offline.d.ts +85 -0
  90. package/src/lib/components/console/pos-offline/use-pos-offline.js +542 -0
  91. package/src/lib/components/console/pos-offline/use-pos-offline.js.map +1 -0
  92. package/src/lib/components/console/pos-ops/pos-shift-history-card.component.js +72 -17
  93. package/src/lib/components/console/pos-ops/pos-shift-history-card.component.js.map +1 -1
  94. package/src/lib/components/console/pos-page.component.js +174 -27
  95. package/src/lib/components/console/pos-page.component.js.map +1 -1
  96. package/src/lib/components/console/products-hub-card.component.js +35 -10
  97. package/src/lib/components/console/products-hub-card.component.js.map +1 -1
  98. package/src/lib/components/console/shipping-settings-card.component.js +1 -1
  99. package/src/lib/components/console/shipping-settings-card.component.js.map +1 -1
  100. package/src/lib/components/console/stock-movements-card.component.js +48 -16
  101. package/src/lib/components/console/stock-movements-card.component.js.map +1 -1
  102. package/src/lib/components/console/store-zones.d.ts +8 -1
  103. package/src/lib/components/console/store-zones.js +11 -1
  104. package/src/lib/components/console/store-zones.js.map +1 -1
  105. package/src/lib/components/order-status.js +66 -0
  106. package/src/lib/components/order-status.js.map +1 -1
  107. package/src/lib/components/product-detail.js +42 -15
  108. package/src/lib/components/product-detail.js.map +1 -1
  109. package/src/lib/constants/local-fulfillment-list-query.d.ts +48 -0
  110. package/src/lib/constants/local-fulfillment-list-query.js +84 -0
  111. package/src/lib/constants/local-fulfillment-list-query.js.map +1 -0
  112. package/src/lib/constants/orders-list-query.d.ts +7 -2
  113. package/src/lib/constants/orders-list-query.js +89 -8
  114. package/src/lib/constants/orders-list-query.js.map +1 -1
  115. package/src/lib/constants/pos-shift-list-query.d.ts +22 -0
  116. package/src/lib/constants/pos-shift-list-query.js +115 -0
  117. package/src/lib/constants/pos-shift-list-query.js.map +1 -0
  118. package/src/lib/constants/product-list-query.d.ts +6 -1
  119. package/src/lib/constants/product-list-query.js +77 -8
  120. package/src/lib/constants/product-list-query.js.map +1 -1
  121. package/src/lib/constants/stock-movement-query.d.ts +3 -2
  122. package/src/lib/constants/stock-movement-query.js +52 -8
  123. package/src/lib/constants/stock-movement-query.js.map +1 -1
  124. package/src/lib/model/buyer-notifications.d.ts +5 -3
  125. package/src/lib/model/buyer-notifications.js +25 -4
  126. package/src/lib/model/buyer-notifications.js.map +1 -1
  127. package/src/lib/model/commerce-orders.d.ts +59 -0
  128. package/src/lib/model/commerce-orders.js.map +1 -1
  129. package/src/lib/model/commerce-pos-kiosk.d.ts +175 -0
  130. package/src/lib/model/commerce-pos-kiosk.js +147 -0
  131. package/src/lib/model/commerce-pos-kiosk.js.map +1 -0
  132. package/src/lib/model/commerce-pos-offline.d.ts +234 -0
  133. package/src/lib/model/commerce-pos-offline.js +301 -0
  134. package/src/lib/model/commerce-pos-offline.js.map +1 -0
  135. package/src/lib/model/commerce-pos-ops.d.ts +16 -6
  136. package/src/lib/model/commerce-pos-ops.js +12 -4
  137. package/src/lib/model/commerce-pos-ops.js.map +1 -1
  138. package/src/lib/model/commerce-pos.d.ts +13 -1
  139. package/src/lib/model/commerce-pos.js +5 -4
  140. package/src/lib/model/commerce-pos.js.map +1 -1
  141. package/src/lib/model/commerce-stock-holds.d.ts +24 -3
  142. package/src/lib/model/commerce-stock-holds.js +26 -8
  143. package/src/lib/model/commerce-stock-holds.js.map +1 -1
  144. package/src/lib/model/commerce.d.ts +3 -0
  145. package/src/lib/model/commerce.js.map +1 -1
  146. package/src/lib/model/index.d.ts +4 -0
  147. package/src/lib/model/index.js +4 -0
  148. package/src/lib/model/index.js.map +1 -1
  149. package/src/lib/model/local-fulfillment-settings.d.ts +178 -0
  150. package/src/lib/model/local-fulfillment-settings.js +361 -0
  151. package/src/lib/model/local-fulfillment-settings.js.map +1 -0
  152. package/src/lib/model/order-list-fields.d.ts +10 -1
  153. package/src/lib/model/order-list-fields.js +1 -0
  154. package/src/lib/model/order-list-fields.js.map +1 -1
  155. package/src/lib/model/order-local-fulfillment.d.ts +131 -0
  156. package/src/lib/model/order-local-fulfillment.js +125 -0
  157. package/src/lib/model/order-local-fulfillment.js.map +1 -0
  158. package/src/lib/model/order-shipping-export.js +4 -0
  159. package/src/lib/model/order-shipping-export.js.map +1 -1
  160. package/src/lib/model/order-status-view.d.ts +20 -1
  161. package/src/lib/model/order-status-view.js +60 -6
  162. package/src/lib/model/order-status-view.js.map +1 -1
  163. package/src/lib/plugin-config.js +6 -2
  164. package/src/lib/plugin-config.js.map +1 -1
  165. package/src/lib/plugin.js +8 -0
  166. package/src/lib/plugin.js.map +1 -1
  167. package/src/lib/pos-kiosk-config.d.ts +26 -0
  168. package/src/lib/pos-kiosk-config.js +59 -0
  169. package/src/lib/pos-kiosk-config.js.map +1 -0
  170. package/src/lib/server/api-v1/openapi.js +8 -0
  171. package/src/lib/server/api-v1/openapi.js.map +1 -1
  172. package/src/lib/server/api-v1/order-view.d.ts +19 -0
  173. package/src/lib/server/api-v1/order-view.js +35 -0
  174. package/src/lib/server/api-v1/order-view.js.map +1 -1
  175. package/src/lib/server/billing-webhook.js +238 -87
  176. package/src/lib/server/billing-webhook.js.map +1 -1
  177. package/src/lib/server/cart-checkout.d.ts +2 -0
  178. package/src/lib/server/cart-checkout.js +173 -13
  179. package/src/lib/server/cart-checkout.js.map +1 -1
  180. package/src/lib/server/checkout-extras.js +18 -4
  181. package/src/lib/server/checkout-extras.js.map +1 -1
  182. package/src/lib/server/checkout.js +71 -14
  183. package/src/lib/server/checkout.js.map +1 -1
  184. package/src/lib/server/local-fulfillment-status.d.ts +86 -0
  185. package/src/lib/server/local-fulfillment-status.js +379 -0
  186. package/src/lib/server/local-fulfillment-status.js.map +1 -0
  187. package/src/lib/server/local-fulfillment.d.ts +190 -0
  188. package/src/lib/server/local-fulfillment.js +580 -0
  189. package/src/lib/server/local-fulfillment.js.map +1 -0
  190. package/src/lib/server/membership-register.js +3 -1
  191. package/src/lib/server/membership-register.js.map +1 -1
  192. package/src/lib/server/order-notifications.js +61 -0
  193. package/src/lib/server/order-notifications.js.map +1 -1
  194. package/src/lib/server/pos-display.d.ts +27 -1
  195. package/src/lib/server/pos-display.js +50 -9
  196. package/src/lib/server/pos-display.js.map +1 -1
  197. package/src/lib/server/pos-kiosk-principal.d.ts +29 -0
  198. package/src/lib/server/pos-kiosk-principal.js +42 -0
  199. package/src/lib/server/pos-kiosk-principal.js.map +1 -0
  200. package/src/lib/server/pos-kiosk.d.ts +54 -0
  201. package/src/lib/server/pos-kiosk.js +972 -0
  202. package/src/lib/server/pos-kiosk.js.map +1 -0
  203. package/src/lib/server/pos-offline-sync.d.ts +85 -0
  204. package/src/lib/server/pos-offline-sync.js +765 -0
  205. package/src/lib/server/pos-offline-sync.js.map +1 -0
  206. package/src/lib/server/pos-order.js +22 -4
  207. package/src/lib/server/pos-order.js.map +1 -1
  208. package/src/lib/server/pos-payment.d.ts +6 -0
  209. package/src/lib/server/pos-payment.js +225 -6
  210. package/src/lib/server/pos-payment.js.map +1 -1
  211. package/src/lib/server/pos-print.js +5 -0
  212. package/src/lib/server/pos-print.js.map +1 -1
  213. package/src/lib/server/pos-return.js +27 -0
  214. package/src/lib/server/pos-return.js.map +1 -1
  215. package/src/lib/server/pos-sale.d.ts +6 -0
  216. package/src/lib/server/pos-sale.js +2 -0
  217. package/src/lib/server/pos-sale.js.map +1 -1
  218. package/src/lib/server/pos-shift.js +8 -1
  219. package/src/lib/server/pos-shift.js.map +1 -1
  220. package/src/lib/server/refund.js +3 -0
  221. package/src/lib/server/refund.js.map +1 -1
  222. package/src/lib/server/stock-hold.d.ts +9 -1
  223. package/src/lib/server/stock-hold.js +15 -7
  224. package/src/lib/server/stock-hold.js.map +1 -1
  225. package/src/lib/server.js +16 -0
  226. package/src/lib/server.js.map +1 -1
  227. package/src/lib/tenant-emails.js +161 -0
  228. package/src/lib/tenant-emails.js.map +1 -1
@@ -0,0 +1,301 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { computeOrderTotals } from "./commerce-orders.js";
18
+ import { computeTaxCents } from "./commerce-tax.js";
19
+ /*==========================================
20
+ * THE OFFLINE REGISTER (AGL-3625): the contract every register client — the
21
+ * web register and the native Aglyn POS app — shares with
22
+ * `POST /api/commerce/pos-offline-sync`.
23
+ *
24
+ * WHAT SELLS OFFLINE IS CASH, AND ONLY CASH.
25
+ *
26
+ * - Card tenders cannot: the reader, Tap to Pay, the typed card and the QR
27
+ * link are all server-driven Stripe payments, and Stripe does not take a
28
+ * server-driven card payment without the server.
29
+ * - Gift cards DO NOT, deliberately. A balance cannot be checked without the
30
+ * server, and a capped "allow it anyway" (Square's offer for cards) turns a
31
+ * short balance into a sale whose money nobody collected — discovered at
32
+ * sync, after the customer left. Square itself does not redeem gift cards
33
+ * offline. A gift card is never over-redeemed silently because it is never
34
+ * redeemed offline at all.
35
+ * - Room charges do not: the stay is validated on the server.
36
+ * - Store credit and rewards do not, for the gift card's reason: a balance
37
+ * is the server's to check.
38
+ * - Codes and store promotions do not: a redemption cap is counted on the
39
+ * server. The cashier's own discount, bounded by the site's ceiling, does.
40
+ *
41
+ * WHAT THE SALE IS: the register prices the basket from its cached catalog and
42
+ * the store's own tax rate, takes the cash and queues the sale with a key it
43
+ * minted. That key is the order's id on the server, so the receipt printed
44
+ * offline carries the barcode the real order answers to, and a sale sent twice
45
+ * — a flapping connection, a retry after a lost answer — lands once.
46
+ *
47
+ * WHAT THE SERVER DOES WITH IT: records it as it happened, never re-prices the
48
+ * money the customer handed over, and FLAGS what does not match — a price
49
+ * that changed, stock that went short, a shift that closed first — on the
50
+ * order and back to the register. A sale is never dropped for a flag.
51
+ *=========================================*/ /** The route both register clients sync through. */ export const POS_OFFLINE_SYNC_ROUTE = '/api/commerce/pos-offline-sync';
52
+ /** The most sales one sync request carries; a longer queue sends in batches. */ export const POS_OFFLINE_SYNC_BATCH_MAX = 25;
53
+ /** Lines one offline sale may carry, the same bound the register's basket has. */ export const POS_OFFLINE_SALE_MAX_LINES = 100;
54
+ /** A sale synced later than this after it was rung is flagged `late-sync`. */ export const POS_OFFLINE_LATE_SYNC_MS = 24 * 60 * 60 * 1000;
55
+ /**
56
+ * How far back a sale's own clock is believed. Older, and the server stamps
57
+ * the sale at this bound and flags it, rather than letting a device clock
58
+ * place a sale anywhere in history.
59
+ */ export const POS_OFFLINE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
60
+ /** How far ahead of the server a device clock may run before it is clamped. */ export const POS_OFFLINE_CLOCK_SKEW_MS = 10 * 60 * 1000;
61
+ /** Every tender the register turns off while offline, and why, in its words. */ export const POS_OFFLINE_DISABLED_TENDERS = {
62
+ card_present: 'Card readers need the connection.',
63
+ card_keyed: 'Typed cards need the connection.',
64
+ card_link: 'The QR card link needs the connection.',
65
+ gift_card: 'A gift card balance can only be checked online.',
66
+ folio: 'Room charges are checked against the stay online.',
67
+ credit: 'Store credit and rewards balances can only be checked online.'
68
+ };
69
+ /** Each flag in the words the order and the register show. */ export const POS_OFFLINE_FLAG_LABELS = {
70
+ 'price-changed': 'A price changed after the register cached it; the sale kept the price it was rung at',
71
+ 'unknown-product': 'A product was deleted before the sale synced; the line kept what the receipt printed',
72
+ 'tax-differs': 'The tax rung differs from the store’s rate now; the sale kept the tax it collected',
73
+ 'totals-restated': 'The register’s totals did not add up; the server restated them from the lines',
74
+ 'discount-over-limit': 'The discount is above this register’s limit',
75
+ 'cash-short': 'The cash recorded is less than the total',
76
+ 'stock-short': 'Stock went short: more was sold than the shelf held',
77
+ 'shift-closed': 'Rung in a shift that closed before the sale synced; its Z report does not include it',
78
+ 'no-shift': 'Rung with no shift open, on a site that requires one',
79
+ 'unknown-register': 'The register it was rung on no longer exists',
80
+ 'register-over-cap': 'The register it was rung on is over the plan’s register limit',
81
+ 'late-sync': 'Synced more than a day after it was rung',
82
+ 'clock-adjusted': 'The device clock was off; the sale is dated when it synced',
83
+ 'cashier-unverified': 'The PIN-switched cashier could not be confirmed; the sale is under the signed-in member'
84
+ };
85
+ /**
86
+ * The totals of an offline sale: the cashier's discount on the goods, then
87
+ * the store's in-person tax on what is left — exactly the arithmetic the
88
+ * online register's sale route uses for the same basket with no promotion.
89
+ * `pricesIncludeTax` adds nothing, as at the online till.
90
+ */ export function posOfflineSaleTotals(input) {
91
+ const lines = input.lines.map((line)=>({
92
+ unitAmountCents: wholeCents(line.unitAmountCents),
93
+ quantity: Math.max(1, Math.round(Number(line.quantity) || 1))
94
+ }));
95
+ const itemsCents = lines.reduce((sum, line)=>sum + line.unitAmountCents * line.quantity, 0);
96
+ const pct = Number.isFinite(input.discountPct) ? Math.min(100, Math.max(0, input.discountPct)) : 0;
97
+ const discountCents = Math.min(itemsCents, Math.round(itemsCents * pct / 100));
98
+ const taxPct = Number(input.tax.pct);
99
+ const taxCents = taxPct > 0 && !input.tax.pricesIncludeTax ? computeTaxCents(itemsCents - discountCents, taxPct) : 0;
100
+ return computeOrderTotals(lines, {
101
+ discountCents,
102
+ taxCents,
103
+ feeCents: 0
104
+ });
105
+ }
106
+ /** Change for cash handed over, never negative. */ export function posOfflineChangeCents(totalCents, tenderedCents) {
107
+ return Math.max(0, wholeCents(tenderedCents) - wholeCents(totalCents));
108
+ }
109
+ /** A key the register minted: an id the order can take, and nothing else. */ export function posOfflineSaleKeyIsValid(key) {
110
+ return typeof key === 'string' && /^[A-Za-z0-9_-]{16,64}$/.test(key) && !/^__.*__$/.test(key);
111
+ }
112
+ /**
113
+ * The sale's time as the server records it: the device's clock when it is
114
+ * believable, clamped to now when it runs ahead, and to the oldest believable
115
+ * moment when it claims to be older.
116
+ */ export function posOfflineSoldAt(soldAtMs, nowMs) {
117
+ const at = Number(soldAtMs);
118
+ if (!Number.isFinite(at) || at <= 0) return {
119
+ atMs: nowMs,
120
+ adjusted: true
121
+ };
122
+ if (at > nowMs + POS_OFFLINE_CLOCK_SKEW_MS) return {
123
+ atMs: nowMs,
124
+ adjusted: true
125
+ };
126
+ if (at < nowMs - POS_OFFLINE_MAX_AGE_MS) return {
127
+ atMs: nowMs,
128
+ adjusted: true
129
+ };
130
+ return {
131
+ atMs: Math.min(at, nowMs),
132
+ adjusted: false
133
+ };
134
+ }
135
+ const bounded = (value, max)=>typeof value === 'string' ? value.trim().slice(0, max) : '';
136
+ const cleanId = (value)=>{
137
+ const id = String(value != null ? value : '').trim();
138
+ return /^[A-Za-z0-9_-]{1,128}$/.test(id) && !/^__.*__$/.test(id) ? id : '';
139
+ };
140
+ function wholeCents(value) {
141
+ const number = Math.round(Number(value));
142
+ return Number.isFinite(number) && number > 0 ? number : 0;
143
+ }
144
+ /**
145
+ * A queued sale read off the wire, every field typed and bounded — or the
146
+ * sentence that says why it cannot be read at all. Pure, so the server and
147
+ * any client that wants to check its own queue answer the same.
148
+ */ export function sanitizePosOfflineSale(raw) {
149
+ var _input_discountPct, _input_totals;
150
+ if (!raw || typeof raw !== 'object') return {
151
+ ok: false,
152
+ error: 'Not a sale'
153
+ };
154
+ const input = raw;
155
+ const saleKey = input['saleKey'];
156
+ if (!posOfflineSaleKeyIsValid(saleKey)) return {
157
+ ok: false,
158
+ error: 'The sale has no valid key'
159
+ };
160
+ const hostId = cleanId(input['hostId']);
161
+ const registerId = cleanId(input['registerId']);
162
+ const signedInUid = bounded(input['signedInUid'], 128);
163
+ if (!hostId || !registerId || !signedInUid) {
164
+ return {
165
+ ok: false,
166
+ error: 'The sale is missing its site, register or member'
167
+ };
168
+ }
169
+ const rawLines = Array.isArray(input['lines']) ? input['lines'] : [];
170
+ if (rawLines.length === 0 || rawLines.length > POS_OFFLINE_SALE_MAX_LINES) {
171
+ return {
172
+ ok: false,
173
+ error: 'The sale has no lines'
174
+ };
175
+ }
176
+ const lines = [];
177
+ for (const line of rawLines){
178
+ const productId = cleanId(line == null ? void 0 : line.productId);
179
+ const name = bounded(line == null ? void 0 : line.name, 200);
180
+ const unit = Math.round(Number(line == null ? void 0 : line.unitAmountCents));
181
+ const quantity = Math.round(Number(line == null ? void 0 : line.quantity));
182
+ if (!productId || !name || !Number.isFinite(unit) || unit < 0 || !(quantity >= 1 && quantity <= 99)) {
183
+ return {
184
+ ok: false,
185
+ error: 'A line of the sale cannot be read'
186
+ };
187
+ }
188
+ const variantId = cleanId(line == null ? void 0 : line.variantId);
189
+ const variantLabel = bounded(line == null ? void 0 : line.variantLabel, 200);
190
+ const sku = bounded(line == null ? void 0 : line.sku, 100);
191
+ const productType = [
192
+ 'physical',
193
+ 'digital',
194
+ 'service'
195
+ ].includes(line == null ? void 0 : line.productType) ? line.productType : undefined;
196
+ const modifiers = Array.isArray(line == null ? void 0 : line.modifiers) ? line.modifiers.slice(0, 50).map((pick)=>({
197
+ groupId: cleanId(pick == null ? void 0 : pick.groupId),
198
+ optionId: cleanId(pick == null ? void 0 : pick.optionId)
199
+ })).filter((pick)=>pick.groupId && pick.optionId) : [];
200
+ lines.push(_extends({
201
+ productId
202
+ }, variantId ? {
203
+ variantId
204
+ } : {}, {
205
+ name
206
+ }, variantLabel ? {
207
+ variantLabel
208
+ } : {}, sku ? {
209
+ sku
210
+ } : {}, productType ? {
211
+ productType
212
+ } : {}, {
213
+ quantity,
214
+ unitAmountCents: unit
215
+ }, modifiers.length ? {
216
+ modifiers
217
+ } : {}));
218
+ }
219
+ const discountPct = Number((_input_discountPct = input['discountPct']) != null ? _input_discountPct : 0);
220
+ if (!Number.isFinite(discountPct) || discountPct < 0 || discountPct > 100) {
221
+ return {
222
+ ok: false,
223
+ error: 'The sale’s discount cannot be read'
224
+ };
225
+ }
226
+ const totals = (_input_totals = input['totals']) != null ? _input_totals : {};
227
+ const customerRaw = input['customer'];
228
+ const customer = {};
229
+ if (customerRaw && typeof customerRaw === 'object') {
230
+ const email = bounded(customerRaw.email, 254).toLowerCase();
231
+ if (email && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) customer.email = email;
232
+ const name = bounded(customerRaw.name, 200);
233
+ if (name) customer.name = name;
234
+ const kind = bounded(customerRaw.kind, 40);
235
+ const id = cleanId(customerRaw.id);
236
+ if (kind && kind !== 'none' && id) {
237
+ customer.kind = kind;
238
+ customer.id = id;
239
+ }
240
+ }
241
+ const shiftId = cleanId(input['shiftId']);
242
+ const locationId = cleanId(input['locationId']);
243
+ const assertion = bounded(input['cashierAssertion'], 2000);
244
+ const orgId = bounded(input['orgId'], 128);
245
+ return {
246
+ ok: true,
247
+ sale: _extends({
248
+ v: 1,
249
+ saleKey,
250
+ hostId,
251
+ orgId,
252
+ signedInUid,
253
+ registerId,
254
+ shiftId: shiftId || null
255
+ }, locationId ? {
256
+ locationId
257
+ } : {}, assertion ? {
258
+ cashierAssertion: assertion
259
+ } : {}, {
260
+ soldAtMs: Number(input['soldAtMs']) || 0,
261
+ lines,
262
+ discountPct,
263
+ totals: {
264
+ itemsCents: wholeCents(totals['itemsCents']),
265
+ shippingCents: 0,
266
+ taxCents: wholeCents(totals['taxCents']),
267
+ discountCents: wholeCents(totals['discountCents']),
268
+ totalCents: wholeCents(totals['totalCents']),
269
+ feeCents: 0
270
+ },
271
+ cashTenderedCents: wholeCents(input['cashTenderedCents']),
272
+ changeCents: wholeCents(input['changeCents'])
273
+ }, Object.keys(customer).length ? {
274
+ customer
275
+ } : {})
276
+ };
277
+ }
278
+ /**
279
+ * Whether the register's totals are the arithmetic of its own lines: the
280
+ * goods are the lines' sum, the discount is the percentage of them, and the
281
+ * total is goods less discount plus the tax it rang. The tax itself is the
282
+ * register's, and checked against the store's rate separately.
283
+ */ export function posOfflineTotalsAddUp(sale) {
284
+ const expected = posOfflineSaleTotals({
285
+ lines: sale.lines,
286
+ discountPct: sale.discountPct,
287
+ tax: {
288
+ pct: null,
289
+ pricesIncludeTax: false
290
+ }
291
+ });
292
+ return expected.itemsCents === sale.totals.itemsCents && expected.discountCents === sale.totals.discountCents && sale.totals.taxCents <= sale.totals.itemsCents && sale.totals.totalCents === expected.itemsCents - expected.discountCents + sale.totals.taxCents;
293
+ }
294
+ /** The flags in the order and words the register lists them. */ export function posOfflineFlagLabels(flags) {
295
+ return flags.map((flag)=>{
296
+ var _POS_OFFLINE_FLAG_LABELS_flag;
297
+ return (_POS_OFFLINE_FLAG_LABELS_flag = POS_OFFLINE_FLAG_LABELS[flag]) != null ? _POS_OFFLINE_FLAG_LABELS_flag : flag;
298
+ });
299
+ }
300
+
301
+ //# sourceMappingURL=commerce-pos-offline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/commerce-pos-offline.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { computeOrderTotals, type OrderLineItem, type OrderTotals } from './commerce-orders'\nimport { computeTaxCents } from './commerce-tax'\nimport type { ModifierSelection } from './product-modifiers'\n\n/*==========================================\n * THE OFFLINE REGISTER (AGL-3625): the contract every register client — the\n * web register and the native Aglyn POS app — shares with\n * `POST /api/commerce/pos-offline-sync`.\n *\n * WHAT SELLS OFFLINE IS CASH, AND ONLY CASH.\n *\n * - Card tenders cannot: the reader, Tap to Pay, the typed card and the QR\n * link are all server-driven Stripe payments, and Stripe does not take a\n * server-driven card payment without the server.\n * - Gift cards DO NOT, deliberately. A balance cannot be checked without the\n * server, and a capped \"allow it anyway\" (Square's offer for cards) turns a\n * short balance into a sale whose money nobody collected — discovered at\n * sync, after the customer left. Square itself does not redeem gift cards\n * offline. A gift card is never over-redeemed silently because it is never\n * redeemed offline at all.\n * - Room charges do not: the stay is validated on the server.\n * - Store credit and rewards do not, for the gift card's reason: a balance\n * is the server's to check.\n * - Codes and store promotions do not: a redemption cap is counted on the\n * server. The cashier's own discount, bounded by the site's ceiling, does.\n *\n * WHAT THE SALE IS: the register prices the basket from its cached catalog and\n * the store's own tax rate, takes the cash and queues the sale with a key it\n * minted. That key is the order's id on the server, so the receipt printed\n * offline carries the barcode the real order answers to, and a sale sent twice\n * — a flapping connection, a retry after a lost answer — lands once.\n *\n * WHAT THE SERVER DOES WITH IT: records it as it happened, never re-prices the\n * money the customer handed over, and FLAGS what does not match — a price\n * that changed, stock that went short, a shift that closed first — on the\n * order and back to the register. A sale is never dropped for a flag.\n *=========================================*/\n\n/** The route both register clients sync through. */\nexport const POS_OFFLINE_SYNC_ROUTE = '/api/commerce/pos-offline-sync'\n\n/** The most sales one sync request carries; a longer queue sends in batches. */\nexport const POS_OFFLINE_SYNC_BATCH_MAX = 25\n\n/** Lines one offline sale may carry, the same bound the register's basket has. */\nexport const POS_OFFLINE_SALE_MAX_LINES = 100\n\n/** A sale synced later than this after it was rung is flagged `late-sync`. */\nexport const POS_OFFLINE_LATE_SYNC_MS = 24 * 60 * 60 * 1000\n\n/**\n * How far back a sale's own clock is believed. Older, and the server stamps\n * the sale at this bound and flags it, rather than letting a device clock\n * place a sale anywhere in history.\n */\nexport const POS_OFFLINE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000\n\n/** How far ahead of the server a device clock may run before it is clamped. */\nexport const POS_OFFLINE_CLOCK_SKEW_MS = 10 * 60 * 1000\n\n/** The one tender an offline sale takes. */\nexport type PosOfflineTender = 'cash'\n\n/** Every tender the register turns off while offline, and why, in its words. */\nexport const POS_OFFLINE_DISABLED_TENDERS: Readonly<Record<string, string>> = {\n card_present: 'Card readers need the connection.',\n card_keyed: 'Typed cards need the connection.',\n card_link: 'The QR card link needs the connection.',\n gift_card: 'A gift card balance can only be checked online.',\n folio: 'Room charges are checked against the stay online.',\n credit: 'Store credit and rewards balances can only be checked online.',\n}\n\n/** One basket line as the register rang it. */\nexport interface PosOfflineSaleLine {\n productId: string\n variantId?: string\n name: string\n /** Variant options and modifier choices, as the receipt printed them. */\n variantLabel?: string\n sku?: string\n productType?: 'physical' | 'digital' | 'service'\n quantity: number\n /** Per unit, modifiers included, from the cached catalog. */\n unitAmountCents: number\n modifiers?: ModifierSelection[]\n}\n\n/** The customer the register attached: only what the sale and its receipt need. */\nexport interface PosOfflineSaleCustomer {\n email?: string\n name?: string\n kind?: string\n id?: string\n}\n\n/** One sale rung while offline, exactly as the register queued it. */\nexport interface PosOfflineSale {\n v: 1\n /** The register's key for this sale, minted once; the order's id on the server. */\n saleKey: string\n hostId: string\n /** The workspace the register was signed in to. */\n orgId: string\n /** Who was signed in on the device: the only member who may sync it. */\n signedInUid: string\n registerId: string\n /** The shift open on the register when the sale was rung. */\n shiftId?: string | null\n locationId?: string\n /** A PIN-switched cashier's assertion, valid when the sale was rung. */\n cashierAssertion?: string\n /** The device's clock when the cash was taken. */\n soldAtMs: number\n lines: PosOfflineSaleLine[]\n /** The cashier's discount, a percentage of the goods. */\n discountPct: number\n /** The totals the receipt printed and the customer paid. */\n totals: OrderTotals\n cashTenderedCents: number\n changeCents: number\n customer?: PosOfflineSaleCustomer\n}\n\n/** What does not match, said on the order and back to the register. */\nexport type PosOfflineFlag =\n | 'price-changed'\n | 'unknown-product'\n | 'tax-differs'\n | 'totals-restated'\n | 'discount-over-limit'\n | 'cash-short'\n | 'stock-short'\n | 'shift-closed'\n | 'no-shift'\n | 'unknown-register'\n | 'register-over-cap'\n | 'late-sync'\n | 'clock-adjusted'\n | 'cashier-unverified'\n\n/** Each flag in the words the order and the register show. */\nexport const POS_OFFLINE_FLAG_LABELS: Record<PosOfflineFlag, string> = {\n 'price-changed': 'A price changed after the register cached it; the sale kept the price it was rung at',\n 'unknown-product': 'A product was deleted before the sale synced; the line kept what the receipt printed',\n 'tax-differs': 'The tax rung differs from the store’s rate now; the sale kept the tax it collected',\n 'totals-restated': 'The register’s totals did not add up; the server restated them from the lines',\n 'discount-over-limit': 'The discount is above this register’s limit',\n 'cash-short': 'The cash recorded is less than the total',\n 'stock-short': 'Stock went short: more was sold than the shelf held',\n 'shift-closed': 'Rung in a shift that closed before the sale synced; its Z report does not include it',\n 'no-shift': 'Rung with no shift open, on a site that requires one',\n 'unknown-register': 'The register it was rung on no longer exists',\n 'register-over-cap': 'The register it was rung on is over the plan’s register limit',\n 'late-sync': 'Synced more than a day after it was rung',\n 'clock-adjusted': 'The device clock was off; the sale is dated when it synced',\n 'cashier-unverified': 'The PIN-switched cashier could not be confirmed; the sale is under the signed-in member',\n}\n\n/** A line whose stock went short when the sale synced. */\nexport interface PosOfflineStockConflict {\n productId: string\n variantId?: string\n name: string\n /** Units sold. */\n requested: number\n /** Units the shelf actually gave up. */\n applied: number\n /** Units sold that the shelf did not hold. */\n shortUnits: number\n}\n\n/** What the order records about having been rung offline. */\nexport interface PosOfflineOrderStamp {\n saleKey: string\n /** The device's clock when the cash was taken. */\n soldAtMs: number\n syncedAtMs: number\n /** Who synced it: always the member who was signed in when it was rung. */\n syncedBy: string\n flags: PosOfflineFlag[]\n stockConflicts?: PosOfflineStockConflict[]\n /** The server's own price per line where it differed, for the merchant to compare. */\n priceDrift?: Array<{ index: number; rungCents: number; currentCents: number }>\n}\n\n/** Why a sale was refused, and whether the register should keep trying. */\nexport type PosOfflineRefusal =\n | 'wrong-staff'\n | 'wrong-site'\n | 'wrong-workspace'\n | 'invalid'\n | 'conflict'\n | 'failed'\n\n/** What became of one sale the register sent. */\nexport type PosOfflineOutcomeStatus = 'recorded' | 'replayed' | 'refused'\n\n/**\n * One sale's answer, in the order the request sent them. `recorded` and\n * `replayed` carry the order; `refused` carries the reason, the sentence and\n * whether to keep trying. One flat shape, so every register client — the web\n * register and the native apps — decodes it the same way.\n */\nexport interface PosOfflineSaleOutcome {\n saleKey: string\n status: PosOfflineOutcomeStatus\n orderId?: string\n number?: number | null\n flags?: PosOfflineFlag[]\n stockConflicts?: PosOfflineStockConflict[]\n reason?: PosOfflineRefusal\n error?: string\n /** Keep it queued and try again later (a transient failure). */\n retry?: boolean\n}\n\nexport interface PosOfflineSyncRequest {\n hostId: string\n sales: PosOfflineSale[]\n}\n\nexport interface PosOfflineSyncResponse {\n results: PosOfflineSaleOutcome[]\n}\n\n/** One register as the offline kit lists it. */\nexport interface PosOfflineKitRegister {\n id: string\n name: string\n locationId?: string\n openShiftId: string | null\n}\n\n/**\n * Everything a register needs to sell offline besides the catalog, from\n * `GET /api/commerce/pos-offline-sync?hostId=`: the server's own answers, so\n * the register and the sync agree about the rate and the rules.\n */\nexport interface PosOfflineKit {\n v: 1\n hostId: string\n orgId: string\n issuedAtMs: number\n /** Offline selling is possible for this site right now. */\n available: boolean\n /** Why not, in the cashier's words, when it is not. */\n unavailableReason?: string\n /** The store's own in-person rate; `null` pct is no tax. */\n tax: { pct: number | null; pricesIncludeTax: boolean }\n maxDiscountPct: number\n requireOpenShift: boolean\n registers: PosOfflineKitRegister[]\n /** What the receipt prints at its head and foot. `logo` is the stored value. */\n receipt: {\n name: string\n logo?: string\n address?: string\n footer?: string\n returnPolicy?: string\n }\n}\n\n/**\n * The totals of an offline sale: the cashier's discount on the goods, then\n * the store's in-person tax on what is left — exactly the arithmetic the\n * online register's sale route uses for the same basket with no promotion.\n * `pricesIncludeTax` adds nothing, as at the online till.\n */\nexport function posOfflineSaleTotals(input: {\n lines: ReadonlyArray<Pick<PosOfflineSaleLine, 'unitAmountCents' | 'quantity'>>\n discountPct: number\n tax: { pct: number | null; pricesIncludeTax: boolean }\n}): OrderTotals {\n const lines = input.lines.map((line) => ({\n unitAmountCents: wholeCents(line.unitAmountCents),\n quantity: Math.max(1, Math.round(Number(line.quantity) || 1)),\n }))\n const itemsCents = lines.reduce((sum, line) => sum + line.unitAmountCents * line.quantity, 0)\n const pct = Number.isFinite(input.discountPct) ? Math.min(100, Math.max(0, input.discountPct)) : 0\n const discountCents = Math.min(itemsCents, Math.round((itemsCents * pct) / 100))\n const taxPct = Number(input.tax.pct)\n const taxCents =\n taxPct > 0 && !input.tax.pricesIncludeTax ? computeTaxCents(itemsCents - discountCents, taxPct) : 0\n return computeOrderTotals(lines as unknown as OrderLineItem[], { discountCents, taxCents, feeCents: 0 })\n}\n\n/** Change for cash handed over, never negative. */\nexport function posOfflineChangeCents(totalCents: number, tenderedCents: number): number {\n return Math.max(0, wholeCents(tenderedCents) - wholeCents(totalCents))\n}\n\n/** A key the register minted: an id the order can take, and nothing else. */\nexport function posOfflineSaleKeyIsValid(key: unknown): key is string {\n return typeof key === 'string' && /^[A-Za-z0-9_-]{16,64}$/.test(key) && !/^__.*__$/.test(key)\n}\n\n/**\n * The sale's time as the server records it: the device's clock when it is\n * believable, clamped to now when it runs ahead, and to the oldest believable\n * moment when it claims to be older.\n */\nexport function posOfflineSoldAt(\n soldAtMs: number,\n nowMs: number,\n): { atMs: number; adjusted: boolean } {\n const at = Number(soldAtMs)\n if (!Number.isFinite(at) || at <= 0) return { atMs: nowMs, adjusted: true }\n if (at > nowMs + POS_OFFLINE_CLOCK_SKEW_MS) return { atMs: nowMs, adjusted: true }\n if (at < nowMs - POS_OFFLINE_MAX_AGE_MS) return { atMs: nowMs, adjusted: true }\n return { atMs: Math.min(at, nowMs), adjusted: false }\n}\n\nconst bounded = (value: unknown, max: number): string =>\n typeof value === 'string' ? value.trim().slice(0, max) : ''\n\nconst cleanId = (value: unknown): string => {\n const id = String(value ?? '').trim()\n return /^[A-Za-z0-9_-]{1,128}$/.test(id) && !/^__.*__$/.test(id) ? id : ''\n}\n\nfunction wholeCents(value: unknown): number {\n const number = Math.round(Number(value))\n return Number.isFinite(number) && number > 0 ? number : 0\n}\n\n/**\n * A queued sale read off the wire, every field typed and bounded — or the\n * sentence that says why it cannot be read at all. Pure, so the server and\n * any client that wants to check its own queue answer the same.\n */\nexport function sanitizePosOfflineSale(\n raw: unknown,\n): { ok: true; sale: PosOfflineSale } | { ok: false; error: string } {\n if (!raw || typeof raw !== 'object') return { ok: false, error: 'Not a sale' }\n const input = raw as Record<string, any>\n const saleKey = input['saleKey']\n if (!posOfflineSaleKeyIsValid(saleKey)) return { ok: false, error: 'The sale has no valid key' }\n const hostId = cleanId(input['hostId'])\n const registerId = cleanId(input['registerId'])\n const signedInUid = bounded(input['signedInUid'], 128)\n if (!hostId || !registerId || !signedInUid) {\n return { ok: false, error: 'The sale is missing its site, register or member' }\n }\n const rawLines = Array.isArray(input['lines']) ? input['lines'] : []\n if (rawLines.length === 0 || rawLines.length > POS_OFFLINE_SALE_MAX_LINES) {\n return { ok: false, error: 'The sale has no lines' }\n }\n const lines: PosOfflineSaleLine[] = []\n for (const line of rawLines) {\n const productId = cleanId(line?.productId)\n const name = bounded(line?.name, 200)\n const unit = Math.round(Number(line?.unitAmountCents))\n const quantity = Math.round(Number(line?.quantity))\n if (!productId || !name || !Number.isFinite(unit) || unit < 0 || !(quantity >= 1 && quantity <= 99)) {\n return { ok: false, error: 'A line of the sale cannot be read' }\n }\n const variantId = cleanId(line?.variantId)\n const variantLabel = bounded(line?.variantLabel, 200)\n const sku = bounded(line?.sku, 100)\n const productType = ['physical', 'digital', 'service'].includes(line?.productType)\n ? (line.productType as PosOfflineSaleLine['productType'])\n : undefined\n const modifiers = Array.isArray(line?.modifiers)\n ? (line.modifiers as unknown[])\n .slice(0, 50)\n .map((pick: any) => ({ groupId: cleanId(pick?.groupId), optionId: cleanId(pick?.optionId) }))\n .filter((pick) => pick.groupId && pick.optionId)\n : []\n lines.push({\n productId,\n ...(variantId ? { variantId } : {}),\n name,\n ...(variantLabel ? { variantLabel } : {}),\n ...(sku ? { sku } : {}),\n ...(productType ? { productType } : {}),\n quantity,\n unitAmountCents: unit,\n ...(modifiers.length ? { modifiers } : {}),\n })\n }\n const discountPct = Number(input['discountPct'] ?? 0)\n if (!Number.isFinite(discountPct) || discountPct < 0 || discountPct > 100) {\n return { ok: false, error: 'The sale’s discount cannot be read' }\n }\n const totals = (input['totals'] ?? {}) as Record<string, unknown>\n const customerRaw = input['customer']\n const customer: PosOfflineSaleCustomer = {}\n if (customerRaw && typeof customerRaw === 'object') {\n const email = bounded((customerRaw as any).email, 254).toLowerCase()\n if (email && /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email)) customer.email = email\n const name = bounded((customerRaw as any).name, 200)\n if (name) customer.name = name\n const kind = bounded((customerRaw as any).kind, 40)\n const id = cleanId((customerRaw as any).id)\n if (kind && kind !== 'none' && id) {\n customer.kind = kind\n customer.id = id\n }\n }\n const shiftId = cleanId(input['shiftId'])\n const locationId = cleanId(input['locationId'])\n const assertion = bounded(input['cashierAssertion'], 2000)\n const orgId = bounded(input['orgId'], 128)\n return {\n ok: true,\n sale: {\n v: 1,\n saleKey,\n hostId,\n orgId,\n signedInUid,\n registerId,\n shiftId: shiftId || null,\n ...(locationId ? { locationId } : {}),\n ...(assertion ? { cashierAssertion: assertion } : {}),\n soldAtMs: Number(input['soldAtMs']) || 0,\n lines,\n discountPct,\n totals: {\n itemsCents: wholeCents(totals['itemsCents']),\n shippingCents: 0,\n taxCents: wholeCents(totals['taxCents']),\n discountCents: wholeCents(totals['discountCents']),\n totalCents: wholeCents(totals['totalCents']),\n feeCents: 0,\n },\n cashTenderedCents: wholeCents(input['cashTenderedCents']),\n changeCents: wholeCents(input['changeCents']),\n ...(Object.keys(customer).length ? { customer } : {}),\n },\n }\n}\n\n/**\n * Whether the register's totals are the arithmetic of its own lines: the\n * goods are the lines' sum, the discount is the percentage of them, and the\n * total is goods less discount plus the tax it rang. The tax itself is the\n * register's, and checked against the store's rate separately.\n */\nexport function posOfflineTotalsAddUp(sale: Pick<PosOfflineSale, 'lines' | 'discountPct' | 'totals'>): boolean {\n const expected = posOfflineSaleTotals({\n lines: sale.lines,\n discountPct: sale.discountPct,\n tax: { pct: null, pricesIncludeTax: false },\n })\n return (\n expected.itemsCents === sale.totals.itemsCents &&\n expected.discountCents === sale.totals.discountCents &&\n sale.totals.taxCents <= sale.totals.itemsCents &&\n sale.totals.totalCents === expected.itemsCents - expected.discountCents + sale.totals.taxCents\n )\n}\n\n/** The flags in the order and words the register lists them. */\nexport function posOfflineFlagLabels(flags: readonly PosOfflineFlag[]): string[] {\n return flags.map((flag) => POS_OFFLINE_FLAG_LABELS[flag] ?? flag)\n}\n"],"names":["computeOrderTotals","computeTaxCents","POS_OFFLINE_SYNC_ROUTE","POS_OFFLINE_SYNC_BATCH_MAX","POS_OFFLINE_SALE_MAX_LINES","POS_OFFLINE_LATE_SYNC_MS","POS_OFFLINE_MAX_AGE_MS","POS_OFFLINE_CLOCK_SKEW_MS","POS_OFFLINE_DISABLED_TENDERS","card_present","card_keyed","card_link","gift_card","folio","credit","POS_OFFLINE_FLAG_LABELS","posOfflineSaleTotals","input","lines","map","line","unitAmountCents","wholeCents","quantity","Math","max","round","Number","itemsCents","reduce","sum","pct","isFinite","discountPct","min","discountCents","taxPct","tax","taxCents","pricesIncludeTax","feeCents","posOfflineChangeCents","totalCents","tenderedCents","posOfflineSaleKeyIsValid","key","test","posOfflineSoldAt","soldAtMs","nowMs","at","atMs","adjusted","bounded","value","trim","slice","cleanId","id","String","number","sanitizePosOfflineSale","raw","ok","error","saleKey","hostId","registerId","signedInUid","rawLines","Array","isArray","length","productId","name","unit","variantId","variantLabel","sku","productType","includes","undefined","modifiers","pick","groupId","optionId","filter","push","totals","customerRaw","customer","email","toLowerCase","kind","shiftId","locationId","assertion","orgId","sale","v","cashierAssertion","shippingCents","cashTenderedCents","changeCents","Object","keys","posOfflineTotalsAddUp","expected","posOfflineFlagLabels","flags","flag"],"mappings":"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"}
@@ -16,7 +16,9 @@
16
16
  */
17
17
  import type { PrintReport } from './commerce-printers';
18
18
  /** How one payment toward a sale was taken, in the register's tender words. */
19
- export type PosTenderMethod = 'cash' | 'card_present' | 'card_keyed' | 'card_link' | 'gift_card' | 'folio';
19
+ export type PosTenderMethod = 'cash' | 'card_present' | 'card_keyed' | 'card_link' | 'gift_card' | 'folio'
20
+ /** Store credit another plugin keeps (AGL-3640). */
21
+ | 'credit';
20
22
  export declare const POS_TENDER_METHODS: readonly PosTenderMethod[];
21
23
  /** What each tender is called on a report, a receipt and a refund. */
22
24
  export declare const POS_TENDER_LABELS: Readonly<Record<PosTenderMethod, string>>;
@@ -36,6 +38,10 @@ export interface PosOrderTender {
36
38
  paymentIntentId?: string;
37
39
  giftCardId?: string;
38
40
  reservationId?: string;
41
+ /** For `credit` (AGL-3640): the provider, its account handle, and its name. */
42
+ creditProviderId?: string;
43
+ creditReference?: string;
44
+ creditLabel?: string;
39
45
  cardBrand?: string;
40
46
  last4?: string;
41
47
  cashTenderedCents?: number;
@@ -70,7 +76,9 @@ export interface PosTenderSource {
70
76
  */
71
77
  export declare function posOrderTenders(order: PosTenderSource | null | undefined): PosOrderTender[];
72
78
  /** A tender as one line of a receipt: `Card •• 4242`. */
73
- export declare function posTenderLabel(tender: Pick<PosOrderTender, 'method' | 'cardBrand' | 'last4'>): string;
79
+ export declare function posTenderLabel(tender: Pick<PosOrderTender, 'method' | 'cardBrand' | 'last4'> & {
80
+ creditLabel?: string;
81
+ }): string;
74
82
  /**
75
83
  * Cash that moved through the drawer without a sale: money put in, money
76
84
  * paid out for a reason (a delivery, petty cash), a drop to the safe, and
@@ -128,12 +136,14 @@ export interface PosShift {
128
136
  cashEvents: PosCashEvent[];
129
137
  closedBy?: string;
130
138
  closedByName?: string;
131
- closedAtMs?: number;
139
+ closedAtMs?: number | null;
132
140
  /** What the closing cashier counted in the drawer. */
133
- countedCashCents?: number;
134
- expectedCashCents?: number;
141
+ countedCashCents?: number | null;
142
+ expectedCashCents?: number | null;
135
143
  /** counted − expected: positive is over, negative is short. */
136
- varianceCents?: number;
144
+ varianceCents?: number | null;
145
+ /** The Z report's `netSalesCents`, flattened so the history can order by it. */
146
+ netSalesCents?: number | null;
137
147
  closingNote?: string;
138
148
  /** The Z report, frozen when the shift closed. */
139
149
  report?: PosShiftReport;
@@ -23,7 +23,8 @@ export const POS_TENDER_METHODS = [
23
23
  'card_keyed',
24
24
  'card_link',
25
25
  'gift_card',
26
- 'folio'
26
+ 'folio',
27
+ 'credit'
27
28
  ];
28
29
  /** What each tender is called on a report, a receipt and a refund. */ export const POS_TENDER_LABELS = {
29
30
  cash: 'Cash',
@@ -31,7 +32,8 @@ export const POS_TENDER_METHODS = [
31
32
  card_keyed: 'Card (keyed)',
32
33
  card_link: 'Card (payment link)',
33
34
  gift_card: 'Gift card',
34
- folio: 'Room charge'
35
+ folio: 'Room charge',
36
+ credit: 'Store credit'
35
37
  };
36
38
  /** The tenders whose money moved through Stripe and goes back through it. */ export const POS_STRIPE_TENDERS = new Set([
37
39
  'card_present',
@@ -71,6 +73,12 @@ const isTenderMethod = (value)=>typeof value === 'string' && POS_TENDER_METHODS.
71
73
  giftCardId: payment.giftCardId
72
74
  } : {}, payment.reservationId ? {
73
75
  reservationId: payment.reservationId
76
+ } : {}, payment.creditProviderId ? {
77
+ creditProviderId: payment.creditProviderId
78
+ } : {}, payment.creditReference ? {
79
+ creditReference: payment.creditReference
80
+ } : {}, payment.creditLabel ? {
81
+ creditLabel: payment.creditLabel
74
82
  } : {}, payment.cardBrand ? {
75
83
  cardBrand: payment.cardBrand
76
84
  } : {}, payment.last4 ? {
@@ -83,8 +91,7 @@ const isTenderMethod = (value)=>typeof value === 'string' && POS_TENDER_METHODS.
83
91
  });
84
92
  }
85
93
  /** A tender as one line of a receipt: `Card •• 4242`. */ export function posTenderLabel(tender) {
86
- var _POS_TENDER_LABELS_tender_method;
87
- const base = (_POS_TENDER_LABELS_tender_method = POS_TENDER_LABELS[tender.method]) != null ? _POS_TENDER_LABELS_tender_method : tender.method;
94
+ const base = tender.method === 'credit' && tender.creditLabel || POS_TENDER_LABELS[tender.method] || tender.method;
88
95
  if (tender.last4) {
89
96
  return `${tender.cardBrand ? capitalize(tender.cardBrand) : base} •• ${tender.last4}`;
90
97
  }
@@ -424,6 +431,7 @@ const dollars = (cents)=>cents == null ? '' : (Math.round(cents) / 100).toFixed(
424
431
  'card_keyed',
425
432
  'card_link',
426
433
  'gift_card',
434
+ 'credit',
427
435
  'folio',
428
436
  'cash'
429
437
  ];