@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.
Files changed (129) hide show
  1. package/README.md +2 -2
  2. package/package.json +13 -13
  3. package/src/lib/components/commerce-console-page.js +29 -2
  4. package/src/lib/components/commerce-console-page.js.map +1 -1
  5. package/src/lib/components/console/commerce-analytics-card.component.js +4 -2
  6. package/src/lib/components/console/commerce-analytics-card.component.js.map +1 -1
  7. package/src/lib/components/console/commerce-pdf-download.d.ts +30 -0
  8. package/src/lib/components/console/commerce-pdf-download.js +53 -0
  9. package/src/lib/components/console/commerce-pdf-download.js.map +1 -0
  10. package/src/lib/components/console/host-orders-card.component.js +5 -1
  11. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  12. package/src/lib/components/console/host-quotes-card.component.d.ts +34 -0
  13. package/src/lib/components/console/host-quotes-card.component.js +263 -0
  14. package/src/lib/components/console/host-quotes-card.component.js.map +1 -0
  15. package/src/lib/components/console/order-invoice.component.d.ts +6 -4
  16. package/src/lib/components/console/order-invoice.component.js +67 -11
  17. package/src/lib/components/console/order-invoice.component.js.map +1 -1
  18. package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js +33 -28
  19. package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js.map +1 -1
  20. package/src/lib/components/console/quote-editor-dialog.component.d.ts +45 -0
  21. package/src/lib/components/console/quote-editor-dialog.component.js +1317 -0
  22. package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -0
  23. package/src/lib/components/console/quote-pay-link.component.d.ts +38 -0
  24. package/src/lib/components/console/quote-pay-link.component.js +213 -0
  25. package/src/lib/components/console/quote-pay-link.component.js.map +1 -0
  26. package/src/lib/components/console/quote-proofs.component.d.ts +37 -0
  27. package/src/lib/components/console/quote-proofs.component.js +626 -0
  28. package/src/lib/components/console/quote-proofs.component.js.map +1 -0
  29. package/src/lib/components/console/quote-settings-card.component.d.ts +29 -0
  30. package/src/lib/components/console/quote-settings-card.component.js +121 -0
  31. package/src/lib/components/console/quote-settings-card.component.js.map +1 -0
  32. package/src/lib/components/console/tax-settings-card.component.js +4 -1
  33. package/src/lib/components/console/tax-settings-card.component.js.map +1 -1
  34. package/src/lib/components/quote-page-body.d.ts +33 -0
  35. package/src/lib/components/quote-page-body.js +724 -0
  36. package/src/lib/components/quote-page-body.js.map +1 -0
  37. package/src/lib/components/quote-page.d.ts +29 -0
  38. package/src/lib/components/quote-page.js +96 -0
  39. package/src/lib/components/quote-page.js.map +1 -0
  40. package/src/lib/components/quote-proof-panel.d.ts +42 -0
  41. package/src/lib/components/quote-proof-panel.js +436 -0
  42. package/src/lib/components/quote-proof-panel.js.map +1 -0
  43. package/src/lib/components/storefront-payment-element.d.ts +7 -1
  44. package/src/lib/components/storefront-payment-element.js +8 -3
  45. package/src/lib/components/storefront-payment-element.js.map +1 -1
  46. package/src/lib/constants/orders-list-query.js +6 -1
  47. package/src/lib/constants/orders-list-query.js.map +1 -1
  48. package/src/lib/constants/quote-page.d.ts +30 -0
  49. package/src/lib/constants/quote-page.js +35 -0
  50. package/src/lib/constants/quote-page.js.map +1 -0
  51. package/src/lib/model/commerce-orders.d.ts +25 -2
  52. package/src/lib/model/commerce-orders.js +10 -1
  53. package/src/lib/model/commerce-orders.js.map +1 -1
  54. package/src/lib/model/commerce-quote-pay.d.ts +256 -0
  55. package/src/lib/model/commerce-quote-pay.js +426 -0
  56. package/src/lib/model/commerce-quote-pay.js.map +1 -0
  57. package/src/lib/model/commerce-quote-proofs.d.ts +159 -0
  58. package/src/lib/model/commerce-quote-proofs.js +348 -0
  59. package/src/lib/model/commerce-quote-proofs.js.map +1 -0
  60. package/src/lib/model/commerce-quotes.d.ts +335 -0
  61. package/src/lib/model/commerce-quotes.js +522 -0
  62. package/src/lib/model/commerce-quotes.js.map +1 -0
  63. package/src/lib/model/index.d.ts +3 -0
  64. package/src/lib/model/index.js +3 -0
  65. package/src/lib/model/index.js.map +1 -1
  66. package/src/lib/model/plugin-permissions.d.ts +1 -1
  67. package/src/lib/model/plugin-permissions.js +20 -0
  68. package/src/lib/model/plugin-permissions.js.map +1 -1
  69. package/src/lib/server/api-v1/orders-and-products.js +6 -2
  70. package/src/lib/server/api-v1/orders-and-products.js.map +1 -1
  71. package/src/lib/server/billing-webhook.js +127 -52
  72. package/src/lib/server/billing-webhook.js.map +1 -1
  73. package/src/lib/server/checkout-session-expiry.d.ts +36 -0
  74. package/src/lib/server/checkout-session-expiry.js +63 -0
  75. package/src/lib/server/checkout-session-expiry.js.map +1 -0
  76. package/src/lib/server/commerce-pdf.d.ts +100 -0
  77. package/src/lib/server/commerce-pdf.js +283 -0
  78. package/src/lib/server/commerce-pdf.js.map +1 -0
  79. package/src/lib/server/draft-order.js +16 -8
  80. package/src/lib/server/draft-order.js.map +1 -1
  81. package/src/lib/server/order-print.d.ts +6 -0
  82. package/src/lib/server/order-print.js +22 -5
  83. package/src/lib/server/order-print.js.map +1 -1
  84. package/src/lib/server/pdf-response.d.ts +31 -0
  85. package/src/lib/server/pdf-response.js +30 -0
  86. package/src/lib/server/pdf-response.js.map +1 -0
  87. package/src/lib/server/person-eraser.d.ts +7 -0
  88. package/src/lib/server/person-eraser.js +24 -2
  89. package/src/lib/server/person-eraser.js.map +1 -1
  90. package/src/lib/server/pos-reader-splash.d.ts +2 -18
  91. package/src/lib/server/pos-reader-splash.js +8 -93
  92. package/src/lib/server/pos-reader-splash.js.map +1 -1
  93. package/src/lib/server/pos-return.js +3 -1
  94. package/src/lib/server/pos-return.js.map +1 -1
  95. package/src/lib/server/quote-link-token.d.ts +39 -0
  96. package/src/lib/server/quote-link-token.js +97 -0
  97. package/src/lib/server/quote-link-token.js.map +1 -0
  98. package/src/lib/server/quote-pay.d.ts +110 -0
  99. package/src/lib/server/quote-pay.js +606 -0
  100. package/src/lib/server/quote-pay.js.map +1 -0
  101. package/src/lib/server/quote-proofs.d.ts +51 -0
  102. package/src/lib/server/quote-proofs.js +500 -0
  103. package/src/lib/server/quote-proofs.js.map +1 -0
  104. package/src/lib/server/quote-uploads.d.ts +33 -0
  105. package/src/lib/server/quote-uploads.js +29 -0
  106. package/src/lib/server/quote-uploads.js.map +1 -0
  107. package/src/lib/server/quote.d.ts +63 -0
  108. package/src/lib/server/quote.js +846 -0
  109. package/src/lib/server/quote.js.map +1 -0
  110. package/src/lib/server/related.js +2 -0
  111. package/src/lib/server/related.js.map +1 -1
  112. package/src/lib/server/site-image-source.d.ts +38 -0
  113. package/src/lib/server/site-image-source.js +130 -0
  114. package/src/lib/server/site-image-source.js.map +1 -0
  115. package/src/lib/server/site-page-resolver.d.ts +2 -0
  116. package/src/lib/server/site-page-resolver.js +75 -0
  117. package/src/lib/server/site-page-resolver.js.map +1 -1
  118. package/src/lib/server.js +33 -1
  119. package/src/lib/server.js.map +1 -1
  120. package/src/lib/site.js +6 -0
  121. package/src/lib/site.js.map +1 -1
  122. package/src/lib/transfer/tracking.server.js +6 -0
  123. package/src/lib/transfer/tracking.server.js.map +1 -1
  124. package/src/lib/utils/commerce-document-pdf.d.ts +77 -0
  125. package/src/lib/utils/commerce-document-pdf.js +608 -0
  126. package/src/lib/utils/commerce-document-pdf.js.map +1 -0
  127. package/src/lib/utils/order-invoice.d.ts +52 -3
  128. package/src/lib/utils/order-invoice.js +116 -64
  129. package/src/lib/utils/order-invoice.js.map +1 -1
