@aglyn/plugins-commerce 1.0.0-beta.289 → 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/commerce-pdf-download.d.ts +30 -0
- package/src/lib/components/console/commerce-pdf-download.js +53 -0
- package/src/lib/components/console/commerce-pdf-download.js.map +1 -0
- 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/host-quotes-card.component.js +20 -4
- package/src/lib/components/console/host-quotes-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 +15 -4
- package/src/lib/components/console/order-invoice.component.js +93 -12
- 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 +154 -5
- package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/quote-proofs.component.d.ts +48 -0
- package/src/lib/components/console/quote-proofs.component.js +694 -0
- package/src/lib/components/console/quote-proofs.component.js.map +1 -0
- 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 +67 -20
- package/src/lib/components/quote-page-body.js.map +1 -1
- package/src/lib/components/quote-proof-panel.d.ts +42 -0
- package/src/lib/components/quote-proof-panel.js +436 -0
- package/src/lib/components/quote-proof-panel.js.map +1 -0
- 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 +76 -6
- package/src/lib/model/commerce-quote-pay.js +105 -39
- package/src/lib/model/commerce-quote-pay.js.map +1 -1
- package/src/lib/model/commerce-quote-proofs.d.ts +159 -0
- package/src/lib/model/commerce-quote-proofs.js +348 -0
- package/src/lib/model/commerce-quote-proofs.js.map +1 -0
- 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 +140 -3
- package/src/lib/model/commerce-quotes.js.map +1 -1
- package/src/lib/model/index.d.ts +4 -0
- package/src/lib/model/index.js +4 -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 +104 -0
- package/src/lib/server/commerce-pdf.js +310 -0
- package/src/lib/server/commerce-pdf.js.map +1 -0
- 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-print.d.ts +6 -0
- package/src/lib/server/order-print.js +22 -5
- package/src/lib/server/order-print.js.map +1 -1
- 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/pdf-response.d.ts +31 -0
- package/src/lib/server/pdf-response.js +30 -0
- package/src/lib/server/pdf-response.js.map +1 -0
- 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/pos-reader-splash.d.ts +2 -18
- package/src/lib/server/pos-reader-splash.js +8 -93
- package/src/lib/server/pos-reader-splash.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 +38 -6
- package/src/lib/server/quote-pay.js +115 -53
- package/src/lib/server/quote-pay.js.map +1 -1
- package/src/lib/server/quote-proofs.d.ts +51 -0
- package/src/lib/server/quote-proofs.js +500 -0
- package/src/lib/server/quote-proofs.js.map +1 -0
- package/src/lib/server/quote-uploads.d.ts +33 -0
- package/src/lib/server/quote-uploads.js +29 -0
- package/src/lib/server/quote-uploads.js.map +1 -0
- package/src/lib/server/quote.d.ts +25 -0
- package/src/lib/server/quote.js +638 -8
- package/src/lib/server/quote.js.map +1 -1
- package/src/lib/server/site-image-source.d.ts +38 -0
- package/src/lib/server/site-image-source.js +130 -0
- package/src/lib/server/site-image-source.js.map +1 -0
- 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 +24 -1
- 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 +82 -0
- package/src/lib/utils/commerce-document-pdf.js +613 -0
- package/src/lib/utils/commerce-document-pdf.js.map +1 -0
- 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 +52 -3
- package/src/lib/utils/order-invoice.js +124 -64
- 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
package/src/lib/server/quote.js
CHANGED
|
@@ -19,6 +19,7 @@ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_wi
|
|
|
19
19
|
import { createResourceUid } from "@aglyn/aglyn/app-utils/create-resource-uid";
|
|
20
20
|
import { checkEntitlement } from "@aglyn/aglyn/server";
|
|
21
21
|
import { searchPluginPeople } from "@aglyn/aglyn/plugin-manager/plugin-person-records";
|
|
22
|
+
import { pluginRecordFactsReader } from "@aglyn/aglyn/plugin-manager/plugin-record-facts";
|
|
22
23
|
import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
|
|
23
24
|
import { isRefusedIdToken } from "@aglyn/tenant-data-admin/server/id-token-refusal";
|
|
24
25
|
import * as CommerceModel from "../model/index.js";
|
|
@@ -26,7 +27,8 @@ import { commerceSitePermissionRefusal } from "../model/plugin-permissions.js";
|
|
|
26
27
|
import { holdsCommerceSiteKey } from "./commerce-site-keys.js";
|
|
27
28
|
import { quoteSaleTaxWithEngine } from "./tax-engine-quote.js";
|
|
28
29
|
import { expireCheckoutSession } from "./checkout-session-expiry.js";
|
|
29
|
-
import {
|
|
30
|
+
import { sendPdf } from "./pdf-response.js";
|
|
31
|
+
import { quotePayLinkUrl, quoteProofLinkUrl } from "./quote-link-token.js";
|
|
30
32
|
export function defaultQuoteDeps() {
|
|
31
33
|
return {
|
|
32
34
|
verifyIdToken: (token)=>firebaseAdmin.app().auth().verifyIdToken(token),
|
|
@@ -42,9 +44,47 @@ export function defaultQuoteDeps() {
|
|
|
42
44
|
quoteTax: (request)=>quoteSaleTaxWithEngine(request),
|
|
43
45
|
expireSession: (sessionId)=>expireCheckoutSession(sessionId),
|
|
44
46
|
payLinkUrl: (host, hostId, quoteId, version)=>quotePayLinkUrl(host, hostId, quoteId, version),
|
|
47
|
+
proofLinkUrl: (host, hostId, quoteId, version, proofId)=>quoteProofLinkUrl(host, hostId, quoteId, version, proofId),
|
|
48
|
+
quotePdf: async (input)=>(await import("./commerce-pdf.js")).quotePdfFile(input),
|
|
49
|
+
uploads: async ()=>(await import("./quote-uploads.js")).quoteUploadCalls(),
|
|
50
|
+
mailer: async ()=>(await import("./quote-email.js")).quoteMailer(),
|
|
51
|
+
requestFacts: async (request)=>{
|
|
52
|
+
var _ref;
|
|
53
|
+
var _pluginRecordFactsReader;
|
|
54
|
+
return (_ref = await ((_pluginRecordFactsReader = pluginRecordFactsReader(CommerceModel.QUOTE_REQUEST_FACTS_RESOURCE)) == null ? void 0 : _pluginRecordFactsReader.reader.read(request))) != null ? _ref : null;
|
|
55
|
+
},
|
|
45
56
|
now: ()=>Date.now()
|
|
46
57
|
};
|
|
47
58
|
}
|
|
59
|
+
/** A request's facts for the member asking, or the refusal to answer with. */ async function readRequest(deps, input) {
|
|
60
|
+
var _ref, _ref1;
|
|
61
|
+
if (!DOC_ID.test(input.submissionId)) return {
|
|
62
|
+
outcome: refuse(400, 'Missing submissionId')
|
|
63
|
+
};
|
|
64
|
+
const owner = await deps.orgForHost(input.hostId);
|
|
65
|
+
const read = await deps.requestFacts({
|
|
66
|
+
orgId: (_ref = owner == null ? void 0 : owner.id) != null ? _ref : '',
|
|
67
|
+
hostId: input.hostId,
|
|
68
|
+
id: input.submissionId,
|
|
69
|
+
uid: input.uid,
|
|
70
|
+
staff: input.staff,
|
|
71
|
+
org: (_ref1 = owner == null ? void 0 : owner.org) != null ? _ref1 : null,
|
|
72
|
+
now: new Date(deps.now())
|
|
73
|
+
});
|
|
74
|
+
if (!read) return {
|
|
75
|
+
outcome: refuse(404, 'Requests aren’t available on this site.')
|
|
76
|
+
};
|
|
77
|
+
if ('error' in read) return {
|
|
78
|
+
outcome: refuse(read.status, read.error)
|
|
79
|
+
};
|
|
80
|
+
const facts = CommerceModel.readQuoteRequestFacts(read.facts);
|
|
81
|
+
if (!facts) return {
|
|
82
|
+
outcome: refuse(404, 'That request is no longer in the Inbox.')
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
facts
|
|
86
|
+
};
|
|
87
|
+
}
|
|
48
88
|
/** Expires the session the pay page opened, when there is one; a failure is logged, never thrown. */ async function closeOpenSession(deps, sessionId, context) {
|
|
49
89
|
if (!sessionId) return;
|
|
50
90
|
const result = await deps.expireSession(sessionId).catch(()=>'open');
|
|
@@ -79,6 +119,11 @@ function headerOf(req, name) {
|
|
|
79
119
|
return String(Array.isArray(value) ? (_value_ = value[0]) != null ? _value_ : '' : value != null ? value : '').trim();
|
|
80
120
|
}
|
|
81
121
|
const DOC_ID = /^(?!__.*__$)[^/]{1,200}$/;
|
|
122
|
+
/** The actions under "Send quotes and invoices to customers" (`commerce.quotes.send`). */ const SEND_ACTIONS = new Set([
|
|
123
|
+
'pay-link',
|
|
124
|
+
'replace-link',
|
|
125
|
+
'send'
|
|
126
|
+
]);
|
|
82
127
|
/** The ship-to as a tax service reads it, or `null` to tax at the store's address. */ function serviceAddress(shipTo) {
|
|
83
128
|
if (!(shipTo == null ? void 0 : shipTo.country)) return null;
|
|
84
129
|
return _extends({
|
|
@@ -197,7 +242,7 @@ const DOC_ID = /^(?!__.*__$)[^/]{1,200}$/;
|
|
|
197
242
|
};
|
|
198
243
|
}
|
|
199
244
|
export async function handleQuote(deps, req) {
|
|
200
|
-
var _body_hostId, _body_action, _body_quoteId, _ref;
|
|
245
|
+
var _body_hostId, _body_action, _body_quoteId, _body_from, _ref;
|
|
201
246
|
if (req.method !== 'POST') return refuse(405, 'Method not allowed');
|
|
202
247
|
const authorization = headerOf(req, 'authorization');
|
|
203
248
|
const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : '';
|
|
@@ -217,7 +262,7 @@ export async function handleQuote(deps, req) {
|
|
|
217
262
|
const hostRef = firestore.collection('hosts').doc(hostId);
|
|
218
263
|
const host = await hostRef.get();
|
|
219
264
|
if (!host.exists) return refuse(404, 'Unknown site');
|
|
220
|
-
const key = action
|
|
265
|
+
const key = SEND_ACTIONS.has(action) ? 'commerce.quotes.send' : 'commerce.quotes';
|
|
221
266
|
if (!holdsCommerceSiteKey(host, decoded.uid, key)) {
|
|
222
267
|
return refuse(403, commerceSitePermissionRefusal(key));
|
|
223
268
|
}
|
|
@@ -281,6 +326,90 @@ export async function handleQuote(deps, req) {
|
|
|
281
326
|
}
|
|
282
327
|
};
|
|
283
328
|
}
|
|
329
|
+
if (action === 'send') {
|
|
330
|
+
var _body_quoteId2, _host_data1;
|
|
331
|
+
const quoteId = String((_body_quoteId2 = body['quoteId']) != null ? _body_quoteId2 : '');
|
|
332
|
+
if (!DOC_ID.test(quoteId)) return refuse(400, 'Missing quoteId');
|
|
333
|
+
return handleSendAction(deps, {
|
|
334
|
+
req,
|
|
335
|
+
body,
|
|
336
|
+
hostId,
|
|
337
|
+
quoteId,
|
|
338
|
+
hostRef,
|
|
339
|
+
host: (_host_data1 = host.data()) != null ? _host_data1 : {},
|
|
340
|
+
firestore,
|
|
341
|
+
actor,
|
|
342
|
+
senderEmail: decoded.email ? String(decoded.email) : null
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
if (action === 'start-from-request') {
|
|
346
|
+
var _body_submissionId;
|
|
347
|
+
const read = await readRequest(deps, {
|
|
348
|
+
hostId,
|
|
349
|
+
submissionId: String((_body_submissionId = body['submissionId']) != null ? _body_submissionId : ''),
|
|
350
|
+
uid: decoded.uid,
|
|
351
|
+
staff: decoded.staff === true
|
|
352
|
+
});
|
|
353
|
+
if ('outcome' in read) return read.outcome;
|
|
354
|
+
const started = CommerceModel.quoteStartFromRequest(String(body['submissionId']), read.facts);
|
|
355
|
+
return {
|
|
356
|
+
status: 200,
|
|
357
|
+
body: {
|
|
358
|
+
prefill: started.prefill,
|
|
359
|
+
request: started.request,
|
|
360
|
+
files: CommerceModel.quoteRequestFileViews(started.files),
|
|
361
|
+
threaded: Boolean(started.emailThread)
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (PROOF_ACTIONS.has(action)) {
|
|
366
|
+
var _body_quoteId3;
|
|
367
|
+
const quoteId = String((_body_quoteId3 = body['quoteId']) != null ? _body_quoteId3 : '');
|
|
368
|
+
if (!DOC_ID.test(quoteId)) return refuse(400, 'Missing quoteId');
|
|
369
|
+
return handleProofAction(deps, {
|
|
370
|
+
action,
|
|
371
|
+
body,
|
|
372
|
+
hostId,
|
|
373
|
+
quoteId,
|
|
374
|
+
hostRef,
|
|
375
|
+
firestore,
|
|
376
|
+
uid: decoded.uid,
|
|
377
|
+
actor
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
if (action === 'pdf') {
|
|
381
|
+
var _body_quoteId4, _host_data2;
|
|
382
|
+
const quoteId = String((_body_quoteId4 = body['quoteId']) != null ? _body_quoteId4 : '');
|
|
383
|
+
if (!DOC_ID.test(quoteId)) return refuse(400, 'Missing quoteId');
|
|
384
|
+
const [snapshot, store] = await Promise.all([
|
|
385
|
+
hostRef.collection('orders').doc(quoteId).get(),
|
|
386
|
+
hostRef.collection('settings').doc('store').get()
|
|
387
|
+
]);
|
|
388
|
+
const order = snapshot.exists ? snapshot.data() : null;
|
|
389
|
+
if (!order || !CommerceModel.isQuote(order) || !order.quote) return refuse(404, 'Unknown quote');
|
|
390
|
+
const hostData = (_host_data2 = host.data()) != null ? _host_data2 : {};
|
|
391
|
+
// The pay link is "Send quotes and invoices to customers"; without it the PDF leaves the link off.
|
|
392
|
+
const payUrl = CommerceModel.isUnpaidQuote(order) && holdsCommerceSiteKey(host, decoded.uid, 'commerce.quotes.send') ? deps.payLinkUrl(hostData, hostId, quoteId, CommerceModel.quoteLinkVersion(order.quote)) : null;
|
|
393
|
+
try {
|
|
394
|
+
var _store_data;
|
|
395
|
+
const pdf = await deps.quotePdf({
|
|
396
|
+
hostId,
|
|
397
|
+
quoteId,
|
|
398
|
+
order,
|
|
399
|
+
host: hostData,
|
|
400
|
+
store: (_store_data = store.data()) != null ? _store_data : null,
|
|
401
|
+
payUrl
|
|
402
|
+
});
|
|
403
|
+
return {
|
|
404
|
+
status: 200,
|
|
405
|
+
body: {},
|
|
406
|
+
pdf
|
|
407
|
+
};
|
|
408
|
+
} catch (error) {
|
|
409
|
+
console.error('commerce quote pdf failed', error);
|
|
410
|
+
return refuse(500, 'The PDF could not be made. Try again.');
|
|
411
|
+
}
|
|
412
|
+
}
|
|
284
413
|
if (action === 'search-customers') {
|
|
285
414
|
var _body_text;
|
|
286
415
|
const text = String((_body_text = body['text']) != null ? _body_text : '').trim().slice(0, 120);
|
|
@@ -332,8 +461,8 @@ export async function handleQuote(deps, req) {
|
|
|
332
461
|
if (!DOC_ID.test(quoteId)) return refuse(400, 'Missing quoteId');
|
|
333
462
|
const ref = hostRef.collection('orders').doc(quoteId);
|
|
334
463
|
const deleted = await firestore.runTransaction(async (transaction)=>{
|
|
335
|
-
var _ref;
|
|
336
|
-
var _order_quote;
|
|
464
|
+
var _ref, _ref1;
|
|
465
|
+
var _order_quote, _order_quote1;
|
|
337
466
|
const snapshot = await transaction.get(ref);
|
|
338
467
|
// Already gone is what a retried delete finds.
|
|
339
468
|
if (!snapshot.exists) return {
|
|
@@ -350,6 +479,11 @@ export async function handleQuote(deps, req) {
|
|
|
350
479
|
if (((_ref = (_order_quote = order.quote) == null ? void 0 : _order_quote.stage) != null ? _ref : 'draft') !== 'draft') {
|
|
351
480
|
return refuse(409, 'Only a draft quote can be deleted.');
|
|
352
481
|
}
|
|
482
|
+
// Earlier revisions are what the customer was shown and answered; a
|
|
483
|
+
// quote that has them stays on record.
|
|
484
|
+
if (Number((_ref1 = (_order_quote1 = order.quote) == null ? void 0 : _order_quote1.revision) != null ? _ref1 : 1) > 1) {
|
|
485
|
+
return refuse(409, 'This quote has earlier revisions the customer saw, so it stays on record.');
|
|
486
|
+
}
|
|
353
487
|
transaction.delete(ref);
|
|
354
488
|
return {
|
|
355
489
|
status: 200,
|
|
@@ -363,6 +497,8 @@ export async function handleQuote(deps, req) {
|
|
|
363
497
|
"closeSession"
|
|
364
498
|
]);
|
|
365
499
|
await closeOpenSession(deps, closeSession, 'quote deleted');
|
|
500
|
+
// Its proofs and anything the customer sent go with it.
|
|
501
|
+
if (outcome.status === 200) await removeQuoteFiles(deps, hostId, quoteId);
|
|
366
502
|
return outcome;
|
|
367
503
|
}
|
|
368
504
|
if (action !== 'create' && action !== 'update') return refuse(400, 'Unknown action');
|
|
@@ -374,6 +510,24 @@ export async function handleQuote(deps, req) {
|
|
|
374
510
|
const parsed = CommerceModel.parseQuoteInput(body['quote']);
|
|
375
511
|
if ('error' in parsed) return refuse(400, parsed.error);
|
|
376
512
|
const input = parsed.input;
|
|
513
|
+
/*
|
|
514
|
+
* A new quote from a request (AGL-4098): the request is read again, here,
|
|
515
|
+
* for the member saving, and what the quote takes from it — which request,
|
|
516
|
+
* its files, its conversation — comes from that read and never from the
|
|
517
|
+
* body. The editable fields (customer, lines, notes) are the editor's.
|
|
518
|
+
*/ const from = (_body_from = body['from']) != null ? _body_from : null;
|
|
519
|
+
let started = null;
|
|
520
|
+
if (action === 'create' && (from == null ? void 0 : from.kind) === 'submission') {
|
|
521
|
+
var _from_id;
|
|
522
|
+
const read = await readRequest(deps, {
|
|
523
|
+
hostId,
|
|
524
|
+
submissionId: String((_from_id = from.id) != null ? _from_id : ''),
|
|
525
|
+
uid: decoded.uid,
|
|
526
|
+
staff: decoded.staff === true
|
|
527
|
+
});
|
|
528
|
+
if ('outcome' in read) return read.outcome;
|
|
529
|
+
started = CommerceModel.quoteStartFromRequest(String(from.id), read.facts);
|
|
530
|
+
}
|
|
377
531
|
const priced = await priceQuote(deps, hostRef, hostId, input);
|
|
378
532
|
if ('status' in priced) return priced;
|
|
379
533
|
const claimed = await claimAttempt(firestore, {
|
|
@@ -411,7 +565,14 @@ export async function handleQuote(deps, req) {
|
|
|
411
565
|
revision: 1
|
|
412
566
|
}, editedQuoteFields(input), {
|
|
413
567
|
tax: priced.tax,
|
|
414
|
-
requireProofApproval: (_input_requireProofApproval = input.requireProofApproval) != null ? _input_requireProofApproval : priced.requireProofApprovalDefault
|
|
568
|
+
requireProofApproval: (_input_requireProofApproval = input.requireProofApproval) != null ? _input_requireProofApproval : priced.requireProofApprovalDefault
|
|
569
|
+
}, started ? _extends({
|
|
570
|
+
request: started.request
|
|
571
|
+
}, started.files.length ? {
|
|
572
|
+
requestFiles: started.files
|
|
573
|
+
} : {}, started.emailThread ? {
|
|
574
|
+
emailThread: started.emailThread
|
|
575
|
+
} : {}) : {}, {
|
|
415
576
|
createdAtMs: now,
|
|
416
577
|
updatedAtMs: now,
|
|
417
578
|
createdBy: actor,
|
|
@@ -432,7 +593,7 @@ export async function handleQuote(deps, req) {
|
|
|
432
593
|
{
|
|
433
594
|
atMs: now,
|
|
434
595
|
event: 'quote',
|
|
435
|
-
detail: `Quote created by ${actor}`
|
|
596
|
+
detail: started ? `Quote created by ${actor} from a request sent through “${started.request.formName}”` : `Quote created by ${actor}`
|
|
436
597
|
}
|
|
437
598
|
],
|
|
438
599
|
quote
|
|
@@ -505,8 +666,477 @@ export async function handleQuote(deps, req) {
|
|
|
505
666
|
return refuse(500, 'The quote could not be saved');
|
|
506
667
|
}
|
|
507
668
|
}
|
|
669
|
+
/*==========================================
|
|
670
|
+
* PROOFS AND REVISIONS (AGL-4096)
|
|
671
|
+
*=========================================*/ const PROOF_ACTIONS = new Set([
|
|
672
|
+
'proof-ticket',
|
|
673
|
+
'proof-finalize',
|
|
674
|
+
'proof-remove',
|
|
675
|
+
'start-revision',
|
|
676
|
+
'revisions',
|
|
677
|
+
'file'
|
|
678
|
+
]);
|
|
679
|
+
/** Deletes a quote's kept files, its proofs and the customer's artwork; never throws. */ async function removeQuoteFiles(deps, hostId, quoteId) {
|
|
680
|
+
try {
|
|
681
|
+
const uploads = await deps.uploads();
|
|
682
|
+
await Promise.all([
|
|
683
|
+
CommerceModel.QUOTE_PROOF_PURPOSE,
|
|
684
|
+
CommerceModel.QUOTE_ARTWORK_PURPOSE
|
|
685
|
+
].map((purpose)=>uploads.removeAll({
|
|
686
|
+
hostId,
|
|
687
|
+
purpose,
|
|
688
|
+
purposeId: quoteId
|
|
689
|
+
})));
|
|
690
|
+
} catch (error) {
|
|
691
|
+
console.error('quote files were not removed', {
|
|
692
|
+
hostId,
|
|
693
|
+
quoteId,
|
|
694
|
+
error
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
/** A file as the console is told about it: everything but where it is kept. */ function fileView(file) {
|
|
699
|
+
const rest = _extends({}, file);
|
|
700
|
+
delete rest.path;
|
|
701
|
+
return rest;
|
|
702
|
+
}
|
|
703
|
+
/** A revision as the console's history lists it. */ function revisionView(snapshot) {
|
|
704
|
+
var _snapshot_proofs, _snapshot_responses;
|
|
705
|
+
return _extends({}, snapshot, {
|
|
706
|
+
proofs: ((_snapshot_proofs = snapshot.proofs) != null ? _snapshot_proofs : []).map(fileView),
|
|
707
|
+
responses: ((_snapshot_responses = snapshot.responses) != null ? _snapshot_responses : []).map((response)=>_extends({}, response, response.files ? {
|
|
708
|
+
files: response.files.map(fileView)
|
|
709
|
+
} : {}))
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Writes `next` over the quote in `transaction`, without the pay page's open
|
|
714
|
+
* session, which was priced from the quote as it stood; the caller closes it.
|
|
715
|
+
*/ function writeEditedQuote(transaction, ref, next) {
|
|
716
|
+
const rest = _extends({}, next);
|
|
717
|
+
const session = rest.checkoutSessionId;
|
|
718
|
+
delete rest.checkoutSessionId;
|
|
719
|
+
transaction.set(ref, rest);
|
|
720
|
+
return session;
|
|
721
|
+
}
|
|
722
|
+
async function handleProofAction(deps, context) {
|
|
723
|
+
const { action, body, hostId, quoteId, hostRef, firestore, uid, actor } = context;
|
|
724
|
+
const ref = hostRef.collection('orders').doc(quoteId);
|
|
725
|
+
const proofScope = {
|
|
726
|
+
hostId,
|
|
727
|
+
purpose: CommerceModel.QUOTE_PROOF_PURPOSE,
|
|
728
|
+
purposeId: quoteId
|
|
729
|
+
};
|
|
730
|
+
if (action === 'revisions' || action === 'file') {
|
|
731
|
+
var _order_quote_revision, _body_fileId, _order_quote_responses, _ref, _ref1, _order_quote_requestFiles;
|
|
732
|
+
const snapshot = await ref.get();
|
|
733
|
+
const order = snapshot.exists ? snapshot.data() : null;
|
|
734
|
+
if (!order || !CommerceModel.isQuote(order) || !order.quote) return refuse(404, 'Unknown quote');
|
|
735
|
+
const current = Number((_order_quote_revision = order.quote.revision) != null ? _order_quote_revision : 1);
|
|
736
|
+
const wanted = body['revision'] === undefined || body['revision'] === null ? current : Number(body['revision']);
|
|
737
|
+
let snapshots = [];
|
|
738
|
+
if (action === 'revisions' || wanted !== current) {
|
|
739
|
+
const listed = await ref.collection('revisions').get();
|
|
740
|
+
snapshots = listed.docs.map((entry)=>entry.data()).filter((entry)=>Number.isInteger(Number(entry == null ? void 0 : entry.revision))).sort((a, b)=>Number(a.revision) - Number(b.revision));
|
|
741
|
+
}
|
|
742
|
+
if (action === 'revisions') return {
|
|
743
|
+
status: 200,
|
|
744
|
+
body: {
|
|
745
|
+
revisions: snapshots.map(revisionView)
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
const fileId = String((_body_fileId = body['fileId']) != null ? _body_fileId : '');
|
|
749
|
+
const source = wanted === current ? {
|
|
750
|
+
proofs: CommerceModel.currentQuoteProofs(order.quote),
|
|
751
|
+
responses: (_order_quote_responses = order.quote.responses) != null ? _order_quote_responses : []
|
|
752
|
+
} : snapshots.find((entry)=>Number(entry.revision) === wanted);
|
|
753
|
+
const files = [
|
|
754
|
+
...(_ref = source == null ? void 0 : source.proofs) != null ? _ref : [],
|
|
755
|
+
...((_ref1 = source == null ? void 0 : source.responses) != null ? _ref1 : []).flatMap((response)=>{
|
|
756
|
+
var _response_files;
|
|
757
|
+
return (_response_files = response.files) != null ? _response_files : [];
|
|
758
|
+
}),
|
|
759
|
+
// The request's files (AGL-4098), on every revision: they came first.
|
|
760
|
+
...(_order_quote_requestFiles = order.quote.requestFiles) != null ? _order_quote_requestFiles : []
|
|
761
|
+
];
|
|
762
|
+
const file = files.find((entry)=>entry.id === fileId);
|
|
763
|
+
if (!(file == null ? void 0 : file.path)) return refuse(404, 'That file is no longer on this quote.');
|
|
764
|
+
return {
|
|
765
|
+
status: 200,
|
|
766
|
+
body: {},
|
|
767
|
+
file: {
|
|
768
|
+
path: file.path,
|
|
769
|
+
name: file.name,
|
|
770
|
+
contentType: file.contentType
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
if (action === 'proof-ticket') {
|
|
775
|
+
const snapshot = await ref.get();
|
|
776
|
+
const order = snapshot.exists ? snapshot.data() : null;
|
|
777
|
+
const refused = CommerceModel.quoteProofChangeRefusal(order, 'add');
|
|
778
|
+
if (refused) return refuse(refused === 'not-a-quote' ? 404 : 409, CommerceModel.quoteProofRefusalMessage(refused));
|
|
779
|
+
// Storage is spent here, so the plan must sell, as a save's must.
|
|
780
|
+
const owner = await deps.orgForHost(hostId);
|
|
781
|
+
if (!checkEntitlement(owner == null ? void 0 : owner.org, 'commerce')) return refuse(403, 'Selling is not enabled');
|
|
782
|
+
const uploads = await deps.uploads();
|
|
783
|
+
const minted = await uploads.mint({
|
|
784
|
+
scope: proofScope,
|
|
785
|
+
request: body['file'],
|
|
786
|
+
caller: {
|
|
787
|
+
uid
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
if ('error' in minted) return refuse(minted.status, minted.error);
|
|
791
|
+
return {
|
|
792
|
+
status: 200,
|
|
793
|
+
body: {
|
|
794
|
+
ticket: minted.ticket,
|
|
795
|
+
upload: minted.upload,
|
|
796
|
+
file: minted.file
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
if (action === 'proof-finalize') {
|
|
801
|
+
const uploads = await deps.uploads();
|
|
802
|
+
const kept = await uploads.finalize({
|
|
803
|
+
scope: proofScope,
|
|
804
|
+
ticket: body['ticket'],
|
|
805
|
+
caller: {
|
|
806
|
+
uid
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
if ('error' in kept) return refuse(kept.status, kept.error);
|
|
810
|
+
const now = deps.now();
|
|
811
|
+
const answer = await firestore.runTransaction(async (transaction)=>{
|
|
812
|
+
const snapshot = await transaction.get(ref);
|
|
813
|
+
const order = snapshot.exists ? snapshot.data() : null;
|
|
814
|
+
if (!order) return {
|
|
815
|
+
outcome: refuse(404, 'Unknown quote')
|
|
816
|
+
};
|
|
817
|
+
// A retried finalize finds its proof already on the quote.
|
|
818
|
+
const already = CommerceModel.currentQuoteProofs(order.quote).find((proof)=>proof.id === kept.file.id);
|
|
819
|
+
if (already) {
|
|
820
|
+
var _ref;
|
|
821
|
+
var _order_quote;
|
|
822
|
+
return {
|
|
823
|
+
outcome: {
|
|
824
|
+
status: 200,
|
|
825
|
+
body: {
|
|
826
|
+
proof: fileView(already),
|
|
827
|
+
updatedAtMs: (_ref = (_order_quote = order.quote) == null ? void 0 : _order_quote.updatedAtMs) != null ? _ref : null
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
const added = CommerceModel.withQuoteProofAdded(order, {
|
|
833
|
+
id: kept.file.id,
|
|
834
|
+
name: kept.file.name,
|
|
835
|
+
contentType: kept.file.contentType,
|
|
836
|
+
size: kept.file.size,
|
|
837
|
+
path: kept.file.path
|
|
838
|
+
}, {
|
|
839
|
+
atMs: now,
|
|
840
|
+
by: actor
|
|
841
|
+
});
|
|
842
|
+
if ('reason' in added) {
|
|
843
|
+
return {
|
|
844
|
+
outcome: refuse(added.reason === 'not-a-quote' ? 404 : 409, CommerceModel.quoteProofRefusalMessage(added.reason))
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
const closeSession = writeEditedQuote(transaction, ref, added.order);
|
|
848
|
+
return {
|
|
849
|
+
outcome: {
|
|
850
|
+
status: 200,
|
|
851
|
+
body: {
|
|
852
|
+
proof: fileView(added.proof),
|
|
853
|
+
updatedAtMs: now
|
|
854
|
+
}
|
|
855
|
+
},
|
|
856
|
+
closeSession
|
|
857
|
+
};
|
|
858
|
+
});
|
|
859
|
+
if (answer.outcome.status !== 200) await uploads.remove(kept.file.path).catch(()=>undefined);
|
|
860
|
+
await closeOpenSession(deps, answer.closeSession, 'quote proof added');
|
|
861
|
+
return answer.outcome;
|
|
862
|
+
}
|
|
863
|
+
if (action === 'proof-remove') {
|
|
864
|
+
var _body_proofId;
|
|
865
|
+
const proofId = String((_body_proofId = body['proofId']) != null ? _body_proofId : '');
|
|
866
|
+
const now = deps.now();
|
|
867
|
+
const answer = await firestore.runTransaction(async (transaction)=>{
|
|
868
|
+
const snapshot = await transaction.get(ref);
|
|
869
|
+
const order = snapshot.exists ? snapshot.data() : null;
|
|
870
|
+
if (!order) return {
|
|
871
|
+
outcome: refuse(404, 'Unknown quote')
|
|
872
|
+
};
|
|
873
|
+
const removed = CommerceModel.withQuoteProofRemoved(order, proofId, {
|
|
874
|
+
atMs: now,
|
|
875
|
+
by: actor
|
|
876
|
+
});
|
|
877
|
+
if ('reason' in removed) {
|
|
878
|
+
return {
|
|
879
|
+
outcome: refuse(removed.reason === 'not-a-quote' || removed.reason === 'unknown-proof' ? 404 : 409, CommerceModel.quoteProofRefusalMessage(removed.reason))
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
const closeSession = writeEditedQuote(transaction, ref, removed.order);
|
|
883
|
+
return {
|
|
884
|
+
outcome: {
|
|
885
|
+
status: 200,
|
|
886
|
+
body: {
|
|
887
|
+
removed: true,
|
|
888
|
+
updatedAtMs: now
|
|
889
|
+
}
|
|
890
|
+
},
|
|
891
|
+
closeSession,
|
|
892
|
+
path: removed.proof.path
|
|
893
|
+
};
|
|
894
|
+
});
|
|
895
|
+
if (answer.path) {
|
|
896
|
+
const uploads = await deps.uploads();
|
|
897
|
+
await uploads.remove(answer.path).catch((error)=>console.error('quote proof file was not deleted', error));
|
|
898
|
+
}
|
|
899
|
+
await closeOpenSession(deps, answer.closeSession, 'quote proof removed');
|
|
900
|
+
return answer.outcome;
|
|
901
|
+
}
|
|
902
|
+
// start-revision
|
|
903
|
+
const now = deps.now();
|
|
904
|
+
const answer = await firestore.runTransaction(async (transaction)=>{
|
|
905
|
+
const snapshot = await transaction.get(ref);
|
|
906
|
+
const order = snapshot.exists ? snapshot.data() : null;
|
|
907
|
+
if (!order) return {
|
|
908
|
+
outcome: refuse(404, 'Unknown quote')
|
|
909
|
+
};
|
|
910
|
+
const started = CommerceModel.startQuoteRevision(order, {
|
|
911
|
+
atMs: now,
|
|
912
|
+
by: actor
|
|
913
|
+
});
|
|
914
|
+
if ('reason' in started) return {
|
|
915
|
+
outcome: refuse(started.reason === 'paid' ? 409 : 404, started.error)
|
|
916
|
+
};
|
|
917
|
+
const revisionRef = ref.collection('revisions').doc(String(started.revision));
|
|
918
|
+
// A revision on record is never written over.
|
|
919
|
+
if ((await transaction.get(revisionRef)).exists) {
|
|
920
|
+
return {
|
|
921
|
+
outcome: refuse(409, `Revision ${started.revision} is already on record. Close the quote and open it again.`)
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
transaction.set(revisionRef, started.snapshot);
|
|
925
|
+
const closeSession = writeEditedQuote(transaction, ref, started.order);
|
|
926
|
+
return {
|
|
927
|
+
outcome: {
|
|
928
|
+
status: 200,
|
|
929
|
+
body: {
|
|
930
|
+
revision: started.revision + 1,
|
|
931
|
+
updatedAtMs: now
|
|
932
|
+
}
|
|
933
|
+
},
|
|
934
|
+
closeSession
|
|
935
|
+
};
|
|
936
|
+
});
|
|
937
|
+
await closeOpenSession(deps, answer.closeSession, 'quote revision started');
|
|
938
|
+
return answer.outcome;
|
|
939
|
+
}
|
|
940
|
+
/*==========================================
|
|
941
|
+
* SENDING THE QUOTE (AGL-4097)
|
|
942
|
+
*
|
|
943
|
+
* { action: 'send', hostId, quoteId, to?, subject?, message } + Idempotency-Key
|
|
944
|
+
*
|
|
945
|
+
* Emails the quote to `to` (else the customer's email) under `subject` (else
|
|
946
|
+
* the conversation's, else "Quote #N from {business}"), with the business's
|
|
947
|
+
* message, the proof, the PDF and the pay link. Refused for a paid quote, one
|
|
948
|
+
* with nowhere to send it, and an address on a suppression list.
|
|
949
|
+
*
|
|
950
|
+
* When the customer was emailed an earlier revision and the link has not
|
|
951
|
+
* been replaced since, the link is replaced first (as Replace link does: the
|
|
952
|
+
* version moves on, the open payment closes) so the earlier email's link
|
|
953
|
+
* says the quote was updated. A resend of the same revision keeps it. Then
|
|
954
|
+
* the send is recorded (`quote.sends`), the conversation moves on
|
|
955
|
+
* (`quote.emailThread`), the quote moves to Sent unless the customer has
|
|
956
|
+
* answered this revision, and the timeline says so.
|
|
957
|
+
*
|
|
958
|
+
* One attempt per key: a retried press replays the answer and never sends a
|
|
959
|
+
* second email. A send that fails releases the key, and a retry finds the
|
|
960
|
+
* link already replaced, so it never replaces it twice.
|
|
961
|
+
*=========================================*/ async function handleSendAction(deps, context) {
|
|
962
|
+
var _ref, _ref1;
|
|
963
|
+
var _order_quote;
|
|
964
|
+
const { req, body, hostId, quoteId, hostRef, host, firestore, actor } = context;
|
|
965
|
+
const parsed = CommerceModel.parseQuoteSendInput(body);
|
|
966
|
+
if ('error' in parsed) return refuse(400, parsed.error);
|
|
967
|
+
const idempotencyKey = headerOf(req, 'idempotency-key').slice(0, 200);
|
|
968
|
+
if (!idempotencyKey) return refuse(400, 'Sending a quote needs an Idempotency-Key header.');
|
|
969
|
+
const ref = hostRef.collection('orders').doc(quoteId);
|
|
970
|
+
const [snapshot, store] = await Promise.all([
|
|
971
|
+
ref.get(),
|
|
972
|
+
hostRef.collection('settings').doc('store').get()
|
|
973
|
+
]);
|
|
974
|
+
const stored = snapshot.exists ? snapshot.data() : null;
|
|
975
|
+
const to = CommerceModel.quoteSendRecipient(stored, parsed.input.to);
|
|
976
|
+
const refused = CommerceModel.quoteSendRefusal(stored, to);
|
|
977
|
+
if (refused) {
|
|
978
|
+
return refuse(refused === 'not-a-quote' ? 404 : refused === 'paid' ? 409 : 400, CommerceModel.quoteSendRefusalMessage(refused));
|
|
979
|
+
}
|
|
980
|
+
if (!deps.payLinkUrl(host, hostId, quoteId, CommerceModel.quoteLinkVersion(stored == null ? void 0 : stored.quote))) {
|
|
981
|
+
return refuse(409, 'This site has no web address yet, so its quotes have no pay link. Publish the site first.');
|
|
982
|
+
}
|
|
983
|
+
const owner = await deps.orgForHost(hostId);
|
|
984
|
+
if (!checkEntitlement(owner == null ? void 0 : owner.org, 'commerce')) return refuse(403, 'Selling is not enabled');
|
|
985
|
+
const mailer = await deps.mailer();
|
|
986
|
+
if (!mailer.configured()) {
|
|
987
|
+
return refuse(503, 'Email isn’t set up here, so quotes can’t be emailed. Copy the pay link and send it yourself.');
|
|
988
|
+
}
|
|
989
|
+
// The bounce and complaint list only (AGL-4098): the customer asked for the
|
|
990
|
+
// quote, so unsubscribing from the site's newsletter does not refuse it.
|
|
991
|
+
if (await mailer.suppression(to)) {
|
|
992
|
+
return refuse(409, `${to} bounced or reported an email as spam, so it can’t be emailed. Copy the pay link and send it another way.`);
|
|
993
|
+
}
|
|
994
|
+
const claimed = await claimAttempt(firestore, {
|
|
995
|
+
kind: 'commerce-quote-send',
|
|
996
|
+
scopeId: `${hostId}:${quoteId}`,
|
|
997
|
+
orgId: (_ref = owner == null ? void 0 : owner.id) != null ? _ref : '',
|
|
998
|
+
key: idempotencyKey,
|
|
999
|
+
busyMessage: 'This quote is already being sent'
|
|
1000
|
+
});
|
|
1001
|
+
if ('replay' in claimed) {
|
|
1002
|
+
return {
|
|
1003
|
+
status: claimed.replay.status,
|
|
1004
|
+
body: claimed.replay.body
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
const claim = claimed.claim;
|
|
1008
|
+
const now = deps.now();
|
|
1009
|
+
let prepared;
|
|
1010
|
+
try {
|
|
1011
|
+
// A new revision's first email replaces the link the customer was sent.
|
|
1012
|
+
prepared = await firestore.runTransaction(async (transaction)=>{
|
|
1013
|
+
var _order_quote_revision;
|
|
1014
|
+
const current = await transaction.get(ref);
|
|
1015
|
+
const order = current.exists ? current.data() : null;
|
|
1016
|
+
const again = CommerceModel.quoteSendRefusal(order, to);
|
|
1017
|
+
if (again || !(order == null ? void 0 : order.quote)) {
|
|
1018
|
+
const reason = again != null ? again : 'not-a-quote';
|
|
1019
|
+
return {
|
|
1020
|
+
outcome: refuse(reason === 'not-a-quote' ? 404 : 409, CommerceModel.quoteSendRefusalMessage(reason))
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
const version = CommerceModel.quoteLinkVersion(order.quote);
|
|
1024
|
+
if (!CommerceModel.quoteSendReplacesLink(order.quote)) return {
|
|
1025
|
+
order,
|
|
1026
|
+
linkVersion: version,
|
|
1027
|
+
linkReplaced: false
|
|
1028
|
+
};
|
|
1029
|
+
const rest = _extends({}, order);
|
|
1030
|
+
delete rest.checkoutSessionId;
|
|
1031
|
+
const replaced = _extends({}, rest, {
|
|
1032
|
+
timeline: CommerceModel.appendOrderEvent(order, 'quote-link-replaced', `Pay link replaced to send revision ${Number((_order_quote_revision = order.quote.revision) != null ? _order_quote_revision : 1)}; the earlier email's link no longer opens the quote`, now),
|
|
1033
|
+
quote: _extends({}, order.quote, {
|
|
1034
|
+
linkVersion: version + 1
|
|
1035
|
+
})
|
|
1036
|
+
});
|
|
1037
|
+
transaction.set(ref, replaced);
|
|
1038
|
+
return {
|
|
1039
|
+
order: replaced,
|
|
1040
|
+
linkVersion: version + 1,
|
|
1041
|
+
linkReplaced: true,
|
|
1042
|
+
closeSession: order.checkoutSessionId
|
|
1043
|
+
};
|
|
1044
|
+
});
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
console.error('commerce quote send failed before sending', error);
|
|
1047
|
+
await claim.release();
|
|
1048
|
+
return refuse(500, 'The quote could not be sent. Try again.');
|
|
1049
|
+
}
|
|
1050
|
+
if ('outcome' in prepared) {
|
|
1051
|
+
await claim.release();
|
|
1052
|
+
return prepared.outcome;
|
|
1053
|
+
}
|
|
1054
|
+
const { order, linkVersion, linkReplaced } = prepared;
|
|
1055
|
+
await closeOpenSession(deps, prepared.closeSession, 'quote link replaced to send a revision');
|
|
1056
|
+
const payUrl = deps.payLinkUrl(host, hostId, quoteId, linkVersion);
|
|
1057
|
+
let delivered;
|
|
1058
|
+
try {
|
|
1059
|
+
var _store_data;
|
|
1060
|
+
delivered = await mailer.deliver({
|
|
1061
|
+
hostId,
|
|
1062
|
+
quoteId,
|
|
1063
|
+
order,
|
|
1064
|
+
host,
|
|
1065
|
+
store: (_store_data = store.data()) != null ? _store_data : null,
|
|
1066
|
+
owner: owner ? {
|
|
1067
|
+
id: owner.id,
|
|
1068
|
+
org: owner.org
|
|
1069
|
+
} : null,
|
|
1070
|
+
to,
|
|
1071
|
+
subject: parsed.input.subject,
|
|
1072
|
+
message: parsed.input.message,
|
|
1073
|
+
payUrl,
|
|
1074
|
+
proofUrl: (proofId)=>deps.proofLinkUrl(host, hostId, quoteId, linkVersion, proofId),
|
|
1075
|
+
linkReplaced,
|
|
1076
|
+
senderEmail: context.senderEmail,
|
|
1077
|
+
atMs: now
|
|
1078
|
+
});
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
console.error('commerce quote send failed', error);
|
|
1081
|
+
await claim.release();
|
|
1082
|
+
return refuse(502, 'The quote email could not be sent. Try again.');
|
|
1083
|
+
}
|
|
1084
|
+
// `strictNullChecks` is off, so the result does not narrow on `sent`.
|
|
1085
|
+
if ('error' in delivered) {
|
|
1086
|
+
await claim.release();
|
|
1087
|
+
return refuse(delivered.status, delivered.error);
|
|
1088
|
+
}
|
|
1089
|
+
const done = delivered;
|
|
1090
|
+
// The email is out: from here a failure is logged, never answered as one,
|
|
1091
|
+
// so a retry does not send it twice.
|
|
1092
|
+
const revision = Math.max(1, Number((_ref1 = (_order_quote = order.quote) == null ? void 0 : _order_quote.revision) != null ? _ref1 : 1) || 1);
|
|
1093
|
+
const send = {
|
|
1094
|
+
revision,
|
|
1095
|
+
to,
|
|
1096
|
+
subject: done.subject,
|
|
1097
|
+
atMs: now,
|
|
1098
|
+
by: actor,
|
|
1099
|
+
messageId: done.messageId,
|
|
1100
|
+
providerId: done.providerId,
|
|
1101
|
+
linkVersion
|
|
1102
|
+
};
|
|
1103
|
+
try {
|
|
1104
|
+
await firestore.runTransaction(async (transaction)=>{
|
|
1105
|
+
const current = await transaction.get(ref);
|
|
1106
|
+
const latest = current.exists ? current.data() : null;
|
|
1107
|
+
if (!(latest == null ? void 0 : latest.quote)) return;
|
|
1108
|
+
transaction.set(ref, CommerceModel.quoteAfterSend(latest, send, done.thread));
|
|
1109
|
+
});
|
|
1110
|
+
} catch (error) {
|
|
1111
|
+
console.error('commerce quote sent but not recorded', {
|
|
1112
|
+
hostId,
|
|
1113
|
+
quoteId,
|
|
1114
|
+
messageId: done.messageId,
|
|
1115
|
+
error
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
const outcome = {
|
|
1119
|
+
status: 200,
|
|
1120
|
+
body: {
|
|
1121
|
+
sent: true,
|
|
1122
|
+
to,
|
|
1123
|
+
atMs: now,
|
|
1124
|
+
revision,
|
|
1125
|
+
linkVersion,
|
|
1126
|
+
linkReplaced
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
await claim.record(outcome.status, outcome.body);
|
|
1130
|
+
return outcome;
|
|
1131
|
+
}
|
|
508
1132
|
/** `POST /api/commerce/quote`. */ export const quoteHandler = async (req, res)=>{
|
|
509
|
-
const
|
|
1133
|
+
const deps = defaultQuoteDeps();
|
|
1134
|
+
const outcome = await handleQuote(deps, req);
|
|
1135
|
+
if (outcome.pdf) return sendPdf(res, outcome.pdf);
|
|
1136
|
+
if (outcome.file) {
|
|
1137
|
+
const uploads = await deps.uploads();
|
|
1138
|
+
return uploads.send(res, outcome.file);
|
|
1139
|
+
}
|
|
510
1140
|
return res.status(outcome.status).json(outcome.body);
|
|
511
1141
|
};
|
|
512
1142
|
|