@aglyn/plugins-commerce 1.0.0-beta.289 → 1.0.0-beta.290
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +13 -13
- package/src/lib/components/console/commerce-pdf-download.d.ts +30 -0
- package/src/lib/components/console/commerce-pdf-download.js +53 -0
- package/src/lib/components/console/commerce-pdf-download.js.map +1 -0
- package/src/lib/components/console/host-quotes-card.component.js +20 -4
- package/src/lib/components/console/host-quotes-card.component.js.map +1 -1
- package/src/lib/components/console/order-invoice.component.d.ts +6 -4
- package/src/lib/components/console/order-invoice.component.js +67 -11
- package/src/lib/components/console/order-invoice.component.js.map +1 -1
- package/src/lib/components/console/quote-editor-dialog.component.js +53 -2
- package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/quote-proofs.component.d.ts +37 -0
- package/src/lib/components/console/quote-proofs.component.js +626 -0
- package/src/lib/components/console/quote-proofs.component.js.map +1 -0
- package/src/lib/components/quote-page-body.js +29 -1
- package/src/lib/components/quote-page-body.js.map +1 -1
- package/src/lib/components/quote-proof-panel.d.ts +42 -0
- package/src/lib/components/quote-proof-panel.js +436 -0
- package/src/lib/components/quote-proof-panel.js.map +1 -0
- package/src/lib/model/commerce-quote-pay.d.ts +27 -0
- package/src/lib/model/commerce-quote-pay.js +36 -3
- package/src/lib/model/commerce-quote-pay.js.map +1 -1
- package/src/lib/model/commerce-quote-proofs.d.ts +159 -0
- package/src/lib/model/commerce-quote-proofs.js +348 -0
- package/src/lib/model/commerce-quote-proofs.js.map +1 -0
- package/src/lib/model/commerce-quotes.d.ts +59 -3
- package/src/lib/model/commerce-quotes.js.map +1 -1
- package/src/lib/model/index.d.ts +1 -0
- package/src/lib/model/index.js +1 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/server/commerce-pdf.d.ts +100 -0
- package/src/lib/server/commerce-pdf.js +283 -0
- package/src/lib/server/commerce-pdf.js.map +1 -0
- package/src/lib/server/order-print.d.ts +6 -0
- package/src/lib/server/order-print.js +22 -5
- package/src/lib/server/order-print.js.map +1 -1
- package/src/lib/server/pdf-response.d.ts +31 -0
- package/src/lib/server/pdf-response.js +30 -0
- package/src/lib/server/pdf-response.js.map +1 -0
- package/src/lib/server/pos-reader-splash.d.ts +2 -18
- package/src/lib/server/pos-reader-splash.js +8 -93
- package/src/lib/server/pos-reader-splash.js.map +1 -1
- package/src/lib/server/quote-pay.d.ts +31 -4
- package/src/lib/server/quote-pay.js +11 -11
- package/src/lib/server/quote-pay.js.map +1 -1
- package/src/lib/server/quote-proofs.d.ts +51 -0
- package/src/lib/server/quote-proofs.js +500 -0
- package/src/lib/server/quote-proofs.js.map +1 -0
- package/src/lib/server/quote-uploads.d.ts +33 -0
- package/src/lib/server/quote-uploads.js +29 -0
- package/src/lib/server/quote-uploads.js.map +1 -0
- package/src/lib/server/quote.d.ts +14 -0
- package/src/lib/server/quote.js +336 -3
- package/src/lib/server/quote.js.map +1 -1
- package/src/lib/server/site-image-source.d.ts +38 -0
- package/src/lib/server/site-image-source.js +130 -0
- package/src/lib/server/site-image-source.js.map +1 -0
- package/src/lib/server.js +19 -1
- package/src/lib/server.js.map +1 -1
- package/src/lib/utils/commerce-document-pdf.d.ts +77 -0
- package/src/lib/utils/commerce-document-pdf.js +608 -0
- package/src/lib/utils/commerce-document-pdf.js.map +1 -0
- package/src/lib/utils/order-invoice.d.ts +52 -3
- package/src/lib/utils/order-invoice.js +116 -64
- package/src/lib/utils/order-invoice.js.map +1 -1
|
@@ -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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"}
|
|
@@ -16,14 +16,20 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { type PluginApiHandler } from '@aglyn/aglyn/server';
|
|
18
18
|
import { type HostOrder } from '../model';
|
|
19
|
+
import type { orderInvoicePdfFile } from './commerce-pdf';
|
|
20
|
+
import { type CommercePdfFile } from './pdf-response';
|
|
19
21
|
export type OrderPrintKind = 'invoice' | 'packing-slip';
|
|
20
22
|
export declare const ORDER_PRINT_KINDS: readonly OrderPrintKind[];
|
|
23
|
+
/** The invoice as a PDF file, rather than a page to print. */
|
|
24
|
+
export declare const ORDER_INVOICE_PDF_KIND = "invoice-pdf";
|
|
21
25
|
type Firestore = FirebaseFirestore.Firestore;
|
|
22
26
|
export interface OrderPrintDeps {
|
|
23
27
|
firestore?: () => Firestore;
|
|
24
28
|
verifyIdToken?: (token: string) => Promise<Record<string, unknown> & {
|
|
25
29
|
uid: string;
|
|
26
30
|
}>;
|
|
31
|
+
/** The invoice's PDF; loads the PDF writer and sharp only when asked. */
|
|
32
|
+
invoicePdf?: (input: Parameters<typeof orderInvoicePdfFile>[0]) => Promise<CommercePdfFile>;
|
|
27
33
|
}
|
|
28
34
|
/** The page and its title for one order: what the console's popup is given. */
|
|
29
35
|
export declare function orderPrintPage(kind: OrderPrintKind, input: {
|
|
@@ -20,11 +20,13 @@ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
|
20
20
|
import { formatOrderNumber, liftLegacyOrder } from "../model/index.js";
|
|
21
21
|
import { buildOrderInvoiceHtml, invoiceStoreFrom } from "../utils/order-invoice.js";
|
|
22
22
|
import { buildPackingSlipHtml } from "../utils/order-packing-slip.js";
|
|
23
|
+
import { sendPdf } from "./pdf-response.js";
|
|
23
24
|
import { validId } from "./products-write.js";
|
|
24
25
|
export const ORDER_PRINT_KINDS = [
|
|
25
26
|
'invoice',
|
|
26
27
|
'packing-slip'
|
|
27
28
|
];
|
|
29
|
+
/** The invoice as a PDF file, rather than a page to print. */ export const ORDER_INVOICE_PDF_KIND = 'invoice-pdf';
|
|
28
30
|
/** The page and its title for one order: what the console's popup is given. */ export function orderPrintPage(kind, input) {
|
|
29
31
|
const number = formatOrderNumber(input.order, input.orderId);
|
|
30
32
|
if (kind === 'packing-slip') {
|
|
@@ -44,7 +46,7 @@ export const ORDER_PRINT_KINDS = [
|
|
|
44
46
|
}
|
|
45
47
|
export function createOrderPrintHandler(options = {}) {
|
|
46
48
|
return async (req, res)=>{
|
|
47
|
-
var _options_firestore, _options_verifyIdToken, _req_query_kind, _req_headers_authorization;
|
|
49
|
+
var _options_firestore, _options_verifyIdToken, _options_invoicePdf, _req_query_kind, _req_headers_authorization;
|
|
48
50
|
if (req.method !== 'GET') return res.status(405).json({
|
|
49
51
|
error: 'Method not allowed'
|
|
50
52
|
});
|
|
@@ -52,13 +54,17 @@ export function createOrderPrintHandler(options = {}) {
|
|
|
52
54
|
const verifyIdToken = (_options_verifyIdToken = options.verifyIdToken) != null ? _options_verifyIdToken : (token)=>firebaseAdmin.app().auth().verifyIdToken(token);
|
|
53
55
|
const hostId = req.query['hostId'];
|
|
54
56
|
const orderId = req.query['orderId'];
|
|
57
|
+
const invoicePdf = (_options_invoicePdf = options.invoicePdf) != null ? _options_invoicePdf : async (input)=>(await import("./commerce-pdf.js")).orderInvoicePdfFile(input);
|
|
55
58
|
const kind = String((_req_query_kind = req.query['kind']) != null ? _req_query_kind : '');
|
|
59
|
+
const asPdf = String(kind) === ORDER_INVOICE_PDF_KIND;
|
|
56
60
|
if (!validId(hostId) || !validId(orderId)) return res.status(400).json({
|
|
57
61
|
error: 'Missing site or order'
|
|
58
62
|
});
|
|
59
|
-
if (!ORDER_PRINT_KINDS.includes(kind))
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
if (!asPdf && !ORDER_PRINT_KINDS.includes(kind)) {
|
|
64
|
+
return res.status(400).json({
|
|
65
|
+
error: 'kind must be invoice, invoice-pdf or packing-slip'
|
|
66
|
+
});
|
|
67
|
+
}
|
|
62
68
|
const authorization = String((_req_headers_authorization = req.headers.authorization) != null ? _req_headers_authorization : '');
|
|
63
69
|
const token = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : '';
|
|
64
70
|
if (!token) return res.status(401).json({
|
|
@@ -90,12 +96,23 @@ export function createOrderPrintHandler(options = {}) {
|
|
|
90
96
|
}
|
|
91
97
|
const [stored, store] = await Promise.all([
|
|
92
98
|
hostRef.collection('orders').doc(orderId).get(),
|
|
93
|
-
kind === 'invoice' ? hostRef.collection('settings').doc('store').get() : Promise.resolve(null)
|
|
99
|
+
kind === 'invoice' || asPdf ? hostRef.collection('settings').doc('store').get() : Promise.resolve(null)
|
|
94
100
|
]);
|
|
95
101
|
if (!stored.exists) return res.status(404).json({
|
|
96
102
|
error: 'This order no longer exists'
|
|
97
103
|
});
|
|
98
104
|
const order = liftLegacyOrder((_stored_data = stored.data()) != null ? _stored_data : {});
|
|
105
|
+
if (asPdf) {
|
|
106
|
+
var _ref1;
|
|
107
|
+
const file = await invoicePdf({
|
|
108
|
+
hostId,
|
|
109
|
+
orderId,
|
|
110
|
+
order,
|
|
111
|
+
host: host.data(),
|
|
112
|
+
store: (_ref1 = store == null ? void 0 : store.data()) != null ? _ref1 : null
|
|
113
|
+
});
|
|
114
|
+
return sendPdf(res, file);
|
|
115
|
+
}
|
|
99
116
|
return res.status(200).json(orderPrintPage(kind, {
|
|
100
117
|
order,
|
|
101
118
|
orderId,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/order-print.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 { holdsCommerceSiteKey } from './commerce-site-keys'\nimport { commerceSitePermissionRefusal } from '../model/plugin-permissions'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { formatOrderNumber, liftLegacyOrder, type HostOrder } from '../model'\nimport { buildOrderInvoiceHtml, invoiceStoreFrom } from '../utils/order-invoice'\nimport { buildPackingSlipHtml } from '../utils/order-packing-slip'\nimport { validId } from './products-write'\n\n/*\n * `GET /api/commerce/order-print?hostId&orderId&kind` (AGL-3841): the order\n * dialog's \"Packing slip\" and \"Invoice\" for the native apps, which print them\n * through the device's own print sheet.\n *\n * The console builds both pages in the browser from the order it is showing\n * (`utils/order-packing-slip.ts`, `utils/order-invoice.ts`) and prints them\n * from a popup. This route runs the same two builders over the stored order,\n * the site and its store settings, and answers the markup with the title the\n * popup has, so an app prints the console's own page rather than a copy of\n * its layout.\n *\n * Gate: who may read the order (`canReadHostSensitive` in the rules: the\n * site's `admin` and `editor`), or staff. A read, so a frozen site still\n * prints.\n */\n\nexport type OrderPrintKind = 'invoice' | 'packing-slip'\n\nexport const ORDER_PRINT_KINDS: readonly OrderPrintKind[] = ['invoice', 'packing-slip']\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport interface OrderPrintDeps {\n firestore?: () => Firestore\n verifyIdToken?: (token: string) => Promise<Record<string, unknown> & { uid: string }>\n}\n\n/** The page and its title for one order: what the console's popup is given. */\nexport function orderPrintPage(\n kind: OrderPrintKind,\n input: {\n order: HostOrder\n orderId: string\n host?: Record<string, unknown> | null\n store?: Record<string, unknown> | null\n },\n): { title: string; html: string } {\n const number = formatOrderNumber(input.order, input.orderId)\n if (kind === 'packing-slip') {\n return { title: `Packing slip ${number}`, html: buildPackingSlipHtml(input.order, input.orderId) }\n }\n return {\n title: `Invoice ${number}`,\n html: buildOrderInvoiceHtml({\n order: input.order,\n orderId: input.orderId,\n store: invoiceStoreFrom(input.host, input.store),\n }),\n }\n}\n\nexport function createOrderPrintHandler(options: OrderPrintDeps = {}): PluginApiHandler {\n return async (req, res) => {\n if (req.method !== 'GET') return res.status(405).json({ error: 'Method not allowed' })\n const firestore = options.firestore ?? (() => firebaseAdmin.app().firestore() as unknown as Firestore)\n const verifyIdToken =\n options.verifyIdToken ?? ((token: string) => firebaseAdmin.app().auth().verifyIdToken(token) as never)\n const hostId = req.query['hostId']\n const orderId = req.query['orderId']\n const kind = String(req.query['kind'] ?? '') as OrderPrintKind\n if (!validId(hostId) || !validId(orderId)) return res.status(400).json({ error: 'Missing site or order' })\n if (!ORDER_PRINT_KINDS.includes(kind)) return res.status(400).json({ error: 'kind must be invoice or packing-slip' })\n const authorization = String(req.headers.authorization ?? '')\n const token = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : ''\n if (!token) return res.status(401).json({ error: 'Unauthenticated' })\n try {\n let decoded: Record<string, unknown> & { uid: string }\n try {\n decoded = await verifyIdToken(token)\n } catch (error) {\n // Only a refused token is the caller's; an Auth outage is a 500 (AGL-2852).\n if (!isRefusedIdToken(error)) throw error\n return res.status(401).json({ error: 'Sign in again to continue' })\n }\n const db = firestore()\n const hostRef = db.collection('hosts').doc(hostId)\n const host = await hostRef.get()\n if (!host.exists) return res.status(404).json({ error: 'Unknown site' })\n // \"Manage orders\" (`commerce.orders`, AGL-4047), read as the rules would read it.\n if (decoded['staff'] !== true && !holdsCommerceSiteKey(host, decoded.uid, 'commerce.orders')) {\n return res.status(403).json({ error: commerceSitePermissionRefusal('commerce.orders') })\n }\n const [stored, store] = await Promise.all([\n hostRef.collection('orders').doc(orderId).get(),\n kind === 'invoice' ? hostRef.collection('settings').doc('store').get() : Promise.resolve(null),\n ])\n if (!stored.exists) return res.status(404).json({ error: 'This order no longer exists' })\n const order = liftLegacyOrder((stored.data() ?? {}) as never)\n return res.status(200).json(\n orderPrintPage(kind, {\n order,\n orderId,\n host: host.data() as Record<string, unknown>,\n store: (store?.data() ?? null) as Record<string, unknown> | null,\n }),\n )\n } catch (error) {\n console.error('[commerce/order-print]', error)\n return res.status(500).json({ error: 'The page could not be made' })\n }\n }\n}\n\nexport const orderPrintHandler = createOrderPrintHandler()\n"],"names":["holdsCommerceSiteKey","commerceSitePermissionRefusal","isRefusedIdToken","firebaseAdmin","formatOrderNumber","liftLegacyOrder","buildOrderInvoiceHtml","invoiceStoreFrom","buildPackingSlipHtml","validId","ORDER_PRINT_KINDS","orderPrintPage","kind","input","number","order","orderId","title","html","store","host","createOrderPrintHandler","options","req","res","method","status","json","error","firestore","app","verifyIdToken","token","auth","hostId","query","String","includes","authorization","headers","startsWith","slice","length","stored","decoded","db","hostRef","collection","doc","get","exists","uid","Promise","all","resolve","data","console","orderPrintHandler"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/order-print.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 { holdsCommerceSiteKey } from './commerce-site-keys'\nimport { commerceSitePermissionRefusal } from '../model/plugin-permissions'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { formatOrderNumber, liftLegacyOrder, type HostOrder } from '../model'\nimport { buildOrderInvoiceHtml, invoiceStoreFrom } from '../utils/order-invoice'\nimport { buildPackingSlipHtml } from '../utils/order-packing-slip'\nimport type { orderInvoicePdfFile } from './commerce-pdf'\nimport { sendPdf, type CommercePdfFile } from './pdf-response'\nimport { validId } from './products-write'\n\n/*\n * `GET /api/commerce/order-print?hostId&orderId&kind` (AGL-3841): the order\n * dialog's \"Packing slip\" and \"Invoice\" for the native apps, which print them\n * through the device's own print sheet; and `kind=invoice-pdf` (AGL-4095), the\n * invoice as a branded PDF file, for the console's \"Download PDF\" and the apps.\n *\n * The console builds both pages in the browser from the order it is showing\n * (`utils/order-packing-slip.ts`, `utils/order-invoice.ts`) and prints them\n * from a popup. This route runs the same two builders over the stored order,\n * the site and its store settings, and answers the markup with the title the\n * popup has, so an app prints the console's own page rather than a copy of\n * its layout.\n *\n * Gate: who may read the order (`canReadHostSensitive` in the rules: the\n * site's `admin` and `editor`), or staff. A read, so a frozen site still\n * prints.\n */\n\nexport type OrderPrintKind = 'invoice' | 'packing-slip'\n\nexport const ORDER_PRINT_KINDS: readonly OrderPrintKind[] = ['invoice', 'packing-slip']\n\n/** The invoice as a PDF file, rather than a page to print. */\nexport const ORDER_INVOICE_PDF_KIND = 'invoice-pdf'\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport interface OrderPrintDeps {\n firestore?: () => Firestore\n verifyIdToken?: (token: string) => Promise<Record<string, unknown> & { uid: string }>\n /** The invoice's PDF; loads the PDF writer and sharp only when asked. */\n invoicePdf?: (input: Parameters<typeof orderInvoicePdfFile>[0]) => Promise<CommercePdfFile>\n}\n\n/** The page and its title for one order: what the console's popup is given. */\nexport function orderPrintPage(\n kind: OrderPrintKind,\n input: {\n order: HostOrder\n orderId: string\n host?: Record<string, unknown> | null\n store?: Record<string, unknown> | null\n },\n): { title: string; html: string } {\n const number = formatOrderNumber(input.order, input.orderId)\n if (kind === 'packing-slip') {\n return { title: `Packing slip ${number}`, html: buildPackingSlipHtml(input.order, input.orderId) }\n }\n return {\n title: `Invoice ${number}`,\n html: buildOrderInvoiceHtml({\n order: input.order,\n orderId: input.orderId,\n store: invoiceStoreFrom(input.host, input.store),\n }),\n }\n}\n\nexport function createOrderPrintHandler(options: OrderPrintDeps = {}): PluginApiHandler {\n return async (req, res) => {\n if (req.method !== 'GET') return res.status(405).json({ error: 'Method not allowed' })\n const firestore = options.firestore ?? (() => firebaseAdmin.app().firestore() as unknown as Firestore)\n const verifyIdToken =\n options.verifyIdToken ?? ((token: string) => firebaseAdmin.app().auth().verifyIdToken(token) as never)\n const hostId = req.query['hostId']\n const orderId = req.query['orderId']\n const invoicePdf =\n options.invoicePdf ?? (async (input) => (await import('./commerce-pdf')).orderInvoicePdfFile(input))\n const kind = String(req.query['kind'] ?? '') as OrderPrintKind\n const asPdf = String(kind) === ORDER_INVOICE_PDF_KIND\n if (!validId(hostId) || !validId(orderId)) return res.status(400).json({ error: 'Missing site or order' })\n if (!asPdf && !ORDER_PRINT_KINDS.includes(kind)) {\n return res.status(400).json({ error: 'kind must be invoice, invoice-pdf or packing-slip' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const token = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : ''\n if (!token) return res.status(401).json({ error: 'Unauthenticated' })\n try {\n let decoded: Record<string, unknown> & { uid: string }\n try {\n decoded = await verifyIdToken(token)\n } catch (error) {\n // Only a refused token is the caller's; an Auth outage is a 500 (AGL-2852).\n if (!isRefusedIdToken(error)) throw error\n return res.status(401).json({ error: 'Sign in again to continue' })\n }\n const db = firestore()\n const hostRef = db.collection('hosts').doc(hostId)\n const host = await hostRef.get()\n if (!host.exists) return res.status(404).json({ error: 'Unknown site' })\n // \"Manage orders\" (`commerce.orders`, AGL-4047), read as the rules would read it.\n if (decoded['staff'] !== true && !holdsCommerceSiteKey(host, decoded.uid, 'commerce.orders')) {\n return res.status(403).json({ error: commerceSitePermissionRefusal('commerce.orders') })\n }\n const [stored, store] = await Promise.all([\n hostRef.collection('orders').doc(orderId).get(),\n kind === 'invoice' || asPdf ? hostRef.collection('settings').doc('store').get() : Promise.resolve(null),\n ])\n if (!stored.exists) return res.status(404).json({ error: 'This order no longer exists' })\n const order = liftLegacyOrder((stored.data() ?? {}) as never)\n if (asPdf) {\n const file = await invoicePdf({\n hostId,\n orderId,\n order,\n host: host.data() as Record<string, unknown>,\n store: (store?.data() ?? null) as Record<string, unknown> | null,\n })\n return sendPdf(res, file)\n }\n return res.status(200).json(\n orderPrintPage(kind, {\n order,\n orderId,\n host: host.data() as Record<string, unknown>,\n store: (store?.data() ?? null) as Record<string, unknown> | null,\n }),\n )\n } catch (error) {\n console.error('[commerce/order-print]', error)\n return res.status(500).json({ error: 'The page could not be made' })\n }\n }\n}\n\nexport const orderPrintHandler = createOrderPrintHandler()\n"],"names":["holdsCommerceSiteKey","commerceSitePermissionRefusal","isRefusedIdToken","firebaseAdmin","formatOrderNumber","liftLegacyOrder","buildOrderInvoiceHtml","invoiceStoreFrom","buildPackingSlipHtml","sendPdf","validId","ORDER_PRINT_KINDS","ORDER_INVOICE_PDF_KIND","orderPrintPage","kind","input","number","order","orderId","title","html","store","host","createOrderPrintHandler","options","req","res","method","status","json","error","firestore","app","verifyIdToken","token","auth","hostId","query","invoicePdf","orderInvoicePdfFile","String","asPdf","includes","authorization","headers","startsWith","slice","length","stored","decoded","db","hostRef","collection","doc","get","exists","uid","Promise","all","resolve","data","file","console","orderPrintHandler"],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
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 } from '@aglyn/aglyn/server';
|
|
18
|
+
/**
|
|
19
|
+
* How a route answers with a quote's or an invoice's PDF (AGL-4095). Kept
|
|
20
|
+
* apart from `commerce-pdf.ts`, which makes the file, so a route sends one
|
|
21
|
+
* without loading the PDF writer or sharp.
|
|
22
|
+
*/
|
|
23
|
+
export interface CommercePdfFile {
|
|
24
|
+
bytes: Uint8Array;
|
|
25
|
+
/** ASCII, ending `.pdf`. */
|
|
26
|
+
filename: string;
|
|
27
|
+
}
|
|
28
|
+
/** The headers a PDF answer carries: a download, never cached, indexed or referred on. */
|
|
29
|
+
export declare function pdfResponseHeaders(file: CommercePdfFile): Record<string, string>;
|
|
30
|
+
/** Sends a PDF on the (req, res) shape. */
|
|
31
|
+
export declare function sendPdf(res: Parameters<PluginApiHandler>[1], file: CommercePdfFile): void;
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
*/ /** The headers a PDF answer carries: a download, never cached, indexed or referred on. */ export function pdfResponseHeaders(file) {
|
|
17
|
+
return {
|
|
18
|
+
'Content-Type': 'application/pdf',
|
|
19
|
+
'Content-Disposition': `attachment; filename="${file.filename}"`,
|
|
20
|
+
'Cache-Control': 'private, no-store',
|
|
21
|
+
'X-Robots-Tag': 'noindex, nofollow',
|
|
22
|
+
'Referrer-Policy': 'no-referrer'
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Sends a PDF on the (req, res) shape. */ export function sendPdf(res, file) {
|
|
26
|
+
for (const [name, value] of Object.entries(pdfResponseHeaders(file)))res.setHeader(name, value);
|
|
27
|
+
res.status(200).send(Buffer.from(file.bytes));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=pdf-response.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/pdf-response.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 } from '@aglyn/aglyn/server'\n\n/**\n * How a route answers with a quote's or an invoice's PDF (AGL-4095). Kept\n * apart from `commerce-pdf.ts`, which makes the file, so a route sends one\n * without loading the PDF writer or sharp.\n */\n\nexport interface CommercePdfFile {\n bytes: Uint8Array\n /** ASCII, ending `.pdf`. */\n filename: string\n}\n\n/** The headers a PDF answer carries: a download, never cached, indexed or referred on. */\nexport function pdfResponseHeaders(file: CommercePdfFile): Record<string, string> {\n return {\n 'Content-Type': 'application/pdf',\n 'Content-Disposition': `attachment; filename=\"${file.filename}\"`,\n 'Cache-Control': 'private, no-store',\n 'X-Robots-Tag': 'noindex, nofollow',\n 'Referrer-Policy': 'no-referrer',\n }\n}\n\n/** Sends a PDF on the (req, res) shape. */\nexport function sendPdf(res: Parameters<PluginApiHandler>[1], file: CommercePdfFile): void {\n for (const [name, value] of Object.entries(pdfResponseHeaders(file))) res.setHeader(name, value)\n res.status(200).send(Buffer.from(file.bytes))\n}\n"],"names":["pdfResponseHeaders","file","filename","sendPdf","res","name","value","Object","entries","setHeader","status","send","Buffer","from","bytes"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAgBD,wFAAwF,GACxF,OAAO,SAASA,mBAAmBC,IAAqB;IACtD,OAAO;QACL,gBAAgB;QAChB,uBAAuB,CAAC,sBAAsB,EAAEA,KAAKC,QAAQ,CAAC,CAAC,CAAC;QAChE,iBAAiB;QACjB,gBAAgB;QAChB,mBAAmB;IACrB;AACF;AAEA,yCAAyC,GACzC,OAAO,SAASC,QAAQC,GAAoC,EAAEH,IAAqB;IACjF,KAAK,MAAM,CAACI,MAAMC,MAAM,IAAIC,OAAOC,OAAO,CAACR,mBAAmBC,OAAQG,IAAIK,SAAS,CAACJ,MAAMC;IAC1FF,IAAIM,MAAM,CAAC,KAAKC,IAAI,CAACC,OAAOC,IAAI,CAACZ,KAAKa,KAAK;AAC7C"}
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import { type MediaCdnScope } from '@aglyn/aglyn/app-utils/media-cdn-scope';
|
|
18
17
|
import type { PluginApiHandler } from '@aglyn/aglyn/server';
|
|
19
18
|
import type { PluginEventPayloads } from '@aglyn/aglyn/plugin-manager/plugin-events';
|
|
20
19
|
import type { PluginJobHostGate } from '@aglyn/aglyn/server';
|
|
21
20
|
import { type PosReaderSplashChoice, type PosReaderSplashFamilyId } from '../model/pos-reader-splash';
|
|
21
|
+
import { type LocatedImageSource } from './site-image-source';
|
|
22
22
|
/** `platformSettings/posReaderSplash`: staff's default screen. */
|
|
23
23
|
export declare const POS_READER_SPLASH_SETTINGS_ID = "posReaderSplash";
|
|
24
24
|
/** One family's default screen, as staff uploaded it. */
|
|
@@ -40,21 +40,6 @@ export declare function posReaderSplashBucket(): import("@google-cloud/storage")
|
|
|
40
40
|
export declare function readPlatformSplash(): Promise<PlatformSplashDoc>;
|
|
41
41
|
/** The default is usable once the S700/S710 screen is uploaded; the smaller one is drawn from it if absent. */
|
|
42
42
|
export declare function platformSplashReady(doc: PlatformSplashDoc): boolean;
|
|
43
|
-
interface LocatedAsset {
|
|
44
|
-
kind: 'asset';
|
|
45
|
-
scope: MediaCdnScope;
|
|
46
|
-
scopeSegment: string;
|
|
47
|
-
mediaId: string;
|
|
48
|
-
doc: Record<string, unknown>;
|
|
49
|
-
/** What changes when the asset's bytes do. */
|
|
50
|
-
version: string;
|
|
51
|
-
}
|
|
52
|
-
interface LocatedUrl {
|
|
53
|
-
kind: 'url';
|
|
54
|
-
url: string;
|
|
55
|
-
version: string;
|
|
56
|
-
}
|
|
57
|
-
type LocatedSource = LocatedAsset | LocatedUrl;
|
|
58
43
|
/**
|
|
59
44
|
* The workspace's own logo: a white-label workspace's brand logo first,
|
|
60
45
|
* then the logo on its organization profile. Never the platform's: the
|
|
@@ -67,7 +52,7 @@ export type SplashSource = {
|
|
|
67
52
|
doc: PlatformSplashDoc;
|
|
68
53
|
} | {
|
|
69
54
|
mode: 'logo' | 'artwork';
|
|
70
|
-
located:
|
|
55
|
+
located: LocatedImageSource;
|
|
71
56
|
};
|
|
72
57
|
export interface PosReaderSplashPlan {
|
|
73
58
|
choice: PosReaderSplashChoice | null;
|
|
@@ -169,4 +154,3 @@ export declare function refreshPosReaderSplashes(gate: {
|
|
|
169
154
|
*/
|
|
170
155
|
export declare function onOrgLogoChanged(payload: Pick<PluginEventPayloads['org.logo.changed'], 'orgId'>, gate?: PluginJobHostGate): Promise<PosReaderSplashSweep>;
|
|
171
156
|
export declare const posReaderSplashHandler: PluginApiHandler;
|
|
172
|
-
export {};
|