@fantastic.dev/better-stripe 0.23.1 → 0.24.0

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 (287) hide show
  1. package/CHANGELOG.md +26 -2
  2. package/README.md +40 -5
  3. package/dist/client/affiliateRecovery/disputes.d.ts +18 -0
  4. package/dist/client/affiliateRecovery/disputes.d.ts.map +1 -0
  5. package/dist/client/affiliateRecovery/disputes.js +33 -0
  6. package/dist/client/affiliateRecovery/disputes.js.map +1 -0
  7. package/dist/client/affiliateRecovery/engine.d.ts +29 -0
  8. package/dist/client/affiliateRecovery/engine.d.ts.map +1 -0
  9. package/dist/client/affiliateRecovery/engine.js +116 -0
  10. package/dist/client/affiliateRecovery/engine.js.map +1 -0
  11. package/dist/client/affiliateRecovery/payoutGate.d.ts +24 -0
  12. package/dist/client/affiliateRecovery/payoutGate.d.ts.map +1 -0
  13. package/dist/client/affiliateRecovery/payoutGate.js +30 -0
  14. package/dist/client/affiliateRecovery/payoutGate.js.map +1 -0
  15. package/dist/client/affiliateRecovery/provider.d.ts +14 -0
  16. package/dist/client/affiliateRecovery/provider.d.ts.map +1 -0
  17. package/dist/client/affiliateRecovery/provider.js +96 -0
  18. package/dist/client/affiliateRecovery/provider.js.map +1 -0
  19. package/dist/client/affiliateRecovery/review.d.ts +28 -0
  20. package/dist/client/affiliateRecovery/review.d.ts.map +1 -0
  21. package/dist/client/affiliateRecovery/review.js +86 -0
  22. package/dist/client/affiliateRecovery/review.js.map +1 -0
  23. package/dist/client/affiliateRecovery/validators.d.ts +402 -0
  24. package/dist/client/affiliateRecovery/validators.d.ts.map +1 -0
  25. package/dist/client/affiliateRecovery/validators.js +27 -0
  26. package/dist/client/affiliateRecovery/validators.js.map +1 -0
  27. package/dist/client/affiliateRecovery.d.ts +131 -0
  28. package/dist/client/affiliateRecovery.d.ts.map +1 -0
  29. package/dist/client/affiliateRecovery.js +69 -0
  30. package/dist/client/affiliateRecovery.js.map +1 -0
  31. package/dist/client/connect/commissionReceipts.d.ts +4 -0
  32. package/dist/client/connect/commissionReceipts.d.ts.map +1 -0
  33. package/dist/client/connect/commissionReceipts.js +17 -0
  34. package/dist/client/connect/commissionReceipts.js.map +1 -0
  35. package/dist/client/connect/payoutAdoptionTestHelpers.d.ts +536 -2
  36. package/dist/client/connect/payoutAdoptionTestHelpers.d.ts.map +1 -1
  37. package/dist/client/connect/payoutSync.d.ts +32 -0
  38. package/dist/client/connect/payoutSync.d.ts.map +1 -0
  39. package/dist/client/connect/payoutSync.js +48 -0
  40. package/dist/client/connect/payoutSync.js.map +1 -0
  41. package/dist/client/connect/payoutSyncTestHelpers.d.ts +1509 -0
  42. package/dist/client/connect/payoutSyncTestHelpers.d.ts.map +1 -0
  43. package/dist/client/connect/payoutSyncTestHelpers.js +62 -0
  44. package/dist/client/connect/payoutSyncTestHelpers.js.map +1 -0
  45. package/dist/client/connect/payoutSyncValidation.d.ts +16 -0
  46. package/dist/client/connect/payoutSyncValidation.d.ts.map +1 -0
  47. package/dist/client/connect/payoutSyncValidation.js +54 -0
  48. package/dist/client/connect/payoutSyncValidation.js.map +1 -0
  49. package/dist/client/connect/payouts.d.ts +3 -1
  50. package/dist/client/connect/payouts.d.ts.map +1 -1
  51. package/dist/client/connect/payouts.js +45 -12
  52. package/dist/client/connect/payouts.js.map +1 -1
  53. package/dist/client/connect/refundPrincipal.d.ts +4 -0
  54. package/dist/client/connect/refundPrincipal.d.ts.map +1 -0
  55. package/dist/client/connect/refundPrincipal.js +21 -0
  56. package/dist/client/connect/refundPrincipal.js.map +1 -0
  57. package/dist/client/connect/reversalExecution.d.ts +47 -0
  58. package/dist/client/connect/reversalExecution.d.ts.map +1 -0
  59. package/dist/client/connect/reversalExecution.js +84 -0
  60. package/dist/client/connect/reversalExecution.js.map +1 -0
  61. package/dist/client/connect/transfers.d.ts +8 -4
  62. package/dist/client/connect/transfers.d.ts.map +1 -1
  63. package/dist/client/connect/transfers.js +52 -57
  64. package/dist/client/connect/transfers.js.map +1 -1
  65. package/dist/client/index.d.ts +29 -9
  66. package/dist/client/index.d.ts.map +1 -1
  67. package/dist/client/index.js +9 -6
  68. package/dist/client/index.js.map +1 -1
  69. package/dist/client/readContracts.d.ts +8 -0
  70. package/dist/client/readContracts.d.ts.map +1 -1
  71. package/dist/client/readContracts.js +7 -0
  72. package/dist/client/readContracts.js.map +1 -1
  73. package/dist/client/settlement/execution.d.ts.map +1 -1
  74. package/dist/client/settlement/execution.js +30 -3
  75. package/dist/client/settlement/execution.js.map +1 -1
  76. package/dist/client/settlement/reconciliation.d.ts.map +1 -1
  77. package/dist/client/settlement/reconciliation.js +16 -2
  78. package/dist/client/settlement/reconciliation.js.map +1 -1
  79. package/dist/client/webhooks/disputeRecovery.d.ts +4 -0
  80. package/dist/client/webhooks/disputeRecovery.d.ts.map +1 -0
  81. package/dist/client/webhooks/disputeRecovery.js +40 -0
  82. package/dist/client/webhooks/disputeRecovery.js.map +1 -0
  83. package/dist/client/webhooks/processors.d.ts.map +1 -1
  84. package/dist/client/webhooks/processors.js +14 -29
  85. package/dist/client/webhooks/processors.js.map +1 -1
  86. package/dist/component/_generated/component.d.ts +560 -0
  87. package/dist/component/_generated/component.d.ts.map +1 -1
  88. package/dist/component/affiliateEarnings/mutations.d.ts +22 -0
  89. package/dist/component/affiliateEarnings/mutations.d.ts.map +1 -0
  90. package/dist/component/affiliateEarnings/mutations.js +160 -0
  91. package/dist/component/affiliateEarnings/mutations.js.map +1 -0
  92. package/dist/component/affiliatePayouts/mutations.d.ts +25 -0
  93. package/dist/component/affiliatePayouts/mutations.d.ts.map +1 -0
  94. package/dist/component/affiliatePayouts/mutations.js +104 -0
  95. package/dist/component/affiliatePayouts/mutations.js.map +1 -0
  96. package/dist/component/affiliatePayouts/queries.d.ts +23 -0
  97. package/dist/component/affiliatePayouts/queries.d.ts.map +1 -0
  98. package/dist/component/affiliatePayouts/queries.js +23 -0
  99. package/dist/component/affiliatePayouts/queries.js.map +1 -0
  100. package/dist/component/affiliatePayouts/recipientGuard.d.ts +12 -0
  101. package/dist/component/affiliatePayouts/recipientGuard.d.ts.map +1 -0
  102. package/dist/component/affiliatePayouts/recipientGuard.js +26 -0
  103. package/dist/component/affiliatePayouts/recipientGuard.js.map +1 -0
  104. package/dist/component/affiliateRecovery/accounting.d.ts +28 -0
  105. package/dist/component/affiliateRecovery/accounting.d.ts.map +1 -0
  106. package/dist/component/affiliateRecovery/accounting.js +71 -0
  107. package/dist/component/affiliateRecovery/accounting.js.map +1 -0
  108. package/dist/component/affiliateRecovery/mutations.d.ts +65 -0
  109. package/dist/component/affiliateRecovery/mutations.d.ts.map +1 -0
  110. package/dist/component/affiliateRecovery/mutations.js +250 -0
  111. package/dist/component/affiliateRecovery/mutations.js.map +1 -0
  112. package/dist/component/affiliateRecovery/queries.d.ts +305 -0
  113. package/dist/component/affiliateRecovery/queries.d.ts.map +1 -0
  114. package/dist/component/affiliateRecovery/queries.js +113 -0
  115. package/dist/component/affiliateRecovery/queries.js.map +1 -0
  116. package/dist/component/affiliateRecovery/schema.d.ts +171 -0
  117. package/dist/component/affiliateRecovery/schema.d.ts.map +1 -0
  118. package/dist/component/affiliateRecovery/schema.js +71 -0
  119. package/dist/component/affiliateRecovery/schema.js.map +1 -0
  120. package/dist/component/connect/mutations.d.ts +36 -23
  121. package/dist/component/connect/mutations.d.ts.map +1 -1
  122. package/dist/component/connect/mutations.js +11 -151
  123. package/dist/component/connect/mutations.js.map +1 -1
  124. package/dist/component/connect/payoutWrites.d.ts +21 -0
  125. package/dist/component/connect/payoutWrites.d.ts.map +1 -0
  126. package/dist/component/connect/payoutWrites.js +19 -0
  127. package/dist/component/connect/payoutWrites.js.map +1 -0
  128. package/dist/component/connect/queries.d.ts +10 -0
  129. package/dist/component/connect/queries.d.ts.map +1 -1
  130. package/dist/component/connect/queries.js +3 -40
  131. package/dist/component/connect/queries.js.map +1 -1
  132. package/dist/component/connect/reversalClaims.d.ts +169 -0
  133. package/dist/component/connect/reversalClaims.d.ts.map +1 -0
  134. package/dist/component/connect/reversalClaims.js +176 -0
  135. package/dist/component/connect/reversalClaims.js.map +1 -0
  136. package/dist/component/connect/reversalDiagnostics.d.ts +59 -0
  137. package/dist/component/connect/reversalDiagnostics.d.ts.map +1 -0
  138. package/dist/component/connect/reversalDiagnostics.js +50 -0
  139. package/dist/component/connect/reversalDiagnostics.js.map +1 -0
  140. package/dist/component/connect/reversalReceipts.d.ts +27 -0
  141. package/dist/component/connect/reversalReceipts.d.ts.map +1 -0
  142. package/dist/component/connect/reversalReceipts.js +85 -0
  143. package/dist/component/connect/reversalReceipts.js.map +1 -0
  144. package/dist/component/connect/schema.d.ts +31 -1
  145. package/dist/component/connect/schema.d.ts.map +1 -1
  146. package/dist/component/connect/schema.js +3 -1
  147. package/dist/component/connect/schema.js.map +1 -1
  148. package/dist/component/connect/validators.d.ts +49 -1
  149. package/dist/component/connect/validators.d.ts.map +1 -1
  150. package/dist/component/connect/validators.js +2 -0
  151. package/dist/component/connect/validators.js.map +1 -1
  152. package/dist/component/earnings/triggers.d.ts +454 -2
  153. package/dist/component/earnings/triggers.d.ts.map +1 -1
  154. package/dist/component/payoutRelease/mutations.d.ts.map +1 -1
  155. package/dist/component/payoutRelease/mutations.js +6 -0
  156. package/dist/component/payoutRelease/mutations.js.map +1 -1
  157. package/dist/component/payoutRelease/queries.d.ts.map +1 -1
  158. package/dist/component/payoutRelease/queries.js +3 -1
  159. package/dist/component/payoutRelease/queries.js.map +1 -1
  160. package/dist/component/payoutSync/helpers.d.ts +44 -0
  161. package/dist/component/payoutSync/helpers.d.ts.map +1 -0
  162. package/dist/component/payoutSync/helpers.js +60 -0
  163. package/dist/component/payoutSync/helpers.js.map +1 -0
  164. package/dist/component/payoutSync/mutations.d.ts +51 -0
  165. package/dist/component/payoutSync/mutations.d.ts.map +1 -0
  166. package/dist/component/payoutSync/mutations.js +88 -0
  167. package/dist/component/payoutSync/mutations.js.map +1 -0
  168. package/dist/component/payoutSync/queries.d.ts +16 -0
  169. package/dist/component/payoutSync/queries.d.ts.map +1 -0
  170. package/dist/component/payoutSync/queries.js +11 -0
  171. package/dist/component/payoutSync/queries.js.map +1 -0
  172. package/dist/component/payoutSync/schema.d.ts +46 -0
  173. package/dist/component/payoutSync/schema.d.ts.map +1 -0
  174. package/dist/component/payoutSync/schema.js +22 -0
  175. package/dist/component/payoutSync/schema.js.map +1 -0
  176. package/dist/component/payoutSync/validators.d.ts +137 -0
  177. package/dist/component/payoutSync/validators.d.ts.map +1 -0
  178. package/dist/component/payoutSync/validators.js +19 -0
  179. package/dist/component/payoutSync/validators.js.map +1 -0
  180. package/dist/component/schema.d.ts +268 -1
  181. package/dist/component/schema.js +11 -0
  182. package/dist/component/schema.js.map +1 -1
  183. package/dist/component/settlement/clawbackSchema.d.ts +161 -0
  184. package/dist/component/settlement/clawbackSchema.d.ts.map +1 -0
  185. package/dist/component/settlement/clawbackSchema.js +73 -0
  186. package/dist/component/settlement/clawbackSchema.js.map +1 -0
  187. package/dist/component/settlement/clawbacks.d.ts +55 -0
  188. package/dist/component/settlement/clawbacks.d.ts.map +1 -0
  189. package/dist/component/settlement/clawbacks.js +146 -0
  190. package/dist/component/settlement/clawbacks.js.map +1 -0
  191. package/dist/component/settlement/mutations.d.ts +16 -0
  192. package/dist/component/settlement/mutations.d.ts.map +1 -1
  193. package/dist/component/settlement/mutations.js +48 -0
  194. package/dist/component/settlement/mutations.js.map +1 -1
  195. package/dist/component/settlement/queries.d.ts +53 -0
  196. package/dist/component/settlement/queries.d.ts.map +1 -1
  197. package/dist/component/settlement/queries.js +26 -0
  198. package/dist/component/settlement/queries.js.map +1 -1
  199. package/dist/react/workflows/AffiliateRecovery.d.ts +46 -0
  200. package/dist/react/workflows/AffiliateRecovery.d.ts.map +1 -0
  201. package/dist/react/workflows/AffiliateRecovery.js +84 -0
  202. package/dist/react/workflows/AffiliateRecovery.js.map +1 -0
  203. package/dist/react/workflows/index.d.ts +2 -0
  204. package/dist/react/workflows/index.d.ts.map +1 -1
  205. package/dist/react/workflows/index.js +1 -0
  206. package/dist/react/workflows/index.js.map +1 -1
  207. package/dist/testing/affiliate-recovery.d.ts +10 -0
  208. package/dist/testing/affiliate-recovery.d.ts.map +1 -0
  209. package/dist/testing/affiliate-recovery.js +126 -0
  210. package/dist/testing/affiliate-recovery.js.map +1 -0
  211. package/dist/testing/index.d.ts +2 -0
  212. package/dist/testing/index.d.ts.map +1 -1
  213. package/dist/testing/index.js +1 -0
  214. package/dist/testing/index.js.map +1 -1
  215. package/docs/affiliate-clawbacks.md +306 -0
  216. package/docs/read-contracts.md +40 -5
  217. package/docs/settlement.md +17 -0
  218. package/package.json +2 -1
  219. package/src/client/affiliateRecovery/disputes.ts +62 -0
  220. package/src/client/affiliateRecovery/engine.ts +196 -0
  221. package/src/client/affiliateRecovery/payoutGate.ts +65 -0
  222. package/src/client/affiliateRecovery/provider.ts +127 -0
  223. package/src/client/affiliateRecovery/review.ts +117 -0
  224. package/src/client/affiliateRecovery/validators.ts +38 -0
  225. package/src/client/affiliateRecovery.ts +173 -0
  226. package/src/client/connect/commissionReceipts.test.ts +157 -0
  227. package/src/client/connect/commissionReceipts.ts +23 -0
  228. package/src/client/connect/payoutSync.test.ts +313 -0
  229. package/src/client/connect/payoutSync.ts +93 -0
  230. package/src/client/connect/payoutSyncTestHelpers.ts +82 -0
  231. package/src/client/connect/payoutSyncValidation.ts +74 -0
  232. package/src/client/connect/payouts.ts +75 -15
  233. package/src/client/connect/refundPrincipal.test.ts +46 -0
  234. package/src/client/connect/refundPrincipal.ts +26 -0
  235. package/src/client/connect/reversalExecution.ts +130 -0
  236. package/src/client/connect/transfers.test.ts +3 -3
  237. package/src/client/connect/transfers.ts +93 -92
  238. package/src/client/index.ts +20 -18
  239. package/src/client/readContracts.ts +12 -0
  240. package/src/client/settlement/execution.test.ts +5 -0
  241. package/src/client/settlement/execution.ts +50 -3
  242. package/src/client/settlement/reconciliation.ts +20 -2
  243. package/src/client/webhooks/disputeRecovery.ts +74 -0
  244. package/src/client/webhooks/processors.test.ts +36 -53
  245. package/src/client/webhooks/processors.ts +18 -31
  246. package/src/component/_generated/api.ts +34 -0
  247. package/src/component/_generated/component.ts +696 -1
  248. package/src/component/affiliate-clawbacks.test.ts +370 -0
  249. package/src/component/affiliate-component-pagination.test.ts +83 -0
  250. package/src/component/affiliate-direct-payout.test.ts +160 -0
  251. package/src/component/affiliate-recovery-lifecycle.test.ts +587 -0
  252. package/src/component/affiliateEarnings/mutations.ts +196 -0
  253. package/src/component/affiliatePayouts/mutations.ts +121 -0
  254. package/src/component/affiliatePayouts/queries.ts +28 -0
  255. package/src/component/affiliatePayouts/recipientGuard.ts +42 -0
  256. package/src/component/affiliateRecovery/accounting.ts +111 -0
  257. package/src/component/affiliateRecovery/mutations.ts +275 -0
  258. package/src/component/affiliateRecovery/queries.ts +138 -0
  259. package/src/component/affiliateRecovery/schema.ts +86 -0
  260. package/src/component/connect/mutations.ts +13 -190
  261. package/src/component/connect/payoutWrites.ts +25 -0
  262. package/src/component/connect/queries.ts +4 -47
  263. package/src/component/connect/reversalClaims.ts +232 -0
  264. package/src/component/connect/reversalDiagnostics.ts +61 -0
  265. package/src/component/connect/reversalReceipts.ts +147 -0
  266. package/src/component/connect/schema.ts +3 -3
  267. package/src/component/connect/validators.ts +2 -0
  268. package/src/component/payout-recovery-integration.test.ts +172 -0
  269. package/src/component/payout-sync.test.ts +117 -0
  270. package/src/component/payoutRelease/mutations.ts +9 -0
  271. package/src/component/payoutRelease/queries.ts +3 -1
  272. package/src/component/payoutSync/helpers.ts +86 -0
  273. package/src/component/payoutSync/mutations.ts +102 -0
  274. package/src/component/payoutSync/queries.ts +10 -0
  275. package/src/component/payoutSync/schema.ts +25 -0
  276. package/src/component/payoutSync/validators.ts +21 -0
  277. package/src/component/schema.ts +20 -0
  278. package/src/component/settlement/clawbackSchema.ts +78 -0
  279. package/src/component/settlement/clawbacks.ts +233 -0
  280. package/src/component/settlement/mutations.ts +54 -0
  281. package/src/component/settlement/queries.ts +35 -0
  282. package/src/component/summary-arithmetic.test.ts +9 -4
  283. package/src/react/workflows/AffiliateRecovery.test.tsx +106 -0
  284. package/src/react/workflows/AffiliateRecovery.tsx +273 -0
  285. package/src/react/workflows/index.ts +9 -0
  286. package/src/testing/affiliate-recovery.ts +137 -0
  287. package/src/testing/index.ts +2 -0
