@aglyn/plugins-commerce 1.0.0-beta.289 → 1.0.0-beta.290
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 +2 -2
- package/package.json +13 -13
- 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-quotes-card.component.js +20 -4
- package/src/lib/components/console/host-quotes-card.component.js.map +1 -1
- package/src/lib/components/console/order-invoice.component.d.ts +6 -4
- package/src/lib/components/console/order-invoice.component.js +67 -11
- package/src/lib/components/console/order-invoice.component.js.map +1 -1
- package/src/lib/components/console/quote-editor-dialog.component.js +53 -2
- package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/quote-proofs.component.d.ts +37 -0
- package/src/lib/components/console/quote-proofs.component.js +626 -0
- package/src/lib/components/console/quote-proofs.component.js.map +1 -0
- package/src/lib/components/quote-page-body.js +29 -1
- 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-quote-pay.d.ts +27 -0
- package/src/lib/model/commerce-quote-pay.js +36 -3
- 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-quotes.d.ts +59 -3
- package/src/lib/model/commerce-quotes.js.map +1 -1
- package/src/lib/model/index.d.ts +1 -0
- package/src/lib/model/index.js +1 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/server/commerce-pdf.d.ts +100 -0
- package/src/lib/server/commerce-pdf.js +283 -0
- package/src/lib/server/commerce-pdf.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/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/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-pay.d.ts +31 -4
- package/src/lib/server/quote-pay.js +11 -11
- 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 +14 -0
- package/src/lib/server/quote.js +336 -3
- 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.js +19 -1
- package/src/lib/server.js.map +1 -1
- package/src/lib/utils/commerce-document-pdf.d.ts +77 -0
- package/src/lib/utils/commerce-document-pdf.js +608 -0
- package/src/lib/utils/commerce-document-pdf.js.map +1 -0
- package/src/lib/utils/order-invoice.d.ts +52 -3
- package/src/lib/utils/order-invoice.js +116 -64
- package/src/lib/utils/order-invoice.js.map +1 -1
|
@@ -18,6 +18,9 @@ import { type PluginApiHandler, type PluginApiRequest } from '@aglyn/aglyn/serve
|
|
|
18
18
|
import { type PluginPersonRecord, type PluginPersonSearchRequest } from '@aglyn/aglyn/plugin-manager/plugin-person-records';
|
|
19
19
|
import { type SaleTaxEngineRequest, type SaleTaxEngineResult } from './tax-engine-quote';
|
|
20
20
|
import { type CheckoutSessionExpiry } from './checkout-session-expiry';
|
|
21
|
+
import type { quotePdfFile } from './commerce-pdf';
|
|
22
|
+
import { type CommercePdfFile } from './pdf-response';
|
|
23
|
+
import type { QuoteUploadCalls } from './quote-uploads';
|
|
21
24
|
export interface QuoteDeps {
|
|
22
25
|
verifyIdToken(token: string): Promise<{
|
|
23
26
|
uid: string;
|
|
@@ -36,12 +39,23 @@ export interface QuoteDeps {
|
|
|
36
39
|
expireSession(sessionId: string): Promise<CheckoutSessionExpiry>;
|
|
37
40
|
/** The quote's pay link at a link version, or null when the site has no address or no signing secret. */
|
|
38
41
|
payLinkUrl(host: Record<string, unknown>, hostId: string, quoteId: string, version: number): string | null;
|
|
42
|
+
/** The quote's PDF; loads the PDF writer and sharp only when asked. */
|
|
43
|
+
quotePdf(input: Parameters<typeof quotePdfFile>[0]): Promise<CommercePdfFile>;
|
|
44
|
+
/** The upload pipeline, loaded only by the proof actions. */
|
|
45
|
+
uploads(): Promise<QuoteUploadCalls>;
|
|
39
46
|
now(): number;
|
|
40
47
|
}
|
|
41
48
|
export declare function defaultQuoteDeps(): QuoteDeps;
|
|
42
49
|
type Outcome = {
|
|
43
50
|
status: number;
|
|
44
51
|
body: Record<string, unknown>;
|
|
52
|
+
pdf?: CommercePdfFile;
|
|
53
|
+
/** A proof or a customer's file, streamed by the handler. */
|
|
54
|
+
file?: {
|
|
55
|
+
path: string;
|
|
56
|
+
name: string;
|
|
57
|
+
contentType: string;
|
|
58
|
+
};
|
|
45
59
|
};
|
|
46
60
|
export declare function handleQuote(deps: QuoteDeps, req: PluginApiRequest): Promise<Outcome>;
|
|
47
61
|
/** `POST /api/commerce/quote`. */
|
package/src/lib/server/quote.js
CHANGED
|
@@ -26,6 +26,7 @@ import { commerceSitePermissionRefusal } from "../model/plugin-permissions.js";
|
|
|
26
26
|
import { holdsCommerceSiteKey } from "./commerce-site-keys.js";
|
|
27
27
|
import { quoteSaleTaxWithEngine } from "./tax-engine-quote.js";
|
|
28
28
|
import { expireCheckoutSession } from "./checkout-session-expiry.js";
|
|
29
|
+
import { sendPdf } from "./pdf-response.js";
|
|
29
30
|
import { quotePayLinkUrl } from "./quote-link-token.js";
|
|
30
31
|
export function defaultQuoteDeps() {
|
|
31
32
|
return {
|
|
@@ -42,6 +43,8 @@ export function defaultQuoteDeps() {
|
|
|
42
43
|
quoteTax: (request)=>quoteSaleTaxWithEngine(request),
|
|
43
44
|
expireSession: (sessionId)=>expireCheckoutSession(sessionId),
|
|
44
45
|
payLinkUrl: (host, hostId, quoteId, version)=>quotePayLinkUrl(host, hostId, quoteId, version),
|
|
46
|
+
quotePdf: async (input)=>(await import("./commerce-pdf.js")).quotePdfFile(input),
|
|
47
|
+
uploads: async ()=>(await import("./quote-uploads.js")).quoteUploadCalls(),
|
|
45
48
|
now: ()=>Date.now()
|
|
46
49
|
};
|
|
47
50
|
}
|
|
@@ -281,6 +284,54 @@ export async function handleQuote(deps, req) {
|
|
|
281
284
|
}
|
|
282
285
|
};
|
|
283
286
|
}
|
|
287
|
+
if (PROOF_ACTIONS.has(action)) {
|
|
288
|
+
var _body_quoteId2;
|
|
289
|
+
const quoteId = String((_body_quoteId2 = body['quoteId']) != null ? _body_quoteId2 : '');
|
|
290
|
+
if (!DOC_ID.test(quoteId)) return refuse(400, 'Missing quoteId');
|
|
291
|
+
return handleProofAction(deps, {
|
|
292
|
+
action,
|
|
293
|
+
body,
|
|
294
|
+
hostId,
|
|
295
|
+
quoteId,
|
|
296
|
+
hostRef,
|
|
297
|
+
firestore,
|
|
298
|
+
uid: decoded.uid,
|
|
299
|
+
actor
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
if (action === 'pdf') {
|
|
303
|
+
var _body_quoteId3, _host_data1;
|
|
304
|
+
const quoteId = String((_body_quoteId3 = body['quoteId']) != null ? _body_quoteId3 : '');
|
|
305
|
+
if (!DOC_ID.test(quoteId)) return refuse(400, 'Missing quoteId');
|
|
306
|
+
const [snapshot, store] = await Promise.all([
|
|
307
|
+
hostRef.collection('orders').doc(quoteId).get(),
|
|
308
|
+
hostRef.collection('settings').doc('store').get()
|
|
309
|
+
]);
|
|
310
|
+
const order = snapshot.exists ? snapshot.data() : null;
|
|
311
|
+
if (!order || !CommerceModel.isQuote(order) || !order.quote) return refuse(404, 'Unknown quote');
|
|
312
|
+
const hostData = (_host_data1 = host.data()) != null ? _host_data1 : {};
|
|
313
|
+
// The pay link is "Send quotes and invoices to customers"; without it the PDF leaves the link off.
|
|
314
|
+
const payUrl = CommerceModel.isUnpaidQuote(order) && holdsCommerceSiteKey(host, decoded.uid, 'commerce.quotes.send') ? deps.payLinkUrl(hostData, hostId, quoteId, CommerceModel.quoteLinkVersion(order.quote)) : null;
|
|
315
|
+
try {
|
|
316
|
+
var _store_data;
|
|
317
|
+
const pdf = await deps.quotePdf({
|
|
318
|
+
hostId,
|
|
319
|
+
quoteId,
|
|
320
|
+
order,
|
|
321
|
+
host: hostData,
|
|
322
|
+
store: (_store_data = store.data()) != null ? _store_data : null,
|
|
323
|
+
payUrl
|
|
324
|
+
});
|
|
325
|
+
return {
|
|
326
|
+
status: 200,
|
|
327
|
+
body: {},
|
|
328
|
+
pdf
|
|
329
|
+
};
|
|
330
|
+
} catch (error) {
|
|
331
|
+
console.error('commerce quote pdf failed', error);
|
|
332
|
+
return refuse(500, 'The PDF could not be made. Try again.');
|
|
333
|
+
}
|
|
334
|
+
}
|
|
284
335
|
if (action === 'search-customers') {
|
|
285
336
|
var _body_text;
|
|
286
337
|
const text = String((_body_text = body['text']) != null ? _body_text : '').trim().slice(0, 120);
|
|
@@ -332,8 +383,8 @@ export async function handleQuote(deps, req) {
|
|
|
332
383
|
if (!DOC_ID.test(quoteId)) return refuse(400, 'Missing quoteId');
|
|
333
384
|
const ref = hostRef.collection('orders').doc(quoteId);
|
|
334
385
|
const deleted = await firestore.runTransaction(async (transaction)=>{
|
|
335
|
-
var _ref;
|
|
336
|
-
var _order_quote;
|
|
386
|
+
var _ref, _ref1;
|
|
387
|
+
var _order_quote, _order_quote1;
|
|
337
388
|
const snapshot = await transaction.get(ref);
|
|
338
389
|
// Already gone is what a retried delete finds.
|
|
339
390
|
if (!snapshot.exists) return {
|
|
@@ -350,6 +401,11 @@ export async function handleQuote(deps, req) {
|
|
|
350
401
|
if (((_ref = (_order_quote = order.quote) == null ? void 0 : _order_quote.stage) != null ? _ref : 'draft') !== 'draft') {
|
|
351
402
|
return refuse(409, 'Only a draft quote can be deleted.');
|
|
352
403
|
}
|
|
404
|
+
// Earlier revisions are what the customer was shown and answered; a
|
|
405
|
+
// quote that has them stays on record.
|
|
406
|
+
if (Number((_ref1 = (_order_quote1 = order.quote) == null ? void 0 : _order_quote1.revision) != null ? _ref1 : 1) > 1) {
|
|
407
|
+
return refuse(409, 'This quote has earlier revisions the customer saw, so it stays on record.');
|
|
408
|
+
}
|
|
353
409
|
transaction.delete(ref);
|
|
354
410
|
return {
|
|
355
411
|
status: 200,
|
|
@@ -363,6 +419,8 @@ export async function handleQuote(deps, req) {
|
|
|
363
419
|
"closeSession"
|
|
364
420
|
]);
|
|
365
421
|
await closeOpenSession(deps, closeSession, 'quote deleted');
|
|
422
|
+
// Its proofs and anything the customer sent go with it.
|
|
423
|
+
if (outcome.status === 200) await removeQuoteFiles(deps, hostId, quoteId);
|
|
366
424
|
return outcome;
|
|
367
425
|
}
|
|
368
426
|
if (action !== 'create' && action !== 'update') return refuse(400, 'Unknown action');
|
|
@@ -505,8 +563,283 @@ export async function handleQuote(deps, req) {
|
|
|
505
563
|
return refuse(500, 'The quote could not be saved');
|
|
506
564
|
}
|
|
507
565
|
}
|
|
566
|
+
/*==========================================
|
|
567
|
+
* PROOFS AND REVISIONS (AGL-4096)
|
|
568
|
+
*=========================================*/ const PROOF_ACTIONS = new Set([
|
|
569
|
+
'proof-ticket',
|
|
570
|
+
'proof-finalize',
|
|
571
|
+
'proof-remove',
|
|
572
|
+
'start-revision',
|
|
573
|
+
'revisions',
|
|
574
|
+
'file'
|
|
575
|
+
]);
|
|
576
|
+
/** Deletes a quote's kept files, its proofs and the customer's artwork; never throws. */ async function removeQuoteFiles(deps, hostId, quoteId) {
|
|
577
|
+
try {
|
|
578
|
+
const uploads = await deps.uploads();
|
|
579
|
+
await Promise.all([
|
|
580
|
+
CommerceModel.QUOTE_PROOF_PURPOSE,
|
|
581
|
+
CommerceModel.QUOTE_ARTWORK_PURPOSE
|
|
582
|
+
].map((purpose)=>uploads.removeAll({
|
|
583
|
+
hostId,
|
|
584
|
+
purpose,
|
|
585
|
+
purposeId: quoteId
|
|
586
|
+
})));
|
|
587
|
+
} catch (error) {
|
|
588
|
+
console.error('quote files were not removed', {
|
|
589
|
+
hostId,
|
|
590
|
+
quoteId,
|
|
591
|
+
error
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
/** A file as the console is told about it: everything but where it is kept. */ function fileView(file) {
|
|
596
|
+
const rest = _extends({}, file);
|
|
597
|
+
delete rest.path;
|
|
598
|
+
return rest;
|
|
599
|
+
}
|
|
600
|
+
/** A revision as the console's history lists it. */ function revisionView(snapshot) {
|
|
601
|
+
var _snapshot_proofs, _snapshot_responses;
|
|
602
|
+
return _extends({}, snapshot, {
|
|
603
|
+
proofs: ((_snapshot_proofs = snapshot.proofs) != null ? _snapshot_proofs : []).map(fileView),
|
|
604
|
+
responses: ((_snapshot_responses = snapshot.responses) != null ? _snapshot_responses : []).map((response)=>_extends({}, response, response.files ? {
|
|
605
|
+
files: response.files.map(fileView)
|
|
606
|
+
} : {}))
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Writes `next` over the quote in `transaction`, without the pay page's open
|
|
611
|
+
* session, which was priced from the quote as it stood; the caller closes it.
|
|
612
|
+
*/ function writeEditedQuote(transaction, ref, next) {
|
|
613
|
+
const rest = _extends({}, next);
|
|
614
|
+
const session = rest.checkoutSessionId;
|
|
615
|
+
delete rest.checkoutSessionId;
|
|
616
|
+
transaction.set(ref, rest);
|
|
617
|
+
return session;
|
|
618
|
+
}
|
|
619
|
+
async function handleProofAction(deps, context) {
|
|
620
|
+
const { action, body, hostId, quoteId, hostRef, firestore, uid, actor } = context;
|
|
621
|
+
const ref = hostRef.collection('orders').doc(quoteId);
|
|
622
|
+
const proofScope = {
|
|
623
|
+
hostId,
|
|
624
|
+
purpose: CommerceModel.QUOTE_PROOF_PURPOSE,
|
|
625
|
+
purposeId: quoteId
|
|
626
|
+
};
|
|
627
|
+
if (action === 'revisions' || action === 'file') {
|
|
628
|
+
var _order_quote_revision, _body_fileId, _order_quote_responses, _ref, _ref1;
|
|
629
|
+
const snapshot = await ref.get();
|
|
630
|
+
const order = snapshot.exists ? snapshot.data() : null;
|
|
631
|
+
if (!order || !CommerceModel.isQuote(order) || !order.quote) return refuse(404, 'Unknown quote');
|
|
632
|
+
const current = Number((_order_quote_revision = order.quote.revision) != null ? _order_quote_revision : 1);
|
|
633
|
+
const wanted = body['revision'] === undefined || body['revision'] === null ? current : Number(body['revision']);
|
|
634
|
+
let snapshots = [];
|
|
635
|
+
if (action === 'revisions' || wanted !== current) {
|
|
636
|
+
const listed = await ref.collection('revisions').get();
|
|
637
|
+
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));
|
|
638
|
+
}
|
|
639
|
+
if (action === 'revisions') return {
|
|
640
|
+
status: 200,
|
|
641
|
+
body: {
|
|
642
|
+
revisions: snapshots.map(revisionView)
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
const fileId = String((_body_fileId = body['fileId']) != null ? _body_fileId : '');
|
|
646
|
+
const source = wanted === current ? {
|
|
647
|
+
proofs: CommerceModel.currentQuoteProofs(order.quote),
|
|
648
|
+
responses: (_order_quote_responses = order.quote.responses) != null ? _order_quote_responses : []
|
|
649
|
+
} : snapshots.find((entry)=>Number(entry.revision) === wanted);
|
|
650
|
+
const files = [
|
|
651
|
+
...(_ref = source == null ? void 0 : source.proofs) != null ? _ref : [],
|
|
652
|
+
...((_ref1 = source == null ? void 0 : source.responses) != null ? _ref1 : []).flatMap((response)=>{
|
|
653
|
+
var _response_files;
|
|
654
|
+
return (_response_files = response.files) != null ? _response_files : [];
|
|
655
|
+
})
|
|
656
|
+
];
|
|
657
|
+
const file = files.find((entry)=>entry.id === fileId);
|
|
658
|
+
if (!(file == null ? void 0 : file.path)) return refuse(404, 'That file is no longer on this quote.');
|
|
659
|
+
return {
|
|
660
|
+
status: 200,
|
|
661
|
+
body: {},
|
|
662
|
+
file: {
|
|
663
|
+
path: file.path,
|
|
664
|
+
name: file.name,
|
|
665
|
+
contentType: file.contentType
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
if (action === 'proof-ticket') {
|
|
670
|
+
const snapshot = await ref.get();
|
|
671
|
+
const order = snapshot.exists ? snapshot.data() : null;
|
|
672
|
+
const refused = CommerceModel.quoteProofChangeRefusal(order, 'add');
|
|
673
|
+
if (refused) return refuse(refused === 'not-a-quote' ? 404 : 409, CommerceModel.quoteProofRefusalMessage(refused));
|
|
674
|
+
// Storage is spent here, so the plan must sell, as a save's must.
|
|
675
|
+
const owner = await deps.orgForHost(hostId);
|
|
676
|
+
if (!checkEntitlement(owner == null ? void 0 : owner.org, 'commerce')) return refuse(403, 'Selling is not enabled');
|
|
677
|
+
const uploads = await deps.uploads();
|
|
678
|
+
const minted = await uploads.mint({
|
|
679
|
+
scope: proofScope,
|
|
680
|
+
request: body['file'],
|
|
681
|
+
caller: {
|
|
682
|
+
uid
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
if ('error' in minted) return refuse(minted.status, minted.error);
|
|
686
|
+
return {
|
|
687
|
+
status: 200,
|
|
688
|
+
body: {
|
|
689
|
+
ticket: minted.ticket,
|
|
690
|
+
upload: minted.upload,
|
|
691
|
+
file: minted.file
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
if (action === 'proof-finalize') {
|
|
696
|
+
const uploads = await deps.uploads();
|
|
697
|
+
const kept = await uploads.finalize({
|
|
698
|
+
scope: proofScope,
|
|
699
|
+
ticket: body['ticket'],
|
|
700
|
+
caller: {
|
|
701
|
+
uid
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
if ('error' in kept) return refuse(kept.status, kept.error);
|
|
705
|
+
const now = deps.now();
|
|
706
|
+
const answer = await firestore.runTransaction(async (transaction)=>{
|
|
707
|
+
const snapshot = await transaction.get(ref);
|
|
708
|
+
const order = snapshot.exists ? snapshot.data() : null;
|
|
709
|
+
if (!order) return {
|
|
710
|
+
outcome: refuse(404, 'Unknown quote')
|
|
711
|
+
};
|
|
712
|
+
// A retried finalize finds its proof already on the quote.
|
|
713
|
+
const already = CommerceModel.currentQuoteProofs(order.quote).find((proof)=>proof.id === kept.file.id);
|
|
714
|
+
if (already) {
|
|
715
|
+
var _ref;
|
|
716
|
+
var _order_quote;
|
|
717
|
+
return {
|
|
718
|
+
outcome: {
|
|
719
|
+
status: 200,
|
|
720
|
+
body: {
|
|
721
|
+
proof: fileView(already),
|
|
722
|
+
updatedAtMs: (_ref = (_order_quote = order.quote) == null ? void 0 : _order_quote.updatedAtMs) != null ? _ref : null
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
const added = CommerceModel.withQuoteProofAdded(order, {
|
|
728
|
+
id: kept.file.id,
|
|
729
|
+
name: kept.file.name,
|
|
730
|
+
contentType: kept.file.contentType,
|
|
731
|
+
size: kept.file.size,
|
|
732
|
+
path: kept.file.path
|
|
733
|
+
}, {
|
|
734
|
+
atMs: now,
|
|
735
|
+
by: actor
|
|
736
|
+
});
|
|
737
|
+
if ('reason' in added) {
|
|
738
|
+
return {
|
|
739
|
+
outcome: refuse(added.reason === 'not-a-quote' ? 404 : 409, CommerceModel.quoteProofRefusalMessage(added.reason))
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
const closeSession = writeEditedQuote(transaction, ref, added.order);
|
|
743
|
+
return {
|
|
744
|
+
outcome: {
|
|
745
|
+
status: 200,
|
|
746
|
+
body: {
|
|
747
|
+
proof: fileView(added.proof),
|
|
748
|
+
updatedAtMs: now
|
|
749
|
+
}
|
|
750
|
+
},
|
|
751
|
+
closeSession
|
|
752
|
+
};
|
|
753
|
+
});
|
|
754
|
+
if (answer.outcome.status !== 200) await uploads.remove(kept.file.path).catch(()=>undefined);
|
|
755
|
+
await closeOpenSession(deps, answer.closeSession, 'quote proof added');
|
|
756
|
+
return answer.outcome;
|
|
757
|
+
}
|
|
758
|
+
if (action === 'proof-remove') {
|
|
759
|
+
var _body_proofId;
|
|
760
|
+
const proofId = String((_body_proofId = body['proofId']) != null ? _body_proofId : '');
|
|
761
|
+
const now = deps.now();
|
|
762
|
+
const answer = await firestore.runTransaction(async (transaction)=>{
|
|
763
|
+
const snapshot = await transaction.get(ref);
|
|
764
|
+
const order = snapshot.exists ? snapshot.data() : null;
|
|
765
|
+
if (!order) return {
|
|
766
|
+
outcome: refuse(404, 'Unknown quote')
|
|
767
|
+
};
|
|
768
|
+
const removed = CommerceModel.withQuoteProofRemoved(order, proofId, {
|
|
769
|
+
atMs: now,
|
|
770
|
+
by: actor
|
|
771
|
+
});
|
|
772
|
+
if ('reason' in removed) {
|
|
773
|
+
return {
|
|
774
|
+
outcome: refuse(removed.reason === 'not-a-quote' || removed.reason === 'unknown-proof' ? 404 : 409, CommerceModel.quoteProofRefusalMessage(removed.reason))
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
const closeSession = writeEditedQuote(transaction, ref, removed.order);
|
|
778
|
+
return {
|
|
779
|
+
outcome: {
|
|
780
|
+
status: 200,
|
|
781
|
+
body: {
|
|
782
|
+
removed: true,
|
|
783
|
+
updatedAtMs: now
|
|
784
|
+
}
|
|
785
|
+
},
|
|
786
|
+
closeSession,
|
|
787
|
+
path: removed.proof.path
|
|
788
|
+
};
|
|
789
|
+
});
|
|
790
|
+
if (answer.path) {
|
|
791
|
+
const uploads = await deps.uploads();
|
|
792
|
+
await uploads.remove(answer.path).catch((error)=>console.error('quote proof file was not deleted', error));
|
|
793
|
+
}
|
|
794
|
+
await closeOpenSession(deps, answer.closeSession, 'quote proof removed');
|
|
795
|
+
return answer.outcome;
|
|
796
|
+
}
|
|
797
|
+
// start-revision
|
|
798
|
+
const now = deps.now();
|
|
799
|
+
const answer = await firestore.runTransaction(async (transaction)=>{
|
|
800
|
+
const snapshot = await transaction.get(ref);
|
|
801
|
+
const order = snapshot.exists ? snapshot.data() : null;
|
|
802
|
+
if (!order) return {
|
|
803
|
+
outcome: refuse(404, 'Unknown quote')
|
|
804
|
+
};
|
|
805
|
+
const started = CommerceModel.startQuoteRevision(order, {
|
|
806
|
+
atMs: now,
|
|
807
|
+
by: actor
|
|
808
|
+
});
|
|
809
|
+
if ('reason' in started) return {
|
|
810
|
+
outcome: refuse(started.reason === 'paid' ? 409 : 404, started.error)
|
|
811
|
+
};
|
|
812
|
+
const revisionRef = ref.collection('revisions').doc(String(started.revision));
|
|
813
|
+
// A revision on record is never written over.
|
|
814
|
+
if ((await transaction.get(revisionRef)).exists) {
|
|
815
|
+
return {
|
|
816
|
+
outcome: refuse(409, `Revision ${started.revision} is already on record. Close the quote and open it again.`)
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
transaction.set(revisionRef, started.snapshot);
|
|
820
|
+
const closeSession = writeEditedQuote(transaction, ref, started.order);
|
|
821
|
+
return {
|
|
822
|
+
outcome: {
|
|
823
|
+
status: 200,
|
|
824
|
+
body: {
|
|
825
|
+
revision: started.revision + 1,
|
|
826
|
+
updatedAtMs: now
|
|
827
|
+
}
|
|
828
|
+
},
|
|
829
|
+
closeSession
|
|
830
|
+
};
|
|
831
|
+
});
|
|
832
|
+
await closeOpenSession(deps, answer.closeSession, 'quote revision started');
|
|
833
|
+
return answer.outcome;
|
|
834
|
+
}
|
|
508
835
|
/** `POST /api/commerce/quote`. */ export const quoteHandler = async (req, res)=>{
|
|
509
|
-
const
|
|
836
|
+
const deps = defaultQuoteDeps();
|
|
837
|
+
const outcome = await handleQuote(deps, req);
|
|
838
|
+
if (outcome.pdf) return sendPdf(res, outcome.pdf);
|
|
839
|
+
if (outcome.file) {
|
|
840
|
+
const uploads = await deps.uploads();
|
|
841
|
+
return uploads.send(res, outcome.file);
|
|
842
|
+
}
|
|
510
843
|
return res.status(outcome.status).json(outcome.body);
|
|
511
844
|
};
|
|
512
845
|
|