@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 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/billing-webhook.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\n// `after()`, never a bare `void promise` (AGL-2327). This handler runs inside\n// the console's `/api/billing/webhook` invocation, which AGL-1133 measured on\n// production is frozen the moment the response is sent — so fire-and-forget\n// work scheduled here simply does not run. The console's own route was\n// migrated for that reason (AGL-2346) and this one, in the same invocation,\n// was not: marketplace revenue and refunds were reporting to nothing, which is\n// the surviving half of \"four server events ship to nothing\" now that the\n// Measurement Protocol credentials have landed (2026-08-17).\nimport { after } from 'next/server'\n\nimport type { BillingWebhookHandler } from '@aglyn/aglyn/server'\nimport {\n firebaseAdmin,\n sendGa4Purchase,\n sendGa4Refund,\n clearConnectPayoutFailure,\n recordConnectPayoutFailure,\n syncConnectAccountStatus,\n} from '@aglyn/tenant-data-admin'\n\n/** Stripe failures a redelivery can actually fix. */\nfunction isTransientStripeStatus(status: number): boolean {\n return status === 429 || status >= 500\n}\n\n/**\n * THE BUYER'S TAX JURISDICTION, from the session Stripe computed the tax on.\n *\n * Aglyn is a marketplace facilitator, so the tax on a facilitated sale is\n * Aglyn's to collect and remit — and a return reports it BY STATE. Checkout\n * already collects everything needed for that (`billing_address_collection:\n * 'required'`, `automatic_tax[enabled]`), and Stripe states the address it\n * computed from back on `customer_details.address`. Nothing read it, so every\n * facilitated sale reached `tx-return.ts` with no jurisdiction and the whole\n * of the marketplace tax could be stated only as a platform total attributable\n * to no state.\n *\n * COUNTRY AND STATE ONLY, and deliberately narrower than the storefront twin.\n * `StorefrontTaxRow` keeps `city` and `postalCode` beside them; the return\n * reads neither, on either collection — it keys every jurisdiction bucket\n * `COUNTRY-STATE`. A purchase document already names `buyerUid`, so anything\n * stored here is personal data by association, and a street-grained address a\n * filing cannot use is data held for no purpose. The two fields kept are the\n * two the return reads.\n *\n * Null when the session states no country. An unstated jurisdiction is COUNTED\n * by the return (`rowsMissingJurisdiction`) and never inferred: a jurisdiction\n * reconstructed after the fact is a guess handed to a tax authority as a fact.\n */\nfunction buyerTaxJurisdiction(\n object: any,\n): { country: string; state: string | null } | null {\n const address = object?.customer_details?.address\n const country =\n typeof address?.country === 'string' ? address.country.trim() : ''\n if (!country) return null\n const state = typeof address?.state === 'string' ? address.state.trim() : ''\n return { country, state: state || null }\n}\n\n/** One authorized GET against the Stripe API, body parsed either way. */\nasync function stripeGet(\n url: string,\n stripeKey: string,\n): Promise<{ ok: boolean; status: number; body: any }> {\n const response = await fetch(url, {\n headers: { Authorization: `Bearer ${stripeKey}` },\n })\n const body = await response.json().catch(() => null)\n return { ok: response.ok, status: response.status, body }\n}\n\n/**\n * The seller's share of a LOST marketplace dispute — or of a REFUND\n * (AGL-1995) — pulled back from the connected account (AGL-1554, by the\n * AGL-1794 policy).\n *\n * Marketplace sales are destination charges on the platform account, so the\n * disputed funds and the dispute fee are debited from the PLATFORM's balance\n * while the seller keeps the `transfer_data[amount]` they were paid — a\n * seller paid in full for a sale the buyer's bank took back, with Aglyn out\n * the principal. AGL-1794 decided this allocation for commerce orders and it\n * carries over unchanged: the seller eats their share, by a\n * `transfers/{id}/reversals` call for the portion that was actually\n * transferred; the platform still eats Stripe's dispute fee, deliberately.\n *\n * PROPORTIONAL, NEVER MORE: `causeAmountCents × transfer.amount ÷\n * charge.amount`, FLOORED, then capped at what the transfer has left\n * (`amount − amount_reversed`) — a transfer partially reversed by a\n * dashboard refund cannot be pulled below its own remainder from here. The\n * reversal CAN drive the connected account negative, which Stripe recovers\n * from future payouts.\n *\n * IDEMPOTENT BY THE PURCHASE DOCUMENT, with the transfer itself as the crash\n * window's backstop — the reverseSellerShare scheme from the commerce\n * webhook, applied to `marketplacePurchases/{sessionId}`:\n * `reversedTransferCents` present (0 included) means this step settled, and\n * a redelivery returns before any Stripe call; a delivery killed between the\n * POST landing and the record writing is healed by ADOPTING the reversal the\n * transfer already carries (`metadata.disputeId` / `metadata.refundId`);\n * and the POST's\n * `Idempotency-Key` hands a racing delivery Stripe's stored response rather\n * than a second reversal.\n *\n * DEFINITIVE failures settle `reversedTransferCents: 0` and DO NOT throw —\n * no redelivery fixes a charge with no transfer, and a throw propagates into\n * a 500 Stripe redelivers forever. TRANSIENT failures (429/5xx/network)\n * throw on purpose: the marker is still unset, so the redelivery IS the\n * retry. A missing STRIPE_SECRET_KEY neither settles nor throws: the\n * revocation half already landed, and the redelivery retries the reversal\n * once a key is present.\n *\n * WHY THE SAME FUNCTION SERVES BOTH DOORS (AGL-1995).\n *\n * The refund door reached Sep 1 never calling this at all: `charge.refunded`\n * revoked the entitlement and sent the GA hit while the publisher kept their\n * 80% and Aglyn absorbed the whole gross. A second, parallel reversal\n * function is exactly the shape AGL-1994 was filed about — the marketplace\n * and commerce Connect twins drifting because a fix landed on one — so the\n * cause is a parameter here rather than a copy.\n *\n * WHAT DOES *NOT* CARRY OVER FROM COMMERCE. `refund.ts` pairs\n * `reverse_transfer` with `refund_application_fee: 'true'`, and the AGL-1794\n * note warns that flag is a one-parameter policy switch. **It has no meaning\n * on this path.** Marketplace checkout deliberately pays the seller with a\n * FIXED `payment_intent_data[transfer_data][amount]` and NOT\n * `application_fee_amount` (checkout.ts:177-181 — the fee form would transfer\n * `amount_total − fee`, and `amount_total` includes the tax the platform\n * owes). There is therefore no application-fee object on a marketplace charge\n * to refund, and `transfer.amount` is the seller's share, NOT `charge.amount`\n * as it is on the commerce side. The proportional maths below already models\n * that correctly, which is why the refund door can reuse it unchanged.\n *\n * The resulting allocation on a full refund is the same one commerce reaches\n * by the opposite parameter: the buyer is made whole, the seller returns\n * their share, and Aglyn gives back its own commission and the tax it\n * collected. A LOST DISPUTE still differs on purpose — the platform keeps its\n * cut there, per AGL-1794 — and that difference survives because the two call\n * sites pass different causes, not because they run different code.\n *\n * ONE PULL-BACK PER PURCHASE. Both doors share the `reversedTransferCents`\n * settle marker, so a refund following a lost dispute (or the reverse) finds\n * the share already recovered and returns before any Stripe call. The seller\n * cannot be debited twice for one sale.\n */\ninterface SellerShareReversalCause {\n /** Which door — decides the metadata key and the idempotency key. */\n kind: 'dispute' | 'refund'\n /** Stripe id of the dispute or the refunded charge. */\n id: string\n /** Cents going back to the buyer. */\n amountCents: number\n /** The charge whose transfer is being pulled back. */\n chargeId: string\n}\n\nasync function reverseMarketplaceSellerShare(\n purchaseRef: FirebaseFirestore.DocumentReference,\n cause: SellerShareReversalCause,\n): Promise<void> {\n const causeId = String(cause?.id ?? '')\n if (!causeId) return\n // Kept as `disputeId` on the wire for `kind: 'dispute'` so reversals\n // stamped before AGL-1995 are still found and ADOPTED by the crash-window\n // backstop below. A refund uses its own key for the same reason.\n const metadataKey = cause.kind === 'dispute' ? 'disputeId' : 'refundId'\n const snapshot = await purchaseRef.get()\n if (!snapshot.exists) return\n // The settle marker — see the doc comment. 0 counts.\n if (snapshot.get('reversedTransferCents') != null) return\n const causeAmountCents = Math.round(Number(cause?.amountCents ?? 0))\n if (!(causeAmountCents > 0)) return\n const stripeKey = process.env.STRIPE_SECRET_KEY\n if (!stripeKey) {\n console.error(\n 'Marketplace transfer reversal skipped: STRIPE_SECRET_KEY is not set (AGL-1554)',\n )\n return\n }\n\n /**\n * Settles the step exactly once, whatever it found.\n *\n * A settle of ZERO is not the same event as a settle of the full share, and\n * until AGL-2140 the document could not tell them apart (AGL-2140). Six\n * paths below abandon the reversal for a DEFINITIVE reason — no charge id,\n * Stripe refusing the charge read, no transfer on the charge, Stripe\n * refusing the transfer read, nothing left to reverse, Stripe refusing the\n * reversal itself — and each wrote `reversedTransferCents: 0`, which the\n * short-circuit at the top of this function then honours forever. The only\n * other trace was a `console.error`.\n *\n * A Stripe 4xx is one of those definitive reasons: `balance_insufficient` on\n * a connected account is a **400**, not a 5xx, so it does not throw and does\n * not redeliver. The publisher's share was therefore forfeited permanently,\n * silently, and with nothing queryable to find it by afterwards.\n *\n * Settling stays — re-running these paths would eventually double-debit a\n * seller, which is worse. What changes is that an abandoned reversal now\n * says so ON THE DOCUMENT: `reversalFailedAt`, a machine-readable\n * `reversalFailedReason`, and `reversalOwedCents` when the amount is known.\n * `where('reversalFailedAt', '!=', null)` is then the recovery queue.\n */\n const settle = async (\n reversedTransferCents: number,\n transferReversalId: string | null,\n /** Why nothing (or less than the cause) was reversed. Omit on success. */\n failure?: { reason: string; owedCents?: number },\n ): Promise<void> => {\n const fresh = await purchaseRef.get()\n if (!fresh.exists || fresh.get('reversedTransferCents') != null) return\n if (failure) {\n console.error('Marketplace seller share NOT reversed — recorded for recovery', {\n purchaseId: purchaseRef.id,\n kind: cause.kind,\n causeId,\n reason: failure.reason,\n owedCents: failure.owedCents ?? null,\n })\n }\n await purchaseRef.set(\n {\n reversedTransferCents,\n ...(transferReversalId ? { transferReversalId } : {}),\n ...(failure\n ? {\n reversalFailedAt:\n firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n reversalFailedReason: failure.reason,\n reversalFailedCause: cause.kind,\n ...(failure.owedCents != null\n ? { reversalOwedCents: failure.owedCents }\n : {}),\n }\n : {}),\n },\n { merge: true },\n )\n }\n\n const chargeId = String(cause?.chargeId ?? '')\n if (!chargeId) {\n await settle(0, null, { reason: 'no-charge-on-cause' })\n return\n }\n const charge = await stripeGet(\n `https://api.stripe.com/v1/charges/${chargeId}`,\n stripeKey,\n )\n if (!charge.ok) {\n if (isTransientStripeStatus(charge.status)) {\n throw new Error(\n `Stripe charge read failed (${charge.status}) for marketplace ${cause.kind} ${causeId}`,\n )\n }\n console.error('Stripe refused the charge read', charge.body?.error)\n await settle(0, null, { reason: 'charge-read-refused' })\n return\n }\n const transferId = String(charge.body?.transfer ?? '')\n const chargeAmountCents = Math.round(Number(charge.body?.amount ?? 0))\n if (!transferId || !(chargeAmountCents > 0)) {\n await settle(0, null, { reason: 'no-transfer-on-charge' })\n return\n }\n const transfer = await stripeGet(\n `https://api.stripe.com/v1/transfers/${transferId}`,\n stripeKey,\n )\n if (!transfer.ok) {\n if (isTransientStripeStatus(transfer.status)) {\n throw new Error(\n `Stripe transfer read failed (${transfer.status}) for marketplace ${cause.kind} ${causeId}`,\n )\n }\n console.error('Stripe refused the transfer read', transfer.body?.error)\n await settle(0, null, { reason: 'transfer-read-refused' })\n return\n }\n // The crash window's backstop: the POST landed on a previous delivery and\n // the record did not. Adopt what exists rather than creating a second one.\n const existing = ((transfer.body?.reversals?.data ?? []) as any[]).find(\n (item) => String(item?.metadata?.[metadataKey] ?? '') === causeId,\n )\n if (existing) {\n await settle(\n Math.round(Number(existing.amount ?? 0)),\n String(existing.id ?? ''),\n )\n return\n }\n const transferCents = Math.round(Number(transfer.body?.amount ?? 0))\n const alreadyReversedCents = Math.round(\n Number(transfer.body?.amount_reversed ?? 0),\n )\n const remainingCents = Math.max(0, transferCents - alreadyReversedCents)\n const shareCents = Math.min(\n Math.floor((causeAmountCents * transferCents) / chargeAmountCents),\n remainingCents,\n )\n if (!(shareCents > 0)) {\n // Not necessarily a fault — an earlier cause may already have pulled the\n // whole transfer back — so `owedCents` is deliberately 0 rather than\n // absent: nothing is owed, and the row says so instead of leaving the\n // amount unknown.\n await settle(0, null, { reason: 'transfer-fully-reversed', owedCents: 0 })\n return\n }\n const params = new URLSearchParams({\n amount: String(shareCents),\n [`metadata[${metadataKey}]`]: causeId,\n 'metadata[purchaseId]': purchaseRef.id,\n })\n const response = await fetch(\n `https://api.stripe.com/v1/transfers/${transferId}/reversals`,\n {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${stripeKey}`,\n 'Content-Type': 'application/x-www-form-urlencoded',\n 'Idempotency-Key': `${cause.kind}-reversal-${causeId}`,\n },\n body: params.toString(),\n },\n )\n const reversal = await response.json().catch(() => null)\n if (!response.ok) {\n if (isTransientStripeStatus(response.status)) {\n throw new Error(\n `Stripe transfer reversal failed (${response.status}) for marketplace ${cause.kind} ${causeId}`,\n )\n }\n // THE ONE THAT COSTS REAL MONEY. A definitive 4xx here — including a\n // `balance_insufficient` on the publisher's account, which is a 400 — used\n // to forfeit the whole share with only a log line.\n console.error('Stripe refused the marketplace transfer reversal', reversal?.error)\n await settle(0, null, {\n reason: 'reversal-refused',\n owedCents: shareCents,\n })\n return\n }\n await settle(\n Math.round(Number(reversal?.amount ?? shareCents)),\n String(reversal?.id ?? ''),\n )\n}\n\n/**\n * THE PUBLISHER'S SHARE OF A *PARTIAL* REFUND, PULLED BACK (AGL-2299).\n *\n * WHAT WAS WRONG. `charge.refunded` fires on EVERY refund; `object.refunded`\n * is true only once the whole charge is gone. Everything above this function\n * is gated on that flag, so a partial refund did nothing at all — no\n * reversal, no record, not even a log line — and the note beside the branch\n * said so deliberately: \"a partial refund is a concession the ledger's split\n * cannot decompose\".\n *\n * That premise is false, and it is expensive. `reverseMarketplaceSellerShare`\n * decomposes exactly this, proportionally, and has since AGL-1554. What the\n * old branch actually did was hand the entire concession to Aglyn: on a $100\n * sale at a 20% take rate the platform holds $20 plus the tax it owes the\n * state, and a $50 goodwill refund is paid out of the PLATFORM's balance —\n * the publisher's $80 already left on the destination transfer. Aglyn is\n * $30 down and the publisher is untouched. Any partial refund larger than the\n * take rate loses money, which is most of them.\n *\n * The publisher agreement §8.4 is not ambiguous about this: *\"You are\n * responsible for refunds, chargebacks, and disputes on your sales. Aglyn may\n * reverse or withhold amounts corresponding to refunded, disputed, or\n * fraudulent transactions.\"* A partial refund is a refunded amount.\n *\n * THE ENTITLEMENT IS NOT TOUCHED, and that part of the old comment stands: a\n * partial refund is a concession, not an un-buy. No `refundedAt`, so\n * `hasLivePurchase` still says the buyer owns it, and no GA4 `refund` — that\n * event nets the WHOLE transaction out of the platform-net accounting\n * (AGL-1850), which a partial refund did not do. Only the money moves.\n *\n * WHY THIS IS A SECOND FUNCTION RATHER THAN A FLAG ON THE FIRST.\n * `reverseMarketplaceSellerShare` is ONE-SHOT by design — `reversedTransferCents`\n * present means \"this purchase's pull-back has settled\", and that marker is\n * what stops a refund following a lost dispute from debiting a publisher\n * twice. Partial refunds are the opposite shape: there can be several, and\n * each must add to what came back. Teaching one function both disciplines is\n * how the settle marker stops meaning anything.\n *\n * IDEMPOTENT WITHOUT A MARKER, because Stripe already keeps the ledger.\n * The target is derived from the charge's CUMULATIVE `amount_refunded`, and\n * what has already come back is `transfer.amount_reversed` — Stripe's own\n * number, not ours:\n *\n * target = floor(amount_refunded × transfer.amount ÷ charge.amount)\n * toReverse = clamp(target, 0, transfer.amount) − transfer.amount_reversed\n *\n * A redelivery recomputes the same target, finds `amount_reversed` already\n * there, and reverses nothing. Three partial refunds converge on the right\n * total whatever order they arrive in. And it composes with the one-shot path\n * in both directions: a later FULL refund computes\n * `min(transfer.amount, remaining)` and takes only the remainder, while a\n * dispute takes its own proportional slice of what is left.\n *\n * FAILURES DO NOT SETTLE ANYTHING. Transient (429/5xx/network) throws so the\n * redelivery is the retry, exactly as the one-shot path does. A definitive\n * 4xx — `balance_insufficient` on a publisher's account is a 400 — records\n * itself in AGL-2140's existing recovery queue (`reversalFailedAt`,\n * `reversalFailedReason`, `reversalOwedCents`) and returns 200 so the endpoint\n * does not redeliver forever. Nothing is permanently forfeited by that: the\n * next partial refund on the same charge recomputes the target from scratch\n * and picks the shortfall back up.\n */\nasync function reverseMarketplacePartialRefundShare(\n purchaseRef: FirebaseFirestore.DocumentReference,\n chargeId: string,\n): Promise<void> {\n if (!chargeId) return\n const snapshot = await purchaseRef.get()\n if (!snapshot.exists) return\n // The one-shot path already settled this purchase — a lost dispute, or a\n // full refund that landed first. Its marker means the pull-back is closed.\n if (snapshot.get('reversedTransferCents') != null) return\n const stripeKey = process.env.STRIPE_SECRET_KEY\n if (!stripeKey) {\n console.error(\n 'Marketplace partial-refund reversal skipped: STRIPE_SECRET_KEY is not set (AGL-2299)',\n )\n return\n }\n // The charge is RE-READ rather than taken from the event, for the same\n // reason the one-shot path re-reads it: the event is a snapshot, and the\n // number that decides this reversal is the charge's CUMULATIVE\n // `amount_refunded`. A redelivery days later, or a drain of a parked\n // partial, must converge on what is true now — not on what was true when\n // the event was minted. It also means an API version that trims `transfer`\n // out of the webhook payload cannot silently turn this into a no-op.\n const charge = await stripeGet(\n `https://api.stripe.com/v1/charges/${chargeId}`,\n stripeKey,\n )\n if (!charge.ok) {\n if (isTransientStripeStatus(charge.status)) {\n throw new Error(\n `Stripe charge read failed (${charge.status}) for marketplace partial refund on ${chargeId}`,\n )\n }\n console.error('Stripe refused the charge read', charge.body?.error)\n return\n }\n const chargeAmountCents = Math.round(Number(charge.body?.amount ?? 0))\n const refundedCents = Math.round(Number(charge.body?.amount_refunded ?? 0))\n if (!(chargeAmountCents > 0) || !(refundedCents > 0)) return\n const transferId = String(charge.body?.transfer ?? '')\n if (!transferId) {\n // No destination transfer means no seller share to pull back — a\n // marketplace charge always has one, so this is a charge we do not own.\n return\n }\n const transfer = await stripeGet(\n `https://api.stripe.com/v1/transfers/${transferId}`,\n stripeKey,\n )\n if (!transfer.ok) {\n if (isTransientStripeStatus(transfer.status)) {\n throw new Error(\n `Stripe transfer read failed (${transfer.status}) for marketplace partial refund on ${chargeId}`,\n )\n }\n console.error('Stripe refused the transfer read', transfer.body?.error)\n await purchaseRef.set(\n {\n partialRefundedCents: refundedCents,\n reversalFailedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n reversalFailedReason: 'transfer-read-refused',\n reversalFailedCause: 'partial-refund',\n },\n { merge: true },\n )\n return\n }\n const transferCents = Math.round(Number(transfer.body?.amount ?? 0))\n const alreadyReversedCents = Math.round(\n Number(transfer.body?.amount_reversed ?? 0),\n )\n const targetCents = Math.min(\n transferCents,\n Math.floor((refundedCents * transferCents) / chargeAmountCents),\n )\n const toReverseCents = targetCents - alreadyReversedCents\n if (!(toReverseCents > 0)) {\n // Already square — a redelivery, or an earlier cause that took more than\n // this refund's proportional slice. Record what the buyer has had back so\n // the seller panel and the ledger agree with Stripe.\n await purchaseRef.set(\n {\n partialRefundedCents: refundedCents,\n partialReversedTransferCents: alreadyReversedCents,\n },\n { merge: true },\n )\n return\n }\n const response = await fetch(\n `https://api.stripe.com/v1/transfers/${transferId}/reversals`,\n {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${stripeKey}`,\n 'Content-Type': 'application/x-www-form-urlencoded',\n // Derived from the charge AND the cumulative target, so a redelivery\n // of the same event replays Stripe's stored response while a genuinely\n // larger second refund gets its own call. Belt and braces: the\n // `amount_reversed` read above already turns a redelivery into a\n // no-op, and this closes the window where the POST landed and our\n // record did not.\n 'Idempotency-Key': `partial-reversal-${chargeId}-${targetCents}`,\n },\n body: new URLSearchParams({\n amount: String(toReverseCents),\n 'metadata[partialRefundOfCharge]': chargeId,\n 'metadata[purchaseId]': purchaseRef.id,\n }).toString(),\n },\n )\n const reversal = await response.json().catch(() => null)\n if (!response.ok) {\n if (isTransientStripeStatus(response.status)) {\n throw new Error(\n `Stripe partial transfer reversal failed (${response.status}) on ${chargeId}`,\n )\n }\n console.error(\n 'Marketplace partial-refund seller share NOT reversed — recorded for recovery (AGL-2299)',\n { purchaseId: purchaseRef.id, chargeId, owedCents: toReverseCents },\n )\n await purchaseRef.set(\n {\n partialRefundedCents: refundedCents,\n partialReversedTransferCents: alreadyReversedCents,\n reversalFailedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n reversalFailedReason: 'reversal-refused',\n reversalFailedCause: 'partial-refund',\n reversalOwedCents: toReverseCents,\n },\n { merge: true },\n )\n return\n }\n await purchaseRef.set(\n {\n partialRefundedCents: refundedCents,\n partialReversedTransferCents:\n alreadyReversedCents + Math.round(Number(reversal?.amount ?? toReverseCents)),\n ...(reversal?.id ? { partialTransferReversalId: String(reversal.id) } : {}),\n },\n { merge: true },\n )\n}\n\n/**\n * WHERE A REFUND THAT ARRIVED TOO EARLY WAITS (AGL-2148).\n *\n * THE WINDOW IS REAL, not exotic. `checkout.session.completed` is what writes\n * `marketplacePurchases/{sessionId}`, and that delivery RETRIES: this endpoint\n * 500s on every transient Stripe failure raised in this file and on any throw\n * from a sibling plugin handler, and Stripe then redelivers for up to three\n * days. A dashboard refund issued inside that window joins on the payment\n * intent, finds nothing, and — before this — the branch simply ended. No\n * `else`. The buyer got their money, the publisher kept their 80%, Aglyn ate\n * the gross, and `hasLivePurchase` saw no `refundedAt`, so the refunded buyer\n * kept the install forever.\n *\n * WHY NOT SIMPLY THROW so Stripe redelivers the refund. Because\n * `charge.refunded` and `charge.dispute.*` arrive on this same endpoint for\n * storefront orders and subscription charges, whose payment intents will never\n * match a marketplace purchase. Throwing on \"not found\" would 500 the webhook\n * for every non-marketplace refund, and the route drops its idempotency claim\n * on a throw — so the commerce and bookings handlers' non-idempotent effects\n * would re-run. The containment would cost more than the defect.\n *\n * SO THE EVENT IS PARKED, and the session landing drains it. Keyed by PAYMENT\n * INTENT, which is the only id the refund door, the dispute door and the\n * session landing all carry: the drain is one `get()` on a known document id,\n * with no query and no composite index.\n *\n * NOTHING IS PARKED SPECULATIVELY. Both doors gate the write on a\n * marketplace discriminator (`metadata.type`, stamped on the PaymentIntent at\n * checkout — see checkout.ts), so a commerce refund writes no orphan at all.\n * That is why this store needs no TTL and no sweeper: a document here means a\n * marketplace purchase is mid-flight, and the session landing removes it.\n */\nconst REFUND_ORPHAN_COLLECTION = 'marketplaceRefundOrphans'\n\n/** A cause plus everything the revocation and the GA hit need to replay it. */\ninterface RefundOutcomeCause extends SellerShareReversalCause {\n /** The join key, and the orphan document id. */\n paymentIntentId: string\n /** GA `refund` currency — the event's, never assumed. */\n currency: string\n /** Stripe customer for the GA hit; falls back to the buyer uid. */\n stripeCustomerId: string\n}\n\n/**\n * What may be parked when the purchase document does not exist yet.\n *\n * `partial-refund` (AGL-2299) is deliberately NOT a `SellerShareReversalCause`\n * kind: it never revokes, never reports to GA, and settles cumulatively\n * against Stripe's own `amount_reversed` rather than through the one-shot\n * marker. It shares only the store and the drain trigger.\n */\ntype ParkedCause = Omit<RefundOutcomeCause, 'kind'> & {\n kind: RefundOutcomeCause['kind'] | 'partial-refund'\n}\n\n/**\n * The three effects a full refund or a LOST dispute applies to a purchase:\n * revoke, report, pull the seller's share back.\n *\n * ONE function for three call sites — the refund door, the dispute door, and\n * the orphan drain — deliberately, and it is the AGL-1994 lesson again: the\n * drain must apply *the same* effects the door would have, and a second copy\n * of them is how the two drift. The drain therefore cannot forget the GA hit\n * or the reversal, because it is not in a position to.\n *\n * IDEMPOTENT AT EVERY LAYER. `refundedAt` read BEFORE the stamp is the GA\n * guard (a redelivery reports nothing twice); `reversedTransferCents` is the\n * reversal's settle marker, so a redelivery — or a drain running after the\n * door already ran — returns before any Stripe call; and the reversal POST\n * carries `Idempotency-Key: <kind>-reversal-<causeId>`, which is the SAME key\n * either path would have used because it is derived from the cause, not from\n * who is applying it. A drained refund and a redelivered `charge.refunded`\n * are therefore indistinguishable to Stripe, and the publisher cannot be\n * debited twice.\n */\nasync function applyMarketplaceRefundOutcome(\n purchaseRef: FirebaseFirestore.DocumentReference,\n cause: RefundOutcomeCause,\n /** Dispute-only fields stamped in the same write. */\n extraStamp: Record<string, unknown> = {},\n): Promise<void> {\n const snapshot = await purchaseRef.get()\n if (!snapshot.exists) return\n // Read BEFORE the stamp: `refundedAt` doubles as the GA guard, so a\n // redelivery that slips past the route's event claim finds the purchase\n // already refunded and reports nothing a second time.\n const alreadyRefunded = Boolean(snapshot.get('refundedAt'))\n await purchaseRef.set(\n {\n ...extraStamp,\n refundedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n refundedCents: cause.amountCents,\n },\n { merge: true },\n )\n // GA4 `refund` (AGL-1850) — the reversal of the AGL-1639 purchase, in the\n // SAME accounting. The purchase reported the platform NET (gross − tax −\n // transfer), so the refund must reverse that number: refunding the\n // tax-inclusive gross would net MORE out of GA than the sale ever put in.\n // Fire-and-forget, after the stamp: an analytics failure must never\n // un-claim a Stripe event.\n if (!alreadyRefunded) {\n const grossCents = Number(snapshot.get('amountCents') ?? 0)\n const taxCents = Number(snapshot.get('taxCents') ?? 0)\n const sellerCents = Number(snapshot.get('transferCents') ?? 0)\n const netCents = grossCents - taxCents - sellerCents\n if (netCents > 0) {\n after(() => sendGa4Refund({\n transactionId: String(purchaseRef.id),\n value: netCents / 100,\n currency: cause.currency,\n items: [],\n stripeCustomerId:\n cause.stripeCustomerId || String(snapshot.get('buyerUid') ?? ''),\n }).catch(() => undefined))\n }\n }\n // AFTER the revocation, so the entitlement never stays live because a\n // Stripe read failed: this half throws on transient failures (the\n // redelivery is the retry, and everything above is idempotent under it) and\n // settles definitively otherwise.\n await reverseMarketplaceSellerShare(purchaseRef, cause)\n}\n\n/**\n * Parks a cause whose purchase document does not exist yet.\n *\n * FIRST CAUSE WINS. The document is not merged over: a redelivery of the same\n * refund would otherwise move `createdAt`, and the (vanishingly unlikely)\n * refund-then-dispute pair inside one window would otherwise blend into a\n * chimera carrying one cause's id and the other's amount. The first cause is\n * the one that revoked, and the reversal settle marker means only one\n * pull-back can happen regardless of which is replayed.\n */\nasync function recordRefundOrphan(\n firestore: FirebaseFirestore.Firestore,\n cause: ParkedCause,\n): Promise<void> {\n if (!cause.paymentIntentId || !cause.id) return\n const orphanRef = firestore\n .collection(REFUND_ORPHAN_COLLECTION)\n .doc(cause.paymentIntentId)\n const parked = await orphanRef.get()\n // FIRST CAUSE WINS — with one exception (AGL-2299). A parked\n // `partial-refund` is strictly weaker than a full refund or a lost dispute:\n // it moves a slice of the seller's share and nothing else. If the same\n // charge is then refunded in full inside the same window, the stronger\n // cause must replace it, or the drain would pull back a slice and leave the\n // buyer entitled to content they have been fully refunded for. The reverse\n // never happens: a fully refunded charge cannot take a partial afterwards.\n if (parked.exists && !(\n parked.get('kind') === 'partial-refund' && cause.kind !== 'partial-refund'\n )) {\n return\n }\n await orphanRef.set(\n {\n kind: cause.kind,\n id: cause.id,\n paymentIntentId: cause.paymentIntentId,\n amountCents: cause.amountCents,\n chargeId: cause.chargeId,\n currency: cause.currency,\n stripeCustomerId: cause.stripeCustomerId,\n createdAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n // Loud on purpose: this is money in flight. If the matching session never\n // lands (a buyer who abandoned checkout cannot be refunded, so in practice\n // it always does), the row is the only trace, and it is queryable.\n console.error(\n 'Marketplace refund landed before its purchase — parked for the session handler (AGL-2148)',\n {\n kind: cause.kind,\n causeId: cause.id,\n paymentIntentId: cause.paymentIntentId,\n amountCents: cause.amountCents,\n },\n )\n}\n\n/**\n * Applies — and removes — a cause parked before this purchase existed.\n *\n * WHETHER THE SESSION PATH MAY REVOKE AND REVERSE FOR A CAUSE IT DID NOT\n * RECEIVE: yes, and it is not really a choice. The alternative is a purchase\n * document that reads as live for money the buyer already got back — a free\n * install and a publisher paid for a sale that was undone.\n *\n * THE DELETE IS LAST, after the reversal returns. A transient Stripe failure\n * inside `reverseMarketplaceSellerShare` THROWS on purpose so the session\n * delivery 500s and Stripe redelivers; if the orphan were dropped first, that\n * redelivery would find nothing to replay and the reversal would be lost\n * permanently. Deleting after a DEFINITIVE settle is correct and deliberate:\n * that settle wrote `reversalFailedAt` / `reversalFailedReason` /\n * `reversalOwedCents` onto the purchase, so the money lands in AGL-2140's\n * existing recovery queue rather than in a second one invented here.\n */\nasync function drainMarketplaceRefundOrphan(\n firestore: FirebaseFirestore.Firestore,\n purchaseRef: FirebaseFirestore.DocumentReference,\n paymentIntentId: string,\n): Promise<void> {\n if (!paymentIntentId) return\n const orphanRef = firestore\n .collection(REFUND_ORPHAN_COLLECTION)\n .doc(paymentIntentId)\n const orphan = await orphanRef.get()\n if (!orphan.exists) return\n const causeId = String(orphan.get('id') ?? '')\n const parkedKind = String(orphan.get('kind') ?? '')\n if (parkedKind === 'partial-refund') {\n // A partial refund never revokes and never reports, so it does not go\n // through `applyMarketplaceRefundOutcome` (AGL-2299). The charge is RE-READ\n // from Stripe rather than replayed from the parked amount: the target is\n // computed from the charge's cumulative `amount_refunded`, and by the time\n // this drains there may have been a second refund the parked document\n // never saw.\n // Throws on a transient Stripe failure, which leaves the orphan in place\n // for the redelivery — the delete below is exactly what must not happen\n // in that case, and it is why it is AFTER this await rather than before.\n await reverseMarketplacePartialRefundShare(\n purchaseRef,\n String(orphan.get('chargeId') ?? ''),\n )\n await orphanRef.delete()\n return\n }\n const kind = parkedKind === 'dispute' ? 'dispute' : 'refund'\n if (causeId) {\n await applyMarketplaceRefundOutcome(\n purchaseRef,\n {\n kind,\n id: causeId,\n paymentIntentId,\n amountCents: Math.round(Number(orphan.get('amountCents') ?? 0)),\n chargeId: String(orphan.get('chargeId') ?? ''),\n currency: String(orphan.get('currency') ?? 'usd'),\n stripeCustomerId: String(orphan.get('stripeCustomerId') ?? ''),\n },\n // A parked dispute is always a LOST one — the only dispute state that\n // moves money — so the outcome lands on the record too, exactly as the\n // dispute door would have stamped it.\n kind === 'dispute'\n ? { disputeId: causeId, disputeStatus: 'lost' }\n : {},\n )\n }\n await orphanRef.delete()\n}\n\n/**\n * A dispute has no metadata of ours to read — the event object is the DISPUTE,\n * not the charge — so the discriminator costs one Stripe read.\n *\n * Only ever reached when the payment-intent join came up empty, so a normal\n * marketplace dispute (purchase present) pays nothing for it, and a commerce\n * or subscription dispute pays one GET before being correctly ignored.\n * Disputes are rare enough for that to be the right trade against parking an\n * orphan for every chargeback on the platform.\n *\n * A FAILED READ DOES NOT THROW, deliberately, and this is the one place the\n * fix accepts a loss. Throwing would 500 the endpoint for a dispute we have\n * not established is ours — the exact blast radius that ruled out \"just throw\"\n * in the first place — and the route drops its idempotency claim on a throw.\n * So a marketplace dispute landing inside the purchase window WHILE the charge\n * read fails loses its orphan. It is logged, and it is two independent\n * unlikely events deep.\n */\nasync function recordDisputeOrphanIfMarketplace(\n firestore: FirebaseFirestore.Firestore,\n object: any,\n paymentIntentId: string,\n): Promise<boolean> {\n const chargeId = String(object?.charge ?? '')\n const stripeKey = process.env.STRIPE_SECRET_KEY\n if (!chargeId || !stripeKey) return false\n const charge = await stripeGet(\n `https://api.stripe.com/v1/charges/${chargeId}`,\n stripeKey,\n )\n if (!charge.ok) {\n console.error(\n 'Could not classify a dispute with no matching purchase — orphan NOT parked (AGL-2148)',\n { chargeId, paymentIntentId, status: charge.status },\n )\n return false\n }\n if (charge.body?.metadata?.type !== 'marketplace-purchase') return false\n await recordRefundOrphan(firestore, {\n kind: 'dispute',\n id: String(object?.id ?? ''),\n paymentIntentId,\n amountCents: Math.round(Number(object?.amount ?? 0)),\n chargeId,\n currency: String(object?.currency ?? 'usd'),\n stripeCustomerId: '',\n })\n // Parked, so it IS marketplace's — the route must not also alert on it\n // (AGL-2429). The three early exits above return false on purpose: two of\n // them mean \"could not classify\", and a dispute nobody could classify is\n // exactly what the route's unattributed alert exists to catch.\n return true\n}\n\n/**\n * Marketplace-purchase section of the platform Stripe webhook (AGL-46/418):\n * keyed by session id (idempotent on Stripe redelivery) — relocated\n * verbatim from the console route; registered via\n * registerMarketplaceConsoleApi. Install gating and the seller ledger read\n * these purchase docs.\n */\nexport const marketplaceBillingWebhookHandler: BillingWebhookHandler = async ({\n type,\n object,\n event,\n}) => {\n // Connect readiness, kept fresh (AGL-1997) — the publisher twin of the\n // commerce sync. The seller panel reads `stripeChargesEnabled` /\n // `stripePayoutsEnabled` off this document, and before this nothing but the\n // publisher reopening the connect route ever refreshed either. Same early\n // return: `account.updated` shares nothing with the purchase sections below.\n // `event.livemode`, not `object.livemode` (AGL-2471) — the Account object\n // has no such field. Two of the three poisoned production linkages were\n // publisherProfiles, and this is the path that heals them.\n if (type === 'account.updated') {\n await syncConnectAccountStatus(\n 'publisherProfiles',\n object,\n event?.livemode,\n )\n return\n }\n\n // A PAYOUT OR TRANSFER THAT NEVER LANDED.\n //\n // Placed beside `account.updated` because it is the same kind of event —\n // account-level, nothing to do with the `metadata.type` order sections\n // below — and returns for the same reason.\n //\n // `payout.failed` is the CONNECTED account's balance failing to reach its\n // bank, so the account id is `event.account`: the Payout object's own\n // `destination` names the bank, not the Connect account. `transfer.failed`\n // is the platform's balance failing to reach the connected account, a\n // platform event whose `destination` IS the account.\n //\n // Recorded and surfaced, never retried: Stripe runs its own retry schedule\n // and a second transfer against an account that just refused one is how a\n // duplicate lands.\n if (type === 'payout.failed' || type === 'transfer.failed') {\n const failedAccountId =\n type === 'payout.failed'\n ? String(event?.account ?? '')\n : String(object?.destination?.id ?? object?.destination ?? '')\n await recordConnectPayoutFailure('publisherProfiles', {\n kind: type === 'payout.failed' ? 'payout' : 'transfer',\n object,\n accountId: failedAccountId,\n livemode: event?.livemode,\n })\n return\n }\n // A later success retires the warning the card shows. The history in\n // `connectPayoutFailures` is kept — \"has this account failed before\" is what\n // that record exists to answer — but a stale warning on a resolved problem\n // trains people to ignore the surface.\n if (type === 'payout.paid') {\n await clearConnectPayoutFailure('publisherProfiles', String(event?.account ?? ''))\n return\n }\n\n // Marketplace purchases (AGL-46): keyed by session id (idempotent on\n // Stripe redelivery). Install gating and the seller ledger read these.\n if (\n type === 'checkout.session.completed' &&\n object?.metadata?.type === 'marketplace-purchase' &&\n object?.payment_status === 'paid'\n ) {\n // Sellers are orgs (AGL-652) — the ledger records which ORG earned it.\n const { listingId, buyerUid, buyerOrgId, sellerOrgId, feeCents, transferCents } =\n object.metadata ?? {}\n if (listingId && buyerUid && sellerOrgId) {\n // The remittance-correct split (AGL-1544), read ONCE and used by both\n // the ledger and the GA hit (AGL-1639) — the two must not be able to\n // describe the same sale differently.\n //\n // `amount_total` is the tax-inclusive GROSS the buyer paid. Out of it:\n // `taxCents` is what the PLATFORM owes the state (collected under the\n // marketplace-provider registration, never ours), and `sellerCents` is\n // the fixed transfer the seller's Connect account received (their\n // share of the pre-tax price). What is left is what Aglyn keeps.\n const grossCents = Number(object?.amount_total ?? 0)\n const taxCents = Number(object?.total_details?.amount_tax ?? 0)\n const sellerCents = Number(transferCents ?? 0)\n const netCents = grossCents - taxCents - sellerCents\n // WHERE that tax is owed. Read from the session Stripe computed it\n // from — see `buyerTaxJurisdiction`.\n const jurisdiction = buyerTaxJurisdiction(object)\n const purchaseRef = firebaseAdmin\n .app()\n .firestore()\n .collection('marketplacePurchases')\n .doc(String(object.id))\n // `createdAt` is stamped ONCE (AGL-2109). Merging the write below fixed\n // the erasure but introduced its own drift: `serverTimestamp()` on a\n // redelivery would move a three-day-old sale to today, which is the\n // field the seller ledger and every revenue period read. One read, on a\n // path that already makes several Stripe round trips, buys a date that\n // means what it says.\n const alreadyRecorded = (await purchaseRef.get()).exists\n await purchaseRef\n .set({\n listingId,\n buyerUid,\n // THE ORG THE PURCHASE LICENSES (AGL-2331).\n //\n // The one field that makes this document an ORGANIZATIONAL licence\n // rather than a personal one. `hasLivePurchase` keys the eight\n // paid-content doors and the duplicate-purchase guard on it; a\n // document written without it falls back to the legacy person-scoped\n // grant, which is exactly right for the purchases that predate\n // AGL-2331 and exactly wrong for anything written after it. Checkout\n // therefore refuses to open a session that cannot name a validated\n // buyer org, so `metadata.buyerOrgId` is present on every session\n // this branch can see from now on.\n //\n // Written CONDITIONALLY, and the merge is why: a redelivery of an\n // older session — Stripe retries for up to three days — carries no\n // `buyerOrgId`, and `{ merge: true }` with an explicit `undefined`\n // is a no-op in the Admin SDK but an explicit `''` is not. Spreading\n // nothing keeps a redelivery from overwriting a real org id with an\n // empty string, which would strip the licence off the org that\n // bought it.\n ...(buyerOrgId ? { buyerOrgId: String(buyerOrgId) } : {}),\n sellerOrgId,\n // Gross − tax − transfer = the platform fee, which feeCents also\n // records independently from the rate resolved at checkout.\n amountCents: grossCents,\n feeCents: Number(feeCents ?? 0),\n taxCents,\n transferCents: sellerCents,\n // The refund trail (AGL-1546): `charge.refunded` carries the\n // payment intent, not the session — without this id a refund\n // could never find the purchase it revokes.\n paymentIntentId: String(object?.payment_intent ?? ''),\n ...(alreadyRecorded\n ? {}\n : {\n createdAt:\n firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n // THE JURISDICTION THE TAX WAS COMPUTED FOR (AGL-2137).\n //\n // Here, inside the first-record branch, for a stronger reason\n // than `createdAt`'s: a row that exists without a jurisdiction\n // must keep not having one. Stripe redelivers this event for\n // up to three days, and a redelivery that stamped a\n // jurisdiction onto an older sale would restate which state a\n // past period's tax is attributed to — a period that may\n // already have been filed. Recording forward is an\n // improvement; reaching backwards is a restatement.\n //\n // Absent, rather than null, when the session states no\n // country: see `buyerTaxJurisdiction`.\n ...(jurisdiction ? { customerAddress: jurisdiction } : {}),\n }),\n },\n // MERGED, like every other write on this path (AGL-2109). This one\n // was a full document REPLACE, and the document it replaces is not\n // only the sale: it is the buyer's ENTITLEMENT (`hasLivePurchase`\n // reads `refundedAt`), the refund trail, the dispute trail, and the\n // transfer-reversal SETTLE MARKER (`reversedTransferCents`, which\n // `reverseMarketplaceSellerShare` short-circuits on so a publisher is\n // never debited twice).\n //\n // Stripe redelivers `checkout.session.completed` for up to three days\n // after any 500, and this endpoint 500s on purpose — for every\n // transient Stripe failure raised in this file and for any throw from\n // a sibling plugin handler that runs after this one\n // (`runBillingWebhookHandlers` awaits them in sequence and lets a\n // throw propagate). So the redelivery is expected traffic, not an\n // exotic race. Landing after a refund it erased `refundedAt` — the\n // refunded buyer silently got their install back — and erased\n // `reversedTransferCents`, re-opening a second reversal against the\n // publisher's Connect account.\n //\n // Merging is safe in the other direction too: every field written\n // here is derived from the session event, so a redelivery restamps\n // identical values.\n { merge: true },\n )\n // Marketplace sales are real revenue and belong in the same GA\n // `purchase` stream as subscriptions (AGL-1561), separated by\n // `item_category` so plugin revenue and subscription revenue can be\n // read apart or together.\n //\n // After the ledger write, and fire-and-forget: the ledger is what\n // grants the install entitlement, and an analytics failure must never\n // throw here — the route deletes its idempotency claim on any throw,\n // which would make Stripe redeliver a purchase that already landed.\n //\n // `transaction_id` is the checkout session id — the same key the\n // ledger doc uses — so GA de-duplicates a redelivery exactly as\n // Firestore does.\n // WHAT COUNTS AS REVENUE ON A MARKETPLACE SALE (AGL-1639)\n //\n // Our NET — the platform fee — and not the gross the buyer paid.\n // Decided once here rather than inferred from whichever Stripe field\n // was nearest, because the three candidates are genuinely different\n // numbers:\n //\n // gross incl. tax reconciles with nothing we own\n // GMV ex-tax what the SELLERS earned, not what we did\n // platform net our books, our MRR, our balance ← this one\n //\n // This is *our* GA property and every other number in it is ours;\n // subscription `purchase` already reports what Aglyn was paid, and\n // marketplace revenue has to mean the same thing or the combined\n // total, ARPA and every revenue-based audience are nonsense. The two\n // stay separable by `item_category`.\n //\n // Tax is excluded rather than folded in, and is deliberately NOT sent\n // as GA4's `tax` param either: `value` is our fee, so a `tax` beside\n // it would not be a component of it, and asserting in GA that Aglyn\n // took this tax is exactly the question the publisher agreement's\n // seller-of-record clause has open. The ledger doc above keeps the\n // full split for anyone who needs it.\n after(() => sendGa4Purchase({\n transactionId: String(object.id),\n value: netCents / 100,\n currency: String(object?.currency ?? 'usd'),\n items: [\n {\n item_id: String(listingId),\n // The listing id, not the display name: a listing's name is\n // seller-authored free text and is not worth risking in a\n // dimension when the id already identifies it.\n item_name: String(listingId),\n item_category: 'marketplace',\n // GA expects the items to sum to `value`; one item, one price.\n price: netCents / 100,\n quantity: 1,\n },\n ],\n clientId: object?.metadata?.ga_client_id,\n stripeCustomerId: String(object?.customer ?? '') || String(buyerUid),\n }).catch(() => undefined))\n // A refund or a lost dispute that arrived before this document existed\n // is applied NOW (AGL-2148) — revocation, GA and the transfer reversal,\n // by the same function the refund door uses. Last on this path, and\n // awaited: it can throw on a transient Stripe failure, which is exactly\n // the redelivery this branch is already idempotent under.\n await drainMarketplaceRefundOrphan(\n firebaseAdmin.app().firestore(),\n purchaseRef,\n String(object?.payment_intent ?? ''),\n )\n }\n }\n\n // Refund revocation (AGL-1546): a FULL refund un-buys the listing —\n // the install gate treats a purchase with `refundedAt` as absent. Only\n // `refunded: true` (the whole charge) revokes; a partial refund is a\n // concession, not a revocation. Keyed by the payment intent stored at\n // completion, and idempotent: a Stripe redelivery restamps the same\n // values on the same doc. Requires the platform webhook endpoint to be\n // subscribed to `charge.refunded` (AGL-1549).\n if (type === 'charge.refunded' && object?.refunded === true) {\n const paymentIntentId = String(object?.payment_intent ?? '')\n if (paymentIntentId) {\n const firestore = firebaseAdmin.app().firestore()\n const purchases = await firestore\n .collection('marketplacePurchases')\n .where('paymentIntentId', '==', paymentIntentId)\n .limit(1)\n .get()\n // Everything a refund does to a purchase, and everything it needs to\n // wait for one — assembled once so the two paths cannot describe the\n // same refund differently.\n const cause: RefundOutcomeCause = {\n kind: 'refund',\n // `charge.refunded` carries the CHARGE, so the charge id is both the\n // cause id and the chargeId — unchanged from AGL-1995.\n id: String(object?.id ?? ''),\n paymentIntentId,\n amountCents: Math.round(Number(object?.amount_refunded ?? 0)),\n chargeId: String(object?.id ?? ''),\n currency: String(object?.currency ?? 'usd'),\n stripeCustomerId: String(object?.customer ?? ''),\n }\n if (!purchases.empty) {\n // The publisher's share comes back (AGL-1995), the entitlement is\n // revoked (AGL-1546) and GA nets the sale out (AGL-1850) — see\n // `applyMarketplaceRefundOutcome`. FULL refunds only, matching the\n // revocation and GA gates: the branch is already inside\n // `object?.refunded === true`. The seller's share of a PARTIAL refund\n // now comes back too, without any of the other three effects — see\n // `reverseMarketplacePartialRefundShare` below. There is no\n // marketplace refund UI, so every refund is issued from the Stripe\n // Dashboard and this webhook is the only code that sees one.\n await applyMarketplaceRefundOutcome(purchases.docs[0].ref, cause)\n } else if (object?.metadata?.type === 'marketplace-purchase') {\n // THE ELSE THAT DID NOT EXIST (AGL-2148). The purchase document is\n // written by `checkout.session.completed`, that delivery retries, and\n // a refund issued inside the window found nothing and was DROPPED.\n //\n // Gated on the PaymentIntent metadata stamped at marketplace checkout\n // (checkout.ts) and copied by Stripe onto the charge, so the\n // storefront and subscription refunds that share this endpoint park\n // nothing. A charge predating that stamp is not parked either — its\n // purchase document has long existed, so the join above finds it.\n await recordRefundOrphan(firestore, cause)\n }\n }\n }\n\n // A PARTIAL refund pulls the publisher's share back too (AGL-2299) — and\n // nothing else. The branch above is gated on `refunded === true`, which is\n // \"the whole charge is gone\"; every smaller refund fell through it and the\n // platform paid the entire concession out of a 20% cut. The entitlement is\n // untouched on purpose: a partial refund is a concession, not an un-buy.\n //\n // Same marketplace discriminator the orphan store uses, for the same\n // reason: `charge.refunded` arrives here for storefront orders and\n // subscription charges too, and this must not read a transfer belonging to\n // one of those.\n //\n // The discriminator is required only on the ORPHAN side, exactly as the\n // full-refund branch requires it: when the payment-intent join FINDS a\n // marketplace purchase, that join is already the proof, and demanding the\n // metadata as well would skip every charge predating the AGL-2148 stamp.\n if (\n type === 'charge.refunded' &&\n object?.refunded !== true &&\n Math.round(Number(object?.amount_refunded ?? 0)) > 0\n ) {\n const paymentIntentId = String(object?.payment_intent ?? '')\n if (paymentIntentId) {\n const firestore = firebaseAdmin.app().firestore()\n const purchases = await firestore\n .collection('marketplacePurchases')\n .where('paymentIntentId', '==', paymentIntentId)\n .limit(1)\n .get()\n if (!purchases.empty) {\n await reverseMarketplacePartialRefundShare(\n purchases.docs[0].ref,\n String(object?.id ?? ''),\n )\n } else if (object?.metadata?.type === 'marketplace-purchase') {\n // The AGL-2148 window, for partials. Parked with the charge on it so\n // the drain can recompute the target from Stripe rather than trusting\n // a number this event carried; the amount rides along for forensics.\n await recordRefundOrphan(firestore, {\n kind: 'partial-refund',\n id: String(object?.id ?? ''),\n paymentIntentId,\n amountCents: Math.round(Number(object?.amount_refunded ?? 0)),\n chargeId: String(object?.id ?? ''),\n currency: String(object?.currency ?? 'usd'),\n stripeCustomerId: String(object?.customer ?? ''),\n })\n }\n }\n }\n\n // Chargebacks (AGL-1554): the AGL-1546 refund arriving by the bank's\n // door. `charge.dispute.*` for tenant storefront orders and platform\n // subscriptions arrives on this endpoint too — the payment-intent join\n // below simply finds no purchase for those and this section stays out of\n // their way, exactly as the refund branch does.\n //\n // A dispute is NOT a refund, and the states matter (the AGL-1554\n // analysis): `created` can still be WON, and nothing un-revokes, so it\n // only flags the purchase for staff visibility. Money moves exclusively\n // on `closed` + `status: 'lost'`: the buyer's entitlement goes the way\n // the money went (`refundedAt`, the field the install gate reads as\n // absent-purchase — final outcomes only), GA nets the AGL-1639 purchase\n // out in the AGL-1850 accounting (platform net, guarded by the same\n // `refundedAt` read the refund branch uses), and the seller's share\n // comes back by the AGL-1794 policy. `won` and `warning_closed` record\n // the outcome and move nothing.\n if (type === 'charge.dispute.created' || type === 'charge.dispute.closed') {\n const disputeId = String(object?.id ?? '')\n const paymentIntentId = String(object?.payment_intent ?? '')\n // Reported to the route so it can tell \"marketplace handled it\" from\n // \"nothing handled it\" (AGL-2429). Left false by the guard below on\n // purpose: a dispute carrying no payment intent cannot be joined to a\n // purchase, which is a fault, not a routine miss.\n let claimed = false\n if (disputeId && paymentIntentId) {\n const firestore = firebaseAdmin.app().firestore()\n const purchases = await firestore\n .collection('marketplacePurchases')\n .where('paymentIntentId', '==', paymentIntentId)\n .limit(1)\n .get()\n if (!purchases.empty) {\n claimed = true\n const purchase = purchases.docs[0]\n const status = String(object?.status ?? '')\n if (type === 'charge.dispute.created') {\n await purchase.ref.set(\n {\n disputeId,\n disputeStatus: status || 'needs_response',\n disputeOpenedAt:\n firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n } else if (status === 'lost') {\n // Revoke, report, pull the seller's share back — the same three\n // effects the refund door applies, through the same function\n // (AGL-2148), so the two doors and the orphan drain cannot drift.\n // The dispute outcome is stamped in the same write.\n await applyMarketplaceRefundOutcome(\n purchase.ref,\n {\n kind: 'dispute',\n id: disputeId,\n paymentIntentId,\n amountCents: Math.round(Number(object?.amount ?? 0)),\n chargeId: String(object?.charge ?? ''),\n currency: String(object?.currency ?? 'usd'),\n // A dispute event carries no Stripe customer; the GA hit falls\n // back to the buyer uid on the purchase, as it always has.\n stripeCustomerId: '',\n },\n { disputeId, disputeStatus: status },\n )\n } else {\n // `won` or `warning_closed`: the money stayed, the entitlement\n // stays, and the outcome lands on the record for whoever flagged\n // it at `created`.\n await purchase.ref.set(\n { disputeId, disputeStatus: status },\n { merge: true },\n )\n }\n } else if (type === 'charge.dispute.closed' &&\n String(object?.status ?? '') === 'lost') {\n // The refund branch's window, by the bank's door (AGL-2148): only a\n // LOST closed dispute moves money, so only that state is worth\n // parking. A `created` landing in the same window loses nothing but\n // the staff flag — the `closed` that follows days later always finds\n // the purchase document.\n claimed = await recordDisputeOrphanIfMarketplace(\n firestore,\n object,\n paymentIntentId,\n )\n }\n }\n return { claimed }\n }\n}\n"],"names":["after","firebaseAdmin","sendGa4Purchase","sendGa4Refund","clearConnectPayoutFailure","recordConnectPayoutFailure","syncConnectAccountStatus","isTransientStripeStatus","status","buyerTaxJurisdiction","object","address","customer_details","country","trim","state","stripeGet","url","stripeKey","response","fetch","headers","Authorization","body","json","catch","ok","reverseMarketplaceSellerShare","purchaseRef","cause","charge","transfer","causeId","String","id","metadataKey","kind","snapshot","get","exists","causeAmountCents","Math","round","Number","amountCents","process","env","STRIPE_SECRET_KEY","console","error","settle","reversedTransferCents","transferReversalId","failure","fresh","purchaseId","reason","owedCents","set","reversalFailedAt","firestore","FieldValue","serverTimestamp","reversalFailedReason","reversalFailedCause","reversalOwedCents","merge","chargeId","Error","transferId","chargeAmountCents","amount","existing","reversals","data","find","item","metadata","transferCents","alreadyReversedCents","amount_reversed","remainingCents","max","shareCents","min","floor","params","URLSearchParams","method","toString","reversal","reverseMarketplacePartialRefundShare","refundedCents","amount_refunded","partialRefundedCents","targetCents","toReverseCents","partialReversedTransferCents","partialTransferReversalId","REFUND_ORPHAN_COLLECTION","applyMarketplaceRefundOutcome","extraStamp","alreadyRefunded","Boolean","refundedAt","grossCents","taxCents","sellerCents","netCents","transactionId","value","currency","items","stripeCustomerId","undefined","recordRefundOrphan","paymentIntentId","orphanRef","collection","doc","parked","createdAt","drainMarketplaceRefundOrphan","orphan","parkedKind","delete","disputeId","disputeStatus","recordDisputeOrphanIfMarketplace","type","marketplaceBillingWebhookHandler","event","livemode","failedAccountId","account","destination","accountId","payment_status","listingId","buyerUid","buyerOrgId","sellerOrgId","feeCents","amount_total","total_details","amount_tax","jurisdiction","app","alreadyRecorded","payment_intent","customerAddress","item_id","item_name","item_category","price","quantity","clientId","ga_client_id","customer","refunded","purchases","where","limit","empty","docs","ref","claimed","purchase","disputeOpenedAt"],"mappings":"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"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type PluginApiHandler } from '@aglyn/aglyn/server';
18
+ /**
19
+ * THE TAX CLASSIFICATION OF A MARKETPLACE SALE (AGL-1553).
20
+ *
21
+ * "Software as a service (SaaS) — business use". Sent explicitly on the line
22
+ * item below, because this session builds its product ad hoc from
23
+ * `price_data` and an ad-hoc product with no `tax_code` is classified by the
24
+ * ACCOUNT-LEVEL preset instead — a value set by hand in the Stripe Dashboard
25
+ * for Aglyn's own subscription products (AGL-1537/1877), which happens to be
26
+ * this same string.
27
+ *
28
+ * So this is not a change of tax position, and deliberately not: it is the
29
+ * position Aglyn already collects on, already verified, and already states in
30
+ * a signed filing. The pending Texas private letter ruling request proposes at
31
+ * Part 6.1 that plugin charges are DATA PROCESSING SERVICES under Tax Code
32
+ * § 151.0035, of which § 151.351 exempts 20%, so tax is due on 80% of the
33
+ * charge; Part 4.5 ¶ 4 represents to the Comptroller that "the payment
34
+ * processor's tax classification applied to these charges is the
35
+ * software-as-a-service category" and that the 80% measure was confirmed by
36
+ * computation. AGL-1817 ran that computation against the live account: $6.60
37
+ * of tax on a $100.00 charge at an 8.25% Texas address — 8.25% x $80.
38
+ *
39
+ * What was actually broken is that none of that was expressed HERE. The
40
+ * classification of every marketplace sale hung on a Dashboard field owned by
41
+ * a different product line. Re-preset the account for storefront goods or
42
+ * bookings and these sales silently re-classify, moving the Texas base off the
43
+ * 80% the filing describes — with no diff to review, no failing test, and no
44
+ * symptom short of reconciling a return. Naming it here is what makes the
45
+ * filed representation survive a Dashboard edit.
46
+ *
47
+ * NOT merged with `PLATFORM_TAX_CODE` in `tools/scripts/lib/stripe-tax-code.mjs`,
48
+ * which stamps Aglyn's OWN products with the same string today. They answer
49
+ * different questions — Aglyn's first-party subscription vs. a third party's
50
+ * sale through Aglyn's marketplace — and the ruling request itself flags at
51
+ * Part 8 ¶ 8 that the Comptroller could characterize the two differently. One
52
+ * shared constant would make a divergence the filing anticipates impossible to
53
+ * express without touching the other business.
54
+ *
55
+ * DO NOT change this value to a downloadable-software code (`txcd_10202003`,
56
+ * `txcd_10203001`) on the reasoning that a plugin is "downloaded". That is the
57
+ * OPPOSING characterization — tangible personal property under Rule 3.308,
58
+ * taxable in full — which Part 6.1 argues against and which the filing already
59
+ * states against itself, in full, at Part 8 ¶¶ 1-2. Adopting
60
+ * it here would raise the tax charged to every buyer by a quarter of the base
61
+ * and make Part 4.5 ¶ 4 false. It is the Comptroller's call, and if the ruling
62
+ * goes that way the change is a one-line edit at this constant plus a
63
+ * prospective conformance — which is exactly what naming it here buys.
64
+ */
65
+ export declare const MARKETPLACE_TAX_CODE = "txcd_10103001";
66
+ /**
67
+ * Checkout for a paid marketplace listing (AGL-46): one-time destination
68
+ * charge to the publisher's Express account. The platform's cut comes from
69
+ * the seller org's ENTITLEMENTS (AGL-1543: 20%, 30% effective-free), sales
70
+ * tax is collected platform-side per the marketplace-provider registration
71
+ * (AGL-1544), and the seller receives a fixed transfer of their pre-tax
72
+ * share. The webhook writes the purchase record on
73
+ * `checkout.session.completed`; install stays gated on that record. 501
74
+ * without Stripe env.
75
+ */
76
+ export declare const checkoutHandler: PluginApiHandler;