@aglyn/plugins-commerce 1.0.0-beta.291 → 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/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/console/invoice-send.component.js +1 -1
- package/src/lib/components/console/invoice-send.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-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-send.component.js +1 -4
- package/src/lib/components/console/quote-send.component.js.map +1 -1
- 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/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 +7 -0
- package/src/lib/model/commerce-invoices.js +11 -0
- package/src/lib/model/commerce-invoices.js.map +1 -1
- package/src/lib/model/commerce-orders.d.ts +6 -3
- 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-quote-email.d.ts +14 -0
- package/src/lib/model/commerce-quote-email.js +25 -9
- package/src/lib/model/commerce-quote-email.js.map +1 -1
- 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/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/server/billing-webhook.js +64 -43
- 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/local-fulfillment.js +5 -1
- package/src/lib/server/local-fulfillment.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.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/quote-email-content.d.ts +1 -0
- package/src/lib/utils/quote-email-content.js +7 -4
- package/src/lib/utils/quote-email-content.js.map +1 -1
|
@@ -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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"}
|
|
@@ -26,6 +26,14 @@ export declare const QUOTE_MAX_SEND_SUBJECT = 200;
|
|
|
26
26
|
export declare function quoteSends(quote: Pick<OrderQuote, 'sends'> | null | undefined): QuoteSendRecord[];
|
|
27
27
|
/** The last time the quote was emailed, or null. */
|
|
28
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;
|
|
29
37
|
/**
|
|
30
38
|
* Whether emailing the quote now gives the customer a new pay link: they were
|
|
31
39
|
* emailed an earlier revision, and the link has not been replaced since.
|
|
@@ -35,6 +43,12 @@ export declare function quoteSendReplacesLink(quote: Pick<OrderQuote, 'sends' |
|
|
|
35
43
|
export declare function quoteSendLabel(quote: Pick<OrderQuote, 'sends' | 'revision'> | null | undefined): string;
|
|
36
44
|
/** Where the quote goes: what was typed in the dialog, else the customer's email. */
|
|
37
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;
|
|
38
52
|
export type QuoteSendRefusal = 'not-a-quote' | 'paid' | 'no-address' | 'bad-address';
|
|
39
53
|
/** Why the quote cannot be emailed to `to` now, if it cannot. */
|
|
40
54
|
export declare function quoteSendRefusal(order: Pick<HostOrder, 'channel' | 'status' | 'quote'> | null | undefined, to: string): QuoteSendRefusal | null;
|
|
@@ -49,28 +49,44 @@ const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
49
49
|
const sends = quoteSends(quote);
|
|
50
50
|
return sends.length ? sends[sends.length - 1] : null;
|
|
51
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
|
+
}
|
|
52
63
|
/**
|
|
53
64
|
* Whether emailing the quote now gives the customer a new pay link: they were
|
|
54
65
|
* emailed an earlier revision, and the link has not been replaced since.
|
|
55
66
|
*/ export function quoteSendReplacesLink(quote) {
|
|
56
|
-
var
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (!(Number(last.revision) < Number((_ref = quote == null ? void 0 : quote.revision) != null ? _ref : 1))) return false;
|
|
60
|
-
return Number(last.linkVersion) === quoteLinkVersion(quote);
|
|
67
|
+
var _lastQuoteSend;
|
|
68
|
+
if (!quoteSendIsNewRevision(quote)) return false;
|
|
69
|
+
return Number((_lastQuoteSend = lastQuoteSend(quote)) == null ? void 0 : _lastQuoteSend.linkVersion) === quoteLinkVersion(quote);
|
|
61
70
|
}
|
|
62
71
|
/** What the console's Send button says: `Send quote`, `Send again`, or `Send revision 2`. */ export function quoteSendLabel(quote) {
|
|
63
72
|
var _ref;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const revision = Number((_ref = quote == null ? void 0 : quote.revision) != null ? _ref : 1);
|
|
67
|
-
return Number(last.revision) < revision ? `Send revision ${revision}` : 'Send again';
|
|
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';
|
|
68
75
|
}
|
|
69
76
|
/** Where the quote goes: what was typed in the dialog, else the customer's email. */ export function quoteSendRecipient(order, typed) {
|
|
70
77
|
var _ref;
|
|
71
78
|
var _order_quote_customer, _order_quote;
|
|
72
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();
|
|
73
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
|
+
}
|
|
74
90
|
/** Why the quote cannot be emailed to `to` now, if it cannot. */ export function quoteSendRefusal(order, to) {
|
|
75
91
|
if (!order || !isQuote(order) || !order.quote) return 'not-a-quote';
|
|
76
92
|
if (!isUnpaidQuote(order)) return 'paid';
|
|
@@ -1 +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 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 const last = lastQuoteSend(quote)\n if (!last) return false\n if (!(Number(last.revision) < Number(quote?.revision ?? 1))) return false\n return Number(last.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 const last = lastQuoteSend(quote)\n if (!last) return 'Send quote'\n const revision = Number(quote?.revision ?? 1)\n return Number(last.revision) < revision ? `Send revision ${revision}` : '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\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","quoteSendReplacesLink","last","Number","revision","linkVersion","quoteSendLabel","quoteSendRecipient","order","typed","String","trim","customer","email","quoteSendRefusal","to","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":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,gBAAgB,QAAwB,uBAAmB;AACpE,SAASC,gBAAgB,QAAQ,0BAAsB;AACvD,SAASC,mBAAmB,QAAQ,6BAAyB;AAC7D,SACEC,iBAAiB,EACjBC,OAAO,EACPC,aAAa,QAIR,uBAAmB;AAE1B;;;;;;;;;;;;;;;;;;;;2CAoB2C,GAE3C,iEAAiE,GACjE,OAAO,MAAMC,kBAAkB,GAAE;AAEjC,wCAAwC,GACxC,OAAO,MAAMC,yBAAyB,KAAK;AAE3C,wCAAwC,GACxC,OAAO,MAAMC,yBAAyB,IAAG;AAEzC,MAAMC,QAAQ;AAEd,+CAA+C,GAC/C,OAAO,SAASC,WAAWC,KAAmD;IAC5E,OAAOC,MAAMC,OAAO,CAACF,yBAAAA,MAAOG,KAAK,IAAIH,MAAMG,KAAK,CAACC,MAAM,CAAC,CAACC,OAASA,QAAQ,OAAOA,SAAS,YAAY,EAAE;AAC1G;AAEA,kDAAkD,GAClD,OAAO,SAASC,cAAcN,KAAmD;IAC/E,MAAMG,QAAQJ,WAAWC;IACzB,OAAOG,MAAMI,MAAM,GAAGJ,KAAK,CAACA,MAAMI,MAAM,GAAG,EAAE,GAAG;AAClD;AAEA;;;CAGC,GACD,OAAO,SAASC,sBACdR,KAAgF;;IAEhF,MAAMS,OAAOH,cAAcN;IAC3B,IAAI,CAACS,MAAM,OAAO;IAClB,IAAI,CAAEC,CAAAA,OAAOD,KAAKE,QAAQ,IAAID,eAAOV,yBAAAA,MAAOW,QAAQ,mBAAI,EAAC,GAAI,OAAO;IACpE,OAAOD,OAAOD,KAAKG,WAAW,MAAMtB,iBAAiBU;AACvD;AAEA,2FAA2F,GAC3F,OAAO,SAASa,eAAeb,KAAgE;;IAC7F,MAAMS,OAAOH,cAAcN;IAC3B,IAAI,CAACS,MAAM,OAAO;IAClB,MAAME,WAAWD,eAAOV,yBAAAA,MAAOW,QAAQ,mBAAI;IAC3C,OAAOD,OAAOD,KAAKE,QAAQ,IAAIA,WAAW,CAAC,cAAc,EAAEA,UAAU,GAAG;AAC1E;AAEA,mFAAmF,GACnF,OAAO,SAASG,mBACdC,KAAkD,EAClDC,KAAgC;;QAEYD,uBAAAA;IAA5C,OAAOE,OAAOD,gBAAAA,QAAS,IAAIE,IAAI,MAAMD,eAAOF,0BAAAA,eAAAA,MAAOf,KAAK,sBAAZe,wBAAAA,aAAcI,QAAQ,qBAAtBJ,sBAAwBK,KAAK,mBAAI,IAAIF,IAAI;AACvF;AAIA,+DAA+D,GAC/D,OAAO,SAASG,iBACdN,KAAyE,EACzEO,EAAU;IAEV,IAAI,CAACP,SAAS,CAACtB,QAAQsB,UAAU,CAACA,MAAMf,KAAK,EAAE,OAAO;IACtD,IAAI,CAACN,cAAcqB,QAAQ,OAAO;IAClC,IAAI,CAACO,IAAI,OAAO;IAChB,IAAIA,GAAGf,MAAM,GAAG,OAAO,CAACT,MAAMyB,IAAI,CAACD,KAAK,OAAO;IAC/C,OAAO;AACT;AAEA,OAAO,SAASE,wBAAwBC,MAAwB;IAC9D,OAAQA;QACN,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;IACX;AACF;AAEA;;;CAGC,GACD,OAAO,SAASC,yBACdX,KAA0C,EAC1CY,YAAoB,EACpBC,OAAgB;;QAEMb,0BAAAA;IAAtB,MAAMc,SAASZ,gBAAOF,eAAAA,MAAMf,KAAK,sBAAXe,2BAAAA,aAAae,WAAW,qBAAxBf,yBAA0BgB,OAAO,mBAAI,IAAIb,IAAI;IACnE,IAAIW,QAAQ,OAAOA;IACnB,MAAMG,OAAOf,OAAOU,uBAAAA,eAAgB,IAAIT,IAAI;IAC5C,MAAMe,SAASzC,kBAAkBuB,OAAOa;IACxC,OAAOI,OAAO,GAAGC,OAAO,MAAM,EAAED,MAAM,GAAGC;AAC3C;AASA,uHAAuH,GACvH,OAAO,SAASC,oBAAoBC,GAAY;IAC9C,MAAMC,SAAUD,OAAO,OAAOA,QAAQ,WAAWA,MAAM,CAAC;IACxD,MAAME,OAAO,CAACC,QAAoB,OAAOA,UAAU,WAAWA,QAAQ;IACtE,MAAMhB,KAAKe,KAAKD,MAAM,CAAC,KAAK,EAAElB,IAAI;IAClC,gCAAgC;IAChC,MAAMa,UAAUM,KAAKD,MAAM,CAAC,UAAU,EAAEG,OAAO,CAAC,YAAY,KAAKrB,IAAI;IACrE,MAAMsB,UAAUH,KAAKD,MAAM,CAAC,UAAU,EAAEG,OAAO,CAAC,UAAU,MAAMrB,IAAI;IACpE,IAAIa,QAAQxB,MAAM,GAAGV,wBAAwB;QAC3C,OAAO;YAAE4C,IAAI;YAAOC,OAAO,CAAC,uBAAuB,EAAE7C,uBAAuB,YAAY,CAAC;QAAC;IAC5F;IACA,IAAI2C,QAAQjC,MAAM,GAAGX,wBAAwB;QAC3C,OAAO;YAAE6C,IAAI;YAAOC,OAAO,CAAC,uBAAuB,EAAE9C,uBAAuB+C,cAAc,CAAC,SAAS,YAAY,CAAC;QAAC;IACpH;IACA,OAAO;QAAEF,IAAI;QAAMG,OAAO;YAAEtB;YAAIS;YAASS;QAAQ;IAAE;AACrD;AAEA;;;;;CAKC,GACD,OAAO,SAASK,eACd9B,KAAgB,EAChBV,IAAqB,EACrBwB,MAAwB;IAExB,MAAM7B,QAAQe,MAAMf,KAAK;IACzB,MAAM8C,WAAWvD,oBAAoBS,WAAW;IAChD,OAAO,aACFe;QACHgC,UAAU1D,iBACR0B,OACA,cACA,CAAC,SAAS,EAAEV,KAAKM,QAAQ,CAAC,YAAY,EAAEN,KAAKiB,EAAE,CAAC,IAAI,EAAEjB,KAAK2C,EAAE,EAAE,EAC/D3C,KAAK4C,IAAI;QAEXjD,OAAO,aACFA;YACHkD,OAAOJ,WAAW9C,MAAMkD,KAAK,GAAG;YAChC/C,OAAO;mBAAIJ,WAAWC;gBAAQK;aAAK,CAAC8C,KAAK,CAAC,CAACxD;YAC3CmC,aAAaD;;;AAGnB"}
|
|
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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"}
|
|
@@ -432,7 +432,7 @@ function parseLine(raw, index) {
|
|
|
432
432
|
error: `Set a price for ${product.name} before quoting it.`
|
|
433
433
|
};
|
|
434
434
|
}
|
|
435
|
-
const chosen = resolveLineModifiers(product, request.modifiers);
|
|
435
|
+
const chosen = resolveLineModifiers(product, request.modifiers, variant.id);
|
|
436
436
|
if (!chosen.ok) return {
|
|
437
437
|
ok: false,
|
|
438
438
|
error: (_chosen_error = chosen.error) != null ? _chosen_error : `Check the choices on ${product.name}.`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/commerce-quotes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { HostProduct, ProductType } from './commerce'\nimport type { QuoteRequestRef } from './commerce-quote-request'\nimport { liftLegacyProduct, variantHasPrice } from './commerce'\nimport type {\n HostOrder,\n OrderAddress,\n OrderLineItem,\n OrderStatus,\n OrderTotals,\n} from './commerce-orders'\nimport { computeTaxCents, resolveTaxRate, type TaxSettings } from './commerce-tax'\nimport { storefrontTaxDecision, type StorefrontTaxMode } from './commerce-tax-decision'\nimport { allocateCentsByWeight } from './commerce-tax-engine'\nimport {\n lineLabelWithModifiers,\n resolveLineModifiers,\n type ModifierSelection,\n} from './product-modifiers'\n\n/*==========================================\n * QUOTES (AGL-4093).\n *\n * A quote is an order that has not been paid yet: a document in\n * `hosts/{hostId}/orders` with `channel: 'quote'`, `status: 'pending'` and a\n * `quote` section holding what only a quote has (the customer it is for, its\n * notes and terms, its discount and shipping, where it stands with the\n * business). Its `number` comes from the store's order counter, so the quote\n * and the order it becomes are one number and one document: paying it flips\n * `status` the way a paid draft order flips.\n *\n * WHAT KEEPS AN UNPAID QUOTE OUT OF EVERY ORDER LIST. A quote document\n * carries none of the fields the order readers ask their queries about:\n * no `createdAtMs` (the Orders list, its filters, the money tiles, the\n * analytics, the exports and the reports all order or range on it), none of\n * the list fields `withOrderListFields` stamps (`customerEmailLower`, the\n * search tokens, `requiresShipping`, `productIds`), and no top-level\n * `customerEmail`, `shippingAddress` or `billingAddress` (the customer's\n * account page, gated content and reviews look orders up by the buyer's\n * address). The customer lives in `quote.customer` until the quote is paid,\n * and whatever pays it writes those order fields then. `isUnpaidQuote` is the\n * test for the few readers that reach orders some other way (by number, by\n * document id, or by a header sort on `status`, `channel` or `number`).\n *\n * Pure: the console's live totals, the server's stored totals, and later the\n * quote's PDF and pay page all come from `computeQuoteTotals`.\n *=========================================*/\n\n/** Where a quote stands with the business. The customer side has no states. */\nexport type QuoteStage = 'draft' | 'sent' | 'changes_requested' | 'approved'\n\nexport const QUOTE_STAGE_LABELS: Record<QuoteStage, string> = {\n draft: 'Draft',\n sent: 'Sent',\n changes_requested: 'Changes requested',\n approved: 'Approved',\n}\n\n/** The person a quote is for: a record the workspace keeps, typed details, or both. */\nexport interface QuoteCustomer {\n /** The person record, as the person-records seam names it (`contact`, …). */\n record?: { kind: string; id: string } | null\n name?: string\n company?: string\n email?: string\n /** `email` lower-cased: what a person erasure finds the quote by. */\n emailLower?: string\n phone?: string\n}\n\n/** Money off the whole quote: an amount, or a percentage of the charged lines. */\nexport interface QuoteDiscount {\n kind: 'amount' | 'percent'\n /** With `amount`: the cents off. */\n amountCents?: number\n /** With `percent`: the percentage off. */\n pct?: number\n}\n\n/** Shipping the business prices by hand. */\nexport interface QuoteShipping {\n amountCents: number\n}\n\n/** Why a quote carries no tax. */\nexport type QuoteNoTaxReason =\n | 'not-collected'\n | 'prices-include-tax'\n | 'no-rate'\n | 'undecided'\n\n/** Where a quote's tax comes from. */\nexport type QuoteTaxKind = 'rate' | 'service' | 'at-payment' | 'none'\n\n/**\n * How a quote's tax was worked out, stored beside the figure so the quote can\n * say it: one of the store's own rates (`rate`, with `pct` and its `label`),\n * its connected tax service's answer (`service`, with `taxCents` and\n * `providerLabel`), Stripe Tax at payment (`at-payment`), or no tax and why\n * (`none`, with `reason`). One flat shape, so the apps can read it as is.\n */\nexport interface QuoteTaxBasis {\n kind: QuoteTaxKind\n pct?: number\n label?: string\n taxCents?: number\n providerLabel?: string\n reason?: QuoteNoTaxReason\n}\n\n/** A proof attached to one revision of a quote: what names it, and the revision it is for. */\nexport interface QuoteProofRef {\n id: string\n revision: number\n}\n\n/**\n * A proof the business attached to a revision (AGL-4096): an image or a\n * design file in the site's private storage, under the quote.\n */\nexport interface QuoteProof extends QuoteProofRef {\n name: string\n contentType: string\n size: number\n /** The private Storage object. Never sent to the customer. */\n path: string\n addedAtMs: number\n /** Who attached it: an email, else a uid. */\n addedBy: string\n}\n\n/** A file the customer sent with a response. */\nexport interface QuoteResponseFile {\n id: string\n name: string\n contentType: string\n size: number\n /** The private Storage object. Never sent to the customer. */\n path: string\n}\n\n/**\n * The customer's answer to a revision's proofs (AGL-4096): approved under a\n * typed name, or changes asked for in a comment, with artwork if they had\n * some. The address and browser are kept as the record of who answered.\n */\nexport interface QuoteCustomerResponse {\n action: 'approve' | 'changes'\n revision: number\n atMs: number\n /** With `approve`: the name they typed. */\n name?: string\n /** With `changes`: what to change. */\n comment?: string\n /** With `changes`: files they sent. */\n files?: QuoteResponseFile[]\n ip: string | null\n userAgent: string | null\n}\n\n/** The record of a revision's approval. */\nexport interface QuoteApproval {\n revision: number\n name: string\n atMs: number\n ip: string | null\n userAgent: string | null\n}\n\n/**\n * A payment that reached a quote it could not pay: one made through a pay\n * link the business replaced or for a price it has since changed\n * (`superseded`), or one made after the quote was already paid\n * (`already-paid`). The money is in Stripe and the quote records no sale for\n * it; the business refunds it or reconciles it by hand.\n */\nexport interface QuoteUnmatchedPayment {\n sessionId: string\n paymentIntentId: string | null\n amountCents: number\n atMs: number\n reason: 'superseded' | 'already-paid'\n}\n\n/**\n * The conversation the quote's emails keep in the customer's mailbox\n * (AGL-4097): the latest message's `Message-ID`, every id in the\n * conversation oldest first, and the subject it runs under. The shape of\n * `EmailThread` (`@aglyn/shared-util-email`), spelled out so the model needs\n * no email library. Started by the first email about the quote (a request's\n * confirmation, or the quote itself); every later one replies into it.\n */\nexport interface QuoteEmailThread {\n messageId: string\n references: string[]\n subject: string\n}\n\n/** One time the quote was emailed to the customer (AGL-4097). */\nexport interface QuoteSendRecord {\n /** The revision the email carried. */\n revision: number\n to: string\n subject: string\n atMs: number\n /** Who sent it: an email, else a uid. */\n by: string\n /** The `Message-ID` the email was sent with. */\n messageId: string\n /** The mail provider's own id for the message, when it answered one. */\n providerId: string | null\n /** The pay link version the email carried. */\n linkVersion: number\n}\n\n/** One shipping rate a draft order offered (AGL-4099), as checkout's resolved rates name it. */\nexport interface OrderDraftShippingRate {\n rateId: string\n name: string\n amountCents: number\n}\n\n/** The shipping a draft order offered when it was made (AGL-4099); see `HostOrder.draftShipping`. */\nexport interface OrderDraftShipping {\n /** Where its rates ship; empty when it offers none. */\n countries: string[]\n /** The rates the buyer picks from at payment; empty when it ships nothing. */\n options: OrderDraftShippingRate[]\n}\n\n/** The lasting pay link of an order that is not a quote (AGL-4099); see `HostOrder.payLink`. */\nexport interface OrderPayLink {\n /** The link's version, absent reading as 1. */\n version?: number\n /** Payments through the link the order could not take, as a quote records them. */\n unmatchedPayments?: QuoteUnmatchedPayment[]\n}\n\n/** One time an order's (or a quote's) invoice was emailed to the customer (AGL-4099). */\nexport interface OrderInvoiceSendRecord {\n to: string\n subject: string\n atMs: number\n /** Who sent it: an email, else a uid. */\n by: string\n /** The `Message-ID` the email was sent with. */\n messageId: string\n /** The mail provider's own id for the message, when it answered one. */\n providerId: string | null\n /** The invoice said it was paid (the record), rather than asking for payment. */\n paid: boolean\n /** The pay link version an unpaid invoice carried; null on a paid one. */\n linkVersion: number | null\n /** The total the invoice showed. */\n totalCents: number\n}\n\n/** What only a quote has, on its order document. */\nexport interface OrderQuote {\n stage: QuoteStage\n /** Which version of the quote this is; versions are what proofs and approvals name. */\n revision: number\n customer: QuoteCustomer\n billTo?: OrderAddress | null\n shipTo?: OrderAddress | null\n notes?: string\n terms?: string\n discount?: QuoteDiscount | null\n shipping?: QuoteShipping | null\n tax: QuoteTaxBasis\n /** The customer approves the current revision's proof before they can pay. Absent reads as off. */\n requireProofApproval?: boolean\n /** The current revision's proofs; earlier revisions keep theirs in `revisions/{n}`. */\n proofs?: QuoteProof[]\n /** The customer's answers to the current revision, oldest first; the last one counts. */\n responses?: QuoteCustomerResponse[]\n /** The current revision's approval, while its latest answer is one. */\n approval?: QuoteApproval | null\n /** The revision the customer approved, when they have. */\n approvedRevision?: number | null\n /**\n * Which pay link opens the quote: a link signed for an older version shows\n * the customer that the quote was replaced, and nothing of it. Absent reads\n * as 1. Bumped when the business replaces the link.\n */\n linkVersion?: number\n /** The conversation its emails keep in the customer's mailbox (AGL-4097). */\n emailThread?: QuoteEmailThread | null\n /** Every time it was emailed to the customer, oldest first (AGL-4097). */\n sends?: QuoteSendRecord[]\n /**\n * The request it was started from (AGL-4098): a \"Request a quote\" form's\n * submission, by id. Its files are listed on the quote as the customer's\n * ({@link requestFiles}), and its confirmation's conversation is the\n * quote's {@link emailThread} until the quote is first sent.\n */\n request?: QuoteRequestRef | null\n /** The request's files, where the request keeps them; downloaded through the quote's own route. */\n requestFiles?: QuoteResponseFile[]\n /** When the customer paid it. */\n paidAtMs?: number\n /** Payments the quote could not take; see {@link QuoteUnmatchedPayment}. */\n unmatchedPayments?: QuoteUnmatchedPayment[]\n createdAtMs: number\n updatedAtMs: number\n /** Who created and last changed it: an email, else a uid. */\n createdBy: string\n updatedBy: string\n}\n\n/** The store-wide quote defaults, at `hosts/{hostId}/settings/store` under `quotes`. */\nexport interface QuoteStoreSettings {\n /** What `requireProofApproval` a new quote starts with. Absent reads as off. */\n requireProofApproval?: boolean\n}\n\n/** Whether new quotes start out asking the customer to approve the proof first. */\nexport function quoteRequiresProofApprovalByDefault(\n settings: QuoteStoreSettings | null | undefined,\n): boolean {\n return settings?.requireProofApproval === true\n}\n\n/**\n * Whether paying waits on the customer approving the proof: the quote asks for\n * approval, its current revision has a proof, and that revision is not the\n * one the customer approved. A revision with no proof never holds a payment.\n */\nexport function quotePaymentBlockedByApproval(\n quote:\n | (Pick<OrderQuote, 'requireProofApproval' | 'revision' | 'approvedRevision'> & {\n proofs?: readonly QuoteProofRef[]\n })\n | null\n | undefined,\n): boolean {\n if (!quote || quote.requireProofApproval !== true) return false\n const revision = Number(quote.revision ?? 1)\n const proofed = (quote.proofs ?? []).some((proof) => Number(proof?.revision) === revision)\n if (!proofed) return false\n return Number(quote.approvedRevision) !== revision\n}\n\n/** Whether an order document is a quote, paid or not. */\nexport function isQuote(order: Pick<Partial<HostOrder>, 'channel'> | null | undefined): boolean {\n return order?.channel === 'quote'\n}\n\n/** Whether an order document is a quote nobody has paid yet, which no order list counts. */\nexport function isUnpaidQuote(\n order: { channel?: unknown; status?: unknown } | null | undefined,\n): boolean {\n return order?.channel === 'quote' && order?.status === 'pending'\n}\n\n/** `Quote #1042`; an order number until it is paid. */\nexport function formatQuoteNumber(order: Pick<HostOrder, 'number'>, docId?: string): string {\n if (order.number != null) return `Quote #${order.number}`\n return docId ? `Quote #${docId.slice(-6).toUpperCase()}` : 'Quote'\n}\n\n/** A quote's status as its list shows it: its stage until it is paid. */\nexport function quoteDisplayStatus(order: {\n status?: OrderStatus | string\n quote?: Pick<OrderQuote, 'stage'> | null\n}): { key: QuoteStage | 'paid' | 'cancelled' | 'refunded'; label: string } {\n if (order.status === 'cancelled') return { key: 'cancelled', label: 'Canceled' }\n if (order.status === 'refunded') return { key: 'refunded', label: 'Refunded' }\n if (order.status && order.status !== 'pending') return { key: 'paid', label: 'Paid' }\n const stage = (order.quote?.stage ?? 'draft') as QuoteStage\n return { key: stage, label: QUOTE_STAGE_LABELS[stage] ?? QUOTE_STAGE_LABELS.draft }\n}\n\n/** Chip colors for {@link quoteDisplayStatus}, as the orders list colors its statuses. */\nexport const QUOTE_STATUS_COLOR: Record<\n ReturnType<typeof quoteDisplayStatus>['key'],\n 'default' | 'success' | 'info' | 'warning' | 'error'\n> = {\n draft: 'default',\n sent: 'info',\n changes_requested: 'warning',\n approved: 'info',\n paid: 'success',\n cancelled: 'default',\n refunded: 'error',\n}\n\n/*==========================================\n * TOTALS\n *=========================================*/\n\n/** A line as the totals read it. */\nexport type QuoteTotalsLine = Pick<OrderLineItem, 'unitAmountCents' | 'quantity'> & {\n included?: boolean\n taxExempt?: boolean\n}\n\nexport interface QuoteTotalsInput {\n lines: readonly QuoteTotalsLine[]\n discount?: QuoteDiscount | null\n shippingCents?: number\n tax: QuoteTaxBasis\n}\n\n/** A quote's totals: the order's, plus what the tax was computed on. */\nexport interface QuoteTotals extends OrderTotals {\n /** The taxable lines after their share of the discount. */\n taxableCents: number\n /** Stripe Tax works the tax out when the customer pays; `taxCents` is 0 until then. */\n taxAtPayment: boolean\n}\n\nconst whole = (value: unknown): number => {\n const number = Math.round(Number(value))\n return Number.isFinite(number) && number > 0 ? number : 0\n}\n\n/** One line's charge: nothing for an included line. */\nexport function quoteLineCents(line: QuoteTotalsLine): number {\n if (line.included) return 0\n return whole(line.unitAmountCents) * whole(line.quantity)\n}\n\n/** The discount in cents on `itemsCents`, never more than it. */\nexport function quoteDiscountCents(\n itemsCents: number,\n discount: QuoteDiscount | null | undefined,\n): number {\n const items = whole(itemsCents)\n if (!discount) return 0\n if (discount.kind === 'amount') return Math.min(items, whole(discount.amountCents))\n if (discount.kind !== 'percent') return 0\n const pct = Math.min(100, Math.max(0, Number(discount.pct) || 0))\n return Math.min(items, Math.round((items * pct) / 100))\n}\n\n/**\n * Each line's charge, its share of the discount (in proportion to the\n * charges, as a checkout spreads a coupon) and what is left: the amounts a tax\n * service is asked about, line by line.\n */\nexport function quoteLineAmounts(\n lines: readonly QuoteTotalsLine[],\n discount: QuoteDiscount | null | undefined,\n): { grossCents: number[]; discountCents: number[]; netCents: number[]; itemsCents: number; totalDiscountCents: number } {\n const grossCents = lines.map(quoteLineCents)\n const itemsCents = grossCents.reduce((sum, cents) => sum + cents, 0)\n const totalDiscountCents = quoteDiscountCents(itemsCents, discount)\n const discountCents = allocateCentsByWeight(totalDiscountCents, grossCents)\n return {\n grossCents,\n discountCents,\n netCents: grossCents.map((cents, index) => cents - discountCents[index]),\n itemsCents,\n totalDiscountCents,\n }\n}\n\n/**\n * A quote's totals. The discount comes off before tax; a store rate taxes the\n * taxable lines (not shipping, as a draft order's rate does); a tax service's\n * answer is taken as given; Stripe Tax and no-tax add nothing now.\n */\nexport function computeQuoteTotals(input: QuoteTotalsInput): QuoteTotals {\n const amounts = quoteLineAmounts(input.lines, input.discount)\n const taxableCents = amounts.netCents.reduce(\n (sum, cents, index) => sum + (input.lines[index]?.taxExempt ? 0 : cents),\n 0,\n )\n const shippingCents = whole(input.shippingCents)\n const tax = input.tax\n const taxCents =\n tax.kind === 'rate'\n ? computeTaxCents(taxableCents, Number(tax.pct) || 0)\n : tax.kind === 'service'\n ? whole(tax.taxCents)\n : 0\n return {\n itemsCents: amounts.itemsCents,\n discountCents: amounts.totalDiscountCents,\n shippingCents,\n taxCents,\n // The platform fee is set when a payment is created, from what it charges.\n feeCents: 0,\n totalCents: amounts.itemsCents - amounts.totalDiscountCents + shippingCents + taxCents,\n taxableCents,\n taxAtPayment: tax.kind === 'at-payment',\n }\n}\n\n/** The order totals a quote document stores. */\nexport function quoteOrderTotals(totals: QuoteTotals): OrderTotals {\n return {\n itemsCents: totals.itemsCents,\n discountCents: totals.discountCents,\n shippingCents: totals.shippingCents,\n taxCents: totals.taxCents,\n feeCents: totals.feeCents,\n totalCents: totals.totalCents,\n }\n}\n\n/*==========================================\n * TAX\n *=========================================*/\n\n/**\n * The tax basis the store's own settings give a quote, before any tax service\n * is asked: Stripe Tax at payment, no tax (and why), or the store's rate for\n * where the quote ships — its ship-to when it has a country, else the store's\n * own address, as the storefront taxes a sale it cannot see the destination of.\n */\nexport function quoteTaxBasisFromSettings(\n settings: TaxSettings | null | undefined,\n shipTo: Pick<OrderAddress, 'country' | 'state'> | null | undefined,\n): QuoteTaxBasis {\n const decision = storefrontTaxDecision({ settings })\n if (decision.kind === 'undecided') return { kind: 'none', reason: 'undecided' }\n if (decision.kind === 'stripe-automatic') return { kind: 'at-payment' }\n if (decision.kind !== 'manual') return { kind: 'none', reason: 'not-collected' }\n if (settings?.pricesIncludeTax) return { kind: 'none', reason: 'prices-include-tax' }\n const address = String(shipTo?.country ?? '').trim()\n ? { country: shipTo?.country, state: shipTo?.state }\n : (settings?.origin ?? {})\n const rate = resolveTaxRate(settings ?? undefined, address)\n if (!rate || !(Number(rate.pct) > 0)) return { kind: 'none', reason: 'no-rate' }\n return { kind: 'rate', pct: Number(rate.pct), ...(rate.label ? { label: rate.label } : {}) }\n}\n\n/** The tax line's label on a quote. */\nexport function describeQuoteTax(basis: QuoteTaxBasis | null | undefined): string {\n if (!basis) return 'Tax'\n switch (basis.kind) {\n case 'rate':\n return basis.label ? `${basis.label} (${basis.pct ?? 0}%)` : `Tax (${basis.pct ?? 0}%)`\n case 'service':\n return basis.providerLabel ? `Tax (${basis.providerLabel})` : 'Tax'\n case 'at-payment':\n return 'Tax is added at payment'\n case 'none':\n return basis.reason === 'prices-include-tax'\n ? 'Prices include tax'\n : basis.reason === 'no-rate'\n ? 'No tax rate for this address'\n : basis.reason === 'undecided'\n ? 'Sales tax is not set up'\n : 'No sales tax'\n default:\n return 'Tax'\n }\n}\n\n/** The order's `taxMode` for a quote's basis; absent while nobody has decided. */\nexport function quoteTaxMode(basis: QuoteTaxBasis, taxCents: number): StorefrontTaxMode | undefined {\n if (basis.kind === 'at-payment') return 'stripe-automatic'\n if (basis.kind === 'none' && basis.reason === 'undecided') return undefined\n return taxCents > 0 ? 'manual' : 'none'\n}\n\n/*==========================================\n * WHAT THE CONSOLE SENDS\n *=========================================*/\n\nexport const QUOTE_MAX_LINES = 100\nexport const QUOTE_MAX_QUANTITY = 9_999\n/** $1,000,000 a unit: the most a typed line may cost. */\nexport const QUOTE_MAX_UNIT_CENTS = 100_000_000\nexport const QUOTE_MAX_TEXT = 5_000\nconst SHORT_TEXT = 200\n\n/** A product line as the console asks for it: what it is, never what it costs. */\nexport interface QuoteCatalogLineRequest {\n kind: 'catalog'\n productId: string\n variantId?: string\n quantity: number\n modifiers?: ModifierSelection[]\n included?: boolean\n description?: string\n}\n\n/** A line typed on the quote, which no product stands behind. */\nexport interface QuoteCustomLineRequest {\n kind: 'custom'\n name: string\n description?: string\n sku?: string\n unitAmountCents: number\n quantity: number\n /** Whether it is something to ship (else a service, which never ships). */\n ships?: boolean\n included?: boolean\n}\n\nexport type QuoteLineRequest = QuoteCatalogLineRequest | QuoteCustomLineRequest\n\n/** Everything a business edits on a quote. */\nexport interface QuoteInput {\n lines: QuoteLineRequest[]\n customer: QuoteCustomer\n billTo: OrderAddress | null\n shipTo: OrderAddress | null\n notes: string\n terms: string\n discount: QuoteDiscount | null\n shipping: QuoteShipping | null\n /** Absent: the store's default (on create) or unchanged (on update). */\n requireProofApproval?: boolean\n}\n\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\nconst DOC_ID = /^(?!__.*__$)[^/]{1,200}$/\n\nconst clean = (value: unknown, max = SHORT_TEXT): string =>\n typeof value === 'string' ? value.trim().slice(0, max) : ''\n\nconst intIn = (value: unknown, min: number, max: number): number | null => {\n const number = Number(value)\n return Number.isInteger(number) && number >= min && number <= max ? number : null\n}\n\nfunction parseAddress(raw: unknown): OrderAddress | null {\n if (!raw || typeof raw !== 'object') return null\n const source = raw as Record<string, unknown>\n const address: OrderAddress = {}\n for (const key of ['name', 'line1', 'line2', 'city', 'state', 'postalCode', 'phone'] as const) {\n const value = clean(source[key])\n if (value) address[key] = value\n }\n const country = clean(source['country'], 2).toUpperCase()\n if (/^[A-Z]{2}$/.test(country)) address.country = country\n return Object.keys(address).length ? address : null\n}\n\nfunction parseLine(raw: unknown, index: number): QuoteLineRequest | string {\n const source = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>\n const at = `Line ${index + 1}`\n const quantity = intIn(source['quantity'], 1, QUOTE_MAX_QUANTITY)\n if (quantity === null) return `${at}: enter a quantity from 1 to ${QUOTE_MAX_QUANTITY.toLocaleString('en-US')}.`\n const included = source['included'] === true\n const description = clean(source['description'], QUOTE_MAX_TEXT)\n if (source['kind'] === 'catalog') {\n const productId = clean(source['productId'])\n if (!DOC_ID.test(productId)) return `${at}: choose a product.`\n const variantId = clean(source['variantId'])\n const modifiers = Array.isArray(source['modifiers'])\n ? (source['modifiers'] as unknown[]).slice(0, 200).map((entry) => {\n const pick = (entry ?? {}) as Record<string, unknown>\n return { groupId: clean(pick['groupId']), optionId: clean(pick['optionId']) }\n })\n : []\n return {\n kind: 'catalog',\n productId,\n ...(variantId ? { variantId } : {}),\n quantity,\n ...(modifiers.length ? { modifiers } : {}),\n ...(included ? { included } : {}),\n ...(description ? { description } : {}),\n }\n }\n if (source['kind'] === 'custom') {\n const name = clean(source['name'])\n if (!name) return `${at}: name the item.`\n const unitAmountCents = included ? 0 : intIn(source['unitAmountCents'], 0, QUOTE_MAX_UNIT_CENTS)\n if (unitAmountCents === null) return `${at}: enter a price from $0 to $1,000,000.`\n const sku = clean(source['sku'], 100)\n return {\n kind: 'custom',\n name,\n ...(description ? { description } : {}),\n ...(sku ? { sku } : {}),\n unitAmountCents,\n quantity,\n ships: source['ships'] !== false,\n ...(included ? { included } : {}),\n }\n }\n return `${at}: add a product or type an item.`\n}\n\n/**\n * The console's quote, checked and trimmed, or what to fix. Prices on product\n * lines are not read from here: the server prices them from the product.\n */\nexport function parseQuoteInput(raw: unknown): { ok: true; input: QuoteInput } | { ok: false; error: string } {\n const source = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>\n const rawLines = Array.isArray(source['lines']) ? (source['lines'] as unknown[]) : []\n if (rawLines.length === 0) return { ok: false, error: 'Add at least one line to the quote.' }\n if (rawLines.length > QUOTE_MAX_LINES) {\n return { ok: false, error: `A quote holds up to ${QUOTE_MAX_LINES} lines.` }\n }\n const lines: QuoteLineRequest[] = []\n for (const [index, entry] of rawLines.entries()) {\n const line = parseLine(entry, index)\n if (typeof line === 'string') return { ok: false, error: line }\n lines.push(line)\n }\n\n const customerSource = (source['customer'] && typeof source['customer'] === 'object'\n ? source['customer']\n : {}) as Record<string, unknown>\n const email = clean(customerSource['email'])\n if (email && !EMAIL.test(email)) return { ok: false, error: 'Enter a valid email for the customer.' }\n const recordSource = (customerSource['record'] ?? null) as Record<string, unknown> | null\n const recordKind = clean(recordSource?.['kind'], 40)\n const recordId = clean(recordSource?.['id'])\n const customer: QuoteCustomer = {\n ...(recordKind && DOC_ID.test(recordId) ? { record: { kind: recordKind, id: recordId } } : {}),\n ...(clean(customerSource['name']) ? { name: clean(customerSource['name']) } : {}),\n ...(clean(customerSource['company']) ? { company: clean(customerSource['company']) } : {}),\n ...(email ? { email, emailLower: email.toLowerCase() } : {}),\n ...(clean(customerSource['phone'], 40) ? { phone: clean(customerSource['phone'], 40) } : {}),\n }\n\n let discount: QuoteDiscount | null = null\n const discountSource = (source['discount'] ?? null) as Record<string, unknown> | null\n if (discountSource?.['kind'] === 'amount') {\n const amountCents = intIn(discountSource['amountCents'], 0, QUOTE_MAX_UNIT_CENTS)\n if (amountCents === null) return { ok: false, error: 'Enter a discount from $0 to $1,000,000.' }\n if (amountCents > 0) discount = { kind: 'amount', amountCents }\n } else if (discountSource?.['kind'] === 'percent') {\n const pct = Number(discountSource['pct'])\n if (!Number.isFinite(pct) || pct < 0 || pct > 100) {\n return { ok: false, error: 'Enter a discount from 0% to 100%.' }\n }\n if (pct > 0) discount = { kind: 'percent', pct: Math.round(pct * 100) / 100 }\n }\n\n let shipping: QuoteShipping | null = null\n const shippingSource = (source['shipping'] ?? null) as Record<string, unknown> | null\n if (shippingSource) {\n const amountCents = intIn(shippingSource['amountCents'], 0, QUOTE_MAX_UNIT_CENTS)\n if (amountCents === null) return { ok: false, error: 'Enter shipping from $0 to $1,000,000.' }\n shipping = { amountCents }\n }\n\n return {\n ok: true,\n input: {\n lines,\n customer,\n billTo: parseAddress(source['billTo']),\n shipTo: parseAddress(source['shipTo']),\n notes: clean(source['notes'], QUOTE_MAX_TEXT),\n terms: clean(source['terms'], QUOTE_MAX_TEXT),\n discount,\n shipping,\n ...(typeof source['requireProofApproval'] === 'boolean'\n ? { requireProofApproval: source['requireProofApproval'] }\n : {}),\n },\n }\n}\n\n/** A priced quote line, and whether its product is tax-exempt. */\nexport type PricedQuoteLine = OrderLineItem & { taxExempt?: boolean }\n\n/** The variant's label as order lines print it: its option values, or nothing for a default. */\nfunction variantOptionsLabel(options: Record<string, string> | undefined): string | undefined {\n return options && Object.keys(options).length ? Object.values(options).join(' / ') : undefined\n}\n\n/**\n * A product line priced from the PRODUCT: its variant's price plus the chosen\n * add-ons, as the register prices one. An included line keeps what it is and\n * charges nothing.\n */\nexport function priceQuoteCatalogLine(\n rawProduct: Partial<HostProduct> | null | undefined,\n request: QuoteCatalogLineRequest,\n): { ok: true; line: PricedQuoteLine } | { ok: false; error: string } {\n if (!rawProduct || rawProduct.deletedAt) {\n return { ok: false, error: 'A product on this quote no longer exists. Remove it and add it again.' }\n }\n const product = liftLegacyProduct(rawProduct)\n const variant = request.variantId\n ? product.variants.find((entry) => entry.id === request.variantId)\n : product.variants[0]\n if (!variant) return { ok: false, error: `Choose an option of ${product.name} that still exists.` }\n if (!request.included && !variantHasPrice(variant)) {\n return { ok: false, error: `Set a price for ${product.name} before quoting it.` }\n }\n const chosen = resolveLineModifiers(product, request.modifiers)\n if (!chosen.ok) return { ok: false, error: chosen.error ?? `Check the choices on ${product.name}.` }\n const variantLabel = lineLabelWithModifiers(variantOptionsLabel(variant.options), chosen.modifiers)\n return {\n ok: true,\n line: {\n productId: request.productId,\n ...(variant.id !== 'default' ? { variantId: variant.id } : {}),\n name: product.name,\n ...(variantLabel ? { variantLabel } : {}),\n ...(variant.sku ? { sku: variant.sku } : {}),\n productType: (product.type ?? 'physical') as ProductType,\n quantity: request.quantity,\n unitAmountCents: request.included\n ? 0\n : Math.round(Number(variant.priceUsd) * 100) + chosen.extraCents,\n ...(chosen.modifiers.length ? { modifiers: chosen.modifiers } : {}),\n ...(request.included ? { included: true } : {}),\n ...(request.description ? { description: request.description } : {}),\n ...(product.taxExempt ? { taxExempt: true } : {}),\n },\n }\n}\n\n/** A typed line: no product, so nothing reads stock, a supplier or a download for it. */\nexport function priceQuoteCustomLine(request: QuoteCustomLineRequest): PricedQuoteLine {\n return {\n productId: '',\n custom: true,\n name: request.name,\n ...(request.sku ? { sku: request.sku } : {}),\n productType: request.ships === false ? 'service' : 'physical',\n quantity: request.quantity,\n unitAmountCents: request.included ? 0 : request.unitAmountCents,\n ...(request.included ? { included: true } : {}),\n ...(request.description ? { description: request.description } : {}),\n }\n}\n\n/** The request a stored line is edited as: what the console sends back on a save. */\nexport function quoteLineRequestFromLine(line: OrderLineItem): QuoteLineRequest {\n if (line.custom || !line.productId) {\n return {\n kind: 'custom',\n name: line.name,\n ...(line.description ? { description: line.description } : {}),\n ...(line.sku ? { sku: line.sku } : {}),\n unitAmountCents: line.unitAmountCents,\n quantity: line.quantity,\n ships: line.productType !== 'service' && line.productType !== 'digital',\n ...(line.included ? { included: true } : {}),\n }\n }\n return {\n kind: 'catalog',\n productId: line.productId,\n ...(line.variantId ? { variantId: line.variantId } : {}),\n quantity: line.quantity,\n ...(line.modifiers?.length\n ? { modifiers: line.modifiers.map(({ groupId, optionId }) => ({ groupId, optionId })) }\n : {}),\n ...(line.included ? { included: true } : {}),\n ...(line.description ? { description: line.description } : {}),\n }\n}\n"],"names":["liftLegacyProduct","variantHasPrice","computeTaxCents","resolveTaxRate","storefrontTaxDecision","allocateCentsByWeight","lineLabelWithModifiers","resolveLineModifiers","QUOTE_STAGE_LABELS","draft","sent","changes_requested","approved","quoteRequiresProofApprovalByDefault","settings","requireProofApproval","quotePaymentBlockedByApproval","quote","revision","Number","proofed","proofs","some","proof","approvedRevision","isQuote","order","channel","isUnpaidQuote","status","formatQuoteNumber","docId","number","slice","toUpperCase","quoteDisplayStatus","key","label","stage","QUOTE_STATUS_COLOR","paid","cancelled","refunded","whole","value","Math","round","isFinite","quoteLineCents","line","included","unitAmountCents","quantity","quoteDiscountCents","itemsCents","discount","items","kind","min","amountCents","pct","max","quoteLineAmounts","lines","grossCents","map","reduce","sum","cents","totalDiscountCents","discountCents","netCents","index","computeQuoteTotals","input","amounts","taxableCents","taxExempt","shippingCents","tax","taxCents","feeCents","totalCents","taxAtPayment","quoteOrderTotals","totals","quoteTaxBasisFromSettings","shipTo","decision","reason","pricesIncludeTax","address","String","country","trim","state","origin","rate","undefined","describeQuoteTax","basis","providerLabel","quoteTaxMode","QUOTE_MAX_LINES","QUOTE_MAX_QUANTITY","QUOTE_MAX_UNIT_CENTS","QUOTE_MAX_TEXT","SHORT_TEXT","EMAIL","DOC_ID","clean","intIn","isInteger","parseAddress","raw","source","test","Object","keys","length","parseLine","at","toLocaleString","description","productId","variantId","modifiers","Array","isArray","entry","pick","groupId","optionId","name","sku","ships","parseQuoteInput","customerSource","rawLines","ok","error","entries","push","email","recordSource","recordKind","recordId","customer","record","id","company","emailLower","toLowerCase","phone","discountSource","shipping","shippingSource","billTo","notes","terms","variantOptionsLabel","options","values","join","priceQuoteCatalogLine","rawProduct","request","chosen","product","deletedAt","variant","variants","find","variantLabel","productType","type","priceUsd","extraCents","priceQuoteCustomLine","custom","quoteLineRequestFromLine"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/commerce-quotes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { HostProduct, ProductType } from './commerce'\nimport type { QuoteRequestRef } from './commerce-quote-request'\nimport { liftLegacyProduct, variantHasPrice } from './commerce'\nimport type {\n HostOrder,\n OrderAddress,\n OrderLineItem,\n OrderStatus,\n OrderTotals,\n} from './commerce-orders'\nimport { computeTaxCents, resolveTaxRate, type TaxSettings } from './commerce-tax'\nimport { storefrontTaxDecision, type StorefrontTaxMode } from './commerce-tax-decision'\nimport { allocateCentsByWeight } from './commerce-tax-engine'\nimport {\n lineLabelWithModifiers,\n resolveLineModifiers,\n type ModifierSelection,\n} from './product-modifiers'\n\n/*==========================================\n * QUOTES (AGL-4093).\n *\n * A quote is an order that has not been paid yet: a document in\n * `hosts/{hostId}/orders` with `channel: 'quote'`, `status: 'pending'` and a\n * `quote` section holding what only a quote has (the customer it is for, its\n * notes and terms, its discount and shipping, where it stands with the\n * business). Its `number` comes from the store's order counter, so the quote\n * and the order it becomes are one number and one document: paying it flips\n * `status` the way a paid draft order flips.\n *\n * WHAT KEEPS AN UNPAID QUOTE OUT OF EVERY ORDER LIST. A quote document\n * carries none of the fields the order readers ask their queries about:\n * no `createdAtMs` (the Orders list, its filters, the money tiles, the\n * analytics, the exports and the reports all order or range on it), none of\n * the list fields `withOrderListFields` stamps (`customerEmailLower`, the\n * search tokens, `requiresShipping`, `productIds`), and no top-level\n * `customerEmail`, `shippingAddress` or `billingAddress` (the customer's\n * account page, gated content and reviews look orders up by the buyer's\n * address). The customer lives in `quote.customer` until the quote is paid,\n * and whatever pays it writes those order fields then. `isUnpaidQuote` is the\n * test for the few readers that reach orders some other way (by number, by\n * document id, or by a header sort on `status`, `channel` or `number`).\n *\n * Pure: the console's live totals, the server's stored totals, and later the\n * quote's PDF and pay page all come from `computeQuoteTotals`.\n *=========================================*/\n\n/** Where a quote stands with the business. The customer side has no states. */\nexport type QuoteStage = 'draft' | 'sent' | 'changes_requested' | 'approved'\n\nexport const QUOTE_STAGE_LABELS: Record<QuoteStage, string> = {\n draft: 'Draft',\n sent: 'Sent',\n changes_requested: 'Changes requested',\n approved: 'Approved',\n}\n\n/** The person a quote is for: a record the workspace keeps, typed details, or both. */\nexport interface QuoteCustomer {\n /** The person record, as the person-records seam names it (`contact`, …). */\n record?: { kind: string; id: string } | null\n name?: string\n company?: string\n email?: string\n /** `email` lower-cased: what a person erasure finds the quote by. */\n emailLower?: string\n phone?: string\n}\n\n/** Money off the whole quote: an amount, or a percentage of the charged lines. */\nexport interface QuoteDiscount {\n kind: 'amount' | 'percent'\n /** With `amount`: the cents off. */\n amountCents?: number\n /** With `percent`: the percentage off. */\n pct?: number\n}\n\n/** Shipping the business prices by hand. */\nexport interface QuoteShipping {\n amountCents: number\n}\n\n/** Why a quote carries no tax. */\nexport type QuoteNoTaxReason =\n | 'not-collected'\n | 'prices-include-tax'\n | 'no-rate'\n | 'undecided'\n\n/** Where a quote's tax comes from. */\nexport type QuoteTaxKind = 'rate' | 'service' | 'at-payment' | 'none'\n\n/**\n * How a quote's tax was worked out, stored beside the figure so the quote can\n * say it: one of the store's own rates (`rate`, with `pct` and its `label`),\n * its connected tax service's answer (`service`, with `taxCents` and\n * `providerLabel`), Stripe Tax at payment (`at-payment`), or no tax and why\n * (`none`, with `reason`). One flat shape, so the apps can read it as is.\n */\nexport interface QuoteTaxBasis {\n kind: QuoteTaxKind\n pct?: number\n label?: string\n taxCents?: number\n providerLabel?: string\n reason?: QuoteNoTaxReason\n}\n\n/** A proof attached to one revision of a quote: what names it, and the revision it is for. */\nexport interface QuoteProofRef {\n id: string\n revision: number\n}\n\n/**\n * A proof the business attached to a revision (AGL-4096): an image or a\n * design file in the site's private storage, under the quote.\n */\nexport interface QuoteProof extends QuoteProofRef {\n name: string\n contentType: string\n size: number\n /** The private Storage object. Never sent to the customer. */\n path: string\n addedAtMs: number\n /** Who attached it: an email, else a uid. */\n addedBy: string\n}\n\n/** A file the customer sent with a response. */\nexport interface QuoteResponseFile {\n id: string\n name: string\n contentType: string\n size: number\n /** The private Storage object. Never sent to the customer. */\n path: string\n}\n\n/**\n * The customer's answer to a revision's proofs (AGL-4096): approved under a\n * typed name, or changes asked for in a comment, with artwork if they had\n * some. The address and browser are kept as the record of who answered.\n */\nexport interface QuoteCustomerResponse {\n action: 'approve' | 'changes'\n revision: number\n atMs: number\n /** With `approve`: the name they typed. */\n name?: string\n /** With `changes`: what to change. */\n comment?: string\n /** With `changes`: files they sent. */\n files?: QuoteResponseFile[]\n ip: string | null\n userAgent: string | null\n}\n\n/** The record of a revision's approval. */\nexport interface QuoteApproval {\n revision: number\n name: string\n atMs: number\n ip: string | null\n userAgent: string | null\n}\n\n/**\n * A payment that reached a quote it could not pay: one made through a pay\n * link the business replaced or for a price it has since changed\n * (`superseded`), or one made after the quote was already paid\n * (`already-paid`). The money is in Stripe and the quote records no sale for\n * it; the business refunds it or reconciles it by hand.\n */\nexport interface QuoteUnmatchedPayment {\n sessionId: string\n paymentIntentId: string | null\n amountCents: number\n atMs: number\n reason: 'superseded' | 'already-paid'\n}\n\n/**\n * The conversation the quote's emails keep in the customer's mailbox\n * (AGL-4097): the latest message's `Message-ID`, every id in the\n * conversation oldest first, and the subject it runs under. The shape of\n * `EmailThread` (`@aglyn/shared-util-email`), spelled out so the model needs\n * no email library. Started by the first email about the quote (a request's\n * confirmation, or the quote itself); every later one replies into it.\n */\nexport interface QuoteEmailThread {\n messageId: string\n references: string[]\n subject: string\n}\n\n/** One time the quote was emailed to the customer (AGL-4097). */\nexport interface QuoteSendRecord {\n /** The revision the email carried. */\n revision: number\n to: string\n subject: string\n atMs: number\n /** Who sent it: an email, else a uid. */\n by: string\n /** The `Message-ID` the email was sent with. */\n messageId: string\n /** The mail provider's own id for the message, when it answered one. */\n providerId: string | null\n /** The pay link version the email carried. */\n linkVersion: number\n}\n\n/** One shipping rate a draft order offered (AGL-4099), as checkout's resolved rates name it. */\nexport interface OrderDraftShippingRate {\n rateId: string\n name: string\n amountCents: number\n}\n\n/** The shipping a draft order offered when it was made (AGL-4099); see `HostOrder.draftShipping`. */\nexport interface OrderDraftShipping {\n /** Where its rates ship; empty when it offers none. */\n countries: string[]\n /** The rates the buyer picks from at payment; empty when it ships nothing. */\n options: OrderDraftShippingRate[]\n}\n\n/** The lasting pay link of an order that is not a quote (AGL-4099); see `HostOrder.payLink`. */\nexport interface OrderPayLink {\n /** The link's version, absent reading as 1. */\n version?: number\n /** Payments through the link the order could not take, as a quote records them. */\n unmatchedPayments?: QuoteUnmatchedPayment[]\n}\n\n/** One time an order's (or a quote's) invoice was emailed to the customer (AGL-4099). */\nexport interface OrderInvoiceSendRecord {\n to: string\n subject: string\n atMs: number\n /** Who sent it: an email, else a uid. */\n by: string\n /** The `Message-ID` the email was sent with. */\n messageId: string\n /** The mail provider's own id for the message, when it answered one. */\n providerId: string | null\n /** The invoice said it was paid (the record), rather than asking for payment. */\n paid: boolean\n /** The pay link version an unpaid invoice carried; null on a paid one. */\n linkVersion: number | null\n /** The total the invoice showed. */\n totalCents: number\n}\n\n/** What only a quote has, on its order document. */\nexport interface OrderQuote {\n stage: QuoteStage\n /** Which version of the quote this is; versions are what proofs and approvals name. */\n revision: number\n customer: QuoteCustomer\n billTo?: OrderAddress | null\n shipTo?: OrderAddress | null\n notes?: string\n terms?: string\n discount?: QuoteDiscount | null\n shipping?: QuoteShipping | null\n tax: QuoteTaxBasis\n /** The customer approves the current revision's proof before they can pay. Absent reads as off. */\n requireProofApproval?: boolean\n /** The current revision's proofs; earlier revisions keep theirs in `revisions/{n}`. */\n proofs?: QuoteProof[]\n /** The customer's answers to the current revision, oldest first; the last one counts. */\n responses?: QuoteCustomerResponse[]\n /** The current revision's approval, while its latest answer is one. */\n approval?: QuoteApproval | null\n /** The revision the customer approved, when they have. */\n approvedRevision?: number | null\n /**\n * Which pay link opens the quote: a link signed for an older version shows\n * the customer that the quote was replaced, and nothing of it. Absent reads\n * as 1. Bumped when the business replaces the link.\n */\n linkVersion?: number\n /** The conversation its emails keep in the customer's mailbox (AGL-4097). */\n emailThread?: QuoteEmailThread | null\n /** Every time it was emailed to the customer, oldest first (AGL-4097). */\n sends?: QuoteSendRecord[]\n /**\n * The request it was started from (AGL-4098): a \"Request a quote\" form's\n * submission, by id. Its files are listed on the quote as the customer's\n * ({@link requestFiles}), and its confirmation's conversation is the\n * quote's {@link emailThread} until the quote is first sent.\n */\n request?: QuoteRequestRef | null\n /** The request's files, where the request keeps them; downloaded through the quote's own route. */\n requestFiles?: QuoteResponseFile[]\n /** When the customer paid it. */\n paidAtMs?: number\n /** Payments the quote could not take; see {@link QuoteUnmatchedPayment}. */\n unmatchedPayments?: QuoteUnmatchedPayment[]\n createdAtMs: number\n updatedAtMs: number\n /** Who created and last changed it: an email, else a uid. */\n createdBy: string\n updatedBy: string\n}\n\n/** The store-wide quote defaults, at `hosts/{hostId}/settings/store` under `quotes`. */\nexport interface QuoteStoreSettings {\n /** What `requireProofApproval` a new quote starts with. Absent reads as off. */\n requireProofApproval?: boolean\n}\n\n/** Whether new quotes start out asking the customer to approve the proof first. */\nexport function quoteRequiresProofApprovalByDefault(\n settings: QuoteStoreSettings | null | undefined,\n): boolean {\n return settings?.requireProofApproval === true\n}\n\n/**\n * Whether paying waits on the customer approving the proof: the quote asks for\n * approval, its current revision has a proof, and that revision is not the\n * one the customer approved. A revision with no proof never holds a payment.\n */\nexport function quotePaymentBlockedByApproval(\n quote:\n | (Pick<OrderQuote, 'requireProofApproval' | 'revision' | 'approvedRevision'> & {\n proofs?: readonly QuoteProofRef[]\n })\n | null\n | undefined,\n): boolean {\n if (!quote || quote.requireProofApproval !== true) return false\n const revision = Number(quote.revision ?? 1)\n const proofed = (quote.proofs ?? []).some((proof) => Number(proof?.revision) === revision)\n if (!proofed) return false\n return Number(quote.approvedRevision) !== revision\n}\n\n/** Whether an order document is a quote, paid or not. */\nexport function isQuote(order: Pick<Partial<HostOrder>, 'channel'> | null | undefined): boolean {\n return order?.channel === 'quote'\n}\n\n/** Whether an order document is a quote nobody has paid yet, which no order list counts. */\nexport function isUnpaidQuote(\n order: { channel?: unknown; status?: unknown } | null | undefined,\n): boolean {\n return order?.channel === 'quote' && order?.status === 'pending'\n}\n\n/** `Quote #1042`; an order number until it is paid. */\nexport function formatQuoteNumber(order: Pick<HostOrder, 'number'>, docId?: string): string {\n if (order.number != null) return `Quote #${order.number}`\n return docId ? `Quote #${docId.slice(-6).toUpperCase()}` : 'Quote'\n}\n\n/** A quote's status as its list shows it: its stage until it is paid. */\nexport function quoteDisplayStatus(order: {\n status?: OrderStatus | string\n quote?: Pick<OrderQuote, 'stage'> | null\n}): { key: QuoteStage | 'paid' | 'cancelled' | 'refunded'; label: string } {\n if (order.status === 'cancelled') return { key: 'cancelled', label: 'Canceled' }\n if (order.status === 'refunded') return { key: 'refunded', label: 'Refunded' }\n if (order.status && order.status !== 'pending') return { key: 'paid', label: 'Paid' }\n const stage = (order.quote?.stage ?? 'draft') as QuoteStage\n return { key: stage, label: QUOTE_STAGE_LABELS[stage] ?? QUOTE_STAGE_LABELS.draft }\n}\n\n/** Chip colors for {@link quoteDisplayStatus}, as the orders list colors its statuses. */\nexport const QUOTE_STATUS_COLOR: Record<\n ReturnType<typeof quoteDisplayStatus>['key'],\n 'default' | 'success' | 'info' | 'warning' | 'error'\n> = {\n draft: 'default',\n sent: 'info',\n changes_requested: 'warning',\n approved: 'info',\n paid: 'success',\n cancelled: 'default',\n refunded: 'error',\n}\n\n/*==========================================\n * TOTALS\n *=========================================*/\n\n/** A line as the totals read it. */\nexport type QuoteTotalsLine = Pick<OrderLineItem, 'unitAmountCents' | 'quantity'> & {\n included?: boolean\n taxExempt?: boolean\n}\n\nexport interface QuoteTotalsInput {\n lines: readonly QuoteTotalsLine[]\n discount?: QuoteDiscount | null\n shippingCents?: number\n tax: QuoteTaxBasis\n}\n\n/** A quote's totals: the order's, plus what the tax was computed on. */\nexport interface QuoteTotals extends OrderTotals {\n /** The taxable lines after their share of the discount. */\n taxableCents: number\n /** Stripe Tax works the tax out when the customer pays; `taxCents` is 0 until then. */\n taxAtPayment: boolean\n}\n\nconst whole = (value: unknown): number => {\n const number = Math.round(Number(value))\n return Number.isFinite(number) && number > 0 ? number : 0\n}\n\n/** One line's charge: nothing for an included line. */\nexport function quoteLineCents(line: QuoteTotalsLine): number {\n if (line.included) return 0\n return whole(line.unitAmountCents) * whole(line.quantity)\n}\n\n/** The discount in cents on `itemsCents`, never more than it. */\nexport function quoteDiscountCents(\n itemsCents: number,\n discount: QuoteDiscount | null | undefined,\n): number {\n const items = whole(itemsCents)\n if (!discount) return 0\n if (discount.kind === 'amount') return Math.min(items, whole(discount.amountCents))\n if (discount.kind !== 'percent') return 0\n const pct = Math.min(100, Math.max(0, Number(discount.pct) || 0))\n return Math.min(items, Math.round((items * pct) / 100))\n}\n\n/**\n * Each line's charge, its share of the discount (in proportion to the\n * charges, as a checkout spreads a coupon) and what is left: the amounts a tax\n * service is asked about, line by line.\n */\nexport function quoteLineAmounts(\n lines: readonly QuoteTotalsLine[],\n discount: QuoteDiscount | null | undefined,\n): { grossCents: number[]; discountCents: number[]; netCents: number[]; itemsCents: number; totalDiscountCents: number } {\n const grossCents = lines.map(quoteLineCents)\n const itemsCents = grossCents.reduce((sum, cents) => sum + cents, 0)\n const totalDiscountCents = quoteDiscountCents(itemsCents, discount)\n const discountCents = allocateCentsByWeight(totalDiscountCents, grossCents)\n return {\n grossCents,\n discountCents,\n netCents: grossCents.map((cents, index) => cents - discountCents[index]),\n itemsCents,\n totalDiscountCents,\n }\n}\n\n/**\n * A quote's totals. The discount comes off before tax; a store rate taxes the\n * taxable lines (not shipping, as a draft order's rate does); a tax service's\n * answer is taken as given; Stripe Tax and no-tax add nothing now.\n */\nexport function computeQuoteTotals(input: QuoteTotalsInput): QuoteTotals {\n const amounts = quoteLineAmounts(input.lines, input.discount)\n const taxableCents = amounts.netCents.reduce(\n (sum, cents, index) => sum + (input.lines[index]?.taxExempt ? 0 : cents),\n 0,\n )\n const shippingCents = whole(input.shippingCents)\n const tax = input.tax\n const taxCents =\n tax.kind === 'rate'\n ? computeTaxCents(taxableCents, Number(tax.pct) || 0)\n : tax.kind === 'service'\n ? whole(tax.taxCents)\n : 0\n return {\n itemsCents: amounts.itemsCents,\n discountCents: amounts.totalDiscountCents,\n shippingCents,\n taxCents,\n // The platform fee is set when a payment is created, from what it charges.\n feeCents: 0,\n totalCents: amounts.itemsCents - amounts.totalDiscountCents + shippingCents + taxCents,\n taxableCents,\n taxAtPayment: tax.kind === 'at-payment',\n }\n}\n\n/** The order totals a quote document stores. */\nexport function quoteOrderTotals(totals: QuoteTotals): OrderTotals {\n return {\n itemsCents: totals.itemsCents,\n discountCents: totals.discountCents,\n shippingCents: totals.shippingCents,\n taxCents: totals.taxCents,\n feeCents: totals.feeCents,\n totalCents: totals.totalCents,\n }\n}\n\n/*==========================================\n * TAX\n *=========================================*/\n\n/**\n * The tax basis the store's own settings give a quote, before any tax service\n * is asked: Stripe Tax at payment, no tax (and why), or the store's rate for\n * where the quote ships — its ship-to when it has a country, else the store's\n * own address, as the storefront taxes a sale it cannot see the destination of.\n */\nexport function quoteTaxBasisFromSettings(\n settings: TaxSettings | null | undefined,\n shipTo: Pick<OrderAddress, 'country' | 'state'> | null | undefined,\n): QuoteTaxBasis {\n const decision = storefrontTaxDecision({ settings })\n if (decision.kind === 'undecided') return { kind: 'none', reason: 'undecided' }\n if (decision.kind === 'stripe-automatic') return { kind: 'at-payment' }\n if (decision.kind !== 'manual') return { kind: 'none', reason: 'not-collected' }\n if (settings?.pricesIncludeTax) return { kind: 'none', reason: 'prices-include-tax' }\n const address = String(shipTo?.country ?? '').trim()\n ? { country: shipTo?.country, state: shipTo?.state }\n : (settings?.origin ?? {})\n const rate = resolveTaxRate(settings ?? undefined, address)\n if (!rate || !(Number(rate.pct) > 0)) return { kind: 'none', reason: 'no-rate' }\n return { kind: 'rate', pct: Number(rate.pct), ...(rate.label ? { label: rate.label } : {}) }\n}\n\n/** The tax line's label on a quote. */\nexport function describeQuoteTax(basis: QuoteTaxBasis | null | undefined): string {\n if (!basis) return 'Tax'\n switch (basis.kind) {\n case 'rate':\n return basis.label ? `${basis.label} (${basis.pct ?? 0}%)` : `Tax (${basis.pct ?? 0}%)`\n case 'service':\n return basis.providerLabel ? `Tax (${basis.providerLabel})` : 'Tax'\n case 'at-payment':\n return 'Tax is added at payment'\n case 'none':\n return basis.reason === 'prices-include-tax'\n ? 'Prices include tax'\n : basis.reason === 'no-rate'\n ? 'No tax rate for this address'\n : basis.reason === 'undecided'\n ? 'Sales tax is not set up'\n : 'No sales tax'\n default:\n return 'Tax'\n }\n}\n\n/** The order's `taxMode` for a quote's basis; absent while nobody has decided. */\nexport function quoteTaxMode(basis: QuoteTaxBasis, taxCents: number): StorefrontTaxMode | undefined {\n if (basis.kind === 'at-payment') return 'stripe-automatic'\n if (basis.kind === 'none' && basis.reason === 'undecided') return undefined\n return taxCents > 0 ? 'manual' : 'none'\n}\n\n/*==========================================\n * WHAT THE CONSOLE SENDS\n *=========================================*/\n\nexport const QUOTE_MAX_LINES = 100\nexport const QUOTE_MAX_QUANTITY = 9_999\n/** $1,000,000 a unit: the most a typed line may cost. */\nexport const QUOTE_MAX_UNIT_CENTS = 100_000_000\nexport const QUOTE_MAX_TEXT = 5_000\nconst SHORT_TEXT = 200\n\n/** A product line as the console asks for it: what it is, never what it costs. */\nexport interface QuoteCatalogLineRequest {\n kind: 'catalog'\n productId: string\n variantId?: string\n quantity: number\n modifiers?: ModifierSelection[]\n included?: boolean\n description?: string\n}\n\n/** A line typed on the quote, which no product stands behind. */\nexport interface QuoteCustomLineRequest {\n kind: 'custom'\n name: string\n description?: string\n sku?: string\n unitAmountCents: number\n quantity: number\n /** Whether it is something to ship (else a service, which never ships). */\n ships?: boolean\n included?: boolean\n}\n\nexport type QuoteLineRequest = QuoteCatalogLineRequest | QuoteCustomLineRequest\n\n/** Everything a business edits on a quote. */\nexport interface QuoteInput {\n lines: QuoteLineRequest[]\n customer: QuoteCustomer\n billTo: OrderAddress | null\n shipTo: OrderAddress | null\n notes: string\n terms: string\n discount: QuoteDiscount | null\n shipping: QuoteShipping | null\n /** Absent: the store's default (on create) or unchanged (on update). */\n requireProofApproval?: boolean\n}\n\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\nconst DOC_ID = /^(?!__.*__$)[^/]{1,200}$/\n\nconst clean = (value: unknown, max = SHORT_TEXT): string =>\n typeof value === 'string' ? value.trim().slice(0, max) : ''\n\nconst intIn = (value: unknown, min: number, max: number): number | null => {\n const number = Number(value)\n return Number.isInteger(number) && number >= min && number <= max ? number : null\n}\n\nfunction parseAddress(raw: unknown): OrderAddress | null {\n if (!raw || typeof raw !== 'object') return null\n const source = raw as Record<string, unknown>\n const address: OrderAddress = {}\n for (const key of ['name', 'line1', 'line2', 'city', 'state', 'postalCode', 'phone'] as const) {\n const value = clean(source[key])\n if (value) address[key] = value\n }\n const country = clean(source['country'], 2).toUpperCase()\n if (/^[A-Z]{2}$/.test(country)) address.country = country\n return Object.keys(address).length ? address : null\n}\n\nfunction parseLine(raw: unknown, index: number): QuoteLineRequest | string {\n const source = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>\n const at = `Line ${index + 1}`\n const quantity = intIn(source['quantity'], 1, QUOTE_MAX_QUANTITY)\n if (quantity === null) return `${at}: enter a quantity from 1 to ${QUOTE_MAX_QUANTITY.toLocaleString('en-US')}.`\n const included = source['included'] === true\n const description = clean(source['description'], QUOTE_MAX_TEXT)\n if (source['kind'] === 'catalog') {\n const productId = clean(source['productId'])\n if (!DOC_ID.test(productId)) return `${at}: choose a product.`\n const variantId = clean(source['variantId'])\n const modifiers = Array.isArray(source['modifiers'])\n ? (source['modifiers'] as unknown[]).slice(0, 200).map((entry) => {\n const pick = (entry ?? {}) as Record<string, unknown>\n return { groupId: clean(pick['groupId']), optionId: clean(pick['optionId']) }\n })\n : []\n return {\n kind: 'catalog',\n productId,\n ...(variantId ? { variantId } : {}),\n quantity,\n ...(modifiers.length ? { modifiers } : {}),\n ...(included ? { included } : {}),\n ...(description ? { description } : {}),\n }\n }\n if (source['kind'] === 'custom') {\n const name = clean(source['name'])\n if (!name) return `${at}: name the item.`\n const unitAmountCents = included ? 0 : intIn(source['unitAmountCents'], 0, QUOTE_MAX_UNIT_CENTS)\n if (unitAmountCents === null) return `${at}: enter a price from $0 to $1,000,000.`\n const sku = clean(source['sku'], 100)\n return {\n kind: 'custom',\n name,\n ...(description ? { description } : {}),\n ...(sku ? { sku } : {}),\n unitAmountCents,\n quantity,\n ships: source['ships'] !== false,\n ...(included ? { included } : {}),\n }\n }\n return `${at}: add a product or type an item.`\n}\n\n/**\n * The console's quote, checked and trimmed, or what to fix. Prices on product\n * lines are not read from here: the server prices them from the product.\n */\nexport function parseQuoteInput(raw: unknown): { ok: true; input: QuoteInput } | { ok: false; error: string } {\n const source = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>\n const rawLines = Array.isArray(source['lines']) ? (source['lines'] as unknown[]) : []\n if (rawLines.length === 0) return { ok: false, error: 'Add at least one line to the quote.' }\n if (rawLines.length > QUOTE_MAX_LINES) {\n return { ok: false, error: `A quote holds up to ${QUOTE_MAX_LINES} lines.` }\n }\n const lines: QuoteLineRequest[] = []\n for (const [index, entry] of rawLines.entries()) {\n const line = parseLine(entry, index)\n if (typeof line === 'string') return { ok: false, error: line }\n lines.push(line)\n }\n\n const customerSource = (source['customer'] && typeof source['customer'] === 'object'\n ? source['customer']\n : {}) as Record<string, unknown>\n const email = clean(customerSource['email'])\n if (email && !EMAIL.test(email)) return { ok: false, error: 'Enter a valid email for the customer.' }\n const recordSource = (customerSource['record'] ?? null) as Record<string, unknown> | null\n const recordKind = clean(recordSource?.['kind'], 40)\n const recordId = clean(recordSource?.['id'])\n const customer: QuoteCustomer = {\n ...(recordKind && DOC_ID.test(recordId) ? { record: { kind: recordKind, id: recordId } } : {}),\n ...(clean(customerSource['name']) ? { name: clean(customerSource['name']) } : {}),\n ...(clean(customerSource['company']) ? { company: clean(customerSource['company']) } : {}),\n ...(email ? { email, emailLower: email.toLowerCase() } : {}),\n ...(clean(customerSource['phone'], 40) ? { phone: clean(customerSource['phone'], 40) } : {}),\n }\n\n let discount: QuoteDiscount | null = null\n const discountSource = (source['discount'] ?? null) as Record<string, unknown> | null\n if (discountSource?.['kind'] === 'amount') {\n const amountCents = intIn(discountSource['amountCents'], 0, QUOTE_MAX_UNIT_CENTS)\n if (amountCents === null) return { ok: false, error: 'Enter a discount from $0 to $1,000,000.' }\n if (amountCents > 0) discount = { kind: 'amount', amountCents }\n } else if (discountSource?.['kind'] === 'percent') {\n const pct = Number(discountSource['pct'])\n if (!Number.isFinite(pct) || pct < 0 || pct > 100) {\n return { ok: false, error: 'Enter a discount from 0% to 100%.' }\n }\n if (pct > 0) discount = { kind: 'percent', pct: Math.round(pct * 100) / 100 }\n }\n\n let shipping: QuoteShipping | null = null\n const shippingSource = (source['shipping'] ?? null) as Record<string, unknown> | null\n if (shippingSource) {\n const amountCents = intIn(shippingSource['amountCents'], 0, QUOTE_MAX_UNIT_CENTS)\n if (amountCents === null) return { ok: false, error: 'Enter shipping from $0 to $1,000,000.' }\n shipping = { amountCents }\n }\n\n return {\n ok: true,\n input: {\n lines,\n customer,\n billTo: parseAddress(source['billTo']),\n shipTo: parseAddress(source['shipTo']),\n notes: clean(source['notes'], QUOTE_MAX_TEXT),\n terms: clean(source['terms'], QUOTE_MAX_TEXT),\n discount,\n shipping,\n ...(typeof source['requireProofApproval'] === 'boolean'\n ? { requireProofApproval: source['requireProofApproval'] }\n : {}),\n },\n }\n}\n\n/** A priced quote line, and whether its product is tax-exempt. */\nexport type PricedQuoteLine = OrderLineItem & { taxExempt?: boolean }\n\n/** The variant's label as order lines print it: its option values, or nothing for a default. */\nfunction variantOptionsLabel(options: Record<string, string> | undefined): string | undefined {\n return options && Object.keys(options).length ? Object.values(options).join(' / ') : undefined\n}\n\n/**\n * A product line priced from the PRODUCT: its variant's price plus the chosen\n * add-ons, as the register prices one. An included line keeps what it is and\n * charges nothing.\n */\nexport function priceQuoteCatalogLine(\n rawProduct: Partial<HostProduct> | null | undefined,\n request: QuoteCatalogLineRequest,\n): { ok: true; line: PricedQuoteLine } | { ok: false; error: string } {\n if (!rawProduct || rawProduct.deletedAt) {\n return { ok: false, error: 'A product on this quote no longer exists. Remove it and add it again.' }\n }\n const product = liftLegacyProduct(rawProduct)\n const variant = request.variantId\n ? product.variants.find((entry) => entry.id === request.variantId)\n : product.variants[0]\n if (!variant) return { ok: false, error: `Choose an option of ${product.name} that still exists.` }\n if (!request.included && !variantHasPrice(variant)) {\n return { ok: false, error: `Set a price for ${product.name} before quoting it.` }\n }\n const chosen = resolveLineModifiers(product, request.modifiers, variant.id)\n if (!chosen.ok) return { ok: false, error: chosen.error ?? `Check the choices on ${product.name}.` }\n const variantLabel = lineLabelWithModifiers(variantOptionsLabel(variant.options), chosen.modifiers)\n return {\n ok: true,\n line: {\n productId: request.productId,\n ...(variant.id !== 'default' ? { variantId: variant.id } : {}),\n name: product.name,\n ...(variantLabel ? { variantLabel } : {}),\n ...(variant.sku ? { sku: variant.sku } : {}),\n productType: (product.type ?? 'physical') as ProductType,\n quantity: request.quantity,\n unitAmountCents: request.included\n ? 0\n : Math.round(Number(variant.priceUsd) * 100) + chosen.extraCents,\n ...(chosen.modifiers.length ? { modifiers: chosen.modifiers } : {}),\n ...(request.included ? { included: true } : {}),\n ...(request.description ? { description: request.description } : {}),\n ...(product.taxExempt ? { taxExempt: true } : {}),\n },\n }\n}\n\n/** A typed line: no product, so nothing reads stock, a supplier or a download for it. */\nexport function priceQuoteCustomLine(request: QuoteCustomLineRequest): PricedQuoteLine {\n return {\n productId: '',\n custom: true,\n name: request.name,\n ...(request.sku ? { sku: request.sku } : {}),\n productType: request.ships === false ? 'service' : 'physical',\n quantity: request.quantity,\n unitAmountCents: request.included ? 0 : request.unitAmountCents,\n ...(request.included ? { included: true } : {}),\n ...(request.description ? { description: request.description } : {}),\n }\n}\n\n/** The request a stored line is edited as: what the console sends back on a save. */\nexport function quoteLineRequestFromLine(line: OrderLineItem): QuoteLineRequest {\n if (line.custom || !line.productId) {\n return {\n kind: 'custom',\n name: line.name,\n ...(line.description ? { description: line.description } : {}),\n ...(line.sku ? { sku: line.sku } : {}),\n unitAmountCents: line.unitAmountCents,\n quantity: line.quantity,\n ships: line.productType !== 'service' && line.productType !== 'digital',\n ...(line.included ? { included: true } : {}),\n }\n }\n return {\n kind: 'catalog',\n productId: line.productId,\n ...(line.variantId ? { variantId: line.variantId } : {}),\n quantity: line.quantity,\n ...(line.modifiers?.length\n ? { modifiers: line.modifiers.map(({ groupId, optionId }) => ({ groupId, optionId })) }\n : {}),\n ...(line.included ? { included: true } : {}),\n ...(line.description ? { description: line.description } : {}),\n }\n}\n"],"names":["liftLegacyProduct","variantHasPrice","computeTaxCents","resolveTaxRate","storefrontTaxDecision","allocateCentsByWeight","lineLabelWithModifiers","resolveLineModifiers","QUOTE_STAGE_LABELS","draft","sent","changes_requested","approved","quoteRequiresProofApprovalByDefault","settings","requireProofApproval","quotePaymentBlockedByApproval","quote","revision","Number","proofed","proofs","some","proof","approvedRevision","isQuote","order","channel","isUnpaidQuote","status","formatQuoteNumber","docId","number","slice","toUpperCase","quoteDisplayStatus","key","label","stage","QUOTE_STATUS_COLOR","paid","cancelled","refunded","whole","value","Math","round","isFinite","quoteLineCents","line","included","unitAmountCents","quantity","quoteDiscountCents","itemsCents","discount","items","kind","min","amountCents","pct","max","quoteLineAmounts","lines","grossCents","map","reduce","sum","cents","totalDiscountCents","discountCents","netCents","index","computeQuoteTotals","input","amounts","taxableCents","taxExempt","shippingCents","tax","taxCents","feeCents","totalCents","taxAtPayment","quoteOrderTotals","totals","quoteTaxBasisFromSettings","shipTo","decision","reason","pricesIncludeTax","address","String","country","trim","state","origin","rate","undefined","describeQuoteTax","basis","providerLabel","quoteTaxMode","QUOTE_MAX_LINES","QUOTE_MAX_QUANTITY","QUOTE_MAX_UNIT_CENTS","QUOTE_MAX_TEXT","SHORT_TEXT","EMAIL","DOC_ID","clean","intIn","isInteger","parseAddress","raw","source","test","Object","keys","length","parseLine","at","toLocaleString","description","productId","variantId","modifiers","Array","isArray","entry","pick","groupId","optionId","name","sku","ships","parseQuoteInput","customerSource","rawLines","ok","error","entries","push","email","recordSource","recordKind","recordId","customer","record","id","company","emailLower","toLowerCase","phone","discountSource","shipping","shippingSource","billTo","notes","terms","variantOptionsLabel","options","values","join","priceQuoteCatalogLine","rawProduct","request","chosen","product","deletedAt","variant","variants","find","variantLabel","productType","type","priceUsd","extraCents","priceQuoteCustomLine","custom","quoteLineRequestFromLine"],"mappings":"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"}
|
|
@@ -451,10 +451,28 @@ export declare function productStockFields(product: Pick<HostProduct, 'variants'
|
|
|
451
451
|
inventory: number | null;
|
|
452
452
|
soldOut: boolean;
|
|
453
453
|
};
|
|
454
|
+
/**
|
|
455
|
+
* How many variants a product's options make (AGL-4100): the product of
|
|
456
|
+
* each usable option's value count, 1 with none. Counted, never built, so
|
|
457
|
+
* an answer past the ceiling costs nothing to reach.
|
|
458
|
+
*/
|
|
459
|
+
export declare function variantCombinationCount(options: ProductOption[] | undefined): number;
|
|
460
|
+
/**
|
|
461
|
+
* Why a product's options cannot be saved as variants, or null when they can
|
|
462
|
+
* (AGL-4100): more combinations than a product holds. Said with the count and
|
|
463
|
+
* the way out, because the choices a merchant typed are kept, not dropped —
|
|
464
|
+
* {@link expandVariantMatrix} would stop at the ceiling, and a product whose
|
|
465
|
+
* options name values no variant sells offers combinations it cannot sell.
|
|
466
|
+
*/
|
|
467
|
+
export declare function variantMatrixProblem(options: ProductOption[] | undefined): string | null;
|
|
454
468
|
/**
|
|
455
469
|
* Cartesian product of option values → the variant option combinations
|
|
456
470
|
* the products hub materializes into a variants matrix. Empty/absent
|
|
457
471
|
* options yield a single default combination ({}).
|
|
472
|
+
*
|
|
473
|
+
* Stops at {@link COMMERCE_MAX_VARIANTS}: ask {@link variantMatrixProblem}
|
|
474
|
+
* first, and refuse rather than build when it answers (AGL-4100) — the
|
|
475
|
+
* matrix past the ceiling is dropped here, silently.
|
|
458
476
|
*/
|
|
459
477
|
export declare function expandVariantMatrix(options: ProductOption[] | undefined): Array<Record<string, string>>;
|
|
460
478
|
/**
|
|
@@ -248,10 +248,36 @@ export const COMMERCE_SLUG_MAX_LENGTH = 80;
|
|
|
248
248
|
soldOut: inventory != null && inventory <= 0 && product.oversellPolicy !== 'backorder'
|
|
249
249
|
};
|
|
250
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* How many variants a product's options make (AGL-4100): the product of
|
|
253
|
+
* each usable option's value count, 1 with none. Counted, never built, so
|
|
254
|
+
* an answer past the ceiling costs nothing to reach.
|
|
255
|
+
*/ export function variantCombinationCount(options) {
|
|
256
|
+
return (options != null ? options : []).filter((option)=>{
|
|
257
|
+
var _ref;
|
|
258
|
+
var _option_values;
|
|
259
|
+
return option.name && ((_ref = (_option_values = option.values) == null ? void 0 : _option_values.length) != null ? _ref : 0) > 0;
|
|
260
|
+
}).reduce((count, option)=>count * option.values.length, 1);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Why a product's options cannot be saved as variants, or null when they can
|
|
264
|
+
* (AGL-4100): more combinations than a product holds. Said with the count and
|
|
265
|
+
* the way out, because the choices a merchant typed are kept, not dropped —
|
|
266
|
+
* {@link expandVariantMatrix} would stop at the ceiling, and a product whose
|
|
267
|
+
* options name values no variant sells offers combinations it cannot sell.
|
|
268
|
+
*/ export function variantMatrixProblem(options) {
|
|
269
|
+
const count = variantCombinationCount(options);
|
|
270
|
+
if (count <= COMMERCE_MAX_VARIANTS) return null;
|
|
271
|
+
return `These options make ${count.toLocaleString('en-US')} combinations, and a product holds at most ` + `${COMMERCE_MAX_VARIANTS}. Remove some values, or move choices that don’t need their own stock ` + '(walls, printing, extras) to Add-ons, priced by option where they cost more on a larger size.';
|
|
272
|
+
}
|
|
251
273
|
/**
|
|
252
274
|
* Cartesian product of option values → the variant option combinations
|
|
253
275
|
* the products hub materializes into a variants matrix. Empty/absent
|
|
254
276
|
* options yield a single default combination ({}).
|
|
277
|
+
*
|
|
278
|
+
* Stops at {@link COMMERCE_MAX_VARIANTS}: ask {@link variantMatrixProblem}
|
|
279
|
+
* first, and refuse rather than build when it answers (AGL-4100) — the
|
|
280
|
+
* matrix past the ceiling is dropped here, silently.
|
|
255
281
|
*/ export function expandVariantMatrix(options) {
|
|
256
282
|
const usable = (options != null ? options : []).filter((option)=>{
|
|
257
283
|
var _ref;
|