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

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 (140) hide show
  1. package/README.md +4 -2
  2. package/package.json +13 -13
  3. package/src/lib/components/commerce-console-page.js +2 -1
  4. package/src/lib/components/commerce-console-page.js.map +1 -1
  5. package/src/lib/components/console/commerce-pdf-download.d.ts +30 -0
  6. package/src/lib/components/console/commerce-pdf-download.js +53 -0
  7. package/src/lib/components/console/commerce-pdf-download.js.map +1 -0
  8. package/src/lib/components/console/host-orders-card.component.d.ts +6 -0
  9. package/src/lib/components/console/host-orders-card.component.js +3 -2
  10. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  11. package/src/lib/components/console/host-quotes-card.component.js +20 -4
  12. package/src/lib/components/console/host-quotes-card.component.js.map +1 -1
  13. package/src/lib/components/console/invoice-send.component.d.ts +44 -0
  14. package/src/lib/components/console/invoice-send.component.js +237 -0
  15. package/src/lib/components/console/invoice-send.component.js.map +1 -0
  16. package/src/lib/components/console/order-detail-dialog.component.d.ts +5 -0
  17. package/src/lib/components/console/order-detail-dialog.component.js +9 -2
  18. package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
  19. package/src/lib/components/console/order-invoice.component.d.ts +15 -4
  20. package/src/lib/components/console/order-invoice.component.js +93 -12
  21. package/src/lib/components/console/order-invoice.component.js.map +1 -1
  22. package/src/lib/components/console/quote-editor-dialog.component.d.ts +29 -0
  23. package/src/lib/components/console/quote-editor-dialog.component.js +154 -5
  24. package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -1
  25. package/src/lib/components/console/quote-proofs.component.d.ts +48 -0
  26. package/src/lib/components/console/quote-proofs.component.js +694 -0
  27. package/src/lib/components/console/quote-proofs.component.js.map +1 -0
  28. package/src/lib/components/console/quote-send.component.d.ts +42 -0
  29. package/src/lib/components/console/quote-send.component.js +307 -0
  30. package/src/lib/components/console/quote-send.component.js.map +1 -0
  31. package/src/lib/components/console/quote-start.component.d.ts +45 -0
  32. package/src/lib/components/console/quote-start.component.js +182 -0
  33. package/src/lib/components/console/quote-start.component.js.map +1 -0
  34. package/src/lib/components/product-options-field-body.d.ts +44 -0
  35. package/src/lib/components/product-options-field-body.js +260 -0
  36. package/src/lib/components/product-options-field-body.js.map +1 -0
  37. package/src/lib/components/product-options-field.d.ts +32 -0
  38. package/src/lib/components/product-options-field.js +128 -0
  39. package/src/lib/components/product-options-field.js.map +1 -0
  40. package/src/lib/components/quote-page-body.js +67 -20
  41. package/src/lib/components/quote-page-body.js.map +1 -1
  42. package/src/lib/components/quote-proof-panel.d.ts +42 -0
  43. package/src/lib/components/quote-proof-panel.js +436 -0
  44. package/src/lib/components/quote-proof-panel.js.map +1 -0
  45. package/src/lib/model/commerce-invoices.d.ts +132 -0
  46. package/src/lib/model/commerce-invoices.js +332 -0
  47. package/src/lib/model/commerce-invoices.js.map +1 -0
  48. package/src/lib/model/commerce-orders.d.ts +29 -2
  49. package/src/lib/model/commerce-orders.js.map +1 -1
  50. package/src/lib/model/commerce-product-field.d.ts +23 -0
  51. package/src/lib/model/commerce-product-field.js +26 -0
  52. package/src/lib/model/commerce-product-field.js.map +1 -0
  53. package/src/lib/model/commerce-quote-email.d.ts +67 -0
  54. package/src/lib/model/commerce-quote-email.js +154 -0
  55. package/src/lib/model/commerce-quote-email.js.map +1 -0
  56. package/src/lib/model/commerce-quote-pay.d.ts +76 -6
  57. package/src/lib/model/commerce-quote-pay.js +105 -39
  58. package/src/lib/model/commerce-quote-pay.js.map +1 -1
  59. package/src/lib/model/commerce-quote-proofs.d.ts +159 -0
  60. package/src/lib/model/commerce-quote-proofs.js +348 -0
  61. package/src/lib/model/commerce-quote-proofs.js.map +1 -0
  62. package/src/lib/model/commerce-quote-request.d.ts +93 -0
  63. package/src/lib/model/commerce-quote-request.js +207 -0
  64. package/src/lib/model/commerce-quote-request.js.map +1 -0
  65. package/src/lib/model/commerce-quotes.d.ts +140 -3
  66. package/src/lib/model/commerce-quotes.js.map +1 -1
  67. package/src/lib/model/index.d.ts +4 -0
  68. package/src/lib/model/index.js +4 -0
  69. package/src/lib/model/index.js.map +1 -1
  70. package/src/lib/plugin.js +19 -0
  71. package/src/lib/plugin.js.map +1 -1
  72. package/src/lib/server/billing-webhook.js +115 -70
  73. package/src/lib/server/billing-webhook.js.map +1 -1
  74. package/src/lib/server/commerce-pdf.d.ts +104 -0
  75. package/src/lib/server/commerce-pdf.js +310 -0
  76. package/src/lib/server/commerce-pdf.js.map +1 -0
  77. package/src/lib/server/draft-order.js +23 -3
  78. package/src/lib/server/draft-order.js.map +1 -1
  79. package/src/lib/server/invoice-email.d.ts +76 -0
  80. package/src/lib/server/invoice-email.js +183 -0
  81. package/src/lib/server/invoice-email.js.map +1 -0
  82. package/src/lib/server/order-invoice-send.d.ts +43 -0
  83. package/src/lib/server/order-invoice-send.js +218 -0
  84. package/src/lib/server/order-invoice-send.js.map +1 -0
  85. package/src/lib/server/order-print.d.ts +6 -0
  86. package/src/lib/server/order-print.js +22 -5
  87. package/src/lib/server/order-print.js.map +1 -1
  88. package/src/lib/server/order-status.d.ts +11 -0
  89. package/src/lib/server/order-status.js +44 -8
  90. package/src/lib/server/order-status.js.map +1 -1
  91. package/src/lib/server/pdf-response.d.ts +31 -0
  92. package/src/lib/server/pdf-response.js +30 -0
  93. package/src/lib/server/pdf-response.js.map +1 -0
  94. package/src/lib/server/person-eraser.d.ts +3 -2
  95. package/src/lib/server/person-eraser.js +25 -6
  96. package/src/lib/server/person-eraser.js.map +1 -1
  97. package/src/lib/server/pos-reader-splash.d.ts +2 -18
  98. package/src/lib/server/pos-reader-splash.js +8 -93
  99. package/src/lib/server/pos-reader-splash.js.map +1 -1
  100. package/src/lib/server/quote-email.d.ts +85 -0
  101. package/src/lib/server/quote-email.js +211 -0
  102. package/src/lib/server/quote-email.js.map +1 -0
  103. package/src/lib/server/quote-link-token.d.ts +11 -0
  104. package/src/lib/server/quote-link-token.js +21 -0
  105. package/src/lib/server/quote-link-token.js.map +1 -1
  106. package/src/lib/server/quote-pay.d.ts +38 -6
  107. package/src/lib/server/quote-pay.js +115 -53
  108. package/src/lib/server/quote-pay.js.map +1 -1
  109. package/src/lib/server/quote-proofs.d.ts +51 -0
  110. package/src/lib/server/quote-proofs.js +500 -0
  111. package/src/lib/server/quote-proofs.js.map +1 -0
  112. package/src/lib/server/quote-uploads.d.ts +33 -0
  113. package/src/lib/server/quote-uploads.js +29 -0
  114. package/src/lib/server/quote-uploads.js.map +1 -0
  115. package/src/lib/server/quote.d.ts +25 -0
  116. package/src/lib/server/quote.js +638 -8
  117. package/src/lib/server/quote.js.map +1 -1
  118. package/src/lib/server/site-image-source.d.ts +38 -0
  119. package/src/lib/server/site-image-source.js +130 -0
  120. package/src/lib/server/site-image-source.js.map +1 -0
  121. package/src/lib/server/site-page-resolver.js +2 -1
  122. package/src/lib/server/site-page-resolver.js.map +1 -1
  123. package/src/lib/server.js +24 -1
  124. package/src/lib/server.js.map +1 -1
  125. package/src/lib/site.js +8 -0
  126. package/src/lib/site.js.map +1 -1
  127. package/src/lib/tenant-emails.js +243 -0
  128. package/src/lib/tenant-emails.js.map +1 -1
  129. package/src/lib/utils/commerce-document-pdf.d.ts +82 -0
  130. package/src/lib/utils/commerce-document-pdf.js +613 -0
  131. package/src/lib/utils/commerce-document-pdf.js.map +1 -0
  132. package/src/lib/utils/invoice-email-content.d.ts +45 -0
  133. package/src/lib/utils/invoice-email-content.js +126 -0
  134. package/src/lib/utils/invoice-email-content.js.map +1 -0
  135. package/src/lib/utils/order-invoice.d.ts +52 -3
  136. package/src/lib/utils/order-invoice.js +124 -64
  137. package/src/lib/utils/order-invoice.js.map +1 -1
  138. package/src/lib/utils/quote-email-content.d.ts +62 -0
  139. package/src/lib/utils/quote-email-content.js +154 -0
  140. package/src/lib/utils/quote-email-content.js.map +1 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/utils/order-invoice.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 formatOrderNumber,\n orderCreatedAtMs,\n type HostOrder,\n type OrderAddress,\n} from '../model/commerce-orders'\nimport { escapeHtml } from './escape-html'\n\n/**\n * The printable invoice (AGL-3611), beside the packing slip: the order as a\n * bill — who sold it, to whom, each line with its unit price, the totals,\n * what was refunded and what was paid in the end.\n *\n * A string of markup, because it is written into a print popup the way the\n * packing slip is, and the browser's print dialog saves it as a PDF. Every\n * value is escaped: the addresses are typed by the shopper, and the popup\n * inherits the console's origin (AGL-2283). No `<style>` and no `style`\n * attributes, because the popup inherits the console's content security\n * policy; the layout is the table attributes HTML still honors.\n */\n\nexport interface OrderInvoiceStore {\n /** The business's name, as its emails sign. */\n name: string\n /** The store's currency, for an order that does not carry its own. */\n currency: string\n /** The receipt footer the store set, printed at the foot. */\n footer?: string | null\n /** The store's terms address, printed at the foot. */\n termsUrl?: string | null\n}\n\n/**\n * The invoice's store from the site's document and its store settings, as\n * the order dialog's Invoice button and `commerce/order-print` read them.\n */\nexport function invoiceStoreFrom(\n host: Record<string, unknown> | undefined | null,\n store: Record<string, unknown> | undefined | null,\n): OrderInvoiceStore {\n const hostData = host ?? {}\n const storeData = store ?? {}\n return {\n name: String(hostData['businessName'] ?? hostData['displayName'] ?? hostData['name'] ?? ''),\n currency: String(storeData['currency'] ?? 'USD'),\n footer: (storeData['receiptFooter'] as string | undefined) ?? null,\n termsUrl: (storeData['termsUrl'] as string | undefined) ?? null,\n }\n}\n\ntype InvoiceOrder = Pick<\n HostOrder,\n | 'number'\n | 'status'\n | 'lineItems'\n | 'totals'\n | 'refundedCents'\n | 'customerName'\n | 'customerEmail'\n | 'shippingAddress'\n | 'billingAddress'\n> & { createdAtMs?: number; currency?: string; amountCents?: number }\n\n/** Money in the order's currency, integer cents in. */\nexport function invoiceMoney(cents: number, currency: string): string {\n const amount = (Number(cents) || 0) / 100\n try {\n return new Intl.NumberFormat('en-US', { style: 'currency', currency: currency || 'USD' }).format(amount)\n } catch {\n return `${amount.toFixed(2)} ${String(currency || '').toUpperCase()}`.trim()\n }\n}\n\nfunction addressLines(address: OrderAddress | undefined | null): string[] {\n if (!address) return []\n return [\n address.name,\n address.line1,\n address.line2,\n [address.city, address.state, address.postalCode].filter(Boolean).join(' '),\n address.country,\n address.phone,\n ]\n .map((part) => String(part ?? '').trim())\n .filter(Boolean)\n}\n\n/** What the payment line says. */\nexport function invoicePaymentStatus(order: Pick<InvoiceOrder, 'status' | 'refundedCents' | 'totals' | 'amountCents'>): string {\n const total = Number(order.totals?.totalCents ?? order.amountCents ?? 0)\n const refunded = Math.max(0, Number(order.refundedCents ?? 0))\n const status = String(order.status ?? '')\n if (status === 'pending' || status === 'draft') return 'Awaiting payment'\n if (status === 'cancelled') return 'Canceled'\n if (refunded > 0 && refunded >= total) return 'Refunded'\n if (refunded > 0) return 'Paid, partially refunded'\n return 'Paid'\n}\n\n/** The invoice's markup: a full document body, every value escaped. */\nexport function buildOrderInvoiceHtml(input: {\n order: InvoiceOrder\n orderId: string\n store: OrderInvoiceStore\n}): string {\n const { order, orderId, store } = input\n const currency = String(order.currency || store.currency || 'USD').toUpperCase()\n const money = (cents: number) => escapeHtml(invoiceMoney(cents, currency))\n const number = formatOrderNumber(order, orderId)\n const createdAt = orderCreatedAtMs(order as never)\n const totals = order.totals\n const itemsCents =\n totals?.itemsCents ??\n (order.lineItems ?? []).reduce((sum, line) => sum + Number(line.unitAmountCents || 0) * Number(line.quantity || 0), 0)\n const totalCents = Number(totals?.totalCents ?? order.amountCents ?? itemsCents)\n const refundedCents = Math.max(0, Number(order.refundedCents ?? 0))\n\n const billTo = [\n ...(order.billingAddress ? addressLines(order.billingAddress) : [order.customerName ?? ''].filter(Boolean)),\n order.customerEmail ?? '',\n ].filter(Boolean)\n const shipTo = addressLines(order.shippingAddress)\n\n const rows = (order.lineItems ?? [])\n .map((line) => {\n const quantity = Number(line.quantity) || 0\n const unit = Number(line.unitAmountCents) || 0\n const detail = [line.variantLabel ? escapeHtml(line.variantLabel) : '', line.sku ? `SKU ${escapeHtml(line.sku)}` : '']\n .filter(Boolean)\n .join(' · ')\n return (\n `<tr><td>${escapeHtml(line.name)}${detail ? `<br/><small>${detail}</small>` : ''}</td>` +\n `<td align=\"right\">${escapeHtml(quantity)}</td>` +\n `<td align=\"right\">${money(unit)}</td>` +\n `<td align=\"right\">${money(unit * quantity)}</td></tr>`\n )\n })\n .join('')\n\n const summary: Array<[string, string]> = [['Subtotal', money(itemsCents)]]\n if (Number(totals?.discountCents ?? 0) > 0) summary.push(['Discount', `−${money(Number(totals?.discountCents))}`])\n if (Number(totals?.shippingCents ?? 0) > 0) summary.push(['Shipping', money(Number(totals?.shippingCents))])\n if (Number(totals?.taxCents ?? 0) > 0) summary.push(['Tax', money(Number(totals?.taxCents))])\n summary.push(['<b>Total</b>', `<b>${money(totalCents)}</b>`])\n if (Number(totals?.tipCents ?? 0) > 0) summary.push(['Tip', money(Number(totals?.tipCents))])\n if (refundedCents > 0) {\n summary.push(['Refunded', `−${money(refundedCents)}`])\n summary.push(['<b>Net paid</b>', `<b>${money(Math.max(0, totalCents - refundedCents))}</b>`])\n }\n\n const block = (title: string, lines: string[]) =>\n lines.length ? `<td valign=\"top\"><b>${title}</b><br/>${lines.map((line) => escapeHtml(line)).join('<br/>')}</td>` : ''\n\n return (\n `<title>${escapeHtml(`Invoice ${number}`)}</title>` +\n `<h2>${escapeHtml(store.name || 'Invoice')}</h2>` +\n `<h3>${escapeHtml(`Invoice ${number}`)}</h3>` +\n `<p>${createdAt ? `Date: ${escapeHtml(new Date(createdAt).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }))}<br/>` : ''}` +\n `Payment: ${escapeHtml(invoicePaymentStatus(order))}<br/>Currency: ${escapeHtml(currency)}</p>` +\n `<table width=\"100%\" cellpadding=\"6\"><tr>${block('Bill to', billTo)}${block('Ship to', shipTo)}</tr></table>` +\n `<table width=\"100%\" cellpadding=\"6\" border=\"1\" cellspacing=\"0\">` +\n `<tr><th align=\"left\">Item</th><th align=\"right\">Qty</th><th align=\"right\">Unit price</th><th align=\"right\">Amount</th></tr>` +\n rows +\n `</table>` +\n `<table width=\"100%\" cellpadding=\"4\">${summary\n .map(([label, value]) => `<tr><td align=\"right\" width=\"80%\">${label}</td><td align=\"right\">${value}</td></tr>`)\n .join('')}</table>` +\n (store.footer ? `<p>${escapeHtml(store.footer)}</p>` : '') +\n (store.termsUrl ? `<p><small>Terms: ${escapeHtml(store.termsUrl)}</small></p>` : '')\n )\n}\n"],"names":["formatOrderNumber","orderCreatedAtMs","escapeHtml","invoiceStoreFrom","host","store","hostData","storeData","name","String","currency","footer","termsUrl","invoiceMoney","cents","amount","Number","Intl","NumberFormat","style","format","toFixed","toUpperCase","trim","addressLines","address","line1","line2","city","state","postalCode","filter","Boolean","join","country","phone","map","part","invoicePaymentStatus","order","total","totals","totalCents","amountCents","refunded","Math","max","refundedCents","status","buildOrderInvoiceHtml","input","orderId","money","number","createdAt","itemsCents","lineItems","reduce","sum","line","unitAmountCents","quantity","billTo","billingAddress","customerName","customerEmail","shipTo","shippingAddress","rows","unit","detail","variantLabel","sku","summary","discountCents","push","shippingCents","taxCents","tipCents","block","title","lines","length","Date","toLocaleDateString","year","month","day","label","value"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/utils/order-invoice.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 formatOrderNumber,\n orderCreatedAtMs,\n type HostOrder,\n type OrderAddress,\n} from '../model/commerce-orders'\nimport { escapeHtml } from './escape-html'\n\n/**\n * The printable invoice (AGL-3611), beside the packing slip: the order as a\n * bill — who sold it, to whom, each line with its unit price, the totals,\n * what was refunded and what was paid in the end.\n *\n * A string of markup, because it is written into a print popup the way the\n * packing slip is, and the browser's print dialog saves it as a PDF. Every\n * value is escaped: the addresses are typed by the shopper, and the popup\n * inherits the console's origin (AGL-2283). No `<style>` and no `style`\n * attributes, because the popup inherits the console's content security\n * policy; the layout is the table attributes HTML still honors.\n */\n\nexport interface OrderInvoiceStore {\n /** The business's name, as its emails sign. */\n name: string\n /** The store's currency, for an order that does not carry its own. */\n currency: string\n /** The receipt footer the store set, printed at the foot. */\n footer?: string | null\n /** The store's terms address, printed at the foot. */\n termsUrl?: string | null\n}\n\n/**\n * The invoice's store from the site's document and its store settings, as\n * the order dialog's Invoice button and `commerce/order-print` read them.\n */\nexport function invoiceStoreFrom(\n host: Record<string, unknown> | undefined | null,\n store: Record<string, unknown> | undefined | null,\n): OrderInvoiceStore {\n const hostData = host ?? {}\n const storeData = store ?? {}\n return {\n name: String(hostData['businessName'] ?? hostData['displayName'] ?? hostData['name'] ?? ''),\n currency: String(storeData['currency'] ?? 'USD'),\n footer: (storeData['receiptFooter'] as string | undefined) ?? null,\n termsUrl: (storeData['termsUrl'] as string | undefined) ?? null,\n }\n}\n\nexport type InvoiceOrder = Pick<\n HostOrder,\n | 'number'\n | 'status'\n | 'lineItems'\n | 'totals'\n | 'refundedCents'\n | 'customerName'\n | 'customerEmail'\n | 'shippingAddress'\n | 'billingAddress'\n> &\n Partial<Pick<HostOrder, 'quote'>> & { createdAtMs?: number; currency?: string; amountCents?: number }\n\n/** Money in the order's currency, integer cents in. */\nexport function invoiceMoney(cents: number, currency: string): string {\n const amount = (Number(cents) || 0) / 100\n try {\n return new Intl.NumberFormat('en-US', { style: 'currency', currency: currency || 'USD' }).format(amount)\n } catch {\n return `${amount.toFixed(2)} ${String(currency || '').toUpperCase()}`.trim()\n }\n}\n\n/** The United States, however an address names it: left off, as a US envelope does. */\nconst US_COUNTRY = /^(us|usa|u\\.s\\.a?\\.?|united states( of america)?)$/i\n\n/**\n * An address as lines, the American way: name, street, `City, ST 12345`,\n * the country unless it is the United States, then the phone when the\n * address carries one and it is wanted.\n */\nexport function invoiceAddressLines(\n address: Partial<OrderAddress> | undefined | null,\n options: { phone?: boolean } = {},\n): string[] {\n if (!address) return []\n const clean = (part: unknown) => String(part ?? '').trim()\n const region = [clean(address.state), clean(address.postalCode)].filter(Boolean).join(' ')\n return [\n clean(address.name),\n clean(address.line1),\n clean(address.line2),\n [clean(address.city), region].filter(Boolean).join(', '),\n US_COUNTRY.test(clean(address.country)) ? '' : clean(address.country),\n options.phone === false ? '' : clean(address.phone),\n ].filter(Boolean)\n}\n\nconst addressLines = (address: OrderAddress | undefined | null) => invoiceAddressLines(address)\n\n/** What the payment line says. */\nexport function invoicePaymentStatus(order: Pick<InvoiceOrder, 'status' | 'refundedCents' | 'totals' | 'amountCents'>): string {\n const total = Number(order.totals?.totalCents ?? order.amountCents ?? 0)\n const refunded = Math.max(0, Number(order.refundedCents ?? 0))\n const status = String(order.status ?? '')\n if (status === 'pending' || status === 'draft') return 'Awaiting payment'\n if (status === 'cancelled') return 'Canceled'\n if (refunded > 0 && refunded >= total) return 'Refunded'\n if (refunded > 0) return 'Paid, partially refunded'\n return 'Paid'\n}\n\n/** One line of an invoice, as both the printed page and the PDF set it. */\nexport interface InvoiceLine {\n name: string\n /** The option chosen and the SKU, as one line under the name. */\n detail: string\n /** A typed line's own description. */\n description: string\n quantity: number\n unitCents: number\n amountCents: number\n /** Included at no charge. */\n included: boolean\n}\n\n/** One row of an invoice's summary: a figure, or a deduction printed with a minus. */\nexport interface InvoiceSummaryRow {\n label: string\n cents: number\n minus?: boolean\n /** The row the summary leads to: the total, or what was paid after refunds. */\n emphasis?: boolean\n}\n\n/** An invoice's content, worked out once for the printed page and the PDF. */\nexport interface OrderInvoiceData {\n /** `#1042`. */\n number: string\n /** When the order was placed; 0 when it does not say. */\n dateMs: number\n currency: string\n paymentStatus: string\n billTo: string[]\n shipTo: string[]\n lines: InvoiceLine[]\n summary: InvoiceSummaryRow[]\n footer: string\n termsUrl: string\n}\n\n/** What an invoice says, from the order and its store. */\nexport function buildOrderInvoiceData(input: {\n order: InvoiceOrder\n orderId: string\n store: OrderInvoiceStore\n}): OrderInvoiceData {\n const { order, orderId, store } = input\n const currency = String(order.currency || store.currency || 'USD').toUpperCase()\n const totals = order.totals\n const itemsCents =\n totals?.itemsCents ??\n (order.lineItems ?? []).reduce((sum, line) => sum + Number(line.unitAmountCents || 0) * Number(line.quantity || 0), 0)\n const totalCents = Number(totals?.totalCents ?? order.amountCents ?? itemsCents)\n const refundedCents = Math.max(0, Number(order.refundedCents ?? 0))\n\n const lines = (order.lineItems ?? []).map((line): InvoiceLine => {\n const quantity = Number(line.quantity) || 0\n const unit = Number(line.unitAmountCents) || 0\n return {\n name: String(line.name ?? ''),\n detail: [line.variantLabel ? String(line.variantLabel) : '', line.sku ? `SKU ${line.sku}` : ''].filter(Boolean).join(' · '),\n description: String(line.description ?? '').trim(),\n quantity,\n unitCents: unit,\n amountCents: unit * quantity,\n included: line.included === true,\n }\n })\n\n const summary: InvoiceSummaryRow[] = [{ label: 'Subtotal', cents: itemsCents }]\n if (Number(totals?.discountCents ?? 0) > 0) summary.push({ label: 'Discount', cents: Number(totals?.discountCents), minus: true })\n if (Number(totals?.shippingCents ?? 0) > 0) summary.push({ label: 'Shipping', cents: Number(totals?.shippingCents) })\n if (Number(totals?.taxCents ?? 0) > 0) summary.push({ label: 'Tax', cents: Number(totals?.taxCents) })\n summary.push({ label: 'Total', cents: totalCents, emphasis: true })\n if (Number(totals?.tipCents ?? 0) > 0) summary.push({ label: 'Tip', cents: Number(totals?.tipCents) })\n if (refundedCents > 0) {\n summary.push({ label: 'Refunded', cents: refundedCents, minus: true })\n summary.push({ label: 'Net paid', cents: Math.max(0, totalCents - refundedCents), emphasis: true })\n }\n\n // An unpaid quote keeps its customer, its addresses and its date in its\n // quote section until it is paid (AGL-4093); its invoice (AGL-4099) reads\n // them there.\n const quote = order.quote\n const billingAddress = order.billingAddress ?? quote?.billTo ?? null\n const customerName = order.customerName ?? quote?.customer?.name ?? quote?.customer?.company ?? ''\n const customerEmail = order.customerEmail ?? quote?.customer?.email ?? ''\n return {\n number: formatOrderNumber(order, orderId),\n dateMs: orderCreatedAtMs(order as never) || Number(quote?.updatedAtMs ?? quote?.createdAtMs) || 0,\n currency,\n paymentStatus: invoicePaymentStatus(order),\n billTo: [\n ...(billingAddress ? addressLines(billingAddress) : [customerName].filter(Boolean)),\n customerEmail,\n ].filter(Boolean),\n shipTo: addressLines(order.shippingAddress ?? quote?.shipTo ?? null),\n lines,\n summary,\n footer: String(store.footer ?? '').trim(),\n termsUrl: String(store.termsUrl ?? '').trim(),\n }\n}\n\n/** The invoice's markup: a full document body, every value escaped. */\nexport function buildOrderInvoiceHtml(input: {\n order: InvoiceOrder\n orderId: string\n store: OrderInvoiceStore\n}): string {\n const data = buildOrderInvoiceData(input)\n const { store } = input\n const money = (cents: number) => escapeHtml(invoiceMoney(cents, data.currency))\n const number = data.number\n\n const rows = data.lines\n .map((line) => {\n const detail = [line.detail, line.description].filter(Boolean).map((part) => escapeHtml(part)).join('<br/>')\n return (\n `<tr><td>${escapeHtml(line.name)}${detail ? `<br/><small>${detail}</small>` : ''}</td>` +\n `<td align=\"right\">${escapeHtml(line.quantity)}</td>` +\n `<td align=\"right\">${line.included ? '' : money(line.unitCents)}</td>` +\n `<td align=\"right\">${line.included ? 'Included' : money(line.amountCents)}</td></tr>`\n )\n })\n .join('')\n\n const summary = data.summary.map(({ label, cents, minus, emphasis }): [string, string] => {\n const figure = `${minus ? '−' : ''}${money(cents)}`\n return emphasis ? [`<b>${label}</b>`, `<b>${figure}</b>`] : [label, figure]\n })\n\n const block = (title: string, lines: string[]) =>\n lines.length ? `<td valign=\"top\"><b>${title}</b><br/>${lines.map((line) => escapeHtml(line)).join('<br/>')}</td>` : ''\n\n return (\n `<title>${escapeHtml(`Invoice ${number}`)}</title>` +\n `<h2>${escapeHtml(store.name || 'Invoice')}</h2>` +\n `<h3>${escapeHtml(`Invoice ${number}`)}</h3>` +\n `<p>${data.dateMs ? `Date: ${escapeHtml(new Date(data.dateMs).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }))}<br/>` : ''}` +\n `Payment: ${escapeHtml(data.paymentStatus)}<br/>Currency: ${escapeHtml(data.currency)}</p>` +\n `<table width=\"100%\" cellpadding=\"6\"><tr>${block('Bill to', data.billTo)}${block('Ship to', data.shipTo)}</tr></table>` +\n `<table width=\"100%\" cellpadding=\"6\" border=\"1\" cellspacing=\"0\">` +\n `<tr><th align=\"left\">Item</th><th align=\"right\">Qty</th><th align=\"right\">Unit price</th><th align=\"right\">Amount</th></tr>` +\n rows +\n `</table>` +\n `<table width=\"100%\" cellpadding=\"4\">${summary\n .map(([label, value]) => `<tr><td align=\"right\" width=\"80%\">${label}</td><td align=\"right\">${value}</td></tr>`)\n .join('')}</table>` +\n (store.footer ? `<p>${escapeHtml(store.footer)}</p>` : '') +\n (store.termsUrl ? `<p><small>Terms: ${escapeHtml(store.termsUrl)}</small></p>` : '')\n )\n}\n"],"names":["formatOrderNumber","orderCreatedAtMs","escapeHtml","invoiceStoreFrom","host","store","hostData","storeData","name","String","currency","footer","termsUrl","invoiceMoney","cents","amount","Number","Intl","NumberFormat","style","format","toFixed","toUpperCase","trim","US_COUNTRY","invoiceAddressLines","address","options","clean","part","region","state","postalCode","filter","Boolean","join","line1","line2","city","test","country","phone","addressLines","invoicePaymentStatus","order","total","totals","totalCents","amountCents","refunded","Math","max","refundedCents","status","buildOrderInvoiceData","input","quote","orderId","itemsCents","lineItems","reduce","sum","line","unitAmountCents","quantity","lines","map","unit","detail","variantLabel","sku","description","unitCents","included","summary","label","discountCents","push","minus","shippingCents","taxCents","emphasis","tipCents","billingAddress","billTo","customerName","customer","company","customerEmail","email","number","dateMs","updatedAtMs","createdAtMs","paymentStatus","shipTo","shippingAddress","buildOrderInvoiceHtml","data","money","rows","figure","block","title","length","Date","toLocaleDateString","year","month","day","value"],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type HostOrder } from '../model';
18
+ /** The catalog key the quote email is sent and designed under. */
19
+ export declare const QUOTE_EMAIL_KEY = "quote-sent";
20
+ /** The most proof images drawn in the email; the rest are named. */
21
+ export declare const QUOTE_EMAIL_MAX_IMAGES = 3;
22
+ /**
23
+ * The column's inner width in the built-in copy: the 600px column less the
24
+ * section's padding on each side.
25
+ */
26
+ export declare const QUOTE_EMAIL_IMAGE_WIDTH = 552;
27
+ /** The theme's colors the composed markup draws with, resolved by the sender. */
28
+ export interface QuoteEmailColors {
29
+ /** The images' outline: the theme's divider. */
30
+ border: string;
31
+ /** The caption under each image: the theme's secondary text. */
32
+ muted: string;
33
+ }
34
+ export interface QuoteEmailContentInput {
35
+ order: HostOrder;
36
+ quoteId: string;
37
+ businessName: string;
38
+ currency: string;
39
+ /** The subject the email goes under. */
40
+ subject: string;
41
+ /** The business's message, as typed; may be empty. */
42
+ message: string;
43
+ /** The quote's pay link, at the version this email carries. */
44
+ payUrl: string;
45
+ /** A proof's address behind that link, or null when it has none. */
46
+ proofUrl: (proofId: string) => string | null;
47
+ /** This email gives the customer a new pay link, and the earlier email's no longer opens the quote. */
48
+ linkReplaced: boolean;
49
+ colors: QuoteEmailColors;
50
+ /** The theme's corner radius in px, for the images. */
51
+ radius?: number;
52
+ }
53
+ export interface QuoteEmailContent {
54
+ /** The catalog email's merge tokens. */
55
+ tokens: Record<string, string>;
56
+ /** The same email as plain text, for a send whose design cannot be rendered. */
57
+ text: string;
58
+ /** The customer approves the proof before they can pay: the button reads "Review and approve". */
59
+ approvalFirst: boolean;
60
+ }
61
+ /** The quote email's tokens and its plain-text twin. */
62
+ export declare function quoteEmailContent(input: QuoteEmailContentInput): QuoteEmailContent;
@@ -0,0 +1,154 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { currentQuoteProofs, formatOrderMoney, formatQuoteNumber, quotePaymentBlockedByApproval } from "../model/index.js";
17
+ import { escapeHtml } from "./escape-html.js";
18
+ /*==========================================
19
+ * WHAT THE QUOTE EMAIL SAYS (AGL-4097).
20
+ *
21
+ * The `quote-sent` catalog email's tokens, composed per send: its heading,
22
+ * what this email is (a new quote, or an updated revision), the business's
23
+ * own message, the quote's number, revision and total, up to three proof
24
+ * images, the other proof files by name, what to do next and the button. The
25
+ * next step and the button follow the business's approval control
26
+ * (`quotePaymentBlockedByApproval`): while the customer must approve the
27
+ * proof first, the button is "Review and approve"; otherwise it is "View and
28
+ * pay" with the total. Both go to the quote's pay link, where the customer
29
+ * does either.
30
+ *
31
+ * Prices appear only in the tokens the catalog names for them
32
+ * (`quote.total`, `quote.buttonLabel`); the composed markup
33
+ * (`quote.proofImages`) carries pictures and file names, nothing else.
34
+ *
35
+ * Pure: the send route and the sample renders call the same function.
36
+ *=========================================*/ /** The catalog key the quote email is sent and designed under. */ export const QUOTE_EMAIL_KEY = 'quote-sent';
37
+ /** The most proof images drawn in the email; the rest are named. */ export const QUOTE_EMAIL_MAX_IMAGES = 3;
38
+ /**
39
+ * The column's inner width in the built-in copy: the 600px column less the
40
+ * section's padding on each side.
41
+ */ export const QUOTE_EMAIL_IMAGE_WIDTH = 552;
42
+ /**
43
+ * Pictures every inbox draws. WebP is left to the quote page, where it shows:
44
+ * Outlook for Windows draws no WebP.
45
+ */ const EMAIL_IMAGE_TYPES = new Set([
46
+ 'image/png',
47
+ 'image/jpeg',
48
+ 'image/gif'
49
+ ]);
50
+ /** The most file names listed before "and N more". */ const MAX_NAMED_FILES = 5;
51
+ /** `a`, `a and b`, `a, b and c`. */ function joinNames(names) {
52
+ if (names.length <= 1) return names.join('');
53
+ return `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`;
54
+ }
55
+ const plural = (count, one, many)=>`${count} ${count === 1 ? one : many}`;
56
+ /** The quote email's tokens and its plain-text twin. */ export function quoteEmailContent(input) {
57
+ var _order_quote, _quote_revision, _ref, _input_radius;
58
+ var _quote_tax, _order_totals;
59
+ const { order, businessName } = input;
60
+ const quote = (_order_quote = order.quote) != null ? _order_quote : {};
61
+ const business = businessName.trim() || 'the business';
62
+ const revision = Math.max(1, Math.floor(Number((_quote_revision = quote.revision) != null ? _quote_revision : 1)) || 1);
63
+ const number = formatQuoteNumber(order, input.quoteId).replace(/^Quote\s+/, '');
64
+ const taxAtPayment = ((_quote_tax = quote.tax) == null ? void 0 : _quote_tax.kind) === 'at-payment';
65
+ const money = formatOrderMoney(Number((_ref = (_order_totals = order.totals) == null ? void 0 : _order_totals.totalCents) != null ? _ref : 0), input.currency);
66
+ const total = taxAtPayment ? `${money} before tax` : money;
67
+ const approvalFirst = quotePaymentBlockedByApproval(quote);
68
+ // The proofs: pictures an inbox draws, up to three, then every other file by name.
69
+ const proofs = currentQuoteProofs(quote);
70
+ const pictured = [];
71
+ const named = [];
72
+ let morePictures = 0;
73
+ for (const proof of proofs){
74
+ var _proof_contentType;
75
+ const url = EMAIL_IMAGE_TYPES.has(String((_proof_contentType = proof.contentType) != null ? _proof_contentType : '').toLowerCase()) ? input.proofUrl(proof.id) : null;
76
+ if (url && pictured.length < QUOTE_EMAIL_MAX_IMAGES) pictured.push({
77
+ name: proof.name,
78
+ url
79
+ });
80
+ else if (url) morePictures += 1;
81
+ else named.push(proof.name);
82
+ }
83
+ const listed = named.slice(0, MAX_NAMED_FILES);
84
+ const overflow = named.length - listed.length + morePictures;
85
+ const parts = [
86
+ ...listed,
87
+ ...overflow > 0 ? [
88
+ `${plural(overflow, 'more file', 'more files')}`
89
+ ] : []
90
+ ];
91
+ const lead = pictured.length ? 'Also with this revision' : named.length === 1 ? 'Proof' : 'Proofs';
92
+ const seeIt = named.length + morePictures === 1 ? 'See it on your quote page.' : 'See them on your quote page.';
93
+ const proofFiles = parts.length ? `${lead}: ${joinNames(parts)}. ${seeIt}` : '';
94
+ const radius = Math.max(0, Math.round(Number((_input_radius = input.radius) != null ? _input_radius : 4)) || 0);
95
+ const proofImages = proofImagesHtml(pictured, input.payUrl, input.colors, radius);
96
+ const heading = revision >= 2 ? 'Your updated quote' : businessName.trim() ? `Your quote from ${businessName.trim()}` : 'Your quote';
97
+ const intro = revision >= 2 ? `${capitalize(business)} updated your quote. This is revision ${revision}, and it replaces the one sent before.` + (input.linkReplaced ? ' The link in the earlier email no longer opens the quote, so use the button below.' : '') + ' The new version is attached as a PDF.' : `${capitalize(business)} put together a quote for you. The details are below, and the quote is attached as a PDF.`;
98
+ const proofed = proofs.length > 0;
99
+ const approved = proofed && Number(quote.approvedRevision) === revision;
100
+ const nextStep = approvalFirst ? 'Look over the proof, then approve it on your quote page. Once you approve it, you can pay there.' : approved ? 'You approved the proof. Pay on your quote page whenever you’re ready.' : proofed ? 'Look over your quote and the proof. On your quote page you can approve the proof or ask for changes, and pay whenever you’re ready.' : 'Look over your quote, and pay it on your quote page whenever you’re ready.';
101
+ const buttonLabel = approvalFirst ? 'Review and approve' : `View and pay ${money}${taxAtPayment ? ' plus tax' : ''}`;
102
+ const message = input.message.trim();
103
+ const tokens = {
104
+ 'quote.subject': input.subject,
105
+ 'quote.number': number,
106
+ 'quote.revision': String(revision),
107
+ 'quote.total': total,
108
+ 'quote.heading': heading,
109
+ 'quote.intro': intro,
110
+ 'quote.message': message,
111
+ 'quote.proofImages': proofImages,
112
+ 'quote.proofFiles': proofFiles,
113
+ 'quote.nextStep': nextStep,
114
+ 'quote.buttonLabel': buttonLabel,
115
+ 'quote.url': input.payUrl
116
+ };
117
+ const text = [
118
+ heading,
119
+ intro,
120
+ message,
121
+ `Quote: ${number}\nRevision: ${revision}\nTotal: ${total}`,
122
+ pictured.length ? `Proof: ${joinNames(pictured.map((proof)=>proof.name))}` : '',
123
+ proofFiles,
124
+ nextStep,
125
+ `${buttonLabel}: ${input.payUrl}`,
126
+ `Reply to this email to reach ${business}.`
127
+ ].filter(Boolean).join('\n\n');
128
+ return {
129
+ tokens,
130
+ text,
131
+ approvalFirst
132
+ };
133
+ }
134
+ /** The space between two pictures side by side, in px. */ const IMAGE_GAP = 12;
135
+ /**
136
+ * The proof pictures, each linked to the quote page with its file name under
137
+ * it: one across the column, or two or three side by side, so the button
138
+ * stays near. A table, the layout every inbox keeps.
139
+ */ function proofImagesHtml(pictured, payUrl, colors, radius) {
140
+ if (!pictured.length) return '';
141
+ const count = pictured.length;
142
+ const width = Math.floor((QUOTE_EMAIL_IMAGE_WIDTH - IMAGE_GAP * (count - 1)) / count);
143
+ const cells = pictured.map((proof, index)=>{
144
+ const left = index === 0 ? 0 : IMAGE_GAP / 2;
145
+ const right = index === count - 1 ? 0 : IMAGE_GAP / 2;
146
+ return `<td style="width:${Math.round(100 / count)}%;vertical-align:top;padding:4px ${right}px 12px ${left}px;">` + `<a href="${escapeHtml(payUrl)}">` + `<img src="${escapeHtml(proof.url)}" alt="${escapeHtml(`Proof: ${proof.name}`)}" width="${width}" ` + `style="display:block;width:100%;max-width:${width}px;height:auto;box-sizing:border-box;` + `border:1px solid ${colors.border};border-radius:${radius}px;" />` + `</a>` + `<div style="font-size:13px;line-height:1.4;color:${colors.muted};padding-top:6px;word-break:break-word;">${escapeHtml(proof.name)}</div>` + `</td>`;
147
+ });
148
+ return `<table role="presentation" style="width:100%;border-collapse:collapse;">` + `<tr>${cells.join('')}</tr>` + `</table>`;
149
+ }
150
+ function capitalize(value) {
151
+ return value ? value[0].toUpperCase() + value.slice(1) : value;
152
+ }
153
+
154
+ //# sourceMappingURL=quote-email-content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/utils/quote-email-content.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 currentQuoteProofs,\n formatOrderMoney,\n formatQuoteNumber,\n quotePaymentBlockedByApproval,\n type HostOrder,\n type OrderQuote,\n} from '../model'\nimport { escapeHtml } from './escape-html'\n\n/*==========================================\n * WHAT THE QUOTE EMAIL SAYS (AGL-4097).\n *\n * The `quote-sent` catalog email's tokens, composed per send: its heading,\n * what this email is (a new quote, or an updated revision), the business's\n * own message, the quote's number, revision and total, up to three proof\n * images, the other proof files by name, what to do next and the button. The\n * next step and the button follow the business's approval control\n * (`quotePaymentBlockedByApproval`): while the customer must approve the\n * proof first, the button is \"Review and approve\"; otherwise it is \"View and\n * pay\" with the total. Both go to the quote's pay link, where the customer\n * does either.\n *\n * Prices appear only in the tokens the catalog names for them\n * (`quote.total`, `quote.buttonLabel`); the composed markup\n * (`quote.proofImages`) carries pictures and file names, nothing else.\n *\n * Pure: the send route and the sample renders call the same function.\n *=========================================*/\n\n/** The catalog key the quote email is sent and designed under. */\nexport const QUOTE_EMAIL_KEY = 'quote-sent'\n\n/** The most proof images drawn in the email; the rest are named. */\nexport const QUOTE_EMAIL_MAX_IMAGES = 3\n\n/**\n * The column's inner width in the built-in copy: the 600px column less the\n * section's padding on each side.\n */\nexport const QUOTE_EMAIL_IMAGE_WIDTH = 552\n\n/**\n * Pictures every inbox draws. WebP is left to the quote page, where it shows:\n * Outlook for Windows draws no WebP.\n */\nconst EMAIL_IMAGE_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif'])\n\n/** The most file names listed before \"and N more\". */\nconst MAX_NAMED_FILES = 5\n\n/** The theme's colors the composed markup draws with, resolved by the sender. */\nexport interface QuoteEmailColors {\n /** The images' outline: the theme's divider. */\n border: string\n /** The caption under each image: the theme's secondary text. */\n muted: string\n}\n\nexport interface QuoteEmailContentInput {\n order: HostOrder\n quoteId: string\n businessName: string\n currency: string\n /** The subject the email goes under. */\n subject: string\n /** The business's message, as typed; may be empty. */\n message: string\n /** The quote's pay link, at the version this email carries. */\n payUrl: string\n /** A proof's address behind that link, or null when it has none. */\n proofUrl: (proofId: string) => string | null\n /** This email gives the customer a new pay link, and the earlier email's no longer opens the quote. */\n linkReplaced: boolean\n colors: QuoteEmailColors\n /** The theme's corner radius in px, for the images. */\n radius?: number\n}\n\nexport interface QuoteEmailContent {\n /** The catalog email's merge tokens. */\n tokens: Record<string, string>\n /** The same email as plain text, for a send whose design cannot be rendered. */\n text: string\n /** The customer approves the proof before they can pay: the button reads \"Review and approve\". */\n approvalFirst: boolean\n}\n\n/** `a`, `a and b`, `a, b and c`. */\nfunction joinNames(names: readonly string[]): string {\n if (names.length <= 1) return names.join('')\n return `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`\n}\n\nconst plural = (count: number, one: string, many: string) => `${count} ${count === 1 ? one : many}`\n\n/** The quote email's tokens and its plain-text twin. */\nexport function quoteEmailContent(input: QuoteEmailContentInput): QuoteEmailContent {\n const { order, businessName } = input\n const quote = (order.quote ?? {}) as OrderQuote\n const business = businessName.trim() || 'the business'\n const revision = Math.max(1, Math.floor(Number(quote.revision ?? 1)) || 1)\n const number = formatQuoteNumber(order, input.quoteId).replace(/^Quote\\s+/, '')\n const taxAtPayment = quote.tax?.kind === 'at-payment'\n const money = formatOrderMoney(Number(order.totals?.totalCents ?? 0), input.currency)\n const total = taxAtPayment ? `${money} before tax` : money\n const approvalFirst = quotePaymentBlockedByApproval(quote)\n\n // The proofs: pictures an inbox draws, up to three, then every other file by name.\n const proofs = currentQuoteProofs(quote)\n const pictured: Array<{ name: string; url: string }> = []\n const named: string[] = []\n let morePictures = 0\n for (const proof of proofs) {\n const url = EMAIL_IMAGE_TYPES.has(String(proof.contentType ?? '').toLowerCase()) ? input.proofUrl(proof.id) : null\n if (url && pictured.length < QUOTE_EMAIL_MAX_IMAGES) pictured.push({ name: proof.name, url })\n else if (url) morePictures += 1\n else named.push(proof.name)\n }\n const listed = named.slice(0, MAX_NAMED_FILES)\n const overflow = named.length - listed.length + morePictures\n const parts = [...listed, ...(overflow > 0 ? [`${plural(overflow, 'more file', 'more files')}`] : [])]\n const lead = pictured.length ? 'Also with this revision' : named.length === 1 ? 'Proof' : 'Proofs'\n const seeIt = named.length + morePictures === 1 ? 'See it on your quote page.' : 'See them on your quote page.'\n const proofFiles = parts.length ? `${lead}: ${joinNames(parts)}. ${seeIt}` : ''\n\n const radius = Math.max(0, Math.round(Number(input.radius ?? 4)) || 0)\n const proofImages = proofImagesHtml(pictured, input.payUrl, input.colors, radius)\n\n const heading = revision >= 2 ? 'Your updated quote' : businessName.trim() ? `Your quote from ${businessName.trim()}` : 'Your quote'\n const intro =\n revision >= 2\n ? `${capitalize(business)} updated your quote. This is revision ${revision}, and it replaces the one sent before.` +\n (input.linkReplaced ? ' The link in the earlier email no longer opens the quote, so use the button below.' : '') +\n ' The new version is attached as a PDF.'\n : `${capitalize(business)} put together a quote for you. The details are below, and the quote is attached as a PDF.`\n\n const proofed = proofs.length > 0\n const approved = proofed && Number(quote.approvedRevision) === revision\n const nextStep = approvalFirst\n ? 'Look over the proof, then approve it on your quote page. Once you approve it, you can pay there.'\n : approved\n ? 'You approved the proof. Pay on your quote page whenever you’re ready.'\n : proofed\n ? 'Look over your quote and the proof. On your quote page you can approve the proof or ask for changes, and pay whenever you’re ready.'\n : 'Look over your quote, and pay it on your quote page whenever you’re ready.'\n const buttonLabel = approvalFirst ? 'Review and approve' : `View and pay ${money}${taxAtPayment ? ' plus tax' : ''}`\n const message = input.message.trim()\n\n const tokens: Record<string, string> = {\n 'quote.subject': input.subject,\n 'quote.number': number,\n 'quote.revision': String(revision),\n 'quote.total': total,\n 'quote.heading': heading,\n 'quote.intro': intro,\n 'quote.message': message,\n 'quote.proofImages': proofImages,\n 'quote.proofFiles': proofFiles,\n 'quote.nextStep': nextStep,\n 'quote.buttonLabel': buttonLabel,\n 'quote.url': input.payUrl,\n }\n\n const text = [\n heading,\n intro,\n message,\n `Quote: ${number}\\nRevision: ${revision}\\nTotal: ${total}`,\n pictured.length ? `Proof: ${joinNames(pictured.map((proof) => proof.name))}` : '',\n proofFiles,\n nextStep,\n `${buttonLabel}: ${input.payUrl}`,\n `Reply to this email to reach ${business}.`,\n ]\n .filter(Boolean)\n .join('\\n\\n')\n\n return { tokens, text, approvalFirst }\n}\n\n/** The space between two pictures side by side, in px. */\nconst IMAGE_GAP = 12\n\n/**\n * The proof pictures, each linked to the quote page with its file name under\n * it: one across the column, or two or three side by side, so the button\n * stays near. A table, the layout every inbox keeps.\n */\nfunction proofImagesHtml(\n pictured: ReadonlyArray<{ name: string; url: string }>,\n payUrl: string,\n colors: QuoteEmailColors,\n radius: number,\n): string {\n if (!pictured.length) return ''\n const count = pictured.length\n const width = Math.floor((QUOTE_EMAIL_IMAGE_WIDTH - IMAGE_GAP * (count - 1)) / count)\n const cells = pictured.map((proof, index) => {\n const left = index === 0 ? 0 : IMAGE_GAP / 2\n const right = index === count - 1 ? 0 : IMAGE_GAP / 2\n return (\n `<td style=\"width:${Math.round(100 / count)}%;vertical-align:top;padding:4px ${right}px 12px ${left}px;\">` +\n `<a href=\"${escapeHtml(payUrl)}\">` +\n `<img src=\"${escapeHtml(proof.url)}\" alt=\"${escapeHtml(`Proof: ${proof.name}`)}\" width=\"${width}\" ` +\n `style=\"display:block;width:100%;max-width:${width}px;height:auto;box-sizing:border-box;` +\n `border:1px solid ${colors.border};border-radius:${radius}px;\" />` +\n `</a>` +\n `<div style=\"font-size:13px;line-height:1.4;color:${colors.muted};padding-top:6px;word-break:break-word;\">${escapeHtml(proof.name)}</div>` +\n `</td>`\n )\n })\n return (\n `<table role=\"presentation\" style=\"width:100%;border-collapse:collapse;\">` +\n `<tr>${cells.join('')}</tr>` +\n `</table>`\n )\n}\n\nfunction capitalize(value: string): string {\n return value ? value[0].toUpperCase() + value.slice(1) : value\n}\n"],"names":["currentQuoteProofs","formatOrderMoney","formatQuoteNumber","quotePaymentBlockedByApproval","escapeHtml","QUOTE_EMAIL_KEY","QUOTE_EMAIL_MAX_IMAGES","QUOTE_EMAIL_IMAGE_WIDTH","EMAIL_IMAGE_TYPES","Set","MAX_NAMED_FILES","joinNames","names","length","join","slice","plural","count","one","many","quoteEmailContent","input","order","quote","businessName","business","trim","revision","Math","max","floor","Number","number","quoteId","replace","taxAtPayment","tax","kind","money","totals","totalCents","currency","total","approvalFirst","proofs","pictured","named","morePictures","proof","url","has","String","contentType","toLowerCase","proofUrl","id","push","name","listed","overflow","parts","lead","seeIt","proofFiles","radius","round","proofImages","proofImagesHtml","payUrl","colors","heading","intro","capitalize","linkReplaced","proofed","approved","approvedRevision","nextStep","buttonLabel","message","tokens","subject","text","map","filter","Boolean","IMAGE_GAP","width","cells","index","left","right","border","muted","value","toUpperCase"],"mappings":"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"}