@aglyn/plugins-marketplace 1.0.0-beta.143

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 (165) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +7 -0
  3. package/package.json +51 -0
  4. package/src/index.d.ts +19 -0
  5. package/src/index.js +20 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/artifact-update-dialog.component.d.ts +27 -0
  8. package/src/lib/components/artifact-update-dialog.component.js +244 -0
  9. package/src/lib/components/artifact-update-dialog.component.js.map +1 -0
  10. package/src/lib/components/host-plugins-card.component.d.ts +23 -0
  11. package/src/lib/components/host-plugins-card.component.js +566 -0
  12. package/src/lib/components/host-plugins-card.component.js.map +1 -0
  13. package/src/lib/components/listing-content.component.d.ts +57 -0
  14. package/src/lib/components/listing-content.component.js +1880 -0
  15. package/src/lib/components/listing-content.component.js.map +1 -0
  16. package/src/lib/components/listing-image.component.d.ts +46 -0
  17. package/src/lib/components/listing-image.component.js +38 -0
  18. package/src/lib/components/listing-image.component.js.map +1 -0
  19. package/src/lib/components/listing-reviews.component.d.ts +22 -0
  20. package/src/lib/components/listing-reviews.component.js +419 -0
  21. package/src/lib/components/listing-reviews.component.js.map +1 -0
  22. package/src/lib/components/marketplace-browse.component.d.ts +38 -0
  23. package/src/lib/components/marketplace-browse.component.js +802 -0
  24. package/src/lib/components/marketplace-browse.component.js.map +1 -0
  25. package/src/lib/components/plugin-site-set-panel.component.d.ts +24 -0
  26. package/src/lib/components/plugin-site-set-panel.component.js +137 -0
  27. package/src/lib/components/plugin-site-set-panel.component.js.map +1 -0
  28. package/src/lib/components/plugin-site-set.component.d.ts +43 -0
  29. package/src/lib/components/plugin-site-set.component.js +425 -0
  30. package/src/lib/components/plugin-site-set.component.js.map +1 -0
  31. package/src/lib/components/rating-input.component.d.ts +3 -0
  32. package/src/lib/components/rating-input.component.js +47 -0
  33. package/src/lib/components/rating-input.component.js.map +1 -0
  34. package/src/lib/components/report-target.component.d.ts +36 -0
  35. package/src/lib/components/report-target.component.js +181 -0
  36. package/src/lib/components/report-target.component.js.map +1 -0
  37. package/src/lib/components/uninstall-impact-dialog.component.d.ts +40 -0
  38. package/src/lib/components/uninstall-impact-dialog.component.js +244 -0
  39. package/src/lib/components/uninstall-impact-dialog.component.js.map +1 -0
  40. package/src/lib/constants/bundle-common.d.ts +8 -0
  41. package/src/lib/constants/bundle-common.js +9 -0
  42. package/src/lib/constants/bundle-common.js.map +1 -0
  43. package/src/lib/hooks/use-artifact-update.d.ts +41 -0
  44. package/src/lib/hooks/use-artifact-update.js +169 -0
  45. package/src/lib/hooks/use-artifact-update.js.map +1 -0
  46. package/src/lib/hooks/use-marketplace-actions.d.ts +37 -0
  47. package/src/lib/hooks/use-marketplace-actions.js +523 -0
  48. package/src/lib/hooks/use-marketplace-actions.js.map +1 -0
  49. package/src/lib/model/artifact-merge.d.ts +154 -0
  50. package/src/lib/model/artifact-merge.js +228 -0
  51. package/src/lib/model/artifact-merge.js.map +1 -0
  52. package/src/lib/model/index.d.ts +43 -0
  53. package/src/lib/model/index.js +41 -0
  54. package/src/lib/model/index.js.map +1 -0
  55. package/src/lib/model/marketplace-props.d.ts +57 -0
  56. package/src/lib/model/marketplace-props.js +441 -0
  57. package/src/lib/model/marketplace-props.js.map +1 -0
  58. package/src/lib/model/marketplace.d.ts +833 -0
  59. package/src/lib/model/marketplace.js +929 -0
  60. package/src/lib/model/marketplace.js.map +1 -0
  61. package/src/lib/model/rating-field.d.ts +24 -0
  62. package/src/lib/model/rating-field.js +33 -0
  63. package/src/lib/model/rating-field.js.map +1 -0
  64. package/src/lib/plugin.d.ts +33 -0
  65. package/src/lib/plugin.js +86 -0
  66. package/src/lib/plugin.js.map +1 -0
  67. package/src/lib/server/billing-webhook.d.ts +25 -0
  68. package/src/lib/server/billing-webhook.js +1062 -0
  69. package/src/lib/server/billing-webhook.js.map +1 -0
  70. package/src/lib/server/checkout.d.ts +76 -0
  71. package/src/lib/server/checkout.js +477 -0
  72. package/src/lib/server/checkout.js.map +1 -0
  73. package/src/lib/server/connect.d.ts +24 -0
  74. package/src/lib/server/connect.js +205 -0
  75. package/src/lib/server/connect.js.map +1 -0
  76. package/src/lib/server/install-dataset-schema.d.ts +32 -0
  77. package/src/lib/server/install-dataset-schema.js +255 -0
  78. package/src/lib/server/install-dataset-schema.js.map +1 -0
  79. package/src/lib/server/install-email-starter.d.ts +65 -0
  80. package/src/lib/server/install-email-starter.js +279 -0
  81. package/src/lib/server/install-email-starter.js.map +1 -0
  82. package/src/lib/server/install-email-template.d.ts +34 -0
  83. package/src/lib/server/install-email-template.js +252 -0
  84. package/src/lib/server/install-email-template.js.map +1 -0
  85. package/src/lib/server/install-layout.d.ts +29 -0
  86. package/src/lib/server/install-layout.js +323 -0
  87. package/src/lib/server/install-layout.js.map +1 -0
  88. package/src/lib/server/install-pin-counts.d.ts +173 -0
  89. package/src/lib/server/install-pin-counts.js +248 -0
  90. package/src/lib/server/install-pin-counts.js.map +1 -0
  91. package/src/lib/server/install-plugin.d.ts +18 -0
  92. package/src/lib/server/install-plugin.js +405 -0
  93. package/src/lib/server/install-plugin.js.map +1 -0
  94. package/src/lib/server/install-template.d.ts +37 -0
  95. package/src/lib/server/install-template.js +348 -0
  96. package/src/lib/server/install-template.js.map +1 -0
  97. package/src/lib/server/install-theme.d.ts +42 -0
  98. package/src/lib/server/install-theme.js +313 -0
  99. package/src/lib/server/install-theme.js.map +1 -0
  100. package/src/lib/server/install.d.ts +27 -0
  101. package/src/lib/server/install.js +276 -0
  102. package/src/lib/server/install.js.map +1 -0
  103. package/src/lib/server/listing-versions.d.ts +26 -0
  104. package/src/lib/server/listing-versions.js +378 -0
  105. package/src/lib/server/listing-versions.js.map +1 -0
  106. package/src/lib/server/preview-image.d.ts +37 -0
  107. package/src/lib/server/preview-image.js +216 -0
  108. package/src/lib/server/preview-image.js.map +1 -0
  109. package/src/lib/server/provenance.d.ts +100 -0
  110. package/src/lib/server/provenance.js +118 -0
  111. package/src/lib/server/provenance.js.map +1 -0
  112. package/src/lib/server/publish-dataset-schema.d.ts +31 -0
  113. package/src/lib/server/publish-dataset-schema.js +202 -0
  114. package/src/lib/server/publish-dataset-schema.js.map +1 -0
  115. package/src/lib/server/publish-email-starter.d.ts +44 -0
  116. package/src/lib/server/publish-email-starter.js +231 -0
  117. package/src/lib/server/publish-email-starter.js.map +1 -0
  118. package/src/lib/server/publish-email-template.d.ts +33 -0
  119. package/src/lib/server/publish-email-template.js +211 -0
  120. package/src/lib/server/publish-email-template.js.map +1 -0
  121. package/src/lib/server/publish-layout.d.ts +33 -0
  122. package/src/lib/server/publish-layout.js +243 -0
  123. package/src/lib/server/publish-layout.js.map +1 -0
  124. package/src/lib/server/publish-plugin.d.ts +18 -0
  125. package/src/lib/server/publish-plugin.js +485 -0
  126. package/src/lib/server/publish-plugin.js.map +1 -0
  127. package/src/lib/server/publish-preconditions.d.ts +86 -0
  128. package/src/lib/server/publish-preconditions.js +160 -0
  129. package/src/lib/server/publish-preconditions.js.map +1 -0
  130. package/src/lib/server/publish-template.d.ts +27 -0
  131. package/src/lib/server/publish-template.js +215 -0
  132. package/src/lib/server/publish-template.js.map +1 -0
  133. package/src/lib/server/publish-theme.d.ts +38 -0
  134. package/src/lib/server/publish-theme.js +186 -0
  135. package/src/lib/server/publish-theme.js.map +1 -0
  136. package/src/lib/server/publish.d.ts +26 -0
  137. package/src/lib/server/publish.js +204 -0
  138. package/src/lib/server/publish.js.map +1 -0
  139. package/src/lib/server/publisher-profile-save.d.ts +30 -0
  140. package/src/lib/server/publisher-profile-save.js +226 -0
  141. package/src/lib/server/publisher-profile-save.js.map +1 -0
  142. package/src/lib/server/publisher-profile.d.ts +86 -0
  143. package/src/lib/server/publisher-profile.js +116 -0
  144. package/src/lib/server/publisher-profile.js.map +1 -0
  145. package/src/lib/server/purchase-entitlement.d.ts +83 -0
  146. package/src/lib/server/purchase-entitlement.js +118 -0
  147. package/src/lib/server/purchase-entitlement.js.map +1 -0
  148. package/src/lib/server/report.d.ts +32 -0
  149. package/src/lib/server/report.js +101 -0
  150. package/src/lib/server/report.js.map +1 -0
  151. package/src/lib/server/reviews.d.ts +41 -0
  152. package/src/lib/server/reviews.js +240 -0
  153. package/src/lib/server/reviews.js.map +1 -0
  154. package/src/lib/server/update-artifact.d.ts +19 -0
  155. package/src/lib/server/update-artifact.js +569 -0
  156. package/src/lib/server/update-artifact.js.map +1 -0
  157. package/src/lib/server/verification-request.d.ts +31 -0
  158. package/src/lib/server/verification-request.js +173 -0
  159. package/src/lib/server/verification-request.js.map +1 -0
  160. package/src/lib/server/version-stats.d.ts +202 -0
  161. package/src/lib/server/version-stats.js +305 -0
  162. package/src/lib/server/version-stats.js.map +1 -0
  163. package/src/lib/server.d.ts +30 -0
  164. package/src/lib/server.js +95 -0
  165. package/src/lib/server.js.map +1 -0
