@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/model/commerce-pos.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\n/*==========================================\n * THE POS TENDER LEDGER (AGL-3607).\n *\n * A register sale used to be settled by exactly one tender and recorded none:\n * whether a POS order was paid in cash, by the QR link or to a room was\n * inferred afterwards from which other fields happened to be set. A sale now\n * carries `payments[]`, one entry per tender, and takes payments until the\n * balance due is zero. Only then is it `paid`.\n *\n * Every figure here is integer cents. `amountCents` is what a payment put\n * toward the SALE; a tip rides beside it in `tipCents` and never counts toward\n * the balance, the platform's take or the store's revenue. Tips belong to the\n * merchant and their staff.\n *\n * Pure and framework-free, so the register, the customer display and the\n * server read one answer.\n *=========================================*/\n\n/** How one payment toward a register sale was taken. */\nexport type OrderPaymentMethod =\n | 'cash'\n /** A Stripe Terminal smart reader (tap, insert or swipe). */\n | 'card_present'\n /** A card typed into the console by staff (card not present). */\n | 'card_keyed'\n /** The Stripe payment page the customer opens from a QR code. */\n | 'card_link'\n | 'gift_card'\n /** Charged to a checked-in reservation's folio. */\n | 'folio'\n /**\n * Store credit another plugin keeps — rewards, referral credit — taken\n * through core's checkout-credits seam (AGL-3640). Which plugin and which\n * account ride on the payment as `creditProviderId` and `creditReference`.\n */\n | 'credit'\n\nexport type OrderPaymentStatus =\n /** Started and not finished: reserves its amount against the balance. */\n | 'pending'\n | 'succeeded'\n /** Declined or errored; may be retried, reserves nothing. */\n | 'failed'\n | 'canceled'\n /** Taken and then handed back when the open sale was voided. */\n | 'reversed'\n\nexport const ORDER_PAYMENT_METHODS: readonly OrderPaymentMethod[] = [\n 'cash',\n 'card_present',\n 'card_keyed',\n 'card_link',\n 'gift_card',\n 'folio',\n 'credit',\n]\n\n/** One payment toward a register sale, on `HostOrder.payments`. */\nexport interface OrderPayment {\n /** Stable id, derived from the register's attempt key so a retry finds it. */\n id: string\n method: OrderPaymentMethod\n /** Cents put toward the sale. Never includes the tip. */\n amountCents: number\n /** Gratuity on top of `amountCents`, the merchant's and not revenue. */\n tipCents?: number\n status: OrderPaymentStatus\n /** When the payment was started. */\n atMs: number\n /** When it reached `succeeded`. */\n settledAtMs?: number\n /** Stripe PaymentIntent for `card_present` / `card_keyed` / a paid link. */\n paymentIntentId?: string\n /** Stripe Checkout Session for `card_link`. */\n checkoutSessionId?: string\n /** The payment page a `card_link` payment shows as a QR. */\n checkoutUrl?: string\n /** The Terminal reader a `card_present` payment runs on. */\n readerId?: string\n /** The gift card's code (its document id) for `gift_card`. */\n giftCardId?: string\n /** For `credit`: the provider that paid it (`{pluginId}.{key}`, AGL-3640). */\n creditProviderId?: string\n /** For `credit`: the provider's own handle on the account it came from. */\n creditReference?: string\n /** For `credit`: what the provider calls it, for the register and receipt. */\n creditLabel?: string\n cardBrand?: string\n last4?: string\n /** Cash handed over for a `cash` payment, and the change given back. */\n cashTenderedCents?: number\n changeCents?: number\n /** The stay a `folio` payment was charged to. */\n reservationId?: string\n /** The folio entry's `atMs`, so a void can remove exactly that entry. */\n folioAtMs?: number\n /**\n * The platform's take attributed to this payment (excludes the tip). For a\n * card payment it is netted from the payout with Stripe's processing cost\n * added, and recorded in `feeCents`; otherwise it accrues to the invoice.\n */\n takeFeeCents?: number\n /** The `application_fee_amount` a card payment carried, or 0. */\n feeCents?: number\n /** Why a `failed` payment failed, in words the cashier can read out. */\n failureMessage?: string\n /** Whether Stripe moved real money; absent for tenders Stripe never saw. */\n livemode?: boolean\n /** The console user who took the payment. */\n cashierId?: string\n}\n\n/** The tenders whose money moves through Stripe. */\nexport const POS_CARD_METHODS: ReadonlySet<OrderPaymentMethod> = new Set([\n 'card_present',\n 'card_keyed',\n 'card_link',\n])\n\nexport function isCardPaymentMethod(method: OrderPaymentMethod): boolean {\n return POS_CARD_METHODS.has(method)\n}\n\n/** Whole non-negative cents from an untrusted value. */\nfunction wholeCents(value: unknown): number {\n const number = Math.round(Number(value))\n return Number.isFinite(number) && number > 0 ? number : 0\n}\n\n/**\n * The order's payments, read through one door.\n *\n * Orders written before the ledger existed carry no `payments`, so one is\n * INFERRED from the fields the single-tender register wrote: a folio sale has\n * `reservationId`, a QR sale has `checkoutSessionId`, and anything else on the\n * POS channel was cash. A pending legacy sale is the QR link still waiting.\n * Online orders paid through Stripe Checkout read as one `card_link` payment.\n * Nothing is written back: this is a reader, never a migration.\n */\nexport function orderPayments(order: {\n payments?: OrderPayment[] | null\n channel?: string\n status?: string\n totals?: { totalCents?: number; tipCents?: number } | null\n amountCents?: number\n reservationId?: string\n checkoutSessionId?: string\n paymentIntentId?: string\n createdAtMs?: number\n}): OrderPayment[] {\n if (Array.isArray(order?.payments)) {\n return order.payments.filter(\n (payment): payment is OrderPayment =>\n Boolean(payment) && typeof payment === 'object' && Boolean(payment.id),\n )\n }\n const totalCents = wholeCents(order?.totals?.totalCents ?? order?.amountCents)\n if (!(totalCents > 0)) return []\n const settled = order?.status !== 'pending' && order?.status !== 'cancelled'\n const atMs = Number(order?.createdAtMs) || 0\n const method: OrderPaymentMethod = order?.reservationId\n ? 'folio'\n : order?.checkoutSessionId || order?.paymentIntentId || order?.channel !== 'pos'\n ? 'card_link'\n : 'cash'\n return [\n {\n id: 'legacy',\n method,\n amountCents: totalCents,\n status: settled ? 'succeeded' : order?.status === 'cancelled' ? 'canceled' : 'pending',\n atMs,\n ...(order?.paymentIntentId ? { paymentIntentId: order.paymentIntentId } : {}),\n ...(order?.checkoutSessionId ? { checkoutSessionId: order.checkoutSessionId } : {}),\n ...(order?.reservationId ? { reservationId: order.reservationId } : {}),\n },\n ]\n}\n\n/** Cents put toward the sale by payments that succeeded. */\nexport function posSettledCents(payments: readonly OrderPayment[]): number {\n return payments\n .filter((payment) => payment.status === 'succeeded')\n .reduce((sum, payment) => sum + wholeCents(payment.amountCents), 0)\n}\n\n/**\n * Cents spoken for: succeeded payments plus the ones still in flight. A\n * reader waiting for a tap has claimed its share of the balance, so a second\n * tender cannot be started for the same money.\n */\nexport function posReservedCents(payments: readonly OrderPayment[]): number {\n return payments\n .filter((payment) => payment.status === 'succeeded' || payment.status === 'pending')\n .reduce((sum, payment) => sum + wholeCents(payment.amountCents), 0)\n}\n\n/** What the customer still owes, by settled payments. */\nexport function posBalanceDueCents(\n totalCents: number,\n payments: readonly OrderPayment[],\n): number {\n return Math.max(0, wholeCents(totalCents) - posSettledCents(payments))\n}\n\n/** What a NEW payment may still take: the balance less what is in flight. */\nexport function posTenderableCents(\n totalCents: number,\n payments: readonly OrderPayment[],\n): number {\n return Math.max(0, wholeCents(totalCents) - posReservedCents(payments))\n}\n\n/** Tips taken across the sale's settled payments. */\nexport function posTipCents(payments: readonly OrderPayment[]): number {\n return payments\n .filter((payment) => payment.status === 'succeeded')\n .reduce((sum, payment) => sum + wholeCents(payment.tipCents), 0)\n}\n\n/**\n * The share of the sale's take a payment of `amountCents` carries.\n *\n * Proportional to the part of the SALE it pays, never to the tip. Card\n * payments carry their share in `application_fee_amount`; whatever the card\n * shares leave is accrued to the invoice at completion\n * ({@link posInvoiceTakeCents}), so the take is exact however the sale was\n * split and however each share rounded.\n */\nexport function posTakeShareCents(input: {\n takeFeeCents: number\n totalCents: number\n amountCents: number\n}): number {\n const take = wholeCents(input.takeFeeCents)\n const total = wholeCents(input.totalCents)\n const amount = Math.min(wholeCents(input.amountCents), total)\n if (!(take > 0) || !(total > 0) || !(amount > 0)) return 0\n return Math.min(take, Math.round((take * amount) / total))\n}\n\n/**\n * What of the sale's take was NOT netted from a card payout, and therefore\n * goes on the merchant's invoice: the whole take less every succeeded card\n * payment's share. Never negative.\n */\nexport function posInvoiceTakeCents(input: {\n takeFeeCents: number\n payments: readonly OrderPayment[]\n}): number {\n const netted = input.payments\n .filter(\n (payment) =>\n payment.status === 'succeeded' && isCardPaymentMethod(payment.method),\n )\n .reduce((sum, payment) => sum + wholeCents(payment.takeFeeCents), 0)\n return Math.max(0, wholeCents(input.takeFeeCents) - netted)\n}\n\n/** How the sale's fee reaches the platform, from what actually paid it. */\nexport function posFeeCollection(\n payments: readonly OrderPayment[],\n): 'payout' | 'invoice' | 'split' {\n const settled = payments.filter((payment) => payment.status === 'succeeded')\n const card = settled.some((payment) => isCardPaymentMethod(payment.method))\n const other = settled.some((payment) => !isCardPaymentMethod(payment.method))\n if (card && other) return 'split'\n return card ? 'payout' : 'invoice'\n}\n\n/** A one-line description of a payment for the register, receipt and timeline. */\nexport function describeOrderPayment(payment: OrderPayment): string {\n const dollars = (cents: number) => `$${(cents / 100).toFixed(2)}`\n const label =\n (payment.method === 'credit' && payment.creditLabel) || ORDER_PAYMENT_METHOD_LABELS[payment.method] || 'Payment'\n const card =\n payment.cardBrand || payment.last4\n ? ` (${[payment.cardBrand, payment.last4 ? `•••• ${payment.last4}` : '']\n .filter(Boolean)\n .join(' ')})`\n : ''\n const tip = wholeCents(payment.tipCents)\n ? ` + ${dollars(wholeCents(payment.tipCents))} tip`\n : ''\n return `${label}${card} ${dollars(wholeCents(payment.amountCents))}${tip}`\n}\n\nexport const ORDER_PAYMENT_METHOD_LABELS: Record<OrderPaymentMethod, string> = {\n cash: 'Cash',\n card_present: 'Card reader',\n card_keyed: 'Typed card',\n card_link: 'Card (QR)',\n gift_card: 'Gift card',\n folio: 'Charged to room',\n credit: 'Store credit',\n}\n\n/*==========================================\n * TIPS (AGL-3607).\n *=========================================*/\n\n/** The presets a merchant starts with. */\nexport const POS_TIP_PERCENTAGES_DEFAULT: readonly number[] = [15, 18, 20, 25]\n\n/**\n * The merchant's tip presets from the plugin setting, a comma list such as\n * `15, 18, 20, 25`. Whole percentages from 1 to 100, at most four, in the order\n * given; anything unreadable falls back to the defaults rather than showing\n * the customer an empty choice.\n */\nexport function posTipPercentages(raw: unknown): number[] {\n const parsed = String(raw ?? '')\n .split(/[,\\s]+/)\n .map((part) => Math.round(Number(part)))\n .filter((value) => Number.isFinite(value) && value >= 1 && value <= 100)\n const unique = [...new Set(parsed)].slice(0, 4)\n return unique.length ? unique : [...POS_TIP_PERCENTAGES_DEFAULT]\n}\n\n/** A percentage tip on `baseCents`, rounded to the cent. */\nexport function posTipFromPercent(baseCents: number, percent: number): number {\n const base = wholeCents(baseCents)\n const pct = Number(percent)\n if (!(base > 0) || !Number.isFinite(pct) || pct <= 0) return 0\n return Math.round((base * pct) / 100)\n}\n\n/** The largest tip the register accepts on one payment: the payment itself. */\nexport function posTipProblem(tipCents: number, amountCents: number): string | null {\n if (!Number.isInteger(tipCents) || tipCents < 0) return 'Enter a tip of $0 or more.'\n if (tipCents > wholeCents(amountCents)) {\n return 'A tip can be at most the amount being paid. Check the amount and try again.'\n }\n return null\n}\n\n/*==========================================\n * THE CUSTOMER DISPLAY (AGL-3608).\n *\n * A paired screen facing the customer reads one document per register,\n * `hosts/{hostId}/registers/{registerId}/display/state`, through the\n * `commerce/pos-display` route and its display token. The register writes\n * the prompt; the customer's answer comes back in `response`.\n *=========================================*/\n\nexport type PosDisplayMode =\n | 'idle'\n /** The live basket, mirrored as the cashier builds it. */\n | 'cart'\n /** The customer picks a tip. */\n | 'tip'\n /** The customer picks how they want their receipt. */\n | 'receipt'\n /** A card payment is waiting on the reader. */\n | 'processing'\n | 'thanks'\n\nexport type PosReceiptChannel = 'email' | 'sms' | 'print' | 'none'\n\nexport interface PosDisplayLine {\n name: string\n variantLabel?: string\n quantity: number\n amountCents: number\n}\n\nexport interface PosDisplayCart {\n lines: PosDisplayLine[]\n itemsCents: number\n discountCents: number\n taxCents: number\n totalCents: number\n /** Taken so far, and what is still due, once payments start. */\n paidCents?: number\n dueCents?: number\n tipCents?: number\n}\n\n/** The customer's answer to the current prompt. */\nexport interface PosDisplayResponse {\n /** The prompt this answers; a stale answer is refused. */\n promptId: string\n tipCents?: number\n tipPercent?: number\n /** `none` when the customer chose no tip. */\n tipChoice?: 'percent' | 'custom' | 'none'\n receiptChannel?: PosReceiptChannel\n email?: string\n phone?: string\n marketingOptIn?: boolean\n atMs: number\n}\n\nexport interface PosDisplayState {\n mode: PosDisplayMode\n updatedAtMs: number\n /**\n * The ISO currency every amount on the screen is in (`usd`). Stamped by\n * the server from the store's own currency, never taken from the register.\n */\n currency?: string\n /** Changes every time the register asks something new. */\n promptId?: string\n cart?: PosDisplayCart\n tip?: {\n /** What a percentage is taken of: the amount being paid now. */\n baseCents: number\n percentages: number[]\n allowCustom: boolean\n }\n receipt?: {\n channels: PosReceiptChannel[]\n /** Whether to offer the store's marketing opt-in under the email field. */\n offerMarketing: boolean\n }\n processing?: { message: string }\n response?: PosDisplayResponse\n}\n\n/** Most lines a display state carries; a longer basket shows its newest. */\nexport const POS_DISPLAY_MAX_LINES = 60\n\n/** A pairing code lives this long before the register must show a new one. */\nexport const POS_DISPLAY_PAIRING_TTL_MS = 10 * 60 * 1000\n\n/** A thank-you screen returns to idle on the display after this long. */\nexport const POS_DISPLAY_THANKS_MS = 8_000\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n/** An email address as the display may store it, or '' when it is not one. */\nexport function posDisplayEmail(value: unknown): string {\n const email = String(value ?? '').trim().toLowerCase().slice(0, 254)\n return EMAIL_PATTERN.test(email) ? email : ''\n}\n\n/** A phone number as digits with an optional leading +, or '' when too short. */\nexport function posDisplayPhone(value: unknown): string {\n const raw = String(value ?? '').trim()\n const digits = raw.replace(/[^0-9]/g, '')\n if (digits.length < 7 || digits.length > 15) return ''\n return `${raw.startsWith('+') ? '+' : ''}${digits}`\n}\n\n/**\n * The display state as the REGISTER may send it: clamped, typed and stripped\n * of anything the customer screen has no business holding. The register is\n * staff and trusted for intent, not for shape.\n */\nexport function sanitizePosDisplayState(\n raw: unknown,\n nowMs: number,\n): PosDisplayState {\n const input = (raw && typeof raw === 'object' ? raw : {}) as Record<string, any>\n const modes: PosDisplayMode[] = ['idle', 'cart', 'tip', 'receipt', 'processing', 'thanks']\n const mode = modes.includes(input['mode']) ? (input['mode'] as PosDisplayMode) : 'idle'\n const state: PosDisplayState = { mode, updatedAtMs: nowMs }\n const promptId = String(input['promptId'] ?? '').slice(0, 64)\n if (promptId) state.promptId = promptId\n const cart = input['cart']\n if (cart && typeof cart === 'object' && mode !== 'idle') {\n const lines = (Array.isArray(cart.lines) ? cart.lines : [])\n .slice(-POS_DISPLAY_MAX_LINES)\n .map((line: any) => ({\n name: String(line?.name ?? '').slice(0, 120),\n ...(line?.variantLabel\n ? { variantLabel: String(line.variantLabel).slice(0, 80) }\n : {}),\n quantity: Math.max(1, Math.min(999, Math.round(Number(line?.quantity) || 1))),\n amountCents: wholeCents(line?.amountCents),\n }))\n state.cart = {\n lines,\n itemsCents: wholeCents(cart.itemsCents),\n discountCents: wholeCents(cart.discountCents),\n taxCents: wholeCents(cart.taxCents),\n totalCents: wholeCents(cart.totalCents),\n ...(cart.paidCents != null ? { paidCents: wholeCents(cart.paidCents) } : {}),\n ...(cart.dueCents != null ? { dueCents: wholeCents(cart.dueCents) } : {}),\n ...(cart.tipCents != null ? { tipCents: wholeCents(cart.tipCents) } : {}),\n }\n }\n if (mode === 'tip') {\n const tip = input['tip'] ?? {}\n state.tip = {\n baseCents: wholeCents(tip.baseCents),\n percentages: posTipPercentages(\n Array.isArray(tip.percentages) ? tip.percentages.join(',') : tip.percentages,\n ),\n allowCustom: tip.allowCustom !== false,\n }\n }\n if (mode === 'receipt') {\n const receipt = input['receipt'] ?? {}\n const allowed: PosReceiptChannel[] = ['email', 'sms', 'print', 'none']\n const channels = (Array.isArray(receipt.channels) ? receipt.channels : [])\n .filter((channel: unknown): channel is PosReceiptChannel =>\n allowed.includes(channel as PosReceiptChannel),\n )\n state.receipt = {\n channels: channels.length ? [...new Set<PosReceiptChannel>(channels)] : ['print', 'none'],\n offerMarketing: receipt.offerMarketing === true,\n }\n }\n if (mode === 'processing') {\n state.processing = {\n message:\n String(input['processing']?.message ?? '').slice(0, 160) ||\n 'Tap, insert or swipe your card on the reader.',\n }\n }\n return state\n}\n\n/**\n * The customer's answer as the DISPLAY may send it, checked against the\n * prompt it claims to answer. Returns null when it answers nothing the\n * register asked: a stale screen, a replay, or a mode that takes no answer.\n */\nexport function sanitizePosDisplayResponse(\n state: PosDisplayState | null | undefined,\n raw: unknown,\n nowMs: number,\n): PosDisplayResponse | null {\n if (!state || !state.promptId) return null\n const input = (raw && typeof raw === 'object' ? raw : {}) as Record<string, any>\n if (String(input['promptId'] ?? '') !== state.promptId) return null\n const response: PosDisplayResponse = { promptId: state.promptId, atMs: nowMs }\n if (state.mode === 'tip' && state.tip) {\n const choice = input['tipChoice']\n if (choice === 'none') {\n response.tipChoice = 'none'\n response.tipCents = 0\n } else if (choice === 'percent') {\n const percent = Math.round(Number(input['tipPercent']))\n if (!state.tip.percentages.includes(percent)) return null\n response.tipChoice = 'percent'\n response.tipPercent = percent\n response.tipCents = posTipFromPercent(state.tip.baseCents, percent)\n } else if (choice === 'custom' && state.tip.allowCustom) {\n const tipCents = Math.round(Number(input['tipCents']))\n if (posTipProblem(tipCents, state.tip.baseCents)) return null\n response.tipChoice = 'custom'\n response.tipCents = tipCents\n } else {\n return null\n }\n return response\n }\n if (state.mode === 'receipt' && state.receipt) {\n const channel = input['receiptChannel'] as PosReceiptChannel\n if (!state.receipt.channels.includes(channel)) return null\n response.receiptChannel = channel\n if (channel === 'email') {\n const email = posDisplayEmail(input['email'])\n if (!email) return null\n response.email = email\n if (state.receipt.offerMarketing && input['marketingOptIn'] === true) {\n response.marketingOptIn = true\n }\n }\n if (channel === 'sms') {\n const phone = posDisplayPhone(input['phone'])\n if (!phone) return null\n response.phone = phone\n }\n return response\n }\n return null\n}\n\n/**\n * The state as the DISPLAY reads it. The customer's own typed address is\n * never echoed back to the screen: once answered, the prompt is done and the\n * screen shows what comes next, so nothing a later customer could read off\n * an unattended tablet is ever sent to it.\n */\nexport function posDisplayPublicState(\n state: PosDisplayState | null | undefined,\n nowMs: number,\n): Omit<PosDisplayState, 'response'> & { answered: boolean } {\n if (!state) return { mode: 'idle', updatedAtMs: nowMs, answered: false }\n const { response, ...rest } = state\n if (rest.mode === 'thanks' && nowMs - rest.updatedAtMs > POS_DISPLAY_THANKS_MS) {\n return { mode: 'idle', updatedAtMs: rest.updatedAtMs, answered: false }\n }\n return {\n ...rest,\n answered: Boolean(response && response.promptId === rest.promptId),\n }\n}\n"],"names":["ORDER_PAYMENT_METHODS","POS_CARD_METHODS","Set","isCardPaymentMethod","method","has","wholeCents","value","number","Math","round","Number","isFinite","orderPayments","order","Array","isArray","payments","filter","payment","Boolean","id","totalCents","totals","amountCents","settled","status","atMs","createdAtMs","reservationId","checkoutSessionId","paymentIntentId","channel","posSettledCents","reduce","sum","posReservedCents","posBalanceDueCents","max","posTenderableCents","posTipCents","tipCents","posTakeShareCents","input","take","takeFeeCents","total","amount","min","posInvoiceTakeCents","netted","posFeeCollection","card","some","other","describeOrderPayment","dollars","cents","toFixed","label","creditLabel","ORDER_PAYMENT_METHOD_LABELS","cardBrand","last4","join","tip","cash","card_present","card_keyed","card_link","gift_card","folio","credit","POS_TIP_PERCENTAGES_DEFAULT","posTipPercentages","raw","parsed","String","split","map","part","unique","slice","length","posTipFromPercent","baseCents","percent","base","pct","posTipProblem","isInteger","POS_DISPLAY_MAX_LINES","POS_DISPLAY_PAIRING_TTL_MS","POS_DISPLAY_THANKS_MS","EMAIL_PATTERN","posDisplayEmail","email","trim","toLowerCase","test","posDisplayPhone","digits","replace","startsWith","sanitizePosDisplayState","nowMs","modes","mode","includes","state","updatedAtMs","promptId","cart","lines","line","name","variantLabel","quantity","itemsCents","discountCents","taxCents","paidCents","dueCents","percentages","allowCustom","receipt","allowed","channels","offerMarketing","processing","message","sanitizePosDisplayResponse","response","choice","tipChoice","tipPercent","receiptChannel","marketingOptIn","phone","posDisplayPublicState","answered","rest"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;2CAgB2C,GAE3C,sDAAsD;;AA6BtD,OAAO,MAAMA,wBAAuD;IAClE;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAA;AAyDD,kDAAkD,GAClD,OAAO,MAAMC,mBAAoD,IAAIC,IAAI;IACvE;IACA;IACA;CACD,EAAC;AAEF,OAAO,SAASC,oBAAoBC,MAA0B;IAC5D,OAAOH,iBAAiBI,GAAG,CAACD;AAC9B;AAEA,sDAAsD,GACtD,SAASE,WAAWC,KAAc;IAChC,MAAMC,SAASC,KAAKC,KAAK,CAACC,OAAOJ;IACjC,OAAOI,OAAOC,QAAQ,CAACJ,WAAWA,SAAS,IAAIA,SAAS;AAC1D;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASK,cAAcC,KAU7B;;QAO+BA;IAN9B,IAAIC,MAAMC,OAAO,CAACF,yBAAAA,MAAOG,QAAQ,GAAG;QAClC,OAAOH,MAAMG,QAAQ,CAACC,MAAM,CAC1B,CAACC,UACCC,QAAQD,YAAY,OAAOA,YAAY,YAAYC,QAAQD,QAAQE,EAAE;IAE3E;IACA,MAAMC,aAAahB,mBAAWQ,0BAAAA,gBAAAA,MAAOS,MAAM,qBAAbT,cAAeQ,UAAU,mBAAIR,yBAAAA,MAAOU,WAAW;IAC7E,IAAI,CAAEF,CAAAA,aAAa,CAAA,GAAI,OAAO,EAAE;IAChC,MAAMG,UAAUX,CAAAA,yBAAAA,MAAOY,MAAM,MAAK,aAAaZ,CAAAA,yBAAAA,MAAOY,MAAM,MAAK;IACjE,MAAMC,OAAOhB,OAAOG,yBAAAA,MAAOc,WAAW,KAAK;IAC3C,MAAMxB,SAA6BU,CAAAA,yBAAAA,MAAOe,aAAa,IACnD,UACAf,CAAAA,yBAAAA,MAAOgB,iBAAiB,MAAIhB,yBAAAA,MAAOiB,eAAe,KAAIjB,CAAAA,yBAAAA,MAAOkB,OAAO,MAAK,QACvE,cACA;IACN,OAAO;QACL;YACEX,IAAI;YACJjB;YACAoB,aAAaF;YACbI,QAAQD,UAAU,cAAcX,CAAAA,yBAAAA,MAAOY,MAAM,MAAK,cAAc,aAAa;YAC7EC;WACIb,CAAAA,yBAAAA,MAAOiB,eAAe,IAAG;YAAEA,iBAAiBjB,MAAMiB,eAAe;QAAC,IAAI,CAAC,GACvEjB,CAAAA,yBAAAA,MAAOgB,iBAAiB,IAAG;YAAEA,mBAAmBhB,MAAMgB,iBAAiB;QAAC,IAAI,CAAC,GAC7EhB,CAAAA,yBAAAA,MAAOe,aAAa,IAAG;YAAEA,eAAef,MAAMe,aAAa;QAAC,IAAI,CAAC;KAExE;AACH;AAEA,0DAA0D,GAC1D,OAAO,SAASI,gBAAgBhB,QAAiC;IAC/D,OAAOA,SACJC,MAAM,CAAC,CAACC,UAAYA,QAAQO,MAAM,KAAK,aACvCQ,MAAM,CAAC,CAACC,KAAKhB,UAAYgB,MAAM7B,WAAWa,QAAQK,WAAW,GAAG;AACrE;AAEA;;;;CAIC,GACD,OAAO,SAASY,iBAAiBnB,QAAiC;IAChE,OAAOA,SACJC,MAAM,CAAC,CAACC,UAAYA,QAAQO,MAAM,KAAK,eAAeP,QAAQO,MAAM,KAAK,WACzEQ,MAAM,CAAC,CAACC,KAAKhB,UAAYgB,MAAM7B,WAAWa,QAAQK,WAAW,GAAG;AACrE;AAEA,uDAAuD,GACvD,OAAO,SAASa,mBACdf,UAAkB,EAClBL,QAAiC;IAEjC,OAAOR,KAAK6B,GAAG,CAAC,GAAGhC,WAAWgB,cAAcW,gBAAgBhB;AAC9D;AAEA,2EAA2E,GAC3E,OAAO,SAASsB,mBACdjB,UAAkB,EAClBL,QAAiC;IAEjC,OAAOR,KAAK6B,GAAG,CAAC,GAAGhC,WAAWgB,cAAcc,iBAAiBnB;AAC/D;AAEA,mDAAmD,GACnD,OAAO,SAASuB,YAAYvB,QAAiC;IAC3D,OAAOA,SACJC,MAAM,CAAC,CAACC,UAAYA,QAAQO,MAAM,KAAK,aACvCQ,MAAM,CAAC,CAACC,KAAKhB,UAAYgB,MAAM7B,WAAWa,QAAQsB,QAAQ,GAAG;AAClE;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASC,kBAAkBC,KAIjC;IACC,MAAMC,OAAOtC,WAAWqC,MAAME,YAAY;IAC1C,MAAMC,QAAQxC,WAAWqC,MAAMrB,UAAU;IACzC,MAAMyB,SAAStC,KAAKuC,GAAG,CAAC1C,WAAWqC,MAAMnB,WAAW,GAAGsB;IACvD,IAAI,CAAEF,CAAAA,OAAO,CAAA,KAAM,CAAEE,CAAAA,QAAQ,CAAA,KAAM,CAAEC,CAAAA,SAAS,CAAA,GAAI,OAAO;IACzD,OAAOtC,KAAKuC,GAAG,CAACJ,MAAMnC,KAAKC,KAAK,CAAC,AAACkC,OAAOG,SAAUD;AACrD;AAEA;;;;CAIC,GACD,OAAO,SAASG,oBAAoBN,KAGnC;IACC,MAAMO,SAASP,MAAM1B,QAAQ,CAC1BC,MAAM,CACL,CAACC,UACCA,QAAQO,MAAM,KAAK,eAAevB,oBAAoBgB,QAAQf,MAAM,GAEvE8B,MAAM,CAAC,CAACC,KAAKhB,UAAYgB,MAAM7B,WAAWa,QAAQ0B,YAAY,GAAG;IACpE,OAAOpC,KAAK6B,GAAG,CAAC,GAAGhC,WAAWqC,MAAME,YAAY,IAAIK;AACtD;AAEA,yEAAyE,GACzE,OAAO,SAASC,iBACdlC,QAAiC;IAEjC,MAAMQ,UAAUR,SAASC,MAAM,CAAC,CAACC,UAAYA,QAAQO,MAAM,KAAK;IAChE,MAAM0B,OAAO3B,QAAQ4B,IAAI,CAAC,CAAClC,UAAYhB,oBAAoBgB,QAAQf,MAAM;IACzE,MAAMkD,QAAQ7B,QAAQ4B,IAAI,CAAC,CAAClC,UAAY,CAAChB,oBAAoBgB,QAAQf,MAAM;IAC3E,IAAIgD,QAAQE,OAAO,OAAO;IAC1B,OAAOF,OAAO,WAAW;AAC3B;AAEA,gFAAgF,GAChF,OAAO,SAASG,qBAAqBpC,OAAqB;IACxD,MAAMqC,UAAU,CAACC,QAAkB,CAAC,CAAC,EAAE,AAACA,CAAAA,QAAQ,GAAE,EAAGC,OAAO,CAAC,IAAI;IACjE,MAAMC,QACJ,AAACxC,QAAQf,MAAM,KAAK,YAAYe,QAAQyC,WAAW,IAAKC,2BAA2B,CAAC1C,QAAQf,MAAM,CAAC,IAAI;IACzG,MAAMgD,OACJjC,QAAQ2C,SAAS,IAAI3C,QAAQ4C,KAAK,GAC9B,CAAC,EAAE,EAAE;QAAC5C,QAAQ2C,SAAS;QAAE3C,QAAQ4C,KAAK,GAAG,CAAC,KAAK,EAAE5C,QAAQ4C,KAAK,EAAE,GAAG;KAAG,CACnE7C,MAAM,CAACE,SACP4C,IAAI,CAAC,KAAK,CAAC,CAAC,GACf;IACN,MAAMC,MAAM3D,WAAWa,QAAQsB,QAAQ,IACnC,CAAC,GAAG,EAAEe,QAAQlD,WAAWa,QAAQsB,QAAQ,GAAG,IAAI,CAAC,GACjD;IACJ,OAAO,GAAGkB,QAAQP,KAAK,CAAC,EAAEI,QAAQlD,WAAWa,QAAQK,WAAW,KAAKyC,KAAK;AAC5E;AAEA,OAAO,MAAMJ,8BAAkE;IAC7EK,MAAM;IACNC,cAAc;IACdC,YAAY;IACZC,WAAW;IACXC,WAAW;IACXC,OAAO;IACPC,QAAQ;AACV,EAAC;AAED;;2CAE2C,GAE3C,wCAAwC,GACxC,OAAO,MAAMC,8BAAiD;IAAC;IAAI;IAAI;IAAI;CAAG,CAAA;AAE9E;;;;;CAKC,GACD,OAAO,SAASC,kBAAkBC,GAAY;IAC5C,MAAMC,SAASC,OAAOF,cAAAA,MAAO,IAC1BG,KAAK,CAAC,UACNC,GAAG,CAAC,CAACC,OAASvE,KAAKC,KAAK,CAACC,OAAOqE,QAChC9D,MAAM,CAAC,CAACX,QAAUI,OAAOC,QAAQ,CAACL,UAAUA,SAAS,KAAKA,SAAS;IACtE,MAAM0E,SAAS;WAAI,IAAI/E,IAAI0E;KAAQ,CAACM,KAAK,CAAC,GAAG;IAC7C,OAAOD,OAAOE,MAAM,GAAGF,SAAS;WAAIR;KAA4B;AAClE;AAEA,0DAA0D,GAC1D,OAAO,SAASW,kBAAkBC,SAAiB,EAAEC,OAAe;IAClE,MAAMC,OAAOjF,WAAW+E;IACxB,MAAMG,MAAM7E,OAAO2E;IACnB,IAAI,CAAEC,CAAAA,OAAO,CAAA,KAAM,CAAC5E,OAAOC,QAAQ,CAAC4E,QAAQA,OAAO,GAAG,OAAO;IAC7D,OAAO/E,KAAKC,KAAK,CAAC,AAAC6E,OAAOC,MAAO;AACnC;AAEA,6EAA6E,GAC7E,OAAO,SAASC,cAAchD,QAAgB,EAAEjB,WAAmB;IACjE,IAAI,CAACb,OAAO+E,SAAS,CAACjD,aAAaA,WAAW,GAAG,OAAO;IACxD,IAAIA,WAAWnC,WAAWkB,cAAc;QACtC,OAAO;IACT;IACA,OAAO;AACT;AAqFA,0EAA0E,GAC1E,OAAO,MAAMmE,wBAAwB,GAAE;AAEvC,4EAA4E,GAC5E,OAAO,MAAMC,6BAA6B,KAAK,KAAK,KAAI;AAExD,uEAAuE,GACvE,OAAO,MAAMC,wBAAwB,KAAK;AAE1C,MAAMC,gBAAgB;AAEtB,4EAA4E,GAC5E,OAAO,SAASC,gBAAgBxF,KAAc;IAC5C,MAAMyF,QAAQnB,OAAOtE,gBAAAA,QAAS,IAAI0F,IAAI,GAAGC,WAAW,GAAGhB,KAAK,CAAC,GAAG;IAChE,OAAOY,cAAcK,IAAI,CAACH,SAASA,QAAQ;AAC7C;AAEA,+EAA+E,GAC/E,OAAO,SAASI,gBAAgB7F,KAAc;IAC5C,MAAMoE,MAAME,OAAOtE,gBAAAA,QAAS,IAAI0F,IAAI;IACpC,MAAMI,SAAS1B,IAAI2B,OAAO,CAAC,WAAW;IACtC,IAAID,OAAOlB,MAAM,GAAG,KAAKkB,OAAOlB,MAAM,GAAG,IAAI,OAAO;IACpD,OAAO,GAAGR,IAAI4B,UAAU,CAAC,OAAO,MAAM,KAAKF,QAAQ;AACrD;AAEA;;;;CAIC,GACD,OAAO,SAASG,wBACd7B,GAAY,EACZ8B,KAAa;QAMW9D;IAJxB,MAAMA,QAASgC,OAAO,OAAOA,QAAQ,WAAWA,MAAM,CAAC;IACvD,MAAM+B,QAA0B;QAAC;QAAQ;QAAQ;QAAO;QAAW;QAAc;KAAS;IAC1F,MAAMC,OAAOD,MAAME,QAAQ,CAACjE,KAAK,CAAC,OAAO,IAAKA,KAAK,CAAC,OAAO,GAAsB;IACjF,MAAMkE,QAAyB;QAAEF;QAAMG,aAAaL;IAAM;IAC1D,MAAMM,WAAWlC,QAAOlC,kBAAAA,KAAK,CAAC,WAAW,YAAjBA,kBAAqB,IAAIuC,KAAK,CAAC,GAAG;IAC1D,IAAI6B,UAAUF,MAAME,QAAQ,GAAGA;IAC/B,MAAMC,OAAOrE,KAAK,CAAC,OAAO;IAC1B,IAAIqE,QAAQ,OAAOA,SAAS,YAAYL,SAAS,QAAQ;QACvD,MAAMM,QAAQ,AAAClG,CAAAA,MAAMC,OAAO,CAACgG,KAAKC,KAAK,IAAID,KAAKC,KAAK,GAAG,EAAE,AAAD,EACtD/B,KAAK,CAAC,CAACS,uBACPZ,GAAG,CAAC,CAACmC;;mBAAe;gBACnBC,MAAMtC,eAAOqC,wBAAAA,KAAMC,IAAI,mBAAI,IAAIjC,KAAK,CAAC,GAAG;eACpCgC,CAAAA,wBAAAA,KAAME,YAAY,IAClB;gBAAEA,cAAcvC,OAAOqC,KAAKE,YAAY,EAAElC,KAAK,CAAC,GAAG;YAAI,IACvD,CAAC;gBACLmC,UAAU5G,KAAK6B,GAAG,CAAC,GAAG7B,KAAKuC,GAAG,CAAC,KAAKvC,KAAKC,KAAK,CAACC,OAAOuG,wBAAAA,KAAMG,QAAQ,KAAK;gBACzE7F,aAAalB,WAAW4G,wBAAAA,KAAM1F,WAAW;;;QAE7CqF,MAAMG,IAAI,GAAG;YACXC;YACAK,YAAYhH,WAAW0G,KAAKM,UAAU;YACtCC,eAAejH,WAAW0G,KAAKO,aAAa;YAC5CC,UAAUlH,WAAW0G,KAAKQ,QAAQ;YAClClG,YAAYhB,WAAW0G,KAAK1F,UAAU;WAClC0F,KAAKS,SAAS,IAAI,OAAO;YAAEA,WAAWnH,WAAW0G,KAAKS,SAAS;QAAE,IAAI,CAAC,GACtET,KAAKU,QAAQ,IAAI,OAAO;YAAEA,UAAUpH,WAAW0G,KAAKU,QAAQ;QAAE,IAAI,CAAC,GACnEV,KAAKvE,QAAQ,IAAI,OAAO;YAAEA,UAAUnC,WAAW0G,KAAKvE,QAAQ;QAAE,IAAI,CAAC;IAE3E;IACA,IAAIkE,SAAS,OAAO;YACNhE;QAAZ,MAAMsB,OAAMtB,aAAAA,KAAK,CAAC,MAAM,YAAZA,aAAgB,CAAC;QAC7BkE,MAAM5C,GAAG,GAAG;YACVoB,WAAW/E,WAAW2D,IAAIoB,SAAS;YACnCsC,aAAajD,kBACX3D,MAAMC,OAAO,CAACiD,IAAI0D,WAAW,IAAI1D,IAAI0D,WAAW,CAAC3D,IAAI,CAAC,OAAOC,IAAI0D,WAAW;YAE9EC,aAAa3D,IAAI2D,WAAW,KAAK;QACnC;IACF;IACA,IAAIjB,SAAS,WAAW;YACNhE;QAAhB,MAAMkF,WAAUlF,iBAAAA,KAAK,CAAC,UAAU,YAAhBA,iBAAoB,CAAC;QACrC,MAAMmF,UAA+B;YAAC;YAAS;YAAO;YAAS;SAAO;QACtE,MAAMC,WAAW,AAAChH,CAAAA,MAAMC,OAAO,CAAC6G,QAAQE,QAAQ,IAAIF,QAAQE,QAAQ,GAAG,EAAE,AAAD,EACrE7G,MAAM,CAAC,CAACc,UACP8F,QAAQlB,QAAQ,CAAC5E;QAErB6E,MAAMgB,OAAO,GAAG;YACdE,UAAUA,SAAS5C,MAAM,GAAG;mBAAI,IAAIjF,IAAuB6H;aAAU,GAAG;gBAAC;gBAAS;aAAO;YACzFC,gBAAgBH,QAAQG,cAAc,KAAK;QAC7C;IACF;IACA,IAAIrB,SAAS,cAAc;;YAGdhE;QAFXkE,MAAMoB,UAAU,GAAG;YACjBC,SACErD,gBAAOlC,oBAAAA,KAAK,CAAC,aAAa,qBAAnBA,kBAAqBuF,OAAO,mBAAI,IAAIhD,KAAK,CAAC,GAAG,QACpD;QACJ;IACF;IACA,OAAO2B;AACT;AAEA;;;;CAIC,GACD,OAAO,SAASsB,2BACdtB,KAAyC,EACzClC,GAAY,EACZ8B,KAAa;QAIF9D;IAFX,IAAI,CAACkE,SAAS,CAACA,MAAME,QAAQ,EAAE,OAAO;IACtC,MAAMpE,QAASgC,OAAO,OAAOA,QAAQ,WAAWA,MAAM,CAAC;IACvD,IAAIE,QAAOlC,kBAAAA,KAAK,CAAC,WAAW,YAAjBA,kBAAqB,QAAQkE,MAAME,QAAQ,EAAE,OAAO;IAC/D,MAAMqB,WAA+B;QAAErB,UAAUF,MAAME,QAAQ;QAAEpF,MAAM8E;IAAM;IAC7E,IAAII,MAAMF,IAAI,KAAK,SAASE,MAAM5C,GAAG,EAAE;QACrC,MAAMoE,SAAS1F,KAAK,CAAC,YAAY;QACjC,IAAI0F,WAAW,QAAQ;YACrBD,SAASE,SAAS,GAAG;YACrBF,SAAS3F,QAAQ,GAAG;QACtB,OAAO,IAAI4F,WAAW,WAAW;YAC/B,MAAM/C,UAAU7E,KAAKC,KAAK,CAACC,OAAOgC,KAAK,CAAC,aAAa;YACrD,IAAI,CAACkE,MAAM5C,GAAG,CAAC0D,WAAW,CAACf,QAAQ,CAACtB,UAAU,OAAO;YACrD8C,SAASE,SAAS,GAAG;YACrBF,SAASG,UAAU,GAAGjD;YACtB8C,SAAS3F,QAAQ,GAAG2C,kBAAkByB,MAAM5C,GAAG,CAACoB,SAAS,EAAEC;QAC7D,OAAO,IAAI+C,WAAW,YAAYxB,MAAM5C,GAAG,CAAC2D,WAAW,EAAE;YACvD,MAAMnF,WAAWhC,KAAKC,KAAK,CAACC,OAAOgC,KAAK,CAAC,WAAW;YACpD,IAAI8C,cAAchD,UAAUoE,MAAM5C,GAAG,CAACoB,SAAS,GAAG,OAAO;YACzD+C,SAASE,SAAS,GAAG;YACrBF,SAAS3F,QAAQ,GAAGA;QACtB,OAAO;YACL,OAAO;QACT;QACA,OAAO2F;IACT;IACA,IAAIvB,MAAMF,IAAI,KAAK,aAAaE,MAAMgB,OAAO,EAAE;QAC7C,MAAM7F,UAAUW,KAAK,CAAC,iBAAiB;QACvC,IAAI,CAACkE,MAAMgB,OAAO,CAACE,QAAQ,CAACnB,QAAQ,CAAC5E,UAAU,OAAO;QACtDoG,SAASI,cAAc,GAAGxG;QAC1B,IAAIA,YAAY,SAAS;YACvB,MAAMgE,QAAQD,gBAAgBpD,KAAK,CAAC,QAAQ;YAC5C,IAAI,CAACqD,OAAO,OAAO;YACnBoC,SAASpC,KAAK,GAAGA;YACjB,IAAIa,MAAMgB,OAAO,CAACG,cAAc,IAAIrF,KAAK,CAAC,iBAAiB,KAAK,MAAM;gBACpEyF,SAASK,cAAc,GAAG;YAC5B;QACF;QACA,IAAIzG,YAAY,OAAO;YACrB,MAAM0G,QAAQtC,gBAAgBzD,KAAK,CAAC,QAAQ;YAC5C,IAAI,CAAC+F,OAAO,OAAO;YACnBN,SAASM,KAAK,GAAGA;QACnB;QACA,OAAON;IACT;IACA,OAAO;AACT;AAEA;;;;;CAKC,GACD,OAAO,SAASO,sBACd9B,KAAyC,EACzCJ,KAAa;IAEb,IAAI,CAACI,OAAO,OAAO;QAAEF,MAAM;QAAQG,aAAaL;QAAOmC,UAAU;IAAM;IACvE,MAAM,EAAER,QAAQ,EAAW,GAAGvB,OAATgC,wCAAShC;;;IAC9B,IAAIgC,KAAKlC,IAAI,KAAK,YAAYF,QAAQoC,KAAK/B,WAAW,GAAGjB,uBAAuB;QAC9E,OAAO;YAAEc,MAAM;YAAQG,aAAa+B,KAAK/B,WAAW;YAAE8B,UAAU;QAAM;IACxE;IACA,OAAO,aACFC;QACHD,UAAUxH,QAAQgH,YAAYA,SAASrB,QAAQ,KAAK8B,KAAK9B,QAAQ;;AAErE"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/commerce-pos.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\n/*==========================================\n * THE POS TENDER LEDGER (AGL-3607).\n *\n * A register sale used to be settled by exactly one tender and recorded none:\n * whether a POS order was paid in cash, by the QR link or to a room was\n * inferred afterwards from which other fields happened to be set. A sale now\n * carries `payments[]`, one entry per tender, and takes payments until the\n * balance due is zero. Only then is it `paid`.\n *\n * Every figure here is integer cents. `amountCents` is what a payment put\n * toward the SALE; a tip rides beside it in `tipCents` and never counts toward\n * the balance, the platform's take or the store's revenue. Tips belong to the\n * merchant and their staff.\n *\n * Pure and framework-free, so the register, the customer display and the\n * server read one answer.\n *=========================================*/\n\n/** How one payment toward a register sale was taken. */\nexport type OrderPaymentMethod =\n | 'cash'\n /** A Stripe Terminal smart reader (tap, insert or swipe). */\n | 'card_present'\n /** A card typed into the console by staff (card not present). */\n | 'card_keyed'\n /** The Stripe payment page the customer opens from a QR code. */\n | 'card_link'\n | 'gift_card'\n /** Charged to a checked-in reservation's folio. */\n | 'folio'\n /**\n * Store credit another plugin keeps — rewards, referral credit — taken\n * through core's checkout-credits seam (AGL-3640). Which plugin and which\n * account ride on the payment as `creditProviderId` and `creditReference`.\n */\n | 'credit'\n\nexport type OrderPaymentStatus =\n /** Started and not finished: reserves its amount against the balance. */\n | 'pending'\n | 'succeeded'\n /** Declined or errored; may be retried, reserves nothing. */\n | 'failed'\n | 'canceled'\n /** Taken and then handed back when the open sale was voided. */\n | 'reversed'\n\nexport const ORDER_PAYMENT_METHODS: readonly OrderPaymentMethod[] = [\n 'cash',\n 'card_present',\n 'card_keyed',\n 'card_link',\n 'gift_card',\n 'folio',\n 'credit',\n]\n\n/** One payment toward a register sale, on `HostOrder.payments`. */\nexport interface OrderPayment {\n /** Stable id, derived from the register's attempt key so a retry finds it. */\n id: string\n method: OrderPaymentMethod\n /** Cents put toward the sale. Never includes the tip. */\n amountCents: number\n /** Gratuity on top of `amountCents`, the merchant's and not revenue. */\n tipCents?: number\n status: OrderPaymentStatus\n /** When the payment was started. */\n atMs: number\n /** When it reached `succeeded`. */\n settledAtMs?: number\n /** Stripe PaymentIntent for `card_present` / `card_keyed` / a paid link. */\n paymentIntentId?: string\n /** Stripe Checkout Session for `card_link`. */\n checkoutSessionId?: string\n /** The payment page a `card_link` payment shows as a QR. */\n checkoutUrl?: string\n /** The Terminal reader a `card_present` payment runs on. */\n readerId?: string\n /** The gift card's code (its document id) for `gift_card`. */\n giftCardId?: string\n /** For `credit`: the provider that paid it (`{pluginId}.{key}`, AGL-3640). */\n creditProviderId?: string\n /** For `credit`: the provider's own handle on the account it came from. */\n creditReference?: string\n /** For `credit`: what the provider calls it, for the register and receipt. */\n creditLabel?: string\n cardBrand?: string\n last4?: string\n /** Cash handed over for a `cash` payment, and the change given back. */\n cashTenderedCents?: number\n changeCents?: number\n /** The stay a `folio` payment was charged to. */\n reservationId?: string\n /** The folio entry's `atMs`, so a void can remove exactly that entry. */\n folioAtMs?: number\n /**\n * The platform's take attributed to this payment (excludes the tip). For a\n * card payment it is netted from the payout with Stripe's processing cost\n * added, and recorded in `feeCents`; otherwise it accrues to the invoice.\n */\n takeFeeCents?: number\n /** The `application_fee_amount` a card payment carried, or 0. */\n feeCents?: number\n /** Why a `failed` payment failed, in words the cashier can read out. */\n failureMessage?: string\n /** Whether Stripe moved real money; absent for tenders Stripe never saw. */\n livemode?: boolean\n /** The console user who took the payment. */\n cashierId?: string\n}\n\n/** The tenders whose money moves through Stripe. */\nexport const POS_CARD_METHODS: ReadonlySet<OrderPaymentMethod> = new Set([\n 'card_present',\n 'card_keyed',\n 'card_link',\n])\n\nexport function isCardPaymentMethod(method: OrderPaymentMethod): boolean {\n return POS_CARD_METHODS.has(method)\n}\n\n/*==========================================\n * WHICH TENDERS CARRY THE PLATFORM FEE (AGL-4036).\n *\n * The take used to follow the sale whatever paid it (AGL-2111): a cash sale\n * owed the same percentage as a card sale and had it added to the monthly\n * invoice. In-person rivals charge nothing on cash, and an event register that\n * bills its operator for the cash in their own apron is not one they will\n * choose, so three tenders now carry none:\n *\n * - cash: no payment service touches it, and the merchant keeps all of it;\n * - gift card: the fee was taken when the card was SOLD, so charging it again\n * when the card is spent would charge the same money twice;\n * - store credit (`credit`): rewards or referral value the merchant gave\n * away, so no money changes hands and an invoiced fee on it would only\n * penalize running a rewards program.\n *\n * Everything else still carries it. Card payments net it from their payout,\n * as always. A room charge (`folio`) is new revenue booked to a stay, so its\n * share goes on the invoice.\n *\n * One predicate, so the register, the completion, the offline sync and the\n * docs cannot disagree about which tender is which.\n *=========================================*/\n\n/** The tenders that carry no platform fee (AGL-4036). */\nexport const POS_FEE_FREE_METHODS: ReadonlySet<OrderPaymentMethod> = new Set([\n 'cash',\n 'gift_card',\n 'credit',\n])\n\n/** Whether a payment by `method` carries its share of the platform's take. */\nexport function paymentCarriesPlatformFee(method: OrderPaymentMethod): boolean {\n return !POS_FEE_FREE_METHODS.has(method)\n}\n\n/**\n * The take a sale owes when ONE tender pays all of it: the whole take, or\n * nothing for a fee-free tender.\n */\nexport function posTenderTakeCents(\n method: OrderPaymentMethod,\n takeFeeCents: number,\n): number {\n return paymentCarriesPlatformFee(method) ? wholeCents(takeFeeCents) : 0\n}\n\n/** Whole non-negative cents from an untrusted value. */\nfunction wholeCents(value: unknown): number {\n const number = Math.round(Number(value))\n return Number.isFinite(number) && number > 0 ? number : 0\n}\n\n/**\n * The order's payments, read through one door.\n *\n * Orders written before the ledger existed carry no `payments`, so one is\n * INFERRED from the fields the single-tender register wrote: a folio sale has\n * `reservationId`, a QR sale has `checkoutSessionId`, and anything else on the\n * POS channel was cash. A pending legacy sale is the QR link still waiting.\n * Online orders paid through Stripe Checkout read as one `card_link` payment.\n * Nothing is written back: this is a reader, never a migration.\n */\nexport function orderPayments(order: {\n payments?: OrderPayment[] | null\n channel?: string\n status?: string\n totals?: { totalCents?: number; tipCents?: number } | null\n amountCents?: number\n reservationId?: string\n checkoutSessionId?: string\n paymentIntentId?: string\n createdAtMs?: number\n}): OrderPayment[] {\n if (Array.isArray(order?.payments)) {\n return order.payments.filter(\n (payment): payment is OrderPayment =>\n Boolean(payment) && typeof payment === 'object' && Boolean(payment.id),\n )\n }\n const totalCents = wholeCents(order?.totals?.totalCents ?? order?.amountCents)\n if (!(totalCents > 0)) return []\n const settled = order?.status !== 'pending' && order?.status !== 'cancelled'\n const atMs = Number(order?.createdAtMs) || 0\n const method: OrderPaymentMethod = order?.reservationId\n ? 'folio'\n : order?.checkoutSessionId || order?.paymentIntentId || order?.channel !== 'pos'\n ? 'card_link'\n : 'cash'\n return [\n {\n id: 'legacy',\n method,\n amountCents: totalCents,\n status: settled ? 'succeeded' : order?.status === 'cancelled' ? 'canceled' : 'pending',\n atMs,\n ...(order?.paymentIntentId ? { paymentIntentId: order.paymentIntentId } : {}),\n ...(order?.checkoutSessionId ? { checkoutSessionId: order.checkoutSessionId } : {}),\n ...(order?.reservationId ? { reservationId: order.reservationId } : {}),\n },\n ]\n}\n\n/** Cents put toward the sale by payments that succeeded. */\nexport function posSettledCents(payments: readonly OrderPayment[]): number {\n return payments\n .filter((payment) => payment.status === 'succeeded')\n .reduce((sum, payment) => sum + wholeCents(payment.amountCents), 0)\n}\n\n/**\n * Cents spoken for: succeeded payments plus the ones still in flight. A\n * reader waiting for a tap has claimed its share of the balance, so a second\n * tender cannot be started for the same money.\n */\nexport function posReservedCents(payments: readonly OrderPayment[]): number {\n return payments\n .filter((payment) => payment.status === 'succeeded' || payment.status === 'pending')\n .reduce((sum, payment) => sum + wholeCents(payment.amountCents), 0)\n}\n\n/** What the customer still owes, by settled payments. */\nexport function posBalanceDueCents(\n totalCents: number,\n payments: readonly OrderPayment[],\n): number {\n return Math.max(0, wholeCents(totalCents) - posSettledCents(payments))\n}\n\n/** What a NEW payment may still take: the balance less what is in flight. */\nexport function posTenderableCents(\n totalCents: number,\n payments: readonly OrderPayment[],\n): number {\n return Math.max(0, wholeCents(totalCents) - posReservedCents(payments))\n}\n\n/** Tips taken across the sale's settled payments. */\nexport function posTipCents(payments: readonly OrderPayment[]): number {\n return payments\n .filter((payment) => payment.status === 'succeeded')\n .reduce((sum, payment) => sum + wholeCents(payment.tipCents), 0)\n}\n\n/**\n * The share of the sale's take a payment of `amountCents` carries.\n *\n * Proportional to the part of the SALE it pays, never to the tip. Card\n * payments carry their share in `application_fee_amount`; whatever the card\n * shares leave is accrued to the invoice at completion\n * ({@link posInvoiceTakeCents}), so the take is exact however the sale was\n * split and however each share rounded.\n */\nexport function posTakeShareCents(input: {\n takeFeeCents: number\n totalCents: number\n amountCents: number\n}): number {\n const take = wholeCents(input.takeFeeCents)\n const total = wholeCents(input.totalCents)\n const amount = Math.min(wholeCents(input.amountCents), total)\n if (!(take > 0) || !(total > 0) || !(amount > 0)) return 0\n return Math.min(take, Math.round((take * amount) / total))\n}\n\n/**\n * {@link posTakeShareCents} for a payment by `method`: its share of the take,\n * or 0 for a tender that carries no platform fee (AGL-4036).\n */\nexport function posPaymentTakeShareCents(input: {\n method: OrderPaymentMethod\n takeFeeCents: number\n totalCents: number\n amountCents: number\n}): number {\n if (!paymentCarriesPlatformFee(input.method)) return 0\n return posTakeShareCents(input)\n}\n\n/**\n * The part of the sale's take its tenders owe (AGL-4036): the whole take when\n * every settled payment carries the fee, and otherwise the share for the part\n * of the sale the fee-carrying payments paid. Cash and gift-card parts owe\n * nothing, so a sale paid all in cash owes 0.\n *\n * Worked from the payments' METHODS, not from the `takeFeeCents` each one\n * recorded, so a sale that was opened before this rule and finished after it\n * is charged by the rule it finished under.\n */\nexport function posOwedTakeCents(input: {\n takeFeeCents: number\n totalCents: number\n payments: readonly OrderPayment[]\n}): number {\n const take = wholeCents(input.takeFeeCents)\n const settled = input.payments.filter((payment) => payment.status === 'succeeded')\n if (settled.every((payment) => paymentCarriesPlatformFee(payment.method))) return take\n const carryingCents = settled\n .filter((payment) => paymentCarriesPlatformFee(payment.method))\n .reduce((sum, payment) => sum + wholeCents(payment.amountCents), 0)\n return posTakeShareCents({\n takeFeeCents: take,\n totalCents: input.totalCents,\n amountCents: carryingCents,\n })\n}\n\n/**\n * What of the sale's take was NOT netted from a card payout, and therefore\n * goes on the merchant's invoice: the take the tenders owe\n * ({@link posOwedTakeCents}) less every succeeded card payment's share. Never\n * negative, and 0 for a sale paid only by card, cash, gift card and store credit.\n */\nexport function posInvoiceTakeCents(input: {\n takeFeeCents: number\n totalCents: number\n payments: readonly OrderPayment[]\n}): number {\n const netted = input.payments\n .filter(\n (payment) =>\n payment.status === 'succeeded' && isCardPaymentMethod(payment.method),\n )\n .reduce((sum, payment) => sum + wholeCents(payment.takeFeeCents), 0)\n return Math.max(0, posOwedTakeCents(input) - netted)\n}\n\n/**\n * How the sale's fee reaches the platform, from the payments that carried it.\n * A cash, gift-card or store-credit part carries none (AGL-4036), so a card + cash sale is\n * `payout`, not `split`.\n */\nexport function posFeeCollection(\n payments: readonly OrderPayment[],\n): 'payout' | 'invoice' | 'split' {\n const settled = payments.filter(\n (payment) =>\n payment.status === 'succeeded' && paymentCarriesPlatformFee(payment.method),\n )\n const card = settled.some((payment) => isCardPaymentMethod(payment.method))\n const other = settled.some((payment) => !isCardPaymentMethod(payment.method))\n if (card && other) return 'split'\n return card ? 'payout' : 'invoice'\n}\n\n/** A one-line description of a payment for the register, receipt and timeline. */\nexport function describeOrderPayment(payment: OrderPayment): string {\n const dollars = (cents: number) => `$${(cents / 100).toFixed(2)}`\n const label =\n (payment.method === 'credit' && payment.creditLabel) || ORDER_PAYMENT_METHOD_LABELS[payment.method] || 'Payment'\n const card =\n payment.cardBrand || payment.last4\n ? ` (${[payment.cardBrand, payment.last4 ? `•••• ${payment.last4}` : '']\n .filter(Boolean)\n .join(' ')})`\n : ''\n const tip = wholeCents(payment.tipCents)\n ? ` + ${dollars(wholeCents(payment.tipCents))} tip`\n : ''\n return `${label}${card} ${dollars(wholeCents(payment.amountCents))}${tip}`\n}\n\nexport const ORDER_PAYMENT_METHOD_LABELS: Record<OrderPaymentMethod, string> = {\n cash: 'Cash',\n card_present: 'Card reader',\n card_keyed: 'Typed card',\n card_link: 'Card (QR)',\n gift_card: 'Gift card',\n folio: 'Charged to room',\n credit: 'Store credit',\n}\n\n/*==========================================\n * TIPS (AGL-3607).\n *=========================================*/\n\n/** The presets a merchant starts with. */\nexport const POS_TIP_PERCENTAGES_DEFAULT: readonly number[] = [15, 18, 20, 25]\n\n/**\n * The merchant's tip presets from the plugin setting, a comma list such as\n * `15, 18, 20, 25`. Whole percentages from 1 to 100, at most four, in the order\n * given; anything unreadable falls back to the defaults rather than showing\n * the customer an empty choice.\n */\nexport function posTipPercentages(raw: unknown): number[] {\n const parsed = String(raw ?? '')\n .split(/[,\\s]+/)\n .map((part) => Math.round(Number(part)))\n .filter((value) => Number.isFinite(value) && value >= 1 && value <= 100)\n const unique = [...new Set(parsed)].slice(0, 4)\n return unique.length ? unique : [...POS_TIP_PERCENTAGES_DEFAULT]\n}\n\n/** A percentage tip on `baseCents`, rounded to the cent. */\nexport function posTipFromPercent(baseCents: number, percent: number): number {\n const base = wholeCents(baseCents)\n const pct = Number(percent)\n if (!(base > 0) || !Number.isFinite(pct) || pct <= 0) return 0\n return Math.round((base * pct) / 100)\n}\n\n/** The largest tip the register accepts on one payment: the payment itself. */\nexport function posTipProblem(tipCents: number, amountCents: number): string | null {\n if (!Number.isInteger(tipCents) || tipCents < 0) return 'Enter a tip of $0 or more.'\n if (tipCents > wholeCents(amountCents)) {\n return 'A tip can be at most the amount being paid. Check the amount and try again.'\n }\n return null\n}\n\n/*==========================================\n * THE CUSTOMER DISPLAY (AGL-3608).\n *\n * A paired screen facing the customer reads one document per register,\n * `hosts/{hostId}/registers/{registerId}/display/state`, through the\n * `commerce/pos-display` route and its display token. The register writes\n * the prompt; the customer's answer comes back in `response`.\n *=========================================*/\n\nexport type PosDisplayMode =\n | 'idle'\n /** The live basket, mirrored as the cashier builds it. */\n | 'cart'\n /** The customer picks a tip. */\n | 'tip'\n /** The customer picks how they want their receipt. */\n | 'receipt'\n /** A card payment is waiting on the reader. */\n | 'processing'\n | 'thanks'\n\nexport type PosReceiptChannel = 'email' | 'sms' | 'print' | 'none'\n\nexport interface PosDisplayLine {\n name: string\n variantLabel?: string\n quantity: number\n amountCents: number\n}\n\nexport interface PosDisplayCart {\n lines: PosDisplayLine[]\n itemsCents: number\n discountCents: number\n taxCents: number\n totalCents: number\n /** Taken so far, and what is still due, once payments start. */\n paidCents?: number\n dueCents?: number\n tipCents?: number\n}\n\n/** The customer's answer to the current prompt. */\nexport interface PosDisplayResponse {\n /** The prompt this answers; a stale answer is refused. */\n promptId: string\n tipCents?: number\n tipPercent?: number\n /** `none` when the customer chose no tip. */\n tipChoice?: 'percent' | 'custom' | 'none'\n receiptChannel?: PosReceiptChannel\n email?: string\n phone?: string\n marketingOptIn?: boolean\n atMs: number\n}\n\nexport interface PosDisplayState {\n mode: PosDisplayMode\n updatedAtMs: number\n /**\n * The ISO currency every amount on the screen is in (`usd`). Stamped by\n * the server from the store's own currency, never taken from the register.\n */\n currency?: string\n /** Changes every time the register asks something new. */\n promptId?: string\n cart?: PosDisplayCart\n tip?: {\n /** What a percentage is taken of: the amount being paid now. */\n baseCents: number\n percentages: number[]\n allowCustom: boolean\n }\n receipt?: {\n channels: PosReceiptChannel[]\n /** Whether to offer the store's marketing opt-in under the email field. */\n offerMarketing: boolean\n }\n processing?: { message: string }\n response?: PosDisplayResponse\n}\n\n/** Most lines a display state carries; a longer basket shows its newest. */\nexport const POS_DISPLAY_MAX_LINES = 60\n\n/** A pairing code lives this long before the register must show a new one. */\nexport const POS_DISPLAY_PAIRING_TTL_MS = 10 * 60 * 1000\n\n/** A thank-you screen returns to idle on the display after this long. */\nexport const POS_DISPLAY_THANKS_MS = 8_000\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n/** An email address as the display may store it, or '' when it is not one. */\nexport function posDisplayEmail(value: unknown): string {\n const email = String(value ?? '').trim().toLowerCase().slice(0, 254)\n return EMAIL_PATTERN.test(email) ? email : ''\n}\n\n/** A phone number as digits with an optional leading +, or '' when too short. */\nexport function posDisplayPhone(value: unknown): string {\n const raw = String(value ?? '').trim()\n const digits = raw.replace(/[^0-9]/g, '')\n if (digits.length < 7 || digits.length > 15) return ''\n return `${raw.startsWith('+') ? '+' : ''}${digits}`\n}\n\n/**\n * The display state as the REGISTER may send it: clamped, typed and stripped\n * of anything the customer screen has no business holding. The register is\n * staff and trusted for intent, not for shape.\n */\nexport function sanitizePosDisplayState(\n raw: unknown,\n nowMs: number,\n): PosDisplayState {\n const input = (raw && typeof raw === 'object' ? raw : {}) as Record<string, any>\n const modes: PosDisplayMode[] = ['idle', 'cart', 'tip', 'receipt', 'processing', 'thanks']\n const mode = modes.includes(input['mode']) ? (input['mode'] as PosDisplayMode) : 'idle'\n const state: PosDisplayState = { mode, updatedAtMs: nowMs }\n const promptId = String(input['promptId'] ?? '').slice(0, 64)\n if (promptId) state.promptId = promptId\n const cart = input['cart']\n if (cart && typeof cart === 'object' && mode !== 'idle') {\n const lines = (Array.isArray(cart.lines) ? cart.lines : [])\n .slice(-POS_DISPLAY_MAX_LINES)\n .map((line: any) => ({\n name: String(line?.name ?? '').slice(0, 120),\n ...(line?.variantLabel\n ? { variantLabel: String(line.variantLabel).slice(0, 80) }\n : {}),\n quantity: Math.max(1, Math.min(999, Math.round(Number(line?.quantity) || 1))),\n amountCents: wholeCents(line?.amountCents),\n }))\n state.cart = {\n lines,\n itemsCents: wholeCents(cart.itemsCents),\n discountCents: wholeCents(cart.discountCents),\n taxCents: wholeCents(cart.taxCents),\n totalCents: wholeCents(cart.totalCents),\n ...(cart.paidCents != null ? { paidCents: wholeCents(cart.paidCents) } : {}),\n ...(cart.dueCents != null ? { dueCents: wholeCents(cart.dueCents) } : {}),\n ...(cart.tipCents != null ? { tipCents: wholeCents(cart.tipCents) } : {}),\n }\n }\n if (mode === 'tip') {\n const tip = input['tip'] ?? {}\n state.tip = {\n baseCents: wholeCents(tip.baseCents),\n percentages: posTipPercentages(\n Array.isArray(tip.percentages) ? tip.percentages.join(',') : tip.percentages,\n ),\n allowCustom: tip.allowCustom !== false,\n }\n }\n if (mode === 'receipt') {\n const receipt = input['receipt'] ?? {}\n const allowed: PosReceiptChannel[] = ['email', 'sms', 'print', 'none']\n const channels = (Array.isArray(receipt.channels) ? receipt.channels : [])\n .filter((channel: unknown): channel is PosReceiptChannel =>\n allowed.includes(channel as PosReceiptChannel),\n )\n state.receipt = {\n channels: channels.length ? [...new Set<PosReceiptChannel>(channels)] : ['print', 'none'],\n offerMarketing: receipt.offerMarketing === true,\n }\n }\n if (mode === 'processing') {\n state.processing = {\n message:\n String(input['processing']?.message ?? '').slice(0, 160) ||\n 'Tap, insert or swipe your card on the reader.',\n }\n }\n return state\n}\n\n/**\n * The customer's answer as the DISPLAY may send it, checked against the\n * prompt it claims to answer. Returns null when it answers nothing the\n * register asked: a stale screen, a replay, or a mode that takes no answer.\n */\nexport function sanitizePosDisplayResponse(\n state: PosDisplayState | null | undefined,\n raw: unknown,\n nowMs: number,\n): PosDisplayResponse | null {\n if (!state || !state.promptId) return null\n const input = (raw && typeof raw === 'object' ? raw : {}) as Record<string, any>\n if (String(input['promptId'] ?? '') !== state.promptId) return null\n const response: PosDisplayResponse = { promptId: state.promptId, atMs: nowMs }\n if (state.mode === 'tip' && state.tip) {\n const choice = input['tipChoice']\n if (choice === 'none') {\n response.tipChoice = 'none'\n response.tipCents = 0\n } else if (choice === 'percent') {\n const percent = Math.round(Number(input['tipPercent']))\n if (!state.tip.percentages.includes(percent)) return null\n response.tipChoice = 'percent'\n response.tipPercent = percent\n response.tipCents = posTipFromPercent(state.tip.baseCents, percent)\n } else if (choice === 'custom' && state.tip.allowCustom) {\n const tipCents = Math.round(Number(input['tipCents']))\n if (posTipProblem(tipCents, state.tip.baseCents)) return null\n response.tipChoice = 'custom'\n response.tipCents = tipCents\n } else {\n return null\n }\n return response\n }\n if (state.mode === 'receipt' && state.receipt) {\n const channel = input['receiptChannel'] as PosReceiptChannel\n if (!state.receipt.channels.includes(channel)) return null\n response.receiptChannel = channel\n if (channel === 'email') {\n const email = posDisplayEmail(input['email'])\n if (!email) return null\n response.email = email\n if (state.receipt.offerMarketing && input['marketingOptIn'] === true) {\n response.marketingOptIn = true\n }\n }\n if (channel === 'sms') {\n const phone = posDisplayPhone(input['phone'])\n if (!phone) return null\n response.phone = phone\n }\n return response\n }\n return null\n}\n\n/**\n * The state as the DISPLAY reads it. The customer's own typed address is\n * never echoed back to the screen: once answered, the prompt is done and the\n * screen shows what comes next, so nothing a later customer could read off\n * an unattended tablet is ever sent to it.\n */\nexport function posDisplayPublicState(\n state: PosDisplayState | null | undefined,\n nowMs: number,\n): Omit<PosDisplayState, 'response'> & { answered: boolean } {\n if (!state) return { mode: 'idle', updatedAtMs: nowMs, answered: false }\n const { response, ...rest } = state\n if (rest.mode === 'thanks' && nowMs - rest.updatedAtMs > POS_DISPLAY_THANKS_MS) {\n return { mode: 'idle', updatedAtMs: rest.updatedAtMs, answered: false }\n }\n return {\n ...rest,\n answered: Boolean(response && response.promptId === rest.promptId),\n }\n}\n"],"names":["ORDER_PAYMENT_METHODS","POS_CARD_METHODS","Set","isCardPaymentMethod","method","has","POS_FEE_FREE_METHODS","paymentCarriesPlatformFee","posTenderTakeCents","takeFeeCents","wholeCents","value","number","Math","round","Number","isFinite","orderPayments","order","Array","isArray","payments","filter","payment","Boolean","id","totalCents","totals","amountCents","settled","status","atMs","createdAtMs","reservationId","checkoutSessionId","paymentIntentId","channel","posSettledCents","reduce","sum","posReservedCents","posBalanceDueCents","max","posTenderableCents","posTipCents","tipCents","posTakeShareCents","input","take","total","amount","min","posPaymentTakeShareCents","posOwedTakeCents","every","carryingCents","posInvoiceTakeCents","netted","posFeeCollection","card","some","other","describeOrderPayment","dollars","cents","toFixed","label","creditLabel","ORDER_PAYMENT_METHOD_LABELS","cardBrand","last4","join","tip","cash","card_present","card_keyed","card_link","gift_card","folio","credit","POS_TIP_PERCENTAGES_DEFAULT","posTipPercentages","raw","parsed","String","split","map","part","unique","slice","length","posTipFromPercent","baseCents","percent","base","pct","posTipProblem","isInteger","POS_DISPLAY_MAX_LINES","POS_DISPLAY_PAIRING_TTL_MS","POS_DISPLAY_THANKS_MS","EMAIL_PATTERN","posDisplayEmail","email","trim","toLowerCase","test","posDisplayPhone","digits","replace","startsWith","sanitizePosDisplayState","nowMs","modes","mode","includes","state","updatedAtMs","promptId","cart","lines","line","name","variantLabel","quantity","itemsCents","discountCents","taxCents","paidCents","dueCents","percentages","allowCustom","receipt","allowed","channels","offerMarketing","processing","message","sanitizePosDisplayResponse","response","choice","tipChoice","tipPercent","receiptChannel","marketingOptIn","phone","posDisplayPublicState","answered","rest"],"mappings":"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"}
|
|
@@ -38,8 +38,20 @@ export interface ResourceSeason {
|
|
|
38
38
|
export interface HostResource {
|
|
39
39
|
name: string;
|
|
40
40
|
description?: string;
|
|
41
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Sleeps N; display only. Never availability: how many guests one unit
|
|
43
|
+
* takes says nothing about how many units there are (see `units`).
|
|
44
|
+
*/
|
|
42
45
|
capacity?: number;
|
|
46
|
+
/**
|
|
47
|
+
* How many identical units this one resource holds (AGL-4044): 12 RV
|
|
48
|
+
* hookups, 30 tent sites, 4 matching cabins. Each night can take this many
|
|
49
|
+
* stays at once. Absent means 1, which is every resource written before the
|
|
50
|
+
* field existed, and one unit behaves exactly as a resource always has.
|
|
51
|
+
* An integer from 1 to {@link MAX_RESOURCE_UNITS}; read it through
|
|
52
|
+
* {@link resourceUnits}, never raw.
|
|
53
|
+
*/
|
|
54
|
+
units?: number;
|
|
43
55
|
photoUrls?: string[];
|
|
44
56
|
amenities?: string[];
|
|
45
57
|
nightlyRateUsd: number;
|
|
@@ -159,17 +171,71 @@ export declare const PENDING_RESERVATION_HOLD_MS: number;
|
|
|
159
171
|
export declare function reservationHoldsDates(reservation: Pick<HostReservation, 'status'> & {
|
|
160
172
|
createdAtMs?: number;
|
|
161
173
|
}, nowMs?: number): boolean;
|
|
174
|
+
/** The most identical units one resource can hold (AGL-4044). */
|
|
175
|
+
export declare const MAX_RESOURCE_UNITS = 500;
|
|
176
|
+
/**
|
|
177
|
+
* How many units a resource holds: the stored `units` as a whole number from 1
|
|
178
|
+
* to {@link MAX_RESOURCE_UNITS}, and 1 for anything else (absent, zero,
|
|
179
|
+
* negative, not a number). Closing a resource is what `blocks` are for, so no
|
|
180
|
+
* stored value can make a resource hold no units at all.
|
|
181
|
+
*/
|
|
182
|
+
export declare function resourceUnits(resource: Pick<HostResource, 'units'> | null | undefined): number;
|
|
183
|
+
/** A reservation as an availability check reads it. */
|
|
184
|
+
export type HeldStay = Pick<HostReservation, 'checkInDayMs' | 'checkOutDayMs' | 'status'> & {
|
|
185
|
+
createdAtMs?: number;
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* How many units are taken on each night from `fromDayMs` up to (not
|
|
189
|
+
* including) `toDayMs`, one entry per night, counting only the stays that
|
|
190
|
+
* still hold their dates ({@link reservationHoldsDates}). A stay takes a night
|
|
191
|
+
* when it overlaps it, with the same `[checkIn, checkOut)` semantics as
|
|
192
|
+
* {@link isRangeAvailable}: a stay that leaves the morning another arrives
|
|
193
|
+
* does not take that night.
|
|
194
|
+
*/
|
|
195
|
+
export declare function unitsTakenByNight(reservations: HeldStay[], fromDayMs: number, toDayMs: number, nowMs?: number): number[];
|
|
196
|
+
/** One night of a resource: how many of its units are still free. */
|
|
197
|
+
export interface ResourceNightUnits {
|
|
198
|
+
dayMs: number;
|
|
199
|
+
unitsLeft: number;
|
|
200
|
+
units: number;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Units left on each of `nights` nights from `fromDayMs`: the resource's
|
|
204
|
+
* units less the stays holding that night, and none on a blocked night,
|
|
205
|
+
* because a block closes the whole resource.
|
|
206
|
+
*/
|
|
207
|
+
export declare function unitsLeftByNight(resource: Pick<HostResource, 'units' | 'blocks'>, reservations: HeldStay[], fromDayMs: number, nights: number, nowMs?: number): ResourceNightUnits[];
|
|
162
208
|
/**
|
|
163
|
-
* Availability: a candidate range
|
|
164
|
-
*
|
|
165
|
-
*
|
|
209
|
+
* Availability: a candidate range is free when, on EVERY night in it, fewer
|
|
210
|
+
* live reservations overlap that night than the resource has units
|
|
211
|
+
* (AGL-4044), and no manual block touches it. `[checkIn, checkOut)`
|
|
212
|
+
* semantics, so back-to-back stays touch without conflict. A block closes the
|
|
213
|
+
* whole resource, every unit at once.
|
|
214
|
+
*
|
|
215
|
+
* One unit (the default, and every resource written before `units`) takes
|
|
216
|
+
* the original path unchanged: the range conflicts when it overlaps any live
|
|
217
|
+
* reservation at all.
|
|
166
218
|
*
|
|
167
219
|
* `nowMs` is a parameter so the pending lapse is testable without freezing the
|
|
168
220
|
* clock. A caller passing reservations without `createdAtMs` gets the same
|
|
169
221
|
* answer as before for every status but `pending`.
|
|
170
222
|
*/
|
|
171
|
-
export declare function isRangeAvailable(resource: Pick<HostResource, 'blocks'>, reservations:
|
|
172
|
-
|
|
173
|
-
|
|
223
|
+
export declare function isRangeAvailable(resource: Pick<HostResource, 'blocks' | 'units'>, reservations: HeldStay[], checkInDayMs: number, checkOutDayMs: number, nowMs?: number): boolean;
|
|
224
|
+
/**
|
|
225
|
+
* The widget's scarcity line starts at this many units left (AGL-4044):
|
|
226
|
+
* "Only 2 left". Never on a one-unit resource, where every free night would
|
|
227
|
+
* read "Only 1 left", and never on a night nothing is booked on.
|
|
228
|
+
*/
|
|
229
|
+
export declare const FEW_UNITS_LEFT = 3;
|
|
230
|
+
/**
|
|
231
|
+
* The fewest units left across `[checkInDayMs, checkOutDayMs)` when that is
|
|
232
|
+
* {@link FEW_UNITS_LEFT} or fewer and at least one, else null. `fewLeft`
|
|
233
|
+
* lists only the scarce nights, as the availability endpoint sends them; a
|
|
234
|
+
* night it leaves out has more than {@link FEW_UNITS_LEFT} free.
|
|
235
|
+
*/
|
|
236
|
+
export declare function fewestUnitsLeft(fewLeft: Array<{
|
|
237
|
+
dayMs: number;
|
|
238
|
+
unitsLeft: number;
|
|
239
|
+
}> | null | undefined, checkInDayMs: number, checkOutDayMs: number): number | null;
|
|
174
240
|
/** Free-cancellation check against the policy window. */
|
|
175
241
|
export declare function canCancelReservation(resource: Pick<HostResource, 'cancellationHours'>, reservation: Pick<HostReservation, 'checkInDayMs' | 'status'>, nowMs?: number): boolean;
|
|
@@ -120,10 +120,67 @@ function inSeason(dayKey, season) {
|
|
|
120
120
|
const createdAtMs = Number((_reservation_createdAtMs = reservation.createdAtMs) != null ? _reservation_createdAtMs : 0);
|
|
121
121
|
return createdAtMs > 0 && nowMs - createdAtMs < PENDING_RESERVATION_HOLD_MS;
|
|
122
122
|
}
|
|
123
|
+
/** The most identical units one resource can hold (AGL-4044). */ export const MAX_RESOURCE_UNITS = 500;
|
|
123
124
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
125
|
+
* How many units a resource holds: the stored `units` as a whole number from 1
|
|
126
|
+
* to {@link MAX_RESOURCE_UNITS}, and 1 for anything else (absent, zero,
|
|
127
|
+
* negative, not a number). Closing a resource is what `blocks` are for, so no
|
|
128
|
+
* stored value can make a resource hold no units at all.
|
|
129
|
+
*/ export function resourceUnits(resource) {
|
|
130
|
+
const units = Math.floor(Number(resource == null ? void 0 : resource.units));
|
|
131
|
+
if (!Number.isFinite(units) || units < 1) return 1;
|
|
132
|
+
return Math.min(MAX_RESOURCE_UNITS, units);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* How many units are taken on each night from `fromDayMs` up to (not
|
|
136
|
+
* including) `toDayMs`, one entry per night, counting only the stays that
|
|
137
|
+
* still hold their dates ({@link reservationHoldsDates}). A stay takes a night
|
|
138
|
+
* when it overlaps it, with the same `[checkIn, checkOut)` semantics as
|
|
139
|
+
* {@link isRangeAvailable}: a stay that leaves the morning another arrives
|
|
140
|
+
* does not take that night.
|
|
141
|
+
*/ export function unitsTakenByNight(reservations, fromDayMs, toDayMs, nowMs = Date.now()) {
|
|
142
|
+
const live = reservations.filter((reservation)=>reservationHoldsDates(reservation, nowMs));
|
|
143
|
+
const taken = [];
|
|
144
|
+
for(let night = fromDayMs; night < toDayMs; night += DAY_MS){
|
|
145
|
+
const nightEnd = Math.min(night + DAY_MS, toDayMs);
|
|
146
|
+
let count = 0;
|
|
147
|
+
for (const reservation of live){
|
|
148
|
+
if (reservation.checkInDayMs < nightEnd && reservation.checkOutDayMs > night) {
|
|
149
|
+
count++;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
taken.push(count);
|
|
153
|
+
}
|
|
154
|
+
return taken;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Units left on each of `nights` nights from `fromDayMs`: the resource's
|
|
158
|
+
* units less the stays holding that night, and none on a blocked night,
|
|
159
|
+
* because a block closes the whole resource.
|
|
160
|
+
*/ export function unitsLeftByNight(resource, reservations, fromDayMs, nights, nowMs = Date.now()) {
|
|
161
|
+
const units = resourceUnits(resource);
|
|
162
|
+
const toDayMs = fromDayMs + Math.max(0, Math.floor(nights)) * DAY_MS;
|
|
163
|
+
return unitsTakenByNight(reservations, fromDayMs, toDayMs, nowMs).map((taken, index)=>{
|
|
164
|
+
var _resource_blocks;
|
|
165
|
+
const dayMs = fromDayMs + index * DAY_MS;
|
|
166
|
+
const blocked = ((_resource_blocks = resource.blocks) != null ? _resource_blocks : []).some((block)=>dayMs < block.toDayMs && dayMs + DAY_MS > block.fromDayMs);
|
|
167
|
+
return {
|
|
168
|
+
dayMs,
|
|
169
|
+
units,
|
|
170
|
+
unitsLeft: blocked ? 0 : Math.max(0, units - taken)
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Availability: a candidate range is free when, on EVERY night in it, fewer
|
|
176
|
+
* live reservations overlap that night than the resource has units
|
|
177
|
+
* (AGL-4044), and no manual block touches it. `[checkIn, checkOut)`
|
|
178
|
+
* semantics, so back-to-back stays touch without conflict. A block closes the
|
|
179
|
+
* whole resource, every unit at once.
|
|
180
|
+
*
|
|
181
|
+
* One unit (the default, and every resource written before `units`) takes
|
|
182
|
+
* the original path unchanged: the range conflicts when it overlaps any live
|
|
183
|
+
* reservation at all.
|
|
127
184
|
*
|
|
128
185
|
* `nowMs` is a parameter so the pending lapse is testable without freezing the
|
|
129
186
|
* clock. A caller passing reservations without `createdAtMs` gets the same
|
|
@@ -131,18 +188,41 @@ function inSeason(dayKey, season) {
|
|
|
131
188
|
*/ export function isRangeAvailable(resource, reservations, checkInDayMs, checkOutDayMs, nowMs = Date.now()) {
|
|
132
189
|
var _resource_blocks;
|
|
133
190
|
if (!(checkOutDayMs > checkInDayMs)) return false;
|
|
134
|
-
for (const reservation of reservations){
|
|
135
|
-
if (!reservationHoldsDates(reservation, nowMs)) continue;
|
|
136
|
-
if (checkInDayMs < reservation.checkOutDayMs && checkOutDayMs > reservation.checkInDayMs) {
|
|
137
|
-
return false;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
191
|
for (const block of (_resource_blocks = resource.blocks) != null ? _resource_blocks : []){
|
|
141
192
|
if (checkInDayMs < block.toDayMs && checkOutDayMs > block.fromDayMs) {
|
|
142
193
|
return false;
|
|
143
194
|
}
|
|
144
195
|
}
|
|
145
|
-
|
|
196
|
+
const units = resourceUnits(resource);
|
|
197
|
+
if (units === 1) {
|
|
198
|
+
for (const reservation of reservations){
|
|
199
|
+
if (!reservationHoldsDates(reservation, nowMs)) continue;
|
|
200
|
+
if (checkInDayMs < reservation.checkOutDayMs && checkOutDayMs > reservation.checkInDayMs) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
return unitsTakenByNight(reservations, checkInDayMs, checkOutDayMs, nowMs).every((taken)=>taken < units);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* The widget's scarcity line starts at this many units left (AGL-4044):
|
|
210
|
+
* "Only 2 left". Never on a one-unit resource, where every free night would
|
|
211
|
+
* read "Only 1 left", and never on a night nothing is booked on.
|
|
212
|
+
*/ export const FEW_UNITS_LEFT = 3;
|
|
213
|
+
/**
|
|
214
|
+
* The fewest units left across `[checkInDayMs, checkOutDayMs)` when that is
|
|
215
|
+
* {@link FEW_UNITS_LEFT} or fewer and at least one, else null. `fewLeft`
|
|
216
|
+
* lists only the scarce nights, as the availability endpoint sends them; a
|
|
217
|
+
* night it leaves out has more than {@link FEW_UNITS_LEFT} free.
|
|
218
|
+
*/ export function fewestUnitsLeft(fewLeft, checkInDayMs, checkOutDayMs) {
|
|
219
|
+
let fewest = null;
|
|
220
|
+
for (const night of fewLeft != null ? fewLeft : []){
|
|
221
|
+
if (night.dayMs < checkInDayMs || night.dayMs >= checkOutDayMs) continue;
|
|
222
|
+
if (!(night.unitsLeft >= 1) || night.unitsLeft > FEW_UNITS_LEFT) continue;
|
|
223
|
+
fewest = fewest === null ? night.unitsLeft : Math.min(fewest, night.unitsLeft);
|
|
224
|
+
}
|
|
225
|
+
return fewest;
|
|
146
226
|
}
|
|
147
227
|
/** Free-cancellation check against the policy window. */ export function canCancelReservation(resource, reservation, nowMs = Date.now()) {
|
|
148
228
|
var _resource_cancellationHours;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/commerce-reservations.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\n/**\n * Reservations v1 (AGL-304): bookable units (cabins) with date-range\n * stays, seasonal pricing, deposits, and cancellation windows —\n * extending the appointment-slot bookings (AGL-159) rather than\n * replacing them. Dates are day-precision epoch-ms at UTC midnight\n * (`dayMs`); nights = checkout day − checkin day. Pure — the\n * reservation APIs and console own I/O.\n */\n\nimport type { StorefrontTaxMode } from './commerce-tax-decision'\n\nexport const DAY_MS = 24 * 60 * 60 * 1000\n\n/** Seasonal pricing window by month-day (year-agnostic, inclusive). */\nexport interface ResourceSeason {\n /** 'MM-DD' inclusive start, e.g. '06-01'. */\n from: string\n /** 'MM-DD' inclusive end; may wrap the year (e.g. '12-15'..'01-05'). */\n to: string\n /** Multiplier on the nightly rate, e.g. 1.5 for peak. */\n multiplier: number\n label?: string\n}\n\n/** `hosts/{hostId}/resources/{id}` doc. */\nexport interface HostResource {\n name: string\n description?: string\n /** Sleeps N; display only. */\n capacity?: number\n photoUrls?: string[]\n amenities?: string[]\n nightlyRateUsd: number\n /** Friday/Saturday nights multiply by this (default 1). */\n weekendMultiplier?: number\n seasons?: ResourceSeason[]\n minNights?: number\n /** Deposit due at reservation: percent of total (1-100). */\n depositPct?: number\n /** Free cancellation until this many hours before check-in. */\n cancellationHours?: number\n /** Manually blocked day ranges (maintenance etc.), dayMs pairs. */\n blocks?: Array<{ fromDayMs: number; toDayMs: number }>\n}\n\nexport type ReservationStatus =\n | 'pending'\n | 'confirmed'\n | 'checked_in'\n | 'checked_out'\n | 'cancelled'\n | 'no_show'\n\n/** `hosts/{hostId}/reservations/{id}` doc. */\nexport interface HostReservation {\n resourceId: string\n status: ReservationStatus\n /** UTC-midnight day of arrival. */\n checkInDayMs: number\n /** UTC-midnight day of departure (exclusive). */\n checkOutDayMs: number\n guestName?: string | null\n guestEmail?: string | null\n nights?: number\n totalCents?: number\n depositCents?: number\n /** Payments applied so far (deposit, folio settlements). */\n paidCents?: number\n /** POS folio lines charged to the stay (AGL-317). */\n folio?: Array<{\n orderId: string\n amountCents: number\n note?: string\n atMs: number\n }>\n checkoutSessionId?: string\n createdAtMs?: number\n /**\n * WHICH tax regime the paid deposit carried (AGL-1969), stamped at\n * confirmation from the same `storefrontTaxModeOf` derivation every order\n * door uses (AGL-2451).\n *\n * `manual` on a stay whose merchant had set a lodging rate\n * (`TaxSettings.lodging`), `none` where they had not — which is the default\n * and every store that has not opted in. Never `stripe-automatic` from this\n * path: Stripe Tax cannot compute occupancy tax without a lodging tax code\n * `reserve.ts` does not send, so the rate is always the merchant's own and\n * is never computed against Aglyn's registrations (AGL-1904).\n *\n * ABSENT is a fourth state and is not `none` — it means the reservation was\n * confirmed before this field existed. See `StorefrontTaxMode`.\n */\n taxMode?: StorefrontTaxMode\n /**\n * The lodging tax charged on top of `paidCents` (AGL-1969), from the\n * merchant's own rate. Absent, never zero, where none was charged.\n *\n * SEPARATE FROM `paidCents` on purpose: that field is the money applied to\n * the STAY and the console divides it by `totalCents` to show what is still\n * owed. Tax is not part of the stay's price and is not the merchant's\n * revenue.\n *\n * LIMITATION, stated rather than decided: this is the rate applied to what\n * was CHARGED, which on a deposit-taking resource is the deposit rather than\n * the stay. Whether a jurisdiction wants occupancy tax on the full stay at\n * booking, on the deposit, or on redemption is not a question this codebase\n * answers — see `reserve.ts` and the merchant copy on the Taxes card.\n */\n taxCents?: number\n}\n\n/** UTC midnight for an instant. */\nexport function toDayMs(atMs: number): number {\n return Math.floor(atMs / DAY_MS) * DAY_MS\n}\n\nfunction monthDay(dayMs: number): string {\n const date = new Date(dayMs)\n const month = String(date.getUTCMonth() + 1).padStart(2, '0')\n const day = String(date.getUTCDate()).padStart(2, '0')\n return `${month}-${day}`\n}\n\nfunction inSeason(dayKey: string, season: ResourceSeason): boolean {\n if (season.from <= season.to) {\n return dayKey >= season.from && dayKey <= season.to\n }\n // Wrapping season (e.g. 12-15 .. 01-05).\n return dayKey >= season.from || dayKey <= season.to\n}\n\n/** Per-night rate in cents for one specific night. */\nexport function nightlyRateCents(\n resource: Pick<\n HostResource,\n 'nightlyRateUsd' | 'weekendMultiplier' | 'seasons'\n >,\n dayMs: number,\n): number {\n let rate = resource.nightlyRateUsd * 100\n const weekday = new Date(dayMs).getUTCDay()\n if ((weekday === 5 || weekday === 6) && resource.weekendMultiplier) {\n rate *= resource.weekendMultiplier\n }\n const dayKey = monthDay(dayMs)\n for (const season of resource.seasons ?? []) {\n if (inSeason(dayKey, season)) {\n rate *= season.multiplier\n break\n }\n }\n return Math.round(rate)\n}\n\nexport interface ReservationQuote {\n nights: number\n subtotalCents: number\n depositCents: number\n totalCents: number\n /** Per-night breakdown for the quote UI. */\n nightlyCents: number[]\n problem?: string\n}\n\n/** Prices a stay; `problem` set (and zeros) when the range is invalid. */\nexport function computeReservationQuote(\n resource: HostResource,\n checkInDayMs: number,\n checkOutDayMs: number,\n): ReservationQuote {\n const empty: ReservationQuote = {\n nights: 0,\n subtotalCents: 0,\n depositCents: 0,\n totalCents: 0,\n nightlyCents: [],\n }\n const nights = Math.round((checkOutDayMs - checkInDayMs) / DAY_MS)\n if (!(nights > 0)) return { ...empty, problem: 'Choose at least one night' }\n if (resource.minNights && nights < resource.minNights) {\n return { ...empty, problem: `Minimum stay is ${resource.minNights} nights` }\n }\n const nightlyCents: number[] = []\n for (let day = checkInDayMs; day < checkOutDayMs; day += DAY_MS) {\n nightlyCents.push(nightlyRateCents(resource, day))\n }\n const subtotalCents = nightlyCents.reduce((sum, cents) => sum + cents, 0)\n const depositPct = Math.min(100, Math.max(0, resource.depositPct ?? 100))\n const depositCents = Math.round((subtotalCents * depositPct) / 100)\n return {\n nights,\n subtotalCents,\n depositCents,\n totalCents: subtotalCents,\n nightlyCents,\n }\n}\n\n/**\n * How long an unpaid `pending` reservation stands between the next guest and\n * the dates it named.\n *\n * A `pending` row is written before the Stripe session opens, and nothing ever\n * clears one: `process-abandoned.ts` sweeps `checkouts`, not `reservations`.\n * So the hold has to lapse by the clock rather than by a job, or one guest who\n * closed the tab at the payment screen takes those dates off the market\n * permanently.\n */\nexport const PENDING_RESERVATION_HOLD_MS = 30 * 60 * 1000\n\n/**\n * Does this reservation still stand between a guest and its dates?\n *\n * THE one answer, because four surfaces ask it — the booking door, the booking\n * transaction's re-read, the storefront availability endpoint behind the\n * date-picker, and the console's walk-in check — and they were not asking it\n * the same way. Only the booking door aged out a stale `pending`, so the\n * storefront painted dates unavailable that the booking door would have sold,\n * and the front desk was told a room was taken while it stood empty.\n *\n * A `pending` row carrying no `createdAtMs` releases rather than holds: an\n * unbounded hold on a row that cannot prove its own age is the failure this\n * lapse exists to prevent, and the booking door re-checks inside its\n * transaction before any money moves.\n */\nexport function reservationHoldsDates(\n reservation: Pick<HostReservation, 'status'> & { createdAtMs?: number },\n nowMs: number = Date.now(),\n): boolean {\n if (reservation.status === 'cancelled' || reservation.status === 'no_show') {\n return false\n }\n if (reservation.status !== 'pending') return true\n const createdAtMs = Number(reservation.createdAtMs ?? 0)\n return createdAtMs > 0 && nowMs - createdAtMs < PENDING_RESERVATION_HOLD_MS\n}\n\n/**\n * Availability: a candidate range conflicts when it overlaps a live\n * reservation ([checkIn, checkOut) semantics — back-to-back stays touch\n * without conflict) or a manual block.\n *\n * `nowMs` is a parameter so the pending lapse is testable without freezing the\n * clock. A caller passing reservations without `createdAtMs` gets the same\n * answer as before for every status but `pending`.\n */\nexport function isRangeAvailable(\n resource: Pick<HostResource, 'blocks'>,\n reservations: Array<\n Pick<HostReservation, 'checkInDayMs' | 'checkOutDayMs' | 'status'> & {\n createdAtMs?: number\n }\n >,\n checkInDayMs: number,\n checkOutDayMs: number,\n nowMs: number = Date.now(),\n): boolean {\n if (!(checkOutDayMs > checkInDayMs)) return false\n for (const reservation of reservations) {\n if (!reservationHoldsDates(reservation, nowMs)) continue\n if (\n checkInDayMs < reservation.checkOutDayMs &&\n checkOutDayMs > reservation.checkInDayMs\n ) {\n return false\n }\n }\n for (const block of resource.blocks ?? []) {\n if (checkInDayMs < block.toDayMs && checkOutDayMs > block.fromDayMs) {\n return false\n }\n }\n return true\n}\n\n/** Free-cancellation check against the policy window. */\nexport function canCancelReservation(\n resource: Pick<HostResource, 'cancellationHours'>,\n reservation: Pick<HostReservation, 'checkInDayMs' | 'status'>,\n nowMs = Date.now(),\n): boolean {\n if (!['pending', 'confirmed'].includes(reservation.status)) return false\n const windowMs = (resource.cancellationHours ?? 0) * 60 * 60 * 1000\n return nowMs <= reservation.checkInDayMs - windowMs\n}\n"],"names":["DAY_MS","toDayMs","atMs","Math","floor","monthDay","dayMs","date","Date","month","String","getUTCMonth","padStart","day","getUTCDate","inSeason","dayKey","season","from","to","nightlyRateCents","resource","rate","nightlyRateUsd","weekday","getUTCDay","weekendMultiplier","seasons","multiplier","round","computeReservationQuote","checkInDayMs","checkOutDayMs","empty","nights","subtotalCents","depositCents","totalCents","nightlyCents","problem","minNights","push","reduce","sum","cents","depositPct","min","max","PENDING_RESERVATION_HOLD_MS","reservationHoldsDates","reservation","nowMs","now","status","createdAtMs","Number","isRangeAvailable","reservations","block","blocks","fromDayMs","canCancelReservation","includes","windowMs","cancellationHours"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;CAOC;AAID,OAAO,MAAMA,SAAS,KAAK,KAAK,KAAK,KAAI;AAoGzC,iCAAiC,GACjC,OAAO,SAASC,QAAQC,IAAY;IAClC,OAAOC,KAAKC,KAAK,CAACF,OAAOF,UAAUA;AACrC;AAEA,SAASK,SAASC,KAAa;IAC7B,MAAMC,OAAO,IAAIC,KAAKF;IACtB,MAAMG,QAAQC,OAAOH,KAAKI,WAAW,KAAK,GAAGC,QAAQ,CAAC,GAAG;IACzD,MAAMC,MAAMH,OAAOH,KAAKO,UAAU,IAAIF,QAAQ,CAAC,GAAG;IAClD,OAAO,GAAGH,MAAM,CAAC,EAAEI,KAAK;AAC1B;AAEA,SAASE,SAASC,MAAc,EAAEC,MAAsB;IACtD,IAAIA,OAAOC,IAAI,IAAID,OAAOE,EAAE,EAAE;QAC5B,OAAOH,UAAUC,OAAOC,IAAI,IAAIF,UAAUC,OAAOE,EAAE;IACrD;IACA,yCAAyC;IACzC,OAAOH,UAAUC,OAAOC,IAAI,IAAIF,UAAUC,OAAOE,EAAE;AACrD;AAEA,oDAAoD,GACpD,OAAO,SAASC,iBACdC,QAGC,EACDf,KAAa;QAQQe;IANrB,IAAIC,OAAOD,SAASE,cAAc,GAAG;IACrC,MAAMC,UAAU,IAAIhB,KAAKF,OAAOmB,SAAS;IACzC,IAAI,AAACD,CAAAA,YAAY,KAAKA,YAAY,CAAA,KAAMH,SAASK,iBAAiB,EAAE;QAClEJ,QAAQD,SAASK,iBAAiB;IACpC;IACA,MAAMV,SAASX,SAASC;IACxB,KAAK,MAAMW,WAAUI,oBAAAA,SAASM,OAAO,YAAhBN,oBAAoB,EAAE,CAAE;QAC3C,IAAIN,SAASC,QAAQC,SAAS;YAC5BK,QAAQL,OAAOW,UAAU;YACzB;QACF;IACF;IACA,OAAOzB,KAAK0B,KAAK,CAACP;AACpB;AAYA,wEAAwE,GACxE,OAAO,SAASQ,wBACdT,QAAsB,EACtBU,YAAoB,EACpBC,aAAqB;QAmBwBX;IAjB7C,MAAMY,QAA0B;QAC9BC,QAAQ;QACRC,eAAe;QACfC,cAAc;QACdC,YAAY;QACZC,cAAc,EAAE;IAClB;IACA,MAAMJ,SAAS/B,KAAK0B,KAAK,CAAC,AAACG,CAAAA,gBAAgBD,YAAW,IAAK/B;IAC3D,IAAI,CAAEkC,CAAAA,SAAS,CAAA,GAAI,OAAO,aAAKD;QAAOM,SAAS;;IAC/C,IAAIlB,SAASmB,SAAS,IAAIN,SAASb,SAASmB,SAAS,EAAE;QACrD,OAAO,aAAKP;YAAOM,SAAS,CAAC,gBAAgB,EAAElB,SAASmB,SAAS,CAAC,OAAO,CAAC;;IAC5E;IACA,MAAMF,eAAyB,EAAE;IACjC,IAAK,IAAIzB,MAAMkB,cAAclB,MAAMmB,eAAenB,OAAOb,OAAQ;QAC/DsC,aAAaG,IAAI,CAACrB,iBAAiBC,UAAUR;IAC/C;IACA,MAAMsB,gBAAgBG,aAAaI,MAAM,CAAC,CAACC,KAAKC,QAAUD,MAAMC,OAAO;IACvE,MAAMC,aAAa1C,KAAK2C,GAAG,CAAC,KAAK3C,KAAK4C,GAAG,CAAC,IAAG1B,uBAAAA,SAASwB,UAAU,YAAnBxB,uBAAuB;IACpE,MAAMe,eAAejC,KAAK0B,KAAK,CAAC,AAACM,gBAAgBU,aAAc;IAC/D,OAAO;QACLX;QACAC;QACAC;QACAC,YAAYF;QACZG;IACF;AACF;AAEA;;;;;;;;;CASC,GACD,OAAO,MAAMU,8BAA8B,KAAK,KAAK,KAAI;AAEzD;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASC,sBACdC,WAAuE,EACvEC,QAAgB3C,KAAK4C,GAAG,EAAE;QAMCF;IAJ3B,IAAIA,YAAYG,MAAM,KAAK,eAAeH,YAAYG,MAAM,KAAK,WAAW;QAC1E,OAAO;IACT;IACA,IAAIH,YAAYG,MAAM,KAAK,WAAW,OAAO;IAC7C,MAAMC,cAAcC,QAAOL,2BAAAA,YAAYI,WAAW,YAAvBJ,2BAA2B;IACtD,OAAOI,cAAc,KAAKH,QAAQG,cAAcN;AAClD;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASQ,iBACdnC,QAAsC,EACtCoC,YAIC,EACD1B,YAAoB,EACpBC,aAAqB,EACrBmB,QAAgB3C,KAAK4C,GAAG,EAAE;QAYN/B;IAVpB,IAAI,CAAEW,CAAAA,gBAAgBD,YAAW,GAAI,OAAO;IAC5C,KAAK,MAAMmB,eAAeO,aAAc;QACtC,IAAI,CAACR,sBAAsBC,aAAaC,QAAQ;QAChD,IACEpB,eAAemB,YAAYlB,aAAa,IACxCA,gBAAgBkB,YAAYnB,YAAY,EACxC;YACA,OAAO;QACT;IACF;IACA,KAAK,MAAM2B,UAASrC,mBAAAA,SAASsC,MAAM,YAAftC,mBAAmB,EAAE,CAAE;QACzC,IAAIU,eAAe2B,MAAMzD,OAAO,IAAI+B,gBAAgB0B,MAAME,SAAS,EAAE;YACnE,OAAO;QACT;IACF;IACA,OAAO;AACT;AAEA,uDAAuD,GACvD,OAAO,SAASC,qBACdxC,QAAiD,EACjD6B,WAA6D,EAC7DC,QAAQ3C,KAAK4C,GAAG,EAAE;QAGA/B;IADlB,IAAI,CAAC;QAAC;QAAW;KAAY,CAACyC,QAAQ,CAACZ,YAAYG,MAAM,GAAG,OAAO;IACnE,MAAMU,WAAW,EAAC1C,8BAAAA,SAAS2C,iBAAiB,YAA1B3C,8BAA8B,KAAK,KAAK,KAAK;IAC/D,OAAO8B,SAASD,YAAYnB,YAAY,GAAGgC;AAC7C"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/commerce-reservations.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\n/**\n * Reservations v1 (AGL-304): bookable units (cabins) with date-range\n * stays, seasonal pricing, deposits, and cancellation windows —\n * extending the appointment-slot bookings (AGL-159) rather than\n * replacing them. Dates are day-precision epoch-ms at UTC midnight\n * (`dayMs`); nights = checkout day − checkin day. Pure — the\n * reservation APIs and console own I/O.\n */\n\nimport type { StorefrontTaxMode } from './commerce-tax-decision'\n\nexport const DAY_MS = 24 * 60 * 60 * 1000\n\n/** Seasonal pricing window by month-day (year-agnostic, inclusive). */\nexport interface ResourceSeason {\n /** 'MM-DD' inclusive start, e.g. '06-01'. */\n from: string\n /** 'MM-DD' inclusive end; may wrap the year (e.g. '12-15'..'01-05'). */\n to: string\n /** Multiplier on the nightly rate, e.g. 1.5 for peak. */\n multiplier: number\n label?: string\n}\n\n/** `hosts/{hostId}/resources/{id}` doc. */\nexport interface HostResource {\n name: string\n description?: string\n /**\n * Sleeps N; display only. Never availability: how many guests one unit\n * takes says nothing about how many units there are (see `units`).\n */\n capacity?: number\n /**\n * How many identical units this one resource holds (AGL-4044): 12 RV\n * hookups, 30 tent sites, 4 matching cabins. Each night can take this many\n * stays at once. Absent means 1, which is every resource written before the\n * field existed, and one unit behaves exactly as a resource always has.\n * An integer from 1 to {@link MAX_RESOURCE_UNITS}; read it through\n * {@link resourceUnits}, never raw.\n */\n units?: number\n photoUrls?: string[]\n amenities?: string[]\n nightlyRateUsd: number\n /** Friday/Saturday nights multiply by this (default 1). */\n weekendMultiplier?: number\n seasons?: ResourceSeason[]\n minNights?: number\n /** Deposit due at reservation: percent of total (1-100). */\n depositPct?: number\n /** Free cancellation until this many hours before check-in. */\n cancellationHours?: number\n /** Manually blocked day ranges (maintenance etc.), dayMs pairs. */\n blocks?: Array<{ fromDayMs: number; toDayMs: number }>\n}\n\nexport type ReservationStatus =\n | 'pending'\n | 'confirmed'\n | 'checked_in'\n | 'checked_out'\n | 'cancelled'\n | 'no_show'\n\n/** `hosts/{hostId}/reservations/{id}` doc. */\nexport interface HostReservation {\n resourceId: string\n status: ReservationStatus\n /** UTC-midnight day of arrival. */\n checkInDayMs: number\n /** UTC-midnight day of departure (exclusive). */\n checkOutDayMs: number\n guestName?: string | null\n guestEmail?: string | null\n nights?: number\n totalCents?: number\n depositCents?: number\n /** Payments applied so far (deposit, folio settlements). */\n paidCents?: number\n /** POS folio lines charged to the stay (AGL-317). */\n folio?: Array<{\n orderId: string\n amountCents: number\n note?: string\n atMs: number\n }>\n checkoutSessionId?: string\n createdAtMs?: number\n /**\n * WHICH tax regime the paid deposit carried (AGL-1969), stamped at\n * confirmation from the same `storefrontTaxModeOf` derivation every order\n * door uses (AGL-2451).\n *\n * `manual` on a stay whose merchant had set a lodging rate\n * (`TaxSettings.lodging`), `none` where they had not — which is the default\n * and every store that has not opted in. Never `stripe-automatic` from this\n * path: Stripe Tax cannot compute occupancy tax without a lodging tax code\n * `reserve.ts` does not send, so the rate is always the merchant's own and\n * is never computed against Aglyn's registrations (AGL-1904).\n *\n * ABSENT is a fourth state and is not `none` — it means the reservation was\n * confirmed before this field existed. See `StorefrontTaxMode`.\n */\n taxMode?: StorefrontTaxMode\n /**\n * The lodging tax charged on top of `paidCents` (AGL-1969), from the\n * merchant's own rate. Absent, never zero, where none was charged.\n *\n * SEPARATE FROM `paidCents` on purpose: that field is the money applied to\n * the STAY and the console divides it by `totalCents` to show what is still\n * owed. Tax is not part of the stay's price and is not the merchant's\n * revenue.\n *\n * LIMITATION, stated rather than decided: this is the rate applied to what\n * was CHARGED, which on a deposit-taking resource is the deposit rather than\n * the stay. Whether a jurisdiction wants occupancy tax on the full stay at\n * booking, on the deposit, or on redemption is not a question this codebase\n * answers — see `reserve.ts` and the merchant copy on the Taxes card.\n */\n taxCents?: number\n}\n\n/** UTC midnight for an instant. */\nexport function toDayMs(atMs: number): number {\n return Math.floor(atMs / DAY_MS) * DAY_MS\n}\n\nfunction monthDay(dayMs: number): string {\n const date = new Date(dayMs)\n const month = String(date.getUTCMonth() + 1).padStart(2, '0')\n const day = String(date.getUTCDate()).padStart(2, '0')\n return `${month}-${day}`\n}\n\nfunction inSeason(dayKey: string, season: ResourceSeason): boolean {\n if (season.from <= season.to) {\n return dayKey >= season.from && dayKey <= season.to\n }\n // Wrapping season (e.g. 12-15 .. 01-05).\n return dayKey >= season.from || dayKey <= season.to\n}\n\n/** Per-night rate in cents for one specific night. */\nexport function nightlyRateCents(\n resource: Pick<\n HostResource,\n 'nightlyRateUsd' | 'weekendMultiplier' | 'seasons'\n >,\n dayMs: number,\n): number {\n let rate = resource.nightlyRateUsd * 100\n const weekday = new Date(dayMs).getUTCDay()\n if ((weekday === 5 || weekday === 6) && resource.weekendMultiplier) {\n rate *= resource.weekendMultiplier\n }\n const dayKey = monthDay(dayMs)\n for (const season of resource.seasons ?? []) {\n if (inSeason(dayKey, season)) {\n rate *= season.multiplier\n break\n }\n }\n return Math.round(rate)\n}\n\nexport interface ReservationQuote {\n nights: number\n subtotalCents: number\n depositCents: number\n totalCents: number\n /** Per-night breakdown for the quote UI. */\n nightlyCents: number[]\n problem?: string\n}\n\n/** Prices a stay; `problem` set (and zeros) when the range is invalid. */\nexport function computeReservationQuote(\n resource: HostResource,\n checkInDayMs: number,\n checkOutDayMs: number,\n): ReservationQuote {\n const empty: ReservationQuote = {\n nights: 0,\n subtotalCents: 0,\n depositCents: 0,\n totalCents: 0,\n nightlyCents: [],\n }\n const nights = Math.round((checkOutDayMs - checkInDayMs) / DAY_MS)\n if (!(nights > 0)) return { ...empty, problem: 'Choose at least one night' }\n if (resource.minNights && nights < resource.minNights) {\n return { ...empty, problem: `Minimum stay is ${resource.minNights} nights` }\n }\n const nightlyCents: number[] = []\n for (let day = checkInDayMs; day < checkOutDayMs; day += DAY_MS) {\n nightlyCents.push(nightlyRateCents(resource, day))\n }\n const subtotalCents = nightlyCents.reduce((sum, cents) => sum + cents, 0)\n const depositPct = Math.min(100, Math.max(0, resource.depositPct ?? 100))\n const depositCents = Math.round((subtotalCents * depositPct) / 100)\n return {\n nights,\n subtotalCents,\n depositCents,\n totalCents: subtotalCents,\n nightlyCents,\n }\n}\n\n/**\n * How long an unpaid `pending` reservation stands between the next guest and\n * the dates it named.\n *\n * A `pending` row is written before the Stripe session opens, and nothing ever\n * clears one: `process-abandoned.ts` sweeps `checkouts`, not `reservations`.\n * So the hold has to lapse by the clock rather than by a job, or one guest who\n * closed the tab at the payment screen takes those dates off the market\n * permanently.\n */\nexport const PENDING_RESERVATION_HOLD_MS = 30 * 60 * 1000\n\n/**\n * Does this reservation still stand between a guest and its dates?\n *\n * THE one answer, because four surfaces ask it — the booking door, the booking\n * transaction's re-read, the storefront availability endpoint behind the\n * date-picker, and the console's walk-in check — and they were not asking it\n * the same way. Only the booking door aged out a stale `pending`, so the\n * storefront painted dates unavailable that the booking door would have sold,\n * and the front desk was told a room was taken while it stood empty.\n *\n * A `pending` row carrying no `createdAtMs` releases rather than holds: an\n * unbounded hold on a row that cannot prove its own age is the failure this\n * lapse exists to prevent, and the booking door re-checks inside its\n * transaction before any money moves.\n */\nexport function reservationHoldsDates(\n reservation: Pick<HostReservation, 'status'> & { createdAtMs?: number },\n nowMs: number = Date.now(),\n): boolean {\n if (reservation.status === 'cancelled' || reservation.status === 'no_show') {\n return false\n }\n if (reservation.status !== 'pending') return true\n const createdAtMs = Number(reservation.createdAtMs ?? 0)\n return createdAtMs > 0 && nowMs - createdAtMs < PENDING_RESERVATION_HOLD_MS\n}\n\n/** The most identical units one resource can hold (AGL-4044). */\nexport const MAX_RESOURCE_UNITS = 500\n\n/**\n * How many units a resource holds: the stored `units` as a whole number from 1\n * to {@link MAX_RESOURCE_UNITS}, and 1 for anything else (absent, zero,\n * negative, not a number). Closing a resource is what `blocks` are for, so no\n * stored value can make a resource hold no units at all.\n */\nexport function resourceUnits(\n resource: Pick<HostResource, 'units'> | null | undefined,\n): number {\n const units = Math.floor(Number(resource?.units))\n if (!Number.isFinite(units) || units < 1) return 1\n return Math.min(MAX_RESOURCE_UNITS, units)\n}\n\n/** A reservation as an availability check reads it. */\nexport type HeldStay = Pick<\n HostReservation,\n 'checkInDayMs' | 'checkOutDayMs' | 'status'\n> & {\n createdAtMs?: number\n}\n\n/**\n * How many units are taken on each night from `fromDayMs` up to (not\n * including) `toDayMs`, one entry per night, counting only the stays that\n * still hold their dates ({@link reservationHoldsDates}). A stay takes a night\n * when it overlaps it, with the same `[checkIn, checkOut)` semantics as\n * {@link isRangeAvailable}: a stay that leaves the morning another arrives\n * does not take that night.\n */\nexport function unitsTakenByNight(\n reservations: HeldStay[],\n fromDayMs: number,\n toDayMs: number,\n nowMs: number = Date.now(),\n): number[] {\n const live = reservations.filter((reservation) =>\n reservationHoldsDates(reservation, nowMs),\n )\n const taken: number[] = []\n for (let night = fromDayMs; night < toDayMs; night += DAY_MS) {\n const nightEnd = Math.min(night + DAY_MS, toDayMs)\n let count = 0\n for (const reservation of live) {\n if (\n reservation.checkInDayMs < nightEnd &&\n reservation.checkOutDayMs > night\n ) {\n count++\n }\n }\n taken.push(count)\n }\n return taken\n}\n\n/** One night of a resource: how many of its units are still free. */\nexport interface ResourceNightUnits {\n dayMs: number\n unitsLeft: number\n units: number\n}\n\n/**\n * Units left on each of `nights` nights from `fromDayMs`: the resource's\n * units less the stays holding that night, and none on a blocked night,\n * because a block closes the whole resource.\n */\nexport function unitsLeftByNight(\n resource: Pick<HostResource, 'units' | 'blocks'>,\n reservations: HeldStay[],\n fromDayMs: number,\n nights: number,\n nowMs: number = Date.now(),\n): ResourceNightUnits[] {\n const units = resourceUnits(resource)\n const toDayMs = fromDayMs + Math.max(0, Math.floor(nights)) * DAY_MS\n return unitsTakenByNight(reservations, fromDayMs, toDayMs, nowMs).map(\n (taken, index) => {\n const dayMs = fromDayMs + index * DAY_MS\n const blocked = (resource.blocks ?? []).some(\n (block) => dayMs < block.toDayMs && dayMs + DAY_MS > block.fromDayMs,\n )\n return {\n dayMs,\n units,\n unitsLeft: blocked ? 0 : Math.max(0, units - taken),\n }\n },\n )\n}\n\n/**\n * Availability: a candidate range is free when, on EVERY night in it, fewer\n * live reservations overlap that night than the resource has units\n * (AGL-4044), and no manual block touches it. `[checkIn, checkOut)`\n * semantics, so back-to-back stays touch without conflict. A block closes the\n * whole resource, every unit at once.\n *\n * One unit (the default, and every resource written before `units`) takes\n * the original path unchanged: the range conflicts when it overlaps any live\n * reservation at all.\n *\n * `nowMs` is a parameter so the pending lapse is testable without freezing the\n * clock. A caller passing reservations without `createdAtMs` gets the same\n * answer as before for every status but `pending`.\n */\nexport function isRangeAvailable(\n resource: Pick<HostResource, 'blocks' | 'units'>,\n reservations: HeldStay[],\n checkInDayMs: number,\n checkOutDayMs: number,\n nowMs: number = Date.now(),\n): boolean {\n if (!(checkOutDayMs > checkInDayMs)) return false\n for (const block of resource.blocks ?? []) {\n if (checkInDayMs < block.toDayMs && checkOutDayMs > block.fromDayMs) {\n return false\n }\n }\n const units = resourceUnits(resource)\n if (units === 1) {\n for (const reservation of reservations) {\n if (!reservationHoldsDates(reservation, nowMs)) continue\n if (\n checkInDayMs < reservation.checkOutDayMs &&\n checkOutDayMs > reservation.checkInDayMs\n ) {\n return false\n }\n }\n return true\n }\n return unitsTakenByNight(\n reservations,\n checkInDayMs,\n checkOutDayMs,\n nowMs,\n ).every((taken) => taken < units)\n}\n\n/**\n * The widget's scarcity line starts at this many units left (AGL-4044):\n * \"Only 2 left\". Never on a one-unit resource, where every free night would\n * read \"Only 1 left\", and never on a night nothing is booked on.\n */\nexport const FEW_UNITS_LEFT = 3\n\n/**\n * The fewest units left across `[checkInDayMs, checkOutDayMs)` when that is\n * {@link FEW_UNITS_LEFT} or fewer and at least one, else null. `fewLeft`\n * lists only the scarce nights, as the availability endpoint sends them; a\n * night it leaves out has more than {@link FEW_UNITS_LEFT} free.\n */\nexport function fewestUnitsLeft(\n fewLeft: Array<{ dayMs: number; unitsLeft: number }> | null | undefined,\n checkInDayMs: number,\n checkOutDayMs: number,\n): number | null {\n let fewest: number | null = null\n for (const night of fewLeft ?? []) {\n if (night.dayMs < checkInDayMs || night.dayMs >= checkOutDayMs) continue\n if (!(night.unitsLeft >= 1) || night.unitsLeft > FEW_UNITS_LEFT) continue\n fewest = fewest === null ? night.unitsLeft : Math.min(fewest, night.unitsLeft)\n }\n return fewest\n}\n\n/** Free-cancellation check against the policy window. */\nexport function canCancelReservation(\n resource: Pick<HostResource, 'cancellationHours'>,\n reservation: Pick<HostReservation, 'checkInDayMs' | 'status'>,\n nowMs = Date.now(),\n): boolean {\n if (!['pending', 'confirmed'].includes(reservation.status)) return false\n const windowMs = (resource.cancellationHours ?? 0) * 60 * 60 * 1000\n return nowMs <= reservation.checkInDayMs - windowMs\n}\n"],"names":["DAY_MS","toDayMs","atMs","Math","floor","monthDay","dayMs","date","Date","month","String","getUTCMonth","padStart","day","getUTCDate","inSeason","dayKey","season","from","to","nightlyRateCents","resource","rate","nightlyRateUsd","weekday","getUTCDay","weekendMultiplier","seasons","multiplier","round","computeReservationQuote","checkInDayMs","checkOutDayMs","empty","nights","subtotalCents","depositCents","totalCents","nightlyCents","problem","minNights","push","reduce","sum","cents","depositPct","min","max","PENDING_RESERVATION_HOLD_MS","reservationHoldsDates","reservation","nowMs","now","status","createdAtMs","Number","MAX_RESOURCE_UNITS","resourceUnits","units","isFinite","unitsTakenByNight","reservations","fromDayMs","live","filter","taken","night","nightEnd","count","unitsLeftByNight","map","index","blocked","blocks","some","block","unitsLeft","isRangeAvailable","every","FEW_UNITS_LEFT","fewestUnitsLeft","fewLeft","fewest","canCancelReservation","includes","windowMs","cancellationHours"],"mappings":"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"}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
import { type ProductModifierGroup } from './product-modifiers';
|
|
18
|
+
import { type ProductCheckIn } from './commerce-check-in-product';
|
|
18
19
|
import { type ProductChannelFacts } from './product-channel';
|
|
19
20
|
import type { PickupLocationSettings } from './order-local-fulfillment';
|
|
20
21
|
/**
|
|
@@ -70,6 +71,11 @@ export interface ProductVariant {
|
|
|
70
71
|
inventoryByLocation?: Record<string, number>;
|
|
71
72
|
/** Media-library image URL shown when this variant is selected. */
|
|
72
73
|
imageUrl?: string;
|
|
74
|
+
/**
|
|
75
|
+
* Whether this variant needs check-in at the window (AGL-4043), over the
|
|
76
|
+
* product's own setting; absent follows the product.
|
|
77
|
+
*/
|
|
78
|
+
checkIn?: ProductCheckIn;
|
|
73
79
|
}
|
|
74
80
|
/** A product's packed size and customs facts (AGL-3612). */
|
|
75
81
|
export interface ProductShippingFacts {
|
|
@@ -248,6 +254,21 @@ export interface HostProduct {
|
|
|
248
254
|
modifierGroups?: ProductModifierGroup[];
|
|
249
255
|
/** Shown under Quick keys on the register's first screen (AGL-3607). */
|
|
250
256
|
posQuickKey?: boolean;
|
|
257
|
+
/**
|
|
258
|
+
* Whether the product is sold online (AGL-4060): its page, the site's
|
|
259
|
+
* product grids, search, the sitemap, the shopping feeds and marketplaces,
|
|
260
|
+
* carts and checkout. `false` keeps it on the register, the kiosk, the
|
|
261
|
+
* Aglyn POS apps and draft orders only. Absent reads as `true`, so every
|
|
262
|
+
* product stored before the switch sells online as it did; read it through
|
|
263
|
+
* {@link productSellsOnline}, never as a truthy field.
|
|
264
|
+
*/
|
|
265
|
+
sellOnline?: boolean;
|
|
266
|
+
/**
|
|
267
|
+
* A pass, a camping spot, parking: each item sold is somebody to check in
|
|
268
|
+
* at the window on the day it was bought, `headcount` people per item
|
|
269
|
+
* (AGL-4043). A variant's own `checkIn` overrides it.
|
|
270
|
+
*/
|
|
271
|
+
checkIn?: ProductCheckIn;
|
|
251
272
|
createdAtMs?: number;
|
|
252
273
|
updatedAtMs?: number;
|
|
253
274
|
deletedAt?: number | null;
|
|
@@ -712,6 +733,25 @@ export interface MembershipProduct {
|
|
|
712
733
|
}>;
|
|
713
734
|
}
|
|
714
735
|
export declare function smartCollectionMatches(product: MembershipProduct, collection: Pick<HostCollection, 'rules' | 'matchAll'>): boolean;
|
|
736
|
+
/**
|
|
737
|
+
* Whether a product is sold online (AGL-4060). Only an explicit `false` turns
|
|
738
|
+
* it off: a product stored before the switch existed carries no
|
|
739
|
+
* `sellOnline` at all and sells online exactly as it did.
|
|
740
|
+
*/
|
|
741
|
+
export declare function productSellsOnline(product: {
|
|
742
|
+
sellOnline?: unknown;
|
|
743
|
+
} | null | undefined): boolean;
|
|
744
|
+
/**
|
|
745
|
+
* Whether a shopper online may see or buy a product (AGL-4060): live, active
|
|
746
|
+
* and sold online. Every online surface (the product page, grids, search,
|
|
747
|
+
* the sitemap, feeds, carts, checkout) asks this; the register, the kiosk,
|
|
748
|
+
* the POS apps and draft orders ask only that it is live and active.
|
|
749
|
+
*/
|
|
750
|
+
export declare function productListedOnline(product: {
|
|
751
|
+
deletedAt?: unknown;
|
|
752
|
+
status?: unknown;
|
|
753
|
+
sellOnline?: unknown;
|
|
754
|
+
} | null | undefined): boolean;
|
|
715
755
|
/**
|
|
716
756
|
* Smart-collection membership: draft/archived/deleted products never
|
|
717
757
|
* match; manual collections answer from productIds.
|