@aglyn/plugins-commerce 1.0.0-beta.291 → 1.0.0-beta.293

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.
Files changed (115) hide show
  1. package/README.md +1 -0
  2. package/package.json +13 -13
  3. package/src/lib/components/cart-fulfillment.d.ts +3 -0
  4. package/src/lib/components/cart-fulfillment.js.map +1 -1
  5. package/src/lib/components/cart.js +14 -4
  6. package/src/lib/components/cart.js.map +1 -1
  7. package/src/lib/components/console/host-quotes-card.component.js +11 -9
  8. package/src/lib/components/console/host-quotes-card.component.js.map +1 -1
  9. package/src/lib/components/console/invoice-send.component.js +1 -1
  10. package/src/lib/components/console/invoice-send.component.js.map +1 -1
  11. package/src/lib/components/console/pos/pos-item-dialog.component.js +3 -2
  12. package/src/lib/components/console/pos/pos-item-dialog.component.js.map +1 -1
  13. package/src/lib/components/console/pos-kiosk-page.component.js +2 -1
  14. package/src/lib/components/console/pos-kiosk-page.component.js.map +1 -1
  15. package/src/lib/components/console/pos-page.component.js +1 -1
  16. package/src/lib/components/console/pos-page.component.js.map +1 -1
  17. package/src/lib/components/console/product-add-ons-fields.component.d.ts +41 -0
  18. package/src/lib/components/console/product-add-ons-fields.component.js +356 -0
  19. package/src/lib/components/console/product-add-ons-fields.component.js.map +1 -0
  20. package/src/lib/components/console/product-editor-dialog.component.js +31 -5
  21. package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
  22. package/src/lib/components/console/product-register-fields.component.d.ts +4 -5
  23. package/src/lib/components/console/product-register-fields.component.js +8 -197
  24. package/src/lib/components/console/product-register-fields.component.js.map +1 -1
  25. package/src/lib/components/console/products-hub-zone.component.js +6 -0
  26. package/src/lib/components/console/products-hub-zone.component.js.map +1 -1
  27. package/src/lib/components/console/quote-pay-link.component.d.ts +8 -1
  28. package/src/lib/components/console/quote-pay-link.component.js +29 -3
  29. package/src/lib/components/console/quote-pay-link.component.js.map +1 -1
  30. package/src/lib/components/console/quote-send.component.js +1 -4
  31. package/src/lib/components/console/quote-send.component.js.map +1 -1
  32. package/src/lib/components/product-add-ons.d.ts +28 -0
  33. package/src/lib/components/product-add-ons.js +136 -0
  34. package/src/lib/components/product-add-ons.js.map +1 -0
  35. package/src/lib/components/product-detail.d.ts +16 -0
  36. package/src/lib/components/product-detail.js +112 -19
  37. package/src/lib/components/product-detail.js.map +1 -1
  38. package/src/lib/components/product-grid.d.ts +8 -2
  39. package/src/lib/components/product-grid.js +6 -1
  40. package/src/lib/components/product-grid.js.map +1 -1
  41. package/src/lib/declarations.console-server.js +15 -0
  42. package/src/lib/declarations.console-server.js.map +1 -1
  43. package/src/lib/model/commerce-cart.d.ts +15 -3
  44. package/src/lib/model/commerce-cart.js +10 -3
  45. package/src/lib/model/commerce-cart.js.map +1 -1
  46. package/src/lib/model/commerce-invoices.d.ts +7 -0
  47. package/src/lib/model/commerce-invoices.js +11 -0
  48. package/src/lib/model/commerce-invoices.js.map +1 -1
  49. package/src/lib/model/commerce-orders.d.ts +6 -3
  50. package/src/lib/model/commerce-orders.js +3 -0
  51. package/src/lib/model/commerce-orders.js.map +1 -1
  52. package/src/lib/model/commerce-pos-kiosk.d.ts +1 -1
  53. package/src/lib/model/commerce-pos-kiosk.js +3 -2
  54. package/src/lib/model/commerce-pos-kiosk.js.map +1 -1
  55. package/src/lib/model/commerce-quote-email.d.ts +14 -0
  56. package/src/lib/model/commerce-quote-email.js +25 -9
  57. package/src/lib/model/commerce-quote-email.js.map +1 -1
  58. package/src/lib/model/commerce-quote-proofs.d.ts +7 -0
  59. package/src/lib/model/commerce-quote-proofs.js +15 -0
  60. package/src/lib/model/commerce-quote-proofs.js.map +1 -1
  61. package/src/lib/model/commerce-quotes.js +1 -1
  62. package/src/lib/model/commerce-quotes.js.map +1 -1
  63. package/src/lib/model/commerce.d.ts +18 -0
  64. package/src/lib/model/commerce.js +26 -0
  65. package/src/lib/model/commerce.js.map +1 -1
  66. package/src/lib/model/product-modifiers.d.ts +74 -4
  67. package/src/lib/model/product-modifiers.js +131 -10
  68. package/src/lib/model/product-modifiers.js.map +1 -1
  69. package/src/lib/server/billing-webhook.js +64 -43
  70. package/src/lib/server/billing-webhook.js.map +1 -1
  71. package/src/lib/server/cart-checkout.js +14 -2
  72. package/src/lib/server/cart-checkout.js.map +1 -1
  73. package/src/lib/server/cart.d.ts +5 -0
  74. package/src/lib/server/cart.js +41 -4
  75. package/src/lib/server/cart.js.map +1 -1
  76. package/src/lib/server/catalog.d.ts +6 -0
  77. package/src/lib/server/catalog.js +2 -0
  78. package/src/lib/server/catalog.js.map +1 -1
  79. package/src/lib/server/checkout-extras.js +3 -1
  80. package/src/lib/server/checkout-extras.js.map +1 -1
  81. package/src/lib/server/checkout.js +28 -3
  82. package/src/lib/server/checkout.js.map +1 -1
  83. package/src/lib/server/local-fulfillment.js +5 -1
  84. package/src/lib/server/local-fulfillment.js.map +1 -1
  85. package/src/lib/server/pos-offline-sync.js +1 -1
  86. package/src/lib/server/pos-offline-sync.js.map +1 -1
  87. package/src/lib/server/pos-order.js +1 -1
  88. package/src/lib/server/pos-order.js.map +1 -1
  89. package/src/lib/server/product.d.ts +26 -0
  90. package/src/lib/server/product.js +18 -0
  91. package/src/lib/server/product.js.map +1 -1
  92. package/src/lib/server/products-write.js +5 -2
  93. package/src/lib/server/products-write.js.map +1 -1
  94. package/src/lib/server/quote-email.js.map +1 -1
  95. package/src/lib/server/quote-link-token.d.ts +4 -2
  96. package/src/lib/server/quote-link-token.js +4 -2
  97. package/src/lib/server/quote-link-token.js.map +1 -1
  98. package/src/lib/server/quote-pay.d.ts +5 -1
  99. package/src/lib/server/quote-pay.js +5 -2
  100. package/src/lib/server/quote-pay.js.map +1 -1
  101. package/src/lib/server/quote-proofs.d.ts +9 -1
  102. package/src/lib/server/quote-proofs.js +92 -23
  103. package/src/lib/server/quote-proofs.js.map +1 -1
  104. package/src/lib/server/quote-upload-keys.d.ts +50 -0
  105. package/src/lib/server/quote-upload-keys.js +86 -0
  106. package/src/lib/server/quote-upload-keys.js.map +1 -0
  107. package/src/lib/server/quote-upload-sweep.d.ts +30 -0
  108. package/src/lib/server/quote-upload-sweep.js +117 -0
  109. package/src/lib/server/quote-upload-sweep.js.map +1 -0
  110. package/src/lib/utils/add-to-cart.d.ts +8 -0
  111. package/src/lib/utils/add-to-cart.js +10 -2
  112. package/src/lib/utils/add-to-cart.js.map +1 -1
  113. package/src/lib/utils/quote-email-content.d.ts +1 -0
  114. package/src/lib/utils/quote-email-content.js +7 -4
  115. package/src/lib/utils/quote-email-content.js.map +1 -1
