@aglyn/plugins-commerce 1.0.0-beta.278 → 1.0.0-beta.283

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 (212) hide show
  1. package/package.json +17 -14
  2. package/src/lib/components/commerce-console-page.js +33 -1
  3. package/src/lib/components/commerce-console-page.js.map +1 -1
  4. package/src/lib/components/console/payments-settings-card.component.js +1 -1
  5. package/src/lib/components/console/payments-settings-card.component.js.map +1 -1
  6. package/src/lib/components/console/pos/pos-hardware-api.d.ts +122 -0
  7. package/src/lib/components/console/pos/pos-hardware-api.js +90 -0
  8. package/src/lib/components/console/pos/pos-hardware-api.js.map +1 -0
  9. package/src/lib/components/console/pos/pos-reader-splash-api.d.ts +58 -0
  10. package/src/lib/components/console/pos/pos-reader-splash-api.js +101 -0
  11. package/src/lib/components/console/pos/pos-reader-splash-api.js.map +1 -0
  12. package/src/lib/components/console/pos-devices-card.component.js +5 -0
  13. package/src/lib/components/console/pos-devices-card.component.js.map +1 -1
  14. package/src/lib/components/console/pos-hardware-card.component.d.ts +26 -0
  15. package/src/lib/components/console/pos-hardware-card.component.js +456 -0
  16. package/src/lib/components/console/pos-hardware-card.component.js.map +1 -0
  17. package/src/lib/components/console/pos-hardware-orders-staff-page.component.d.ts +14 -0
  18. package/src/lib/components/console/pos-hardware-orders-staff-page.component.js +519 -0
  19. package/src/lib/components/console/pos-hardware-orders-staff-page.component.js.map +1 -0
  20. package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.d.ts +27 -0
  21. package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.js +444 -0
  22. package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.js.map +1 -0
  23. package/src/lib/components/console/pos-ops/pos-check-in-zone.component.d.ts +9 -0
  24. package/src/lib/components/console/pos-ops/pos-check-in-zone.component.js +37 -0
  25. package/src/lib/components/console/pos-ops/pos-check-in-zone.component.js.map +1 -0
  26. package/src/lib/components/console/pos-ops/pos-check-ins-card.component.d.ts +24 -0
  27. package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js +346 -0
  28. package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js.map +1 -0
  29. package/src/lib/components/console/pos-ops/pos-customer-lookup.component.d.ts +5 -1
  30. package/src/lib/components/console/pos-ops/pos-customer-lookup.component.js +83 -4
  31. package/src/lib/components/console/pos-ops/pos-customer-lookup.component.js.map +1 -1
  32. package/src/lib/components/console/pos-ops/pos-day-totals-card.component.d.ts +35 -0
  33. package/src/lib/components/console/pos-ops/pos-day-totals-card.component.js +546 -0
  34. package/src/lib/components/console/pos-ops/pos-day-totals-card.component.js.map +1 -0
  35. package/src/lib/components/console/pos-ops/pos-operations-bar.component.d.ts +2 -1
  36. package/src/lib/components/console/pos-ops/pos-operations-bar.component.js +20 -1
  37. package/src/lib/components/console/pos-ops/pos-operations-bar.component.js.map +1 -1
  38. package/src/lib/components/console/pos-ops/pos-ops-api.d.ts +1 -1
  39. package/src/lib/components/console/pos-ops/pos-ops-api.js.map +1 -1
  40. package/src/lib/components/console/pos-page.component.js +6 -3
  41. package/src/lib/components/console/pos-page.component.js.map +1 -1
  42. package/src/lib/components/console/pos-reader-splash-setting.component.d.ts +22 -0
  43. package/src/lib/components/console/pos-reader-splash-setting.component.js +275 -0
  44. package/src/lib/components/console/pos-reader-splash-setting.component.js.map +1 -0
  45. package/src/lib/components/console/pos-reader-splash-staff-page.component.d.ts +14 -0
  46. package/src/lib/components/console/pos-reader-splash-staff-page.component.js +301 -0
  47. package/src/lib/components/console/pos-reader-splash-staff-page.component.js.map +1 -0
  48. package/src/lib/components/console/pos-zones.d.ts +37 -3
  49. package/src/lib/components/console/pos-zones.js +11 -1
  50. package/src/lib/components/console/pos-zones.js.map +1 -1
  51. package/src/lib/components/console/product-check-in-fields.component.d.ts +20 -0
  52. package/src/lib/components/console/product-check-in-fields.component.js +172 -0
  53. package/src/lib/components/console/product-check-in-fields.component.js.map +1 -0
  54. package/src/lib/components/console/product-editor-dialog.component.js +48 -9
  55. package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
  56. package/src/lib/components/console/product-register-fields.component.d.ts +12 -0
  57. package/src/lib/components/console/product-register-fields.component.js +39 -1
  58. package/src/lib/components/console/product-register-fields.component.js.map +1 -1
  59. package/src/lib/components/console/products-hub-card.component.js +21 -6
  60. package/src/lib/components/console/products-hub-card.component.js.map +1 -1
  61. package/src/lib/components/console/reservations-card.component.js +239 -70
  62. package/src/lib/components/console/reservations-card.component.js.map +1 -1
  63. package/src/lib/components/reservation-widget.js +16 -0
  64. package/src/lib/components/reservation-widget.js.map +1 -1
  65. package/src/lib/constants/operator-alerts.d.ts +6 -0
  66. package/src/lib/constants/operator-alerts.js +20 -1
  67. package/src/lib/constants/operator-alerts.js.map +1 -1
  68. package/src/lib/declarations.console-server.js +7 -0
  69. package/src/lib/declarations.console-server.js.map +1 -1
  70. package/src/lib/declarations.d.ts +26 -0
  71. package/src/lib/declarations.js +34 -0
  72. package/src/lib/declarations.js.map +1 -0
  73. package/src/lib/model/commerce-check-in-product.d.ts +37 -0
  74. package/src/lib/model/commerce-check-in-product.js +49 -0
  75. package/src/lib/model/commerce-check-in-product.js.map +1 -0
  76. package/src/lib/model/commerce-check-in.d.ts +187 -0
  77. package/src/lib/model/commerce-check-in.js +468 -0
  78. package/src/lib/model/commerce-check-in.js.map +1 -0
  79. package/src/lib/model/commerce-pos-day-report.d.ts +209 -0
  80. package/src/lib/model/commerce-pos-day-report.js +572 -0
  81. package/src/lib/model/commerce-pos-day-report.js.map +1 -0
  82. package/src/lib/model/commerce-pos-offline.d.ts +65 -3
  83. package/src/lib/model/commerce-pos-offline.js +104 -7
  84. package/src/lib/model/commerce-pos-offline.js.map +1 -1
  85. package/src/lib/model/commerce-pos-ops.d.ts +3 -0
  86. package/src/lib/model/commerce-pos-ops.js +1 -0
  87. package/src/lib/model/commerce-pos-ops.js.map +1 -1
  88. package/src/lib/model/commerce-pos.d.ts +43 -3
  89. package/src/lib/model/commerce-pos.js +73 -5
  90. package/src/lib/model/commerce-pos.js.map +1 -1
  91. package/src/lib/model/commerce-reservations.d.ts +73 -7
  92. package/src/lib/model/commerce-reservations.js +90 -10
  93. package/src/lib/model/commerce-reservations.js.map +1 -1
  94. package/src/lib/model/commerce.d.ts +40 -0
  95. package/src/lib/model/commerce.js +22 -4
  96. package/src/lib/model/commerce.js.map +1 -1
  97. package/src/lib/model/index.d.ts +1 -0
  98. package/src/lib/model/index.js +1 -0
  99. package/src/lib/model/index.js.map +1 -1
  100. package/src/lib/model/plugin-permissions.d.ts +1 -1
  101. package/src/lib/model/plugin-permissions.js +2 -1
  102. package/src/lib/model/plugin-permissions.js.map +1 -1
  103. package/src/lib/model/pos-hardware.d.ts +288 -0
  104. package/src/lib/model/pos-hardware.js +499 -0
  105. package/src/lib/model/pos-hardware.js.map +1 -0
  106. package/src/lib/model/pos-offline-card-limits.d.ts +20 -0
  107. package/src/lib/model/pos-offline-card-limits.js +28 -0
  108. package/src/lib/model/pos-offline-card-limits.js.map +1 -0
  109. package/src/lib/model/pos-reader-splash.d.ts +114 -0
  110. package/src/lib/model/pos-reader-splash.js +141 -0
  111. package/src/lib/model/pos-reader-splash.js.map +1 -0
  112. package/src/lib/model/product-write.d.ts +6 -0
  113. package/src/lib/plugin-config.d.ts +10 -0
  114. package/src/lib/plugin-config.js +40 -0
  115. package/src/lib/plugin-config.js.map +1 -1
  116. package/src/lib/plugin.js +28 -0
  117. package/src/lib/plugin.js.map +1 -1
  118. package/src/lib/server/cart-checkout.js +3 -1
  119. package/src/lib/server/cart-checkout.js.map +1 -1
  120. package/src/lib/server/cart.js +12 -1
  121. package/src/lib/server/cart.js.map +1 -1
  122. package/src/lib/server/catalog.js +49 -12
  123. package/src/lib/server/catalog.js.map +1 -1
  124. package/src/lib/server/checkout-extras.js +1 -1
  125. package/src/lib/server/checkout-extras.js.map +1 -1
  126. package/src/lib/server/checkout.js +7 -0
  127. package/src/lib/server/checkout.js.map +1 -1
  128. package/src/lib/server/person-eraser.d.ts +8 -1
  129. package/src/lib/server/person-eraser.js +23 -1
  130. package/src/lib/server/person-eraser.js.map +1 -1
  131. package/src/lib/server/pos-check-in.d.ts +49 -0
  132. package/src/lib/server/pos-check-in.js +652 -0
  133. package/src/lib/server/pos-check-in.js.map +1 -0
  134. package/src/lib/server/pos-day-report.d.ts +46 -0
  135. package/src/lib/server/pos-day-report.js +299 -0
  136. package/src/lib/server/pos-day-report.js.map +1 -0
  137. package/src/lib/server/pos-hardware-admin.d.ts +50 -0
  138. package/src/lib/server/pos-hardware-admin.js +366 -0
  139. package/src/lib/server/pos-hardware-admin.js.map +1 -0
  140. package/src/lib/server/pos-hardware-notify.d.ts +27 -0
  141. package/src/lib/server/pos-hardware-notify.js +142 -0
  142. package/src/lib/server/pos-hardware-notify.js.map +1 -0
  143. package/src/lib/server/pos-hardware-user-eraser.d.ts +26 -0
  144. package/src/lib/server/pos-hardware-user-eraser.js +67 -0
  145. package/src/lib/server/pos-hardware-user-eraser.js.map +1 -0
  146. package/src/lib/server/pos-hardware-webhook.d.ts +31 -0
  147. package/src/lib/server/pos-hardware-webhook.js +176 -0
  148. package/src/lib/server/pos-hardware-webhook.js.map +1 -0
  149. package/src/lib/server/pos-hardware.d.ts +58 -0
  150. package/src/lib/server/pos-hardware.js +234 -0
  151. package/src/lib/server/pos-hardware.js.map +1 -0
  152. package/src/lib/server/pos-offline-card-reconcile.d.ts +38 -0
  153. package/src/lib/server/pos-offline-card-reconcile.js +58 -0
  154. package/src/lib/server/pos-offline-card-reconcile.js.map +1 -0
  155. package/src/lib/server/pos-offline-sync.d.ts +32 -0
  156. package/src/lib/server/pos-offline-sync.js +191 -15
  157. package/src/lib/server/pos-offline-sync.js.map +1 -1
  158. package/src/lib/server/pos-ops-routes.d.ts +4 -2
  159. package/src/lib/server/pos-ops-routes.js +14 -2
  160. package/src/lib/server/pos-ops-routes.js.map +1 -1
  161. package/src/lib/server/pos-order.js +39 -51
  162. package/src/lib/server/pos-order.js.map +1 -1
  163. package/src/lib/server/pos-payment.js +8 -4
  164. package/src/lib/server/pos-payment.js.map +1 -1
  165. package/src/lib/server/pos-reader-splash-admin.d.ts +36 -0
  166. package/src/lib/server/pos-reader-splash-admin.js +295 -0
  167. package/src/lib/server/pos-reader-splash-admin.js.map +1 -0
  168. package/src/lib/server/pos-reader-splash-render.d.ts +40 -0
  169. package/src/lib/server/pos-reader-splash-render.js +138 -0
  170. package/src/lib/server/pos-reader-splash-render.js.map +1 -0
  171. package/src/lib/server/pos-reader-splash-server.d.ts +32 -0
  172. package/src/lib/server/pos-reader-splash-server.js +32 -0
  173. package/src/lib/server/pos-reader-splash-server.js.map +1 -0
  174. package/src/lib/server/pos-reader-splash.d.ts +105 -0
  175. package/src/lib/server/pos-reader-splash.js +548 -0
  176. package/src/lib/server/pos-reader-splash.js.map +1 -0
  177. package/src/lib/server/pos-sale.d.ts +2 -1
  178. package/src/lib/server/pos-sale.js +3 -1
  179. package/src/lib/server/pos-sale.js.map +1 -1
  180. package/src/lib/server/pos-terminal.d.ts +67 -4
  181. package/src/lib/server/pos-terminal.js +239 -32
  182. package/src/lib/server/pos-terminal.js.map +1 -1
  183. package/src/lib/server/process-restock.js +4 -0
  184. package/src/lib/server/process-restock.js.map +1 -1
  185. package/src/lib/server/product-card.js +2 -2
  186. package/src/lib/server/product-card.js.map +1 -1
  187. package/src/lib/server/product-catalog.d.ts +2 -0
  188. package/src/lib/server/product-catalog.js +11 -4
  189. package/src/lib/server/product-catalog.js.map +1 -1
  190. package/src/lib/server/product.js +3 -1
  191. package/src/lib/server/product.js.map +1 -1
  192. package/src/lib/server/related.js +8 -1
  193. package/src/lib/server/related.js.map +1 -1
  194. package/src/lib/server/reservation-availability-query.d.ts +35 -0
  195. package/src/lib/server/reservation-availability-query.js +34 -0
  196. package/src/lib/server/reservation-availability-query.js.map +1 -0
  197. package/src/lib/server/reservation-availability.d.ts +21 -0
  198. package/src/lib/server/reservation-availability.js +64 -10
  199. package/src/lib/server/reservation-availability.js.map +1 -1
  200. package/src/lib/server/reservation-walk-in.d.ts +28 -0
  201. package/src/lib/server/reservation-walk-in.js +166 -0
  202. package/src/lib/server/reservation-walk-in.js.map +1 -0
  203. package/src/lib/server/reserve.js +13 -10
  204. package/src/lib/server/reserve.js.map +1 -1
  205. package/src/lib/server/reviews.js +3 -1
  206. package/src/lib/server/reviews.js.map +1 -1
  207. package/src/lib/server/site-page-resolver.js +5 -2
  208. package/src/lib/server/site-page-resolver.js.map +1 -1
  209. package/src/lib/server.js +26 -0
  210. package/src/lib/server.js.map +1 -1
  211. package/src/lib/testing/fake-firestore.js +72 -0
  212. package/src/lib/testing/fake-firestore.js.map +1 -1
