@aglyn/plugins-commerce 1.0.0-beta.290 → 1.0.0-beta.291
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/package.json +13 -13
- package/src/lib/components/commerce-console-page.js +2 -1
- package/src/lib/components/commerce-console-page.js.map +1 -1
- package/src/lib/components/console/host-orders-card.component.d.ts +6 -0
- package/src/lib/components/console/host-orders-card.component.js +3 -2
- package/src/lib/components/console/host-orders-card.component.js.map +1 -1
- package/src/lib/components/console/invoice-send.component.d.ts +44 -0
- package/src/lib/components/console/invoice-send.component.js +237 -0
- package/src/lib/components/console/invoice-send.component.js.map +1 -0
- package/src/lib/components/console/order-detail-dialog.component.d.ts +5 -0
- package/src/lib/components/console/order-detail-dialog.component.js +9 -2
- package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
- package/src/lib/components/console/order-invoice.component.d.ts +9 -0
- package/src/lib/components/console/order-invoice.component.js +27 -2
- package/src/lib/components/console/order-invoice.component.js.map +1 -1
- package/src/lib/components/console/quote-editor-dialog.component.d.ts +29 -0
- package/src/lib/components/console/quote-editor-dialog.component.js +102 -4
- package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/quote-proofs.component.d.ts +11 -0
- package/src/lib/components/console/quote-proofs.component.js +68 -0
- package/src/lib/components/console/quote-proofs.component.js.map +1 -1
- package/src/lib/components/console/quote-send.component.d.ts +42 -0
- package/src/lib/components/console/quote-send.component.js +307 -0
- package/src/lib/components/console/quote-send.component.js.map +1 -0
- package/src/lib/components/console/quote-start.component.d.ts +45 -0
- package/src/lib/components/console/quote-start.component.js +182 -0
- package/src/lib/components/console/quote-start.component.js.map +1 -0
- package/src/lib/components/product-options-field-body.d.ts +44 -0
- package/src/lib/components/product-options-field-body.js +260 -0
- package/src/lib/components/product-options-field-body.js.map +1 -0
- package/src/lib/components/product-options-field.d.ts +32 -0
- package/src/lib/components/product-options-field.js +128 -0
- package/src/lib/components/product-options-field.js.map +1 -0
- package/src/lib/components/quote-page-body.js +39 -20
- package/src/lib/components/quote-page-body.js.map +1 -1
- package/src/lib/model/commerce-invoices.d.ts +132 -0
- package/src/lib/model/commerce-invoices.js +332 -0
- package/src/lib/model/commerce-invoices.js.map +1 -0
- package/src/lib/model/commerce-orders.d.ts +29 -2
- package/src/lib/model/commerce-orders.js.map +1 -1
- package/src/lib/model/commerce-product-field.d.ts +23 -0
- package/src/lib/model/commerce-product-field.js +26 -0
- package/src/lib/model/commerce-product-field.js.map +1 -0
- package/src/lib/model/commerce-quote-email.d.ts +67 -0
- package/src/lib/model/commerce-quote-email.js +154 -0
- package/src/lib/model/commerce-quote-email.js.map +1 -0
- package/src/lib/model/commerce-quote-pay.d.ts +49 -6
- package/src/lib/model/commerce-quote-pay.js +69 -36
- package/src/lib/model/commerce-quote-pay.js.map +1 -1
- package/src/lib/model/commerce-quote-request.d.ts +93 -0
- package/src/lib/model/commerce-quote-request.js +207 -0
- package/src/lib/model/commerce-quote-request.js.map +1 -0
- package/src/lib/model/commerce-quotes.d.ts +81 -0
- package/src/lib/model/commerce-quotes.js.map +1 -1
- package/src/lib/model/index.d.ts +3 -0
- package/src/lib/model/index.js +3 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/plugin.js +19 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/billing-webhook.js +115 -70
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/commerce-pdf.d.ts +5 -1
- package/src/lib/server/commerce-pdf.js +37 -10
- package/src/lib/server/commerce-pdf.js.map +1 -1
- package/src/lib/server/draft-order.js +23 -3
- package/src/lib/server/draft-order.js.map +1 -1
- package/src/lib/server/invoice-email.d.ts +76 -0
- package/src/lib/server/invoice-email.js +183 -0
- package/src/lib/server/invoice-email.js.map +1 -0
- package/src/lib/server/order-invoice-send.d.ts +43 -0
- package/src/lib/server/order-invoice-send.js +218 -0
- package/src/lib/server/order-invoice-send.js.map +1 -0
- package/src/lib/server/order-status.d.ts +11 -0
- package/src/lib/server/order-status.js +44 -8
- package/src/lib/server/order-status.js.map +1 -1
- package/src/lib/server/person-eraser.d.ts +3 -2
- package/src/lib/server/person-eraser.js +25 -6
- package/src/lib/server/person-eraser.js.map +1 -1
- package/src/lib/server/quote-email.d.ts +85 -0
- package/src/lib/server/quote-email.js +211 -0
- package/src/lib/server/quote-email.js.map +1 -0
- package/src/lib/server/quote-link-token.d.ts +11 -0
- package/src/lib/server/quote-link-token.js +21 -0
- package/src/lib/server/quote-link-token.js.map +1 -1
- package/src/lib/server/quote-pay.d.ts +8 -3
- package/src/lib/server/quote-pay.js +105 -43
- package/src/lib/server/quote-pay.js.map +1 -1
- package/src/lib/server/quote.d.ts +11 -0
- package/src/lib/server/quote.js +309 -12
- package/src/lib/server/quote.js.map +1 -1
- package/src/lib/server/site-page-resolver.js +2 -1
- package/src/lib/server/site-page-resolver.js.map +1 -1
- package/src/lib/server.js +5 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/site.js +8 -0
- package/src/lib/site.js.map +1 -1
- package/src/lib/tenant-emails.js +243 -0
- package/src/lib/tenant-emails.js.map +1 -1
- package/src/lib/utils/commerce-document-pdf.d.ts +5 -0
- package/src/lib/utils/commerce-document-pdf.js +6 -1
- package/src/lib/utils/commerce-document-pdf.js.map +1 -1
- package/src/lib/utils/invoice-email-content.d.ts +45 -0
- package/src/lib/utils/invoice-email-content.js +126 -0
- package/src/lib/utils/invoice-email-content.js.map +1 -0
- package/src/lib/utils/order-invoice.d.ts +1 -1
- package/src/lib/utils/order-invoice.js +14 -6
- package/src/lib/utils/order-invoice.js.map +1 -1
- package/src/lib/utils/quote-email-content.d.ts +62 -0
- package/src/lib/utils/quote-email-content.js +154 -0
- package/src/lib/utils/quote-email-content.js.map +1 -0
|
@@ -0,0 +1,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'
|
|
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
|
|
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
|
|
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 =
|
|
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:
|
|
138
|
-
shipTo:
|
|
139
|
-
lines: lines
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
|
327
|
-
|
|
328
|
-
|
|
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) !==
|
|
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((
|
|
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 ` +
|
|
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
|