@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.
- package/package.json +17 -14
- package/src/lib/components/commerce-console-page.js +33 -1
- package/src/lib/components/commerce-console-page.js.map +1 -1
- package/src/lib/components/console/payments-settings-card.component.js +1 -1
- package/src/lib/components/console/payments-settings-card.component.js.map +1 -1
- package/src/lib/components/console/pos/pos-hardware-api.d.ts +122 -0
- package/src/lib/components/console/pos/pos-hardware-api.js +90 -0
- package/src/lib/components/console/pos/pos-hardware-api.js.map +1 -0
- package/src/lib/components/console/pos/pos-reader-splash-api.d.ts +58 -0
- package/src/lib/components/console/pos/pos-reader-splash-api.js +101 -0
- package/src/lib/components/console/pos/pos-reader-splash-api.js.map +1 -0
- package/src/lib/components/console/pos-devices-card.component.js +5 -0
- package/src/lib/components/console/pos-devices-card.component.js.map +1 -1
- package/src/lib/components/console/pos-hardware-card.component.d.ts +26 -0
- package/src/lib/components/console/pos-hardware-card.component.js +456 -0
- package/src/lib/components/console/pos-hardware-card.component.js.map +1 -0
- package/src/lib/components/console/pos-hardware-orders-staff-page.component.d.ts +14 -0
- package/src/lib/components/console/pos-hardware-orders-staff-page.component.js +519 -0
- package/src/lib/components/console/pos-hardware-orders-staff-page.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.d.ts +27 -0
- package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.js +444 -0
- package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-check-in-zone.component.d.ts +9 -0
- package/src/lib/components/console/pos-ops/pos-check-in-zone.component.js +37 -0
- package/src/lib/components/console/pos-ops/pos-check-in-zone.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-check-ins-card.component.d.ts +24 -0
- package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js +346 -0
- package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-customer-lookup.component.d.ts +5 -1
- package/src/lib/components/console/pos-ops/pos-customer-lookup.component.js +83 -4
- package/src/lib/components/console/pos-ops/pos-customer-lookup.component.js.map +1 -1
- package/src/lib/components/console/pos-ops/pos-day-totals-card.component.d.ts +35 -0
- package/src/lib/components/console/pos-ops/pos-day-totals-card.component.js +546 -0
- package/src/lib/components/console/pos-ops/pos-day-totals-card.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-operations-bar.component.d.ts +2 -1
- package/src/lib/components/console/pos-ops/pos-operations-bar.component.js +20 -1
- package/src/lib/components/console/pos-ops/pos-operations-bar.component.js.map +1 -1
- package/src/lib/components/console/pos-ops/pos-ops-api.d.ts +1 -1
- package/src/lib/components/console/pos-ops/pos-ops-api.js.map +1 -1
- package/src/lib/components/console/pos-page.component.js +6 -3
- package/src/lib/components/console/pos-page.component.js.map +1 -1
- package/src/lib/components/console/pos-reader-splash-setting.component.d.ts +22 -0
- package/src/lib/components/console/pos-reader-splash-setting.component.js +275 -0
- package/src/lib/components/console/pos-reader-splash-setting.component.js.map +1 -0
- package/src/lib/components/console/pos-reader-splash-staff-page.component.d.ts +14 -0
- package/src/lib/components/console/pos-reader-splash-staff-page.component.js +301 -0
- package/src/lib/components/console/pos-reader-splash-staff-page.component.js.map +1 -0
- package/src/lib/components/console/pos-zones.d.ts +37 -3
- package/src/lib/components/console/pos-zones.js +11 -1
- package/src/lib/components/console/pos-zones.js.map +1 -1
- package/src/lib/components/console/product-check-in-fields.component.d.ts +20 -0
- package/src/lib/components/console/product-check-in-fields.component.js +172 -0
- package/src/lib/components/console/product-check-in-fields.component.js.map +1 -0
- package/src/lib/components/console/product-editor-dialog.component.js +48 -9
- package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/product-register-fields.component.d.ts +12 -0
- package/src/lib/components/console/product-register-fields.component.js +39 -1
- package/src/lib/components/console/product-register-fields.component.js.map +1 -1
- package/src/lib/components/console/products-hub-card.component.js +21 -6
- package/src/lib/components/console/products-hub-card.component.js.map +1 -1
- package/src/lib/components/console/reservations-card.component.js +239 -70
- package/src/lib/components/console/reservations-card.component.js.map +1 -1
- package/src/lib/components/reservation-widget.js +16 -0
- package/src/lib/components/reservation-widget.js.map +1 -1
- package/src/lib/constants/operator-alerts.d.ts +6 -0
- package/src/lib/constants/operator-alerts.js +20 -1
- package/src/lib/constants/operator-alerts.js.map +1 -1
- package/src/lib/declarations.console-server.js +7 -0
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/declarations.d.ts +26 -0
- package/src/lib/declarations.js +34 -0
- package/src/lib/declarations.js.map +1 -0
- package/src/lib/model/commerce-check-in-product.d.ts +37 -0
- package/src/lib/model/commerce-check-in-product.js +49 -0
- package/src/lib/model/commerce-check-in-product.js.map +1 -0
- package/src/lib/model/commerce-check-in.d.ts +187 -0
- package/src/lib/model/commerce-check-in.js +468 -0
- package/src/lib/model/commerce-check-in.js.map +1 -0
- package/src/lib/model/commerce-pos-day-report.d.ts +209 -0
- package/src/lib/model/commerce-pos-day-report.js +572 -0
- package/src/lib/model/commerce-pos-day-report.js.map +1 -0
- package/src/lib/model/commerce-pos-offline.d.ts +65 -3
- package/src/lib/model/commerce-pos-offline.js +104 -7
- package/src/lib/model/commerce-pos-offline.js.map +1 -1
- package/src/lib/model/commerce-pos-ops.d.ts +3 -0
- package/src/lib/model/commerce-pos-ops.js +1 -0
- package/src/lib/model/commerce-pos-ops.js.map +1 -1
- package/src/lib/model/commerce-pos.d.ts +43 -3
- package/src/lib/model/commerce-pos.js +73 -5
- package/src/lib/model/commerce-pos.js.map +1 -1
- package/src/lib/model/commerce-reservations.d.ts +73 -7
- package/src/lib/model/commerce-reservations.js +90 -10
- package/src/lib/model/commerce-reservations.js.map +1 -1
- package/src/lib/model/commerce.d.ts +40 -0
- package/src/lib/model/commerce.js +22 -4
- package/src/lib/model/commerce.js.map +1 -1
- package/src/lib/model/index.d.ts +1 -0
- package/src/lib/model/index.js +1 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/model/plugin-permissions.d.ts +1 -1
- package/src/lib/model/plugin-permissions.js +2 -1
- package/src/lib/model/plugin-permissions.js.map +1 -1
- package/src/lib/model/pos-hardware.d.ts +288 -0
- package/src/lib/model/pos-hardware.js +499 -0
- package/src/lib/model/pos-hardware.js.map +1 -0
- package/src/lib/model/pos-offline-card-limits.d.ts +20 -0
- package/src/lib/model/pos-offline-card-limits.js +28 -0
- package/src/lib/model/pos-offline-card-limits.js.map +1 -0
- package/src/lib/model/pos-reader-splash.d.ts +114 -0
- package/src/lib/model/pos-reader-splash.js +141 -0
- package/src/lib/model/pos-reader-splash.js.map +1 -0
- package/src/lib/model/product-write.d.ts +6 -0
- package/src/lib/plugin-config.d.ts +10 -0
- package/src/lib/plugin-config.js +40 -0
- package/src/lib/plugin-config.js.map +1 -1
- package/src/lib/plugin.js +28 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/cart-checkout.js +3 -1
- package/src/lib/server/cart-checkout.js.map +1 -1
- package/src/lib/server/cart.js +12 -1
- package/src/lib/server/cart.js.map +1 -1
- package/src/lib/server/catalog.js +49 -12
- package/src/lib/server/catalog.js.map +1 -1
- package/src/lib/server/checkout-extras.js +1 -1
- package/src/lib/server/checkout-extras.js.map +1 -1
- package/src/lib/server/checkout.js +7 -0
- package/src/lib/server/checkout.js.map +1 -1
- package/src/lib/server/person-eraser.d.ts +8 -1
- package/src/lib/server/person-eraser.js +23 -1
- package/src/lib/server/person-eraser.js.map +1 -1
- package/src/lib/server/pos-check-in.d.ts +49 -0
- package/src/lib/server/pos-check-in.js +652 -0
- package/src/lib/server/pos-check-in.js.map +1 -0
- package/src/lib/server/pos-day-report.d.ts +46 -0
- package/src/lib/server/pos-day-report.js +299 -0
- package/src/lib/server/pos-day-report.js.map +1 -0
- package/src/lib/server/pos-hardware-admin.d.ts +50 -0
- package/src/lib/server/pos-hardware-admin.js +366 -0
- package/src/lib/server/pos-hardware-admin.js.map +1 -0
- package/src/lib/server/pos-hardware-notify.d.ts +27 -0
- package/src/lib/server/pos-hardware-notify.js +142 -0
- package/src/lib/server/pos-hardware-notify.js.map +1 -0
- package/src/lib/server/pos-hardware-user-eraser.d.ts +26 -0
- package/src/lib/server/pos-hardware-user-eraser.js +67 -0
- package/src/lib/server/pos-hardware-user-eraser.js.map +1 -0
- package/src/lib/server/pos-hardware-webhook.d.ts +31 -0
- package/src/lib/server/pos-hardware-webhook.js +176 -0
- package/src/lib/server/pos-hardware-webhook.js.map +1 -0
- package/src/lib/server/pos-hardware.d.ts +58 -0
- package/src/lib/server/pos-hardware.js +234 -0
- package/src/lib/server/pos-hardware.js.map +1 -0
- package/src/lib/server/pos-offline-card-reconcile.d.ts +38 -0
- package/src/lib/server/pos-offline-card-reconcile.js +58 -0
- package/src/lib/server/pos-offline-card-reconcile.js.map +1 -0
- package/src/lib/server/pos-offline-sync.d.ts +32 -0
- package/src/lib/server/pos-offline-sync.js +191 -15
- package/src/lib/server/pos-offline-sync.js.map +1 -1
- package/src/lib/server/pos-ops-routes.d.ts +4 -2
- package/src/lib/server/pos-ops-routes.js +14 -2
- package/src/lib/server/pos-ops-routes.js.map +1 -1
- package/src/lib/server/pos-order.js +39 -51
- package/src/lib/server/pos-order.js.map +1 -1
- package/src/lib/server/pos-payment.js +8 -4
- package/src/lib/server/pos-payment.js.map +1 -1
- package/src/lib/server/pos-reader-splash-admin.d.ts +36 -0
- package/src/lib/server/pos-reader-splash-admin.js +295 -0
- package/src/lib/server/pos-reader-splash-admin.js.map +1 -0
- package/src/lib/server/pos-reader-splash-render.d.ts +40 -0
- package/src/lib/server/pos-reader-splash-render.js +138 -0
- package/src/lib/server/pos-reader-splash-render.js.map +1 -0
- package/src/lib/server/pos-reader-splash-server.d.ts +32 -0
- package/src/lib/server/pos-reader-splash-server.js +32 -0
- package/src/lib/server/pos-reader-splash-server.js.map +1 -0
- package/src/lib/server/pos-reader-splash.d.ts +105 -0
- package/src/lib/server/pos-reader-splash.js +548 -0
- package/src/lib/server/pos-reader-splash.js.map +1 -0
- package/src/lib/server/pos-sale.d.ts +2 -1
- package/src/lib/server/pos-sale.js +3 -1
- package/src/lib/server/pos-sale.js.map +1 -1
- package/src/lib/server/pos-terminal.d.ts +67 -4
- package/src/lib/server/pos-terminal.js +239 -32
- package/src/lib/server/pos-terminal.js.map +1 -1
- package/src/lib/server/process-restock.js +4 -0
- package/src/lib/server/process-restock.js.map +1 -1
- package/src/lib/server/product-card.js +2 -2
- package/src/lib/server/product-card.js.map +1 -1
- package/src/lib/server/product-catalog.d.ts +2 -0
- package/src/lib/server/product-catalog.js +11 -4
- package/src/lib/server/product-catalog.js.map +1 -1
- package/src/lib/server/product.js +3 -1
- package/src/lib/server/product.js.map +1 -1
- package/src/lib/server/related.js +8 -1
- package/src/lib/server/related.js.map +1 -1
- package/src/lib/server/reservation-availability-query.d.ts +35 -0
- package/src/lib/server/reservation-availability-query.js +34 -0
- package/src/lib/server/reservation-availability-query.js.map +1 -0
- package/src/lib/server/reservation-availability.d.ts +21 -0
- package/src/lib/server/reservation-availability.js +64 -10
- package/src/lib/server/reservation-availability.js.map +1 -1
- package/src/lib/server/reservation-walk-in.d.ts +28 -0
- package/src/lib/server/reservation-walk-in.js +166 -0
- package/src/lib/server/reservation-walk-in.js.map +1 -0
- package/src/lib/server/reserve.js +13 -10
- package/src/lib/server/reserve.js.map +1 -1
- package/src/lib/server/reviews.js +3 -1
- package/src/lib/server/reviews.js.map +1 -1
- package/src/lib/server/site-page-resolver.js +5 -2
- package/src/lib/server/site-page-resolver.js.map +1 -1
- package/src/lib/server.js +26 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/fake-firestore.js +72 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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),
|