@aglyn/plugins-commerce 1.0.0-beta.221 → 1.0.0-beta.222

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 (125) hide show
  1. package/README.md +4 -2
  2. package/package.json +14 -13
  3. package/src/lib/components/console/catalog-organization-card.component.js +9 -0
  4. package/src/lib/components/console/catalog-organization-card.component.js.map +1 -1
  5. package/src/lib/components/console/discounts-card.component.js +8 -0
  6. package/src/lib/components/console/discounts-card.component.js.map +1 -1
  7. package/src/lib/components/console/gift-cards-card.component.js +8 -0
  8. package/src/lib/components/console/gift-cards-card.component.js.map +1 -1
  9. package/src/lib/components/console/host-coupons-card.component.js +8 -0
  10. package/src/lib/components/console/host-coupons-card.component.js.map +1 -1
  11. package/src/lib/components/console/host-orders-card.component.js +34 -70
  12. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  13. package/src/lib/components/console/product-zones.d.ts +7 -6
  14. package/src/lib/components/console/product-zones.js +5 -4
  15. package/src/lib/components/console/product-zones.js.map +1 -1
  16. package/src/lib/components/console/products-hub-card.component.js +86 -284
  17. package/src/lib/components/console/products-hub-card.component.js.map +1 -1
  18. package/src/lib/declarations.console-server.d.ts +38 -0
  19. package/src/lib/declarations.console-server.js +67 -0
  20. package/src/lib/declarations.console-server.js.map +1 -0
  21. package/src/lib/model/commerce-gift-cards.d.ts +26 -0
  22. package/src/lib/model/commerce-gift-cards.js +42 -0
  23. package/src/lib/model/commerce-gift-cards.js.map +1 -1
  24. package/src/lib/model/commerce-orders.d.ts +0 -57
  25. package/src/lib/model/commerce-orders.js +0 -80
  26. package/src/lib/model/commerce-orders.js.map +1 -1
  27. package/src/lib/model/index.d.ts +0 -1
  28. package/src/lib/model/index.js +0 -1
  29. package/src/lib/model/index.js.map +1 -1
  30. package/src/lib/plugin.js +5 -0
  31. package/src/lib/plugin.js.map +1 -1
  32. package/src/lib/server/api-v1/openapi.d.ts +27 -0
  33. package/src/lib/server/api-v1/openapi.js +219 -0
  34. package/src/lib/server/api-v1/openapi.js.map +1 -0
  35. package/src/lib/server/api-v1/orders-and-products.d.ts +19 -0
  36. package/src/lib/server/api-v1/orders-and-products.js +419 -0
  37. package/src/lib/server/api-v1/orders-and-products.js.map +1 -0
  38. package/src/lib/server/billing-webhook.js +10 -13
  39. package/src/lib/server/billing-webhook.js.map +1 -1
  40. package/src/lib/server/download.js +4 -1
  41. package/src/lib/server/download.js.map +1 -1
  42. package/src/lib/server/fulfill-order.d.ts +52 -11
  43. package/src/lib/server/fulfill-order.js +6 -9
  44. package/src/lib/server/fulfill-order.js.map +1 -1
  45. package/src/lib/server/gift-card-issue.d.ts +57 -0
  46. package/src/lib/server/gift-card-issue.js +155 -0
  47. package/src/lib/server/gift-card-issue.js.map +1 -0
  48. package/src/lib/server/gift-cards.js +25 -84
  49. package/src/lib/server/gift-cards.js.map +1 -1
  50. package/src/lib/server/membership-register.js +9 -8
  51. package/src/lib/server/membership-register.js.map +1 -1
  52. package/src/lib/server/newsletter.js +7 -5
  53. package/src/lib/server/newsletter.js.map +1 -1
  54. package/src/lib/server/paid-media-delivery.d.ts +21 -0
  55. package/src/lib/server/paid-media-delivery.js +23 -5
  56. package/src/lib/server/paid-media-delivery.js.map +1 -1
  57. package/src/lib/server/paid-media-serve-count.d.ts +28 -0
  58. package/src/lib/server/paid-media-serve-count.js +46 -0
  59. package/src/lib/server/paid-media-serve-count.js.map +1 -0
  60. package/src/lib/server/person-eraser.d.ts +45 -0
  61. package/src/lib/server/person-eraser.js +57 -0
  62. package/src/lib/server/person-eraser.js.map +1 -0
  63. package/src/lib/server/refund.js +15 -15
  64. package/src/lib/server/refund.js.map +1 -1
  65. package/src/lib/server/restock-flag.d.ts +2 -2
  66. package/src/lib/server/restock-flag.js +1 -1
  67. package/src/lib/server/restock-flag.js.map +1 -1
  68. package/src/lib/server/stream.js +4 -1
  69. package/src/lib/server/stream.js.map +1 -1
  70. package/src/lib/server.js +5 -21
  71. package/src/lib/server.js.map +1 -1
  72. package/src/lib/testing/stand-in-person-records.d.ts +43 -0
  73. package/src/lib/testing/stand-in-person-records.js +63 -0
  74. package/src/lib/testing/stand-in-person-records.js.map +1 -0
  75. package/src/lib/transfer/gift-card-confirm-step.component.d.ts +22 -0
  76. package/src/lib/transfer/gift-card-confirm-step.component.js +259 -0
  77. package/src/lib/transfer/gift-card-confirm-step.component.js.map +1 -0
  78. package/src/lib/transfer/gift-cards-plan.d.ts +29 -0
  79. package/src/lib/transfer/gift-cards-plan.js +40 -0
  80. package/src/lib/transfer/gift-cards-plan.js.map +1 -0
  81. package/src/lib/transfer/gift-cards.server.d.ts +34 -0
  82. package/src/lib/transfer/gift-cards.server.js +296 -0
  83. package/src/lib/transfer/gift-cards.server.js.map +1 -0
  84. package/src/lib/transfer/list-filter.d.ts +29 -0
  85. package/src/lib/transfer/list-filter.js +112 -0
  86. package/src/lib/transfer/list-filter.js.map +1 -0
  87. package/src/lib/transfer/product-import-results.d.ts +29 -0
  88. package/src/lib/transfer/product-import-results.js +49 -0
  89. package/src/lib/transfer/product-import-results.js.map +1 -0
  90. package/src/lib/transfer/product-import-step.component.d.ts +30 -0
  91. package/src/lib/transfer/product-import-step.component.js +99 -0
  92. package/src/lib/transfer/product-import-step.component.js.map +1 -0
  93. package/src/lib/transfer/product-transfer.d.ts +211 -0
  94. package/src/lib/transfer/product-transfer.js +1594 -0
  95. package/src/lib/transfer/product-transfer.js.map +1 -0
  96. package/src/lib/transfer/products.server.d.ts +44 -0
  97. package/src/lib/transfer/products.server.js +550 -0
  98. package/src/lib/transfer/products.server.js.map +1 -0
  99. package/src/lib/transfer/records-transfer.d.ts +182 -0
  100. package/src/lib/transfer/records-transfer.js +1153 -0
  101. package/src/lib/transfer/records-transfer.js.map +1 -0
  102. package/src/lib/transfer/records.server.d.ts +68 -0
  103. package/src/lib/transfer/records.server.js +455 -0
  104. package/src/lib/transfer/records.server.js.map +1 -0
  105. package/src/lib/transfer/register-transfer-resources.d.ts +17 -0
  106. package/src/lib/transfer/register-transfer-resources.js +171 -0
  107. package/src/lib/transfer/register-transfer-resources.js.map +1 -0
  108. package/src/lib/transfer/register-transfer-ui.d.ts +23 -0
  109. package/src/lib/transfer/register-transfer-ui.js +78 -0
  110. package/src/lib/transfer/register-transfer-ui.js.map +1 -0
  111. package/src/lib/transfer/server-common.d.ts +76 -0
  112. package/src/lib/transfer/server-common.js +165 -0
  113. package/src/lib/transfer/server-common.js.map +1 -0
  114. package/src/lib/transfer/transfer-header-actions.component.d.ts +39 -0
  115. package/src/lib/transfer/transfer-header-actions.component.js +79 -0
  116. package/src/lib/transfer/transfer-header-actions.component.js.map +1 -0
  117. package/src/lib/transfer/transfer-keys.d.ts +31 -0
  118. package/src/lib/transfer/transfer-keys.js +56 -0
  119. package/src/lib/transfer/transfer-keys.js.map +1 -0
  120. package/src/lib/model/commerce-io.d.ts +0 -36
  121. package/src/lib/model/commerce-io.js +0 -245
  122. package/src/lib/model/commerce-io.js.map +0 -1
  123. package/src/lib/server/contact-refund.d.ts +0 -117
  124. package/src/lib/server/contact-refund.js +0 -196
  125. package/src/lib/server/contact-refund.js.map +0 -1
