@fantastic.dev/better-stripe 0.21.0 → 0.23.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 (107) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +8 -5
  3. package/dist/client/connect/payoutActivationProof.d.ts +16 -0
  4. package/dist/client/connect/payoutActivationProof.d.ts.map +1 -0
  5. package/dist/client/connect/payoutActivationProof.js +72 -0
  6. package/dist/client/connect/payoutActivationProof.js.map +1 -0
  7. package/dist/client/connect/payoutAdoption.d.ts +35 -0
  8. package/dist/client/connect/payoutAdoption.d.ts.map +1 -0
  9. package/dist/client/connect/payoutAdoption.js +230 -0
  10. package/dist/client/connect/payoutAdoption.js.map +1 -0
  11. package/dist/client/connect/payoutAdoptionAge.d.ts +19 -0
  12. package/dist/client/connect/payoutAdoptionAge.d.ts.map +1 -0
  13. package/dist/client/connect/payoutAdoptionAge.js +56 -0
  14. package/dist/client/connect/payoutAdoptionAge.js.map +1 -0
  15. package/dist/client/connect/payoutAdoptionAllocation.d.ts +24 -0
  16. package/dist/client/connect/payoutAdoptionAllocation.d.ts.map +1 -0
  17. package/dist/client/connect/payoutAdoptionAllocation.js +78 -0
  18. package/dist/client/connect/payoutAdoptionAllocation.js.map +1 -0
  19. package/dist/client/connect/payoutExecution.d.ts +34 -0
  20. package/dist/client/connect/payoutExecution.d.ts.map +1 -0
  21. package/dist/client/connect/payoutExecution.js +189 -0
  22. package/dist/client/connect/payoutExecution.js.map +1 -0
  23. package/dist/client/connect/payoutHistory.d.ts +22 -0
  24. package/dist/client/connect/payoutHistory.d.ts.map +1 -0
  25. package/dist/client/connect/payoutHistory.js +122 -0
  26. package/dist/client/connect/payoutHistory.js.map +1 -0
  27. package/dist/client/connect/payoutReconciliation.d.ts.map +1 -1
  28. package/dist/client/connect/payoutReconciliation.js +6 -2
  29. package/dist/client/connect/payoutReconciliation.js.map +1 -1
  30. package/dist/client/payoutPolicy.d.ts +10 -1
  31. package/dist/client/payoutPolicy.d.ts.map +1 -1
  32. package/dist/client/payoutPolicy.js +82 -7
  33. package/dist/client/payoutPolicy.js.map +1 -1
  34. package/dist/component/_generated/component.d.ts +269 -0
  35. package/dist/component/_generated/component.d.ts.map +1 -1
  36. package/dist/component/earnings/triggers.d.ts +40 -4
  37. package/dist/component/earnings/triggers.d.ts.map +1 -1
  38. package/dist/component/payoutPolicy/adoption.d.ts +9 -0
  39. package/dist/component/payoutPolicy/adoption.d.ts.map +1 -0
  40. package/dist/component/payoutPolicy/adoption.js +45 -0
  41. package/dist/component/payoutPolicy/adoption.js.map +1 -0
  42. package/dist/component/payoutPolicy/mutations.d.ts +96 -0
  43. package/dist/component/payoutPolicy/mutations.d.ts.map +1 -1
  44. package/dist/component/payoutPolicy/mutations.js +38 -5
  45. package/dist/component/payoutPolicy/mutations.js.map +1 -1
  46. package/dist/component/payoutPolicy/queries.d.ts +16 -0
  47. package/dist/component/payoutPolicy/queries.d.ts.map +1 -1
  48. package/dist/component/payoutPolicy/schema.d.ts +299 -2
  49. package/dist/component/payoutPolicy/schema.d.ts.map +1 -1
  50. package/dist/component/payoutPolicy/schema.js +7 -0
  51. package/dist/component/payoutPolicy/schema.js.map +1 -1
  52. package/dist/component/payoutRelease/adoption.d.ts +269 -0
  53. package/dist/component/payoutRelease/adoption.d.ts.map +1 -0
  54. package/dist/component/payoutRelease/adoption.js +197 -0
  55. package/dist/component/payoutRelease/adoption.js.map +1 -0
  56. package/dist/component/payoutRelease/helpers.d.ts +4 -0
  57. package/dist/component/payoutRelease/helpers.d.ts.map +1 -1
  58. package/dist/component/payoutRelease/helpers.js +20 -0
  59. package/dist/component/payoutRelease/helpers.js.map +1 -1
  60. package/dist/component/payoutRelease/mutations.d.ts +87 -0
  61. package/dist/component/payoutRelease/mutations.d.ts.map +1 -1
  62. package/dist/component/payoutRelease/mutations.js +13 -1
  63. package/dist/component/payoutRelease/mutations.js.map +1 -1
  64. package/dist/component/payoutRelease/observation.d.ts +18 -0
  65. package/dist/component/payoutRelease/observation.d.ts.map +1 -0
  66. package/dist/component/payoutRelease/observation.js +11 -0
  67. package/dist/component/payoutRelease/observation.js.map +1 -0
  68. package/dist/component/payoutRelease/queries.d.ts +74 -0
  69. package/dist/component/payoutRelease/queries.d.ts.map +1 -1
  70. package/dist/component/payoutRelease/queries.js +29 -0
  71. package/dist/component/payoutRelease/queries.js.map +1 -1
  72. package/dist/component/payoutRelease/schema.d.ts +13 -19
  73. package/dist/component/payoutRelease/schema.d.ts.map +1 -1
  74. package/dist/component/payoutRelease/schema.js +4 -9
  75. package/dist/component/payoutRelease/schema.js.map +1 -1
  76. package/dist/component/schema.d.ts +75 -2
  77. package/docs/settlement.md +181 -40
  78. package/package.json +1 -1
  79. package/src/client/connect/payoutActivationProof.ts +101 -0
  80. package/src/client/connect/payoutAdoption.test.ts +421 -0
  81. package/src/client/connect/payoutAdoption.ts +330 -0
  82. package/src/client/connect/payoutAdoptionAge.test.ts +89 -0
  83. package/src/client/connect/payoutAdoptionAge.ts +74 -0
  84. package/src/client/connect/payoutAdoptionAllocation.test.ts +132 -0
  85. package/src/client/connect/payoutAdoptionAllocation.ts +106 -0
  86. package/src/client/connect/payoutExecution.test.ts +504 -0
  87. package/src/client/connect/payoutExecution.ts +265 -0
  88. package/src/client/connect/payoutHistory.test.ts +145 -0
  89. package/src/client/connect/payoutHistory.ts +149 -0
  90. package/src/client/connect/payoutReconciliation.test.ts +29 -0
  91. package/src/client/connect/payoutReconciliation.ts +10 -2
  92. package/src/client/payoutPolicy.test.ts +16 -0
  93. package/src/client/payoutPolicy.ts +121 -5
  94. package/src/component/_generated/api.ts +6 -0
  95. package/src/component/_generated/component.ts +272 -1
  96. package/src/component/payout-adoption.test.ts +272 -0
  97. package/src/component/payout-policy-adoption.test.ts +131 -0
  98. package/src/component/payout-release-snapshot.test.ts +57 -0
  99. package/src/component/payoutPolicy/adoption.ts +76 -0
  100. package/src/component/payoutPolicy/mutations.ts +46 -3
  101. package/src/component/payoutPolicy/schema.ts +9 -0
  102. package/src/component/payoutRelease/adoption.ts +262 -0
  103. package/src/component/payoutRelease/helpers.ts +21 -0
  104. package/src/component/payoutRelease/mutations.ts +19 -1
  105. package/src/component/payoutRelease/observation.ts +10 -0
  106. package/src/component/payoutRelease/queries.ts +35 -0
  107. package/src/component/payoutRelease/schema.ts +4 -9