@@ -0,0 +1,606 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { checkEntitlement, claimAttempt, deriveStripeObjectKey, resolveTransactionFeeCents } from "@aglyn/aglyn/server";
18
+ import { resolveHostToken } from "@aglyn/aglyn/app-utils/host-tokens";
19
+ import { readClientIp } from "@aglyn/aglyn/app-utils/request-ip";
20
+ import { siteReturnOrigin } from "@aglyn/aglyn/app-utils/site-return-url";
21
+ import { consumeRateLimit, firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
22
+ import { merchantAccountIsReady } from "@aglyn/tenant-data-admin/server/payment-provider";
23
+ import { checkoutSessionCardAuthenticationParams } from "@aglyn/tenant-data-admin/server/stripe-card-authentication";
24
+ import { quotePageHref } from "../constants/quote-page.js";
25
+ import * as CommerceModel from "../model/index.js";
26
+ import { expireCheckoutSession } from "./checkout-session-expiry.js";
27
+ import { resolveManualTaxRateId } from "./manual-tax-rate.js";
28
+ import { applyNativeCheckoutParams, nativeCheckoutStripeHeaders, readCheckoutSessionPayload, resolveNativeCheckoutMode } from "./native-checkout.js";
29
+ import { orderStatusUrl } from "./order-status-token.js";
30
+ import { ensureCheckoutDomain } from "./payment-method-domains.js";
31
+ import { verifyQuoteLinkToken } from "./quote-link-token.js";
32
+ /*==========================================
33
+ * THE CUSTOMER'S QUOTE PAGE AND ITS PAY BUTTON (AGL-4094).
34
+ *
35
+ * GET /api/commerce/quote-view?hostId=&q=<quoteId>&t=<token>
36
+ * → 200 QuotePageView | 200 { state: 'replaced', storeName } | 404
37
+ * POST /api/commerce/quote-pay { hostId, q, t, v } + Idempotency-Key
38
+ * → 200 { url } (Stripe's page) | 200 { clientSecret, publishableKey, sessionId, wallets? }
39
+ * (the in-page form) | 409 { error, reason } | 410 { error, replaced: true } | 404
40
+ *
41
+ * The signed link (`quote-link-token.ts`) is the whole credential. A wrong
42
+ * token answers exactly as a missing quote does, so neither route tells
43
+ * anyone which quote ids exist; both are rate-limited per address per site
44
+ * BEFORE the token check, so guessing costs what looking does. A link signed
45
+ * for an older version of the quote opens the replaced page and nothing else.
46
+ *
47
+ * Pay opens a FRESH Checkout Session from the stored quote on every press:
48
+ * the prices are the stored lines', never anything the page sends, and the
49
+ * session the previous press opened is expired first, so two sessions for one
50
+ * quote can never both be open. The quote keeps the open one in
51
+ * `checkoutSessionId`. A Stripe session lapses after a day; the quote and its
52
+ * link do not, because the next press opens another.
53
+ *
54
+ * The payment completes in `billing-webhook.ts`, which turns this same
55
+ * document into the order (`paidQuoteOrder`); nothing here marks it paid.
56
+ *=========================================*/ /** Lookups and presses one address may make per site, per ten minutes. */ export const QUOTE_VIEW_LOOKUPS_PER_WINDOW = 60;
57
+ export const QUOTE_PAY_PRESSES_PER_WINDOW = 20;
58
+ export const QUOTE_WINDOW_MS = 10 * 60000;
59
+ export function defaultQuotePayDeps() {
60
+ return {
61
+ firestore: ()=>firebaseAdmin.app().firestore(),
62
+ consumeRateLimit: (key, options)=>consumeRateLimit(key, options),
63
+ orgForHost: async (hostId)=>{
64
+ const found = await getOrgForHost(hostId);
65
+ return found ? {
66
+ id: found.orgId,
67
+ org: found.org
68
+ } : null;
69
+ },
70
+ nativeMode: (orgId)=>resolveNativeCheckoutMode(orgId),
71
+ taxRateId: (input)=>resolveManualTaxRateId(input),
72
+ createSession: async (params, headers)=>{
73
+ const response = await fetch('https://api.stripe.com/v1/checkout/sessions', {
74
+ method: 'POST',
75
+ headers: _extends({
76
+ Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,
77
+ 'Content-Type': 'application/x-www-form-urlencoded'
78
+ }, headers),
79
+ body: params.toString()
80
+ });
81
+ return {
82
+ ok: response.ok,
83
+ session: await response.json().catch(()=>({}))
84
+ };
85
+ },
86
+ expireSession: (sessionId)=>expireCheckoutSession(sessionId),
87
+ ensureDomain: (input)=>ensureCheckoutDomain(input).catch(()=>null),
88
+ now: ()=>Date.now()
89
+ };
90
+ }
91
+ const DOC_ID = /^(?!__.*__$)[^/]{1,200}$/;
92
+ const NOT_FOUND = {
93
+ status: 404,
94
+ body: {
95
+ error: 'We could not find that quote. Check the link you were sent.'
96
+ }
97
+ };
98
+ function bodyOf(req) {
99
+ const raw = req.body;
100
+ if (typeof raw === 'string') {
101
+ try {
102
+ const parsed = JSON.parse(raw);
103
+ return parsed && typeof parsed === 'object' ? parsed : {};
104
+ } catch (unused) {
105
+ return {};
106
+ }
107
+ }
108
+ return raw && typeof raw === 'object' ? raw : {};
109
+ }
110
+ function headerOf(req, name) {
111
+ var _req_headers, _headers_name_toLowerCase, _value_;
112
+ const headers = (_req_headers = req.headers) != null ? _req_headers : {};
113
+ const value = (_headers_name_toLowerCase = headers[name.toLowerCase()]) != null ? _headers_name_toLowerCase : headers[name];
114
+ return String(Array.isArray(value) ? (_value_ = value[0]) != null ? _value_ : '' : value != null ? value : '').trim();
115
+ }
116
+ /** The rate limit a link route spends per address per site, BEFORE the token is checked. */ export async function limitedQuoteLink(deps, req, key, limit) {
117
+ var _readClientIp;
118
+ var _req_socket;
119
+ const ip = (_readClientIp = readClientIp(req.headers, {
120
+ remoteAddress: (_req_socket = req.socket) == null ? void 0 : _req_socket.remoteAddress
121
+ })) != null ? _readClientIp : 'no-address';
122
+ const rate = await deps.consumeRateLimit(`${key}:${ip}`, {
123
+ limit,
124
+ windowMs: QUOTE_WINDOW_MS
125
+ });
126
+ if (rate.allowed) return null;
127
+ return {
128
+ status: 429,
129
+ body: {
130
+ error: 'Too many tries. Try again in a few minutes.'
131
+ },
132
+ headers: {
133
+ 'Retry-After': String(Math.max(1, Math.ceil((rate.resetMs - deps.now()) / 1000)))
134
+ }
135
+ };
136
+ }
137
+ /**
138
+ * The quote a link names, after its token: the quote, the replaced answer for
139
+ * a link to an older version, or the one not-found answer for everything else.
140
+ */ export async function loadLinkedQuote(deps, hostId, quoteId, token) {
141
+ var _hostSnapshot_data, _resolveHostToken;
142
+ const version = verifyQuoteLinkToken(hostId, quoteId, token);
143
+ if (version === null) return {
144
+ outcome: NOT_FOUND
145
+ };
146
+ const hostRef = deps.firestore().collection('hosts').doc(hostId);
147
+ const [hostSnapshot, store, orderSnapshot] = await Promise.all([
148
+ hostRef.get(),
149
+ hostRef.collection('settings').doc('store').get(),
150
+ hostRef.collection('orders').doc(quoteId).get()
151
+ ]);
152
+ if (!hostSnapshot.exists || !orderSnapshot.exists) return {
153
+ outcome: NOT_FOUND
154
+ };
155
+ const order = orderSnapshot.data();
156
+ if (!CommerceModel.isQuote(order) || !order.quote) return {
157
+ outcome: NOT_FOUND
158
+ };
159
+ const host = (_hostSnapshot_data = hostSnapshot.data()) != null ? _hostSnapshot_data : {};
160
+ const storeName = String((_resolveHostToken = resolveHostToken('businessName', host)) != null ? _resolveHostToken : '');
161
+ const current = CommerceModel.quoteLinkVersion(order.quote);
162
+ if (version > current) return {
163
+ outcome: NOT_FOUND
164
+ };
165
+ if (version < current) {
166
+ return {
167
+ outcome: {
168
+ status: 410,
169
+ body: {
170
+ state: 'replaced',
171
+ storeName,
172
+ replaced: true,
173
+ error: `This quote was updated, and ${storeName || 'the business'} sent you a newer link.`
174
+ }
175
+ }
176
+ };
177
+ }
178
+ return {
179
+ loaded: {
180
+ hostRef,
181
+ host,
182
+ order,
183
+ storeName,
184
+ store
185
+ }
186
+ };
187
+ }
188
+ /** The link's site, quote and token from a query or a body, and whether all three are there. */ export function quoteLinkParams(source) {
189
+ var _source_hostId, _source_q, _source_t;
190
+ const hostId = String((_source_hostId = source['hostId']) != null ? _source_hostId : '');
191
+ const quoteId = String((_source_q = source['q']) != null ? _source_q : '').slice(0, 200);
192
+ const token = String((_source_t = source['t']) != null ? _source_t : '').slice(0, 200);
193
+ const complete = DOC_ID.test(hostId) && DOC_ID.test(quoteId) && Boolean(token);
194
+ return {
195
+ hostId,
196
+ quoteId,
197
+ token,
198
+ complete
199
+ };
200
+ }
201
+ /** `GET /api/commerce/quote-view`: the quote page's data. */ export async function handleQuoteView(deps, req) {
202
+ var _req_query, _store_get;
203
+ if (req.method !== 'GET') return {
204
+ status: 405,
205
+ body: {
206
+ error: 'Method not allowed'
207
+ }
208
+ };
209
+ const { hostId, quoteId, token, complete } = quoteLinkParams((_req_query = req.query) != null ? _req_query : {});
210
+ if (!complete) return {
211
+ status: 400,
212
+ body: {
213
+ error: 'This link is incomplete.'
214
+ }
215
+ };
216
+ const refused = await limitedQuoteLink(deps, req, `commerce-quote-view:${hostId}`, QUOTE_VIEW_LOOKUPS_PER_WINDOW);
217
+ if (refused) return refused;
218
+ const linked = await loadLinkedQuote(deps, hostId, quoteId, token);
219
+ if ('outcome' in linked) {
220
+ // The replaced page is a page, not a failure: answered as data.
221
+ if (linked.outcome.status === 410) {
222
+ const { storeName } = linked.outcome.body;
223
+ return {
224
+ status: 200,
225
+ body: {
226
+ state: 'replaced',
227
+ storeName
228
+ }
229
+ };
230
+ }
231
+ return linked.outcome;
232
+ }
233
+ const { host, order, storeName, store } = linked.loaded;
234
+ const view = CommerceModel.buildQuotePageView({
235
+ order,
236
+ orderId: quoteId,
237
+ storeName,
238
+ currency: String((_store_get = store.get('currency')) != null ? _store_get : 'USD'),
239
+ receiptUrl: CommerceModel.isUnpaidQuote(order) ? null : orderStatusUrl(host, hostId, quoteId)
240
+ });
241
+ return {
242
+ status: 200,
243
+ body: view
244
+ };
245
+ }
246
+ /** The Checkout Session a quote's Pay opens, as form params. Pure over its inputs. */ export function buildQuoteSessionParams(input) {
247
+ var _ref;
248
+ var _quote_customer;
249
+ const { charge, order } = input;
250
+ const quote = order.quote;
251
+ const params = new URLSearchParams(_extends({
252
+ mode: 'payment'
253
+ }, checkoutSessionCardAuthenticationParams('payment'), charge.taxAtPayment ? {
254
+ 'automatic_tax[enabled]': 'true'
255
+ } : {}));
256
+ charge.lines.forEach((line, index)=>{
257
+ const at = `line_items[${index}]`;
258
+ params.set(`${at}[quantity]`, '1');
259
+ params.set(`${at}[price_data][currency]`, 'usd');
260
+ params.set(`${at}[price_data][unit_amount]`, String(line.amountCents));
261
+ params.set(`${at}[price_data][product_data][name]`, line.name.slice(0, 120));
262
+ if (line.description) params.set(`${at}[price_data][product_data][description]`, line.description);
263
+ // Stripe Tax's nontaxable code, for tax-exempt products and a tax charged as its own line.
264
+ if (!line.taxable) params.set(`${at}[price_data][product_data][tax_code]`, 'txcd_00000000');
265
+ if (line.taxable && input.taxRateId) params.set(`${at}[tax_rates][0]`, input.taxRateId);
266
+ });
267
+ // Shipping the business priced for the quote's ship-to, charged as the one
268
+ // option, with the address asked for where it ships (and where Stripe Tax
269
+ // reads the tax from). The quote's own address stands when it asks nothing.
270
+ if (charge.shippingCents > 0) {
271
+ var _ref1;
272
+ var _quote_shipTo;
273
+ const country = String((_ref1 = (_quote_shipTo = quote.shipTo) == null ? void 0 : _quote_shipTo.country) != null ? _ref1 : '').toUpperCase();
274
+ CommerceModel.appendShippingAddressCollectionParams(params, /^[A-Z]{2}$/.test(country) ? [
275
+ country
276
+ ] : CommerceModel.CHECKOUT_SHIPPING_COUNTRIES);
277
+ CommerceModel.appendCheckoutShippingParams(params, [
278
+ {
279
+ rateId: 'quote',
280
+ name: 'Shipping',
281
+ amountCents: charge.shippingCents
282
+ }
283
+ ]);
284
+ }
285
+ const shippingFloorCents = charge.shippingCents;
286
+ for (const [key, value] of Object.entries(CommerceModel.destinationChargeParams({
287
+ accountId: input.accountId,
288
+ feeCents: input.feeCents,
289
+ taxOwner: charge.taxAtPayment ? 'platform' : 'merchant',
290
+ merchantGoodsCents: charge.goodsCents,
291
+ shippingFloorCents
292
+ }))){
293
+ params.set(key, value);
294
+ }
295
+ const email = String((_ref = (_quote_customer = quote.customer) == null ? void 0 : _quote_customer.email) != null ? _ref : '').trim();
296
+ if (email) params.set('customer_email', email);
297
+ const separator = input.returnUrl.includes('?') ? '&' : '?';
298
+ params.set('success_url', `${input.returnUrl}${separator}session_id={CHECKOUT_SESSION_ID}`);
299
+ params.set('cancel_url', input.returnUrl);
300
+ // What the `commerce-draft` webhook branch reads to turn this quote into
301
+ // its order, and to tell a payment for the quote as it stands from one for
302
+ // a replaced link or an earlier edit.
303
+ params.set('metadata[type]', 'commerce-draft');
304
+ params.set('metadata[hostId]', input.hostId);
305
+ params.set('metadata[orderId]', input.quoteId);
306
+ params.set('metadata[quoteId]', input.quoteId);
307
+ params.set('metadata[quoteLinkVersion]', String(CommerceModel.quoteLinkVersion(quote)));
308
+ params.set('metadata[quoteUpdatedAtMs]', String(Number(quote.updatedAtMs) || 0));
309
+ params.set('metadata[feeCents]', String(input.feeCents));
310
+ // A tax charged as its own line is invisible to Stripe's tax total; this
311
+ // witness is what records it as the business's tax.
312
+ if (charge.taxCents > 0 && charge.taxPct === null) {
313
+ params.set('metadata[taxCents]', String(charge.taxCents));
314
+ }
315
+ if (charge.taxAtPayment) {
316
+ params.set('metadata[transferCents]', String(CommerceModel.platformLiableTransferCents({
317
+ feeCents: input.feeCents,
318
+ merchantGoodsCents: charge.goodsCents,
319
+ shippingFloorCents
320
+ })));
321
+ params.set('metadata[transferShippingCents]', String(shippingFloorCents));
322
+ }
323
+ CommerceModel.appendStorefrontPaymentMethodParams(params, input.paymentMethods);
324
+ return params;
325
+ }
326
+ const QUOTE_UPDATED_MESSAGE = 'This quote was updated since you opened it. Reload the page to see it before you pay.';
327
+ const refusal = (status, reason, error, extra = {})=>({
328
+ status,
329
+ body: _extends({
330
+ error,
331
+ reason
332
+ }, extra)
333
+ });
334
+ /** `POST /api/commerce/quote-pay`: a fresh Checkout Session for the quote. */ export async function handleQuotePay(deps, req) {
335
+ var _store_get, _ref, _order_checkoutSessionId, _ref1;
336
+ var _order_quote, _owner_org, _order_quote1;
337
+ if (req.method !== 'POST') return {
338
+ status: 405,
339
+ body: {
340
+ error: 'Method not allowed'
341
+ }
342
+ };
343
+ if (!process.env.STRIPE_SECRET_KEY) return {
344
+ status: 501,
345
+ body: {
346
+ error: 'Payments are not configured.'
347
+ }
348
+ };
349
+ const { hostId, quoteId, token, complete } = quoteLinkParams(bodyOf(req));
350
+ if (!complete) return {
351
+ status: 400,
352
+ body: {
353
+ error: 'This link is incomplete.'
354
+ }
355
+ };
356
+ const idempotencyKey = headerOf(req, 'idempotency-key').slice(0, 200);
357
+ if (!idempotencyKey) return {
358
+ status: 400,
359
+ body: {
360
+ error: 'Paying needs an Idempotency-Key header.'
361
+ }
362
+ };
363
+ const refused = await limitedQuoteLink(deps, req, `commerce-quote-pay:${hostId}`, QUOTE_PAY_PRESSES_PER_WINDOW);
364
+ if (refused) return refused;
365
+ const linked = await loadLinkedQuote(deps, hostId, quoteId, token);
366
+ if ('outcome' in linked) return linked.outcome;
367
+ const { hostRef, host, order, storeName, store } = linked.loaded;
368
+ // Every refusal the customer can be given is above the claim, so none of
369
+ // them spends the press. `v` is the edit of the quote the page showed: a
370
+ // quote changed since is shown again before it is paid, never charged unseen.
371
+ const seen = Number(bodyOf(req)['v']);
372
+ if (Number.isFinite(seen) && seen > 0 && seen !== (Number((_order_quote = order.quote) == null ? void 0 : _order_quote.updatedAtMs) || 0)) {
373
+ return refusal(409, 'updated', QUOTE_UPDATED_MESSAGE);
374
+ }
375
+ const blocked = CommerceModel.quotePayRefusal(order, quoteId);
376
+ if (blocked) return refusal(409, blocked, CommerceModel.quotePayRefusalMessage(blocked, storeName));
377
+ const owner = await deps.orgForHost(hostId);
378
+ const unavailable = refusal(409, 'payments-unavailable', `${storeName || 'This business'} can’t take payments online right now. Contact them to pay this quote.`);
379
+ if (!checkEntitlement(owner == null ? void 0 : owner.org, 'commerce')) return unavailable;
380
+ const ownerUid = owner == null ? void 0 : (_owner_org = owner.org) == null ? void 0 : _owner_org.ownerUid;
381
+ const profile = ownerUid ? await deps.firestore().collection('profiles').doc(String(ownerUid)).get() : null;
382
+ const accountId = profile == null ? void 0 : profile.get('stripeAccountId');
383
+ const ready = merchantAccountIsReady({
384
+ accountId,
385
+ chargesEnabled: profile == null ? void 0 : profile.get('stripeChargesEnabled'),
386
+ accountLivemode: profile == null ? void 0 : profile.get('stripeAccountLivemode')
387
+ }, {
388
+ subject: `quote ${quoteId} on host ${hostId}`
389
+ });
390
+ if (!ready) return unavailable;
391
+ const taxSettings = (_store_get = store.get('tax')) != null ? _store_get : {};
392
+ if (CommerceModel.storefrontTaxDecision({
393
+ settings: taxSettings
394
+ }).kind === 'undecided') {
395
+ return refusal(409, 'tax-undecided', CommerceModel.quotePayRefusalMessage('tax-undecided', storeName));
396
+ }
397
+ const charge = CommerceModel.quoteCheckoutCharge(order, quoteId);
398
+ const feeCents = resolveTransactionFeeCents(owner == null ? void 0 : owner.org, charge.feeProductType, charge.goodsCents, charge.totalCents);
399
+ const nativeMode = await deps.nativeMode(String((_ref = owner == null ? void 0 : owner.id) != null ? _ref : ''));
400
+ const site = host;
401
+ const origin = siteReturnOrigin({
402
+ candidates: [
403
+ headerOf(req, 'referer'),
404
+ `https://${headerOf(req, 'host')}`
405
+ ],
406
+ site,
407
+ requestHost: headerOf(req, 'host')
408
+ });
409
+ const returnUrl = `${origin}${quotePageHref(quoteId, token)}`;
410
+ const openedFor = (_order_checkoutSessionId = order.checkoutSessionId) != null ? _order_checkoutSessionId : '';
411
+ const openedAtEdit = Number((_order_quote1 = order.quote) == null ? void 0 : _order_quote1.updatedAtMs) || 0;
412
+ const claimed = await claimAttempt(deps.firestore(), {
413
+ kind: 'commerce-quote-pay',
414
+ scopeId: `${hostId}:${quoteId}`,
415
+ orgId: String((_ref1 = owner == null ? void 0 : owner.id) != null ? _ref1 : ''),
416
+ key: idempotencyKey,
417
+ busyMessage: 'Your payment is already being set up'
418
+ });
419
+ if ('replay' in claimed) {
420
+ return {
421
+ status: claimed.replay.status,
422
+ body: claimed.replay.body
423
+ };
424
+ }
425
+ const claim = claimed.claim;
426
+ const stripeKey = (object)=>{
427
+ const derived = deriveStripeObjectKey(claim.stripeKey, object);
428
+ return derived ? {
429
+ 'Idempotency-Key': derived
430
+ } : {};
431
+ };
432
+ let minted = '';
433
+ try {
434
+ // The session the last press opened is closed before another opens. One
435
+ // that was paid a moment ago means the quote is paid; one that cannot be
436
+ // closed means a second session would be a second way to pay.
437
+ if (openedFor) {
438
+ const previous = await deps.expireSession(openedFor);
439
+ if (previous === 'complete') {
440
+ await claim.release();
441
+ return refusal(409, 'paid', 'This quote has just been paid. Your receipt is on its way.', {
442
+ confirming: true
443
+ });
444
+ }
445
+ if (previous === 'open') {
446
+ await claim.release();
447
+ return {
448
+ status: 502,
449
+ body: {
450
+ error: 'Your payment could not be set up. Try again.'
451
+ }
452
+ };
453
+ }
454
+ }
455
+ let taxRateId = '';
456
+ if (charge.taxPct !== null && charge.taxableCents > 0) {
457
+ var _ref2;
458
+ var _order_quote2;
459
+ const basis = (_order_quote2 = order.quote) == null ? void 0 : _order_quote2.tax;
460
+ taxRateId = (_ref2 = await deps.taxRateId({
461
+ hostRef,
462
+ taxPct: charge.taxPct,
463
+ taxLabel: (basis == null ? void 0 : basis.kind) === 'rate' ? basis.label || `Tax (${charge.taxPct}%)` : 'Sales tax',
464
+ headers: stripeKey(`tax-rate-${Math.round(charge.taxPct * 10000)}`)
465
+ })) != null ? _ref2 : '';
466
+ if (!taxRateId) {
467
+ // Never an untaxed session for a taxed quote.
468
+ await claim.release();
469
+ return {
470
+ status: 502,
471
+ body: {
472
+ error: 'Your payment could not be set up. Try again.'
473
+ }
474
+ };
475
+ }
476
+ }
477
+ const paymentMethods = CommerceModel.resolveStorefrontPaymentMethodControls(CommerceModel.normalizeStorefrontPaymentMethodSettings(store.get('paymentMethods')));
478
+ const params = buildQuoteSessionParams({
479
+ hostId,
480
+ quoteId,
481
+ order,
482
+ charge,
483
+ accountId: String(accountId),
484
+ feeCents,
485
+ taxRateId,
486
+ returnUrl,
487
+ paymentMethods
488
+ });
489
+ if (nativeMode.native) {
490
+ var _params_get;
491
+ applyNativeCheckoutParams(params, (_params_get = params.get('success_url')) != null ? _params_get : returnUrl);
492
+ }
493
+ const domain = nativeMode.native ? deps.ensureDomain({
494
+ pageUrl: returnUrl,
495
+ site,
496
+ hostId
497
+ }) : Promise.resolve(null);
498
+ const created = await deps.createSession(params, _extends({}, stripeKey('session'), nativeCheckoutStripeHeaders(nativeMode)));
499
+ await domain;
500
+ const payload = created.ok ? readCheckoutSessionPayload(created.session, nativeMode, paymentMethods.wallets) : null;
501
+ if (!payload || !created.session.id) {
502
+ var _created_session;
503
+ console.error('Stripe quote checkout error', (_created_session = created.session) == null ? void 0 : _created_session.error);
504
+ await claim.release();
505
+ return {
506
+ status: 502,
507
+ body: {
508
+ error: 'Your payment could not be set up. Try again.'
509
+ }
510
+ };
511
+ }
512
+ minted = String(created.session.id);
513
+ // The quote keeps the session it now has open. Whatever another press
514
+ // stored meanwhile is closed after, so the last press is the one open
515
+ // session; a quote that was paid, replaced or edited meanwhile closes
516
+ // this one instead.
517
+ const orderRef = hostRef.collection('orders').doc(quoteId);
518
+ const settled = await deps.firestore().runTransaction(async (transaction)=>{
519
+ var _current_quote;
520
+ const snapshot = await transaction.get(orderRef);
521
+ const current = snapshot.exists ? snapshot.data() : null;
522
+ if (!current || !CommerceModel.isQuote(current)) return {
523
+ outcome: NOT_FOUND,
524
+ displaced: ''
525
+ };
526
+ if (!CommerceModel.isUnpaidQuote(current)) {
527
+ return {
528
+ outcome: refusal(409, 'paid', CommerceModel.quotePayRefusalMessage('paid', storeName)),
529
+ displaced: ''
530
+ };
531
+ }
532
+ if (CommerceModel.quoteLinkVersion(current.quote) !== CommerceModel.quoteLinkVersion(order.quote)) {
533
+ return {
534
+ outcome: {
535
+ status: 410,
536
+ body: {
537
+ replaced: true,
538
+ error: `This quote was updated, and ${storeName || 'the business'} sent you a newer link.`
539
+ }
540
+ },
541
+ displaced: ''
542
+ };
543
+ }
544
+ if ((Number((_current_quote = current.quote) == null ? void 0 : _current_quote.updatedAtMs) || 0) !== openedAtEdit) {
545
+ return {
546
+ outcome: refusal(409, 'updated', QUOTE_UPDATED_MESSAGE),
547
+ displaced: ''
548
+ };
549
+ }
550
+ transaction.set(orderRef, {
551
+ checkoutSessionId: minted
552
+ }, {
553
+ merge: true
554
+ });
555
+ const displaced = current.checkoutSessionId && current.checkoutSessionId !== openedFor ? current.checkoutSessionId : '';
556
+ return {
557
+ outcome: null,
558
+ displaced
559
+ };
560
+ });
561
+ if (settled.outcome) {
562
+ await deps.expireSession(minted);
563
+ await claim.release();
564
+ return settled.outcome;
565
+ }
566
+ if (settled.displaced && settled.displaced !== minted) await deps.expireSession(settled.displaced);
567
+ await claim.record(200, payload);
568
+ return {
569
+ status: 200,
570
+ body: payload
571
+ };
572
+ } catch (error) {
573
+ console.error('quote pay failed', error);
574
+ if (minted) await deps.expireSession(minted).catch(()=>'open');
575
+ await claim.release();
576
+ return {
577
+ status: 500,
578
+ body: {
579
+ error: 'Your payment could not be set up. Try again.'
580
+ }
581
+ };
582
+ }
583
+ }
584
+ /** Sends a link route's answer, never cached or indexed. */ export function sendQuoteLinkOutcome(res, outcome) {
585
+ var _outcome_headers;
586
+ res.setHeader('Cache-Control', 'private, no-store');
587
+ res.setHeader('X-Robots-Tag', 'noindex, nofollow');
588
+ for (const [name, value] of Object.entries((_outcome_headers = outcome.headers) != null ? _outcome_headers : {}))res.setHeader(name, value);
589
+ return res.status(outcome.status).json(outcome.body);
590
+ }
591
+ /** `GET /api/commerce/quote-view`. */ export const quoteViewHandler = async (req, res)=>{
592
+ try {
593
+ return sendQuoteLinkOutcome(res, await handleQuoteView(defaultQuotePayDeps(), req));
594
+ } catch (error) {
595
+ console.error('quote view failed', error);
596
+ return sendQuoteLinkOutcome(res, {
597
+ status: 500,
598
+ body: {
599
+ error: 'This quote could not be loaded. Try again.'
600
+ }
601
+ });
602
+ }
603
+ };
604
+ /** `POST /api/commerce/quote-pay`. */ export const quotePayHandler = async (req, res)=>sendQuoteLinkOutcome(res, await handleQuotePay(defaultQuotePayDeps(), req));
605
+
606
+ //# sourceMappingURL=quote-pay.js.map