@@ -32,8 +32,28 @@ export declare const POS_OFFLINE_LATE_SYNC_MS: number;
32
32
  export declare const POS_OFFLINE_MAX_AGE_MS: number;
33
33
  /** How far ahead of the server a device clock may run before it is clamped. */
34
34
  export declare const POS_OFFLINE_CLOCK_SKEW_MS: number;
35
- /** The one tender an offline sale takes. */
36
- export type PosOfflineTender = 'cash';
35
+ /** What an offline sale was paid with. A sale with no tender is cash (the shape before AGL-4039). */
36
+ export type PosOfflineTender = 'cash' | 'card';
37
+ export { POS_OFFLINE_CARD_MAX_SALE_CENTS_LIMIT, POS_OFFLINE_CARD_RECONNECT_WITHIN_MS, } from './pos-offline-card-limits';
38
+ /** The `metadata[kind]` of a PaymentIntent the app created for a card rung offline. */
39
+ export declare const POS_OFFLINE_CARD_INTENT_KIND = "pos-offline";
40
+ /**
41
+ * Where a card rung offline stands. `stored`: on the device, not yet sent to
42
+ * Stripe. `forwarded`: Stripe has it and it is authorized, waiting for the
43
+ * sync to capture it. `declined`: the bank refused it when it was forwarded.
44
+ * `failed`: Stripe could not take it at all (no PaymentIntent was made).
45
+ */
46
+ export type PosOfflineCardForward = 'stored' | 'forwarded' | 'declined' | 'failed';
47
+ /** The card side of a sale rung offline on the app's own reader. */
48
+ export interface PosOfflineSaleCard {
49
+ forward: PosOfflineCardForward;
50
+ /** Stripe's PaymentIntent id, once the SDK forwarded it. */
51
+ paymentIntentId?: string | null;
52
+ brand?: string;
53
+ last4?: string;
54
+ /** The bank's or Stripe's words for a decline or failure. */
55
+ declineMessage?: string;
56
+ }
37
57
  /** Every tender the register turns off while offline, and why, in its words. */