@@ -1,3 +1,9 @@
1
+ import { reconcileAffiliateCharge } from "../affiliateRecovery/engine.js";
2
+ import {
3
+ executeClaimedSlices,
4
+ type ClaimedSlice,
5
+ type ReversalClaim,
6
+ } from "./reversalExecution.js";
1
7
  import { recoverSettlementTransfers } from "../settlement/reconciliation.js";
2
8
  import { executeResolvedTransfers } from "../settlement/execution.js";
3
9
  import type Stripe from "stripe";
@@ -19,90 +25,22 @@ type LedgerTransfer = {
19
25
  currency?: string;
20
26
  };
21
27
 
22
- type ClaimedSlice = {
23
- stripeTransferId: string;
24
- /** Cumulative reversed amount this slice starts from. */
25
- from: number;
26
- /** Cumulative reversed amount this slice raises the leg to. */
27
- to: number;
28
- /** The leg's confirmed `reversedAmount` at claim time. */
29
- confirmed: number;
30
- };
31
-
32
- type ReversalClaim = { replay: boolean; slices: ClaimedSlice[] };
33
-
34
- /**
35
- * Execute the slices an operation claimed via `claimReversalSlices` (BTS-63).
36
- * The claim already reserved this money atomically, so all that's left is
37
- * moving it: one `bs_rev_<operationId>_<transferId>` reversal per slice, then
38
- * confirming the new cumulative total in the ledger. A replayed claim always
39
- * re-sends byte-identical params under the same key, which Stripe answers
40
- * from its idempotency cache without moving money twice.
41
- *
42
- * Because Stripe prunes idempotency keys after ~24h (and a crash can land
43
- * between the Stripe call and the ledger confirm), a replay first checks the
44
- * transfer's live `amount_reversed`: if the slice's money already moved, it
45
- * just confirms the ledger instead of re-sending the reversal.
46
- *
47
- * A slice only executes once every predecessor slice on its leg is confirmed
48
- * (`confirmed ≥ from`). Without that gate, a sibling operation landing behind
49
- * a transiently-failed claim would execute and confirm PAST the hole; the
50
- * failed operation's retry would then see `confirmed ≥ to` and silently skip
51
- * — its money would never move while the ledger says it did. Throwing instead
52
- * fails this operation's webhook event, and at-least-once delivery retries
53
- * both operations until the predecessor fills its hole and each claimed slice
54
- * has actually moved. The ledger's `reversedAmount` therefore always covers a
55
- * solid, hole-free prefix of the reversed axis — which is exactly what makes
56
- * the `confirmed`/`amount_reversed` checks above sound.
57
- */
58
- async function executeClaimedSlices(
59
- stripe: Stripe,
28
+ async function trackedAffiliateTransfers(
60
29
  component: Component,
61
30
  ctx: RunCtx,
62
- operationId: string,
63
- claim: ReversalClaim,
64
- ): Promise<{ reversals: { stripeTransferId: string; amount: number }[] }> {
65
- const reversals: { stripeTransferId: string; amount: number }[] = [];
66
- for (const slice of claim.slices) {
67
- if (slice.confirmed >= slice.to) continue; // executed and recorded already
68
-
69
- if (slice.confirmed < slice.from) {
70
- throw new Error(
71
- `reversal slice [${slice.from},${slice.to}) on ${slice.stripeTransferId} ` +
72
- `blocked: predecessor claim unexecuted (confirmed=${slice.confirmed})`,
73
- );
74
- }
75
-
76
- if (claim.replay) {
77
- const transfer = await stripe.transfers.retrieve(slice.stripeTransferId);
78
- if ((transfer.amount_reversed ?? 0) >= slice.to) {
79
- // The money moved on a previous attempt that died before confirming.
80
- await runMutationOrThrow(
81
- ctx,
82
- componentRef(component, "connect/mutations/recordTransferReversal"),
83
- {
84
- stripeTransferId: slice.stripeTransferId,
85
- reversedAmount: slice.to,
86
- },
87
- );
88
- continue;
89
- }
90
- }
91
-
92
- const amount = slice.to - slice.from;
93
- await stripe.transfers.createReversal(
94
- slice.stripeTransferId,
95
- { amount },
96
- { idempotencyKey: `bs_rev_${operationId}_${slice.stripeTransferId}` },
97
- );
98
- await runMutationOrThrow(
99
- ctx,
100
- componentRef(component, "connect/mutations/recordTransferReversal"),
101
- { stripeTransferId: slice.stripeTransferId, reversedAmount: slice.to },
102
- );
103
- reversals.push({ stripeTransferId: slice.stripeTransferId, amount });
104
- }
105
- return { reversals };
31
+ sourceChargeId: string,
32
+ ) {
33
+ const rows = (await ctx.runQuery(
34
+ componentRef(component, "affiliateRecovery/queries/chargeObligations"),
35
+ { sourceChargeId },
36
+ )) as { accounted?: boolean; stripeTransferId?: string }[] | null;
37
+ return new Set(
38
+ Array.isArray(rows)
39
+ ? rows
40
+ .filter((row) => row.accounted && row.stripeTransferId)
41
+ .map((row) => row.stripeTransferId!)
42
+ : [],
43
+ );
106
44
  }
107
45
 
108
46
  /** A wedged reversal claim surfaced by {@link listWedgedReversalClaims}. */
@@ -120,6 +58,7 @@ type ReversalOpRecord = {
120
58
  sourceChargeId: string;
121
59
  slices: { stripeTransferId: string; from: number; to: number }[];
122
60
  _creationTime: number;
61
+ commissionExposure?: { kind: "refund" | "dispute" };
123
62
  };
124
63
 
125
64
  /** Stripe prunes idempotency keys after ~24h; a release must wait past this. */
@@ -212,19 +151,43 @@ export async function reclaimReversalClaim(
212
151
  const leg = (await ctx.runQuery(
213
152
  componentRef(component, "connect/queries/getTransferByStripeId"),
214
153
  { stripeTransferId: s.stripeTransferId },
215
- )) as { reversedAmount?: number } | null;
216
- slices.push({ ...s, confirmed: leg?.reversedAmount ?? 0 });
154
+ )) as { reversedAmount?: number; role?: string } | null;
155
+ slices.push({
156
+ ...s,
157
+ confirmed: leg?.reversedAmount ?? 0,
158
+ ...(op.commissionExposure
159
+ ? { commissionTracked: leg?.role === "affiliate" }
160
+ : {}),
161
+ });
217
162
  }