@@ -94,6 +94,13 @@ export declare function invoiceSends(order: Pick<HostOrder, 'invoiceSends'> | nu
94
94
  export declare function lastInvoiceSend(order: Pick<HostOrder, 'invoiceSends'> | null | undefined): OrderInvoiceSendRecord | null;
95
95
  /** Where the invoice goes: what was typed, else the order's customer, else a quote's. */
96
96
  export declare function invoiceSendRecipient(order: Pick<HostOrder, 'customerEmail' | 'quote'> | null | undefined, typed: string | null | undefined): string;
97
+ /**
98
+ * Who the console's Send invoice form starts addressed to: the customer's
99
+ * email, else where the last invoice went, else (for a quote) where the quote
100
+ * was last emailed, so an order or quote that names no email still opens
101
+ * addressed to the customer it was already sent to.
102
+ */
103
+ export declare function invoiceSendPrefill(order: Pick<HostOrder, 'customerEmail' | 'quote' | 'invoiceSends'> | null | undefined): string;
97
104
  /**
98
105
  * The conversation an invoice replies into: a quote's own (its emails, and a
99
106
  * request's confirmation before them), else the order's, which the first
@@ -16,6 +16,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
16
16
  * limitations under the License.
17
17
  */ import { appendOrderEvent, formatOrderNumber, orderCreatedAtMs } from "./commerce-orders.js";
18
18
  import { QUOTE_MIN_CHARGE_CENTS, payLinkCheckoutCharge, quoteCheckoutCharge, quotePageAddress, quotePageLines, quotePageStatus, quotePayRefusal, quotePayRefusalMessage } from "./commerce-quote-pay.js";
19
+ import { lastQuoteSend } from "./commerce-quote-email.js";
19
20
  import { isQuote } from "./commerce-quotes.js";
20
21
  import { lineRequiresShipping } from "./order-fulfillment.js";
21
22
  /** The most invoice sends an order keeps on record; the oldest go first. */ export const INVOICE_MAX_SENDS = 50;
@@ -238,6 +239,16 @@ export function invoicePayRefusal(order, orderId) {
238
239
  var _order_quote_customer, _order_quote;
239
240
  return String(typed != null ? typed : '').trim() || String((_ref = order == null ? void 0 : order.customerEmail) != null ? _ref : '').trim() || String((_ref1 = 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 ? _ref1 : '').trim();
240
241
  }
242
+ /**
243
+ * Who the console's Send invoice form starts addressed to: the customer's
244
+ * email, else where the last invoice went, else (for a quote) where the quote
245
+ * was last emailed, so an order or quote that names no email still opens
246
+ * addressed to the customer it was already sent to.
247
+ */ export function invoiceSendPrefill(order) {
248
+ var _ref, _ref1;
249
+ var _lastInvoiceSend, _lastQuoteSend;
250
+ return invoiceSendRecipient(order, null) || String((_ref = (_lastInvoiceSend = lastInvoiceSend(order)) == null ? void 0 : _lastInvoiceSend.to) != null ? _ref : '').trim() || String((_ref1 = (_lastQuoteSend = lastQuoteSend(order == null ? void 0 : order.quote)) == null ? void 0 : _lastQuoteSend.to) != null ? _ref1 : '').trim();
251
+ }
241
252
  /**
242
253
  * The conversation an invoice replies into: a quote's own (its emails, and a
243
254
  * request's confirmation before them), else the order's, which the first
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/commerce-invoices.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n appendOrderEvent,\n formatOrderNumber,\n orderCreatedAtMs,\n type HostOrder,\n type OrderStatus,\n type OrderTimelineEvent,\n} from './commerce-orders'\nimport {\n QUOTE_MIN_CHARGE_CENTS,\n payLinkCheckoutCharge,\n quoteCheckoutCharge,\n quotePageAddress,\n quotePageLines,\n quotePageStatus,\n quotePayRefusal,\n quotePayRefusalMessage,\n type QuoteCheckoutCharge,\n type QuotePageView,\n type QuotePayRefusal,\n} from './commerce-quote-pay'\nimport {\n isQuote,\n type OrderInvoiceSendRecord,\n type QuoteEmailThread,\n type QuoteTaxBasis,\n} from './commerce-quotes'\nimport type { ResolvedShippingRate } from './commerce-shipping'\nimport { lineRequiresShipping } from './order-fulfillment'\n\n/*==========================================\n * INVOICES FOR ORDERS AND QUOTES (AGL-4099).\n *\n * The business emails an invoice for an order or a quote: unpaid, it asks\n * for payment through ONE lasting pay link; paid, it is the record. The link\n * is slice 2's (`/pay-quote?q=&t=`, AGL-4094), generalized rather than made\n * twice:\n *\n * - THE TOKEN is unchanged: `<version>.<HMAC(quote-link:hostId:orderId:version)>`,\n * no expiry. An order id names one document under `hosts/{hostId}/orders`\n * whatever its channel, so the same signature serves any order.\n * - THE VERSION is the quote's `quote.linkVersion` for a quote, as before,\n * so every link already sent keeps verifying; any other order keeps its own\n * as `payLink.version` (`orderPayLinkVersion`). Absent reads as 1.\n * - WHICH ORDERS the link opens ({@link orderPayLinkKind}): a quote, and a\n * draft order (`channel: 'draft'`), the two a business composes for a\n * customer to pay online. A register sale still open takes its payment at\n * the register, an online order was paid at checkout, and a marketplace\n * order on its channel, so none of them is linked; their invoices go out\n * paid, as the record.\n * - THE PAGE shows a non-quote order as \"Invoice #N\", with no proofs and no\n * approval ({@link buildInvoicePageView}).\n * - THE CHARGE is priced from the stored lines and totals on every press\n * ({@link invoiceCheckoutCharge}): the draft's discount, its frozen tax (or\n * Stripe Tax at payment where the draft used it), and the shipping rates\n * the draft offered when it was made (`draftShipping`).\n *\n * Pure: the console, the routes and the webhook read the same answers.\n *=========================================*/\n\n/** Which page a pay link opens for an order: a quote, an invoice, or none. */\nexport type OrderPayLinkKind = 'quote' | 'invoice'\n\n/** The most invoice sends an order keeps on record; the oldest go first. */\nexport const INVOICE_MAX_SENDS = 50\n\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\nconst cents = (value: unknown): number => {\n const number = Math.round(Number(value))\n return Number.isFinite(number) ? number : 0\n}\n\nconst text = (value: unknown): string | null => {\n const trimmed = typeof value === 'string' ? value.trim() : ''\n return trimmed ? trimmed : null\n}\n\n/**\n * Which page the lasting pay link opens for an order, or null when it opens\n * none: a quote (with its quote section), or a draft order as an invoice.\n */\nexport function orderPayLinkKind(\n order: Pick<HostOrder, 'channel'> & Partial<Pick<HostOrder, 'quote'>> | null | undefined,\n): OrderPayLinkKind | null {\n if (!order) return null\n if (isQuote(order)) return order.quote ? 'quote' : null\n return order.channel === 'draft' ? 'invoice' : null\n}\n\n/** `Invoice #1042`: the order's own number, which a paid quote keeps too. */\nexport function formatInvoiceNumber(order: Pick<HostOrder, 'number'>, orderId?: string): string {\n return `Invoice ${formatOrderNumber(order, orderId)}`\n}\n\nconst PAID_STATUSES: ReadonlySet<OrderStatus | string> = new Set([\n 'paid',\n 'partially_fulfilled',\n 'fulfilled',\n 'delivered',\n])\n\n/** The order is paid (and not refunded or canceled): its invoice is the record. */\nexport function invoiceIsPaid(order: Pick<HostOrder, 'status'> | null | undefined): boolean {\n return PAID_STATUSES.has(String(order?.status ?? ''))\n}\n\n/**\n * When the order was paid: a quote's stamp, else the timeline's `paid`\n * event, else (an order created paid, as checkout's are) when it was made.\n * Null for an order not paid.\n */\nexport function orderPaidAtMs(\n order: Pick<HostOrder, 'status' | 'quote' | 'timeline'> & { createdAtMs?: number } | null | undefined,\n): number | null {\n if (!order || order.status === 'pending') return null\n const stamped = Number(order.quote?.paidAtMs)\n if (Number.isFinite(stamped) && stamped > 0) return stamped\n const event = (order.timeline ?? []).find((entry) => entry?.event === 'paid')\n if (event && Number(event.atMs) > 0) return Number(event.atMs)\n const created = orderCreatedAtMs(order as never)\n return created > 0 ? created : null\n}\n\n/*==========================================\n * WHAT AN INVOICE'S PAY LINK CHARGES\n *=========================================*/\n\n/** What an invoice's Checkout Session charges, with the rates a draft lets the buyer pick from. */\nexport interface InvoiceCheckoutCharge extends QuoteCheckoutCharge {\n /** The draft's shipping rates, picked at payment; empty when shipping is fixed or there is none. */\n shippingOptions: ResolvedShippingRate[]\n /** Where those rates ship; empty with none. */\n shippingCountries: string[]\n}\n\n/**\n * The tax basis an order's stored figures give its invoice: Stripe Tax at\n * payment where the order was composed under it, its frozen tax as a fixed\n * figure (charged as the rate that yields it on the taxed line, the way a tax\n * service's quote is), else none.\n */\nexport function invoiceTaxBasis(order: Pick<HostOrder, 'taxMode' | 'totals'>): QuoteTaxBasis {\n if (order.taxMode === 'stripe-automatic') return { kind: 'at-payment' }\n const taxCents = Math.max(0, cents(order.totals?.taxCents))\n if (taxCents > 0) return { kind: 'service', taxCents }\n return { kind: 'none', reason: 'not-collected' }\n}\n\n/** A stored draft rate, as Stripe is given it, or null for one that is not a rate. */\nfunction draftRate(option: unknown): ResolvedShippingRate | null {\n if (!option || typeof option !== 'object') return null\n const source = option as Record<string, unknown>\n const name = text(source['name'])\n const amountCents = cents(source['amountCents'])\n if (!name || amountCents < 0) return null\n return { rateId: String(source['rateId'] ?? '') || name, name, amountCents }\n}\n\n/**\n * What an invoice's pay link charges, from the stored order alone. A quote is\n * charged as slice 2 charges it; any other order from its lines, its\n * discount, its tax ({@link invoiceTaxBasis}) and, for a draft, the shipping\n * rates it offered when it was made (Stripe asks for the address and the\n * buyer picks one, as on the draft's first link).\n */\nexport function invoiceCheckoutCharge(\n order: Pick<HostOrder, 'number' | 'lineItems' | 'quote' | 'totals' | 'taxMode'> &\n Partial<Pick<HostOrder, 'draftShipping'>>,\n orderId: string,\n): InvoiceCheckoutCharge {\n if (order.quote) return { ...quoteCheckoutCharge(order, orderId), shippingOptions: [], shippingCountries: [] }\n const discountCents = Math.max(0, cents(order.totals?.discountCents))\n const charge = payLinkCheckoutCharge({\n lineItems: order.lineItems ?? [],\n tax: invoiceTaxBasis(order),\n discount: discountCents > 0 ? { kind: 'amount', amountCents: discountCents } : null,\n shippingCents: 0,\n label: formatInvoiceNumber(order, orderId),\n })\n const options = (order.draftShipping?.options ?? [])\n .map(draftRate)\n .filter((rate): rate is ResolvedShippingRate => rate !== null)\n .slice(0, 5)\n const countries = (order.draftShipping?.countries ?? [])\n .map((country) => String(country ?? '').trim().toUpperCase())\n .filter((country) => /^[A-Z]{2}$/.test(country))\n return { ...charge, shippingOptions: options, shippingCountries: options.length ? countries : [] }\n}\n\n/*==========================================\n * WHETHER AN INVOICE CAN BE PAID NOW\n *=========================================*/\n\n/**\n * Why an order cannot be paid through its pay link now: a quote's reasons,\n * and two of an invoice's own. `not-payable` is an order the link does not\n * open (a register sale still open, say); `shipping-unknown` is a draft made\n * before drafts kept their shipping rates, whose shipping cannot be charged.\n */\nexport type InvoicePayRefusal = QuotePayRefusal | 'not-payable' | 'shipping-unknown'\n\nexport function invoicePayRefusal(\n order: Pick<HostOrder, 'number' | 'status' | 'lineItems' | 'quote' | 'totals' | 'taxMode' | 'channel'> &\n Partial<Pick<HostOrder, 'draftShipping'>>,\n orderId: string,\n): InvoicePayRefusal | null {\n const kind = orderPayLinkKind(order)\n if (kind === 'quote') return quotePayRefusal(order, orderId)\n if (order.status === 'cancelled' || order.status === 'refunded') return 'closed'\n if (order.status !== 'pending') return 'paid'\n if (kind !== 'invoice') return 'not-payable'\n const ships = (order.lineItems ?? []).some((line) => line && !line.included && lineRequiresShipping(line))\n if (ships && !order.draftShipping) return 'shipping-unknown'\n if (invoiceCheckoutCharge(order, orderId).totalCents < QUOTE_MIN_CHARGE_CENTS) return 'nothing-to-pay'\n return null\n}\n\n/** What the customer is told for a refusal, in the page's words: \"quote\" or \"invoice\". */\nexport function invoicePayRefusalMessage(\n reason: InvoicePayRefusal,\n storeName: string,\n kind: OrderPayLinkKind,\n): string {\n const business = storeName.trim() || 'the business'\n if (kind === 'quote' && reason !== 'not-payable' && reason !== 'shipping-unknown') {\n return quotePayRefusalMessage(reason, storeName)\n }\n switch (reason) {\n case 'paid':\n return 'This invoice has been paid.'\n case 'closed':\n return `This invoice is closed. Contact ${business} with any questions.`\n case 'approval-required':\n return `${business} asks you to approve the proof before you pay.`\n case 'tax-undecided':\n return `${business} has not finished setting up payments. Contact them to pay this invoice.`\n case 'nothing-to-pay':\n return `This invoice has nothing to pay by card. Contact ${business} with any questions.`\n case 'not-payable':\n case 'shipping-unknown':\n return `This invoice can’t be paid online. Contact ${business} to pay it.`\n }\n}\n\n/*==========================================\n * THE CUSTOMER'S PAGE FOR AN INVOICE\n *=========================================*/\n\n/**\n * The pay link's page for an order that is not a quote: \"Invoice #N\", its\n * lines and totals, the customer's addresses, and Pay while it is unpaid. No\n * proofs, no approval, no notes or terms (an order carries none). Allow-listed\n * as the quote's view is: the order's email, phone, Stripe ids and timeline\n * never leave the server.\n */\nexport function buildInvoicePageView(input: {\n order: Pick<\n HostOrder,\n | 'number'\n | 'status'\n | 'channel'\n | 'lineItems'\n | 'totals'\n | 'quote'\n | 'timeline'\n | 'taxMode'\n | 'customerName'\n | 'billingAddress'\n | 'shippingAddress'\n > &\n Partial<Pick<HostOrder, 'draftShipping'>> & { createdAtMs?: number }\n orderId: string\n storeName: string\n currency: string\n /** The order's status page, once paid. */\n receiptUrl?: string | null\n}): QuotePageView {\n const { order } = input\n const status = quotePageStatus(order.status)\n const unpaid = status.status === 'unpaid'\n const charge = invoiceCheckoutCharge(order, input.orderId)\n const stored = order.totals\n const totals =\n unpaid || !stored\n ? {\n itemsCents: charge.itemsCents,\n discountCents: charge.discountCents,\n shippingCents: charge.shippingCents,\n taxCents: charge.taxCents,\n totalCents: charge.totalCents,\n }\n : {\n itemsCents: cents(stored.itemsCents),\n discountCents: cents(stored.discountCents),\n shippingCents: cents(stored.shippingCents),\n taxCents: cents(stored.taxCents),\n totalCents: cents(stored.totalCents),\n }\n const taxAtPayment = unpaid && charge.taxAtPayment\n const created = orderCreatedAtMs(order as never)\n return {\n state: 'quote',\n kind: 'invoice',\n storeName: input.storeName,\n number: formatInvoiceNumber(order, input.orderId),\n dateMs: created > 0 ? created : null,\n // An invoice is not edited; the page has no version to send back.\n version: 0,\n currency: input.currency || 'USD',\n customerName: text(order.customerName),\n company: null,\n billTo: quotePageAddress(order.billingAddress),\n shipTo: quotePageAddress(order.shippingAddress),\n lines: quotePageLines(order.lineItems ?? []),\n totals,\n discountLabel: null,\n taxLabel: taxAtPayment ? 'Tax is added at payment' : 'Tax',\n taxAtPayment,\n shippingAtPayment: unpaid && charge.shippingOptions.length > 0,\n notes: null,\n terms: null,\n status: status.status,\n statusLabel: status.label,\n paidAtMs: orderPaidAtMs(order),\n receiptUrl: unpaid ? null : (input.receiptUrl ?? null),\n approvalRequired: false,\n payable: unpaid && invoicePayRefusal(order, input.orderId) === null,\n revision: 1,\n proofs: [],\n response: null,\n respondable: false,\n }\n}\n\n/*==========================================\n * EMAILING AN INVOICE\n *=========================================*/\n\n/** The order's invoice sends on record, oldest first. */\nexport function invoiceSends(order: Pick<HostOrder, 'invoiceSends'> | null | undefined): OrderInvoiceSendRecord[] {\n return Array.isArray(order?.invoiceSends)\n ? order.invoiceSends.filter((send) => send && typeof send === 'object')\n : []\n}\n\n/** The last time the order's invoice was emailed, or null. */\nexport function lastInvoiceSend(order: Pick<HostOrder, 'invoiceSends'> | null | undefined): OrderInvoiceSendRecord | null {\n const sends = invoiceSends(order)\n return sends.length ? sends[sends.length - 1] : null\n}\n\n/** Where the invoice goes: what was typed, else the order's customer, else a quote's. */\nexport function invoiceSendRecipient(\n order: Pick<HostOrder, 'customerEmail' | 'quote'> | null | undefined,\n typed: string | null | undefined,\n): string {\n return (\n String(typed ?? '').trim() ||\n String(order?.customerEmail ?? '').trim() ||\n String(order?.quote?.customer?.email ?? '').trim()\n )\n}\n\n/**\n * The conversation an invoice replies into: a quote's own (its emails, and a\n * request's confirmation before them), else the order's, which the first\n * invoice starts.\n */\nexport function invoiceEmailThread(\n order: Pick<HostOrder, 'quote' | 'emailThread'> | null | undefined,\n): QuoteEmailThread | null {\n if (order?.quote) return order.quote.emailThread ?? null\n return order?.emailThread ?? null\n}\n\n/**\n * The subject an invoice defaults to: the conversation's, so it stays one\n * conversation in the customer's mailbox, else `Invoice #1042 from Acme Signs`.\n */\nexport function invoiceEmailDefaultSubject(\n order: Pick<HostOrder, 'number' | 'quote' | 'emailThread'>,\n businessName: string,\n orderId?: string,\n): string {\n const thread = String(invoiceEmailThread(order)?.subject ?? '').trim()\n if (thread) return thread\n const name = String(businessName ?? '').trim()\n const number = formatInvoiceNumber(order, orderId)\n return name ? `${number} from ${name}` : number\n}\n\n/** Why an invoice cannot be emailed now, and what the console says. */\nexport type InvoiceSendRefusal =\n | 'unknown'\n | 'closed'\n | 'empty'\n | 'not-payable'\n | 'approval-required'\n | 'tax-undecided'\n | 'nothing-to-pay'\n | 'shipping-unknown'\n | 'no-address'\n | 'bad-address'\n\n/**\n * Why the order's invoice cannot be emailed to `to` now, if it cannot:\n * a canceled or refunded order, one with nothing on it, an unpaid order its\n * pay link cannot take payment for (and why), and the address.\n */\nexport function invoiceSendRefusal(\n order:\n | (Pick<HostOrder, 'number' | 'status' | 'channel' | 'lineItems' | 'totals' | 'quote' | 'taxMode'> &\n Partial<Pick<HostOrder, 'draftShipping'>>)\n | null\n | undefined,\n orderId: string,\n to: string,\n): InvoiceSendRefusal | null {\n if (!order) return 'unknown'\n if (order.status === 'cancelled' || order.status === 'refunded') return 'closed'\n const lines = (order.lineItems ?? []).filter(Boolean)\n if (!lines.length && cents(order.totals?.totalCents) <= 0) return 'empty'\n if (!invoiceIsPaid(order)) {\n const refused = invoicePayRefusal(order, orderId)\n if (refused === 'closed') return 'closed'\n if (refused && refused !== 'paid') return refused\n }\n if (!to) return 'no-address'\n if (to.length > 320 || !EMAIL.test(to)) return 'bad-address'\n return null\n}\n\n/** What the console says for a refused invoice send. */\nexport function invoiceSendRefusalMessage(reason: InvoiceSendRefusal): string {\n switch (reason) {\n case 'unknown':\n return 'Unknown order'\n case 'closed':\n return 'This order was canceled or refunded, so it has no invoice to send.'\n case 'empty':\n return 'This order has no items and no total, so there is nothing to put on an invoice.'\n case 'not-payable':\n return 'This order isn’t paid yet, and it can’t be paid online from an invoice. Take its payment where the sale was made, then send the invoice.'\n case 'approval-required':\n return 'This quote asks your customer to approve the proof before they pay, and they haven’t yet. Send the quote so they can approve it, then send the invoice.'\n case 'tax-undecided':\n return 'Sales tax isn’t set up for this store yet, so the invoice can’t be paid online. Finish the tax settings, then send it.'\n case 'nothing-to-pay':\n return 'This invoice totals less than $0.50, so it can’t be paid by card.'\n case 'shipping-unknown':\n return 'This draft order was made before invoices could charge its shipping, so its invoice can’t be paid online. Make a new draft order and send its invoice.'\n case 'no-address':\n return 'This order has no email for the customer. Type where to send the invoice.'\n case 'bad-address':\n return 'Enter a valid email address to send the invoice to.'\n }\n}\n\n/** The HTTP status a refused send answers with. */\nexport function invoiceSendRefusalStatus(reason: InvoiceSendRefusal): number {\n if (reason === 'unknown') return 404\n if (reason === 'no-address' || reason === 'bad-address') return 400\n return 409\n}\n\n/**\n * What an invoice email changes on the order, as a merge: the send on\n * record, a line on the timeline, and the conversation moved on, on the\n * quote for a quote and on the order otherwise.\n */\nexport function invoiceSendPatch(\n order: Pick<HostOrder, 'timeline' | 'invoiceSends' | 'quote'>,\n send: OrderInvoiceSendRecord,\n thread: QuoteEmailThread,\n): {\n timeline: OrderTimelineEvent[]\n invoiceSends: OrderInvoiceSendRecord[]\n emailThread?: QuoteEmailThread\n quote?: { emailThread: QuoteEmailThread }\n} {\n return {\n timeline: appendOrderEvent(\n order,\n 'invoice-sent',\n // The address is on the send record, which erasure clears; the timeline keeps none.\n `${send.paid ? 'Paid invoice' : 'Invoice'} emailed to the customer by ${send.by}`,\n send.atMs,\n ),\n invoiceSends: [...invoiceSends(order), send].slice(-INVOICE_MAX_SENDS),\n ...(order.quote ? { quote: { emailThread: thread } } : { emailThread: thread }),\n }\n}\n"],"names":["appendOrderEvent","formatOrderNumber","orderCreatedAtMs","QUOTE_MIN_CHARGE_CENTS","payLinkCheckoutCharge","quoteCheckoutCharge","quotePageAddress","quotePageLines","quotePageStatus","quotePayRefusal","quotePayRefusalMessage","isQuote","lineRequiresShipping","INVOICE_MAX_SENDS","EMAIL","cents","value","number","Math","round","Number","isFinite","text","trimmed","trim","orderPayLinkKind","order","quote","channel","formatInvoiceNumber","orderId","PAID_STATUSES","Set","invoiceIsPaid","has","String","status","orderPaidAtMs","stamped","paidAtMs","event","timeline","find","entry","atMs","created","invoiceTaxBasis","taxMode","kind","taxCents","max","totals","reason","draftRate","option","source","name","amountCents","rateId","invoiceCheckoutCharge","shippingOptions","shippingCountries","discountCents","charge","lineItems","tax","discount","shippingCents","label","options","draftShipping","map","filter","rate","slice","countries","country","toUpperCase","test","length","invoicePayRefusal","ships","some","line","included","totalCents","invoicePayRefusalMessage","storeName","business","buildInvoicePageView","input","unpaid","stored","itemsCents","taxAtPayment","state","dateMs","version","currency","customerName","company","billTo","billingAddress","shipTo","shippingAddress","lines","discountLabel","taxLabel","shippingAtPayment","notes","terms","statusLabel","receiptUrl","approvalRequired","payable","revision","proofs","response","respondable","invoiceSends","Array","isArray","send","lastInvoiceSend","sends","invoiceSendRecipient","typed","customerEmail","customer","email","invoiceEmailThread","emailThread","invoiceEmailDefaultSubject","businessName","thread","subject","invoiceSendRefusal","to","Boolean","refused","invoiceSendRefusalMessage","invoiceSendRefusalStatus","invoiceSendPatch","paid","by"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/commerce-invoices.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n appendOrderEvent,\n formatOrderNumber,\n orderCreatedAtMs,\n type HostOrder,\n type OrderStatus,\n type OrderTimelineEvent,\n} from './commerce-orders'\nimport {\n QUOTE_MIN_CHARGE_CENTS,\n payLinkCheckoutCharge,\n quoteCheckoutCharge,\n quotePageAddress,\n quotePageLines,\n quotePageStatus,\n quotePayRefusal,\n quotePayRefusalMessage,\n type QuoteCheckoutCharge,\n type QuotePageView,\n type QuotePayRefusal,\n} from './commerce-quote-pay'\nimport { lastQuoteSend } from './commerce-quote-email'\nimport {\n isQuote,\n type OrderInvoiceSendRecord,\n type QuoteEmailThread,\n type QuoteTaxBasis,\n} from './commerce-quotes'\nimport type { ResolvedShippingRate } from './commerce-shipping'\nimport { lineRequiresShipping } from './order-fulfillment'\n\n/*==========================================\n * INVOICES FOR ORDERS AND QUOTES (AGL-4099).\n *\n * The business emails an invoice for an order or a quote: unpaid, it asks\n * for payment through ONE lasting pay link; paid, it is the record. The link\n * is slice 2's (`/pay-quote?q=&t=`, AGL-4094), generalized rather than made\n * twice:\n *\n * - THE TOKEN is unchanged: `<version>.<HMAC(quote-link:hostId:orderId:version)>`,\n * no expiry. An order id names one document under `hosts/{hostId}/orders`\n * whatever its channel, so the same signature serves any order.\n * - THE VERSION is the quote's `quote.linkVersion` for a quote, as before,\n * so every link already sent keeps verifying; any other order keeps its own\n * as `payLink.version` (`orderPayLinkVersion`). Absent reads as 1.\n * - WHICH ORDERS the link opens ({@link orderPayLinkKind}): a quote, and a\n * draft order (`channel: 'draft'`), the two a business composes for a\n * customer to pay online. A register sale still open takes its payment at\n * the register, an online order was paid at checkout, and a marketplace\n * order on its channel, so none of them is linked; their invoices go out\n * paid, as the record.\n * - THE PAGE shows a non-quote order as \"Invoice #N\", with no proofs and no\n * approval ({@link buildInvoicePageView}).\n * - THE CHARGE is priced from the stored lines and totals on every press\n * ({@link invoiceCheckoutCharge}): the draft's discount, its frozen tax (or\n * Stripe Tax at payment where the draft used it), and the shipping rates\n * the draft offered when it was made (`draftShipping`).\n *\n * Pure: the console, the routes and the webhook read the same answers.\n *=========================================*/\n\n/** Which page a pay link opens for an order: a quote, an invoice, or none. */\nexport type OrderPayLinkKind = 'quote' | 'invoice'\n\n/** The most invoice sends an order keeps on record; the oldest go first. */\nexport const INVOICE_MAX_SENDS = 50\n\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\nconst cents = (value: unknown): number => {\n const number = Math.round(Number(value))\n return Number.isFinite(number) ? number : 0\n}\n\nconst text = (value: unknown): string | null => {\n const trimmed = typeof value === 'string' ? value.trim() : ''\n return trimmed ? trimmed : null\n}\n\n/**\n * Which page the lasting pay link opens for an order, or null when it opens\n * none: a quote (with its quote section), or a draft order as an invoice.\n */\nexport function orderPayLinkKind(\n order: Pick<HostOrder, 'channel'> & Partial<Pick<HostOrder, 'quote'>> | null | undefined,\n): OrderPayLinkKind | null {\n if (!order) return null\n if (isQuote(order)) return order.quote ? 'quote' : null\n return order.channel === 'draft' ? 'invoice' : null\n}\n\n/** `Invoice #1042`: the order's own number, which a paid quote keeps too. */\nexport function formatInvoiceNumber(order: Pick<HostOrder, 'number'>, orderId?: string): string {\n return `Invoice ${formatOrderNumber(order, orderId)}`\n}\n\nconst PAID_STATUSES: ReadonlySet<OrderStatus | string> = new Set([\n 'paid',\n 'partially_fulfilled',\n 'fulfilled',\n 'delivered',\n])\n\n/** The order is paid (and not refunded or canceled): its invoice is the record. */\nexport function invoiceIsPaid(order: Pick<HostOrder, 'status'> | null | undefined): boolean {\n return PAID_STATUSES.has(String(order?.status ?? ''))\n}\n\n/**\n * When the order was paid: a quote's stamp, else the timeline's `paid`\n * event, else (an order created paid, as checkout's are) when it was made.\n * Null for an order not paid.\n */\nexport function orderPaidAtMs(\n order: Pick<HostOrder, 'status' | 'quote' | 'timeline'> & { createdAtMs?: number } | null | undefined,\n): number | null {\n if (!order || order.status === 'pending') return null\n const stamped = Number(order.quote?.paidAtMs)\n if (Number.isFinite(stamped) && stamped > 0) return stamped\n const event = (order.timeline ?? []).find((entry) => entry?.event === 'paid')\n if (event && Number(event.atMs) > 0) return Number(event.atMs)\n const created = orderCreatedAtMs(order as never)\n return created > 0 ? created : null\n}\n\n/*==========================================\n * WHAT AN INVOICE'S PAY LINK CHARGES\n *=========================================*/\n\n/** What an invoice's Checkout Session charges, with the rates a draft lets the buyer pick from. */\nexport interface InvoiceCheckoutCharge extends QuoteCheckoutCharge {\n /** The draft's shipping rates, picked at payment; empty when shipping is fixed or there is none. */\n shippingOptions: ResolvedShippingRate[]\n /** Where those rates ship; empty with none. */\n shippingCountries: string[]\n}\n\n/**\n * The tax basis an order's stored figures give its invoice: Stripe Tax at\n * payment where the order was composed under it, its frozen tax as a fixed\n * figure (charged as the rate that yields it on the taxed line, the way a tax\n * service's quote is), else none.\n */\nexport function invoiceTaxBasis(order: Pick<HostOrder, 'taxMode' | 'totals'>): QuoteTaxBasis {\n if (order.taxMode === 'stripe-automatic') return { kind: 'at-payment' }\n const taxCents = Math.max(0, cents(order.totals?.taxCents))\n if (taxCents > 0) return { kind: 'service', taxCents }\n return { kind: 'none', reason: 'not-collected' }\n}\n\n/** A stored draft rate, as Stripe is given it, or null for one that is not a rate. */\nfunction draftRate(option: unknown): ResolvedShippingRate | null {\n if (!option || typeof option !== 'object') return null\n const source = option as Record<string, unknown>\n const name = text(source['name'])\n const amountCents = cents(source['amountCents'])\n if (!name || amountCents < 0) return null\n return { rateId: String(source['rateId'] ?? '') || name, name, amountCents }\n}\n\n/**\n * What an invoice's pay link charges, from the stored order alone. A quote is\n * charged as slice 2 charges it; any other order from its lines, its\n * discount, its tax ({@link invoiceTaxBasis}) and, for a draft, the shipping\n * rates it offered when it was made (Stripe asks for the address and the\n * buyer picks one, as on the draft's first link).\n */\nexport function invoiceCheckoutCharge(\n order: Pick<HostOrder, 'number' | 'lineItems' | 'quote' | 'totals' | 'taxMode'> &\n Partial<Pick<HostOrder, 'draftShipping'>>,\n orderId: string,\n): InvoiceCheckoutCharge {\n if (order.quote) return { ...quoteCheckoutCharge(order, orderId), shippingOptions: [], shippingCountries: [] }\n const discountCents = Math.max(0, cents(order.totals?.discountCents))\n const charge = payLinkCheckoutCharge({\n lineItems: order.lineItems ?? [],\n tax: invoiceTaxBasis(order),\n discount: discountCents > 0 ? { kind: 'amount', amountCents: discountCents } : null,\n shippingCents: 0,\n label: formatInvoiceNumber(order, orderId),\n })\n const options = (order.draftShipping?.options ?? [])\n .map(draftRate)\n .filter((rate): rate is ResolvedShippingRate => rate !== null)\n .slice(0, 5)\n const countries = (order.draftShipping?.countries ?? [])\n .map((country) => String(country ?? '').trim().toUpperCase())\n .filter((country) => /^[A-Z]{2}$/.test(country))\n return { ...charge, shippingOptions: options, shippingCountries: options.length ? countries : [] }\n}\n\n/*==========================================\n * WHETHER AN INVOICE CAN BE PAID NOW\n *=========================================*/\n\n/**\n * Why an order cannot be paid through its pay link now: a quote's reasons,\n * and two of an invoice's own. `not-payable` is an order the link does not\n * open (a register sale still open, say); `shipping-unknown` is a draft made\n * before drafts kept their shipping rates, whose shipping cannot be charged.\n */\nexport type InvoicePayRefusal = QuotePayRefusal | 'not-payable' | 'shipping-unknown'\n\nexport function invoicePayRefusal(\n order: Pick<HostOrder, 'number' | 'status' | 'lineItems' | 'quote' | 'totals' | 'taxMode' | 'channel'> &\n Partial<Pick<HostOrder, 'draftShipping'>>,\n orderId: string,\n): InvoicePayRefusal | null {\n const kind = orderPayLinkKind(order)\n if (kind === 'quote') return quotePayRefusal(order, orderId)\n if (order.status === 'cancelled' || order.status === 'refunded') return 'closed'\n if (order.status !== 'pending') return 'paid'\n if (kind !== 'invoice') return 'not-payable'\n const ships = (order.lineItems ?? []).some((line) => line && !line.included && lineRequiresShipping(line))\n if (ships && !order.draftShipping) return 'shipping-unknown'\n if (invoiceCheckoutCharge(order, orderId).totalCents < QUOTE_MIN_CHARGE_CENTS) return 'nothing-to-pay'\n return null\n}\n\n/** What the customer is told for a refusal, in the page's words: \"quote\" or \"invoice\". */\nexport function invoicePayRefusalMessage(\n reason: InvoicePayRefusal,\n storeName: string,\n kind: OrderPayLinkKind,\n): string {\n const business = storeName.trim() || 'the business'\n if (kind === 'quote' && reason !== 'not-payable' && reason !== 'shipping-unknown') {\n return quotePayRefusalMessage(reason, storeName)\n }\n switch (reason) {\n case 'paid':\n return 'This invoice has been paid.'\n case 'closed':\n return `This invoice is closed. Contact ${business} with any questions.`\n case 'approval-required':\n return `${business} asks you to approve the proof before you pay.`\n case 'tax-undecided':\n return `${business} has not finished setting up payments. Contact them to pay this invoice.`\n case 'nothing-to-pay':\n return `This invoice has nothing to pay by card. Contact ${business} with any questions.`\n case 'not-payable':\n case 'shipping-unknown':\n return `This invoice can’t be paid online. Contact ${business} to pay it.`\n }\n}\n\n/*==========================================\n * THE CUSTOMER'S PAGE FOR AN INVOICE\n *=========================================*/\n\n/**\n * The pay link's page for an order that is not a quote: \"Invoice #N\", its\n * lines and totals, the customer's addresses, and Pay while it is unpaid. No\n * proofs, no approval, no notes or terms (an order carries none). Allow-listed\n * as the quote's view is: the order's email, phone, Stripe ids and timeline\n * never leave the server.\n */\nexport function buildInvoicePageView(input: {\n order: Pick<\n HostOrder,\n | 'number'\n | 'status'\n | 'channel'\n | 'lineItems'\n | 'totals'\n | 'quote'\n | 'timeline'\n | 'taxMode'\n | 'customerName'\n | 'billingAddress'\n | 'shippingAddress'\n > &\n Partial<Pick<HostOrder, 'draftShipping'>> & { createdAtMs?: number }\n orderId: string\n storeName: string\n currency: string\n /** The order's status page, once paid. */\n receiptUrl?: string | null\n}): QuotePageView {\n const { order } = input\n const status = quotePageStatus(order.status)\n const unpaid = status.status === 'unpaid'\n const charge = invoiceCheckoutCharge(order, input.orderId)\n const stored = order.totals\n const totals =\n unpaid || !stored\n ? {\n itemsCents: charge.itemsCents,\n discountCents: charge.discountCents,\n shippingCents: charge.shippingCents,\n taxCents: charge.taxCents,\n totalCents: charge.totalCents,\n }\n : {\n itemsCents: cents(stored.itemsCents),\n discountCents: cents(stored.discountCents),\n shippingCents: cents(stored.shippingCents),\n taxCents: cents(stored.taxCents),\n totalCents: cents(stored.totalCents),\n }\n const taxAtPayment = unpaid && charge.taxAtPayment\n const created = orderCreatedAtMs(order as never)\n return {\n state: 'quote',\n kind: 'invoice',\n storeName: input.storeName,\n number: formatInvoiceNumber(order, input.orderId),\n dateMs: created > 0 ? created : null,\n // An invoice is not edited; the page has no version to send back.\n version: 0,\n currency: input.currency || 'USD',\n customerName: text(order.customerName),\n company: null,\n billTo: quotePageAddress(order.billingAddress),\n shipTo: quotePageAddress(order.shippingAddress),\n lines: quotePageLines(order.lineItems ?? []),\n totals,\n discountLabel: null,\n taxLabel: taxAtPayment ? 'Tax is added at payment' : 'Tax',\n taxAtPayment,\n shippingAtPayment: unpaid && charge.shippingOptions.length > 0,\n notes: null,\n terms: null,\n status: status.status,\n statusLabel: status.label,\n paidAtMs: orderPaidAtMs(order),\n receiptUrl: unpaid ? null : (input.receiptUrl ?? null),\n approvalRequired: false,\n payable: unpaid && invoicePayRefusal(order, input.orderId) === null,\n revision: 1,\n proofs: [],\n response: null,\n respondable: false,\n }\n}\n\n/*==========================================\n * EMAILING AN INVOICE\n *=========================================*/\n\n/** The order's invoice sends on record, oldest first. */\nexport function invoiceSends(order: Pick<HostOrder, 'invoiceSends'> | null | undefined): OrderInvoiceSendRecord[] {\n return Array.isArray(order?.invoiceSends)\n ? order.invoiceSends.filter((send) => send && typeof send === 'object')\n : []\n}\n\n/** The last time the order's invoice was emailed, or null. */\nexport function lastInvoiceSend(order: Pick<HostOrder, 'invoiceSends'> | null | undefined): OrderInvoiceSendRecord | null {\n const sends = invoiceSends(order)\n return sends.length ? sends[sends.length - 1] : null\n}\n\n/** Where the invoice goes: what was typed, else the order's customer, else a quote's. */\nexport function invoiceSendRecipient(\n order: Pick<HostOrder, 'customerEmail' | 'quote'> | null | undefined,\n typed: string | null | undefined,\n): string {\n return (\n String(typed ?? '').trim() ||\n String(order?.customerEmail ?? '').trim() ||\n String(order?.quote?.customer?.email ?? '').trim()\n )\n}\n\n/**\n * Who the console's Send invoice form starts addressed to: the customer's\n * email, else where the last invoice went, else (for a quote) where the quote\n * was last emailed, so an order or quote that names no email still opens\n * addressed to the customer it was already sent to.\n */\nexport function invoiceSendPrefill(order: Pick<HostOrder, 'customerEmail' | 'quote' | 'invoiceSends'> | null | undefined): string {\n return (\n invoiceSendRecipient(order, null) ||\n String(lastInvoiceSend(order)?.to ?? '').trim() ||\n String(lastQuoteSend(order?.quote)?.to ?? '').trim()\n )\n}\n\n/**\n * The conversation an invoice replies into: a quote's own (its emails, and a\n * request's confirmation before them), else the order's, which the first\n * invoice starts.\n */\nexport function invoiceEmailThread(\n order: Pick<HostOrder, 'quote' | 'emailThread'> | null | undefined,\n): QuoteEmailThread | null {\n if (order?.quote) return order.quote.emailThread ?? null\n return order?.emailThread ?? null\n}\n\n/**\n * The subject an invoice defaults to: the conversation's, so it stays one\n * conversation in the customer's mailbox, else `Invoice #1042 from Acme Signs`.\n */\nexport function invoiceEmailDefaultSubject(\n order: Pick<HostOrder, 'number' | 'quote' | 'emailThread'>,\n businessName: string,\n orderId?: string,\n): string {\n const thread = String(invoiceEmailThread(order)?.subject ?? '').trim()\n if (thread) return thread\n const name = String(businessName ?? '').trim()\n const number = formatInvoiceNumber(order, orderId)\n return name ? `${number} from ${name}` : number\n}\n\n/** Why an invoice cannot be emailed now, and what the console says. */\nexport type InvoiceSendRefusal =\n | 'unknown'\n | 'closed'\n | 'empty'\n | 'not-payable'\n | 'approval-required'\n | 'tax-undecided'\n | 'nothing-to-pay'\n | 'shipping-unknown'\n | 'no-address'\n | 'bad-address'\n\n/**\n * Why the order's invoice cannot be emailed to `to` now, if it cannot:\n * a canceled or refunded order, one with nothing on it, an unpaid order its\n * pay link cannot take payment for (and why), and the address.\n */\nexport function invoiceSendRefusal(\n order:\n | (Pick<HostOrder, 'number' | 'status' | 'channel' | 'lineItems' | 'totals' | 'quote' | 'taxMode'> &\n Partial<Pick<HostOrder, 'draftShipping'>>)\n | null\n | undefined,\n orderId: string,\n to: string,\n): InvoiceSendRefusal | null {\n if (!order) return 'unknown'\n if (order.status === 'cancelled' || order.status === 'refunded') return 'closed'\n const lines = (order.lineItems ?? []).filter(Boolean)\n if (!lines.length && cents(order.totals?.totalCents) <= 0) return 'empty'\n if (!invoiceIsPaid(order)) {\n const refused = invoicePayRefusal(order, orderId)\n if (refused === 'closed') return 'closed'\n if (refused && refused !== 'paid') return refused\n }\n if (!to) return 'no-address'\n if (to.length > 320 || !EMAIL.test(to)) return 'bad-address'\n return null\n}\n\n/** What the console says for a refused invoice send. */\nexport function invoiceSendRefusalMessage(reason: InvoiceSendRefusal): string {\n switch (reason) {\n case 'unknown':\n return 'Unknown order'\n case 'closed':\n return 'This order was canceled or refunded, so it has no invoice to send.'\n case 'empty':\n return 'This order has no items and no total, so there is nothing to put on an invoice.'\n case 'not-payable':\n return 'This order isn’t paid yet, and it can’t be paid online from an invoice. Take its payment where the sale was made, then send the invoice.'\n case 'approval-required':\n return 'This quote asks your customer to approve the proof before they pay, and they haven’t yet. Send the quote so they can approve it, then send the invoice.'\n case 'tax-undecided':\n return 'Sales tax isn’t set up for this store yet, so the invoice can’t be paid online. Finish the tax settings, then send it.'\n case 'nothing-to-pay':\n return 'This invoice totals less than $0.50, so it can’t be paid by card.'\n case 'shipping-unknown':\n return 'This draft order was made before invoices could charge its shipping, so its invoice can’t be paid online. Make a new draft order and send its invoice.'\n case 'no-address':\n return 'This order has no email for the customer. Type where to send the invoice.'\n case 'bad-address':\n return 'Enter a valid email address to send the invoice to.'\n }\n}\n\n/** The HTTP status a refused send answers with. */\nexport function invoiceSendRefusalStatus(reason: InvoiceSendRefusal): number {\n if (reason === 'unknown') return 404\n if (reason === 'no-address' || reason === 'bad-address') return 400\n return 409\n}\n\n/**\n * What an invoice email changes on the order, as a merge: the send on\n * record, a line on the timeline, and the conversation moved on, on the\n * quote for a quote and on the order otherwise.\n */\nexport function invoiceSendPatch(\n order: Pick<HostOrder, 'timeline' | 'invoiceSends' | 'quote'>,\n send: OrderInvoiceSendRecord,\n thread: QuoteEmailThread,\n): {\n timeline: OrderTimelineEvent[]\n invoiceSends: OrderInvoiceSendRecord[]\n emailThread?: QuoteEmailThread\n quote?: { emailThread: QuoteEmailThread }\n} {\n return {\n timeline: appendOrderEvent(\n order,\n 'invoice-sent',\n // The address is on the send record, which erasure clears; the timeline keeps none.\n `${send.paid ? 'Paid invoice' : 'Invoice'} emailed to the customer by ${send.by}`,\n send.atMs,\n ),\n invoiceSends: [...invoiceSends(order), send].slice(-INVOICE_MAX_SENDS),\n ...(order.quote ? { quote: { emailThread: thread } } : { emailThread: thread }),\n }\n}\n"],"names":["appendOrderEvent","formatOrderNumber","orderCreatedAtMs","QUOTE_MIN_CHARGE_CENTS","payLinkCheckoutCharge","quoteCheckoutCharge","quotePageAddress","quotePageLines","quotePageStatus","quotePayRefusal","quotePayRefusalMessage","lastQuoteSend","isQuote","lineRequiresShipping","INVOICE_MAX_SENDS","EMAIL","cents","value","number","Math","round","Number","isFinite","text","trimmed","trim","orderPayLinkKind","order","quote","channel","formatInvoiceNumber","orderId","PAID_STATUSES","Set","invoiceIsPaid","has","String","status","orderPaidAtMs","stamped","paidAtMs","event","timeline","find","entry","atMs","created","invoiceTaxBasis","taxMode","kind","taxCents","max","totals","reason","draftRate","option","source","name","amountCents","rateId","invoiceCheckoutCharge","shippingOptions","shippingCountries","discountCents","charge","lineItems","tax","discount","shippingCents","label","options","draftShipping","map","filter","rate","slice","countries","country","toUpperCase","test","length","invoicePayRefusal","ships","some","line","included","totalCents","invoicePayRefusalMessage","storeName","business","buildInvoicePageView","input","unpaid","stored","itemsCents","taxAtPayment","state","dateMs","version","currency","customerName","company","billTo","billingAddress","shipTo","shippingAddress","lines","discountLabel","taxLabel","shippingAtPayment","notes","terms","statusLabel","receiptUrl","approvalRequired","payable","revision","proofs","response","respondable","invoiceSends","Array","isArray","send","lastInvoiceSend","sends","invoiceSendRecipient","typed","customerEmail","customer","email","invoiceSendPrefill","to","invoiceEmailThread","emailThread","invoiceEmailDefaultSubject","businessName","thread","subject","invoiceSendRefusal","Boolean","refused","invoiceSendRefusalMessage","invoiceSendRefusalStatus","invoiceSendPatch","paid","by"],"mappings":"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"}
@@ -109,8 +109,9 @@ export interface OrderLineItem {
109
109
  /** Per-unit price in cents at purchase time, its modifiers included. */
110
110
  unitAmountCents: number;
111
111
  /**
112
- * Choices added at the register (AGL-3607), as sold. Already folded into
113
- * `variantLabel` and `unitAmountCents`; kept for reports and reorders.
112
+ * Choices added at the register (AGL-3607) or on the website (AGL-4100),
113
+ * as sold. Already folded into `variantLabel` and `unitAmountCents`; kept
114
+ * for reports and reorders.
114
115
  */
115
116
  modifiers?: OrderLineModifier[];
116
117
  /** Supplier at purchase time (dropship routing, AGL-289). */
@@ -877,11 +878,13 @@ export interface BuyNowSessionSource extends CheckoutSessionTotalsSource {
877
878
  export interface BuyNowProductSnapshot {
878
879
  /** Product name at purchase time. */
879
880
  name: string;
880
- /** Joined variant options, e.g. `Large / Blue`. */
881
+ /** Joined variant options, then any add-ons, e.g. `Large / Blue / Walls`. */
881
882
  variantLabel?: string;
882
883
  sku?: string;
883
884
  productType?: ProductType;
884
885
  supplierId?: string;
886
+ /** The add-ons bought with it (AGL-4100), already in `unitAmountCents` and the label. */
887
+ modifiers?: OrderLineModifier[];
885
888
  }
886
889
  /**
887
890
  * The stored line items and totals for a `commerce-order` (buy-now) session
@@ -336,6 +336,7 @@ export function computeCheckoutSessionTotals(lineItems, session, parts) {
336
336
  * which is why it lives here and is not duplicated there.
337
337
  */ export function computeBuyNowOrder(session, snapshot) {
338
338
  var _ref, _ref1, _metadata_quantity, _ref2, _ref3, _ref4, _ref5, _metadata_variantId, _metadata_productId;
339
+ var _snapshot_modifiers;
339
340
  const metadata = (_ref = session == null ? void 0 : session.metadata) != null ? _ref : {};
340
341
  const details = (_ref1 = session == null ? void 0 : session.total_details) != null ? _ref1 : {};
341
342
  const rawQuantity = Math.round(Number((_metadata_quantity = metadata.quantity) != null ? _metadata_quantity : 1));
@@ -368,6 +369,8 @@ export function computeCheckoutSessionTotals(lineItems, session, parts) {
368
369
  productType: snapshot.productType
369
370
  } : {}, snapshot.supplierId ? {
370
371
  supplierId: snapshot.supplierId
372
+ } : {}, ((_snapshot_modifiers = snapshot.modifiers) == null ? void 0 : _snapshot_modifiers.length) ? {
373
+ modifiers: snapshot.modifiers
371
374
  } : {}, {
372
375
  quantity,
373
376
  unitAmountCents