38
58
  export declare const POS_OFFLINE_DISABLED_TENDERS: Readonly<Record<string, string>>;
39
59
  /** One basket line as the register rang it. */
@@ -80,12 +100,17 @@ export interface PosOfflineSale {
80
100
  discountPct: number;
81
101
  /** The totals the receipt printed and the customer paid. */
82
102
  totals: OrderTotals;
103
+ /** Cash handed over; 0 for a card sale. */
83
104
  cashTenderedCents: number;
84
105
  changeCents: number;
85
106
  customer?: PosOfflineSaleCustomer;
107
+ /** `card` only on the native app's own reader (AGL-4039); absent means cash. */
108
+ tender?: PosOfflineTender;
109
+ /** The card, when `tender` is `card`. */
110
+ card?: PosOfflineSaleCard;
86
111
  }
87
112
  /** What does not match, said on the order and back to the register. */
88
- export type PosOfflineFlag = 'price-changed' | 'unknown-product' | 'tax-differs' | 'totals-restated' | 'discount-over-limit' | 'cash-short' | 'stock-short' | 'shift-closed' | 'no-shift' | 'unknown-register' | 'register-over-cap' | 'late-sync' | 'clock-adjusted' | 'cashier-unverified';
113
+ export type PosOfflineFlag = 'price-changed' | 'unknown-product' | 'tax-differs' | 'totals-restated' | 'discount-over-limit' | 'cash-short' | 'stock-short' | 'shift-closed' | 'no-shift' | 'unknown-register' | 'register-over-cap' | 'late-sync' | 'clock-adjusted' | 'cashier-unverified' | 'card-declined' | 'card-not-charged';
89
114
  /** Each flag in the words the order and the register show. */
