@fantastic.dev/better-stripe 0.23.0 → 0.23.2

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 (87) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/dist/client/connect/payoutAdoption.d.ts.map +1 -1
  4. package/dist/client/connect/payoutAdoption.js +4 -3
  5. package/dist/client/connect/payoutAdoption.js.map +1 -1
  6. package/dist/client/connect/payoutAdoptionTestHelpers.d.ts +2659 -0
  7. package/dist/client/connect/payoutAdoptionTestHelpers.d.ts.map +1 -0
  8. package/dist/client/connect/payoutAdoptionTestHelpers.js +231 -0
  9. package/dist/client/connect/payoutAdoptionTestHelpers.js.map +1 -0
  10. package/dist/client/connect/payoutSync.d.ts +32 -0
  11. package/dist/client/connect/payoutSync.d.ts.map +1 -0
  12. package/dist/client/connect/payoutSync.js +48 -0
  13. package/dist/client/connect/payoutSync.js.map +1 -0
  14. package/dist/client/connect/payoutSyncTestHelpers.d.ts +1263 -0
  15. package/dist/client/connect/payoutSyncTestHelpers.d.ts.map +1 -0
  16. package/dist/client/connect/payoutSyncTestHelpers.js +62 -0
  17. package/dist/client/connect/payoutSyncTestHelpers.js.map +1 -0
  18. package/dist/client/connect/payoutSyncValidation.d.ts +16 -0
  19. package/dist/client/connect/payoutSyncValidation.d.ts.map +1 -0
  20. package/dist/client/connect/payoutSyncValidation.js +54 -0
  21. package/dist/client/connect/payoutSyncValidation.js.map +1 -0
  22. package/dist/client/index.d.ts +26 -9
  23. package/dist/client/index.d.ts.map +1 -1
  24. package/dist/client/index.js +3 -5
  25. package/dist/client/index.js.map +1 -1
  26. package/dist/client/readContracts.d.ts +8 -0
  27. package/dist/client/readContracts.d.ts.map +1 -1
  28. package/dist/client/readContracts.js +7 -0
  29. package/dist/client/readContracts.js.map +1 -1
  30. package/dist/component/_generated/component.d.ts +69 -0
  31. package/dist/component/_generated/component.d.ts.map +1 -1
  32. package/dist/component/connect/mutations.d.ts.map +1 -1
  33. package/dist/component/connect/mutations.js +4 -27
  34. package/dist/component/connect/mutations.js.map +1 -1
  35. package/dist/component/connect/payoutWrites.d.ts +21 -0
  36. package/dist/component/connect/payoutWrites.d.ts.map +1 -0
  37. package/dist/component/connect/payoutWrites.js +19 -0
  38. package/dist/component/connect/payoutWrites.js.map +1 -0
  39. package/dist/component/earnings/triggers.d.ts +44 -0
  40. package/dist/component/earnings/triggers.d.ts.map +1 -1
  41. package/dist/component/payoutSync/helpers.d.ts +44 -0
  42. package/dist/component/payoutSync/helpers.d.ts.map +1 -0
  43. package/dist/component/payoutSync/helpers.js +60 -0
  44. package/dist/component/payoutSync/helpers.js.map +1 -0
  45. package/dist/component/payoutSync/mutations.d.ts +51 -0
  46. package/dist/component/payoutSync/mutations.d.ts.map +1 -0
  47. package/dist/component/payoutSync/mutations.js +88 -0
  48. package/dist/component/payoutSync/mutations.js.map +1 -0
  49. package/dist/component/payoutSync/queries.d.ts +16 -0
  50. package/dist/component/payoutSync/queries.d.ts.map +1 -0
  51. package/dist/component/payoutSync/queries.js +11 -0
  52. package/dist/component/payoutSync/queries.js.map +1 -0
  53. package/dist/component/payoutSync/schema.d.ts +46 -0
  54. package/dist/component/payoutSync/schema.d.ts.map +1 -0
  55. package/dist/component/payoutSync/schema.js +22 -0
  56. package/dist/component/payoutSync/schema.js.map +1 -0
  57. package/dist/component/payoutSync/validators.d.ts +137 -0
  58. package/dist/component/payoutSync/validators.d.ts.map +1 -0
  59. package/dist/component/payoutSync/validators.js +19 -0
  60. package/dist/component/payoutSync/validators.js.map +1 -0
  61. package/dist/component/schema.d.ts +21 -0
  62. package/dist/component/schema.js +2 -0
  63. package/dist/component/schema.js.map +1 -1
  64. package/docs/read-contracts.md +40 -5
  65. package/docs/settlement.md +6 -0
  66. package/package.json +1 -1
  67. package/src/client/connect/payoutAdoption.test.ts +6 -241
  68. package/src/client/connect/payoutAdoption.ts +4 -3
  69. package/src/client/connect/payoutAdoptionTestHelpers.ts +253 -0
  70. package/src/client/connect/payoutReturnedAdoption.test.ts +157 -0
  71. package/src/client/connect/payoutSync.test.ts +313 -0
  72. package/src/client/connect/payoutSync.ts +93 -0
  73. package/src/client/connect/payoutSyncTestHelpers.ts +82 -0
  74. package/src/client/connect/payoutSyncValidation.ts +74 -0
  75. package/src/client/index.ts +14 -11
  76. package/src/client/readContracts.ts +12 -0
  77. package/src/component/_generated/api.ts +10 -0
  78. package/src/component/_generated/component.ts +82 -0
  79. package/src/component/connect/mutations.ts +4 -30
  80. package/src/component/connect/payoutWrites.ts +25 -0
  81. package/src/component/payout-sync.test.ts +117 -0
  82. package/src/component/payoutSync/helpers.ts +86 -0
  83. package/src/component/payoutSync/mutations.ts +102 -0
  84. package/src/component/payoutSync/queries.ts +10 -0
  85. package/src/component/payoutSync/schema.ts +25 -0
  86. package/src/component/payoutSync/validators.ts +21 -0
  87. package/src/component/schema.ts +2 -0