@@ -0,0 +1,1062 @@
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
+ */ // `after()`, never a bare `void promise` (AGL-2327). This handler runs inside
18
+ // the console's `/api/billing/webhook` invocation, which AGL-1133 measured on
19
+ // production is frozen the moment the response is sent — so fire-and-forget
20
+ // work scheduled here simply does not run. The console's own route was
21
+ // migrated for that reason (AGL-2346) and this one, in the same invocation,
22
+ // was not: marketplace revenue and refunds were reporting to nothing, which is
23
+ // the surviving half of "four server events ship to nothing" now that the
24
+ // Measurement Protocol credentials have landed (2026-08-17).
25
+ import { after } from "next/server";
26
+ import { firebaseAdmin, sendGa4Purchase, sendGa4Refund, clearConnectPayoutFailure, recordConnectPayoutFailure, syncConnectAccountStatus } from "@aglyn/tenant-data-admin";
27
+ /** Stripe failures a redelivery can actually fix. */ function isTransientStripeStatus(status) {
28
+ return status === 429 || status >= 500;
29
+ }
30
+ /**
31
+ * THE BUYER'S TAX JURISDICTION, from the session Stripe computed the tax on.
32
+ *
33
+ * Aglyn is a marketplace facilitator, so the tax on a facilitated sale is
34
+ * Aglyn's to collect and remit — and a return reports it BY STATE. Checkout
35
+ * already collects everything needed for that (`billing_address_collection:
36
+ * 'required'`, `automatic_tax[enabled]`), and Stripe states the address it
37
+ * computed from back on `customer_details.address`. Nothing read it, so every
38
+ * facilitated sale reached `tx-return.ts` with no jurisdiction and the whole
39
+ * of the marketplace tax could be stated only as a platform total attributable
40
+ * to no state.
41
+ *
42
+ * COUNTRY AND STATE ONLY, and deliberately narrower than the storefront twin.
43
+ * `StorefrontTaxRow` keeps `city` and `postalCode` beside them; the return
44
+ * reads neither, on either collection — it keys every jurisdiction bucket
45
+ * `COUNTRY-STATE`. A purchase document already names `buyerUid`, so anything
46
+ * stored here is personal data by association, and a street-grained address a
47
+ * filing cannot use is data held for no purpose. The two fields kept are the
48
+ * two the return reads.
49
+ *
50
+ * Null when the session states no country. An unstated jurisdiction is COUNTED
51
+ * by the return (`rowsMissingJurisdiction`) and never inferred: a jurisdiction
52
+ * reconstructed after the fact is a guess handed to a tax authority as a fact.
53
+ */ function buyerTaxJurisdiction(object) {
54
+ var _object_customer_details;
55
+ const address = object == null ? void 0 : (_object_customer_details = object.customer_details) == null ? void 0 : _object_customer_details.address;
56
+ const country = typeof (address == null ? void 0 : address.country) === 'string' ? address.country.trim() : '';
57
+ if (!country) return null;
58
+ const state = typeof (address == null ? void 0 : address.state) === 'string' ? address.state.trim() : '';
59
+ return {
60
+ country,
61
+ state: state || null
62
+ };
63
+ }
64
+ /** One authorized GET against the Stripe API, body parsed either way. */ async function stripeGet(url, stripeKey) {
65
+ const response = await fetch(url, {
66
+ headers: {
67
+ Authorization: `Bearer ${stripeKey}`
68
+ }
69
+ });
70
+ const body = await response.json().catch(()=>null);
71
+ return {
72
+ ok: response.ok,
73
+ status: response.status,
74
+ body
75
+ };
76
+ }
77
+ async function reverseMarketplaceSellerShare(purchaseRef, cause) {
78
+ var _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8, _ref9;
79
+ var _charge_body, _charge_body1, _transfer_body_reversals, _transfer_body, _transfer_body1, _transfer_body2;
80
+ const causeId = String((_ref = cause == null ? void 0 : cause.id) != null ? _ref : '');
81
+ if (!causeId) return;
82
+ // Kept as `disputeId` on the wire for `kind: 'dispute'` so reversals
83
+ // stamped before AGL-1995 are still found and ADOPTED by the crash-window
84
+ // backstop below. A refund uses its own key for the same reason.
85
+ const metadataKey = cause.kind === 'dispute' ? 'disputeId' : 'refundId';
86
+ const snapshot = await purchaseRef.get();
87
+ if (!snapshot.exists) return;
88
+ // The settle marker — see the doc comment. 0 counts.
89
+ if (snapshot.get('reversedTransferCents') != null) return;
90
+ const causeAmountCents = Math.round(Number((_ref1 = cause == null ? void 0 : cause.amountCents) != null ? _ref1 : 0));
91
+ if (!(causeAmountCents > 0)) return;
92
+ const stripeKey = process.env.STRIPE_SECRET_KEY;
93
+ if (!stripeKey) {
94
+ console.error('Marketplace transfer reversal skipped: STRIPE_SECRET_KEY is not set (AGL-1554)');
95
+ return;
96
+ }
97
+ /**
98
+ * Settles the step exactly once, whatever it found.
99
+ *
100
+ * A settle of ZERO is not the same event as a settle of the full share, and
101
+ * until AGL-2140 the document could not tell them apart (AGL-2140). Six
102
+ * paths below abandon the reversal for a DEFINITIVE reason — no charge id,
103
+ * Stripe refusing the charge read, no transfer on the charge, Stripe
104
+ * refusing the transfer read, nothing left to reverse, Stripe refusing the
105
+ * reversal itself — and each wrote `reversedTransferCents: 0`, which the
106
+ * short-circuit at the top of this function then honours forever. The only
107
+ * other trace was a `console.error`.
108
+ *
109
+ * A Stripe 4xx is one of those definitive reasons: `balance_insufficient` on
110
+ * a connected account is a **400**, not a 5xx, so it does not throw and does
111
+ * not redeliver. The publisher's share was therefore forfeited permanently,
112
+ * silently, and with nothing queryable to find it by afterwards.
113
+ *
114
+ * Settling stays — re-running these paths would eventually double-debit a
115
+ * seller, which is worse. What changes is that an abandoned reversal now
116
+ * says so ON THE DOCUMENT: `reversalFailedAt`, a machine-readable
117
+ * `reversalFailedReason`, and `reversalOwedCents` when the amount is known.
118
+ * `where('reversalFailedAt', '!=', null)` is then the recovery queue.
119
+ */ const settle = async (reversedTransferCents, transferReversalId, /** Why nothing (or less than the cause) was reversed. Omit on success. */ failure)=>{
120
+ const fresh = await purchaseRef.get();
121
+ if (!fresh.exists || fresh.get('reversedTransferCents') != null) return;
122
+ if (failure) {
123
+ var _failure_owedCents;
124
+ console.error('Marketplace seller share NOT reversed — recorded for recovery', {
125
+ purchaseId: purchaseRef.id,
126
+ kind: cause.kind,
127
+ causeId,
128
+ reason: failure.reason,
129
+ owedCents: (_failure_owedCents = failure.owedCents) != null ? _failure_owedCents : null
130
+ });
131
+ }
132
+ await purchaseRef.set(_extends({
133
+ reversedTransferCents
134
+ }, transferReversalId ? {
135
+ transferReversalId
136
+ } : {}, failure ? _extends({
137
+ reversalFailedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),
138
+ reversalFailedReason: failure.reason,
139
+ reversalFailedCause: cause.kind
140
+ }, failure.owedCents != null ? {
141
+ reversalOwedCents: failure.owedCents
142
+ } : {}) : {}), {
143
+ merge: true
144
+ });
145
+ };
146
+ const chargeId = String((_ref2 = cause == null ? void 0 : cause.chargeId) != null ? _ref2 : '');
147
+ if (!chargeId) {
148
+ await settle(0, null, {
149
+ reason: 'no-charge-on-cause'
150
+ });
151
+ return;
152
+ }
153
+ const charge = await stripeGet(`https://api.stripe.com/v1/charges/${chargeId}`, stripeKey);
154
+ if (!charge.ok) {
155
+ var _charge_body2;
156
+ if (isTransientStripeStatus(charge.status)) {
157
+ throw new Error(`Stripe charge read failed (${charge.status}) for marketplace ${cause.kind} ${causeId}`);
158
+ }
159
+ console.error('Stripe refused the charge read', (_charge_body2 = charge.body) == null ? void 0 : _charge_body2.error);
160
+ await settle(0, null, {
161
+ reason: 'charge-read-refused'
162
+ });
163
+ return;
164
+ }
165
+ const transferId = String((_ref3 = (_charge_body = charge.body) == null ? void 0 : _charge_body.transfer) != null ? _ref3 : '');
166
+ const chargeAmountCents = Math.round(Number((_ref4 = (_charge_body1 = charge.body) == null ? void 0 : _charge_body1.amount) != null ? _ref4 : 0));
167
+ if (!transferId || !(chargeAmountCents > 0)) {
168
+ await settle(0, null, {
169
+ reason: 'no-transfer-on-charge'
170
+ });
171
+ return;
172
+ }
173
+ const transfer = await stripeGet(`https://api.stripe.com/v1/transfers/${transferId}`, stripeKey);
174
+ if (!transfer.ok) {
175
+ var _transfer_body3;
176
+ if (isTransientStripeStatus(transfer.status)) {
177
+ throw new Error(`Stripe transfer read failed (${transfer.status}) for marketplace ${cause.kind} ${causeId}`);
178
+ }
179
+ console.error('Stripe refused the transfer read', (_transfer_body3 = transfer.body) == null ? void 0 : _transfer_body3.error);
180
+ await settle(0, null, {
181
+ reason: 'transfer-read-refused'
182
+ });
183
+ return;
184
+ }
185
+ // The crash window's backstop: the POST landed on a previous delivery and
186
+ // the record did not. Adopt what exists rather than creating a second one.
187
+ const existing = ((_ref5 = (_transfer_body = transfer.body) == null ? void 0 : (_transfer_body_reversals = _transfer_body.reversals) == null ? void 0 : _transfer_body_reversals.data) != null ? _ref5 : []).find((item)=>{
188
+ var _ref;
189
+ var _item_metadata;
190
+ return String((_ref = item == null ? void 0 : (_item_metadata = item.metadata) == null ? void 0 : _item_metadata[metadataKey]) != null ? _ref : '') === causeId;
191
+ });
192
+ if (existing) {
193
+ var _existing_amount, _existing_id;
194
+ await settle(Math.round(Number((_existing_amount = existing.amount) != null ? _existing_amount : 0)), String((_existing_id = existing.id) != null ? _existing_id : ''));
195
+ return;
196
+ }
197
+ const transferCents = Math.round(Number((_ref6 = (_transfer_body1 = transfer.body) == null ? void 0 : _transfer_body1.amount) != null ? _ref6 : 0));
198
+ const alreadyReversedCents = Math.round(Number((_ref7 = (_transfer_body2 = transfer.body) == null ? void 0 : _transfer_body2.amount_reversed) != null ? _ref7 : 0));
199
+ const remainingCents = Math.max(0, transferCents - alreadyReversedCents);
200
+ const shareCents = Math.min(Math.floor(causeAmountCents * transferCents / chargeAmountCents), remainingCents);
201
+ if (!(shareCents > 0)) {
202
+ // Not necessarily a fault — an earlier cause may already have pulled the
203
+ // whole transfer back — so `owedCents` is deliberately 0 rather than
204
+ // absent: nothing is owed, and the row says so instead of leaving the
205
+ // amount unknown.
206
+ await settle(0, null, {
207
+ reason: 'transfer-fully-reversed',
208
+ owedCents: 0
209
+ });
210
+ return;
211
+ }
212
+ const params = new URLSearchParams({
213
+ amount: String(shareCents),
214
+ [`metadata[${metadataKey}]`]: causeId,
215
+ 'metadata[purchaseId]': purchaseRef.id
216
+ });
217
+ const response = await fetch(`https://api.stripe.com/v1/transfers/${transferId}/reversals`, {
218
+ method: 'POST',
219
+ headers: {
220
+ Authorization: `Bearer ${stripeKey}`,
221
+ 'Content-Type': 'application/x-www-form-urlencoded',
222
+ 'Idempotency-Key': `${cause.kind}-reversal-${causeId}`
223
+ },
224
+ body: params.toString()
225
+ });
226
+ const reversal = await response.json().catch(()=>null);
227
+ if (!response.ok) {
228
+ if (isTransientStripeStatus(response.status)) {
229
+ throw new Error(`Stripe transfer reversal failed (${response.status}) for marketplace ${cause.kind} ${causeId}`);
230
+ }
231
+ // THE ONE THAT COSTS REAL MONEY. A definitive 4xx here — including a
232
+ // `balance_insufficient` on the publisher's account, which is a 400 — used
233
+ // to forfeit the whole share with only a log line.
234
+ console.error('Stripe refused the marketplace transfer reversal', reversal == null ? void 0 : reversal.error);
235
+ await settle(0, null, {
236
+ reason: 'reversal-refused',
237
+ owedCents: shareCents
238
+ });
239
+ return;
240
+ }
241
+ await settle(Math.round(Number((_ref8 = reversal == null ? void 0 : reversal.amount) != null ? _ref8 : shareCents)), String((_ref9 = reversal == null ? void 0 : reversal.id) != null ? _ref9 : ''));
242
+ }
243
+ /**
244
+ * THE PUBLISHER'S SHARE OF A *PARTIAL* REFUND, PULLED BACK (AGL-2299).
245
+ *
246
+ * WHAT WAS WRONG. `charge.refunded` fires on EVERY refund; `object.refunded`
247
+ * is true only once the whole charge is gone. Everything above this function
248
+ * is gated on that flag, so a partial refund did nothing at all — no
249
+ * reversal, no record, not even a log line — and the note beside the branch
250
+ * said so deliberately: "a partial refund is a concession the ledger's split
251
+ * cannot decompose".
252
+ *
253
+ * That premise is false, and it is expensive. `reverseMarketplaceSellerShare`
254
+ * decomposes exactly this, proportionally, and has since AGL-1554. What the
255
+ * old branch actually did was hand the entire concession to Aglyn: on a $100
256
+ * sale at a 20% take rate the platform holds $20 plus the tax it owes the
257
+ * state, and a $50 goodwill refund is paid out of the PLATFORM's balance —
258
+ * the publisher's $80 already left on the destination transfer. Aglyn is
259
+ * $30 down and the publisher is untouched. Any partial refund larger than the
260
+ * take rate loses money, which is most of them.
261
+ *
262
+ * The publisher agreement §8.4 is not ambiguous about this: *"You are
263
+ * responsible for refunds, chargebacks, and disputes on your sales. Aglyn may
264
+ * reverse or withhold amounts corresponding to refunded, disputed, or
265
+ * fraudulent transactions."* A partial refund is a refunded amount.
266
+ *
267
+ * THE ENTITLEMENT IS NOT TOUCHED, and that part of the old comment stands: a
268
+ * partial refund is a concession, not an un-buy. No `refundedAt`, so
269
+ * `hasLivePurchase` still says the buyer owns it, and no GA4 `refund` — that
270
+ * event nets the WHOLE transaction out of the platform-net accounting
271
+ * (AGL-1850), which a partial refund did not do. Only the money moves.
272
+ *
273
+ * WHY THIS IS A SECOND FUNCTION RATHER THAN A FLAG ON THE FIRST.
274
+ * `reverseMarketplaceSellerShare` is ONE-SHOT by design — `reversedTransferCents`
275
+ * present means "this purchase's pull-back has settled", and that marker is
276
+ * what stops a refund following a lost dispute from debiting a publisher
277
+ * twice. Partial refunds are the opposite shape: there can be several, and
278
+ * each must add to what came back. Teaching one function both disciplines is
279
+ * how the settle marker stops meaning anything.
280
+ *
281
+ * IDEMPOTENT WITHOUT A MARKER, because Stripe already keeps the ledger.
282
+ * The target is derived from the charge's CUMULATIVE `amount_refunded`, and
283
+ * what has already come back is `transfer.amount_reversed` — Stripe's own
284
+ * number, not ours:
285
+ *
286
+ * target = floor(amount_refunded × transfer.amount ÷ charge.amount)
287
+ * toReverse = clamp(target, 0, transfer.amount) − transfer.amount_reversed
288
+ *
289
+ * A redelivery recomputes the same target, finds `amount_reversed` already
290
+ * there, and reverses nothing. Three partial refunds converge on the right
291
+ * total whatever order they arrive in. And it composes with the one-shot path
292
+ * in both directions: a later FULL refund computes
293
+ * `min(transfer.amount, remaining)` and takes only the remainder, while a
294
+ * dispute takes its own proportional slice of what is left.
295
+ *
296
+ * FAILURES DO NOT SETTLE ANYTHING. Transient (429/5xx/network) throws so the
297
+ * redelivery is the retry, exactly as the one-shot path does. A definitive
298
+ * 4xx — `balance_insufficient` on a publisher's account is a 400 — records
299
+ * itself in AGL-2140's existing recovery queue (`reversalFailedAt`,
300
+ * `reversalFailedReason`, `reversalOwedCents`) and returns 200 so the endpoint
301
+ * does not redeliver forever. Nothing is permanently forfeited by that: the
302
+ * next partial refund on the same charge recomputes the target from scratch
303
+ * and picks the shortfall back up.
304
+ */ async function reverseMarketplacePartialRefundShare(purchaseRef, chargeId) {
305
+ var _ref, _ref1, _ref2, _ref3, _ref4, _ref5;
306
+ var _charge_body, _charge_body1, _charge_body2, _transfer_body, _transfer_body1;
307
+ if (!chargeId) return;
308
+ const snapshot = await purchaseRef.get();
309
+ if (!snapshot.exists) return;
310
+ // The one-shot path already settled this purchase — a lost dispute, or a
311
+ // full refund that landed first. Its marker means the pull-back is closed.
312
+ if (snapshot.get('reversedTransferCents') != null) return;
313
+ const stripeKey = process.env.STRIPE_SECRET_KEY;
314
+ if (!stripeKey) {
315
+ console.error('Marketplace partial-refund reversal skipped: STRIPE_SECRET_KEY is not set (AGL-2299)');
316
+ return;
317
+ }
318
+ // The charge is RE-READ rather than taken from the event, for the same
319
+ // reason the one-shot path re-reads it: the event is a snapshot, and the
320
+ // number that decides this reversal is the charge's CUMULATIVE
321
+ // `amount_refunded`. A redelivery days later, or a drain of a parked
322
+ // partial, must converge on what is true now — not on what was true when
323
+ // the event was minted. It also means an API version that trims `transfer`
324
+ // out of the webhook payload cannot silently turn this into a no-op.
325
+ const charge = await stripeGet(`https://api.stripe.com/v1/charges/${chargeId}`, stripeKey);
326
+ if (!charge.ok) {
327
+ var _charge_body3;
328
+ if (isTransientStripeStatus(charge.status)) {
329
+ throw new Error(`Stripe charge read failed (${charge.status}) for marketplace partial refund on ${chargeId}`);
330
+ }
331
+ console.error('Stripe refused the charge read', (_charge_body3 = charge.body) == null ? void 0 : _charge_body3.error);
332
+ return;
333
+ }
334
+ const chargeAmountCents = Math.round(Number((_ref = (_charge_body = charge.body) == null ? void 0 : _charge_body.amount) != null ? _ref : 0));
335
+ const refundedCents = Math.round(Number((_ref1 = (_charge_body1 = charge.body) == null ? void 0 : _charge_body1.amount_refunded) != null ? _ref1 : 0));
336
+ if (!(chargeAmountCents > 0) || !(refundedCents > 0)) return;
337
+ const transferId = String((_ref2 = (_charge_body2 = charge.body) == null ? void 0 : _charge_body2.transfer) != null ? _ref2 : '');
338
+ if (!transferId) {
339
+ // No destination transfer means no seller share to pull back — a
340
+ // marketplace charge always has one, so this is a charge we do not own.
341
+ return;
342
+ }
343
+ const transfer = await stripeGet(`https://api.stripe.com/v1/transfers/${transferId}`, stripeKey);
344
+ if (!transfer.ok) {
345
+ var _transfer_body2;
346
+ if (isTransientStripeStatus(transfer.status)) {
347
+ throw new Error(`Stripe transfer read failed (${transfer.status}) for marketplace partial refund on ${chargeId}`);
348
+ }
349
+ console.error('Stripe refused the transfer read', (_transfer_body2 = transfer.body) == null ? void 0 : _transfer_body2.error);
350
+ await purchaseRef.set({
351
+ partialRefundedCents: refundedCents,
352
+ reversalFailedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),
353
+ reversalFailedReason: 'transfer-read-refused',
354
+ reversalFailedCause: 'partial-refund'
355
+ }, {
356
+ merge: true
357
+ });
358
+ return;
359
+ }
360
+ const transferCents = Math.round(Number((_ref3 = (_transfer_body = transfer.body) == null ? void 0 : _transfer_body.amount) != null ? _ref3 : 0));
361
+ const alreadyReversedCents = Math.round(Number((_ref4 = (_transfer_body1 = transfer.body) == null ? void 0 : _transfer_body1.amount_reversed) != null ? _ref4 : 0));
362
+ const targetCents = Math.min(transferCents, Math.floor(refundedCents * transferCents / chargeAmountCents));
363
+ const toReverseCents = targetCents - alreadyReversedCents;
364
+ if (!(toReverseCents > 0)) {
365
+ // Already square — a redelivery, or an earlier cause that took more than
366
+ // this refund's proportional slice. Record what the buyer has had back so
367
+ // the seller panel and the ledger agree with Stripe.
368
+ await purchaseRef.set({
369
+ partialRefundedCents: refundedCents,
370
+ partialReversedTransferCents: alreadyReversedCents
371
+ }, {
372
+ merge: true
373
+ });
374
+ return;
375
+ }
376
+ const response = await fetch(`https://api.stripe.com/v1/transfers/${transferId}/reversals`, {
377
+ method: 'POST',
378
+ headers: {
379
+ Authorization: `Bearer ${stripeKey}`,
380
+ 'Content-Type': 'application/x-www-form-urlencoded',
381
+ // Derived from the charge AND the cumulative target, so a redelivery
382
+ // of the same event replays Stripe's stored response while a genuinely
383
+ // larger second refund gets its own call. Belt and braces: the
384
+ // `amount_reversed` read above already turns a redelivery into a
385
+ // no-op, and this closes the window where the POST landed and our
386
+ // record did not.
387
+ 'Idempotency-Key': `partial-reversal-${chargeId}-${targetCents}`
388
+ },
389
+ body: new URLSearchParams({
390
+ amount: String(toReverseCents),
391
+ 'metadata[partialRefundOfCharge]': chargeId,
392
+ 'metadata[purchaseId]': purchaseRef.id
393
+ }).toString()
394
+ });
395
+ const reversal = await response.json().catch(()=>null);
396
+ if (!response.ok) {
397
+ if (isTransientStripeStatus(response.status)) {
398
+ throw new Error(`Stripe partial transfer reversal failed (${response.status}) on ${chargeId}`);
399
+ }
400
+ console.error('Marketplace partial-refund seller share NOT reversed — recorded for recovery (AGL-2299)', {
401
+ purchaseId: purchaseRef.id,
402
+ chargeId,
403
+ owedCents: toReverseCents
404
+ });
405
+ await purchaseRef.set({
406
+ partialRefundedCents: refundedCents,
407
+ partialReversedTransferCents: alreadyReversedCents,
408
+ reversalFailedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),
409
+ reversalFailedReason: 'reversal-refused',
410
+ reversalFailedCause: 'partial-refund',
411
+ reversalOwedCents: toReverseCents
412
+ }, {
413
+ merge: true
414
+ });
415
+ return;
416
+ }
417
+ await purchaseRef.set(_extends({
418
+ partialRefundedCents: refundedCents,
419
+ partialReversedTransferCents: alreadyReversedCents + Math.round(Number((_ref5 = reversal == null ? void 0 : reversal.amount) != null ? _ref5 : toReverseCents))
420
+ }, (reversal == null ? void 0 : reversal.id) ? {
421
+ partialTransferReversalId: String(reversal.id)
422
+ } : {}), {
423
+ merge: true
424
+ });
425
+ }
426
+ /**
427
+ * WHERE A REFUND THAT ARRIVED TOO EARLY WAITS (AGL-2148).
428
+ *
429
+ * THE WINDOW IS REAL, not exotic. `checkout.session.completed` is what writes
430
+ * `marketplacePurchases/{sessionId}`, and that delivery RETRIES: this endpoint
431
+ * 500s on every transient Stripe failure raised in this file and on any throw
432
+ * from a sibling plugin handler, and Stripe then redelivers for up to three
433
+ * days. A dashboard refund issued inside that window joins on the payment
434
+ * intent, finds nothing, and — before this — the branch simply ended. No
435
+ * `else`. The buyer got their money, the publisher kept their 80%, Aglyn ate
436
+ * the gross, and `hasLivePurchase` saw no `refundedAt`, so the refunded buyer
437
+ * kept the install forever.
438
+ *
439
+ * WHY NOT SIMPLY THROW so Stripe redelivers the refund. Because
440
+ * `charge.refunded` and `charge.dispute.*` arrive on this same endpoint for
441
+ * storefront orders and subscription charges, whose payment intents will never
442
+ * match a marketplace purchase. Throwing on "not found" would 500 the webhook
443
+ * for every non-marketplace refund, and the route drops its idempotency claim
444
+ * on a throw — so the commerce and bookings handlers' non-idempotent effects
445
+ * would re-run. The containment would cost more than the defect.
446
+ *
447
+ * SO THE EVENT IS PARKED, and the session landing drains it. Keyed by PAYMENT
448
+ * INTENT, which is the only id the refund door, the dispute door and the
449
+ * session landing all carry: the drain is one `get()` on a known document id,
450
+ * with no query and no composite index.
451
+ *
452
+ * NOTHING IS PARKED SPECULATIVELY. Both doors gate the write on a
453
+ * marketplace discriminator (`metadata.type`, stamped on the PaymentIntent at
454
+ * checkout — see checkout.ts), so a commerce refund writes no orphan at all.
455
+ * That is why this store needs no TTL and no sweeper: a document here means a
456
+ * marketplace purchase is mid-flight, and the session landing removes it.
457
+ */ const REFUND_ORPHAN_COLLECTION = 'marketplaceRefundOrphans';
458
+ /**
459
+ * The three effects a full refund or a LOST dispute applies to a purchase:
460
+ * revoke, report, pull the seller's share back.
461
+ *
462
+ * ONE function for three call sites — the refund door, the dispute door, and
463
+ * the orphan drain — deliberately, and it is the AGL-1994 lesson again: the
464
+ * drain must apply *the same* effects the door would have, and a second copy
465
+ * of them is how the two drift. The drain therefore cannot forget the GA hit
466
+ * or the reversal, because it is not in a position to.
467
+ *
468
+ * IDEMPOTENT AT EVERY LAYER. `refundedAt` read BEFORE the stamp is the GA
469
+ * guard (a redelivery reports nothing twice); `reversedTransferCents` is the
470
+ * reversal's settle marker, so a redelivery — or a drain running after the
471
+ * door already ran — returns before any Stripe call; and the reversal POST
472
+ * carries `Idempotency-Key: <kind>-reversal-<causeId>`, which is the SAME key
473
+ * either path would have used because it is derived from the cause, not from
474
+ * who is applying it. A drained refund and a redelivered `charge.refunded`
475
+ * are therefore indistinguishable to Stripe, and the publisher cannot be
476
+ * debited twice.
477
+ */ async function applyMarketplaceRefundOutcome(purchaseRef, cause, /** Dispute-only fields stamped in the same write. */ extraStamp = {}) {
478
+ const snapshot = await purchaseRef.get();
479
+ if (!snapshot.exists) return;
480
+ // Read BEFORE the stamp: `refundedAt` doubles as the GA guard, so a
481
+ // redelivery that slips past the route's event claim finds the purchase
482
+ // already refunded and reports nothing a second time.
483
+ const alreadyRefunded = Boolean(snapshot.get('refundedAt'));
484
+ await purchaseRef.set(_extends({}, extraStamp, {
485
+ refundedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),
486
+ refundedCents: cause.amountCents
487
+ }), {
488
+ merge: true
489
+ });
490
+ // GA4 `refund` (AGL-1850) — the reversal of the AGL-1639 purchase, in the
491
+ // SAME accounting. The purchase reported the platform NET (gross − tax −
492
+ // transfer), so the refund must reverse that number: refunding the
493
+ // tax-inclusive gross would net MORE out of GA than the sale ever put in.
494
+ // Fire-and-forget, after the stamp: an analytics failure must never
495
+ // un-claim a Stripe event.
496
+ if (!alreadyRefunded) {
497
+ var _snapshot_get, _snapshot_get1, _snapshot_get2;
498
+ const grossCents = Number((_snapshot_get = snapshot.get('amountCents')) != null ? _snapshot_get : 0);
499
+ const taxCents = Number((_snapshot_get1 = snapshot.get('taxCents')) != null ? _snapshot_get1 : 0);
500
+ const sellerCents = Number((_snapshot_get2 = snapshot.get('transferCents')) != null ? _snapshot_get2 : 0);
501
+ const netCents = grossCents - taxCents - sellerCents;
502
+ if (netCents > 0) {
503
+ after(()=>{
504
+ var _snapshot_get;
505
+ return sendGa4Refund({
506
+ transactionId: String(purchaseRef.id),
507
+ value: netCents / 100,
508
+ currency: cause.currency,
509
+ items: [],
510
+ stripeCustomerId: cause.stripeCustomerId || String((_snapshot_get = snapshot.get('buyerUid')) != null ? _snapshot_get : '')
511
+ }).catch(()=>undefined);
512
+ });
513
+ }
514
+ }
515
+ // AFTER the revocation, so the entitlement never stays live because a
516
+ // Stripe read failed: this half throws on transient failures (the
517
+ // redelivery is the retry, and everything above is idempotent under it) and
518
+ // settles definitively otherwise.
519
+ await reverseMarketplaceSellerShare(purchaseRef, cause);
520
+ }
521
+ /**
522
+ * Parks a cause whose purchase document does not exist yet.
523
+ *
524
+ * FIRST CAUSE WINS. The document is not merged over: a redelivery of the same
525
+ * refund would otherwise move `createdAt`, and the (vanishingly unlikely)
526
+ * refund-then-dispute pair inside one window would otherwise blend into a
527
+ * chimera carrying one cause's id and the other's amount. The first cause is
528
+ * the one that revoked, and the reversal settle marker means only one
529
+ * pull-back can happen regardless of which is replayed.
530
+ */ async function recordRefundOrphan(firestore, cause) {
531
+ if (!cause.paymentIntentId || !cause.id) return;
532
+ const orphanRef = firestore.collection(REFUND_ORPHAN_COLLECTION).doc(cause.paymentIntentId);
533
+ const parked = await orphanRef.get();
534
+ // FIRST CAUSE WINS — with one exception (AGL-2299). A parked
535
+ // `partial-refund` is strictly weaker than a full refund or a lost dispute:
536
+ // it moves a slice of the seller's share and nothing else. If the same
537
+ // charge is then refunded in full inside the same window, the stronger
538
+ // cause must replace it, or the drain would pull back a slice and leave the
539
+ // buyer entitled to content they have been fully refunded for. The reverse
540
+ // never happens: a fully refunded charge cannot take a partial afterwards.
541
+ if (parked.exists && !(parked.get('kind') === 'partial-refund' && cause.kind !== 'partial-refund')) {
542
+ return;
543
+ }
544
+ await orphanRef.set({
545
+ kind: cause.kind,
546
+ id: cause.id,
547
+ paymentIntentId: cause.paymentIntentId,
548
+ amountCents: cause.amountCents,
549
+ chargeId: cause.chargeId,
550
+ currency: cause.currency,
551
+ stripeCustomerId: cause.stripeCustomerId,
552
+ createdAt: firebaseAdmin.firestore.FieldValue.serverTimestamp()
553
+ }, {
554
+ merge: true
555
+ });
556
+ // Loud on purpose: this is money in flight. If the matching session never
557
+ // lands (a buyer who abandoned checkout cannot be refunded, so in practice
558
+ // it always does), the row is the only trace, and it is queryable.
559
+ console.error('Marketplace refund landed before its purchase — parked for the session handler (AGL-2148)', {
560
+ kind: cause.kind,
561
+ causeId: cause.id,
562
+ paymentIntentId: cause.paymentIntentId,
563
+ amountCents: cause.amountCents
564
+ });
565
+ }
566
+ /**
567
+ * Applies — and removes — a cause parked before this purchase existed.
568
+ *
569
+ * WHETHER THE SESSION PATH MAY REVOKE AND REVERSE FOR A CAUSE IT DID NOT
570
+ * RECEIVE: yes, and it is not really a choice. The alternative is a purchase
571
+ * document that reads as live for money the buyer already got back — a free
572
+ * install and a publisher paid for a sale that was undone.
573
+ *
574
+ * THE DELETE IS LAST, after the reversal returns. A transient Stripe failure
575
+ * inside `reverseMarketplaceSellerShare` THROWS on purpose so the session
576
+ * delivery 500s and Stripe redelivers; if the orphan were dropped first, that
577
+ * redelivery would find nothing to replay and the reversal would be lost
578
+ * permanently. Deleting after a DEFINITIVE settle is correct and deliberate:
579
+ * that settle wrote `reversalFailedAt` / `reversalFailedReason` /
580
+ * `reversalOwedCents` onto the purchase, so the money lands in AGL-2140's
581
+ * existing recovery queue rather than in a second one invented here.
582
+ */ async function drainMarketplaceRefundOrphan(firestore, purchaseRef, paymentIntentId) {
583
+ var _orphan_get, _orphan_get1;
584
+ if (!paymentIntentId) return;
585
+ const orphanRef = firestore.collection(REFUND_ORPHAN_COLLECTION).doc(paymentIntentId);
586
+ const orphan = await orphanRef.get();
587
+ if (!orphan.exists) return;
588
+ const causeId = String((_orphan_get = orphan.get('id')) != null ? _orphan_get : '');
589
+ const parkedKind = String((_orphan_get1 = orphan.get('kind')) != null ? _orphan_get1 : '');
590
+ if (parkedKind === 'partial-refund') {
591
+ var _orphan_get2;
592
+ // A partial refund never revokes and never reports, so it does not go
593
+ // through `applyMarketplaceRefundOutcome` (AGL-2299). The charge is RE-READ
594
+ // from Stripe rather than replayed from the parked amount: the target is
595
+ // computed from the charge's cumulative `amount_refunded`, and by the time
596
+ // this drains there may have been a second refund the parked document
597
+ // never saw.
598
+ // Throws on a transient Stripe failure, which leaves the orphan in place
599
+ // for the redelivery — the delete below is exactly what must not happen
600
+ // in that case, and it is why it is AFTER this await rather than before.
601
+ await reverseMarketplacePartialRefundShare(purchaseRef, String((_orphan_get2 = orphan.get('chargeId')) != null ? _orphan_get2 : ''));
602
+ await orphanRef.delete();
603
+ return;
604
+ }
605
+ const kind = parkedKind === 'dispute' ? 'dispute' : 'refund';
606
+ if (causeId) {
607
+ var _orphan_get3, _orphan_get4, _orphan_get5, _orphan_get6;
608
+ await applyMarketplaceRefundOutcome(purchaseRef, {
609
+ kind,
610
+ id: causeId,
611
+ paymentIntentId,
612
+ amountCents: Math.round(Number((_orphan_get3 = orphan.get('amountCents')) != null ? _orphan_get3 : 0)),
613
+ chargeId: String((_orphan_get4 = orphan.get('chargeId')) != null ? _orphan_get4 : ''),
614
+ currency: String((_orphan_get5 = orphan.get('currency')) != null ? _orphan_get5 : 'usd'),
615
+ stripeCustomerId: String((_orphan_get6 = orphan.get('stripeCustomerId')) != null ? _orphan_get6 : '')
616
+ }, // A parked dispute is always a LOST one — the only dispute state that
617
+ // moves money — so the outcome lands on the record too, exactly as the
618
+ // dispute door would have stamped it.
619
+ kind === 'dispute' ? {
620
+ disputeId: causeId,
621
+ disputeStatus: 'lost'
622
+ } : {});
623
+ }
624
+ await orphanRef.delete();
625
+ }
626
+ /**
627
+ * A dispute has no metadata of ours to read — the event object is the DISPUTE,
628
+ * not the charge — so the discriminator costs one Stripe read.
629
+ *
630
+ * Only ever reached when the payment-intent join came up empty, so a normal
631
+ * marketplace dispute (purchase present) pays nothing for it, and a commerce
632
+ * or subscription dispute pays one GET before being correctly ignored.
633
+ * Disputes are rare enough for that to be the right trade against parking an
634
+ * orphan for every chargeback on the platform.
635
+ *
636
+ * A FAILED READ DOES NOT THROW, deliberately, and this is the one place the
637
+ * fix accepts a loss. Throwing would 500 the endpoint for a dispute we have
638
+ * not established is ours — the exact blast radius that ruled out "just throw"
639
+ * in the first place — and the route drops its idempotency claim on a throw.
640
+ * So a marketplace dispute landing inside the purchase window WHILE the charge
641
+ * read fails loses its orphan. It is logged, and it is two independent
642
+ * unlikely events deep.
643
+ */ async function recordDisputeOrphanIfMarketplace(firestore, object, paymentIntentId) {
644
+ var _ref, _ref1, _ref2, _ref3;
645
+ var _charge_body_metadata, _charge_body;
646
+ const chargeId = String((_ref = object == null ? void 0 : object.charge) != null ? _ref : '');
647
+ const stripeKey = process.env.STRIPE_SECRET_KEY;
648
+ if (!chargeId || !stripeKey) return false;
649
+ const charge = await stripeGet(`https://api.stripe.com/v1/charges/${chargeId}`, stripeKey);
650
+ if (!charge.ok) {
651
+ console.error('Could not classify a dispute with no matching purchase — orphan NOT parked (AGL-2148)', {
652
+ chargeId,
653
+ paymentIntentId,
654
+ status: charge.status
655
+ });
656
+ return false;
657
+ }
658
+ if (((_charge_body = charge.body) == null ? void 0 : (_charge_body_metadata = _charge_body.metadata) == null ? void 0 : _charge_body_metadata.type) !== 'marketplace-purchase') return false;
659
+ await recordRefundOrphan(firestore, {
660
+ kind: 'dispute',
661
+ id: String((_ref1 = object == null ? void 0 : object.id) != null ? _ref1 : ''),
662
+ paymentIntentId,
663
+ amountCents: Math.round(Number((_ref2 = object == null ? void 0 : object.amount) != null ? _ref2 : 0)),
664
+ chargeId,
665
+ currency: String((_ref3 = object == null ? void 0 : object.currency) != null ? _ref3 : 'usd'),
666
+ stripeCustomerId: ''
667
+ });
668
+ // Parked, so it IS marketplace's — the route must not also alert on it
669
+ // (AGL-2429). The three early exits above return false on purpose: two of
670
+ // them mean "could not classify", and a dispute nobody could classify is
671
+ // exactly what the route's unattributed alert exists to catch.
672
+ return true;
673
+ }
674
+ /**
675
+ * Marketplace-purchase section of the platform Stripe webhook (AGL-46/418):
676
+ * keyed by session id (idempotent on Stripe redelivery) — relocated
677
+ * verbatim from the console route; registered via
678
+ * registerMarketplaceConsoleApi. Install gating and the seller ledger read
679
+ * these purchase docs.
680
+ */ export const marketplaceBillingWebhookHandler = async ({ type, object, event })=>{
681
+ var _ref;
682
+ var _object_metadata;
683
+ // Connect readiness, kept fresh (AGL-1997) — the publisher twin of the
684
+ // commerce sync. The seller panel reads `stripeChargesEnabled` /
685
+ // `stripePayoutsEnabled` off this document, and before this nothing but the
686
+ // publisher reopening the connect route ever refreshed either. Same early
687
+ // return: `account.updated` shares nothing with the purchase sections below.
688
+ // `event.livemode`, not `object.livemode` (AGL-2471) — the Account object
689
+ // has no such field. Two of the three poisoned production linkages were
690
+ // publisherProfiles, and this is the path that heals them.
691
+ if (type === 'account.updated') {
692
+ await syncConnectAccountStatus('publisherProfiles', object, event == null ? void 0 : event.livemode);
693
+ return;
694
+ }
695
+ // A PAYOUT OR TRANSFER THAT NEVER LANDED.
696
+ //
697
+ // Placed beside `account.updated` because it is the same kind of event —
698
+ // account-level, nothing to do with the `metadata.type` order sections
699
+ // below — and returns for the same reason.
700
+ //
701
+ // `payout.failed` is the CONNECTED account's balance failing to reach its
702
+ // bank, so the account id is `event.account`: the Payout object's own
703
+ // `destination` names the bank, not the Connect account. `transfer.failed`
704
+ // is the platform's balance failing to reach the connected account, a
705
+ // platform event whose `destination` IS the account.
706
+ //
707
+ // Recorded and surfaced, never retried: Stripe runs its own retry schedule
708
+ // and a second transfer against an account that just refused one is how a
709
+ // duplicate lands.
710
+ if (type === 'payout.failed' || type === 'transfer.failed') {
711
+ var _ref1, _ref2, _ref3;
712
+ var _object_destination;
713
+ const failedAccountId = type === 'payout.failed' ? String((_ref1 = event == null ? void 0 : event.account) != null ? _ref1 : '') : String((_ref2 = (_ref3 = object == null ? void 0 : (_object_destination = object.destination) == null ? void 0 : _object_destination.id) != null ? _ref3 : object == null ? void 0 : object.destination) != null ? _ref2 : '');
714
+ await recordConnectPayoutFailure('publisherProfiles', {
715
+ kind: type === 'payout.failed' ? 'payout' : 'transfer',
716
+ object,
717
+ accountId: failedAccountId,
718
+ livemode: event == null ? void 0 : event.livemode
719
+ });
720
+ return;
721
+ }
722
+ // A later success retires the warning the card shows. The history in
723
+ // `connectPayoutFailures` is kept — "has this account failed before" is what
724
+ // that record exists to answer — but a stale warning on a resolved problem
725
+ // trains people to ignore the surface.
726
+ if (type === 'payout.paid') {
727
+ var _ref4;
728
+ await clearConnectPayoutFailure('publisherProfiles', String((_ref4 = event == null ? void 0 : event.account) != null ? _ref4 : ''));
729
+ return;
730
+ }
731
+ // Marketplace purchases (AGL-46): keyed by session id (idempotent on
732
+ // Stripe redelivery). Install gating and the seller ledger read these.
733
+ if (type === 'checkout.session.completed' && (object == null ? void 0 : (_object_metadata = object.metadata) == null ? void 0 : _object_metadata.type) === 'marketplace-purchase' && (object == null ? void 0 : object.payment_status) === 'paid') {
734
+ var _object_metadata1;
735
+ // Sellers are orgs (AGL-652) — the ledger records which ORG earned it.
736
+ const { listingId, buyerUid, buyerOrgId, sellerOrgId, feeCents, transferCents } = (_object_metadata1 = object.metadata) != null ? _object_metadata1 : {};
737
+ if (listingId && buyerUid && sellerOrgId) {
738
+ var _ref5, _ref6, _ref7, _ref8;
739
+ var _object_total_details;
740
+ // The remittance-correct split (AGL-1544), read ONCE and used by both
741
+ // the ledger and the GA hit (AGL-1639) — the two must not be able to
742
+ // describe the same sale differently.
743
+ //
744
+ // `amount_total` is the tax-inclusive GROSS the buyer paid. Out of it:
745
+ // `taxCents` is what the PLATFORM owes the state (collected under the
746
+ // marketplace-provider registration, never ours), and `sellerCents` is
747
+ // the fixed transfer the seller's Connect account received (their
748
+ // share of the pre-tax price). What is left is what Aglyn keeps.
749
+ const grossCents = Number((_ref5 = object == null ? void 0 : object.amount_total) != null ? _ref5 : 0);
750
+ const taxCents = Number((_ref6 = object == null ? void 0 : (_object_total_details = object.total_details) == null ? void 0 : _object_total_details.amount_tax) != null ? _ref6 : 0);
751
+ const sellerCents = Number(transferCents != null ? transferCents : 0);
752
+ const netCents = grossCents - taxCents - sellerCents;
753
+ // WHERE that tax is owed. Read from the session Stripe computed it
754
+ // from — see `buyerTaxJurisdiction`.
755
+ const jurisdiction = buyerTaxJurisdiction(object);
756
+ const purchaseRef = firebaseAdmin.app().firestore().collection('marketplacePurchases').doc(String(object.id));
757
+ // `createdAt` is stamped ONCE (AGL-2109). Merging the write below fixed
758
+ // the erasure but introduced its own drift: `serverTimestamp()` on a
759
+ // redelivery would move a three-day-old sale to today, which is the
760
+ // field the seller ledger and every revenue period read. One read, on a
761
+ // path that already makes several Stripe round trips, buys a date that
762
+ // means what it says.
763
+ const alreadyRecorded = (await purchaseRef.get()).exists;
764
+ await purchaseRef.set(_extends({
765
+ listingId,
766
+ buyerUid
767
+ }, buyerOrgId ? {
768
+ buyerOrgId: String(buyerOrgId)
769
+ } : {}, {
770
+ sellerOrgId,
771
+ // Gross − tax − transfer = the platform fee, which feeCents also
772
+ // records independently from the rate resolved at checkout.
773
+ amountCents: grossCents,
774
+ feeCents: Number(feeCents != null ? feeCents : 0),
775
+ taxCents,
776
+ transferCents: sellerCents,
777
+ // The refund trail (AGL-1546): `charge.refunded` carries the
778
+ // payment intent, not the session — without this id a refund
779
+ // could never find the purchase it revokes.
780
+ paymentIntentId: String((_ref7 = object == null ? void 0 : object.payment_intent) != null ? _ref7 : '')
781
+ }, alreadyRecorded ? {} : _extends({
782
+ createdAt: firebaseAdmin.firestore.FieldValue.serverTimestamp()
783
+ }, jurisdiction ? {
784
+ customerAddress: jurisdiction
785
+ } : {})), // MERGED, like every other write on this path (AGL-2109). This one
786
+ // was a full document REPLACE, and the document it replaces is not
787
+ // only the sale: it is the buyer's ENTITLEMENT (`hasLivePurchase`
788
+ // reads `refundedAt`), the refund trail, the dispute trail, and the
789
+ // transfer-reversal SETTLE MARKER (`reversedTransferCents`, which
790
+ // `reverseMarketplaceSellerShare` short-circuits on so a publisher is
791
+ // never debited twice).
792
+ //
793
+ // Stripe redelivers `checkout.session.completed` for up to three days
794
+ // after any 500, and this endpoint 500s on purpose — for every
795
+ // transient Stripe failure raised in this file and for any throw from
796
+ // a sibling plugin handler that runs after this one
797
+ // (`runBillingWebhookHandlers` awaits them in sequence and lets a
798
+ // throw propagate). So the redelivery is expected traffic, not an
799
+ // exotic race. Landing after a refund it erased `refundedAt` — the
800
+ // refunded buyer silently got their install back — and erased
801
+ // `reversedTransferCents`, re-opening a second reversal against the
802
+ // publisher's Connect account.
803
+ //
804
+ // Merging is safe in the other direction too: every field written
805
+ // here is derived from the session event, so a redelivery restamps
806
+ // identical values.
807
+ {
808
+ merge: true
809
+ });
810
+ // Marketplace sales are real revenue and belong in the same GA
811
+ // `purchase` stream as subscriptions (AGL-1561), separated by
812
+ // `item_category` so plugin revenue and subscription revenue can be
813
+ // read apart or together.
814
+ //
815
+ // After the ledger write, and fire-and-forget: the ledger is what
816
+ // grants the install entitlement, and an analytics failure must never
817
+ // throw here — the route deletes its idempotency claim on any throw,
818
+ // which would make Stripe redeliver a purchase that already landed.
819
+ //
820
+ // `transaction_id` is the checkout session id — the same key the
821
+ // ledger doc uses — so GA de-duplicates a redelivery exactly as
822
+ // Firestore does.
823
+ // WHAT COUNTS AS REVENUE ON A MARKETPLACE SALE (AGL-1639)
824
+ //
825
+ // Our NET — the platform fee — and not the gross the buyer paid.
826
+ // Decided once here rather than inferred from whichever Stripe field
827
+ // was nearest, because the three candidates are genuinely different
828
+ // numbers:
829
+ //
830
+ // gross incl. tax reconciles with nothing we own
831
+ // GMV ex-tax what the SELLERS earned, not what we did
832
+ // platform net our books, our MRR, our balance ← this one
833
+ //
834
+ // This is *our* GA property and every other number in it is ours;
835
+ // subscription `purchase` already reports what Aglyn was paid, and
836
+ // marketplace revenue has to mean the same thing or the combined
837
+ // total, ARPA and every revenue-based audience are nonsense. The two
838
+ // stay separable by `item_category`.
839
+ //
840
+ // Tax is excluded rather than folded in, and is deliberately NOT sent
841
+ // as GA4's `tax` param either: `value` is our fee, so a `tax` beside
842
+ // it would not be a component of it, and asserting in GA that Aglyn
843
+ // took this tax is exactly the question the publisher agreement's
844
+ // seller-of-record clause has open. The ledger doc above keeps the
845
+ // full split for anyone who needs it.
846
+ after(()=>{
847
+ var _ref, _ref1;
848
+ var _object_metadata;
849
+ return sendGa4Purchase({
850
+ transactionId: String(object.id),
851
+ value: netCents / 100,
852
+ currency: String((_ref = object == null ? void 0 : object.currency) != null ? _ref : 'usd'),
853
+ items: [
854
+ {
855
+ item_id: String(listingId),
856
+ // The listing id, not the display name: a listing's name is
857
+ // seller-authored free text and is not worth risking in a
858
+ // dimension when the id already identifies it.
859
+ item_name: String(listingId),
860
+ item_category: 'marketplace',
861
+ // GA expects the items to sum to `value`; one item, one price.
862
+ price: netCents / 100,
863
+ quantity: 1
864
+ }
865
+ ],
866
+ clientId: object == null ? void 0 : (_object_metadata = object.metadata) == null ? void 0 : _object_metadata.ga_client_id,
867
+ stripeCustomerId: String((_ref1 = object == null ? void 0 : object.customer) != null ? _ref1 : '') || String(buyerUid)
868
+ }).catch(()=>undefined);
869
+ });
870
+ // A refund or a lost dispute that arrived before this document existed
871
+ // is applied NOW (AGL-2148) — revocation, GA and the transfer reversal,
872
+ // by the same function the refund door uses. Last on this path, and
873
+ // awaited: it can throw on a transient Stripe failure, which is exactly
874
+ // the redelivery this branch is already idempotent under.
875
+ await drainMarketplaceRefundOrphan(firebaseAdmin.app().firestore(), purchaseRef, String((_ref8 = object == null ? void 0 : object.payment_intent) != null ? _ref8 : ''));
876
+ }
877
+ }
878
+ // Refund revocation (AGL-1546): a FULL refund un-buys the listing —
879
+ // the install gate treats a purchase with `refundedAt` as absent. Only
880
+ // `refunded: true` (the whole charge) revokes; a partial refund is a
881
+ // concession, not a revocation. Keyed by the payment intent stored at
882
+ // completion, and idempotent: a Stripe redelivery restamps the same
883
+ // values on the same doc. Requires the platform webhook endpoint to be
884
+ // subscribed to `charge.refunded` (AGL-1549).
885
+ if (type === 'charge.refunded' && (object == null ? void 0 : object.refunded) === true) {
886
+ var _ref9;
887
+ const paymentIntentId = String((_ref9 = object == null ? void 0 : object.payment_intent) != null ? _ref9 : '');
888
+ if (paymentIntentId) {
889
+ var _ref10, _ref11, _ref12, _ref13, _ref14;
890
+ var _object_metadata2;
891
+ const firestore = firebaseAdmin.app().firestore();
892
+ const purchases = await firestore.collection('marketplacePurchases').where('paymentIntentId', '==', paymentIntentId).limit(1).get();
893
+ // Everything a refund does to a purchase, and everything it needs to
894
+ // wait for one — assembled once so the two paths cannot describe the
895
+ // same refund differently.
896
+ const cause = {
897
+ kind: 'refund',
898
+ // `charge.refunded` carries the CHARGE, so the charge id is both the
899
+ // cause id and the chargeId — unchanged from AGL-1995.
900
+ id: String((_ref10 = object == null ? void 0 : object.id) != null ? _ref10 : ''),
901
+ paymentIntentId,
902
+ amountCents: Math.round(Number((_ref11 = object == null ? void 0 : object.amount_refunded) != null ? _ref11 : 0)),
903
+ chargeId: String((_ref12 = object == null ? void 0 : object.id) != null ? _ref12 : ''),
904
+ currency: String((_ref13 = object == null ? void 0 : object.currency) != null ? _ref13 : 'usd'),
905
+ stripeCustomerId: String((_ref14 = object == null ? void 0 : object.customer) != null ? _ref14 : '')
906
+ };
907
+ if (!purchases.empty) {
908
+ // The publisher's share comes back (AGL-1995), the entitlement is
909
+ // revoked (AGL-1546) and GA nets the sale out (AGL-1850) — see
910
+ // `applyMarketplaceRefundOutcome`. FULL refunds only, matching the
911
+ // revocation and GA gates: the branch is already inside
912
+ // `object?.refunded === true`. The seller's share of a PARTIAL refund
913
+ // now comes back too, without any of the other three effects — see
914
+ // `reverseMarketplacePartialRefundShare` below. There is no
915
+ // marketplace refund UI, so every refund is issued from the Stripe
916
+ // Dashboard and this webhook is the only code that sees one.
917
+ await applyMarketplaceRefundOutcome(purchases.docs[0].ref, cause);
918
+ } else if ((object == null ? void 0 : (_object_metadata2 = object.metadata) == null ? void 0 : _object_metadata2.type) === 'marketplace-purchase') {
919
+ // THE ELSE THAT DID NOT EXIST (AGL-2148). The purchase document is
920
+ // written by `checkout.session.completed`, that delivery retries, and
921
+ // a refund issued inside the window found nothing and was DROPPED.
922
+ //
923
+ // Gated on the PaymentIntent metadata stamped at marketplace checkout
924
+ // (checkout.ts) and copied by Stripe onto the charge, so the
925
+ // storefront and subscription refunds that share this endpoint park
926
+ // nothing. A charge predating that stamp is not parked either — its
927
+ // purchase document has long existed, so the join above finds it.
928
+ await recordRefundOrphan(firestore, cause);
929
+ }
930
+ }
931
+ }
932
+ // A PARTIAL refund pulls the publisher's share back too (AGL-2299) — and
933
+ // nothing else. The branch above is gated on `refunded === true`, which is
934
+ // "the whole charge is gone"; every smaller refund fell through it and the
935
+ // platform paid the entire concession out of a 20% cut. The entitlement is
936
+ // untouched on purpose: a partial refund is a concession, not an un-buy.
937
+ //
938
+ // Same marketplace discriminator the orphan store uses, for the same
939
+ // reason: `charge.refunded` arrives here for storefront orders and
940
+ // subscription charges too, and this must not read a transfer belonging to
941
+ // one of those.
942
+ //
943
+ // The discriminator is required only on the ORPHAN side, exactly as the
944
+ // full-refund branch requires it: when the payment-intent join FINDS a
945
+ // marketplace purchase, that join is already the proof, and demanding the
946
+ // metadata as well would skip every charge predating the AGL-2148 stamp.
947
+ if (type === 'charge.refunded' && (object == null ? void 0 : object.refunded) !== true && Math.round(Number((_ref = object == null ? void 0 : object.amount_refunded) != null ? _ref : 0)) > 0) {
948
+ var _ref15;
949
+ const paymentIntentId = String((_ref15 = object == null ? void 0 : object.payment_intent) != null ? _ref15 : '');
950
+ if (paymentIntentId) {
951
+ var _object_metadata3;
952
+ const firestore = firebaseAdmin.app().firestore();
953
+ const purchases = await firestore.collection('marketplacePurchases').where('paymentIntentId', '==', paymentIntentId).limit(1).get();
954
+ if (!purchases.empty) {
955
+ var _ref16;
956
+ await reverseMarketplacePartialRefundShare(purchases.docs[0].ref, String((_ref16 = object == null ? void 0 : object.id) != null ? _ref16 : ''));
957
+ } else if ((object == null ? void 0 : (_object_metadata3 = object.metadata) == null ? void 0 : _object_metadata3.type) === 'marketplace-purchase') {
958
+ var _ref17, _ref18, _ref19, _ref20, _ref21;
959
+ // The AGL-2148 window, for partials. Parked with the charge on it so
960
+ // the drain can recompute the target from Stripe rather than trusting
961
+ // a number this event carried; the amount rides along for forensics.
962
+ await recordRefundOrphan(firestore, {
963
+ kind: 'partial-refund',
964
+ id: String((_ref17 = object == null ? void 0 : object.id) != null ? _ref17 : ''),
965
+ paymentIntentId,
966
+ amountCents: Math.round(Number((_ref18 = object == null ? void 0 : object.amount_refunded) != null ? _ref18 : 0)),
967
+ chargeId: String((_ref19 = object == null ? void 0 : object.id) != null ? _ref19 : ''),
968
+ currency: String((_ref20 = object == null ? void 0 : object.currency) != null ? _ref20 : 'usd'),
969
+ stripeCustomerId: String((_ref21 = object == null ? void 0 : object.customer) != null ? _ref21 : '')
970
+ });
971
+ }
972
+ }
973
+ }
974
+ // Chargebacks (AGL-1554): the AGL-1546 refund arriving by the bank's
975
+ // door. `charge.dispute.*` for tenant storefront orders and platform
976
+ // subscriptions arrives on this endpoint too — the payment-intent join
977
+ // below simply finds no purchase for those and this section stays out of
978
+ // their way, exactly as the refund branch does.
979
+ //
980
+ // A dispute is NOT a refund, and the states matter (the AGL-1554
981
+ // analysis): `created` can still be WON, and nothing un-revokes, so it
982
+ // only flags the purchase for staff visibility. Money moves exclusively
983
+ // on `closed` + `status: 'lost'`: the buyer's entitlement goes the way
984
+ // the money went (`refundedAt`, the field the install gate reads as
985
+ // absent-purchase — final outcomes only), GA nets the AGL-1639 purchase
986
+ // out in the AGL-1850 accounting (platform net, guarded by the same
987
+ // `refundedAt` read the refund branch uses), and the seller's share
988
+ // comes back by the AGL-1794 policy. `won` and `warning_closed` record
989
+ // the outcome and move nothing.
990
+ if (type === 'charge.dispute.created' || type === 'charge.dispute.closed') {
991
+ var _ref22, _ref23;
992
+ const disputeId = String((_ref22 = object == null ? void 0 : object.id) != null ? _ref22 : '');
993
+ const paymentIntentId = String((_ref23 = object == null ? void 0 : object.payment_intent) != null ? _ref23 : '');
994
+ // Reported to the route so it can tell "marketplace handled it" from
995
+ // "nothing handled it" (AGL-2429). Left false by the guard below on
996
+ // purpose: a dispute carrying no payment intent cannot be joined to a
997
+ // purchase, which is a fault, not a routine miss.
998
+ let claimed = false;
999
+ if (disputeId && paymentIntentId) {
1000
+ var _ref24;
1001
+ const firestore = firebaseAdmin.app().firestore();
1002
+ const purchases = await firestore.collection('marketplacePurchases').where('paymentIntentId', '==', paymentIntentId).limit(1).get();
1003
+ if (!purchases.empty) {
1004
+ var _ref25;
1005
+ claimed = true;
1006
+ const purchase = purchases.docs[0];
1007
+ const status = String((_ref25 = object == null ? void 0 : object.status) != null ? _ref25 : '');
1008
+ if (type === 'charge.dispute.created') {
1009
+ await purchase.ref.set({
1010
+ disputeId,
1011
+ disputeStatus: status || 'needs_response',
1012
+ disputeOpenedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp()
1013
+ }, {
1014
+ merge: true
1015
+ });
1016
+ } else if (status === 'lost') {
1017
+ var _ref26, _ref27, _ref28;
1018
+ // Revoke, report, pull the seller's share back — the same three
1019
+ // effects the refund door applies, through the same function
1020
+ // (AGL-2148), so the two doors and the orphan drain cannot drift.
1021
+ // The dispute outcome is stamped in the same write.
1022
+ await applyMarketplaceRefundOutcome(purchase.ref, {
1023
+ kind: 'dispute',
1024
+ id: disputeId,
1025
+ paymentIntentId,
1026
+ amountCents: Math.round(Number((_ref26 = object == null ? void 0 : object.amount) != null ? _ref26 : 0)),
1027
+ chargeId: String((_ref27 = object == null ? void 0 : object.charge) != null ? _ref27 : ''),
1028
+ currency: String((_ref28 = object == null ? void 0 : object.currency) != null ? _ref28 : 'usd'),
1029
+ // A dispute event carries no Stripe customer; the GA hit falls
1030
+ // back to the buyer uid on the purchase, as it always has.
1031
+ stripeCustomerId: ''
1032
+ }, {
1033
+ disputeId,
1034
+ disputeStatus: status
1035
+ });
1036
+ } else {
1037
+ // `won` or `warning_closed`: the money stayed, the entitlement
1038
+ // stays, and the outcome lands on the record for whoever flagged
1039
+ // it at `created`.
1040
+ await purchase.ref.set({
1041
+ disputeId,
1042
+ disputeStatus: status
1043
+ }, {
1044
+ merge: true
1045
+ });
1046
+ }
1047
+ } else if (type === 'charge.dispute.closed' && String((_ref24 = object == null ? void 0 : object.status) != null ? _ref24 : '') === 'lost') {
1048
+ // The refund branch's window, by the bank's door (AGL-2148): only a
1049
+ // LOST closed dispute moves money, so only that state is worth
1050
+ // parking. A `created` landing in the same window loses nothing but
1051
+ // the staff flag — the `closed` that follows days later always finds
1052
+ // the purchase document.
1053
+ claimed = await recordDisputeOrphanIfMarketplace(firestore, object, paymentIntentId);
1054
+ }
1055
+ }
1056
+ return {
1057
+ claimed
1058
+ };
1059
+ }
1060
+ };
1061
+
1062
+ //# sourceMappingURL=billing-webhook.js.map