@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,67 @@
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 './commerce-orders';
18
+ import { type OrderQuote, type QuoteEmailThread, type QuoteSendRecord } from './commerce-quotes';
19
+ /** The most sends a quote keeps on record; the oldest go first. */
20
+ export declare const QUOTE_MAX_SENDS = 50;
21
+ /** The longest message a send carries. */
22
+ export declare const QUOTE_MAX_SEND_MESSAGE = 5000;
23
+ /** The longest subject a send carries. */
24
+ export declare const QUOTE_MAX_SEND_SUBJECT = 200;
25
+ /** The quote's sends on record, oldest first. */
26
+ export declare function quoteSends(quote: Pick<OrderQuote, 'sends'> | null | undefined): QuoteSendRecord[];
27
+ /** The last time the quote was emailed, or null. */
28
+ export declare function lastQuoteSend(quote: Pick<OrderQuote, 'sends'> | null | undefined): QuoteSendRecord | null;
29
+ /**
30
+ * Whether emailing the quote now gives the customer a new pay link: they were
31
+ * emailed an earlier revision, and the link has not been replaced since.
32
+ */
33
+ export declare function quoteSendReplacesLink(quote: Pick<OrderQuote, 'sends' | 'revision' | 'linkVersion'> | null | undefined): boolean;
34
+ /** What the console's Send button says: `Send quote`, `Send again`, or `Send revision 2`. */
35
+ export declare function quoteSendLabel(quote: Pick<OrderQuote, 'sends' | 'revision'> | null | undefined): string;
36
+ /** Where the quote goes: what was typed in the dialog, else the customer's email. */
37
+ export declare function quoteSendRecipient(order: Pick<HostOrder, 'quote'> | null | undefined, typed: string | null | undefined): string;
38
+ export type QuoteSendRefusal = 'not-a-quote' | 'paid' | 'no-address' | 'bad-address';
39
+ /** Why the quote cannot be emailed to `to` now, if it cannot. */
40
+ export declare function quoteSendRefusal(order: Pick<HostOrder, 'channel' | 'status' | 'quote'> | null | undefined, to: string): QuoteSendRefusal | null;
41
+ export declare function quoteSendRefusalMessage(reason: QuoteSendRefusal): string;
42
+ /**
43
+ * The subject a send defaults to: the conversation's, so it stays one
44
+ * conversation, else `Quote #1042 from Acme Signs`.
45
+ */
46
+ export declare function quoteEmailDefaultSubject(order: Pick<HostOrder, 'number' | 'quote'>, businessName: string, quoteId?: string): string;
47
+ /** What the console sends: where, under what subject, and the business's message. */
48
+ export interface QuoteSendInput {
49
+ to: string;
50
+ subject: string;
51
+ message: string;
52
+ }
53
+ /** A send as the route received it, trimmed, or what to fix. `to` and `subject` may be empty: the defaults fill them. */
54
+ export declare function parseQuoteSendInput(raw: unknown): {
55
+ ok: true;
56
+ input: QuoteSendInput;
57
+ } | {
58
+ ok: false;
59
+ error: string;
60
+ };
61
+ /**
62
+ * The quote once an email went out: the send on record, the conversation
63
+ * moved on, a line on the timeline, and the quote at Sent, unless the
64
+ * customer has already answered this revision (approved it, or asked for
65
+ * changes), which stays what it says.
66
+ */
67
+ export declare function quoteAfterSend(order: HostOrder, send: QuoteSendRecord, thread: QuoteEmailThread): HostOrder;
@@ -0,0 +1,154 @@
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 } from "./commerce-orders.js";
18
+ import { quoteLinkVersion } from "./commerce-quote-pay.js";
19
+ import { latestQuoteResponse } from "./commerce-quote-proofs.js";
20
+ import { formatQuoteNumber, isQuote, isUnpaidQuote } from "./commerce-quotes.js";
21
+ /*==========================================
22
+ * EMAILING A QUOTE TO THE CUSTOMER (AGL-4097).
23
+ *
24
+ * The business sends the quote from the console: one email in the site's
25
+ * look with its message, the proof, the PDF and the pay button, in the
26
+ * customer's existing conversation when there is one. Each send is recorded
27
+ * on the quote (`quote.sends`) with the revision it carried and the pay link
28
+ * version it carried, and the conversation is kept as `quote.emailThread`.
29
+ *
30
+ * THE PAY LINK ON A NEW REVISION. When the customer was emailed an earlier
31
+ * revision, the first email of a new revision gives them a new pay link and
32
+ * retires the old one (slice 2's Replace link), so the link in the earlier
33
+ * email opens a page saying the quote was updated. A resend of the same
34
+ * revision keeps the link. "Was the link already replaced since that email"
35
+ * is read off the record (the link version it carried), which is also what
36
+ * keeps a send that failed after replacing the link from replacing it again
37
+ * when it is retried.
38
+ *
39
+ * Pure: the console's Send button and the server's send action read the
40
+ * same answers.
41
+ *=========================================*/ /** The most sends a quote keeps on record; the oldest go first. */ export const QUOTE_MAX_SENDS = 50;
42
+ /** The longest message a send carries. */ export const QUOTE_MAX_SEND_MESSAGE = 5000;
43
+ /** The longest subject a send carries. */ export const QUOTE_MAX_SEND_SUBJECT = 200;
44
+ const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
45
+ /** The quote's sends on record, oldest first. */ export function quoteSends(quote) {
46
+ return Array.isArray(quote == null ? void 0 : quote.sends) ? quote.sends.filter((send)=>send && typeof send === 'object') : [];
47
+ }
48
+ /** The last time the quote was emailed, or null. */ export function lastQuoteSend(quote) {
49
+ const sends = quoteSends(quote);
50
+ return sends.length ? sends[sends.length - 1] : null;
51
+ }
52
+ /**
53
+ * Whether emailing the quote now gives the customer a new pay link: they were
54
+ * emailed an earlier revision, and the link has not been replaced since.
55
+ */ export function quoteSendReplacesLink(quote) {
56
+ var _ref;
57
+ const last = lastQuoteSend(quote);
58
+ if (!last) return false;
59
+ if (!(Number(last.revision) < Number((_ref = quote == null ? void 0 : quote.revision) != null ? _ref : 1))) return false;
60
+ return Number(last.linkVersion) === quoteLinkVersion(quote);
61
+ }
62
+ /** What the console's Send button says: `Send quote`, `Send again`, or `Send revision 2`. */ export function quoteSendLabel(quote) {
63
+ var _ref;
64
+ const last = lastQuoteSend(quote);
65
+ if (!last) return 'Send quote';
66
+ const revision = Number((_ref = quote == null ? void 0 : quote.revision) != null ? _ref : 1);
67
+ return Number(last.revision) < revision ? `Send revision ${revision}` : 'Send again';
68
+ }
69
+ /** Where the quote goes: what was typed in the dialog, else the customer's email. */ export function quoteSendRecipient(order, typed) {
70
+ var _ref;
71
+ var _order_quote_customer, _order_quote;
72
+ return String(typed != null ? typed : '').trim() || String((_ref = 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 ? _ref : '').trim();
73
+ }
74
+ /** Why the quote cannot be emailed to `to` now, if it cannot. */ export function quoteSendRefusal(order, to) {
75
+ if (!order || !isQuote(order) || !order.quote) return 'not-a-quote';
76
+ if (!isUnpaidQuote(order)) return 'paid';
77
+ if (!to) return 'no-address';
78
+ if (to.length > 320 || !EMAIL.test(to)) return 'bad-address';
79
+ return null;
80
+ }
81
+ export function quoteSendRefusalMessage(reason) {
82
+ switch(reason){
83
+ case 'not-a-quote':
84
+ return 'Unknown quote';
85
+ case 'paid':
86
+ return 'This quote has been paid, so it is an order now and is not sent again.';
87
+ case 'no-address':
88
+ return 'This quote has no email for the customer. Add one to the quote, or type where to send it.';
89
+ case 'bad-address':
90
+ return 'Enter a valid email address to send the quote to.';
91
+ }
92
+ }
93
+ /**
94
+ * The subject a send defaults to: the conversation's, so it stays one
95
+ * conversation, else `Quote #1042 from Acme Signs`.
96
+ */ export function quoteEmailDefaultSubject(order, businessName, quoteId) {
97
+ var _ref;
98
+ var _order_quote_emailThread, _order_quote;
99
+ const thread = String((_ref = (_order_quote = order.quote) == null ? void 0 : (_order_quote_emailThread = _order_quote.emailThread) == null ? void 0 : _order_quote_emailThread.subject) != null ? _ref : '').trim();
100
+ if (thread) return thread;
101
+ const name = String(businessName != null ? businessName : '').trim();
102
+ const number = formatQuoteNumber(order, quoteId);
103
+ return name ? `${number} from ${name}` : number;
104
+ }
105
+ /** A send as the route received it, trimmed, or what to fix. `to` and `subject` may be empty: the defaults fill them. */ export function parseQuoteSendInput(raw) {
106
+ const source = raw && typeof raw === 'object' ? raw : {};
107
+ const text = (value)=>typeof value === 'string' ? value : '';
108
+ const to = text(source['to']).trim();
109
+ // A subject is one header line.
110
+ const subject = text(source['subject']).replace(/[\r\n]+/g, ' ').trim();
111
+ const message = text(source['message']).replace(/\r\n?/g, '\n').trim();
112
+ if (subject.length > QUOTE_MAX_SEND_SUBJECT) {
113
+ return {
114
+ ok: false,
115
+ error: `Keep the subject under ${QUOTE_MAX_SEND_SUBJECT} characters.`
116
+ };
117
+ }
118
+ if (message.length > QUOTE_MAX_SEND_MESSAGE) {
119
+ return {
120
+ ok: false,
121
+ error: `Keep the message under ${QUOTE_MAX_SEND_MESSAGE.toLocaleString('en-US')} characters.`
122
+ };
123
+ }
124
+ return {
125
+ ok: true,
126
+ input: {
127
+ to,
128
+ subject,
129
+ message
130
+ }
131
+ };
132
+ }
133
+ /**
134
+ * The quote once an email went out: the send on record, the conversation
135
+ * moved on, a line on the timeline, and the quote at Sent, unless the
136
+ * customer has already answered this revision (approved it, or asked for
137
+ * changes), which stays what it says.
138
+ */ export function quoteAfterSend(order, send, thread) {
139
+ const quote = order.quote;
140
+ const answered = latestQuoteResponse(quote) !== null;
141
+ return _extends({}, order, {
142
+ timeline: appendOrderEvent(order, 'quote-sent', `Revision ${send.revision} emailed to ${send.to} by ${send.by}`, send.atMs),
143
+ quote: _extends({}, quote, {
144
+ stage: answered ? quote.stage : 'sent',
145
+ sends: [
146
+ ...quoteSends(quote),
147
+ send
148
+ ].slice(-QUOTE_MAX_SENDS),
149
+ emailThread: thread
150
+ })
151
+ });
152
+ }
153
+
154
+ //# sourceMappingURL=commerce-quote-email.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/commerce-quote-email.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 { appendOrderEvent, type HostOrder } from './commerce-orders'\nimport { quoteLinkVersion } from './commerce-quote-pay'\nimport { latestQuoteResponse } from './commerce-quote-proofs'\nimport {\n formatQuoteNumber,\n isQuote,\n isUnpaidQuote,\n type OrderQuote,\n type QuoteEmailThread,\n type QuoteSendRecord,\n} from './commerce-quotes'\n\n/*==========================================\n * EMAILING A QUOTE TO THE CUSTOMER (AGL-4097).\n *\n * The business sends the quote from the console: one email in the site's\n * look with its message, the proof, the PDF and the pay button, in the\n * customer's existing conversation when there is one. Each send is recorded\n * on the quote (`quote.sends`) with the revision it carried and the pay link\n * version it carried, and the conversation is kept as `quote.emailThread`.\n *\n * THE PAY LINK ON A NEW REVISION. When the customer was emailed an earlier\n * revision, the first email of a new revision gives them a new pay link and\n * retires the old one (slice 2's Replace link), so the link in the earlier\n * email opens a page saying the quote was updated. A resend of the same\n * revision keeps the link. \"Was the link already replaced since that email\"\n * is read off the record (the link version it carried), which is also what\n * keeps a send that failed after replacing the link from replacing it again\n * when it is retried.\n *\n * Pure: the console's Send button and the server's send action read the\n * same answers.\n *=========================================*/\n\n/** The most sends a quote keeps on record; the oldest go first. */\nexport const QUOTE_MAX_SENDS = 50\n\n/** The longest message a send carries. */\nexport const QUOTE_MAX_SEND_MESSAGE = 5_000\n\n/** The longest subject a send carries. */\nexport const QUOTE_MAX_SEND_SUBJECT = 200\n\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n/** The quote's sends on record, oldest first. */\nexport function quoteSends(quote: Pick<OrderQuote, 'sends'> | null | undefined): QuoteSendRecord[] {\n return Array.isArray(quote?.sends) ? quote.sends.filter((send) => send && typeof send === 'object') : []\n}\n\n/** The last time the quote was emailed, or null. */\nexport function lastQuoteSend(quote: Pick<OrderQuote, 'sends'> | null | undefined): QuoteSendRecord | null {\n const sends = quoteSends(quote)\n return sends.length ? sends[sends.length - 1] : null\n}\n\n/**\n * Whether emailing the quote now gives the customer a new pay link: they were\n * emailed an earlier revision, and the link has not been replaced since.\n */\nexport function quoteSendReplacesLink(\n quote: Pick<OrderQuote, 'sends' | 'revision' | 'linkVersion'> | null | undefined,\n): boolean {\n const last = lastQuoteSend(quote)\n if (!last) return false\n if (!(Number(last.revision) < Number(quote?.revision ?? 1))) return false\n return Number(last.linkVersion) === quoteLinkVersion(quote)\n}\n\n/** What the console's Send button says: `Send quote`, `Send again`, or `Send revision 2`. */\nexport function quoteSendLabel(quote: Pick<OrderQuote, 'sends' | 'revision'> | null | undefined): string {\n const last = lastQuoteSend(quote)\n if (!last) return 'Send quote'\n const revision = Number(quote?.revision ?? 1)\n return Number(last.revision) < revision ? `Send revision ${revision}` : 'Send again'\n}\n\n/** Where the quote goes: what was typed in the dialog, else the customer's email. */\nexport function quoteSendRecipient(\n order: Pick<HostOrder, 'quote'> | null | undefined,\n typed: string | null | undefined,\n): string {\n return String(typed ?? '').trim() || String(order?.quote?.customer?.email ?? '').trim()\n}\n\nexport type QuoteSendRefusal = 'not-a-quote' | 'paid' | 'no-address' | 'bad-address'\n\n/** Why the quote cannot be emailed to `to` now, if it cannot. */\nexport function quoteSendRefusal(\n order: Pick<HostOrder, 'channel' | 'status' | 'quote'> | null | undefined,\n to: string,\n): QuoteSendRefusal | null {\n if (!order || !isQuote(order) || !order.quote) return 'not-a-quote'\n if (!isUnpaidQuote(order)) return 'paid'\n if (!to) return 'no-address'\n if (to.length > 320 || !EMAIL.test(to)) return 'bad-address'\n return null\n}\n\nexport function quoteSendRefusalMessage(reason: QuoteSendRefusal): string {\n switch (reason) {\n case 'not-a-quote':\n return 'Unknown quote'\n case 'paid':\n return 'This quote has been paid, so it is an order now and is not sent again.'\n case 'no-address':\n return 'This quote has no email for the customer. Add one to the quote, or type where to send it.'\n case 'bad-address':\n return 'Enter a valid email address to send the quote to.'\n }\n}\n\n/**\n * The subject a send defaults to: the conversation's, so it stays one\n * conversation, else `Quote #1042 from Acme Signs`.\n */\nexport function quoteEmailDefaultSubject(\n order: Pick<HostOrder, 'number' | 'quote'>,\n businessName: string,\n quoteId?: string,\n): string {\n const thread = String(order.quote?.emailThread?.subject ?? '').trim()\n if (thread) return thread\n const name = String(businessName ?? '').trim()\n const number = formatQuoteNumber(order, quoteId)\n return name ? `${number} from ${name}` : number\n}\n\n/** What the console sends: where, under what subject, and the business's message. */\nexport interface QuoteSendInput {\n to: string\n subject: string\n message: string\n}\n\n/** A send as the route received it, trimmed, or what to fix. `to` and `subject` may be empty: the defaults fill them. */\nexport function parseQuoteSendInput(raw: unknown): { ok: true; input: QuoteSendInput } | { ok: false; error: string } {\n const source = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>\n const text = (value: unknown) => (typeof value === 'string' ? value : '')\n const to = text(source['to']).trim()\n // A subject is one header line.\n const subject = text(source['subject']).replace(/[\\r\\n]+/g, ' ').trim()\n const message = text(source['message']).replace(/\\r\\n?/g, '\\n').trim()\n if (subject.length > QUOTE_MAX_SEND_SUBJECT) {\n return { ok: false, error: `Keep the subject under ${QUOTE_MAX_SEND_SUBJECT} characters.` }\n }\n if (message.length > QUOTE_MAX_SEND_MESSAGE) {\n return { ok: false, error: `Keep the message under ${QUOTE_MAX_SEND_MESSAGE.toLocaleString('en-US')} characters.` }\n }\n return { ok: true, input: { to, subject, message } }\n}\n\n/**\n * The quote once an email went out: the send on record, the conversation\n * moved on, a line on the timeline, and the quote at Sent, unless the\n * customer has already answered this revision (approved it, or asked for\n * changes), which stays what it says.\n */\nexport function quoteAfterSend(\n order: HostOrder,\n send: QuoteSendRecord,\n thread: QuoteEmailThread,\n): HostOrder {\n const quote = order.quote as OrderQuote\n const answered = latestQuoteResponse(quote) !== null\n return {\n ...order,\n timeline: appendOrderEvent(\n order,\n 'quote-sent',\n `Revision ${send.revision} emailed to ${send.to} by ${send.by}`,\n send.atMs,\n ),\n quote: {\n ...quote,\n stage: answered ? quote.stage : 'sent',\n sends: [...quoteSends(quote), send].slice(-QUOTE_MAX_SENDS),\n emailThread: thread,\n },\n }\n}\n"],"names":["appendOrderEvent","quoteLinkVersion","latestQuoteResponse","formatQuoteNumber","isQuote","isUnpaidQuote","QUOTE_MAX_SENDS","QUOTE_MAX_SEND_MESSAGE","QUOTE_MAX_SEND_SUBJECT","EMAIL","quoteSends","quote","Array","isArray","sends","filter","send","lastQuoteSend","length","quoteSendReplacesLink","last","Number","revision","linkVersion","quoteSendLabel","quoteSendRecipient","order","typed","String","trim","customer","email","quoteSendRefusal","to","test","quoteSendRefusalMessage","reason","quoteEmailDefaultSubject","businessName","quoteId","thread","emailThread","subject","name","number","parseQuoteSendInput","raw","source","text","value","replace","message","ok","error","toLocaleString","input","quoteAfterSend","answered","timeline","by","atMs","stage","slice"],"mappings":"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"}
@@ -15,14 +15,23 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  import type { ProductType } from './commerce';
18
- import { type HostOrder, type OrderAddress } from './commerce-orders';
19
- import { type OrderQuote, type QuoteUnmatchedPayment } from './commerce-quotes';
18
+ import { type HostOrder, type OrderAddress, type OrderLineItem, type OrderStatus } from './commerce-orders';
19
+ import { type OrderQuote, type QuoteDiscount, type QuoteTaxBasis, type QuoteUnmatchedPayment } from './commerce-quotes';
20
20
  import type { StorefrontTaxMode } from './commerce-tax-decision';
21
21
  import { type OrderListFields } from './order-list-fields';
22
22
  /** Stripe's smallest card charge; a quote that totals less cannot be paid by card. */
23
23
  export declare const QUOTE_MIN_CHARGE_CENTS = 50;
24
24
  /** The pay link version a quote is on; absent reads as the first. */
25
25
  export declare function quoteLinkVersion(quote: Pick<OrderQuote, 'linkVersion'> | null | undefined): number;
26
+ /**
27
+ * The pay link version any order is on (AGL-4099). A quote keeps its version
28
+ * where slice 2 put it, `quote.linkVersion`, so every link already sent keeps
29
+ * verifying; any other order keeps it as `payLink.version`. Absent reads as
30
+ * the first either way.
31
+ */
32
+ export declare function orderPayLinkVersion(order: Partial<Pick<HostOrder, 'quote' | 'payLink'>> | null | undefined): number;
33
+ /** Payments the order's pay link could not take: a quote's on the quote, any other order's on `payLink`. */
34
+ export declare function orderPayLinkUnmatched(order: Partial<Pick<HostOrder, 'quote' | 'payLink'>> | null | undefined): QuoteUnmatchedPayment[];
26
35
  /** An address as the quote page prints it: no phone. */
27
36
  export interface QuotePageAddress {
28
37
  name: string | null;
@@ -75,8 +84,14 @@ export interface QuotePageResponse {
75
84
  */
76
85
  export interface QuotePageView {
77
86
  state: 'quote';
87
+ /**
88
+ * What the page shows (AGL-4099): a quote, or an invoice for an order the
89
+ * business sent one for. An invoice has no proofs and no approval, and the
90
+ * page says "invoice" where a quote's says "quote".
91
+ */
92
+ kind: 'quote' | 'invoice';
78
93
  storeName: string;
79
- /** `Quote #1042`. */
94
+ /** `Quote #1042`, or `Invoice #1042`. */
80
95
  number: string;
81
96
  /** When the quote was last written, which is the date it carries. */
82
97
  dateMs: number | null;
@@ -104,6 +119,12 @@ export interface QuotePageView {
104
119
  taxLabel: string;
105
120
  /** Stripe Tax works the tax out when the customer pays; `taxCents` is 0 until then. */
106
121
  taxAtPayment: boolean;
122
+ /**
123
+ * The customer picks a shipping rate when they pay (an invoiced draft
124
+ * order, AGL-4099); `shippingCents` is 0 until then. A quote's shipping is
125
+ * priced on it, so this is false for every quote.
126
+ */
127
+ shippingAtPayment: boolean;
107
128
  notes: string | null;
108
129
  terms: string | null;
109
130
  status: QuotePageStatus;
@@ -129,6 +150,13 @@ export interface QuotePageReplacedView {
129
150
  state: 'replaced';
130
151
  storeName: string;
131
152
  }
153
+ /** An address as the pay link's page prints it, or null when it says nothing. */
154
+ export declare function quotePageAddress(address: OrderAddress | null | undefined): QuotePageAddress | null;
155
+ /** Where an order stands as the pay link's page says it. */
156
+ export declare function quotePageStatus(status: OrderStatus | string | undefined): {
157
+ status: QuotePageStatus;
158
+ label: string;
159
+ };
132
160
  /**
133
161
  * The quote page's view of an order document on the `quote` channel. Totals
134
162
  * are the stored lines' through {@link computeQuoteTotals} until the quote is
@@ -142,6 +170,8 @@ export declare function buildQuotePageView(input: {
142
170
  /** The order's status page, for a paid quote. */
143
171
  receiptUrl?: string | null;
144
172
  }): QuotePageView;
173
+ /** Order lines as the pay link's page lists them: what each charges, nothing about stock or cost. */
174
+ export declare function quotePageLines(lines: readonly OrderLineItem[]): QuotePageLine[];
145
175
  /** One line of a quote's Checkout Session. */
146
176
  export interface QuoteCheckoutLine {
147
177
  name: string;
@@ -187,6 +217,19 @@ export interface QuoteCheckoutCharge {
187
217
  * line it amounts to. Included lines charge nothing and are not sent.
188
218
  */
189
219
  export declare function quoteCheckoutCharge(order: Pick<HostOrder, 'number' | 'lineItems' | 'quote'>, orderId: string): QuoteCheckoutCharge;
220
+ /**
221
+ * What a pay link's Checkout Session charges for stored lines, a discount,
222
+ * fixed shipping and a tax basis, under a label (`Quote #1042`,
223
+ * `Invoice #1042`): {@link quoteCheckoutCharge}'s pricing, shared with an
224
+ * invoiced order's (AGL-4099), so both are charged the same way.
225
+ */
226
+ export declare function payLinkCheckoutCharge(input: {
227
+ lineItems: readonly OrderLineItem[];
228
+ tax: QuoteTaxBasis;
229
+ discount: QuoteDiscount | null;
230
+ shippingCents: number;
231
+ label: string;
232
+ }): QuoteCheckoutCharge;
190
233
  export type QuotePayRefusal = 'paid' | 'closed' | 'approval-required' | 'tax-undecided' | 'nothing-to-pay';
191
234
  /** Why a quote cannot be paid now, or null when it can. */
192
235
  export declare function quotePayRefusal(order: Pick<HostOrder, 'number' | 'status' | 'lineItems' | 'quote'>, orderId: string): QuotePayRefusal | null;
@@ -229,11 +272,11 @@ export interface QuotePaymentFacts {
229
272
  * the business is told, so a payment is never fulfilled twice and never lost.
230
273
  */
231
274
  export type QuotePaymentDisposition = 'pay' | 'superseded' | 'already-paid' | 'redelivery';
232
- export declare function quotePaymentDisposition(order: Pick<HostOrder, 'status' | 'quote' | 'checkoutSessionId' | 'paymentIntentId'>, payment: Pick<QuotePaymentFacts, 'sessionId' | 'paymentIntentId' | 'linkVersion' | 'quoteUpdatedAtMs'>): QuotePaymentDisposition;
275
+ export declare function quotePaymentDisposition(order: Pick<HostOrder, 'status' | 'quote' | 'checkoutSessionId' | 'paymentIntentId'> & Partial<Pick<HostOrder, 'payLink'>>, payment: Pick<QuotePaymentFacts, 'sessionId' | 'paymentIntentId' | 'linkVersion' | 'quoteUpdatedAtMs'>): QuotePaymentDisposition;
233
276
  /** A payment the quote could not take, as {@link QuoteUnmatchedPayment} records it. */
234
277
  export declare function quoteUnmatchedPayment(facts: Pick<QuotePaymentFacts, 'sessionId' | 'paymentIntentId' | 'amountTotalCents' | 'paidAtMs'>, reason: QuoteUnmatchedPayment['reason']): QuoteUnmatchedPayment;
235
- /** The timeline line for a payment the quote could not take. */
236
- export declare function quoteUnmatchedPaymentDetail(entry: QuoteUnmatchedPayment): string;
278
+ /** The timeline line for a payment the quote (or an order's invoice, AGL-4099) could not take. */
279
+ export declare function quoteUnmatchedPaymentDetail(entry: QuoteUnmatchedPayment, noun?: 'quote' | 'invoice'): string;
237
280
  /**
238
281
  * The order a paid quote becomes, the whole document as it will stand. A
239
282
  * quote carries none of the fields order lists read (`commerce-quotes.ts`);
@@ -44,6 +44,22 @@ import { withOrderListFields } from "./order-list-fields.js";
44
44
  const version = Math.floor(Number(quote == null ? void 0 : quote.linkVersion));
45
45
  return Number.isFinite(version) && version >= 1 ? version : 1;
46
46
  }
47
+ /**
48
+ * The pay link version any order is on (AGL-4099). A quote keeps its version
49
+ * where slice 2 put it, `quote.linkVersion`, so every link already sent keeps
50
+ * verifying; any other order keeps it as `payLink.version`. Absent reads as
51
+ * the first either way.
52
+ */ export function orderPayLinkVersion(order) {
53
+ var _order_payLink;
54
+ if (order == null ? void 0 : order.quote) return quoteLinkVersion(order.quote);
55
+ const version = Math.floor(Number(order == null ? void 0 : (_order_payLink = order.payLink) == null ? void 0 : _order_payLink.version));
56
+ return Number.isFinite(version) && version >= 1 ? version : 1;
57
+ }
58
+ /** Payments the order's pay link could not take: a quote's on the quote, any other order's on `payLink`. */ export function orderPayLinkUnmatched(order) {
59
+ var _order_payLink;
60
+ const list = (order == null ? void 0 : order.quote) ? order.quote.unmatchedPayments : order == null ? void 0 : (_order_payLink = order.payLink) == null ? void 0 : _order_payLink.unmatchedPayments;
61
+ return Array.isArray(list) ? list.filter((entry)=>entry && typeof entry === 'object') : [];
62
+ }
47
63
  const text = (value)=>{
48
64
  const trimmed = typeof value === 'string' ? value.trim() : '';
49
65
  return trimmed ? trimmed : null;
@@ -52,7 +68,7 @@ const cents = (value)=>{
52
68
  const number = Math.round(Number(value));
53
69
  return Number.isFinite(number) ? number : 0;
54
70
  };
55
- function pageAddress(address) {
71
+ /** An address as the pay link's page prints it, or null when it says nothing. */ export function quotePageAddress(address) {
56
72
  if (!address) return null;
57
73
  const view = {
58
74
  name: text(address.name),
@@ -65,7 +81,7 @@ function pageAddress(address) {
65
81
  };
66
82
  return Object.values(view).some(Boolean) ? view : null;
67
83
  }
68
- function pageStatus(status) {
84
+ /** Where an order stands as the pay link's page says it. */ export function quotePageStatus(status) {
69
85
  if (status === 'pending') return {
70
86
  status: 'unpaid',
71
87
  label: 'Unpaid'
@@ -106,7 +122,7 @@ function discountLabel(discount) {
106
122
  const { order } = input;
107
123
  const quote = order.quote;
108
124
  const lines = (_order_lineItems = order.lineItems) != null ? _order_lineItems : [];
109
- const status = pageStatus(order.status);
125
+ const status = quotePageStatus(order.status);
110
126
  const unpaid = status.status === 'unpaid';
111
127
  const charge = quoteCheckoutCharge(order, input.orderId);
112
128
  const stored = order.totals;
@@ -127,6 +143,7 @@ function discountLabel(discount) {
127
143
  const taxAtPayment = unpaid && (quote == null ? void 0 : (_quote_tax = quote.tax) == null ? void 0 : _quote_tax.kind) === 'at-payment';
128
144
  return {
129
145
  state: 'quote',
146
+ kind: 'quote',
130
147
  storeName: input.storeName,
131
148
  number: formatQuoteNumber(order, input.orderId),
132
149
  dateMs: Number((_ref = quote == null ? void 0 : quote.updatedAtMs) != null ? _ref : quote == null ? void 0 : quote.createdAtMs) || null,
@@ -134,29 +151,15 @@ function discountLabel(discount) {
134
151
  currency: input.currency || 'USD',
135
152
  customerName: (_text = text(quote == null ? void 0 : (_quote_customer = quote.customer) == null ? void 0 : _quote_customer.name)) != null ? _text : text(quote == null ? void 0 : (_quote_customer1 = quote.customer) == null ? void 0 : _quote_customer1.company),
136
153
  company: text(quote == null ? void 0 : (_quote_customer2 = quote.customer) == null ? void 0 : _quote_customer2.name) ? text(quote == null ? void 0 : (_quote_customer3 = quote.customer) == null ? void 0 : _quote_customer3.company) : null,
137
- billTo: pageAddress(quote == null ? void 0 : quote.billTo),
138
- shipTo: pageAddress(quote == null ? void 0 : quote.shipTo),
139
- lines: lines.map((line)=>{
140
- var _line_name;
141
- const included = line.included === true;
142
- const quantity = Math.max(0, cents(line.quantity));
143
- const unitAmountCents = included ? 0 : Math.max(0, cents(line.unitAmountCents));
144
- return {
145
- name: String((_line_name = line.name) != null ? _line_name : ''),
146
- variantLabel: text(line.variantLabel),
147
- description: text(line.description),
148
- sku: text(line.sku),
149
- quantity,
150
- unitAmountCents,
151
- amountCents: unitAmountCents * quantity,
152
- included
153
- };
154
- }),
154
+ billTo: quotePageAddress(quote == null ? void 0 : quote.billTo),
155
+ shipTo: quotePageAddress(quote == null ? void 0 : quote.shipTo),
156
+ lines: quotePageLines(lines),
155
157
  totals,
156
158
  discountLabel: discountLabel(quote == null ? void 0 : quote.discount),
157
159
  // Once paid, the tax is what was charged: Stripe's figure under Stripe Tax.
158
160
  taxLabel: unpaid || (quote == null ? void 0 : (_quote_tax1 = quote.tax) == null ? void 0 : _quote_tax1.kind) !== 'at-payment' ? describeQuoteTax(quote == null ? void 0 : quote.tax) : 'Tax',
159
161
  taxAtPayment,
162
+ shippingAtPayment: false,
160
163
  notes: text(quote == null ? void 0 : quote.notes),
161
164
  terms: text(quote == null ? void 0 : quote.terms),
162
165
  status: status.status,
@@ -185,6 +188,24 @@ function discountLabel(discount) {
185
188
  }) === null
186
189
  };
187
190
  }
191
+ /** Order lines as the pay link's page lists them: what each charges, nothing about stock or cost. */ export function quotePageLines(lines) {
192
+ return lines.map((line)=>{
193
+ var _line_name;
194
+ const included = line.included === true;
195
+ const quantity = Math.max(0, cents(line.quantity));
196
+ const unitAmountCents = included ? 0 : Math.max(0, cents(line.unitAmountCents));
197
+ return {
198
+ name: String((_line_name = line.name) != null ? _line_name : ''),
199
+ variantLabel: text(line.variantLabel),
200
+ description: text(line.description),
201
+ sku: text(line.sku),
202
+ quantity,
203
+ unitAmountCents,
204
+ amountCents: unitAmountCents * quantity,
205
+ included
206
+ };
207
+ });
208
+ }
188
209
  /** The latest answer, as the page may show it back to the link's holder: never the address or browser. */ function pageResponse(quote) {
189
210
  var _ref;
190
211
  var _latest_files;
@@ -230,13 +251,24 @@ function lineSummary(lines) {
230
251
  */ export function quoteCheckoutCharge(order, orderId) {
231
252
  var _order_lineItems, _ref, _ref1;
232
253
  var _order_quote, _order_quote1, _order_quote_shipping, _order_quote2;
233
- const lineItems = (_order_lineItems = order.lineItems) != null ? _order_lineItems : [];
234
- const tax = (_ref = (_order_quote = order.quote) == null ? void 0 : _order_quote.tax) != null ? _ref : {
235
- kind: 'none',
236
- reason: 'not-collected'
237
- };
238
- const discount = (_ref1 = (_order_quote1 = order.quote) == null ? void 0 : _order_quote1.discount) != null ? _ref1 : null;
239
- const shippingCents = Math.max(0, cents((_order_quote2 = order.quote) == null ? void 0 : (_order_quote_shipping = _order_quote2.shipping) == null ? void 0 : _order_quote_shipping.amountCents));
254
+ return payLinkCheckoutCharge({
255
+ lineItems: (_order_lineItems = order.lineItems) != null ? _order_lineItems : [],
256
+ tax: (_ref = (_order_quote = order.quote) == null ? void 0 : _order_quote.tax) != null ? _ref : {
257
+ kind: 'none',
258
+ reason: 'not-collected'
259
+ },
260
+ discount: (_ref1 = (_order_quote1 = order.quote) == null ? void 0 : _order_quote1.discount) != null ? _ref1 : null,
261
+ shippingCents: Math.max(0, cents((_order_quote2 = order.quote) == null ? void 0 : (_order_quote_shipping = _order_quote2.shipping) == null ? void 0 : _order_quote_shipping.amountCents)),
262
+ label: formatQuoteNumber(order, orderId)
263
+ });
264
+ }
265
+ /**
266
+ * What a pay link's Checkout Session charges for stored lines, a discount,
267
+ * fixed shipping and a tax basis, under a label (`Quote #1042`,
268
+ * `Invoice #1042`): {@link quoteCheckoutCharge}'s pricing, shared with an
269
+ * invoiced order's (AGL-4099), so both are charged the same way.
270
+ */ export function payLinkCheckoutCharge(input) {
271
+ const { lineItems, tax, discount, shippingCents, label } = input;
240
272
  const totals = computeQuoteTotals({
241
273
  lines: lineItems,
242
274
  discount,
@@ -244,7 +276,6 @@ function lineSummary(lines) {
244
276
  tax
245
277
  });
246
278
  const amounts = quoteLineAmounts(lineItems, discount);
247
- const label = formatQuoteNumber(order, orderId);
248
279
  const groups = [
249
280
  true,
250
281
  false
@@ -323,20 +354,22 @@ function lineSummary(lines) {
323
354
  }
324
355
  }
325
356
  export function quotePaymentDisposition(order, payment) {
326
- var _ref;
327
- var _order_quote, _order_quote1;
328
- const recorded = ((_ref = (_order_quote = order.quote) == null ? void 0 : _order_quote.unmatchedPayments) != null ? _ref : []).some((entry)=>(entry == null ? void 0 : entry.sessionId) === payment.sessionId);
357
+ var _order_quote;
358
+ // Any order's pay link answers the same way (AGL-4099): an invoiced order
359
+ // reads its version and its recorded payments off `payLink`, and its
360
+ // session carries no quote edit to compare.
361
+ const recorded = orderPayLinkUnmatched(order).some((entry)=>(entry == null ? void 0 : entry.sessionId) === payment.sessionId);
329
362
  if (recorded) return 'redelivery';
330
363
  if (order.status !== 'pending') {
331
364
  const same = order.checkoutSessionId && order.checkoutSessionId === payment.sessionId || payment.paymentIntentId && order.paymentIntentId === payment.paymentIntentId;
332
365
  return same ? 'redelivery' : 'already-paid';
333
366
  }
334
367
  const sentVersion = payment.linkVersion;
335
- if (sentVersion != null && sentVersion !== '' && Number(sentVersion) !== quoteLinkVersion(order.quote)) {
368
+ if (sentVersion != null && sentVersion !== '' && Number(sentVersion) !== orderPayLinkVersion(order)) {
336
369
  return 'superseded';
337
370
  }
338
371
  const sentEdit = payment.quoteUpdatedAtMs;
339
- if (sentEdit != null && sentEdit !== '' && Number(sentEdit) !== Number((_order_quote1 = order.quote) == null ? void 0 : _order_quote1.updatedAtMs)) {
372
+ if (sentEdit != null && sentEdit !== '' && Number(sentEdit) !== Number((_order_quote = order.quote) == null ? void 0 : _order_quote.updatedAtMs)) {
340
373
  return 'superseded';
341
374
  }
342
375
  return 'pay';
@@ -350,9 +383,9 @@ export function quotePaymentDisposition(order, payment) {
350
383
  reason
351
384
  };
352
385
  }
353
- /** The timeline line for a payment the quote could not take. */ export function quoteUnmatchedPaymentDetail(entry) {
386
+ /** The timeline line for a payment the quote (or an order's invoice, AGL-4099) could not take. */ export function quoteUnmatchedPaymentDetail(entry, noun = 'quote') {
354
387
  const amount = `$${(entry.amountCents / 100).toFixed(2)}`;
355
- return entry.reason === 'superseded' ? `A payment of ${amount} came through a pay link that was replaced, or for an earlier ` + 'version of this quote. The money is in Stripe and this quote records no sale for it; ' + 'refund it in Stripe, or reconcile it by hand.' : `A second payment of ${amount} came in after this quote was paid. The money is in Stripe ` + 'and no second sale was recorded; refund it in Stripe, or reconcile it by hand.';
388
+ return entry.reason === 'superseded' ? `A payment of ${amount} came through a pay link that was replaced, or for an earlier ` + `version of this ${noun}. The money is in Stripe and this ${noun} records no sale for it; ` + 'refund it in Stripe, or reconcile it by hand.' : `A second payment of ${amount} came in after this ${noun} was paid. The money is in Stripe ` + 'and no second sale was recorded; refund it in Stripe, or reconcile it by hand.';
356
389
  }
357
390
  /**
358
391
  * The order a paid quote becomes, the whole document as it will stand. A