@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,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"}
@@ -48,8 +48,9 @@ export declare function erasedOrderPatch(data: FirebaseFirestore.DocumentData |
48
48
  /**
49
49
  * A quote's customer, erased (AGL-4093). An unpaid quote keeps its customer
50
50
  * in its `quote` section rather than on the order's own fields, so it is
51
- * found by `quote.customer.emailLower` and loses the person and both
52
- * addresses; its lines and totals stay, as an order's do.
51
+ * found by `quote.customer.emailLower` and loses the person, both
52
+ * addresses and the address each email went to; its lines and totals stay,
53
+ * as an order's do.
53
54
  */
54
55
  export declare function erasedQuotePatch(data: FirebaseFirestore.DocumentData | undefined, now: number): Record<string, unknown>;
55
56
  export declare function erasedCheckInPatch(data: FirebaseFirestore.DocumentData | undefined, now: number): Record<string, unknown>;
@@ -19,9 +19,19 @@ import { FieldValue } from "firebase-admin/firestore";
19
19
  import { firebaseAdmin } from "@aglyn/tenant-data-admin/server/firebase-admin";
20
20
  const LABEL = 'commerce person eraser';
21
21
  /** Where a quote keeps its customer's address (`commerce-quotes.ts`). */ const QUOTE_EMAIL_FIELD = 'quote.customer.emailLower';
22
+ /**
23
+ * Each time the order's invoice was emailed names where it went (AGL-4099);
24
+ * the record of the send stays, without the address.
25
+ */ function erasedInvoiceSends(data) {
26
+ return Array.isArray(data == null ? void 0 : data['invoiceSends']) ? {
27
+ invoiceSends: data['invoiceSends'].map((send)=>_extends({}, send, {
28
+ to: ''
29
+ }))
30
+ } : {};
31
+ }
22
32
  /** The patch that takes the buyer off one order, as it stands. */ export function erasedOrderPatch(data, now) {
23
33
  const addressTokens = new Set(Array.isArray(data == null ? void 0 : data['customerEmailTokens']) ? data['customerEmailTokens'] : []);
24
- return _extends({
34
+ return _extends({}, erasedInvoiceSends(data), {
25
35
  customerEmail: null,
26
36
  customerName: null,
27
37
  shippingAddress: FieldValue.delete(),
@@ -36,19 +46,28 @@ const LABEL = 'commerce person eraser';
36
46
  /** The patch that takes the customer off one check-in at the window (AGL-4043). */ /**
37
47
  * A quote's customer, erased (AGL-4093). An unpaid quote keeps its customer
38
48
  * in its `quote` section rather than on the order's own fields, so it is
39
- * found by `quote.customer.emailLower` and loses the person and both
40
- * addresses; its lines and totals stay, as an order's do.
49
+ * found by `quote.customer.emailLower` and loses the person, both
50
+ * addresses and the address each email went to; its lines and totals stay,
51
+ * as an order's do.
41
52
  */ export function erasedQuotePatch(data, now) {
42
53
  var _ref;
43
54
  const quote = (_ref = data == null ? void 0 : data['quote']) != null ? _ref : {};
44
- return {
55
+ // Each time it was emailed names where it went (AGL-4097); the record of
56
+ // the send stays, without the address.
57
+ const sends = Array.isArray(quote['sends']) ? {
58
+ sends: quote['sends'].map((send)=>_extends({}, send, {
59
+ to: ''
60
+ }))
61
+ } : {};
62
+ return _extends({
45
63
  quote: _extends({}, quote, {
46
64
  customer: {},
47
65
  billTo: null,
48
66
  shipTo: null
49
- }),
67
+ }, sends)
68
+ }, erasedInvoiceSends(data), {
50
69
  customerErasedAtMs: now
51
- };
70
+ });
52
71
  }
53
72
  export function erasedCheckInPatch(data, now) {
54
73
  var _ref, _ref1;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/person-eraser.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 {\n PluginPersonEraser,\n PluginPersonErasureReport,\n} from '@aglyn/aglyn/plugin-manager/plugin-person-erasure'\nimport { updateWhereEquals } from '@aglyn/tenant-data-admin/server/paged-sweeps'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\n\n/**\n * COMMERCE'S SHARE OF A PERSON ERASURE (AGL-2623, AGL-3080).\n *\n * An order is the merchant's record of a sale: the amounts, the line items\n * and the tax are theirs to keep and the law expects them kept. So the person\n * is taken OFF every order they placed on a site of the workspace — name,\n * email, both addresses — and a stamp says when; the order itself stays.\n *\n * The address also lives in what the orders list queries by (AGL-3321): its\n * lower-cased key, its search prefixes, and the quick search's token array,\n * which holds those prefixes beside the order number's and the item names'.\n * The first two go; the prefixes are taken back out of the third. A prefix\n * the number or an item name shares with the address goes with it — a search\n * for that prefix misses this order until the orders backfill restamps it —\n * and nothing left on the order answers to the address.\n *\n * A check-in at the window (AGL-4043) copies the buyer's name, address and\n * phone, so the same person is taken off each of theirs, by the stored\n * lower-cased address; the check-in itself — who was due, who checked them\n * in, the wristband — stays, as the order does.\n *\n * A DRY RUN counts the orders and check-ins and writes nothing.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport interface CommercePersonEraserDeps {\n firestore(): Firestore\n}\n\nconst LABEL = 'commerce person eraser'\n\n/** Where a quote keeps its customer's address (`commerce-quotes.ts`). */\nconst QUOTE_EMAIL_FIELD = 'quote.customer.emailLower'\n\n/** The patch that takes the buyer off one order, as it stands. */\nexport function erasedOrderPatch(\n data: FirebaseFirestore.DocumentData | undefined,\n now: number,\n): Record<string, unknown> {\n const addressTokens = new Set(\n Array.isArray(data?.['customerEmailTokens']) ? data['customerEmailTokens'] : [],\n )\n return {\n customerEmail: null,\n customerName: null,\n shippingAddress: FieldValue.delete(),\n billingAddress: FieldValue.delete(),\n customerErasedAtMs: now,\n customerEmailLower: null,\n customerEmailTokens: [],\n ...(Array.isArray(data?.['searchTokens'])\n ? {\n searchTokens: (data['searchTokens'] as unknown[]).filter(\n (token) => !addressTokens.has(token),\n ),\n }\n : {}),\n }\n}\n\n/** The patch that takes the customer off one check-in at the window (AGL-4043). */\n/**\n * A quote's customer, erased (AGL-4093). An unpaid quote keeps its customer\n * in its `quote` section rather than on the order's own fields, so it is\n * found by `quote.customer.emailLower` and loses the person and both\n * addresses; its lines and totals stay, as an order's do.\n */\nexport function erasedQuotePatch(\n data: FirebaseFirestore.DocumentData | undefined,\n now: number,\n): Record<string, unknown> {\n const quote = (data?.['quote'] ?? {}) as Record<string, unknown>\n return {\n quote: { ...quote, customer: {}, billTo: null, shipTo: null },\n customerErasedAtMs: now,\n }\n}\n\nexport function erasedCheckInPatch(\n data: FirebaseFirestore.DocumentData | undefined,\n now: number,\n): Record<string, unknown> {\n // One key per order or stay, so the people one order brought still read\n // as one party, and as nobody's name.\n const source = String(data?.['orderId'] ?? data?.['reservationId'] ?? '')\n return {\n customerKey: `erased:${source}`,\n customerName: null,\n customerEmail: null,\n customerEmailLower: null,\n customerPhone: null,\n contactId: null,\n searchTokens: [],\n customerErasedAtMs: now,\n }\n}\n\nexport function createCommercePersonEraser(deps: CommercePersonEraserDeps): PluginPersonEraser {\n return async ({ orgId, email, dryRun, atMs }): Promise<PluginPersonErasureReport> => {\n const db = deps.firestore()\n const hosts = await db.collection('hosts').where('orgId', '==', orgId).get()\n let orders = 0\n let checkIns = 0\n let quotes = 0\n const emailLower = email.trim().toLowerCase()\n for (const host of hosts.docs) {\n const placed = host.ref.collection('orders')\n const checked = host.ref.collection('checkIns')\n if (dryRun) {\n orders += (await placed.where('customerEmail', '==', email).get()).size\n checkIns += (await checked.where('customerEmailLower', '==', emailLower).get()).size\n quotes += (await placed.where(QUOTE_EMAIL_FIELD, '==', emailLower).get()).size\n continue\n }\n checkIns += await updateWhereEquals(\n db,\n checked,\n 'customerEmailLower',\n emailLower,\n (data) => erasedCheckInPatch(data, atMs),\n LABEL,\n )\n orders += await updateWhereEquals(\n db,\n placed,\n 'customerEmail',\n email,\n (data) => erasedOrderPatch(data, atMs),\n LABEL,\n )\n quotes += await updateWhereEquals(\n db,\n placed,\n QUOTE_EMAIL_FIELD,\n emailLower,\n (data) => erasedQuotePatch(data, atMs),\n LABEL,\n )\n }\n return { orders, checkIns, quotes }\n }\n}\n\n/** The eraser over the platform's own Firestore, as the declarations register it. */\nexport const commercePersonEraser: PluginPersonEraser = (request) =>\n createCommercePersonEraser({ firestore: () => firebaseAdmin.app().firestore() })(request)\n"],"names":["updateWhereEquals","FieldValue","firebaseAdmin","LABEL","QUOTE_EMAIL_FIELD","erasedOrderPatch","data","now","addressTokens","Set","Array","isArray","customerEmail","customerName","shippingAddress","delete","billingAddress","customerErasedAtMs","customerEmailLower","customerEmailTokens","searchTokens","filter","token","has","erasedQuotePatch","quote","customer","billTo","shipTo","erasedCheckInPatch","source","String","customerKey","customerPhone","contactId","createCommercePersonEraser","deps","orgId","email","dryRun","atMs","db","firestore","hosts","collection","where","get","orders","checkIns","quotes","emailLower","trim","toLowerCase","host","docs","placed","ref","checked","size","commercePersonEraser","request","app"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/person-eraser.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 {\n PluginPersonEraser,\n PluginPersonErasureReport,\n} from '@aglyn/aglyn/plugin-manager/plugin-person-erasure'\nimport { updateWhereEquals } from '@aglyn/tenant-data-admin/server/paged-sweeps'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\n\n/**\n * COMMERCE'S SHARE OF A PERSON ERASURE (AGL-2623, AGL-3080).\n *\n * An order is the merchant's record of a sale: the amounts, the line items\n * and the tax are theirs to keep and the law expects them kept. So the person\n * is taken OFF every order they placed on a site of the workspace — name,\n * email, both addresses — and a stamp says when; the order itself stays.\n *\n * The address also lives in what the orders list queries by (AGL-3321): its\n * lower-cased key, its search prefixes, and the quick search's token array,\n * which holds those prefixes beside the order number's and the item names'.\n * The first two go; the prefixes are taken back out of the third. A prefix\n * the number or an item name shares with the address goes with it — a search\n * for that prefix misses this order until the orders backfill restamps it —\n * and nothing left on the order answers to the address.\n *\n * A check-in at the window (AGL-4043) copies the buyer's name, address and\n * phone, so the same person is taken off each of theirs, by the stored\n * lower-cased address; the check-in itself — who was due, who checked them\n * in, the wristband — stays, as the order does.\n *\n * A DRY RUN counts the orders and check-ins and writes nothing.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport interface CommercePersonEraserDeps {\n firestore(): Firestore\n}\n\nconst LABEL = 'commerce person eraser'\n\n/** Where a quote keeps its customer's address (`commerce-quotes.ts`). */\nconst QUOTE_EMAIL_FIELD = 'quote.customer.emailLower'\n\n/**\n * Each time the order's invoice was emailed names where it went (AGL-4099);\n * the record of the send stays, without the address.\n */\nfunction erasedInvoiceSends(data: FirebaseFirestore.DocumentData | undefined): Record<string, unknown> {\n return Array.isArray(data?.['invoiceSends'])\n ? { invoiceSends: (data['invoiceSends'] as Array<Record<string, unknown>>).map((send) => ({ ...send, to: '' })) }\n : {}\n}\n\n/** The patch that takes the buyer off one order, as it stands. */\nexport function erasedOrderPatch(\n data: FirebaseFirestore.DocumentData | undefined,\n now: number,\n): Record<string, unknown> {\n const addressTokens = new Set(\n Array.isArray(data?.['customerEmailTokens']) ? data['customerEmailTokens'] : [],\n )\n return {\n ...erasedInvoiceSends(data),\n customerEmail: null,\n customerName: null,\n shippingAddress: FieldValue.delete(),\n billingAddress: FieldValue.delete(),\n customerErasedAtMs: now,\n customerEmailLower: null,\n customerEmailTokens: [],\n ...(Array.isArray(data?.['searchTokens'])\n ? {\n searchTokens: (data['searchTokens'] as unknown[]).filter(\n (token) => !addressTokens.has(token),\n ),\n }\n : {}),\n }\n}\n\n/** The patch that takes the customer off one check-in at the window (AGL-4043). */\n/**\n * A quote's customer, erased (AGL-4093). An unpaid quote keeps its customer\n * in its `quote` section rather than on the order's own fields, so it is\n * found by `quote.customer.emailLower` and loses the person, both\n * addresses and the address each email went to; its lines and totals stay,\n * as an order's do.\n */\nexport function erasedQuotePatch(\n data: FirebaseFirestore.DocumentData | undefined,\n now: number,\n): Record<string, unknown> {\n const quote = (data?.['quote'] ?? {}) as Record<string, unknown>\n // Each time it was emailed names where it went (AGL-4097); the record of\n // the send stays, without the address.\n const sends = Array.isArray(quote['sends'])\n ? { sends: (quote['sends'] as Array<Record<string, unknown>>).map((send) => ({ ...send, to: '' })) }\n : {}\n return {\n quote: { ...quote, customer: {}, billTo: null, shipTo: null, ...sends },\n ...erasedInvoiceSends(data),\n customerErasedAtMs: now,\n }\n}\n\nexport function erasedCheckInPatch(\n data: FirebaseFirestore.DocumentData | undefined,\n now: number,\n): Record<string, unknown> {\n // One key per order or stay, so the people one order brought still read\n // as one party, and as nobody's name.\n const source = String(data?.['orderId'] ?? data?.['reservationId'] ?? '')\n return {\n customerKey: `erased:${source}`,\n customerName: null,\n customerEmail: null,\n customerEmailLower: null,\n customerPhone: null,\n contactId: null,\n searchTokens: [],\n customerErasedAtMs: now,\n }\n}\n\nexport function createCommercePersonEraser(deps: CommercePersonEraserDeps): PluginPersonEraser {\n return async ({ orgId, email, dryRun, atMs }): Promise<PluginPersonErasureReport> => {\n const db = deps.firestore()\n const hosts = await db.collection('hosts').where('orgId', '==', orgId).get()\n let orders = 0\n let checkIns = 0\n let quotes = 0\n const emailLower = email.trim().toLowerCase()\n for (const host of hosts.docs) {\n const placed = host.ref.collection('orders')\n const checked = host.ref.collection('checkIns')\n if (dryRun) {\n orders += (await placed.where('customerEmail', '==', email).get()).size\n checkIns += (await checked.where('customerEmailLower', '==', emailLower).get()).size\n quotes += (await placed.where(QUOTE_EMAIL_FIELD, '==', emailLower).get()).size\n continue\n }\n checkIns += await updateWhereEquals(\n db,\n checked,\n 'customerEmailLower',\n emailLower,\n (data) => erasedCheckInPatch(data, atMs),\n LABEL,\n )\n orders += await updateWhereEquals(\n db,\n placed,\n 'customerEmail',\n email,\n (data) => erasedOrderPatch(data, atMs),\n LABEL,\n )\n quotes += await updateWhereEquals(\n db,\n placed,\n QUOTE_EMAIL_FIELD,\n emailLower,\n (data) => erasedQuotePatch(data, atMs),\n LABEL,\n )\n }\n return { orders, checkIns, quotes }\n }\n}\n\n/** The eraser over the platform's own Firestore, as the declarations register it. */\nexport const commercePersonEraser: PluginPersonEraser = (request) =>\n createCommercePersonEraser({ firestore: () => firebaseAdmin.app().firestore() })(request)\n"],"names":["updateWhereEquals","FieldValue","firebaseAdmin","LABEL","QUOTE_EMAIL_FIELD","erasedInvoiceSends","data","Array","isArray","invoiceSends","map","send","to","erasedOrderPatch","now","addressTokens","Set","customerEmail","customerName","shippingAddress","delete","billingAddress","customerErasedAtMs","customerEmailLower","customerEmailTokens","searchTokens","filter","token","has","erasedQuotePatch","quote","sends","customer","billTo","shipTo","erasedCheckInPatch","source","String","customerKey","customerPhone","contactId","createCommercePersonEraser","deps","orgId","email","dryRun","atMs","db","firestore","hosts","collection","where","get","orders","checkIns","quotes","emailLower","trim","toLowerCase","host","docs","placed","ref","checked","size","commercePersonEraser","request","app"],"mappings":"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"}
@@ -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: LocatedSource;
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 {};
@@ -15,10 +15,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */ import { createHash } from "node:crypto";
18
- import { parseMediaCdnScope } from "@aglyn/aglyn/app-utils/media-cdn-scope";
19
- import { formatMediaRef, isMediaCdnPath, MEDIA_CDN_ROUTE, mediaRefFromStorageUrl, parseMediaRef, resolveMediaSrc } from "@aglyn/aglyn/app-utils/media-ref";
18
+ import { formatMediaRef, resolveMediaSrc } from "@aglyn/aglyn/app-utils/media-ref";
20
19
  import { checkEntitlement, resolveBrandingProfile } from "@aglyn/aglyn/app-utils/plan-entitlements";
21
- import { visibleToHost } from "@aglyn/aglyn/app-utils/scope-tokens";
22
20
  import { firebaseAdmin, getOrgDoc, getOrgForHost } from "@aglyn/tenant-data-admin";
23
21
  import { siteLockdownJobGate } from "@aglyn/tenant-data-admin/server/tenant-write-lockdown";
24
22
  import { isPosReaderSplashChoice, POS_READER_SPLASH_DEVICE_TYPES, POS_READER_SPLASH_FAMILIES, POS_READER_SPLASH_SOURCE_MAX_BYTES, POS_READER_SPLASH_SOURCE_TYPES, posReaderSplashArtworkProblem, posReaderSplashEffectiveChoice } from "../model/pos-reader-splash.js";
@@ -26,6 +24,7 @@ import { authorizePosStaff, posRequestBody } from "./pos-auth.js";
26
24
  import { PosReaderSplashRenderError, posReaderSplashImageFacts, renderPosReaderSplash } from "./pos-reader-splash-render.js";
27
25
  import { POS_STRIPE_VERSION, posTerminalAvailable } from "./pos-stripe.js";
28
26
  import { savePosReaderConfiguration } from "./pos-terminal.js";
27
+ import { locateImageSource, locateMediaImage, readImageSourceBytes } from "./site-image-source.js";
29
28
  /*==========================================
30
29
  * A STORE'S CARD READERS SHOW ITS START-UP SCREEN (AGL-4048).
31
30
  *
@@ -107,96 +106,12 @@ function platformSplashVersion(doc) {
107
106
  }
108
107
  const digest = (value)=>createHash('sha256').update(value).digest('hex');
109
108
  const cleanString = (value)=>typeof value === 'string' ? value.trim() : '';
110
- async function locateMediaAsset(scopeSegment, mediaId, hostId, orgId) {
111
- var _doc_contentType, _ref, _doc_updatedAtMs;
112
- const scope = parseMediaCdnScope(scopeSegment);
113
- if (!scope || !/^[A-Za-z0-9_-]{1,64}$/.test(mediaId)) return null;
114
- const owned = scope.isOrg ? scope.scopeId === orgId : scope.scopeId === hostId;
115
- if (!owned) return null;
116
- const snapshot = await firestore().collection(scope.isOrg ? 'orgs' : 'hosts').doc(scope.scopeId).collection('media').doc(mediaId).get();
117
- const doc = snapshot.exists ? snapshot.data() : null;
118
- if (!doc || doc['deletedAt'] || doc['private']) return null;
119
- if (scope.isOrg && !visibleToHost(doc['visibleTo'], hostId)) return null;
120
- if (!/^image\//.test(String((_doc_contentType = doc['contentType']) != null ? _doc_contentType : ''))) return null;
121
- if (typeof doc['sizeBytes'] === 'number' && doc['sizeBytes'] > POS_READER_SPLASH_SOURCE_MAX_BYTES) return null;
122
- const version = cleanString(doc['contentSha256']) || cleanString(doc['contentHash']) || String((_ref = (_doc_updatedAtMs = doc['updatedAtMs']) != null ? _doc_updatedAtMs : doc['sizeBytes']) != null ? _ref : '');
123
- return {
124
- kind: 'asset',
125
- scope,
126
- scopeSegment,
127
- mediaId,
128
- doc,
129
- version
130
- };
131
- }
132
- /** Where a stored logo or image value lives, or null when it cannot be read. */ async function locateSource(value, hostId, orgId) {
133
- const stored = parseMediaRef(value) ? value : mediaRefFromStorageUrl(value);
134
- const ref = stored ? parseMediaRef(stored) : null;
135
- if (ref) return locateMediaAsset(ref.scope, ref.mediaId, hostId, orgId);
136
- if (isMediaCdnPath(value)) {
137
- const rest = value.slice(`${MEDIA_CDN_ROUTE}/`.length).split('?')[0];
138
- const slash = rest.indexOf('/');
139
- if (slash <= 0) return null;
140
- return locateMediaAsset(rest.slice(0, slash), rest.slice(slash + 1), hostId, orgId);
141
- }
142
- // `isMediaCdnPath` narrows a string it refuses to `never`; the value is still the string.
143
- const address = value;
144
- if (/^https:\/\//.test(address) && address.length <= 2000) return {
145
- kind: 'url',
146
- url: address,
147
- version: address
148
- };
149
- return null;
150
- }
151
- async function readAssetBytes(asset) {
152
- const { mediaCdnServeBlock, mediaStoragePathInScope } = await import("./pos-reader-splash-server.js");
153
- const blocked = await mediaCdnServeBlock(asset.scope, {
154
- contentSha256: typeof asset.doc['contentSha256'] === 'string' ? asset.doc['contentSha256'] : undefined,
155
- contentHash: typeof asset.doc['contentHash'] === 'string' ? asset.doc['contentHash'] : undefined,
156
- scopeSegment: asset.scopeSegment,
157
- mediaId: asset.mediaId
158
- });
159
- if (blocked) return null;
160
- const objectPath = mediaStoragePathInScope({
161
- storagePath: asset.doc['storagePath'],
162
- base: `${asset.scope.isOrg ? 'orgs' : 'hosts'}/${asset.scope.scopeId}`,
163
- mediaId: asset.mediaId
164
- });
165
- const [buffer] = await posReaderSplashBucket().file(objectPath).download();
166
- return buffer.length > POS_READER_SPLASH_SOURCE_MAX_BYTES ? null : buffer;
167
- }
168
- const FETCH_TIMEOUT_MS = 8000;
169
- async function fetchPublicImage(url) {
170
- const { createPinnedDispatcher, resolvePublicIp } = await import("./pos-reader-splash-server.js");
171
- const pinnedIp = await resolvePublicIp(new URL(url).hostname);
172
- if (!pinnedIp) return null;
173
- const dispatcher = createPinnedDispatcher(pinnedIp);
174
- const controller = new AbortController();
175
- const timeout = setTimeout(()=>controller.abort(), FETCH_TIMEOUT_MS);
176
- try {
177
- var _response_headers_get, _response_headers_get1;
178
- const init = {
179
- redirect: 'error',
180
- signal: controller.signal,
181
- dispatcher
182
- };
183
- const response = await fetch(url, init);
184
- if (!response.ok) return null;
185
- if (!/^image\//.test((_response_headers_get = response.headers.get('content-type')) != null ? _response_headers_get : '')) return null;
186
- const declared = Number((_response_headers_get1 = response.headers.get('content-length')) != null ? _response_headers_get1 : 0);
187
- if (declared > POS_READER_SPLASH_SOURCE_MAX_BYTES) return null;
188
- const buffer = Buffer.from(await response.arrayBuffer());
189
- return buffer.length > POS_READER_SPLASH_SOURCE_MAX_BYTES ? null : buffer;
190
- } catch (unused) {
191
- return null;
192
- } finally{
193
- clearTimeout(timeout);
194
- dispatcher.close == null ? void 0 : dispatcher.close.call(dispatcher).catch(()=>undefined);
195
- }
196
- }
197
- async function readSourceBytes(source) {
198
- return source.kind === 'asset' ? readAssetBytes(source) : fetchPublicImage(source.url);
199
- }
109
+ /*==========================================
110
+ * READING A SOURCE IMAGE: `site-image-source.ts`, with the reader screen's
111
+ * size cap.
112
+ *=========================================*/ const locateMediaAsset = (scopeSegment, mediaId, hostId, orgId)=>locateMediaImage(scopeSegment, mediaId, hostId, orgId, POS_READER_SPLASH_SOURCE_MAX_BYTES);
113
+ const locateSource = (value, hostId, orgId)=>locateImageSource(value, hostId, orgId, POS_READER_SPLASH_SOURCE_MAX_BYTES);
114
+ const readSourceBytes = (source)=>readImageSourceBytes(source, POS_READER_SPLASH_SOURCE_MAX_BYTES);
200
115
  /*==========================================
201
116
  * WHAT A STORE SHOWS.
202
117
  *=========================================*/ /**