@fantastic.dev/better-stripe 0.22.0 → 0.23.1

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 (97) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +5 -3
  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 +231 -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/payoutAdoptionTestHelpers.d.ts +2617 -0
  20. package/dist/client/connect/payoutAdoptionTestHelpers.d.ts.map +1 -0
  21. package/dist/client/connect/payoutAdoptionTestHelpers.js +231 -0
  22. package/dist/client/connect/payoutAdoptionTestHelpers.js.map +1 -0
  23. package/dist/client/connect/payoutReconciliation.d.ts.map +1 -1
  24. package/dist/client/connect/payoutReconciliation.js +4 -2
  25. package/dist/client/connect/payoutReconciliation.js.map +1 -1
  26. package/dist/client/payoutPolicy.d.ts +7 -1
  27. package/dist/client/payoutPolicy.d.ts.map +1 -1
  28. package/dist/client/payoutPolicy.js +78 -7
  29. package/dist/client/payoutPolicy.js.map +1 -1
  30. package/dist/component/_generated/component.d.ts +196 -0
  31. package/dist/component/_generated/component.d.ts.map +1 -1
  32. package/dist/component/earnings/triggers.d.ts +38 -4
  33. package/dist/component/earnings/triggers.d.ts.map +1 -1
  34. package/dist/component/payoutPolicy/adoption.d.ts +9 -0
  35. package/dist/component/payoutPolicy/adoption.d.ts.map +1 -0
  36. package/dist/component/payoutPolicy/adoption.js +45 -0
  37. package/dist/component/payoutPolicy/adoption.js.map +1 -0
  38. package/dist/component/payoutPolicy/mutations.d.ts +96 -0
  39. package/dist/component/payoutPolicy/mutations.d.ts.map +1 -1
  40. package/dist/component/payoutPolicy/mutations.js +38 -5
  41. package/dist/component/payoutPolicy/mutations.js.map +1 -1
  42. package/dist/component/payoutPolicy/queries.d.ts +16 -0
  43. package/dist/component/payoutPolicy/queries.d.ts.map +1 -1
  44. package/dist/component/payoutPolicy/schema.d.ts +299 -2
  45. package/dist/component/payoutPolicy/schema.d.ts.map +1 -1
  46. package/dist/component/payoutPolicy/schema.js +7 -0
  47. package/dist/component/payoutPolicy/schema.js.map +1 -1
  48. package/dist/component/payoutRelease/adoption.d.ts +269 -0
  49. package/dist/component/payoutRelease/adoption.d.ts.map +1 -0
  50. package/dist/component/payoutRelease/adoption.js +197 -0
  51. package/dist/component/payoutRelease/adoption.js.map +1 -0
  52. package/dist/component/payoutRelease/helpers.d.ts +3 -0
  53. package/dist/component/payoutRelease/helpers.d.ts.map +1 -1
  54. package/dist/component/payoutRelease/helpers.js +20 -0
  55. package/dist/component/payoutRelease/helpers.js.map +1 -1
  56. package/dist/component/payoutRelease/mutations.d.ts +81 -0
  57. package/dist/component/payoutRelease/mutations.d.ts.map +1 -1
  58. package/dist/component/payoutRelease/mutations.js +6 -0
  59. package/dist/component/payoutRelease/mutations.js.map +1 -1
  60. package/dist/component/payoutRelease/observation.d.ts +18 -0
  61. package/dist/component/payoutRelease/observation.d.ts.map +1 -0
  62. package/dist/component/payoutRelease/observation.js +11 -0
  63. package/dist/component/payoutRelease/observation.js.map +1 -0
  64. package/dist/component/payoutRelease/queries.d.ts +3 -0
  65. package/dist/component/payoutRelease/queries.d.ts.map +1 -1
  66. package/dist/component/payoutRelease/schema.d.ts +8 -19
  67. package/dist/component/payoutRelease/schema.d.ts.map +1 -1
  68. package/dist/component/payoutRelease/schema.js +3 -9
  69. package/dist/component/payoutRelease/schema.js.map +1 -1
  70. package/dist/component/schema.d.ts +73 -2
  71. package/docs/settlement.md +127 -40
  72. package/package.json +1 -1
  73. package/src/client/connect/payoutActivationProof.ts +101 -0
  74. package/src/client/connect/payoutAdoption.test.ts +186 -0
  75. package/src/client/connect/payoutAdoption.ts +331 -0
  76. package/src/client/connect/payoutAdoptionAge.test.ts +89 -0
  77. package/src/client/connect/payoutAdoptionAge.ts +74 -0
  78. package/src/client/connect/payoutAdoptionAllocation.test.ts +132 -0
  79. package/src/client/connect/payoutAdoptionAllocation.ts +106 -0
  80. package/src/client/connect/payoutAdoptionTestHelpers.ts +253 -0
  81. package/src/client/connect/payoutReconciliation.test.ts +16 -0
  82. package/src/client/connect/payoutReconciliation.ts +6 -2
  83. package/src/client/connect/payoutReturnedAdoption.test.ts +157 -0
  84. package/src/client/payoutPolicy.test.ts +16 -0
  85. package/src/client/payoutPolicy.ts +107 -5
  86. package/src/component/_generated/api.ts +6 -0
  87. package/src/component/_generated/component.ts +192 -1
  88. package/src/component/payout-adoption.test.ts +272 -0
  89. package/src/component/payout-policy-adoption.test.ts +131 -0
  90. package/src/component/payoutPolicy/adoption.ts +76 -0
  91. package/src/component/payoutPolicy/mutations.ts +46 -3
  92. package/src/component/payoutPolicy/schema.ts +9 -0
  93. package/src/component/payoutRelease/adoption.ts +262 -0
  94. package/src/component/payoutRelease/helpers.ts +21 -0
  95. package/src/component/payoutRelease/mutations.ts +10 -0
  96. package/src/component/payoutRelease/observation.ts +10 -0
  97. package/src/component/payoutRelease/schema.ts +3 -9