163
+ const tracked = op.commissionExposure
164
+ ? null
165
+ : await trackedAffiliateTransfers(component, ctx, op.sourceChargeId);
166
+ const usesRecoveryLedger = !!op.commissionExposure || !!tracked?.size;
218
167
  const { reversals } = await executeClaimedSlices(
219
168
  stripe,
220
169
  component,
221
170
  ctx,
222
171
  op.operationId,
223
- { replay: true, slices },
172
+ {
173
+ replay: true,
174
+ slices: slices.filter(
175
+ (s) => !s.commissionTracked && !tracked?.has(s.stripeTransferId),
176
+ ),
177
+ },
224
178
  );
179
+ if (usesRecoveryLedger)
180
+ await reconcileAffiliateCharge(stripe, component, ctx, op.sourceChargeId);
225
181
  return { mode: "reexecute", reversals };
226
182
  }
227
183
 
184
+ if (
185
+ op.commissionExposure ||
186
+ (await trackedAffiliateTransfers(component, ctx, op.sourceChargeId)).size
187
+ )
188
+ throw new Error(
189
+ "Affiliate exposure audit claims cannot be released; use affiliateRecovery.retry or reviewLegacy.",
190
+ );
228
191
  // release: age-gate against the idempotency window, then verify live Stripe.
