@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,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Closing a Checkout Session so it can no longer be paid, and saying what it
|
|
19
|
+
* was (AGL-4094).
|
|
20
|
+
*
|
|
21
|
+
* Stripe answers 200 to `expire` for a session that was open, and 400 for one
|
|
22
|
+
* that is not — already expired, or already PAID. Those two must not be read
|
|
23
|
+
* alike: a caller about to open a fresh session for the same quote has to
|
|
24
|
+
* know that the old one was paid a moment ago, or it hands the customer a
|
|
25
|
+
* second way to pay. So a refused expiry reads the session back and answers
|
|
26
|
+
* its status.
|
|
27
|
+
*
|
|
28
|
+
* - `expired`: it can no longer be paid (it was open and is now closed, or it
|
|
29
|
+
* had expired already).
|
|
30
|
+
* - `complete`: it was paid; the webhook has, or will, record it.
|
|
31
|
+
* - `missing`: Stripe does not know it (another mode's id, or never created).
|
|
32
|
+
* - `open`: it could not be closed and may still be paid. The caller treats
|
|
33
|
+
* this as a failure to retry, never as closed.
|
|
34
|
+
*/
|
|
35
|
+
export type CheckoutSessionExpiry = 'expired' | 'complete' | 'missing' | 'open';
|
|
36
|
+
export declare function expireCheckoutSession(sessionId: string): Promise<CheckoutSessionExpiry>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* Closing a Checkout Session so it can no longer be paid, and saying what it
|
|
18
|
+
* was (AGL-4094).
|
|
19
|
+
*
|
|
20
|
+
* Stripe answers 200 to `expire` for a session that was open, and 400 for one
|
|
21
|
+
* that is not — already expired, or already PAID. Those two must not be read
|
|
22
|
+
* alike: a caller about to open a fresh session for the same quote has to
|
|
23
|
+
* know that the old one was paid a moment ago, or it hands the customer a
|
|
24
|
+
* second way to pay. So a refused expiry reads the session back and answers
|
|
25
|
+
* its status.
|
|
26
|
+
*
|
|
27
|
+
* - `expired`: it can no longer be paid (it was open and is now closed, or it
|
|
28
|
+
* had expired already).
|
|
29
|
+
* - `complete`: it was paid; the webhook has, or will, record it.
|
|
30
|
+
* - `missing`: Stripe does not know it (another mode's id, or never created).
|
|
31
|
+
* - `open`: it could not be closed and may still be paid. The caller treats
|
|
32
|
+
* this as a failure to retry, never as closed.
|
|
33
|
+
*/ const SESSION_ID = /^cs_(test|live)_[A-Za-z0-9]{8,255}$/;
|
|
34
|
+
export async function expireCheckoutSession(sessionId) {
|
|
35
|
+
var _process_env_STRIPE_SECRET_KEY;
|
|
36
|
+
if (!SESSION_ID.test(String(sessionId != null ? sessionId : ''))) return 'missing';
|
|
37
|
+
const secretKey = String((_process_env_STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY) != null ? _process_env_STRIPE_SECRET_KEY : '').trim();
|
|
38
|
+
if (!secretKey) return 'open';
|
|
39
|
+
const headers = {
|
|
40
|
+
Authorization: `Bearer ${secretKey}`,
|
|
41
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
42
|
+
};
|
|
43
|
+
try {
|
|
44
|
+
const expired = await fetch(`https://api.stripe.com/v1/checkout/sessions/${sessionId}/expire`, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers
|
|
47
|
+
});
|
|
48
|
+
if (expired.ok) return 'expired';
|
|
49
|
+
const read = await fetch(`https://api.stripe.com/v1/checkout/sessions/${sessionId}`, {
|
|
50
|
+
headers
|
|
51
|
+
});
|
|
52
|
+
if (read.status === 404) return 'missing';
|
|
53
|
+
if (!read.ok) return 'open';
|
|
54
|
+
const session = await read.json().catch(()=>({}));
|
|
55
|
+
if (session.status === 'complete') return 'complete';
|
|
56
|
+
if (session.status === 'expired') return 'expired';
|
|
57
|
+
return 'open';
|
|
58
|
+
} catch (unused) {
|
|
59
|
+
return 'open';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
//# sourceMappingURL=checkout-session-expiry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/checkout-session-expiry.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Closing a Checkout Session so it can no longer be paid, and saying what it\n * was (AGL-4094).\n *\n * Stripe answers 200 to `expire` for a session that was open, and 400 for one\n * that is not — already expired, or already PAID. Those two must not be read\n * alike: a caller about to open a fresh session for the same quote has to\n * know that the old one was paid a moment ago, or it hands the customer a\n * second way to pay. So a refused expiry reads the session back and answers\n * its status.\n *\n * - `expired`: it can no longer be paid (it was open and is now closed, or it\n * had expired already).\n * - `complete`: it was paid; the webhook has, or will, record it.\n * - `missing`: Stripe does not know it (another mode's id, or never created).\n * - `open`: it could not be closed and may still be paid. The caller treats\n * this as a failure to retry, never as closed.\n */\nexport type CheckoutSessionExpiry = 'expired' | 'complete' | 'missing' | 'open'\n\nconst SESSION_ID = /^cs_(test|live)_[A-Za-z0-9]{8,255}$/\n\nexport async function expireCheckoutSession(sessionId: string): Promise<CheckoutSessionExpiry> {\n if (!SESSION_ID.test(String(sessionId ?? ''))) return 'missing'\n const secretKey = String(process.env.STRIPE_SECRET_KEY ?? '').trim()\n if (!secretKey) return 'open'\n const headers = {\n Authorization: `Bearer ${secretKey}`,\n 'Content-Type': 'application/x-www-form-urlencoded',\n }\n try {\n const expired = await fetch(`https://api.stripe.com/v1/checkout/sessions/${sessionId}/expire`, {\n method: 'POST',\n headers,\n })\n if (expired.ok) return 'expired'\n const read = await fetch(`https://api.stripe.com/v1/checkout/sessions/${sessionId}`, { headers })\n if (read.status === 404) return 'missing'\n if (!read.ok) return 'open'\n const session = (await read.json().catch(() => ({}))) as { status?: string }\n if (session.status === 'complete') return 'complete'\n if (session.status === 'expired') return 'expired'\n return 'open'\n } catch {\n return 'open'\n }\n}\n"],"names":["SESSION_ID","expireCheckoutSession","sessionId","process","test","String","secretKey","env","STRIPE_SECRET_KEY","trim","headers","Authorization","expired","fetch","method","ok","read","status","session","json","catch"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;CAiBC,GAGD,MAAMA,aAAa;AAEnB,OAAO,eAAeC,sBAAsBC,SAAiB;QAElCC;IADzB,IAAI,CAACH,WAAWI,IAAI,CAACC,OAAOH,oBAAAA,YAAa,MAAM,OAAO;IACtD,MAAMI,YAAYD,QAAOF,iCAAAA,QAAQI,GAAG,CAACC,iBAAiB,YAA7BL,iCAAiC,IAAIM,IAAI;IAClE,IAAI,CAACH,WAAW,OAAO;IACvB,MAAMI,UAAU;QACdC,eAAe,CAAC,OAAO,EAAEL,WAAW;QACpC,gBAAgB;IAClB;IACA,IAAI;QACF,MAAMM,UAAU,MAAMC,MAAM,CAAC,4CAA4C,EAAEX,UAAU,OAAO,CAAC,EAAE;YAC7FY,QAAQ;YACRJ;QACF;QACA,IAAIE,QAAQG,EAAE,EAAE,OAAO;QACvB,MAAMC,OAAO,MAAMH,MAAM,CAAC,4CAA4C,EAAEX,WAAW,EAAE;YAAEQ;QAAQ;QAC/F,IAAIM,KAAKC,MAAM,KAAK,KAAK,OAAO;QAChC,IAAI,CAACD,KAAKD,EAAE,EAAE,OAAO;QACrB,MAAMG,UAAW,MAAMF,KAAKG,IAAI,GAAGC,KAAK,CAAC,IAAO,CAAA,CAAC,CAAA;QACjD,IAAIF,QAAQD,MAAM,KAAK,YAAY,OAAO;QAC1C,IAAIC,QAAQD,MAAM,KAAK,WAAW,OAAO;QACzC,OAAO;IACT,EAAE,eAAM;QACN,OAAO;IACT;AACF"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { PluginApiHandler, PluginApiRequest } from '@aglyn/aglyn/server';
|
|
18
|
+
import { type PdfJpegImage } from '@aglyn/shared-util-tools/pdf/pdf-file';
|
|
19
|
+
import { loadSharp } from '@aglyn/tenant-data-admin/server/media-variants';
|
|
20
|
+
import * as CommerceModel from '../model';
|
|
21
|
+
import { type CommercePdfBrand, type CommercePdfColors } from '../utils/commerce-document-pdf';
|
|
22
|
+
import { type CommercePdfFile } from './pdf-response';
|
|
23
|
+
import { type QuoteLinkOutcome, type QuotePayDeps } from './quote-pay';
|
|
24
|
+
type HostDocument = Record<string, unknown>;
|
|
25
|
+
/**
|
|
26
|
+
* The business's contact as the document prints it: its postal address (a
|
|
27
|
+
* block of text, a line per line), the email visitors write to, its phone,
|
|
28
|
+
* and its web address without the scheme.
|
|
29
|
+
*/
|
|
30
|
+
export declare function commercePdfContactLines(host: HostDocument | null | undefined): string[];
|
|
31
|
+
/** The site theme's colors for a document, or the neutral ones when the theme cannot be read. */
|
|
32
|
+
export declare function commercePdfSiteColors(host: HostDocument | null | undefined): CommercePdfColors;
|
|
33
|
+
/**
|
|
34
|
+
* A logo's bytes as a JPEG the PDF can carry: turned upright, fitted inside
|
|
35
|
+
* {@link LOGO_PIXELS}, transparency flattened onto white, and small enough to
|
|
36
|
+
* attach. Null when the bytes are not an image sharp reads.
|
|
37
|
+
*/
|
|
38
|
+
export declare function logoJpeg(source: Buffer, sharpFactory?: Awaited<ReturnType<typeof loadSharp>>): Promise<PdfJpegImage | null>;
|
|
39
|
+
export interface CommercePdfBrandDeps {
|
|
40
|
+
/** The site's logo as a JPEG, or null; never throws. */
|
|
41
|
+
logo(input: {
|
|
42
|
+
hostId: string;
|
|
43
|
+
orgId: string;
|
|
44
|
+
value: string;
|
|
45
|
+
}): Promise<PdfJpegImage | null>;
|
|
46
|
+
orgForHost(hostId: string): Promise<{
|
|
47
|
+
id?: string;
|
|
48
|
+
org?: unknown;
|
|
49
|
+
} | null>;
|
|
50
|
+
}
|
|
51
|
+
export declare function defaultCommercePdfBrandDeps(): CommercePdfBrandDeps;
|
|
52
|
+
/** Who a site's documents are from: its name, contact, colors, logo and time zone. */
|
|
53
|
+
export declare function loadCommercePdfBrand(input: {
|
|
54
|
+
hostId: string;
|
|
55
|
+
host: HostDocument;
|
|
56
|
+
owner?: {
|
|
57
|
+
id?: string;
|
|
58
|
+
org?: unknown;
|
|
59
|
+
} | null;
|
|
60
|
+
}, deps?: CommercePdfBrandDeps): Promise<CommercePdfBrand>;
|
|
61
|
+
/** `acme-signs-quote-1042.pdf`: the business, the document and its number, in ASCII. */
|
|
62
|
+
export declare function commercePdfFilename(businessName: string, document: 'quote' | 'invoice', number: unknown, docId: string): string;
|
|
63
|
+
/** A quote's PDF: what its customer's page shows, from the business, with its pay link when given one. */
|
|
64
|
+
export declare function quotePdfFile(input: {
|
|
65
|
+
hostId: string;
|
|
66
|
+
quoteId: string;
|
|
67
|
+
order: CommerceModel.HostOrder;
|
|
68
|
+
host: HostDocument;
|
|
69
|
+
store: HostDocument | null | undefined;
|
|
70
|
+
owner?: {
|
|
71
|
+
id?: string;
|
|
72
|
+
org?: unknown;
|
|
73
|
+
} | null;
|
|
74
|
+
/** Printed on a quote still to be paid; null leaves it off. */
|
|
75
|
+
payUrl: string | null;
|
|
76
|
+
}, deps?: CommercePdfBrandDeps): Promise<CommercePdfFile>;
|
|
77
|
+
/** An order's invoice PDF, a paid quote's included. */
|
|
78
|
+
export declare function orderInvoicePdfFile(input: {
|
|
79
|
+
hostId: string;
|
|
80
|
+
orderId: string;
|
|
81
|
+
order: CommerceModel.HostOrder;
|
|
82
|
+
host: HostDocument;
|
|
83
|
+
store: HostDocument | null | undefined;
|
|
84
|
+
owner?: {
|
|
85
|
+
id?: string;
|
|
86
|
+
org?: unknown;
|
|
87
|
+
} | null;
|
|
88
|
+
}, deps?: CommercePdfBrandDeps): Promise<CommercePdfFile>;
|
|
89
|
+
/** Downloads one address may make per site, per ten minutes. */
|
|
90
|
+
export declare const QUOTE_PDF_DOWNLOADS_PER_WINDOW = 20;
|
|
91
|
+
export interface QuotePdfRouteDeps extends QuotePayDeps {
|
|
92
|
+
quotePdf: typeof quotePdfFile;
|
|
93
|
+
}
|
|
94
|
+
export declare function handleQuotePdf(deps: QuotePdfRouteDeps, req: PluginApiRequest): Promise<QuoteLinkOutcome | {
|
|
95
|
+
status: 200;
|
|
96
|
+
pdf: CommercePdfFile;
|
|
97
|
+
}>;
|
|
98
|
+
/** `GET /api/commerce/quote-pdf`. */
|
|
99
|
+
export declare const quotePdfHandler: PluginApiHandler;
|
|
100
|
+
export {};
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import { resolveSiteTimeZone } from "@aglyn/aglyn/app-utils/collection-entry-date";
|
|
18
|
+
import { resolveHostToken } from "@aglyn/aglyn/app-utils/host-tokens";
|
|
19
|
+
import { resolveSiteTheme } from "@aglyn/aglyn/app-utils/site-theme";
|
|
20
|
+
import { buildEmailPalette, emailPaletteBaseForHost } from "@aglyn/shared-util-email";
|
|
21
|
+
import { readJpeg } from "@aglyn/shared-util-tools/pdf/pdf-file";
|
|
22
|
+
import { getOrgForHost } from "@aglyn/tenant-data-admin";
|
|
23
|
+
import { loadSharp } from "@aglyn/tenant-data-admin/server/media-variants";
|
|
24
|
+
import * as CommerceModel from "../model/index.js";
|
|
25
|
+
import { COMMERCE_PDF_NEUTRAL_COLORS, commercePdfColorsFromPalette, renderOrderInvoicePdf, renderQuotePdf } from "../utils/commerce-document-pdf.js";
|
|
26
|
+
import { invoiceStoreFrom } from "../utils/order-invoice.js";
|
|
27
|
+
import { orderStatusUrl } from "./order-status-token.js";
|
|
28
|
+
import { sendPdf } from "./pdf-response.js";
|
|
29
|
+
import { quotePayLinkUrl } from "./quote-link-token.js";
|
|
30
|
+
import { defaultQuotePayDeps, limitedQuoteLink, loadLinkedQuote, quoteLinkParams, sendQuoteLinkOutcome } from "./quote-pay.js";
|
|
31
|
+
import { locateImageSource, readImageSourceBytes } from "./site-image-source.js";
|
|
32
|
+
/*==========================================
|
|
33
|
+
* A QUOTE'S AND AN ORDER'S PDF ON THE SERVER (AGL-4095).
|
|
34
|
+
*
|
|
35
|
+
* What the renderers in `utils/commerce-document-pdf.ts` are given comes from
|
|
36
|
+
* here: the site's business name and contact, its theme's colors, its logo
|
|
37
|
+
* (read from storage or its public address, converted to a JPEG on white by
|
|
38
|
+
* sharp, and left out when there is none or it cannot be read), its time
|
|
39
|
+
* zone, and the quote or order itself.
|
|
40
|
+
*
|
|
41
|
+
* Three doors use it, each loading this module only when it is asked for a
|
|
42
|
+
* PDF, so sharp and the PDF writer stay out of every other request:
|
|
43
|
+
*
|
|
44
|
+
* POST commerce/quote { action: 'pdf' } the console's quote ("Create and edit quotes")
|
|
45
|
+
* GET commerce/order-print?kind=invoice-pdf the console's and the apps' invoice ("Manage orders")
|
|
46
|
+
* GET commerce/quote-pdf?hostId=&q=&t= the customer's quote, by its signed link
|
|
47
|
+
*=========================================*/ /** A logo source larger than this is not read. */ const LOGO_SOURCE_MAX_BYTES = 10000000;
|
|
48
|
+
/** The logo's pixels: about four times the box it is drawn in, never enlarged. */ const LOGO_PIXELS = {
|
|
49
|
+
width: 720,
|
|
50
|
+
height: 200
|
|
51
|
+
};
|
|
52
|
+
/** A converted logo larger than this is left out rather than bloat every copy of the document. */ const LOGO_MAX_JPEG_BYTES = 350000;
|
|
53
|
+
/** A source whose decoded pixels exceed this is refused rather than decoded. */ const LOGO_MAX_INPUT_PIXELS = 50000000;
|
|
54
|
+
/** The ground a logo's transparent pixels are flattened onto: the page's white. */ const LOGO_GROUND = {
|
|
55
|
+
r: 255,
|
|
56
|
+
g: 255,
|
|
57
|
+
b: 255
|
|
58
|
+
};
|
|
59
|
+
const trimmed = (value)=>typeof value === 'string' ? value.trim() : '';
|
|
60
|
+
/**
|
|
61
|
+
* The business's contact as the document prints it: its postal address (a
|
|
62
|
+
* block of text, a line per line), the email visitors write to, its phone,
|
|
63
|
+
* and its web address without the scheme.
|
|
64
|
+
*/ export function commercePdfContactLines(host) {
|
|
65
|
+
var _source_seo_entity, _source_seo, _source_business, _source_seo_entity1, _source_seo1;
|
|
66
|
+
const source = host != null ? host : {};
|
|
67
|
+
const address = trimmed(resolveHostToken('address', source)).split(/\r?\n/).map((line)=>line.trim()).filter(Boolean);
|
|
68
|
+
const email = trimmed((_source_seo = source.seo) == null ? void 0 : (_source_seo_entity = _source_seo.entity) == null ? void 0 : _source_seo_entity.email) || trimmed((_source_business = source.business) == null ? void 0 : _source_business.supportEmail);
|
|
69
|
+
const phone = trimmed((_source_seo1 = source.seo) == null ? void 0 : (_source_seo_entity1 = _source_seo1.entity) == null ? void 0 : _source_seo_entity1.telephone);
|
|
70
|
+
const site = trimmed(resolveHostToken('url', source)).replace(/^https?:\/\//, '').replace(/\/$/, '');
|
|
71
|
+
return [
|
|
72
|
+
...address,
|
|
73
|
+
email,
|
|
74
|
+
phone,
|
|
75
|
+
site
|
|
76
|
+
].filter(Boolean);
|
|
77
|
+
}
|
|
78
|
+
/** The site theme's colors for a document, or the neutral ones when the theme cannot be read. */ export function commercePdfSiteColors(host) {
|
|
79
|
+
try {
|
|
80
|
+
var _ref;
|
|
81
|
+
var _theme_colorSchemes;
|
|
82
|
+
const theme = resolveSiteTheme(host != null ? host : null);
|
|
83
|
+
return commercePdfColorsFromPalette(buildEmailPalette({
|
|
84
|
+
base: emailPaletteBaseForHost(host),
|
|
85
|
+
colors: (_ref = theme == null ? void 0 : (_theme_colorSchemes = theme.colorSchemes) == null ? void 0 : _theme_colorSchemes.light) != null ? _ref : null
|
|
86
|
+
}));
|
|
87
|
+
} catch (unused) {
|
|
88
|
+
return _extends({}, COMMERCE_PDF_NEUTRAL_COLORS);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** sharp's decoding options; an SVG is rasterized dense enough for the logo's pixels. */ async function decodeOptions(sharp, source) {
|
|
92
|
+
var _meta_width, _meta_height;
|
|
93
|
+
const base = {
|
|
94
|
+
limitInputPixels: LOGO_MAX_INPUT_PIXELS,
|
|
95
|
+
failOn: 'error'
|
|
96
|
+
};
|
|
97
|
+
const meta = await sharp(source, base).metadata();
|
|
98
|
+
if (meta.format !== 'svg') return base;
|
|
99
|
+
const longest = Math.max((_meta_width = meta.width) != null ? _meta_width : 0, (_meta_height = meta.height) != null ? _meta_height : 0);
|
|
100
|
+
if (!longest) return base;
|
|
101
|
+
return _extends({}, base, {
|
|
102
|
+
density: Math.min(2400, Math.max(1, Math.ceil(72 * LOGO_PIXELS.width / longest)))
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* A logo's bytes as a JPEG the PDF can carry: turned upright, fitted inside
|
|
107
|
+
* {@link LOGO_PIXELS}, transparency flattened onto white, and small enough to
|
|
108
|
+
* attach. Null when the bytes are not an image sharp reads.
|
|
109
|
+
*/ export async function logoJpeg(source, sharpFactory) {
|
|
110
|
+
const sharp = sharpFactory != null ? sharpFactory : await loadSharp();
|
|
111
|
+
const options = await decodeOptions(sharp, source);
|
|
112
|
+
for (const quality of [
|
|
113
|
+
86,
|
|
114
|
+
72
|
|
115
|
+
]){
|
|
116
|
+
const jpeg = await sharp(source, options).rotate().resize(LOGO_PIXELS.width, LOGO_PIXELS.height, {
|
|
117
|
+
fit: 'inside',
|
|
118
|
+
withoutEnlargement: true
|
|
119
|
+
}).flatten({
|
|
120
|
+
background: LOGO_GROUND
|
|
121
|
+
}).jpeg({
|
|
122
|
+
quality
|
|
123
|
+
}).toBuffer();
|
|
124
|
+
if (jpeg.length <= LOGO_MAX_JPEG_BYTES) return readJpeg(new Uint8Array(jpeg));
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
export function defaultCommercePdfBrandDeps() {
|
|
129
|
+
return {
|
|
130
|
+
logo: async ({ hostId, orgId, value })=>{
|
|
131
|
+
try {
|
|
132
|
+
const located = await locateImageSource(value, hostId, orgId, LOGO_SOURCE_MAX_BYTES);
|
|
133
|
+
const bytes = located ? await readImageSourceBytes(located, LOGO_SOURCE_MAX_BYTES) : null;
|
|
134
|
+
return bytes ? await logoJpeg(bytes) : null;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.warn('[commerce/pdf] the site logo was left out', error);
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
orgForHost: async (hostId)=>{
|
|
141
|
+
const found = await getOrgForHost(hostId);
|
|
142
|
+
return found ? {
|
|
143
|
+
id: found.orgId,
|
|
144
|
+
org: found.org
|
|
145
|
+
} : null;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** Who a site's documents are from: its name, contact, colors, logo and time zone. */ export async function loadCommercePdfBrand(input, deps = defaultCommercePdfBrandDeps()) {
|
|
150
|
+
var _ref, _ref1;
|
|
151
|
+
const host = input.host;
|
|
152
|
+
const owner = input.owner === undefined ? await deps.orgForHost(input.hostId).catch(()=>null) : input.owner;
|
|
153
|
+
const logoValue = trimmed(resolveHostToken('logo', host));
|
|
154
|
+
const logo = logoValue ? await deps.logo({
|
|
155
|
+
hostId: input.hostId,
|
|
156
|
+
orgId: String((_ref = owner == null ? void 0 : owner.id) != null ? _ref : ''),
|
|
157
|
+
value: logoValue
|
|
158
|
+
}) : null;
|
|
159
|
+
return {
|
|
160
|
+
businessName: trimmed(resolveHostToken('businessName', host)) || trimmed(host['name']),
|
|
161
|
+
contactLines: commercePdfContactLines(host),
|
|
162
|
+
logo,
|
|
163
|
+
colors: commercePdfSiteColors(host),
|
|
164
|
+
timeZone: resolveSiteTimeZone((_ref1 = owner == null ? void 0 : owner.org) != null ? _ref1 : null, host)
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/** `acme-signs-quote-1042.pdf`: the business, the document and its number, in ASCII. */ export function commercePdfFilename(businessName, document, number, docId) {
|
|
168
|
+
const slug = (value)=>value.normalize('NFKD').replace(/[̀-ͯ]/g, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40);
|
|
169
|
+
const numbered = number != null && String(number).trim() ? String(number) : docId.slice(-6);
|
|
170
|
+
return `${[
|
|
171
|
+
slug(businessName),
|
|
172
|
+
document,
|
|
173
|
+
slug(numbered)
|
|
174
|
+
].filter(Boolean).join('-')}.pdf`;
|
|
175
|
+
}
|
|
176
|
+
/** A quote's PDF: what its customer's page shows, from the business, with its pay link when given one. */ export async function quotePdfFile(input, deps) {
|
|
177
|
+
var _ref;
|
|
178
|
+
var _input_store, _input_order_quote;
|
|
179
|
+
const brand = await loadCommercePdfBrand({
|
|
180
|
+
hostId: input.hostId,
|
|
181
|
+
host: input.host,
|
|
182
|
+
owner: input.owner
|
|
183
|
+
}, deps);
|
|
184
|
+
const view = CommerceModel.buildQuotePageView({
|
|
185
|
+
order: input.order,
|
|
186
|
+
orderId: input.quoteId,
|
|
187
|
+
storeName: brand.businessName,
|
|
188
|
+
currency: String((_ref = (_input_store = input.store) == null ? void 0 : _input_store['currency']) != null ? _ref : 'USD'),
|
|
189
|
+
receiptUrl: CommerceModel.isUnpaidQuote(input.order) ? null : orderStatusUrl(input.host, input.hostId, input.quoteId)
|
|
190
|
+
});
|
|
191
|
+
return {
|
|
192
|
+
bytes: renderQuotePdf({
|
|
193
|
+
brand,
|
|
194
|
+
view,
|
|
195
|
+
revision: Number((_input_order_quote = input.order.quote) == null ? void 0 : _input_order_quote.revision) || 1,
|
|
196
|
+
payUrl: input.payUrl
|
|
197
|
+
}),
|
|
198
|
+
filename: commercePdfFilename(brand.businessName, 'quote', input.order.number, input.quoteId)
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/** An order's invoice PDF, a paid quote's included. */ export async function orderInvoicePdfFile(input, deps) {
|
|
202
|
+
const brand = await loadCommercePdfBrand({
|
|
203
|
+
hostId: input.hostId,
|
|
204
|
+
host: input.host,
|
|
205
|
+
owner: input.owner
|
|
206
|
+
}, deps);
|
|
207
|
+
return {
|
|
208
|
+
bytes: renderOrderInvoicePdf({
|
|
209
|
+
brand,
|
|
210
|
+
order: input.order,
|
|
211
|
+
orderId: input.orderId,
|
|
212
|
+
store: invoiceStoreFrom(input.host, input.store)
|
|
213
|
+
}),
|
|
214
|
+
filename: commercePdfFilename(brand.businessName, 'invoice', input.order.number, input.orderId)
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/*==========================================
|
|
218
|
+
* THE CUSTOMER'S DOWNLOAD: GET commerce/quote-pdf?hostId=&q=&t=
|
|
219
|
+
*
|
|
220
|
+
* The same signed link and the same rules as the quote page's data
|
|
221
|
+
* (`commerce/quote-view`): a wrong token answers exactly as a missing quote,
|
|
222
|
+
* a link to an older version of the quote is refused, and every address is
|
|
223
|
+
* rate-limited per site before the token is checked. The PDF is the page's
|
|
224
|
+
* own view, so it shows nothing the page does not.
|
|
225
|
+
*=========================================*/ /** Downloads one address may make per site, per ten minutes. */ export const QUOTE_PDF_DOWNLOADS_PER_WINDOW = 20;
|
|
226
|
+
export async function handleQuotePdf(deps, req) {
|
|
227
|
+
var _req_query, _store_data;
|
|
228
|
+
if (req.method !== 'GET') return {
|
|
229
|
+
status: 405,
|
|
230
|
+
body: {
|
|
231
|
+
error: 'Method not allowed'
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
const { hostId, quoteId, token, complete } = quoteLinkParams((_req_query = req.query) != null ? _req_query : {});
|
|
235
|
+
if (!complete) return {
|
|
236
|
+
status: 400,
|
|
237
|
+
body: {
|
|
238
|
+
error: 'This link is incomplete.'
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
const refused = await limitedQuoteLink(deps, req, `commerce-quote-pdf:${hostId}`, QUOTE_PDF_DOWNLOADS_PER_WINDOW);
|
|
242
|
+
if (refused) return refused;
|
|
243
|
+
const linked = await loadLinkedQuote(deps, hostId, quoteId, token);
|
|
244
|
+
if ('outcome' in linked) return linked.outcome;
|
|
245
|
+
const { host, order, store } = linked.loaded;
|
|
246
|
+
const payUrl = CommerceModel.isUnpaidQuote(order) ? quotePayLinkUrl(host, hostId, quoteId, CommerceModel.quoteLinkVersion(order.quote)) : null;
|
|
247
|
+
const owner = await deps.orgForHost(hostId).catch(()=>null);
|
|
248
|
+
const pdf = await deps.quotePdf({
|
|
249
|
+
hostId,
|
|
250
|
+
quoteId,
|
|
251
|
+
order,
|
|
252
|
+
host,
|
|
253
|
+
store: (_store_data = store.data()) != null ? _store_data : null,
|
|
254
|
+
owner: owner ? {
|
|
255
|
+
id: owner.id,
|
|
256
|
+
org: owner.org
|
|
257
|
+
} : null,
|
|
258
|
+
payUrl
|
|
259
|
+
});
|
|
260
|
+
return {
|
|
261
|
+
status: 200,
|
|
262
|
+
pdf
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/** `GET /api/commerce/quote-pdf`. */ export const quotePdfHandler = async (req, res)=>{
|
|
266
|
+
try {
|
|
267
|
+
const outcome = await handleQuotePdf(_extends({}, defaultQuotePayDeps(), {
|
|
268
|
+
quotePdf: quotePdfFile
|
|
269
|
+
}), req);
|
|
270
|
+
if ('pdf' in outcome) return sendPdf(res, outcome.pdf);
|
|
271
|
+
return sendQuoteLinkOutcome(res, outcome);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
console.error('quote pdf failed', error);
|
|
274
|
+
return sendQuoteLinkOutcome(res, {
|
|
275
|
+
status: 500,
|
|
276
|
+
body: {
|
|
277
|
+
error: 'This quote could not be made into a PDF. Try again.'
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
//# sourceMappingURL=commerce-pdf.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/commerce-pdf.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginApiHandler, PluginApiRequest } from '@aglyn/aglyn/server'\nimport { resolveSiteTimeZone } from '@aglyn/aglyn/app-utils/collection-entry-date'\nimport { resolveHostToken, type HostTokenSource } from '@aglyn/aglyn/app-utils/host-tokens'\nimport { resolveSiteTheme, type ThemeHostDocument } from '@aglyn/aglyn/app-utils/site-theme'\nimport { buildEmailPalette, emailPaletteBaseForHost } from '@aglyn/shared-util-email'\nimport { readJpeg, type PdfJpegImage } from '@aglyn/shared-util-tools/pdf/pdf-file'\nimport { getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { loadSharp } from '@aglyn/tenant-data-admin/server/media-variants'\nimport * as CommerceModel from '../model'\nimport {\n COMMERCE_PDF_NEUTRAL_COLORS,\n commercePdfColorsFromPalette,\n renderOrderInvoicePdf,\n renderQuotePdf,\n type CommercePdfBrand,\n type CommercePdfColors,\n} from '../utils/commerce-document-pdf'\nimport { invoiceStoreFrom } from '../utils/order-invoice'\nimport { orderStatusUrl } from './order-status-token'\nimport { sendPdf, type CommercePdfFile } from './pdf-response'\nimport { quotePayLinkUrl } from './quote-link-token'\nimport {\n defaultQuotePayDeps,\n limitedQuoteLink,\n loadLinkedQuote,\n quoteLinkParams,\n sendQuoteLinkOutcome,\n type QuoteLinkOutcome,\n type QuotePayDeps,\n} from './quote-pay'\nimport { locateImageSource, readImageSourceBytes } from './site-image-source'\n\n/*==========================================\n * A QUOTE'S AND AN ORDER'S PDF ON THE SERVER (AGL-4095).\n *\n * What the renderers in `utils/commerce-document-pdf.ts` are given comes from\n * here: the site's business name and contact, its theme's colors, its logo\n * (read from storage or its public address, converted to a JPEG on white by\n * sharp, and left out when there is none or it cannot be read), its time\n * zone, and the quote or order itself.\n *\n * Three doors use it, each loading this module only when it is asked for a\n * PDF, so sharp and the PDF writer stay out of every other request:\n *\n * POST commerce/quote { action: 'pdf' } the console's quote (\"Create and edit quotes\")\n * GET commerce/order-print?kind=invoice-pdf the console's and the apps' invoice (\"Manage orders\")\n * GET commerce/quote-pdf?hostId=&q=&t= the customer's quote, by its signed link\n *=========================================*/\n\n/** A logo source larger than this is not read. */\nconst LOGO_SOURCE_MAX_BYTES = 10_000_000\n\n/** The logo's pixels: about four times the box it is drawn in, never enlarged. */\nconst LOGO_PIXELS = { width: 720, height: 200 } as const\n\n/** A converted logo larger than this is left out rather than bloat every copy of the document. */\nconst LOGO_MAX_JPEG_BYTES = 350_000\n\n/** A source whose decoded pixels exceed this is refused rather than decoded. */\nconst LOGO_MAX_INPUT_PIXELS = 50_000_000\n\n/** The ground a logo's transparent pixels are flattened onto: the page's white. */\nconst LOGO_GROUND = { r: 255, g: 255, b: 255 } as const\n\ntype HostDocument = Record<string, unknown>\n\nconst trimmed = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/**\n * The business's contact as the document prints it: its postal address (a\n * block of text, a line per line), the email visitors write to, its phone,\n * and its web address without the scheme.\n */\nexport function commercePdfContactLines(host: HostDocument | null | undefined): string[] {\n const source = (host ?? {}) as HostTokenSource & {\n seo?: { entity?: { email?: unknown; telephone?: unknown } }\n business?: { supportEmail?: unknown }\n }\n const address = trimmed(resolveHostToken('address', source))\n .split(/\\r?\\n/)\n .map((line) => line.trim())\n .filter(Boolean)\n const email = trimmed(source.seo?.entity?.email) || trimmed(source.business?.supportEmail)\n const phone = trimmed(source.seo?.entity?.telephone)\n const site = trimmed(resolveHostToken('url', source)).replace(/^https?:\\/\\//, '').replace(/\\/$/, '')\n return [...address, email, phone, site].filter(Boolean)\n}\n\n/** The site theme's colors for a document, or the neutral ones when the theme cannot be read. */\nexport function commercePdfSiteColors(host: HostDocument | null | undefined): CommercePdfColors {\n try {\n const theme = resolveSiteTheme((host ?? null) as ThemeHostDocument | null)\n return commercePdfColorsFromPalette(\n buildEmailPalette({\n base: emailPaletteBaseForHost(host as { cname?: string; subdomain?: string } | null),\n colors: (theme?.colorSchemes?.light ?? null) as Record<string, unknown> | null,\n }),\n )\n } catch {\n return { ...COMMERCE_PDF_NEUTRAL_COLORS }\n }\n}\n\n/** sharp's decoding options; an SVG is rasterized dense enough for the logo's pixels. */\nasync function decodeOptions(sharp: Awaited<ReturnType<typeof loadSharp>>, source: Buffer) {\n const base = { limitInputPixels: LOGO_MAX_INPUT_PIXELS, failOn: 'error' as const }\n const meta = await sharp(source, base).metadata()\n if (meta.format !== 'svg') return base\n const longest = Math.max(meta.width ?? 0, meta.height ?? 0)\n if (!longest) return base\n return { ...base, density: Math.min(2400, Math.max(1, Math.ceil((72 * LOGO_PIXELS.width) / longest))) }\n}\n\n/**\n * A logo's bytes as a JPEG the PDF can carry: turned upright, fitted inside\n * {@link LOGO_PIXELS}, transparency flattened onto white, and small enough to\n * attach. Null when the bytes are not an image sharp reads.\n */\nexport async function logoJpeg(source: Buffer, sharpFactory?: Awaited<ReturnType<typeof loadSharp>>): Promise<PdfJpegImage | null> {\n const sharp = sharpFactory ?? (await loadSharp())\n const options = await decodeOptions(sharp, source)\n for (const quality of [86, 72]) {\n const jpeg = await sharp(source, options)\n .rotate()\n .resize(LOGO_PIXELS.width, LOGO_PIXELS.height, { fit: 'inside', withoutEnlargement: true })\n .flatten({ background: LOGO_GROUND })\n .jpeg({ quality })\n .toBuffer()\n if (jpeg.length <= LOGO_MAX_JPEG_BYTES) return readJpeg(new Uint8Array(jpeg))\n }\n return null\n}\n\nexport interface CommercePdfBrandDeps {\n /** The site's logo as a JPEG, or null; never throws. */\n logo(input: { hostId: string; orgId: string; value: string }): Promise<PdfJpegImage | null>\n orgForHost(hostId: string): Promise<{ id?: string; org?: unknown } | null>\n}\n\nexport function defaultCommercePdfBrandDeps(): CommercePdfBrandDeps {\n return {\n logo: async ({ hostId, orgId, value }) => {\n try {\n const located = await locateImageSource(value, hostId, orgId, LOGO_SOURCE_MAX_BYTES)\n const bytes = located ? await readImageSourceBytes(located, LOGO_SOURCE_MAX_BYTES) : null\n return bytes ? await logoJpeg(bytes) : null\n } catch (error) {\n console.warn('[commerce/pdf] the site logo was left out', error)\n return null\n }\n },\n orgForHost: async (hostId) => {\n const found = await getOrgForHost(hostId)\n return found ? { id: found.orgId, org: found.org } : null\n },\n }\n}\n\n/** Who a site's documents are from: its name, contact, colors, logo and time zone. */\nexport async function loadCommercePdfBrand(\n input: { hostId: string; host: HostDocument; owner?: { id?: string; org?: unknown } | null },\n deps: CommercePdfBrandDeps = defaultCommercePdfBrandDeps(),\n): Promise<CommercePdfBrand> {\n const host = input.host\n const owner = input.owner === undefined ? await deps.orgForHost(input.hostId).catch(() => null) : input.owner\n const logoValue = trimmed(resolveHostToken('logo', host as HostTokenSource))\n const logo = logoValue ? await deps.logo({ hostId: input.hostId, orgId: String(owner?.id ?? ''), value: logoValue }) : null\n return {\n businessName: trimmed(resolveHostToken('businessName', host as HostTokenSource)) || trimmed(host['name']),\n contactLines: commercePdfContactLines(host),\n logo,\n colors: commercePdfSiteColors(host),\n timeZone: resolveSiteTimeZone(\n (owner?.org ?? null) as { timeZone?: string } | null,\n host as { timeZone?: string },\n ),\n }\n}\n\n/** `acme-signs-quote-1042.pdf`: the business, the document and its number, in ASCII. */\nexport function commercePdfFilename(businessName: string, document: 'quote' | 'invoice', number: unknown, docId: string): string {\n const slug = (value: string) =>\n value\n .normalize('NFKD')\n .replace(/[̀-ͯ]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 40)\n const numbered = number != null && String(number).trim() ? String(number) : docId.slice(-6)\n return `${[slug(businessName), document, slug(numbered)].filter(Boolean).join('-')}.pdf`\n}\n\n/** A quote's PDF: what its customer's page shows, from the business, with its pay link when given one. */\nexport async function quotePdfFile(\n input: {\n hostId: string\n quoteId: string\n order: CommerceModel.HostOrder\n host: HostDocument\n store: HostDocument | null | undefined\n owner?: { id?: string; org?: unknown } | null\n /** Printed on a quote still to be paid; null leaves it off. */\n payUrl: string | null\n },\n deps?: CommercePdfBrandDeps,\n): Promise<CommercePdfFile> {\n const brand = await loadCommercePdfBrand({ hostId: input.hostId, host: input.host, owner: input.owner }, deps)\n const view = CommerceModel.buildQuotePageView({\n order: input.order,\n orderId: input.quoteId,\n storeName: brand.businessName,\n currency: String(input.store?.['currency'] ?? 'USD'),\n receiptUrl: CommerceModel.isUnpaidQuote(input.order) ? null : orderStatusUrl(input.host, input.hostId, input.quoteId),\n })\n return {\n bytes: renderQuotePdf({ brand, view, revision: Number(input.order.quote?.revision) || 1, payUrl: input.payUrl }),\n filename: commercePdfFilename(brand.businessName, 'quote', input.order.number, input.quoteId),\n }\n}\n\n/** An order's invoice PDF, a paid quote's included. */\nexport async function orderInvoicePdfFile(\n input: {\n hostId: string\n orderId: string\n order: CommerceModel.HostOrder\n host: HostDocument\n store: HostDocument | null | undefined\n owner?: { id?: string; org?: unknown } | null\n },\n deps?: CommercePdfBrandDeps,\n): Promise<CommercePdfFile> {\n const brand = await loadCommercePdfBrand({ hostId: input.hostId, host: input.host, owner: input.owner }, deps)\n return {\n bytes: renderOrderInvoicePdf({\n brand,\n order: input.order,\n orderId: input.orderId,\n store: invoiceStoreFrom(input.host, input.store),\n }),\n filename: commercePdfFilename(brand.businessName, 'invoice', input.order.number, input.orderId),\n }\n}\n\n/*==========================================\n * THE CUSTOMER'S DOWNLOAD: GET commerce/quote-pdf?hostId=&q=&t=\n *\n * The same signed link and the same rules as the quote page's data\n * (`commerce/quote-view`): a wrong token answers exactly as a missing quote,\n * a link to an older version of the quote is refused, and every address is\n * rate-limited per site before the token is checked. The PDF is the page's\n * own view, so it shows nothing the page does not.\n *=========================================*/\n\n/** Downloads one address may make per site, per ten minutes. */\nexport const QUOTE_PDF_DOWNLOADS_PER_WINDOW = 20\n\nexport interface QuotePdfRouteDeps extends QuotePayDeps {\n quotePdf: typeof quotePdfFile\n}\n\nexport async function handleQuotePdf(\n deps: QuotePdfRouteDeps,\n req: PluginApiRequest,\n): Promise<QuoteLinkOutcome | { status: 200; pdf: CommercePdfFile }> {\n if (req.method !== 'GET') return { status: 405, body: { error: 'Method not allowed' } }\n const { hostId, quoteId, token, complete } = quoteLinkParams((req.query ?? {}) as Record<string, unknown>)\n if (!complete) return { status: 400, body: { error: 'This link is incomplete.' } }\n const refused = await limitedQuoteLink(deps, req, `commerce-quote-pdf:${hostId}`, QUOTE_PDF_DOWNLOADS_PER_WINDOW)\n if (refused) return refused\n const linked = await loadLinkedQuote(deps, hostId, quoteId, token)\n if ('outcome' in linked) return linked.outcome\n const { host, order, store } = linked.loaded\n const payUrl = CommerceModel.isUnpaidQuote(order)\n ? quotePayLinkUrl(host, hostId, quoteId, CommerceModel.quoteLinkVersion(order.quote))\n : null\n const owner = await deps.orgForHost(hostId).catch(() => null)\n const pdf = await deps.quotePdf({\n hostId,\n quoteId,\n order,\n host,\n store: (store.data() ?? null) as HostDocument | null,\n owner: owner ? { id: owner.id, org: owner.org } : null,\n payUrl,\n })\n return { status: 200, pdf }\n}\n\n/** `GET /api/commerce/quote-pdf`. */\nexport const quotePdfHandler: PluginApiHandler = async (req, res) => {\n try {\n const outcome = await handleQuotePdf({ ...defaultQuotePayDeps(), quotePdf: quotePdfFile }, req)\n if ('pdf' in outcome) return sendPdf(res, outcome.pdf)\n return sendQuoteLinkOutcome(res, outcome)\n } catch (error) {\n console.error('quote pdf failed', error)\n return sendQuoteLinkOutcome(res, { status: 500, body: { error: 'This quote could not be made into a PDF. Try again.' } })\n }\n}\n"],"names":["resolveSiteTimeZone","resolveHostToken","resolveSiteTheme","buildEmailPalette","emailPaletteBaseForHost","readJpeg","getOrgForHost","loadSharp","CommerceModel","COMMERCE_PDF_NEUTRAL_COLORS","commercePdfColorsFromPalette","renderOrderInvoicePdf","renderQuotePdf","invoiceStoreFrom","orderStatusUrl","sendPdf","quotePayLinkUrl","defaultQuotePayDeps","limitedQuoteLink","loadLinkedQuote","quoteLinkParams","sendQuoteLinkOutcome","locateImageSource","readImageSourceBytes","LOGO_SOURCE_MAX_BYTES","LOGO_PIXELS","width","height","LOGO_MAX_JPEG_BYTES","LOGO_MAX_INPUT_PIXELS","LOGO_GROUND","r","g","b","trimmed","value","trim","commercePdfContactLines","host","source","address","split","map","line","filter","Boolean","email","seo","entity","business","supportEmail","phone","telephone","site","replace","commercePdfSiteColors","theme","base","colors","colorSchemes","light","decodeOptions","sharp","meta","limitInputPixels","failOn","metadata","format","longest","Math","max","density","min","ceil","logoJpeg","sharpFactory","options","quality","jpeg","rotate","resize","fit","withoutEnlargement","flatten","background","toBuffer","length","Uint8Array","defaultCommercePdfBrandDeps","logo","hostId","orgId","located","bytes","error","console","warn","orgForHost","found","id","org","loadCommercePdfBrand","input","deps","owner","undefined","catch","logoValue","String","businessName","contactLines","timeZone","commercePdfFilename","document","number","docId","slug","normalize","toLowerCase","slice","numbered","join","quotePdfFile","brand","view","buildQuotePageView","order","orderId","quoteId","storeName","currency","store","receiptUrl","isUnpaidQuote","revision","Number","quote","payUrl","filename","orderInvoicePdfFile","QUOTE_PDF_DOWNLOADS_PER_WINDOW","handleQuotePdf","req","method","status","body","token","complete","query","refused","linked","outcome","loaded","quoteLinkVersion","pdf","quotePdf","data","quotePdfHandler","res"],"mappings":"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"}
|
|
@@ -23,6 +23,7 @@ import { merchantAccountIsReady } from "@aglyn/tenant-data-admin/server/payment-
|
|
|
23
23
|
import { checkoutSessionCardAuthenticationParams } from "@aglyn/tenant-data-admin/server/stripe-card-authentication";
|
|
24
24
|
import { planCheckoutShippingWithCarriers } from "./carrier-shipping.js";
|
|
25
25
|
import { resolveManualTaxRateId } from "./manual-tax-rate.js";
|
|
26
|
+
import { orderStatusUrl } from "./order-status-token.js";
|
|
26
27
|
import { holdPromotionSlot, promotionHoldKey } from "./promotion-hold.js";
|
|
27
28
|
import { buildRoute, claimAttempt, Route } from "@aglyn/aglyn/server";
|
|
28
29
|
/**
|
|
@@ -158,12 +159,14 @@ import { buildRoute, claimAttempt, Route } from "@aglyn/aglyn/server";
|
|
|
158
159
|
error: 'Payments are not set up yet'
|
|
159
160
|
});
|
|
160
161
|
}
|
|
161
|
-
// Stripe returns the
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
162
|
+
// Where Stripe returns the BUYER is the business's own site (AGL-4094):
|
|
163
|
+
// the order's status page after paying, the site's home after backing
|
|
164
|
+
// out — see the params below. The console Products page is only the
|
|
165
|
+
// fallback for a site with no public address or no signing secret, which
|
|
166
|
+
// no published site lacks. Console products is the plugin route keyed by
|
|
167
|
+
// org slug + host SUBDOMAIN; server code cannot call
|
|
168
|
+
// useConsoleHostRoute, so resolve them here. Without both, fall back to
|
|
169
|
+
// the origin root rather than a fabricated dead path.
|
|
167
170
|
const consoleSubdomain = (await firestore.collection('hostIndex').doc(hostId).get()).get('subdomain');
|
|
168
171
|
const consoleOrgSlug = ownerOrg == null ? void 0 : (_ownerOrg_org1 = ownerOrg.org) == null ? void 0 : _ownerOrg_org1.slug;
|
|
169
172
|
// The console's own host, as the POS door reads it — never the `Origin`
|
|
@@ -625,6 +628,11 @@ import { buildRoute, claimAttempt, Route } from "@aglyn/aglyn/server";
|
|
|
625
628
|
});
|
|
626
629
|
}
|
|
627
630
|
}
|
|
631
|
+
// The buyer's way back, on the business's site: the order's status page
|
|
632
|
+
// (which reads the paid state from the server) and the site's home.
|
|
633
|
+
const siteData = hostSnapshot.data();
|
|
634
|
+
const buyerStatusUrl = orderStatusUrl(siteData, hostId, orderRef.id);
|
|
635
|
+
const buyerSiteUrl = buyerStatusUrl ? `${new URL(buyerStatusUrl).origin}/` : '';
|
|
628
636
|
const params = new URLSearchParams(_extends({
|
|
629
637
|
mode: 'payment'
|
|
630
638
|
}, checkoutSessionCardAuthenticationParams('payment'), useStripeTax ? {
|
|
@@ -648,8 +656,8 @@ import { buildRoute, claimAttempt, Route } from "@aglyn/aglyn/server";
|
|
|
648
656
|
}), email ? {
|
|
649
657
|
customer_email: email
|
|
650
658
|
} : {}, {
|
|
651
|
-
success_url: `${consoleProductsUrl}?draft=paid`,
|
|
652
|
-
cancel_url: `${consoleProductsUrl}?draft=canceled`,
|
|
659
|
+
success_url: buyerStatusUrl != null ? buyerStatusUrl : `${consoleProductsUrl}?draft=paid`,
|
|
660
|
+
cancel_url: buyerSiteUrl || `${consoleProductsUrl}?draft=canceled`,
|
|
653
661
|
'metadata[type]': 'commerce-draft',
|
|
654
662
|
'metadata[hostId]': hostId,
|
|
655
663
|
'metadata[orderId]': orderRef.id,
|