@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,102 @@
1
+ import { v } from "convex/values";
2
+ import { mutation } from "../earnings/triggers.js";
3
+ import { writePayout } from "../connect/payoutWrites.js";
4
+ import {
5
+ claimByPayout,
6
+ payoutById,
7
+ validateSnapshot,
8
+ fingerprint,
9
+ } from "./helpers.js";
10
+ import { identity, prepared, result } from "./validators.js";
11
+ export const begin = mutation({
12
+ args: identity,
13
+ returns: v.number(),
14
+ handler: async (ctx, args) => {
15
+ const claim = await claimByPayout(ctx, args);
16
+ await payoutById(ctx, args);
17
+ const generation = (claim?.generation ?? 0) + 1;
18
+ if (!Number.isSafeInteger(generation))
19
+ throw new Error("Payout synchronization generation exhausted.");
20
+ if (claim)
21
+ await ctx.db.patch("payoutSyncClaims", claim._id, {
22
+ generation,
23
+ status: "pending",
24
+ startedAt: Date.now(),
25
+ });
26
+ else
27
+ await ctx.db.insert("payoutSyncClaims", {
28
+ ...args,
29
+ generation,
30
+ status: "pending",
31
+ startedAt: Date.now(),
32
+ });
33
+ return generation;
34
+ },
35
+ });
36
+ export const finish = mutation({
37
+ args: prepared,
38
+ returns: result,
39
+ handler: async (ctx, args) => {
40
+ const claim = await claimByPayout(ctx, args);
41
+ if (!claim) throw new Error("Payout synchronization claim is missing.");
42
+ if (claim.generation !== args.generation)
43
+ return { status: "superseded" as const, generation: args.generation };
44
+ validateSnapshot(args.payout);
45
+ if (
46
+ args.payout.accountId !== args.accountId ||
47
+ args.payout.stripePayoutId !== args.stripePayoutId
48
+ )
49
+ throw new Error(
50
+ "Payout synchronization snapshot identity does not match.",
51
+ );
52
+ const existing = await payoutById(ctx, args);
53
+ if (
54
+ existing &&
55
+ (existing.amount !== args.payout.amount ||
56
+ existing.currency !== args.payout.currency)
57
+ )
58
+ throw new Error("Existing payout original money does not match.");
59
+ const nextFingerprint = await fingerprint(args.payout);
60
+ if (
61
+ claim.status === "accepted" &&
62
+ claim.acceptedFingerprint !== nextFingerprint
63
+ )
64
+ throw new Error("An accepted payout observation cannot be replaced.");
65
+ const changed =
66
+ !existing || (await fingerprint(existing)) !== nextFingerprint;
67
+ if (claim.status === "accepted" && changed)
68
+ throw new Error(
69
+ "Accepted payout state changed outside its synchronization.",
70
+ );
71
+ if (claim.status === "accepted")
72
+ return {
73
+ status: "accepted" as const,
74
+ generation: args.generation,
75
+ changed: false,
76
+ payout: existing!,
77
+ };
78
+ const payout = changed ? await writePayout(ctx, args.payout) : existing!;
79
+ await ctx.db.patch("payoutSyncClaims", claim._id, {
80
+ status: "accepted",
81
+ acceptedGeneration: args.generation,
82
+ acceptedAt: Date.now(),
83
+ acceptedFingerprint: nextFingerprint,
84
+ });
85
+ return {
86
+ status: "accepted" as const,
87
+ generation: args.generation,
88
+ changed,
89
+ payout,
90
+ };
91
+ },
92
+ });
93
+ export const markUnknown = mutation({
94
+ args: { ...identity, generation: v.number() },
95
+ returns: v.null(),
96
+ handler: async (ctx, args) => {
97
+ const claim = await claimByPayout(ctx, args);
98
+ if (claim?.generation === args.generation && claim.status === "pending")
99
+ await ctx.db.patch("payoutSyncClaims", claim._id, { status: "unknown" });
100
+ return null;
101
+ },
102
+ });
@@ -0,0 +1,10 @@
1
+ import { v } from "convex/values";
2
+ import { query } from "../_generated/server.js";
3
+ import { claimByPayout } from "./helpers.js";
4
+ import { claimDoc } from "./schema.js";
5
+ import { identity } from "./validators.js";
6
+ export const get = query({
7
+ args: identity,
8
+ returns: v.union(claimDoc, v.null()),
9
+ handler: claimByPayout,
10
+ });
@@ -0,0 +1,25 @@
1
+ import { defineTable } from "convex/server";
2
+ import { v } from "convex/values";
3
+ import { identity } from "./validators.js";
4
+ const fields = {
5
+ ...identity,
6
+ generation: v.number(),
7
+ status: v.union(
8
+ v.literal("pending"),
9
+ v.literal("accepted"),
10
+ v.literal("unknown"),
11
+ ),
12
+ startedAt: v.number(),
13
+ acceptedGeneration: v.optional(v.number()),
14
+ acceptedAt: v.optional(v.number()),
15
+ acceptedFingerprint: v.optional(v.string()),
16
+ };
17
+ export const claimDoc = v.object({
18
+ ...fields,
19
+ _id: v.id("payoutSyncClaims"),
20
+ _creationTime: v.number(),
21
+ });
22
+ /** Operational generations only. Financial facts remain in the existing payouts table. */
23
+ export const claimsTable = defineTable(fields).index("by_stripe_payout_id", [
24
+ "stripePayoutId",
25
+ ]);
@@ -0,0 +1,21 @@
1
+ import { v } from "convex/values";
2
+ import { payoutFields, payoutDocValidator } from "../connect/validators.js";
3
+ export const identity = { stripePayoutId: v.string(), accountId: v.string() };
4
+ export const snapshot = v.object({
5
+ ...payoutFields,
6
+ metadata: v.optional(v.record(v.string(), v.string())),
7
+ });
8
+ export const prepared = {
9
+ ...identity,
10
+ generation: v.number(),
11
+ payout: snapshot,
12
+ };
13
+ export const result = v.union(
14
+ v.object({
15
+ status: v.literal("accepted"),
16
+ generation: v.number(),
17
+ changed: v.boolean(),
18
+ payout: payoutDocValidator,
19
+ }),
20
+ v.object({ status: v.literal("superseded"), generation: v.number() }),
21
+ );
@@ -1,3 +1,15 @@
1
+ import { claimsTable as payoutSyncClaimsTable } from "./payoutSync/schema.js";
2
+ import {
3
+ payoutReleasesTable as affiliatePayoutReleases,
4
+ operationsTable as affiliateOperations,
5
+ earningsTable as affiliateEarnings,
6
+ offsetsTable as affiliateOffsets,
7
+ balancesTable as affiliateBalances,
8
+ } from "./affiliateRecovery/schema.js";
9
+ import {
10
+ affiliateObligationsTable,
11
+ affiliateReversalReceiptsTable,
12
+ } from "./settlement/clawbackSchema.js";
1
13
  import {
2
14
  feeBalancesTable as payoutFeeBalancesTable,
3
15
  claimsTable as payoutReleaseClaimsTable,
@@ -37,6 +49,14 @@ import { pricesTable, productsTable } from "./products/schema";
37
49
  import { webhookEventsTable } from "./webhooks/schema";
38
50
 
39
51
  export default defineSchema({
52
+ affiliateObligations: affiliateObligationsTable,
53
+ affiliateRecoveryOperations: affiliateOperations,
54
+ affiliatePayoutReleases,
55
+ affiliateEarnings,
56
+ affiliateOffsets,
57
+ affiliateRecoveryBalances: affiliateBalances,
58
+ affiliateReversalReceipts: affiliateReversalReceiptsTable,
59
+ payoutSyncClaims: payoutSyncClaimsTable,
40
60
  payoutFeeBalances: payoutFeeBalancesTable,
41
61
  payoutReleaseClaims: payoutReleaseClaimsTable,
42
62
  payoutSourceAllocations: payoutSourceAllocationsTable,
@@ -0,0 +1,78 @@
1
+ import { defineTable } from "convex/server";
2
+ import { v } from "convex/values";
3
+
4
+ /** Principal snapshots on the original charge axis, never incremental debits. */
5
+ export const commissionExposureValidator = v.object({
6
+ kind: v.union(v.literal("refund"), v.literal("dispute")),
7
+ chargeAmount: v.number(),
8
+ principal: v.number(),
9
+ finalized: v.boolean(),
10
+ disputeState: v.optional(
11
+ v.union(v.literal("open"), v.literal("lost"), v.literal("won")),
12
+ ),
13
+ disputeId: v.optional(v.string()),
14
+ refundedPrincipal: v.optional(v.number()),
15
+ refundId: v.optional(v.string()),
16
+ });
17
+
18
+ export const obligationFields = {
19
+ stripeTransferId: v.optional(v.string()),
20
+ sourceChargeId: v.string(),
21
+ sourceInvoiceId: v.optional(v.string()),
22
+ paymentIntentId: v.optional(v.string()),
23
+ paymentId: v.optional(v.string()),
24
+ storeAccountId: v.optional(v.string()),
25
+ lastRefundId: v.optional(v.string()),
26
+ affiliateAccountId: v.string(),
27
+ currency: v.string(),
28
+ chargeAmount: v.number(),
29
+ originalCommission: v.number(),
30
+ refundedPrincipal: v.number(),
31
+ disputedPrincipal: v.number(),
32
+ lostPrincipal: v.number(),
33
+ finalizedAmount: v.number(),
34
+ provisionalAmount: v.number(),
35
+ /** Confirmed transfer reversal total; reservations are not receipts. */
36
+ recoveredAmount: v.number(),
37
+ /** Existing reversals may have no attributable refund/dispute receipt. */
38
+ needsReconciliation: v.boolean(),
39
+ accounted: v.optional(v.boolean()),
40
+ legacyReceiptIds: v.optional(v.array(v.string())),
41
+ legacyRestorationIds: v.optional(v.array(v.string())),
42
+ operatorNote: v.optional(v.string()),
43
+ earningId: v.optional(v.id("affiliateEarnings")),
44
+ releasedCommission: v.optional(v.number()),
45
+ offsetAmount: v.optional(v.number()),
46
+ restoredAmount: v.optional(v.number()),
47
+ reservedRecovery: v.optional(v.number()),
48
+ reservedRestoration: v.optional(v.number()),
49
+ debtAmount: v.optional(v.number()),
50
+ creditAmount: v.optional(v.number()),
51
+ debtStatus: v.optional(v.union(v.literal("open"), v.literal("settled"))),
52
+ disputeState: v.optional(
53
+ v.union(v.literal("open"), v.literal("lost"), v.literal("won")),
54
+ ),
55
+ disputeId: v.optional(v.string()),
56
+ activeOperationId: v.optional(v.id("affiliateRecoveryOperations")),
57
+ lastState: v.optional(v.string()),
58
+ lastError: v.optional(v.string()),
59
+ operationSequence: v.optional(v.number()),
60
+ };
61
+ export const affiliateObligationsTable = defineTable(obligationFields)
62
+ .index("by_transfer", ["stripeTransferId"])
63
+ .index("by_charge", ["sourceChargeId"])
64
+ .index("by_affiliate_currency_debt", [
65
+ "affiliateAccountId",
66
+ "currency",
67
+ "debtStatus",
68
+ ])
69
+ .index("by_affiliate_currency", ["affiliateAccountId", "currency"]);
70
+ export const affiliateReversalReceiptsTable = defineTable({
71
+ stripeReversalId: v.string(),
72
+ stripeTransferId: v.string(),
73
+ operationId: v.string(),
74
+ amount: v.number(),
75
+ })
76
+ .index("by_receipt", ["stripeReversalId"])
77
+ .index("by_transfer_operation", ["stripeTransferId", "operationId"])
78
+ .index("by_transfer", ["stripeTransferId"]);
@@ -0,0 +1,233 @@
1
+ import { updateObligation } from "../affiliateRecovery/accounting.js";
2
+ import type { Infer } from "convex/values";
3
+ import type { Doc } from "../_generated/dataModel.js";
4
+ import type { MutationCtx } from "../_generated/server.js";
5
+ import { commissionExposureValidator } from "./clawbackSchema.js";
6
+
7
+ export type CommissionExposure = Infer<typeof commissionExposureValidator>;
8
+
9
+ /** Exact round-half-up proportional commission in integer currency units. */
10
+ export function proportionalCommission(
11
+ commission: number,
12
+ principal: number,
13
+ charge: number,
14
+ ): number {
15
+ for (const n of [commission, principal, charge]) {
16
+ if (!Number.isSafeInteger(n) || n < 0)
17
+ throw new Error(
18
+ "Commission recovery requires non-negative integer units.",
19
+ );
20
+ }
21
+ if (charge === 0)
22
+ throw new Error("Commission recovery requires a positive charge.");
23
+ const numerator = BigInt(commission) * BigInt(Math.min(principal, charge));
24
+ return Number((2n * numerator + BigInt(charge)) / (2n * BigInt(charge)));
25
+ }
26
+
27
+ /** Lazily migrate one immutable original affiliate leg, also used by backfill. */
28
+ export async function ensureCommissionObligation(
29
+ ctx: MutationCtx,
30
+ transfer: Doc<"transfers">,
31
+ chargeAmount: number,
32
+ ) {
33
+ if (
34
+ transfer.role !== "affiliate" ||
35
+ transfer.reinstatement ||
36
+ !transfer.sourceChargeId
37
+ )
38
+ throw new Error(
39
+ "Commission obligation requires an original affiliate transfer.",
40
+ );
41
+ const existing = await ctx.db
42
+ .query("affiliateObligations")
43
+ .withIndex("by_transfer", (q) =>
44
+ q.eq("stripeTransferId", transfer.stripeTransferId),
45
+ )
46
+ .unique();
47
+ if (existing) {
48
+ if (
49
+ existing.chargeAmount !== chargeAmount ||
50
+ (existing.releasedCommission ?? existing.originalCommission) !==
51
+ transfer.amount ||
52
+ existing.affiliateAccountId !== transfer.destinationAccountId ||
53
+ existing.currency !== transfer.currency ||
54
+ existing.sourceChargeId !== transfer.sourceChargeId ||
55
+ (transfer.sourceInvoiceId !== undefined &&
56
+ existing.sourceInvoiceId !== transfer.sourceInvoiceId)
57
+ )
58
+ throw new Error("Original affiliate commission identity cannot change.");
59
+ return existing;
60
+ }
61
+ proportionalCommission(transfer.amount, 0, chargeAmount);
62
+ const allocation = await ctx.db
63
+ .query("settlementAllocations")
64
+ .withIndex("by_source_charge", (q) =>
65
+ q.eq("sourceChargeId", transfer.sourceChargeId!),
66
+ )
67
+ .unique();
68
+ const earning = await ctx.db
69
+ .query("affiliateEarnings")
70
+ .withIndex("by_charge_affiliate", (q) =>
71
+ q
72
+ .eq("sourceChargeId", transfer.sourceChargeId!)
73
+ .eq("affiliateAccountId", transfer.destinationAccountId),
74
+ )
75
+ .unique();
76
+ if (
77
+ allocation &&
78
+ (allocation.gross !== chargeAmount ||
79
+ allocation.currency !== transfer.currency ||
80
+ !allocation.transfers.some(
81
+ (leg) =>
82
+ leg.role === "affiliate" &&
83
+ leg.destinationAccountId === transfer.destinationAccountId &&
84
+ leg.amount === (earning?.originalCommission ?? transfer.amount),
85
+ ))
86
+ )
87
+ throw new Error("Commission recovery conflicts with original settlement.");
88
+ const recoveredAmount = transfer.reversedAmount ?? 0;
89
+ if (
90
+ !Number.isSafeInteger(recoveredAmount) ||
91
+ recoveredAmount < 0 ||
92
+ recoveredAmount > transfer.amount
93
+ )
94
+ throw new Error("Invalid original affiliate reversal balance.");
95
+ const id = await ctx.db.insert("affiliateObligations", {
96
+ stripeTransferId: transfer.stripeTransferId,
97
+ sourceChargeId: transfer.sourceChargeId,
98
+ sourceInvoiceId: transfer.sourceInvoiceId ?? allocation?.sourceInvoiceId,
99
+ paymentIntentId: allocation?.paymentIntentId,
100
+ paymentId: transfer.paymentId ?? allocation?.paymentId,
101
+ storeAccountId: allocation?.transfers.find((leg) => leg.role === "store")
102
+ ?.destinationAccountId,
103
+ affiliateAccountId: transfer.destinationAccountId,
104
+ currency: transfer.currency,
105
+ chargeAmount,
106
+ originalCommission: earning?.originalCommission ?? transfer.amount,
107
+ releasedCommission: transfer.amount,
108
+ earningId: earning?._id,
109
+ refundedPrincipal: 0,
110
+ disputedPrincipal: 0,
111
+ lostPrincipal: 0,
112
+ finalizedAmount: 0,
113
+ provisionalAmount: 0,
114
+ recoveredAmount,
115
+ needsReconciliation:
116
+ recoveredAmount > 0 || (transfer.reversalClaimedAmount ?? 0) > 0,
117
+ });
118
+ return (await ctx.db.get("affiliateObligations", id))!;
119
+ }
120
+
121
+ /** Refunds and disputes share the same original-principal exposure axis. */
122
+ export async function applyCommissionExposure(
123
+ ctx: MutationCtx,
124
+ transfer: Doc<"transfers">,
125
+ exposure: CommissionExposure,
126
+ ) {
127
+ proportionalCommission(
128
+ transfer.amount,
129
+ exposure.principal,
130
+ exposure.chargeAmount,
131
+ );
132
+ if (exposure.kind === "refund" && !exposure.finalized)
133
+ throw new Error("Pending refunds cannot create commission obligations.");
134
+ const row = await ensureCommissionObligation(
135
+ ctx,
136
+ transfer,
137
+ exposure.chargeAmount,
138
+ );
139
+ return applyObligationExposure(ctx, row, exposure);
140
+ }
141
+ export async function applyObligationExposure(
142
+ ctx: MutationCtx,
143
+ row: Doc<"affiliateObligations">,
144
+ exposure: CommissionExposure,
145
+ ) {
146
+ proportionalCommission(
147
+ row.originalCommission,
148
+ exposure.principal,
149
+ exposure.chargeAmount,
150
+ );
151
+ if (row.chargeAmount !== exposure.chargeAmount)
152
+ throw new Error("Original charge amount cannot change.");
153
+ const principal = Math.min(exposure.principal, exposure.chargeAmount);
154
+ const refundedPrincipal = Math.max(
155
+ row.refundedPrincipal,
156
+ exposure.refundedPrincipal ?? 0,
157
+ exposure.kind === "refund" ? principal : 0,
158
+ );
159
+ let disputedPrincipal = Math.max(
160
+ row.disputedPrincipal,
161
+ exposure.kind === "dispute" ? principal : 0,
162
+ );
163
+ let lostPrincipal = Math.max(
164
+ row.lostPrincipal,
165
+ exposure.kind === "dispute" && exposure.finalized ? principal : 0,
166
+ );
167
+ const incomingState =
168
+ exposure.disputeState ?? (exposure.finalized ? "lost" : "open");
169
+ const rank = { open: 0, lost: 1, won: 2 };
170
+ const disputeState =
171
+ exposure.kind === "dispute" &&
172
+ rank[incomingState] >= rank[row.disputeState ?? "open"]
173
+ ? incomingState
174
+ : row.disputeState;
175
+ if (disputeState === "won") {
176
+ disputedPrincipal = 0;
177
+ lostPrincipal = 0;
178
+ }
179
+ if (exposure.kind === "dispute" && disputeState === "lost")
180
+ lostPrincipal = Math.max(lostPrincipal, disputedPrincipal);
181
+ const finalizedAmount = proportionalCommission(
182
+ row.originalCommission,
183
+ Math.max(refundedPrincipal, lostPrincipal),
184
+ row.chargeAmount,
185
+ );
186
+ const target = proportionalCommission(
187
+ row.originalCommission,
188
+ Math.max(refundedPrincipal, disputedPrincipal),
189
+ row.chargeAmount,
190
+ );
191
+ await updateObligation(ctx, row, {
192
+ disputeState,
193
+ lastRefundId: exposure.refundId ?? row.lastRefundId,
194
+ disputeId: exposure.disputeId ?? row.disputeId,
195
+ refundedPrincipal,
196
+ disputedPrincipal,
197
+ lostPrincipal,
198
+ finalizedAmount,
199
+ provisionalAmount: target - finalizedAmount,
200
+ });
201
+ return target;
202
+ }
203
+
204
+ /** Webhook upserts cannot rewrite a frozen commission or its confirmed balance. */
205
+ export async function syncCommissionTransfer(
206
+ ctx: MutationCtx,
207
+ transfer: Doc<"transfers">,
208
+ ) {
209
+ const row = await ctx.db
210
+ .query("affiliateObligations")
211
+ .withIndex("by_transfer", (q) =>
212
+ q.eq("stripeTransferId", transfer.stripeTransferId),
213
+ )
214
+ .unique();
215
+ if (!row) return;
216
+ await ensureCommissionObligation(ctx, transfer, row.chargeAmount);
217
+ const recoveredAmount = Math.max(
218
+ row.recoveredAmount,
219
+ transfer.reversedAmount ?? 0,
220
+ );
221
+ if (
222
+ !Number.isSafeInteger(recoveredAmount) ||
223
+ recoveredAmount > row.originalCommission
224
+ )
225
+ throw new Error("Invalid confirmed affiliate reversal balance.");
226
+ if (recoveredAmount > row.recoveredAmount && row.activeOperationId) return;
227
+ await updateObligation(ctx, row, {
228
+ recoveredAmount,
229
+ ...(recoveredAmount > row.recoveredAmount && row.accounted
230
+ ? { needsReconciliation: true }
231
+ : {}),
232
+ });
233
+ }
@@ -1,3 +1,8 @@
1
+ import { paginator } from "convex-helpers/server/pagination";
2
+ import schema from "../schema.js";
3
+ import { updateObligation } from "../affiliateRecovery/accounting.js";
4
+ import { paginationOptsValidator } from "convex/server";
5
+ import { ensureCommissionObligation } from "./clawbacks.js";
1
6
  import { v } from "convex/values";
2
7
  import { mutation } from "../_generated/server.js";
3
8
  import { allocationFields, commissionFields, termValidator } from "./schema.js";
@@ -117,3 +122,52 @@ export const claimCommissionInvoice = mutation({
117
122
  };
118
123
  },
119
124
  });
125
+
126
+ /** Explicit, resumable migration. Never fabricate legacy refund provenance. */
127
+ export const migrateAffiliateObligations = mutation({
128
+ args: { paginationOpts: paginationOptsValidator },
129
+ returns: v.object({
130
+ migrated: v.number(),
131
+ skipped: v.number(),
132
+ isDone: v.boolean(),
133
+ continueCursor: v.string(),
134
+ }),
135
+ handler: async (ctx, args) => {
136
+ const page = await paginator(ctx.db, schema)
137
+ .query("transfers")
138
+ .paginate({
139
+ ...args.paginationOpts,
140
+ numItems: Math.min(100, args.paginationOpts.numItems),
141
+ });
142
+ let migrated = 0;
143
+ let skipped = 0;
144
+ for (const transfer of page.page) {
145
+ if (transfer.role !== "affiliate" || transfer.reinstatement) continue;
146
+ const allocation = transfer.sourceChargeId
147
+ ? await ctx.db
148
+ .query("settlementAllocations")
149
+ .withIndex("by_source_charge", (q) =>
150
+ q.eq("sourceChargeId", transfer.sourceChargeId!),
151
+ )
152
+ .unique()
153
+ : null;
154
+ if (!allocation) {
155
+ skipped++;
156
+ continue;
157
+ }
158
+ const obligation = await ensureCommissionObligation(
159
+ ctx,
160
+ transfer,
161
+ allocation.gross,
162
+ );
163
+ await updateObligation(ctx, obligation, {});
164
+ migrated++;
165
+ }
166
+ return {
167
+ migrated,
168
+ skipped,
169
+ isDone: page.isDone,
170
+ continueCursor: page.continueCursor,
171
+ };
172
+ },
173
+ });
@@ -1,3 +1,10 @@
1
+ import { paginator } from "convex-helpers/server/pagination";
2
+ import schema from "../schema.js";
3
+ import {
4
+ paginationOptsValidator,
5
+ paginationResultValidator,
6
+ } from "convex/server";
7
+ import { obligationFields } from "./clawbackSchema.js";
1
8
  import { v } from "convex/values";
2
9
  import { query } from "../_generated/server.js";
3
10
  import { allocationFields, commissionFields } from "./schema.js";
@@ -36,3 +43,31 @@ export const getCommissionTerm = query({
36
43
  };
37
44
  },
38
45
  });