90
115
  export declare const POS_OFFLINE_FLAG_LABELS: Record<PosOfflineFlag, string>;
91
116
  /** A line whose stock went short when the sale synced. */
@@ -153,6 +178,19 @@ export interface PosOfflineKitRegister {
153
178
  locationId?: string;
154
179
  openShiftId: string | null;
155
180
  }
181
+ /** What the native app needs to take a card offline on its own reader (AGL-4039). */
182
+ export interface PosOfflineKitCard {
183
+ available: boolean;
184
+ /** Why not, in the cashier's words, when it is not. */
185
+ unavailableReason?: string;
186
+ /** The connected account the charge transfers to (a destination charge, as online). */
187
+ destination: string;
188
+ currency: string;
189
+ /** The limit of one card sale rung offline, in cents: the store's, or Stripe's own ceiling when it set none. */
190
+ maxSaleCents: number;
191
+ /** The limit of every card one device takes offline in a day, in cents; 0 is no daily limit. */
192
+ maxDeviceDailyCents: number;
193
+ }
156
194
  /**
157
195
  * Everything a register needs to sell offline besides the catalog, from
158
196
  * `GET /api/commerce/pos-offline-sync?hostId=`: the server's own answers, so
@@ -175,6 +213,8 @@ export interface PosOfflineKit {
175
213
  maxDiscountPct: number;
176
214
  requireOpenShift: boolean;
177
215
  registers: PosOfflineKitRegister[];
216
+ /** Card sales on the app's own reader while offline; absent from a server that does not take them. */
217
+ card?: PosOfflineKitCard;
178
218
  /** What the receipt prints at its head and foot. `logo` is the stored value. */