@@ -1,4 +1,25 @@
1
1
  declare const _default: import("convex/server").SchemaDefinition<{
2
+ payoutSyncClaims: import("convex/server").TableDefinition<import("convex/values").VObject<{
3
+ acceptedAt?: number | undefined;
4
+ acceptedFingerprint?: string | undefined;
5
+ acceptedGeneration?: number | undefined;
6
+ accountId: string;
7
+ generation: number;
8
+ startedAt: number;
9
+ status: "accepted" | "pending" | "unknown";
10
+ stripePayoutId: string;
11
+ }, {
12
+ stripePayoutId: import("convex/values").VString<string, "required">;
13
+ accountId: import("convex/values").VString<string, "required">;
14
+ generation: import("convex/values").VFloat64<number, "required">;
15
+ status: import("convex/values").VUnion<"accepted" | "pending" | "unknown", [import("convex/values").VLiteral<"pending", "required">, import("convex/values").VLiteral<"accepted", "required">, import("convex/values").VLiteral<"unknown", "required">], "required", never>;
16
+ startedAt: import("convex/values").VFloat64<number, "required">;
17
+ acceptedGeneration: import("convex/values").VFloat64<number | undefined, "optional">;
18
+ acceptedAt: import("convex/values").VFloat64<number | undefined, "optional">;
19
+ acceptedFingerprint: import("convex/values").VString<string | undefined, "optional">;
20
+ }, "required", "acceptedAt" | "acceptedFingerprint" | "acceptedGeneration" | "accountId" | "generation" | "startedAt" | "status" | "stripePayoutId">, {
21
+ by_stripe_payout_id: ["stripePayoutId", "_creationTime"];
22
+ }, {}, {}>;
2
23
  payoutFeeBalances: import("convex/server").TableDefinition<import("convex/values").VObject<{
3
24
  accountId: string;
4
25
  amount: number;
@@ -1,3 +1,4 @@
1
+ import { claimsTable as payoutSyncClaimsTable } from "./payoutSync/schema.js";
1
2
  import { feeBalancesTable as payoutFeeBalancesTable, claimsTable as payoutReleaseClaimsTable, sourcesTable as payoutSourceAllocationsTable, } from "./payoutRelease/schema.js";
2
3
  import { policiesTable, operationsTable } from "./payoutPolicy/schema.js";
3
4
  import { currencyTotalsTable } from "./earnings/schema.js";
@@ -10,6 +11,7 @@ import { accountsTable } from "./core/schema";
10
11
  import { pricesTable, productsTable } from "./products/schema";
11
12
  import { webhookEventsTable } from "./webhooks/schema";
12
13
  export default defineSchema({
14
+ payoutSyncClaims: payoutSyncClaimsTable,
13
15
  payoutFeeBalances: payoutFeeBalancesTable,
14
16
  payoutReleaseClaims: payoutReleaseClaimsTable,
15
17
  payoutSourceAllocations: payoutSourceAllocationsTable,
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/component/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,IAAI,sBAAsB,EAC1C,WAAW,IAAI,wBAAwB,EACvC,YAAY,IAAI,4BAA4B,GAC7C,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EACL,WAAW,EACX,aAAa,EACb,WAAW,EACX,iBAAiB,GAClB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE7C,OAAO,EACL,qBAAqB,EACrB,aAAa,EACb,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,aAAa,EACb,aAAa,EACb,YAAY,EACZ,sBAAsB,EACtB,YAAY,EACZ,wBAAwB,EACxB,cAAc,GACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD,eAAe,YAAY,CAAC;IAC1B,iBAAiB,EAAE,sBAAsB;IACzC,mBAAmB,EAAE,wBAAwB;IAC7C,uBAAuB,EAAE,4BAA4B;IACrD,cAAc,EAAE,aAAa;IAC7B,sBAAsB,EAAE,eAAe;IACvC,cAAc,EAAE,mBAAmB;IACnC,cAAc,EAAE,WAAW;IAC3B,gBAAgB,EAAE,aAAa;IAC/B,cAAc,EAAE,WAAW;IAC3B,oBAAoB,EAAE,iBAAiB;IACvC,qBAAqB,EAAE,gBAAgB;IACvC,eAAe,EAAE,oBAAoB;IACrC,mBAAmB,EAAE,wBAAwB;IAC7C,OAAO;IACP,QAAQ,EAAE,aAAa;IAEvB,WAAW;IACX,QAAQ,EAAE,aAAa;IACvB,MAAM,EAAE,WAAW;IAEnB,UAAU;IACV,aAAa,EAAE,kBAAkB;IACjC,gBAAgB,EAAE,qBAAqB;IACvC,QAAQ,EAAE,aAAa;IAEvB,UAAU;IACV,QAAQ,EAAE,aAAa;IACvB,OAAO,EAAE,YAAY;IACrB,iBAAiB,EAAE,sBAAsB;IACzC,OAAO,EAAE,YAAY;IACrB,QAAQ,EAAE,aAAa;IACvB,SAAS,EAAE,cAAc;IACzB,mBAAmB,EAAE,wBAAwB;IAE7C,WAAW;IACX,aAAa,EAAE,kBAAkB;CAClC,CAAC,CAAC"}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/component/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,IAAI,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAC9E,OAAO,EACL,gBAAgB,IAAI,sBAAsB,EAC1C,WAAW,IAAI,wBAAwB,EACvC,YAAY,IAAI,4BAA4B,GAC7C,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EACL,WAAW,EACX,aAAa,EACb,WAAW,EACX,iBAAiB,GAClB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE7C,OAAO,EACL,qBAAqB,EACrB,aAAa,EACb,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,aAAa,EACb,aAAa,EACb,YAAY,EACZ,sBAAsB,EACtB,YAAY,EACZ,wBAAwB,EACxB,cAAc,GACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD,eAAe,YAAY,CAAC;IAC1B,gBAAgB,EAAE,qBAAqB;IACvC,iBAAiB,EAAE,sBAAsB;IACzC,mBAAmB,EAAE,wBAAwB;IAC7C,uBAAuB,EAAE,4BAA4B;IACrD,cAAc,EAAE,aAAa;IAC7B,sBAAsB,EAAE,eAAe;IACvC,cAAc,EAAE,mBAAmB;IACnC,cAAc,EAAE,WAAW;IAC3B,gBAAgB,EAAE,aAAa;IAC/B,cAAc,EAAE,WAAW;IAC3B,oBAAoB,EAAE,iBAAiB;IACvC,qBAAqB,EAAE,gBAAgB;IACvC,eAAe,EAAE,oBAAoB;IACrC,mBAAmB,EAAE,wBAAwB;IAC7C,OAAO;IACP,QAAQ,EAAE,aAAa;IAEvB,WAAW;IACX,QAAQ,EAAE,aAAa;IACvB,MAAM,EAAE,WAAW;IAEnB,UAAU;IACV,aAAa,EAAE,kBAAkB;IACjC,gBAAgB,EAAE,qBAAqB;IACvC,QAAQ,EAAE,aAAa;IAEvB,UAAU;IACV,QAAQ,EAAE,aAAa;IACvB,OAAO,EAAE,YAAY;IACrB,iBAAiB,EAAE,sBAAsB;IACzC,OAAO,EAAE,YAAY;IACrB,QAAQ,EAAE,aAAa;IACvB,SAAS,EAAE,cAAc;IACzB,mBAAmB,EAAE,wBAAwB;IAE7C,WAAW;IACX,aAAa,EAAE,kBAAkB;CAClC,CAAC,CAAC"}
@@ -126,14 +126,49 @@ accounts and direct callers to the new methods. Their historical scans remain
126
126
  subject to Convex read limits; migrate busy accounts to the maintained summaries.
127
127
  Full referred-sale history uses `paginateTransfersByAccount`, not the preview.
128
128
 
129
+ ## Current payout observations
130
+
131
+ Call `betterStripe.syncPayoutFromStripe(ctx, { stripePayoutId, scope })` from an
132
+ authorized server action. Use `scope: { type: "connected", accountId }` for the
133
+ recipient's connected account or explicitly use `scope: { type: "platform" }`
134
+ for a platform payout. Resolve scope from trusted host configuration and recipient
135
+ ownership. Never accept an arbitrary account or platform mode from the browser.
136
+ The method reads Stripe and updates the existing component payout record; it
137
+ never creates, cancels or releases a bank payout.
138
+
139
+ The operation works for Stripe-managed automatic and manually triggered payouts.
140
+ It retrieves the current payout under that account's Stripe request scope and
141
+ validates the payout identity, original amount and currency, status, arrival date,
142
+ method and failure details. It does not require a mature balance transaction or a
143
+ manual execution claim. A current status observation does not establish funding
144
+ maturity, payout eligibility or a policy minimum.
145
+
146
+ A durable claim assigns each read a per-payout generation. Only the newest
147
+ claimed generation can commit. A delayed older response returns `superseded`
148
+ without changing the financial row. A successful result returns `accepted`, its
149
+ generation, `changed` and the stored payout. Identical observations preserve the
150
+ financial row and exact currency totals. Retrying the same committed generation
151
+ also preserves its acceptance timestamp. A later current provider observation
152
+ can legitimately change paid to failed; no terminal-status ranking is applied.
153
+
154
+ Provider errors or invalid snapshots throw and leave the last accepted financial
155
+ observation intact. The latest incomplete claim becomes unknown when that marker
156
+ can be saved. A lost mutation response may leave a pending or already accepted
157
+ claim. Retry the operation to obtain a new generation and read Stripe again.
158
+ Never substitute the webhook event body when the current provider read fails.
159
+ The claim table holds reconciliation metadata, not a second money ledger.
160
+
161
+ This method does not yet replace the existing signed webhook dispatcher. The
162
+ prepared generation must be integrated into the shared host callback transaction.
163
+ Until that wiring is adopted, legacy webhook deliveries still apply
164
+ event-body snapshots and can regress on distinct out-of-order events. Do not call
165
+ this method and then blindly apply an old event-body upsert afterward.
166
+
129
167
  ## Payout failures
130
168
 
131
169
  Component payout rows and live `RecipientPayoutRow` snapshots expose optional
132
- `failureCode` and `failureMessage`. Signed payout events write the provider's
133
- current snapshot to the existing payout identity. A later snapshot with absent
134
- or null provider failure fields clears stored details. Legacy rows remain valid.
135
- Ordinary event replay is deduplicated. This is snapshot reconciliation and does
136
- not guarantee convergence for globally out-of-order events.
170
+ `failureCode` and `failureMessage`. A later accepted snapshot with absent or null
171
+ provider failure fields clears stored details. Legacy rows remain valid.
137
172
 
138
173
  Host apps should translate known codes into safe recovery instructions and a
139
174
  settings link. Do not log or blindly render raw provider messages. Authorize
@@ -459,6 +459,12 @@ the currency. Existing package claims supply their immutable allocations on
459
459
  subsequent calls; use `payouts: []` to verify/reconcile already imported history.
460
460
  The same flow supports explicit accounting for automatic historical payouts.
461
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
+
462
468
  The package retrieves actual recipient funding and payout receipts, verifies
463
469
  full history against live available/pending cash, and validates each allocation
464
470
  against source maturity at the historical payout date. Future credits and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fantastic.dev/better-stripe",
3
- "version": "0.23.0",
3
+ "version": "0.23.2",
4
4
  "description": "A Convex component for Stripe V2 Accounts API integration.",
5
5
  "keywords": [
6
6
  "stripe",
@@ -1,247 +1,12 @@
1
1
  // @vitest-environment edge-runtime
2
- import { convexTest } from "convex-test";
3
- import { makeFunctionReference } from "convex/server";
4
2
  import { expect, it, vi } from "vitest";
5
3
  import type Stripe from "stripe";
6
- import schema from "../../component/schema.js";
7
- import type { Component, RunCtx } from "../helpers.js";
8
- import { releaseRecipientPayout } from "./payoutExecution.js";
9
- import { adoptRecipientPayouts } from "./payoutAdoption.js";
10
- const modules = import.meta.glob("../../component/**/*.*s");
11
- const symbol = Symbol.for("toReferencePath");
12
- const component = {
13
- payoutPolicy: {
14
- queries: { get: { [symbol]: "betterStripe/payoutPolicy/queries/get" } },
15
- },
16
- } as unknown as Component;
17
- const scope = {
18
- accountId: "acct_recipient",
19
- ownerKey: "owner",
20
- operationKey: "release-1",
21
- currency: "usd",
22
- independentLiabilities: 0,
23
- };
24
- async function setup(amounts = [600, 400]) {
25
- const t = convexTest(schema, modules);
26
- await t.run(async (ctx) => {
27
- await ctx.db.insert("payoutPolicies", {
28
- accountId: scope.accountId,
29
- ownerKey: scope.ownerKey,
30
- mode: "stripe_managed",
31
- status: "ready",
32
- revision: 0,
33
- operationKey: "enable",
34
- country: "US",
35
- thresholds: [],
36
- holdDeadline: Date.now() + 86400000,
37
- });
38
- for (let i = 0; i < amounts.length; i++)
39
- await ctx.db.insert("transfers", {
40
- stripeTransferId: `tr_${i}`,
41
- destinationAccountId: scope.accountId,
42
- sourceChargeId: `ch_${i}`,
43
- amount: amounts[i],
44
- currency: "usd",
45
- status: "paid",
46
- role: i % 2 ? "affiliate" : "store",
47
- });
48
- });
49
- const path = (ref: Record<symbol, string>) =>
50
- ref[symbol].replace("betterStripe/", "").replace(/\/(\w+)$/, ":$1");
51
- const ctx = {
52
- runQuery: (ref: Record<symbol, string>, args: never) =>
53
- t.query(makeFunctionReference<"query">(path(ref)), args),
54
- runMutation: (ref: Record<symbol, string>, args: never) =>
55
- t.mutation(makeFunctionReference<"mutation">(path(ref)), args),
56
- } as unknown as RunCtx;
57
- let transactions = amounts.map((amount, i) => ({
58
- id: `txn_${i}`,
59
- source: `py_${i}`,
60
- amount,
61
- fee: 0,
62
- net: amount,
63
- currency: "usd",
64
- created: 100,
65
- available_on: 200,
66
- status: "available",
67
- type: "payment",
68
- exchange_rate: null,
69
- }));
70
- let payout: Record<string, unknown> | undefined;
71
- const provider = {
72
- accounts: {
73
- listExternalAccounts: vi.fn(async () => ({
74
- data: [
75
- {
76
- id: "ba_one",
77
- object: "bank_account",
78
- currency: "usd",
79
- default_for_currency: true,
80
- },
81
- ],
82
- has_more: false,
83
- })),
84
- },
85
- transfers: {
86
- retrieve: vi.fn(async (id: string) => {
87
- const i = Number(id.slice(3));
88
- return {
89
- id,
90
- destination: scope.accountId,
91
- destination_payment: `py_${i}`,
92
- source_transaction: `ch_${i}`,
93
- amount: amounts[i],
94
- currency: "usd",
95
- amount_reversed: 0,
96
- };
97
- }),
98
- listReversals: vi.fn(async () => ({ data: [], has_more: false })),
99
- },
100
- charges: {
101
- retrieve: vi.fn(async (id: string) => {
102
- const i = Number(id.slice(3));
103
- return {
104
- id,
105
- source_transfer: `tr_${i}`,
106
- amount: amounts[i],
107
- currency: "usd",
108
- paid: true,
109
- status: "succeeded",
110
- balance_transaction: `txn_${i}`,
111
- };
112
- }),
113
- },
114
- balanceTransactions: {
115
- list: vi.fn(async (params: { source?: string; limit?: number }) => ({
116
- data: params.source
117
- ? transactions.filter((t) => t.source === params.source)
118
- : transactions.slice(0, params.limit),
119
- has_more: false,
120
- })),
121
- retrieve: vi.fn(async (id: string) =>
122
- transactions.find((t) => t.id === id),
123
- ),
124
- },
125
- balance: {
126
- retrieve: vi.fn(async () => ({
127
- available: [
128
- {
129
- currency: "usd",
130
- amount: transactions
131
- .filter((t) => t.status === "available")
132
- .reduce((a, t) => a + t.net, 0),
133
- source_types: {
134
- card: transactions
135
- .filter((t) => t.status === "available")
136
- .reduce((a, t) => a + t.net, 0),
137
- },
138
- },
139
- ],
140
- pending: [],
141
- })),
142
- },
143
- payouts: {
144
- list: vi.fn(async () => ({
145
- data: payout ? [payout] : [],
146
- has_more: false,
147
- })),
148
- retrieve: vi.fn(async () => payout),
149
- create: vi.fn(
150
- async (params: {
151
- amount: number;
152
- currency: string;
153
- metadata: object;
154
- }) => {
155
- payout = {
156
- id: "po_one",
157
- ...params,
158
- status: "paid",
159
- automatic: false,
160
- method: "standard",
161
- type: "bank_account",
162
- source_type: "card",
163
- destination: "ba_one",
164
- created: Math.floor(Date.now() / 1000),
165
- arrival_date: Math.floor(Date.now() / 1000),
166
- balance_transaction: "txn_debit",
167
- failure_balance_transaction: null,
168
- reversed_by: null,
169
- };
170
- transactions = [
171
- {
172
- id: "txn_debit",
173
- source: "po_one",
174
- amount: -params.amount,
175
- fee: 0,
176
- net: -params.amount,
177
- currency: "usd",
178
- created: Math.floor(Date.now() / 1000),
179
- available_on: Math.floor(Date.now() / 1000),
180
- status: "available",
181
- type: "payout",
182
- exchange_rate: null,
183
- },
184
- ...transactions,
185
- ];
186
- return payout;
187
- },
188
- ),
189
- },
190
- };
191
- const verify = vi.fn(async () => ({
192
- status: "verified" as const,
193
- policy: (await t.run((ctx) => ctx.db.query("payoutPolicies").first()))!,
194
- }));
195
- const run = (extra = {}) =>
196
- releaseRecipientPayout(
197
- provider as unknown as Stripe,
198
- component,
199
- ctx,
200
- { ...scope, ...extra },
201
- verify,
202
- );
203
- return {
204
- t,
205
- ctx,
206
- provider,
207
- verify,
208
- run,
209
- setTransactions: (rows: typeof transactions) => {
210
- transactions = rows;
211
- },
212
- transactions: () => transactions,
213
- };
214
- }
215
- async function historical() {
216
- const s = await setup();
217
- await s.provider.payouts.create({
218
- amount: 1000,
219
- currency: "usd",
220
- metadata: {},
221
- });
222
- s.provider.payouts.create.mockClear();
223
- const opts = {
224
- accountId: scope.accountId,
225
- ownerKey: scope.ownerKey,
226
- currency: "usd",
227
- expectedRevision: 0,
228
- payouts: [
229
- {
230
- stripePayoutId: "po_one",
231
- allocations: [
232
- { stripeTransferId: "tr_0", amount: 600 },
233
- { stripeTransferId: "tr_1", amount: 400 },
234
- ],
235
- },
236
- ],
237
- };
238
- const adopt = (extra = {}) =>
239
- adoptRecipientPayouts(s.provider as unknown as Stripe, component, s.ctx, {
240
- ...opts,
241
- ...extra,
242
- });
243
- return { ...s, adopt, opts };
244
- }
4
+ import {
5
+ component,
6
+ scope,
7
+ setup,
8
+ historical,
9
+ } from "./payoutAdoptionTestHelpers.js";
245
10
  it("imports a verified historical manual payout with exact source allocations", async () => {
246
11
  const s = await historical();
247
12
  expect(await s.adopt()).toMatchObject({
@@ -303,8 +303,9 @@ export async function adoptRecipientPayouts(
303
303
  )) as { claims: Claim[] };
304
304
  claims.push(...result.claims);
305
305
  }
306
- const paid = claims
307
- .filter((c) => c.status === "paid")
306
+ // Returned cash gets a fresh holding age after consuming its original principal.
307
+ const originalPaid = claims
308
+ .filter((c) => c.status === "paid" || c.status === "returned")
308
309
  .flatMap((c) => c.allocations);
309
310
  const fees = claims.reduce((total, c) => add(total, c.providerFee ?? 0), 0);
310
311
  const returned = proofPayouts
@@ -315,7 +316,7 @@ export async function adoptRecipientPayouts(
315
316
  const oldestHeldAt = oldestUnpaidFunding(
316
317
  opts.currency,
317
318
  sources,
318
- paid,
319
+ originalPaid,
319
320
  fees,
320
321
  returned,
321
322
  );
@@ -0,0 +1,253 @@
1
+ // @vitest-environment edge-runtime
2
+ import { convexTest } from "convex-test";
3
+ import { makeFunctionReference } from "convex/server";
4
+ import { vi } from "vitest";
5
+ import type Stripe from "stripe";
6
+ import schema from "../../component/schema.js";
7
+ import type { Component, RunCtx } from "../helpers.js";
8
+ import { releaseRecipientPayout } from "./payoutExecution.js";
9
+ import { adoptRecipientPayouts } from "./payoutAdoption.js";
10
+ const modules = import.meta.glob("../../component/**/*.*s");
11
+ const symbol = Symbol.for("toReferencePath");
12
+ export const component = {
13
+ payoutPolicy: {
14
+ queries: { get: { [symbol]: "betterStripe/payoutPolicy/queries/get" } },
15
+ },
16
+ } as unknown as Component;
17
+ export const scope = {
18
+ accountId: "acct_recipient",
19
+ ownerKey: "owner",
20
+ operationKey: "release-1",
21
+ currency: "usd",
22
+ independentLiabilities: 0,
23
+ };
24
+ export async function setup(amounts = [600, 400], currency = "usd") {
25
+ const t = convexTest(schema, modules);
26
+ await t.run(async (ctx) => {
27
+ await ctx.db.insert("payoutPolicies", {
28
+ accountId: scope.accountId,
29
+ ownerKey: scope.ownerKey,
30
+ mode: "stripe_managed",
31
+ status: "ready",
32
+ revision: 0,
33
+ operationKey: "enable",
34
+ country: "US",
35
+ thresholds: [],
36
+ holdDeadline: Date.now() + 86400000,
37
+ });
38
+ for (let i = 0; i < amounts.length; i++)
39
+ await ctx.db.insert("transfers", {
40
+ stripeTransferId: `tr_${i}`,
41
+ destinationAccountId: scope.accountId,
42
+ sourceChargeId: `ch_${i}`,
43
+ amount: amounts[i],
44
+ currency,
45
+ status: "paid",
46
+ role: i % 2 ? "affiliate" : "store",
47
+ });
48
+ });
49
+ const path = (ref: Record<symbol, string>) =>
50
+ ref[symbol].replace("betterStripe/", "").replace(/\/(\w+)$/, ":$1");
51
+ const ctx = {
52
+ runQuery: (ref: Record<symbol, string>, args: never) =>
53
+ t.query(makeFunctionReference<"query">(path(ref)), args),
54
+ runMutation: (ref: Record<symbol, string>, args: never) =>
55
+ t.mutation(makeFunctionReference<"mutation">(path(ref)), args),
56
+ } as unknown as RunCtx;
57
+ let transactions = amounts.map((amount, i) => ({
58
+ id: `txn_${i}`,
59
+ source: `py_${i}`,
60
+ amount,
61
+ fee: 0,
62
+ net: amount,
63
+ currency,
64
+ created: 100,
65
+ available_on: 200,
66
+ status: "available",
67
+ type: "payment",
68
+ exchange_rate: null,
69
+ }));
70
+ let payout: Record<string, unknown> | undefined;
71
+ const history: Record<string, unknown>[] = [];
72
+ const provider = {
73
+ accounts: {
74
+ listExternalAccounts: vi.fn(async () => ({
75
+ data: [
76
+ {
77
+ id: "ba_one",
78
+ object: "bank_account",
79
+ currency,
80
+ default_for_currency: true,
81
+ },
82
+ ],
83
+ has_more: false,
84
+ })),
85
+ },
86
+ transfers: {
87
+ retrieve: vi.fn(async (id: string) => {
88
+ const i = Number(id.slice(3));
89
+ return {
90
+ id,
91
+ destination: scope.accountId,
92
+ destination_payment: `py_${i}`,
93
+ source_transaction: `ch_${i}`,
94
+ amount: amounts[i],
95
+ currency,
96
+ amount_reversed: 0,
97
+ };
98
+ }),
99
+ listReversals: vi.fn(async () => ({ data: [], has_more: false })),
100
+ },
101
+ charges: {
102
+ retrieve: vi.fn(async (id: string) => {
103
+ const i = Number(id.slice(3));
104
+ return {
105
+ id,
106
+ source_transfer: `tr_${i}`,
107
+ amount: amounts[i],
108
+ currency,
109
+ paid: true,
110
+ status: "succeeded",
111
+ balance_transaction: `txn_${i}`,
112
+ };
113
+ }),
114
+ },
115
+ balanceTransactions: {
116
+ list: vi.fn(async (params: { source?: string; limit?: number }) => ({
117
+ data: params.source
118
+ ? transactions.filter((t) => t.source === params.source)
119
+ : transactions.slice(0, params.limit),
120
+ has_more: false,
121
+ })),
122
+ retrieve: vi.fn(async (id: string) =>
123
+ transactions.find((t) => t.id === id),
124
+ ),
125
+ },
126
+ balance: {
127
+ retrieve: vi.fn(async () => ({
128
+ available: [
129
+ {
130
+ currency,
131
+ amount: transactions
132
+ .filter((t) => t.status === "available")
133
+ .reduce((a, t) => a + t.net, 0),
134
+ source_types: {
135
+ card: transactions
136
+ .filter((t) => t.status === "available")
137
+ .reduce((a, t) => a + t.net, 0),
138
+ },
139
+ },
140
+ ],
141
+ pending: [],
142
+ })),
143
+ },
144
+ payouts: {
145
+ list: vi.fn(async () => ({
146
+ data: [...history].reverse(),
147
+ has_more: false,
148
+ })),
149
+ retrieve: vi.fn(async (id?: string) =>
150
+ id ? history.find((p) => p.id === id) : payout,
151
+ ),
152
+ create: vi.fn(
153
+ async (params: {
154
+ amount: number;
155
+ currency: string;
156
+ metadata: object;
157
+ }) => {
158
+ const id = history.length ? `po_${history.length + 1}` : "po_one";
159
+ const debitId = history.length
160
+ ? `txn_debit_${history.length + 1}`
161
+ : "txn_debit";
162
+ payout = {
163
+ id,
164
+ ...params,
165
+ status: "paid",
166
+ automatic: false,
167
+ method: "standard",
168
+ type: "bank_account",
169
+ source_type: "card",
170
+ destination: "ba_one",
171
+ created: Math.floor(Date.now() / 1000),
172
+ arrival_date: Math.floor(Date.now() / 1000),
173
+ balance_transaction: debitId,
174
+ failure_balance_transaction: null,
175
+ reversed_by: null,
176
+ };
177
+ transactions = [
178
+ {
179
+ id: debitId,
180
+ source: id,
181
+ amount: -params.amount,
182
+ fee: 0,
183
+ net: -params.amount,
184
+ currency,
185
+ created: Math.floor(Date.now() / 1000),
186
+ available_on: Math.floor(Date.now() / 1000),
187
+ status: "available",
188
+ type: "payout",
189
+ exchange_rate: null,
190
+ },
191
+ ...transactions,
192
+ ];
193
+ history.push(payout);
194
+ return payout;
195
+ },
196
+ ),
197
+ },
198
+ };
199
+ const verify = vi.fn(async () => ({
200
+ status: "verified" as const,
201
+ policy: (await t.run((ctx) => ctx.db.query("payoutPolicies").first()))!,
202
+ }));
203
+ const run = (extra = {}) =>
204
+ releaseRecipientPayout(
205
+ provider as unknown as Stripe,
206
+ component,
207
+ ctx,
208
+ { ...scope, currency, ...extra },
209
+ verify,
210
+ );
211
+ return {
212
+ t,
213
+ ctx,
214
+ provider,
215
+ verify,
216
+ run,
217
+ setTransactions: (rows: typeof transactions) => {
218
+ transactions = rows;
219
+ },
220
+ transactions: () => transactions,
221
+ };
222
+ }
223
+ export async function historical(currency = "usd") {
224
+ const amounts = [600, 400];
225
+ const s = await setup(amounts, currency);
226
+ await s.provider.payouts.create({
227
+ amount: 1000,
228
+ currency,
229
+ metadata: {},
230
+ });
231
+ s.provider.payouts.create.mockClear();
232
+ const opts = {
233
+ accountId: scope.accountId,
234
+ ownerKey: scope.ownerKey,
235
+ currency,
236
+ expectedRevision: 0,
237
+ payouts: [
238
+ {
239
+ stripePayoutId: "po_one",
240
+ allocations: [
241
+ { stripeTransferId: "tr_0", amount: 600 },
242
+ { stripeTransferId: "tr_1", amount: 400 },
243
+ ],
244
+ },
245
+ ],
246
+ };
247
+ const adopt = (extra = {}) =>
248
+ adoptRecipientPayouts(s.provider as unknown as Stripe, component, s.ctx, {
249
+ ...opts,
250
+ ...extra,
251
+ });
252
+ return { ...s, adopt, opts, amounts };
253
+ }