@aglyn/plugins-commerce 1.0.0-beta.289 → 1.0.0-beta.291

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/README.md +4 -2
  2. package/package.json +13 -13
  3. package/src/lib/components/commerce-console-page.js +2 -1
  4. package/src/lib/components/commerce-console-page.js.map +1 -1
  5. package/src/lib/components/console/commerce-pdf-download.d.ts +30 -0
  6. package/src/lib/components/console/commerce-pdf-download.js +53 -0
  7. package/src/lib/components/console/commerce-pdf-download.js.map +1 -0
  8. package/src/lib/components/console/host-orders-card.component.d.ts +6 -0
  9. package/src/lib/components/console/host-orders-card.component.js +3 -2
  10. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  11. package/src/lib/components/console/host-quotes-card.component.js +20 -4
  12. package/src/lib/components/console/host-quotes-card.component.js.map +1 -1
  13. package/src/lib/components/console/invoice-send.component.d.ts +44 -0
  14. package/src/lib/components/console/invoice-send.component.js +237 -0
  15. package/src/lib/components/console/invoice-send.component.js.map +1 -0
  16. package/src/lib/components/console/order-detail-dialog.component.d.ts +5 -0
  17. package/src/lib/components/console/order-detail-dialog.component.js +9 -2
  18. package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
  19. package/src/lib/components/console/order-invoice.component.d.ts +15 -4
  20. package/src/lib/components/console/order-invoice.component.js +93 -12
  21. package/src/lib/components/console/order-invoice.component.js.map +1 -1
  22. package/src/lib/components/console/quote-editor-dialog.component.d.ts +29 -0
  23. package/src/lib/components/console/quote-editor-dialog.component.js +154 -5
  24. package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -1
  25. package/src/lib/components/console/quote-proofs.component.d.ts +48 -0
  26. package/src/lib/components/console/quote-proofs.component.js +694 -0
  27. package/src/lib/components/console/quote-proofs.component.js.map +1 -0
  28. package/src/lib/components/console/quote-send.component.d.ts +42 -0
  29. package/src/lib/components/console/quote-send.component.js +307 -0
  30. package/src/lib/components/console/quote-send.component.js.map +1 -0
  31. package/src/lib/components/console/quote-start.component.d.ts +45 -0
  32. package/src/lib/components/console/quote-start.component.js +182 -0
  33. package/src/lib/components/console/quote-start.component.js.map +1 -0
  34. package/src/lib/components/product-options-field-body.d.ts +44 -0
  35. package/src/lib/components/product-options-field-body.js +260 -0
  36. package/src/lib/components/product-options-field-body.js.map +1 -0
  37. package/src/lib/components/product-options-field.d.ts +32 -0
  38. package/src/lib/components/product-options-field.js +128 -0
  39. package/src/lib/components/product-options-field.js.map +1 -0
  40. package/src/lib/components/quote-page-body.js +67 -20
  41. package/src/lib/components/quote-page-body.js.map +1 -1
  42. package/src/lib/components/quote-proof-panel.d.ts +42 -0
  43. package/src/lib/components/quote-proof-panel.js +436 -0
  44. package/src/lib/components/quote-proof-panel.js.map +1 -0
  45. package/src/lib/model/commerce-invoices.d.ts +132 -0
  46. package/src/lib/model/commerce-invoices.js +332 -0
  47. package/src/lib/model/commerce-invoices.js.map +1 -0
  48. package/src/lib/model/commerce-orders.d.ts +29 -2
  49. package/src/lib/model/commerce-orders.js.map +1 -1
  50. package/src/lib/model/commerce-product-field.d.ts +23 -0
  51. package/src/lib/model/commerce-product-field.js +26 -0
  52. package/src/lib/model/commerce-product-field.js.map +1 -0
  53. package/src/lib/model/commerce-quote-email.d.ts +67 -0
  54. package/src/lib/model/commerce-quote-email.js +154 -0
  55. package/src/lib/model/commerce-quote-email.js.map +1 -0
  56. package/src/lib/model/commerce-quote-pay.d.ts +76 -6
  57. package/src/lib/model/commerce-quote-pay.js +105 -39
  58. package/src/lib/model/commerce-quote-pay.js.map +1 -1
  59. package/src/lib/model/commerce-quote-proofs.d.ts +159 -0
  60. package/src/lib/model/commerce-quote-proofs.js +348 -0
  61. package/src/lib/model/commerce-quote-proofs.js.map +1 -0
  62. package/src/lib/model/commerce-quote-request.d.ts +93 -0
  63. package/src/lib/model/commerce-quote-request.js +207 -0
  64. package/src/lib/model/commerce-quote-request.js.map +1 -0
  65. package/src/lib/model/commerce-quotes.d.ts +140 -3
  66. package/src/lib/model/commerce-quotes.js.map +1 -1
  67. package/src/lib/model/index.d.ts +4 -0
  68. package/src/lib/model/index.js +4 -0
  69. package/src/lib/model/index.js.map +1 -1
  70. package/src/lib/plugin.js +19 -0
  71. package/src/lib/plugin.js.map +1 -1
  72. package/src/lib/server/billing-webhook.js +115 -70
  73. package/src/lib/server/billing-webhook.js.map +1 -1
  74. package/src/lib/server/commerce-pdf.d.ts +104 -0
  75. package/src/lib/server/commerce-pdf.js +310 -0
  76. package/src/lib/server/commerce-pdf.js.map +1 -0
  77. package/src/lib/server/draft-order.js +23 -3
  78. package/src/lib/server/draft-order.js.map +1 -1
  79. package/src/lib/server/invoice-email.d.ts +76 -0
  80. package/src/lib/server/invoice-email.js +183 -0
  81. package/src/lib/server/invoice-email.js.map +1 -0
  82. package/src/lib/server/order-invoice-send.d.ts +43 -0
  83. package/src/lib/server/order-invoice-send.js +218 -0
  84. package/src/lib/server/order-invoice-send.js.map +1 -0
  85. package/src/lib/server/order-print.d.ts +6 -0
  86. package/src/lib/server/order-print.js +22 -5
  87. package/src/lib/server/order-print.js.map +1 -1
  88. package/src/lib/server/order-status.d.ts +11 -0
  89. package/src/lib/server/order-status.js +44 -8
  90. package/src/lib/server/order-status.js.map +1 -1
  91. package/src/lib/server/pdf-response.d.ts +31 -0
  92. package/src/lib/server/pdf-response.js +30 -0
  93. package/src/lib/server/pdf-response.js.map +1 -0
  94. package/src/lib/server/person-eraser.d.ts +3 -2
  95. package/src/lib/server/person-eraser.js +25 -6
  96. package/src/lib/server/person-eraser.js.map +1 -1
  97. package/src/lib/server/pos-reader-splash.d.ts +2 -18
  98. package/src/lib/server/pos-reader-splash.js +8 -93
  99. package/src/lib/server/pos-reader-splash.js.map +1 -1
  100. package/src/lib/server/quote-email.d.ts +85 -0
  101. package/src/lib/server/quote-email.js +211 -0
  102. package/src/lib/server/quote-email.js.map +1 -0
  103. package/src/lib/server/quote-link-token.d.ts +11 -0
  104. package/src/lib/server/quote-link-token.js +21 -0
  105. package/src/lib/server/quote-link-token.js.map +1 -1
  106. package/src/lib/server/quote-pay.d.ts +38 -6
  107. package/src/lib/server/quote-pay.js +115 -53
  108. package/src/lib/server/quote-pay.js.map +1 -1
  109. package/src/lib/server/quote-proofs.d.ts +51 -0
  110. package/src/lib/server/quote-proofs.js +500 -0
  111. package/src/lib/server/quote-proofs.js.map +1 -0
  112. package/src/lib/server/quote-uploads.d.ts +33 -0
  113. package/src/lib/server/quote-uploads.js +29 -0
  114. package/src/lib/server/quote-uploads.js.map +1 -0
  115. package/src/lib/server/quote.d.ts +25 -0
  116. package/src/lib/server/quote.js +638 -8
  117. package/src/lib/server/quote.js.map +1 -1
  118. package/src/lib/server/site-image-source.d.ts +38 -0
  119. package/src/lib/server/site-image-source.js +130 -0
  120. package/src/lib/server/site-image-source.js.map +1 -0
  121. package/src/lib/server/site-page-resolver.js +2 -1
  122. package/src/lib/server/site-page-resolver.js.map +1 -1
  123. package/src/lib/server.js +24 -1
  124. package/src/lib/server.js.map +1 -1
  125. package/src/lib/site.js +8 -0
  126. package/src/lib/site.js.map +1 -1
  127. package/src/lib/tenant-emails.js +243 -0
  128. package/src/lib/tenant-emails.js.map +1 -1
  129. package/src/lib/utils/commerce-document-pdf.d.ts +82 -0
  130. package/src/lib/utils/commerce-document-pdf.js +613 -0
  131. package/src/lib/utils/commerce-document-pdf.js.map +1 -0
  132. package/src/lib/utils/invoice-email-content.d.ts +45 -0
  133. package/src/lib/utils/invoice-email-content.js +126 -0
  134. package/src/lib/utils/invoice-email-content.js.map +1 -0
  135. package/src/lib/utils/order-invoice.d.ts +52 -3
  136. package/src/lib/utils/order-invoice.js +124 -64
  137. package/src/lib/utils/order-invoice.js.map +1 -1
  138. package/src/lib/utils/quote-email-content.d.ts +62 -0
  139. package/src/lib/utils/quote-email-content.js +154 -0
  140. package/src/lib/utils/quote-email-content.js.map +1 -0