@@ -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
 
@@ -423,10 +415,105 @@ unless a supported provider contract exposes it.
423
415
  Current atomic collection accepts up to 100 account transfers and 100 historical
424
416
  claims. Larger accounts return `held/snapshot_limit`, without paying a partial
425
417
  page. Provider history uses a 1–100 page budget, default 10; exhaustion stays held.
426
- Explicit prior-payout/source adoption, resumable larger-account collection,
427
- independent provider-debit allocation, recurring example jobs and React controls
428
- remain integration work. Existing funded accounts still follow the policy
429
- migration safeguards; do not bypass `migration_required` to activate this API.
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.
430
422
 
431
423
  Sources: [payout creation and source balance selection](https://docs.stripe.com/api/payouts/create),
432
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
+ For a verified bank return, holding-age proof retains the original paid
463
+ principal and adds the returned credit at its availability time. The returned
464
+ claim contributes no currently paid source allocation. Net provider fees still
465
+ reduce the combined unpaid balance. Replaying adoption preserves the same claim;
466
+ a later release can include returned cash and new funding exactly once.
467
+
468
+ The package retrieves actual recipient funding and payout receipts, verifies
469
+ full history against live available/pending cash, and validates each allocation
470
+ against source maturity at the historical payout date. Future credits and
471
+ not-yet-mature bank returns cannot fund an earlier payout. Terminal paid and
472
+ verified returned payouts are supported; pending or ambiguous outcomes stay
473
+ held. Unknown credits, debits, fees or unallocated payouts require further
474
+ reconciliation. All unknown historical payouts import as one component
475
+ transaction, including fees and source allocations. Changed mappings and stale
476
+ competing snapshots reject without mixing their writes. Replays never create a
477
+ payout. Historical paid principal remains paid after later refunds, so debt
478
+ reduces later qualifying funds rather than making the old commission payable
479
+ again. An adopted payout can reconcile a subsequent bank failure without
480
+ requiring a package marker that was absent from its original Stripe object.
481
+
482
+ After successful import, explicitly request funded-account adoption during
483
+ preparation, using the latest owned policy revision:
484
+
485
+ ```ts
486
+ const prepared = await stripe.payoutPolicy.prepare(ctx, {
487
+ accountId,
488
+ ownerKey,
489
+ operationKey,
490
+ expectedRevision: policy.revision,
491
+ mode: "custom",
492
+ thresholds: [{ currency: "usd", amount: 1000 }],
493
+ consent: true,
494
+ adoptExistingBalance: true,
495
+ });
496
+ if (prepared.status === "prepared") {
497
+ await stripe.payoutPolicy.apply(ctx, { accountId, ownerKey, operationKey });
498
+ }
499
+ ```
500
+
501
+ Preparation verifies all settlement currencies, full source membership and prior
502
+ payouts. It repeats provider-history and balance checks before a delayed initial
503
+ attempt, then checks local financial fingerprints under OCC before recording
504
+ the attempt marker. New unallocated history cancels an unattempted activation
505
+ while preserving the automatic schedule. Refresh the policy revision and use a
506
+ new operation key after cancellation. If a manual schedule appears without the
507
+ package's attempt marker, the operation reports schedule drift rather than
508
+ pretending the package performed the change. Provider timeouts retain the
509
+ persisted attempt and immutable schedule request for reconciliation.
510
+
511
+ Holding age follows the unpaid funds. Paid original credits do not age newer
512
+ funds; source debt and verified payout fees consume older ownership first.
513
+ Verified bank-return cash starts a new holding age when it becomes available.
514
+ Preparation cannot restart an expired holding period, and later checks can only
515
+ shorten the pending operation's deadline. `migration_required.adoptionReason`
516
+ explains unknown history or an expired holding period. This flow does not enable
517
+ Dashboard accounts with withdrawal bypasses, Global Payouts, unsupported source
518
+ rails, unknown numeric provider floors or accounts beyond the current atomic
519
+ 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.22.0",
3
+ "version": "0.23.1",
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
+ }
@@ -0,0 +1,186 @@
1
+ // @vitest-environment edge-runtime
2
+ import { expect, it, vi } from "vitest";
3
+ import type Stripe from "stripe";
4
+ import {
5
+ component,
6
+ scope,
7
+ setup,
8
+ historical,
9
+ } from "./payoutAdoptionTestHelpers.js";
10
+ it("imports a verified historical manual payout with exact source allocations", async () => {
11
+ const s = await historical();
12
+ expect(await s.adopt()).toMatchObject({
13
+ status: "adopted",
14
+ claims: [{ historical: true, status: "paid", amount: 1000 }],
15
+ });
16
+ await s.adopt();
17
+ expect(s.provider.payouts.create).not.toHaveBeenCalled();
18
+ expect(
19
+ await s.t.run((ctx) => ctx.db.query("payoutSourceAllocations").collect()),
20
+ ).toMatchObject([
21
+ { paidAmount: 600, reservedAmount: 0 },
22
+ { paidAmount: 400, reservedAmount: 0 },
23
+ ]);
24
+ });
25
+ it("requires explicit historical allocations instead of inferring ownership from balance", async () => {
26
+ const s = await historical();
27
+ expect(await s.adopt({ payouts: [] })).toMatchObject({
28
+ status: "held",
29
+ reason: "historical_allocation_required",
30
+ });
31
+ expect(
32
+ await s.t.run((ctx) => ctx.db.query("payoutReleaseClaims").collect()),
33
+ ).toEqual([]);
34
+ });
35
+ it("rejects unavailable funding without writing any partial import", async () => {
36
+ const s = await historical();
37
+ s.setTransactions(
38
+ s
39
+ .transactions()
40
+ .map((t) =>
41
+ t.source === "py_1"
42
+ ? { ...t, available_on: Math.floor(Date.now() / 1000) + 100 }
43
+ : t,
44
+ ),
45
+ );
46
+ await expect(s.adopt()).rejects.toThrow(/funding/i);
47
+ expect(
48
+ await s.t.run((ctx) => ctx.db.query("payoutReleaseClaims").collect()),
49
+ ).toEqual([]);
50
+ });
51
+ it("holds unrelated history before writing source allocations", async () => {
52
+ const s = await historical();
53
+ s.setTransactions([
54
+ ...s.transactions(),
55
+ { ...s.transactions()[0], id: "txn_unowned", source: "po_unknown" },
56
+ ]);
57
+ expect(await s.adopt()).toMatchObject({
58
+ status: "held",
59
+ reason: "unallocated_history",
60
+ });
61
+ expect(
62
+ await s.t.run((ctx) => ctx.db.query("payoutSourceAllocations").collect()),
63
+ ).toEqual([]);
64
+ });
65
+ async function activation() {
66
+ const s = await setup();
67
+ const now = Math.floor(Date.now() / 1000);
68
+ s.setTransactions(
69
+ s
70
+ .transactions()
71
+ .map((t) => ({ ...t, created: now - 100, available_on: now - 50 })),
72
+ );
73
+ let schedule: Record<string, unknown> = {
74
+ interval: "weekly",
75
+ weekly_payout_days: ["monday"],
76
+ };
77
+ const settings = {
78
+ retrieve: vi.fn(async () => ({
79
+ payments: { payouts: { status: "enabled", schedule } },
80
+ })),
81
+ update: vi.fn(
82
+ async (args: {
83
+ payments: { payouts: { schedule: Record<string, unknown> } };
84
+ }) => {
85
+ schedule = args.payments.payouts.schedule;
86
+ return { payments: { payouts: { status: "enabled", schedule } } };
87
+ },
88
+ ),
89
+ };
90
+ Object.assign(s.provider, {
91
+ v2: {
92
+ core: {
93
+ accounts: {
94
+ retrieve: vi.fn(async () => ({
95
+ id: scope.accountId,
96
+ dashboard: "none",
97
+ identity: { country: "US" },
98
+ })),
99
+ },
100
+ },
101
+ },
102
+ balanceSettings: settings,
103
+ });
104
+ const { PayoutPolicyClient } = await import("../payoutPolicy.js");
105
+ const client = new PayoutPolicyClient(
106
+ component,
107
+ () => s.provider as unknown as Stripe,
108
+ );
109
+ const opts = {
110
+ accountId: scope.accountId,
111
+ ownerKey: scope.ownerKey,
112
+ operationKey: "activate",
113
+ expectedRevision: 0,
114
+ mode: "custom" as const,
115
+ thresholds: [{ currency: "usd", amount: 1000 }],
116
+ consent: true as const,
117
+ adoptExistingBalance: true as const,
118
+ };
119
+ return { ...s, client, settings, opts };
120
+ }
121
+ it("explicit funded activation verifies complete history, then releases through the public package", async () => {
122
+ const s = await activation();
123
+ expect(await s.client.prepare(s.ctx, s.opts)).toMatchObject({
124
+ status: "prepared",
125
+ operation: { adoption: { oldestHeldAt: expect.any(Number) } },
126
+ });
127
+ expect(
128
+ await s.client.apply(s.ctx, { ...scope, operationKey: "activate" }),
129
+ ).toMatchObject({ status: "ready" });
130
+ expect(await s.client.release(s.ctx, scope)).toMatchObject({
131
+ status: "updated",
132
+ claim: { status: "paid", amount: 1000 },
133
+ });
134
+ expect(s.provider.payouts.create).toHaveBeenCalledTimes(1);
135
+ });
136
+ it("new unallocated provider history before a delayed attempt cancels activation safely", async () => {
137
+ const s = await activation();
138
+ await s.client.prepare(s.ctx, s.opts);
139
+ s.setTransactions([
140
+ ...s.transactions(),
141
+ {
142
+ ...s.transactions()[0],
143
+ id: "txn_unknown",
144
+ source: "py_unknown",
145
+ amount: 10,
146
+ net: 10,
147
+ },
148
+ ]);
149
+ expect(
150
+ await s.client.apply(s.ctx, { ...scope, operationKey: "activate" }),
151
+ ).toMatchObject({
152
+ status: "migration_required",
153
+ adoptionReason: "unallocated_history",
154
+ });
155
+ expect(s.settings.update).not.toHaveBeenCalled();
156
+ expect(await s.client.get(s.ctx, scope)).toMatchObject({
157
+ mode: "stripe_managed",
158
+ status: "ready",
159
+ });
160
+ });
161
+ it("funded activation remains explicit", async () => {
162
+ const s = await activation();
163
+ expect(
164
+ await s.client.prepare(s.ctx, {
165
+ ...s.opts,
166
+ adoptExistingBalance: undefined,
167
+ }),
168
+ ).toMatchObject({ status: "migration_required", reason: "existing_balance" });
169
+ expect(s.settings.update).not.toHaveBeenCalled();
170
+ });
171
+ it("cannot restart the holding period for already-expired funds", async () => {
172
+ const s = await activation();
173
+ s.setTransactions(
174
+ s.transactions().map((t) => ({ ...t, created: 100, available_on: 200 })),
175
+ );
176
+ expect(await s.client.prepare(s.ctx, s.opts)).toMatchObject({
177
+ status: "migration_required",
178
+ adoptionReason: "holding_deadline_expired",
179
+ });
180
+ expect(await s.client.get(s.ctx, scope)).toMatchObject({
181
+ mode: "stripe_managed",
182
+ status: "ready",
183
+ revision: 0,
184
+ });
185
+ expect(s.settings.update).not.toHaveBeenCalled();
186
+ });