@@ -26,8 +26,10 @@ declare const _default: import("convex/server").SchemaDefinition<{
26
26
  currency: string;
27
27
  debitFee?: number | undefined;
28
28
  debitTransactionId?: string | undefined;
29
+ destinationId?: string | undefined;
29
30
  expectedRevision: number;
30
31
  feeRevision?: number | undefined;
32
+ historical?: boolean | undefined;
31
33
  observations: {
32
34
  currency: string;
33
35
  ledgerFingerprint: string;
@@ -50,10 +52,12 @@ declare const _default: import("convex/server").SchemaDefinition<{
50
52
  }, {
51
53
  accountId: import("convex/values").VString<string, "required">;
52
54
  ownerKey: import("convex/values").VString<string, "required">;
55
+ historical: import("convex/values").VBoolean<boolean | undefined, "optional">;
53
56
  operationKey: import("convex/values").VString<string, "required">;
54
57
  expectedRevision: import("convex/values").VFloat64<number, "required">;
55
58
  currency: import("convex/values").VString<string, "required">;
56
59
  amount: import("convex/values").VFloat64<number, "required">;
60
+ destinationId: import("convex/values").VString<string | undefined, "optional">;
57
61
  allocations: import("convex/values").VArray<{
58
62
  amount: number;
59
63
  role?: "affiliate" | "other" | "store" | undefined;
@@ -107,7 +111,7 @@ declare const _default: import("convex/server").SchemaDefinition<{
107
111
  requestId: import("convex/values").VString<string, "required">;
108
112
  code: import("convex/values").VUnion<"amount_too_small" | "balance_insufficient", [import("convex/values").VLiteral<"balance_insufficient", "required">, import("convex/values").VLiteral<"amount_too_small", "required">], "required", never>;
109
113
  }, "optional", "code" | "requestId">;
110
- }, "required", "accountId" | "allocations" | "amount" | "attemptedAt" | "currency" | "debitFee" | "debitTransactionId" | "expectedRevision" | "feeRevision" | "observations" | "operationKey" | "ownerKey" | "providerFee" | "rejection" | "rejection.code" | "rejection.requestId" | "returnTransactionId" | "status" | "stripePayoutId">, {
114
+ }, "required", "accountId" | "allocations" | "amount" | "attemptedAt" | "currency" | "debitFee" | "debitTransactionId" | "destinationId" | "expectedRevision" | "feeRevision" | "historical" | "observations" | "operationKey" | "ownerKey" | "providerFee" | "rejection" | "rejection.code" | "rejection.requestId" | "returnTransactionId" | "status" | "stripePayoutId">, {
111
115
  by_account_status: ["accountId", "status", "_creationTime"];
112
116
  by_debit_receipt: ["debitTransactionId", "_creationTime"];
113
117
  by_operation: ["operationKey", "_creationTime"];
@@ -185,6 +189,22 @@ declare const _default: import("convex/server").SchemaDefinition<{
185
189
  }, {}, {}>;
186
190
  payoutPolicyOperations: import("convex/server").TableDefinition<import("convex/values").VObject<{
187
191
  accountId: string;
192
+ adoption?: {
193
+ claims: {
194
+ fingerprint: string;
195
+ operationKey: string;
196
+ }[];
197
+ observations: {
198
+ currency: string;
199
+ ledgerFingerprint: string;
200
+ matureNet: number;
201
+ observedAt: number;
202
+ pendingCredits: number;
203
+ stripeTransferId: string;
204
+ transactionIds: string[];
205
+ }[];
206
+ oldestHeldAt?: number | undefined;
207
+ } | undefined;
188
208
  attempted?: boolean | undefined;
189
209
  country: string;
190
210
  expectedRevision: number;
@@ -216,6 +236,59 @@ declare const _default: import("convex/server").SchemaDefinition<{
216
236
  }, {
217
237
  accountId: import("convex/values").VString<string, "required">;
218
238
  ownerKey: import("convex/values").VString<string, "required">;
239
+ adoption: import("convex/values").VObject<{
240
+ claims: {
241
+ fingerprint: string;
242
+ operationKey: string;
243
+ }[];
244
+ observations: {
245
+ currency: string;
246
+ ledgerFingerprint: string;
247
+ matureNet: number;
248
+ observedAt: number;
249
+ pendingCredits: number;
250
+ stripeTransferId: string;
251
+ transactionIds: string[];
252
+ }[];
253
+ oldestHeldAt?: number | undefined;
254
+ } | undefined, {
255
+ observations: import("convex/values").VArray<{
256
+ currency: string;
257
+ ledgerFingerprint: string;
258
+ matureNet: number;
259
+ observedAt: number;
260
+ pendingCredits: number;
261
+ stripeTransferId: string;
262
+ transactionIds: string[];
263
+ }[], import("convex/values").VObject<{
264
+ currency: string;
265
+ ledgerFingerprint: string;
266
+ matureNet: number;
267
+ observedAt: number;
268
+ pendingCredits: number;
269
+ stripeTransferId: string;
270
+ transactionIds: string[];
271
+ }, {
272
+ stripeTransferId: import("convex/values").VString<string, "required">;
273
+ currency: import("convex/values").VString<string, "required">;
274
+ matureNet: import("convex/values").VFloat64<number, "required">;
275
+ pendingCredits: import("convex/values").VFloat64<number, "required">;
276
+ ledgerFingerprint: import("convex/values").VString<string, "required">;
277
+ observedAt: import("convex/values").VFloat64<number, "required">;
278
+ transactionIds: import("convex/values").VArray<string[], import("convex/values").VString<string, "required">, "required">;
279
+ }, "required", "currency" | "ledgerFingerprint" | "matureNet" | "observedAt" | "pendingCredits" | "stripeTransferId" | "transactionIds">, "required">;
280
+ claims: import("convex/values").VArray<{
281
+ fingerprint: string;
282
+ operationKey: string;
283
+ }[], import("convex/values").VObject<{
284
+ fingerprint: string;
285
+ operationKey: string;
286
+ }, {
287
+ operationKey: import("convex/values").VString<string, "required">;
288
+ fingerprint: import("convex/values").VString<string, "required">;
289
+ }, "required", "fingerprint" | "operationKey">, "required">;
290
+ oldestHeldAt: import("convex/values").VFloat64<number | undefined, "optional">;
291
+ }, "optional", "claims" | "observations" | "oldestHeldAt">;
219
292
  operationKey: import("convex/values").VString<string, "required">;
220
293
  expectedRevision: import("convex/values").VFloat64<number, "required">;
221
294
  mode: import("convex/values").VUnion<"custom" | "stripe_managed", [import("convex/values").VLiteral<"stripe_managed", "required">, import("convex/values").VLiteral<"custom", "required">], "required", never>;
@@ -261,7 +334,7 @@ declare const _default: import("convex/server").SchemaDefinition<{
261
334
  }, "required", "interval" | "monthlyDays" | "weeklyDays">;
262
335
  status: import("convex/values").VUnion<"canceled" | "complete" | "pending", [import("convex/values").VLiteral<"pending", "required">, import("convex/values").VLiteral<"complete", "required">, import("convex/values").VLiteral<"canceled", "required">], "required", never>;
263
336
  attempted: import("convex/values").VBoolean<boolean | undefined, "optional">;
264
- }, "required", "accountId" | "attempted" | "country" | "expectedRevision" | "mode" | "observedAt" | "observedSchedule" | "observedSchedule.interval" | "observedSchedule.monthlyDays" | "observedSchedule.weeklyDays" | "operationKey" | "ownerKey" | "previousSchedule" | "previousSchedule.interval" | "previousSchedule.monthlyDays" | "previousSchedule.weeklyDays" | "revision" | "status" | "targetSchedule" | "targetSchedule.interval" | "targetSchedule.monthlyDays" | "targetSchedule.weeklyDays" | "thresholds">, {
337
+ }, "required", "accountId" | "adoption" | "adoption.claims" | "adoption.observations" | "adoption.oldestHeldAt" | "attempted" | "country" | "expectedRevision" | "mode" | "observedAt" | "observedSchedule" | "observedSchedule.interval" | "observedSchedule.monthlyDays" | "observedSchedule.weeklyDays" | "operationKey" | "ownerKey" | "previousSchedule" | "previousSchedule.interval" | "previousSchedule.monthlyDays" | "previousSchedule.weeklyDays" | "revision" | "status" | "targetSchedule" | "targetSchedule.interval" | "targetSchedule.monthlyDays" | "targetSchedule.weeklyDays" | "thresholds">, {
265
338
  by_operation: ["operationKey", "_creationTime"];
266
339
  }, {}, {}>;
267
340
  currencyTotals: import("convex/server").TableDefinition<import("convex/values").VObject<{
@@ -135,14 +135,10 @@ Balance Settings API does not expose a numeric payout-floor field; this package
135
135
  does not invent one. `minimum_balance_by_currency` is a retained reserve after a
136
136
  payout, **not a minimum payout trigger**. [Stripe reserve documentation](https://docs.stripe.com/payouts/minimum-balances-for-automatic-payouts).
137
137
 
138
- Custom application thresholds need a separately controlled release boundary:
139
-
140
- - Before transfer: retain funds on the platform and maintain a durable payable
141
- ledger, maturity, adjustments, and release claims. This changes settlement timing
142
- and must account for existing transferred balances.
143
- - Before bank payout: use a manual payout schedule where the platform has
144
- permission, and run an automatic application job. Otherwise provider automatic
145
- sweeps can bypass the application threshold. A retained reserve cannot fix that.
138
+ For a custom threshold, BetterStripe holds funds in the recipient's Stripe
139
+ balance and controls the bank-payout schedule. Verified manual controls and a
140
+ host automatic job prevent provider sweeps from bypassing the threshold. A
141
+ retained reserve cannot serve as the trigger.
146
142
 
147
143
  Manual holding limits depend on country: Thailand 10 days, US 2 years, other
148
144
  countries generally 90 days. Schedule permissions depend on account controls and
@@ -152,7 +148,7 @@ See [payout schedule permissions](https://docs.stripe.com/connect/manage-payout-
152
148
  and [manual payout restrictions](https://docs.stripe.com/connect/manual-payouts).
153
149
 
154
150
  Custom threshold enforcement is not provided by the snapshot or settlement APIs.
155
- It requires a selected retention boundary and migration policy. Applications must
151
+ Use the recipient-held policy, adoption and release APIs below. Applications must
156
152
  not describe a displayed threshold as enforced while Stripe controls automatic
157
153
  payouts. A numeric below/exact/above provider-floor demonstration is unavailable
158
154
  without a supported provider floor read; surface this capability limitation.
@@ -183,15 +179,15 @@ obtain control. Stripe also validates update permissions. Before each subsequent
183
179
  payout attempt, `verify` must confirm live manual settings, unchanged country,
184
180
  and an unexpired policy; stored `ready` state alone is insufficient.
185
181
 
186
- Initial activation rejects existing nonzero available or pending balances,
187
- in-flight payouts, and existing manual schedules with `migration_required`.
188
- The preflight is repeated before an initial schedule update; a delayed activation
189
- with newly arrived funds is canceled before changing Stripe. This is a bounded
190
- preflight, not an ownership inference or automatic adoption.
191
- Already-mixed funds need a reconciled source ledger and explicit adoption before
192
- custom release can be enabled. Zero-balance activation does not classify later
193
- unrelated deposits as earnings. Payout claims must use that source ledger and a
194
- fresh provider balance when the separate release boundary is integrated.
182
+ Ordinary initial activation requires zero available/pending balances, no past or
183
+ in-flight payouts, and an automatic schedule. Existing balances, payout history
184
+ or a manual schedule return `migration_required`. The ordinary preflight repeats
185
+ before a delayed initial write. For an owned funded account, use the explicit
186
+ [historical import and funded activation flow](#historical-import-and-funded-activation)
187
+ below. Unknown money is never classified as earnings merely because it is
188
+ available. Existing manual schedules still require an authorized migration to a
189
+ known automatic schedule before custom activation; the package does not guess
190
+ which schedule should be restored later.
195
191
 
196
192
  The host must schedule a recurring server job that pages `due(ctx,
197
193
  { cursor: null, numItems: 100 })`, follows its cursor, and calls `expire` for each
@@ -208,10 +204,9 @@ For opt-out, prepare a new operation with the latest revision,
208
204
  available after Dashboard control is lost, subject to Stripe permissions. It
209
205
  restores only the captured schedule; other balance settings remain untouched.
210
206
 
211
- This is the durable policy/control boundary. Automatic combined payout execution,
212
- adoption of existing balances, and its UI integration are separate work and are
213
- not supplied by these schedule APIs. Do not expose a custom minimum as fully
214
- enforced until those boundaries and the recurring expiry job are installed.
207
+ The policy/control APIs compose with historical adoption and combined release.
208
+ Install recurring release and expiry jobs before exposing custom scheduling.
209
+ React/example job integration and larger-account collection remain separate work.
215
210
  `verify.providerMinimum` is `null`: the Balance Settings API does not expose a
216
211
  numeric payout-floor field, and retained balance reserves are not payout triggers.
217
212
 
@@ -219,14 +214,13 @@ Sources: [payout schedules](https://docs.stripe.com/connect/manage-payout-schedu
219
214
  [manual payout holding limits](https://docs.stripe.com/connect/manual-payouts), and
220
215
  [Dashboard control limits](https://docs.stripe.com/connect/platform-controls-for-stripe-dashboard-accounts#control-payout-timing).
221
216
 
222
- ### Transactional claim ledger implementation status
217
+ ### Transactional source allocations
223
218
 
224
- The component contains `payoutRelease` claim and source-allocation tables. These
225
- are server-side building blocks for the release boundary, with no automatic
226
- payout-creation or scheduled bank-payout integration yet. Do not call the claim
227
- mutation with UI totals or a transfer receipt alone. Its trusted upstream caller
228
- must establish complete provider history, still-unpaid ownership, adoption of
229
- prior payouts, independent liabilities, and live payout controls first.
219
+ The package release method owns provider verification and payout creation over
220
+ `payoutRelease` claim and source-allocation tables. Use that method rather than
221
+ calling trusted component mutations with UI totals or an isolated transfer
222
+ receipt. Complete provider history, still-unpaid ownership, prior payouts,
223
+ independent liabilities and live controls must be established together.
230
224
 
231
225
  A claim combines mature creator and affiliate entitlement within one currency.
232
226
  It deducts persisted paid and reserved allocations, keeps negative entitlement
@@ -242,13 +236,11 @@ release an unaccepted claim within a conservative 23-hour window. It requires
242
236
  its own unique Stripe request receipt; ambiguous errors and older attempts stay
243
237
  held. This window stays below Stripe's [24-hour v1 idempotency retention minimum](https://docs.stripe.com/api/idempotent_requests).
244
238
 
245
- The current claim snapshot is deliberately bounded to 100 account transfer rows
246
- and 1,000 receipt IDs per source. Larger or incomplete histories reject the whole
247
- claim, without using partial totals. A resumable reconciliation/adoption process,
248
- provider payout creation and receipt verification, historical fee adoption, other account debit allocation, and
249
- recurring release/expiry integration remain required before custom payout
250
- minimums can be presented as an operational feature. In-memory claim tests are
251
- not evidence of a completed Stripe bank payout.
239
+ Current atomic snapshots support 100 account transfers, 100 historical claims and
240
+ 1,000 receipt IDs per source. Larger/incomplete histories hold the whole operation
241
+ without partial totals. Resumable larger-account collection, other provider-debit
242
+ allocation and recurring/UI integration remain required. The package does not
243
+ claim that component tests alone demonstrate a Stripe bank payout.
252
244
 
253
245
  ## Verified recipient funding receipts
254
246
 
@@ -366,7 +358,156 @@ require explicit allocation. Original debit receipts and their fees remain
366
358
  immutable. Older returned claims can receive verified fee evidence without
367
359
  releasing their principal again.
368
360
 
369
- These APIs reconcile existing claims; full automatic qualification/creation,
370
- historical source adoption, large-history collection, other account debit
371
- allocation and recurring/UI integration remain separate work. Do not expose a
372
- custom minimum as fully enforced solely because reconciliation is available.
361
+ These receipt APIs reconcile existing claims. Use the release API below for
362
+ qualification and creation; receipt reconciliation by itself does not enforce a
363
+ minimum.
364
+
365
+ ## Combined recipient release
366
+
367
+ After authorized custom policy activation, a host scheduled action can call:
368
+
369
+ ```ts
370
+ const result = await stripe.payoutPolicy.release(ctx, {
371
+ accountId,
372
+ ownerKey,
373
+ operationKey, // stable unique identity for this scheduled operation
374
+ currency: "usd",
375
+ independentLiabilities, // authorized outstanding off-provider debts/reserves
376
+ maxPages: 10,
377
+ });
378
+ ```
379
+
380
+ Authenticate the recipient and policy, and read all known off-provider debts and
381
+ reserves before invoking this server-only method. Never pass an untrusted client
382
+ amount or assume that an omitted debt is zero. Provider source refunds and payout
383
+ fees are already reconciled by the package and must not be counted again in
384
+ `independentLiabilities`. The host must install the recurring release and expiry
385
+ jobs before offering custom scheduling. This method does not install a cron.
386
+
387
+ The package verifies live manual schedule controls, reads every account source,
388
+ retrieves transfer and payout receipts, and compares the complete settlement
389
+ currency balance history with those verified receipts. Unknown credits, refunds,
390
+ fees and prior payouts return `held/unallocated_history`. A current balance is
391
+ never treated as proof of unpaid ownership. History must reconcile to the live
392
+ available and pending balance; changing or incomplete snapshots stay held.
393
+
394
+ Mature unpaid creator and affiliate sources qualify together in integer minor
395
+ units. Pending credits do not qualify. Outstanding source reservations, prior
396
+ paid allocations, actual refunds, payout fees and supplied independent liabilities
397
+ reduce eligibility. The cap uses the available card-source balance, since the
398
+ immutable provider request explicitly selects `source_type: "card"` and a
399
+ standard bank payout. The complete bank-account listing must identify one
400
+ default bank for the settlement currency. Its ID is persisted with the claim,
401
+ passed explicitly to Stripe, and checked again in the payout receipt. Other rails and currencies cannot increase that cap.
402
+
403
+ A qualifying operation reserves its exact source allocations in a component
404
+ transaction, verifies live controls again, and records the attempted marker before
405
+ calling Stripe. The provider idempotency key and request are fixed by the durable
406
+ claim. Concurrent callers therefore share one financial operation. Subsequent
407
+ calls to an attempted operation only reconcile provider receipts, including when
408
+ the original call timed out. They never blindly recreate an unidentified payout.
409
+ A verified no-object `balance_insufficient` or `amount_too_small` rejection releases
410
+ reservations; unknown errors keep them held. An interrupted unattempted operation
411
+ is canceled on replay, and a new scheduled operation can collect fresh receipts.
412
+ Below-threshold money remains owed. The numeric provider floor remains `null`
413
+ unless a supported provider contract exposes it.
414
+
415
+ Current atomic collection accepts up to 100 account transfers and 100 historical
416
+ claims. Larger accounts return `held/snapshot_limit`, without paying a partial
417
+ page. Provider history uses a 1–100 page budget, default 10; exhaustion stays held.
418
+ Use the historical import and funded activation flow below for known old funds.
419
+ Resumable larger-account collection, independent provider-debit allocation,
420
+ recurring example jobs and React controls remain integration work. Unknown funds
421
+ and unsupported accounts keep explicit migration holds.
422
+
423
+ Sources: [payout creation and source balance selection](https://docs.stripe.com/api/payouts/create),
424
+ [complete balance history and automatic-only payout filtering](https://docs.stripe.com/api/balance_transactions/list).
425
+
426
+ ## Historical import and funded activation
427
+
428
+ The host authenticates one recipient account and owner, authorizes historical
429
+ source mappings, and records consent before custom scheduling. Start with an
430
+ owned `stripe_managed` policy at its current revision. If no policy exists,
431
+ prepare/apply a default policy with `expectedRevision: -1`,
432
+ `mode: "stripe_managed"`, and `thresholds: []`. This records ownership and keeps
433
+ the current automatic schedule.
434
+
435
+ Import past payouts whose allocations are not already recorded by BetterStripe:
436
+
437
+ ```ts
438
+ const imported = await stripe.payoutPolicy.adoptPayouts(ctx, {
439
+ accountId,
440
+ ownerKey,
441
+ expectedRevision: policy.revision,
442
+ currency: "usd",
443
+ payouts: [
444
+ {
445
+ stripePayoutId: historicalPayoutId,
446
+ allocations: [
447
+ { stripeTransferId: creatorTransferId, amount: 600 },
448
+ { stripeTransferId: affiliateTransferId, amount: 400 },
449
+ ],
450
+ },
451
+ ],
452
+ });
453
+ ```
454
+
455
+ Amounts use integer settlement-currency minor units. The package does not infer
456
+ manual-payout ownership from a present balance or Stripe's automatic-only payout
457
+ filter. Supply authorized accounting for every unrecorded historical payout in
458
+ the currency. Existing package claims supply their immutable allocations on
459
+ subsequent calls; use `payouts: []` to verify/reconcile already imported history.
460
+ The same flow supports explicit accounting for automatic historical payouts.
461
+
462
+ The package retrieves actual recipient funding and payout receipts, verifies
463
+ full history against live available/pending cash, and validates each allocation
464
+ against source maturity at the historical payout date. Future credits and
465
+ not-yet-mature bank returns cannot fund an earlier payout. Terminal paid and
466
+ verified returned payouts are supported; pending or ambiguous outcomes stay
467
+ held. Unknown credits, debits, fees or unallocated payouts require further
468
+ reconciliation. All unknown historical payouts import as one component
469
+ transaction, including fees and source allocations. Changed mappings and stale
470
+ competing snapshots reject without mixing their writes. Replays never create a
471
+ payout. Historical paid principal remains paid after later refunds, so debt
472
+ reduces later qualifying funds rather than making the old commission payable
473
+ again. An adopted payout can reconcile a subsequent bank failure without
474
+ requiring a package marker that was absent from its original Stripe object.
475
+
476
+ After successful import, explicitly request funded-account adoption during
477
+ preparation, using the latest owned policy revision:
478
+
479
+ ```ts
480
+ const prepared = await stripe.payoutPolicy.prepare(ctx, {
481
+ accountId,
482
+ ownerKey,
483
+ operationKey,
484
+ expectedRevision: policy.revision,
485
+ mode: "custom",
486
+ thresholds: [{ currency: "usd", amount: 1000 }],
487
+ consent: true,
488
+ adoptExistingBalance: true,
489
+ });
490
+ if (prepared.status === "prepared") {
491
+ await stripe.payoutPolicy.apply(ctx, { accountId, ownerKey, operationKey });
492
+ }
493
+ ```
494
+
495
+ Preparation verifies all settlement currencies, full source membership and prior
496
+ payouts. It repeats provider-history and balance checks before a delayed initial
497
+ attempt, then checks local financial fingerprints under OCC before recording
498
+ the attempt marker. New unallocated history cancels an unattempted activation
499
+ while preserving the automatic schedule. Refresh the policy revision and use a
500
+ new operation key after cancellation. If a manual schedule appears without the
501
+ package's attempt marker, the operation reports schedule drift rather than
502
+ pretending the package performed the change. Provider timeouts retain the
503
+ persisted attempt and immutable schedule request for reconciliation.
504
+
505
+ Holding age follows the unpaid funds. Paid original credits do not age newer
506
+ funds; source debt and verified payout fees consume older ownership first.
507
+ Verified bank-return cash starts a new holding age when it becomes available.
508
+ Preparation cannot restart an expired holding period, and later checks can only
509
+ shorten the pending operation's deadline. `migration_required.adoptionReason`
510
+ explains unknown history or an expired holding period. This flow does not enable
511
+ Dashboard accounts with withdrawal bypasses, Global Payouts, unsupported source
512
+ rails, unknown numeric provider floors or accounts beyond the current atomic
513
+ snapshot bounds. It installs no host cron or UI.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fantastic.dev/better-stripe",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "description": "A Convex component for Stripe V2 Accounts API integration.",
5
5
  "keywords": [
6
6
  "stripe",
@@ -0,0 +1,101 @@
1
+ import type Stripe from "stripe";
2
+ import type { Infer } from "convex/values";
3
+ import type { Component, RunCtx } from "../helpers.js";
4
+ import type { adoptionProof } from "../../component/payoutPolicy/schema.js";
5
+ import type { claimDoc } from "../../component/payoutRelease/schema.js";
6
+ import { payoutClaimFingerprint } from "../../component/payoutRelease/helpers.js";
7
+ import { componentRef } from "../webhooks/helpers.js";
8
+ import { adoptRecipientPayouts } from "./payoutAdoption.js";
9
+ function balanceIdentity(b: Stripe.Balance) {
10
+ return JSON.stringify(
11
+ [b.available, b.pending].map((rows) =>
12
+ rows
13
+ .map((t) => [
14
+ t.currency,
15
+ t.amount,
16
+ Object.entries(t.source_types ?? {}).sort(),
17
+ ])
18
+ .sort((a, b) => String(a[0]).localeCompare(String(b[0]))),
19
+ ),
20
+ );
21
+ }
22
+ function transactionIdentity(t?: Stripe.BalanceTransaction) {
23
+ return t
24
+ ? JSON.stringify([
25
+ t.id,
26
+ t.source,
27
+ t.amount,
28
+ t.net,
29
+ t.currency,
30
+ t.status,
31
+ t.available_on,
32
+ ])
33
+ : "none";
34
+ }
35
+ /** Complete read/reconcile proof. Unknown historical payouts require explicit import first. */
36
+ export async function collectAdoptionProof(
37
+ stripe: Stripe,
38
+ component: Component,
39
+ ctx: RunCtx,
40
+ scope: { accountId: string; ownerKey: string },
41
+ expectedRevision: number,
42
+ configuredCurrencies: string[],
43
+ ): Promise<
44
+ | { status: "verified"; proof: Infer<typeof adoptionProof> }
45
+ | { status: "held"; reason: string }
46
+ > {
47
+ const request = { stripeAccount: scope.accountId };
48
+ const initial = await stripe.balance.retrieve(undefined, request);
49
+ const first = await stripe.balanceTransactions.list({ limit: 1 }, request);
50
+ const queue = new Set([
51
+ ...configuredCurrencies,
52
+ ...initial.available.map((t) => t.currency),
53
+ ...initial.pending.map((t) => t.currency),
54
+ ...(first.data[0] ? [first.data[0].currency] : []),
55
+ ]);
56
+ const completed = new Set<string>();
57
+ let observations: Infer<typeof adoptionProof>["observations"] = [],
58
+ oldestHeldAt: number | undefined;
59
+ for (const currency of queue) {
60
+ if (completed.size >= 50)
61
+ return { status: "held", reason: "currency_history_incomplete" };
62
+ const result = await adoptRecipientPayouts(
63
+ stripe,
64
+ component,
65
+ ctx,
66
+ { ...scope, currency, expectedRevision, payouts: [] },
67
+ true,
68
+ );
69
+ if (result.status !== "adopted") return result;
70
+ completed.add(currency);
71
+ for (const c of result.historyCurrencies) queue.add(c);
72
+ observations = result.observations;
73
+ if (result.oldestHeldAt !== undefined)
74
+ oldestHeldAt = Math.min(oldestHeldAt ?? Infinity, result.oldestHeldAt);
75
+ }
76
+ const balance = await stripe.balance.retrieve(undefined, request);
77
+ const latest = await stripe.balanceTransactions.list({ limit: 1 }, request);
78
+ if (
79
+ balanceIdentity(initial) !== balanceIdentity(balance) ||
80
+ transactionIdentity(first.data[0]) !== transactionIdentity(latest.data[0])
81
+ )
82
+ return { status: "held", reason: "provider_changed" };
83
+ const snapshot = (await ctx.runQuery(
84
+ componentRef(component, "payoutRelease/queries/snapshot"),
85
+ { accountId: scope.accountId, ownerKey: scope.ownerKey },
86
+ )) as
87
+ | { status: "ready"; claims: Infer<typeof claimDoc>[] }
88
+ | { status: "held"; reason: string };
89
+ if (snapshot.status !== "ready") return snapshot;
90
+ return {
91
+ status: "verified",
92
+ proof: {
93
+ observations,
94
+ claims: snapshot.claims.map((c) => ({
95
+ operationKey: c.operationKey,
96
+ fingerprint: payoutClaimFingerprint(c),
97
+ })),
98
+ ...(oldestHeldAt !== undefined ? { oldestHeldAt } : {}),
99
+ },
100
+ };
101
+ }