@@ -0,0 +1,43 @@
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, type PluginApiRequest } from '@aglyn/aglyn/server';
18
+ import type { InvoiceMailer } from './invoice-email';
19
+ export interface OrderInvoiceSendDeps {
20
+ verifyIdToken(token: string): Promise<{
21
+ uid: string;
22
+ email?: string | null;
23
+ }>;
24
+ firestore(): FirebaseFirestore.Firestore;
25
+ orgForHost(hostId: string): Promise<{
26
+ id?: string;
27
+ org?: unknown;
28
+ } | null>;
29
+ /** The order's pay link at a link version, or null when the site has no address or no signing secret. */
30
+ payLinkUrl(host: Record<string, unknown>, hostId: string, orderId: string, version: number): string | null;
31
+ /** The invoice email: the renderer, the PDF and sharp, loaded only by a send. */
32
+ mailer(): Promise<InvoiceMailer>;
33
+ now(): number;
34
+ }
35
+ export declare function defaultOrderInvoiceSendDeps(): OrderInvoiceSendDeps;
36
+ type Outcome = {
37
+ status: number;
38
+ body: Record<string, unknown>;
39
+ };
40
+ export declare function handleOrderInvoiceSend(deps: OrderInvoiceSendDeps, req: PluginApiRequest): Promise<Outcome>;
41
+ /** `POST /api/commerce/order-invoice-send`. */
42
+ export declare const orderInvoiceSendHandler: PluginApiHandler;
43
+ export {};
@@ -0,0 +1,218 @@
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 { checkEntitlement, claimAttempt } from "@aglyn/aglyn/server";
17
+ import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
18
+ import { isRefusedIdToken } from "@aglyn/tenant-data-admin/server/id-token-refusal";
19
+ import * as CommerceModel from "../model/index.js";
20
+ import { commerceSitePermissionRefusal } from "../model/plugin-permissions.js";
21
+ import { holdsCommerceSiteKey } from "./commerce-site-keys.js";
22
+ import { quotePayLinkUrl } from "./quote-link-token.js";
23
+ export function defaultOrderInvoiceSendDeps() {
24
+ return {
25
+ verifyIdToken: (token)=>firebaseAdmin.app().auth().verifyIdToken(token),
26
+ firestore: ()=>firebaseAdmin.app().firestore(),
27
+ orgForHost: async (hostId)=>{
28
+ const found = await getOrgForHost(hostId);
29
+ return found ? {
30
+ id: found.orgId,
31
+ org: found.org
32
+ } : null;
33
+ },
34
+ payLinkUrl: (host, hostId, orderId, version)=>quotePayLinkUrl(host, hostId, orderId, version),
35
+ mailer: async ()=>(await import("./invoice-email.js")).invoiceMailer(),
36
+ now: ()=>Date.now()
37
+ };
38
+ }
39
+ const refuse = (status, error)=>({
40
+ status,
41
+ body: {
42
+ error
43
+ }
44
+ });
45
+ const DOC_ID = /^(?!__.*__$)[^/]{1,200}$/;
46
+ function bodyOf(req) {
47
+ const raw = req.body;
48
+ if (typeof raw === 'string') {
49
+ try {
50
+ const parsed = JSON.parse(raw);
51
+ return parsed && typeof parsed === 'object' ? parsed : {};
52
+ } catch (unused) {
53
+ return {};
54
+ }
55
+ }
56
+ return raw && typeof raw === 'object' ? raw : {};
57
+ }
58
+ function headerOf(req, name) {
59
+ var _req_headers, _headers_name_toLowerCase, _value_;
60
+ const headers = (_req_headers = req.headers) != null ? _req_headers : {};
61
+ const value = (_headers_name_toLowerCase = headers[name.toLowerCase()]) != null ? _headers_name_toLowerCase : headers[name];
62
+ return String(Array.isArray(value) ? (_value_ = value[0]) != null ? _value_ : '' : value != null ? value : '').trim();
63
+ }
64
+ const NO_ADDRESS = 'This site has no web address yet, so its invoices have no pay link. Publish the site first.';
65
+ export async function handleOrderInvoiceSend(deps, req) {
66
+ var _body_hostId, _body_orderId, _hostSnapshot_data, _ref;
67
+ if (req.method !== 'POST') return refuse(405, 'Method not allowed');
68
+ const authorization = headerOf(req, 'authorization');
69
+ const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : '';
70
+ if (!idToken) return refuse(401, 'Unauthenticated');
71
+ const body = bodyOf(req);
72
+ const hostId = String((_body_hostId = body['hostId']) != null ? _body_hostId : '');
73
+ const orderId = String((_body_orderId = body['orderId']) != null ? _body_orderId : '');
74
+ if (!DOC_ID.test(hostId)) return refuse(400, 'Missing hostId');
75
+ if (!DOC_ID.test(orderId)) return refuse(400, 'Missing orderId');
76
+ let decoded;
77
+ try {
78
+ decoded = await deps.verifyIdToken(idToken);
79
+ } catch (error) {
80
+ if (!isRefusedIdToken(error)) throw error;
81
+ return refuse(401, 'Unauthenticated');
82
+ }
83
+ const firestore = deps.firestore();
84
+ const hostRef = firestore.collection('hosts').doc(hostId);
85
+ const hostSnapshot = await hostRef.get();
86
+ if (!hostSnapshot.exists) return refuse(404, 'Unknown site');
87
+ if (!holdsCommerceSiteKey(hostSnapshot, decoded.uid, 'commerce.quotes.send')) {
88
+ return refuse(403, commerceSitePermissionRefusal('commerce.quotes.send'));
89
+ }
90
+ const host = (_hostSnapshot_data = hostSnapshot.data()) != null ? _hostSnapshot_data : {};
91
+ const actor = String(decoded.email || decoded.uid);
92
+ const parsed = CommerceModel.parseQuoteSendInput(body);
93
+ if ('error' in parsed) return refuse(400, parsed.error);
94
+ const idempotencyKey = headerOf(req, 'idempotency-key').slice(0, 200);
95
+ if (!idempotencyKey) return refuse(400, 'Sending an invoice needs an Idempotency-Key header.');
96
+ const ref = hostRef.collection('orders').doc(orderId);
97
+ const [snapshot, store] = await Promise.all([
98
+ ref.get(),
99
+ hostRef.collection('settings').doc('store').get()
100
+ ]);
101
+ const order = snapshot.exists ? snapshot.data() : null;
102
+ const to = CommerceModel.invoiceSendRecipient(order, parsed.input.to);
103
+ const refused = CommerceModel.invoiceSendRefusal(order, orderId, to);
104
+ if (refused) {
105
+ return refuse(CommerceModel.invoiceSendRefusalStatus(refused), CommerceModel.invoiceSendRefusalMessage(refused));
106
+ }
107
+ const current = order;
108
+ const paid = CommerceModel.invoiceIsPaid(current);
109
+ const linkVersion = paid ? null : CommerceModel.orderPayLinkVersion(current);
110
+ const payUrl = linkVersion === null ? null : deps.payLinkUrl(host, hostId, orderId, linkVersion);
111
+ if (linkVersion !== null && !payUrl) return refuse(409, NO_ADDRESS);
112
+ const owner = await deps.orgForHost(hostId);
113
+ if (!checkEntitlement(owner == null ? void 0 : owner.org, 'commerce')) return refuse(403, 'Selling is not enabled');
114
+ const mailer = await deps.mailer();
115
+ if (!mailer.configured()) {
116
+ return refuse(503, 'Email isn’t set up here, so invoices can’t be emailed.');
117
+ }
118
+ // The bounce and complaint list only: an invoice is transactional, so an
119
+ // unsubscribe from the site's newsletter does not stop it, as it does not
120
+ // stop a receipt.
121
+ if (await mailer.suppression(to)) {
122
+ return refuse(409, `${to} bounced or reported an email as spam, so it can’t be emailed. Send the invoice another way.`);
123
+ }
124
+ const claimed = await claimAttempt(firestore, {
125
+ kind: 'commerce-order-invoice-send',
126
+ scopeId: `${hostId}:${orderId}`,
127
+ orgId: (_ref = owner == null ? void 0 : owner.id) != null ? _ref : '',
128
+ key: idempotencyKey,
129
+ busyMessage: 'This invoice is already being sent'
130
+ });
131
+ if ('replay' in claimed) {
132
+ return {
133
+ status: claimed.replay.status,
134
+ body: claimed.replay.body
135
+ };
136
+ }
137
+ const claim = claimed.claim;
138
+ const now = deps.now();
139
+ let delivered;
140
+ try {
141
+ var _store_data;
142
+ delivered = await mailer.deliver({
143
+ hostId,
144
+ orderId,
145
+ order: current,
146
+ host,
147
+ store: (_store_data = store.data()) != null ? _store_data : null,
148
+ owner: owner ? {
149
+ id: owner.id,
150
+ org: owner.org
151
+ } : null,
152
+ to,
153
+ subject: parsed.input.subject,
154
+ message: parsed.input.message,
155
+ payUrl,
156
+ senderEmail: decoded.email ? String(decoded.email) : null,
157
+ atMs: now
158
+ });
159
+ } catch (error) {
160
+ console.error('commerce invoice send failed', error);
161
+ await claim.release();
162
+ return refuse(502, 'The invoice email could not be sent. Try again.');
163
+ }
164
+ // `strictNullChecks` is off, so the result does not narrow on `sent`.
165
+ if ('error' in delivered) {
166
+ await claim.release();
167
+ return refuse(delivered.status, delivered.error);
168
+ }
169
+ const done = delivered;
170
+ // The email is out: from here a failure is logged, never answered as one,
171
+ // so a retry does not send it twice.
172
+ const send = {
173
+ to,
174
+ subject: done.subject,
175
+ atMs: now,
176
+ by: actor,
177
+ messageId: done.messageId,
178
+ providerId: done.providerId,
179
+ paid: done.paid,
180
+ linkVersion: done.paid ? null : linkVersion,
181
+ totalCents: done.totalCents
182
+ };
183
+ try {
184
+ await firestore.runTransaction(async (transaction)=>{
185
+ const latest = await transaction.get(ref);
186
+ if (!latest.exists) return;
187
+ const fresh = latest.data();
188
+ transaction.set(ref, CommerceModel.invoiceSendPatch(fresh, send, done.thread), {
189
+ merge: true
190
+ });
191
+ });
192
+ } catch (error) {
193
+ console.error('commerce invoice sent but not recorded', {
194
+ hostId,
195
+ orderId,
196
+ messageId: done.messageId,
197
+ error
198
+ });
199
+ }
200
+ const outcome = {
201
+ status: 200,
202
+ body: {
203
+ sent: true,
204
+ to,
205
+ atMs: now,
206
+ paid: done.paid,
207
+ linkVersion: send.linkVersion
208
+ }
209
+ };
210
+ await claim.record(outcome.status, outcome.body);
211
+ return outcome;
212
+ }
213
+ /** `POST /api/commerce/order-invoice-send`. */ export const orderInvoiceSendHandler = async (req, res)=>{
214
+ const outcome = await handleOrderInvoiceSend(defaultOrderInvoiceSendDeps(), req);
215
+ return res.status(outcome.status).json(outcome.body);
216
+ };
217
+
218
+ //# sourceMappingURL=order-invoice-send.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/order-invoice-send.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 { checkEntitlement, claimAttempt, type PluginApiHandler, type PluginApiRequest } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport * as CommerceModel from '../model'\nimport { commerceSitePermissionRefusal } from '../model/plugin-permissions'\nimport { holdsCommerceSiteKey } from './commerce-site-keys'\nimport type { InvoiceMailer } from './invoice-email'\nimport { quotePayLinkUrl } from './quote-link-token'\n\n/*==========================================\n * SEND INVOICE (AGL-4099): `POST /api/commerce/order-invoice-send`.\n *\n * { hostId, orderId, to?, subject?, message? } + Idempotency-Key\n * → { sent: true, to, atMs, paid, linkVersion }\n *\n * Emails the invoice for an order or a quote, under \"Send quotes and\n * invoices to customers\" (`commerce.quotes.send`), the key a quote's send\n * already asks: its label names invoices, and an invoice for an order is the\n * same act toward the same customer.\n *\n * - UNPAID (a draft order, or a quote), the email asks for payment with one\n * button to the order's lasting pay link (`/pay-quote`, its version on\n * `quote.linkVersion` for a quote and `payLink.version` otherwise); the\n * link opens a fresh payment from the stored order whenever it is pressed.\n * - PAID, the email is the record: no button.\n *\n * Refused, in the console's words: an unknown order, a canceled or refunded\n * one, one with nothing on it, an unpaid one its link cannot take payment\n * for (a register sale still open, a quote waiting on the proof's approval,\n * tax not set up, under 50 cents, a draft older than invoices), no address\n * or a bad one, a site with no web address for the link, selling not on,\n * email not set up, and an address that bounced or complained.\n *\n * The send is recorded on the order (`invoiceSends`), on its timeline, and as\n * the conversation it moved on: the quote's for a quote, the order's own\n * otherwise. One attempt per Idempotency-Key, so a retried press is answered\n * from the first and the customer never gets two.\n *=========================================*/\n\nexport interface OrderInvoiceSendDeps {\n verifyIdToken(token: string): Promise<{ uid: string; email?: string | null }>\n firestore(): FirebaseFirestore.Firestore\n orgForHost(hostId: string): Promise<{ id?: string; org?: unknown } | null>\n /** The order's pay link at a link version, or null when the site has no address or no signing secret. */\n payLinkUrl(host: Record<string, unknown>, hostId: string, orderId: string, version: number): string | null\n /** The invoice email: the renderer, the PDF and sharp, loaded only by a send. */\n mailer(): Promise<InvoiceMailer>\n now(): number\n}\n\nexport function defaultOrderInvoiceSendDeps(): OrderInvoiceSendDeps {\n return {\n verifyIdToken: (token) => firebaseAdmin.app().auth().verifyIdToken(token),\n firestore: () => firebaseAdmin.app().firestore(),\n orgForHost: async (hostId) => {\n const found = await getOrgForHost(hostId)\n return found ? { id: found.orgId, org: found.org } : null\n },\n payLinkUrl: (host, hostId, orderId, version) => quotePayLinkUrl(host, hostId, orderId, version),\n mailer: async () => (await import('./invoice-email')).invoiceMailer(),\n now: () => Date.now(),\n }\n}\n\ntype Outcome = { status: number; body: Record<string, unknown> }\n\nconst refuse = (status: number, error: string): Outcome => ({ status, body: { error } })\n\nconst DOC_ID = /^(?!__.*__$)[^/]{1,200}$/\n\nfunction bodyOf(req: PluginApiRequest): Record<string, unknown> {\n const raw = req.body\n if (typeof raw === 'string') {\n try {\n const parsed = JSON.parse(raw)\n return parsed && typeof parsed === 'object' ? parsed : {}\n } catch {\n return {}\n }\n }\n return raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {}\n}\n\nfunction headerOf(req: PluginApiRequest, name: string): string {\n const headers = (req.headers ?? {}) as Record<string, unknown>\n const value = headers[name.toLowerCase()] ?? headers[name]\n return String(Array.isArray(value) ? (value[0] ?? '') : (value ?? '')).trim()\n}\n\nconst NO_ADDRESS = 'This site has no web address yet, so its invoices have no pay link. Publish the site first.'\n\nexport async function handleOrderInvoiceSend(deps: OrderInvoiceSendDeps, req: PluginApiRequest): Promise<Outcome> {\n if (req.method !== 'POST') return refuse(405, 'Method not allowed')\n const authorization = headerOf(req, 'authorization')\n const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : ''\n if (!idToken) return refuse(401, 'Unauthenticated')\n const body = bodyOf(req)\n const hostId = String(body['hostId'] ?? '')\n const orderId = String(body['orderId'] ?? '')\n if (!DOC_ID.test(hostId)) return refuse(400, 'Missing hostId')\n if (!DOC_ID.test(orderId)) return refuse(400, 'Missing orderId')\n\n let decoded: { uid: string; email?: string | null }\n try {\n decoded = await deps.verifyIdToken(idToken)\n } catch (error) {\n if (!isRefusedIdToken(error)) throw error\n return refuse(401, 'Unauthenticated')\n }\n const firestore = deps.firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) return refuse(404, 'Unknown site')\n if (!holdsCommerceSiteKey(hostSnapshot, decoded.uid, 'commerce.quotes.send')) {\n return refuse(403, commerceSitePermissionRefusal('commerce.quotes.send'))\n }\n const host = (hostSnapshot.data() ?? {}) as Record<string, unknown>\n const actor = String(decoded.email || decoded.uid)\n\n const parsed = CommerceModel.parseQuoteSendInput(body)\n if ('error' in parsed) return refuse(400, parsed.error)\n const idempotencyKey = headerOf(req, 'idempotency-key').slice(0, 200)\n if (!idempotencyKey) return refuse(400, 'Sending an invoice needs an Idempotency-Key header.')\n\n const ref = hostRef.collection('orders').doc(orderId)\n const [snapshot, store] = await Promise.all([ref.get(), hostRef.collection('settings').doc('store').get()])\n const order = snapshot.exists ? (snapshot.data() as CommerceModel.HostOrder) : null\n const to = CommerceModel.invoiceSendRecipient(order, parsed.input.to)\n const refused = CommerceModel.invoiceSendRefusal(order, orderId, to)\n if (refused) {\n return refuse(CommerceModel.invoiceSendRefusalStatus(refused), CommerceModel.invoiceSendRefusalMessage(refused))\n }\n const current = order as CommerceModel.HostOrder\n const paid = CommerceModel.invoiceIsPaid(current)\n const linkVersion = paid ? null : CommerceModel.orderPayLinkVersion(current)\n const payUrl = linkVersion === null ? null : deps.payLinkUrl(host, hostId, orderId, linkVersion)\n if (linkVersion !== null && !payUrl) return refuse(409, NO_ADDRESS)\n\n const owner = await deps.orgForHost(hostId)\n if (!checkEntitlement(owner?.org as never, 'commerce')) return refuse(403, 'Selling is not enabled')\n const mailer = await deps.mailer()\n if (!mailer.configured()) {\n return refuse(503, 'Email isn’t set up here, so invoices can’t be emailed.')\n }\n // The bounce and complaint list only: an invoice is transactional, so an\n // unsubscribe from the site's newsletter does not stop it, as it does not\n // stop a receipt.\n if (await mailer.suppression(to)) {\n return refuse(409, `${to} bounced or reported an email as spam, so it can’t be emailed. Send the invoice another way.`)\n }\n\n const claimed = await claimAttempt(firestore, {\n kind: 'commerce-order-invoice-send',\n scopeId: `${hostId}:${orderId}`,\n orgId: owner?.id ?? '',\n key: idempotencyKey,\n busyMessage: 'This invoice is already being sent',\n })\n if ('replay' in claimed) {\n return { status: claimed.replay.status, body: claimed.replay.body as Record<string, unknown> }\n }\n const claim = claimed.claim\n const now = deps.now()\n\n let delivered: Awaited<ReturnType<InvoiceMailer['deliver']>>\n try {\n delivered = await mailer.deliver({\n hostId,\n orderId,\n order: current,\n host,\n store: (store.data() ?? null) as Record<string, unknown> | null,\n owner: owner ? { id: owner.id, org: owner.org } : null,\n to,\n subject: parsed.input.subject,\n message: parsed.input.message,\n payUrl,\n senderEmail: decoded.email ? String(decoded.email) : null,\n atMs: now,\n })\n } catch (error) {\n console.error('commerce invoice send failed', error)\n await claim.release()\n return refuse(502, 'The invoice email could not be sent. Try again.')\n }\n // `strictNullChecks` is off, so the result does not narrow on `sent`.\n if ('error' in delivered) {\n await claim.release()\n return refuse(delivered.status, delivered.error)\n }\n const done = delivered as Extract<typeof delivered, { sent: true }>\n\n // The email is out: from here a failure is logged, never answered as one,\n // so a retry does not send it twice.\n const send: CommerceModel.OrderInvoiceSendRecord = {\n to,\n subject: done.subject,\n atMs: now,\n by: actor,\n messageId: done.messageId,\n providerId: done.providerId,\n paid: done.paid,\n linkVersion: done.paid ? null : linkVersion,\n totalCents: done.totalCents,\n }\n try {\n await firestore.runTransaction(async (transaction) => {\n const latest = await transaction.get(ref)\n if (!latest.exists) return\n const fresh = latest.data() as CommerceModel.HostOrder\n transaction.set(ref, CommerceModel.invoiceSendPatch(fresh, send, done.thread), { merge: true })\n })\n } catch (error) {\n console.error('commerce invoice sent but not recorded', { hostId, orderId, messageId: done.messageId, error })\n }\n const outcome: Outcome = {\n status: 200,\n body: { sent: true, to, atMs: now, paid: done.paid, linkVersion: send.linkVersion },\n }\n await claim.record(outcome.status, outcome.body)\n return outcome\n}\n\n/** `POST /api/commerce/order-invoice-send`. */\nexport const orderInvoiceSendHandler: PluginApiHandler = async (req, res) => {\n const outcome = await handleOrderInvoiceSend(defaultOrderInvoiceSendDeps(), req)\n return res.status(outcome.status).json(outcome.body)\n}\n"],"names":["checkEntitlement","claimAttempt","firebaseAdmin","getOrgForHost","isRefusedIdToken","CommerceModel","commerceSitePermissionRefusal","holdsCommerceSiteKey","quotePayLinkUrl","defaultOrderInvoiceSendDeps","verifyIdToken","token","app","auth","firestore","orgForHost","hostId","found","id","orgId","org","payLinkUrl","host","orderId","version","mailer","invoiceMailer","now","Date","refuse","status","error","body","DOC_ID","bodyOf","req","raw","parsed","JSON","parse","headerOf","name","headers","value","toLowerCase","String","Array","isArray","trim","NO_ADDRESS","handleOrderInvoiceSend","deps","hostSnapshot","method","authorization","idToken","startsWith","slice","length","test","decoded","hostRef","collection","doc","get","exists","uid","data","actor","email","parseQuoteSendInput","idempotencyKey","ref","snapshot","store","Promise","all","order","to","invoiceSendRecipient","input","refused","invoiceSendRefusal","invoiceSendRefusalStatus","invoiceSendRefusalMessage","current","paid","invoiceIsPaid","linkVersion","orderPayLinkVersion","payUrl","owner","configured","suppression","claimed","kind","scopeId","key","busyMessage","replay","claim","delivered","deliver","subject","message","senderEmail","atMs","console","release","done","send","by","messageId","providerId","totalCents","runTransaction","transaction","latest","fresh","set","invoiceSendPatch","thread","merge","outcome","sent","record","orderInvoiceSendHandler","res","json"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,gBAAgB,EAAEC,YAAY,QAAsD,sBAAqB;AAClH,SAASC,aAAa,EAAEC,aAAa,QAAQ,2BAA0B;AACvE,SAASC,gBAAgB,QAAQ,mDAAkD;AACnF,YAAYC,mBAAmB,oBAAU;AACzC,SAASC,6BAA6B,QAAQ,iCAA6B;AAC3E,SAASC,oBAAoB,QAAQ,0BAAsB;AAE3D,SAASC,eAAe,QAAQ,wBAAoB;AA2CpD,OAAO,SAASC;IACd,OAAO;QACLC,eAAe,CAACC,QAAUT,cAAcU,GAAG,GAAGC,IAAI,GAAGH,aAAa,CAACC;QACnEG,WAAW,IAAMZ,cAAcU,GAAG,GAAGE,SAAS;QAC9CC,YAAY,OAAOC;YACjB,MAAMC,QAAQ,MAAMd,cAAca;YAClC,OAAOC,QAAQ;gBAAEC,IAAID,MAAME,KAAK;gBAAEC,KAAKH,MAAMG,GAAG;YAAC,IAAI;QACvD;QACAC,YAAY,CAACC,MAAMN,QAAQO,SAASC,UAAYhB,gBAAgBc,MAAMN,QAAQO,SAASC;QACvFC,QAAQ,UAAY,AAAC,CAAA,MAAM,MAAM,CAAC,qBAAiB,EAAGC,aAAa;QACnEC,KAAK,IAAMC,KAAKD,GAAG;IACrB;AACF;AAIA,MAAME,SAAS,CAACC,QAAgBC,QAA4B,CAAA;QAAED;QAAQE,MAAM;YAAED;QAAM;IAAE,CAAA;AAEtF,MAAME,SAAS;AAEf,SAASC,OAAOC,GAAqB;IACnC,MAAMC,MAAMD,IAAIH,IAAI;IACpB,IAAI,OAAOI,QAAQ,UAAU;QAC3B,IAAI;YACF,MAAMC,SAASC,KAAKC,KAAK,CAACH;YAC1B,OAAOC,UAAU,OAAOA,WAAW,WAAWA,SAAS,CAAC;QAC1D,EAAE,eAAM;YACN,OAAO,CAAC;QACV;IACF;IACA,OAAOD,OAAO,OAAOA,QAAQ,WAAYA,MAAkC,CAAC;AAC9E;AAEA,SAASI,SAASL,GAAqB,EAAEM,IAAY;QAClCN,cACHO,2BACwBC;IAFtC,MAAMD,WAAWP,eAAAA,IAAIO,OAAO,YAAXP,eAAe,CAAC;IACjC,MAAMQ,SAAQD,4BAAAA,OAAO,CAACD,KAAKG,WAAW,GAAG,YAA3BF,4BAA+BA,OAAO,CAACD,KAAK;IAC1D,OAAOI,OAAOC,MAAMC,OAAO,CAACJ,UAAUA,UAAAA,KAAK,CAAC,EAAE,YAARA,UAAY,KAAOA,gBAAAA,QAAS,IAAKK,IAAI;AAC7E;AAEA,MAAMC,aAAa;AAEnB,OAAO,eAAeC,uBAAuBC,IAA0B,EAAEhB,GAAqB;QAMtEH,cACCA,eAkBToB;IAxBd,IAAIjB,IAAIkB,MAAM,KAAK,QAAQ,OAAOxB,OAAO,KAAK;IAC9C,MAAMyB,gBAAgBd,SAASL,KAAK;IACpC,MAAMoB,UAAUD,cAAcE,UAAU,CAAC,aAAaF,cAAcG,KAAK,CAAC,UAAUC,MAAM,IAAI;IAC9F,IAAI,CAACH,SAAS,OAAO1B,OAAO,KAAK;IACjC,MAAMG,OAAOE,OAAOC;IACpB,MAAMnB,SAAS6B,QAAOb,eAAAA,IAAI,CAAC,SAAS,YAAdA,eAAkB;IACxC,MAAMT,UAAUsB,QAAOb,gBAAAA,IAAI,CAAC,UAAU,YAAfA,gBAAmB;IAC1C,IAAI,CAACC,OAAO0B,IAAI,CAAC3C,SAAS,OAAOa,OAAO,KAAK;IAC7C,IAAI,CAACI,OAAO0B,IAAI,CAACpC,UAAU,OAAOM,OAAO,KAAK;IAE9C,IAAI+B;IACJ,IAAI;QACFA,UAAU,MAAMT,KAAKzC,aAAa,CAAC6C;IACrC,EAAE,OAAOxB,OAAO;QACd,IAAI,CAAC3B,iBAAiB2B,QAAQ,MAAMA;QACpC,OAAOF,OAAO,KAAK;IACrB;IACA,MAAMf,YAAYqC,KAAKrC,SAAS;IAChC,MAAM+C,UAAU/C,UAAUgD,UAAU,CAAC,SAASC,GAAG,CAAC/C;IAClD,MAAMoC,eAAe,MAAMS,QAAQG,GAAG;IACtC,IAAI,CAACZ,aAAaa,MAAM,EAAE,OAAOpC,OAAO,KAAK;IAC7C,IAAI,CAACtB,qBAAqB6C,cAAcQ,QAAQM,GAAG,EAAE,yBAAyB;QAC5E,OAAOrC,OAAO,KAAKvB,8BAA8B;IACnD;IACA,MAAMgB,QAAQ8B,qBAAAA,aAAae,IAAI,cAAjBf,qBAAuB,CAAC;IACtC,MAAMgB,QAAQvB,OAAOe,QAAQS,KAAK,IAAIT,QAAQM,GAAG;IAEjD,MAAM7B,SAAShC,cAAciE,mBAAmB,CAACtC;IACjD,IAAI,WAAWK,QAAQ,OAAOR,OAAO,KAAKQ,OAAON,KAAK;IACtD,MAAMwC,iBAAiB/B,SAASL,KAAK,mBAAmBsB,KAAK,CAAC,GAAG;IACjE,IAAI,CAACc,gBAAgB,OAAO1C,OAAO,KAAK;IAExC,MAAM2C,MAAMX,QAAQC,UAAU,CAAC,UAAUC,GAAG,CAACxC;IAC7C,MAAM,CAACkD,UAAUC,MAAM,GAAG,MAAMC,QAAQC,GAAG,CAAC;QAACJ,IAAIR,GAAG;QAAIH,QAAQC,UAAU,CAAC,YAAYC,GAAG,CAAC,SAASC,GAAG;KAAG;IAC1G,MAAMa,QAAQJ,SAASR,MAAM,GAAIQ,SAASN,IAAI,KAAiC;IAC/E,MAAMW,KAAKzE,cAAc0E,oBAAoB,CAACF,OAAOxC,OAAO2C,KAAK,CAACF,EAAE;IACpE,MAAMG,UAAU5E,cAAc6E,kBAAkB,CAACL,OAAOtD,SAASuD;IACjE,IAAIG,SAAS;QACX,OAAOpD,OAAOxB,cAAc8E,wBAAwB,CAACF,UAAU5E,cAAc+E,yBAAyB,CAACH;IACzG;IACA,MAAMI,UAAUR;IAChB,MAAMS,OAAOjF,cAAckF,aAAa,CAACF;IACzC,MAAMG,cAAcF,OAAO,OAAOjF,cAAcoF,mBAAmB,CAACJ;IACpE,MAAMK,SAASF,gBAAgB,OAAO,OAAOrC,KAAK9B,UAAU,CAACC,MAAMN,QAAQO,SAASiE;IACpF,IAAIA,gBAAgB,QAAQ,CAACE,QAAQ,OAAO7D,OAAO,KAAKoB;IAExD,MAAM0C,QAAQ,MAAMxC,KAAKpC,UAAU,CAACC;IACpC,IAAI,CAAChB,iBAAiB2F,yBAAAA,MAAOvE,GAAG,EAAW,aAAa,OAAOS,OAAO,KAAK;IAC3E,MAAMJ,SAAS,MAAM0B,KAAK1B,MAAM;IAChC,IAAI,CAACA,OAAOmE,UAAU,IAAI;QACxB,OAAO/D,OAAO,KAAK;IACrB;IACA,yEAAyE;IACzE,0EAA0E;IAC1E,kBAAkB;IAClB,IAAI,MAAMJ,OAAOoE,WAAW,CAACf,KAAK;QAChC,OAAOjD,OAAO,KAAK,GAAGiD,GAAG,4FAA4F,CAAC;IACxH;IAEA,MAAMgB,UAAU,MAAM7F,aAAaa,WAAW;QAC5CiF,MAAM;QACNC,SAAS,GAAGhF,OAAO,CAAC,EAAEO,SAAS;QAC/BJ,KAAK,UAAEwE,yBAAAA,MAAOzE,EAAE,mBAAI;QACpB+E,KAAK1B;QACL2B,aAAa;IACf;IACA,IAAI,YAAYJ,SAAS;QACvB,OAAO;YAAEhE,QAAQgE,QAAQK,MAAM,CAACrE,MAAM;YAAEE,MAAM8D,QAAQK,MAAM,CAACnE,IAAI;QAA4B;IAC/F;IACA,MAAMoE,QAAQN,QAAQM,KAAK;IAC3B,MAAMzE,MAAMwB,KAAKxB,GAAG;IAEpB,IAAI0E;IACJ,IAAI;YAMQ3B;QALV2B,YAAY,MAAM5E,OAAO6E,OAAO,CAAC;YAC/BtF;YACAO;YACAsD,OAAOQ;YACP/D;YACAoD,KAAK,GAAGA,cAAAA,MAAMP,IAAI,cAAVO,cAAgB;YACxBiB,OAAOA,QAAQ;gBAAEzE,IAAIyE,MAAMzE,EAAE;gBAAEE,KAAKuE,MAAMvE,GAAG;YAAC,IAAI;YAClD0D;YACAyB,SAASlE,OAAO2C,KAAK,CAACuB,OAAO;YAC7BC,SAASnE,OAAO2C,KAAK,CAACwB,OAAO;YAC7Bd;YACAe,aAAa7C,QAAQS,KAAK,GAAGxB,OAAOe,QAAQS,KAAK,IAAI;YACrDqC,MAAM/E;QACR;IACF,EAAE,OAAOI,OAAO;QACd4E,QAAQ5E,KAAK,CAAC,gCAAgCA;QAC9C,MAAMqE,MAAMQ,OAAO;QACnB,OAAO/E,OAAO,KAAK;IACrB;IACA,sEAAsE;IACtE,IAAI,WAAWwE,WAAW;QACxB,MAAMD,MAAMQ,OAAO;QACnB,OAAO/E,OAAOwE,UAAUvE,MAAM,EAAEuE,UAAUtE,KAAK;IACjD;IACA,MAAM8E,OAAOR;IAEb,0EAA0E;IAC1E,qCAAqC;IACrC,MAAMS,OAA6C;QACjDhC;QACAyB,SAASM,KAAKN,OAAO;QACrBG,MAAM/E;QACNoF,IAAI3C;QACJ4C,WAAWH,KAAKG,SAAS;QACzBC,YAAYJ,KAAKI,UAAU;QAC3B3B,MAAMuB,KAAKvB,IAAI;QACfE,aAAaqB,KAAKvB,IAAI,GAAG,OAAOE;QAChC0B,YAAYL,KAAKK,UAAU;IAC7B;IACA,IAAI;QACF,MAAMpG,UAAUqG,cAAc,CAAC,OAAOC;YACpC,MAAMC,SAAS,MAAMD,YAAYpD,GAAG,CAACQ;YACrC,IAAI,CAAC6C,OAAOpD,MAAM,EAAE;YACpB,MAAMqD,QAAQD,OAAOlD,IAAI;YACzBiD,YAAYG,GAAG,CAAC/C,KAAKnE,cAAcmH,gBAAgB,CAACF,OAAOR,MAAMD,KAAKY,MAAM,GAAG;gBAAEC,OAAO;YAAK;QAC/F;IACF,EAAE,OAAO3F,OAAO;QACd4E,QAAQ5E,KAAK,CAAC,0CAA0C;YAAEf;YAAQO;YAASyF,WAAWH,KAAKG,SAAS;YAAEjF;QAAM;IAC9G;IACA,MAAM4F,UAAmB;QACvB7F,QAAQ;QACRE,MAAM;YAAE4F,MAAM;YAAM9C;YAAI4B,MAAM/E;YAAK2D,MAAMuB,KAAKvB,IAAI;YAAEE,aAAasB,KAAKtB,WAAW;QAAC;IACpF;IACA,MAAMY,MAAMyB,MAAM,CAACF,QAAQ7F,MAAM,EAAE6F,QAAQ3F,IAAI;IAC/C,OAAO2F;AACT;AAEA,6CAA6C,GAC7C,OAAO,MAAMG,0BAA4C,OAAO3F,KAAK4F;IACnE,MAAMJ,UAAU,MAAMzE,uBAAuBzC,+BAA+B0B;IAC5E,OAAO4F,IAAIjG,MAAM,CAAC6F,QAAQ7F,MAAM,EAAEkG,IAAI,CAACL,QAAQ3F,IAAI;AACrD,EAAC"}
@@ -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)) return res.status(400).json({
60
- error: 'kind must be invoice or packing-slip'
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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"}
@@ -39,6 +39,17 @@ export type OrderStatusActionsProvider = (input: {
39
39
  export declare function registerOrderStatusActions(provider: OrderStatusActionsProvider): void;
40
40
  /** Specs only. */
41
41
  export declare function clearOrderStatusActions(): void;
42
+ /**
43
+ * Pay, on the status page of an order still to be paid that its lasting pay
44
+ * link can take payment for (AGL-4099): a draft order or a quote, opening the
45
+ * same `/pay-quote` page its invoice links to. None for anything else.
46
+ */
47
+ export declare function orderStatusPayAction(input: {
48
+ hostId: string;
49
+ orderId: string;
50
+ order: CommerceModel.HostOrder;
51
+ currency: string;
52
+ }): CommerceModel.OrderStatusAction | null;
42
53
  /**
43
54
  * The guest order-status page's data (AGL-3610): `GET
44
55
  * /api/commerce/order-status?hostId=…&o=<orderId>&t=<token>` on the store.
@@ -18,6 +18,8 @@ import { readClientIp } from "@aglyn/aglyn/app-utils/request-ip";
18
18
  import { consumeRateLimit, firebaseAdmin } from "@aglyn/tenant-data-admin";
19
19
  import * as CommerceModel from "../model/index.js";
20
20
  import { verifyOrderStatusToken } from "./order-status-token.js";
21
+ import { mintQuoteLinkToken } from "./quote-link-token.js";
22
+ import { quotePageHref } from "../constants/quote-page.js";
21
23
  import { resolvePluginTrackingPages } from "@aglyn/aglyn/plugin-manager/plugin-tracking-pages";
22
24
  /** How long the page waits for a tracking service's own page link (AGL-3635). */ export const TRACKING_PAGE_TIMEOUT_MS = 1500;
23
25
  /** Status lookups one address may make per site, per ten minutes. */ export const ORDER_STATUS_LOOKUPS_PER_WINDOW = 60;
@@ -41,6 +43,28 @@ async function actionsFor(input) {
41
43
  }));
42
44
  return answers.flat().filter((action)=>action && action.id && action.label && /^(https:\/\/|\/)/.test(String(action.url)));
43
45
  }
46
+ /**
47
+ * Pay, on the status page of an order still to be paid that its lasting pay
48
+ * link can take payment for (AGL-4099): a draft order or a quote, opening the
49
+ * same `/pay-quote` page its invoice links to. None for anything else.
50
+ */ export function orderStatusPayAction(input) {
51
+ const { hostId, orderId, order } = input;
52
+ if (order.status !== 'pending' || !CommerceModel.orderPayLinkKind(order)) return null;
53
+ if (CommerceModel.invoicePayRefusal(order, orderId)) return null;
54
+ const token = mintQuoteLinkToken(hostId, orderId, CommerceModel.orderPayLinkVersion(order));
55
+ if (!token) return null;
56
+ const charge = CommerceModel.invoiceCheckoutCharge(order, orderId);
57
+ const later = [
58
+ charge.shippingOptions.length > 0 ? 'shipping' : '',
59
+ charge.taxAtPayment ? 'tax' : ''
60
+ ].filter(Boolean).join(' and ');
61
+ const amount = CommerceModel.formatOrderMoney(charge.totalCents, input.currency);
62
+ return {
63
+ id: 'pay',
64
+ label: later ? `Pay ${amount} plus ${later}` : `Pay ${amount}`,
65
+ url: quotePageHref(orderId, token)
66
+ };
67
+ }
44
68
  /**
45
69
  * The guest order-status page's data (AGL-3610): `GET
46
70
  * /api/commerce/order-status?hostId=…&o=<orderId>&t=<token>` on the store.
@@ -92,7 +116,7 @@ async function actionsFor(input) {
92
116
  });
93
117
  if (!verifyOrderStatusToken(hostId, orderId, token)) return notFound();
94
118
  try {
95
- var _orderSnapshot_data, _resolveHostToken, _settingsSnapshot_get;
119
+ var _orderSnapshot_data, _settingsSnapshot_get, _resolveHostToken;
96
120
  const firestore = firebaseAdmin.app().firestore();
97
121
  const hostRef = firestore.collection('hosts').doc(hostId);
98
122
  const [hostSnapshot, settingsSnapshot, orderSnapshot] = await Promise.all([
@@ -102,18 +126,30 @@ async function actionsFor(input) {
102
126
  ]);
103
127
  if (!hostSnapshot.exists || !orderSnapshot.exists) return notFound();
104
128
  const order = CommerceModel.liftLegacyOrder((_orderSnapshot_data = orderSnapshot.data()) != null ? _orderSnapshot_data : {});
129
+ const currency = String((_settingsSnapshot_get = settingsSnapshot.get('currency')) != null ? _settingsSnapshot_get : 'USD');
130
+ const pay = orderStatusPayAction({
131
+ hostId,
132
+ orderId,
133
+ order,
134
+ currency
135
+ });
105
136
  const view = CommerceModel.buildOrderStatusView({
106
137
  order,
107
138
  orderId,
108
139
  storeName: String((_resolveHostToken = resolveHostToken('businessName', hostSnapshot.data())) != null ? _resolveHostToken : ''),
109
- currency: String((_settingsSnapshot_get = settingsSnapshot.get('currency')) != null ? _settingsSnapshot_get : 'USD'),
140
+ currency,
110
141
  number: CommerceModel.formatOrderNumber(order, orderId),
111
- actions: await actionsFor({
112
- hostId,
113
- orderId,
114
- order,
115
- token
116
- })
142
+ actions: [
143
+ ...pay ? [
144
+ pay
145
+ ] : [],
146
+ ...await actionsFor({
147
+ hostId,
148
+ orderId,
149
+ order,
150
+ token
151
+ })
152
+ ]
117
153
  });
118
154
  // A tracking service's own page for each parcel, where the merchant
119
155
  // follows them through one (AGL-3635); the carrier's link otherwise.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/order-status.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'\nimport {\n resolveHostToken,\n type HostTokenSource,\n} from '@aglyn/aglyn/app-utils/host-tokens'\nimport { readClientIp } from '@aglyn/aglyn/app-utils/request-ip'\nimport { consumeRateLimit, firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport * as CommerceModel from '../model'\nimport { verifyOrderStatusToken } from './order-status-token'\nimport { resolvePluginTrackingPages } from '@aglyn/aglyn/plugin-manager/plugin-tracking-pages'\n\n/** How long the page waits for a tracking service's own page link (AGL-3635). */\nexport const TRACKING_PAGE_TIMEOUT_MS = 1_500\n\n/** Status lookups one address may make per site, per ten minutes. */\nexport const ORDER_STATUS_LOOKUPS_PER_WINDOW = 60\nexport const ORDER_STATUS_WINDOW_MS = 10 * 60_000\n\n/**\n * Adds buyer actions to the status page (AGL-3610) — the typed extension\n * point for \"Request a return\", which the order-processing work (AGL-3611)\n * owns. A provider sees the verified order and answers the actions that apply\n * to it (none, usually); the page draws each as a button to its `url`. Kept\n * inside commerce: returns are this plugin's, so no cross-plugin seam is\n * needed, and a provider that throws costs the page its buttons, not the page.\n */\nexport type OrderStatusActionsProvider = (input: {\n hostId: string\n orderId: string\n order: CommerceModel.HostOrder\n /** The verified status token, for a provider building a signed follow-up link. */\n token: string\n}) => CommerceModel.OrderStatusAction[] | Promise<CommerceModel.OrderStatusAction[]>\n\nconst actionProviders: OrderStatusActionsProvider[] = []\n\nexport function registerOrderStatusActions(provider: OrderStatusActionsProvider): void {\n if (!actionProviders.includes(provider)) actionProviders.push(provider)\n}\n\n/** Specs only. */\nexport function clearOrderStatusActions(): void {\n actionProviders.length = 0\n}\n\nasync function actionsFor(\n input: Parameters<OrderStatusActionsProvider>[0],\n): Promise<CommerceModel.OrderStatusAction[]> {\n const answers = await Promise.all(\n actionProviders.map(async (provider) => {\n try {\n return (await provider(input)) ?? []\n } catch (error) {\n console.error('[order-status] actions provider failed', error)\n return []\n }\n }),\n )\n return answers\n .flat()\n .filter(\n (action) =>\n action && action.id && action.label && /^(https:\\/\\/|\\/)/.test(String(action.url)),\n )\n}\n\n/**\n * The guest order-status page's data (AGL-3610): `GET\n * /api/commerce/order-status?hostId=…&o=<orderId>&t=<token>` on the store.\n *\n * The token is the whole credential — `verifyOrderStatusToken`, constant\n * time — and a wrong one answers exactly as a missing order does (404), so\n * the route cannot be used to learn which order ids exist. Rate-limited per\n * address per site, durable, BEFORE the token check so guessing costs the\n * same as looking. The answer is `buildOrderStatusView`, an allow-listed\n * projection, never the order document; `Cache-Control: private, no-store`\n * and `X-Robots-Tag: noindex` keep it out of shared caches and search.\n *\n * Registered as a `recipientLink` route: an emailed link has to keep working\n * for the buyer it was sent to, so the per-site enablement gates do not\n * stand between them and their order.\n */\nexport const orderStatusHandler: PluginApiHandler = async (req, res) => {\n res.setHeader('Cache-Control', 'private, no-store')\n res.setHeader('X-Robots-Tag', 'noindex, nofollow')\n if (req.method !== 'GET') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.query.hostId ?? '')\n const orderId = String(req.query.o ?? '').slice(0, 200)\n const token = String(req.query.t ?? '').slice(0, 200)\n if (!hostId || !orderId || !token || /^__.*__$/.test(hostId) || /^__.*__$/.test(orderId) || orderId.includes('/')) {\n return res.status(400).json({ error: 'This link is incomplete.' })\n }\n const ip =\n readClientIp(req.headers, { remoteAddress: req.socket?.remoteAddress }) ??\n 'no-address'\n const rate = await consumeRateLimit(`commerce-order-status:${hostId}:${ip}`, {\n limit: ORDER_STATUS_LOOKUPS_PER_WINDOW,\n windowMs: ORDER_STATUS_WINDOW_MS,\n })\n if (!rate.allowed) {\n res.setHeader(\n 'Retry-After',\n String(Math.max(1, Math.ceil((rate.resetMs - Date.now()) / 1000))),\n )\n return res.status(429).json({ error: 'Too many lookups. Try again in a few minutes.' })\n }\n const notFound = () =>\n res.status(404).json({ error: 'We could not find that order. Check the link in your email.' })\n if (!verifyOrderStatusToken(hostId, orderId, token)) return notFound()\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const [hostSnapshot, settingsSnapshot, orderSnapshot] = await Promise.all([\n hostRef.get(),\n hostRef.collection('settings').doc('store').get(),\n hostRef.collection('orders').doc(orderId).get(),\n ])\n if (!hostSnapshot.exists || !orderSnapshot.exists) return notFound()\n const order = CommerceModel.liftLegacyOrder((orderSnapshot.data() ?? {}) as never)\n const view = CommerceModel.buildOrderStatusView({\n order,\n orderId,\n storeName: String(\n resolveHostToken('businessName', hostSnapshot.data() as HostTokenSource) ?? '',\n ),\n currency: String(settingsSnapshot.get('currency') ?? 'USD'),\n number: CommerceModel.formatOrderNumber(order, orderId),\n actions: await actionsFor({ hostId, orderId, order, token }),\n })\n // A tracking service's own page for each parcel, where the merchant\n // follows them through one (AGL-3635); the carrier's link otherwise.\n const pages = await resolvePluginTrackingPages(\n view.shipments\n .filter((shipment) => shipment.trackingNumber)\n .map((shipment) => ({\n hostId,\n recordId: orderId,\n carrier: shipment.carrier,\n trackingNumber: String(shipment.trackingNumber),\n })),\n { timeoutMs: TRACKING_PAGE_TIMEOUT_MS },\n )\n for (const shipment of view.shipments) {\n const page = shipment.trackingNumber ? pages.get(shipment.trackingNumber) : undefined\n if (page) shipment.trackingUrl = page\n }\n return res.status(200).json(view)\n } catch (error) {\n console.error('orderStatus failed', error)\n return res.status(500).json({ error: 'Your order could not be loaded. Try again.' })\n }\n}\n"],"names":["resolveHostToken","readClientIp","consumeRateLimit","firebaseAdmin","CommerceModel","verifyOrderStatusToken","resolvePluginTrackingPages","TRACKING_PAGE_TIMEOUT_MS","ORDER_STATUS_LOOKUPS_PER_WINDOW","ORDER_STATUS_WINDOW_MS","actionProviders","registerOrderStatusActions","provider","includes","push","clearOrderStatusActions","length","actionsFor","input","answers","Promise","all","map","error","console","flat","filter","action","id","label","test","String","url","orderStatusHandler","req","res","setHeader","method","status","json","hostId","query","orderId","o","slice","token","t","ip","headers","remoteAddress","socket","rate","limit","windowMs","allowed","Math","max","ceil","resetMs","Date","now","notFound","orderSnapshot","settingsSnapshot","firestore","app","hostRef","collection","doc","hostSnapshot","get","exists","order","liftLegacyOrder","data","view","buildOrderStatusView","storeName","currency","number","formatOrderNumber","actions","pages","shipments","shipment","trackingNumber","recordId","carrier","timeoutMs","page","undefined","trackingUrl"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/order-status.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'\nimport {\n resolveHostToken,\n type HostTokenSource,\n} from '@aglyn/aglyn/app-utils/host-tokens'\nimport { readClientIp } from '@aglyn/aglyn/app-utils/request-ip'\nimport { consumeRateLimit, firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport * as CommerceModel from '../model'\nimport { verifyOrderStatusToken } from './order-status-token'\nimport { mintQuoteLinkToken } from './quote-link-token'\nimport { quotePageHref } from '../constants/quote-page'\nimport { resolvePluginTrackingPages } from '@aglyn/aglyn/plugin-manager/plugin-tracking-pages'\n\n/** How long the page waits for a tracking service's own page link (AGL-3635). */\nexport const TRACKING_PAGE_TIMEOUT_MS = 1_500\n\n/** Status lookups one address may make per site, per ten minutes. */\nexport const ORDER_STATUS_LOOKUPS_PER_WINDOW = 60\nexport const ORDER_STATUS_WINDOW_MS = 10 * 60_000\n\n/**\n * Adds buyer actions to the status page (AGL-3610) — the typed extension\n * point for \"Request a return\", which the order-processing work (AGL-3611)\n * owns. A provider sees the verified order and answers the actions that apply\n * to it (none, usually); the page draws each as a button to its `url`. Kept\n * inside commerce: returns are this plugin's, so no cross-plugin seam is\n * needed, and a provider that throws costs the page its buttons, not the page.\n */\nexport type OrderStatusActionsProvider = (input: {\n hostId: string\n orderId: string\n order: CommerceModel.HostOrder\n /** The verified status token, for a provider building a signed follow-up link. */\n token: string\n}) => CommerceModel.OrderStatusAction[] | Promise<CommerceModel.OrderStatusAction[]>\n\nconst actionProviders: OrderStatusActionsProvider[] = []\n\nexport function registerOrderStatusActions(provider: OrderStatusActionsProvider): void {\n if (!actionProviders.includes(provider)) actionProviders.push(provider)\n}\n\n/** Specs only. */\nexport function clearOrderStatusActions(): void {\n actionProviders.length = 0\n}\n\nasync function actionsFor(\n input: Parameters<OrderStatusActionsProvider>[0],\n): Promise<CommerceModel.OrderStatusAction[]> {\n const answers = await Promise.all(\n actionProviders.map(async (provider) => {\n try {\n return (await provider(input)) ?? []\n } catch (error) {\n console.error('[order-status] actions provider failed', error)\n return []\n }\n }),\n )\n return answers\n .flat()\n .filter(\n (action) =>\n action && action.id && action.label && /^(https:\\/\\/|\\/)/.test(String(action.url)),\n )\n}\n\n/**\n * Pay, on the status page of an order still to be paid that its lasting pay\n * link can take payment for (AGL-4099): a draft order or a quote, opening the\n * same `/pay-quote` page its invoice links to. None for anything else.\n */\nexport function orderStatusPayAction(input: {\n hostId: string\n orderId: string\n order: CommerceModel.HostOrder\n currency: string\n}): CommerceModel.OrderStatusAction | null {\n const { hostId, orderId, order } = input\n if (order.status !== 'pending' || !CommerceModel.orderPayLinkKind(order)) return null\n if (CommerceModel.invoicePayRefusal(order, orderId)) return null\n const token = mintQuoteLinkToken(hostId, orderId, CommerceModel.orderPayLinkVersion(order))\n if (!token) return null\n const charge = CommerceModel.invoiceCheckoutCharge(order, orderId)\n const later = [charge.shippingOptions.length > 0 ? 'shipping' : '', charge.taxAtPayment ? 'tax' : '']\n .filter(Boolean)\n .join(' and ')\n const amount = CommerceModel.formatOrderMoney(charge.totalCents, input.currency)\n return { id: 'pay', label: later ? `Pay ${amount} plus ${later}` : `Pay ${amount}`, url: quotePageHref(orderId, token) }\n}\n\n/**\n * The guest order-status page's data (AGL-3610): `GET\n * /api/commerce/order-status?hostId=…&o=<orderId>&t=<token>` on the store.\n *\n * The token is the whole credential — `verifyOrderStatusToken`, constant\n * time — and a wrong one answers exactly as a missing order does (404), so\n * the route cannot be used to learn which order ids exist. Rate-limited per\n * address per site, durable, BEFORE the token check so guessing costs the\n * same as looking. The answer is `buildOrderStatusView`, an allow-listed\n * projection, never the order document; `Cache-Control: private, no-store`\n * and `X-Robots-Tag: noindex` keep it out of shared caches and search.\n *\n * Registered as a `recipientLink` route: an emailed link has to keep working\n * for the buyer it was sent to, so the per-site enablement gates do not\n * stand between them and their order.\n */\nexport const orderStatusHandler: PluginApiHandler = async (req, res) => {\n res.setHeader('Cache-Control', 'private, no-store')\n res.setHeader('X-Robots-Tag', 'noindex, nofollow')\n if (req.method !== 'GET') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.query.hostId ?? '')\n const orderId = String(req.query.o ?? '').slice(0, 200)\n const token = String(req.query.t ?? '').slice(0, 200)\n if (!hostId || !orderId || !token || /^__.*__$/.test(hostId) || /^__.*__$/.test(orderId) || orderId.includes('/')) {\n return res.status(400).json({ error: 'This link is incomplete.' })\n }\n const ip =\n readClientIp(req.headers, { remoteAddress: req.socket?.remoteAddress }) ??\n 'no-address'\n const rate = await consumeRateLimit(`commerce-order-status:${hostId}:${ip}`, {\n limit: ORDER_STATUS_LOOKUPS_PER_WINDOW,\n windowMs: ORDER_STATUS_WINDOW_MS,\n })\n if (!rate.allowed) {\n res.setHeader(\n 'Retry-After',\n String(Math.max(1, Math.ceil((rate.resetMs - Date.now()) / 1000))),\n )\n return res.status(429).json({ error: 'Too many lookups. Try again in a few minutes.' })\n }\n const notFound = () =>\n res.status(404).json({ error: 'We could not find that order. Check the link in your email.' })\n if (!verifyOrderStatusToken(hostId, orderId, token)) return notFound()\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const [hostSnapshot, settingsSnapshot, orderSnapshot] = await Promise.all([\n hostRef.get(),\n hostRef.collection('settings').doc('store').get(),\n hostRef.collection('orders').doc(orderId).get(),\n ])\n if (!hostSnapshot.exists || !orderSnapshot.exists) return notFound()\n const order = CommerceModel.liftLegacyOrder((orderSnapshot.data() ?? {}) as never)\n const currency = String(settingsSnapshot.get('currency') ?? 'USD')\n const pay = orderStatusPayAction({ hostId, orderId, order, currency })\n const view = CommerceModel.buildOrderStatusView({\n order,\n orderId,\n storeName: String(\n resolveHostToken('businessName', hostSnapshot.data() as HostTokenSource) ?? '',\n ),\n currency,\n number: CommerceModel.formatOrderNumber(order, orderId),\n actions: [...(pay ? [pay] : []), ...(await actionsFor({ hostId, orderId, order, token }))],\n })\n // A tracking service's own page for each parcel, where the merchant\n // follows them through one (AGL-3635); the carrier's link otherwise.\n const pages = await resolvePluginTrackingPages(\n view.shipments\n .filter((shipment) => shipment.trackingNumber)\n .map((shipment) => ({\n hostId,\n recordId: orderId,\n carrier: shipment.carrier,\n trackingNumber: String(shipment.trackingNumber),\n })),\n { timeoutMs: TRACKING_PAGE_TIMEOUT_MS },\n )\n for (const shipment of view.shipments) {\n const page = shipment.trackingNumber ? pages.get(shipment.trackingNumber) : undefined\n if (page) shipment.trackingUrl = page\n }\n return res.status(200).json(view)\n } catch (error) {\n console.error('orderStatus failed', error)\n return res.status(500).json({ error: 'Your order could not be loaded. Try again.' })\n }\n}\n"],"names":["resolveHostToken","readClientIp","consumeRateLimit","firebaseAdmin","CommerceModel","verifyOrderStatusToken","mintQuoteLinkToken","quotePageHref","resolvePluginTrackingPages","TRACKING_PAGE_TIMEOUT_MS","ORDER_STATUS_LOOKUPS_PER_WINDOW","ORDER_STATUS_WINDOW_MS","actionProviders","registerOrderStatusActions","provider","includes","push","clearOrderStatusActions","length","actionsFor","input","answers","Promise","all","map","error","console","flat","filter","action","id","label","test","String","url","orderStatusPayAction","hostId","orderId","order","status","orderPayLinkKind","invoicePayRefusal","token","orderPayLinkVersion","charge","invoiceCheckoutCharge","later","shippingOptions","taxAtPayment","Boolean","join","amount","formatOrderMoney","totalCents","currency","orderStatusHandler","req","res","setHeader","method","json","query","o","slice","t","ip","headers","remoteAddress","socket","rate","limit","windowMs","allowed","Math","max","ceil","resetMs","Date","now","notFound","orderSnapshot","settingsSnapshot","firestore","app","hostRef","collection","doc","hostSnapshot","get","exists","liftLegacyOrder","data","pay","view","buildOrderStatusView","storeName","number","formatOrderNumber","actions","pages","shipments","shipment","trackingNumber","recordId","carrier","timeoutMs","page","undefined","trackingUrl"],"mappings":"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"}