@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
@@ -0,0 +1,587 @@
1
+ // @vitest-environment edge-runtime
2
+ import type Stripe from "stripe";
3
+ import { convexTest } from "convex-test";
4
+ import { describe, expect, it } from "vitest";
5
+ import { api } from "./_generated/api.js";
6
+ import schema from "./schema.js";
7
+ import type { Component, RunCtx } from "../client/helpers.js";
8
+ import {
9
+ reverseTransfers,
10
+ reclaimReversalClaim,
11
+ reverseTransfersForRefund,
12
+ reverseTransfersForDispute,
13
+ } from "../client/connect/transfers.js";
14
+ import { executeResolvedTransfers } from "../client/settlement/execution.js";
15
+ import { reconcileObligation } from "../client/affiliateRecovery/engine.js";
16
+ const modules = import.meta.glob("./**/*.*s");
17
+ const REF = Symbol.for("toReferencePath");
18
+ function component(path = "betterStripe"): Component {
19
+ return new Proxy(
20
+ {},
21
+ {
22
+ get: (_t, key) =>
23
+ key === REF
24
+ ? path
25
+ : typeof key === "string"
26
+ ? component(`${path}/${key}`)
27
+ : undefined,
28
+ },
29
+ ) as Component;
30
+ }
31
+ const list = <T>(values: T[]) => ({
32
+ then: <R>(resolve: (v: { data: T[]; has_more: boolean }) => R) =>
33
+ Promise.resolve(resolve({ data: values, has_more: false })),
34
+ async *[Symbol.asyncIterator]() {
35
+ yield* values;
36
+ },
37
+ });
38
+ async function setup() {
39
+ const t = convexTest(schema, modules);
40
+ const route = (ref: unknown) =>
41
+ (ref as Record<symbol, string>)[REF].replace("betterStripe/", "")
42
+ .split("/")
43
+ .reduce(
44
+ (node, key) => (node as Record<string, unknown>)[key],
45
+ api as unknown,
46
+ );
47
+ const query = t.query as unknown as (
48
+ ref: unknown,
49
+ args: unknown,
50
+ ) => Promise<unknown>;
51
+ const mutation = t.mutation as unknown as (
52
+ ref: unknown,
53
+ args: unknown,
54
+ ) => Promise<unknown>;
55
+ const ctx = {
56
+ runQuery: (ref: unknown, args: unknown) => query(route(ref), args),
57
+ runMutation: (ref: unknown, args: unknown) => mutation(route(ref), args),
58
+ } as unknown as RunCtx;
59
+ type Transfer = {
60
+ id: string;
61
+ amount: number;
62
+ currency: string;
63
+ destination: string;
64
+ source_transaction?: string;
65
+ transfer_group?: string;
66
+ amount_reversed: number;
67
+ metadata: Record<string, string>;
68
+ };
69
+ const transfers: Transfer[] = [],
70
+ receipts: {
71
+ id: string;
72
+ amount: number;
73
+ transfer: string;
74
+ metadata: Record<string, string>;
75
+ }[] = [];
76
+ let restoreInsufficient = false,
77
+ restoreTimeout = false;
78
+ let insufficient = false,
79
+ timeout = false,
80
+ enabled = true;
81
+ const keys = new Map<string, unknown>();
82
+ const provider = {
83
+ v2: {
84
+ core: {
85
+ accounts: {
86
+ retrieve: async () => ({
87
+ configuration: {
88
+ recipient: {
89
+ capabilities: {
90
+ stripe_balance: {
91
+ stripe_transfers: {
92
+ status: enabled ? "active" : "restricted",
93
+ },
94
+ },
95
+ },
96
+ },
97
+ },
98
+ }),
99
+ },
100
+ },
101
+ },
102
+ charges: { retrieve: async () => ({ amount: 10000, created: 1 }) },
103
+ transfers: {
104
+ list: (args: { transfer_group?: string }) =>
105
+ list(
106
+ transfers.filter(
107
+ (r) =>
108
+ !args.transfer_group || r.transfer_group === args.transfer_group,
109
+ ),
110
+ ),
111
+ retrieve: async (id: string) => {
112
+ const row = transfers.find((r) => r.id === id);
113
+ if (!row) throw new Error("Unknown transfer");
114
+ return row;
115
+ },
116
+ listReversals: (id: string) =>
117
+ list(receipts.filter((r) => r.transfer === id)),
118
+ create: async (
119
+ args: Omit<Transfer, "id" | "amount_reversed">,
120
+ opts: { idempotencyKey: string },
121
+ ) => {
122
+ if (keys.has(opts.idempotencyKey)) return keys.get(opts.idempotencyKey);
123
+ if (restoreInsufficient && args.metadata?.bsRecoveryOperation)
124
+ throw {
125
+ type: "StripeInvalidRequestError",
126
+ code: "balance_insufficient",
127
+ message: "Platform restoration funds unavailable.",
128
+ };
129
+ const row = {
130
+ ...args,
131
+ id: `tr_${transfers.length + 1}`,
132
+ amount_reversed: 0,
133
+ };
134
+ transfers.push(row);
135
+ keys.set(opts.idempotencyKey, row);
136
+ if (restoreTimeout && args.metadata?.bsRecoveryOperation) {
137
+ restoreTimeout = false;
138
+ throw {
139
+ type: "StripeConnectionError",
140
+ message: "Restoration timeout after success.",
141
+ };
142
+ }
143
+ return row;
144
+ },
145
+ createReversal: async (
146
+ id: string,
147
+ args: { amount: number; metadata: Record<string, string> },
148
+ opts: { idempotencyKey: string },
149
+ ) => {
150
+ if (keys.has(opts.idempotencyKey)) return keys.get(opts.idempotencyKey);
151
+ if (insufficient)
152
+ throw {
153
+ type: "StripeInvalidRequestError",
154
+ code: "balance_insufficient",
155
+ message: "Insufficient connected balance after external payout.",
156
+ };
157
+ const transfer = transfers.find((r) => r.id === id)!;
158
+ if (args.amount > transfer.amount - transfer.amount_reversed)
159
+ throw new Error("Excess reversal");
160
+ transfer.amount_reversed += args.amount;
161
+ const receipt = {
162
+ ...args,
163
+ transfer: id,
164
+ id: `trr_${receipts.length + 1}`,
165
+ };
166
+ receipts.push(receipt);
167
+ keys.set(opts.idempotencyKey, receipt);
168
+ if (timeout) {
169
+ timeout = false;
170
+ throw {
171
+ type: "StripeConnectionError",
172
+ message: "Timeout after success.",
173
+ };
174
+ }
175
+ return receipt;
176
+ },
177
+ },
178
+ };
179
+ const stripe = provider as unknown as Stripe,
180
+ comp = component();
181
+ const settle = (charge: string, commission = 1001, currency = "usd") =>
182
+ executeResolvedTransfers(stripe, comp, ctx, {
183
+ sourceChargeId: charge,
184
+ sourceInvoiceId: `in_${charge}`,
185
+ paymentIntentId: `pi_${charge}`,
186
+ gross: 10000,
187
+ currency,
188
+ processingFee: 0,
189
+ platformNet: 10000 - commission,
190
+ transfers: [
191
+ {
192
+ destinationAccountId: "acct_a",
193
+ amount: commission,
194
+ role: "affiliate",
195
+ },
196
+ ],
197
+ });
198
+ const refund = (charge: string, principal = 10000) =>
199
+ reverseTransfersForRefund(stripe, comp, ctx, {
200
+ sourceChargeId: charge,
201
+ chargeAmount: 10000,
202
+ refundId: `re_${charge}_${principal}`,
203
+ amountRefunded: principal,
204
+ });
205
+ const dispute = (
206
+ charge: string,
207
+ state: "open" | "won" | "lost",
208
+ refundedPrincipal = 0,
209
+ amount = 10000,
210
+ ) =>
211
+ reverseTransfersForDispute(stripe, comp, ctx, {
212
+ sourceChargeId: charge,
213
+ chargeAmount: 10000,
214
+ operationId: `dp_${charge}`,
215
+ amount,
216
+ finalized: state === "lost",
217
+ disputeState: state,
218
+ refundedPrincipal,
219
+ });
220
+ const snapshot = (currency = "usd") =>
221
+ t.query(api.affiliateRecovery.queries.recoverySnapshot, {
222
+ affiliateAccountId: "acct_a",
223
+ currency,
224
+ paginationOpts: { numItems: 100, cursor: null },
225
+ });
226
+ return {
227
+ t,
228
+ ctx,
229
+ stripe,
230
+ comp,
231
+ transfers,
232
+ receipts,
233
+ settle,
234
+ refund,
235
+ dispute,
236
+ snapshot,
237
+ failRestoration: () => {
238
+ restoreInsufficient = true;
239
+ },
240
+ allowRestoration: () => {
241
+ restoreInsufficient = false;
242
+ },
243
+ timeoutRestoration: () => {
244
+ restoreTimeout = true;
245
+ },
246
+ failBalance: () => {
247
+ insufficient = true;
248
+ },
249
+ allowBalance: () => {
250
+ insufficient = false;
251
+ },
252
+ timeout: () => {
253
+ timeout = true;
254
+ },
255
+ disable: () => {
256
+ enabled = false;
257
+ },
258
+ };
259
+ }
260
+ describe("affiliate recovery lifecycle", () => {
261
+ it("recovers the exact original commission and records a receipt once", async () => {
262
+ const f = await setup();
263
+ await f.settle("ch_1");
264
+ await f.refund("ch_1", 5000);
265
+ await f.refund("ch_1", 5000);
266
+ expect(f.receipts.map((r) => r.amount)).toEqual([501]);
267
+ expect(
268
+ await f.t.query(api.connect.queries.getAccountEarningsByCurrency, {
269
+ destinationAccountId: "acct_a",
270
+ }),
271
+ ).toMatchObject({
272
+ totals: [{ currency: "usd", gross: 1001, reversed: 501, net: 500 }],
273
+ });
274
+ expect((await f.snapshot()).balance).toMatchObject({
275
+ recoveredAmount: 501,
276
+ outstandingDebt: 0,
277
+ reservedRecovery: 0,
278
+ });
279
+ });
280
+ it("carries paid-out debt forward and atomically offsets future commissions in the same currency", async () => {
281
+ const f = await setup();
282
+ await f.settle("ch_1");
283
+ f.failBalance();
284
+ await f.refund("ch_1");
285
+ expect((await f.snapshot()).balance).toMatchObject({
286
+ outstandingDebt: 1001,
287
+ recoveredAmount: 0,
288
+ });
289
+ await f.settle("ch_jpy", 700, "jpy");
290
+ expect((await f.snapshot()).balance!.outstandingDebt).toBe(1001);
291
+ await Promise.all([f.settle("ch_2", 700), f.settle("ch_3", 700)]);
292
+ const result = await f.snapshot();
293
+ expect(result.balance).toMatchObject({
294
+ outstandingDebt: 0,
295
+ offsetAmount: 1001,
296
+ withheldAmount: 1001,
297
+ netPayable: 0,
298
+ });
299
+ expect(
300
+ f.transfers
301
+ .filter((r) => ["ch_2", "ch_3"].includes(r.source_transaction ?? ""))
302
+ .reduce((s, r) => s + r.amount, 0),
303
+ ).toBe(399);
304
+ f.allowBalance();
305
+ await reconcileObligation(
306
+ f.stripe,
307
+ f.comp,
308
+ f.ctx,
309
+ result.obligations.page.find((r) => r.sourceChargeId === "ch_1")!._id,
310
+ );
311
+ expect(f.receipts).toHaveLength(0);
312
+ });
313
+ it("preserves a fully withheld invoice commission when that invoice is refunded", async () => {
314
+ const f = await setup();
315
+ await f.settle("ch_1");
316
+ f.failBalance();
317
+ await f.refund("ch_1");
318
+ await f.settle("ch_2", 700);
319
+ await f.refund("ch_2");
320
+ expect((await f.snapshot()).balance).toMatchObject({
321
+ outstandingDebt: 1001,
322
+ offsetAmount: 700,
323
+ });
324
+ expect(f.transfers.some((r) => r.source_transaction === "ch_2")).toBe(
325
+ false,
326
+ );
327
+ expect(
328
+ (await f.snapshot()).obligations.page.find(
329
+ (r) => r.sourceChargeId === "ch_2",
330
+ ),
331
+ ).toMatchObject({
332
+ originalCommission: 700,
333
+ releasedCommission: 0,
334
+ finalizedAmount: 700,
335
+ debtAmount: 700,
336
+ });
337
+ });
338
+ it("blocks release on an unknown outcome and reconciles provider success without duplicate recovery", async () => {
339
+ const f = await setup();
340
+ await f.settle("ch_1");
341
+ f.timeout();
342
+ await f.refund("ch_1");
343
+ expect((await f.snapshot()).balance!.reservedRecovery).toBe(1001);
344
+ await expect(f.settle("ch_2", 700)).rejects.toThrow(
345
+ "pending reconciliation",
346
+ );
347
+ await f.refund("ch_1");
348
+ await f.settle("ch_2", 700);
349
+ expect(f.receipts).toHaveLength(1);
350
+ expect((await f.snapshot()).balance).toMatchObject({
351
+ outstandingDebt: 0,
352
+ reservedRecovery: 0,
353
+ recoveredAmount: 1001,
354
+ offsetAmount: 0,
355
+ });
356
+ });
357
+ it.each([true, false])(
358
+ "retains independent refunds on a dispute win (refund first %s)",
359
+ async (first) => {
360
+ const f = await setup();
361
+ await f.settle("ch_1");
362
+ if (first) await f.refund("ch_1", 5000);
363
+ await f.dispute("ch_1", "open");
364
+ if (!first) await f.refund("ch_1", 5000);
365
+ await f.dispute("ch_1", "won", 5000);
366
+ await f.dispute("ch_1", "won", 5000);
367
+ await f.dispute("ch_1", "open", 5000);
368
+ expect((await f.snapshot()).balance).toMatchObject({
369
+ recoveredAmount: 1001,
370
+ restoredAmount: 500,
371
+ outstandingDebt: 0,
372
+ restorationDue: 0,
373
+ });
374
+ expect(
375
+ f.transfers
376
+ .filter((r) => r.metadata.bsRecoveryOperation)
377
+ .map((r) => r.amount),
378
+ ).toEqual([500]);
379
+ expect(
380
+ await f.t.query(api.connect.queries.getAccountEarningsByCurrency, {
381
+ destinationAccountId: "acct_a",
382
+ }),
383
+ ).toMatchObject({
384
+ totals: [{ currency: "usd", gross: 1501, reversed: 1001, net: 500 }],
385
+ });
386
+ expect((await f.snapshot()).obligations.page[0]).toMatchObject({
387
+ disputeState: "won",
388
+ finalizedAmount: 501,
389
+ provisionalAmount: 0,
390
+ });
391
+ },
392
+ );
393
+ it("finalizes a partial lost dispute without another debit", async () => {
394
+ const f = await setup();
395
+ await f.settle("ch_1");
396
+ await f.dispute("ch_1", "open", 0, 2500);
397
+ await f.dispute("ch_1", "lost", 0, 2500);
398
+ await f.dispute("ch_1", "open", 0, 2500);
399
+ expect(f.receipts.map((r) => r.amount)).toEqual([250]);
400
+ expect((await f.snapshot()).obligations.page[0]).toMatchObject({
401
+ finalizedAmount: 250,
402
+ provisionalAmount: 0,
403
+ });
404
+ });
405
+ it("cancels unrecovered debt and restores only actual offsets on a win", async () => {
406
+ const f = await setup();
407
+ await f.settle("ch_1");
408
+ f.failBalance();
409
+ await f.dispute("ch_1", "open");
410
+ await f.settle("ch_2", 400);
411
+ await f.dispute("ch_1", "won");
412
+ expect((await f.snapshot()).balance).toMatchObject({
413
+ outstandingDebt: 0,
414
+ offsetAmount: 400,
415
+ restoredAmount: 400,
416
+ recoveredAmount: 0,
417
+ });
418
+ });
419
+ it("surfaces unsupported Connect paths and preserves debt for operator action", async () => {
420
+ const f = await setup();
421
+ await f.settle("ch_1");
422
+ f.disable();
423
+ await f.refund("ch_1");
424
+ expect((await f.snapshot()).obligations.page[0]).toMatchObject({
425
+ lastState: "unsupported",
426
+ debtAmount: 1001,
427
+ });
428
+ expect(f.receipts).toHaveLength(0);
429
+ });
430
+ it("coordinates affiliate payout minimums, debt, unknown reservations and concurrent release", async () => {
431
+ const f = await setup();
432
+ await f.settle("ch_1");
433
+ const args = {
434
+ affiliateAccountId: "acct_a",
435
+ currency: "usd",
436
+ amount: 501,
437
+ minimumAmount: 500,
438
+ operationKey: "po_1",
439
+ now: 1,
440
+ };
441
+ await expect(
442
+ f.t.mutation(api.affiliatePayouts.mutations.reservePayout, {
443
+ ...args,
444
+ amount: 499,
445
+ }),
446
+ ).rejects.toThrow("minimum");
447
+ await f.t.mutation(api.affiliatePayouts.mutations.reservePayout, args);
448
+ await expect(
449
+ f.t.mutation(api.affiliatePayouts.mutations.reservePayout, {
450
+ ...args,
451
+ operationKey: "po_2",
452
+ }),
453
+ ).rejects.toThrow("before payout release");
454
+ await expect(f.settle("ch_2", 700)).rejects.toThrow("payout is pending");
455
+ await f.t.mutation(api.affiliatePayouts.mutations.finishPayout, {
456
+ operationKey: "po_1",
457
+ state: "unknown",
458
+ });
459
+ expect((await f.snapshot()).balance!.activePayoutKey).toBe("po_1");
460
+ await f.t.mutation(api.affiliatePayouts.mutations.finishPayout, {
461
+ operationKey: "po_1",
462
+ state: "submitted",
463
+ stripePayoutId: "po_receipt",
464
+ });
465
+ await f.settle("ch_2", 700);
466
+ f.failBalance();
467
+ await f.refund("ch_1");
468
+ await expect(
469
+ f.t.mutation(api.affiliatePayouts.mutations.reservePayout, {
470
+ ...args,
471
+ operationKey: "po_3",
472
+ }),
473
+ ).rejects.toThrow("before payout release");
474
+ });
475
+ it("keeps failed restoration visible until a provider receipt confirms payment", async () => {
476
+ const f = await setup();
477
+ await f.settle("ch_1");
478
+ await f.dispute("ch_1", "open");
479
+ f.failRestoration();
480
+ await f.dispute("ch_1", "won");
481
+ expect((await f.snapshot()).balance).toMatchObject({
482
+ restoredAmount: 0,
483
+ restorationDue: 1001,
484
+ reservedRestoration: 0,
485
+ });
486
+ f.allowRestoration();
487
+ await reconcileObligation(
488
+ f.stripe,
489
+ f.comp,
490
+ f.ctx,
491
+ (await f.snapshot()).obligations.page[0]._id,
492
+ );
493
+ expect((await f.snapshot()).balance).toMatchObject({
494
+ restoredAmount: 1001,
495
+ restorationDue: 0,
496
+ });
497
+ });
498
+ it("reconciles a restoration timeout without paying twice", async () => {
499
+ const f = await setup();
500
+ await f.settle("ch_1");
501
+ await f.dispute("ch_1", "open");
502
+ f.timeoutRestoration();
503
+ await f.dispute("ch_1", "won");
504
+ expect((await f.snapshot()).balance).toMatchObject({
505
+ restoredAmount: 0,
506
+ reservedRestoration: 1001,
507
+ });
508
+ await expect(f.settle("ch_2", 400)).rejects.toThrow(
509
+ "pending reconciliation",
510
+ );
511
+ await reconcileObligation(
512
+ f.stripe,
513
+ f.comp,
514
+ f.ctx,
515
+ (await f.snapshot()).obligations.page[0]._id,
516
+ );
517
+ expect((await f.snapshot()).balance).toMatchObject({
518
+ restoredAmount: 1001,
519
+ reservedRestoration: 0,
520
+ });
521
+ expect(
522
+ f.transfers.filter((r) => r.metadata.bsRecoveryOperation),
523
+ ).toHaveLength(1);
524
+ });
525
+ it("handles terminal events first and offsets a refund after full restoration", async () => {
526
+ const f = await setup();
527
+ await f.settle("ch_1");
528
+ await f.dispute("ch_1", "won");
529
+ await f.dispute("ch_1", "open");
530
+ expect(f.receipts).toHaveLength(0);
531
+ const g = await setup();
532
+ await g.settle("ch_1");
533
+ await g.dispute("ch_1", "open");
534
+ await g.dispute("ch_1", "won");
535
+ await g.refund("ch_1", 5000);
536
+ expect((await g.snapshot()).balance!.outstandingDebt).toBe(501);
537
+ await g.settle("ch_2", 700);
538
+ expect((await g.snapshot()).balance).toMatchObject({
539
+ outstandingDebt: 0,
540
+ offsetAmount: 501,
541
+ restoredAmount: 1001,
542
+ });
543
+ expect(g.receipts).toHaveLength(1);
544
+ });
545
+ it("prevents legacy reversal utilities from bypassing already-applied offsets", async () => {
546
+ const f = await setup();
547
+ await f.settle("ch_1");
548
+ await f.t.mutation(api.connect.mutations.claimReversalSlices, {
549
+ sourceChargeId: "ch_1",
550
+ operationId: "legacy_op",
551
+ mode: { kind: "full" },
552
+ });
553
+ await reclaimReversalClaim(f.stripe, f.comp, f.ctx, {
554
+ operationId: "legacy_op",
555
+ });
556
+ f.failBalance();
557
+ await f.refund("ch_1");
558
+ await f.settle("ch_2", 1001);
559
+ await reclaimReversalClaim(f.stripe, f.comp, f.ctx, {
560
+ operationId: "legacy_op",
561
+ });
562
+ expect((await f.snapshot()).balance!.offsetAmount).toBe(1001);
563
+ await expect(
564
+ reverseTransfers(f.stripe, f.comp, f.ctx, { sourceChargeId: "ch_1" }),
565
+ ).rejects.toThrow("affiliateRecovery.retry");
566
+ expect(f.receipts).toHaveLength(0);
567
+ });
568
+ it("does not treat an unattributed reversal aggregate as permission to collect again", async () => {
569
+ const f = await setup();
570
+ await f.settle("ch_1");
571
+ f.timeout();
572
+ await f.refund("ch_1");
573
+ f.receipts[0].metadata = {};
574
+ await f.refund("ch_1");
575
+ expect(f.receipts).toHaveLength(1);
576
+ expect((await f.snapshot()).balance).toMatchObject({
577
+ recoveredAmount: 0,
578
+ reservedRecovery: 1001,
579
+ });
580
+ await expect(f.settle("ch_2", 400)).rejects.toThrow(
581
+ "pending reconciliation",
582
+ );
583
+ expect((await f.snapshot()).obligations.page[0].lastError).toContain(
584
+ "attributable operation receipt",
585
+ );
586
+ });
587
+ });