229
192
  const minAgeMs = opts.minAgeMs ?? REVERSAL_IDEMPOTENCY_WINDOW_MS;
230
193
  const now = opts.now ?? Date.now();
@@ -306,6 +269,13 @@ export async function reverseTransfers(
306
269
  }
307
270
 
308
271
  await recoverSettlementTransfers(stripe, component, ctx, opts.sourceChargeId);
272
+ if (
273
+ (await trackedAffiliateTransfers(component, ctx, opts.sourceChargeId)).size
274
+ )
275
+ throw new Error(
276
+ "Use affiliateRecovery.retry for tracked affiliate commission recovery.",
277
+ );
278
+
309
279
  if (opts.operationId) {
310
280
  const claim = (await runMutationOrThrow(
311
281
  ctx,
@@ -389,9 +359,9 @@ export async function reverseTransfers(
389
359
  }
390
360
 
391
361
  /**
392
- * Reverse a split sale's transfers pro-rata to the cumulatively refunded
393
- * amount (BTS-34) — the refund counterpart of the dispute clawback. Every leg
394
- * converges on a cumulative reversal TARGET, `round(leg × amountRefunded /
362
+ * Reverse a split sale's transfers pro-rata to cumulative succeeded refunds.
363
+ * Affiliate legs retain the larger refund/dispute commission exposure. Other
364
+ * legs converge on a cumulative reversal TARGET, `round(leg × amountRefunded /
395
365
  * chargeAmount)`, and only the delta above the ledger's recorded
396
366
  * `reversedAmount` is reversed. Delta-to-target (rather than percent-of-
397
367
  * original like {@link reverseTransfers}) is what makes at-least-once webhook
@@ -416,7 +386,7 @@ export async function reverseTransfersForRefund(
416
386
  refundId: string;
417
387
  /** The original charge amount (minor units). */
418
388
  chargeAmount: number;
419
- /** Stripe's cumulative `charge.amount_refunded`, including this refund. */
389
+ /** Cumulative principal of succeeded refunds; exclude pending/failed refunds. */
420
390
  amountRefunded: number;
421
391
  },
422
392
  ): Promise<{ reversals: { stripeTransferId: string; amount: number }[] }> {
@@ -431,6 +401,13 @@ export async function reverseTransfersForRefund(
431
401
  {
432
402
  operationId: opts.refundId,
433
403
  sourceChargeId: opts.sourceChargeId,
404
+ commissionExposure: {
405
+ kind: "refund",
406
+ refundId: opts.refundId,
407
+ chargeAmount: opts.chargeAmount,
408
+ principal: opts.amountRefunded,
409
+ finalized: true,
410
+ },
434
411
  mode: {
435
412
  kind: "fraction" as const,
436
413
  chargeAmount: opts.chargeAmount,
@@ -438,9 +415,19 @@ export async function reverseTransfersForRefund(
438
415
  },
439
416
  },
440
417
  )) as ReversalClaim;
441
- return executeClaimedSlices(stripe, component, ctx, opts.refundId, claim);
418
+ const result = await executeClaimedSlices(
419
+ stripe,
420
+ component,
421
+ ctx,
422
+ opts.refundId,
423
+ { ...claim, slices: claim.slices.filter((s) => !s.commissionTracked) },
424
+ );
425
+ await reconcileAffiliateCharge(stripe, component, ctx, opts.sourceChargeId);
426
+ return result;
442
427
  }
443
428
 
429
+ export { reverseTransfersForDispute } from "../affiliateRecovery/disputes.js";
430
+
444
431
  /**
445
432
  * Webhook-driven split transfer engine (BTS-22). After a charge succeeds for a
446
433
  * separate-charges sale, fan funds out to each split recipient: compute the
@@ -489,7 +476,14 @@ export async function reinstateTransfers(
489
476
  stripe: Stripe,
490
477
  component: Component,
491
478
  ctx: RunCtx,
492
- opts: { sourceChargeId: string; operationId: string; currency: string },
479
+ opts: {
480
+ sourceChargeId: string;
481
+ operationId: string;
482
+ currency: string;
483
+ excludeAffiliates?: boolean;
484
+ chargeAmount?: number;
485
+ refundedPrincipal?: number;
486
+ },
493
487
  ): Promise<{ reinstated: { destinationAccountId: string; amount: number }[] }> {
494
488
  const transfers = ((await ctx.runQuery(
495
489
  componentRef(component, "connect/queries/listTransfersByCharge"),
@@ -498,7 +492,14 @@ export async function reinstateTransfers(
498
492
 
499
493
  const reinstated: { destinationAccountId: string; amount: number }[] = [];
500
494
  for (const t of transfers) {
501
- const amount = t.reversedAmount ?? 0;
495
+ if (opts.excludeAffiliates && t.role === "affiliate") continue;
496
+ const independentRefund = opts.chargeAmount
497
+ ? Math.round(
498
+ t.amount *
499
+ Math.min(1, (opts.refundedPrincipal ?? 0) / opts.chargeAmount),
500
+ )
501
+ : 0;
502
+ const amount = Math.max(0, (t.reversedAmount ?? 0) - independentRefund);
502
503
  if (amount <= 0) continue; // nothing was clawed back from this leg
503
504
  // Use the original leg's currency; fall back to the dispute currency for
504
505
  // legacy rows that predate the stored currency — never assume USD.
@@ -1,3 +1,10 @@
1
+ import { AffiliateRecoveryClient } from "./affiliateRecovery.js";
2
+ export * from "./affiliateRecovery.js";
3
+ import * as payoutSyncImpl from "./connect/payoutSync.js";
4
+ export type {
5
+ PayoutSyncOptions,
6
+ PayoutSyncResult,
7
+ } from "./connect/payoutSync.js";
1
8
  import { PayoutPolicyClient } from "./payoutPolicy.js";
2
9
  export * from "./payoutPolicy.js";
3
10
  import type { CreateCheckoutSessionOptions } from "./billing/checkoutOptions.js";
@@ -76,12 +83,8 @@ export type {
76
83
  import { internalActionGeneric, internalMutationGeneric } from "convex/server";
77
84
  import { v } from "convex/values";
78
85
  import Stripe from "stripe";
79
-
80
86
  import type { ComponentApi } from "../component/_generated/component.js";
81
- import type {
82
- DisputeStatus,
83
- PayoutStatus,
84
- } from "../component/connect/validators.js";
87
+ import type { DisputeStatus } from "../component/connect/validators.js";
85
88
  import type { SplitRecipient } from "../component/lib/fees.js";
86
89
  import * as checkoutImpl from "./billing/checkout.js";
87
90
  import * as invoicesImpl from "./billing/invoices.js";
@@ -143,7 +146,6 @@ import type { AsyncHookName, TriggerDispatcherName } from "./types/triggers.js";
143
146
  import * as dataImpl from "./utils/data.js";
144
147
  import type { StripeMode } from "./utils/stripeDashboardUrl.js";
145
148
  import * as webhookEndpointsImpl from "./utils/webhookEndpoints.js";
146
-
147
149
  // Re-export standalone webhook registration
148
150
  export { registerRoutes } from "./webhooks.js";
149
151
  export {
@@ -152,13 +154,11 @@ export {
152
154
  STRIPE_DASHBOARD_BASE_URL,
153
155
  StripeDashboardResourcePath,
154
156
  } from "./utils/stripeDashboardUrl.js";
155
-
156
157
  // Re-export payment method utilities
157
158
  export {
158
159
  getPaymentMethodCard,
159
160
  getPaymentMethodOwner,
160
161
  } from "./connect/paymentMethodUtils.js";
161
-
162
162
  // Re-export webhook event constants and types
163
163
  export {
164
164
  ALL_BETTER_STRIPE_EVENTS,
@@ -169,7 +169,6 @@ export type {
169
169
  BetterStripeV2WebhookEvent,
170
170
  BetterStripeWebhookEvent,
171
171
  } from "./utils/webhookEndpoints.js";
172
-
173
172
  // Re-export types for consumers
174
173
  export type {
175
174
  BetterStripeOptions,
@@ -208,7 +207,6 @@ export type {
208
207
  V2ThinEvent,
209
208
  WebhookActionCtx,
210
209
  } from "./types.js";
211
-
212
210
  export type BetterStripeComponent = ComponentApi;
213
211
 
214
212
  // BETTER STRIPE CLIENT
@@ -1149,7 +1147,7 @@ export class BetterStripe extends ReadContractClient {
1149
1147
  // PAYOUT METHODS
1150
1148
 
1151
1149
  async createPayout(ctx: RunCtx, opts: payoutsImpl.CreatePayoutOptions) {
1152
- return payoutsImpl.createPayout(this.stripe(), ctx, opts);
1150
+ return payoutsImpl.createPayout(this.stripe(), ctx, opts, this.component);
1153
1151
  }
1154
1152
 
1155
1153
  /**
@@ -1255,6 +1253,9 @@ export class BetterStripe extends ReadContractClient {
1255
1253
  ) {
1256
1254
  return providerReads.getCouponFromStripe(this.stripe(), opts);
1257
1255
  }
1256
+ get affiliateRecovery() {
1257
+ return new AffiliateRecoveryClient(this.component, () => this.stripe());
1258
+ }
1258
1259
  get payoutPolicy() {
1259
1260
  return new PayoutPolicyClient(this.component, () => this.stripe());
1260
1261
  }
@@ -1300,15 +1301,16 @@ export class BetterStripe extends ReadContractClient {
1300
1301
  return payoutsImpl.getAccountBalance(this.stripe(), opts);
1301
1302
  }
1302
1303
 
1303
- async getPayout(ctx: RunCtx, opts: { payoutId: string }) {
1304
- return payoutsImpl.getPayout(this.component, ctx, opts);
1305
- }
1306
-
1307
- async listPayouts(
1304
+ async syncPayoutFromStripe(
1308
1305
  ctx: RunCtx,
1309
- opts?: { stripeAccountId?: string; status?: PayoutStatus; limit?: number },
1306
+ opts: payoutSyncImpl.PayoutSyncOptions,
1310
1307
  ) {
1311
- return payoutsImpl.listPayouts(this.component, ctx, opts);
1308
+ return payoutSyncImpl.syncPayoutFromStripe(
1309
+ this.stripe(),
1310
+ this.component,
1311
+ ctx,
1312
+ opts,
1313
+ );
1312
1314
  }
1313
1315
 
1314
1316
  // REFUND METHODS
@@ -1,3 +1,4 @@
1
+ import * as payoutsImpl from "./connect/payouts.js";
1
2
  import type { ComponentApi } from "../component/_generated/component.js";
2
3
  import type { PaginationOptions, PaginationResult } from "convex/server";
3
4
  import type { Component, RunCtx } from "./helpers.js";
@@ -51,6 +52,17 @@ export class ReadContractClient {
51
52
  );
52
53
  }
53
54
 
55
+ async getPayout(ctx: RunCtx, opts: { payoutId: string }) {
56
+ return payoutsImpl.getPayout(this.component, ctx, opts);
57
+ }
58
+
59
+ async listPayouts(
60
+ ctx: RunCtx,
61
+ opts?: { stripeAccountId?: string; status?: PayoutStatus; limit?: number },
62
+ ) {
63
+ return payoutsImpl.listPayouts(this.component, ctx, opts);
64
+ }
65
+
54
66
  async getAccountEarningsByCurrency(
55
67
  ctx: RunCtx,
56
68
  opts: { destinationAccountId: string; previewLimit?: number },
@@ -26,6 +26,11 @@ it("recovers provider success after a ledger failure even without an idempotency
26
26
  async (r: Record<symbol, string>, args: Record<string, unknown>) => {
27
27
  if (r[TO_REF].endsWith("/reserveSettlementAllocation"))
28
28
  return claimed ?? (claimed = { ...args });
29
+ if (
30
+ r[TO_REF].includes("affiliateRecovery/") ||
31
+ r[TO_REF].includes("affiliateEarnings/")
32
+ )
33
+ return undefined;
29
34
  if (fail) {
30
35
  fail = false;
31
36
  throw new Error("ledger unavailable");
@@ -1,3 +1,4 @@
1
+ import { reconcileAffiliateCharge } from "../affiliateRecovery/engine.js";
1
2
  import {
2
3
  sourceTransfers,
3
4
  matchingTransfer,
@@ -60,13 +61,34 @@ export async function executeResolvedTransfers(
60
61
  transfers: saved.transfers,
61
62
  paymentId: saved.paymentId,
62
63
  };
64
+ const earnings = new Map<string, { _id: string; releaseAmount: number }>();
65
+ for (const leg of claim.transfers)
66
+ if (leg.role === "affiliate" && leg.amount > 0) {
67
+ const earning = (await runMutationOrThrow(
68
+ ctx,
69
+ componentRef(component, "affiliateEarnings/mutations/reserveEarning"),
70
+ {
71
+ sourceChargeId: claim.sourceChargeId,
72
+ affiliateAccountId: leg.destinationAccountId,
73
+ },
74
+ )) as { _id: string; releaseAmount: number } | undefined;
75
+ if (earning?._id) earnings.set(leg.destinationAccountId, earning);
76
+ }
77
+ const effective = claim.transfers.map((leg) =>
78
+ leg.role === "affiliate" && earnings.has(leg.destinationAccountId)
79
+ ? {
80
+ ...leg,
81
+ amount: earnings.get(leg.destinationAccountId)!.releaseAmount,
82
+ }
83
+ : leg,
84
+ );
63
85
  const existing = ((await ctx.runQuery(
64
86
  componentRef(component, "connect/queries/listTransfersByCharge"),
65
87
  { sourceChargeId: claim.sourceChargeId },
66
88
  )) ?? []) as Ledger[];
67
89
  let provider = await sourceTransfers(stripe, claim.sourceChargeId);
68
90
  if (
69
- claim.transfers.some(
91
+ effective.some(
70
92
  (leg) =>
71
93
  leg.amount > 0 &&
72
94
  !existing.some(
@@ -87,8 +109,20 @@ export async function executeResolvedTransfers(
87
109
  ];
88
110
  }
89
111
  const executedTransfers: SettlementResult["executedTransfers"] = [];
90
- for (const leg of claim.transfers) {
91
- if (leg.amount === 0) continue;
112
+ for (const leg of effective) {
113
+ const earning =
114
+ leg.role === "affiliate"
115
+ ? earnings.get(leg.destinationAccountId)
116
+ : undefined;
117
+ if (leg.amount === 0) {
118
+ if (earning)
119
+ await runMutationOrThrow(
120
+ ctx,
121
+ componentRef(component, "affiliateEarnings/mutations/confirmEarning"),
122
+ { earningId: earning._id },
123
+ );
124
+ continue;
125
+ }
92
126
  const recorded = existing.find(
93
127
  (t) =>
94
128
  t.destinationAccountId === leg.destinationAccountId &&
@@ -123,6 +157,12 @@ export async function executeResolvedTransfers(
123
157
  transferId = transfer.id;
124
158
  await recordSettlementTransfer(component, ctx, claim, leg, transfer);
125
159
  }
160
+ if (earning)
161
+ await runMutationOrThrow(
162
+ ctx,
163
+ componentRef(component, "affiliateEarnings/mutations/confirmEarning"),
164
+ { earningId: earning._id, stripeTransferId: transferId },
165
+ );
126
166
  executedTransfers.push({
127
167
  stripeTransferId: transferId,
128
168
  destinationAccountId: leg.destinationAccountId,
@@ -130,5 +170,12 @@ export async function executeResolvedTransfers(
130
170
  amount: leg.amount,
131
171
  });
132
172
  }
173
+ if (earnings.size)
174
+ await reconcileAffiliateCharge(
175
+ stripe,
176
+ component,
177
+ ctx,
178
+ claim.sourceChargeId,
179
+ );
133
180
  return { ...claim, status: "settled", executedTransfers };
134
181
  }
@@ -97,9 +97,26 @@ export async function recoverSettlementTransfers(
97
97
  { sourceChargeId },
98
98
  )) as ResolvedSettlement | null;
99
99
  if (!claim || typeof claim.sourceChargeId !== "string") return;
100
+ const effective = [];
101
+ for (const leg of claim.transfers) {
102
+ const earning =
103
+ leg.role === "affiliate"
104
+ ? ((await ctx.runQuery(
105
+ componentRef(component, "affiliateRecovery/queries/getEarning"),
106
+ { sourceChargeId, affiliateAccountId: leg.destinationAccountId },
107
+ )) as { releaseAmount?: number } | null)
108
+ : null;
109
+ effective.push({
110
+ ...leg,
111
+ amount:
112
+ typeof earning?.releaseAmount === "number"
113
+ ? earning.releaseAmount
114
+ : leg.amount,
115
+ });
116
+ }
100
117
  let provider = await sourceTransfers(stripe, sourceChargeId);
101
118
  if (
102
- claim.transfers.some(
119
+ effective.some(
103
120
  (leg) => leg.amount > 0 && !matchingTransfer(provider, claim, leg),
104
121
  )
105
122
  )
@@ -111,7 +128,8 @@ export async function recoverSettlementTransfers(
111
128
  ].map((t) => [t.id, t]),
112
129
  ).values(),
113
130
  ];
114
- for (const leg of claim.transfers) {
131
+ for (const leg of effective) {
132
+ if (leg.amount === 0) continue;
115
133
  const transfer = matchingTransfer(provider, claim, leg);
116
134
  if (transfer)
117
135
  await recordSettlementTransfer(component, ctx, claim, leg, transfer);
@@ -0,0 +1,74 @@
1
+ import { recoverSettlementTransfers } from "../settlement/reconciliation.js";
2
+ import { componentRef } from "./helpers.js";
3
+ import type Stripe from "stripe";
4
+ import type { WebhookContext } from "./helpers.js";
5
+ import {
6
+ reinstateTransfers,
7
+ reverseTransfersForDispute,
8
+ } from "../connect/transfers.js";
9
+ import { successfulRefundPrincipal } from "../connect/refundPrincipal.js";
10
+ export async function reconcileDisputeMoney(
11
+ whCtx: WebhookContext,
12
+ dispute: Stripe.Dispute,
13
+ event: string,
14
+ originalCharge?: Stripe.Charge,
15
+ ) {
16
+ if (dispute.status.startsWith("warning_") || dispute.status === "prevented")
17
+ return;
18
+ const chargeId =
19
+ typeof dispute.charge === "string" ? dispute.charge : dispute.charge?.id;
20
+ if (
21
+ !chargeId ||
22
+ !["created", "funds_withdrawn", "closed", "funds_reinstated"].includes(
23
+ event,
24
+ )
25
+ )
26
+ return;
27
+ await recoverSettlementTransfers(
28
+ whCtx.stripe,
29
+ whCtx.component,
30
+ whCtx.ctx,
31
+ chargeId,
32
+ );
33
+ const legs = await whCtx.ctx.runQuery(
34
+ componentRef(whCtx.component, "connect/queries/listTransfersByCharge"),
35
+ { sourceChargeId: chargeId },
36
+ );
37
+ const obligations = await whCtx.ctx.runQuery(
38
+ componentRef(
39
+ whCtx.component,
40
+ "affiliateRecovery/queries/chargeObligations",
41
+ ),
42
+ { sourceChargeId: chargeId },
43
+ );
44
+ if (
45
+ (!Array.isArray(legs) || !legs.length) &&
46
+ (!Array.isArray(obligations) || !obligations.length)
47
+ )
48
+ return;
49
+ const won = event === "funds_reinstated" || dispute.status === "won";
50
+ const charge =
51
+ originalCharge ?? (await whCtx.stripe.charges.retrieve(chargeId));
52
+ const refundedPrincipal = await successfulRefundPrincipal(
53
+ whCtx.stripe,
54
+ chargeId,
55
+ );
56
+ await reverseTransfersForDispute(whCtx.stripe, whCtx.component, whCtx.ctx, {
57
+ sourceChargeId: chargeId,
58
+ operationId: dispute.id,
59
+ chargeAmount: charge.amount,
60
+ amount: dispute.amount,
61
+ finalized: dispute.status === "lost",
62
+ disputeState: won ? "won" : dispute.status === "lost" ? "lost" : "open",
63
+ refundedPrincipal,
64
+ });
65
+ if (won)
66
+ await reinstateTransfers(whCtx.stripe, whCtx.component, whCtx.ctx, {
67
+ sourceChargeId: chargeId,
68
+ operationId: dispute.id,
69
+ currency: dispute.currency,
70
+ excludeAffiliates: true,
71
+ chargeAmount: charge.amount,
72
+ refundedPrincipal,
73
+ });
74
+ }