@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
@@ -0,0 +1,132 @@
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, type OrderTimelineEvent } from './commerce-orders';
18
+ import { type QuoteCheckoutCharge, type QuotePageView, type QuotePayRefusal } from './commerce-quote-pay';
19
+ import { type OrderInvoiceSendRecord, type QuoteEmailThread, type QuoteTaxBasis } from './commerce-quotes';
20
+ import type { ResolvedShippingRate } from './commerce-shipping';
21
+ /** Which page a pay link opens for an order: a quote, an invoice, or none. */
22
+ export type OrderPayLinkKind = 'quote' | 'invoice';
23
+ /** The most invoice sends an order keeps on record; the oldest go first. */
24
+ export declare const INVOICE_MAX_SENDS = 50;
25
+ /**
26
+ * Which page the lasting pay link opens for an order, or null when it opens
27
+ * none: a quote (with its quote section), or a draft order as an invoice.
28
+ */
29
+ export declare function orderPayLinkKind(order: Pick<HostOrder, 'channel'> & Partial<Pick<HostOrder, 'quote'>> | null | undefined): OrderPayLinkKind | null;
30
+ /** `Invoice #1042`: the order's own number, which a paid quote keeps too. */
31
+ export declare function formatInvoiceNumber(order: Pick<HostOrder, 'number'>, orderId?: string): string;
32
+ /** The order is paid (and not refunded or canceled): its invoice is the record. */
33
+ export declare function invoiceIsPaid(order: Pick<HostOrder, 'status'> | null | undefined): boolean;
34
+ /**
35
+ * When the order was paid: a quote's stamp, else the timeline's `paid`
36
+ * event, else (an order created paid, as checkout's are) when it was made.
37
+ * Null for an order not paid.
38
+ */
39
+ export declare function orderPaidAtMs(order: Pick<HostOrder, 'status' | 'quote' | 'timeline'> & {
40
+ createdAtMs?: number;
41
+ } | null | undefined): number | null;
42
+ /** What an invoice's Checkout Session charges, with the rates a draft lets the buyer pick from. */
43
+ export interface InvoiceCheckoutCharge extends QuoteCheckoutCharge {
44
+ /** The draft's shipping rates, picked at payment; empty when shipping is fixed or there is none. */
45
+ shippingOptions: ResolvedShippingRate[];
46
+ /** Where those rates ship; empty with none. */
47
+ shippingCountries: string[];
48
+ }
49
+ /**
50
+ * The tax basis an order's stored figures give its invoice: Stripe Tax at
51
+ * payment where the order was composed under it, its frozen tax as a fixed
52
+ * figure (charged as the rate that yields it on the taxed line, the way a tax
53
+ * service's quote is), else none.
54
+ */
55
+ export declare function invoiceTaxBasis(order: Pick<HostOrder, 'taxMode' | 'totals'>): QuoteTaxBasis;
56
+ /**
57
+ * What an invoice's pay link charges, from the stored order alone. A quote is
58
+ * charged as slice 2 charges it; any other order from its lines, its
59
+ * discount, its tax ({@link invoiceTaxBasis}) and, for a draft, the shipping
60
+ * rates it offered when it was made (Stripe asks for the address and the
61
+ * buyer picks one, as on the draft's first link).
62
+ */
63
+ export declare function invoiceCheckoutCharge(order: Pick<HostOrder, 'number' | 'lineItems' | 'quote' | 'totals' | 'taxMode'> & Partial<Pick<HostOrder, 'draftShipping'>>, orderId: string): InvoiceCheckoutCharge;
64
+ /**
65
+ * Why an order cannot be paid through its pay link now: a quote's reasons,
66
+ * and two of an invoice's own. `not-payable` is an order the link does not
67
+ * open (a register sale still open, say); `shipping-unknown` is a draft made
68
+ * before drafts kept their shipping rates, whose shipping cannot be charged.
69
+ */
70
+ export type InvoicePayRefusal = QuotePayRefusal | 'not-payable' | 'shipping-unknown';
71
+ export declare function invoicePayRefusal(order: Pick<HostOrder, 'number' | 'status' | 'lineItems' | 'quote' | 'totals' | 'taxMode' | 'channel'> & Partial<Pick<HostOrder, 'draftShipping'>>, orderId: string): InvoicePayRefusal | null;
72
+ /** What the customer is told for a refusal, in the page's words: "quote" or "invoice". */
73
+ export declare function invoicePayRefusalMessage(reason: InvoicePayRefusal, storeName: string, kind: OrderPayLinkKind): string;
74
+ /**
75
+ * The pay link's page for an order that is not a quote: "Invoice #N", its
76
+ * lines and totals, the customer's addresses, and Pay while it is unpaid. No
77
+ * proofs, no approval, no notes or terms (an order carries none). Allow-listed
78
+ * as the quote's view is: the order's email, phone, Stripe ids and timeline
79
+ * never leave the server.
80
+ */
81
+ export declare function buildInvoicePageView(input: {
82
+ order: Pick<HostOrder, 'number' | 'status' | 'channel' | 'lineItems' | 'totals' | 'quote' | 'timeline' | 'taxMode' | 'customerName' | 'billingAddress' | 'shippingAddress'> & Partial<Pick<HostOrder, 'draftShipping'>> & {
83
+ createdAtMs?: number;
84
+ };
85
+ orderId: string;
86
+ storeName: string;
87
+ currency: string;
88
+ /** The order's status page, once paid. */
89
+ receiptUrl?: string | null;
90
+ }): QuotePageView;
91
+ /** The order's invoice sends on record, oldest first. */
92
+ export declare function invoiceSends(order: Pick<HostOrder, 'invoiceSends'> | null | undefined): OrderInvoiceSendRecord[];
93
+ /** The last time the order's invoice was emailed, or null. */
94
+ export declare function lastInvoiceSend(order: Pick<HostOrder, 'invoiceSends'> | null | undefined): OrderInvoiceSendRecord | null;
95
+ /** Where the invoice goes: what was typed, else the order's customer, else a quote's. */
96
+ export declare function invoiceSendRecipient(order: Pick<HostOrder, 'customerEmail' | 'quote'> | null | undefined, typed: string | null | undefined): string;
97
+ /**
98
+ * The conversation an invoice replies into: a quote's own (its emails, and a
99
+ * request's confirmation before them), else the order's, which the first
100
+ * invoice starts.
101
+ */
102
+ export declare function invoiceEmailThread(order: Pick<HostOrder, 'quote' | 'emailThread'> | null | undefined): QuoteEmailThread | null;
103
+ /**
104
+ * The subject an invoice defaults to: the conversation's, so it stays one
105
+ * conversation in the customer's mailbox, else `Invoice #1042 from Acme Signs`.
106
+ */
107
+ export declare function invoiceEmailDefaultSubject(order: Pick<HostOrder, 'number' | 'quote' | 'emailThread'>, businessName: string, orderId?: string): string;
108
+ /** Why an invoice cannot be emailed now, and what the console says. */
109
+ export type InvoiceSendRefusal = 'unknown' | 'closed' | 'empty' | 'not-payable' | 'approval-required' | 'tax-undecided' | 'nothing-to-pay' | 'shipping-unknown' | 'no-address' | 'bad-address';
110
+ /**
111
+ * Why the order's invoice cannot be emailed to `to` now, if it cannot:
112
+ * a canceled or refunded order, one with nothing on it, an unpaid order its
113
+ * pay link cannot take payment for (and why), and the address.
114
+ */
115
+ export declare function invoiceSendRefusal(order: (Pick<HostOrder, 'number' | 'status' | 'channel' | 'lineItems' | 'totals' | 'quote' | 'taxMode'> & Partial<Pick<HostOrder, 'draftShipping'>>) | null | undefined, orderId: string, to: string): InvoiceSendRefusal | null;
116
+ /** What the console says for a refused invoice send. */
117
+ export declare function invoiceSendRefusalMessage(reason: InvoiceSendRefusal): string;
118
+ /** The HTTP status a refused send answers with. */
119
+ export declare function invoiceSendRefusalStatus(reason: InvoiceSendRefusal): number;
120
+ /**
121
+ * What an invoice email changes on the order, as a merge: the send on
122
+ * record, a line on the timeline, and the conversation moved on, on the
123
+ * quote for a quote and on the order otherwise.
124
+ */
125
+ export declare function invoiceSendPatch(order: Pick<HostOrder, 'timeline' | 'invoiceSends' | 'quote'>, send: OrderInvoiceSendRecord, thread: QuoteEmailThread): {
126
+ timeline: OrderTimelineEvent[];
127
+ invoiceSends: OrderInvoiceSendRecord[];
128
+ emailThread?: QuoteEmailThread;
129
+ quote?: {
130
+ emailThread: QuoteEmailThread;
131
+ };
132
+ };
@@ -0,0 +1,332 @@
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 { appendOrderEvent, formatOrderNumber, orderCreatedAtMs } from "./commerce-orders.js";
18
+ import { QUOTE_MIN_CHARGE_CENTS, payLinkCheckoutCharge, quoteCheckoutCharge, quotePageAddress, quotePageLines, quotePageStatus, quotePayRefusal, quotePayRefusalMessage } from "./commerce-quote-pay.js";
19
+ import { isQuote } from "./commerce-quotes.js";
20
+ import { lineRequiresShipping } from "./order-fulfillment.js";
21
+ /** The most invoice sends an order keeps on record; the oldest go first. */ export const INVOICE_MAX_SENDS = 50;
22
+ const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
23
+ const cents = (value)=>{
24
+ const number = Math.round(Number(value));
25
+ return Number.isFinite(number) ? number : 0;
26
+ };
27
+ const text = (value)=>{
28
+ const trimmed = typeof value === 'string' ? value.trim() : '';
29
+ return trimmed ? trimmed : null;
30
+ };
31
+ /**
32
+ * Which page the lasting pay link opens for an order, or null when it opens
33
+ * none: a quote (with its quote section), or a draft order as an invoice.
34
+ */ export function orderPayLinkKind(order) {
35
+ if (!order) return null;
36
+ if (isQuote(order)) return order.quote ? 'quote' : null;
37
+ return order.channel === 'draft' ? 'invoice' : null;
38
+ }
39
+ /** `Invoice #1042`: the order's own number, which a paid quote keeps too. */ export function formatInvoiceNumber(order, orderId) {
40
+ return `Invoice ${formatOrderNumber(order, orderId)}`;
41
+ }
42
+ const PAID_STATUSES = new Set([
43
+ 'paid',
44
+ 'partially_fulfilled',
45
+ 'fulfilled',
46
+ 'delivered'
47
+ ]);
48
+ /** The order is paid (and not refunded or canceled): its invoice is the record. */ export function invoiceIsPaid(order) {
49
+ var _ref;
50
+ return PAID_STATUSES.has(String((_ref = order == null ? void 0 : order.status) != null ? _ref : ''));
51
+ }
52
+ /**
53
+ * When the order was paid: a quote's stamp, else the timeline's `paid`
54
+ * event, else (an order created paid, as checkout's are) when it was made.
55
+ * Null for an order not paid.
56
+ */ export function orderPaidAtMs(order) {
57
+ var _order_timeline;
58
+ var _order_quote;
59
+ if (!order || order.status === 'pending') return null;
60
+ const stamped = Number((_order_quote = order.quote) == null ? void 0 : _order_quote.paidAtMs);
61
+ if (Number.isFinite(stamped) && stamped > 0) return stamped;
62
+ const event = ((_order_timeline = order.timeline) != null ? _order_timeline : []).find((entry)=>(entry == null ? void 0 : entry.event) === 'paid');
63
+ if (event && Number(event.atMs) > 0) return Number(event.atMs);
64
+ const created = orderCreatedAtMs(order);
65
+ return created > 0 ? created : null;
66
+ }
67
+ /**
68
+ * The tax basis an order's stored figures give its invoice: Stripe Tax at
69
+ * payment where the order was composed under it, its frozen tax as a fixed
70
+ * figure (charged as the rate that yields it on the taxed line, the way a tax
71
+ * service's quote is), else none.
72
+ */ export function invoiceTaxBasis(order) {
73
+ var _order_totals;
74
+ if (order.taxMode === 'stripe-automatic') return {
75
+ kind: 'at-payment'
76
+ };
77
+ const taxCents = Math.max(0, cents((_order_totals = order.totals) == null ? void 0 : _order_totals.taxCents));
78
+ if (taxCents > 0) return {
79
+ kind: 'service',
80
+ taxCents
81
+ };
82
+ return {
83
+ kind: 'none',
84
+ reason: 'not-collected'
85
+ };
86
+ }
87
+ /** A stored draft rate, as Stripe is given it, or null for one that is not a rate. */ function draftRate(option) {
88
+ var _source_rateId;
89
+ if (!option || typeof option !== 'object') return null;
90
+ const source = option;
91
+ const name = text(source['name']);
92
+ const amountCents = cents(source['amountCents']);
93
+ if (!name || amountCents < 0) return null;
94
+ return {
95
+ rateId: String((_source_rateId = source['rateId']) != null ? _source_rateId : '') || name,
96
+ name,
97
+ amountCents
98
+ };
99
+ }
100
+ /**
101
+ * What an invoice's pay link charges, from the stored order alone. A quote is
102
+ * charged as slice 2 charges it; any other order from its lines, its
103
+ * discount, its tax ({@link invoiceTaxBasis}) and, for a draft, the shipping
104
+ * rates it offered when it was made (Stripe asks for the address and the
105
+ * buyer picks one, as on the draft's first link).
106
+ */ export function invoiceCheckoutCharge(order, orderId) {
107
+ var _order_lineItems, _ref, _ref1;
108
+ var _order_totals, _order_draftShipping, _order_draftShipping1;
109
+ if (order.quote) return _extends({}, quoteCheckoutCharge(order, orderId), {
110
+ shippingOptions: [],
111
+ shippingCountries: []
112
+ });
113
+ const discountCents = Math.max(0, cents((_order_totals = order.totals) == null ? void 0 : _order_totals.discountCents));
114
+ const charge = payLinkCheckoutCharge({
115
+ lineItems: (_order_lineItems = order.lineItems) != null ? _order_lineItems : [],
116
+ tax: invoiceTaxBasis(order),
117
+ discount: discountCents > 0 ? {
118
+ kind: 'amount',
119
+ amountCents: discountCents
120
+ } : null,
121
+ shippingCents: 0,
122
+ label: formatInvoiceNumber(order, orderId)
123
+ });
124
+ const options = ((_ref = (_order_draftShipping = order.draftShipping) == null ? void 0 : _order_draftShipping.options) != null ? _ref : []).map(draftRate).filter((rate)=>rate !== null).slice(0, 5);
125
+ const countries = ((_ref1 = (_order_draftShipping1 = order.draftShipping) == null ? void 0 : _order_draftShipping1.countries) != null ? _ref1 : []).map((country)=>String(country != null ? country : '').trim().toUpperCase()).filter((country)=>/^[A-Z]{2}$/.test(country));
126
+ return _extends({}, charge, {
127
+ shippingOptions: options,
128
+ shippingCountries: options.length ? countries : []
129
+ });
130
+ }
131
+ export function invoicePayRefusal(order, orderId) {
132
+ var _order_lineItems;
133
+ const kind = orderPayLinkKind(order);
134
+ if (kind === 'quote') return quotePayRefusal(order, orderId);
135
+ if (order.status === 'cancelled' || order.status === 'refunded') return 'closed';
136
+ if (order.status !== 'pending') return 'paid';
137
+ if (kind !== 'invoice') return 'not-payable';
138
+ const ships = ((_order_lineItems = order.lineItems) != null ? _order_lineItems : []).some((line)=>line && !line.included && lineRequiresShipping(line));
139
+ if (ships && !order.draftShipping) return 'shipping-unknown';
140
+ if (invoiceCheckoutCharge(order, orderId).totalCents < QUOTE_MIN_CHARGE_CENTS) return 'nothing-to-pay';
141
+ return null;
142
+ }
143
+ /** What the customer is told for a refusal, in the page's words: "quote" or "invoice". */ export function invoicePayRefusalMessage(reason, storeName, kind) {
144
+ const business = storeName.trim() || 'the business';
145
+ if (kind === 'quote' && reason !== 'not-payable' && reason !== 'shipping-unknown') {
146
+ return quotePayRefusalMessage(reason, storeName);
147
+ }
148
+ switch(reason){
149
+ case 'paid':
150
+ return 'This invoice has been paid.';
151
+ case 'closed':
152
+ return `This invoice is closed. Contact ${business} with any questions.`;
153
+ case 'approval-required':
154
+ return `${business} asks you to approve the proof before you pay.`;
155
+ case 'tax-undecided':
156
+ return `${business} has not finished setting up payments. Contact them to pay this invoice.`;
157
+ case 'nothing-to-pay':
158
+ return `This invoice has nothing to pay by card. Contact ${business} with any questions.`;
159
+ case 'not-payable':
160
+ case 'shipping-unknown':
161
+ return `This invoice can’t be paid online. Contact ${business} to pay it.`;
162
+ }
163
+ }
164
+ /*==========================================
165
+ * THE CUSTOMER'S PAGE FOR AN INVOICE
166
+ *=========================================*/ /**
167
+ * The pay link's page for an order that is not a quote: "Invoice #N", its
168
+ * lines and totals, the customer's addresses, and Pay while it is unpaid. No
169
+ * proofs, no approval, no notes or terms (an order carries none). Allow-listed
170
+ * as the quote's view is: the order's email, phone, Stripe ids and timeline
171
+ * never leave the server.
172
+ */ export function buildInvoicePageView(input) {
173
+ var _order_lineItems, _input_receiptUrl;
174
+ const { order } = input;
175
+ const status = quotePageStatus(order.status);
176
+ const unpaid = status.status === 'unpaid';
177
+ const charge = invoiceCheckoutCharge(order, input.orderId);
178
+ const stored = order.totals;
179
+ const totals = unpaid || !stored ? {
180
+ itemsCents: charge.itemsCents,
181
+ discountCents: charge.discountCents,
182
+ shippingCents: charge.shippingCents,
183
+ taxCents: charge.taxCents,
184
+ totalCents: charge.totalCents
185
+ } : {
186
+ itemsCents: cents(stored.itemsCents),
187
+ discountCents: cents(stored.discountCents),
188
+ shippingCents: cents(stored.shippingCents),
189
+ taxCents: cents(stored.taxCents),
190
+ totalCents: cents(stored.totalCents)
191
+ };
192
+ const taxAtPayment = unpaid && charge.taxAtPayment;
193
+ const created = orderCreatedAtMs(order);
194
+ return {
195
+ state: 'quote',
196
+ kind: 'invoice',
197
+ storeName: input.storeName,
198
+ number: formatInvoiceNumber(order, input.orderId),
199
+ dateMs: created > 0 ? created : null,
200
+ // An invoice is not edited; the page has no version to send back.
201
+ version: 0,
202
+ currency: input.currency || 'USD',
203
+ customerName: text(order.customerName),
204
+ company: null,
205
+ billTo: quotePageAddress(order.billingAddress),
206
+ shipTo: quotePageAddress(order.shippingAddress),
207
+ lines: quotePageLines((_order_lineItems = order.lineItems) != null ? _order_lineItems : []),
208
+ totals,
209
+ discountLabel: null,
210
+ taxLabel: taxAtPayment ? 'Tax is added at payment' : 'Tax',
211
+ taxAtPayment,
212
+ shippingAtPayment: unpaid && charge.shippingOptions.length > 0,
213
+ notes: null,
214
+ terms: null,
215
+ status: status.status,
216
+ statusLabel: status.label,
217
+ paidAtMs: orderPaidAtMs(order),
218
+ receiptUrl: unpaid ? null : (_input_receiptUrl = input.receiptUrl) != null ? _input_receiptUrl : null,
219
+ approvalRequired: false,
220
+ payable: unpaid && invoicePayRefusal(order, input.orderId) === null,
221
+ revision: 1,
222
+ proofs: [],
223
+ response: null,
224
+ respondable: false
225
+ };
226
+ }
227
+ /*==========================================
228
+ * EMAILING AN INVOICE
229
+ *=========================================*/ /** The order's invoice sends on record, oldest first. */ export function invoiceSends(order) {
230
+ return Array.isArray(order == null ? void 0 : order.invoiceSends) ? order.invoiceSends.filter((send)=>send && typeof send === 'object') : [];
231
+ }
232
+ /** The last time the order's invoice was emailed, or null. */ export function lastInvoiceSend(order) {
233
+ const sends = invoiceSends(order);
234
+ return sends.length ? sends[sends.length - 1] : null;
235
+ }
236
+ /** Where the invoice goes: what was typed, else the order's customer, else a quote's. */ export function invoiceSendRecipient(order, typed) {
237
+ var _ref, _ref1;
238
+ var _order_quote_customer, _order_quote;
239
+ return String(typed != null ? typed : '').trim() || String((_ref = order == null ? void 0 : order.customerEmail) != null ? _ref : '').trim() || String((_ref1 = order == null ? void 0 : (_order_quote = order.quote) == null ? void 0 : (_order_quote_customer = _order_quote.customer) == null ? void 0 : _order_quote_customer.email) != null ? _ref1 : '').trim();
240
+ }
241
+ /**
242
+ * The conversation an invoice replies into: a quote's own (its emails, and a
243
+ * request's confirmation before them), else the order's, which the first
244
+ * invoice starts.
245
+ */ export function invoiceEmailThread(order) {
246
+ var _order_quote_emailThread, _ref;
247
+ if (order == null ? void 0 : order.quote) return (_order_quote_emailThread = order.quote.emailThread) != null ? _order_quote_emailThread : null;
248
+ return (_ref = order == null ? void 0 : order.emailThread) != null ? _ref : null;
249
+ }
250
+ /**
251
+ * The subject an invoice defaults to: the conversation's, so it stays one
252
+ * conversation in the customer's mailbox, else `Invoice #1042 from Acme Signs`.
253
+ */ export function invoiceEmailDefaultSubject(order, businessName, orderId) {
254
+ var _ref;
255
+ var _invoiceEmailThread;
256
+ const thread = String((_ref = (_invoiceEmailThread = invoiceEmailThread(order)) == null ? void 0 : _invoiceEmailThread.subject) != null ? _ref : '').trim();
257
+ if (thread) return thread;
258
+ const name = String(businessName != null ? businessName : '').trim();
259
+ const number = formatInvoiceNumber(order, orderId);
260
+ return name ? `${number} from ${name}` : number;
261
+ }
262
+ /**
263
+ * Why the order's invoice cannot be emailed to `to` now, if it cannot:
264
+ * a canceled or refunded order, one with nothing on it, an unpaid order its
265
+ * pay link cannot take payment for (and why), and the address.
266
+ */ export function invoiceSendRefusal(order, orderId, to) {
267
+ var _order_lineItems;
268
+ var _order_totals;
269
+ if (!order) return 'unknown';
270
+ if (order.status === 'cancelled' || order.status === 'refunded') return 'closed';
271
+ const lines = ((_order_lineItems = order.lineItems) != null ? _order_lineItems : []).filter(Boolean);
272
+ if (!lines.length && cents((_order_totals = order.totals) == null ? void 0 : _order_totals.totalCents) <= 0) return 'empty';
273
+ if (!invoiceIsPaid(order)) {
274
+ const refused = invoicePayRefusal(order, orderId);
275
+ if (refused === 'closed') return 'closed';
276
+ if (refused && refused !== 'paid') return refused;
277
+ }
278
+ if (!to) return 'no-address';
279
+ if (to.length > 320 || !EMAIL.test(to)) return 'bad-address';
280
+ return null;
281
+ }
282
+ /** What the console says for a refused invoice send. */ export function invoiceSendRefusalMessage(reason) {
283
+ switch(reason){
284
+ case 'unknown':
285
+ return 'Unknown order';
286
+ case 'closed':
287
+ return 'This order was canceled or refunded, so it has no invoice to send.';
288
+ case 'empty':
289
+ return 'This order has no items and no total, so there is nothing to put on an invoice.';
290
+ case 'not-payable':
291
+ return 'This order isn’t paid yet, and it can’t be paid online from an invoice. Take its payment where the sale was made, then send the invoice.';
292
+ case 'approval-required':
293
+ return 'This quote asks your customer to approve the proof before they pay, and they haven’t yet. Send the quote so they can approve it, then send the invoice.';
294
+ case 'tax-undecided':
295
+ return 'Sales tax isn’t set up for this store yet, so the invoice can’t be paid online. Finish the tax settings, then send it.';
296
+ case 'nothing-to-pay':
297
+ return 'This invoice totals less than $0.50, so it can’t be paid by card.';
298
+ case 'shipping-unknown':
299
+ return 'This draft order was made before invoices could charge its shipping, so its invoice can’t be paid online. Make a new draft order and send its invoice.';
300
+ case 'no-address':
301
+ return 'This order has no email for the customer. Type where to send the invoice.';
302
+ case 'bad-address':
303
+ return 'Enter a valid email address to send the invoice to.';
304
+ }
305
+ }
306
+ /** The HTTP status a refused send answers with. */ export function invoiceSendRefusalStatus(reason) {
307
+ if (reason === 'unknown') return 404;
308
+ if (reason === 'no-address' || reason === 'bad-address') return 400;
309
+ return 409;
310
+ }
311
+ /**
312
+ * What an invoice email changes on the order, as a merge: the send on
313
+ * record, a line on the timeline, and the conversation moved on, on the
314
+ * quote for a quote and on the order otherwise.
315
+ */ export function invoiceSendPatch(order, send, thread) {
316
+ return _extends({
317
+ timeline: appendOrderEvent(order, 'invoice-sent', // The address is on the send record, which erasure clears; the timeline keeps none.
318
+ `${send.paid ? 'Paid invoice' : 'Invoice'} emailed to the customer by ${send.by}`, send.atMs),
319
+ invoiceSends: [
320
+ ...invoiceSends(order),
321
+ send
322
+ ].slice(-INVOICE_MAX_SENDS)
323
+ }, order.quote ? {
324
+ quote: {
325
+ emailThread: thread
326
+ }
327
+ } : {
328
+ emailThread: thread
329
+ });
330
+ }
331
+
332
+ //# sourceMappingURL=commerce-invoices.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/commerce-invoices.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n appendOrderEvent,\n formatOrderNumber,\n orderCreatedAtMs,\n type HostOrder,\n type OrderStatus,\n type OrderTimelineEvent,\n} from './commerce-orders'\nimport {\n QUOTE_MIN_CHARGE_CENTS,\n payLinkCheckoutCharge,\n quoteCheckoutCharge,\n quotePageAddress,\n quotePageLines,\n quotePageStatus,\n quotePayRefusal,\n quotePayRefusalMessage,\n type QuoteCheckoutCharge,\n type QuotePageView,\n type QuotePayRefusal,\n} from './commerce-quote-pay'\nimport {\n isQuote,\n type OrderInvoiceSendRecord,\n type QuoteEmailThread,\n type QuoteTaxBasis,\n} from './commerce-quotes'\nimport type { ResolvedShippingRate } from './commerce-shipping'\nimport { lineRequiresShipping } from './order-fulfillment'\n\n/*==========================================\n * INVOICES FOR ORDERS AND QUOTES (AGL-4099).\n *\n * The business emails an invoice for an order or a quote: unpaid, it asks\n * for payment through ONE lasting pay link; paid, it is the record. The link\n * is slice 2's (`/pay-quote?q=&t=`, AGL-4094), generalized rather than made\n * twice:\n *\n * - THE TOKEN is unchanged: `<version>.<HMAC(quote-link:hostId:orderId:version)>`,\n * no expiry. An order id names one document under `hosts/{hostId}/orders`\n * whatever its channel, so the same signature serves any order.\n * - THE VERSION is the quote's `quote.linkVersion` for a quote, as before,\n * so every link already sent keeps verifying; any other order keeps its own\n * as `payLink.version` (`orderPayLinkVersion`). Absent reads as 1.\n * - WHICH ORDERS the link opens ({@link orderPayLinkKind}): a quote, and a\n * draft order (`channel: 'draft'`), the two a business composes for a\n * customer to pay online. A register sale still open takes its payment at\n * the register, an online order was paid at checkout, and a marketplace\n * order on its channel, so none of them is linked; their invoices go out\n * paid, as the record.\n * - THE PAGE shows a non-quote order as \"Invoice #N\", with no proofs and no\n * approval ({@link buildInvoicePageView}).\n * - THE CHARGE is priced from the stored lines and totals on every press\n * ({@link invoiceCheckoutCharge}): the draft's discount, its frozen tax (or\n * Stripe Tax at payment where the draft used it), and the shipping rates\n * the draft offered when it was made (`draftShipping`).\n *\n * Pure: the console, the routes and the webhook read the same answers.\n *=========================================*/\n\n/** Which page a pay link opens for an order: a quote, an invoice, or none. */\nexport type OrderPayLinkKind = 'quote' | 'invoice'\n\n/** The most invoice sends an order keeps on record; the oldest go first. */\nexport const INVOICE_MAX_SENDS = 50\n\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\nconst cents = (value: unknown): number => {\n const number = Math.round(Number(value))\n return Number.isFinite(number) ? number : 0\n}\n\nconst text = (value: unknown): string | null => {\n const trimmed = typeof value === 'string' ? value.trim() : ''\n return trimmed ? trimmed : null\n}\n\n/**\n * Which page the lasting pay link opens for an order, or null when it opens\n * none: a quote (with its quote section), or a draft order as an invoice.\n */\nexport function orderPayLinkKind(\n order: Pick<HostOrder, 'channel'> & Partial<Pick<HostOrder, 'quote'>> | null | undefined,\n): OrderPayLinkKind | null {\n if (!order) return null\n if (isQuote(order)) return order.quote ? 'quote' : null\n return order.channel === 'draft' ? 'invoice' : null\n}\n\n/** `Invoice #1042`: the order's own number, which a paid quote keeps too. */\nexport function formatInvoiceNumber(order: Pick<HostOrder, 'number'>, orderId?: string): string {\n return `Invoice ${formatOrderNumber(order, orderId)}`\n}\n\nconst PAID_STATUSES: ReadonlySet<OrderStatus | string> = new Set([\n 'paid',\n 'partially_fulfilled',\n 'fulfilled',\n 'delivered',\n])\n\n/** The order is paid (and not refunded or canceled): its invoice is the record. */\nexport function invoiceIsPaid(order: Pick<HostOrder, 'status'> | null | undefined): boolean {\n return PAID_STATUSES.has(String(order?.status ?? ''))\n}\n\n/**\n * When the order was paid: a quote's stamp, else the timeline's `paid`\n * event, else (an order created paid, as checkout's are) when it was made.\n * Null for an order not paid.\n */\nexport function orderPaidAtMs(\n order: Pick<HostOrder, 'status' | 'quote' | 'timeline'> & { createdAtMs?: number } | null | undefined,\n): number | null {\n if (!order || order.status === 'pending') return null\n const stamped = Number(order.quote?.paidAtMs)\n if (Number.isFinite(stamped) && stamped > 0) return stamped\n const event = (order.timeline ?? []).find((entry) => entry?.event === 'paid')\n if (event && Number(event.atMs) > 0) return Number(event.atMs)\n const created = orderCreatedAtMs(order as never)\n return created > 0 ? created : null\n}\n\n/*==========================================\n * WHAT AN INVOICE'S PAY LINK CHARGES\n *=========================================*/\n\n/** What an invoice's Checkout Session charges, with the rates a draft lets the buyer pick from. */\nexport interface InvoiceCheckoutCharge extends QuoteCheckoutCharge {\n /** The draft's shipping rates, picked at payment; empty when shipping is fixed or there is none. */\n shippingOptions: ResolvedShippingRate[]\n /** Where those rates ship; empty with none. */\n shippingCountries: string[]\n}\n\n/**\n * The tax basis an order's stored figures give its invoice: Stripe Tax at\n * payment where the order was composed under it, its frozen tax as a fixed\n * figure (charged as the rate that yields it on the taxed line, the way a tax\n * service's quote is), else none.\n */\nexport function invoiceTaxBasis(order: Pick<HostOrder, 'taxMode' | 'totals'>): QuoteTaxBasis {\n if (order.taxMode === 'stripe-automatic') return { kind: 'at-payment' }\n const taxCents = Math.max(0, cents(order.totals?.taxCents))\n if (taxCents > 0) return { kind: 'service', taxCents }\n return { kind: 'none', reason: 'not-collected' }\n}\n\n/** A stored draft rate, as Stripe is given it, or null for one that is not a rate. */\nfunction draftRate(option: unknown): ResolvedShippingRate | null {\n if (!option || typeof option !== 'object') return null\n const source = option as Record<string, unknown>\n const name = text(source['name'])\n const amountCents = cents(source['amountCents'])\n if (!name || amountCents < 0) return null\n return { rateId: String(source['rateId'] ?? '') || name, name, amountCents }\n}\n\n/**\n * What an invoice's pay link charges, from the stored order alone. A quote is\n * charged as slice 2 charges it; any other order from its lines, its\n * discount, its tax ({@link invoiceTaxBasis}) and, for a draft, the shipping\n * rates it offered when it was made (Stripe asks for the address and the\n * buyer picks one, as on the draft's first link).\n */\nexport function invoiceCheckoutCharge(\n order: Pick<HostOrder, 'number' | 'lineItems' | 'quote' | 'totals' | 'taxMode'> &\n Partial<Pick<HostOrder, 'draftShipping'>>,\n orderId: string,\n): InvoiceCheckoutCharge {\n if (order.quote) return { ...quoteCheckoutCharge(order, orderId), shippingOptions: [], shippingCountries: [] }\n const discountCents = Math.max(0, cents(order.totals?.discountCents))\n const charge = payLinkCheckoutCharge({\n lineItems: order.lineItems ?? [],\n tax: invoiceTaxBasis(order),\n discount: discountCents > 0 ? { kind: 'amount', amountCents: discountCents } : null,\n shippingCents: 0,\n label: formatInvoiceNumber(order, orderId),\n })\n const options = (order.draftShipping?.options ?? [])\n .map(draftRate)\n .filter((rate): rate is ResolvedShippingRate => rate !== null)\n .slice(0, 5)\n const countries = (order.draftShipping?.countries ?? [])\n .map((country) => String(country ?? '').trim().toUpperCase())\n .filter((country) => /^[A-Z]{2}$/.test(country))\n return { ...charge, shippingOptions: options, shippingCountries: options.length ? countries : [] }\n}\n\n/*==========================================\n * WHETHER AN INVOICE CAN BE PAID NOW\n *=========================================*/\n\n/**\n * Why an order cannot be paid through its pay link now: a quote's reasons,\n * and two of an invoice's own. `not-payable` is an order the link does not\n * open (a register sale still open, say); `shipping-unknown` is a draft made\n * before drafts kept their shipping rates, whose shipping cannot be charged.\n */\nexport type InvoicePayRefusal = QuotePayRefusal | 'not-payable' | 'shipping-unknown'\n\nexport function invoicePayRefusal(\n order: Pick<HostOrder, 'number' | 'status' | 'lineItems' | 'quote' | 'totals' | 'taxMode' | 'channel'> &\n Partial<Pick<HostOrder, 'draftShipping'>>,\n orderId: string,\n): InvoicePayRefusal | null {\n const kind = orderPayLinkKind(order)\n if (kind === 'quote') return quotePayRefusal(order, orderId)\n if (order.status === 'cancelled' || order.status === 'refunded') return 'closed'\n if (order.status !== 'pending') return 'paid'\n if (kind !== 'invoice') return 'not-payable'\n const ships = (order.lineItems ?? []).some((line) => line && !line.included && lineRequiresShipping(line))\n if (ships && !order.draftShipping) return 'shipping-unknown'\n if (invoiceCheckoutCharge(order, orderId).totalCents < QUOTE_MIN_CHARGE_CENTS) return 'nothing-to-pay'\n return null\n}\n\n/** What the customer is told for a refusal, in the page's words: \"quote\" or \"invoice\". */\nexport function invoicePayRefusalMessage(\n reason: InvoicePayRefusal,\n storeName: string,\n kind: OrderPayLinkKind,\n): string {\n const business = storeName.trim() || 'the business'\n if (kind === 'quote' && reason !== 'not-payable' && reason !== 'shipping-unknown') {\n return quotePayRefusalMessage(reason, storeName)\n }\n switch (reason) {\n case 'paid':\n return 'This invoice has been paid.'\n case 'closed':\n return `This invoice is closed. Contact ${business} with any questions.`\n case 'approval-required':\n return `${business} asks you to approve the proof before you pay.`\n case 'tax-undecided':\n return `${business} has not finished setting up payments. Contact them to pay this invoice.`\n case 'nothing-to-pay':\n return `This invoice has nothing to pay by card. Contact ${business} with any questions.`\n case 'not-payable':\n case 'shipping-unknown':\n return `This invoice can’t be paid online. Contact ${business} to pay it.`\n }\n}\n\n/*==========================================\n * THE CUSTOMER'S PAGE FOR AN INVOICE\n *=========================================*/\n\n/**\n * The pay link's page for an order that is not a quote: \"Invoice #N\", its\n * lines and totals, the customer's addresses, and Pay while it is unpaid. No\n * proofs, no approval, no notes or terms (an order carries none). Allow-listed\n * as the quote's view is: the order's email, phone, Stripe ids and timeline\n * never leave the server.\n */\nexport function buildInvoicePageView(input: {\n order: Pick<\n HostOrder,\n | 'number'\n | 'status'\n | 'channel'\n | 'lineItems'\n | 'totals'\n | 'quote'\n | 'timeline'\n | 'taxMode'\n | 'customerName'\n | 'billingAddress'\n | 'shippingAddress'\n > &\n Partial<Pick<HostOrder, 'draftShipping'>> & { createdAtMs?: number }\n orderId: string\n storeName: string\n currency: string\n /** The order's status page, once paid. */\n receiptUrl?: string | null\n}): QuotePageView {\n const { order } = input\n const status = quotePageStatus(order.status)\n const unpaid = status.status === 'unpaid'\n const charge = invoiceCheckoutCharge(order, input.orderId)\n const stored = order.totals\n const totals =\n unpaid || !stored\n ? {\n itemsCents: charge.itemsCents,\n discountCents: charge.discountCents,\n shippingCents: charge.shippingCents,\n taxCents: charge.taxCents,\n totalCents: charge.totalCents,\n }\n : {\n itemsCents: cents(stored.itemsCents),\n discountCents: cents(stored.discountCents),\n shippingCents: cents(stored.shippingCents),\n taxCents: cents(stored.taxCents),\n totalCents: cents(stored.totalCents),\n }\n const taxAtPayment = unpaid && charge.taxAtPayment\n const created = orderCreatedAtMs(order as never)\n return {\n state: 'quote',\n kind: 'invoice',\n storeName: input.storeName,\n number: formatInvoiceNumber(order, input.orderId),\n dateMs: created > 0 ? created : null,\n // An invoice is not edited; the page has no version to send back.\n version: 0,\n currency: input.currency || 'USD',\n customerName: text(order.customerName),\n company: null,\n billTo: quotePageAddress(order.billingAddress),\n shipTo: quotePageAddress(order.shippingAddress),\n lines: quotePageLines(order.lineItems ?? []),\n totals,\n discountLabel: null,\n taxLabel: taxAtPayment ? 'Tax is added at payment' : 'Tax',\n taxAtPayment,\n shippingAtPayment: unpaid && charge.shippingOptions.length > 0,\n notes: null,\n terms: null,\n status: status.status,\n statusLabel: status.label,\n paidAtMs: orderPaidAtMs(order),\n receiptUrl: unpaid ? null : (input.receiptUrl ?? null),\n approvalRequired: false,\n payable: unpaid && invoicePayRefusal(order, input.orderId) === null,\n revision: 1,\n proofs: [],\n response: null,\n respondable: false,\n }\n}\n\n/*==========================================\n * EMAILING AN INVOICE\n *=========================================*/\n\n/** The order's invoice sends on record, oldest first. */\nexport function invoiceSends(order: Pick<HostOrder, 'invoiceSends'> | null | undefined): OrderInvoiceSendRecord[] {\n return Array.isArray(order?.invoiceSends)\n ? order.invoiceSends.filter((send) => send && typeof send === 'object')\n : []\n}\n\n/** The last time the order's invoice was emailed, or null. */\nexport function lastInvoiceSend(order: Pick<HostOrder, 'invoiceSends'> | null | undefined): OrderInvoiceSendRecord | null {\n const sends = invoiceSends(order)\n return sends.length ? sends[sends.length - 1] : null\n}\n\n/** Where the invoice goes: what was typed, else the order's customer, else a quote's. */\nexport function invoiceSendRecipient(\n order: Pick<HostOrder, 'customerEmail' | 'quote'> | null | undefined,\n typed: string | null | undefined,\n): string {\n return (\n String(typed ?? '').trim() ||\n String(order?.customerEmail ?? '').trim() ||\n String(order?.quote?.customer?.email ?? '').trim()\n )\n}\n\n/**\n * The conversation an invoice replies into: a quote's own (its emails, and a\n * request's confirmation before them), else the order's, which the first\n * invoice starts.\n */\nexport function invoiceEmailThread(\n order: Pick<HostOrder, 'quote' | 'emailThread'> | null | undefined,\n): QuoteEmailThread | null {\n if (order?.quote) return order.quote.emailThread ?? null\n return order?.emailThread ?? null\n}\n\n/**\n * The subject an invoice defaults to: the conversation's, so it stays one\n * conversation in the customer's mailbox, else `Invoice #1042 from Acme Signs`.\n */\nexport function invoiceEmailDefaultSubject(\n order: Pick<HostOrder, 'number' | 'quote' | 'emailThread'>,\n businessName: string,\n orderId?: string,\n): string {\n const thread = String(invoiceEmailThread(order)?.subject ?? '').trim()\n if (thread) return thread\n const name = String(businessName ?? '').trim()\n const number = formatInvoiceNumber(order, orderId)\n return name ? `${number} from ${name}` : number\n}\n\n/** Why an invoice cannot be emailed now, and what the console says. */\nexport type InvoiceSendRefusal =\n | 'unknown'\n | 'closed'\n | 'empty'\n | 'not-payable'\n | 'approval-required'\n | 'tax-undecided'\n | 'nothing-to-pay'\n | 'shipping-unknown'\n | 'no-address'\n | 'bad-address'\n\n/**\n * Why the order's invoice cannot be emailed to `to` now, if it cannot:\n * a canceled or refunded order, one with nothing on it, an unpaid order its\n * pay link cannot take payment for (and why), and the address.\n */\nexport function invoiceSendRefusal(\n order:\n | (Pick<HostOrder, 'number' | 'status' | 'channel' | 'lineItems' | 'totals' | 'quote' | 'taxMode'> &\n Partial<Pick<HostOrder, 'draftShipping'>>)\n | null\n | undefined,\n orderId: string,\n to: string,\n): InvoiceSendRefusal | null {\n if (!order) return 'unknown'\n if (order.status === 'cancelled' || order.status === 'refunded') return 'closed'\n const lines = (order.lineItems ?? []).filter(Boolean)\n if (!lines.length && cents(order.totals?.totalCents) <= 0) return 'empty'\n if (!invoiceIsPaid(order)) {\n const refused = invoicePayRefusal(order, orderId)\n if (refused === 'closed') return 'closed'\n if (refused && refused !== 'paid') return refused\n }\n if (!to) return 'no-address'\n if (to.length > 320 || !EMAIL.test(to)) return 'bad-address'\n return null\n}\n\n/** What the console says for a refused invoice send. */\nexport function invoiceSendRefusalMessage(reason: InvoiceSendRefusal): string {\n switch (reason) {\n case 'unknown':\n return 'Unknown order'\n case 'closed':\n return 'This order was canceled or refunded, so it has no invoice to send.'\n case 'empty':\n return 'This order has no items and no total, so there is nothing to put on an invoice.'\n case 'not-payable':\n return 'This order isn’t paid yet, and it can’t be paid online from an invoice. Take its payment where the sale was made, then send the invoice.'\n case 'approval-required':\n return 'This quote asks your customer to approve the proof before they pay, and they haven’t yet. Send the quote so they can approve it, then send the invoice.'\n case 'tax-undecided':\n return 'Sales tax isn’t set up for this store yet, so the invoice can’t be paid online. Finish the tax settings, then send it.'\n case 'nothing-to-pay':\n return 'This invoice totals less than $0.50, so it can’t be paid by card.'\n case 'shipping-unknown':\n return 'This draft order was made before invoices could charge its shipping, so its invoice can’t be paid online. Make a new draft order and send its invoice.'\n case 'no-address':\n return 'This order has no email for the customer. Type where to send the invoice.'\n case 'bad-address':\n return 'Enter a valid email address to send the invoice to.'\n }\n}\n\n/** The HTTP status a refused send answers with. */\nexport function invoiceSendRefusalStatus(reason: InvoiceSendRefusal): number {\n if (reason === 'unknown') return 404\n if (reason === 'no-address' || reason === 'bad-address') return 400\n return 409\n}\n\n/**\n * What an invoice email changes on the order, as a merge: the send on\n * record, a line on the timeline, and the conversation moved on, on the\n * quote for a quote and on the order otherwise.\n */\nexport function invoiceSendPatch(\n order: Pick<HostOrder, 'timeline' | 'invoiceSends' | 'quote'>,\n send: OrderInvoiceSendRecord,\n thread: QuoteEmailThread,\n): {\n timeline: OrderTimelineEvent[]\n invoiceSends: OrderInvoiceSendRecord[]\n emailThread?: QuoteEmailThread\n quote?: { emailThread: QuoteEmailThread }\n} {\n return {\n timeline: appendOrderEvent(\n order,\n 'invoice-sent',\n // The address is on the send record, which erasure clears; the timeline keeps none.\n `${send.paid ? 'Paid invoice' : 'Invoice'} emailed to the customer by ${send.by}`,\n send.atMs,\n ),\n invoiceSends: [...invoiceSends(order), send].slice(-INVOICE_MAX_SENDS),\n ...(order.quote ? { quote: { emailThread: thread } } : { emailThread: thread }),\n }\n}\n"],"names":["appendOrderEvent","formatOrderNumber","orderCreatedAtMs","QUOTE_MIN_CHARGE_CENTS","payLinkCheckoutCharge","quoteCheckoutCharge","quotePageAddress","quotePageLines","quotePageStatus","quotePayRefusal","quotePayRefusalMessage","isQuote","lineRequiresShipping","INVOICE_MAX_SENDS","EMAIL","cents","value","number","Math","round","Number","isFinite","text","trimmed","trim","orderPayLinkKind","order","quote","channel","formatInvoiceNumber","orderId","PAID_STATUSES","Set","invoiceIsPaid","has","String","status","orderPaidAtMs","stamped","paidAtMs","event","timeline","find","entry","atMs","created","invoiceTaxBasis","taxMode","kind","taxCents","max","totals","reason","draftRate","option","source","name","amountCents","rateId","invoiceCheckoutCharge","shippingOptions","shippingCountries","discountCents","charge","lineItems","tax","discount","shippingCents","label","options","draftShipping","map","filter","rate","slice","countries","country","toUpperCase","test","length","invoicePayRefusal","ships","some","line","included","totalCents","invoicePayRefusalMessage","storeName","business","buildInvoicePageView","input","unpaid","stored","itemsCents","taxAtPayment","state","dateMs","version","currency","customerName","company","billTo","billingAddress","shipTo","shippingAddress","lines","discountLabel","taxLabel","shippingAtPayment","notes","terms","statusLabel","receiptUrl","approvalRequired","payable","revision","proofs","response","respondable","invoiceSends","Array","isArray","send","lastInvoiceSend","sends","invoiceSendRecipient","typed","customerEmail","customer","email","invoiceEmailThread","emailThread","invoiceEmailDefaultSubject","businessName","thread","subject","invoiceSendRefusal","to","Boolean","refused","invoiceSendRefusalMessage","invoiceSendRefusalStatus","invoiceSendPatch","paid","by"],"mappings":"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"}
@@ -23,7 +23,7 @@
23
23
  * I/O here.