46
+
47
+ /** Trusted host API: authorize account ownership before forwarding arguments. */
48
+ export const listAffiliateObligations = query({
49
+ args: {
50
+ affiliateAccountId: v.string(),
51
+ currency: v.string(),
52
+ paginationOpts: paginationOptsValidator,
53
+ },
54
+ returns: paginationResultValidator(
55
+ v.object({
56
+ ...obligationFields,
57
+ _id: v.id("affiliateObligations"),
58
+ _creationTime: v.number(),
59
+ }),
60
+ ),
61
+ handler: (ctx, args) =>
62
+ paginator(ctx.db, schema)
63
+ .query("affiliateObligations")
64
+ .withIndex("by_affiliate_currency", (q) =>
65
+ q
66
+ .eq("affiliateAccountId", args.affiliateAccountId)
67
+ .eq("currency", args.currency),
68
+ )
69
+ .paginate({
70
+ ...args.paginationOpts,
71
+ numItems: Math.min(100, args.paginationOpts.numItems),
72
+ }),
73
+ });
@@ -103,15 +103,20 @@ it("rolls back subtract/reapply updates that would overflow", async () => {
103
103
  ).toMatchObject({ totals: [{ gross: max, transferCount: 2 }] });
104
104
  });
105
105
 
106
- it("rolls back a reversal whose cumulative summary would overflow", async () => {
106
+ it("rolls back a reversal when a legacy cumulative summary would overflow", async () => {
107
107
  const t = convexTest(schema, modules);
108
108
  await t.mutation(api.connect.mutations.upsertTransfer, {
109
109
  ...transfer("tr_a", 1),
110
- reversedAmount: max - 1,
110
+ reversedAmount: 0,
111
111
  });
112
112
  await t.mutation(api.connect.mutations.upsertTransfer, {
113
113
  ...transfer("tr_b", 1),
114
- reversedAmount: 1,
114
+ reversedAmount: 0,
115
+ });
116
+ // Imported legacy totals can be inconsistent even when the new receipt is valid.
117
+ await t.run(async (ctx) => {
118
+ const totals = (await ctx.db.query("currencyTotals").first())!;
119
+ await ctx.db.patch("currencyTotals", totals._id, { reversed: max });
115
120
  });
116
121
  const before = await t.query(
117
122
  api.connect.queries.getAccountEarningsByCurrency,
@@ -120,7 +125,7 @@ it("rolls back a reversal whose cumulative summary would overflow", async () =>
120
125
  await expect(
121
126
  t.mutation(api.connect.mutations.recordTransferReversal, {
122
127
  stripeTransferId: "tr_b",
123
- reversedAmount: 2,
128
+ reversedAmount: 1,
124
129
  }),
125
130
  ).rejects.toThrow(/safe integer/i);
126
131
  expect(