@aglyn/plugins-commerce 1.0.0-beta.288 → 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/commerce-console-page.js +29 -2
- package/src/lib/components/commerce-console-page.js.map +1 -1
- package/src/lib/components/console/commerce-analytics-card.component.js +4 -2
- package/src/lib/components/console/commerce-analytics-card.component.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.js +5 -1
- package/src/lib/components/console/host-orders-card.component.js.map +1 -1
- package/src/lib/components/console/host-quotes-card.component.d.ts +34 -0
- package/src/lib/components/console/host-quotes-card.component.js +263 -0
- package/src/lib/components/console/host-quotes-card.component.js.map +1 -0
- 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/pos-ops/pos-check-ins-card.component.js +33 -28
- package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js.map +1 -1
- package/src/lib/components/console/quote-editor-dialog.component.d.ts +45 -0
- package/src/lib/components/console/quote-editor-dialog.component.js +1317 -0
- package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -0
- package/src/lib/components/console/quote-pay-link.component.d.ts +38 -0
- package/src/lib/components/console/quote-pay-link.component.js +213 -0
- package/src/lib/components/console/quote-pay-link.component.js.map +1 -0
- 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/console/quote-settings-card.component.d.ts +29 -0
- package/src/lib/components/console/quote-settings-card.component.js +121 -0
- package/src/lib/components/console/quote-settings-card.component.js.map +1 -0
- package/src/lib/components/console/tax-settings-card.component.js +4 -1
- package/src/lib/components/console/tax-settings-card.component.js.map +1 -1
- package/src/lib/components/quote-page-body.d.ts +33 -0
- package/src/lib/components/quote-page-body.js +724 -0
- package/src/lib/components/quote-page-body.js.map +1 -0
- package/src/lib/components/quote-page.d.ts +29 -0
- package/src/lib/components/quote-page.js +96 -0
- package/src/lib/components/quote-page.js.map +1 -0
- 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/components/storefront-payment-element.d.ts +7 -1
- package/src/lib/components/storefront-payment-element.js +8 -3
- package/src/lib/components/storefront-payment-element.js.map +1 -1
- package/src/lib/constants/orders-list-query.js +6 -1
- package/src/lib/constants/orders-list-query.js.map +1 -1
- package/src/lib/constants/quote-page.d.ts +30 -0
- package/src/lib/constants/quote-page.js +35 -0
- package/src/lib/constants/quote-page.js.map +1 -0
- package/src/lib/model/commerce-orders.d.ts +25 -2
- package/src/lib/model/commerce-orders.js +10 -1
- package/src/lib/model/commerce-orders.js.map +1 -1
- package/src/lib/model/commerce-quote-pay.d.ts +256 -0
- package/src/lib/model/commerce-quote-pay.js +426 -0
- package/src/lib/model/commerce-quote-pay.js.map +1 -0
- 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 +335 -0
- package/src/lib/model/commerce-quotes.js +522 -0
- package/src/lib/model/commerce-quotes.js.map +1 -0
- 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/model/plugin-permissions.d.ts +1 -1
- package/src/lib/model/plugin-permissions.js +20 -0
- package/src/lib/model/plugin-permissions.js.map +1 -1
- package/src/lib/server/api-v1/orders-and-products.js +6 -2
- package/src/lib/server/api-v1/orders-and-products.js.map +1 -1
- package/src/lib/server/billing-webhook.js +127 -52
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/checkout-session-expiry.d.ts +36 -0
- package/src/lib/server/checkout-session-expiry.js +63 -0
- package/src/lib/server/checkout-session-expiry.js.map +1 -0
- 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/draft-order.js +16 -8
- package/src/lib/server/draft-order.js.map +1 -1
- 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/person-eraser.d.ts +7 -0
- package/src/lib/server/person-eraser.js +24 -2
- 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/pos-return.js +3 -1
- package/src/lib/server/pos-return.js.map +1 -1
- package/src/lib/server/quote-link-token.d.ts +39 -0
- package/src/lib/server/quote-link-token.js +97 -0
- package/src/lib/server/quote-link-token.js.map +1 -0
- package/src/lib/server/quote-pay.d.ts +110 -0
- package/src/lib/server/quote-pay.js +606 -0
- package/src/lib/server/quote-pay.js.map +1 -0
- 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 +63 -0
- package/src/lib/server/quote.js +846 -0
- package/src/lib/server/quote.js.map +1 -0
- package/src/lib/server/related.js +2 -0
- package/src/lib/server/related.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.d.ts +2 -0
- package/src/lib/server/site-page-resolver.js +75 -0
- package/src/lib/server/site-page-resolver.js.map +1 -1
- package/src/lib/server.js +33 -1
- package/src/lib/server.js.map +1 -1
- package/src/lib/site.js +6 -0
- package/src/lib/site.js.map +1 -1
- package/src/lib/transfer/tracking.server.js +6 -0
- package/src/lib/transfer/tracking.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
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
|
|
3
|
+
/**
|
|
4
|
+
* @license
|
|
5
|
+
* Copyright 2026 Aglyn LLC
|
|
6
|
+
*
|
|
7
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
* you may not use this file except in compliance with the License.
|
|
9
|
+
* You may obtain a copy of the License at
|
|
10
|
+
*
|
|
11
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
*
|
|
13
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
* See the License for the specific language governing permissions and
|
|
17
|
+
* limitations under the License.
|
|
18
|
+
*/ import { claimAttempt } from "@aglyn/aglyn/server";
|
|
19
|
+
import { createResourceUid } from "@aglyn/aglyn/app-utils/create-resource-uid";
|
|
20
|
+
import { checkEntitlement } from "@aglyn/aglyn/server";
|
|
21
|
+
import { searchPluginPeople } from "@aglyn/aglyn/plugin-manager/plugin-person-records";
|
|
22
|
+
import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
|
|
23
|
+
import { isRefusedIdToken } from "@aglyn/tenant-data-admin/server/id-token-refusal";
|
|
24
|
+
import * as CommerceModel from "../model/index.js";
|
|
25
|
+
import { commerceSitePermissionRefusal } from "../model/plugin-permissions.js";
|
|
26
|
+
import { holdsCommerceSiteKey } from "./commerce-site-keys.js";
|
|
27
|
+
import { quoteSaleTaxWithEngine } from "./tax-engine-quote.js";
|
|
28
|
+
import { expireCheckoutSession } from "./checkout-session-expiry.js";
|
|
29
|
+
import { sendPdf } from "./pdf-response.js";
|
|
30
|
+
import { quotePayLinkUrl } from "./quote-link-token.js";
|
|
31
|
+
export function defaultQuoteDeps() {
|
|
32
|
+
return {
|
|
33
|
+
verifyIdToken: (token)=>firebaseAdmin.app().auth().verifyIdToken(token),
|
|
34
|
+
firestore: ()=>firebaseAdmin.app().firestore(),
|
|
35
|
+
orgForHost: async (hostId)=>{
|
|
36
|
+
const found = await getOrgForHost(hostId);
|
|
37
|
+
return found ? {
|
|
38
|
+
id: found.orgId,
|
|
39
|
+
org: found.org
|
|
40
|
+
} : null;
|
|
41
|
+
},
|
|
42
|
+
searchPeople: (request)=>searchPluginPeople(request),
|
|
43
|
+
quoteTax: (request)=>quoteSaleTaxWithEngine(request),
|
|
44
|
+
expireSession: (sessionId)=>expireCheckoutSession(sessionId),
|
|
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(),
|
|
48
|
+
now: ()=>Date.now()
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Expires the session the pay page opened, when there is one; a failure is logged, never thrown. */ async function closeOpenSession(deps, sessionId, context) {
|
|
52
|
+
if (!sessionId) return;
|
|
53
|
+
const result = await deps.expireSession(sessionId).catch(()=>'open');
|
|
54
|
+
if (result === 'open') {
|
|
55
|
+
console.error(`${context}: the quote's Stripe session was not expired and may still be paid`, {
|
|
56
|
+
sessionId
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const refuse = (status, error)=>({
|
|
61
|
+
status,
|
|
62
|
+
body: {
|
|
63
|
+
error
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
function bodyOf(req) {
|
|
67
|
+
const raw = req.body;
|
|
68
|
+
if (typeof raw === 'string') {
|
|
69
|
+
try {
|
|
70
|
+
const parsed = JSON.parse(raw);
|
|
71
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
72
|
+
} catch (unused) {
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return raw && typeof raw === 'object' ? raw : {};
|
|
77
|
+
}
|
|
78
|
+
function headerOf(req, name) {
|
|
79
|
+
var _req_headers, _headers_name_toLowerCase, _value_;
|
|
80
|
+
const headers = (_req_headers = req.headers) != null ? _req_headers : {};
|
|
81
|
+
const value = (_headers_name_toLowerCase = headers[name.toLowerCase()]) != null ? _headers_name_toLowerCase : headers[name];
|
|
82
|
+
return String(Array.isArray(value) ? (_value_ = value[0]) != null ? _value_ : '' : value != null ? value : '').trim();
|
|
83
|
+
}
|
|
84
|
+
const DOC_ID = /^(?!__.*__$)[^/]{1,200}$/;
|
|
85
|
+
/** The ship-to as a tax service reads it, or `null` to tax at the store's address. */ function serviceAddress(shipTo) {
|
|
86
|
+
if (!(shipTo == null ? void 0 : shipTo.country)) return null;
|
|
87
|
+
return _extends({
|
|
88
|
+
country: shipTo.country
|
|
89
|
+
}, shipTo.line1 ? {
|
|
90
|
+
line1: shipTo.line1
|
|
91
|
+
} : {}, shipTo.line2 ? {
|
|
92
|
+
line2: shipTo.line2
|
|
93
|
+
} : {}, shipTo.city ? {
|
|
94
|
+
city: shipTo.city
|
|
95
|
+
} : {}, shipTo.state ? {
|
|
96
|
+
region: shipTo.state
|
|
97
|
+
} : {}, shipTo.postalCode ? {
|
|
98
|
+
postalCode: shipTo.postalCode
|
|
99
|
+
} : {});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Prices a quote: each product line from its product, the tax from the
|
|
103
|
+
* store's settings and, where the site connected one, its tax service.
|
|
104
|
+
*/ async function priceQuote(deps, hostRef, hostId, input) {
|
|
105
|
+
var _store_get, _ref;
|
|
106
|
+
var _input_shipping;
|
|
107
|
+
const productIds = [
|
|
108
|
+
...new Set(input.lines.flatMap((line)=>line.kind === 'catalog' ? [
|
|
109
|
+
line.productId
|
|
110
|
+
] : []))
|
|
111
|
+
];
|
|
112
|
+
const productDocs = await Promise.all(productIds.map((id)=>hostRef.collection('products').doc(id).get()));
|
|
113
|
+
const products = new Map(productDocs.map((snapshot, index)=>[
|
|
114
|
+
productIds[index],
|
|
115
|
+
snapshot.exists ? snapshot.data() : null
|
|
116
|
+
]));
|
|
117
|
+
const lineItems = [];
|
|
118
|
+
for (const line of input.lines){
|
|
119
|
+
if (line.kind === 'custom') {
|
|
120
|
+
lineItems.push(CommerceModel.priceQuoteCustomLine(line));
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const priced = CommerceModel.priceQuoteCatalogLine(products.get(line.productId), line);
|
|
124
|
+
if ('error' in priced) return refuse(400, priced.error);
|
|
125
|
+
lineItems.push(priced.line);
|
|
126
|
+
}
|
|
127
|
+
const store = await hostRef.collection('settings').doc('store').get();
|
|
128
|
+
const taxSettings = (_store_get = store.get('tax')) != null ? _store_get : {};
|
|
129
|
+
let tax = CommerceModel.quoteTaxBasisFromSettings(taxSettings, input.shipTo);
|
|
130
|
+
const shippingCents = (_ref = (_input_shipping = input.shipping) == null ? void 0 : _input_shipping.amountCents) != null ? _ref : 0;
|
|
131
|
+
const amounts = CommerceModel.quoteLineAmounts(lineItems, input.discount);
|
|
132
|
+
// A tax service answers only for a store on its own rates; it is asked about
|
|
133
|
+
// what each line is charged after its share of the discount.
|
|
134
|
+
if (taxSettings.mode === 'manual' && amounts.itemsCents > 0) {
|
|
135
|
+
var _input_customer_email;
|
|
136
|
+
const serviceLines = lineItems.map((line, index)=>({
|
|
137
|
+
line,
|
|
138
|
+
index,
|
|
139
|
+
amountCents: amounts.netCents[index]
|
|
140
|
+
})).filter((entry)=>entry.amountCents > 0);
|
|
141
|
+
const answer = await deps.quoteTax(_extends({
|
|
142
|
+
hostId,
|
|
143
|
+
settings: taxSettings,
|
|
144
|
+
channel: 'invoice',
|
|
145
|
+
lines: serviceLines.map(({ line, index, amountCents })=>_extends({
|
|
146
|
+
id: String(index)
|
|
147
|
+
}, line.productId ? {
|
|
148
|
+
productId: line.productId
|
|
149
|
+
} : {}, line.variantId ? {
|
|
150
|
+
variantId: line.variantId
|
|
151
|
+
} : {}, line.sku ? {
|
|
152
|
+
sku: line.sku
|
|
153
|
+
} : {}, {
|
|
154
|
+
description: [
|
|
155
|
+
line.name,
|
|
156
|
+
line.variantLabel
|
|
157
|
+
].filter(Boolean).join(' — '),
|
|
158
|
+
quantity: line.quantity,
|
|
159
|
+
amountCents
|
|
160
|
+
}, line.taxExempt ? {
|
|
161
|
+
exempt: true
|
|
162
|
+
} : {}))
|
|
163
|
+
}, shippingCents ? {
|
|
164
|
+
shippingCents
|
|
165
|
+
} : {}, {
|
|
166
|
+
shipTo: serviceAddress(input.shipTo),
|
|
167
|
+
customerEmail: (_input_customer_email = input.customer.email) != null ? _input_customer_email : null
|
|
168
|
+
}));
|
|
169
|
+
if (answer.quote) {
|
|
170
|
+
tax = {
|
|
171
|
+
kind: 'service',
|
|
172
|
+
taxCents: answer.quote.taxCents,
|
|
173
|
+
providerLabel: answer.quote.providerLabel || answer.quote.provider
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const totals = CommerceModel.computeQuoteTotals({
|
|
178
|
+
lines: lineItems,
|
|
179
|
+
discount: input.discount,
|
|
180
|
+
shippingCents,
|
|
181
|
+
tax
|
|
182
|
+
});
|
|
183
|
+
return {
|
|
184
|
+
lineItems,
|
|
185
|
+
totals: CommerceModel.quoteOrderTotals(totals),
|
|
186
|
+
tax,
|
|
187
|
+
taxMode: CommerceModel.quoteTaxMode(tax, totals.taxCents),
|
|
188
|
+
requireProofApprovalDefault: CommerceModel.quoteRequiresProofApprovalByDefault(store.get('quotes'))
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/** The editable half of the `quote` section, every key written so a save clears what was removed. */ function editedQuoteFields(input) {
|
|
192
|
+
return {
|
|
193
|
+
customer: input.customer,
|
|
194
|
+
billTo: input.billTo,
|
|
195
|
+
shipTo: input.shipTo,
|
|
196
|
+
notes: input.notes,
|
|
197
|
+
terms: input.terms,
|
|
198
|
+
discount: input.discount,
|
|
199
|
+
shipping: input.shipping
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
export async function handleQuote(deps, req) {
|
|
203
|
+
var _body_hostId, _body_action, _body_quoteId, _ref;
|
|
204
|
+
if (req.method !== 'POST') return refuse(405, 'Method not allowed');
|
|
205
|
+
const authorization = headerOf(req, 'authorization');
|
|
206
|
+
const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : '';
|
|
207
|
+
if (!idToken) return refuse(401, 'Unauthenticated');
|
|
208
|
+
const body = bodyOf(req);
|
|
209
|
+
const hostId = String((_body_hostId = body['hostId']) != null ? _body_hostId : '');
|
|
210
|
+
const action = String((_body_action = body['action']) != null ? _body_action : '');
|
|
211
|
+
if (!DOC_ID.test(hostId)) return refuse(400, 'Missing hostId');
|
|
212
|
+
let decoded;
|
|
213
|
+
try {
|
|
214
|
+
decoded = await deps.verifyIdToken(idToken);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (!isRefusedIdToken(error)) throw error;
|
|
217
|
+
return refuse(401, 'Unauthenticated');
|
|
218
|
+
}
|
|
219
|
+
const firestore = deps.firestore();
|
|
220
|
+
const hostRef = firestore.collection('hosts').doc(hostId);
|
|
221
|
+
const host = await hostRef.get();
|
|
222
|
+
if (!host.exists) return refuse(404, 'Unknown site');
|
|
223
|
+
const key = action === 'pay-link' || action === 'replace-link' ? 'commerce.quotes.send' : 'commerce.quotes';
|
|
224
|
+
if (!holdsCommerceSiteKey(host, decoded.uid, key)) {
|
|
225
|
+
return refuse(403, commerceSitePermissionRefusal(key));
|
|
226
|
+
}
|
|
227
|
+
const actor = String(decoded.email || decoded.uid);
|
|
228
|
+
if (action === 'pay-link' || action === 'replace-link') {
|
|
229
|
+
var _body_quoteId1, _host_data;
|
|
230
|
+
const quoteId = String((_body_quoteId1 = body['quoteId']) != null ? _body_quoteId1 : '');
|
|
231
|
+
if (!DOC_ID.test(quoteId)) return refuse(400, 'Missing quoteId');
|
|
232
|
+
const ref = hostRef.collection('orders').doc(quoteId);
|
|
233
|
+
const now = deps.now();
|
|
234
|
+
const answer = await firestore.runTransaction(async (transaction)=>{
|
|
235
|
+
const snapshot = await transaction.get(ref);
|
|
236
|
+
const order = snapshot.exists ? snapshot.data() : null;
|
|
237
|
+
if (!order || !CommerceModel.isQuote(order) || !order.quote) return {
|
|
238
|
+
outcome: refuse(404, 'Unknown quote')
|
|
239
|
+
};
|
|
240
|
+
const version = CommerceModel.quoteLinkVersion(order.quote);
|
|
241
|
+
if (action === 'pay-link') return {
|
|
242
|
+
outcome: {
|
|
243
|
+
status: 200,
|
|
244
|
+
body: {
|
|
245
|
+
linkVersion: version
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
if (!CommerceModel.isUnpaidQuote(order)) {
|
|
250
|
+
return {
|
|
251
|
+
outcome: refuse(409, 'This quote has been paid, so its link can no longer be replaced.')
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const next = version + 1;
|
|
255
|
+
// The open session goes with the link it was opened from.
|
|
256
|
+
const rest = _extends({}, order);
|
|
257
|
+
delete rest.checkoutSessionId;
|
|
258
|
+
transaction.set(ref, _extends({}, rest, {
|
|
259
|
+
timeline: CommerceModel.appendOrderEvent(order, 'quote-link-replaced', `Pay link replaced by ${actor}; the earlier link no longer opens the quote`, now),
|
|
260
|
+
quote: _extends({}, order.quote, {
|
|
261
|
+
linkVersion: next
|
|
262
|
+
})
|
|
263
|
+
}));
|
|
264
|
+
return {
|
|
265
|
+
outcome: {
|
|
266
|
+
status: 200,
|
|
267
|
+
body: {
|
|
268
|
+
linkVersion: next
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
closeSession: order.checkoutSessionId
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
if (answer.closeSession) await closeOpenSession(deps, answer.closeSession, 'quote link replaced');
|
|
275
|
+
if (answer.outcome.status !== 200) return answer.outcome;
|
|
276
|
+
const linkVersion = Number(answer.outcome.body['linkVersion']);
|
|
277
|
+
const url = deps.payLinkUrl((_host_data = host.data()) != null ? _host_data : {}, hostId, quoteId, linkVersion);
|
|
278
|
+
if (!url) return refuse(409, 'This site has no web address yet, so its quotes have no pay link. Publish the site first.');
|
|
279
|
+
return {
|
|
280
|
+
status: 200,
|
|
281
|
+
body: {
|
|
282
|
+
url,
|
|
283
|
+
linkVersion
|
|
284
|
+
}
|
|
285
|
+
};
|
|
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
|
+
}
|
|
335
|
+
if (action === 'search-customers') {
|
|
336
|
+
var _body_text;
|
|
337
|
+
const text = String((_body_text = body['text']) != null ? _body_text : '').trim().slice(0, 120);
|
|
338
|
+
if (text.length < 2) return {
|
|
339
|
+
status: 200,
|
|
340
|
+
body: {
|
|
341
|
+
available: true,
|
|
342
|
+
customers: []
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
const owner = await deps.orgForHost(hostId);
|
|
346
|
+
const people = await deps.searchPeople({
|
|
347
|
+
hostId,
|
|
348
|
+
orgId: (owner == null ? void 0 : owner.id) || null,
|
|
349
|
+
text,
|
|
350
|
+
limit: 10
|
|
351
|
+
});
|
|
352
|
+
if (people === null) return {
|
|
353
|
+
status: 200,
|
|
354
|
+
body: {
|
|
355
|
+
available: false,
|
|
356
|
+
customers: []
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
return {
|
|
360
|
+
status: 200,
|
|
361
|
+
body: {
|
|
362
|
+
available: true,
|
|
363
|
+
customers: people.map((person)=>{
|
|
364
|
+
var _person_data;
|
|
365
|
+
const data = (_person_data = person.data) != null ? _person_data : {};
|
|
366
|
+
const read = (key)=>typeof data[key] === 'string' ? String(data[key]).trim() : '';
|
|
367
|
+
return {
|
|
368
|
+
record: {
|
|
369
|
+
kind: person.kind,
|
|
370
|
+
id: person.id
|
|
371
|
+
},
|
|
372
|
+
name: read('name'),
|
|
373
|
+
company: read('company'),
|
|
374
|
+
email: person.email,
|
|
375
|
+
phone: read('phone') || null
|
|
376
|
+
};
|
|
377
|
+
})
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
const quoteId = String((_body_quoteId = body['quoteId']) != null ? _body_quoteId : '');
|
|
382
|
+
if (action === 'delete') {
|
|
383
|
+
if (!DOC_ID.test(quoteId)) return refuse(400, 'Missing quoteId');
|
|
384
|
+
const ref = hostRef.collection('orders').doc(quoteId);
|
|
385
|
+
const deleted = await firestore.runTransaction(async (transaction)=>{
|
|
386
|
+
var _ref, _ref1;
|
|
387
|
+
var _order_quote, _order_quote1;
|
|
388
|
+
const snapshot = await transaction.get(ref);
|
|
389
|
+
// Already gone is what a retried delete finds.
|
|
390
|
+
if (!snapshot.exists) return {
|
|
391
|
+
status: 200,
|
|
392
|
+
body: {
|
|
393
|
+
deleted: true
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
const order = snapshot.data();
|
|
397
|
+
if (!CommerceModel.isQuote(order)) return refuse(404, 'Unknown quote');
|
|
398
|
+
if (!CommerceModel.isUnpaidQuote(order)) {
|
|
399
|
+
return refuse(409, 'This quote has been paid, so it is an order now and stays on record.');
|
|
400
|
+
}
|
|
401
|
+
if (((_ref = (_order_quote = order.quote) == null ? void 0 : _order_quote.stage) != null ? _ref : 'draft') !== 'draft') {
|
|
402
|
+
return refuse(409, 'Only a draft quote can be deleted.');
|
|
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
|
+
}
|
|
409
|
+
transaction.delete(ref);
|
|
410
|
+
return {
|
|
411
|
+
status: 200,
|
|
412
|
+
body: {
|
|
413
|
+
deleted: true
|
|
414
|
+
},
|
|
415
|
+
closeSession: order.checkoutSessionId
|
|
416
|
+
};
|
|
417
|
+
});
|
|
418
|
+
const { closeSession } = deleted, outcome = _object_without_properties_loose(deleted, [
|
|
419
|
+
"closeSession"
|
|
420
|
+
]);
|
|
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);
|
|
424
|
+
return outcome;
|
|
425
|
+
}
|
|
426
|
+
if (action !== 'create' && action !== 'update') return refuse(400, 'Unknown action');
|
|
427
|
+
if (action === 'update' && !DOC_ID.test(quoteId)) return refuse(400, 'Missing quoteId');
|
|
428
|
+
const idempotencyKey = headerOf(req, 'idempotency-key').slice(0, 200);
|
|
429
|
+
if (!idempotencyKey) return refuse(400, 'A quote save needs an Idempotency-Key header.');
|
|
430
|
+
const owner = await deps.orgForHost(hostId);
|
|
431
|
+
if (!checkEntitlement(owner == null ? void 0 : owner.org, 'commerce')) return refuse(403, 'Selling is not enabled');
|
|
432
|
+
const parsed = CommerceModel.parseQuoteInput(body['quote']);
|
|
433
|
+
if ('error' in parsed) return refuse(400, parsed.error);
|
|
434
|
+
const input = parsed.input;
|
|
435
|
+
const priced = await priceQuote(deps, hostRef, hostId, input);
|
|
436
|
+
if ('status' in priced) return priced;
|
|
437
|
+
const claimed = await claimAttempt(firestore, {
|
|
438
|
+
kind: action === 'create' ? 'commerce-quote' : 'commerce-quote-update',
|
|
439
|
+
scopeId: action === 'create' ? hostId : `${hostId}:${quoteId}`,
|
|
440
|
+
orgId: (_ref = owner == null ? void 0 : owner.id) != null ? _ref : '',
|
|
441
|
+
key: idempotencyKey,
|
|
442
|
+
busyMessage: 'This quote is already being saved'
|
|
443
|
+
});
|
|
444
|
+
if ('replay' in claimed) {
|
|
445
|
+
return {
|
|
446
|
+
status: claimed.replay.status,
|
|
447
|
+
body: claimed.replay.body
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
const claim = claimed.claim;
|
|
451
|
+
const now = deps.now();
|
|
452
|
+
try {
|
|
453
|
+
let outcome;
|
|
454
|
+
if (action === 'create') {
|
|
455
|
+
const orderRef = hostRef.collection('orders').doc(createResourceUid());
|
|
456
|
+
const counterRef = hostRef.collection('counters').doc('orders');
|
|
457
|
+
let number = 0;
|
|
458
|
+
await firestore.runTransaction(async (transaction)=>{
|
|
459
|
+
var _counter_get, _input_requireProofApproval;
|
|
460
|
+
const counter = await transaction.get(counterRef);
|
|
461
|
+
number = Number((_counter_get = counter.get('next')) != null ? _counter_get : 1);
|
|
462
|
+
transaction.set(counterRef, {
|
|
463
|
+
next: number + 1
|
|
464
|
+
}, {
|
|
465
|
+
merge: true
|
|
466
|
+
});
|
|
467
|
+
const quote = _extends({
|
|
468
|
+
stage: 'draft',
|
|
469
|
+
revision: 1
|
|
470
|
+
}, editedQuoteFields(input), {
|
|
471
|
+
tax: priced.tax,
|
|
472
|
+
requireProofApproval: (_input_requireProofApproval = input.requireProofApproval) != null ? _input_requireProofApproval : priced.requireProofApprovalDefault,
|
|
473
|
+
createdAtMs: now,
|
|
474
|
+
updatedAtMs: now,
|
|
475
|
+
createdBy: actor,
|
|
476
|
+
updatedBy: actor
|
|
477
|
+
});
|
|
478
|
+
// No `createdAtMs`, list fields or customer fields at the top: an
|
|
479
|
+
// unpaid quote is not an order any list counts (`commerce-quotes.ts`).
|
|
480
|
+
transaction.set(orderRef, _extends({
|
|
481
|
+
number,
|
|
482
|
+
status: 'pending',
|
|
483
|
+
channel: 'quote',
|
|
484
|
+
lineItems: priced.lineItems,
|
|
485
|
+
totals: priced.totals
|
|
486
|
+
}, priced.taxMode ? {
|
|
487
|
+
taxMode: priced.taxMode
|
|
488
|
+
} : {}, {
|
|
489
|
+
timeline: [
|
|
490
|
+
{
|
|
491
|
+
atMs: now,
|
|
492
|
+
event: 'quote',
|
|
493
|
+
detail: `Quote created by ${actor}`
|
|
494
|
+
}
|
|
495
|
+
],
|
|
496
|
+
quote
|
|
497
|
+
}));
|
|
498
|
+
});
|
|
499
|
+
outcome = {
|
|
500
|
+
status: 200,
|
|
501
|
+
body: {
|
|
502
|
+
quoteId: orderRef.id,
|
|
503
|
+
number,
|
|
504
|
+
updatedAtMs: now
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
} else {
|
|
508
|
+
const baseUpdatedAtMs = Number(body['baseUpdatedAtMs']);
|
|
509
|
+
const orderRef = hostRef.collection('orders').doc(quoteId);
|
|
510
|
+
let closeSession;
|
|
511
|
+
outcome = await firestore.runTransaction(async (transaction)=>{
|
|
512
|
+
var _input_requireProofApproval, _existing_number;
|
|
513
|
+
closeSession = undefined;
|
|
514
|
+
const snapshot = await transaction.get(orderRef);
|
|
515
|
+
const existing = snapshot.exists ? snapshot.data() : null;
|
|
516
|
+
if (!existing || !CommerceModel.isQuote(existing) || !existing.quote) {
|
|
517
|
+
return refuse(404, 'Unknown quote');
|
|
518
|
+
}
|
|
519
|
+
if (!CommerceModel.isUnpaidQuote(existing)) {
|
|
520
|
+
return refuse(409, 'This quote has been paid, so it can no longer be changed.');
|
|
521
|
+
}
|
|
522
|
+
if (Number(existing.quote.updatedAtMs) !== baseUpdatedAtMs) {
|
|
523
|
+
return refuse(409, 'Someone else changed this quote. Close it and open it again to see their changes.');
|
|
524
|
+
}
|
|
525
|
+
// The tax mode is restated from this pricing, and dropped when it has
|
|
526
|
+
// none. The pay page's open session was priced from the quote before
|
|
527
|
+
// this edit, so it is closed and forgotten.
|
|
528
|
+
const rest = _extends({}, existing);
|
|
529
|
+
delete rest.taxMode;
|
|
530
|
+
delete rest.checkoutSessionId;
|
|
531
|
+
closeSession = existing.checkoutSessionId;
|
|
532
|
+
transaction.set(orderRef, _extends({}, rest, {
|
|
533
|
+
lineItems: priced.lineItems,
|
|
534
|
+
totals: priced.totals
|
|
535
|
+
}, priced.taxMode ? {
|
|
536
|
+
taxMode: priced.taxMode
|
|
537
|
+
} : {}, {
|
|
538
|
+
timeline: CommerceModel.appendOrderEvent(existing, 'quote-updated', `Quote edited by ${actor}`, now),
|
|
539
|
+
quote: _extends({}, existing.quote, editedQuoteFields(input), {
|
|
540
|
+
tax: priced.tax,
|
|
541
|
+
requireProofApproval: (_input_requireProofApproval = input.requireProofApproval) != null ? _input_requireProofApproval : existing.quote.requireProofApproval === true,
|
|
542
|
+
updatedAtMs: now,
|
|
543
|
+
updatedBy: actor
|
|
544
|
+
})
|
|
545
|
+
}));
|
|
546
|
+
return {
|
|
547
|
+
status: 200,
|
|
548
|
+
body: {
|
|
549
|
+
quoteId,
|
|
550
|
+
number: (_existing_number = existing.number) != null ? _existing_number : null,
|
|
551
|
+
updatedAtMs: now
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
});
|
|
555
|
+
if (outcome.status === 200) await closeOpenSession(deps, closeSession, 'quote edited');
|
|
556
|
+
}
|
|
557
|
+
if (outcome.status === 200) await claim.record(outcome.status, outcome.body);
|
|
558
|
+
else await claim.release();
|
|
559
|
+
return outcome;
|
|
560
|
+
} catch (error) {
|
|
561
|
+
console.error('commerce quote save failed', error);
|
|
562
|
+
await claim.release();
|
|
563
|
+
return refuse(500, 'The quote could not be saved');
|
|
564
|
+
}
|
|
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
|
+
}
|
|
835
|
+
/** `POST /api/commerce/quote`. */ export const quoteHandler = async (req, res)=>{
|
|
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
|
+
}
|
|
843
|
+
return res.status(outcome.status).json(outcome.body);
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
//# sourceMappingURL=quote.js.map
|