24
24
  */
25
25
  import type { OrderLineModifier } from './product-modifiers';
26
- import type { OrderQuote } from './commerce-quotes';
26
+ import type { OrderDraftShipping, OrderInvoiceSendRecord, OrderPayLink, OrderQuote, QuoteEmailThread } from './commerce-quotes';
27
27
  import type { PaymentRisk } from '@aglyn/aglyn/app-utils/payment-risk';
28
28
  import type { ProductType } from './commerce';
29
29
  import type { StorefrontTaxMode } from './commerce-tax-decision';
@@ -636,8 +636,35 @@ export interface HostOrder {
636
636
  */
637
637
  subscriptionId?: string;
638
638
  invoiceId?: string;
639
- /** Draft orders (AGL-287): the link sent to the buyer. */
639
+ /**
640
+ * Draft orders (AGL-287): the Stripe page the draft was first sent as. It
641
+ * lapses after a day; the order's invoice carries its lasting pay link
642
+ * instead (AGL-4099, `commerce-invoices.ts`).
643
+ */
640
644
  paymentLinkUrl?: string;
645
+ /**
646
+ * The shipping a draft order offered when it was made (AGL-4099): the
647
+ * countries it ships to and the rates the buyer picks from, so its pay link
648
+ * offers the same ones on any later day. Empty `options` when it ships
649
+ * nothing; absent on a draft made before invoices, and on every other order.
650
+ */
651
+ draftShipping?: OrderDraftShipping;
652
+ /** The promotion a draft order was priced with (AGL-4099), counted when its pay link is paid. */
653
+ discountId?: string;
654
+ /**
655
+ * The lasting pay link of an order that is not a quote (AGL-4099): its
656
+ * version and the payments it could not take. A quote keeps both in its
657
+ * `quote` section, where slice 2 put them.
658
+ */
659
+ payLink?: OrderPayLink;
660
+ /**
661
+ * The conversation an order's invoice emails keep in the customer's
662
+ * mailbox (AGL-4099), started by the first invoice. A quote's emails,
663
+ * invoices included, keep `quote.emailThread` instead.
664
+ */
665
+ emailThread?: QuoteEmailThread | null;
666
+ /** Every time the order's invoice was emailed (AGL-4099), oldest first. */
667
+ invoiceSends?: OrderInvoiceSendRecord[];
641
668
  /** What only a quote has (channel `quote`, AGL-4093); see `commerce-quotes.ts`. */
642
669
  quote?: OrderQuote;
643
670
  refundedCents?: number;