@aglyn/plugins-commerce 1.0.0-beta.277 → 1.0.0-beta.282

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
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/pos-hardware.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 { createHash } from 'crypto'\nimport type { PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, readOrgBilling } from '@aglyn/tenant-data-admin'\nimport { hostConsoleUrl } from '@aglyn/tenant-data-admin/server/host-console-link'\nimport {\n encodePosHardwareLines,\n isPosHardwareShippingMethod,\n POS_HARDWARE_ANCHOR,\n parsePosHardwareConfig,\n POS_HARDWARE_CURRENCY,\n POS_HARDWARE_METADATA_TYPE,\n POS_HARDWARE_ORDERS_COLLECTION,\n POS_HARDWARE_SALES_COLLECTION,\n POS_HARDWARE_SHIP_COUNTRIES,\n POS_HARDWARE_SHIPPING_LABELS,\n posHardwareOffers,\n posHardwareSku,\n pricePosHardwareCart,\n type PosHardwareConfig,\n type PosHardwareOrder,\n} from '../model/pos-hardware'\nimport { authorizePosStaff, posIdempotencyKey, posRequestBody } from './pos-auth'\nimport { posStripe, posTerminalAvailable } from './pos-stripe'\n\n/*==========================================\n * `POST /api/commerce/pos-hardware` (AGL-4038): the merchant's half of the\n * POS hardware shop, behind the Hardware card on Products → Point of sale.\n *\n * catalog what is on offer and at what price, or `available: false`\n * checkout a Stripe Checkout Session for the basket, on the PLATFORM\n * orders this site's hardware orders, newest first\n *\n * Gated like the register (`authorizePosStaff`: an admin or editor of the\n * site with `managePos`, on a plan with POS), because the readers it sells\n * are the register's.\n *\n * THE SALE IS THE PLATFORM'S. The session is created with the platform key\n * and NO connected account, `transfer_data` or application fee: the merchant\n * is the buyer here, and the platform is the seller of record that places\n * the order with Stripe. The order document is written by the webhook once\n * Stripe says the session is paid (`pos-hardware-webhook.ts`), never here.\n *=========================================*/\n\n/** The shop's config from this deployment, or `null` while it is hidden. */\nexport function posHardwareConfig(\n env: Record<string, string | undefined> = process.env,\n): PosHardwareConfig | null {\n // A reader the platform cannot yet take payments on is not for sale: the\n // shop opens only where the register offers card readers at all.\n if (!posTerminalAvailable()) return null\n return parsePosHardwareConfig(env)\n}\n\n/** The order's id, derived from the attempt so a repeated press is one order. */\nexport function posHardwareOrderId(orgId: string, hostId: string, attemptKey: string): string {\n return `hw_${createHash('sha256').update(`${orgId}:${hostId}:${attemptKey}`).digest('hex').slice(0, 20)}`\n}\n\n/** The number a person reads: `HW-` and six characters of the id. */\nexport function posHardwareOrderNumber(orderId: string): string {\n return `HW-${orderId.replace(/^hw_/, '').slice(0, 6).toUpperCase()}`\n}\n\nexport function hardwareOrdersRef(orgId: string) {\n return firebaseAdmin\n .app()\n .firestore()\n .collection('orgs')\n .doc(orgId)\n .collection(POS_HARDWARE_ORDERS_COLLECTION)\n}\n\n/**\n * `posHardwareSales/{orderId}`, the retained sale record beside each order\n * (see `PosHardwareSaleRecord`). Top-level on purpose: the workspace erasure\n * cannot reach it.\n */\nexport function posHardwareSaleRef(orderId: string) {\n return firebaseAdmin.app().firestore().collection(POS_HARDWARE_SALES_COLLECTION).doc(orderId)\n}\n\n/** An order as the merchant's card shows it: no Stripe ids, no staff notes. */\nexport function merchantHardwareOrderView(id: string, order: PosHardwareOrder) {\n return {\n id,\n number: posHardwareOrderNumber(id),\n status: order.status,\n lines: (order.lines ?? []).map((line) => ({\n sku: line.sku,\n name: line.name,\n quantity: line.quantity,\n unitCents: line.unitCents,\n lineCents: line.lineCents,\n })),\n subtotalCents: order.subtotalCents ?? 0,\n shippingCents: order.shipping?.cents ?? 0,\n shippingLabel: order.shipping?.label ?? '',\n taxCents: order.taxCents ?? 0,\n totalCents: order.totalCents ?? 0,\n tracking: order.tracking ?? null,\n createdAtMs: order.createdAtMs ?? 0,\n updatedAtMs: order.updatedAtMs ?? order.createdAtMs ?? 0,\n }\n}\n\nexport type MerchantHardwareOrderView = ReturnType<typeof merchantHardwareOrderView>\n\n/** The POS settings page of a site, as an absolute console URL. */\nexport async function posSettingsUrl(hostId: string): Promise<string> {\n return hostConsoleUrl(hostId, `/${hostId}/products/pos`)\n}\n\n/** `base` with `query` added before any `#fragment`, then `#hash`. */\nexport function withQueryAndHash(base: string, query: string, hash: string): string {\n const [path] = base.split('#')\n const joiner = path.includes('?') ? '&' : '?'\n return `${path}${joiner}${query}#${hash}`\n}\n\n\nexport const posHardwareHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' })\n const body = posRequestBody(req)\n const hostId = String(body['hostId'] ?? '')\n const gate = await authorizePosStaff(req, hostId)\n if ('error' in gate) return res.status(gate.status).json({ error: gate.error })\n const staff = gate.staff\n const config = posHardwareConfig()\n const action = String(body['action'] ?? '')\n try {\n switch (action) {\n case 'catalog':\n if (!config) return res.status(200).json({ available: false })\n return res.status(200).json({\n available: true,\n offers: posHardwareOffers(config),\n shipping: config.shipping.map((option) => ({\n method: option.method,\n label: POS_HARDWARE_SHIPPING_LABELS[option.method],\n cents: option.cents,\n })),\n automaticTax: config.automaticTax,\n shipCountries: POS_HARDWARE_SHIP_COUNTRIES,\n })\n case 'orders': {\n const snapshot = await hardwareOrdersRef(staff.orgId)\n .where('hostId', '==', staff.hostId)\n .orderBy('createdAtMs', 'desc')\n .limit(50)\n .get()\n return res.status(200).json({\n orders: snapshot.docs.map((doc: any) =>\n merchantHardwareOrderView(String(doc.id), doc.data() as PosHardwareOrder),\n ),\n })\n }\n case 'checkout': {\n if (!config) return res.status(404).json({ error: 'The hardware shop is not available.' })\n const attemptKey = posIdempotencyKey(req)\n if (!attemptKey) return res.status(400).json({ error: 'Missing Idempotency-Key' })\n if (!staff.orgId) return res.status(409).json({ error: 'This site has no workspace.' })\n const priced = pricePosHardwareCart(config, body['lines'])\n if ('error' in priced) return res.status(400).json({ error: priced.error })\n const method = body['shippingMethod']\n const shipping = isPosHardwareShippingMethod(method)\n ? config.shipping.find((option) => option.method === method)\n : undefined\n if (!shipping) return res.status(400).json({ error: 'Choose a shipping speed.' })\n\n const orderId = posHardwareOrderId(staff.orgId, staff.hostId, attemptKey)\n const billing = await readOrgBilling(staff.orgId).catch(() => ({}))\n const customer = String((billing as { stripeCustomerId?: unknown }).stripeCustomerId ?? '').trim()\n const base = await posSettingsUrl(staff.hostId)\n const params: Record<string, string | number | boolean> = {\n mode: 'payment',\n client_reference_id: orderId,\n success_url: withQueryAndHash(base, 'hardwareOrder=placed', POS_HARDWARE_ANCHOR),\n cancel_url: withQueryAndHash(base, 'hardwareOrder=canceled', POS_HARDWARE_ANCHOR),\n 'payment_method_types[0]': 'card',\n 'shipping_address_collection[allowed_countries][0]': POS_HARDWARE_SHIP_COUNTRIES[0],\n 'phone_number_collection[enabled]': true,\n 'shipping_options[0][shipping_rate_data][type]': 'fixed_amount',\n 'shipping_options[0][shipping_rate_data][display_name]': POS_HARDWARE_SHIPPING_LABELS[shipping.method],\n 'shipping_options[0][shipping_rate_data][fixed_amount][amount]': shipping.cents,\n 'shipping_options[0][shipping_rate_data][fixed_amount][currency]': POS_HARDWARE_CURRENCY,\n 'metadata[type]': POS_HARDWARE_METADATA_TYPE,\n 'metadata[orderId]': orderId,\n 'metadata[orgId]': staff.orgId,\n 'metadata[hostId]': staff.hostId,\n 'metadata[uid]': staff.uid,\n 'metadata[lines]': encodePosHardwareLines(priced.lines),\n 'metadata[shippingMethod]': shipping.method,\n 'metadata[shippingCents]': shipping.cents,\n 'metadata[subtotalCents]': priced.subtotalCents,\n 'payment_intent_data[description]': `Card reader order ${posHardwareOrderNumber(orderId)}`,\n 'payment_intent_data[metadata][type]': POS_HARDWARE_METADATA_TYPE,\n 'payment_intent_data[metadata][orderId]': orderId,\n 'payment_intent_data[metadata][orgId]': staff.orgId,\n ...(customer ? { customer } : {}),\n ...(config.automaticTax\n ? {\n 'automatic_tax[enabled]': true,\n 'shipping_options[0][shipping_rate_data][tax_behavior]': 'exclusive',\n // A returning workspace's saved address is kept up to date\n // from what Checkout collects, which automatic tax requires.\n ...(customer ? { 'customer_update[shipping]': 'auto' } : {}),\n }\n : {}),\n }\n priced.lines.forEach((line, index) => {\n const sku = posHardwareSku(line.sku)\n params[`line_items[${index}][quantity]`] = line.quantity\n // A literal, not POS_HARDWARE_CURRENCY: check:facilitator-charge-shape\n // reads Stripe Tax beside a usd line item only when it says so (AGL-1956).\n params[`line_items[${index}][price_data][currency]`] = 'usd'\n params[`line_items[${index}][price_data][unit_amount]`] = line.unitCents\n params[`line_items[${index}][price_data][product_data][name]`] = line.name\n if (sku) params[`line_items[${index}][price_data][product_data][description]`] = sku.use\n params[`line_items[${index}][price_data][product_data][metadata][sku]`] = line.sku\n if (config.automaticTax) params[`line_items[${index}][price_data][tax_behavior]`] = 'exclusive'\n })\n const created = await posStripe('POST', 'checkout/sessions', {\n params,\n idempotencyKey: `pos-hw-checkout-${orderId}`,\n })\n const url = String(created.body?.url ?? '')\n if (!created.ok || !url) {\n console.error('[pos-hardware] checkout refused', created.status, created.body?.error?.code)\n return res.status(502).json({ error: 'Checkout could not be started. Try again in a moment.' })\n }\n return res.status(200).json({ url, orderId })\n }\n default:\n return res.status(400).json({ error: 'Unknown action' })\n }\n } catch (error) {\n console.error('[pos-hardware]', action, error)\n return res.status(500).json({ error: 'The hardware shop could not answer' })\n }\n}\n"],"names":["createHash","firebaseAdmin","readOrgBilling","hostConsoleUrl","encodePosHardwareLines","isPosHardwareShippingMethod","POS_HARDWARE_ANCHOR","parsePosHardwareConfig","POS_HARDWARE_CURRENCY","POS_HARDWARE_METADATA_TYPE","POS_HARDWARE_ORDERS_COLLECTION","POS_HARDWARE_SALES_COLLECTION","POS_HARDWARE_SHIP_COUNTRIES","POS_HARDWARE_SHIPPING_LABELS","posHardwareOffers","posHardwareSku","pricePosHardwareCart","authorizePosStaff","posIdempotencyKey","posRequestBody","posStripe","posTerminalAvailable","posHardwareConfig","env","process","posHardwareOrderId","orgId","hostId","attemptKey","update","digest","slice","posHardwareOrderNumber","orderId","replace","toUpperCase","hardwareOrdersRef","app","firestore","collection","doc","posHardwareSaleRef","merchantHardwareOrderView","id","order","number","status","lines","map","line","sku","name","quantity","unitCents","lineCents","subtotalCents","shippingCents","shipping","cents","shippingLabel","label","taxCents","totalCents","tracking","createdAtMs","updatedAtMs","posSettingsUrl","withQueryAndHash","base","query","hash","path","split","joiner","includes","posHardwareHandler","req","res","body","method","json","error","String","gate","staff","config","action","available","offers","option","automaticTax","shipCountries","snapshot","where","orderBy","limit","get","orders","docs","data","created","priced","find","undefined","billing","catch","customer","stripeCustomerId","trim","params","mode","client_reference_id","success_url","cancel_url","uid","forEach","index","use","idempotencyKey","url","ok","console","code"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,UAAU,QAAQ,SAAQ;AAEnC,SAASC,aAAa,EAAEC,cAAc,QAAQ,2BAA0B;AACxE,SAASC,cAAc,QAAQ,oDAAmD;AAClF,SACEC,sBAAsB,EACtBC,2BAA2B,EAC3BC,mBAAmB,EACnBC,sBAAsB,EACtBC,qBAAqB,EACrBC,0BAA0B,EAC1BC,8BAA8B,EAC9BC,6BAA6B,EAC7BC,2BAA2B,EAC3BC,4BAA4B,EAC5BC,iBAAiB,EACjBC,cAAc,EACdC,oBAAoB,QAGf,2BAAuB;AAC9B,SAASC,iBAAiB,EAAEC,iBAAiB,EAAEC,cAAc,QAAQ,gBAAY;AACjF,SAASC,SAAS,EAAEC,oBAAoB,QAAQ,kBAAc;AAE9D;;;;;;;;;;;;;;;;;2CAiB2C,GAE3C,0EAA0E,GAC1E,OAAO,SAASC,kBACdC,MAA0CC,QAAQD,GAAG;IAErD,yEAAyE;IACzE,iEAAiE;IACjE,IAAI,CAACF,wBAAwB,OAAO;IACpC,OAAOd,uBAAuBgB;AAChC;AAEA,+EAA+E,GAC/E,OAAO,SAASE,mBAAmBC,KAAa,EAAEC,MAAc,EAAEC,UAAkB;IAClF,OAAO,CAAC,GAAG,EAAE5B,WAAW,UAAU6B,MAAM,CAAC,GAAGH,MAAM,CAAC,EAAEC,OAAO,CAAC,EAAEC,YAAY,EAAEE,MAAM,CAAC,OAAOC,KAAK,CAAC,GAAG,KAAK;AAC3G;AAEA,mEAAmE,GACnE,OAAO,SAASC,uBAAuBC,OAAe;IACpD,OAAO,CAAC,GAAG,EAAEA,QAAQC,OAAO,CAAC,QAAQ,IAAIH,KAAK,CAAC,GAAG,GAAGI,WAAW,IAAI;AACtE;AAEA,OAAO,SAASC,kBAAkBV,KAAa;IAC7C,OAAOzB,cACJoC,GAAG,GACHC,SAAS,GACTC,UAAU,CAAC,QACXC,GAAG,CAACd,OACJa,UAAU,CAAC7B;AAChB;AAEA;;;;CAIC,GACD,OAAO,SAAS+B,mBAAmBR,OAAe;IAChD,OAAOhC,cAAcoC,GAAG,GAAGC,SAAS,GAAGC,UAAU,CAAC5B,+BAA+B6B,GAAG,CAACP;AACvF;AAEA,6EAA6E,GAC7E,OAAO,SAASS,0BAA0BC,EAAU,EAAEC,KAAuB;QAKjEA,cAOOA,mCAGLA,iBACEA,mBACFA,iBACGA,oBACAA,OAAAA;QANEA,iBACAA;IAbjB,OAAO;QACLD;QACAE,QAAQb,uBAAuBW;QAC/BG,QAAQF,MAAME,MAAM;QACpBC,OAAO,EAACH,eAAAA,MAAMG,KAAK,YAAXH,eAAe,EAAE,EAAEI,GAAG,CAAC,CAACC,OAAU,CAAA;gBACxCC,KAAKD,KAAKC,GAAG;gBACbC,MAAMF,KAAKE,IAAI;gBACfC,UAAUH,KAAKG,QAAQ;gBACvBC,WAAWJ,KAAKI,SAAS;gBACzBC,WAAWL,KAAKK,SAAS;YAC3B,CAAA;QACAC,aAAa,GAAEX,uBAAAA,MAAMW,aAAa,YAAnBX,uBAAuB;QACtCY,aAAa,WAAEZ,kBAAAA,MAAMa,QAAQ,qBAAdb,gBAAgBc,KAAK,mBAAI;QACxCC,aAAa,YAAEf,mBAAAA,MAAMa,QAAQ,qBAAdb,iBAAgBgB,KAAK,oBAAI;QACxCC,QAAQ,GAAEjB,kBAAAA,MAAMiB,QAAQ,YAAdjB,kBAAkB;QAC5BkB,UAAU,GAAElB,oBAAAA,MAAMkB,UAAU,YAAhBlB,oBAAoB;QAChCmB,QAAQ,GAAEnB,kBAAAA,MAAMmB,QAAQ,YAAdnB,kBAAkB;QAC5BoB,WAAW,GAAEpB,qBAAAA,MAAMoB,WAAW,YAAjBpB,qBAAqB;QAClCqB,WAAW,GAAErB,SAAAA,qBAAAA,MAAMqB,WAAW,YAAjBrB,qBAAqBA,MAAMoB,WAAW,YAAtCpB,QAA0C;IACzD;AACF;AAIA,iEAAiE,GACjE,OAAO,eAAesB,eAAevC,MAAc;IACjD,OAAOxB,eAAewB,QAAQ,CAAC,CAAC,EAAEA,OAAO,aAAa,CAAC;AACzD;AAEA,oEAAoE,GACpE,OAAO,SAASwC,iBAAiBC,IAAY,EAAEC,KAAa,EAAEC,IAAY;IACxE,MAAM,CAACC,KAAK,GAAGH,KAAKI,KAAK,CAAC;IAC1B,MAAMC,SAASF,KAAKG,QAAQ,CAAC,OAAO,MAAM;IAC1C,OAAO,GAAGH,OAAOE,SAASJ,MAAM,CAAC,EAAEC,MAAM;AAC3C;AAGA,OAAO,MAAMK,qBAAuC,OAAOC,KAAKC;QAGxCC,cAKAA;IAPtB,IAAIF,IAAIG,MAAM,KAAK,QAAQ,OAAOF,IAAI/B,MAAM,CAAC,KAAKkC,IAAI,CAAC;QAAEC,OAAO;IAAqB;IACrF,MAAMH,OAAO3D,eAAeyD;IAC5B,MAAMjD,SAASuD,QAAOJ,eAAAA,IAAI,CAAC,SAAS,YAAdA,eAAkB;IACxC,MAAMK,OAAO,MAAMlE,kBAAkB2D,KAAKjD;IAC1C,IAAI,WAAWwD,MAAM,OAAON,IAAI/B,MAAM,CAACqC,KAAKrC,MAAM,EAAEkC,IAAI,CAAC;QAAEC,OAAOE,KAAKF,KAAK;IAAC;IAC7E,MAAMG,QAAQD,KAAKC,KAAK;IACxB,MAAMC,SAAS/D;IACf,MAAMgE,SAASJ,QAAOJ,eAAAA,IAAI,CAAC,SAAS,YAAdA,eAAkB;IACxC,IAAI;QACF,OAAQQ;YACN,KAAK;gBACH,IAAI,CAACD,QAAQ,OAAOR,IAAI/B,MAAM,CAAC,KAAKkC,IAAI,CAAC;oBAAEO,WAAW;gBAAM;gBAC5D,OAAOV,IAAI/B,MAAM,CAAC,KAAKkC,IAAI,CAAC;oBAC1BO,WAAW;oBACXC,QAAQ1E,kBAAkBuE;oBAC1B5B,UAAU4B,OAAO5B,QAAQ,CAACT,GAAG,CAAC,CAACyC,SAAY,CAAA;4BACzCV,QAAQU,OAAOV,MAAM;4BACrBnB,OAAO/C,4BAA4B,CAAC4E,OAAOV,MAAM,CAAC;4BAClDrB,OAAO+B,OAAO/B,KAAK;wBACrB,CAAA;oBACAgC,cAAcL,OAAOK,YAAY;oBACjCC,eAAe/E;gBACjB;YACF,KAAK;gBAAU;oBACb,MAAMgF,WAAW,MAAMxD,kBAAkBgD,MAAM1D,KAAK,EACjDmE,KAAK,CAAC,UAAU,MAAMT,MAAMzD,MAAM,EAClCmE,OAAO,CAAC,eAAe,QACvBC,KAAK,CAAC,IACNC,GAAG;oBACN,OAAOnB,IAAI/B,MAAM,CAAC,KAAKkC,IAAI,CAAC;wBAC1BiB,QAAQL,SAASM,IAAI,CAAClD,GAAG,CAAC,CAACR,MACzBE,0BAA0BwC,OAAO1C,IAAIG,EAAE,GAAGH,IAAI2D,IAAI;oBAEtD;gBACF;YACA,KAAK;gBAAY;wBAeS;wBAsDLC;oBApEnB,IAAI,CAACf,QAAQ,OAAOR,IAAI/B,MAAM,CAAC,KAAKkC,IAAI,CAAC;wBAAEC,OAAO;oBAAsC;oBACxF,MAAMrD,aAAaV,kBAAkB0D;oBACrC,IAAI,CAAChD,YAAY,OAAOiD,IAAI/B,MAAM,CAAC,KAAKkC,IAAI,CAAC;wBAAEC,OAAO;oBAA0B;oBAChF,IAAI,CAACG,MAAM1D,KAAK,EAAE,OAAOmD,IAAI/B,MAAM,CAAC,KAAKkC,IAAI,CAAC;wBAAEC,OAAO;oBAA8B;oBACrF,MAAMoB,SAASrF,qBAAqBqE,QAAQP,IAAI,CAAC,QAAQ;oBACzD,IAAI,WAAWuB,QAAQ,OAAOxB,IAAI/B,MAAM,CAAC,KAAKkC,IAAI,CAAC;wBAAEC,OAAOoB,OAAOpB,KAAK;oBAAC;oBACzE,MAAMF,SAASD,IAAI,CAAC,iBAAiB;oBACrC,MAAMrB,WAAWpD,4BAA4B0E,UACzCM,OAAO5B,QAAQ,CAAC6C,IAAI,CAAC,CAACb,SAAWA,OAAOV,MAAM,KAAKA,UACnDwB;oBACJ,IAAI,CAAC9C,UAAU,OAAOoB,IAAI/B,MAAM,CAAC,KAAKkC,IAAI,CAAC;wBAAEC,OAAO;oBAA2B;oBAE/E,MAAMhD,UAAUR,mBAAmB2D,MAAM1D,KAAK,EAAE0D,MAAMzD,MAAM,EAAEC;oBAC9D,MAAM4E,UAAU,MAAMtG,eAAekF,MAAM1D,KAAK,EAAE+E,KAAK,CAAC,IAAO,CAAA,CAAC,CAAA;oBAChE,MAAMC,WAAWxB,QAAO,4BAAA,AAACsB,QAA2CG,gBAAgB,YAA5D,4BAAgE,IAAIC,IAAI;oBAChG,MAAMxC,OAAO,MAAMF,eAAekB,MAAMzD,MAAM;oBAC9C,MAAMkF,SAAoD;wBACxDC,MAAM;wBACNC,qBAAqB9E;wBACrB+E,aAAa7C,iBAAiBC,MAAM,wBAAwB9D;wBAC5D2G,YAAY9C,iBAAiBC,MAAM,0BAA0B9D;wBAC7D,2BAA2B;wBAC3B,qDAAqDM,2BAA2B,CAAC,EAAE;wBACnF,oCAAoC;wBACpC,iDAAiD;wBACjD,yDAAyDC,4BAA4B,CAAC4C,SAASsB,MAAM,CAAC;wBACtG,iEAAiEtB,SAASC,KAAK;wBAC/E,mEAAmElD;wBACnE,kBAAkBC;wBAClB,qBAAqBwB;wBACrB,mBAAmBmD,MAAM1D,KAAK;wBAC9B,oBAAoB0D,MAAMzD,MAAM;wBAChC,iBAAiByD,MAAM8B,GAAG;wBAC1B,mBAAmB9G,uBAAuBiG,OAAOtD,KAAK;wBACtD,4BAA4BU,SAASsB,MAAM;wBAC3C,2BAA2BtB,SAASC,KAAK;wBACzC,2BAA2B2C,OAAO9C,aAAa;wBAC/C,oCAAoC,CAAC,kBAAkB,EAAEvB,uBAAuBC,UAAU;wBAC1F,uCAAuCxB;wBACvC,0CAA0CwB;wBAC1C,wCAAwCmD,MAAM1D,KAAK;uBAC/CgF,WAAW;wBAAEA;oBAAS,IAAI,CAAC,GAC3BrB,OAAOK,YAAY,GACnB;wBACE,0BAA0B;wBAC1B,yDAAyD;uBAGrDgB,WAAW;wBAAE,6BAA6B;oBAAO,IAAI,CAAC,KAE5D,CAAC;oBAEPL,OAAOtD,KAAK,CAACoE,OAAO,CAAC,CAAClE,MAAMmE;wBAC1B,MAAMlE,MAAMnC,eAAekC,KAAKC,GAAG;wBACnC2D,MAAM,CAAC,CAAC,WAAW,EAAEO,MAAM,WAAW,CAAC,CAAC,GAAGnE,KAAKG,QAAQ;wBACxD,uEAAuE;wBACvE,2EAA2E;wBAC3EyD,MAAM,CAAC,CAAC,WAAW,EAAEO,MAAM,uBAAuB,CAAC,CAAC,GAAG;wBACvDP,MAAM,CAAC,CAAC,WAAW,EAAEO,MAAM,0BAA0B,CAAC,CAAC,GAAGnE,KAAKI,SAAS;wBACxEwD,MAAM,CAAC,CAAC,WAAW,EAAEO,MAAM,iCAAiC,CAAC,CAAC,GAAGnE,KAAKE,IAAI;wBAC1E,IAAID,KAAK2D,MAAM,CAAC,CAAC,WAAW,EAAEO,MAAM,wCAAwC,CAAC,CAAC,GAAGlE,IAAImE,GAAG;wBACxFR,MAAM,CAAC,CAAC,WAAW,EAAEO,MAAM,0CAA0C,CAAC,CAAC,GAAGnE,KAAKC,GAAG;wBAClF,IAAImC,OAAOK,YAAY,EAAEmB,MAAM,CAAC,CAAC,WAAW,EAAEO,MAAM,2BAA2B,CAAC,CAAC,GAAG;oBACtF;oBACA,MAAMhB,UAAU,MAAMhF,UAAU,QAAQ,qBAAqB;wBAC3DyF;wBACAS,gBAAgB,CAAC,gBAAgB,EAAErF,SAAS;oBAC9C;oBACA,MAAMsF,MAAMrC,gBAAOkB,gBAAAA,QAAQtB,IAAI,qBAAZsB,cAAcmB,GAAG,mBAAI;oBACxC,IAAI,CAACnB,QAAQoB,EAAE,IAAI,CAACD,KAAK;4BAC0CnB,qBAAAA;wBAAjEqB,QAAQxC,KAAK,CAAC,mCAAmCmB,QAAQtD,MAAM,GAAEsD,iBAAAA,QAAQtB,IAAI,sBAAZsB,sBAAAA,eAAcnB,KAAK,qBAAnBmB,oBAAqBsB,IAAI;wBAC1F,OAAO7C,IAAI/B,MAAM,CAAC,KAAKkC,IAAI,CAAC;4BAAEC,OAAO;wBAAwD;oBAC/F;oBACA,OAAOJ,IAAI/B,MAAM,CAAC,KAAKkC,IAAI,CAAC;wBAAEuC;wBAAKtF;oBAAQ;gBAC7C;YACA;gBACE,OAAO4C,IAAI/B,MAAM,CAAC,KAAKkC,IAAI,CAAC;oBAAEC,OAAO;gBAAiB;QAC1D;IACF,EAAE,OAAOA,OAAO;QACdwC,QAAQxC,KAAK,CAAC,kBAAkBK,QAAQL;QACxC,OAAOJ,IAAI/B,MAAM,CAAC,KAAKkC,IAAI,CAAC;YAAEC,OAAO;QAAqC;IAC5E;AACF,EAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { BillingWebhookHandler } from '@aglyn/aglyn/server';
18
+ /** What the site keeps about one offline card's PaymentIntent. */
19
+ export interface PosOfflineCardChargeRecord {
20
+ saleKey: string;
21
+ paymentIntentId: string;
22
+ status: string;
23
+ amountCents: number;
24
+ registerId: string;
25
+ livemode: boolean;
26
+ /** Whether the sale has synced (its order exists). */
27
+ synced: boolean;
28
+ lastEvent: string;
29
+ updatedAtMs: number;
30
+ }
31
+ export declare function reconcilePosOfflineCardEvent(input: {
32
+ type: string;
33
+ object: any;
34
+ }, firestore?: FirebaseFirestore.Firestore, now?: () => number): Promise<{
35
+ hostId: string;
36
+ } | null>;
37
+ /** The platform webhook's handler for offline card intents: claims only its own. */
38
+ export declare const posOfflineCardBillingWebhookHandler: BillingWebhookHandler;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
17
+ import { POS_OFFLINE_CARD_INTENT_KIND, posOfflineSaleKeyIsValid } from "../model/commerce-pos-offline.js";
18
+ export async function reconcilePosOfflineCardEvent(input, firestore, now = Date.now) {
19
+ var _input_object, _intent_metadata, _metadata_hostId, _metadata_saleKey, _intent_id, _intent_status, _intent_amount, _metadata_registerId;
20
+ if (!input.type.startsWith('payment_intent.')) return null;
21
+ const intent = (_input_object = input.object) != null ? _input_object : {};
22
+ const metadata = (_intent_metadata = intent.metadata) != null ? _intent_metadata : {};
23
+ if (metadata['kind'] !== POS_OFFLINE_CARD_INTENT_KIND) return null;
24
+ const hostId = String((_metadata_hostId = metadata['hostId']) != null ? _metadata_hostId : '');
25
+ const saleKey = String((_metadata_saleKey = metadata['saleKey']) != null ? _metadata_saleKey : '');
26
+ if (!/^[A-Za-z0-9_-]{1,128}$/.test(hostId) || !posOfflineSaleKeyIsValid(saleKey)) return null;
27
+ const hostRef = (firestore != null ? firestore : firebaseAdmin.app().firestore()).collection('hosts').doc(hostId);
28
+ const synced = (await hostRef.collection('posOfflineSales').doc(saleKey).get()).exists;
29
+ const record = {
30
+ saleKey,
31
+ paymentIntentId: String((_intent_id = intent.id) != null ? _intent_id : ''),
32
+ status: String((_intent_status = intent.status) != null ? _intent_status : ''),
33
+ amountCents: Number((_intent_amount = intent.amount) != null ? _intent_amount : 0),
34
+ registerId: String((_metadata_registerId = metadata['registerId']) != null ? _metadata_registerId : '').slice(0, 128),
35
+ livemode: Boolean(intent.livemode),
36
+ synced,
37
+ lastEvent: input.type,
38
+ updatedAtMs: now()
39
+ };
40
+ await hostRef.collection('terminal').doc('config').collection('offlineCardCharges').doc(saleKey).set(record, {
41
+ merge: true
42
+ });
43
+ return {
44
+ hostId
45
+ };
46
+ }
47
+ /** The platform webhook's handler for offline card intents: claims only its own. */ export const posOfflineCardBillingWebhookHandler = async ({ type, object })=>{
48
+ const reconciled = await reconcilePosOfflineCardEvent({
49
+ type,
50
+ object
51
+ });
52
+ return reconciled ? {
53
+ claimed: true,
54
+ hostId: reconciled.hostId
55
+ } : undefined;
56
+ };
57
+
58
+ //# sourceMappingURL=pos-offline-card-reconcile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/pos-offline-card-reconcile.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 type { BillingWebhookHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport {\n POS_OFFLINE_CARD_INTENT_KIND,\n posOfflineSaleKeyIsValid,\n} from '../model/commerce-pos-offline'\n\n/*==========================================\n * A CARD TAKEN OFFLINE, AS STRIPE REPORTS IT (AGL-4039).\n *\n * The Aglyn POS app creates the PaymentIntent of a card it takes offline, so\n * Stripe can know of a charge before the sale itself reaches the server: the\n * SDK forwards the card the moment a connection returns, and the sale syncs\n * right after, from the same device. Usually seconds apart. If the sale never\n * arrives (the device was lost or wiped between the two), the charge must not\n * be invisible: each event on such an intent is kept on the site, by the\n * sale's key, under the site's server-only Terminal document\n * (`terminal/config/offlineCardCharges/{saleKey}`), beside whether the sale\n * has synced. The sync itself never depends on this: it reads Stripe.\n *\n * Self-selecting: every other event returns `null` untouched. Registered on\n * the platform webhook from the commerce server module, beside the other\n * commerce handlers, rather than as a branch of the storefront sections.\n *=========================================*/\n\n/** What the site keeps about one offline card's PaymentIntent. */\nexport interface PosOfflineCardChargeRecord {\n saleKey: string\n paymentIntentId: string\n status: string\n amountCents: number\n registerId: string\n livemode: boolean\n /** Whether the sale has synced (its order exists). */\n synced: boolean\n lastEvent: string\n updatedAtMs: number\n}\n\nexport async function reconcilePosOfflineCardEvent(\n input: { type: string; object: any },\n firestore?: FirebaseFirestore.Firestore,\n now: () => number = Date.now,\n): Promise<{ hostId: string } | null> {\n if (!input.type.startsWith('payment_intent.')) return null\n const intent = input.object ?? {}\n const metadata = (intent.metadata ?? {}) as Record<string, string>\n if (metadata['kind'] !== POS_OFFLINE_CARD_INTENT_KIND) return null\n const hostId = String(metadata['hostId'] ?? '')\n const saleKey = String(metadata['saleKey'] ?? '')\n if (\n !/^[A-Za-z0-9_-]{1,128}$/.test(hostId) ||\n !posOfflineSaleKeyIsValid(saleKey)\n )\n return null\n const hostRef = (firestore ?? firebaseAdmin.app().firestore())\n .collection('hosts')\n .doc(hostId)\n const synced = (\n await hostRef.collection('posOfflineSales').doc(saleKey).get()\n ).exists\n const record: PosOfflineCardChargeRecord = {\n saleKey,\n paymentIntentId: String(intent.id ?? ''),\n status: String(intent.status ?? ''),\n amountCents: Number(intent.amount ?? 0),\n registerId: String(metadata['registerId'] ?? '').slice(0, 128),\n livemode: Boolean(intent.livemode),\n synced,\n lastEvent: input.type,\n updatedAtMs: now(),\n }\n await hostRef\n .collection('terminal')\n .doc('config')\n .collection('offlineCardCharges')\n .doc(saleKey)\n .set(record, { merge: true })\n return { hostId }\n}\n\n/** The platform webhook's handler for offline card intents: claims only its own. */\nexport const posOfflineCardBillingWebhookHandler: BillingWebhookHandler =\n async ({ type, object }) => {\n const reconciled = await reconcilePosOfflineCardEvent({ type, object })\n return reconciled ? { claimed: true, hostId: reconciled.hostId } : undefined\n }\n"],"names":["firebaseAdmin","POS_OFFLINE_CARD_INTENT_KIND","posOfflineSaleKeyIsValid","reconcilePosOfflineCardEvent","input","firestore","now","Date","intent","metadata","type","startsWith","object","hostId","String","saleKey","test","hostRef","app","collection","doc","synced","get","exists","record","paymentIntentId","id","status","amountCents","Number","amount","registerId","slice","livemode","Boolean","lastEvent","updatedAtMs","set","merge","posOfflineCardBillingWebhookHandler","reconciled","claimed","undefined"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAASA,aAAa,QAAQ,2BAA0B;AACxD,SACEC,4BAA4B,EAC5BC,wBAAwB,QACnB,mCAA+B;AAkCtC,OAAO,eAAeC,6BACpBC,KAAoC,EACpCC,SAAuC,EACvCC,MAAoBC,KAAKD,GAAG;QAGbF,eACGI,kBAEIC,kBACCA,mBAcGD,YACTA,gBACKA,gBACDC;IAtBrB,IAAI,CAACL,MAAMM,IAAI,CAACC,UAAU,CAAC,oBAAoB,OAAO;IACtD,MAAMH,UAASJ,gBAAAA,MAAMQ,MAAM,YAAZR,gBAAgB,CAAC;IAChC,MAAMK,YAAYD,mBAAAA,OAAOC,QAAQ,YAAfD,mBAAmB,CAAC;IACtC,IAAIC,QAAQ,CAAC,OAAO,KAAKR,8BAA8B,OAAO;IAC9D,MAAMY,SAASC,QAAOL,mBAAAA,QAAQ,CAAC,SAAS,YAAlBA,mBAAsB;IAC5C,MAAMM,UAAUD,QAAOL,oBAAAA,QAAQ,CAAC,UAAU,YAAnBA,oBAAuB;IAC9C,IACE,CAAC,yBAAyBO,IAAI,CAACH,WAC/B,CAACX,yBAAyBa,UAE1B,OAAO;IACT,MAAME,UAAU,CAACZ,oBAAAA,YAAaL,cAAckB,GAAG,GAAGb,SAAS,IACxDc,UAAU,CAAC,SACXC,GAAG,CAACP;IACP,MAAMQ,SAAS,AACb,CAAA,MAAMJ,QAAQE,UAAU,CAAC,mBAAmBC,GAAG,CAACL,SAASO,GAAG,EAAC,EAC7DC,MAAM;IACR,MAAMC,SAAqC;QACzCT;QACAU,iBAAiBX,QAAON,aAAAA,OAAOkB,EAAE,YAATlB,aAAa;QACrCmB,QAAQb,QAAON,iBAAAA,OAAOmB,MAAM,YAAbnB,iBAAiB;QAChCoB,aAAaC,QAAOrB,iBAAAA,OAAOsB,MAAM,YAAbtB,iBAAiB;QACrCuB,YAAYjB,QAAOL,uBAAAA,QAAQ,CAAC,aAAa,YAAtBA,uBAA0B,IAAIuB,KAAK,CAAC,GAAG;QAC1DC,UAAUC,QAAQ1B,OAAOyB,QAAQ;QACjCZ;QACAc,WAAW/B,MAAMM,IAAI;QACrB0B,aAAa9B;IACf;IACA,MAAMW,QACHE,UAAU,CAAC,YACXC,GAAG,CAAC,UACJD,UAAU,CAAC,sBACXC,GAAG,CAACL,SACJsB,GAAG,CAACb,QAAQ;QAAEc,OAAO;IAAK;IAC7B,OAAO;QAAEzB;IAAO;AAClB;AAEA,kFAAkF,GAClF,OAAO,MAAM0B,sCACX,OAAO,EAAE7B,IAAI,EAAEE,MAAM,EAAE;IACrB,MAAM4B,aAAa,MAAMrC,6BAA6B;QAAEO;QAAME;IAAO;IACrE,OAAO4B,aAAa;QAAEC,SAAS;QAAM5B,QAAQ2B,WAAW3B,MAAM;IAAC,IAAI6B;AACrE,EAAC"}
@@ -19,6 +19,7 @@ import type { PluginApiHandler, PluginApiRequest } from '@aglyn/aglyn/server';
19
19
  import * as CommerceModel from '../model';
20
20
  import { type PosOfflineFlag, type PosOfflineKit, type PosOfflineStockConflict } from '../model/commerce-pos-offline';
21
21
  import { type PosOpsDeps, type PosOpsStaff } from './pos-ops-gate';
22
+ import { type PosOfflineCardIntent } from './pos-terminal';
22
23
  import { type StockDecrementOutcome } from './reserve-stock';
23
24
  /** The register's sync record: no customer data, only what dedupe and resume need. */
24
25
  export interface PosOfflineSyncRecord {
@@ -36,6 +37,8 @@ export interface PosOfflineSyncRecord {
36
37
  stockLines: number[];
37
38
  stockDone: boolean;
38
39
  notified: boolean;
40
+ /** A card taken offline that was declined or never charged: no receipt and no "paid" for it. */
41
+ unpaid?: boolean;
39
42
  }
40
43
  export interface PosOfflineAfterSale {
41
44
  hostId: string;
@@ -61,7 +64,36 @@ export interface PosOfflineSyncDeps extends PosOpsDeps {
61
64
  }): Promise<StockDecrementOutcome>;
62
65
  /** What a completed sale tells the rest of the workspace. Never throws. */
63
66
  afterSale(input: PosOfflineAfterSale): Promise<void>;
67
+ /** Cards taken offline on the app's own reader (AGL-4039); absent, the server takes none. */
68
+ card?: PosOfflineCardDeps;
64
69
  }
70
+ /** The Stripe side of a card taken offline on the app's own reader. */
71
+ export interface PosOfflineCardDeps {
72
+ /** Whether Terminal readers are offered at all, and the site's Location once one exists. */
73
+ terminal(hostId: string): Promise<{
74
+ available: boolean;
75
+ locationId: string | null;
76
+ }>;
77
+ /** The merchant's connected account, when it can take card payments. */
78
+ merchantAccount(hostId: string, org: Record<string, any> | null): Promise<string | null>;
79
+ /** Turns Stripe's offline mode on for the site's readers; false when Stripe refused. */
80
+ enableOffline(hostId: string): Promise<boolean>;
81
+ retrieve(paymentIntentId: string): Promise<{
82
+ ok: true;
83
+ intent: PosOfflineCardIntent;
84
+ } | {
85
+ ok: false;
86
+ error: string;
87
+ }>;
88
+ capture(paymentIntentId: string, applicationFeeCents: number, idempotencyKey: string): Promise<{
89
+ ok: true;
90
+ intent: PosOfflineCardIntent;
91
+ } | {
92
+ ok: false;
93
+ error: string;
94
+ }>;
95
+ }
96
+ export declare function defaultPosOfflineCardDeps(): PosOfflineCardDeps;
65
97
  export declare function defaultPosOfflineSyncDeps(): PosOfflineSyncDeps;
66
98
  type Outcome = {
67
99
  status: number;
@@ -18,16 +18,30 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import recordCapturedContact from "@aglyn/aglyn/plugin-manager/record-captured-contact";
19
19
  import { FieldValue } from "firebase-admin/firestore";
20
20
  import * as CommerceModel from "../model/index.js";
21
- import { posOfflineFlagLabels, posOfflineSaleTotals, posOfflineSoldAt, posOfflineTotalsAddUp, POS_OFFLINE_LATE_SYNC_MS, POS_OFFLINE_SYNC_BATCH_MAX, sanitizePosOfflineSale } from "../model/commerce-pos-offline.js";
21
+ import { posOfflineFlagLabels, posOfflineSaleTotals, posOfflineSoldAt, posOfflineTotalsAddUp, POS_OFFLINE_CARD_INTENT_KIND, POS_OFFLINE_LATE_SYNC_MS, POS_OFFLINE_SYNC_BATCH_MAX, sanitizePosOfflineSale } from "../model/commerce-pos-offline.js";
22
22
  import { ORDER_PAID_EVENT } from "../model/order-events.js";
23
- import { posMaxDiscountPct } from "../plugin-config.js";
23
+ import { posMaxDiscountPct, posOfflineCardSettings } from "../plugin-config.js";
24
24
  import { alertLowStockCrossing } from "./low-stock.js";
25
25
  import { notifyOrderBuyer } from "./order-notifications.js";
26
26
  import { raiseOrderEvent } from "./order-events.js";
27
27
  import { offlineFeeMonthKey } from "./pos-fee-month.js";
28
28
  import { authorizePosOps, defaultPosOpsDeps, posOpsBody, posOpsCleanId, resolvePosStaffAssertion } from "./pos-ops-gate.js";
29
29
  import { notifyPosSaleCompleted } from "./pos-sale.js";
30
+ import { POS_CURRENCY, posCardApplicationFeeCents, posTerminalAvailable } from "./pos-stripe.js";
31
+ import { capturePosOfflineCardIntent, ensurePosOfflineCardConfiguration, posMerchantAccount, posTerminalLocationId, retrievePosOfflineCardIntent } from "./pos-terminal.js";
30
32
  import { decrementVariantStock } from "./reserve-stock.js";
33
+ export function defaultPosOfflineCardDeps() {
34
+ return {
35
+ terminal: async (hostId)=>({
36
+ available: posTerminalAvailable(),
37
+ locationId: posTerminalAvailable() ? await posTerminalLocationId(hostId) : null
38
+ }),
39
+ merchantAccount: posMerchantAccount,
40
+ enableOffline: ensurePosOfflineCardConfiguration,
41
+ retrieve: retrievePosOfflineCardIntent,
42
+ capture: capturePosOfflineCardIntent
43
+ };
44
+ }
31
45
  export function defaultPosOfflineSyncDeps() {
32
46
  const base = defaultPosOpsDeps();
33
47
  return _extends({}, base, {
@@ -104,7 +118,8 @@ export function defaultPosOfflineSyncDeps() {
104
118
  orderId: input.orderId,
105
119
  key: 'paid'
106
120
  }));
107
- }
121
+ },
122
+ card: defaultPosOfflineCardDeps()
108
123
  });