179
219
  receipt: {
180
220
  name: string;
@@ -232,3 +272,25 @@ export declare function sanitizePosOfflineSale(raw: unknown): {
232
272
  export declare function posOfflineTotalsAddUp(sale: Pick<PosOfflineSale, 'lines' | 'discountPct' | 'totals'>): boolean;
233
273
  /** The flags in the order and words the register lists them. */
234
274
  export declare function posOfflineFlagLabels(flags: readonly PosOfflineFlag[]): string[];
275
+ /** The card side of a queued sale, typed and bounded, or `null` when it cannot be read. */
276
+ export declare function sanitizePosOfflineSaleCard(raw: unknown): PosOfflineSaleCard | null;
277
+ /**
278
+ * Why a card cannot be taken offline for `totalCents`, in the cashier's
279
+ * words, or `null` when it can: the store has not turned it on, the sale is
280
+ * over the store's (or Stripe's) limit per sale, or the cards this device
281
+ * has taken offline today would pass the store's daily limit per device (a
282
+ * daily limit of 0 is none: the store did not set one).
283
+ */
284
+ export declare function posOfflineCardRefusal(input: {
285
+ card: Pick<PosOfflineKitCard, 'available' | 'unavailableReason' | 'maxSaleCents' | 'maxDeviceDailyCents'> | null | undefined;
286
+ totalCents: number;
287
+ /** Every card this device took offline today, in cents. */
288
+ takenTodayCents: number;
289
+ }): string | null;
290
+ /**
291
+ * What the app writes on the PaymentIntent it creates for a card rung
292
+ * offline: the sale key (the order's id once it syncs) and where it was rung,
293
+ * so the server can match the charge to the sale even if the sale's own sync
294
+ * never arrives.
295
+ */
296
+ export declare function posOfflineCardIntentMetadata(sale: Pick<PosOfflineSale, 'saleKey' | 'hostId' | 'orgId' | 'registerId'>): Record<string, string>;
@@ -16,16 +16,28 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
16
16
  * limitations under the License.
17
17
  */ import { computeOrderTotals } from "./commerce-orders.js";
18
18
  import { computeTaxCents } from "./commerce-tax.js";
19
+ import { POS_OFFLINE_CARD_MAX_SALE_CENTS_LIMIT } from "./pos-offline-card-limits.js";
19
20
  /*==========================================
20
21
  * THE OFFLINE REGISTER (AGL-3625): the contract every register client — the
21
22
  * web register and the native Aglyn POS app — shares with
22
23
  * `POST /api/commerce/pos-offline-sync`.
23
24
  *
24
- * WHAT SELLS OFFLINE IS CASH, AND ONLY CASH.
25
+ * WHAT SELLS OFFLINE IS CASH, AND A CARD ON THE NATIVE APP'S OWN READER.
25
26
  *
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.
27
+ * - Cash, on every register client.
28
+ * - A card, only on the native Aglyn POS app's own Stripe Terminal reader
29
+ * (AGL-4039): Stripe's SDK stores the card on the device ("store and
30
+ * forward") and forwards it once a connection returns. The app creates
31
+ * the PaymentIntent itself, a destination charge with MANUAL capture and
32
+ * NO application fee; the server captures it when the sale syncs and
33
+ * sizes the platform fee then, exactly as for an online card sale. A bank
34
+ * can still decline it at that point, after the customer left: the order
35
+ * is then recorded unpaid and flagged, and the register says so.
36
+ * Off until the store turns it on, then within the store's two limits:
37
+ * per sale, and per device per day (as Shopify POS words it).
38
+ * - Every other card tender cannot: Tap to Pay on Android, the server's
39
+ * smart readers, the typed card and the QR link are server-driven, and
40
+ * Stripe does not take a server-driven card payment without the server.
29
41
  * - Gift cards DO NOT, deliberately. A balance cannot be checked without the
30
42
  * server, and a capped "allow it anyway" (Square's offer for cards) turns a
31
43
  * short balance into a sale whose money nobody collected — discovered at
@@ -58,6 +70,8 @@ import { computeTaxCents } from "./commerce-tax.js";
58
70
  * place a sale anywhere in history.
59
71
  */ export const POS_OFFLINE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
60
72
  /** 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;
73
+ export { POS_OFFLINE_CARD_MAX_SALE_CENTS_LIMIT, POS_OFFLINE_CARD_RECONNECT_WITHIN_MS } from "./pos-offline-card-limits.js";
74
+ /** The `metadata[kind]` of a PaymentIntent the app created for a card rung offline. */ export const POS_OFFLINE_CARD_INTENT_KIND = 'pos-offline';
61
75
  /** Every tender the register turns off while offline, and why, in its words. */ export const POS_OFFLINE_DISABLED_TENDERS = {
62
76
  card_present: 'Card readers need the connection.',
63
77
  card_keyed: 'Typed cards need the connection.',
@@ -80,7 +94,9 @@ import { computeTaxCents } from "./commerce-tax.js";
80
94
  'register-over-cap': 'The register it was rung on is over the plan’s register limit',
81
95
  'late-sync': 'Synced more than a day after it was rung',
82
96
  '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'
97
+ 'cashier-unverified': 'The PIN-switched cashier could not be confirmed; the sale is under the signed-in member',
98
+ 'card-declined': 'The card taken offline was declined when it was charged; the order is unpaid',
99
+ 'card-not-charged': 'The card taken offline could not be charged; the order is unpaid'
84
100
  };
85
101
  /**
86
102
  * The totals of an offline sale: the cashier's discount on the goods, then
@@ -242,6 +258,15 @@ function wholeCents(value) {
242
258
  const locationId = cleanId(input['locationId']);
243
259
  const assertion = bounded(input['cashierAssertion'], 2000);
244
260
  const orgId = bounded(input['orgId'], 128);
261
+ let card;
262
+ if (input['tender'] === 'card') {
263
+ const read = sanitizePosOfflineSaleCard(input['card']);
264
+ if (!read) return {
265
+ ok: false,
266
+ error: 'The card of the sale cannot be read'
267
+ };
268
+ card = read;
269
+ }
245
270
  return {
246
271
  ok: true,
247
272
  sale: _extends({
@@ -268,10 +293,13 @@ function wholeCents(value) {
268
293
  totalCents: wholeCents(totals['totalCents']),
269
294
  feeCents: 0
270
295
  },
271
- cashTenderedCents: wholeCents(input['cashTenderedCents']),
272
- changeCents: wholeCents(input['changeCents'])
296
+ cashTenderedCents: card ? 0 : wholeCents(input['cashTenderedCents']),
297
+ changeCents: card ? 0 : wholeCents(input['changeCents'])
273
298
  }, Object.keys(customer).length ? {
274
299
  customer
300
+ } : {}, card ? {
301
+ tender: 'card',
302
+ card
275
303
  } : {})
276
304
  };
277
305
  }
@@ -297,5 +325,74 @@ function wholeCents(value) {
297
325
  return (_POS_OFFLINE_FLAG_LABELS_flag = POS_OFFLINE_FLAG_LABELS[flag]) != null ? _POS_OFFLINE_FLAG_LABELS_flag : flag;
298
326
  });
299
327
  }
328
+ const CARD_FORWARDS = [
329
+ 'stored',
330
+ 'forwarded',
331
+ 'declined',
332
+ 'failed'
333
+ ];
334
+ /** The card side of a queued sale, typed and bounded, or `null` when it cannot be read. */ export function sanitizePosOfflineSaleCard(raw) {
335
+ if (!raw || typeof raw !== 'object') return null;
336
+ const input = raw;
337
+ const forward = input['forward'];
338
+ if (!CARD_FORWARDS.includes(forward)) return null;
339
+ const intent = typeof input['paymentIntentId'] === 'string' ? input['paymentIntentId'].trim() : '';
340
+ if (intent && !/^pi_[A-Za-z0-9]{8,64}$/.test(intent)) return null;
341
+ if (forward === 'forwarded' && !intent) return null;
342
+ const brand = bounded(input['brand'], 24);
343
+ const last4 = typeof input['last4'] === 'string' && /^\d{4}$/.test(input['last4']) ? input['last4'] : '';
344
+ const declineMessage = bounded(input['declineMessage'], 300);
345
+ return _extends({
346
+ forward
347
+ }, intent ? {
348
+ paymentIntentId: intent
349
+ } : {}, brand ? {
350
+ brand
351
+ } : {}, last4 ? {
352
+ last4
353
+ } : {}, declineMessage ? {
354
+ declineMessage
355
+ } : {});
356
+ }
357
+ /**
358
+ * Why a card cannot be taken offline for `totalCents`, in the cashier's
359
+ * words, or `null` when it can: the store has not turned it on, the sale is
360
+ * over the store's (or Stripe's) limit per sale, or the cards this device
361
+ * has taken offline today would pass the store's daily limit per device (a
362
+ * daily limit of 0 is none: the store did not set one).
363
+ */ export function posOfflineCardRefusal(input) {
364
+ const card = input.card;
365
+ if (!card) return 'Card sales offline need the register to connect once first.';
366
+ if (!card.available) return card.unavailableReason || 'Card sales offline are not turned on for this store.';
367
+ const total = wholeCents(input.totalCents);
368
+ if (total <= 0) return 'There is nothing to charge.';
369
+ const perSale = Math.min(wholeCents(card.maxSaleCents), POS_OFFLINE_CARD_MAX_SALE_CENTS_LIMIT);
370
+ if (total > perSale) return `Offline, a card sale can be up to ${dollars(perSale)}. Take cash, or charge it when you are back online.`;
371
+ const today = wholeCents(input.takenTodayCents);
372
+ const daily = wholeCents(card.maxDeviceDailyCents);
373
+ if (daily > 0 && today + total > daily) {
374
+ return `This device has taken ${dollars(today)} in cards offline today, and its daily limit is ${dollars(daily)}. Take cash, or wait for the connection.`;
375
+ }
376
+ return null;
377
+ }
378
+ /**
379
+ * What the app writes on the PaymentIntent it creates for a card rung
380
+ * offline: the sale key (the order's id once it syncs) and where it was rung,
381
+ * so the server can match the charge to the sale even if the sale's own sync
382
+ * never arrives.
383
+ */ export function posOfflineCardIntentMetadata(sale) {
384
+ return {
385
+ kind: POS_OFFLINE_CARD_INTENT_KIND,
386
+ saleKey: sale.saleKey,
387
+ hostId: sale.hostId,
388
+ orgId: sale.orgId,
389
+ registerId: sale.registerId
390
+ };
391
+ }
392
+ function dollars(cents) {
393
+ const whole = Math.floor(cents / 100);
394
+ const rest = cents % 100;
395
+ return `$${whole.toLocaleString('en-US')}.${String(rest).padStart(2, '0')}`;
396
+ }
300
397
 
301
398
  //# sourceMappingURL=commerce-pos-offline.js.map
@@ -1 +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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"}
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'\nimport { POS_OFFLINE_CARD_MAX_SALE_CENTS_LIMIT } from './pos-offline-card-limits'\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 A CARD ON THE NATIVE APP'S OWN READER.\n *\n * - Cash, on every register client.\n * - A card, only on the native Aglyn POS app's own Stripe Terminal reader\n * (AGL-4039): Stripe's SDK stores the card on the device (\"store and\n * forward\") and forwards it once a connection returns. The app creates\n * the PaymentIntent itself, a destination charge with MANUAL capture and\n * NO application fee; the server captures it when the sale syncs and\n * sizes the platform fee then, exactly as for an online card sale. A bank\n * can still decline it at that point, after the customer left: the order\n * is then recorded unpaid and flagged, and the register says so.\n * Off until the store turns it on, then within the store's two limits:\n * per sale, and per device per day (as Shopify POS words it).\n * - Every other card tender cannot: Tap to Pay on Android, the server's\n * smart readers, the typed card and the QR link are server-driven, and\n * Stripe does not take a 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/** What an offline sale was paid with. A sale with no tender is cash (the shape before AGL-4039). */\nexport type PosOfflineTender = 'cash' | 'card'\n\nexport {\n POS_OFFLINE_CARD_MAX_SALE_CENTS_LIMIT,\n POS_OFFLINE_CARD_RECONNECT_WITHIN_MS,\n} from './pos-offline-card-limits'\n\n/** The `metadata[kind]` of a PaymentIntent the app created for a card rung offline. */\nexport const POS_OFFLINE_CARD_INTENT_KIND = 'pos-offline'\n\n/**\n * Where a card rung offline stands. `stored`: on the device, not yet sent to\n * Stripe. `forwarded`: Stripe has it and it is authorized, waiting for the\n * sync to capture it. `declined`: the bank refused it when it was forwarded.\n * `failed`: Stripe could not take it at all (no PaymentIntent was made).\n */\nexport type PosOfflineCardForward = 'stored' | 'forwarded' | 'declined' | 'failed'\n\n/** The card side of a sale rung offline on the app's own reader. */\nexport interface PosOfflineSaleCard {\n forward: PosOfflineCardForward\n /** Stripe's PaymentIntent id, once the SDK forwarded it. */\n paymentIntentId?: string | null\n brand?: string\n last4?: string\n /** The bank's or Stripe's words for a decline or failure. */\n declineMessage?: string\n}\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 /** Cash handed over; 0 for a card sale. */\n cashTenderedCents: number\n changeCents: number\n customer?: PosOfflineSaleCustomer\n /** `card` only on the native app's own reader (AGL-4039); absent means cash. */\n tender?: PosOfflineTender\n /** The card, when `tender` is `card`. */\n card?: PosOfflineSaleCard\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 | 'card-declined'\n | 'card-not-charged'\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 'card-declined': 'The card taken offline was declined when it was charged; the order is unpaid',\n 'card-not-charged': 'The card taken offline could not be charged; the order is unpaid',\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/** What the native app needs to take a card offline on its own reader (AGL-4039). */\nexport interface PosOfflineKitCard {\n available: boolean\n /** Why not, in the cashier's words, when it is not. */\n unavailableReason?: string\n /** The connected account the charge transfers to (a destination charge, as online). */\n destination: string\n currency: string\n /** The limit of one card sale rung offline, in cents: the store's, or Stripe's own ceiling when it set none. */\n maxSaleCents: number\n /** The limit of every card one device takes offline in a day, in cents; 0 is no daily limit. */\n maxDeviceDailyCents: number\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 /** Card sales on the app's own reader while offline; absent from a server that does not take them. */\n card?: PosOfflineKitCard\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 let card: PosOfflineSaleCard | undefined\n if (input['tender'] === 'card') {\n const read = sanitizePosOfflineSaleCard(input['card'])\n if (!read) return { ok: false, error: 'The card of the sale cannot be read' }\n card = read\n }\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: card ? 0 : wholeCents(input['cashTenderedCents']),\n changeCents: card ? 0 : wholeCents(input['changeCents']),\n ...(Object.keys(customer).length ? { customer } : {}),\n ...(card ? { tender: 'card' as const, card } : {}),\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\nconst CARD_FORWARDS: readonly PosOfflineCardForward[] = ['stored', 'forwarded', 'declined', 'failed']\n\n/** The card side of a queued sale, typed and bounded, or `null` when it cannot be read. */\nexport function sanitizePosOfflineSaleCard(raw: unknown): PosOfflineSaleCard | null {\n if (!raw || typeof raw !== 'object') return null\n const input = raw as Record<string, unknown>\n const forward = input['forward'] as PosOfflineCardForward\n if (!CARD_FORWARDS.includes(forward)) return null\n const intent = typeof input['paymentIntentId'] === 'string' ? input['paymentIntentId'].trim() : ''\n if (intent && !/^pi_[A-Za-z0-9]{8,64}$/.test(intent)) return null\n if (forward === 'forwarded' && !intent) return null\n const brand = bounded(input['brand'], 24)\n const last4 = typeof input['last4'] === 'string' && /^\\d{4}$/.test(input['last4']) ? input['last4'] : ''\n const declineMessage = bounded(input['declineMessage'], 300)\n return {\n forward,\n ...(intent ? { paymentIntentId: intent } : {}),\n ...(brand ? { brand } : {}),\n ...(last4 ? { last4 } : {}),\n ...(declineMessage ? { declineMessage } : {}),\n }\n}\n\n/**\n * Why a card cannot be taken offline for `totalCents`, in the cashier's\n * words, or `null` when it can: the store has not turned it on, the sale is\n * over the store's (or Stripe's) limit per sale, or the cards this device\n * has taken offline today would pass the store's daily limit per device (a\n * daily limit of 0 is none: the store did not set one).\n */\nexport function posOfflineCardRefusal(input: {\n card: Pick<PosOfflineKitCard, 'available' | 'unavailableReason' | 'maxSaleCents' | 'maxDeviceDailyCents'> | null | undefined\n totalCents: number\n /** Every card this device took offline today, in cents. */\n takenTodayCents: number\n}): string | null {\n const card = input.card\n if (!card) return 'Card sales offline need the register to connect once first.'\n if (!card.available) return card.unavailableReason || 'Card sales offline are not turned on for this store.'\n const total = wholeCents(input.totalCents)\n if (total <= 0) return 'There is nothing to charge.'\n const perSale = Math.min(wholeCents(card.maxSaleCents), POS_OFFLINE_CARD_MAX_SALE_CENTS_LIMIT)\n if (total > perSale) return `Offline, a card sale can be up to ${dollars(perSale)}. Take cash, or charge it when you are back online.`\n const today = wholeCents(input.takenTodayCents)\n const daily = wholeCents(card.maxDeviceDailyCents)\n if (daily > 0 && today + total > daily) {\n return `This device has taken ${dollars(today)} in cards offline today, and its daily limit is ${dollars(daily)}. Take cash, or wait for the connection.`\n }\n return null\n}\n\n/**\n * What the app writes on the PaymentIntent it creates for a card rung\n * offline: the sale key (the order's id once it syncs) and where it was rung,\n * so the server can match the charge to the sale even if the sale's own sync\n * never arrives.\n */\nexport function posOfflineCardIntentMetadata(\n sale: Pick<PosOfflineSale, 'saleKey' | 'hostId' | 'orgId' | 'registerId'>,\n): Record<string, string> {\n return {\n kind: POS_OFFLINE_CARD_INTENT_KIND,\n saleKey: sale.saleKey,\n hostId: sale.hostId,\n orgId: sale.orgId,\n registerId: sale.registerId,\n }\n}\n\nfunction dollars(cents: number): string {\n const whole = Math.floor(cents / 100)\n const rest = cents % 100\n return `$${whole.toLocaleString('en-US')}.${String(rest).padStart(2, '0')}`\n}\n"],"names":["computeOrderTotals","computeTaxCents","POS_OFFLINE_CARD_MAX_SALE_CENTS_LIMIT","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_CARD_RECONNECT_WITHIN_MS","POS_OFFLINE_CARD_INTENT_KIND","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","card","read","sanitizePosOfflineSaleCard","sale","v","cashierAssertion","shippingCents","cashTenderedCents","changeCents","Object","keys","tender","posOfflineTotalsAddUp","expected","posOfflineFlagLabels","flags","flag","CARD_FORWARDS","forward","intent","brand","last4","declineMessage","paymentIntentId","posOfflineCardRefusal","available","unavailableReason","total","perSale","maxSaleCents","dollars","today","takenTodayCents","daily","maxDeviceDailyCents","posOfflineCardIntentMetadata","cents","whole","floor","rest","toLocaleString","padStart"],"mappings":"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"}
@@ -206,6 +206,9 @@ export declare function computePosShiftReport(input: {
206
206
  export declare function posCashVarianceCents(countedCashCents: number, expectedCashCents: number): number;
207
207
  /** `$12.34`, `-$0.50` — the register's money format, store currency USD. */
208
208
  export declare function posMoney(cents: number): string;
209
+ /** One CSV cell, quoted when it needs to be and never read as a formula. */
210
+ declare function csvCell(value: unknown): string;
211
+ export { csvCell as posCsvCell };
209
212
  /** The shift history as a spreadsheet: one row per shift, money in dollars. */
210
213
  export declare function posShiftsCsv(shifts: ReadonlyArray<PosShift & {
211
214
  id: string;
@@ -204,6 +204,7 @@ export const POS_CASH_EVENT_LABELS = {
204
204
  if (/^[=+\-@\t\r]/.test(cell) && !/^-?\d+(\.\d+)?$/.test(cell)) cell = `'${cell}`;
205
205
  return /[",\r\n]/.test(cell) ? `"${cell.replace(/"/g, '""')}"` : cell;
206
206
  }
207
+ export { csvCell as posCsvCell };
207
208
  const dollars = (cents)=>cents == null ? '' : (Math.round(cents) / 100).toFixed(2);
208
209
  /** The shift history as a spreadsheet: one row per shift, money in dollars. */ export function posShiftsCsv(shifts, memberName = (uid)=>uid) {
209
210
  const header = [