@@ -1,196 +0,0 @@
1
- import { _ as _extends } from "@swc/helpers/_/_extends";
2
- /**
3
- * @license
4
- * Copyright 2026 Aglyn LLC
5
- *
6
- * Licensed under the Apache License, Version 2.0 (the "License");
7
- * you may not use this file except in compliance with the License.
8
- * You may obtain a copy of the License at
9
- *
10
- * http://www.apache.org/licenses/LICENSE-2.0
11
- *
12
- * Unless required by applicable law or agreed to in writing, software
13
- * distributed under the License is distributed on an "AS IS" BASIS,
14
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
- * See the License for the specific language governing permissions and
16
- * limitations under the License.
17
- */ import { mergeContactInteraction, normalizeContactEmail } from "@aglyn/aglyn/server";
18
- import { firebaseAdmin, orgDataCollectionForHost } from "@aglyn/tenant-data-admin";
19
- // Leaf imports, not the barrel: `refund.spec.ts` and most specs in this
20
- // library mock `@aglyn/tenant-data-admin` wholesale, and a permissive stub of
21
- // either function would turn every case below green without a document ever
22
- // having to exist.
23
- import { findContactByEmail } from "@aglyn/tenant-data-admin/server/contact-email-index";
24
- import { updateExisting } from "@aglyn/tenant-data-admin/server/update-existing";
25
- /**
26
- * Money going BACK to a customer, recorded on their contact (AGL-1754).
27
- *
28
- * `upsertHostContact` accumulates `ltvCents` and nothing anywhere decremented
29
- * it, so a customer who bought $500 and returned all of it read identically to
30
- * one who bought $500 and kept it — and the first surface built on the field
31
- * (RFM segmentation, a lifetime-value column, a best-customers list) would have
32
- * ranked a serial returner among the merchant's best customers.
33
- *
34
- * WHAT THIS WRITES, AND WHY IT IS NOT A DECREMENT
35
- *
36
- * `ltvCents` is left exactly as it is and the reversal is recorded BESIDE it:
37
- *
38
- * | field | meaning |
39
- * | -- | -- |
40
- * | `ltvCents` | gross money handed over, unchanged (AGL-1748) |
41
- * | `refundedCents` | gross money handed back |
42
- * | `ordersCount` | orders placed, unchanged |
43
- * | `refundedOrdersCount` | of those, how many ended fully reversed |
44
- * | `lastRefundAtMs` | when money last went back |
45
- *
46
- * This is the shape AGL-1747 chose two days earlier for the same question on
47
- * the orders CSV — `amountUsd` stayed gross under its existing name and
48
- * `refundedUsd`/`netUsd` were appended — and it is the shape AGL-1748 named
49
- * when it recorded the gross semantics in `upsert-contact.ts`. Netting in
50
- * place would make a stored number mean one thing for rows written before this
51
- * commit and another for rows written after, with nothing on the document to
52
- * tell the two apart. Two fields cost one number's worth of storage and keep
53
- * both facts, and a reader that wants the net has `ltvCents - refundedCents`.
54
- *
55
- * It also answers "can it go negative?" by moving the question off storage.
56
- * Both figures are monotonic counters of money that actually moved in one
57
- * direction, so neither can go negative. The DERIVED net can, for exactly one
58
- * reason: an order placed before AGL-1748 taught POS, drafts, reservations and
59
- * bookings to pass `purchaseCents` at all, whose sale was never added and whose
60
- * refund is subtracted here. That is a true statement about the data, and
61
- * clamping it away at write time would erase the only evidence that a purchase
62
- * is missing. AGL-1753 is the backfill that reconciles it, by SETTING both
63
- * fields from the orders — which already carry `refundedCents`, so whatever a
64
- * reader decides here costs the rebuild nothing. A merchant-facing reader
65
- * should show gross and refunded, and clamp only the net it ranks on.
66
- *
67
- * `ordersCount` is deliberately NOT decremented. A fully refunded order still
68
- * happened, and a decrement cannot be undone by a reader that would rather
69
- * count it — while `refundedOrdersCount` beside it lets RFM's frequency term
70
- * net the two, or not, at the point of display. Same argument as the amount,
71
- * one field along.
72
- *
73
- * WHEN THE CONTACT IS MISSING, THIS REFUSES — and records the refusal.
74
- *
75
- * Refusing is right on AGL-1732's test, "does refusing discard money or work
76
- * that already happened?", because it does not: the refund is durable on the
77
- * order (`refundedCents`, the timeline entry, Stripe), and the contact is a
78
- * derived aggregate that AGL-1753's rebuild reads back out of those same
79
- * orders. Creating one here would be worse three ways — creation is audience
80
- * BAND gated, so it hard-bands a Free org (raising the AGL-891 dropped-contacts
81
- * alert for a record the merchant never had) or meters onto a paid invoice; the
82
- * contact it created would hold a refund and no purchase, which is the negative
83
- * lifetime value above with nothing to reconcile it against; and it is the
84
- * phantom-document shape the `updateExisting` sweep spent yesterday closing.
85
- *
86
- * So the write is an `update()`, never `set(…, { merge: true })` — the contact
87
- * may have been deleted between the query and the write, and a merge-set would
88
- * resurrect it as a document holding nothing but a refund. Every refusal
89
- * increments `hosts/{hostId}/counters/contactRefundsUnmatched`, the counter
90
- * shape `contactsDropped` already uses, so a refund that reached no contact is
91
- * countable rather than silent.
92
- *
93
- * CHARGEBACKS (AGL-1554) are a separate decision and are NOT handled here.
94
- * This takes `kind` so the handler that eventually answers
95
- * `charge.dispute.closed` can record a lost dispute through the same writer
96
- * with the same fields: money reversed is money reversed, whichever door it
97
- * left by, and only the wording of the timeline entry differs.
98
- */ export async function recordContactRefund(options) {
99
- // Swallow-all, like `upsertHostContact`: the money has already moved and the
100
- // order already records it, so nothing here may fail the refund.
101
- try {
102
- var _snapshot_get, _snapshot_get1;
103
- const amountCents = Math.round(Number(options.amountCents));
104
- if (!(amountCents > 0)) return;
105
- const email = normalizeContactEmail(options.email);
106
- if (!email) {
107
- await recordUnmatchedRefund(options.hostId, options.orderId, 'no-email');
108
- return;
109
- }
110
- // Contacts are ORG-scoped (AGL-237), not `hosts/{h}/contacts`, and reads
111
- // narrow to what this host may see (AGL-1039) — the same lookup
112
- // `upsertHostContact` makes, through the org's address index
113
- // (AGL-2633), so both writers resolve the same document even when the
114
- // order's address is one a merge folded into the buyer's record.
115
- const contactsRef = await orgDataCollectionForHost(options.hostId, 'contacts');
116
- const snapshot = await findContactByEmail(contactsRef, email, {
117
- hostId: options.hostId
118
- });
119
- if (!snapshot) {
120
- await recordUnmatchedRefund(options.hostId, options.orderId, 'no-contact');
121
- return;
122
- }
123
- const atMs = Date.now();
124
- const interaction = {
125
- // `refId` names an order, so the interaction is an order interaction.
126
- // There is no `'refund'` ContactSource and this deliberately does not
127
- // add one: `sources` records which capture silo produced the contact,
128
- // and a refund captures nobody.
129
- type: 'order',
130
- atMs,
131
- refId: options.orderId,
132
- summary: `$${(amountCents / 100).toFixed(2)} ` + (options.kind === 'chargeback' ? 'charged back' : 'refunded') + (options.closedTheOrder ? ' (full)' : '')
133
- };
134
- // Reused for the newest-first ordering and the interactions cap; only the
135
- // timeline is taken. The `sources` half of the merge is discarded on
136
- // purpose, per the note above.
137
- const { interactions } = mergeContactInteraction({
138
- sources: (_snapshot_get = snapshot.get('sources')) != null ? _snapshot_get : {},
139
- interactions: (_snapshot_get1 = snapshot.get('interactions')) != null ? _snapshot_get1 : []
140
- }, {
141
- source: 'order',
142
- interaction
143
- });
144
- const FieldValue = firebaseAdmin.firestore.FieldValue;
145
- // Every field is top-level. `update()` replaces a nested map wholesale
146
- // rather than merging into it, so anything under a map would have to be
147
- // written as a dotted path; there is nothing here that is.
148
- //
149
- // The amounts are increments, so two refunds settling at once both land.
150
- // `interactions` is a read-modify-write and therefore CAN lose one of two
151
- // concurrent entries — the same exposure `upsertHostContact` has on the
152
- // same array, and it costs a timeline line rather than a number.
153
- const landed = await updateExisting(snapshot.ref, _extends({
154
- refundedCents: FieldValue.increment(amountCents)
155
- }, options.closedTheOrder ? {
156
- refundedOrdersCount: FieldValue.increment(1)
157
- } : {}, {
158
- lastRefundAtMs: atMs,
159
- interactions,
160
- updatedAt: FieldValue.serverTimestamp()
161
- }));
162
- if (!landed) {
163
- await recordUnmatchedRefund(options.hostId, options.orderId, 'contact-deleted');
164
- }
165
- } catch (error) {
166
- console.error('recordContactRefund failed', error);
167
- }
168
- }
169
- /**
170
- * A refund that reached no contact, recorded rather than dropped (AGL-1754).
171
- *
172
- * The `set(…, { merge: true })` here IS meant to conjure the document — it is
173
- * a counter, and the shape is `counters/contactsDropped`'s, so an operator
174
- * reading a host's counters finds both kinds of missed contact record in one
175
- * place. The reason is stamped because the three cases want different answers:
176
- * `no-email` is an order that never identified its buyer, `no-contact` is the
177
- * AGL-1753 backfill's work, and `contact-deleted` is a contact removed between
178
- * the sale and the refund, which nothing should recreate.
179
- */ async function recordUnmatchedRefund(hostId, orderId, reason) {
180
- console.warn('refund reached no contact', {
181
- hostId,
182
- orderId,
183
- reason
184
- });
185
- const FieldValue = firebaseAdmin.firestore.FieldValue;
186
- await firebaseAdmin.app().firestore().collection('hosts').doc(hostId).collection('counters').doc('contactRefundsUnmatched').set({
187
- total: FieldValue.increment(1),
188
- lastOrderId: orderId,
189
- lastReason: reason,
190
- lastAtMs: Date.now()
191
- }, {
192
- merge: true
193
- }).catch(()=>undefined);
194
- }
195
-
196
- //# sourceMappingURL=contact-refund.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/contact-refund.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 {\n mergeContactInteraction,\n normalizeContactEmail,\n type ContactInteraction,\n} from '@aglyn/aglyn/server'\nimport {\n firebaseAdmin,\n orgDataCollectionForHost,\n} from '@aglyn/tenant-data-admin'\n// Leaf imports, not the barrel: `refund.spec.ts` and most specs in this\n// library mock `@aglyn/tenant-data-admin` wholesale, and a permissive stub of\n// either function would turn every case below green without a document ever\n// having to exist.\nimport { findContactByEmail } from '@aglyn/tenant-data-admin/server/contact-email-index'\nimport { updateExisting } from '@aglyn/tenant-data-admin/server/update-existing'\n\n/**\n * Money going BACK to a customer, recorded on their contact (AGL-1754).\n *\n * `upsertHostContact` accumulates `ltvCents` and nothing anywhere decremented\n * it, so a customer who bought $500 and returned all of it read identically to\n * one who bought $500 and kept it — and the first surface built on the field\n * (RFM segmentation, a lifetime-value column, a best-customers list) would have\n * ranked a serial returner among the merchant's best customers.\n *\n * WHAT THIS WRITES, AND WHY IT IS NOT A DECREMENT\n *\n * `ltvCents` is left exactly as it is and the reversal is recorded BESIDE it:\n *\n * | field | meaning |\n * | -- | -- |\n * | `ltvCents` | gross money handed over, unchanged (AGL-1748) |\n * | `refundedCents` | gross money handed back |\n * | `ordersCount` | orders placed, unchanged |\n * | `refundedOrdersCount` | of those, how many ended fully reversed |\n * | `lastRefundAtMs` | when money last went back |\n *\n * This is the shape AGL-1747 chose two days earlier for the same question on\n * the orders CSV — `amountUsd` stayed gross under its existing name and\n * `refundedUsd`/`netUsd` were appended — and it is the shape AGL-1748 named\n * when it recorded the gross semantics in `upsert-contact.ts`. Netting in\n * place would make a stored number mean one thing for rows written before this\n * commit and another for rows written after, with nothing on the document to\n * tell the two apart. Two fields cost one number's worth of storage and keep\n * both facts, and a reader that wants the net has `ltvCents - refundedCents`.\n *\n * It also answers \"can it go negative?\" by moving the question off storage.\n * Both figures are monotonic counters of money that actually moved in one\n * direction, so neither can go negative. The DERIVED net can, for exactly one\n * reason: an order placed before AGL-1748 taught POS, drafts, reservations and\n * bookings to pass `purchaseCents` at all, whose sale was never added and whose\n * refund is subtracted here. That is a true statement about the data, and\n * clamping it away at write time would erase the only evidence that a purchase\n * is missing. AGL-1753 is the backfill that reconciles it, by SETTING both\n * fields from the orders — which already carry `refundedCents`, so whatever a\n * reader decides here costs the rebuild nothing. A merchant-facing reader\n * should show gross and refunded, and clamp only the net it ranks on.\n *\n * `ordersCount` is deliberately NOT decremented. A fully refunded order still\n * happened, and a decrement cannot be undone by a reader that would rather\n * count it — while `refundedOrdersCount` beside it lets RFM's frequency term\n * net the two, or not, at the point of display. Same argument as the amount,\n * one field along.\n *\n * WHEN THE CONTACT IS MISSING, THIS REFUSES — and records the refusal.\n *\n * Refusing is right on AGL-1732's test, \"does refusing discard money or work\n * that already happened?\", because it does not: the refund is durable on the\n * order (`refundedCents`, the timeline entry, Stripe), and the contact is a\n * derived aggregate that AGL-1753's rebuild reads back out of those same\n * orders. Creating one here would be worse three ways — creation is audience\n * BAND gated, so it hard-bands a Free org (raising the AGL-891 dropped-contacts\n * alert for a record the merchant never had) or meters onto a paid invoice; the\n * contact it created would hold a refund and no purchase, which is the negative\n * lifetime value above with nothing to reconcile it against; and it is the\n * phantom-document shape the `updateExisting` sweep spent yesterday closing.\n *\n * So the write is an `update()`, never `set(…, { merge: true })` — the contact\n * may have been deleted between the query and the write, and a merge-set would\n * resurrect it as a document holding nothing but a refund. Every refusal\n * increments `hosts/{hostId}/counters/contactRefundsUnmatched`, the counter\n * shape `contactsDropped` already uses, so a refund that reached no contact is\n * countable rather than silent.\n *\n * CHARGEBACKS (AGL-1554) are a separate decision and are NOT handled here.\n * This takes `kind` so the handler that eventually answers\n * `charge.dispute.closed` can record a lost dispute through the same writer\n * with the same fields: money reversed is money reversed, whichever door it\n * left by, and only the wording of the timeline entry differs.\n */\nexport async function recordContactRefund(options: {\n hostId: string\n /** The order the money went back on — the contact timeline's `refId`. */\n orderId: string\n /**\n * The buyer as the ORDER recorded them, raw. Normalized here, because that\n * is the key the contacts collection is stored under and orders are not\n * consistent about it (AGL-1753 item 3). No `name` rides along on purpose: a\n * refund is not a capture point and must not rewrite who the contact is.\n */\n email: unknown\n /**\n * Cents reversed by THIS attempt, never the order total. `refund.ts` caps\n * each attempt against what is left, so several partials on one order sum to\n * at most the order total; this follows that number rather than recomputing\n * it.\n */\n amountCents: number\n /**\n * True only for the write that moved the order INTO `refunded`. Two partials\n * settling concurrently can both observe a fully refunded total, so the\n * caller decides this from the status transition it made, not from the\n * total it read.\n */\n closedTheOrder: boolean\n kind?: 'refund' | 'chargeback'\n}): Promise<void> {\n // Swallow-all, like `upsertHostContact`: the money has already moved and the\n // order already records it, so nothing here may fail the refund.\n try {\n const amountCents = Math.round(Number(options.amountCents))\n if (!(amountCents > 0)) return\n const email = normalizeContactEmail(options.email)\n if (!email) {\n await recordUnmatchedRefund(options.hostId, options.orderId, 'no-email')\n return\n }\n // Contacts are ORG-scoped (AGL-237), not `hosts/{h}/contacts`, and reads\n // narrow to what this host may see (AGL-1039) — the same lookup\n // `upsertHostContact` makes, through the org's address index\n // (AGL-2633), so both writers resolve the same document even when the\n // order's address is one a merge folded into the buyer's record.\n const contactsRef = await orgDataCollectionForHost(\n options.hostId,\n 'contacts',\n )\n const snapshot = await findContactByEmail(contactsRef, email, {\n hostId: options.hostId,\n })\n if (!snapshot) {\n await recordUnmatchedRefund(options.hostId, options.orderId, 'no-contact')\n return\n }\n const atMs = Date.now()\n const interaction: ContactInteraction = {\n // `refId` names an order, so the interaction is an order interaction.\n // There is no `'refund'` ContactSource and this deliberately does not\n // add one: `sources` records which capture silo produced the contact,\n // and a refund captures nobody.\n type: 'order',\n atMs,\n refId: options.orderId,\n summary:\n `$${(amountCents / 100).toFixed(2)} ` +\n (options.kind === 'chargeback' ? 'charged back' : 'refunded') +\n (options.closedTheOrder ? ' (full)' : ''),\n }\n // Reused for the newest-first ordering and the interactions cap; only the\n // timeline is taken. The `sources` half of the merge is discarded on\n // purpose, per the note above.\n const { interactions } = mergeContactInteraction(\n {\n sources: snapshot.get('sources') ?? {},\n interactions: snapshot.get('interactions') ?? [],\n },\n { source: 'order', interaction },\n )\n\n const FieldValue = firebaseAdmin.firestore.FieldValue\n // Every field is top-level. `update()` replaces a nested map wholesale\n // rather than merging into it, so anything under a map would have to be\n // written as a dotted path; there is nothing here that is.\n //\n // The amounts are increments, so two refunds settling at once both land.\n // `interactions` is a read-modify-write and therefore CAN lose one of two\n // concurrent entries — the same exposure `upsertHostContact` has on the\n // same array, and it costs a timeline line rather than a number.\n const landed = await updateExisting(snapshot.ref, {\n refundedCents: FieldValue.increment(amountCents),\n ...(options.closedTheOrder\n ? { refundedOrdersCount: FieldValue.increment(1) }\n : {}),\n lastRefundAtMs: atMs,\n interactions,\n updatedAt: FieldValue.serverTimestamp(),\n })\n if (!landed) {\n await recordUnmatchedRefund(\n options.hostId,\n options.orderId,\n 'contact-deleted',\n )\n }\n } catch (error) {\n console.error('recordContactRefund failed', error)\n }\n}\n\n/**\n * A refund that reached no contact, recorded rather than dropped (AGL-1754).\n *\n * The `set(…, { merge: true })` here IS meant to conjure the document — it is\n * a counter, and the shape is `counters/contactsDropped`'s, so an operator\n * reading a host's counters finds both kinds of missed contact record in one\n * place. The reason is stamped because the three cases want different answers:\n * `no-email` is an order that never identified its buyer, `no-contact` is the\n * AGL-1753 backfill's work, and `contact-deleted` is a contact removed between\n * the sale and the refund, which nothing should recreate.\n */\nasync function recordUnmatchedRefund(\n hostId: string,\n orderId: string,\n reason: 'no-email' | 'no-contact' | 'contact-deleted',\n): Promise<void> {\n console.warn('refund reached no contact', { hostId, orderId, reason })\n const FieldValue = firebaseAdmin.firestore.FieldValue\n await firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('counters')\n .doc('contactRefundsUnmatched')\n .set(\n {\n total: FieldValue.increment(1),\n lastOrderId: orderId,\n lastReason: reason,\n lastAtMs: Date.now(),\n },\n { merge: true },\n )\n .catch(() => undefined)\n}\n"],"names":["mergeContactInteraction","normalizeContactEmail","firebaseAdmin","orgDataCollectionForHost","findContactByEmail","updateExisting","recordContactRefund","options","snapshot","amountCents","Math","round","Number","email","recordUnmatchedRefund","hostId","orderId","contactsRef","atMs","Date","now","interaction","type","refId","summary","toFixed","kind","closedTheOrder","interactions","sources","get","source","FieldValue","firestore","landed","ref","refundedCents","increment","refundedOrdersCount","lastRefundAtMs","updatedAt","serverTimestamp","error","console","reason","warn","app","collection","doc","set","total","lastOrderId","lastReason","lastAtMs","merge","catch","undefined"],"mappings":"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"}