109
124
  }
110
125
  const money = (cents)=>`$${(Math.max(0, cents) / 100).toFixed(2)}`;
@@ -168,6 +183,7 @@ async function readStoreTax(staff) {
168
183
  staff.hostRef.collection('settings').doc('store').get()
169
184
  ]);
170
185
  const tax = posInPersonTax(taxSettings);
186
+ const card = await buildPosOfflineKitCard(deps, staff, config);
171
187
  return _extends({
172
188
  v: 1,
173
189
  hostId: staff.hostId,
@@ -198,7 +214,10 @@ async function readStoreTax(staff) {
198
214
  } : {}, {
199
215
  openShiftId: posOpsCleanId(register.data['openShiftId']) || null
200
216
  });
201
- }),
217
+ })
218
+ }, card ? {
219
+ card
220
+ } : {}, {
202
221
  receipt: _extends({
203
222
  name: String((_host_get = host.get('displayName')) != null ? _host_get : '') || 'Receipt'
204
223
  }, typeof host.get('logoUrl') === 'string' && host.get('logoUrl') ? {
@@ -212,6 +231,107 @@ async function readStoreTax(staff) {
212
231
  } : {})
213
232
  });
214
233
  }
234
+ /**
235
+ * What the app needs to take a card offline on its own reader (AGL-4039), or
236
+ * why it cannot. Absent where this server takes no offline cards.
237
+ */ async function buildPosOfflineKitCard(deps, staff, config) {
238
+ if (!deps.card) return undefined;
239
+ const settings = posOfflineCardSettings(config);
240
+ const base = {
241
+ destination: '',
242
+ currency: POS_CURRENCY,
243
+ maxSaleCents: settings.maxSaleCents,
244
+ maxDeviceDailyCents: settings.maxDeviceDailyCents
245
+ };
246
+ const off = (unavailableReason)=>_extends({}, base, {
247
+ available: false,
248
+ unavailableReason
249
+ });
250
+ if (!settings.enabled) return off('Card sales offline are not turned on in this store’s point of sale settings.');
251
+ const terminal = await deps.card.terminal(staff.hostId);
252
+ if (!terminal.available) return off('Card readers are not set up for this store yet.');
253
+ if (!terminal.locationId) return off('Connect a card reader once while online, then cards keep working offline.');
254
+ const destination = await deps.card.merchantAccount(staff.hostId, staff.org);
255
+ if (!destination) return off('Finish connecting Stripe to take cards.');
256
+ if (!await deps.card.enableOffline(staff.hostId)) {
257
+ return off('Card sales offline could not be turned on for this store’s readers yet. Try again later.');
258
+ }
259
+ return _extends({}, base, {
260
+ destination,
261
+ available: true
262
+ });
263
+ }
264
+ /**
265
+ * Reads the card of a sale rung offline and settles it: captured with the
266
+ * platform fee (the card tender's share of the take, by the one tender rule,
267
+ * plus Stripe's processing cost, exactly as an online reader payment), or
268
+ * unpaid when the bank declined it or Stripe never took it. Anything Stripe
269
+ * has not finished yet waits on the register for the next sync.
270
+ */ async function settleOfflineCard(context, sale, takeFee) {
271
+ const { deps, staff } = context;
272
+ const { saleKey } = sale;
273
+ const card = sale.card;
274
+ const wait = (error)=>({
275
+ kind: 'wait',
276
+ outcome: refused(saleKey, 'failed', error, true)
277
+ });
278
+ if (card.forward === 'stored') return wait('The card is still on this register. It syncs once Stripe has it.');
279
+ if (!deps.card) return wait('Card sales taken offline cannot sync to this store yet.');
280
+ const details = _extends({}, card.brand ? {
281
+ brand: card.brand
282
+ } : {}, card.last4 ? {
283
+ last4: card.last4
284
+ } : {});
285
+ if (!card.paymentIntentId) {
286
+ return _extends({
287
+ kind: 'unpaid',
288
+ flag: card.forward === 'declined' ? 'card-declined' : 'card-not-charged',
289
+ message: card.declineMessage || 'Stripe did not take the card.'
290
+ }, details);
291
+ }
292
+ const read = await deps.card.retrieve(card.paymentIntentId);
293
+ if ('error' in read) return wait(`The card payment could not be checked yet: ${read.error}`);
294
+ let intent = read.intent;
295
+ if (intent.metadata['kind'] !== POS_OFFLINE_CARD_INTENT_KIND || intent.metadata['saleKey'] !== saleKey || intent.metadata['hostId'] !== staff.hostId || intent.amount !== sale.totals.totalCents || intent.currency !== POS_CURRENCY) {
296
+ return {
297
+ kind: 'wait',
298
+ outcome: refused(saleKey, 'invalid', 'The card payment does not belong to this sale.', false)
299
+ };
300
+ }
301
+ const found = _extends({}, details, intent.brand ? {
302
+ brand: intent.brand
303
+ } : {}, intent.last4 ? {
304
+ last4: intent.last4
305
+ } : {});
306
+ const takeShareCents = CommerceModel.posTenderTakeCents('card_present', takeFee);
307
+ if (intent.status === 'requires_capture') {
308
+ const feeCents = posCardApplicationFeeCents({
309
+ takeShareCents,
310
+ chargeCents: intent.amount
311
+ });
312
+ const captured = await deps.card.capture(intent.id, feeCents, `pos-offline-capture:${staff.hostId}:${saleKey}`);
313
+ if ('error' in captured) return wait(`The card payment could not be captured yet: ${captured.error}`);
314
+ intent = captured.intent;
315
+ }
316
+ if (intent.status === 'succeeded') {
317
+ return _extends({
318
+ kind: 'captured',
319
+ intentId: intent.id,
320
+ feeCents: intent.applicationFeeCents,
321
+ takeShareCents,
322
+ livemode: intent.livemode
323
+ }, found);
324
+ }
325
+ if (intent.status === 'requires_payment_method' || intent.status === 'canceled') {
326
+ return _extends({
327
+ kind: 'unpaid',
328
+ flag: 'card-declined',
329
+ intentId: intent.id,
330
+ message: intent.failureMessage || card.declineMessage || 'The card was declined.'
331
+ }, found);
332
+ }
333
+ return wait('The card payment is still processing. It syncs when Stripe finishes.');
334
+ }
215
335
  const refused = (saleKey, reason, error, retry)=>({
216
336
  saleKey,
217
337
  status: 'refused',
@@ -330,6 +450,18 @@ const replayOf = (record)=>({
330
450
  }
331
451
  if (!stockDone) return current;
332
452
  }
453
+ if (!current.notified && current.unpaid) {
454
+ // A card that was never paid gets no receipt and no "paid": the order
455
+ // waits, flagged, for the merchant to collect it another way.
456
+ current = _extends({}, current, {
457
+ notified: true,
458
+ status: 'settled'
459
+ });
460
+ await recordRef.update({
461
+ notified: true,
462
+ status: 'settled'
463
+ });
464
+ }
333
465
  if (!current.notified) {
334
466
  var _order_customerEmail, _order_customerName;
335
467
  const email = String((_order_customerEmail = order.customerEmail) != null ? _order_customerEmail : '');
@@ -486,7 +618,8 @@ async function syncOne(context, raw) {
486
618
  flags.add('tax-differs');
487
619
  }
488
620
  if (sale.discountPct > posMaxDiscountPct(context.config)) flags.add('discount-over-limit');
489
- if (sale.cashTenderedCents < totals.totalCents) flags.add('cash-short');
621
+ const byCard = sale.tender === 'card';
622
+ if (!byCard && sale.cashTenderedCents < totals.totalCents) flags.add('cash-short');
490
623
  // Where it was rung.
491
624
  const register = context.registers.get(sale.registerId);
492
625
  if (!register) flags.add('unknown-register');
@@ -515,11 +648,22 @@ async function syncOne(context, raw) {
515
648
  if (memberUid) cashierId = memberUid;
516
649
  else flags.add('cashier-unverified');
517
650
  }
518
- const takeFee = takeFeeCents(deps, staff, lineItems, totals);
651
+ // The offline register takes cash only, and cash carries no platform fee
652
+ // (AGL-4036): the plan's take is worked out and then settled by the one
653
+ // predicate every tender answers to, so a sale synced from the queue is
654
+ // charged exactly what the same sale rung online is.
655
+ const planTake = takeFeeCents(deps, staff, lineItems, totals);
656
+ // A card taken offline settles with Stripe BEFORE the order is written: a
657
+ // capture that fails leaves the sale on the register for the next sync,
658
+ // and a repeat after a lost answer finds the first capture (AGL-4039).
659
+ const settled = byCard ? await settleOfflineCard(context, sale, planTake) : null;
660
+ if ((settled == null ? void 0 : settled.kind) === 'wait') return settled.outcome;
661
+ if ((settled == null ? void 0 : settled.kind) === 'unpaid') flags.add(settled.flag);
662
+ const takeFee = byCard ? (settled == null ? void 0 : settled.kind) === 'captured' ? settled.takeShareCents : 0 : CommerceModel.posTenderTakeCents('cash', planTake);
519
663
  totals = _extends({}, totals, {
520
664
  feeCents: takeFee
521
665
  });
522
- const changeCents = Math.max(0, sale.cashTenderedCents - totals.totalCents);
666
+ const changeCents = byCard ? 0 : Math.max(0, sale.cashTenderedCents - totals.totalCents);
523
667
  const flagList = [
524
668
  ...flags
525
669
  ];
@@ -553,11 +697,15 @@ async function syncOne(context, raw) {
553
697
  }, {
554
698
  merge: true
555
699
  });
556
- const paidDetail = `Cash — received ${money(sale.cashTenderedCents)}, change ${money(changeCents)}. ` + 'Rung while the register was offline.';
700
+ const cardName = settled ? [
701
+ settled.brand ? settled.brand.toUpperCase() : 'Card',
702
+ settled.last4 ? `•••• ${settled.last4}` : ''
703
+ ].filter(Boolean).join(' ') : '';
704
+ const paidDetail = !settled ? `Cash — received ${money(sale.cashTenderedCents)}, change ${money(changeCents)}. ` + 'Rung while the register was offline.' : settled.kind === 'captured' ? `${cardName} — ${money(totals.totalCents)}, taken while the register was offline and charged when it reconnected.` : `${cardName} — ${money(totals.totalCents)} taken while the register was offline was not paid: ${settled.message}`.slice(0, 500);
557
705
  const timeline = [
558
706
  {
559
707
  atMs: soldAt.atMs,
560
- event: 'paid',
708
+ event: (settled == null ? void 0 : settled.kind) === 'unpaid' ? 'payment-failed' : 'paid',
561
709
  detail: paidDetail
562
710
  },
563
711
  {
@@ -568,7 +716,7 @@ async function syncOne(context, raw) {
568
716
  ];
569
717
  transaction.create(orderRef, CommerceModel.withOrderListFields(orderRef.id, _extends({
570
718
  number,
571
- status: 'paid',
719
+ status: (settled == null ? void 0 : settled.kind) === 'unpaid' ? 'pending' : 'paid',
572
720
  channel: 'pos',
573
721
  registerId: sale.registerId,
574
722
  cashierId
@@ -591,9 +739,34 @@ async function syncOne(context, raw) {
591
739
  totals,
592
740
  taxMode: 'decision' in context.tax ? CommerceModel.storefrontTaxModeForDecision(context.tax.decision, totals.taxCents) : totals.taxCents > 0 ? 'manual' : 'none'
593
741
  }, takeFee > 0 ? {
594
- feeCollection: 'invoice'
742
+ feeCollection: settled ? 'payout' : 'invoice'
595
743
  } : {}, {
596
- payments: [
744
+ payments: settled ? [
745
+ _extends({
746
+ id: 'pay_card_offline',
747
+ method: 'card_present',
748
+ amountCents: totals.totalCents,
749
+ status: settled.kind === 'captured' ? 'succeeded' : 'failed',
750
+ atMs: soldAt.atMs
751
+ }, settled.kind === 'captured' ? {
752
+ settledAtMs: now,
753
+ takeFeeCents: settled.takeShareCents,
754
+ feeCents: settled.feeCents,
755
+ livemode: settled.livemode
756
+ } : {
757
+ takeFeeCents: 0,
758
+ feeCents: 0,
759
+ failureMessage: settled.message.slice(0, 300)
760
+ }, settled.intentId ? {
761
+ paymentIntentId: settled.intentId
762
+ } : {}, settled.brand ? {
763
+ cardBrand: settled.brand
764
+ } : {}, settled.last4 ? {
765
+ last4: settled.last4
766
+ } : {}, {
767
+ cashierId
768
+ })
769
+ ] : [
597
770
  {
598
771
  id: 'pay_cash',
599
772
  method: 'cash',
@@ -614,7 +787,7 @@ async function syncOne(context, raw) {
614
787
  createdAtMs: soldAt.atMs,
615
788
  createdAt: FieldValue.serverTimestamp()
616
789
  })));
617
- const record = {
790
+ const record = _extends({
618
791
  saleKey: sale.saleKey,
619
792
  orderId: orderRef.id,
620
793
  number,
@@ -628,10 +801,13 @@ async function syncOne(context, raw) {
628
801
  stockLines: [],
629
802
  stockDone: false,
630
803
  notified: false
631
- };
804
+ }, (settled == null ? void 0 : settled.kind) === 'unpaid' ? {
805
+ unpaid: true
806
+ } : {});
632
807
  transaction.create(recordRef, record);
633
808
  // The take accrues with the order, in the same transaction (AGL-2111).
634
- if (takeFee > 0 && feeRef) {
809
+ // A card's share was netted from its payout by the capture instead.
810
+ if (takeFee > 0 && feeRef && !settled) {
635
811
  transaction.set(feeRef, {
636
812
  month: feeMonth,
637
813
  feeCents: FieldValue.increment(takeFee),