@aglyn/plugins-commerce 1.0.0-beta.290 → 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 (111) 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/host-orders-card.component.d.ts +6 -0
  6. package/src/lib/components/console/host-orders-card.component.js +3 -2
  7. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  8. package/src/lib/components/console/invoice-send.component.d.ts +44 -0
  9. package/src/lib/components/console/invoice-send.component.js +237 -0
  10. package/src/lib/components/console/invoice-send.component.js.map +1 -0
  11. package/src/lib/components/console/order-detail-dialog.component.d.ts +5 -0
  12. package/src/lib/components/console/order-detail-dialog.component.js +9 -2
  13. package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
  14. package/src/lib/components/console/order-invoice.component.d.ts +9 -0
  15. package/src/lib/components/console/order-invoice.component.js +27 -2
  16. package/src/lib/components/console/order-invoice.component.js.map +1 -1
  17. package/src/lib/components/console/quote-editor-dialog.component.d.ts +29 -0
  18. package/src/lib/components/console/quote-editor-dialog.component.js +102 -4
  19. package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -1
  20. package/src/lib/components/console/quote-proofs.component.d.ts +11 -0
  21. package/src/lib/components/console/quote-proofs.component.js +68 -0
  22. package/src/lib/components/console/quote-proofs.component.js.map +1 -1
  23. package/src/lib/components/console/quote-send.component.d.ts +42 -0
  24. package/src/lib/components/console/quote-send.component.js +307 -0
  25. package/src/lib/components/console/quote-send.component.js.map +1 -0
  26. package/src/lib/components/console/quote-start.component.d.ts +45 -0
  27. package/src/lib/components/console/quote-start.component.js +182 -0
  28. package/src/lib/components/console/quote-start.component.js.map +1 -0
  29. package/src/lib/components/product-options-field-body.d.ts +44 -0
  30. package/src/lib/components/product-options-field-body.js +260 -0
  31. package/src/lib/components/product-options-field-body.js.map +1 -0
  32. package/src/lib/components/product-options-field.d.ts +32 -0
  33. package/src/lib/components/product-options-field.js +128 -0
  34. package/src/lib/components/product-options-field.js.map +1 -0
  35. package/src/lib/components/quote-page-body.js +39 -20
  36. package/src/lib/components/quote-page-body.js.map +1 -1
  37. package/src/lib/model/commerce-invoices.d.ts +132 -0
  38. package/src/lib/model/commerce-invoices.js +332 -0
  39. package/src/lib/model/commerce-invoices.js.map +1 -0
  40. package/src/lib/model/commerce-orders.d.ts +29 -2
  41. package/src/lib/model/commerce-orders.js.map +1 -1
  42. package/src/lib/model/commerce-product-field.d.ts +23 -0
  43. package/src/lib/model/commerce-product-field.js +26 -0
  44. package/src/lib/model/commerce-product-field.js.map +1 -0
  45. package/src/lib/model/commerce-quote-email.d.ts +67 -0
  46. package/src/lib/model/commerce-quote-email.js +154 -0
  47. package/src/lib/model/commerce-quote-email.js.map +1 -0
  48. package/src/lib/model/commerce-quote-pay.d.ts +49 -6
  49. package/src/lib/model/commerce-quote-pay.js +69 -36
  50. package/src/lib/model/commerce-quote-pay.js.map +1 -1
  51. package/src/lib/model/commerce-quote-request.d.ts +93 -0
  52. package/src/lib/model/commerce-quote-request.js +207 -0
  53. package/src/lib/model/commerce-quote-request.js.map +1 -0
  54. package/src/lib/model/commerce-quotes.d.ts +81 -0
  55. package/src/lib/model/commerce-quotes.js.map +1 -1
  56. package/src/lib/model/index.d.ts +3 -0
  57. package/src/lib/model/index.js +3 -0
  58. package/src/lib/model/index.js.map +1 -1
  59. package/src/lib/plugin.js +19 -0
  60. package/src/lib/plugin.js.map +1 -1
  61. package/src/lib/server/billing-webhook.js +115 -70
  62. package/src/lib/server/billing-webhook.js.map +1 -1
  63. package/src/lib/server/commerce-pdf.d.ts +5 -1
  64. package/src/lib/server/commerce-pdf.js +37 -10
  65. package/src/lib/server/commerce-pdf.js.map +1 -1
  66. package/src/lib/server/draft-order.js +23 -3
  67. package/src/lib/server/draft-order.js.map +1 -1
  68. package/src/lib/server/invoice-email.d.ts +76 -0
  69. package/src/lib/server/invoice-email.js +183 -0
  70. package/src/lib/server/invoice-email.js.map +1 -0
  71. package/src/lib/server/order-invoice-send.d.ts +43 -0
  72. package/src/lib/server/order-invoice-send.js +218 -0
  73. package/src/lib/server/order-invoice-send.js.map +1 -0
  74. package/src/lib/server/order-status.d.ts +11 -0
  75. package/src/lib/server/order-status.js +44 -8
  76. package/src/lib/server/order-status.js.map +1 -1
  77. package/src/lib/server/person-eraser.d.ts +3 -2
  78. package/src/lib/server/person-eraser.js +25 -6
  79. package/src/lib/server/person-eraser.js.map +1 -1
  80. package/src/lib/server/quote-email.d.ts +85 -0
  81. package/src/lib/server/quote-email.js +211 -0
  82. package/src/lib/server/quote-email.js.map +1 -0
  83. package/src/lib/server/quote-link-token.d.ts +11 -0
  84. package/src/lib/server/quote-link-token.js +21 -0
  85. package/src/lib/server/quote-link-token.js.map +1 -1
  86. package/src/lib/server/quote-pay.d.ts +8 -3
  87. package/src/lib/server/quote-pay.js +105 -43
  88. package/src/lib/server/quote-pay.js.map +1 -1
  89. package/src/lib/server/quote.d.ts +11 -0
  90. package/src/lib/server/quote.js +309 -12
  91. package/src/lib/server/quote.js.map +1 -1
  92. package/src/lib/server/site-page-resolver.js +2 -1
  93. package/src/lib/server/site-page-resolver.js.map +1 -1
  94. package/src/lib/server.js +5 -0
  95. package/src/lib/server.js.map +1 -1
  96. package/src/lib/site.js +8 -0
  97. package/src/lib/site.js.map +1 -1
  98. package/src/lib/tenant-emails.js +243 -0
  99. package/src/lib/tenant-emails.js.map +1 -1
  100. package/src/lib/utils/commerce-document-pdf.d.ts +5 -0
  101. package/src/lib/utils/commerce-document-pdf.js +6 -1
  102. package/src/lib/utils/commerce-document-pdf.js.map +1 -1
  103. package/src/lib/utils/invoice-email-content.d.ts +45 -0
  104. package/src/lib/utils/invoice-email-content.js +126 -0
  105. package/src/lib/utils/invoice-email-content.js.map +1 -0
  106. package/src/lib/utils/order-invoice.d.ts +1 -1
  107. package/src/lib/utils/order-invoice.js +14 -6
  108. package/src/lib/utils/order-invoice.js.map +1 -1
  109. package/src/lib/utils/quote-email-content.d.ts +62 -0
  110. package/src/lib/utils/quote-email-content.js +154 -0
  111. package/src/lib/utils/quote-email-content.js.map +1 -0
