@aglyn/plugins-commerce 1.0.0-beta.278 → 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.
- 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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/pos-terminal.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 'node:crypto'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { merchantAccountIsReady } from '@aglyn/tenant-data-admin/server/payment-provider'\nimport {\n cardAuthenticationParams,\n checkoutSessionCardAuthenticationParams,\n} from '@aglyn/tenant-data-admin/server/stripe-card-authentication'\nimport * as CommerceModel from '../model'\nimport type { PosRegisterSettings } from '../plugin-config'\nimport {\n POS_CURRENCY,\n posCardApplicationFeeCents,\n posStripe,\n posStripeErrorMessage,\n posStripeTestMode,\n posTerminalAvailable,\n} from './pos-stripe'\nimport { applyPosPayment, readPosSale, type PosLiftedOrder, type PosPaymentOutcome } from './pos-sale'\n\n/*==========================================\n * STRIPE TERMINAL AND THE REGISTER'S OTHER CARD TENDERS (AGL-3607).\n *\n * ## Where the readers live\n *\n * Every register charge is a DESTINATION charge on the platform account, so\n * Stripe's rule for that model applies: Locations and Readers are created on\n * the PLATFORM with the platform key, and identify their merchant by\n * metadata. One Location per site (`hosts/{hostId}/terminal/config`), and one\n * `hosts/{hostId}/terminalReaders/{readerId}` document per reader, written\n * only here. The document is what makes a reader THIS site's: every call that\n * names a reader reads it under the caller's own site first, so one store can\n * never drive, rename or remove another's reader, and the reader's own\n * `metadata.hostId` is checked again before a payment is sent to it.\n *\n * ## Why the reader charges with MANUAL capture\n *\n * On-reader tipping changes the amount after the PaymentIntent exists: the\n * reader adds the tip on confirmation and reports it in\n * `amount_details.tip.amount`. `application_fee_amount` set at creation was\n * sized on the pre-tip amount, and the platform's take must exclude the tip\n * while Stripe's processing cost (debited from the platform) is on the whole\n * charge. So the reader only AUTHORIZES; {@link refreshPosCardPayment} reads\n * the tip, re-prices the fee as\n *\n * take share (of the sale, never of the tip) + card processing (whole charge)\n *\n * and captures with that exact `application_fee_amount`. Stripe's own Connect\n * guidance for Terminal is the same: inspect, fix the application fee, then\n * capture. An authorization expires after two days, and a register capture\n * happens seconds after the tap.\n *=========================================*/\n\nconst NOT_A_SALE = { ok: false as const, status: 409, error: 'This sale is no longer open' }\n\n/** `hosts/{hostId}/terminalReaders/{readerId}`. */\nexport interface PosTerminalReader {\n label: string\n registerId?: string\n stripeLocationId: string\n deviceType?: string\n serialNumber?: string\n status?: 'online' | 'offline' | string\n livemode: boolean\n createdAtMs: number\n createdBy?: string\n lastSeenAtMs?: number\n}\n\n/**\n * The payment without its failure reason. Removed, never set to `undefined`:\n * Firestore refuses an undefined field inside the `payments` array.\n */\nfunction withoutFailure(payment: CommerceModel.OrderPayment): CommerceModel.OrderPayment {\n const next = { ...payment }\n delete next.failureMessage\n return next\n}\n\n/** The merchant's connected account, when it can take card payments now. */\nexport async function posMerchantAccount(\n hostId: string,\n org: Record<string, any> | null,\n): Promise<string | null> {\n const ownerUid = String(org?.['ownerUid'] ?? '')\n if (!ownerUid) return null\n const profile = await firebaseAdmin.app().firestore().collection('profiles').doc(ownerUid).get()\n const accountId = profile.get('stripeAccountId')\n if (\n !merchantAccountIsReady(\n {\n accountId,\n chargesEnabled: profile.get('stripeChargesEnabled'),\n accountLivemode: profile.get('stripeAccountLivemode'),\n },\n { subject: `POS card payment host ${hostId}` },\n )\n ) {\n return null\n }\n return String(accountId)\n}\n\nfunction terminalConfigRef(hostId: string) {\n return firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('terminal')\n .doc('config')\n}\n\nfunction readerRef(hostId: string, readerId: string) {\n return firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('terminalReaders')\n .doc(readerId)\n}\n\n/** A reader id as Stripe mints them; anything else never reaches Stripe. */\nexport function isTerminalReaderId(value: unknown): value is string {\n return typeof value === 'string' && /^tmr_[A-Za-z0-9]{6,64}$/.test(value)\n}\n\n/** The reader, when it is THIS site's; null for anyone else's or none. */\nexport async function ownedTerminalReader(\n hostId: string,\n readerId: unknown,\n): Promise<(PosTerminalReader & { id: string }) | null> {\n if (!isTerminalReaderId(readerId)) return null\n const snapshot = await readerRef(hostId, readerId).get()\n if (!snapshot.exists) return null\n return { ...(snapshot.data() as PosTerminalReader), id: readerId }\n}\n\n/** A Terminal Location's address, as the console sends it. */\nexport interface PosTerminalAddress {\n line1: string\n line2?: string\n city: string\n state?: string\n postalCode: string\n country: string\n}\n\nexport function posTerminalAddressProblem(address: Partial<PosTerminalAddress> | undefined): string | null {\n if (!address?.line1 || !address?.city || !address?.postalCode || !address?.country) {\n return 'Enter the street, city, postal code and country where the reader is used.'\n }\n if (!/^[A-Z]{2}$/.test(String(address.country))) return 'Choose a two-letter country code, like US.'\n return null\n}\n\n/**\n * The idempotency key of a site's Location request. Two presses with the same\n * address share it, so a double tap makes one Location; a corrected address\n * gets its own, since Stripe refuses a key reused with other parameters for a\n * day (a merchant fixing a typo would otherwise read that refusal).\n */\nexport function posTerminalLocationKey(hostId: string, displayName: string, address: PosTerminalAddress): string {\n const fingerprint = createHash('sha256')\n .update(\n JSON.stringify([\n displayName.slice(0, 100),\n address.line1,\n address.line2 ?? '',\n address.city,\n address.state ?? '',\n address.postalCode,\n address.country,\n ]),\n )\n .digest('hex')\n .slice(0, 16)\n return `pos-location:${hostId}:${fingerprint}`\n}\n\n/**\n * The site's Terminal Location, created on first use (on the PLATFORM, with\n * the merchant named in metadata). Its address is the store's, because Stripe\n * uses it to configure the reader for the right country.\n */\nexport async function ensurePosTerminalLocation(options: {\n hostId: string\n orgId: string\n displayName: string\n address?: PosTerminalAddress\n}): Promise<{ ok: true; locationId: string } | { ok: false; status: number; error: string }> {\n const ref = terminalConfigRef(options.hostId)\n const existing = await ref.get()\n const known = String(existing.get('stripeLocationId') ?? '')\n if (known) return { ok: true, locationId: known }\n const problem = posTerminalAddressProblem(options.address)\n if (problem) return { ok: false, status: 400, error: problem }\n const address = options.address as PosTerminalAddress\n const created = await posStripe('POST', 'terminal/locations', {\n idempotencyKey: posTerminalLocationKey(options.hostId, options.displayName, address),\n params: {\n display_name: options.displayName.slice(0, 100) || 'Store',\n 'address[line1]': address.line1,\n 'address[line2]': address.line2 || undefined,\n 'address[city]': address.city,\n 'address[state]': address.state || undefined,\n 'address[postal_code]': address.postalCode,\n 'address[country]': address.country,\n 'metadata[orgId]': options.orgId,\n 'metadata[hostId]': options.hostId,\n },\n })\n if (!created.ok || !created.body?.id) {\n return {\n ok: false,\n status: 502,\n error: posStripeErrorMessage(created.body, 'The reader location could not be set up.'),\n }\n }\n await ref.set(\n {\n stripeLocationId: String(created.body.id),\n livemode: Boolean(created.body.livemode),\n createdAtMs: Date.now(),\n },\n { merge: true },\n )\n return { ok: true, locationId: String(created.body.id) }\n}\n\n/** The tipping a reader shows: Stripe offers three buttons, so three presets. */\nexport function posReaderTippingKey(settings: PosRegisterSettings): string {\n return settings.tippingEnabled ? settings.tipPercentages.slice(0, 3).join(',') : 'off'\n}\n\n/**\n * Keeps the site's reader tipping in step with the merchant's settings: one\n * Terminal Configuration per site, assigned to its Location, rewritten only\n * when the presets changed. Never fatal: a reader that keeps the previous\n * presets still takes the payment.\n */\nexport async function syncPosReaderTipping(\n hostId: string,\n settings: PosRegisterSettings,\n): Promise<void> {\n if (!settings.tippingEnabled) return\n const ref = terminalConfigRef(hostId)\n const config = await ref.get()\n const locationId = String(config.get('stripeLocationId') ?? '')\n if (!locationId) return\n const key = posReaderTippingKey(settings)\n if (config.get('tippingKey') === key) return\n const percentages = settings.tipPercentages.slice(0, 3)\n const configurationId = String(config.get('stripeConfigurationId') ?? '')\n const params = {\n [`tipping[${POS_CURRENCY}][percentages]`]: percentages.map(String),\n }\n const saved = await posStripe(\n 'POST',\n configurationId ? `terminal/configurations/${configurationId}` : 'terminal/configurations',\n {\n params,\n ...(configurationId ? {} : { idempotencyKey: `pos-configuration:${hostId}:${key}` }),\n },\n )\n if (!saved.ok || !saved.body?.id) {\n console.error('[pos-terminal] tipping configuration failed', hostId, saved.body?.error)\n return\n }\n if (!configurationId) {\n const assigned = await posStripe('POST', `terminal/locations/${locationId}`, {\n params: { configuration_overrides: String(saved.body.id) },\n })\n if (!assigned.ok) {\n console.error('[pos-terminal] configuration assignment failed', hostId, assigned.body?.error)\n return\n }\n }\n await ref.set(\n { stripeConfigurationId: String(saved.body.id), tippingKey: key },\n { merge: true },\n )\n}\n\n/** Reads a reader from Stripe and keeps its status on the site's document. */\nexport async function refreshPosReaderStatus(\n hostId: string,\n readerId: string,\n): Promise<{ status: string; action: any } | null> {\n const fetched = await posStripe('GET', `terminal/readers/${readerId}`)\n if (!fetched.ok) return null\n const status = String(fetched.body?.status ?? 'offline')\n await readerRef(hostId, readerId)\n .set({ status, lastSeenAtMs: Date.now() }, { merge: true })\n .catch(() => undefined)\n return { status, action: fetched.body?.action ?? null }\n}\n\n/*==========================================\n * TAKING A CARD PAYMENT.\n *=========================================*/\n\ninterface CardStart {\n hostId: string\n orderId: string\n paymentId: string\n amountCents: number\n /** A tip already chosen (on the customer display or by the cashier). */\n tipCents: number\n cashierId: string\n org: Record<string, any> | null\n}\n\n/** Reserves a pending card payment in the ledger, or finds the one this attempt made. */\nasync function reserveCardPayment(\n start: CardStart,\n method: CommerceModel.OrderPaymentMethod,\n extra: Partial<CommerceModel.OrderPayment>,\n): Promise<PosPaymentOutcome> {\n return await applyPosPayment({\n hostId: start.hostId,\n orderId: start.orderId,\n paymentId: start.paymentId,\n decide: ({ order, payments, existing }) => {\n if (existing) {\n // The same attempt again: hand back what it started. A failed card\n // is re-armed by `retry`, not by pressing the tender a second time.\n return { kind: 'keep' }\n }\n if (order.status !== 'pending') return { kind: 'refuse', ...NOT_A_SALE }\n const total = Number(order.totals?.totalCents ?? 0)\n const tenderable = CommerceModel.posTenderableCents(total, payments)\n if (!(start.amountCents > 0)) {\n return { kind: 'refuse', status: 400, error: 'Enter an amount to charge.' }\n }\n if (start.amountCents > tenderable) {\n return {\n kind: 'refuse',\n status: 409,\n error:\n tenderable > 0\n ? `Only $${(tenderable / 100).toFixed(2)} is left to pay on this sale.`\n : 'Nothing is left to pay on this sale.',\n }\n }\n const tipProblem = CommerceModel.posTipProblem(start.tipCents, start.amountCents)\n if (tipProblem) return { kind: 'refuse', status: 400, error: tipProblem }\n const takeFeeCents = CommerceModel.posTakeShareCents({\n takeFeeCents: Number(order.posTakeFeeCents ?? 0),\n totalCents: total,\n amountCents: start.amountCents,\n })\n return {\n kind: 'put',\n payment: {\n id: start.paymentId,\n method,\n amountCents: start.amountCents,\n ...(start.tipCents > 0 ? { tipCents: start.tipCents } : {}),\n status: 'pending',\n atMs: Date.now(),\n takeFeeCents,\n cashierId: start.cashierId,\n ...extra,\n },\n }\n },\n })\n}\n\n/** Writes Stripe's answer onto a pending card payment. */\nasync function patchCardPayment(\n hostId: string,\n orderId: string,\n paymentId: string,\n patch: (payment: CommerceModel.OrderPayment) => CommerceModel.OrderPayment | null,\n event?: { event: string; detail?: string },\n): Promise<PosPaymentOutcome> {\n return await applyPosPayment({\n hostId,\n orderId,\n paymentId,\n decide: ({ existing }) => {\n if (!existing) return { kind: 'refuse', status: 404, error: 'Unknown payment' }\n const next = patch(existing)\n if (!next) return { kind: 'keep' }\n return { kind: 'put', payment: next, ...(event ? { event } : {}) }\n },\n })\n}\n\nfunction paymentIntentParams(\n start: CardStart,\n payment: CommerceModel.OrderPayment,\n destination: string,\n extra: Record<string, string | number | boolean | undefined | readonly string[]>,\n) {\n const chargeCents = start.amountCents + start.tipCents\n const fee = posCardApplicationFeeCents({\n takeShareCents: Number(payment.takeFeeCents ?? 0),\n chargeCents,\n })\n return {\n amount: chargeCents,\n currency: POS_CURRENCY,\n 'transfer_data[destination]': destination,\n ...(fee > 0 ? { application_fee_amount: fee } : {}),\n 'metadata[kind]': 'pos',\n 'metadata[hostId]': start.hostId,\n 'metadata[orderId]': start.orderId,\n 'metadata[paymentId]': start.paymentId,\n 'metadata[tipCents]': String(start.tipCents),\n ...extra,\n }\n}\n\n/**\n * Creates (or finds) the card-present PaymentIntent for one payment WITHOUT\n * choosing how the card is collected: reserves the amount in the ledger and\n * creates a manual-capture destination charge to the merchant (settled on\n * Aglyn's platform account, with no `on_behalf_of`, as ToS §10.7 states),\n * keyed by the payment so a retry returns the same intent.\n *\n * Two collectors use it. {@link startCardPresentPayment} pushes the intent\n * to a smart reader the site owns; a native companion app (AGL-3618)\n * collects it with the Stripe Terminal SDK (Tap to Pay, Bluetooth readers)\n * using the returned `clientSecret`. However the card is collected, the\n * payment is recorded by {@link refreshPosCardPayment}, keyed on the intent's\n * metadata, from the register's poll or the webhook alike.\n */\nexport async function createCardPresentIntent(\n start: CardStart & { readerId?: string },\n): Promise<PosPaymentOutcome & { clientSecret?: string; paymentIntentId?: string }> {\n // Live card-present payments wait for Terminal to be switched on for the\n // platform (`STRIPE_TERMINAL_LIVE_ENABLED`); the hidden controls are not\n // the only gate.\n if (!posTerminalAvailable()) {\n return { ok: false, status: 409, error: 'Card readers are not available for this store yet.' }\n }\n const destination = await posMerchantAccount(start.hostId, start.org)\n if (!destination) return { ok: false, status: 409, error: 'Card payments not set up' }\n const reserved = await reserveCardPayment(\n start,\n 'card_present',\n start.readerId ? { readerId: start.readerId } : {},\n )\n if (!reserved.ok || !reserved.payment) return reserved\n const payment = reserved.payment\n if (payment.status !== 'pending') return reserved\n // Keyed by the payment: a second call for the same payment replays the\n // intent Stripe already made rather than opening another.\n const intent = await posStripe('POST', 'payment_intents', {\n idempotencyKey: `pos-pi:${payment.id}`,\n params: paymentIntentParams(start, payment, destination, {\n payment_method_types:\n POS_CURRENCY === 'usd' ? ['card_present'] : ['card_present', 'interac_present'],\n capture_method: 'manual',\n ...(start.readerId ? { 'metadata[readerId]': start.readerId } : {}),\n description: 'In-store purchase',\n }),\n })\n if (!intent.ok || !intent.body?.id) {\n return await patchCardPayment(start.hostId, start.orderId, payment.id, (current) => ({\n ...current,\n status: 'failed',\n failureMessage: posStripeErrorMessage(intent.body, 'The card payment could not be started.'),\n }))\n }\n const paymentIntentId = String(intent.body.id)\n const recorded = payment.paymentIntentId\n ? reserved\n : await patchCardPayment(start.hostId, start.orderId, payment.id, (current) => ({\n ...current,\n paymentIntentId,\n livemode: Boolean(intent.body.livemode),\n }))\n return {\n ...recorded,\n paymentIntentId,\n ...(intent.body.client_secret ? { clientSecret: String(intent.body.client_secret) } : {}),\n }\n}\n\n/**\n * A smart-reader payment: the card-present intent from\n * {@link createCardPresentIntent}, pushed to a reader THIS site owns. The\n * register then polls {@link refreshPosCardPayment} until the reader answers;\n * webhooks land on the same function.\n */\nexport async function startCardPresentPayment(\n start: CardStart & { readerId: string; settings: PosRegisterSettings },\n): Promise<PosPaymentOutcome> {\n const reader = await ownedTerminalReader(start.hostId, start.readerId)\n if (!reader) return { ok: false, status: 404, error: 'That card reader is not on this site.' }\n await syncPosReaderTipping(start.hostId, start.settings).catch((error) =>\n console.error('[pos-terminal] tipping sync failed', error),\n )\n const created = await createCardPresentIntent({ ...start, readerId: reader.id })\n if (!created.ok || !created.payment || !created.paymentIntentId) return created\n if (created.payment.status !== 'pending') return created\n return await sendToReader({\n hostId: start.hostId,\n orderId: start.orderId,\n paymentId: created.payment.id,\n readerId: reader.id,\n paymentIntentId: created.paymentIntentId,\n // A tip the customer already chose on the display is in the amount;\n // asking again on the reader would ask twice.\n skipTipping: !start.settings.tippingEnabled || start.tipCents > 0,\n attempt: 0,\n })\n}\n\n/** The site's Terminal Location id, or null before a reader was ever set up. */\nexport async function posTerminalLocationId(hostId: string): Promise<string | null> {\n const config = await terminalConfigRef(hostId).get()\n return String(config.get('stripeLocationId') ?? '') || null\n}\n\n/**\n * A Terminal SDK connection token scoped to the site's own Location, for a\n * native companion app's SDK (AGL-3618). Scoping it to the Location is what\n * keeps one store's app from discovering another store's readers: Stripe\n * only lets a location-scoped token connect to readers in that Location.\n * Null when the site has no Location yet, never an unscoped token.\n */\nexport async function createPosTerminalConnectionToken(\n hostId: string,\n): Promise<{ secret: string; locationId: string } | null> {\n const locationId = await posTerminalLocationId(hostId)\n if (!locationId) return null\n const token = await posStripe('POST', 'terminal/connection_tokens', {\n params: { location: locationId },\n })\n if (!token.ok || !token.body?.secret) return null\n return { secret: String(token.body.secret), locationId }\n}\n\nasync function sendToReader(input: {\n hostId: string\n orderId: string\n paymentId: string\n readerId: string\n paymentIntentId: string\n skipTipping: boolean\n attempt: number\n}): Promise<PosPaymentOutcome> {\n const processed = await posStripe(\n 'POST',\n `terminal/readers/${input.readerId}/process_payment_intent`,\n {\n idempotencyKey: `pos-process:${input.paymentId}:${input.attempt}`,\n params: {\n payment_intent: input.paymentIntentId,\n 'process_config[enable_customer_cancellation]': true,\n ...(input.skipTipping ? { 'process_config[skip_tipping]': true } : {}),\n },\n },\n )\n if (!processed.ok) {\n return await patchCardPayment(input.hostId, input.orderId, input.paymentId, (current) => ({\n ...current,\n status: 'failed',\n failureMessage: posStripeErrorMessage(processed.body, 'The card reader could not start the payment.'),\n }))\n }\n return await patchCardPayment(input.hostId, input.orderId, input.paymentId, (current) => {\n if (!current.failureMessage) return null\n return withoutFailure(current)\n })\n}\n\n/**\n * Re-arms a declined or failed reader payment on the SAME PaymentIntent, as\n * Stripe asks (a new one per attempt risks a double charge).\n */\nexport async function retryCardPresentPayment(input: {\n hostId: string\n orderId: string\n paymentId: string\n settings: PosRegisterSettings\n}): Promise<PosPaymentOutcome> {\n const order = await readPosSale(input.hostId, input.orderId)\n if (!order || order.status !== 'pending') return NOT_A_SALE\n const payment = CommerceModel.orderPayments(order).find((entry) => entry.id === input.paymentId)\n if (!payment || payment.method !== 'card_present' || !payment.paymentIntentId || !payment.readerId) {\n return { ok: false, status: 404, error: 'Unknown payment' }\n }\n if (payment.status === 'succeeded') return { ok: true, order, payment, completed: false, changed: false }\n const reader = await ownedTerminalReader(input.hostId, payment.readerId)\n if (!reader) return { ok: false, status: 404, error: 'That card reader is not on this site.' }\n // Back to pending first, so the amount is reserved again before the reader\n // is asked; a sale paid meanwhile by another tender refuses here.\n const rearmed = await applyPosPayment({\n hostId: input.hostId,\n orderId: input.orderId,\n paymentId: input.paymentId,\n decide: ({ order: fresh, payments, existing }) => {\n if (!existing) return { kind: 'refuse', status: 404, error: 'Unknown payment' }\n if (existing.status === 'pending') return { kind: 'keep' }\n if (existing.status !== 'failed' && existing.status !== 'canceled') {\n return { kind: 'refuse', status: 409, error: 'This payment cannot be retried.' }\n }\n const tenderable = CommerceModel.posTenderableCents(\n Number(fresh.totals?.totalCents ?? 0),\n payments.filter((entry) => entry.id !== existing.id),\n )\n if (existing.amountCents > tenderable) {\n return { kind: 'refuse', status: 409, error: 'The balance changed. Start a new card payment.' }\n }\n return { kind: 'put', payment: { ...withoutFailure(existing), status: 'pending' } }\n },\n })\n if (!rearmed.ok) return rearmed\n const attempt = Date.now()\n return await sendToReader({\n hostId: input.hostId,\n orderId: input.orderId,\n paymentId: input.paymentId,\n readerId: reader.id,\n paymentIntentId: payment.paymentIntentId,\n skipTipping: !input.settings.tippingEnabled || Number(payment.tipCents ?? 0) > 0,\n attempt,\n })\n}\n\n/**\n * A typed card (card not present): a PaymentIntent the console confirms with\n * the Payment Element. No MOTO: the card is keyed into Stripe's own field and\n * goes through Stripe's normal online checks, 3-D Secure included.\n */\nexport async function startCardKeyedPayment(\n start: CardStart & { paymentMethodId?: string },\n): Promise<PosPaymentOutcome & { clientSecret?: string }> {\n const paymentMethodId = start.paymentMethodId ?? ''\n if (paymentMethodId && !isPaymentMethodId(paymentMethodId)) {\n return { ok: false, status: 400, error: 'The card could not be read. Enter it again.' }\n }\n const destination = await posMerchantAccount(start.hostId, start.org)\n if (!destination) return { ok: false, status: 409, error: 'Card payments not set up' }\n const reserved = await reserveCardPayment(start, 'card_keyed', {})\n if (!reserved.ok || !reserved.payment) return reserved\n const payment = reserved.payment\n if (payment.status !== 'pending') return reserved\n const intent = await posStripe('POST', 'payment_intents', {\n idempotencyKey: `pos-pi:${payment.id}`,\n params: paymentIntentParams(start, payment, destination, {\n payment_method_types: ['card'],\n description: 'In-store purchase (typed card)',\n // 3-D Secure from the one seam every card payment shares (AGL-3360).\n ...cardAuthenticationParams('one-time'),\n }),\n })\n if (!intent.ok || !intent.body?.id || !intent.body?.client_secret) {\n return await patchCardPayment(start.hostId, start.orderId, payment.id, (current) => ({\n ...current,\n status: 'failed',\n failureMessage: posStripeErrorMessage(intent.body, 'The card payment could not be started.'),\n }))\n }\n const updated = payment.paymentIntentId\n ? reserved\n : await patchCardPayment(start.hostId, start.orderId, payment.id, (current) => ({\n ...current,\n paymentIntentId: String(intent.body.id),\n livemode: Boolean(intent.body.livemode),\n }))\n if (!paymentMethodId) return { ...updated, clientSecret: String(intent.body.client_secret) }\n return await confirmKeyedCard(start, payment.id, String(intent.body.id), paymentMethodId)\n}\n\n/** A PaymentMethod id as Stripe mints them; anything else never reaches Stripe. */\nfunction isPaymentMethodId(value: string): boolean {\n return /^pm_[A-Za-z0-9]{8,64}$/.test(value)\n}\n\n/** What a typed card that needs the cardholder's bank to approve it says. */\nexport const KEYED_CARD_NEEDS_AUTHENTICATION =\n 'This card needs the cardholder to approve the payment with their bank, which a typed card cannot do. Use Card (QR) so they can approve it on their phone.'\n\n/**\n * A card typed into the register's own card fields (AGL-3848): the device\n * sent the number to Stripe alone, which answered with a PaymentMethod, and\n * hands this route only its id. The server confirms the intent with it, with\n * the platform's secret key, and records the payment from Stripe's answer as\n * every card payment is. A card whose bank asks for the cardholder's approval\n * (3-D Secure) cannot be approved at the register, so that intent is released\n * and the payment fails with words that point at Card (QR).\n */\nasync function confirmKeyedCard(\n start: CardStart,\n paymentId: string,\n intentId: string,\n paymentMethodId: string,\n): Promise<PosPaymentOutcome> {\n const input = { hostId: start.hostId, orderId: start.orderId, paymentId }\n const confirmed = await posStripe('POST', `payment_intents/${intentId}/confirm`, {\n idempotencyKey: `pos-keyed-confirm:${intentId}:${paymentMethodId}`,\n params: { payment_method: paymentMethodId },\n })\n const status = String(confirmed.body?.status ?? confirmed.body?.error?.payment_intent?.status ?? '')\n if (status === 'requires_action') {\n await posStripe('POST', `payment_intents/${intentId}/cancel`, {\n idempotencyKey: `pos-cancel:${intentId}`,\n })\n return await patchCardPayment(start.hostId, start.orderId, paymentId, (current) =>\n current.status === 'pending'\n ? { ...current, status: 'failed', failureMessage: KEYED_CARD_NEEDS_AUTHENTICATION }\n : null,\n )\n }\n if (!confirmed.ok && confirmed.body?.error && !confirmed.body.error.payment_intent) {\n // Refused before the card was tried (a stale method, a bad request): say so, leave nothing pending.\n const message = posStripeErrorMessage(confirmed.body, 'The card was not accepted.')\n await posStripe('POST', `payment_intents/${intentId}/cancel`, {\n idempotencyKey: `pos-cancel:${intentId}`,\n })\n return await patchCardPayment(start.hostId, start.orderId, paymentId, (current) =>\n current.status === 'pending' ? { ...current, status: 'failed', failureMessage: message } : null,\n )\n }\n // Approved, declined or still processing: Stripe's own read of the intent decides.\n return await refreshPosCardPayment(input)\n}\n\n/**\n * The customer pays on their own phone from a QR code: a Stripe payment page\n * for this payment's amount (and tip, as its own line). Settled by\n * `checkout.session.completed` or by the register's poll.\n */\nexport async function startCardLinkPayment(\n start: CardStart & { returnUrl: string },\n): Promise<PosPaymentOutcome> {\n const destination = await posMerchantAccount(start.hostId, start.org)\n if (!destination) return { ok: false, status: 409, error: 'Card payments not set up' }\n const reserved = await reserveCardPayment(start, 'card_link', {})\n if (!reserved.ok || !reserved.payment) return reserved\n const payment = reserved.payment\n if (payment.checkoutSessionId || payment.status !== 'pending') return reserved\n const fee = posCardApplicationFeeCents({\n takeShareCents: Number(payment.takeFeeCents ?? 0),\n chargeCents: start.amountCents + start.tipCents,\n })\n const session = await posStripe('POST', 'checkout/sessions', {\n idempotencyKey: `pos-link:${payment.id}`,\n params: {\n mode: 'payment',\n ...checkoutSessionCardAuthenticationParams('payment'),\n 'payment_method_types': ['card'],\n 'line_items[0][quantity]': 1,\n 'line_items[0][price_data][currency]': POS_CURRENCY,\n 'line_items[0][price_data][unit_amount]': start.amountCents,\n 'line_items[0][price_data][product_data][name]': 'In-store purchase',\n ...(start.tipCents > 0\n ? {\n 'line_items[1][quantity]': 1,\n 'line_items[1][price_data][currency]': POS_CURRENCY,\n 'line_items[1][price_data][unit_amount]': start.tipCents,\n 'line_items[1][price_data][product_data][name]': 'Tip',\n }\n : {}),\n 'payment_intent_data[transfer_data][destination]': destination,\n ...(fee > 0 ? { 'payment_intent_data[application_fee_amount]': fee } : {}),\n 'payment_intent_data[metadata][kind]': 'pos',\n 'payment_intent_data[metadata][hostId]': start.hostId,\n 'payment_intent_data[metadata][orderId]': start.orderId,\n 'payment_intent_data[metadata][paymentId]': payment.id,\n 'payment_intent_data[metadata][tipCents]': String(start.tipCents),\n success_url: `${start.returnUrl}?paid=1`,\n cancel_url: `${start.returnUrl}?paid=0`,\n 'metadata[type]': 'pos-payment',\n 'metadata[hostId]': start.hostId,\n 'metadata[orderId]': start.orderId,\n 'metadata[paymentId]': payment.id,\n },\n })\n if (!session.ok || !session.body?.url) {\n return await patchCardPayment(start.hostId, start.orderId, payment.id, (current) => ({\n ...current,\n status: 'failed',\n failureMessage: posStripeErrorMessage(session.body, 'The payment page could not be created.'),\n }))\n }\n return await patchCardPayment(start.hostId, start.orderId, payment.id, (current) => ({\n ...current,\n checkoutSessionId: String(session.body.id),\n checkoutUrl: String(session.body.url),\n livemode: Boolean(session.body.livemode),\n }))\n}\n\n/*==========================================\n * SETTLING A CARD PAYMENT.\n *=========================================*/\n\n/** The card's brand and last four, off the charge Stripe made. */\nfunction cardDetailsOf(intent: any): { cardBrand?: string; last4?: string } {\n const charge = typeof intent?.latest_charge === 'object' ? intent.latest_charge : null\n const details =\n charge?.payment_method_details?.card_present ??\n charge?.payment_method_details?.interac_present ??\n charge?.payment_method_details?.card ??\n null\n return {\n ...(details?.brand ? { cardBrand: String(details.brand) } : {}),\n ...(details?.last4 ? { last4: String(details.last4) } : {}),\n }\n}\n\n/** The tip on an intent: what the display chose plus what the reader added. */\nexport function posIntentTipCents(intent: any): number {\n const preset = Math.max(0, Math.round(Number(intent?.metadata?.tipCents ?? 0)) || 0)\n const onReader = Math.max(0, Math.round(Number(intent?.amount_details?.tip?.amount ?? 0)) || 0)\n return preset + onReader\n}\n\n/**\n * The fee a register card charge is captured with: the payment's share of the\n * take (fixed at reservation, the sale's share and never the tip's) plus the\n * card processing cost on the whole amount, tip included.\n */\nexport function posCaptureFeeCents(payment: CommerceModel.OrderPayment, intent: any): number {\n return posCardApplicationFeeCents({\n takeShareCents: Number(payment.takeFeeCents ?? 0),\n chargeCents: Math.max(0, Math.round(Number(intent?.amount ?? 0))),\n })\n}\n\n/**\n * Brings one card payment up to date with Stripe, from a webhook or the\n * register's poll alike: captures an authorized reader payment with its final\n * fee, records a success, a decline or a cancellation. Idempotent: the\n * capture is keyed by the intent, and the ledger change is a no-op when the\n * payment already says what Stripe says.\n */\nexport async function refreshPosCardPayment(input: {\n hostId: string\n orderId: string\n paymentId: string\n}): Promise<PosPaymentOutcome> {\n const order = await readPosSale(input.hostId, input.orderId)\n if (!order) return { ok: false, status: 404, error: 'Unknown sale' }\n const payment = CommerceModel.orderPayments(order).find((entry) => entry.id === input.paymentId)\n if (!payment) return { ok: false, status: 404, error: 'Unknown payment' }\n if (!CommerceModel.isCardPaymentMethod(payment.method)) {\n return { ok: true, order, payment, completed: false, changed: false }\n }\n let paymentIntentId = payment.paymentIntentId ?? ''\n if (!paymentIntentId && payment.checkoutSessionId) {\n const session = await posStripe('GET', `checkout/sessions/${payment.checkoutSessionId}`)\n if (session.ok && session.body?.status === 'expired' && payment.status === 'pending') {\n return await patchCardPayment(input.hostId, input.orderId, payment.id, (current) =>\n current.status === 'pending' ? { ...current, status: 'canceled' } : null,\n )\n }\n paymentIntentId = String(session.body?.payment_intent ?? '')\n }\n if (!paymentIntentId) return { ok: true, order, payment, completed: false, changed: false }\n let intent = (\n await posStripe('GET', `payment_intents/${paymentIntentId}`, {\n params: { 'expand[]': 'latest_charge' },\n })\n ).body\n if (!intent?.id) return { ok: true, order, payment, completed: false, changed: false }\n if (intent.metadata?.hostId !== input.hostId || intent.metadata?.paymentId !== payment.id) {\n console.error('[pos-terminal] intent does not belong to this payment', paymentIntentId)\n return { ok: false, status: 409, error: 'This payment does not match its card charge.' }\n }\n if (intent.status === 'requires_capture') {\n if (order.status !== 'pending' && payment.status !== 'pending') {\n // Nothing to capture into: the sale is gone. Release the hold.\n await posStripe('POST', `payment_intents/${intent.id}/cancel`, {\n idempotencyKey: `pos-release:${intent.id}`,\n })\n return { ok: true, order, payment, completed: false, changed: false }\n }\n const fee = posCaptureFeeCents(payment, intent)\n const captured = await posStripe('POST', `payment_intents/${intent.id}/capture`, {\n idempotencyKey: `pos-capture:${intent.id}`,\n params: {\n ...(fee > 0 ? { application_fee_amount: fee } : {}),\n 'expand[]': 'latest_charge',\n },\n })\n if (captured.ok && captured.body?.id) intent = captured.body\n else {\n console.error('[pos-terminal] capture failed', intent.id, captured.body?.error)\n return { ok: true, order, payment, completed: false, changed: false }\n }\n }\n if (intent.status === 'succeeded') {\n const tipCents = posIntentTipCents(intent)\n const amountCents = Math.max(0, Math.round(Number(intent.amount ?? 0))) - tipCents\n return await patchCardPayment(\n input.hostId,\n input.orderId,\n payment.id,\n (current) => {\n if (current.status === 'succeeded' && current.paymentIntentId === intent.id) return null\n return {\n ...withoutFailure(current),\n status: 'succeeded',\n settledAtMs: Date.now(),\n paymentIntentId: String(intent.id),\n // What the charge actually put toward the sale. Equal to the\n // reserved amount; restated from Stripe so the ledger records the\n // money that moved rather than the money that was asked for.\n amountCents: amountCents > 0 ? amountCents : current.amountCents,\n ...(tipCents > 0 ? { tipCents } : {}),\n feeCents: Math.max(0, Math.round(Number(intent.application_fee_amount ?? 0))),\n livemode: Boolean(intent.livemode),\n ...cardDetailsOf(intent),\n }\n },\n )\n }\n if (intent.status === 'canceled') {\n return await patchCardPayment(input.hostId, input.orderId, payment.id, (current) =>\n current.status === 'pending' || current.status === 'failed'\n ? { ...current, status: 'canceled' }\n : null,\n )\n }\n if (intent.status === 'requires_payment_method' && intent.last_payment_error) {\n const message = String(intent.last_payment_error?.message ?? 'The card was declined.')\n return await patchCardPayment(input.hostId, input.orderId, payment.id, (current) =>\n current.status === 'pending' ? { ...current, status: 'failed', failureMessage: message } : null,\n )\n }\n // Still waiting on the customer. A reader action that already failed (a\n // card removed, a cancel on the reader) says so on the reader, not the\n // intent, so ask the reader too.\n if (payment.method === 'card_present' && payment.readerId && payment.status === 'pending') {\n const reader = await refreshPosReaderStatus(input.hostId, payment.readerId)\n const action = reader?.action\n if (\n action?.type === 'process_payment_intent' &&\n action?.process_payment_intent?.payment_intent === intent.id &&\n action?.status === 'failed'\n ) {\n const message = String(action.failure_message ?? 'The reader could not take the card.')\n return await patchCardPayment(input.hostId, input.orderId, payment.id, (current) =>\n current.status === 'pending' ? { ...current, status: 'failed', failureMessage: message } : null,\n )\n }\n }\n const fresh = await readPosSale(input.hostId, input.orderId)\n return {\n ok: true,\n order: fresh ?? order,\n payment:\n CommerceModel.orderPayments(fresh ?? order).find((entry) => entry.id === payment.id) ?? payment,\n completed: false,\n changed: false,\n }\n}\n\n/**\n * Stops a card payment that has not been paid: clears the reader, cancels the\n * intent, expires the payment page. A charge that already went through is\n * left alone and recorded as paid, never cancelled under the customer.\n */\nexport async function cancelPosCardPayment(input: {\n hostId: string\n orderId: string\n paymentId: string\n}): Promise<PosPaymentOutcome> {\n const refreshed = await refreshPosCardPayment(input)\n if (!refreshed.ok || !refreshed.payment) return refreshed\n const payment = refreshed.payment\n if (payment.status === 'succeeded' || payment.status === 'canceled') return refreshed\n if (payment.method === 'card_present' && payment.readerId) {\n const cleared = await posStripe('POST', `terminal/readers/${payment.readerId}/cancel_action`)\n if (!cleared.ok && cleared.body?.error?.code === 'terminal_reader_busy') {\n return {\n ok: false,\n status: 409,\n error: 'The customer is mid-payment on the reader. Wait for it to finish.',\n }\n }\n }\n if (payment.paymentIntentId) {\n await posStripe('POST', `payment_intents/${payment.paymentIntentId}/cancel`, {\n idempotencyKey: `pos-cancel:${payment.paymentIntentId}`,\n })\n }\n if (payment.checkoutSessionId) {\n await posStripe('POST', `checkout/sessions/${payment.checkoutSessionId}/expire`, {\n idempotencyKey: `pos-expire:${payment.checkoutSessionId}`,\n })\n }\n // Re-read: a payment that completed in the gap is recorded as paid.\n const settled = await refreshPosCardPayment(input)\n if (settled.ok && settled.payment?.status === 'succeeded') return settled\n return await patchCardPayment(input.hostId, input.orderId, payment.id, (current) =>\n current.status === 'pending' || current.status === 'failed'\n ? { ...current, status: 'canceled' }\n : null,\n )\n}\n\n/**\n * TEST MODE ONLY: Stripe's simulated reader is \"tapped\" with a test card.\n * Refused outright on a live key, whatever the request says.\n */\nexport async function simulatePosReaderTap(input: {\n hostId: string\n orderId: string\n paymentId: string\n /** `4000000000000002` declines; anything else Stripe's default approves. */\n cardNumber?: string\n}): Promise<PosPaymentOutcome> {\n if (!posStripeTestMode()) return { ok: false, status: 403, error: 'Only in test mode' }\n const order = await readPosSale(input.hostId, input.orderId)\n if (!order) return { ok: false, status: 404, error: 'Unknown sale' }\n const payment = CommerceModel.orderPayments(order).find((entry) => entry.id === input.paymentId)\n if (!payment?.readerId || payment.method !== 'card_present') {\n return { ok: false, status: 404, error: 'Unknown payment' }\n }\n const reader = await ownedTerminalReader(input.hostId, payment.readerId)\n if (!reader) return { ok: false, status: 404, error: 'That card reader is not on this site.' }\n const presented = await posStripe(\n 'POST',\n `test_helpers/terminal/readers/${reader.id}/present_payment_method`,\n {\n params: {\n type: 'card_present',\n ...(input.cardNumber && /^[0-9]{12,19}$/.test(input.cardNumber)\n ? { 'card_present[number]': input.cardNumber }\n : {}),\n },\n },\n )\n if (!presented.ok) {\n return {\n ok: false,\n status: 502,\n error: posStripeErrorMessage(presented.body, 'The simulated tap failed.'),\n }\n }\n return await refreshPosCardPayment(input)\n}\n\n/*==========================================\n * WEBHOOKS (the small branch `billing-webhook.ts` calls).\n *=========================================*/\n\n/** Where a Stripe object says a register payment lives, or null. */\nfunction posPaymentRef(\n metadata: Record<string, unknown> | undefined,\n): { hostId: string; orderId: string; paymentId: string } | null {\n const hostId = String(metadata?.['hostId'] ?? '')\n const orderId = String(metadata?.['orderId'] ?? '')\n const paymentId = String(metadata?.['paymentId'] ?? '')\n if (!hostId || !orderId || !paymentId) return null\n return { hostId, orderId, paymentId }\n}\n\n/**\n * The register's Stripe events: a reader finished or failed an action, a\n * register PaymentIntent was authorized, paid, declined or cancelled, or a\n * register QR page was paid or expired. Each resolves to ONE payment and is\n * answered by re-reading Stripe through {@link refreshPosCardPayment}, never\n * by trusting the event's own copy (whose API version predates the tip\n * fields). Returns the site when the event was the register's, so the\n * dispatcher can count it as claimed; null for every other event.\n */\nexport async function handlePosStripeEvent(input: {\n type: string\n object: any\n}): Promise<{ hostId: string } | null> {\n const { type, object } = input\n let ref: { hostId: string; orderId: string; paymentId: string } | null = null\n if (type === 'terminal.reader.action_succeeded' || type === 'terminal.reader.action_failed') {\n const intentId = String(object?.action?.process_payment_intent?.payment_intent ?? '')\n if (object?.action?.type !== 'process_payment_intent' || !intentId) return null\n const intent = await posStripe('GET', `payment_intents/${intentId}`)\n if (!intent.ok || intent.body?.metadata?.kind !== 'pos') return null\n ref = posPaymentRef(intent.body.metadata)\n } else if (\n type === 'payment_intent.amount_capturable_updated' ||\n type === 'payment_intent.succeeded' ||\n type === 'payment_intent.payment_failed' ||\n type === 'payment_intent.canceled'\n ) {\n if (object?.metadata?.kind !== 'pos') return null\n ref = posPaymentRef(object.metadata)\n } else if (\n (type === 'checkout.session.completed' || type === 'checkout.session.expired') &&\n object?.metadata?.type === 'pos-payment'\n ) {\n ref = posPaymentRef(object.metadata)\n } else {\n return null\n }\n if (!ref) return null\n // A reader can be re-registered by nobody else, but an event is only a\n // claim: the intent's own metadata must name a reader this site owns.\n await refreshPosCardPayment(ref).catch((error) => {\n console.error('[pos-terminal] webhook settle failed', ref, error)\n // Rethrown so the delivery is retried: a lost settlement is a sale the\n // register cannot complete without its poll.\n throw error\n })\n return { hostId: ref.hostId }\n}\n\nexport type { PosLiftedOrder }\n"],"names":["createHash","firebaseAdmin","merchantAccountIsReady","cardAuthenticationParams","checkoutSessionCardAuthenticationParams","CommerceModel","POS_CURRENCY","posCardApplicationFeeCents","posStripe","posStripeErrorMessage","posStripeTestMode","posTerminalAvailable","applyPosPayment","readPosSale","NOT_A_SALE","ok","status","error","withoutFailure","payment","next","failureMessage","posMerchantAccount","hostId","org","ownerUid","String","profile","app","firestore","collection","doc","get","accountId","chargesEnabled","accountLivemode","subject","terminalConfigRef","readerRef","readerId","isTerminalReaderId","value","test","ownedTerminalReader","snapshot","exists","data","id","posTerminalAddressProblem","address","line1","city","postalCode","country","posTerminalLocationKey","displayName","fingerprint","update","JSON","stringify","slice","line2","state","digest","ensurePosTerminalLocation","options","existing","created","ref","known","locationId","problem","idempotencyKey","params","display_name","undefined","orgId","body","set","stripeLocationId","livemode","Boolean","createdAtMs","Date","now","merge","posReaderTippingKey","settings","tippingEnabled","tipPercentages","join","syncPosReaderTipping","config","saved","key","percentages","configurationId","map","console","assigned","configuration_overrides","stripeConfigurationId","tippingKey","refreshPosReaderStatus","fetched","lastSeenAtMs","catch","action","reserveCardPayment","start","method","extra","orderId","paymentId","decide","order","payments","kind","total","Number","totals","totalCents","tenderable","posTenderableCents","amountCents","toFixed","tipProblem","posTipProblem","tipCents","takeFeeCents","posTakeShareCents","posTakeFeeCents","atMs","cashierId","patchCardPayment","patch","event","paymentIntentParams","destination","chargeCents","fee","takeShareCents","amount","currency","application_fee_amount","createCardPresentIntent","intent","reserved","payment_method_types","capture_method","description","current","paymentIntentId","recorded","client_secret","clientSecret","startCardPresentPayment","reader","sendToReader","skipTipping","attempt","posTerminalLocationId","createPosTerminalConnectionToken","token","location","secret","input","processed","payment_intent","retryCardPresentPayment","orderPayments","find","entry","completed","changed","rearmed","fresh","filter","startCardKeyedPayment","paymentMethodId","isPaymentMethodId","updated","confirmKeyedCard","KEYED_CARD_NEEDS_AUTHENTICATION","intentId","confirmed","payment_method","message","refreshPosCardPayment","startCardLinkPayment","session","checkoutSessionId","mode","success_url","returnUrl","cancel_url","url","checkoutUrl","cardDetailsOf","charge","latest_charge","details","payment_method_details","card_present","interac_present","card","brand","cardBrand","last4","posIntentTipCents","preset","Math","max","round","metadata","onReader","amount_details","tip","posCaptureFeeCents","isCardPaymentMethod","captured","settledAtMs","feeCents","last_payment_error","type","process_payment_intent","failure_message","cancelPosCardPayment","settled","refreshed","cleared","code","simulatePosReaderTap","presented","cardNumber","posPaymentRef","handlePosStripeEvent","object"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/pos-terminal.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 'node:crypto'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { merchantAccountIsReady } from '@aglyn/tenant-data-admin/server/payment-provider'\nimport {\n cardAuthenticationParams,\n checkoutSessionCardAuthenticationParams,\n} from '@aglyn/tenant-data-admin/server/stripe-card-authentication'\nimport * as CommerceModel from '../model'\nimport type { PosRegisterSettings } from '../plugin-config'\nimport {\n POS_CURRENCY,\n posCardApplicationFeeCents,\n posStripe,\n posStripeErrorMessage,\n posStripeTestMode,\n posTerminalAvailable,\n} from './pos-stripe'\nimport { applyPosPayment, readPosSale, type PosLiftedOrder, type PosPaymentOutcome } from './pos-sale'\n\n/*==========================================\n * STRIPE TERMINAL AND THE REGISTER'S OTHER CARD TENDERS (AGL-3607).\n *\n * ## Where the readers live\n *\n * Every register charge is a DESTINATION charge on the platform account, so\n * Stripe's rule for that model applies: Locations and Readers are created on\n * the PLATFORM with the platform key, and identify their merchant by\n * metadata. One Location per site (`hosts/{hostId}/terminal/config`), and one\n * `hosts/{hostId}/terminalReaders/{readerId}` document per reader, written\n * only here. The document is what makes a reader THIS site's: every call that\n * names a reader reads it under the caller's own site first, so one store can\n * never drive, rename or remove another's reader, and the reader's own\n * `metadata.hostId` is checked again before a payment is sent to it.\n *\n * ## Why the reader charges with MANUAL capture\n *\n * On-reader tipping changes the amount after the PaymentIntent exists: the\n * reader adds the tip on confirmation and reports it in\n * `amount_details.tip.amount`. `application_fee_amount` set at creation was\n * sized on the pre-tip amount, and the platform's take must exclude the tip\n * while Stripe's processing cost (debited from the platform) is on the whole\n * charge. So the reader only AUTHORIZES; {@link refreshPosCardPayment} reads\n * the tip, re-prices the fee as\n *\n * take share (of the sale, never of the tip) + card processing (whole charge)\n *\n * and captures with that exact `application_fee_amount`. Stripe's own Connect\n * guidance for Terminal is the same: inspect, fix the application fee, then\n * capture. An authorization expires after two days, and a register capture\n * happens seconds after the tap.\n *=========================================*/\n\nconst NOT_A_SALE = { ok: false as const, status: 409, error: 'This sale is no longer open' }\n\n/** `hosts/{hostId}/terminalReaders/{readerId}`. */\nexport interface PosTerminalReader {\n label: string\n registerId?: string\n stripeLocationId: string\n deviceType?: string\n serialNumber?: string\n status?: 'online' | 'offline' | string\n livemode: boolean\n createdAtMs: number\n createdBy?: string\n lastSeenAtMs?: number\n}\n\n/**\n * The payment without its failure reason. Removed, never set to `undefined`:\n * Firestore refuses an undefined field inside the `payments` array.\n */\nfunction withoutFailure(payment: CommerceModel.OrderPayment): CommerceModel.OrderPayment {\n const next = { ...payment }\n delete next.failureMessage\n return next\n}\n\n/** The merchant's connected account, when it can take card payments now. */\nexport async function posMerchantAccount(\n hostId: string,\n org: Record<string, any> | null,\n): Promise<string | null> {\n const ownerUid = String(org?.['ownerUid'] ?? '')\n if (!ownerUid) return null\n const profile = await firebaseAdmin.app().firestore().collection('profiles').doc(ownerUid).get()\n const accountId = profile.get('stripeAccountId')\n if (\n !merchantAccountIsReady(\n {\n accountId,\n chargesEnabled: profile.get('stripeChargesEnabled'),\n accountLivemode: profile.get('stripeAccountLivemode'),\n },\n { subject: `POS card payment host ${hostId}` },\n )\n ) {\n return null\n }\n return String(accountId)\n}\n\nfunction terminalConfigRef(hostId: string) {\n return firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('terminal')\n .doc('config')\n}\n\nfunction readerRef(hostId: string, readerId: string) {\n return firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('terminalReaders')\n .doc(readerId)\n}\n\n/** A reader id as Stripe mints them; anything else never reaches Stripe. */\nexport function isTerminalReaderId(value: unknown): value is string {\n return typeof value === 'string' && /^tmr_[A-Za-z0-9]{6,64}$/.test(value)\n}\n\n/** The reader, when it is THIS site's; null for anyone else's or none. */\nexport async function ownedTerminalReader(\n hostId: string,\n readerId: unknown,\n): Promise<(PosTerminalReader & { id: string }) | null> {\n if (!isTerminalReaderId(readerId)) return null\n const snapshot = await readerRef(hostId, readerId).get()\n if (!snapshot.exists) return null\n return { ...(snapshot.data() as PosTerminalReader), id: readerId }\n}\n\n/** A Terminal Location's address, as the console sends it. */\nexport interface PosTerminalAddress {\n line1: string\n line2?: string\n city: string\n state?: string\n postalCode: string\n country: string\n}\n\nexport function posTerminalAddressProblem(address: Partial<PosTerminalAddress> | undefined): string | null {\n if (!address?.line1 || !address?.city || !address?.postalCode || !address?.country) {\n return 'Enter the street, city, postal code and country where the reader is used.'\n }\n if (!/^[A-Z]{2}$/.test(String(address.country))) return 'Choose a two-letter country code, like US.'\n return null\n}\n\n/**\n * The idempotency key of a site's Location request. Two presses with the same\n * address share it, so a double tap makes one Location; a corrected address\n * gets its own, since Stripe refuses a key reused with other parameters for a\n * day (a merchant fixing a typo would otherwise read that refusal).\n */\nexport function posTerminalLocationKey(hostId: string, displayName: string, address: PosTerminalAddress): string {\n const fingerprint = createHash('sha256')\n .update(\n JSON.stringify([\n displayName.slice(0, 100),\n address.line1,\n address.line2 ?? '',\n address.city,\n address.state ?? '',\n address.postalCode,\n address.country,\n ]),\n )\n .digest('hex')\n .slice(0, 16)\n return `pos-location:${hostId}:${fingerprint}`\n}\n\n/**\n * The site's Terminal Location, created on first use (on the PLATFORM, with\n * the merchant named in metadata). Its address is the store's, because Stripe\n * uses it to configure the reader for the right country.\n */\nexport async function ensurePosTerminalLocation(options: {\n hostId: string\n orgId: string\n displayName: string\n address?: PosTerminalAddress\n}): Promise<{ ok: true; locationId: string } | { ok: false; status: number; error: string }> {\n const ref = terminalConfigRef(options.hostId)\n const existing = await ref.get()\n const known = String(existing.get('stripeLocationId') ?? '')\n if (known) return { ok: true, locationId: known }\n const problem = posTerminalAddressProblem(options.address)\n if (problem) return { ok: false, status: 400, error: problem }\n const address = options.address as PosTerminalAddress\n const created = await posStripe('POST', 'terminal/locations', {\n idempotencyKey: posTerminalLocationKey(options.hostId, options.displayName, address),\n params: {\n display_name: options.displayName.slice(0, 100) || 'Store',\n 'address[line1]': address.line1,\n 'address[line2]': address.line2 || undefined,\n 'address[city]': address.city,\n 'address[state]': address.state || undefined,\n 'address[postal_code]': address.postalCode,\n 'address[country]': address.country,\n 'metadata[orgId]': options.orgId,\n 'metadata[hostId]': options.hostId,\n },\n })\n if (!created.ok || !created.body?.id) {\n return {\n ok: false,\n status: 502,\n error: posStripeErrorMessage(created.body, 'The reader location could not be set up.'),\n }\n }\n await ref.set(\n {\n stripeLocationId: String(created.body.id),\n livemode: Boolean(created.body.livemode),\n createdAtMs: Date.now(),\n },\n { merge: true },\n )\n // A new Location starts with the store's reader start-up screen (AGL-4048).\n // Never fatal, and loaded only here: it draws images.\n await import('./pos-reader-splash')\n .then((splash) => splash.applyPosReaderSplash(options.hostId))\n .catch((error) => console.error('[pos-terminal] start-up screen not applied', options.hostId, error))\n return { ok: true, locationId: String(created.body.id) }\n}\n\n/** The tipping a reader shows: Stripe offers three buttons, so three presets. */\nexport function posReaderTippingKey(settings: PosRegisterSettings): string {\n return settings.tippingEnabled ? settings.tipPercentages.slice(0, 3).join(',') : 'off'\n}\n\ntype PosStripeParams = Record<string, string | number | boolean | undefined | null | readonly string[]>\n\n/**\n * Writes `params` onto the site's ONE Terminal Configuration, creating it and\n * assigning it to the site's Location on first use. Reader tipping and the\n * start-up screen (AGL-4048) both live on it, because a Location takes one\n * configuration override: a second configuration assigned for the screen\n * would silently drop the tipping presets, and the other way round. Stripe\n * leaves every parameter an update does not name as it was, so each caller\n * sends only its own.\n *\n * The first write is the only race. Tipping's runs as a payment starts, the\n * screen's when the POS devices card saves, and two at once would each make a\n * configuration. So the new id is CLAIMED in a transaction: the loser writes\n * its parameters onto the winner's configuration and deletes its own.\n *\n * `createKey` keys only the create (`pos-configuration:{hostId}:{createKey}`),\n * so a retried first write makes one configuration. An update is not keyed:\n * it sets values, and repeating it changes nothing.\n */\nexport async function savePosReaderConfiguration(\n hostId: string,\n params: PosStripeParams,\n createKey: string,\n): Promise<{ ok: true; configurationId: string } | { ok: false; error: string }> {\n const ref = terminalConfigRef(hostId)\n const config = await ref.get()\n const locationId = String(config.get('stripeLocationId') ?? '')\n if (!locationId) return { ok: false, error: 'no-location' }\n const known = String(config.get('stripeConfigurationId') ?? '')\n if (known) {\n const updated = await posStripe('POST', `terminal/configurations/${known}`, { params })\n if (!updated.ok) return { ok: false, error: String(updated.body?.error?.code ?? updated.status) }\n // A configuration recorded before AGL-4048 was only ever recorded once\n // it was assigned, so only an explicit `false` still owes the assignment.\n if (config.get('stripeConfigurationAssigned') === false) {\n const assigned = await assignPosReaderConfiguration(hostId, locationId, known)\n if (!assigned) return { ok: false, error: 'assignment' }\n }\n return { ok: true, configurationId: known }\n }\n const created = await posStripe('POST', 'terminal/configurations', {\n params,\n idempotencyKey: `pos-configuration:${hostId}:${createKey}`,\n })\n if (!created.ok || !created.body?.id) {\n return { ok: false, error: String(created.body?.error?.code ?? created.status) }\n }\n const createdId = String(created.body.id)\n const winner: string = await firebaseAdmin\n .app()\n .firestore()\n .runTransaction(async (transaction: any) => {\n const fresh = await transaction.get(ref)\n const existing = String(fresh.get('stripeConfigurationId') ?? '')\n if (existing) return existing\n transaction.set(\n ref,\n { stripeConfigurationId: createdId, stripeConfigurationAssigned: false },\n { merge: true },\n )\n return createdId\n })\n if (winner !== createdId) {\n const updated = await posStripe('POST', `terminal/configurations/${winner}`, { params })\n await posStripe('DELETE', `terminal/configurations/${createdId}`).catch(() => undefined)\n if (!updated.ok) return { ok: false, error: String(updated.body?.error?.code ?? updated.status) }\n return { ok: true, configurationId: winner }\n }\n const assigned = await assignPosReaderConfiguration(hostId, locationId, createdId)\n return assigned ? { ok: true, configurationId: createdId } : { ok: false, error: 'assignment' }\n}\n\nasync function assignPosReaderConfiguration(\n hostId: string,\n locationId: string,\n configurationId: string,\n): Promise<boolean> {\n const assigned = await posStripe('POST', `terminal/locations/${locationId}`, {\n params: { configuration_overrides: configurationId },\n })\n if (!assigned.ok) {\n console.error('[pos-terminal] configuration assignment failed', hostId, assigned.body?.error)\n return false\n }\n await terminalConfigRef(hostId).set({ stripeConfigurationAssigned: true }, { merge: true })\n return true\n}\n\n/**\n * Keeps the site's reader tipping in step with the merchant's settings, on\n * the site's one Terminal Configuration ({@link savePosReaderConfiguration}),\n * rewritten only when the presets changed. Never fatal: a reader that keeps\n * the previous presets still takes the payment.\n */\nexport async function syncPosReaderTipping(\n hostId: string,\n settings: PosRegisterSettings,\n): Promise<void> {\n if (!settings.tippingEnabled) return\n const ref = terminalConfigRef(hostId)\n const config = await ref.get()\n if (!config.get('stripeLocationId')) return\n const key = posReaderTippingKey(settings)\n if (config.get('tippingKey') === key) return\n const percentages = settings.tipPercentages.slice(0, 3)\n const saved = await savePosReaderConfiguration(\n hostId,\n { [`tipping[${POS_CURRENCY}][percentages]`]: percentages.map(String) },\n key,\n )\n if ('error' in saved) {\n console.error('[pos-terminal] tipping configuration failed', hostId, saved.error)\n return\n }\n await ref.set({ tippingKey: key }, { merge: true })\n}\n\n/** Reads a reader from Stripe and keeps its status on the site's document. */\nexport async function refreshPosReaderStatus(\n hostId: string,\n readerId: string,\n): Promise<{ status: string; action: any } | null> {\n const fetched = await posStripe('GET', `terminal/readers/${readerId}`)\n if (!fetched.ok) return null\n const status = String(fetched.body?.status ?? 'offline')\n await readerRef(hostId, readerId)\n .set({ status, lastSeenAtMs: Date.now() }, { merge: true })\n .catch(() => undefined)\n return { status, action: fetched.body?.action ?? null }\n}\n\n/*==========================================\n * TAKING A CARD PAYMENT.\n *=========================================*/\n\ninterface CardStart {\n hostId: string\n orderId: string\n paymentId: string\n amountCents: number\n /** A tip already chosen (on the customer display or by the cashier). */\n tipCents: number\n cashierId: string\n org: Record<string, any> | null\n}\n\n/** Reserves a pending card payment in the ledger, or finds the one this attempt made. */\nasync function reserveCardPayment(\n start: CardStart,\n method: CommerceModel.OrderPaymentMethod,\n extra: Partial<CommerceModel.OrderPayment>,\n): Promise<PosPaymentOutcome> {\n return await applyPosPayment({\n hostId: start.hostId,\n orderId: start.orderId,\n paymentId: start.paymentId,\n decide: ({ order, payments, existing }) => {\n if (existing) {\n // The same attempt again: hand back what it started. A failed card\n // is re-armed by `retry`, not by pressing the tender a second time.\n return { kind: 'keep' }\n }\n if (order.status !== 'pending') return { kind: 'refuse', ...NOT_A_SALE }\n const total = Number(order.totals?.totalCents ?? 0)\n const tenderable = CommerceModel.posTenderableCents(total, payments)\n if (!(start.amountCents > 0)) {\n return { kind: 'refuse', status: 400, error: 'Enter an amount to charge.' }\n }\n if (start.amountCents > tenderable) {\n return {\n kind: 'refuse',\n status: 409,\n error:\n tenderable > 0\n ? `Only $${(tenderable / 100).toFixed(2)} is left to pay on this sale.`\n : 'Nothing is left to pay on this sale.',\n }\n }\n const tipProblem = CommerceModel.posTipProblem(start.tipCents, start.amountCents)\n if (tipProblem) return { kind: 'refuse', status: 400, error: tipProblem }\n const takeFeeCents = CommerceModel.posTakeShareCents({\n takeFeeCents: Number(order.posTakeFeeCents ?? 0),\n totalCents: total,\n amountCents: start.amountCents,\n })\n return {\n kind: 'put',\n payment: {\n id: start.paymentId,\n method,\n amountCents: start.amountCents,\n ...(start.tipCents > 0 ? { tipCents: start.tipCents } : {}),\n status: 'pending',\n atMs: Date.now(),\n takeFeeCents,\n cashierId: start.cashierId,\n ...extra,\n },\n }\n },\n })\n}\n\n/** Writes Stripe's answer onto a pending card payment. */\nasync function patchCardPayment(\n hostId: string,\n orderId: string,\n paymentId: string,\n patch: (payment: CommerceModel.OrderPayment) => CommerceModel.OrderPayment | null,\n event?: { event: string; detail?: string },\n): Promise<PosPaymentOutcome> {\n return await applyPosPayment({\n hostId,\n orderId,\n paymentId,\n decide: ({ existing }) => {\n if (!existing) return { kind: 'refuse', status: 404, error: 'Unknown payment' }\n const next = patch(existing)\n if (!next) return { kind: 'keep' }\n return { kind: 'put', payment: next, ...(event ? { event } : {}) }\n },\n })\n}\n\nfunction paymentIntentParams(\n start: CardStart,\n payment: CommerceModel.OrderPayment,\n destination: string,\n extra: Record<string, string | number | boolean | undefined | readonly string[]>,\n) {\n const chargeCents = start.amountCents + start.tipCents\n const fee = posCardApplicationFeeCents({\n takeShareCents: Number(payment.takeFeeCents ?? 0),\n chargeCents,\n })\n return {\n amount: chargeCents,\n currency: POS_CURRENCY,\n 'transfer_data[destination]': destination,\n ...(fee > 0 ? { application_fee_amount: fee } : {}),\n 'metadata[kind]': 'pos',\n 'metadata[hostId]': start.hostId,\n 'metadata[orderId]': start.orderId,\n 'metadata[paymentId]': start.paymentId,\n 'metadata[tipCents]': String(start.tipCents),\n ...extra,\n }\n}\n\n/**\n * Creates (or finds) the card-present PaymentIntent for one payment WITHOUT\n * choosing how the card is collected: reserves the amount in the ledger and\n * creates a manual-capture destination charge to the merchant (settled on\n * Aglyn's platform account, with no `on_behalf_of`, as ToS §10.7 states),\n * keyed by the payment so a retry returns the same intent.\n *\n * Two collectors use it. {@link startCardPresentPayment} pushes the intent\n * to a smart reader the site owns; a native companion app (AGL-3618)\n * collects it with the Stripe Terminal SDK (Tap to Pay, Bluetooth readers)\n * using the returned `clientSecret`. However the card is collected, the\n * payment is recorded by {@link refreshPosCardPayment}, keyed on the intent's\n * metadata, from the register's poll or the webhook alike.\n */\nexport async function createCardPresentIntent(\n start: CardStart & { readerId?: string },\n): Promise<PosPaymentOutcome & { clientSecret?: string; paymentIntentId?: string }> {\n // Live card-present payments wait for Terminal to be switched on for the\n // platform (`STRIPE_TERMINAL_LIVE_ENABLED`); the hidden controls are not\n // the only gate.\n if (!posTerminalAvailable()) {\n return { ok: false, status: 409, error: 'Card readers are not available for this store yet.' }\n }\n const destination = await posMerchantAccount(start.hostId, start.org)\n if (!destination) return { ok: false, status: 409, error: 'Card payments not set up' }\n const reserved = await reserveCardPayment(\n start,\n 'card_present',\n start.readerId ? { readerId: start.readerId } : {},\n )\n if (!reserved.ok || !reserved.payment) return reserved\n const payment = reserved.payment\n if (payment.status !== 'pending') return reserved\n // Keyed by the payment: a second call for the same payment replays the\n // intent Stripe already made rather than opening another.\n const intent = await posStripe('POST', 'payment_intents', {\n idempotencyKey: `pos-pi:${payment.id}`,\n params: paymentIntentParams(start, payment, destination, {\n payment_method_types:\n POS_CURRENCY === 'usd' ? ['card_present'] : ['card_present', 'interac_present'],\n capture_method: 'manual',\n ...(start.readerId ? { 'metadata[readerId]': start.readerId } : {}),\n description: 'In-store purchase',\n }),\n })\n if (!intent.ok || !intent.body?.id) {\n return await patchCardPayment(start.hostId, start.orderId, payment.id, (current) => ({\n ...current,\n status: 'failed',\n failureMessage: posStripeErrorMessage(intent.body, 'The card payment could not be started.'),\n }))\n }\n const paymentIntentId = String(intent.body.id)\n const recorded = payment.paymentIntentId\n ? reserved\n : await patchCardPayment(start.hostId, start.orderId, payment.id, (current) => ({\n ...current,\n paymentIntentId,\n livemode: Boolean(intent.body.livemode),\n }))\n return {\n ...recorded,\n paymentIntentId,\n ...(intent.body.client_secret ? { clientSecret: String(intent.body.client_secret) } : {}),\n }\n}\n\n/**\n * A smart-reader payment: the card-present intent from\n * {@link createCardPresentIntent}, pushed to a reader THIS site owns. The\n * register then polls {@link refreshPosCardPayment} until the reader answers;\n * webhooks land on the same function.\n */\nexport async function startCardPresentPayment(\n start: CardStart & { readerId: string; settings: PosRegisterSettings },\n): Promise<PosPaymentOutcome> {\n const reader = await ownedTerminalReader(start.hostId, start.readerId)\n if (!reader) return { ok: false, status: 404, error: 'That card reader is not on this site.' }\n await syncPosReaderTipping(start.hostId, start.settings).catch((error) =>\n console.error('[pos-terminal] tipping sync failed', error),\n )\n const created = await createCardPresentIntent({ ...start, readerId: reader.id })\n if (!created.ok || !created.payment || !created.paymentIntentId) return created\n if (created.payment.status !== 'pending') return created\n return await sendToReader({\n hostId: start.hostId,\n orderId: start.orderId,\n paymentId: created.payment.id,\n readerId: reader.id,\n paymentIntentId: created.paymentIntentId,\n // A tip the customer already chose on the display is in the amount;\n // asking again on the reader would ask twice.\n skipTipping: !start.settings.tippingEnabled || start.tipCents > 0,\n attempt: 0,\n })\n}\n\n/** The site's Terminal Location id, or null before a reader was ever set up. */\nexport async function posTerminalLocationId(hostId: string): Promise<string | null> {\n const config = await terminalConfigRef(hostId).get()\n return String(config.get('stripeLocationId') ?? '') || null\n}\n\n/**\n * A Terminal SDK connection token scoped to the site's own Location, for a\n * native companion app's SDK (AGL-3618). Scoping it to the Location is what\n * keeps one store's app from discovering another store's readers: Stripe\n * only lets a location-scoped token connect to readers in that Location.\n * Null when the site has no Location yet, never an unscoped token.\n */\nexport async function createPosTerminalConnectionToken(\n hostId: string,\n): Promise<{ secret: string; locationId: string } | null> {\n const locationId = await posTerminalLocationId(hostId)\n if (!locationId) return null\n const token = await posStripe('POST', 'terminal/connection_tokens', {\n params: { location: locationId },\n })\n if (!token.ok || !token.body?.secret) return null\n return { secret: String(token.body.secret), locationId }\n}\n\nasync function sendToReader(input: {\n hostId: string\n orderId: string\n paymentId: string\n readerId: string\n paymentIntentId: string\n skipTipping: boolean\n attempt: number\n}): Promise<PosPaymentOutcome> {\n const processed = await posStripe(\n 'POST',\n `terminal/readers/${input.readerId}/process_payment_intent`,\n {\n idempotencyKey: `pos-process:${input.paymentId}:${input.attempt}`,\n params: {\n payment_intent: input.paymentIntentId,\n 'process_config[enable_customer_cancellation]': true,\n ...(input.skipTipping ? { 'process_config[skip_tipping]': true } : {}),\n },\n },\n )\n if (!processed.ok) {\n return await patchCardPayment(input.hostId, input.orderId, input.paymentId, (current) => ({\n ...current,\n status: 'failed',\n failureMessage: posStripeErrorMessage(processed.body, 'The card reader could not start the payment.'),\n }))\n }\n return await patchCardPayment(input.hostId, input.orderId, input.paymentId, (current) => {\n if (!current.failureMessage) return null\n return withoutFailure(current)\n })\n}\n\n/**\n * Re-arms a declined or failed reader payment on the SAME PaymentIntent, as\n * Stripe asks (a new one per attempt risks a double charge).\n */\nexport async function retryCardPresentPayment(input: {\n hostId: string\n orderId: string\n paymentId: string\n settings: PosRegisterSettings\n}): Promise<PosPaymentOutcome> {\n const order = await readPosSale(input.hostId, input.orderId)\n if (!order || order.status !== 'pending') return NOT_A_SALE\n const payment = CommerceModel.orderPayments(order).find((entry) => entry.id === input.paymentId)\n if (!payment || payment.method !== 'card_present' || !payment.paymentIntentId || !payment.readerId) {\n return { ok: false, status: 404, error: 'Unknown payment' }\n }\n if (payment.status === 'succeeded') return { ok: true, order, payment, completed: false, changed: false }\n const reader = await ownedTerminalReader(input.hostId, payment.readerId)\n if (!reader) return { ok: false, status: 404, error: 'That card reader is not on this site.' }\n // Back to pending first, so the amount is reserved again before the reader\n // is asked; a sale paid meanwhile by another tender refuses here.\n const rearmed = await applyPosPayment({\n hostId: input.hostId,\n orderId: input.orderId,\n paymentId: input.paymentId,\n decide: ({ order: fresh, payments, existing }) => {\n if (!existing) return { kind: 'refuse', status: 404, error: 'Unknown payment' }\n if (existing.status === 'pending') return { kind: 'keep' }\n if (existing.status !== 'failed' && existing.status !== 'canceled') {\n return { kind: 'refuse', status: 409, error: 'This payment cannot be retried.' }\n }\n const tenderable = CommerceModel.posTenderableCents(\n Number(fresh.totals?.totalCents ?? 0),\n payments.filter((entry) => entry.id !== existing.id),\n )\n if (existing.amountCents > tenderable) {\n return { kind: 'refuse', status: 409, error: 'The balance changed. Start a new card payment.' }\n }\n return { kind: 'put', payment: { ...withoutFailure(existing), status: 'pending' } }\n },\n })\n if (!rearmed.ok) return rearmed\n const attempt = Date.now()\n return await sendToReader({\n hostId: input.hostId,\n orderId: input.orderId,\n paymentId: input.paymentId,\n readerId: reader.id,\n paymentIntentId: payment.paymentIntentId,\n skipTipping: !input.settings.tippingEnabled || Number(payment.tipCents ?? 0) > 0,\n attempt,\n })\n}\n\n/**\n * A typed card (card not present): a PaymentIntent the console confirms with\n * the Payment Element. No MOTO: the card is keyed into Stripe's own field and\n * goes through Stripe's normal online checks, 3-D Secure included.\n */\nexport async function startCardKeyedPayment(\n start: CardStart & { paymentMethodId?: string },\n): Promise<PosPaymentOutcome & { clientSecret?: string }> {\n const paymentMethodId = start.paymentMethodId ?? ''\n if (paymentMethodId && !isPaymentMethodId(paymentMethodId)) {\n return { ok: false, status: 400, error: 'The card could not be read. Enter it again.' }\n }\n const destination = await posMerchantAccount(start.hostId, start.org)\n if (!destination) return { ok: false, status: 409, error: 'Card payments not set up' }\n const reserved = await reserveCardPayment(start, 'card_keyed', {})\n if (!reserved.ok || !reserved.payment) return reserved\n const payment = reserved.payment\n if (payment.status !== 'pending') return reserved\n const intent = await posStripe('POST', 'payment_intents', {\n idempotencyKey: `pos-pi:${payment.id}`,\n params: paymentIntentParams(start, payment, destination, {\n payment_method_types: ['card'],\n description: 'In-store purchase (typed card)',\n // 3-D Secure from the one seam every card payment shares (AGL-3360).\n ...cardAuthenticationParams('one-time'),\n }),\n })\n if (!intent.ok || !intent.body?.id || !intent.body?.client_secret) {\n return await patchCardPayment(start.hostId, start.orderId, payment.id, (current) => ({\n ...current,\n status: 'failed',\n failureMessage: posStripeErrorMessage(intent.body, 'The card payment could not be started.'),\n }))\n }\n const updated = payment.paymentIntentId\n ? reserved\n : await patchCardPayment(start.hostId, start.orderId, payment.id, (current) => ({\n ...current,\n paymentIntentId: String(intent.body.id),\n livemode: Boolean(intent.body.livemode),\n }))\n if (!paymentMethodId) return { ...updated, clientSecret: String(intent.body.client_secret) }\n return await confirmKeyedCard(start, payment.id, String(intent.body.id), paymentMethodId)\n}\n\n/** A PaymentMethod id as Stripe mints them; anything else never reaches Stripe. */\nfunction isPaymentMethodId(value: string): boolean {\n return /^pm_[A-Za-z0-9]{8,64}$/.test(value)\n}\n\n/** What a typed card that needs the cardholder's bank to approve it says. */\nexport const KEYED_CARD_NEEDS_AUTHENTICATION =\n 'This card needs the cardholder to approve the payment with their bank, which a typed card cannot do. Use Card (QR) so they can approve it on their phone.'\n\n/**\n * A card typed into the register's own card fields (AGL-3848): the device\n * sent the number to Stripe alone, which answered with a PaymentMethod, and\n * hands this route only its id. The server confirms the intent with it, with\n * the platform's secret key, and records the payment from Stripe's answer as\n * every card payment is. A card whose bank asks for the cardholder's approval\n * (3-D Secure) cannot be approved at the register, so that intent is released\n * and the payment fails with words that point at Card (QR).\n */\nasync function confirmKeyedCard(\n start: CardStart,\n paymentId: string,\n intentId: string,\n paymentMethodId: string,\n): Promise<PosPaymentOutcome> {\n const input = { hostId: start.hostId, orderId: start.orderId, paymentId }\n const confirmed = await posStripe('POST', `payment_intents/${intentId}/confirm`, {\n idempotencyKey: `pos-keyed-confirm:${intentId}:${paymentMethodId}`,\n params: { payment_method: paymentMethodId },\n })\n const status = String(confirmed.body?.status ?? confirmed.body?.error?.payment_intent?.status ?? '')\n if (status === 'requires_action') {\n await posStripe('POST', `payment_intents/${intentId}/cancel`, {\n idempotencyKey: `pos-cancel:${intentId}`,\n })\n return await patchCardPayment(start.hostId, start.orderId, paymentId, (current) =>\n current.status === 'pending'\n ? { ...current, status: 'failed', failureMessage: KEYED_CARD_NEEDS_AUTHENTICATION }\n : null,\n )\n }\n if (!confirmed.ok && confirmed.body?.error && !confirmed.body.error.payment_intent) {\n // Refused before the card was tried (a stale method, a bad request): say so, leave nothing pending.\n const message = posStripeErrorMessage(confirmed.body, 'The card was not accepted.')\n await posStripe('POST', `payment_intents/${intentId}/cancel`, {\n idempotencyKey: `pos-cancel:${intentId}`,\n })\n return await patchCardPayment(start.hostId, start.orderId, paymentId, (current) =>\n current.status === 'pending' ? { ...current, status: 'failed', failureMessage: message } : null,\n )\n }\n // Approved, declined or still processing: Stripe's own read of the intent decides.\n return await refreshPosCardPayment(input)\n}\n\n/**\n * The customer pays on their own phone from a QR code: a Stripe payment page\n * for this payment's amount (and tip, as its own line). Settled by\n * `checkout.session.completed` or by the register's poll.\n */\nexport async function startCardLinkPayment(\n start: CardStart & { returnUrl: string },\n): Promise<PosPaymentOutcome> {\n const destination = await posMerchantAccount(start.hostId, start.org)\n if (!destination) return { ok: false, status: 409, error: 'Card payments not set up' }\n const reserved = await reserveCardPayment(start, 'card_link', {})\n if (!reserved.ok || !reserved.payment) return reserved\n const payment = reserved.payment\n if (payment.checkoutSessionId || payment.status !== 'pending') return reserved\n const fee = posCardApplicationFeeCents({\n takeShareCents: Number(payment.takeFeeCents ?? 0),\n chargeCents: start.amountCents + start.tipCents,\n })\n const session = await posStripe('POST', 'checkout/sessions', {\n idempotencyKey: `pos-link:${payment.id}`,\n params: {\n mode: 'payment',\n ...checkoutSessionCardAuthenticationParams('payment'),\n 'payment_method_types': ['card'],\n 'line_items[0][quantity]': 1,\n 'line_items[0][price_data][currency]': POS_CURRENCY,\n 'line_items[0][price_data][unit_amount]': start.amountCents,\n 'line_items[0][price_data][product_data][name]': 'In-store purchase',\n ...(start.tipCents > 0\n ? {\n 'line_items[1][quantity]': 1,\n 'line_items[1][price_data][currency]': POS_CURRENCY,\n 'line_items[1][price_data][unit_amount]': start.tipCents,\n 'line_items[1][price_data][product_data][name]': 'Tip',\n }\n : {}),\n 'payment_intent_data[transfer_data][destination]': destination,\n ...(fee > 0 ? { 'payment_intent_data[application_fee_amount]': fee } : {}),\n 'payment_intent_data[metadata][kind]': 'pos',\n 'payment_intent_data[metadata][hostId]': start.hostId,\n 'payment_intent_data[metadata][orderId]': start.orderId,\n 'payment_intent_data[metadata][paymentId]': payment.id,\n 'payment_intent_data[metadata][tipCents]': String(start.tipCents),\n success_url: `${start.returnUrl}?paid=1`,\n cancel_url: `${start.returnUrl}?paid=0`,\n 'metadata[type]': 'pos-payment',\n 'metadata[hostId]': start.hostId,\n 'metadata[orderId]': start.orderId,\n 'metadata[paymentId]': payment.id,\n },\n })\n if (!session.ok || !session.body?.url) {\n return await patchCardPayment(start.hostId, start.orderId, payment.id, (current) => ({\n ...current,\n status: 'failed',\n failureMessage: posStripeErrorMessage(session.body, 'The payment page could not be created.'),\n }))\n }\n return await patchCardPayment(start.hostId, start.orderId, payment.id, (current) => ({\n ...current,\n checkoutSessionId: String(session.body.id),\n checkoutUrl: String(session.body.url),\n livemode: Boolean(session.body.livemode),\n }))\n}\n\n/*==========================================\n * SETTLING A CARD PAYMENT.\n *=========================================*/\n\n/** The card's brand and last four, off the charge Stripe made. */\nfunction cardDetailsOf(intent: any): { cardBrand?: string; last4?: string } {\n const charge = typeof intent?.latest_charge === 'object' ? intent.latest_charge : null\n const details =\n charge?.payment_method_details?.card_present ??\n charge?.payment_method_details?.interac_present ??\n charge?.payment_method_details?.card ??\n null\n return {\n ...(details?.brand ? { cardBrand: String(details.brand) } : {}),\n ...(details?.last4 ? { last4: String(details.last4) } : {}),\n }\n}\n\n/** The tip on an intent: what the display chose plus what the reader added. */\nexport function posIntentTipCents(intent: any): number {\n const preset = Math.max(0, Math.round(Number(intent?.metadata?.tipCents ?? 0)) || 0)\n const onReader = Math.max(0, Math.round(Number(intent?.amount_details?.tip?.amount ?? 0)) || 0)\n return preset + onReader\n}\n\n/**\n * The fee a register card charge is captured with: the payment's share of the\n * take (fixed at reservation, the sale's share and never the tip's) plus the\n * card processing cost on the whole amount, tip included.\n */\nexport function posCaptureFeeCents(payment: CommerceModel.OrderPayment, intent: any): number {\n return posCardApplicationFeeCents({\n takeShareCents: Number(payment.takeFeeCents ?? 0),\n chargeCents: Math.max(0, Math.round(Number(intent?.amount ?? 0))),\n })\n}\n\n/**\n * Brings one card payment up to date with Stripe, from a webhook or the\n * register's poll alike: captures an authorized reader payment with its final\n * fee, records a success, a decline or a cancellation. Idempotent: the\n * capture is keyed by the intent, and the ledger change is a no-op when the\n * payment already says what Stripe says.\n */\nexport async function refreshPosCardPayment(input: {\n hostId: string\n orderId: string\n paymentId: string\n}): Promise<PosPaymentOutcome> {\n const order = await readPosSale(input.hostId, input.orderId)\n if (!order) return { ok: false, status: 404, error: 'Unknown sale' }\n const payment = CommerceModel.orderPayments(order).find((entry) => entry.id === input.paymentId)\n if (!payment) return { ok: false, status: 404, error: 'Unknown payment' }\n if (!CommerceModel.isCardPaymentMethod(payment.method)) {\n return { ok: true, order, payment, completed: false, changed: false }\n }\n let paymentIntentId = payment.paymentIntentId ?? ''\n if (!paymentIntentId && payment.checkoutSessionId) {\n const session = await posStripe('GET', `checkout/sessions/${payment.checkoutSessionId}`)\n if (session.ok && session.body?.status === 'expired' && payment.status === 'pending') {\n return await patchCardPayment(input.hostId, input.orderId, payment.id, (current) =>\n current.status === 'pending' ? { ...current, status: 'canceled' } : null,\n )\n }\n paymentIntentId = String(session.body?.payment_intent ?? '')\n }\n if (!paymentIntentId) return { ok: true, order, payment, completed: false, changed: false }\n let intent = (\n await posStripe('GET', `payment_intents/${paymentIntentId}`, {\n params: { 'expand[]': 'latest_charge' },\n })\n ).body\n if (!intent?.id) return { ok: true, order, payment, completed: false, changed: false }\n if (intent.metadata?.hostId !== input.hostId || intent.metadata?.paymentId !== payment.id) {\n console.error('[pos-terminal] intent does not belong to this payment', paymentIntentId)\n return { ok: false, status: 409, error: 'This payment does not match its card charge.' }\n }\n if (intent.status === 'requires_capture') {\n if (order.status !== 'pending' && payment.status !== 'pending') {\n // Nothing to capture into: the sale is gone. Release the hold.\n await posStripe('POST', `payment_intents/${intent.id}/cancel`, {\n idempotencyKey: `pos-release:${intent.id}`,\n })\n return { ok: true, order, payment, completed: false, changed: false }\n }\n const fee = posCaptureFeeCents(payment, intent)\n const captured = await posStripe('POST', `payment_intents/${intent.id}/capture`, {\n idempotencyKey: `pos-capture:${intent.id}`,\n params: {\n ...(fee > 0 ? { application_fee_amount: fee } : {}),\n 'expand[]': 'latest_charge',\n },\n })\n if (captured.ok && captured.body?.id) intent = captured.body\n else {\n console.error('[pos-terminal] capture failed', intent.id, captured.body?.error)\n return { ok: true, order, payment, completed: false, changed: false }\n }\n }\n if (intent.status === 'succeeded') {\n const tipCents = posIntentTipCents(intent)\n const amountCents = Math.max(0, Math.round(Number(intent.amount ?? 0))) - tipCents\n return await patchCardPayment(\n input.hostId,\n input.orderId,\n payment.id,\n (current) => {\n if (current.status === 'succeeded' && current.paymentIntentId === intent.id) return null\n return {\n ...withoutFailure(current),\n status: 'succeeded',\n settledAtMs: Date.now(),\n paymentIntentId: String(intent.id),\n // What the charge actually put toward the sale. Equal to the\n // reserved amount; restated from Stripe so the ledger records the\n // money that moved rather than the money that was asked for.\n amountCents: amountCents > 0 ? amountCents : current.amountCents,\n ...(tipCents > 0 ? { tipCents } : {}),\n feeCents: Math.max(0, Math.round(Number(intent.application_fee_amount ?? 0))),\n livemode: Boolean(intent.livemode),\n ...cardDetailsOf(intent),\n }\n },\n )\n }\n if (intent.status === 'canceled') {\n return await patchCardPayment(input.hostId, input.orderId, payment.id, (current) =>\n current.status === 'pending' || current.status === 'failed'\n ? { ...current, status: 'canceled' }\n : null,\n )\n }\n if (intent.status === 'requires_payment_method' && intent.last_payment_error) {\n const message = String(intent.last_payment_error?.message ?? 'The card was declined.')\n return await patchCardPayment(input.hostId, input.orderId, payment.id, (current) =>\n current.status === 'pending' ? { ...current, status: 'failed', failureMessage: message } : null,\n )\n }\n // Still waiting on the customer. A reader action that already failed (a\n // card removed, a cancel on the reader) says so on the reader, not the\n // intent, so ask the reader too.\n if (payment.method === 'card_present' && payment.readerId && payment.status === 'pending') {\n const reader = await refreshPosReaderStatus(input.hostId, payment.readerId)\n const action = reader?.action\n if (\n action?.type === 'process_payment_intent' &&\n action?.process_payment_intent?.payment_intent === intent.id &&\n action?.status === 'failed'\n ) {\n const message = String(action.failure_message ?? 'The reader could not take the card.')\n return await patchCardPayment(input.hostId, input.orderId, payment.id, (current) =>\n current.status === 'pending' ? { ...current, status: 'failed', failureMessage: message } : null,\n )\n }\n }\n const fresh = await readPosSale(input.hostId, input.orderId)\n return {\n ok: true,\n order: fresh ?? order,\n payment:\n CommerceModel.orderPayments(fresh ?? order).find((entry) => entry.id === payment.id) ?? payment,\n completed: false,\n changed: false,\n }\n}\n\n/**\n * Stops a card payment that has not been paid: clears the reader, cancels the\n * intent, expires the payment page. A charge that already went through is\n * left alone and recorded as paid, never cancelled under the customer.\n */\nexport async function cancelPosCardPayment(input: {\n hostId: string\n orderId: string\n paymentId: string\n}): Promise<PosPaymentOutcome> {\n const refreshed = await refreshPosCardPayment(input)\n if (!refreshed.ok || !refreshed.payment) return refreshed\n const payment = refreshed.payment\n if (payment.status === 'succeeded' || payment.status === 'canceled') return refreshed\n if (payment.method === 'card_present' && payment.readerId) {\n const cleared = await posStripe('POST', `terminal/readers/${payment.readerId}/cancel_action`)\n if (!cleared.ok && cleared.body?.error?.code === 'terminal_reader_busy') {\n return {\n ok: false,\n status: 409,\n error: 'The customer is mid-payment on the reader. Wait for it to finish.',\n }\n }\n }\n if (payment.paymentIntentId) {\n await posStripe('POST', `payment_intents/${payment.paymentIntentId}/cancel`, {\n idempotencyKey: `pos-cancel:${payment.paymentIntentId}`,\n })\n }\n if (payment.checkoutSessionId) {\n await posStripe('POST', `checkout/sessions/${payment.checkoutSessionId}/expire`, {\n idempotencyKey: `pos-expire:${payment.checkoutSessionId}`,\n })\n }\n // Re-read: a payment that completed in the gap is recorded as paid.\n const settled = await refreshPosCardPayment(input)\n if (settled.ok && settled.payment?.status === 'succeeded') return settled\n return await patchCardPayment(input.hostId, input.orderId, payment.id, (current) =>\n current.status === 'pending' || current.status === 'failed'\n ? { ...current, status: 'canceled' }\n : null,\n )\n}\n\n/**\n * TEST MODE ONLY: Stripe's simulated reader is \"tapped\" with a test card.\n * Refused outright on a live key, whatever the request says.\n */\nexport async function simulatePosReaderTap(input: {\n hostId: string\n orderId: string\n paymentId: string\n /** `4000000000000002` declines; anything else Stripe's default approves. */\n cardNumber?: string\n}): Promise<PosPaymentOutcome> {\n if (!posStripeTestMode()) return { ok: false, status: 403, error: 'Only in test mode' }\n const order = await readPosSale(input.hostId, input.orderId)\n if (!order) return { ok: false, status: 404, error: 'Unknown sale' }\n const payment = CommerceModel.orderPayments(order).find((entry) => entry.id === input.paymentId)\n if (!payment?.readerId || payment.method !== 'card_present') {\n return { ok: false, status: 404, error: 'Unknown payment' }\n }\n const reader = await ownedTerminalReader(input.hostId, payment.readerId)\n if (!reader) return { ok: false, status: 404, error: 'That card reader is not on this site.' }\n const presented = await posStripe(\n 'POST',\n `test_helpers/terminal/readers/${reader.id}/present_payment_method`,\n {\n params: {\n type: 'card_present',\n ...(input.cardNumber && /^[0-9]{12,19}$/.test(input.cardNumber)\n ? { 'card_present[number]': input.cardNumber }\n : {}),\n },\n },\n )\n if (!presented.ok) {\n return {\n ok: false,\n status: 502,\n error: posStripeErrorMessage(presented.body, 'The simulated tap failed.'),\n }\n }\n return await refreshPosCardPayment(input)\n}\n\n/*==========================================\n * WEBHOOKS (the small branch `billing-webhook.ts` calls).\n *=========================================*/\n\n/** Where a Stripe object says a register payment lives, or null. */\nfunction posPaymentRef(\n metadata: Record<string, unknown> | undefined,\n): { hostId: string; orderId: string; paymentId: string } | null {\n const hostId = String(metadata?.['hostId'] ?? '')\n const orderId = String(metadata?.['orderId'] ?? '')\n const paymentId = String(metadata?.['paymentId'] ?? '')\n if (!hostId || !orderId || !paymentId) return null\n return { hostId, orderId, paymentId }\n}\n\n/**\n * The register's Stripe events: a reader finished or failed an action, a\n * register PaymentIntent was authorized, paid, declined or cancelled, or a\n * register QR page was paid or expired. Each resolves to ONE payment and is\n * answered by re-reading Stripe through {@link refreshPosCardPayment}, never\n * by trusting the event's own copy (whose API version predates the tip\n * fields). Returns the site when the event was the register's, so the\n * dispatcher can count it as claimed; null for every other event.\n */\nexport async function handlePosStripeEvent(input: {\n type: string\n object: any\n}): Promise<{ hostId: string } | null> {\n const { type, object } = input\n let ref: { hostId: string; orderId: string; paymentId: string } | null = null\n if (type === 'terminal.reader.action_succeeded' || type === 'terminal.reader.action_failed') {\n const intentId = String(object?.action?.process_payment_intent?.payment_intent ?? '')\n if (object?.action?.type !== 'process_payment_intent' || !intentId) return null\n const intent = await posStripe('GET', `payment_intents/${intentId}`)\n if (!intent.ok || intent.body?.metadata?.kind !== 'pos') return null\n ref = posPaymentRef(intent.body.metadata)\n } else if (\n type === 'payment_intent.amount_capturable_updated' ||\n type === 'payment_intent.succeeded' ||\n type === 'payment_intent.payment_failed' ||\n type === 'payment_intent.canceled'\n ) {\n if (object?.metadata?.kind !== 'pos') return null\n ref = posPaymentRef(object.metadata)\n } else if (\n (type === 'checkout.session.completed' || type === 'checkout.session.expired') &&\n object?.metadata?.type === 'pos-payment'\n ) {\n ref = posPaymentRef(object.metadata)\n } else {\n return null\n }\n if (!ref) return null\n // A reader can be re-registered by nobody else, but an event is only a\n // claim: the intent's own metadata must name a reader this site owns.\n await refreshPosCardPayment(ref).catch((error) => {\n console.error('[pos-terminal] webhook settle failed', ref, error)\n // Rethrown so the delivery is retried: a lost settlement is a sale the\n // register cannot complete without its poll.\n throw error\n })\n return { hostId: ref.hostId }\n}\n\nexport type { PosLiftedOrder }\n\n/*==========================================\n * CARDS TAKEN OFFLINE ON THE APP'S OWN READER (AGL-4039).\n *\n * The Aglyn POS app creates these PaymentIntents itself (Stripe stores the\n * card on the device and forwards it once a connection returns), so the\n * server meets them only at the sale's sync: it reads the intent, checks it\n * is the sale's, and captures it with the platform fee, the same capture an\n * online reader payment gets after its tip is known.\n *=========================================*/\n\n/** A PaymentIntent the offline sync reads, reduced to what it checks. */\nexport interface PosOfflineCardIntent {\n id: string\n status: string\n amount: number\n currency: string\n metadata: Record<string, string>\n destination: string\n applicationFeeCents: number\n livemode: boolean\n brand?: string\n last4?: string\n failureMessage?: string\n}\n\nfunction offlineCardIntent(body: any): PosOfflineCardIntent {\n const charge = body?.latest_charge && typeof body.latest_charge === 'object' ? body.latest_charge : null\n const present = charge?.payment_method_details?.card_present ?? {}\n const error = body?.last_payment_error ?? null\n return {\n id: String(body?.id ?? ''),\n status: String(body?.status ?? ''),\n amount: Number(body?.amount ?? 0),\n currency: String(body?.currency ?? ''),\n metadata: (body?.metadata ?? {}) as Record<string, string>,\n destination: String(body?.transfer_data?.destination ?? ''),\n applicationFeeCents: Number(body?.application_fee_amount ?? 0),\n livemode: Boolean(body?.livemode),\n ...(present.brand ? { brand: String(present.brand) } : {}),\n ...(present.last4 ? { last4: String(present.last4) } : {}),\n ...(error ? { failureMessage: String(error.message ?? error.decline_code ?? 'The card was declined.') } : {}),\n }\n}\n\n/** Reads one offline card's PaymentIntent, with its charge's card details. */\nexport async function retrievePosOfflineCardIntent(\n paymentIntentId: string,\n): Promise<{ ok: true; intent: PosOfflineCardIntent } | { ok: false; error: string }> {\n const fetched = await posStripe('GET', `payment_intents/${encodeURIComponent(paymentIntentId)}`, {\n params: { 'expand[]': ['latest_charge'] },\n })\n if (!fetched.ok) return { ok: false, error: posStripeErrorMessage(fetched.body, 'The card payment could not be read.') }\n return { ok: true, intent: offlineCardIntent(fetched.body) }\n}\n\n/** Captures one authorized offline card with the platform's fee; a repeat finds the first capture. */\nexport async function capturePosOfflineCardIntent(\n paymentIntentId: string,\n applicationFeeCents: number,\n idempotencyKey: string,\n): Promise<{ ok: true; intent: PosOfflineCardIntent } | { ok: false; error: string }> {\n const captured = await posStripe('POST', `payment_intents/${encodeURIComponent(paymentIntentId)}/capture`, {\n idempotencyKey,\n params: {\n ...(applicationFeeCents > 0 ? { application_fee_amount: applicationFeeCents } : {}),\n 'expand[]': ['latest_charge'],\n },\n })\n if (!captured.ok) return { ok: false, error: posStripeErrorMessage(captured.body, 'The card payment could not be captured.') }\n return { ok: true, intent: offlineCardIntent(captured.body) }\n}\n\n/**\n * Turns Stripe's offline mode on for the site's readers: the same per-site\n * Terminal Configuration the reader tipping uses, with `offline[enabled]`,\n * assigned to the site's Location. Done once; a reader picks it up the next\n * time it connects online. False when Stripe refused it.\n */\nexport async function ensurePosOfflineCardConfiguration(hostId: string): Promise<boolean> {\n const ref = terminalConfigRef(hostId)\n const config = await ref.get()\n if (config.get('offlineEnabled') === true) return true\n const locationId = String(config.get('stripeLocationId') ?? '')\n if (!locationId) return false\n const configurationId = String(config.get('stripeConfigurationId') ?? '')\n const saved = await posStripe(\n 'POST',\n configurationId ? `terminal/configurations/${configurationId}` : 'terminal/configurations',\n {\n params: { 'offline[enabled]': true },\n ...(configurationId ? {} : { idempotencyKey: `pos-configuration:${hostId}:offline` }),\n },\n )\n if (!saved.ok || !saved.body?.id) {\n console.error('[pos-terminal] offline configuration failed', hostId, saved.body?.error)\n return false\n }\n if (!configurationId) {\n const assigned = await posStripe('POST', `terminal/locations/${locationId}`, {\n params: { configuration_overrides: String(saved.body.id) },\n })\n if (!assigned.ok) {\n console.error('[pos-terminal] offline configuration assignment failed', hostId, assigned.body?.error)\n return false\n }\n }\n await ref.set({ stripeConfigurationId: String(saved.body.id), offlineEnabled: true }, { merge: true })\n return true\n}\n"],"names":["createHash","firebaseAdmin","merchantAccountIsReady","cardAuthenticationParams","checkoutSessionCardAuthenticationParams","CommerceModel","POS_CURRENCY","posCardApplicationFeeCents","posStripe","posStripeErrorMessage","posStripeTestMode","posTerminalAvailable","applyPosPayment","readPosSale","NOT_A_SALE","ok","status","error","withoutFailure","payment","next","failureMessage","posMerchantAccount","hostId","org","ownerUid","String","profile","app","firestore","collection","doc","get","accountId","chargesEnabled","accountLivemode","subject","terminalConfigRef","readerRef","readerId","isTerminalReaderId","value","test","ownedTerminalReader","snapshot","exists","data","id","posTerminalAddressProblem","address","line1","city","postalCode","country","posTerminalLocationKey","displayName","fingerprint","update","JSON","stringify","slice","line2","state","digest","ensurePosTerminalLocation","options","existing","created","ref","known","locationId","problem","idempotencyKey","params","display_name","undefined","orgId","body","set","stripeLocationId","livemode","Boolean","createdAtMs","Date","now","merge","then","splash","applyPosReaderSplash","catch","console","posReaderTippingKey","settings","tippingEnabled","tipPercentages","join","savePosReaderConfiguration","createKey","config","updated","code","assigned","assignPosReaderConfiguration","configurationId","createdId","winner","runTransaction","transaction","fresh","stripeConfigurationId","stripeConfigurationAssigned","configuration_overrides","syncPosReaderTipping","key","percentages","saved","map","tippingKey","refreshPosReaderStatus","fetched","lastSeenAtMs","action","reserveCardPayment","start","method","extra","orderId","paymentId","decide","order","payments","kind","total","Number","totals","totalCents","tenderable","posTenderableCents","amountCents","toFixed","tipProblem","posTipProblem","tipCents","takeFeeCents","posTakeShareCents","posTakeFeeCents","atMs","cashierId","patchCardPayment","patch","event","paymentIntentParams","destination","chargeCents","fee","takeShareCents","amount","currency","application_fee_amount","createCardPresentIntent","intent","reserved","payment_method_types","capture_method","description","current","paymentIntentId","recorded","client_secret","clientSecret","startCardPresentPayment","reader","sendToReader","skipTipping","attempt","posTerminalLocationId","createPosTerminalConnectionToken","token","location","secret","input","processed","payment_intent","retryCardPresentPayment","orderPayments","find","entry","completed","changed","rearmed","filter","startCardKeyedPayment","paymentMethodId","isPaymentMethodId","confirmKeyedCard","KEYED_CARD_NEEDS_AUTHENTICATION","intentId","confirmed","payment_method","message","refreshPosCardPayment","startCardLinkPayment","session","checkoutSessionId","mode","success_url","returnUrl","cancel_url","url","checkoutUrl","cardDetailsOf","charge","latest_charge","details","payment_method_details","card_present","interac_present","card","brand","cardBrand","last4","posIntentTipCents","preset","Math","max","round","metadata","onReader","amount_details","tip","posCaptureFeeCents","isCardPaymentMethod","captured","settledAtMs","feeCents","last_payment_error","type","process_payment_intent","failure_message","cancelPosCardPayment","settled","refreshed","cleared","simulatePosReaderTap","presented","cardNumber","posPaymentRef","handlePosStripeEvent","object","offlineCardIntent","present","transfer_data","applicationFeeCents","decline_code","retrievePosOfflineCardIntent","encodeURIComponent","capturePosOfflineCardIntent","ensurePosOfflineCardConfiguration","offlineEnabled"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,UAAU,QAAQ,cAAa;AACxC,SAASC,aAAa,QAAQ,2BAA0B;AACxD,SAASC,sBAAsB,QAAQ,mDAAkD;AACzF,SACEC,wBAAwB,EACxBC,uCAAuC,QAClC,6DAA4D;AACnE,YAAYC,mBAAmB,oBAAU;AAEzC,SACEC,YAAY,EACZC,0BAA0B,EAC1BC,SAAS,EACTC,qBAAqB,EACrBC,iBAAiB,EACjBC,oBAAoB,QACf,kBAAc;AACrB,SAASC,eAAe,EAAEC,WAAW,QAAqD,gBAAY;AAEtG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2CA+B2C,GAE3C,MAAMC,aAAa;IAAEC,IAAI;IAAgBC,QAAQ;IAAKC,OAAO;AAA8B;AAgB3F;;;CAGC,GACD,SAASC,eAAeC,OAAmC;IACzD,MAAMC,OAAO,aAAKD;IAClB,OAAOC,KAAKC,cAAc;IAC1B,OAAOD;AACT;AAEA,0EAA0E,GAC1E,OAAO,eAAeE,mBACpBC,MAAc,EACdC,GAA+B;;IAE/B,MAAMC,WAAWC,eAAOF,uBAAAA,GAAK,CAAC,WAAW,mBAAI;IAC7C,IAAI,CAACC,UAAU,OAAO;IACtB,MAAME,UAAU,MAAM1B,cAAc2B,GAAG,GAAGC,SAAS,GAAGC,UAAU,CAAC,YAAYC,GAAG,CAACN,UAAUO,GAAG;IAC9F,MAAMC,YAAYN,QAAQK,GAAG,CAAC;IAC9B,IACE,CAAC9B,uBACC;QACE+B;QACAC,gBAAgBP,QAAQK,GAAG,CAAC;QAC5BG,iBAAiBR,QAAQK,GAAG,CAAC;IAC/B,GACA;QAAEI,SAAS,CAAC,sBAAsB,EAAEb,QAAQ;IAAC,IAE/C;QACA,OAAO;IACT;IACA,OAAOG,OAAOO;AAChB;AAEA,SAASI,kBAAkBd,MAAc;IACvC,OAAOtB,cACJ2B,GAAG,GACHC,SAAS,GACTC,UAAU,CAAC,SACXC,GAAG,CAACR,QACJO,UAAU,CAAC,YACXC,GAAG,CAAC;AACT;AAEA,SAASO,UAAUf,MAAc,EAAEgB,QAAgB;IACjD,OAAOtC,cACJ2B,GAAG,GACHC,SAAS,GACTC,UAAU,CAAC,SACXC,GAAG,CAACR,QACJO,UAAU,CAAC,mBACXC,GAAG,CAACQ;AACT;AAEA,0EAA0E,GAC1E,OAAO,SAASC,mBAAmBC,KAAc;IAC/C,OAAO,OAAOA,UAAU,YAAY,0BAA0BC,IAAI,CAACD;AACrE;AAEA,wEAAwE,GACxE,OAAO,eAAeE,oBACpBpB,MAAc,EACdgB,QAAiB;IAEjB,IAAI,CAACC,mBAAmBD,WAAW,OAAO;IAC1C,MAAMK,WAAW,MAAMN,UAAUf,QAAQgB,UAAUP,GAAG;IACtD,IAAI,CAACY,SAASC,MAAM,EAAE,OAAO;IAC7B,OAAO,aAAMD,SAASE,IAAI;QAA0BC,IAAIR;;AAC1D;AAYA,OAAO,SAASS,0BAA0BC,OAAgD;IACxF,IAAI,EAACA,2BAAAA,QAASC,KAAK,KAAI,EAACD,2BAAAA,QAASE,IAAI,KAAI,EAACF,2BAAAA,QAASG,UAAU,KAAI,EAACH,2BAAAA,QAASI,OAAO,GAAE;QAClF,OAAO;IACT;IACA,IAAI,CAAC,aAAaX,IAAI,CAAChB,OAAOuB,QAAQI,OAAO,IAAI,OAAO;IACxD,OAAO;AACT;AAEA;;;;;CAKC,GACD,OAAO,SAASC,uBAAuB/B,MAAc,EAAEgC,WAAmB,EAAEN,OAA2B;QAM/FA,gBAEAA;IAPN,MAAMO,cAAcxD,WAAW,UAC5ByD,MAAM,CACLC,KAAKC,SAAS,CAAC;QACbJ,YAAYK,KAAK,CAAC,GAAG;QACrBX,QAAQC,KAAK;SACbD,iBAAAA,QAAQY,KAAK,YAAbZ,iBAAiB;QACjBA,QAAQE,IAAI;SACZF,iBAAAA,QAAQa,KAAK,YAAbb,iBAAiB;QACjBA,QAAQG,UAAU;QAClBH,QAAQI,OAAO;KAChB,GAEFU,MAAM,CAAC,OACPH,KAAK,CAAC,GAAG;IACZ,OAAO,CAAC,aAAa,EAAErC,OAAO,CAAC,EAAEiC,aAAa;AAChD;AAEA;;;;CAIC,GACD,OAAO,eAAeQ,0BAA0BC,OAK/C;QAGsBC;QAmBDC;IArBpB,MAAMC,MAAM/B,kBAAkB4B,QAAQ1C,MAAM;IAC5C,MAAM2C,WAAW,MAAME,IAAIpC,GAAG;IAC9B,MAAMqC,QAAQ3C,QAAOwC,gBAAAA,SAASlC,GAAG,CAAC,+BAAbkC,gBAAoC;IACzD,IAAIG,OAAO,OAAO;QAAEtD,IAAI;QAAMuD,YAAYD;IAAM;IAChD,MAAME,UAAUvB,0BAA0BiB,QAAQhB,OAAO;IACzD,IAAIsB,SAAS,OAAO;QAAExD,IAAI;QAAOC,QAAQ;QAAKC,OAAOsD;IAAQ;IAC7D,MAAMtB,UAAUgB,QAAQhB,OAAO;IAC/B,MAAMkB,UAAU,MAAM3D,UAAU,QAAQ,sBAAsB;QAC5DgE,gBAAgBlB,uBAAuBW,QAAQ1C,MAAM,EAAE0C,QAAQV,WAAW,EAAEN;QAC5EwB,QAAQ;YACNC,cAAcT,QAAQV,WAAW,CAACK,KAAK,CAAC,GAAG,QAAQ;YACnD,kBAAkBX,QAAQC,KAAK;YAC/B,kBAAkBD,QAAQY,KAAK,IAAIc;YACnC,iBAAiB1B,QAAQE,IAAI;YAC7B,kBAAkBF,QAAQa,KAAK,IAAIa;YACnC,wBAAwB1B,QAAQG,UAAU;YAC1C,oBAAoBH,QAAQI,OAAO;YACnC,mBAAmBY,QAAQW,KAAK;YAChC,oBAAoBX,QAAQ1C,MAAM;QACpC;IACF;IACA,IAAI,CAAC4C,QAAQpD,EAAE,IAAI,GAACoD,gBAAAA,QAAQU,IAAI,qBAAZV,cAAcpB,EAAE,GAAE;QACpC,OAAO;YACLhC,IAAI;YACJC,QAAQ;YACRC,OAAOR,sBAAsB0D,QAAQU,IAAI,EAAE;QAC7C;IACF;IACA,MAAMT,IAAIU,GAAG,CACX;QACEC,kBAAkBrD,OAAOyC,QAAQU,IAAI,CAAC9B,EAAE;QACxCiC,UAAUC,QAAQd,QAAQU,IAAI,CAACG,QAAQ;QACvCE,aAAaC,KAAKC,GAAG;IACvB,GACA;QAAEC,OAAO;IAAK;IAEhB,4EAA4E;IAC5E,sDAAsD;IACtD,MAAM,MAAM,CAAC,0BACVC,IAAI,CAAC,CAACC,SAAWA,OAAOC,oBAAoB,CAACvB,QAAQ1C,MAAM,GAC3DkE,KAAK,CAAC,CAACxE,QAAUyE,QAAQzE,KAAK,CAAC,8CAA8CgD,QAAQ1C,MAAM,EAAEN;IAChG,OAAO;QAAEF,IAAI;QAAMuD,YAAY5C,OAAOyC,QAAQU,IAAI,CAAC9B,EAAE;IAAE;AACzD;AAEA,+EAA+E,GAC/E,OAAO,SAAS4C,oBAAoBC,QAA6B;IAC/D,OAAOA,SAASC,cAAc,GAAGD,SAASE,cAAc,CAAClC,KAAK,CAAC,GAAG,GAAGmC,IAAI,CAAC,OAAO;AACnF;AAIA;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,eAAeC,2BACpBzE,MAAc,EACdkD,MAAuB,EACvBwB,SAAiB;QAISC,aAELA;QAgBD/B;IApBpB,MAAMC,MAAM/B,kBAAkBd;IAC9B,MAAM2E,SAAS,MAAM9B,IAAIpC,GAAG;IAC5B,MAAMsC,aAAa5C,QAAOwE,cAAAA,OAAOlE,GAAG,CAAC,+BAAXkE,cAAkC;IAC5D,IAAI,CAAC5B,YAAY,OAAO;QAAEvD,IAAI;QAAOE,OAAO;IAAc;IAC1D,MAAMoD,QAAQ3C,QAAOwE,eAAAA,OAAOlE,GAAG,CAAC,oCAAXkE,eAAuC;IAC5D,IAAI7B,OAAO;;YAE0C8B,qBAAAA;QADnD,MAAMA,UAAU,MAAM3F,UAAU,QAAQ,CAAC,wBAAwB,EAAE6D,OAAO,EAAE;YAAEI;QAAO;QACrF,IAAI,CAAC0B,QAAQpF,EAAE,EAAE,OAAO;YAAEA,IAAI;YAAOE,OAAOS,gBAAOyE,gBAAAA,QAAQtB,IAAI,sBAAZsB,sBAAAA,cAAclF,KAAK,qBAAnBkF,oBAAqBC,IAAI,mBAAID,QAAQnF,MAAM;QAAE;QAChG,uEAAuE;QACvE,0EAA0E;QAC1E,IAAIkF,OAAOlE,GAAG,CAAC,mCAAmC,OAAO;YACvD,MAAMqE,WAAW,MAAMC,6BAA6B/E,QAAQ+C,YAAYD;YACxE,IAAI,CAACgC,UAAU,OAAO;gBAAEtF,IAAI;gBAAOE,OAAO;YAAa;QACzD;QACA,OAAO;YAAEF,IAAI;YAAMwF,iBAAiBlC;QAAM;IAC5C;IACA,MAAMF,UAAU,MAAM3D,UAAU,QAAQ,2BAA2B;QACjEiE;QACAD,gBAAgB,CAAC,kBAAkB,EAAEjD,OAAO,CAAC,EAAE0E,WAAW;IAC5D;IACA,IAAI,CAAC9B,QAAQpD,EAAE,IAAI,GAACoD,gBAAAA,QAAQU,IAAI,qBAAZV,cAAcpB,EAAE,GAAE;;YACFoB,qBAAAA;QAAlC,OAAO;YAAEpD,IAAI;YAAOE,OAAOS,iBAAOyC,iBAAAA,QAAQU,IAAI,sBAAZV,sBAAAA,eAAclD,KAAK,qBAAnBkD,oBAAqBiC,IAAI,oBAAIjC,QAAQnD,MAAM;QAAE;IACjF;IACA,MAAMwF,YAAY9E,OAAOyC,QAAQU,IAAI,CAAC9B,EAAE;IACxC,MAAM0D,SAAiB,MAAMxG,cAC1B2B,GAAG,GACHC,SAAS,GACT6E,cAAc,CAAC,OAAOC;YAEGC;QADxB,MAAMA,QAAQ,MAAMD,YAAY3E,GAAG,CAACoC;QACpC,MAAMF,WAAWxC,QAAOkF,aAAAA,MAAM5E,GAAG,CAAC,oCAAV4E,aAAsC;QAC9D,IAAI1C,UAAU,OAAOA;QACrByC,YAAY7B,GAAG,CACbV,KACA;YAAEyC,uBAAuBL;YAAWM,6BAA6B;QAAM,GACvE;YAAEzB,OAAO;QAAK;QAEhB,OAAOmB;IACT;IACF,IAAIC,WAAWD,WAAW;;YAG2BL,sBAAAA;QAFnD,MAAMA,UAAU,MAAM3F,UAAU,QAAQ,CAAC,wBAAwB,EAAEiG,QAAQ,EAAE;YAAEhC;QAAO;QACtF,MAAMjE,UAAU,UAAU,CAAC,wBAAwB,EAAEgG,WAAW,EAAEf,KAAK,CAAC,IAAMd;QAC9E,IAAI,CAACwB,QAAQpF,EAAE,EAAE,OAAO;YAAEA,IAAI;YAAOE,OAAOS,iBAAOyE,iBAAAA,QAAQtB,IAAI,sBAAZsB,uBAAAA,eAAclF,KAAK,qBAAnBkF,qBAAqBC,IAAI,oBAAID,QAAQnF,MAAM;QAAE;QAChG,OAAO;YAAED,IAAI;YAAMwF,iBAAiBE;QAAO;IAC7C;IACA,MAAMJ,WAAW,MAAMC,6BAA6B/E,QAAQ+C,YAAYkC;IACxE,OAAOH,WAAW;QAAEtF,IAAI;QAAMwF,iBAAiBC;IAAU,IAAI;QAAEzF,IAAI;QAAOE,OAAO;IAAa;AAChG;AAEA,eAAeqF,6BACb/E,MAAc,EACd+C,UAAkB,EAClBiC,eAAuB;IAEvB,MAAMF,WAAW,MAAM7F,UAAU,QAAQ,CAAC,mBAAmB,EAAE8D,YAAY,EAAE;QAC3EG,QAAQ;YAAEsC,yBAAyBR;QAAgB;IACrD;IACA,IAAI,CAACF,SAAStF,EAAE,EAAE;YACwDsF;QAAxEX,QAAQzE,KAAK,CAAC,kDAAkDM,SAAQ8E,iBAAAA,SAASxB,IAAI,qBAAbwB,eAAepF,KAAK;QAC5F,OAAO;IACT;IACA,MAAMoB,kBAAkBd,QAAQuD,GAAG,CAAC;QAAEgC,6BAA6B;IAAK,GAAG;QAAEzB,OAAO;IAAK;IACzF,OAAO;AACT;AAEA;;;;;CAKC,GACD,OAAO,eAAe2B,qBACpBzF,MAAc,EACdqE,QAA6B;IAE7B,IAAI,CAACA,SAASC,cAAc,EAAE;IAC9B,MAAMzB,MAAM/B,kBAAkBd;IAC9B,MAAM2E,SAAS,MAAM9B,IAAIpC,GAAG;IAC5B,IAAI,CAACkE,OAAOlE,GAAG,CAAC,qBAAqB;IACrC,MAAMiF,MAAMtB,oBAAoBC;IAChC,IAAIM,OAAOlE,GAAG,CAAC,kBAAkBiF,KAAK;IACtC,MAAMC,cAActB,SAASE,cAAc,CAAClC,KAAK,CAAC,GAAG;IACrD,MAAMuD,QAAQ,MAAMnB,2BAClBzE,QACA;QAAE,CAAC,CAAC,QAAQ,EAAEjB,aAAa,cAAc,CAAC,CAAC,EAAE4G,YAAYE,GAAG,CAAC1F;IAAQ,GACrEuF;IAEF,IAAI,WAAWE,OAAO;QACpBzB,QAAQzE,KAAK,CAAC,+CAA+CM,QAAQ4F,MAAMlG,KAAK;QAChF;IACF;IACA,MAAMmD,IAAIU,GAAG,CAAC;QAAEuC,YAAYJ;IAAI,GAAG;QAAE5B,OAAO;IAAK;AACnD;AAEA,4EAA4E,GAC5E,OAAO,eAAeiC,uBACpB/F,MAAc,EACdgB,QAAgB;;QAIMgF,eAIGA;IANzB,MAAMA,UAAU,MAAM/G,UAAU,OAAO,CAAC,iBAAiB,EAAE+B,UAAU;IACrE,IAAI,CAACgF,QAAQxG,EAAE,EAAE,OAAO;IACxB,MAAMC,SAASU,gBAAO6F,gBAAAA,QAAQ1C,IAAI,qBAAZ0C,cAAcvG,MAAM,mBAAI;IAC9C,MAAMsB,UAAUf,QAAQgB,UACrBuC,GAAG,CAAC;QAAE9D;QAAQwG,cAAcrC,KAAKC,GAAG;IAAG,GAAG;QAAEC,OAAO;IAAK,GACxDI,KAAK,CAAC,IAAMd;IACf,OAAO;QAAE3D;QAAQyG,MAAM,YAAEF,iBAAAA,QAAQ1C,IAAI,qBAAZ0C,eAAcE,MAAM,oBAAI;IAAK;AACxD;AAiBA,uFAAuF,GACvF,eAAeC,mBACbC,KAAgB,EAChBC,MAAwC,EACxCC,KAA0C;IAE1C,OAAO,MAAMjH,gBAAgB;QAC3BW,QAAQoG,MAAMpG,MAAM;QACpBuG,SAASH,MAAMG,OAAO;QACtBC,WAAWJ,MAAMI,SAAS;QAC1BC,QAAQ,CAAC,EAAEC,KAAK,EAAEC,QAAQ,EAAEhE,QAAQ,EAAE;sBAyBb+D;gBAlBFA;YANrB,IAAI/D,UAAU;gBACZ,mEAAmE;gBACnE,oEAAoE;gBACpE,OAAO;oBAAEiE,MAAM;gBAAO;YACxB;YACA,IAAIF,MAAMjH,MAAM,KAAK,WAAW,OAAO;gBAAEmH,MAAM;eAAarH;YAC5D,MAAMsH,QAAQC,gBAAOJ,gBAAAA,MAAMK,MAAM,qBAAZL,cAAcM,UAAU,mBAAI;YACjD,MAAMC,aAAanI,cAAcoI,kBAAkB,CAACL,OAAOF;YAC3D,IAAI,CAAEP,CAAAA,MAAMe,WAAW,GAAG,CAAA,GAAI;gBAC5B,OAAO;oBAAEP,MAAM;oBAAUnH,QAAQ;oBAAKC,OAAO;gBAA6B;YAC5E;YACA,IAAI0G,MAAMe,WAAW,GAAGF,YAAY;gBAClC,OAAO;oBACLL,MAAM;oBACNnH,QAAQ;oBACRC,OACEuH,aAAa,IACT,CAAC,MAAM,EAAE,AAACA,CAAAA,aAAa,GAAE,EAAGG,OAAO,CAAC,GAAG,6BAA6B,CAAC,GACrE;gBACR;YACF;YACA,MAAMC,aAAavI,cAAcwI,aAAa,CAAClB,MAAMmB,QAAQ,EAAEnB,MAAMe,WAAW;YAChF,IAAIE,YAAY,OAAO;gBAAET,MAAM;gBAAUnH,QAAQ;gBAAKC,OAAO2H;YAAW;YACxE,MAAMG,eAAe1I,cAAc2I,iBAAiB,CAAC;gBACnDD,cAAcV,QAAOJ,yBAAAA,MAAMgB,eAAe,YAArBhB,yBAAyB;gBAC9CM,YAAYH;gBACZM,aAAaf,MAAMe,WAAW;YAChC;YACA,OAAO;gBACLP,MAAM;gBACNhH,SAAS;oBACP4B,IAAI4E,MAAMI,SAAS;oBACnBH;oBACAc,aAAaf,MAAMe,WAAW;mBAC1Bf,MAAMmB,QAAQ,GAAG,IAAI;oBAAEA,UAAUnB,MAAMmB,QAAQ;gBAAC,IAAI,CAAC;oBACzD9H,QAAQ;oBACRkI,MAAM/D,KAAKC,GAAG;oBACd2D;oBACAI,WAAWxB,MAAMwB,SAAS;mBACvBtB;YAEP;QACF;IACF;AACF;AAEA,wDAAwD,GACxD,eAAeuB,iBACb7H,MAAc,EACduG,OAAe,EACfC,SAAiB,EACjBsB,KAAiF,EACjFC,KAA0C;IAE1C,OAAO,MAAM1I,gBAAgB;QAC3BW;QACAuG;QACAC;QACAC,QAAQ,CAAC,EAAE9D,QAAQ,EAAE;YACnB,IAAI,CAACA,UAAU,OAAO;gBAAEiE,MAAM;gBAAUnH,QAAQ;gBAAKC,OAAO;YAAkB;YAC9E,MAAMG,OAAOiI,MAAMnF;YACnB,IAAI,CAAC9C,MAAM,OAAO;gBAAE+G,MAAM;YAAO;YACjC,OAAO;gBAAEA,MAAM;gBAAOhH,SAASC;eAAUkI,QAAQ;gBAAEA;YAAM,IAAI,CAAC;QAChE;IACF;AACF;AAEA,SAASC,oBACP5B,KAAgB,EAChBxG,OAAmC,EACnCqI,WAAmB,EACnB3B,KAAgF;QAIvD1G;IAFzB,MAAMsI,cAAc9B,MAAMe,WAAW,GAAGf,MAAMmB,QAAQ;IACtD,MAAMY,MAAMnJ,2BAA2B;QACrCoJ,gBAAgBtB,QAAOlH,wBAAAA,QAAQ4H,YAAY,YAApB5H,wBAAwB;QAC/CsI;IACF;IACA,OAAO;QACLG,QAAQH;QACRI,UAAUvJ;QACV,8BAA8BkJ;OAC1BE,MAAM,IAAI;QAAEI,wBAAwBJ;IAAI,IAAI,CAAC;QACjD,kBAAkB;QAClB,oBAAoB/B,MAAMpG,MAAM;QAChC,qBAAqBoG,MAAMG,OAAO;QAClC,uBAAuBH,MAAMI,SAAS;QACtC,sBAAsBrG,OAAOiG,MAAMmB,QAAQ;OACxCjB;AAEP;AAEA;;;;;;;;;;;;;CAaC,GACD,OAAO,eAAekC,wBACpBpC,KAAwC;QA8BrBqC;IA5BnB,yEAAyE;IACzE,yEAAyE;IACzE,iBAAiB;IACjB,IAAI,CAACrJ,wBAAwB;QAC3B,OAAO;YAAEI,IAAI;YAAOC,QAAQ;YAAKC,OAAO;QAAqD;IAC/F;IACA,MAAMuI,cAAc,MAAMlI,mBAAmBqG,MAAMpG,MAAM,EAAEoG,MAAMnG,GAAG;IACpE,IAAI,CAACgI,aAAa,OAAO;QAAEzI,IAAI;QAAOC,QAAQ;QAAKC,OAAO;IAA2B;IACrF,MAAMgJ,WAAW,MAAMvC,mBACrBC,OACA,gBACAA,MAAMpF,QAAQ,GAAG;QAAEA,UAAUoF,MAAMpF,QAAQ;IAAC,IAAI,CAAC;IAEnD,IAAI,CAAC0H,SAASlJ,EAAE,IAAI,CAACkJ,SAAS9I,OAAO,EAAE,OAAO8I;IAC9C,MAAM9I,UAAU8I,SAAS9I,OAAO;IAChC,IAAIA,QAAQH,MAAM,KAAK,WAAW,OAAOiJ;IACzC,uEAAuE;IACvE,0DAA0D;IAC1D,MAAMD,SAAS,MAAMxJ,UAAU,QAAQ,mBAAmB;QACxDgE,gBAAgB,CAAC,OAAO,EAAErD,QAAQ4B,EAAE,EAAE;QACtC0B,QAAQ8E,oBAAoB5B,OAAOxG,SAASqI,aAAa;YACvDU,sBACE5J,iBAAiB,QAAQ;gBAAC;aAAe,GAAG;gBAAC;gBAAgB;aAAkB;YACjF6J,gBAAgB;WACZxC,MAAMpF,QAAQ,GAAG;YAAE,sBAAsBoF,MAAMpF,QAAQ;QAAC,IAAI,CAAC;YACjE6H,aAAa;;IAEjB;IACA,IAAI,CAACJ,OAAOjJ,EAAE,IAAI,GAACiJ,eAAAA,OAAOnF,IAAI,qBAAXmF,aAAajH,EAAE,GAAE;QAClC,OAAO,MAAMqG,iBAAiBzB,MAAMpG,MAAM,EAAEoG,MAAMG,OAAO,EAAE3G,QAAQ4B,EAAE,EAAE,CAACsH,UAAa,aAChFA;gBACHrJ,QAAQ;gBACRK,gBAAgBZ,sBAAsBuJ,OAAOnF,IAAI,EAAE;;IAEvD;IACA,MAAMyF,kBAAkB5I,OAAOsI,OAAOnF,IAAI,CAAC9B,EAAE;IAC7C,MAAMwH,WAAWpJ,QAAQmJ,eAAe,GACpCL,WACA,MAAMb,iBAAiBzB,MAAMpG,MAAM,EAAEoG,MAAMG,OAAO,EAAE3G,QAAQ4B,EAAE,EAAE,CAACsH,UAAa,aACzEA;YACHC;YACAtF,UAAUC,QAAQ+E,OAAOnF,IAAI,CAACG,QAAQ;;IAE5C,OAAO,aACFuF;QACHD;OACIN,OAAOnF,IAAI,CAAC2F,aAAa,GAAG;QAAEC,cAAc/I,OAAOsI,OAAOnF,IAAI,CAAC2F,aAAa;IAAE,IAAI,CAAC;AAE3F;AAEA;;;;;CAKC,GACD,OAAO,eAAeE,wBACpB/C,KAAsE;IAEtE,MAAMgD,SAAS,MAAMhI,oBAAoBgF,MAAMpG,MAAM,EAAEoG,MAAMpF,QAAQ;IACrE,IAAI,CAACoI,QAAQ,OAAO;QAAE5J,IAAI;QAAOC,QAAQ;QAAKC,OAAO;IAAwC;IAC7F,MAAM+F,qBAAqBW,MAAMpG,MAAM,EAAEoG,MAAM/B,QAAQ,EAAEH,KAAK,CAAC,CAACxE,QAC9DyE,QAAQzE,KAAK,CAAC,sCAAsCA;IAEtD,MAAMkD,UAAU,MAAM4F,wBAAwB,aAAKpC;QAAOpF,UAAUoI,OAAO5H,EAAE;;IAC7E,IAAI,CAACoB,QAAQpD,EAAE,IAAI,CAACoD,QAAQhD,OAAO,IAAI,CAACgD,QAAQmG,eAAe,EAAE,OAAOnG;IACxE,IAAIA,QAAQhD,OAAO,CAACH,MAAM,KAAK,WAAW,OAAOmD;IACjD,OAAO,MAAMyG,aAAa;QACxBrJ,QAAQoG,MAAMpG,MAAM;QACpBuG,SAASH,MAAMG,OAAO;QACtBC,WAAW5D,QAAQhD,OAAO,CAAC4B,EAAE;QAC7BR,UAAUoI,OAAO5H,EAAE;QACnBuH,iBAAiBnG,QAAQmG,eAAe;QACxC,oEAAoE;QACpE,8CAA8C;QAC9CO,aAAa,CAAClD,MAAM/B,QAAQ,CAACC,cAAc,IAAI8B,MAAMmB,QAAQ,GAAG;QAChEgC,SAAS;IACX;AACF;AAEA,8EAA8E,GAC9E,OAAO,eAAeC,sBAAsBxJ,MAAc;QAE1C2E;IADd,MAAMA,SAAS,MAAM7D,kBAAkBd,QAAQS,GAAG;IAClD,OAAON,QAAOwE,cAAAA,OAAOlE,GAAG,CAAC,+BAAXkE,cAAkC,OAAO;AACzD;AAEA;;;;;;CAMC,GACD,OAAO,eAAe8E,iCACpBzJ,MAAc;QAOI0J;IALlB,MAAM3G,aAAa,MAAMyG,sBAAsBxJ;IAC/C,IAAI,CAAC+C,YAAY,OAAO;IACxB,MAAM2G,QAAQ,MAAMzK,UAAU,QAAQ,8BAA8B;QAClEiE,QAAQ;YAAEyG,UAAU5G;QAAW;IACjC;IACA,IAAI,CAAC2G,MAAMlK,EAAE,IAAI,GAACkK,cAAAA,MAAMpG,IAAI,qBAAVoG,YAAYE,MAAM,GAAE,OAAO;IAC7C,OAAO;QAAEA,QAAQzJ,OAAOuJ,MAAMpG,IAAI,CAACsG,MAAM;QAAG7G;IAAW;AACzD;AAEA,eAAesG,aAAaQ,KAQ3B;IACC,MAAMC,YAAY,MAAM7K,UACtB,QACA,CAAC,iBAAiB,EAAE4K,MAAM7I,QAAQ,CAAC,uBAAuB,CAAC,EAC3D;QACEiC,gBAAgB,CAAC,YAAY,EAAE4G,MAAMrD,SAAS,CAAC,CAAC,EAAEqD,MAAMN,OAAO,EAAE;QACjErG,QAAQ;YACN6G,gBAAgBF,MAAMd,eAAe;YACrC,gDAAgD;WAC5Cc,MAAMP,WAAW,GAAG;YAAE,gCAAgC;QAAK,IAAI,CAAC;IAExE;IAEF,IAAI,CAACQ,UAAUtK,EAAE,EAAE;QACjB,OAAO,MAAMqI,iBAAiBgC,MAAM7J,MAAM,EAAE6J,MAAMtD,OAAO,EAAEsD,MAAMrD,SAAS,EAAE,CAACsC,UAAa,aACrFA;gBACHrJ,QAAQ;gBACRK,gBAAgBZ,sBAAsB4K,UAAUxG,IAAI,EAAE;;IAE1D;IACA,OAAO,MAAMuE,iBAAiBgC,MAAM7J,MAAM,EAAE6J,MAAMtD,OAAO,EAAEsD,MAAMrD,SAAS,EAAE,CAACsC;QAC3E,IAAI,CAACA,QAAQhJ,cAAc,EAAE,OAAO;QACpC,OAAOH,eAAemJ;IACxB;AACF;AAEA;;;CAGC,GACD,OAAO,eAAekB,wBAAwBH,KAK7C;QAwCyDjK;IAvCxD,MAAM8G,QAAQ,MAAMpH,YAAYuK,MAAM7J,MAAM,EAAE6J,MAAMtD,OAAO;IAC3D,IAAI,CAACG,SAASA,MAAMjH,MAAM,KAAK,WAAW,OAAOF;IACjD,MAAMK,UAAUd,cAAcmL,aAAa,CAACvD,OAAOwD,IAAI,CAAC,CAACC,QAAUA,MAAM3I,EAAE,KAAKqI,MAAMrD,SAAS;IAC/F,IAAI,CAAC5G,WAAWA,QAAQyG,MAAM,KAAK,kBAAkB,CAACzG,QAAQmJ,eAAe,IAAI,CAACnJ,QAAQoB,QAAQ,EAAE;QAClG,OAAO;YAAExB,IAAI;YAAOC,QAAQ;YAAKC,OAAO;QAAkB;IAC5D;IACA,IAAIE,QAAQH,MAAM,KAAK,aAAa,OAAO;QAAED,IAAI;QAAMkH;QAAO9G;QAASwK,WAAW;QAAOC,SAAS;IAAM;IACxG,MAAMjB,SAAS,MAAMhI,oBAAoByI,MAAM7J,MAAM,EAAEJ,QAAQoB,QAAQ;IACvE,IAAI,CAACoI,QAAQ,OAAO;QAAE5J,IAAI;QAAOC,QAAQ;QAAKC,OAAO;IAAwC;IAC7F,2EAA2E;IAC3E,kEAAkE;IAClE,MAAM4K,UAAU,MAAMjL,gBAAgB;QACpCW,QAAQ6J,MAAM7J,MAAM;QACpBuG,SAASsD,MAAMtD,OAAO;QACtBC,WAAWqD,MAAMrD,SAAS;QAC1BC,QAAQ,CAAC,EAAEC,OAAOrB,KAAK,EAAEsB,QAAQ,EAAEhE,QAAQ,EAAE;;gBAOlC0C;YANT,IAAI,CAAC1C,UAAU,OAAO;gBAAEiE,MAAM;gBAAUnH,QAAQ;gBAAKC,OAAO;YAAkB;YAC9E,IAAIiD,SAASlD,MAAM,KAAK,WAAW,OAAO;gBAAEmH,MAAM;YAAO;YACzD,IAAIjE,SAASlD,MAAM,KAAK,YAAYkD,SAASlD,MAAM,KAAK,YAAY;gBAClE,OAAO;oBAAEmH,MAAM;oBAAUnH,QAAQ;oBAAKC,OAAO;gBAAkC;YACjF;YACA,MAAMuH,aAAanI,cAAcoI,kBAAkB,CACjDJ,gBAAOzB,gBAAAA,MAAM0B,MAAM,qBAAZ1B,cAAc2B,UAAU,mBAAI,IACnCL,SAAS4D,MAAM,CAAC,CAACJ,QAAUA,MAAM3I,EAAE,KAAKmB,SAASnB,EAAE;YAErD,IAAImB,SAASwE,WAAW,GAAGF,YAAY;gBACrC,OAAO;oBAAEL,MAAM;oBAAUnH,QAAQ;oBAAKC,OAAO;gBAAiD;YAChG;YACA,OAAO;gBAAEkH,MAAM;gBAAOhH,SAAS,aAAKD,eAAegD;oBAAWlD,QAAQ;;YAAY;QACpF;IACF;IACA,IAAI,CAAC6K,QAAQ9K,EAAE,EAAE,OAAO8K;IACxB,MAAMf,UAAU3F,KAAKC,GAAG;IACxB,OAAO,MAAMwF,aAAa;QACxBrJ,QAAQ6J,MAAM7J,MAAM;QACpBuG,SAASsD,MAAMtD,OAAO;QACtBC,WAAWqD,MAAMrD,SAAS;QAC1BxF,UAAUoI,OAAO5H,EAAE;QACnBuH,iBAAiBnJ,QAAQmJ,eAAe;QACxCO,aAAa,CAACO,MAAMxF,QAAQ,CAACC,cAAc,IAAIwC,QAAOlH,oBAAAA,QAAQ2H,QAAQ,YAAhB3H,oBAAoB,KAAK;QAC/E2J;IACF;AACF;AAEA;;;;CAIC,GACD,OAAO,eAAeiB,sBACpBpE,KAA+C;QAEvBA;QAmBLqC,cAAoBA;IAnBvC,MAAMgC,mBAAkBrE,yBAAAA,MAAMqE,eAAe,YAArBrE,yBAAyB;IACjD,IAAIqE,mBAAmB,CAACC,kBAAkBD,kBAAkB;QAC1D,OAAO;YAAEjL,IAAI;YAAOC,QAAQ;YAAKC,OAAO;QAA8C;IACxF;IACA,MAAMuI,cAAc,MAAMlI,mBAAmBqG,MAAMpG,MAAM,EAAEoG,MAAMnG,GAAG;IACpE,IAAI,CAACgI,aAAa,OAAO;QAAEzI,IAAI;QAAOC,QAAQ;QAAKC,OAAO;IAA2B;IACrF,MAAMgJ,WAAW,MAAMvC,mBAAmBC,OAAO,cAAc,CAAC;IAChE,IAAI,CAACsC,SAASlJ,EAAE,IAAI,CAACkJ,SAAS9I,OAAO,EAAE,OAAO8I;IAC9C,MAAM9I,UAAU8I,SAAS9I,OAAO;IAChC,IAAIA,QAAQH,MAAM,KAAK,WAAW,OAAOiJ;IACzC,MAAMD,SAAS,MAAMxJ,UAAU,QAAQ,mBAAmB;QACxDgE,gBAAgB,CAAC,OAAO,EAAErD,QAAQ4B,EAAE,EAAE;QACtC0B,QAAQ8E,oBAAoB5B,OAAOxG,SAASqI,aAAa;YACvDU,sBAAsB;gBAAC;aAAO;YAC9BE,aAAa;WAEVjK,yBAAyB;IAEhC;IACA,IAAI,CAAC6J,OAAOjJ,EAAE,IAAI,GAACiJ,eAAAA,OAAOnF,IAAI,qBAAXmF,aAAajH,EAAE,KAAI,GAACiH,gBAAAA,OAAOnF,IAAI,qBAAXmF,cAAaQ,aAAa,GAAE;QACjE,OAAO,MAAMpB,iBAAiBzB,MAAMpG,MAAM,EAAEoG,MAAMG,OAAO,EAAE3G,QAAQ4B,EAAE,EAAE,CAACsH,UAAa,aAChFA;gBACHrJ,QAAQ;gBACRK,gBAAgBZ,sBAAsBuJ,OAAOnF,IAAI,EAAE;;IAEvD;IACA,MAAMsB,UAAUhF,QAAQmJ,eAAe,GACnCL,WACA,MAAMb,iBAAiBzB,MAAMpG,MAAM,EAAEoG,MAAMG,OAAO,EAAE3G,QAAQ4B,EAAE,EAAE,CAACsH,UAAa,aACzEA;YACHC,iBAAiB5I,OAAOsI,OAAOnF,IAAI,CAAC9B,EAAE;YACtCiC,UAAUC,QAAQ+E,OAAOnF,IAAI,CAACG,QAAQ;;IAE5C,IAAI,CAACgH,iBAAiB,OAAO,aAAK7F;QAASsE,cAAc/I,OAAOsI,OAAOnF,IAAI,CAAC2F,aAAa;;IACzF,OAAO,MAAM0B,iBAAiBvE,OAAOxG,QAAQ4B,EAAE,EAAErB,OAAOsI,OAAOnF,IAAI,CAAC9B,EAAE,GAAGiJ;AAC3E;AAEA,iFAAiF,GACjF,SAASC,kBAAkBxJ,KAAa;IACtC,OAAO,yBAAyBC,IAAI,CAACD;AACvC;AAEA,2EAA2E,GAC3E,OAAO,MAAM0J,kCACX,4JAA2J;AAE7J;;;;;;;;CAQC,GACD,eAAeD,iBACbvE,KAAgB,EAChBI,SAAiB,EACjBqE,QAAgB,EAChBJ,eAAuB;QAODK;QAAAA,iBAA0BA,sCAAAA,uBAAAA,kBAW3BA;IAhBrB,MAAMjB,QAAQ;QAAE7J,QAAQoG,MAAMpG,MAAM;QAAEuG,SAASH,MAAMG,OAAO;QAAEC;IAAU;IACxE,MAAMsE,YAAY,MAAM7L,UAAU,QAAQ,CAAC,gBAAgB,EAAE4L,SAAS,QAAQ,CAAC,EAAE;QAC/E5H,gBAAgB,CAAC,kBAAkB,EAAE4H,SAAS,CAAC,EAAEJ,iBAAiB;QAClEvH,QAAQ;YAAE6H,gBAAgBN;QAAgB;IAC5C;IACA,MAAMhL,SAASU,QAAO2K,iBAAAA,kBAAAA,UAAUxH,IAAI,qBAAdwH,gBAAgBrL,MAAM,qBAAIqL,mBAAAA,UAAUxH,IAAI,sBAAdwH,wBAAAA,iBAAgBpL,KAAK,sBAArBoL,uCAAAA,sBAAuBf,cAAc,qBAArCe,qCAAuCrL,MAAM,YAAvEqL,OAA2E;IACjG,IAAIrL,WAAW,mBAAmB;QAChC,MAAMR,UAAU,QAAQ,CAAC,gBAAgB,EAAE4L,SAAS,OAAO,CAAC,EAAE;YAC5D5H,gBAAgB,CAAC,WAAW,EAAE4H,UAAU;QAC1C;QACA,OAAO,MAAMhD,iBAAiBzB,MAAMpG,MAAM,EAAEoG,MAAMG,OAAO,EAAEC,WAAW,CAACsC,UACrEA,QAAQrJ,MAAM,KAAK,YACf,aAAKqJ;gBAASrJ,QAAQ;gBAAUK,gBAAgB8K;iBAChD;IAER;IACA,IAAI,CAACE,UAAUtL,EAAE,MAAIsL,mBAAAA,UAAUxH,IAAI,qBAAdwH,iBAAgBpL,KAAK,KAAI,CAACoL,UAAUxH,IAAI,CAAC5D,KAAK,CAACqK,cAAc,EAAE;QAClF,oGAAoG;QACpG,MAAMiB,UAAU9L,sBAAsB4L,UAAUxH,IAAI,EAAE;QACtD,MAAMrE,UAAU,QAAQ,CAAC,gBAAgB,EAAE4L,SAAS,OAAO,CAAC,EAAE;YAC5D5H,gBAAgB,CAAC,WAAW,EAAE4H,UAAU;QAC1C;QACA,OAAO,MAAMhD,iBAAiBzB,MAAMpG,MAAM,EAAEoG,MAAMG,OAAO,EAAEC,WAAW,CAACsC,UACrEA,QAAQrJ,MAAM,KAAK,YAAY,aAAKqJ;gBAASrJ,QAAQ;gBAAUK,gBAAgBkL;iBAAY;IAE/F;IACA,mFAAmF;IACnF,OAAO,MAAMC,sBAAsBpB;AACrC;AAEA;;;;CAIC,GACD,OAAO,eAAeqB,qBACpB9E,KAAwC;QASfxG;QAoCLuL;IA3CpB,MAAMlD,cAAc,MAAMlI,mBAAmBqG,MAAMpG,MAAM,EAAEoG,MAAMnG,GAAG;IACpE,IAAI,CAACgI,aAAa,OAAO;QAAEzI,IAAI;QAAOC,QAAQ;QAAKC,OAAO;IAA2B;IACrF,MAAMgJ,WAAW,MAAMvC,mBAAmBC,OAAO,aAAa,CAAC;IAC/D,IAAI,CAACsC,SAASlJ,EAAE,IAAI,CAACkJ,SAAS9I,OAAO,EAAE,OAAO8I;IAC9C,MAAM9I,UAAU8I,SAAS9I,OAAO;IAChC,IAAIA,QAAQwL,iBAAiB,IAAIxL,QAAQH,MAAM,KAAK,WAAW,OAAOiJ;IACtE,MAAMP,MAAMnJ,2BAA2B;QACrCoJ,gBAAgBtB,QAAOlH,wBAAAA,QAAQ4H,YAAY,YAApB5H,wBAAwB;QAC/CsI,aAAa9B,MAAMe,WAAW,GAAGf,MAAMmB,QAAQ;IACjD;IACA,MAAM4D,UAAU,MAAMlM,UAAU,QAAQ,qBAAqB;QAC3DgE,gBAAgB,CAAC,SAAS,EAAErD,QAAQ4B,EAAE,EAAE;QACxC0B,QAAQ;YACNmI,MAAM;WACHxM,wCAAwC;YAC3C,wBAAwB;gBAAC;aAAO;YAChC,2BAA2B;YAC3B,uCAAuCE;YACvC,0CAA0CqH,MAAMe,WAAW;YAC3D,iDAAiD;WAC7Cf,MAAMmB,QAAQ,GAAG,IACjB;YACE,2BAA2B;YAC3B,uCAAuCxI;YACvC,0CAA0CqH,MAAMmB,QAAQ;YACxD,iDAAiD;QACnD,IACA,CAAC;YACL,mDAAmDU;WAC/CE,MAAM,IAAI;YAAE,+CAA+CA;QAAI,IAAI,CAAC;YACxE,uCAAuC;YACvC,yCAAyC/B,MAAMpG,MAAM;YACrD,0CAA0CoG,MAAMG,OAAO;YACvD,4CAA4C3G,QAAQ4B,EAAE;YACtD,2CAA2CrB,OAAOiG,MAAMmB,QAAQ;YAChE+D,aAAa,GAAGlF,MAAMmF,SAAS,CAAC,OAAO,CAAC;YACxCC,YAAY,GAAGpF,MAAMmF,SAAS,CAAC,OAAO,CAAC;YACvC,kBAAkB;YAClB,oBAAoBnF,MAAMpG,MAAM;YAChC,qBAAqBoG,MAAMG,OAAO;YAClC,uBAAuB3G,QAAQ4B,EAAE;;IAErC;IACA,IAAI,CAAC2J,QAAQ3L,EAAE,IAAI,GAAC2L,gBAAAA,QAAQ7H,IAAI,qBAAZ6H,cAAcM,GAAG,GAAE;QACrC,OAAO,MAAM5D,iBAAiBzB,MAAMpG,MAAM,EAAEoG,MAAMG,OAAO,EAAE3G,QAAQ4B,EAAE,EAAE,CAACsH,UAAa,aAChFA;gBACHrJ,QAAQ;gBACRK,gBAAgBZ,sBAAsBiM,QAAQ7H,IAAI,EAAE;;IAExD;IACA,OAAO,MAAMuE,iBAAiBzB,MAAMpG,MAAM,EAAEoG,MAAMG,OAAO,EAAE3G,QAAQ4B,EAAE,EAAE,CAACsH,UAAa,aAChFA;YACHsC,mBAAmBjL,OAAOgL,QAAQ7H,IAAI,CAAC9B,EAAE;YACzCkK,aAAavL,OAAOgL,QAAQ7H,IAAI,CAACmI,GAAG;YACpChI,UAAUC,QAAQyH,QAAQ7H,IAAI,CAACG,QAAQ;;AAE3C;AAEA;;2CAE2C,GAE3C,gEAAgE,GAChE,SAASkI,cAAclD,MAAW;QAG9BmD,MAAAA;QAAAA,gCACAA,iCACAA;IAJF,MAAMA,SAAS,QAAOnD,0BAAAA,OAAQoD,aAAa,MAAK,WAAWpD,OAAOoD,aAAa,GAAG;IAClF,MAAMC,WACJF,QAAAA,iBAAAA,2BAAAA,iCAAAA,OAAQG,sBAAsB,qBAA9BH,+BAAgCI,YAAY,oBAC5CJ,2BAAAA,kCAAAA,OAAQG,sBAAsB,qBAA9BH,gCAAgCK,eAAe,YAD/CL,QAEAA,2BAAAA,kCAAAA,OAAQG,sBAAsB,qBAA9BH,gCAAgCM,IAAI,YAFpCN,OAGA;IACF,OAAO,aACDE,CAAAA,2BAAAA,QAASK,KAAK,IAAG;QAAEC,WAAWjM,OAAO2L,QAAQK,KAAK;IAAE,IAAI,CAAC,GACzDL,CAAAA,2BAAAA,QAASO,KAAK,IAAG;QAAEA,OAAOlM,OAAO2L,QAAQO,KAAK;IAAE,IAAI,CAAC;AAE7D;AAEA,6EAA6E,GAC7E,OAAO,SAASC,kBAAkB7D,MAAW;;QACEA,kBACEA,4BAAAA;IAD/C,MAAM8D,SAASC,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,CAAC5F,eAAO2B,2BAAAA,mBAAAA,OAAQkE,QAAQ,qBAAhBlE,iBAAkBlB,QAAQ,mBAAI,OAAO;IAClF,MAAMqF,WAAWJ,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,CAAC5F,gBAAO2B,2BAAAA,yBAAAA,OAAQoE,cAAc,sBAAtBpE,6BAAAA,uBAAwBqE,GAAG,qBAA3BrE,2BAA6BJ,MAAM,oBAAI,OAAO;IAC7F,OAAOkE,SAASK;AAClB;AAEA;;;;CAIC,GACD,OAAO,SAASG,mBAAmBnN,OAAmC,EAAE6I,MAAW;QAExD7I;IADzB,OAAOZ,2BAA2B;QAChCoJ,gBAAgBtB,QAAOlH,wBAAAA,QAAQ4H,YAAY,YAApB5H,wBAAwB;QAC/CsI,aAAasE,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,CAAC5F,eAAO2B,0BAAAA,OAAQJ,MAAM,mBAAI;IAC/D;AACF;AAEA;;;;;;CAMC,GACD,OAAO,eAAe4C,sBAAsBpB,KAI3C;QAQuBjK,0BAwGlBd;QAvFA2J,kBAA4CA;IAxBhD,MAAM/B,QAAQ,MAAMpH,YAAYuK,MAAM7J,MAAM,EAAE6J,MAAMtD,OAAO;IAC3D,IAAI,CAACG,OAAO,OAAO;QAAElH,IAAI;QAAOC,QAAQ;QAAKC,OAAO;IAAe;IACnE,MAAME,UAAUd,cAAcmL,aAAa,CAACvD,OAAOwD,IAAI,CAAC,CAACC,QAAUA,MAAM3I,EAAE,KAAKqI,MAAMrD,SAAS;IAC/F,IAAI,CAAC5G,SAAS,OAAO;QAAEJ,IAAI;QAAOC,QAAQ;QAAKC,OAAO;IAAkB;IACxE,IAAI,CAACZ,cAAckO,mBAAmB,CAACpN,QAAQyG,MAAM,GAAG;QACtD,OAAO;YAAE7G,IAAI;YAAMkH;YAAO9G;YAASwK,WAAW;YAAOC,SAAS;QAAM;IACtE;IACA,IAAItB,mBAAkBnJ,2BAAAA,QAAQmJ,eAAe,YAAvBnJ,2BAA2B;IACjD,IAAI,CAACmJ,mBAAmBnJ,QAAQwL,iBAAiB,EAAE;;YAE/BD,eAKOA;QANzB,MAAMA,UAAU,MAAMlM,UAAU,OAAO,CAAC,kBAAkB,EAAEW,QAAQwL,iBAAiB,EAAE;QACvF,IAAID,QAAQ3L,EAAE,IAAI2L,EAAAA,gBAAAA,QAAQ7H,IAAI,qBAAZ6H,cAAc1L,MAAM,MAAK,aAAaG,QAAQH,MAAM,KAAK,WAAW;YACpF,OAAO,MAAMoI,iBAAiBgC,MAAM7J,MAAM,EAAE6J,MAAMtD,OAAO,EAAE3G,QAAQ4B,EAAE,EAAE,CAACsH,UACtEA,QAAQrJ,MAAM,KAAK,YAAY,aAAKqJ;oBAASrJ,QAAQ;qBAAe;QAExE;QACAsJ,kBAAkB5I,gBAAOgL,iBAAAA,QAAQ7H,IAAI,qBAAZ6H,eAAcpB,cAAc,mBAAI;IAC3D;IACA,IAAI,CAAChB,iBAAiB,OAAO;QAAEvJ,IAAI;QAAMkH;QAAO9G;QAASwK,WAAW;QAAOC,SAAS;IAAM;IAC1F,IAAI5B,SAAS,AACX,CAAA,MAAMxJ,UAAU,OAAO,CAAC,gBAAgB,EAAE8J,iBAAiB,EAAE;QAC3D7F,QAAQ;YAAE,YAAY;QAAgB;IACxC,EAAC,EACDI,IAAI;IACN,IAAI,EAACmF,0BAAAA,OAAQjH,EAAE,GAAE,OAAO;QAAEhC,IAAI;QAAMkH;QAAO9G;QAASwK,WAAW;QAAOC,SAAS;IAAM;IACrF,IAAI5B,EAAAA,mBAAAA,OAAOkE,QAAQ,qBAAflE,iBAAiBzI,MAAM,MAAK6J,MAAM7J,MAAM,IAAIyI,EAAAA,oBAAAA,OAAOkE,QAAQ,qBAAflE,kBAAiBjC,SAAS,MAAK5G,QAAQ4B,EAAE,EAAE;QACzF2C,QAAQzE,KAAK,CAAC,yDAAyDqJ;QACvE,OAAO;YAAEvJ,IAAI;YAAOC,QAAQ;YAAKC,OAAO;QAA+C;IACzF;IACA,IAAI+I,OAAOhJ,MAAM,KAAK,oBAAoB;YAgBrBwN;QAfnB,IAAIvG,MAAMjH,MAAM,KAAK,aAAaG,QAAQH,MAAM,KAAK,WAAW;YAC9D,+DAA+D;YAC/D,MAAMR,UAAU,QAAQ,CAAC,gBAAgB,EAAEwJ,OAAOjH,EAAE,CAAC,OAAO,CAAC,EAAE;gBAC7DyB,gBAAgB,CAAC,YAAY,EAAEwF,OAAOjH,EAAE,EAAE;YAC5C;YACA,OAAO;gBAAEhC,IAAI;gBAAMkH;gBAAO9G;gBAASwK,WAAW;gBAAOC,SAAS;YAAM;QACtE;QACA,MAAMlC,MAAM4E,mBAAmBnN,SAAS6I;QACxC,MAAMwE,WAAW,MAAMhO,UAAU,QAAQ,CAAC,gBAAgB,EAAEwJ,OAAOjH,EAAE,CAAC,QAAQ,CAAC,EAAE;YAC/EyB,gBAAgB,CAAC,YAAY,EAAEwF,OAAOjH,EAAE,EAAE;YAC1C0B,QAAQ,aACFiF,MAAM,IAAI;gBAAEI,wBAAwBJ;YAAI,IAAI,CAAC;gBACjD,YAAY;;QAEhB;QACA,IAAI8E,SAASzN,EAAE,MAAIyN,iBAAAA,SAAS3J,IAAI,qBAAb2J,eAAezL,EAAE,GAAEiH,SAASwE,SAAS3J,IAAI;aACvD;gBACuD2J;YAA1D9I,QAAQzE,KAAK,CAAC,iCAAiC+I,OAAOjH,EAAE,GAAEyL,kBAAAA,SAAS3J,IAAI,qBAAb2J,gBAAevN,KAAK;YAC9E,OAAO;gBAAEF,IAAI;gBAAMkH;gBAAO9G;gBAASwK,WAAW;gBAAOC,SAAS;YAAM;QACtE;IACF;IACA,IAAI5B,OAAOhJ,MAAM,KAAK,aAAa;YAEiBgJ;QADlD,MAAMlB,WAAW+E,kBAAkB7D;QACnC,MAAMtB,cAAcqF,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,CAAC5F,QAAO2B,iBAAAA,OAAOJ,MAAM,YAAbI,iBAAiB,OAAOlB;QAC1E,OAAO,MAAMM,iBACXgC,MAAM7J,MAAM,EACZ6J,MAAMtD,OAAO,EACb3G,QAAQ4B,EAAE,EACV,CAACsH;gBAY2CL;YAX1C,IAAIK,QAAQrJ,MAAM,KAAK,eAAeqJ,QAAQC,eAAe,KAAKN,OAAOjH,EAAE,EAAE,OAAO;YACpF,OAAO,aACF7B,eAAemJ;gBAClBrJ,QAAQ;gBACRyN,aAAatJ,KAAKC,GAAG;gBACrBkF,iBAAiB5I,OAAOsI,OAAOjH,EAAE;gBACjC,6DAA6D;gBAC7D,kEAAkE;gBAClE,6DAA6D;gBAC7D2F,aAAaA,cAAc,IAAIA,cAAc2B,QAAQ3B,WAAW;eAC5DI,WAAW,IAAI;gBAAEA;YAAS,IAAI,CAAC;gBACnC4F,UAAUX,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,CAAC5F,QAAO2B,iCAAAA,OAAOF,sBAAsB,YAA7BE,iCAAiC;gBACzEhF,UAAUC,QAAQ+E,OAAOhF,QAAQ;eAC9BkI,cAAclD;QAErB;IAEJ;IACA,IAAIA,OAAOhJ,MAAM,KAAK,YAAY;QAChC,OAAO,MAAMoI,iBAAiBgC,MAAM7J,MAAM,EAAE6J,MAAMtD,OAAO,EAAE3G,QAAQ4B,EAAE,EAAE,CAACsH,UACtEA,QAAQrJ,MAAM,KAAK,aAAaqJ,QAAQrJ,MAAM,KAAK,WAC/C,aAAKqJ;gBAASrJ,QAAQ;iBACtB;IAER;IACA,IAAIgJ,OAAOhJ,MAAM,KAAK,6BAA6BgJ,OAAO2E,kBAAkB,EAAE;;YACrD3E;QAAvB,MAAMuC,UAAU7K,iBAAOsI,6BAAAA,OAAO2E,kBAAkB,qBAAzB3E,2BAA2BuC,OAAO,oBAAI;QAC7D,OAAO,MAAMnD,iBAAiBgC,MAAM7J,MAAM,EAAE6J,MAAMtD,OAAO,EAAE3G,QAAQ4B,EAAE,EAAE,CAACsH,UACtEA,QAAQrJ,MAAM,KAAK,YAAY,aAAKqJ;gBAASrJ,QAAQ;gBAAUK,gBAAgBkL;iBAAY;IAE/F;IACA,wEAAwE;IACxE,uEAAuE;IACvE,iCAAiC;IACjC,IAAIpL,QAAQyG,MAAM,KAAK,kBAAkBzG,QAAQoB,QAAQ,IAAIpB,QAAQH,MAAM,KAAK,WAAW;YAKvFyG;QAJF,MAAMkD,SAAS,MAAMrD,uBAAuB8D,MAAM7J,MAAM,EAAEJ,QAAQoB,QAAQ;QAC1E,MAAMkF,SAASkD,0BAAAA,OAAQlD,MAAM;QAC7B,IACEA,CAAAA,0BAAAA,OAAQmH,IAAI,MAAK,4BACjBnH,CAAAA,2BAAAA,iCAAAA,OAAQoH,sBAAsB,qBAA9BpH,+BAAgC6D,cAAc,MAAKtB,OAAOjH,EAAE,IAC5D0E,CAAAA,0BAAAA,OAAQzG,MAAM,MAAK,UACnB;gBACuByG;YAAvB,MAAM8E,UAAU7K,QAAO+F,0BAAAA,OAAOqH,eAAe,YAAtBrH,0BAA0B;YACjD,OAAO,MAAM2B,iBAAiBgC,MAAM7J,MAAM,EAAE6J,MAAMtD,OAAO,EAAE3G,QAAQ4B,EAAE,EAAE,CAACsH,UACtEA,QAAQrJ,MAAM,KAAK,YAAY,aAAKqJ;oBAASrJ,QAAQ;oBAAUK,gBAAgBkL;qBAAY;QAE/F;IACF;IACA,MAAM3F,QAAQ,MAAM/F,YAAYuK,MAAM7J,MAAM,EAAE6J,MAAMtD,OAAO;IAC3D,OAAO;QACL/G,IAAI;QACJkH,KAAK,EAAErB,gBAAAA,QAASqB;QAChB9G,OAAO,GACLd,oCAAAA,cAAcmL,aAAa,CAAC5E,gBAAAA,QAASqB,OAAOwD,IAAI,CAAC,CAACC,QAAUA,MAAM3I,EAAE,KAAK5B,QAAQ4B,EAAE,aAAnF1C,oCAAwFc;QAC1FwK,WAAW;QACXC,SAAS;IACX;AACF;AAEA;;;;CAIC,GACD,OAAO,eAAemD,qBAAqB3D,KAI1C;QA2BmB4D;IA1BlB,MAAMC,YAAY,MAAMzC,sBAAsBpB;IAC9C,IAAI,CAAC6D,UAAUlO,EAAE,IAAI,CAACkO,UAAU9N,OAAO,EAAE,OAAO8N;IAChD,MAAM9N,UAAU8N,UAAU9N,OAAO;IACjC,IAAIA,QAAQH,MAAM,KAAK,eAAeG,QAAQH,MAAM,KAAK,YAAY,OAAOiO;IAC5E,IAAI9N,QAAQyG,MAAM,KAAK,kBAAkBzG,QAAQoB,QAAQ,EAAE;YAEtC2M,qBAAAA;QADnB,MAAMA,UAAU,MAAM1O,UAAU,QAAQ,CAAC,iBAAiB,EAAEW,QAAQoB,QAAQ,CAAC,cAAc,CAAC;QAC5F,IAAI,CAAC2M,QAAQnO,EAAE,IAAImO,EAAAA,gBAAAA,QAAQrK,IAAI,sBAAZqK,sBAAAA,cAAcjO,KAAK,qBAAnBiO,oBAAqB9I,IAAI,MAAK,wBAAwB;YACvE,OAAO;gBACLrF,IAAI;gBACJC,QAAQ;gBACRC,OAAO;YACT;QACF;IACF;IACA,IAAIE,QAAQmJ,eAAe,EAAE;QAC3B,MAAM9J,UAAU,QAAQ,CAAC,gBAAgB,EAAEW,QAAQmJ,eAAe,CAAC,OAAO,CAAC,EAAE;YAC3E9F,gBAAgB,CAAC,WAAW,EAAErD,QAAQmJ,eAAe,EAAE;QACzD;IACF;IACA,IAAInJ,QAAQwL,iBAAiB,EAAE;QAC7B,MAAMnM,UAAU,QAAQ,CAAC,kBAAkB,EAAEW,QAAQwL,iBAAiB,CAAC,OAAO,CAAC,EAAE;YAC/EnI,gBAAgB,CAAC,WAAW,EAAErD,QAAQwL,iBAAiB,EAAE;QAC3D;IACF;IACA,oEAAoE;IACpE,MAAMqC,UAAU,MAAMxC,sBAAsBpB;IAC5C,IAAI4D,QAAQjO,EAAE,IAAIiO,EAAAA,mBAAAA,QAAQ7N,OAAO,qBAAf6N,iBAAiBhO,MAAM,MAAK,aAAa,OAAOgO;IAClE,OAAO,MAAM5F,iBAAiBgC,MAAM7J,MAAM,EAAE6J,MAAMtD,OAAO,EAAE3G,QAAQ4B,EAAE,EAAE,CAACsH,UACtEA,QAAQrJ,MAAM,KAAK,aAAaqJ,QAAQrJ,MAAM,KAAK,WAC/C,aAAKqJ;YAASrJ,QAAQ;aACtB;AAER;AAEA;;;CAGC,GACD,OAAO,eAAemO,qBAAqB/D,KAM1C;IACC,IAAI,CAAC1K,qBAAqB,OAAO;QAAEK,IAAI;QAAOC,QAAQ;QAAKC,OAAO;IAAoB;IACtF,MAAMgH,QAAQ,MAAMpH,YAAYuK,MAAM7J,MAAM,EAAE6J,MAAMtD,OAAO;IAC3D,IAAI,CAACG,OAAO,OAAO;QAAElH,IAAI;QAAOC,QAAQ;QAAKC,OAAO;IAAe;IACnE,MAAME,UAAUd,cAAcmL,aAAa,CAACvD,OAAOwD,IAAI,CAAC,CAACC,QAAUA,MAAM3I,EAAE,KAAKqI,MAAMrD,SAAS;IAC/F,IAAI,EAAC5G,2BAAAA,QAASoB,QAAQ,KAAIpB,QAAQyG,MAAM,KAAK,gBAAgB;QAC3D,OAAO;YAAE7G,IAAI;YAAOC,QAAQ;YAAKC,OAAO;QAAkB;IAC5D;IACA,MAAM0J,SAAS,MAAMhI,oBAAoByI,MAAM7J,MAAM,EAAEJ,QAAQoB,QAAQ;IACvE,IAAI,CAACoI,QAAQ,OAAO;QAAE5J,IAAI;QAAOC,QAAQ;QAAKC,OAAO;IAAwC;IAC7F,MAAMmO,YAAY,MAAM5O,UACtB,QACA,CAAC,8BAA8B,EAAEmK,OAAO5H,EAAE,CAAC,uBAAuB,CAAC,EACnE;QACE0B,QAAQ;YACNmK,MAAM;WACFxD,MAAMiE,UAAU,IAAI,iBAAiB3M,IAAI,CAAC0I,MAAMiE,UAAU,IAC1D;YAAE,wBAAwBjE,MAAMiE,UAAU;QAAC,IAC3C,CAAC;IAET;IAEF,IAAI,CAACD,UAAUrO,EAAE,EAAE;QACjB,OAAO;YACLA,IAAI;YACJC,QAAQ;YACRC,OAAOR,sBAAsB2O,UAAUvK,IAAI,EAAE;QAC/C;IACF;IACA,OAAO,MAAM2H,sBAAsBpB;AACrC;AAEA;;2CAE2C,GAE3C,kEAAkE,GAClE,SAASkE,cACPpB,QAA6C;;IAE7C,MAAM3M,SAASG,eAAOwM,4BAAAA,QAAU,CAAC,SAAS,mBAAI;IAC9C,MAAMpG,UAAUpG,gBAAOwM,4BAAAA,QAAU,CAAC,UAAU,oBAAI;IAChD,MAAMnG,YAAYrG,gBAAOwM,4BAAAA,QAAU,CAAC,YAAY,oBAAI;IACpD,IAAI,CAAC3M,UAAU,CAACuG,WAAW,CAACC,WAAW,OAAO;IAC9C,OAAO;QAAExG;QAAQuG;QAASC;IAAU;AACtC;AAEA;;;;;;;;CAQC,GACD,OAAO,eAAewH,qBAAqBnE,KAG1C;QAmBGoE;IAlBF,MAAM,EAAEZ,IAAI,EAAEY,MAAM,EAAE,GAAGpE;IACzB,IAAIhH,MAAqE;IACzE,IAAIwK,SAAS,sCAAsCA,SAAS,iCAAiC;;YACnEY,uCAAAA,gBACpBA,iBAEcxF,uBAAAA;QAHlB,MAAMoC,WAAW1K,eAAO8N,2BAAAA,iBAAAA,OAAQ/H,MAAM,sBAAd+H,wCAAAA,eAAgBX,sBAAsB,qBAAtCW,sCAAwClE,cAAc,mBAAI;QAClF,IAAIkE,CAAAA,2BAAAA,kBAAAA,OAAQ/H,MAAM,qBAAd+H,gBAAgBZ,IAAI,MAAK,4BAA4B,CAACxC,UAAU,OAAO;QAC3E,MAAMpC,SAAS,MAAMxJ,UAAU,OAAO,CAAC,gBAAgB,EAAE4L,UAAU;QACnE,IAAI,CAACpC,OAAOjJ,EAAE,IAAIiJ,EAAAA,eAAAA,OAAOnF,IAAI,sBAAXmF,wBAAAA,aAAakE,QAAQ,qBAArBlE,sBAAuB7B,IAAI,MAAK,OAAO,OAAO;QAChE/D,MAAMkL,cAActF,OAAOnF,IAAI,CAACqJ,QAAQ;IAC1C,OAAO,IACLU,SAAS,8CACTA,SAAS,8BACTA,SAAS,mCACTA,SAAS,2BACT;YACIY;QAAJ,IAAIA,CAAAA,2BAAAA,oBAAAA,OAAQtB,QAAQ,qBAAhBsB,kBAAkBrH,IAAI,MAAK,OAAO,OAAO;QAC7C/D,MAAMkL,cAAcE,OAAOtB,QAAQ;IACrC,OAAO,IACL,AAACU,CAAAA,SAAS,gCAAgCA,SAAS,0BAAyB,KAC5EY,CAAAA,2BAAAA,mBAAAA,OAAQtB,QAAQ,qBAAhBsB,iBAAkBZ,IAAI,MAAK,eAC3B;QACAxK,MAAMkL,cAAcE,OAAOtB,QAAQ;IACrC,OAAO;QACL,OAAO;IACT;IACA,IAAI,CAAC9J,KAAK,OAAO;IACjB,uEAAuE;IACvE,sEAAsE;IACtE,MAAMoI,sBAAsBpI,KAAKqB,KAAK,CAAC,CAACxE;QACtCyE,QAAQzE,KAAK,CAAC,wCAAwCmD,KAAKnD;QAC3D,uEAAuE;QACvE,6CAA6C;QAC7C,MAAMA;IACR;IACA,OAAO;QAAEM,QAAQ6C,IAAI7C,MAAM;IAAC;AAC9B;AA6BA,SAASkO,kBAAkB5K,IAAS;sEAeK5D,OAAAA;QAbvBkM,gCAQMtI;IATtB,MAAMsI,SAAStI,CAAAA,wBAAAA,KAAMuI,aAAa,KAAI,OAAOvI,KAAKuI,aAAa,KAAK,WAAWvI,KAAKuI,aAAa,GAAG;IACpG,MAAMsC,kBAAUvC,2BAAAA,iCAAAA,OAAQG,sBAAsB,qBAA9BH,+BAAgCI,YAAY,mBAAI,CAAC;IACjE,MAAMtM,iBAAQ4D,wBAAAA,KAAM8J,kBAAkB,oBAAI;IAC1C,OAAO;QACL5L,IAAIrB,gBAAOmD,wBAAAA,KAAM9B,EAAE,oBAAI;QACvB/B,QAAQU,gBAAOmD,wBAAAA,KAAM7D,MAAM,oBAAI;QAC/B4I,QAAQvB,gBAAOxD,wBAAAA,KAAM+E,MAAM,oBAAI;QAC/BC,UAAUnI,gBAAOmD,wBAAAA,KAAMgF,QAAQ,oBAAI;QACnCqE,QAAQ,WAAGrJ,wBAAAA,KAAMqJ,QAAQ,oBAAI,CAAC;QAC9B1E,aAAa9H,gBAAOmD,yBAAAA,sBAAAA,KAAM8K,aAAa,qBAAnB9K,oBAAqB2E,WAAW,oBAAI;QACxDoG,qBAAqBvH,gBAAOxD,wBAAAA,KAAMiF,sBAAsB,oBAAI;QAC5D9E,UAAUC,QAAQJ,wBAAAA,KAAMG,QAAQ;OAC5B0K,QAAQhC,KAAK,GAAG;QAAEA,OAAOhM,OAAOgO,QAAQhC,KAAK;IAAE,IAAI,CAAC,GACpDgC,QAAQ9B,KAAK,GAAG;QAAEA,OAAOlM,OAAOgO,QAAQ9B,KAAK;IAAE,IAAI,CAAC,GACpD3M,QAAQ;QAAEI,gBAAgBK,QAAOT,SAAAA,iBAAAA,MAAMsL,OAAO,YAAbtL,iBAAiBA,MAAM4O,YAAY,YAAnC5O,QAAuC;IAA0B,IAAI,CAAC;AAE/G;AAEA,4EAA4E,GAC5E,OAAO,eAAe6O,6BACpBxF,eAAuB;IAEvB,MAAM/C,UAAU,MAAM/G,UAAU,OAAO,CAAC,gBAAgB,EAAEuP,mBAAmBzF,kBAAkB,EAAE;QAC/F7F,QAAQ;YAAE,YAAY;gBAAC;aAAgB;QAAC;IAC1C;IACA,IAAI,CAAC8C,QAAQxG,EAAE,EAAE,OAAO;QAAEA,IAAI;QAAOE,OAAOR,sBAAsB8G,QAAQ1C,IAAI,EAAE;IAAuC;IACvH,OAAO;QAAE9D,IAAI;QAAMiJ,QAAQyF,kBAAkBlI,QAAQ1C,IAAI;IAAE;AAC7D;AAEA,oGAAoG,GACpG,OAAO,eAAemL,4BACpB1F,eAAuB,EACvBsF,mBAA2B,EAC3BpL,cAAsB;IAEtB,MAAMgK,WAAW,MAAMhO,UAAU,QAAQ,CAAC,gBAAgB,EAAEuP,mBAAmBzF,iBAAiB,QAAQ,CAAC,EAAE;QACzG9F;QACAC,QAAQ,aACFmL,sBAAsB,IAAI;YAAE9F,wBAAwB8F;QAAoB,IAAI,CAAC;YACjF,YAAY;gBAAC;aAAgB;;IAEjC;IACA,IAAI,CAACpB,SAASzN,EAAE,EAAE,OAAO;QAAEA,IAAI;QAAOE,OAAOR,sBAAsB+N,SAAS3J,IAAI,EAAE;IAA2C;IAC7H,OAAO;QAAE9D,IAAI;QAAMiJ,QAAQyF,kBAAkBjB,SAAS3J,IAAI;IAAE;AAC9D;AAEA;;;;;CAKC,GACD,OAAO,eAAeoL,kCAAkC1O,MAAc;QAI1C2E,aAEKA;QASbiB;IAdlB,MAAM/C,MAAM/B,kBAAkBd;IAC9B,MAAM2E,SAAS,MAAM9B,IAAIpC,GAAG;IAC5B,IAAIkE,OAAOlE,GAAG,CAAC,sBAAsB,MAAM,OAAO;IAClD,MAAMsC,aAAa5C,QAAOwE,cAAAA,OAAOlE,GAAG,CAAC,+BAAXkE,cAAkC;IAC5D,IAAI,CAAC5B,YAAY,OAAO;IACxB,MAAMiC,kBAAkB7E,QAAOwE,eAAAA,OAAOlE,GAAG,CAAC,oCAAXkE,eAAuC;IACtE,MAAMiB,QAAQ,MAAM3G,UAClB,QACA+F,kBAAkB,CAAC,wBAAwB,EAAEA,iBAAiB,GAAG,2BACjE;QACE9B,QAAQ;YAAE,oBAAoB;QAAK;OAC/B8B,kBAAkB,CAAC,IAAI;QAAE/B,gBAAgB,CAAC,kBAAkB,EAAEjD,OAAO,QAAQ,CAAC;IAAC;IAGvF,IAAI,CAAC4F,MAAMpG,EAAE,IAAI,GAACoG,cAAAA,MAAMtC,IAAI,qBAAVsC,YAAYpE,EAAE,GAAE;YACqCoE;QAArEzB,QAAQzE,KAAK,CAAC,+CAA+CM,SAAQ4F,eAAAA,MAAMtC,IAAI,qBAAVsC,aAAYlG,KAAK;QACtF,OAAO;IACT;IACA,IAAI,CAACsF,iBAAiB;QACpB,MAAMF,WAAW,MAAM7F,UAAU,QAAQ,CAAC,mBAAmB,EAAE8D,YAAY,EAAE;YAC3EG,QAAQ;gBAAEsC,yBAAyBrF,OAAOyF,MAAMtC,IAAI,CAAC9B,EAAE;YAAE;QAC3D;QACA,IAAI,CAACsD,SAAStF,EAAE,EAAE;gBACgEsF;YAAhFX,QAAQzE,KAAK,CAAC,0DAA0DM,SAAQ8E,iBAAAA,SAASxB,IAAI,qBAAbwB,eAAepF,KAAK;YACpG,OAAO;QACT;IACF;IACA,MAAMmD,IAAIU,GAAG,CAAC;QAAE+B,uBAAuBnF,OAAOyF,MAAMtC,IAAI,CAAC9B,EAAE;QAAGmN,gBAAgB;IAAK,GAAG;QAAE7K,OAAO;IAAK;IACpG,OAAO;AACT"}
|
|
@@ -114,6 +114,10 @@ import { loadHostEmailWithTokens, renderLoadedHostEmailWithTokens } from "@aglyn
|
|
|
114
114
|
}
|
|
115
115
|
const total = CommerceModel.productInventory(product);
|
|
116
116
|
if (total != null && total <= 0) continue;
|
|
117
|
+
// Sold in person only (AGL-4060): the email's one button is the product's
|
|
118
|
+
// page, which is not served. The alert waits, unstamped, and goes out once
|
|
119
|
+
// the product sells online again.
|
|
120
|
+
if (!CommerceModel.productSellsOnline(product)) continue;
|
|
117
121
|
let loaded = templateCache.get(hostRef.id);
|
|
118
122
|
if (loaded === undefined) {
|
|
119
123
|
var _ref1, _Aglyn_hostPublicOrigin;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/process-restock.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 * as Aglyn from '@aglyn/aglyn/server'\nimport * as CommerceModel from '../model'\nimport {\n firebaseAdmin,\n getOrgForHost,\n hostSendingIdentity,\n meterHostEmail,\n} from '@aglyn/tenant-data-admin'\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\nimport {\n isDeferrableSendResult,\n isEmailConfigured,\n sendEmail,\n} from '@aglyn/shared-util-email'\nimport {\n type PluginApiHandler,\n type PluginJobHostGate,\n} from '@aglyn/aglyn/server'\n// The leaf, not the barrel: the console's `x-cron-secret` door asks the\n// site's lockdown directly (AGL-3356) — core's registry has no resolver in\n// the console process — and a spec that substitutes the barrel must still\n// reach the real verdict.\nimport { siteLockdownJobGate } from '@aglyn/tenant-data-admin/server/tenant-write-lockdown'\nimport {\n loadHostEmailWithTokens,\n renderLoadedHostEmailWithTokens,\n type LoadedHostEmailWithTokens,\n} from '@aglyn/tenant-data-admin/server/host-email-tokens'\n\nexport interface RestockScanResult {\n scanned: number\n sent: number\n /** Alerts left unstamped because their site is locked (AGL-2495). */\n skippedLocked: number\n}\n\n/**\n * The back-in-stock pass itself, separated from its HTTP door (AGL-2227).\n *\n * Same finding as `process-abandoned.ts` beside it: `x-cron-secret`-gated and\n * **never invoked by anything**. `commerce/notify-restock` accepts the\n * shopper's \"tell me when it's back\" from the storefront and writes\n * `hosts/{hostId}/restockAlerts`; this is the only thing that would ever mail\n * them. Shoppers have been entering an address into a queue with no drain.\n *\n * Exported so `server.ts` can put it on the platform job beat.\n */\nexport async function scanRestockAlerts(\n /** The lockdown gate, injected by the caller (AGL-2495). Not optional. */\n gate: PluginJobHostGate,\n): Promise<RestockScanResult> {\n const firestore = firebaseAdmin.app().firestore()\n const alerts = await firestore\n .collectionGroup('restockAlerts')\n .where('notifiedAtMs', '==', null)\n .limit(200)\n .get()\n let sent = 0\n const productCache = new Map<string, CommerceModel.HostProduct | null>()\n // Resolve each host's designed template once per run (AGL-770).\n const templateCache = new Map<string, LoadedHostEmailWithTokens | null>()\n // White-label brand per host (White-Label Phase 3): resolved once per host\n // from the owning org doc through the one shared resolver.\n const brandingByHost = new Map<string, Aglyn.ResolvedBrandingProfile>()\n /*\n * One identity resolution per SITE, not per message.\n *\n * The same reason the branding map above exists: this sweep runs across\n * every site, and an uncached resolution here would be two Firestore reads\n * per recipient rather than per host. Scoped to the run, so a domain\n * un-verified by the re-check sweep is picked up on the next one.\n */\n const identityByHost = new Map<\n string,\n Awaited<ReturnType<typeof hostSendingIdentity>>\n >()\n /** Each site's public origin, for the unsubscribe link on the alert. */\n const siteBaseByHost = new Map<string, string>()\n /**\n * Each site's consent group, off the same org read as its branding, so the\n * gate honors an unsubscribe from any site the org declared one sender\n * with this one, and the org's word on whether its sites wait for each\n * other's confirmation click.\n */\n const consentGroupByHost = new Map<string, Aglyn.ConsentGroup>()\n let skippedLocked = 0\n for (const docSnapshot of alerts.docs) {\n const hostRef = docSnapshot.ref.parent.parent\n if (!hostRef) continue\n // LOCKDOWN (AGL-2495), before anything in this body writes. The poisoned-\n // row retirement below stamps `notifiedAtMs`, which is a write, and the\n // email is a message sent in a suspended merchant's name — both wait.\n //\n // SKIPPED, NOT DROPPED: an alert that is not stamped is re-scanned on the\n // next beat, which is exactly the behaviour the `notifiedAtMs == null`\n // query already relies on.\n if (await gate.isLocked(hostRef.id)) {\n skippedLocked += 1\n continue\n }\n const data = docSnapshot.data() as any\n // AGL-1774: `productId` is a STORED FIELD written by an unauthenticated\n // POST (`notify-restock.ts`), and this line is where it becomes a path\n // component. `.doc()` appends a slash-separated path and throws\n // SYNCHRONOUSLY on an even component count — a throw that landed here,\n // outside the loop body's own error handling, so it aborted the entire\n // run. The scan is a `collectionGroup`, so that run is platform-wide;\n // and because the offending alert was never stamped `notifiedAtMs` it\n // returned on every subsequent run, taking every alert ordered after it\n // down too. One anonymous request stopped back-in-stock email for every\n // merchant, permanently.\n //\n // `notify-restock.ts` now refuses such a value at the door, but this\n // guard is not redundant with that one: rows written BEFORE the fix are\n // already in the database, and a create-time check does nothing about\n // them. Treated as \"no product\" rather than as an error, which is the\n // correct refusal by AGL-1760's test — there is no product, so there is\n // no email owed and no work to discard — and it stamps the alert below\n // so a poisoned row is retired instead of re-scanned forever.\n const productId = isDocumentId(data.productId) ? data.productId : ''\n const cacheKey = `${hostRef.id}:${productId}`\n if (productId && !productCache.has(cacheKey)) {\n const productSnapshot = await hostRef\n .collection('products')\n .doc(productId)\n .get()\n productCache.set(\n cacheKey,\n productSnapshot.exists\n ? CommerceModel.liftLegacyProduct(productSnapshot.data() as any)\n : null,\n )\n }\n const product = productId ? productCache.get(cacheKey) : null\n if (!product) {\n await docSnapshot.ref\n .set({ notifiedAtMs: Date.now(), skipped: true }, { merge: true })\n .catch(() => undefined)\n continue\n }\n const total = CommerceModel.productInventory(product)\n if (total != null && total <= 0) continue\n let loaded = templateCache.get(hostRef.id)\n if (loaded === undefined) {\n loaded = await // The template, or the built-in copy in the site's header and\n // footer, once per site for the whole batch (AGL-3287, AGL-3370).\n loadHostEmailWithTokens(firestore, hostRef.id, 'back-in-stock')\n templateCache.set(hostRef.id, loaded)\n const owner = await getOrgForHost(hostRef.id).catch(() => null)\n brandingByHost.set(\n hostRef.id,\n Aglyn.resolveBrandingProfile(owner?.org as never),\n )\n consentGroupByHost.set(\n hostRef.id,\n Aglyn.consentGroupForHost(\n (owner?.org as Record<string, unknown> | undefined) ?? null,\n hostRef.id,\n ),\n )\n // The site's own origin, for the unsubscribe link. Resolved once per\n // host beside the branding, because this sweep is a `collectionGroup`\n // over every site's alerts and a read per alert would be a read per\n // shopper.\n const hostSnapshot = await hostRef.get().catch(() => null)\n siteBaseByHost.set(\n hostRef.id,\n Aglyn.hostPublicOrigin({\n cname: hostSnapshot?.get('cname'),\n subdomain: hostSnapshot?.get('subdomain'),\n }) ?? '',\n )\n }\n // ABSOLUTE, on the site's own origin (AGL-3432): a bare `/products/…`\n // path is a dead link in every inbox, and it is the email's only button.\n const productUrl = `${siteBaseByHost.get(hostRef.id) ?? ''}/products/${product.slug}`\n const designed = loaded\n ? renderLoadedHostEmailWithTokens(\n loaded,\n {\n 'product.name': String(product.name ?? ''),\n 'product.url': productUrl,\n },\n )\n : null\n /*\n * MARKETING, and `'bulk'` priority, which this sweep is entitled to\n * because it is resumable: an alert left unstamped is re-scanned by the\n * `notifiedAtMs == null` query on the next beat, so a refusal means \"not\n * this hour\" rather than a shopper who asked to be told and never was.\n *\n * The gate adds the unsubscribe header pair and a visible link, checks\n * both suppression lists, and counts this against how much mail the\n * shopper has had from this site today.\n */\n const consentGroup =\n consentGroupByHost.get(hostRef.id) ?? Aglyn.soloConsentGroup(hostRef.id)\n const result = await sendEmail({\n to: String(data.email),\n subject: designed?.subject ?? `Back in stock: ${product.name}`,\n text:\n designed?.text ||\n `${product.name} is back in stock:\\n\\n${productUrl}`,\n ...(designed?.html ? { html: designed.html } : {}),\n fromName: brandingByHost.get(hostRef.id)?.fromName,\n sendingIdentity: await hostSendingIdentity(hostRef.id, identityByHost),\n audience: 'tenant',\n context: 'restock alert',\n priority: 'bulk',\n /*\n * A restock alert IS the \"Product updates\" stream — the catalog entry\n * names restocks in its own description. Declared so a shopper who\n * unticked that stream on the preference page is not mailed it: the\n * gate asks the topic only for callers that name one, so a sender that\n * left this off would keep mailing them.\n */\n marketing: {\n hostId: hostRef.id,\n siteBase: siteBaseByHost.get(hostRef.id) ?? '',\n topicId: CommerceModel.PRODUCT_UPDATES_TOPIC_ID,\n consentHostIds: consentGroup.hostIds,\n consentAwaitsConfirmation: consentGroup.awaitsConfirmation,\n },\n })\n /*\n * SKIPPED, NOT DROPPED — the lockdown rule above, applied to the two\n * refusals a later beat can pass.\n *\n * `notifiedAtMs` is what retires an alert from this sweep. Stamping it\n * after the hourly ceiling or the frequency cap refused would drop a\n * request from a shopper who asked to be told; NOT stamping it after a\n * suppression or a rejection would re-read the same doomed row forever,\n * inside a window of two hundred, starving every alert behind it.\n */\n if (isDeferrableSendResult(result)) continue\n // Cost meter (AGL-1438). One alert per shopper who asked to be told,\n // like the abandoned-checkout reminder beside it, and on the DELIVERED\n // message only — a suppressed recipient produced no message and no cost.\n if (result.sent) await meterHostEmail(hostRef.id)\n await docSnapshot.ref\n .set({ notifiedAtMs: Date.now() }, { merge: true })\n .catch(() => undefined)\n if (result.sent) sent += 1\n }\n return { scanned: alerts.size, sent, skippedLocked }\n}\n\n/**\n * HTTP door for the pass above (AGL-326), kept for manual/ops invocation and\n * for the console's \"send now\" control.\n */\nexport const processRestockHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const cronSecret = process.env.CRON_SECRET\n if (!cronSecret || req.headers['x-cron-secret'] !== cronSecret) {\n return res.status(401).json({ error: 'Unauthorized' })\n }\n if (!isEmailConfigured()) {\n return res.status(501).json({ error: 'Email is not configured.' })\n }\n try {\n // The manual door asks the same question the beat does (AGL-2495).\n return res.status(200).json(await scanRestockAlerts(siteLockdownJobGate()))\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Processing failed' })\n }\n}\n"],"names":["Aglyn","CommerceModel","firebaseAdmin","getOrgForHost","hostSendingIdentity","meterHostEmail","isDocumentId","isDeferrableSendResult","isEmailConfigured","sendEmail","siteLockdownJobGate","loadHostEmailWithTokens","renderLoadedHostEmailWithTokens","scanRestockAlerts","gate","firestore","app","alerts","collectionGroup","where","limit","get","sent","productCache","Map","templateCache","brandingByHost","identityByHost","siteBaseByHost","consentGroupByHost","skippedLocked","docSnapshot","docs","product","hostRef","ref","parent","isLocked","id","data","productId","cacheKey","has","productSnapshot","collection","doc","set","exists","liftLegacyProduct","notifiedAtMs","Date","now","skipped","merge","catch","undefined","total","productInventory","loaded","owner","resolveBrandingProfile","org","consentGroupForHost","hostSnapshot","hostPublicOrigin","cname","subdomain","productUrl","slug","designed","String","name","consentGroup","soloConsentGroup","result","to","email","subject","text","html","fromName","sendingIdentity","audience","context","priority","marketing","hostId","siteBase","topicId","PRODUCT_UPDATES_TOPIC_ID","consentHostIds","hostIds","consentAwaitsConfirmation","awaitsConfirmation","scanned","size","processRestockHandler","req","res","method","status","json","error","cronSecret","process","env","CRON_SECRET","headers","console"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/process-restock.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 * as Aglyn from '@aglyn/aglyn/server'\nimport * as CommerceModel from '../model'\nimport {\n firebaseAdmin,\n getOrgForHost,\n hostSendingIdentity,\n meterHostEmail,\n} from '@aglyn/tenant-data-admin'\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\nimport {\n isDeferrableSendResult,\n isEmailConfigured,\n sendEmail,\n} from '@aglyn/shared-util-email'\nimport {\n type PluginApiHandler,\n type PluginJobHostGate,\n} from '@aglyn/aglyn/server'\n// The leaf, not the barrel: the console's `x-cron-secret` door asks the\n// site's lockdown directly (AGL-3356) — core's registry has no resolver in\n// the console process — and a spec that substitutes the barrel must still\n// reach the real verdict.\nimport { siteLockdownJobGate } from '@aglyn/tenant-data-admin/server/tenant-write-lockdown'\nimport {\n loadHostEmailWithTokens,\n renderLoadedHostEmailWithTokens,\n type LoadedHostEmailWithTokens,\n} from '@aglyn/tenant-data-admin/server/host-email-tokens'\n\nexport interface RestockScanResult {\n scanned: number\n sent: number\n /** Alerts left unstamped because their site is locked (AGL-2495). */\n skippedLocked: number\n}\n\n/**\n * The back-in-stock pass itself, separated from its HTTP door (AGL-2227).\n *\n * Same finding as `process-abandoned.ts` beside it: `x-cron-secret`-gated and\n * **never invoked by anything**. `commerce/notify-restock` accepts the\n * shopper's \"tell me when it's back\" from the storefront and writes\n * `hosts/{hostId}/restockAlerts`; this is the only thing that would ever mail\n * them. Shoppers have been entering an address into a queue with no drain.\n *\n * Exported so `server.ts` can put it on the platform job beat.\n */\nexport async function scanRestockAlerts(\n /** The lockdown gate, injected by the caller (AGL-2495). Not optional. */\n gate: PluginJobHostGate,\n): Promise<RestockScanResult> {\n const firestore = firebaseAdmin.app().firestore()\n const alerts = await firestore\n .collectionGroup('restockAlerts')\n .where('notifiedAtMs', '==', null)\n .limit(200)\n .get()\n let sent = 0\n const productCache = new Map<string, CommerceModel.HostProduct | null>()\n // Resolve each host's designed template once per run (AGL-770).\n const templateCache = new Map<string, LoadedHostEmailWithTokens | null>()\n // White-label brand per host (White-Label Phase 3): resolved once per host\n // from the owning org doc through the one shared resolver.\n const brandingByHost = new Map<string, Aglyn.ResolvedBrandingProfile>()\n /*\n * One identity resolution per SITE, not per message.\n *\n * The same reason the branding map above exists: this sweep runs across\n * every site, and an uncached resolution here would be two Firestore reads\n * per recipient rather than per host. Scoped to the run, so a domain\n * un-verified by the re-check sweep is picked up on the next one.\n */\n const identityByHost = new Map<\n string,\n Awaited<ReturnType<typeof hostSendingIdentity>>\n >()\n /** Each site's public origin, for the unsubscribe link on the alert. */\n const siteBaseByHost = new Map<string, string>()\n /**\n * Each site's consent group, off the same org read as its branding, so the\n * gate honors an unsubscribe from any site the org declared one sender\n * with this one, and the org's word on whether its sites wait for each\n * other's confirmation click.\n */\n const consentGroupByHost = new Map<string, Aglyn.ConsentGroup>()\n let skippedLocked = 0\n for (const docSnapshot of alerts.docs) {\n const hostRef = docSnapshot.ref.parent.parent\n if (!hostRef) continue\n // LOCKDOWN (AGL-2495), before anything in this body writes. The poisoned-\n // row retirement below stamps `notifiedAtMs`, which is a write, and the\n // email is a message sent in a suspended merchant's name — both wait.\n //\n // SKIPPED, NOT DROPPED: an alert that is not stamped is re-scanned on the\n // next beat, which is exactly the behaviour the `notifiedAtMs == null`\n // query already relies on.\n if (await gate.isLocked(hostRef.id)) {\n skippedLocked += 1\n continue\n }\n const data = docSnapshot.data() as any\n // AGL-1774: `productId` is a STORED FIELD written by an unauthenticated\n // POST (`notify-restock.ts`), and this line is where it becomes a path\n // component. `.doc()` appends a slash-separated path and throws\n // SYNCHRONOUSLY on an even component count — a throw that landed here,\n // outside the loop body's own error handling, so it aborted the entire\n // run. The scan is a `collectionGroup`, so that run is platform-wide;\n // and because the offending alert was never stamped `notifiedAtMs` it\n // returned on every subsequent run, taking every alert ordered after it\n // down too. One anonymous request stopped back-in-stock email for every\n // merchant, permanently.\n //\n // `notify-restock.ts` now refuses such a value at the door, but this\n // guard is not redundant with that one: rows written BEFORE the fix are\n // already in the database, and a create-time check does nothing about\n // them. Treated as \"no product\" rather than as an error, which is the\n // correct refusal by AGL-1760's test — there is no product, so there is\n // no email owed and no work to discard — and it stamps the alert below\n // so a poisoned row is retired instead of re-scanned forever.\n const productId = isDocumentId(data.productId) ? data.productId : ''\n const cacheKey = `${hostRef.id}:${productId}`\n if (productId && !productCache.has(cacheKey)) {\n const productSnapshot = await hostRef\n .collection('products')\n .doc(productId)\n .get()\n productCache.set(\n cacheKey,\n productSnapshot.exists\n ? CommerceModel.liftLegacyProduct(productSnapshot.data() as any)\n : null,\n )\n }\n const product = productId ? productCache.get(cacheKey) : null\n if (!product) {\n await docSnapshot.ref\n .set({ notifiedAtMs: Date.now(), skipped: true }, { merge: true })\n .catch(() => undefined)\n continue\n }\n const total = CommerceModel.productInventory(product)\n if (total != null && total <= 0) continue\n // Sold in person only (AGL-4060): the email's one button is the product's\n // page, which is not served. The alert waits, unstamped, and goes out once\n // the product sells online again.\n if (!CommerceModel.productSellsOnline(product)) continue\n let loaded = templateCache.get(hostRef.id)\n if (loaded === undefined) {\n loaded = await // The template, or the built-in copy in the site's header and\n // footer, once per site for the whole batch (AGL-3287, AGL-3370).\n loadHostEmailWithTokens(firestore, hostRef.id, 'back-in-stock')\n templateCache.set(hostRef.id, loaded)\n const owner = await getOrgForHost(hostRef.id).catch(() => null)\n brandingByHost.set(\n hostRef.id,\n Aglyn.resolveBrandingProfile(owner?.org as never),\n )\n consentGroupByHost.set(\n hostRef.id,\n Aglyn.consentGroupForHost(\n (owner?.org as Record<string, unknown> | undefined) ?? null,\n hostRef.id,\n ),\n )\n // The site's own origin, for the unsubscribe link. Resolved once per\n // host beside the branding, because this sweep is a `collectionGroup`\n // over every site's alerts and a read per alert would be a read per\n // shopper.\n const hostSnapshot = await hostRef.get().catch(() => null)\n siteBaseByHost.set(\n hostRef.id,\n Aglyn.hostPublicOrigin({\n cname: hostSnapshot?.get('cname'),\n subdomain: hostSnapshot?.get('subdomain'),\n }) ?? '',\n )\n }\n // ABSOLUTE, on the site's own origin (AGL-3432): a bare `/products/…`\n // path is a dead link in every inbox, and it is the email's only button.\n const productUrl = `${siteBaseByHost.get(hostRef.id) ?? ''}/products/${product.slug}`\n const designed = loaded\n ? renderLoadedHostEmailWithTokens(\n loaded,\n {\n 'product.name': String(product.name ?? ''),\n 'product.url': productUrl,\n },\n )\n : null\n /*\n * MARKETING, and `'bulk'` priority, which this sweep is entitled to\n * because it is resumable: an alert left unstamped is re-scanned by the\n * `notifiedAtMs == null` query on the next beat, so a refusal means \"not\n * this hour\" rather than a shopper who asked to be told and never was.\n *\n * The gate adds the unsubscribe header pair and a visible link, checks\n * both suppression lists, and counts this against how much mail the\n * shopper has had from this site today.\n */\n const consentGroup =\n consentGroupByHost.get(hostRef.id) ?? Aglyn.soloConsentGroup(hostRef.id)\n const result = await sendEmail({\n to: String(data.email),\n subject: designed?.subject ?? `Back in stock: ${product.name}`,\n text:\n designed?.text ||\n `${product.name} is back in stock:\\n\\n${productUrl}`,\n ...(designed?.html ? { html: designed.html } : {}),\n fromName: brandingByHost.get(hostRef.id)?.fromName,\n sendingIdentity: await hostSendingIdentity(hostRef.id, identityByHost),\n audience: 'tenant',\n context: 'restock alert',\n priority: 'bulk',\n /*\n * A restock alert IS the \"Product updates\" stream — the catalog entry\n * names restocks in its own description. Declared so a shopper who\n * unticked that stream on the preference page is not mailed it: the\n * gate asks the topic only for callers that name one, so a sender that\n * left this off would keep mailing them.\n */\n marketing: {\n hostId: hostRef.id,\n siteBase: siteBaseByHost.get(hostRef.id) ?? '',\n topicId: CommerceModel.PRODUCT_UPDATES_TOPIC_ID,\n consentHostIds: consentGroup.hostIds,\n consentAwaitsConfirmation: consentGroup.awaitsConfirmation,\n },\n })\n /*\n * SKIPPED, NOT DROPPED — the lockdown rule above, applied to the two\n * refusals a later beat can pass.\n *\n * `notifiedAtMs` is what retires an alert from this sweep. Stamping it\n * after the hourly ceiling or the frequency cap refused would drop a\n * request from a shopper who asked to be told; NOT stamping it after a\n * suppression or a rejection would re-read the same doomed row forever,\n * inside a window of two hundred, starving every alert behind it.\n */\n if (isDeferrableSendResult(result)) continue\n // Cost meter (AGL-1438). One alert per shopper who asked to be told,\n // like the abandoned-checkout reminder beside it, and on the DELIVERED\n // message only — a suppressed recipient produced no message and no cost.\n if (result.sent) await meterHostEmail(hostRef.id)\n await docSnapshot.ref\n .set({ notifiedAtMs: Date.now() }, { merge: true })\n .catch(() => undefined)\n if (result.sent) sent += 1\n }\n return { scanned: alerts.size, sent, skippedLocked }\n}\n\n/**\n * HTTP door for the pass above (AGL-326), kept for manual/ops invocation and\n * for the console's \"send now\" control.\n */\nexport const processRestockHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const cronSecret = process.env.CRON_SECRET\n if (!cronSecret || req.headers['x-cron-secret'] !== cronSecret) {\n return res.status(401).json({ error: 'Unauthorized' })\n }\n if (!isEmailConfigured()) {\n return res.status(501).json({ error: 'Email is not configured.' })\n }\n try {\n // The manual door asks the same question the beat does (AGL-2495).\n return res.status(200).json(await scanRestockAlerts(siteLockdownJobGate()))\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Processing failed' })\n }\n}\n"],"names":["Aglyn","CommerceModel","firebaseAdmin","getOrgForHost","hostSendingIdentity","meterHostEmail","isDocumentId","isDeferrableSendResult","isEmailConfigured","sendEmail","siteLockdownJobGate","loadHostEmailWithTokens","renderLoadedHostEmailWithTokens","scanRestockAlerts","gate","firestore","app","alerts","collectionGroup","where","limit","get","sent","productCache","Map","templateCache","brandingByHost","identityByHost","siteBaseByHost","consentGroupByHost","skippedLocked","docSnapshot","docs","product","hostRef","ref","parent","isLocked","id","data","productId","cacheKey","has","productSnapshot","collection","doc","set","exists","liftLegacyProduct","notifiedAtMs","Date","now","skipped","merge","catch","undefined","total","productInventory","productSellsOnline","loaded","owner","resolveBrandingProfile","org","consentGroupForHost","hostSnapshot","hostPublicOrigin","cname","subdomain","productUrl","slug","designed","String","name","consentGroup","soloConsentGroup","result","to","email","subject","text","html","fromName","sendingIdentity","audience","context","priority","marketing","hostId","siteBase","topicId","PRODUCT_UPDATES_TOPIC_ID","consentHostIds","hostIds","consentAwaitsConfirmation","awaitsConfirmation","scanned","size","processRestockHandler","req","res","method","status","json","error","cronSecret","process","env","CRON_SECRET","headers","console"],"mappings":"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"}
|
|
@@ -17,7 +17,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
17
17
|
*/ import { registerPluginRecordCardReader } from "@aglyn/aglyn/plugin-manager/plugin-record-cards";
|
|
18
18
|
import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
19
19
|
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
20
|
-
import { productPriceRange } from "../model/index.js";
|
|
20
|
+
import { productPriceRange, productSellsOnline } from "../model/index.js";
|
|
21
21
|
/**
|
|
22
22
|
* What a product looks like in one line and one image, for a surface that is
|
|
23
23
|
* not this plugin's own — a designed email that features it, today.
|
|
@@ -36,7 +36,7 @@ import { productPriceRange } from "../model/index.js";
|
|
|
36
36
|
caption: `$${minPrice}`
|
|
37
37
|
} : {}, ((_data_imageUrl = data['imageUrl']) != null ? _data_imageUrl : (_data_mediaUrls = data['mediaUrls']) == null ? void 0 : _data_mediaUrls[0]) ? {
|
|
38
38
|
imageUrl: (_data_imageUrl1 = data['imageUrl']) != null ? _data_imageUrl1 : (_data_mediaUrls1 = data['mediaUrls']) == null ? void 0 : _data_mediaUrls1[0]
|
|
39
|
-
} : {}, data['slug'] ? {
|
|
39
|
+
} : {}, data['slug'] && productSellsOnline(data) ? {
|
|
40
40
|
path: `/products/${data['slug']}`
|
|
41
41
|
} : {});
|
|
42
42
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/product-card.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 {\n registerPluginRecordCardReader,\n type PluginRecordCard,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-cards'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { productPriceRange } from '../model'\n\n/**\n * What a product looks like in one line and one image, for a surface that is\n * not this plugin's own — a designed email that features it, today.\n *\n * The rule that matters is the caption: a product's \"from\" price is the lowest\n * variant price, and {@link productPriceRange} is the one place that is worked\n * out. A caller that read the product document itself would have to import it\n * to agree, which is how the marketing plugin came to depend on this one.\n */\nexport function productCard(\n productId: string,\n data: Record<string, any>,\n): PluginRecordCard {\n const [minPrice] = productPriceRange(data as Parameters<typeof productPriceRange>[0])\n return {\n title: String(data['name'] ?? productId),\n ...(minPrice ? { caption: `$${minPrice}` } : {}),\n ...(data['imageUrl'] ?? data['mediaUrls']?.[0]\n ? { imageUrl: data['imageUrl'] ?? data['mediaUrls']?.[0] }\n : {}),\n ...(data['slug'] ? { path: `/products/${data['slug']}` } : {}),\n }\n}\n\n/** Registered from both server registrars, so either app's handlers can ask. */\nexport function registerProductCardReader(): void {\n registerPluginRecordCardReader(\n 'product',\n {\n async read({ hostId, id }) {\n const snapshot = await firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('products')\n .doc(id)\n .get()\n return snapshot.exists\n ? productCard(id, snapshot.data() as Record<string, any>)\n : null\n },\n },\n { pluginId: BUNDLE_ID },\n )\n}\n"],"names":["registerPluginRecordCardReader","firebaseAdmin","BUNDLE_ID","productPriceRange","productCard","productId","data","minPrice","title","String","caption","imageUrl","path","registerProductCardReader","read","hostId","id","snapshot","app","firestore","collection","doc","get","exists","pluginId"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,8BAA8B,QAEzB,kDAAiD;AACxD,SAASC,aAAa,QAAQ,2BAA0B;AACxD,SAASC,SAAS,QAAQ,gCAA4B;AACtD,SAASC,iBAAiB,QAAQ,oBAAU;
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/product-card.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 {\n registerPluginRecordCardReader,\n type PluginRecordCard,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-cards'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { productPriceRange, productSellsOnline } from '../model'\n\n/**\n * What a product looks like in one line and one image, for a surface that is\n * not this plugin's own — a designed email that features it, today.\n *\n * The rule that matters is the caption: a product's \"from\" price is the lowest\n * variant price, and {@link productPriceRange} is the one place that is worked\n * out. A caller that read the product document itself would have to import it\n * to agree, which is how the marketing plugin came to depend on this one.\n */\nexport function productCard(\n productId: string,\n data: Record<string, any>,\n): PluginRecordCard {\n const [minPrice] = productPriceRange(data as Parameters<typeof productPriceRange>[0])\n return {\n title: String(data['name'] ?? productId),\n ...(minPrice ? { caption: `$${minPrice}` } : {}),\n ...(data['imageUrl'] ?? data['mediaUrls']?.[0]\n ? { imageUrl: data['imageUrl'] ?? data['mediaUrls']?.[0] }\n : {}),\n // A product sold in person only (AGL-4060) has no page to link to: the\n // card still shows it, and opens nothing.\n ...(data['slug'] && productSellsOnline(data) ? { path: `/products/${data['slug']}` } : {}),\n }\n}\n\n/** Registered from both server registrars, so either app's handlers can ask. */\nexport function registerProductCardReader(): void {\n registerPluginRecordCardReader(\n 'product',\n {\n async read({ hostId, id }) {\n const snapshot = await firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('products')\n .doc(id)\n .get()\n return snapshot.exists\n ? productCard(id, snapshot.data() as Record<string, any>)\n : null\n },\n },\n { pluginId: BUNDLE_ID },\n )\n}\n"],"names":["registerPluginRecordCardReader","firebaseAdmin","BUNDLE_ID","productPriceRange","productSellsOnline","productCard","productId","data","minPrice","title","String","caption","imageUrl","path","registerProductCardReader","read","hostId","id","snapshot","app","firestore","collection","doc","get","exists","pluginId"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,8BAA8B,QAEzB,kDAAiD;AACxD,SAASC,aAAa,QAAQ,2BAA0B;AACxD,SAASC,SAAS,QAAQ,gCAA4B;AACtD,SAASC,iBAAiB,EAAEC,kBAAkB,QAAQ,oBAAU;AAEhE;;;;;;;;CAQC,GACD,OAAO,SAASC,YACdC,SAAiB,EACjBC,IAAyB;QAITA,YAEVA,gBACYA;QADQA,iBACYA;IALtC,MAAM,CAACC,SAAS,GAAGL,kBAAkBI;IACrC,OAAO;QACLE,OAAOC,QAAOH,aAAAA,IAAI,CAAC,OAAO,YAAZA,aAAgBD;OAC1BE,WAAW;QAAEG,SAAS,CAAC,CAAC,EAAEH,UAAU;IAAC,IAAI,CAAC,GAC1CD,EAAAA,iBAAAA,IAAI,CAAC,WAAW,YAAhBA,kBAAoBA,kBAAAA,IAAI,CAAC,YAAY,qBAAjBA,eAAmB,CAAC,EAAE,IAC1C;QAAEK,QAAQ,GAAEL,kBAAAA,IAAI,CAAC,WAAW,YAAhBA,mBAAoBA,mBAAAA,IAAI,CAAC,YAAY,qBAAjBA,gBAAmB,CAAC,EAAE;IAAC,IACvD,CAAC,GAGDA,IAAI,CAAC,OAAO,IAAIH,mBAAmBG,QAAQ;QAAEM,MAAM,CAAC,UAAU,EAAEN,IAAI,CAAC,OAAO,EAAE;IAAC,IAAI,CAAC;AAE5F;AAEA,8EAA8E,GAC9E,OAAO,SAASO;IACdd,+BACE,WACA;QACE,MAAMe,MAAK,EAAEC,MAAM,EAAEC,EAAE,EAAE;YACvB,MAAMC,WAAW,MAAMjB,cACpBkB,GAAG,GACHC,SAAS,GACTC,UAAU,CAAC,SACXC,GAAG,CAACN,QACJK,UAAU,CAAC,YACXC,GAAG,CAACL,IACJM,GAAG;YACN,OAAOL,SAASM,MAAM,GAClBnB,YAAYY,IAAIC,SAASX,IAAI,MAC7B;QACN;IACF,GACA;QAAEkB,UAAUvB;IAAU;AAE1B"}
|
|
@@ -31,6 +31,8 @@ export declare function offerShipping(settings: CommerceModel.ShippingSettings |
|
|
|
31
31
|
weightGrams?: number;
|
|
32
32
|
}): CatalogShippingPrice[];
|
|
33
33
|
interface PageContext {
|
|
34
|
+
/** Offers products sold in person only too (AGL-4060), marked as such. */
|
|
35
|
+
inPersonOnly?: boolean;
|
|
34
36
|
origin: string | null;
|
|
35
37
|
categories: ReadonlyMap<string, CommerceModel.ProductCategory>;
|
|
36
38
|
shipping: CommerceModel.ShippingSettings | undefined;
|