@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.
- package/README.md +4 -2
- package/package.json +13 -13
- package/src/lib/components/commerce-console-page.js +2 -1
- package/src/lib/components/commerce-console-page.js.map +1 -1
- package/src/lib/components/console/host-orders-card.component.d.ts +6 -0
- package/src/lib/components/console/host-orders-card.component.js +3 -2
- package/src/lib/components/console/host-orders-card.component.js.map +1 -1
- package/src/lib/components/console/invoice-send.component.d.ts +44 -0
- package/src/lib/components/console/invoice-send.component.js +237 -0
- package/src/lib/components/console/invoice-send.component.js.map +1 -0
- package/src/lib/components/console/order-detail-dialog.component.d.ts +5 -0
- package/src/lib/components/console/order-detail-dialog.component.js +9 -2
- package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
- package/src/lib/components/console/order-invoice.component.d.ts +9 -0
- package/src/lib/components/console/order-invoice.component.js +27 -2
- package/src/lib/components/console/order-invoice.component.js.map +1 -1
- package/src/lib/components/console/quote-editor-dialog.component.d.ts +29 -0
- package/src/lib/components/console/quote-editor-dialog.component.js +102 -4
- package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/quote-proofs.component.d.ts +11 -0
- package/src/lib/components/console/quote-proofs.component.js +68 -0
- package/src/lib/components/console/quote-proofs.component.js.map +1 -1
- package/src/lib/components/console/quote-send.component.d.ts +42 -0
- package/src/lib/components/console/quote-send.component.js +307 -0
- package/src/lib/components/console/quote-send.component.js.map +1 -0
- package/src/lib/components/console/quote-start.component.d.ts +45 -0
- package/src/lib/components/console/quote-start.component.js +182 -0
- package/src/lib/components/console/quote-start.component.js.map +1 -0
- package/src/lib/components/product-options-field-body.d.ts +44 -0
- package/src/lib/components/product-options-field-body.js +260 -0
- package/src/lib/components/product-options-field-body.js.map +1 -0
- package/src/lib/components/product-options-field.d.ts +32 -0
- package/src/lib/components/product-options-field.js +128 -0
- package/src/lib/components/product-options-field.js.map +1 -0
- package/src/lib/components/quote-page-body.js +39 -20
- package/src/lib/components/quote-page-body.js.map +1 -1
- package/src/lib/model/commerce-invoices.d.ts +132 -0
- package/src/lib/model/commerce-invoices.js +332 -0
- package/src/lib/model/commerce-invoices.js.map +1 -0
- package/src/lib/model/commerce-orders.d.ts +29 -2
- package/src/lib/model/commerce-orders.js.map +1 -1
- package/src/lib/model/commerce-product-field.d.ts +23 -0
- package/src/lib/model/commerce-product-field.js +26 -0
- package/src/lib/model/commerce-product-field.js.map +1 -0
- package/src/lib/model/commerce-quote-email.d.ts +67 -0
- package/src/lib/model/commerce-quote-email.js +154 -0
- package/src/lib/model/commerce-quote-email.js.map +1 -0
- package/src/lib/model/commerce-quote-pay.d.ts +49 -6
- package/src/lib/model/commerce-quote-pay.js +69 -36
- package/src/lib/model/commerce-quote-pay.js.map +1 -1
- package/src/lib/model/commerce-quote-request.d.ts +93 -0
- package/src/lib/model/commerce-quote-request.js +207 -0
- package/src/lib/model/commerce-quote-request.js.map +1 -0
- package/src/lib/model/commerce-quotes.d.ts +81 -0
- package/src/lib/model/commerce-quotes.js.map +1 -1
- package/src/lib/model/index.d.ts +3 -0
- package/src/lib/model/index.js +3 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/plugin.js +19 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/billing-webhook.js +115 -70
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/commerce-pdf.d.ts +5 -1
- package/src/lib/server/commerce-pdf.js +37 -10
- package/src/lib/server/commerce-pdf.js.map +1 -1
- package/src/lib/server/draft-order.js +23 -3
- package/src/lib/server/draft-order.js.map +1 -1
- package/src/lib/server/invoice-email.d.ts +76 -0
- package/src/lib/server/invoice-email.js +183 -0
- package/src/lib/server/invoice-email.js.map +1 -0
- package/src/lib/server/order-invoice-send.d.ts +43 -0
- package/src/lib/server/order-invoice-send.js +218 -0
- package/src/lib/server/order-invoice-send.js.map +1 -0
- package/src/lib/server/order-status.d.ts +11 -0
- package/src/lib/server/order-status.js +44 -8
- package/src/lib/server/order-status.js.map +1 -1
- package/src/lib/server/person-eraser.d.ts +3 -2
- package/src/lib/server/person-eraser.js +25 -6
- package/src/lib/server/person-eraser.js.map +1 -1
- package/src/lib/server/quote-email.d.ts +85 -0
- package/src/lib/server/quote-email.js +211 -0
- package/src/lib/server/quote-email.js.map +1 -0
- package/src/lib/server/quote-link-token.d.ts +11 -0
- package/src/lib/server/quote-link-token.js +21 -0
- package/src/lib/server/quote-link-token.js.map +1 -1
- package/src/lib/server/quote-pay.d.ts +8 -3
- package/src/lib/server/quote-pay.js +105 -43
- package/src/lib/server/quote-pay.js.map +1 -1
- package/src/lib/server/quote.d.ts +11 -0
- package/src/lib/server/quote.js +309 -12
- package/src/lib/server/quote.js.map +1 -1
- package/src/lib/server/site-page-resolver.js +2 -1
- package/src/lib/server/site-page-resolver.js.map +1 -1
- package/src/lib/server.js +5 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/site.js +8 -0
- package/src/lib/site.js.map +1 -1
- package/src/lib/tenant-emails.js +243 -0
- package/src/lib/tenant-emails.js.map +1 -1
- package/src/lib/utils/commerce-document-pdf.d.ts +5 -0
- package/src/lib/utils/commerce-document-pdf.js +6 -1
- package/src/lib/utils/commerce-document-pdf.js.map +1 -1
- package/src/lib/utils/invoice-email-content.d.ts +45 -0
- package/src/lib/utils/invoice-email-content.js +126 -0
- package/src/lib/utils/invoice-email-content.js.map +1 -0
- package/src/lib/utils/order-invoice.d.ts +1 -1
- package/src/lib/utils/order-invoice.js +14 -6
- package/src/lib/utils/order-invoice.js.map +1 -1
- package/src/lib/utils/quote-email-content.d.ts +62 -0
- package/src/lib/utils/quote-email-content.js +154 -0
- package/src/lib/utils/quote-email-content.js.map +1 -0
|
@@ -0,0 +1,45 @@
|
|
|
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 invoice email is sent and designed under. */
|
|
19
|
+
export declare const INVOICE_EMAIL_KEY = "order-invoice";
|
|
20
|
+
export interface InvoiceEmailContentInput {
|
|
21
|
+
order: HostOrder;
|
|
22
|
+
orderId: string;
|
|
23
|
+
businessName: string;
|
|
24
|
+
currency: string;
|
|
25
|
+
/** The subject the email goes under. */
|
|
26
|
+
subject: string;
|
|
27
|
+
/** The business's message, as typed; may be empty. */
|
|
28
|
+
message: string;
|
|
29
|
+
/** The order's pay link, for an invoice still to be paid; null for a paid one. */
|
|
30
|
+
payUrl: string | null;
|
|
31
|
+
/** The site's time zone, for the dates. */
|
|
32
|
+
timeZone?: string | null;
|
|
33
|
+
}
|
|
34
|
+
export interface InvoiceEmailContent {
|
|
35
|
+
/** The catalog email's merge tokens. */
|
|
36
|
+
tokens: Record<string, string>;
|
|
37
|
+
/** The same email as plain text, for a send whose design cannot be rendered. */
|
|
38
|
+
text: string;
|
|
39
|
+
/** The invoice says it is paid: no button. */
|
|
40
|
+
paid: boolean;
|
|
41
|
+
/** The total it shows. */
|
|
42
|
+
totalCents: number;
|
|
43
|
+
}
|
|
44
|
+
/** The invoice email's tokens and its plain-text twin. */
|
|
45
|
+
export declare function invoiceEmailContent(input: InvoiceEmailContentInput): InvoiceEmailContent;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { formatOrderMoney, formatOrderNumber, invoiceCheckoutCharge, invoiceIsPaid, orderCreatedAtMs, orderPaidAtMs } from "../model/index.js";
|
|
18
|
+
/*==========================================
|
|
19
|
+
* WHAT THE INVOICE EMAIL SAYS (AGL-4099).
|
|
20
|
+
*
|
|
21
|
+
* The `order-invoice` catalog email's tokens, composed per send, curated for
|
|
22
|
+
* the two things an invoice can be:
|
|
23
|
+
*
|
|
24
|
+
* - UNPAID: "Invoice #1042 from Acme Signs", what it is for, the business's
|
|
25
|
+
* own message, the number, date, total and what is due, how to pay, and
|
|
26
|
+
* one button, "Pay $1,250.00", to the order's lasting pay link on the
|
|
27
|
+
* business's site.
|
|
28
|
+
* - PAID: the same heading and details with the date it was paid, "This
|
|
29
|
+
* invoice is paid; keep it for your records", and no button.
|
|
30
|
+
*
|
|
31
|
+
* The invoice itself is attached as a PDF by the sender. The figures are the
|
|
32
|
+
* ones the pay link charges (`invoiceCheckoutCharge`) while it is unpaid, and
|
|
33
|
+
* the order's own once paid, so the email, the page and the PDF agree.
|
|
34
|
+
*
|
|
35
|
+
* Pure: the send route and the sample renders call the same function.
|
|
36
|
+
*=========================================*/ /** The catalog key the invoice email is sent and designed under. */ export const INVOICE_EMAIL_KEY = 'order-invoice';
|
|
37
|
+
/** A date the American way, in the site's time zone: `October 28, 2026`. */ function longDate(ms, timeZone) {
|
|
38
|
+
if (!ms || !Number.isFinite(ms)) return '';
|
|
39
|
+
const options = {
|
|
40
|
+
year: 'numeric',
|
|
41
|
+
month: 'long',
|
|
42
|
+
day: 'numeric'
|
|
43
|
+
};
|
|
44
|
+
try {
|
|
45
|
+
return new Intl.DateTimeFormat('en-US', _extends({}, options, timeZone ? {
|
|
46
|
+
timeZone
|
|
47
|
+
} : {})).format(new Date(ms));
|
|
48
|
+
} catch (unused) {
|
|
49
|
+
return new Intl.DateTimeFormat('en-US', _extends({}, options, {
|
|
50
|
+
timeZone: 'UTC'
|
|
51
|
+
})).format(new Date(ms));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function capitalize(value) {
|
|
55
|
+
return value ? value[0].toUpperCase() + value.slice(1) : value;
|
|
56
|
+
}
|
|
57
|
+
/** `before tax`, `before shipping`, `before shipping and tax`, or nothing. */ function laterParts(shipping, tax) {
|
|
58
|
+
if (shipping && tax) return 'shipping and tax';
|
|
59
|
+
if (shipping) return 'shipping';
|
|
60
|
+
if (tax) return 'tax';
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
/** The invoice email's tokens and its plain-text twin. */ export function invoiceEmailContent(input) {
|
|
64
|
+
var _input_payUrl, _ref, _ref1, _order_refundedCents;
|
|
65
|
+
var _order_totals;
|
|
66
|
+
const { order, orderId, timeZone } = input;
|
|
67
|
+
const name = input.businessName.trim();
|
|
68
|
+
const business = name || 'the business';
|
|
69
|
+
const number = formatOrderNumber(order, orderId);
|
|
70
|
+
const paid = invoiceIsPaid(order);
|
|
71
|
+
const payUrl = paid ? '' : String((_input_payUrl = input.payUrl) != null ? _input_payUrl : '').trim();
|
|
72
|
+
const forWhat = order.quote ? 'your quote' : 'your order';
|
|
73
|
+
const quoteSection = order.quote;
|
|
74
|
+
const dateMs = orderCreatedAtMs(order) || Number((_ref = quoteSection == null ? void 0 : quoteSection.updatedAtMs) != null ? _ref : quoteSection == null ? void 0 : quoteSection.createdAtMs) || 0;
|
|
75
|
+
const date = longDate(dateMs, timeZone);
|
|
76
|
+
// What it shows: what the link charges while it is unpaid, the order's own figures once paid.
|
|
77
|
+
const charge = paid ? null : invoiceCheckoutCharge(order, orderId);
|
|
78
|
+
const totalCents = charge ? charge.totalCents : Math.max(0, Math.round(Number((_ref1 = (_order_totals = order.totals) == null ? void 0 : _order_totals.totalCents) != null ? _ref1 : 0)));
|
|
79
|
+
const money = formatOrderMoney(totalCents, input.currency);
|
|
80
|
+
const later = charge ? laterParts(charge.shippingOptions.length > 0, charge.taxAtPayment) : '';
|
|
81
|
+
const total = later ? `${money} before ${later}` : money;
|
|
82
|
+
const refundedCents = Math.max(0, Math.round(Number((_order_refundedCents = order.refundedCents) != null ? _order_refundedCents : 0)));
|
|
83
|
+
const paidOn = longDate(orderPaidAtMs(order), timeZone);
|
|
84
|
+
const status = paid ? `${paidOn ? `Paid on ${paidOn}` : 'Paid'}` + (refundedCents > 0 ? `, ${formatOrderMoney(refundedCents, input.currency)} refunded` : '') : `Unpaid, ${money}${later ? ` plus ${later}` : ''} due`;
|
|
85
|
+
const heading = name ? `Invoice ${number} from ${name}` : `Invoice ${number}`;
|
|
86
|
+
const intro = paid ? `Here is the invoice for ${forWhat} with ${business}` + (paidOn ? `, paid on ${paidOn}` : ', which is paid') + '. It is attached as a PDF for your records.' : `${capitalize(business)} sent you an invoice for ${forWhat}. The details are below, and the invoice is attached as a PDF.`;
|
|
87
|
+
const nextStep = paid ? 'This invoice is paid; keep it for your records.' : payUrl ? 'Use the button below to pay it securely by card.' + (later ? ` ${capitalize(later)} ${later === 'shipping and tax' ? 'are' : 'is'} added when you pay.` : '') + ' The button keeps working until the invoice is paid.' : `Reply to this email to arrange payment with ${business}.`;
|
|
88
|
+
const buttonLabel = payUrl ? `Pay ${money}${later ? ` plus ${later}` : ''}` : '';
|
|
89
|
+
const message = input.message.trim();
|
|
90
|
+
const details = [
|
|
91
|
+
`Invoice: ${number}`,
|
|
92
|
+
date ? `Date: ${date}` : '',
|
|
93
|
+
`Total: ${total}`,
|
|
94
|
+
`Status: ${status}`
|
|
95
|
+
].filter(Boolean).join('\n');
|
|
96
|
+
const tokens = {
|
|
97
|
+
'invoice.subject': input.subject,
|
|
98
|
+
'invoice.number': number,
|
|
99
|
+
'invoice.date': date,
|
|
100
|
+
'invoice.total': total,
|
|
101
|
+
'invoice.status': status,
|
|
102
|
+
'invoice.heading': heading,
|
|
103
|
+
'invoice.intro': intro,
|
|
104
|
+
'invoice.message': message,
|
|
105
|
+
'invoice.nextStep': nextStep,
|
|
106
|
+
'invoice.buttonLabel': buttonLabel,
|
|
107
|
+
'invoice.url': payUrl
|
|
108
|
+
};
|
|
109
|
+
const text = [
|
|
110
|
+
heading,
|
|
111
|
+
intro,
|
|
112
|
+
message,
|
|
113
|
+
details,
|
|
114
|
+
nextStep,
|
|
115
|
+
buttonLabel ? `${buttonLabel}: ${payUrl}` : '',
|
|
116
|
+
`Reply to this email to reach ${business}.`
|
|
117
|
+
].filter(Boolean).join('\n\n');
|
|
118
|
+
return {
|
|
119
|
+
tokens,
|
|
120
|
+
text,
|
|
121
|
+
paid,
|
|
122
|
+
totalCents
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
//# sourceMappingURL=invoice-email-content.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/utils/invoice-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 formatOrderMoney,\n formatOrderNumber,\n invoiceCheckoutCharge,\n invoiceIsPaid,\n orderCreatedAtMs,\n orderPaidAtMs,\n type HostOrder,\n} from '../model'\n\n/*==========================================\n * WHAT THE INVOICE EMAIL SAYS (AGL-4099).\n *\n * The `order-invoice` catalog email's tokens, composed per send, curated for\n * the two things an invoice can be:\n *\n * - UNPAID: \"Invoice #1042 from Acme Signs\", what it is for, the business's\n * own message, the number, date, total and what is due, how to pay, and\n * one button, \"Pay $1,250.00\", to the order's lasting pay link on the\n * business's site.\n * - PAID: the same heading and details with the date it was paid, \"This\n * invoice is paid; keep it for your records\", and no button.\n *\n * The invoice itself is attached as a PDF by the sender. The figures are the\n * ones the pay link charges (`invoiceCheckoutCharge`) while it is unpaid, and\n * the order's own once paid, so the email, the page and the PDF agree.\n *\n * Pure: the send route and the sample renders call the same function.\n *=========================================*/\n\n/** The catalog key the invoice email is sent and designed under. */\nexport const INVOICE_EMAIL_KEY = 'order-invoice'\n\nexport interface InvoiceEmailContentInput {\n order: HostOrder\n orderId: 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 order's pay link, for an invoice still to be paid; null for a paid one. */\n payUrl: string | null\n /** The site's time zone, for the dates. */\n timeZone?: string | null\n}\n\nexport interface InvoiceEmailContent {\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 invoice says it is paid: no button. */\n paid: boolean\n /** The total it shows. */\n totalCents: number\n}\n\n/** A date the American way, in the site's time zone: `October 28, 2026`. */\nfunction longDate(ms: number | null | undefined, timeZone?: string | null): string {\n if (!ms || !Number.isFinite(ms)) return ''\n const options: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'long', day: 'numeric' }\n try {\n return new Intl.DateTimeFormat('en-US', { ...options, ...(timeZone ? { timeZone } : {}) }).format(new Date(ms))\n } catch {\n return new Intl.DateTimeFormat('en-US', { ...options, timeZone: 'UTC' }).format(new Date(ms))\n }\n}\n\nfunction capitalize(value: string): string {\n return value ? value[0].toUpperCase() + value.slice(1) : value\n}\n\n/** `before tax`, `before shipping`, `before shipping and tax`, or nothing. */\nfunction laterParts(shipping: boolean, tax: boolean): string {\n if (shipping && tax) return 'shipping and tax'\n if (shipping) return 'shipping'\n if (tax) return 'tax'\n return ''\n}\n\n/** The invoice email's tokens and its plain-text twin. */\nexport function invoiceEmailContent(input: InvoiceEmailContentInput): InvoiceEmailContent {\n const { order, orderId, timeZone } = input\n const name = input.businessName.trim()\n const business = name || 'the business'\n const number = formatOrderNumber(order, orderId)\n const paid = invoiceIsPaid(order)\n const payUrl = paid ? '' : String(input.payUrl ?? '').trim()\n const forWhat = order.quote ? 'your quote' : 'your order'\n const quoteSection = order.quote\n const dateMs =\n orderCreatedAtMs(order as never) || Number(quoteSection?.updatedAtMs ?? quoteSection?.createdAtMs) || 0\n const date = longDate(dateMs, timeZone)\n\n // What it shows: what the link charges while it is unpaid, the order's own figures once paid.\n const charge = paid ? null : invoiceCheckoutCharge(order, orderId)\n const totalCents = charge ? charge.totalCents : Math.max(0, Math.round(Number(order.totals?.totalCents ?? 0)))\n const money = formatOrderMoney(totalCents, input.currency)\n const later = charge ? laterParts(charge.shippingOptions.length > 0, charge.taxAtPayment) : ''\n const total = later ? `${money} before ${later}` : money\n const refundedCents = Math.max(0, Math.round(Number(order.refundedCents ?? 0)))\n const paidOn = longDate(orderPaidAtMs(order), timeZone)\n const status = paid\n ? `${paidOn ? `Paid on ${paidOn}` : 'Paid'}` +\n (refundedCents > 0 ? `, ${formatOrderMoney(refundedCents, input.currency)} refunded` : '')\n : `Unpaid, ${money}${later ? ` plus ${later}` : ''} due`\n\n const heading = name ? `Invoice ${number} from ${name}` : `Invoice ${number}`\n const intro = paid\n ? `Here is the invoice for ${forWhat} with ${business}` +\n (paidOn ? `, paid on ${paidOn}` : ', which is paid') +\n '. It is attached as a PDF for your records.'\n : `${capitalize(business)} sent you an invoice for ${forWhat}. The details are below, and the invoice is attached as a PDF.`\n const nextStep = paid\n ? 'This invoice is paid; keep it for your records.'\n : payUrl\n ? 'Use the button below to pay it securely by card.' +\n (later ? ` ${capitalize(later)} ${later === 'shipping and tax' ? 'are' : 'is'} added when you pay.` : '') +\n ' The button keeps working until the invoice is paid.'\n : `Reply to this email to arrange payment with ${business}.`\n const buttonLabel = payUrl ? `Pay ${money}${later ? ` plus ${later}` : ''}` : ''\n const message = input.message.trim()\n const details = [`Invoice: ${number}`, date ? `Date: ${date}` : '', `Total: ${total}`, `Status: ${status}`]\n .filter(Boolean)\n .join('\\n')\n\n const tokens: Record<string, string> = {\n 'invoice.subject': input.subject,\n 'invoice.number': number,\n 'invoice.date': date,\n 'invoice.total': total,\n 'invoice.status': status,\n 'invoice.heading': heading,\n 'invoice.intro': intro,\n 'invoice.message': message,\n 'invoice.nextStep': nextStep,\n 'invoice.buttonLabel': buttonLabel,\n 'invoice.url': payUrl,\n }\n\n const text = [\n heading,\n intro,\n message,\n details,\n nextStep,\n buttonLabel ? `${buttonLabel}: ${payUrl}` : '',\n `Reply to this email to reach ${business}.`,\n ]\n .filter(Boolean)\n .join('\\n\\n')\n\n return { tokens, text, paid, totalCents }\n}\n"],"names":["formatOrderMoney","formatOrderNumber","invoiceCheckoutCharge","invoiceIsPaid","orderCreatedAtMs","orderPaidAtMs","INVOICE_EMAIL_KEY","longDate","ms","timeZone","Number","isFinite","options","year","month","day","Intl","DateTimeFormat","format","Date","capitalize","value","toUpperCase","slice","laterParts","shipping","tax","invoiceEmailContent","input","order","orderId","name","businessName","trim","business","number","paid","payUrl","String","forWhat","quote","quoteSection","dateMs","updatedAtMs","createdAtMs","date","charge","totalCents","Math","max","round","totals","money","currency","later","shippingOptions","length","taxAtPayment","total","refundedCents","paidOn","status","heading","intro","nextStep","buttonLabel","message","details","filter","Boolean","join","tokens","subject","text"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,gBAAgB,EAChBC,iBAAiB,EACjBC,qBAAqB,EACrBC,aAAa,EACbC,gBAAgB,EAChBC,aAAa,QAER,oBAAU;AAEjB;;;;;;;;;;;;;;;;;;2CAkB2C,GAE3C,kEAAkE,GAClE,OAAO,MAAMC,oBAAoB,gBAAe;AA4BhD,0EAA0E,GAC1E,SAASC,SAASC,EAA6B,EAAEC,QAAwB;IACvE,IAAI,CAACD,MAAM,CAACE,OAAOC,QAAQ,CAACH,KAAK,OAAO;IACxC,MAAMI,UAAsC;QAAEC,MAAM;QAAWC,OAAO;QAAQC,KAAK;IAAU;IAC7F,IAAI;QACF,OAAO,IAAIC,KAAKC,cAAc,CAAC,SAAS,aAAKL,SAAaH,WAAW;YAAEA;QAAS,IAAI,CAAC,IAAMS,MAAM,CAAC,IAAIC,KAAKX;IAC7G,EAAE,eAAM;QACN,OAAO,IAAIQ,KAAKC,cAAc,CAAC,SAAS,aAAKL;YAASH,UAAU;YAASS,MAAM,CAAC,IAAIC,KAAKX;IAC3F;AACF;AAEA,SAASY,WAAWC,KAAa;IAC/B,OAAOA,QAAQA,KAAK,CAAC,EAAE,CAACC,WAAW,KAAKD,MAAME,KAAK,CAAC,KAAKF;AAC3D;AAEA,4EAA4E,GAC5E,SAASG,WAAWC,QAAiB,EAAEC,GAAY;IACjD,IAAID,YAAYC,KAAK,OAAO;IAC5B,IAAID,UAAU,OAAO;IACrB,IAAIC,KAAK,OAAO;IAChB,OAAO;AACT;AAEA,wDAAwD,GACxD,OAAO,SAASC,oBAAoBC,KAA+B;QAM/BA,4BAakBC;QAJ0BA;IAd9E,MAAM,EAAEA,KAAK,EAAEC,OAAO,EAAErB,QAAQ,EAAE,GAAGmB;IACrC,MAAMG,OAAOH,MAAMI,YAAY,CAACC,IAAI;IACpC,MAAMC,WAAWH,QAAQ;IACzB,MAAMI,SAASlC,kBAAkB4B,OAAOC;IACxC,MAAMM,OAAOjC,cAAc0B;IAC3B,MAAMQ,SAASD,OAAO,KAAKE,QAAOV,gBAAAA,MAAMS,MAAM,YAAZT,gBAAgB,IAAIK,IAAI;IAC1D,MAAMM,UAAUV,MAAMW,KAAK,GAAG,eAAe;IAC7C,MAAMC,eAAeZ,MAAMW,KAAK;IAChC,MAAME,SACJtC,iBAAiByB,UAAmBnB,eAAO+B,gCAAAA,aAAcE,WAAW,mBAAIF,gCAAAA,aAAcG,WAAW,KAAK;IACxG,MAAMC,OAAOtC,SAASmC,QAAQjC;IAE9B,8FAA8F;IAC9F,MAAMqC,SAASV,OAAO,OAAOlC,sBAAsB2B,OAAOC;IAC1D,MAAMiB,aAAaD,SAASA,OAAOC,UAAU,GAAGC,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,CAACxC,iBAAOmB,gBAAAA,MAAMsB,MAAM,qBAAZtB,cAAckB,UAAU,oBAAI;IAC1G,MAAMK,QAAQpD,iBAAiB+C,YAAYnB,MAAMyB,QAAQ;IACzD,MAAMC,QAAQR,SAAStB,WAAWsB,OAAOS,eAAe,CAACC,MAAM,GAAG,GAAGV,OAAOW,YAAY,IAAI;IAC5F,MAAMC,QAAQJ,QAAQ,GAAGF,MAAM,QAAQ,EAAEE,OAAO,GAAGF;IACnD,MAAMO,gBAAgBX,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,CAACxC,QAAOmB,uBAAAA,MAAM8B,aAAa,YAAnB9B,uBAAuB;IAC3E,MAAM+B,SAASrD,SAASF,cAAcwB,QAAQpB;IAC9C,MAAMoD,SAASzB,OACX,GAAGwB,SAAS,CAAC,QAAQ,EAAEA,QAAQ,GAAG,QAAQ,GACzCD,CAAAA,gBAAgB,IAAI,CAAC,EAAE,EAAE3D,iBAAiB2D,eAAe/B,MAAMyB,QAAQ,EAAE,SAAS,CAAC,GAAG,EAAC,IACxF,CAAC,QAAQ,EAAED,QAAQE,QAAQ,CAAC,MAAM,EAAEA,OAAO,GAAG,GAAG,IAAI,CAAC;IAE1D,MAAMQ,UAAU/B,OAAO,CAAC,QAAQ,EAAEI,OAAO,MAAM,EAAEJ,MAAM,GAAG,CAAC,QAAQ,EAAEI,QAAQ;IAC7E,MAAM4B,QAAQ3B,OACV,CAAC,wBAAwB,EAAEG,QAAQ,MAAM,EAAEL,UAAU,GACpD0B,CAAAA,SAAS,CAAC,UAAU,EAAEA,QAAQ,GAAG,iBAAgB,IAClD,gDACA,GAAGxC,WAAWc,UAAU,yBAAyB,EAAEK,QAAQ,8DAA8D,CAAC;IAC9H,MAAMyB,WAAW5B,OACb,oDACAC,SACE,qDACCiB,CAAAA,QAAQ,CAAC,CAAC,EAAElC,WAAWkC,OAAO,CAAC,EAAEA,UAAU,qBAAqB,QAAQ,KAAK,oBAAoB,CAAC,GAAG,EAAC,IACvG,yDACA,CAAC,4CAA4C,EAAEpB,SAAS,CAAC,CAAC;IAChE,MAAM+B,cAAc5B,SAAS,CAAC,IAAI,EAAEe,QAAQE,QAAQ,CAAC,MAAM,EAAEA,OAAO,GAAG,IAAI,GAAG;IAC9E,MAAMY,UAAUtC,MAAMsC,OAAO,CAACjC,IAAI;IAClC,MAAMkC,UAAU;QAAC,CAAC,SAAS,EAAEhC,QAAQ;QAAEU,OAAO,CAAC,MAAM,EAAEA,MAAM,GAAG;QAAI,CAAC,OAAO,EAAEa,OAAO;QAAE,CAAC,QAAQ,EAAEG,QAAQ;KAAC,CACxGO,MAAM,CAACC,SACPC,IAAI,CAAC;IAER,MAAMC,SAAiC;QACrC,mBAAmB3C,MAAM4C,OAAO;QAChC,kBAAkBrC;QAClB,gBAAgBU;QAChB,iBAAiBa;QACjB,kBAAkBG;QAClB,mBAAmBC;QACnB,iBAAiBC;QACjB,mBAAmBG;QACnB,oBAAoBF;QACpB,uBAAuBC;QACvB,eAAe5B;IACjB;IAEA,MAAMoC,OAAO;QACXX;QACAC;QACAG;QACAC;QACAH;QACAC,cAAc,GAAGA,YAAY,EAAE,EAAE5B,QAAQ,GAAG;QAC5C,CAAC,6BAA6B,EAAEH,SAAS,CAAC,CAAC;KAC5C,CACEkC,MAAM,CAACC,SACPC,IAAI,CAAC;IAER,OAAO;QAAEC;QAAQE;QAAMrC;QAAMW;IAAW;AAC1C"}
|
|
@@ -42,7 +42,7 @@ export interface OrderInvoiceStore {
|
|
|
42
42
|
* the order dialog's Invoice button and `commerce/order-print` read them.
|
|
43
43
|
*/
|
|
44
44
|
export declare function invoiceStoreFrom(host: Record<string, unknown> | undefined | null, store: Record<string, unknown> | undefined | null): OrderInvoiceStore;
|
|
45
|
-
export type InvoiceOrder = Pick<HostOrder, 'number' | 'status' | 'lineItems' | 'totals' | 'refundedCents' | 'customerName' | 'customerEmail' | 'shippingAddress' | 'billingAddress'> & {
|
|
45
|
+
export type InvoiceOrder = Pick<HostOrder, 'number' | 'status' | 'lineItems' | 'totals' | 'refundedCents' | 'customerName' | 'customerEmail' | 'shippingAddress' | 'billingAddress'> & Partial<Pick<HostOrder, 'quote'>> & {
|
|
46
46
|
createdAtMs?: number;
|
|
47
47
|
currency?: string;
|
|
48
48
|
amountCents?: number;
|
|
@@ -78,7 +78,8 @@ const addressLines = (address)=>invoiceAddressLines(address);
|
|
|
78
78
|
return 'Paid';
|
|
79
79
|
}
|
|
80
80
|
/** What an invoice says, from the order and its store. */ export function buildOrderInvoiceData(input) {
|
|
81
|
-
var _ref, _order_lineItems, _ref1, _ref2, _order_refundedCents, _order_lineItems1, _ref3, _ref4, _ref5, _ref6, _order_customerName, _order_customerEmail, _store_footer, _store_termsUrl;
|
|
81
|
+
var _ref, _order_lineItems, _ref1, _ref2, _order_refundedCents, _order_lineItems1, _ref3, _ref4, _ref5, _ref6, _ref7, _order_billingAddress, _ref8, _ref9, _order_customerName, _ref10, _order_customerEmail, _ref11, _ref12, _order_shippingAddress, _store_footer, _store_termsUrl;
|
|
82
|
+
var _quote_customer, _quote_customer1, _quote_customer2;
|
|
82
83
|
const { order, orderId, store } = input;
|
|
83
84
|
const currency = String(order.currency || store.currency || 'USD').toUpperCase();
|
|
84
85
|
const totals = order.totals;
|
|
@@ -142,18 +143,25 @@ const addressLines = (address)=>invoiceAddressLines(address);
|
|
|
142
143
|
emphasis: true
|
|
143
144
|
});
|
|
144
145
|
}
|
|
146
|
+
// An unpaid quote keeps its customer, its addresses and its date in its
|
|
147
|
+
// quote section until it is paid (AGL-4093); its invoice (AGL-4099) reads
|
|
148
|
+
// them there.
|
|
149
|
+
const quote = order.quote;
|
|
150
|
+
const billingAddress = (_ref7 = (_order_billingAddress = order.billingAddress) != null ? _order_billingAddress : quote == null ? void 0 : quote.billTo) != null ? _ref7 : null;
|
|
151
|
+
const customerName = (_ref8 = (_ref9 = (_order_customerName = order.customerName) != null ? _order_customerName : quote == null ? void 0 : (_quote_customer = quote.customer) == null ? void 0 : _quote_customer.name) != null ? _ref9 : quote == null ? void 0 : (_quote_customer1 = quote.customer) == null ? void 0 : _quote_customer1.company) != null ? _ref8 : '';
|
|
152
|
+
const customerEmail = (_ref10 = (_order_customerEmail = order.customerEmail) != null ? _order_customerEmail : quote == null ? void 0 : (_quote_customer2 = quote.customer) == null ? void 0 : _quote_customer2.email) != null ? _ref10 : '';
|
|
145
153
|
return {
|
|
146
154
|
number: formatOrderNumber(order, orderId),
|
|
147
|
-
dateMs: orderCreatedAtMs(order),
|
|
155
|
+
dateMs: orderCreatedAtMs(order) || Number((_ref11 = quote == null ? void 0 : quote.updatedAtMs) != null ? _ref11 : quote == null ? void 0 : quote.createdAtMs) || 0,
|
|
148
156
|
currency,
|
|
149
157
|
paymentStatus: invoicePaymentStatus(order),
|
|
150
158
|
billTo: [
|
|
151
|
-
...
|
|
152
|
-
|
|
159
|
+
...billingAddress ? addressLines(billingAddress) : [
|
|
160
|
+
customerName
|
|
153
161
|
].filter(Boolean),
|
|
154
|
-
|
|
162
|
+
customerEmail
|
|
155
163
|
].filter(Boolean),
|
|
156
|
-
shipTo: addressLines(order.shippingAddress),
|
|
164
|
+
shipTo: addressLines((_ref12 = (_order_shippingAddress = order.shippingAddress) != null ? _order_shippingAddress : quote == null ? void 0 : quote.shipTo) != null ? _ref12 : null),
|
|
157
165
|
lines,
|
|
158
166
|
summary,
|
|
159
167
|
footer: String((_store_footer = store.footer) != null ? _store_footer : '').trim(),
|
|
@@ -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\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> & { 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 return {\n number: formatOrderNumber(order, orderId),\n dateMs: orderCreatedAtMs(order as never),\n currency,\n paymentStatus: invoicePaymentStatus(order),\n billTo: [\n ...(order.billingAddress ? addressLines(order.billingAddress) : [order.customerName ?? ''].filter(Boolean)),\n order.customerEmail ?? '',\n ].filter(Boolean),\n shipTo: addressLines(order.shippingAddress),\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","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","number","dateMs","paymentStatus","billTo","billingAddress","customerName","customerEmail","shipTo","shippingAddress","buildOrderInvoiceHtml","data","money","rows","figure","block","title","length","Date","toLocaleDateString","year","month","day","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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"}
|