@aglyn/plugins-commerce 1.0.0-beta.290 → 1.0.0-beta.292
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/README.md +4 -2
- package/package.json +13 -13
- package/src/lib/components/cart-fulfillment.d.ts +3 -0
- package/src/lib/components/cart-fulfillment.js.map +1 -1
- package/src/lib/components/cart.js +14 -4
- package/src/lib/components/cart.js.map +1 -1
- package/src/lib/components/commerce-console-page.js +2 -1
- package/src/lib/components/commerce-console-page.js.map +1 -1
- package/src/lib/components/console/host-orders-card.component.d.ts +6 -0
- package/src/lib/components/console/host-orders-card.component.js +3 -2
- package/src/lib/components/console/host-orders-card.component.js.map +1 -1
- package/src/lib/components/console/invoice-send.component.d.ts +44 -0
- package/src/lib/components/console/invoice-send.component.js +237 -0
- package/src/lib/components/console/invoice-send.component.js.map +1 -0
- package/src/lib/components/console/order-detail-dialog.component.d.ts +5 -0
- package/src/lib/components/console/order-detail-dialog.component.js +9 -2
- package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
- package/src/lib/components/console/order-invoice.component.d.ts +9 -0
- package/src/lib/components/console/order-invoice.component.js +27 -2
- package/src/lib/components/console/order-invoice.component.js.map +1 -1
- package/src/lib/components/console/pos/pos-item-dialog.component.js +3 -2
- package/src/lib/components/console/pos/pos-item-dialog.component.js.map +1 -1
- package/src/lib/components/console/pos-kiosk-page.component.js +2 -1
- package/src/lib/components/console/pos-kiosk-page.component.js.map +1 -1
- package/src/lib/components/console/pos-page.component.js +1 -1
- package/src/lib/components/console/pos-page.component.js.map +1 -1
- package/src/lib/components/console/product-add-ons-fields.component.d.ts +41 -0
- package/src/lib/components/console/product-add-ons-fields.component.js +356 -0
- package/src/lib/components/console/product-add-ons-fields.component.js.map +1 -0
- package/src/lib/components/console/product-editor-dialog.component.js +31 -5
- package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/product-register-fields.component.d.ts +4 -5
- package/src/lib/components/console/product-register-fields.component.js +8 -197
- package/src/lib/components/console/product-register-fields.component.js.map +1 -1
- package/src/lib/components/console/products-hub-zone.component.js +6 -0
- package/src/lib/components/console/products-hub-zone.component.js.map +1 -1
- package/src/lib/components/console/quote-editor-dialog.component.d.ts +29 -0
- package/src/lib/components/console/quote-editor-dialog.component.js +102 -4
- package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/quote-pay-link.component.d.ts +8 -1
- package/src/lib/components/console/quote-pay-link.component.js +29 -3
- package/src/lib/components/console/quote-pay-link.component.js.map +1 -1
- package/src/lib/components/console/quote-proofs.component.d.ts +11 -0
- package/src/lib/components/console/quote-proofs.component.js +68 -0
- package/src/lib/components/console/quote-proofs.component.js.map +1 -1
- package/src/lib/components/console/quote-send.component.d.ts +42 -0
- package/src/lib/components/console/quote-send.component.js +304 -0
- package/src/lib/components/console/quote-send.component.js.map +1 -0
- package/src/lib/components/console/quote-start.component.d.ts +45 -0
- package/src/lib/components/console/quote-start.component.js +182 -0
- package/src/lib/components/console/quote-start.component.js.map +1 -0
- package/src/lib/components/product-add-ons.d.ts +28 -0
- package/src/lib/components/product-add-ons.js +136 -0
- package/src/lib/components/product-add-ons.js.map +1 -0
- package/src/lib/components/product-detail.d.ts +16 -0
- package/src/lib/components/product-detail.js +112 -19
- package/src/lib/components/product-detail.js.map +1 -1
- package/src/lib/components/product-grid.d.ts +8 -2
- package/src/lib/components/product-grid.js +6 -1
- package/src/lib/components/product-grid.js.map +1 -1
- package/src/lib/components/product-options-field-body.d.ts +44 -0
- package/src/lib/components/product-options-field-body.js +260 -0
- package/src/lib/components/product-options-field-body.js.map +1 -0
- package/src/lib/components/product-options-field.d.ts +32 -0
- package/src/lib/components/product-options-field.js +128 -0
- package/src/lib/components/product-options-field.js.map +1 -0
- package/src/lib/components/quote-page-body.js +39 -20
- package/src/lib/components/quote-page-body.js.map +1 -1
- package/src/lib/model/commerce-cart.d.ts +15 -3
- package/src/lib/model/commerce-cart.js +10 -3
- package/src/lib/model/commerce-cart.js.map +1 -1
- package/src/lib/model/commerce-invoices.d.ts +139 -0
- package/src/lib/model/commerce-invoices.js +343 -0
- package/src/lib/model/commerce-invoices.js.map +1 -0
- package/src/lib/model/commerce-orders.d.ts +35 -5
- package/src/lib/model/commerce-orders.js +3 -0
- package/src/lib/model/commerce-orders.js.map +1 -1
- package/src/lib/model/commerce-pos-kiosk.d.ts +1 -1
- package/src/lib/model/commerce-pos-kiosk.js +3 -2
- package/src/lib/model/commerce-pos-kiosk.js.map +1 -1
- package/src/lib/model/commerce-product-field.d.ts +23 -0
- package/src/lib/model/commerce-product-field.js +26 -0
- package/src/lib/model/commerce-product-field.js.map +1 -0
- package/src/lib/model/commerce-quote-email.d.ts +81 -0
- package/src/lib/model/commerce-quote-email.js +170 -0
- package/src/lib/model/commerce-quote-email.js.map +1 -0
- package/src/lib/model/commerce-quote-pay.d.ts +49 -6
- package/src/lib/model/commerce-quote-pay.js +69 -36
- package/src/lib/model/commerce-quote-pay.js.map +1 -1
- package/src/lib/model/commerce-quote-request.d.ts +93 -0
- package/src/lib/model/commerce-quote-request.js +207 -0
- package/src/lib/model/commerce-quote-request.js.map +1 -0
- package/src/lib/model/commerce-quotes.d.ts +81 -0
- package/src/lib/model/commerce-quotes.js +1 -1
- package/src/lib/model/commerce-quotes.js.map +1 -1
- package/src/lib/model/commerce.d.ts +18 -0
- package/src/lib/model/commerce.js +26 -0
- package/src/lib/model/commerce.js.map +1 -1
- package/src/lib/model/index.d.ts +3 -0
- package/src/lib/model/index.js +3 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/model/product-modifiers.d.ts +74 -4
- package/src/lib/model/product-modifiers.js +131 -10
- package/src/lib/model/product-modifiers.js.map +1 -1
- package/src/lib/plugin.js +19 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/billing-webhook.js +164 -98
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/cart-checkout.js +14 -2
- package/src/lib/server/cart-checkout.js.map +1 -1
- package/src/lib/server/cart.d.ts +5 -0
- package/src/lib/server/cart.js +41 -4
- package/src/lib/server/cart.js.map +1 -1
- package/src/lib/server/catalog.d.ts +6 -0
- package/src/lib/server/catalog.js +2 -0
- package/src/lib/server/catalog.js.map +1 -1
- package/src/lib/server/checkout-extras.js +3 -1
- package/src/lib/server/checkout-extras.js.map +1 -1
- package/src/lib/server/checkout.js +28 -3
- package/src/lib/server/checkout.js.map +1 -1
- package/src/lib/server/commerce-pdf.d.ts +5 -1
- package/src/lib/server/commerce-pdf.js +37 -10
- package/src/lib/server/commerce-pdf.js.map +1 -1
- package/src/lib/server/draft-order.js +23 -3
- package/src/lib/server/draft-order.js.map +1 -1
- package/src/lib/server/invoice-email.d.ts +76 -0
- package/src/lib/server/invoice-email.js +183 -0
- package/src/lib/server/invoice-email.js.map +1 -0
- package/src/lib/server/local-fulfillment.js +5 -1
- package/src/lib/server/local-fulfillment.js.map +1 -1
- package/src/lib/server/order-invoice-send.d.ts +43 -0
- package/src/lib/server/order-invoice-send.js +218 -0
- package/src/lib/server/order-invoice-send.js.map +1 -0
- package/src/lib/server/order-status.d.ts +11 -0
- package/src/lib/server/order-status.js +44 -8
- package/src/lib/server/order-status.js.map +1 -1
- package/src/lib/server/person-eraser.d.ts +3 -2
- package/src/lib/server/person-eraser.js +25 -6
- package/src/lib/server/person-eraser.js.map +1 -1
- package/src/lib/server/pos-offline-sync.js +1 -1
- package/src/lib/server/pos-offline-sync.js.map +1 -1
- package/src/lib/server/pos-order.js +1 -1
- package/src/lib/server/pos-order.js.map +1 -1
- package/src/lib/server/product.d.ts +26 -0
- package/src/lib/server/product.js +18 -0
- package/src/lib/server/product.js.map +1 -1
- package/src/lib/server/products-write.js +5 -2
- package/src/lib/server/products-write.js.map +1 -1
- package/src/lib/server/quote-email.d.ts +85 -0
- package/src/lib/server/quote-email.js +211 -0
- package/src/lib/server/quote-email.js.map +1 -0
- package/src/lib/server/quote-link-token.d.ts +11 -0
- package/src/lib/server/quote-link-token.js +21 -0
- package/src/lib/server/quote-link-token.js.map +1 -1
- package/src/lib/server/quote-pay.d.ts +8 -3
- package/src/lib/server/quote-pay.js +105 -43
- package/src/lib/server/quote-pay.js.map +1 -1
- package/src/lib/server/quote.d.ts +11 -0
- package/src/lib/server/quote.js +309 -12
- package/src/lib/server/quote.js.map +1 -1
- package/src/lib/server/site-page-resolver.js +2 -1
- package/src/lib/server/site-page-resolver.js.map +1 -1
- package/src/lib/server.js +5 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/site.js +8 -0
- package/src/lib/site.js.map +1 -1
- package/src/lib/tenant-emails.js +243 -0
- package/src/lib/tenant-emails.js.map +1 -1
- package/src/lib/utils/add-to-cart.d.ts +8 -0
- package/src/lib/utils/add-to-cart.js +10 -2
- package/src/lib/utils/add-to-cart.js.map +1 -1
- package/src/lib/utils/commerce-document-pdf.d.ts +5 -0
- package/src/lib/utils/commerce-document-pdf.js +6 -1
- package/src/lib/utils/commerce-document-pdf.js.map +1 -1
- package/src/lib/utils/invoice-email-content.d.ts +45 -0
- package/src/lib/utils/invoice-email-content.js +126 -0
- package/src/lib/utils/invoice-email-content.js.map +1 -0
- package/src/lib/utils/order-invoice.d.ts +1 -1
- package/src/lib/utils/order-invoice.js +14 -6
- package/src/lib/utils/order-invoice.js.map +1 -1
- package/src/lib/utils/quote-email-content.d.ts +63 -0
- package/src/lib/utils/quote-email-content.js +157 -0
- package/src/lib/utils/quote-email-content.js.map +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/commerce-pos-kiosk.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { posTipFromPercent, posTipProblem, type PosReceiptChannel } from './commerce-pos'\nimport type { ModifierSelection, ProductModifierGroup } from './product-modifiers'\n\n/*==========================================\n * THE SELF-SERVICE KIOSK (AGL-3623).\n *\n * A register's second kind of paired screen: where the customer display\n * (AGL-3608) mirrors the cashier's basket, a kiosk lets the customer build\n * their own order — a product grid with categories and modifiers, a cart, a\n * tip, payment on the register's card reader or Tap to Pay, or \"pay at\n * counter\", which sends the order to the register's queue for a cashier.\n *\n * It pairs exactly like a display (the same six-digit code and the same\n * hashed device token, with `mode: 'kiosk'` on both), and it never holds a\n * staff session: every request carries the device token and the server\n * prices everything. These are the shapes the kiosk route and every client\n * of it — the web kiosk and the native POS app — agree on.\n *=========================================*/\n\n/** Which kind of paired screen a device token opens. */\nexport type PosDeviceMode = 'display' | 'kiosk'\n\n/** A product as the kiosk grid shows it: no cost, no stock count, no supplier. */\nexport interface PosKioskProduct {\n id: string\n name: string\n description?: string\n imageUrl?: string\n categoryIds: string[]\n /** The product's option axes (\"Size\": S, M, L), for the variant picker. */\n options: Array<{ name: string; values: string[] }>\n variants: PosKioskVariant[]\n modifierGroups: ProductModifierGroup[]\n}\n\nexport interface PosKioskVariant {\n id: string\n /** Option selections keyed by option name; empty for the default variant. */\n options: Record<string, string>\n priceCents: number\n /** A tracked variant at or below zero whose product refuses to oversell. */\n soldOut: boolean\n}\n\nexport interface PosKioskCategory {\n id: string\n name: string\n}\n\nexport interface PosKioskCatalog {\n products: PosKioskProduct[]\n categories: PosKioskCategory[]\n /** The store's currency, lower case (`usd`). */\n currency: string\n}\n\n/** What the kiosk offers, resolved on the server for one paired register. */\nexport interface PosKioskContext {\n branding: {\n name: string\n logoUrl: string | null\n logoDarkUrl: string | null\n message: string\n }\n currency: string\n tipping: { enabled: boolean; percentages: number[] }\n payments: {\n /** A Stripe Terminal smart reader is assigned to this register. */\n reader: boolean\n /** Card-present payments are switched on for the platform (Tap to Pay needs the native app too). */\n cardPresent: boolean\n /** \"Pay at counter\" sends the order to the register's queue. */\n payAtCounter: boolean\n }\n receipts: PosReceiptChannel[]\n offerMarketing: boolean\n /** Seconds without a touch before the kiosk asks \"Still there?\". */\n idleSeconds: number\n /** Stripe test mode: a test card can be tapped from the payment screen. */\n testMode: boolean\n}\n\n/** One cart line as the kiosk sends it: ids and a count, never a price. */\nexport interface PosKioskLine {\n productId: string\n variantId?: string\n quantity: number\n modifiers?: ModifierSelection[]\n}\n\n/** A line of a priced kiosk order, as the customer's screen shows it. */\nexport interface PosKioskSaleLine {\n name: string\n variantLabel?: string\n quantity: number\n amountCents: number\n}\n\nexport type PosKioskSaleStatus =\n /** Priced and waiting for a card at the kiosk. */\n | 'open'\n /** Sent to the register's queue; a cashier takes payment. */\n | 'queued'\n | 'paid'\n | 'voided'\n\n/** A kiosk order as its own screen reads it back. */\nexport interface PosKioskSale {\n orderId: string\n /** The order number the customer is called by. */\n number: number\n status: PosKioskSaleStatus\n lines: PosKioskSaleLine[]\n itemsCents: number\n discountCents: number\n taxCents: number\n totalCents: number\n tipCents: number\n paidCents: number\n dueCents: number\n /** The card payment in flight, if any. */\n payment: { id: string; status: string; failureMessage?: string } | null\n}\n\n/** Seconds without a touch before the kiosk asks \"Still there?\", by default. */\nexport const POS_KIOSK_IDLE_SECONDS_DEFAULT = 90\nexport const POS_KIOSK_IDLE_SECONDS_MIN = 30\nexport const POS_KIOSK_IDLE_SECONDS_MAX = 600\n\n/** How long \"Still there?\" waits before the kiosk clears itself. */\nexport const POS_KIOSK_IDLE_WARNING_SECONDS = 20\n\n/** The order-number screen returns to the start after this long. */\nexport const POS_KIOSK_DONE_SECONDS = 15\n\n/** Most lines one kiosk order carries. */\nexport const POS_KIOSK_MAX_LINES = 40\n\n/** Most of one line a customer may order at a kiosk. */\nexport const POS_KIOSK_MAX_QUANTITY = 20\n\n/** Most products the kiosk grid lists in one category. */\nexport const POS_KIOSK_CATALOG_LIMIT = 200\n\n/** Wrong staff PINs a kiosk takes before it locks its unlock screen. */\nexport const POS_KIOSK_UNLOCK_MAX_ATTEMPTS = 5\n\n/** How long a kiosk's unlock screen stays locked after too many wrong PINs. */\nexport const POS_KIOSK_UNLOCK_LOCKOUT_MS = 15 * 60 * 1000\n\n/** How long a staff unlock keeps the kiosk's settings open. */\nexport const POS_KIOSK_UNLOCK_TTL_MS = 5 * 60 * 1000\n\nconst ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/\n\n/**\n * The cart as the kiosk may send it, or why not. Ids are shape-checked and\n * quantities bounded here; everything else — whether the product exists, is\n * for sale, what it costs and whether its modifiers are valid — is the sale\n * route's to decide from the product documents.\n */\nexport function sanitizePosKioskLines(\n raw: unknown,\n): { ok: true; lines: PosKioskLine[] } | { ok: false; error: string } {\n if (!Array.isArray(raw) || raw.length === 0) return { ok: false, error: 'Your cart is empty.' }\n if (raw.length > POS_KIOSK_MAX_LINES) {\n return { ok: false, error: `An order can have at most ${POS_KIOSK_MAX_LINES} items.` }\n }\n const lines: PosKioskLine[] = []\n for (const entry of raw as Array<Record<string, unknown>>) {\n const productId = String(entry?.['productId'] ?? '')\n const variantId = entry?.['variantId'] == null ? '' : String(entry['variantId'])\n const quantity = Number(entry?.['quantity'])\n if (!ID_PATTERN.test(productId) || (variantId && !ID_PATTERN.test(variantId))) {\n return { ok: false, error: 'An item in your cart is no longer available.' }\n }\n if (!Number.isInteger(quantity) || quantity < 1 || quantity > POS_KIOSK_MAX_QUANTITY) {\n return { ok: false, error: `Order 1 to ${POS_KIOSK_MAX_QUANTITY} of each item.` }\n }\n const rawModifiers = Array.isArray(entry?.['modifiers']) ? (entry['modifiers'] as unknown[]) : []\n if (rawModifiers.length > 200) return { ok: false, error: 'Too many choices on one item.' }\n const modifiers: ModifierSelection[] = []\n for (const pick of rawModifiers as Array<Record<string, unknown>>) {\n const groupId = String(pick?.['groupId'] ?? '')\n const optionId = String(pick?.['optionId'] ?? '')\n if (!ID_PATTERN.test(groupId) || !ID_PATTERN.test(optionId)) {\n return { ok: false, error: 'A choice on an item is no longer offered.' }\n }\n modifiers.push({ groupId, optionId })\n }\n lines.push({\n productId,\n ...(variantId ? { variantId } : {}),\n quantity,\n ...(modifiers.length ? { modifiers } : {}),\n })\n }\n return { ok: true, lines }\n}\n\n/**\n * The tip a kiosk customer chose, recomputed from the server's own figure\n * for what is being paid: a percentage only from the store's presets, a\n * custom amount bounded like every register tip. Null when the choice is\n * not one the store offers.\n */\nexport function posKioskTipCents(\n choice: unknown,\n input: { baseCents: number; enabled: boolean; percentages: readonly number[]; percent?: unknown; cents?: unknown },\n): number | null {\n if (choice === undefined || choice === null || choice === 'none') return 0\n if (!input.enabled) return null\n if (choice === 'percent') {\n const percent = Math.round(Number(input.percent))\n if (!input.percentages.includes(percent)) return null\n return posTipFromPercent(input.baseCents, percent)\n }\n if (choice === 'custom') {\n const cents = Math.round(Number(input.cents))\n return posTipProblem(cents, input.baseCents) ? null : cents\n }\n return null\n}\n\n/** The idle timeout a merchant set, bounded. */\nexport function posKioskIdleSeconds(raw: unknown): number {\n const value = Math.round(Number(raw))\n if (!Number.isFinite(value) || value <= 0) return POS_KIOSK_IDLE_SECONDS_DEFAULT\n return Math.min(POS_KIOSK_IDLE_SECONDS_MAX, Math.max(POS_KIOSK_IDLE_SECONDS_MIN, value))\n}\n\n/**\n * The kiosk's estimate of its cart, from the catalog prices it was given —\n * shown while the customer shops and never sent. The order is priced again,\n * with tax, by the server when they check out.\n */\nexport function posKioskEstimateCents(\n lines: readonly PosKioskLine[],\n products: readonly PosKioskProduct[],\n): number {\n let total = 0\n for (const line of lines) {\n const product = products.find((candidate) => candidate.id === line.productId)\n if (!product) continue\n const variant =\n product.variants.find((candidate) => candidate.id === line.variantId) ?? product.variants[0]\n if (!variant) continue\n const extra = (line.modifiers ?? []).reduce((sum, pick) => {\n const group = product.modifierGroups.find((candidate) => candidate.id === pick.groupId)\n const option = group?.options.find((candidate) => candidate.id === pick.optionId)\n return sum + (option?.priceCents ?? 0)\n }, 0)\n total += (variant.priceCents + extra) * line.quantity\n }\n return total\n}\n"],"names":["posTipFromPercent","posTipProblem","POS_KIOSK_IDLE_SECONDS_DEFAULT","POS_KIOSK_IDLE_SECONDS_MIN","POS_KIOSK_IDLE_SECONDS_MAX","POS_KIOSK_IDLE_WARNING_SECONDS","POS_KIOSK_DONE_SECONDS","POS_KIOSK_MAX_LINES","POS_KIOSK_MAX_QUANTITY","POS_KIOSK_CATALOG_LIMIT","POS_KIOSK_UNLOCK_MAX_ATTEMPTS","POS_KIOSK_UNLOCK_LOCKOUT_MS","POS_KIOSK_UNLOCK_TTL_MS","ID_PATTERN","sanitizePosKioskLines","raw","Array","isArray","length","ok","error","lines","entry","productId","String","variantId","quantity","Number","test","isInteger","rawModifiers","modifiers","pick","groupId","optionId","push","posKioskTipCents","choice","input","undefined","enabled","percent","Math","round","percentages","includes","baseCents","cents","posKioskIdleSeconds","value","isFinite","min","max","posKioskEstimateCents","products","total","line","product","find","candidate","id","variant","variants","extra","reduce","sum","group","modifierGroups","option","options","priceCents"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/commerce-pos-kiosk.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { posTipFromPercent, posTipProblem, type PosReceiptChannel } from './commerce-pos'\nimport { modifierOptionPriceCents, type ModifierSelection, type ProductModifierGroup } from './product-modifiers'\n\n/*==========================================\n * THE SELF-SERVICE KIOSK (AGL-3623).\n *\n * A register's second kind of paired screen: where the customer display\n * (AGL-3608) mirrors the cashier's basket, a kiosk lets the customer build\n * their own order — a product grid with categories and modifiers, a cart, a\n * tip, payment on the register's card reader or Tap to Pay, or \"pay at\n * counter\", which sends the order to the register's queue for a cashier.\n *\n * It pairs exactly like a display (the same six-digit code and the same\n * hashed device token, with `mode: 'kiosk'` on both), and it never holds a\n * staff session: every request carries the device token and the server\n * prices everything. These are the shapes the kiosk route and every client\n * of it — the web kiosk and the native POS app — agree on.\n *=========================================*/\n\n/** Which kind of paired screen a device token opens. */\nexport type PosDeviceMode = 'display' | 'kiosk'\n\n/** A product as the kiosk grid shows it: no cost, no stock count, no supplier. */\nexport interface PosKioskProduct {\n id: string\n name: string\n description?: string\n imageUrl?: string\n categoryIds: string[]\n /** The product's option axes (\"Size\": S, M, L), for the variant picker. */\n options: Array<{ name: string; values: string[] }>\n variants: PosKioskVariant[]\n modifierGroups: ProductModifierGroup[]\n}\n\nexport interface PosKioskVariant {\n id: string\n /** Option selections keyed by option name; empty for the default variant. */\n options: Record<string, string>\n priceCents: number\n /** A tracked variant at or below zero whose product refuses to oversell. */\n soldOut: boolean\n}\n\nexport interface PosKioskCategory {\n id: string\n name: string\n}\n\nexport interface PosKioskCatalog {\n products: PosKioskProduct[]\n categories: PosKioskCategory[]\n /** The store's currency, lower case (`usd`). */\n currency: string\n}\n\n/** What the kiosk offers, resolved on the server for one paired register. */\nexport interface PosKioskContext {\n branding: {\n name: string\n logoUrl: string | null\n logoDarkUrl: string | null\n message: string\n }\n currency: string\n tipping: { enabled: boolean; percentages: number[] }\n payments: {\n /** A Stripe Terminal smart reader is assigned to this register. */\n reader: boolean\n /** Card-present payments are switched on for the platform (Tap to Pay needs the native app too). */\n cardPresent: boolean\n /** \"Pay at counter\" sends the order to the register's queue. */\n payAtCounter: boolean\n }\n receipts: PosReceiptChannel[]\n offerMarketing: boolean\n /** Seconds without a touch before the kiosk asks \"Still there?\". */\n idleSeconds: number\n /** Stripe test mode: a test card can be tapped from the payment screen. */\n testMode: boolean\n}\n\n/** One cart line as the kiosk sends it: ids and a count, never a price. */\nexport interface PosKioskLine {\n productId: string\n variantId?: string\n quantity: number\n modifiers?: ModifierSelection[]\n}\n\n/** A line of a priced kiosk order, as the customer's screen shows it. */\nexport interface PosKioskSaleLine {\n name: string\n variantLabel?: string\n quantity: number\n amountCents: number\n}\n\nexport type PosKioskSaleStatus =\n /** Priced and waiting for a card at the kiosk. */\n | 'open'\n /** Sent to the register's queue; a cashier takes payment. */\n | 'queued'\n | 'paid'\n | 'voided'\n\n/** A kiosk order as its own screen reads it back. */\nexport interface PosKioskSale {\n orderId: string\n /** The order number the customer is called by. */\n number: number\n status: PosKioskSaleStatus\n lines: PosKioskSaleLine[]\n itemsCents: number\n discountCents: number\n taxCents: number\n totalCents: number\n tipCents: number\n paidCents: number\n dueCents: number\n /** The card payment in flight, if any. */\n payment: { id: string; status: string; failureMessage?: string } | null\n}\n\n/** Seconds without a touch before the kiosk asks \"Still there?\", by default. */\nexport const POS_KIOSK_IDLE_SECONDS_DEFAULT = 90\nexport const POS_KIOSK_IDLE_SECONDS_MIN = 30\nexport const POS_KIOSK_IDLE_SECONDS_MAX = 600\n\n/** How long \"Still there?\" waits before the kiosk clears itself. */\nexport const POS_KIOSK_IDLE_WARNING_SECONDS = 20\n\n/** The order-number screen returns to the start after this long. */\nexport const POS_KIOSK_DONE_SECONDS = 15\n\n/** Most lines one kiosk order carries. */\nexport const POS_KIOSK_MAX_LINES = 40\n\n/** Most of one line a customer may order at a kiosk. */\nexport const POS_KIOSK_MAX_QUANTITY = 20\n\n/** Most products the kiosk grid lists in one category. */\nexport const POS_KIOSK_CATALOG_LIMIT = 200\n\n/** Wrong staff PINs a kiosk takes before it locks its unlock screen. */\nexport const POS_KIOSK_UNLOCK_MAX_ATTEMPTS = 5\n\n/** How long a kiosk's unlock screen stays locked after too many wrong PINs. */\nexport const POS_KIOSK_UNLOCK_LOCKOUT_MS = 15 * 60 * 1000\n\n/** How long a staff unlock keeps the kiosk's settings open. */\nexport const POS_KIOSK_UNLOCK_TTL_MS = 5 * 60 * 1000\n\nconst ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/\n\n/**\n * The cart as the kiosk may send it, or why not. Ids are shape-checked and\n * quantities bounded here; everything else — whether the product exists, is\n * for sale, what it costs and whether its modifiers are valid — is the sale\n * route's to decide from the product documents.\n */\nexport function sanitizePosKioskLines(\n raw: unknown,\n): { ok: true; lines: PosKioskLine[] } | { ok: false; error: string } {\n if (!Array.isArray(raw) || raw.length === 0) return { ok: false, error: 'Your cart is empty.' }\n if (raw.length > POS_KIOSK_MAX_LINES) {\n return { ok: false, error: `An order can have at most ${POS_KIOSK_MAX_LINES} items.` }\n }\n const lines: PosKioskLine[] = []\n for (const entry of raw as Array<Record<string, unknown>>) {\n const productId = String(entry?.['productId'] ?? '')\n const variantId = entry?.['variantId'] == null ? '' : String(entry['variantId'])\n const quantity = Number(entry?.['quantity'])\n if (!ID_PATTERN.test(productId) || (variantId && !ID_PATTERN.test(variantId))) {\n return { ok: false, error: 'An item in your cart is no longer available.' }\n }\n if (!Number.isInteger(quantity) || quantity < 1 || quantity > POS_KIOSK_MAX_QUANTITY) {\n return { ok: false, error: `Order 1 to ${POS_KIOSK_MAX_QUANTITY} of each item.` }\n }\n const rawModifiers = Array.isArray(entry?.['modifiers']) ? (entry['modifiers'] as unknown[]) : []\n if (rawModifiers.length > 200) return { ok: false, error: 'Too many choices on one item.' }\n const modifiers: ModifierSelection[] = []\n for (const pick of rawModifiers as Array<Record<string, unknown>>) {\n const groupId = String(pick?.['groupId'] ?? '')\n const optionId = String(pick?.['optionId'] ?? '')\n if (!ID_PATTERN.test(groupId) || !ID_PATTERN.test(optionId)) {\n return { ok: false, error: 'A choice on an item is no longer offered.' }\n }\n modifiers.push({ groupId, optionId })\n }\n lines.push({\n productId,\n ...(variantId ? { variantId } : {}),\n quantity,\n ...(modifiers.length ? { modifiers } : {}),\n })\n }\n return { ok: true, lines }\n}\n\n/**\n * The tip a kiosk customer chose, recomputed from the server's own figure\n * for what is being paid: a percentage only from the store's presets, a\n * custom amount bounded like every register tip. Null when the choice is\n * not one the store offers.\n */\nexport function posKioskTipCents(\n choice: unknown,\n input: { baseCents: number; enabled: boolean; percentages: readonly number[]; percent?: unknown; cents?: unknown },\n): number | null {\n if (choice === undefined || choice === null || choice === 'none') return 0\n if (!input.enabled) return null\n if (choice === 'percent') {\n const percent = Math.round(Number(input.percent))\n if (!input.percentages.includes(percent)) return null\n return posTipFromPercent(input.baseCents, percent)\n }\n if (choice === 'custom') {\n const cents = Math.round(Number(input.cents))\n return posTipProblem(cents, input.baseCents) ? null : cents\n }\n return null\n}\n\n/** The idle timeout a merchant set, bounded. */\nexport function posKioskIdleSeconds(raw: unknown): number {\n const value = Math.round(Number(raw))\n if (!Number.isFinite(value) || value <= 0) return POS_KIOSK_IDLE_SECONDS_DEFAULT\n return Math.min(POS_KIOSK_IDLE_SECONDS_MAX, Math.max(POS_KIOSK_IDLE_SECONDS_MIN, value))\n}\n\n/**\n * The kiosk's estimate of its cart, from the catalog prices it was given —\n * shown while the customer shops and never sent. The order is priced again,\n * with tax, by the server when they check out.\n */\nexport function posKioskEstimateCents(\n lines: readonly PosKioskLine[],\n products: readonly PosKioskProduct[],\n): number {\n let total = 0\n for (const line of lines) {\n const product = products.find((candidate) => candidate.id === line.productId)\n if (!product) continue\n const variant =\n product.variants.find((candidate) => candidate.id === line.variantId) ?? product.variants[0]\n if (!variant) continue\n const extra = (line.modifiers ?? []).reduce((sum, pick) => {\n const group = product.modifierGroups.find((candidate) => candidate.id === pick.groupId)\n const option = group?.options.find((candidate) => candidate.id === pick.optionId)\n // A choice priced by variant adds its price on this one (AGL-4100).\n return sum + (option ? modifierOptionPriceCents(option, variant.id) : 0)\n }, 0)\n total += (variant.priceCents + extra) * line.quantity\n }\n return total\n}\n"],"names":["posTipFromPercent","posTipProblem","modifierOptionPriceCents","POS_KIOSK_IDLE_SECONDS_DEFAULT","POS_KIOSK_IDLE_SECONDS_MIN","POS_KIOSK_IDLE_SECONDS_MAX","POS_KIOSK_IDLE_WARNING_SECONDS","POS_KIOSK_DONE_SECONDS","POS_KIOSK_MAX_LINES","POS_KIOSK_MAX_QUANTITY","POS_KIOSK_CATALOG_LIMIT","POS_KIOSK_UNLOCK_MAX_ATTEMPTS","POS_KIOSK_UNLOCK_LOCKOUT_MS","POS_KIOSK_UNLOCK_TTL_MS","ID_PATTERN","sanitizePosKioskLines","raw","Array","isArray","length","ok","error","lines","entry","productId","String","variantId","quantity","Number","test","isInteger","rawModifiers","modifiers","pick","groupId","optionId","push","posKioskTipCents","choice","input","undefined","enabled","percent","Math","round","percentages","includes","baseCents","cents","posKioskIdleSeconds","value","isFinite","min","max","posKioskEstimateCents","products","total","line","product","find","candidate","id","variant","variants","extra","reduce","sum","group","modifierGroups","option","options","priceCents"],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The key the Product and options field posts its exact choice under, beside
|
|
19
|
+
* its readable answer (AGL-4098): `product__data` for a field named
|
|
20
|
+
* `product`. The suffix is the core's form protocol, so the forms plugin keeps
|
|
21
|
+
* it apart from the answers without knowing what a product is.
|
|
22
|
+
*/
|
|
23
|
+
export declare function formFieldDataKey(field: string): string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { FORM_FIELD_DATA_SUFFIX } from "@aglyn/aglyn/app-utils/forms";
|
|
17
|
+
/**
|
|
18
|
+
* The key the Product and options field posts its exact choice under, beside
|
|
19
|
+
* its readable answer (AGL-4098): `product__data` for a field named
|
|
20
|
+
* `product`. The suffix is the core's form protocol, so the forms plugin keeps
|
|
21
|
+
* it apart from the answers without knowing what a product is.
|
|
22
|
+
*/ export function formFieldDataKey(field) {
|
|
23
|
+
return `${field}${FORM_FIELD_DATA_SUFFIX}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=commerce-product-field.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/commerce-product-field.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { FORM_FIELD_DATA_SUFFIX } from '@aglyn/aglyn/app-utils/forms'\n\n/**\n * The key the Product and options field posts its exact choice under, beside\n * its readable answer (AGL-4098): `product__data` for a field named\n * `product`. The suffix is the core's form protocol, so the forms plugin keeps\n * it apart from the answers without knowing what a product is.\n */\nexport function formFieldDataKey(field: string): string {\n return `${field}${FORM_FIELD_DATA_SUFFIX}`\n}\n"],"names":["FORM_FIELD_DATA_SUFFIX","formFieldDataKey","field"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,sBAAsB,QAAQ,+BAA8B;AAErE;;;;;CAKC,GACD,OAAO,SAASC,iBAAiBC,KAAa;IAC5C,OAAO,GAAGA,QAAQF,wBAAwB;AAC5C"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type HostOrder } from './commerce-orders';
|
|
18
|
+
import { type OrderQuote, type QuoteEmailThread, type QuoteSendRecord } from './commerce-quotes';
|
|
19
|
+
/** The most sends a quote keeps on record; the oldest go first. */
|
|
20
|
+
export declare const QUOTE_MAX_SENDS = 50;
|
|
21
|
+
/** The longest message a send carries. */
|
|
22
|
+
export declare const QUOTE_MAX_SEND_MESSAGE = 5000;
|
|
23
|
+
/** The longest subject a send carries. */
|
|
24
|
+
export declare const QUOTE_MAX_SEND_SUBJECT = 200;
|
|
25
|
+
/** The quote's sends on record, oldest first. */
|
|
26
|
+
export declare function quoteSends(quote: Pick<OrderQuote, 'sends'> | null | undefined): QuoteSendRecord[];
|
|
27
|
+
/** The last time the quote was emailed, or null. */
|
|
28
|
+
export declare function lastQuoteSend(quote: Pick<OrderQuote, 'sends'> | null | undefined): QuoteSendRecord | null;
|
|
29
|
+
/**
|
|
30
|
+
* Whether emailing the quote now hands the customer a new revision of a quote
|
|
31
|
+
* they were already emailed: an earlier revision went out, and this one has
|
|
32
|
+
* not yet. The quote's first email (whatever revision it is at, since
|
|
33
|
+
* revisions nobody was sent replace nothing) and a resend of the revision
|
|
34
|
+
* already sent are neither, so their email does not call itself an update.
|
|
35
|
+
*/
|
|
36
|
+
export declare function quoteSendIsNewRevision(quote: Pick<OrderQuote, 'sends' | 'revision'> | null | undefined): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Whether emailing the quote now gives the customer a new pay link: they were
|
|
39
|
+
* emailed an earlier revision, and the link has not been replaced since.
|
|
40
|
+
*/
|
|
41
|
+
export declare function quoteSendReplacesLink(quote: Pick<OrderQuote, 'sends' | 'revision' | 'linkVersion'> | null | undefined): boolean;
|
|
42
|
+
/** What the console's Send button says: `Send quote`, `Send again`, or `Send revision 2`. */
|
|
43
|
+
export declare function quoteSendLabel(quote: Pick<OrderQuote, 'sends' | 'revision'> | null | undefined): string;
|
|
44
|
+
/** Where the quote goes: what was typed in the dialog, else the customer's email. */
|
|
45
|
+
export declare function quoteSendRecipient(order: Pick<HostOrder, 'quote'> | null | undefined, typed: string | null | undefined): string;
|
|
46
|
+
/**
|
|
47
|
+
* Who the console's send form starts addressed to: the customer's email, else
|
|
48
|
+
* where the quote was last emailed, so Send again and Send revision N go
|
|
49
|
+
* where the last one went when the quote names no email.
|
|
50
|
+
*/
|
|
51
|
+
export declare function quoteSendPrefill(order: Pick<HostOrder, 'quote'> | null | undefined): string;
|
|
52
|
+
export type QuoteSendRefusal = 'not-a-quote' | 'paid' | 'no-address' | 'bad-address';
|
|
53
|
+
/** Why the quote cannot be emailed to `to` now, if it cannot. */
|
|
54
|
+
export declare function quoteSendRefusal(order: Pick<HostOrder, 'channel' | 'status' | 'quote'> | null | undefined, to: string): QuoteSendRefusal | null;
|
|
55
|
+
export declare function quoteSendRefusalMessage(reason: QuoteSendRefusal): string;
|
|
56
|
+
/**
|
|
57
|
+
* The subject a send defaults to: the conversation's, so it stays one
|
|
58
|
+
* conversation, else `Quote #1042 from Acme Signs`.
|
|
59
|
+
*/
|
|
60
|
+
export declare function quoteEmailDefaultSubject(order: Pick<HostOrder, 'number' | 'quote'>, businessName: string, quoteId?: string): string;
|
|
61
|
+
/** What the console sends: where, under what subject, and the business's message. */
|
|
62
|
+
export interface QuoteSendInput {
|
|
63
|
+
to: string;
|
|
64
|
+
subject: string;
|
|
65
|
+
message: string;
|
|
66
|
+
}
|
|
67
|
+
/** A send as the route received it, trimmed, or what to fix. `to` and `subject` may be empty: the defaults fill them. */
|
|
68
|
+
export declare function parseQuoteSendInput(raw: unknown): {
|
|
69
|
+
ok: true;
|
|
70
|
+
input: QuoteSendInput;
|
|
71
|
+
} | {
|
|
72
|
+
ok: false;
|
|
73
|
+
error: string;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* The quote once an email went out: the send on record, the conversation
|
|
77
|
+
* moved on, a line on the timeline, and the quote at Sent, unless the
|
|
78
|
+
* customer has already answered this revision (approved it, or asked for
|
|
79
|
+
* changes), which stays what it says.
|
|
80
|
+
*/
|
|
81
|
+
export declare function quoteAfterSend(order: HostOrder, send: QuoteSendRecord, thread: QuoteEmailThread): HostOrder;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { appendOrderEvent } from "./commerce-orders.js";
|
|
18
|
+
import { quoteLinkVersion } from "./commerce-quote-pay.js";
|
|
19
|
+
import { latestQuoteResponse } from "./commerce-quote-proofs.js";
|
|
20
|
+
import { formatQuoteNumber, isQuote, isUnpaidQuote } from "./commerce-quotes.js";
|
|
21
|
+
/*==========================================
|
|
22
|
+
* EMAILING A QUOTE TO THE CUSTOMER (AGL-4097).
|
|
23
|
+
*
|
|
24
|
+
* The business sends the quote from the console: one email in the site's
|
|
25
|
+
* look with its message, the proof, the PDF and the pay button, in the
|
|
26
|
+
* customer's existing conversation when there is one. Each send is recorded
|
|
27
|
+
* on the quote (`quote.sends`) with the revision it carried and the pay link
|
|
28
|
+
* version it carried, and the conversation is kept as `quote.emailThread`.
|
|
29
|
+
*
|
|
30
|
+
* THE PAY LINK ON A NEW REVISION. When the customer was emailed an earlier
|
|
31
|
+
* revision, the first email of a new revision gives them a new pay link and
|
|
32
|
+
* retires the old one (slice 2's Replace link), so the link in the earlier
|
|
33
|
+
* email opens a page saying the quote was updated. A resend of the same
|
|
34
|
+
* revision keeps the link. "Was the link already replaced since that email"
|
|
35
|
+
* is read off the record (the link version it carried), which is also what
|
|
36
|
+
* keeps a send that failed after replacing the link from replacing it again
|
|
37
|
+
* when it is retried.
|
|
38
|
+
*
|
|
39
|
+
* Pure: the console's Send button and the server's send action read the
|
|
40
|
+
* same answers.
|
|
41
|
+
*=========================================*/ /** The most sends a quote keeps on record; the oldest go first. */ export const QUOTE_MAX_SENDS = 50;
|
|
42
|
+
/** The longest message a send carries. */ export const QUOTE_MAX_SEND_MESSAGE = 5000;
|
|
43
|
+
/** The longest subject a send carries. */ export const QUOTE_MAX_SEND_SUBJECT = 200;
|
|
44
|
+
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
45
|
+
/** The quote's sends on record, oldest first. */ export function quoteSends(quote) {
|
|
46
|
+
return Array.isArray(quote == null ? void 0 : quote.sends) ? quote.sends.filter((send)=>send && typeof send === 'object') : [];
|
|
47
|
+
}
|
|
48
|
+
/** The last time the quote was emailed, or null. */ export function lastQuoteSend(quote) {
|
|
49
|
+
const sends = quoteSends(quote);
|
|
50
|
+
return sends.length ? sends[sends.length - 1] : null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Whether emailing the quote now hands the customer a new revision of a quote
|
|
54
|
+
* they were already emailed: an earlier revision went out, and this one has
|
|
55
|
+
* not yet. The quote's first email (whatever revision it is at, since
|
|
56
|
+
* revisions nobody was sent replace nothing) and a resend of the revision
|
|
57
|
+
* already sent are neither, so their email does not call itself an update.
|
|
58
|
+
*/ export function quoteSendIsNewRevision(quote) {
|
|
59
|
+
var _ref;
|
|
60
|
+
const last = lastQuoteSend(quote);
|
|
61
|
+
return Boolean(last) && Number(last.revision) < Number((_ref = quote == null ? void 0 : quote.revision) != null ? _ref : 1);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Whether emailing the quote now gives the customer a new pay link: they were
|
|
65
|
+
* emailed an earlier revision, and the link has not been replaced since.
|
|
66
|
+
*/ export function quoteSendReplacesLink(quote) {
|
|
67
|
+
var _lastQuoteSend;
|
|
68
|
+
if (!quoteSendIsNewRevision(quote)) return false;
|
|
69
|
+
return Number((_lastQuoteSend = lastQuoteSend(quote)) == null ? void 0 : _lastQuoteSend.linkVersion) === quoteLinkVersion(quote);
|
|
70
|
+
}
|
|
71
|
+
/** What the console's Send button says: `Send quote`, `Send again`, or `Send revision 2`. */ export function quoteSendLabel(quote) {
|
|
72
|
+
var _ref;
|
|
73
|
+
if (!lastQuoteSend(quote)) return 'Send quote';
|
|
74
|
+
return quoteSendIsNewRevision(quote) ? `Send revision ${Number((_ref = quote == null ? void 0 : quote.revision) != null ? _ref : 1)}` : 'Send again';
|
|
75
|
+
}
|
|
76
|
+
/** Where the quote goes: what was typed in the dialog, else the customer's email. */ export function quoteSendRecipient(order, typed) {
|
|
77
|
+
var _ref;
|
|
78
|
+
var _order_quote_customer, _order_quote;
|
|
79
|
+
return String(typed != null ? typed : '').trim() || String((_ref = order == null ? void 0 : (_order_quote = order.quote) == null ? void 0 : (_order_quote_customer = _order_quote.customer) == null ? void 0 : _order_quote_customer.email) != null ? _ref : '').trim();
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Who the console's send form starts addressed to: the customer's email, else
|
|
83
|
+
* where the quote was last emailed, so Send again and Send revision N go
|
|
84
|
+
* where the last one went when the quote names no email.
|
|
85
|
+
*/ export function quoteSendPrefill(order) {
|
|
86
|
+
var _ref;
|
|
87
|
+
var _lastQuoteSend;
|
|
88
|
+
return quoteSendRecipient(order, null) || String((_ref = (_lastQuoteSend = lastQuoteSend(order == null ? void 0 : order.quote)) == null ? void 0 : _lastQuoteSend.to) != null ? _ref : '').trim();
|
|
89
|
+
}
|
|
90
|
+
/** Why the quote cannot be emailed to `to` now, if it cannot. */ export function quoteSendRefusal(order, to) {
|
|
91
|
+
if (!order || !isQuote(order) || !order.quote) return 'not-a-quote';
|
|
92
|
+
if (!isUnpaidQuote(order)) return 'paid';
|
|
93
|
+
if (!to) return 'no-address';
|
|
94
|
+
if (to.length > 320 || !EMAIL.test(to)) return 'bad-address';
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
export function quoteSendRefusalMessage(reason) {
|
|
98
|
+
switch(reason){
|
|
99
|
+
case 'not-a-quote':
|
|
100
|
+
return 'Unknown quote';
|
|
101
|
+
case 'paid':
|
|
102
|
+
return 'This quote has been paid, so it is an order now and is not sent again.';
|
|
103
|
+
case 'no-address':
|
|
104
|
+
return 'This quote has no email for the customer. Add one to the quote, or type where to send it.';
|
|
105
|
+
case 'bad-address':
|
|
106
|
+
return 'Enter a valid email address to send the quote to.';
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The subject a send defaults to: the conversation's, so it stays one
|
|
111
|
+
* conversation, else `Quote #1042 from Acme Signs`.
|
|
112
|
+
*/ export function quoteEmailDefaultSubject(order, businessName, quoteId) {
|
|
113
|
+
var _ref;
|
|
114
|
+
var _order_quote_emailThread, _order_quote;
|
|
115
|
+
const thread = String((_ref = (_order_quote = order.quote) == null ? void 0 : (_order_quote_emailThread = _order_quote.emailThread) == null ? void 0 : _order_quote_emailThread.subject) != null ? _ref : '').trim();
|
|
116
|
+
if (thread) return thread;
|
|
117
|
+
const name = String(businessName != null ? businessName : '').trim();
|
|
118
|
+
const number = formatQuoteNumber(order, quoteId);
|
|
119
|
+
return name ? `${number} from ${name}` : number;
|
|
120
|
+
}
|
|
121
|
+
/** A send as the route received it, trimmed, or what to fix. `to` and `subject` may be empty: the defaults fill them. */ export function parseQuoteSendInput(raw) {
|
|
122
|
+
const source = raw && typeof raw === 'object' ? raw : {};
|
|
123
|
+
const text = (value)=>typeof value === 'string' ? value : '';
|
|
124
|
+
const to = text(source['to']).trim();
|
|
125
|
+
// A subject is one header line.
|
|
126
|
+
const subject = text(source['subject']).replace(/[\r\n]+/g, ' ').trim();
|
|
127
|
+
const message = text(source['message']).replace(/\r\n?/g, '\n').trim();
|
|
128
|
+
if (subject.length > QUOTE_MAX_SEND_SUBJECT) {
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
error: `Keep the subject under ${QUOTE_MAX_SEND_SUBJECT} characters.`
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (message.length > QUOTE_MAX_SEND_MESSAGE) {
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
error: `Keep the message under ${QUOTE_MAX_SEND_MESSAGE.toLocaleString('en-US')} characters.`
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
ok: true,
|
|
142
|
+
input: {
|
|
143
|
+
to,
|
|
144
|
+
subject,
|
|
145
|
+
message
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* The quote once an email went out: the send on record, the conversation
|
|
151
|
+
* moved on, a line on the timeline, and the quote at Sent, unless the
|
|
152
|
+
* customer has already answered this revision (approved it, or asked for
|
|
153
|
+
* changes), which stays what it says.
|
|
154
|
+
*/ export function quoteAfterSend(order, send, thread) {
|
|
155
|
+
const quote = order.quote;
|
|
156
|
+
const answered = latestQuoteResponse(quote) !== null;
|
|
157
|
+
return _extends({}, order, {
|
|
158
|
+
timeline: appendOrderEvent(order, 'quote-sent', `Revision ${send.revision} emailed to ${send.to} by ${send.by}`, send.atMs),
|
|
159
|
+
quote: _extends({}, quote, {
|
|
160
|
+
stage: answered ? quote.stage : 'sent',
|
|
161
|
+
sends: [
|
|
162
|
+
...quoteSends(quote),
|
|
163
|
+
send
|
|
164
|
+
].slice(-QUOTE_MAX_SENDS),
|
|
165
|
+
emailThread: thread
|
|
166
|
+
})
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
//# sourceMappingURL=commerce-quote-email.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/commerce-quote-email.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { appendOrderEvent, type HostOrder } from './commerce-orders'\nimport { quoteLinkVersion } from './commerce-quote-pay'\nimport { latestQuoteResponse } from './commerce-quote-proofs'\nimport {\n formatQuoteNumber,\n isQuote,\n isUnpaidQuote,\n type OrderQuote,\n type QuoteEmailThread,\n type QuoteSendRecord,\n} from './commerce-quotes'\n\n/*==========================================\n * EMAILING A QUOTE TO THE CUSTOMER (AGL-4097).\n *\n * The business sends the quote from the console: one email in the site's\n * look with its message, the proof, the PDF and the pay button, in the\n * customer's existing conversation when there is one. Each send is recorded\n * on the quote (`quote.sends`) with the revision it carried and the pay link\n * version it carried, and the conversation is kept as `quote.emailThread`.\n *\n * THE PAY LINK ON A NEW REVISION. When the customer was emailed an earlier\n * revision, the first email of a new revision gives them a new pay link and\n * retires the old one (slice 2's Replace link), so the link in the earlier\n * email opens a page saying the quote was updated. A resend of the same\n * revision keeps the link. \"Was the link already replaced since that email\"\n * is read off the record (the link version it carried), which is also what\n * keeps a send that failed after replacing the link from replacing it again\n * when it is retried.\n *\n * Pure: the console's Send button and the server's send action read the\n * same answers.\n *=========================================*/\n\n/** The most sends a quote keeps on record; the oldest go first. */\nexport const QUOTE_MAX_SENDS = 50\n\n/** The longest message a send carries. */\nexport const QUOTE_MAX_SEND_MESSAGE = 5_000\n\n/** The longest subject a send carries. */\nexport const QUOTE_MAX_SEND_SUBJECT = 200\n\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n/** The quote's sends on record, oldest first. */\nexport function quoteSends(quote: Pick<OrderQuote, 'sends'> | null | undefined): QuoteSendRecord[] {\n return Array.isArray(quote?.sends) ? quote.sends.filter((send) => send && typeof send === 'object') : []\n}\n\n/** The last time the quote was emailed, or null. */\nexport function lastQuoteSend(quote: Pick<OrderQuote, 'sends'> | null | undefined): QuoteSendRecord | null {\n const sends = quoteSends(quote)\n return sends.length ? sends[sends.length - 1] : null\n}\n\n/**\n * Whether emailing the quote now hands the customer a new revision of a quote\n * they were already emailed: an earlier revision went out, and this one has\n * not yet. The quote's first email (whatever revision it is at, since\n * revisions nobody was sent replace nothing) and a resend of the revision\n * already sent are neither, so their email does not call itself an update.\n */\nexport function quoteSendIsNewRevision(quote: Pick<OrderQuote, 'sends' | 'revision'> | null | undefined): boolean {\n const last = lastQuoteSend(quote)\n return Boolean(last) && Number(last.revision) < Number(quote?.revision ?? 1)\n}\n\n/**\n * Whether emailing the quote now gives the customer a new pay link: they were\n * emailed an earlier revision, and the link has not been replaced since.\n */\nexport function quoteSendReplacesLink(\n quote: Pick<OrderQuote, 'sends' | 'revision' | 'linkVersion'> | null | undefined,\n): boolean {\n if (!quoteSendIsNewRevision(quote)) return false\n return Number(lastQuoteSend(quote)?.linkVersion) === quoteLinkVersion(quote)\n}\n\n/** What the console's Send button says: `Send quote`, `Send again`, or `Send revision 2`. */\nexport function quoteSendLabel(quote: Pick<OrderQuote, 'sends' | 'revision'> | null | undefined): string {\n if (!lastQuoteSend(quote)) return 'Send quote'\n return quoteSendIsNewRevision(quote) ? `Send revision ${Number(quote?.revision ?? 1)}` : 'Send again'\n}\n\n/** Where the quote goes: what was typed in the dialog, else the customer's email. */\nexport function quoteSendRecipient(\n order: Pick<HostOrder, 'quote'> | null | undefined,\n typed: string | null | undefined,\n): string {\n return String(typed ?? '').trim() || String(order?.quote?.customer?.email ?? '').trim()\n}\n\n/**\n * Who the console's send form starts addressed to: the customer's email, else\n * where the quote was last emailed, so Send again and Send revision N go\n * where the last one went when the quote names no email.\n */\nexport function quoteSendPrefill(order: Pick<HostOrder, 'quote'> | null | undefined): string {\n return quoteSendRecipient(order, null) || String(lastQuoteSend(order?.quote)?.to ?? '').trim()\n}\n\nexport type QuoteSendRefusal = 'not-a-quote' | 'paid' | 'no-address' | 'bad-address'\n\n/** Why the quote cannot be emailed to `to` now, if it cannot. */\nexport function quoteSendRefusal(\n order: Pick<HostOrder, 'channel' | 'status' | 'quote'> | null | undefined,\n to: string,\n): QuoteSendRefusal | null {\n if (!order || !isQuote(order) || !order.quote) return 'not-a-quote'\n if (!isUnpaidQuote(order)) return 'paid'\n if (!to) return 'no-address'\n if (to.length > 320 || !EMAIL.test(to)) return 'bad-address'\n return null\n}\n\nexport function quoteSendRefusalMessage(reason: QuoteSendRefusal): string {\n switch (reason) {\n case 'not-a-quote':\n return 'Unknown quote'\n case 'paid':\n return 'This quote has been paid, so it is an order now and is not sent again.'\n case 'no-address':\n return 'This quote has no email for the customer. Add one to the quote, or type where to send it.'\n case 'bad-address':\n return 'Enter a valid email address to send the quote to.'\n }\n}\n\n/**\n * The subject a send defaults to: the conversation's, so it stays one\n * conversation, else `Quote #1042 from Acme Signs`.\n */\nexport function quoteEmailDefaultSubject(\n order: Pick<HostOrder, 'number' | 'quote'>,\n businessName: string,\n quoteId?: string,\n): string {\n const thread = String(order.quote?.emailThread?.subject ?? '').trim()\n if (thread) return thread\n const name = String(businessName ?? '').trim()\n const number = formatQuoteNumber(order, quoteId)\n return name ? `${number} from ${name}` : number\n}\n\n/** What the console sends: where, under what subject, and the business's message. */\nexport interface QuoteSendInput {\n to: string\n subject: string\n message: string\n}\n\n/** A send as the route received it, trimmed, or what to fix. `to` and `subject` may be empty: the defaults fill them. */\nexport function parseQuoteSendInput(raw: unknown): { ok: true; input: QuoteSendInput } | { ok: false; error: string } {\n const source = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>\n const text = (value: unknown) => (typeof value === 'string' ? value : '')\n const to = text(source['to']).trim()\n // A subject is one header line.\n const subject = text(source['subject']).replace(/[\\r\\n]+/g, ' ').trim()\n const message = text(source['message']).replace(/\\r\\n?/g, '\\n').trim()\n if (subject.length > QUOTE_MAX_SEND_SUBJECT) {\n return { ok: false, error: `Keep the subject under ${QUOTE_MAX_SEND_SUBJECT} characters.` }\n }\n if (message.length > QUOTE_MAX_SEND_MESSAGE) {\n return { ok: false, error: `Keep the message under ${QUOTE_MAX_SEND_MESSAGE.toLocaleString('en-US')} characters.` }\n }\n return { ok: true, input: { to, subject, message } }\n}\n\n/**\n * The quote once an email went out: the send on record, the conversation\n * moved on, a line on the timeline, and the quote at Sent, unless the\n * customer has already answered this revision (approved it, or asked for\n * changes), which stays what it says.\n */\nexport function quoteAfterSend(\n order: HostOrder,\n send: QuoteSendRecord,\n thread: QuoteEmailThread,\n): HostOrder {\n const quote = order.quote as OrderQuote\n const answered = latestQuoteResponse(quote) !== null\n return {\n ...order,\n timeline: appendOrderEvent(\n order,\n 'quote-sent',\n `Revision ${send.revision} emailed to ${send.to} by ${send.by}`,\n send.atMs,\n ),\n quote: {\n ...quote,\n stage: answered ? quote.stage : 'sent',\n sends: [...quoteSends(quote), send].slice(-QUOTE_MAX_SENDS),\n emailThread: thread,\n },\n }\n}\n"],"names":["appendOrderEvent","quoteLinkVersion","latestQuoteResponse","formatQuoteNumber","isQuote","isUnpaidQuote","QUOTE_MAX_SENDS","QUOTE_MAX_SEND_MESSAGE","QUOTE_MAX_SEND_SUBJECT","EMAIL","quoteSends","quote","Array","isArray","sends","filter","send","lastQuoteSend","length","quoteSendIsNewRevision","last","Boolean","Number","revision","quoteSendReplacesLink","linkVersion","quoteSendLabel","quoteSendRecipient","order","typed","String","trim","customer","email","quoteSendPrefill","to","quoteSendRefusal","test","quoteSendRefusalMessage","reason","quoteEmailDefaultSubject","businessName","quoteId","thread","emailThread","subject","name","number","parseQuoteSendInput","raw","source","text","value","replace","message","ok","error","toLocaleString","input","quoteAfterSend","answered","timeline","by","atMs","stage","slice"],"mappings":"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"}
|
|
@@ -15,14 +15,23 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
import type { ProductType } from './commerce';
|
|
18
|
-
import { type HostOrder, type OrderAddress } from './commerce-orders';
|
|
19
|
-
import { type OrderQuote, type QuoteUnmatchedPayment } from './commerce-quotes';
|
|
18
|
+
import { type HostOrder, type OrderAddress, type OrderLineItem, type OrderStatus } from './commerce-orders';
|
|
19
|
+
import { type OrderQuote, type QuoteDiscount, type QuoteTaxBasis, type QuoteUnmatchedPayment } from './commerce-quotes';
|
|
20
20
|
import type { StorefrontTaxMode } from './commerce-tax-decision';
|
|
21
21
|
import { type OrderListFields } from './order-list-fields';
|
|
22
22
|
/** Stripe's smallest card charge; a quote that totals less cannot be paid by card. */
|
|
23
23
|
export declare const QUOTE_MIN_CHARGE_CENTS = 50;
|
|
24
24
|
/** The pay link version a quote is on; absent reads as the first. */
|
|
25
25
|
export declare function quoteLinkVersion(quote: Pick<OrderQuote, 'linkVersion'> | null | undefined): number;
|
|
26
|
+
/**
|
|
27
|
+
* The pay link version any order is on (AGL-4099). A quote keeps its version
|
|
28
|
+
* where slice 2 put it, `quote.linkVersion`, so every link already sent keeps
|
|
29
|
+
* verifying; any other order keeps it as `payLink.version`. Absent reads as
|
|
30
|
+
* the first either way.
|
|
31
|
+
*/
|
|
32
|
+
export declare function orderPayLinkVersion(order: Partial<Pick<HostOrder, 'quote' | 'payLink'>> | null | undefined): number;
|
|
33
|
+
/** Payments the order's pay link could not take: a quote's on the quote, any other order's on `payLink`. */
|
|
34
|
+
export declare function orderPayLinkUnmatched(order: Partial<Pick<HostOrder, 'quote' | 'payLink'>> | null | undefined): QuoteUnmatchedPayment[];
|
|
26
35
|
/** An address as the quote page prints it: no phone. */
|
|
27
36
|
export interface QuotePageAddress {
|
|
28
37
|
name: string | null;
|
|
@@ -75,8 +84,14 @@ export interface QuotePageResponse {
|
|
|
75
84
|
*/
|
|
76
85
|
export interface QuotePageView {
|
|
77
86
|
state: 'quote';
|
|
87
|
+
/**
|
|
88
|
+
* What the page shows (AGL-4099): a quote, or an invoice for an order the
|
|
89
|
+
* business sent one for. An invoice has no proofs and no approval, and the
|
|
90
|
+
* page says "invoice" where a quote's says "quote".
|
|
91
|
+
*/
|
|
92
|
+
kind: 'quote' | 'invoice';
|
|
78
93
|
storeName: string;
|
|
79
|
-
/** `Quote #1042`. */
|
|
94
|
+
/** `Quote #1042`, or `Invoice #1042`. */
|
|
80
95
|
number: string;
|
|
81
96
|
/** When the quote was last written, which is the date it carries. */
|
|
82
97
|
dateMs: number | null;
|
|
@@ -104,6 +119,12 @@ export interface QuotePageView {
|
|
|
104
119
|
taxLabel: string;
|
|
105
120
|
/** Stripe Tax works the tax out when the customer pays; `taxCents` is 0 until then. */
|
|
106
121
|
taxAtPayment: boolean;
|
|
122
|
+
/**
|
|
123
|
+
* The customer picks a shipping rate when they pay (an invoiced draft
|
|
124
|
+
* order, AGL-4099); `shippingCents` is 0 until then. A quote's shipping is
|
|
125
|
+
* priced on it, so this is false for every quote.
|
|
126
|
+
*/
|
|
127
|
+
shippingAtPayment: boolean;
|
|
107
128
|
notes: string | null;
|
|
108
129
|
terms: string | null;
|
|
109
130
|
status: QuotePageStatus;
|
|
@@ -129,6 +150,13 @@ export interface QuotePageReplacedView {
|
|
|
129
150
|
state: 'replaced';
|
|
130
151
|
storeName: string;
|
|
131
152
|
}
|
|
153
|
+
/** An address as the pay link's page prints it, or null when it says nothing. */
|
|
154
|
+
export declare function quotePageAddress(address: OrderAddress | null | undefined): QuotePageAddress | null;
|
|
155
|
+
/** Where an order stands as the pay link's page says it. */
|
|
156
|
+
export declare function quotePageStatus(status: OrderStatus | string | undefined): {
|
|
157
|
+
status: QuotePageStatus;
|
|
158
|
+
label: string;
|
|
159
|
+
};
|
|
132
160
|
/**
|
|
133
161
|
* The quote page's view of an order document on the `quote` channel. Totals
|
|
134
162
|
* are the stored lines' through {@link computeQuoteTotals} until the quote is
|
|
@@ -142,6 +170,8 @@ export declare function buildQuotePageView(input: {
|
|
|
142
170
|
/** The order's status page, for a paid quote. */
|
|
143
171
|
receiptUrl?: string | null;
|
|
144
172
|
}): QuotePageView;
|
|
173
|
+
/** Order lines as the pay link's page lists them: what each charges, nothing about stock or cost. */
|
|
174
|
+
export declare function quotePageLines(lines: readonly OrderLineItem[]): QuotePageLine[];
|
|
145
175
|
/** One line of a quote's Checkout Session. */
|
|
146
176
|
export interface QuoteCheckoutLine {
|
|
147
177
|
name: string;
|
|
@@ -187,6 +217,19 @@ export interface QuoteCheckoutCharge {
|
|
|
187
217
|
* line it amounts to. Included lines charge nothing and are not sent.
|
|
188
218
|
*/
|
|
189
219
|
export declare function quoteCheckoutCharge(order: Pick<HostOrder, 'number' | 'lineItems' | 'quote'>, orderId: string): QuoteCheckoutCharge;
|
|
220
|
+
/**
|
|
221
|
+
* What a pay link's Checkout Session charges for stored lines, a discount,
|
|
222
|
+
* fixed shipping and a tax basis, under a label (`Quote #1042`,
|
|
223
|
+
* `Invoice #1042`): {@link quoteCheckoutCharge}'s pricing, shared with an
|
|
224
|
+
* invoiced order's (AGL-4099), so both are charged the same way.
|
|
225
|
+
*/
|
|
226
|
+
export declare function payLinkCheckoutCharge(input: {
|
|
227
|
+
lineItems: readonly OrderLineItem[];
|
|
228
|
+
tax: QuoteTaxBasis;
|
|
229
|
+
discount: QuoteDiscount | null;
|
|
230
|
+
shippingCents: number;
|
|
231
|
+
label: string;
|
|
232
|
+
}): QuoteCheckoutCharge;
|
|
190
233
|
export type QuotePayRefusal = 'paid' | 'closed' | 'approval-required' | 'tax-undecided' | 'nothing-to-pay';
|
|
191
234
|
/** Why a quote cannot be paid now, or null when it can. */
|
|
192
235
|
export declare function quotePayRefusal(order: Pick<HostOrder, 'number' | 'status' | 'lineItems' | 'quote'>, orderId: string): QuotePayRefusal | null;
|
|
@@ -229,11 +272,11 @@ export interface QuotePaymentFacts {
|
|
|
229
272
|
* the business is told, so a payment is never fulfilled twice and never lost.
|
|
230
273
|
*/
|
|
231
274
|
export type QuotePaymentDisposition = 'pay' | 'superseded' | 'already-paid' | 'redelivery';
|
|
232
|
-
export declare function quotePaymentDisposition(order: Pick<HostOrder, 'status' | 'quote' | 'checkoutSessionId' | 'paymentIntentId'
|
|
275
|
+
export declare function quotePaymentDisposition(order: Pick<HostOrder, 'status' | 'quote' | 'checkoutSessionId' | 'paymentIntentId'> & Partial<Pick<HostOrder, 'payLink'>>, payment: Pick<QuotePaymentFacts, 'sessionId' | 'paymentIntentId' | 'linkVersion' | 'quoteUpdatedAtMs'>): QuotePaymentDisposition;
|
|
233
276
|
/** A payment the quote could not take, as {@link QuoteUnmatchedPayment} records it. */
|
|
234
277
|
export declare function quoteUnmatchedPayment(facts: Pick<QuotePaymentFacts, 'sessionId' | 'paymentIntentId' | 'amountTotalCents' | 'paidAtMs'>, reason: QuoteUnmatchedPayment['reason']): QuoteUnmatchedPayment;
|
|
235
|
-
/** The timeline line for a payment the quote could not take. */
|
|
236
|
-
export declare function quoteUnmatchedPaymentDetail(entry: QuoteUnmatchedPayment): string;
|
|
278
|
+
/** The timeline line for a payment the quote (or an order's invoice, AGL-4099) could not take. */
|
|
279
|
+
export declare function quoteUnmatchedPaymentDetail(entry: QuoteUnmatchedPayment, noun?: 'quote' | 'invoice'): string;
|
|
237
280
|
/**
|
|
238
281
|
* The order a paid quote becomes, the whole document as it will stand. A
|
|
239
282
|
* quote carries none of the fields order lists read (`commerce-quotes.ts`);
|