@@ -74,7 +74,7 @@ export declare function quotePdfFile(input: {
74
74
  /** Printed on a quote still to be paid; null leaves it off. */
75
75
  payUrl: string | null;
76
76
  }, deps?: CommercePdfBrandDeps): Promise<CommercePdfFile>;
77
- /** An order's invoice PDF, a paid quote's included. */
77
+ /** An order's invoice PDF, a paid quote's included; an unpaid one carries its pay link when given (AGL-4099). */
78
78
  export declare function orderInvoicePdfFile(input: {
79
79
  hostId: string;
80
80
  orderId: string;
@@ -85,11 +85,15 @@ export declare function orderInvoicePdfFile(input: {
85
85
  id?: string;
86
86
  org?: unknown;
87
87
  } | null;
88
+ /** Printed on an invoice still to be paid; absent or null leaves it off. */
89
+ payUrl?: string | null;
88
90
  }, deps?: CommercePdfBrandDeps): Promise<CommercePdfFile>;
89
91
  /** Downloads one address may make per site, per ten minutes. */
90
92
  export declare const QUOTE_PDF_DOWNLOADS_PER_WINDOW = 20;
91
93
  export interface QuotePdfRouteDeps extends QuotePayDeps {
92
94
  quotePdf: typeof quotePdfFile;
95
+ /** An invoice's PDF, for the link of an order that is not a quote (AGL-4099). */
96
+ invoicePdf?: typeof orderInvoicePdfFile;
93
97
  }
94
98
  export declare function handleQuotePdf(deps: QuotePdfRouteDeps, req: PluginApiRequest): Promise<QuoteLinkOutcome | {
95
99
  status: 200;
@@ -15,7 +15,7 @@
15
15
  * limitations under the License.
16
16
  */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
17
  import { resolveSiteTimeZone } from "@aglyn/aglyn/app-utils/collection-entry-date";
18
- import { resolveHostToken } from "@aglyn/aglyn/app-utils/host-tokens";
18
+ import { siteContactEmail, resolveHostToken } from "@aglyn/aglyn/app-utils/host-tokens";
19
19
  import { resolveSiteTheme } from "@aglyn/aglyn/app-utils/site-theme";
20
20
  import { buildEmailPalette, emailPaletteBaseForHost } from "@aglyn/shared-util-email";
21
21
  import { readJpeg } from "@aglyn/shared-util-tools/pdf/pdf-file";
@@ -43,7 +43,10 @@ import { locateImageSource, readImageSourceBytes } from "./site-image-source.js"
43
43
  *
44
44
  * POST commerce/quote { action: 'pdf' } the console's quote ("Create and edit quotes")
45
45
  * GET commerce/order-print?kind=invoice-pdf the console's and the apps' invoice ("Manage orders")
46
- * GET commerce/quote-pdf?hostId=&q=&t= the customer's quote, by its signed link
46
+ * GET commerce/quote-pdf?hostId=&q=&t= the customer's quote, by its signed link,
47
+ * or an order's invoice by the same kind (AGL-4099)
48
+ *
49
+ * And the invoice email (AGL-4099, `invoice-email.ts`) attaches the invoice.
47
50
  *=========================================*/ /** A logo source larger than this is not read. */ const LOGO_SOURCE_MAX_BYTES = 10000000;
48
51
  /** The logo's pixels: about four times the box it is drawn in, never enlarged. */ const LOGO_PIXELS = {
49
52
  width: 720,
@@ -62,11 +65,12 @@ const trimmed = (value)=>typeof value === 'string' ? value.trim() : '';
62
65
  * block of text, a line per line), the email visitors write to, its phone,
63
66
  * and its web address without the scheme.
64
67
  */ export function commercePdfContactLines(host) {
65
- var _source_seo_entity, _source_seo, _source_business, _source_seo_entity1, _source_seo1;
68
+ var _siteContactEmail;
69
+ var _source_seo_entity, _source_seo;
66
70
  const source = host != null ? host : {};
67
71
  const address = trimmed(resolveHostToken('address', source)).split(/\r?\n/).map((line)=>line.trim()).filter(Boolean);
68
- const email = trimmed((_source_seo = source.seo) == null ? void 0 : (_source_seo_entity = _source_seo.entity) == null ? void 0 : _source_seo_entity.email) || trimmed((_source_business = source.business) == null ? void 0 : _source_business.supportEmail);
69
- const phone = trimmed((_source_seo1 = source.seo) == null ? void 0 : (_source_seo_entity1 = _source_seo1.entity) == null ? void 0 : _source_seo_entity1.telephone);
72
+ const email = (_siteContactEmail = siteContactEmail(source)) != null ? _siteContactEmail : '';
73
+ const phone = trimmed((_source_seo = source.seo) == null ? void 0 : (_source_seo_entity = _source_seo.entity) == null ? void 0 : _source_seo_entity.telephone);
70
74
  const site = trimmed(resolveHostToken('url', source)).replace(/^https?:\/\//, '').replace(/\/$/, '');
71
75
  return [
72
76
  ...address,
@@ -198,7 +202,8 @@ export function defaultCommercePdfBrandDeps() {
198
202
  filename: commercePdfFilename(brand.businessName, 'quote', input.order.number, input.quoteId)
199
203
  };
200
204
  }
201
- /** An order's invoice PDF, a paid quote's included. */ export async function orderInvoicePdfFile(input, deps) {
205
+ /** An order's invoice PDF, a paid quote's included; an unpaid one carries its pay link when given (AGL-4099). */ export async function orderInvoicePdfFile(input, deps) {
206
+ var _input_payUrl;
202
207
  const brand = await loadCommercePdfBrand({
203
208
  hostId: input.hostId,
204
209
  host: input.host,
@@ -209,7 +214,8 @@ export function defaultCommercePdfBrandDeps() {
209
214
  brand,
210
215
  order: input.order,
211
216
  orderId: input.orderId,
212
- store: invoiceStoreFrom(input.host, input.store)
217
+ store: invoiceStoreFrom(input.host, input.store),
218
+ payUrl: (_input_payUrl = input.payUrl) != null ? _input_payUrl : null
213
219
  }),
214
220
  filename: commercePdfFilename(brand.businessName, 'invoice', input.order.number, input.orderId)
215
221
  };
@@ -240,11 +246,32 @@ export async function handleQuotePdf(deps, req) {
240
246
  };
241
247
  const refused = await limitedQuoteLink(deps, req, `commerce-quote-pdf:${hostId}`, QUOTE_PDF_DOWNLOADS_PER_WINDOW);
242
248
  if (refused) return refused;
243
- const linked = await loadLinkedQuote(deps, hostId, quoteId, token);
249
+ const linked = await loadLinkedQuote(deps, hostId, quoteId, token, {
250
+ invoices: true
251
+ });
244
252
  if ('outcome' in linked) return linked.outcome;
245
- const { host, order, store } = linked.loaded;
246
- const payUrl = CommerceModel.isUnpaidQuote(order) ? quotePayLinkUrl(host, hostId, quoteId, CommerceModel.quoteLinkVersion(order.quote)) : null;
253
+ const { host, order, store, kind } = linked.loaded;
254
+ const payUrl = order.status === 'pending' ? quotePayLinkUrl(host, hostId, quoteId, CommerceModel.orderPayLinkVersion(order)) : null;
247
255
  const owner = await deps.orgForHost(hostId).catch(()=>null);
256
+ if (kind === 'invoice') {
257
+ var _deps_invoicePdf, _store_data1;
258
+ const pdf = await ((_deps_invoicePdf = deps.invoicePdf) != null ? _deps_invoicePdf : orderInvoicePdfFile)({
259
+ hostId,
260
+ orderId: quoteId,
261
+ order,
262
+ host,
263
+ store: (_store_data1 = store.data()) != null ? _store_data1 : null,
264
+ owner: owner ? {
265
+ id: owner.id,
266
+ org: owner.org
267
+ } : null,
268
+ payUrl
269
+ });
270
+ return {
271
+ status: 200,
272
+ pdf
273
+ };
274
+ }
248
275
  const pdf = await deps.quotePdf({
249
276
  hostId,
250
277
  quoteId,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/commerce-pdf.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginApiHandler, PluginApiRequest } from '@aglyn/aglyn/server'\nimport { resolveSiteTimeZone } from '@aglyn/aglyn/app-utils/collection-entry-date'\nimport { resolveHostToken, type HostTokenSource } from '@aglyn/aglyn/app-utils/host-tokens'\nimport { resolveSiteTheme, type ThemeHostDocument } from '@aglyn/aglyn/app-utils/site-theme'\nimport { buildEmailPalette, emailPaletteBaseForHost } from '@aglyn/shared-util-email'\nimport { readJpeg, type PdfJpegImage } from '@aglyn/shared-util-tools/pdf/pdf-file'\nimport { getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { loadSharp } from '@aglyn/tenant-data-admin/server/media-variants'\nimport * as CommerceModel from '../model'\nimport {\n COMMERCE_PDF_NEUTRAL_COLORS,\n commercePdfColorsFromPalette,\n renderOrderInvoicePdf,\n renderQuotePdf,\n type CommercePdfBrand,\n type CommercePdfColors,\n} from '../utils/commerce-document-pdf'\nimport { invoiceStoreFrom } from '../utils/order-invoice'\nimport { orderStatusUrl } from './order-status-token'\nimport { sendPdf, type CommercePdfFile } from './pdf-response'\nimport { quotePayLinkUrl } from './quote-link-token'\nimport {\n defaultQuotePayDeps,\n limitedQuoteLink,\n loadLinkedQuote,\n quoteLinkParams,\n sendQuoteLinkOutcome,\n type QuoteLinkOutcome,\n type QuotePayDeps,\n} from './quote-pay'\nimport { locateImageSource, readImageSourceBytes } from './site-image-source'\n\n/*==========================================\n * A QUOTE'S AND AN ORDER'S PDF ON THE SERVER (AGL-4095).\n *\n * What the renderers in `utils/commerce-document-pdf.ts` are given comes from\n * here: the site's business name and contact, its theme's colors, its logo\n * (read from storage or its public address, converted to a JPEG on white by\n * sharp, and left out when there is none or it cannot be read), its time\n * zone, and the quote or order itself.\n *\n * Three doors use it, each loading this module only when it is asked for a\n * PDF, so sharp and the PDF writer stay out of every other request:\n *\n * POST commerce/quote { action: 'pdf' } the console's quote (\"Create and edit quotes\")\n * GET commerce/order-print?kind=invoice-pdf the console's and the apps' invoice (\"Manage orders\")\n * GET commerce/quote-pdf?hostId=&q=&t= the customer's quote, by its signed link\n *=========================================*/\n\n/** A logo source larger than this is not read. */\nconst LOGO_SOURCE_MAX_BYTES = 10_000_000\n\n/** The logo's pixels: about four times the box it is drawn in, never enlarged. */\nconst LOGO_PIXELS = { width: 720, height: 200 } as const\n\n/** A converted logo larger than this is left out rather than bloat every copy of the document. */\nconst LOGO_MAX_JPEG_BYTES = 350_000\n\n/** A source whose decoded pixels exceed this is refused rather than decoded. */\nconst LOGO_MAX_INPUT_PIXELS = 50_000_000\n\n/** The ground a logo's transparent pixels are flattened onto: the page's white. */\nconst LOGO_GROUND = { r: 255, g: 255, b: 255 } as const\n\ntype HostDocument = Record<string, unknown>\n\nconst trimmed = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/**\n * The business's contact as the document prints it: its postal address (a\n * block of text, a line per line), the email visitors write to, its phone,\n * and its web address without the scheme.\n */\nexport function commercePdfContactLines(host: HostDocument | null | undefined): string[] {\n const source = (host ?? {}) as HostTokenSource & {\n seo?: { entity?: { email?: unknown; telephone?: unknown } }\n business?: { supportEmail?: unknown }\n }\n const address = trimmed(resolveHostToken('address', source))\n .split(/\\r?\\n/)\n .map((line) => line.trim())\n .filter(Boolean)\n const email = trimmed(source.seo?.entity?.email) || trimmed(source.business?.supportEmail)\n const phone = trimmed(source.seo?.entity?.telephone)\n const site = trimmed(resolveHostToken('url', source)).replace(/^https?:\\/\\//, '').replace(/\\/$/, '')\n return [...address, email, phone, site].filter(Boolean)\n}\n\n/** The site theme's colors for a document, or the neutral ones when the theme cannot be read. */\nexport function commercePdfSiteColors(host: HostDocument | null | undefined): CommercePdfColors {\n try {\n const theme = resolveSiteTheme((host ?? null) as ThemeHostDocument | null)\n return commercePdfColorsFromPalette(\n buildEmailPalette({\n base: emailPaletteBaseForHost(host as { cname?: string; subdomain?: string } | null),\n colors: (theme?.colorSchemes?.light ?? null) as Record<string, unknown> | null,\n }),\n )\n } catch {\n return { ...COMMERCE_PDF_NEUTRAL_COLORS }\n }\n}\n\n/** sharp's decoding options; an SVG is rasterized dense enough for the logo's pixels. */\nasync function decodeOptions(sharp: Awaited<ReturnType<typeof loadSharp>>, source: Buffer) {\n const base = { limitInputPixels: LOGO_MAX_INPUT_PIXELS, failOn: 'error' as const }\n const meta = await sharp(source, base).metadata()\n if (meta.format !== 'svg') return base\n const longest = Math.max(meta.width ?? 0, meta.height ?? 0)\n if (!longest) return base\n return { ...base, density: Math.min(2400, Math.max(1, Math.ceil((72 * LOGO_PIXELS.width) / longest))) }\n}\n\n/**\n * A logo's bytes as a JPEG the PDF can carry: turned upright, fitted inside\n * {@link LOGO_PIXELS}, transparency flattened onto white, and small enough to\n * attach. Null when the bytes are not an image sharp reads.\n */\nexport async function logoJpeg(source: Buffer, sharpFactory?: Awaited<ReturnType<typeof loadSharp>>): Promise<PdfJpegImage | null> {\n const sharp = sharpFactory ?? (await loadSharp())\n const options = await decodeOptions(sharp, source)\n for (const quality of [86, 72]) {\n const jpeg = await sharp(source, options)\n .rotate()\n .resize(LOGO_PIXELS.width, LOGO_PIXELS.height, { fit: 'inside', withoutEnlargement: true })\n .flatten({ background: LOGO_GROUND })\n .jpeg({ quality })\n .toBuffer()\n if (jpeg.length <= LOGO_MAX_JPEG_BYTES) return readJpeg(new Uint8Array(jpeg))\n }\n return null\n}\n\nexport interface CommercePdfBrandDeps {\n /** The site's logo as a JPEG, or null; never throws. */\n logo(input: { hostId: string; orgId: string; value: string }): Promise<PdfJpegImage | null>\n orgForHost(hostId: string): Promise<{ id?: string; org?: unknown } | null>\n}\n\nexport function defaultCommercePdfBrandDeps(): CommercePdfBrandDeps {\n return {\n logo: async ({ hostId, orgId, value }) => {\n try {\n const located = await locateImageSource(value, hostId, orgId, LOGO_SOURCE_MAX_BYTES)\n const bytes = located ? await readImageSourceBytes(located, LOGO_SOURCE_MAX_BYTES) : null\n return bytes ? await logoJpeg(bytes) : null\n } catch (error) {\n console.warn('[commerce/pdf] the site logo was left out', error)\n return null\n }\n },\n orgForHost: async (hostId) => {\n const found = await getOrgForHost(hostId)\n return found ? { id: found.orgId, org: found.org } : null\n },\n }\n}\n\n/** Who a site's documents are from: its name, contact, colors, logo and time zone. */\nexport async function loadCommercePdfBrand(\n input: { hostId: string; host: HostDocument; owner?: { id?: string; org?: unknown } | null },\n deps: CommercePdfBrandDeps = defaultCommercePdfBrandDeps(),\n): Promise<CommercePdfBrand> {\n const host = input.host\n const owner = input.owner === undefined ? await deps.orgForHost(input.hostId).catch(() => null) : input.owner\n const logoValue = trimmed(resolveHostToken('logo', host as HostTokenSource))\n const logo = logoValue ? await deps.logo({ hostId: input.hostId, orgId: String(owner?.id ?? ''), value: logoValue }) : null\n return {\n businessName: trimmed(resolveHostToken('businessName', host as HostTokenSource)) || trimmed(host['name']),\n contactLines: commercePdfContactLines(host),\n logo,\n colors: commercePdfSiteColors(host),\n timeZone: resolveSiteTimeZone(\n (owner?.org ?? null) as { timeZone?: string } | null,\n host as { timeZone?: string },\n ),\n }\n}\n\n/** `acme-signs-quote-1042.pdf`: the business, the document and its number, in ASCII. */\nexport function commercePdfFilename(businessName: string, document: 'quote' | 'invoice', number: unknown, docId: string): string {\n const slug = (value: string) =>\n value\n .normalize('NFKD')\n .replace(/[̀-ͯ]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 40)\n const numbered = number != null && String(number).trim() ? String(number) : docId.slice(-6)\n return `${[slug(businessName), document, slug(numbered)].filter(Boolean).join('-')}.pdf`\n}\n\n/** A quote's PDF: what its customer's page shows, from the business, with its pay link when given one. */\nexport async function quotePdfFile(\n input: {\n hostId: string\n quoteId: string\n order: CommerceModel.HostOrder\n host: HostDocument\n store: HostDocument | null | undefined\n owner?: { id?: string; org?: unknown } | null\n /** Printed on a quote still to be paid; null leaves it off. */\n payUrl: string | null\n },\n deps?: CommercePdfBrandDeps,\n): Promise<CommercePdfFile> {\n const brand = await loadCommercePdfBrand({ hostId: input.hostId, host: input.host, owner: input.owner }, deps)\n const view = CommerceModel.buildQuotePageView({\n order: input.order,\n orderId: input.quoteId,\n storeName: brand.businessName,\n currency: String(input.store?.['currency'] ?? 'USD'),\n receiptUrl: CommerceModel.isUnpaidQuote(input.order) ? null : orderStatusUrl(input.host, input.hostId, input.quoteId),\n })\n return {\n bytes: renderQuotePdf({ brand, view, revision: Number(input.order.quote?.revision) || 1, payUrl: input.payUrl }),\n filename: commercePdfFilename(brand.businessName, 'quote', input.order.number, input.quoteId),\n }\n}\n\n/** An order's invoice PDF, a paid quote's included. */\nexport async function orderInvoicePdfFile(\n input: {\n hostId: string\n orderId: string\n order: CommerceModel.HostOrder\n host: HostDocument\n store: HostDocument | null | undefined\n owner?: { id?: string; org?: unknown } | null\n },\n deps?: CommercePdfBrandDeps,\n): Promise<CommercePdfFile> {\n const brand = await loadCommercePdfBrand({ hostId: input.hostId, host: input.host, owner: input.owner }, deps)\n return {\n bytes: renderOrderInvoicePdf({\n brand,\n order: input.order,\n orderId: input.orderId,\n store: invoiceStoreFrom(input.host, input.store),\n }),\n filename: commercePdfFilename(brand.businessName, 'invoice', input.order.number, input.orderId),\n }\n}\n\n/*==========================================\n * THE CUSTOMER'S DOWNLOAD: GET commerce/quote-pdf?hostId=&q=&t=\n *\n * The same signed link and the same rules as the quote page's data\n * (`commerce/quote-view`): a wrong token answers exactly as a missing quote,\n * a link to an older version of the quote is refused, and every address is\n * rate-limited per site before the token is checked. The PDF is the page's\n * own view, so it shows nothing the page does not.\n *=========================================*/\n\n/** Downloads one address may make per site, per ten minutes. */\nexport const QUOTE_PDF_DOWNLOADS_PER_WINDOW = 20\n\nexport interface QuotePdfRouteDeps extends QuotePayDeps {\n quotePdf: typeof quotePdfFile\n}\n\nexport async function handleQuotePdf(\n deps: QuotePdfRouteDeps,\n req: PluginApiRequest,\n): Promise<QuoteLinkOutcome | { status: 200; pdf: CommercePdfFile }> {\n if (req.method !== 'GET') return { status: 405, body: { error: 'Method not allowed' } }\n const { hostId, quoteId, token, complete } = quoteLinkParams((req.query ?? {}) as Record<string, unknown>)\n if (!complete) return { status: 400, body: { error: 'This link is incomplete.' } }\n const refused = await limitedQuoteLink(deps, req, `commerce-quote-pdf:${hostId}`, QUOTE_PDF_DOWNLOADS_PER_WINDOW)\n if (refused) return refused\n const linked = await loadLinkedQuote(deps, hostId, quoteId, token)\n if ('outcome' in linked) return linked.outcome\n const { host, order, store } = linked.loaded\n const payUrl = CommerceModel.isUnpaidQuote(order)\n ? quotePayLinkUrl(host, hostId, quoteId, CommerceModel.quoteLinkVersion(order.quote))\n : null\n const owner = await deps.orgForHost(hostId).catch(() => null)\n const pdf = await deps.quotePdf({\n hostId,\n quoteId,\n order,\n host,\n store: (store.data() ?? null) as HostDocument | null,\n owner: owner ? { id: owner.id, org: owner.org } : null,\n payUrl,\n })\n return { status: 200, pdf }\n}\n\n/** `GET /api/commerce/quote-pdf`. */\nexport const quotePdfHandler: PluginApiHandler = async (req, res) => {\n try {\n const outcome = await handleQuotePdf({ ...defaultQuotePayDeps(), quotePdf: quotePdfFile }, req)\n if ('pdf' in outcome) return sendPdf(res, outcome.pdf)\n return sendQuoteLinkOutcome(res, outcome)\n } catch (error) {\n console.error('quote pdf failed', error)\n return sendQuoteLinkOutcome(res, { status: 500, body: { error: 'This quote could not be made into a PDF. Try again.' } })\n }\n}\n"],"names":["resolveSiteTimeZone","resolveHostToken","resolveSiteTheme","buildEmailPalette","emailPaletteBaseForHost","readJpeg","getOrgForHost","loadSharp","CommerceModel","COMMERCE_PDF_NEUTRAL_COLORS","commercePdfColorsFromPalette","renderOrderInvoicePdf","renderQuotePdf","invoiceStoreFrom","orderStatusUrl","sendPdf","quotePayLinkUrl","defaultQuotePayDeps","limitedQuoteLink","loadLinkedQuote","quoteLinkParams","sendQuoteLinkOutcome","locateImageSource","readImageSourceBytes","LOGO_SOURCE_MAX_BYTES","LOGO_PIXELS","width","height","LOGO_MAX_JPEG_BYTES","LOGO_MAX_INPUT_PIXELS","LOGO_GROUND","r","g","b","trimmed","value","trim","commercePdfContactLines","host","source","address","split","map","line","filter","Boolean","email","seo","entity","business","supportEmail","phone","telephone","site","replace","commercePdfSiteColors","theme","base","colors","colorSchemes","light","decodeOptions","sharp","meta","limitInputPixels","failOn","metadata","format","longest","Math","max","density","min","ceil","logoJpeg","sharpFactory","options","quality","jpeg","rotate","resize","fit","withoutEnlargement","flatten","background","toBuffer","length","Uint8Array","defaultCommercePdfBrandDeps","logo","hostId","orgId","located","bytes","error","console","warn","orgForHost","found","id","org","loadCommercePdfBrand","input","deps","owner","undefined","catch","logoValue","String","businessName","contactLines","timeZone","commercePdfFilename","document","number","docId","slug","normalize","toLowerCase","slice","numbered","join","quotePdfFile","brand","view","buildQuotePageView","order","orderId","quoteId","storeName","currency","store","receiptUrl","isUnpaidQuote","revision","Number","quote","payUrl","filename","orderInvoicePdfFile","QUOTE_PDF_DOWNLOADS_PER_WINDOW","handleQuotePdf","req","method","status","body","token","complete","query","refused","linked","outcome","loaded","quoteLinkVersion","pdf","quotePdf","data","quotePdfHandler","res"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/commerce-pdf.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginApiHandler, PluginApiRequest } from '@aglyn/aglyn/server'\nimport { resolveSiteTimeZone } from '@aglyn/aglyn/app-utils/collection-entry-date'\nimport { siteContactEmail, resolveHostToken, type HostTokenSource } from '@aglyn/aglyn/app-utils/host-tokens'\nimport { resolveSiteTheme, type ThemeHostDocument } from '@aglyn/aglyn/app-utils/site-theme'\nimport { buildEmailPalette, emailPaletteBaseForHost } from '@aglyn/shared-util-email'\nimport { readJpeg, type PdfJpegImage } from '@aglyn/shared-util-tools/pdf/pdf-file'\nimport { getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { loadSharp } from '@aglyn/tenant-data-admin/server/media-variants'\nimport * as CommerceModel from '../model'\nimport {\n COMMERCE_PDF_NEUTRAL_COLORS,\n commercePdfColorsFromPalette,\n renderOrderInvoicePdf,\n renderQuotePdf,\n type CommercePdfBrand,\n type CommercePdfColors,\n} from '../utils/commerce-document-pdf'\nimport { invoiceStoreFrom } from '../utils/order-invoice'\nimport { orderStatusUrl } from './order-status-token'\nimport { sendPdf, type CommercePdfFile } from './pdf-response'\nimport { quotePayLinkUrl } from './quote-link-token'\nimport {\n defaultQuotePayDeps,\n limitedQuoteLink,\n loadLinkedQuote,\n quoteLinkParams,\n sendQuoteLinkOutcome,\n type QuoteLinkOutcome,\n type QuotePayDeps,\n} from './quote-pay'\nimport { locateImageSource, readImageSourceBytes } from './site-image-source'\n\n/*==========================================\n * A QUOTE'S AND AN ORDER'S PDF ON THE SERVER (AGL-4095).\n *\n * What the renderers in `utils/commerce-document-pdf.ts` are given comes from\n * here: the site's business name and contact, its theme's colors, its logo\n * (read from storage or its public address, converted to a JPEG on white by\n * sharp, and left out when there is none or it cannot be read), its time\n * zone, and the quote or order itself.\n *\n * Three doors use it, each loading this module only when it is asked for a\n * PDF, so sharp and the PDF writer stay out of every other request:\n *\n * POST commerce/quote { action: 'pdf' } the console's quote (\"Create and edit quotes\")\n * GET commerce/order-print?kind=invoice-pdf the console's and the apps' invoice (\"Manage orders\")\n * GET commerce/quote-pdf?hostId=&q=&t= the customer's quote, by its signed link,\n * or an order's invoice by the same kind (AGL-4099)\n *\n * And the invoice email (AGL-4099, `invoice-email.ts`) attaches the invoice.\n *=========================================*/\n\n/** A logo source larger than this is not read. */\nconst LOGO_SOURCE_MAX_BYTES = 10_000_000\n\n/** The logo's pixels: about four times the box it is drawn in, never enlarged. */\nconst LOGO_PIXELS = { width: 720, height: 200 } as const\n\n/** A converted logo larger than this is left out rather than bloat every copy of the document. */\nconst LOGO_MAX_JPEG_BYTES = 350_000\n\n/** A source whose decoded pixels exceed this is refused rather than decoded. */\nconst LOGO_MAX_INPUT_PIXELS = 50_000_000\n\n/** The ground a logo's transparent pixels are flattened onto: the page's white. */\nconst LOGO_GROUND = { r: 255, g: 255, b: 255 } as const\n\ntype HostDocument = Record<string, unknown>\n\nconst trimmed = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/**\n * The business's contact as the document prints it: its postal address (a\n * block of text, a line per line), the email visitors write to, its phone,\n * and its web address without the scheme.\n */\nexport function commercePdfContactLines(host: HostDocument | null | undefined): string[] {\n const source = (host ?? {}) as HostTokenSource & {\n seo?: { entity?: { email?: unknown; telephone?: unknown } }\n business?: { supportEmail?: unknown }\n }\n const address = trimmed(resolveHostToken('address', source))\n .split(/\\r?\\n/)\n .map((line) => line.trim())\n .filter(Boolean)\n const email = siteContactEmail(source) ?? ''\n const phone = trimmed(source.seo?.entity?.telephone)\n const site = trimmed(resolveHostToken('url', source)).replace(/^https?:\\/\\//, '').replace(/\\/$/, '')\n return [...address, email, phone, site].filter(Boolean)\n}\n\n/** The site theme's colors for a document, or the neutral ones when the theme cannot be read. */\nexport function commercePdfSiteColors(host: HostDocument | null | undefined): CommercePdfColors {\n try {\n const theme = resolveSiteTheme((host ?? null) as ThemeHostDocument | null)\n return commercePdfColorsFromPalette(\n buildEmailPalette({\n base: emailPaletteBaseForHost(host as { cname?: string; subdomain?: string } | null),\n colors: (theme?.colorSchemes?.light ?? null) as Record<string, unknown> | null,\n }),\n )\n } catch {\n return { ...COMMERCE_PDF_NEUTRAL_COLORS }\n }\n}\n\n/** sharp's decoding options; an SVG is rasterized dense enough for the logo's pixels. */\nasync function decodeOptions(sharp: Awaited<ReturnType<typeof loadSharp>>, source: Buffer) {\n const base = { limitInputPixels: LOGO_MAX_INPUT_PIXELS, failOn: 'error' as const }\n const meta = await sharp(source, base).metadata()\n if (meta.format !== 'svg') return base\n const longest = Math.max(meta.width ?? 0, meta.height ?? 0)\n if (!longest) return base\n return { ...base, density: Math.min(2400, Math.max(1, Math.ceil((72 * LOGO_PIXELS.width) / longest))) }\n}\n\n/**\n * A logo's bytes as a JPEG the PDF can carry: turned upright, fitted inside\n * {@link LOGO_PIXELS}, transparency flattened onto white, and small enough to\n * attach. Null when the bytes are not an image sharp reads.\n */\nexport async function logoJpeg(source: Buffer, sharpFactory?: Awaited<ReturnType<typeof loadSharp>>): Promise<PdfJpegImage | null> {\n const sharp = sharpFactory ?? (await loadSharp())\n const options = await decodeOptions(sharp, source)\n for (const quality of [86, 72]) {\n const jpeg = await sharp(source, options)\n .rotate()\n .resize(LOGO_PIXELS.width, LOGO_PIXELS.height, { fit: 'inside', withoutEnlargement: true })\n .flatten({ background: LOGO_GROUND })\n .jpeg({ quality })\n .toBuffer()\n if (jpeg.length <= LOGO_MAX_JPEG_BYTES) return readJpeg(new Uint8Array(jpeg))\n }\n return null\n}\n\nexport interface CommercePdfBrandDeps {\n /** The site's logo as a JPEG, or null; never throws. */\n logo(input: { hostId: string; orgId: string; value: string }): Promise<PdfJpegImage | null>\n orgForHost(hostId: string): Promise<{ id?: string; org?: unknown } | null>\n}\n\nexport function defaultCommercePdfBrandDeps(): CommercePdfBrandDeps {\n return {\n logo: async ({ hostId, orgId, value }) => {\n try {\n const located = await locateImageSource(value, hostId, orgId, LOGO_SOURCE_MAX_BYTES)\n const bytes = located ? await readImageSourceBytes(located, LOGO_SOURCE_MAX_BYTES) : null\n return bytes ? await logoJpeg(bytes) : null\n } catch (error) {\n console.warn('[commerce/pdf] the site logo was left out', error)\n return null\n }\n },\n orgForHost: async (hostId) => {\n const found = await getOrgForHost(hostId)\n return found ? { id: found.orgId, org: found.org } : null\n },\n }\n}\n\n/** Who a site's documents are from: its name, contact, colors, logo and time zone. */\nexport async function loadCommercePdfBrand(\n input: { hostId: string; host: HostDocument; owner?: { id?: string; org?: unknown } | null },\n deps: CommercePdfBrandDeps = defaultCommercePdfBrandDeps(),\n): Promise<CommercePdfBrand> {\n const host = input.host\n const owner = input.owner === undefined ? await deps.orgForHost(input.hostId).catch(() => null) : input.owner\n const logoValue = trimmed(resolveHostToken('logo', host as HostTokenSource))\n const logo = logoValue ? await deps.logo({ hostId: input.hostId, orgId: String(owner?.id ?? ''), value: logoValue }) : null\n return {\n businessName: trimmed(resolveHostToken('businessName', host as HostTokenSource)) || trimmed(host['name']),\n contactLines: commercePdfContactLines(host),\n logo,\n colors: commercePdfSiteColors(host),\n timeZone: resolveSiteTimeZone(\n (owner?.org ?? null) as { timeZone?: string } | null,\n host as { timeZone?: string },\n ),\n }\n}\n\n/** `acme-signs-quote-1042.pdf`: the business, the document and its number, in ASCII. */\nexport function commercePdfFilename(businessName: string, document: 'quote' | 'invoice', number: unknown, docId: string): string {\n const slug = (value: string) =>\n value\n .normalize('NFKD')\n .replace(/[̀-ͯ]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 40)\n const numbered = number != null && String(number).trim() ? String(number) : docId.slice(-6)\n return `${[slug(businessName), document, slug(numbered)].filter(Boolean).join('-')}.pdf`\n}\n\n/** A quote's PDF: what its customer's page shows, from the business, with its pay link when given one. */\nexport async function quotePdfFile(\n input: {\n hostId: string\n quoteId: string\n order: CommerceModel.HostOrder\n host: HostDocument\n store: HostDocument | null | undefined\n owner?: { id?: string; org?: unknown } | null\n /** Printed on a quote still to be paid; null leaves it off. */\n payUrl: string | null\n },\n deps?: CommercePdfBrandDeps,\n): Promise<CommercePdfFile> {\n const brand = await loadCommercePdfBrand({ hostId: input.hostId, host: input.host, owner: input.owner }, deps)\n const view = CommerceModel.buildQuotePageView({\n order: input.order,\n orderId: input.quoteId,\n storeName: brand.businessName,\n currency: String(input.store?.['currency'] ?? 'USD'),\n receiptUrl: CommerceModel.isUnpaidQuote(input.order) ? null : orderStatusUrl(input.host, input.hostId, input.quoteId),\n })\n return {\n bytes: renderQuotePdf({ brand, view, revision: Number(input.order.quote?.revision) || 1, payUrl: input.payUrl }),\n filename: commercePdfFilename(brand.businessName, 'quote', input.order.number, input.quoteId),\n }\n}\n\n/** An order's invoice PDF, a paid quote's included; an unpaid one carries its pay link when given (AGL-4099). */\nexport async function orderInvoicePdfFile(\n input: {\n hostId: string\n orderId: string\n order: CommerceModel.HostOrder\n host: HostDocument\n store: HostDocument | null | undefined\n owner?: { id?: string; org?: unknown } | null\n /** Printed on an invoice still to be paid; absent or null leaves it off. */\n payUrl?: string | null\n },\n deps?: CommercePdfBrandDeps,\n): Promise<CommercePdfFile> {\n const brand = await loadCommercePdfBrand({ hostId: input.hostId, host: input.host, owner: input.owner }, deps)\n return {\n bytes: renderOrderInvoicePdf({\n brand,\n order: input.order,\n orderId: input.orderId,\n store: invoiceStoreFrom(input.host, input.store),\n payUrl: input.payUrl ?? null,\n }),\n filename: commercePdfFilename(brand.businessName, 'invoice', input.order.number, input.orderId),\n }\n}\n\n/*==========================================\n * THE CUSTOMER'S DOWNLOAD: GET commerce/quote-pdf?hostId=&q=&t=\n *\n * The same signed link and the same rules as the quote page's data\n * (`commerce/quote-view`): a wrong token answers exactly as a missing quote,\n * a link to an older version of the quote is refused, and every address is\n * rate-limited per site before the token is checked. The PDF is the page's\n * own view, so it shows nothing the page does not.\n *=========================================*/\n\n/** Downloads one address may make per site, per ten minutes. */\nexport const QUOTE_PDF_DOWNLOADS_PER_WINDOW = 20\n\nexport interface QuotePdfRouteDeps extends QuotePayDeps {\n quotePdf: typeof quotePdfFile\n /** An invoice's PDF, for the link of an order that is not a quote (AGL-4099). */\n invoicePdf?: typeof orderInvoicePdfFile\n}\n\nexport async function handleQuotePdf(\n deps: QuotePdfRouteDeps,\n req: PluginApiRequest,\n): Promise<QuoteLinkOutcome | { status: 200; pdf: CommercePdfFile }> {\n if (req.method !== 'GET') return { status: 405, body: { error: 'Method not allowed' } }\n const { hostId, quoteId, token, complete } = quoteLinkParams((req.query ?? {}) as Record<string, unknown>)\n if (!complete) return { status: 400, body: { error: 'This link is incomplete.' } }\n const refused = await limitedQuoteLink(deps, req, `commerce-quote-pdf:${hostId}`, QUOTE_PDF_DOWNLOADS_PER_WINDOW)\n if (refused) return refused\n const linked = await loadLinkedQuote(deps, hostId, quoteId, token, { invoices: true })\n if ('outcome' in linked) return linked.outcome\n const { host, order, store, kind } = linked.loaded\n const payUrl =\n order.status === 'pending' ? quotePayLinkUrl(host, hostId, quoteId, CommerceModel.orderPayLinkVersion(order)) : null\n const owner = await deps.orgForHost(hostId).catch(() => null)\n if (kind === 'invoice') {\n const pdf = await (deps.invoicePdf ?? orderInvoicePdfFile)({\n hostId,\n orderId: quoteId,\n order,\n host,\n store: (store.data() ?? null) as HostDocument | null,\n owner: owner ? { id: owner.id, org: owner.org } : null,\n payUrl,\n })\n return { status: 200, pdf }\n }\n const pdf = await deps.quotePdf({\n hostId,\n quoteId,\n order,\n host,\n store: (store.data() ?? null) as HostDocument | null,\n owner: owner ? { id: owner.id, org: owner.org } : null,\n payUrl,\n })\n return { status: 200, pdf }\n}\n\n/** `GET /api/commerce/quote-pdf`. */\nexport const quotePdfHandler: PluginApiHandler = async (req, res) => {\n try {\n const outcome = await handleQuotePdf({ ...defaultQuotePayDeps(), quotePdf: quotePdfFile }, req)\n if ('pdf' in outcome) return sendPdf(res, outcome.pdf)\n return sendQuoteLinkOutcome(res, outcome)\n } catch (error) {\n console.error('quote pdf failed', error)\n return sendQuoteLinkOutcome(res, { status: 500, body: { error: 'This quote could not be made into a PDF. Try again.' } })\n }\n}\n"],"names":["resolveSiteTimeZone","siteContactEmail","resolveHostToken","resolveSiteTheme","buildEmailPalette","emailPaletteBaseForHost","readJpeg","getOrgForHost","loadSharp","CommerceModel","COMMERCE_PDF_NEUTRAL_COLORS","commercePdfColorsFromPalette","renderOrderInvoicePdf","renderQuotePdf","invoiceStoreFrom","orderStatusUrl","sendPdf","quotePayLinkUrl","defaultQuotePayDeps","limitedQuoteLink","loadLinkedQuote","quoteLinkParams","sendQuoteLinkOutcome","locateImageSource","readImageSourceBytes","LOGO_SOURCE_MAX_BYTES","LOGO_PIXELS","width","height","LOGO_MAX_JPEG_BYTES","LOGO_MAX_INPUT_PIXELS","LOGO_GROUND","r","g","b","trimmed","value","trim","commercePdfContactLines","host","source","address","split","map","line","filter","Boolean","email","phone","seo","entity","telephone","site","replace","commercePdfSiteColors","theme","base","colors","colorSchemes","light","decodeOptions","sharp","meta","limitInputPixels","failOn","metadata","format","longest","Math","max","density","min","ceil","logoJpeg","sharpFactory","options","quality","jpeg","rotate","resize","fit","withoutEnlargement","flatten","background","toBuffer","length","Uint8Array","defaultCommercePdfBrandDeps","logo","hostId","orgId","located","bytes","error","console","warn","orgForHost","found","id","org","loadCommercePdfBrand","input","deps","owner","undefined","catch","logoValue","String","businessName","contactLines","timeZone","commercePdfFilename","document","number","docId","slug","normalize","toLowerCase","slice","numbered","join","quotePdfFile","brand","view","buildQuotePageView","order","orderId","quoteId","storeName","currency","store","receiptUrl","isUnpaidQuote","revision","Number","quote","payUrl","filename","orderInvoicePdfFile","QUOTE_PDF_DOWNLOADS_PER_WINDOW","handleQuotePdf","req","method","status","body","token","complete","query","refused","linked","invoices","outcome","kind","loaded","orderPayLinkVersion","pdf","invoicePdf","data","quotePdf","quotePdfHandler","res"],"mappings":"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"}
@@ -564,7 +564,7 @@ import { buildRoute, claimAttempt, Route } from "@aglyn/aglyn/server";
564
564
  const orderRef = hostRef.collection('orders').doc();
565
565
  const counterRef = hostRef.collection('counters').doc('orders');
566
566
  await firestore.runTransaction(async (transaction)=>{
567
- var _counter_get, _decoded_email;
567
+ var _counter_get, _shippingPlan_countries, _decoded_email;
568
568
  const counter = await transaction.get(counterRef);
569
569
  const number = Number((_counter_get = counter.get('next')) != null ? _counter_get : 1);
570
570
  transaction.set(counterRef, {
@@ -573,7 +573,7 @@ import { buildRoute, claimAttempt, Route } from "@aglyn/aglyn/server";
573
573
  merge: true
574
574
  });
575
575
  // With the fields the orders list queries by (AGL-3321).
576
- transaction.set(orderRef, CommerceModel.withOrderListFields(orderRef.id, {
576
+ transaction.set(orderRef, CommerceModel.withOrderListFields(orderRef.id, _extends({
577
577
  number,
578
578
  status: 'pending',
579
579
  channel: 'draft',
@@ -591,6 +591,26 @@ import { buildRoute, claimAttempt, Route } from "@aglyn/aglyn/server";
591
591
  // manual draft the two agree by construction.
592
592
  taxMode: CommerceModel.storefrontTaxModeForDecision(taxDecision, taxCents),
593
593
  customerEmail: email || null,
594
+ // WHAT ITS INVOICE'S LASTING PAY LINK CHARGES LATER (AGL-4099). The
595
+ // session below lapses in a day; an invoice for this draft opens a
596
+ // fresh one from the stored order whenever the buyer presses Pay, and
597
+ // offers the same shipping rates this link does, and counts the same
598
+ // promotion once paid. Stored even when there are no rates, so an
599
+ // invoice can tell "ships nothing" from a draft made before this.
600
+ draftShipping: {
601
+ countries: shippingOptions.length > 0 ? Array.from((_shippingPlan_countries = shippingPlan.countries) != null ? _shippingPlan_countries : []) : [],
602
+ options: shippingOptions.map((option)=>{
603
+ var _option_rateId, _option_name, _option_amountCents;
604
+ return {
605
+ rateId: String((_option_rateId = option.rateId) != null ? _option_rateId : ''),
606
+ name: String((_option_name = option.name) != null ? _option_name : ''),
607
+ amountCents: Math.max(0, Math.round(Number((_option_amountCents = option.amountCents) != null ? _option_amountCents : 0)))
608
+ };
609
+ })
610
+ }
611
+ }, appliedDiscountId ? {
612
+ discountId: appliedDiscountId
613
+ } : {}, {
594
614
  timeline: [
595
615
  {
596
616
  atMs: Date.now(),
@@ -600,7 +620,7 @@ import { buildRoute, claimAttempt, Route } from "@aglyn/aglyn/server";
600
620
  ],
601
621
  createdAtMs: Date.now(),
602
622
  createdAt: firebaseAdmin.firestore.FieldValue.serverTimestamp()
603
- }));
623
+ })));
604
624
  });
605
625
  // The tax rate is minted here, BELOW the claim, because it creates a real
606
626
  // Stripe object (AGL-1953). Cached per host, so it is usually a read.