@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.
- package/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/dist/client/connect/payoutAdoption.d.ts.map +1 -1
- package/dist/client/connect/payoutAdoption.js +4 -3
- package/dist/client/connect/payoutAdoption.js.map +1 -1
- package/dist/client/connect/payoutAdoptionTestHelpers.d.ts +2659 -0
- package/dist/client/connect/payoutAdoptionTestHelpers.d.ts.map +1 -0
- package/dist/client/connect/payoutAdoptionTestHelpers.js +231 -0
- package/dist/client/connect/payoutAdoptionTestHelpers.js.map +1 -0
- package/dist/client/connect/payoutSync.d.ts +32 -0
- package/dist/client/connect/payoutSync.d.ts.map +1 -0
- package/dist/client/connect/payoutSync.js +48 -0
- package/dist/client/connect/payoutSync.js.map +1 -0
- package/dist/client/connect/payoutSyncTestHelpers.d.ts +1263 -0
- package/dist/client/connect/payoutSyncTestHelpers.d.ts.map +1 -0
- package/dist/client/connect/payoutSyncTestHelpers.js +62 -0
- package/dist/client/connect/payoutSyncTestHelpers.js.map +1 -0
- package/dist/client/connect/payoutSyncValidation.d.ts +16 -0
- package/dist/client/connect/payoutSyncValidation.d.ts.map +1 -0
- package/dist/client/connect/payoutSyncValidation.js +54 -0
- package/dist/client/connect/payoutSyncValidation.js.map +1 -0
- package/dist/client/index.d.ts +26 -9
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +3 -5
- package/dist/client/index.js.map +1 -1
- package/dist/client/readContracts.d.ts +8 -0
- package/dist/client/readContracts.d.ts.map +1 -1
- package/dist/client/readContracts.js +7 -0
- package/dist/client/readContracts.js.map +1 -1
- package/dist/component/_generated/component.d.ts +69 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/connect/mutations.d.ts.map +1 -1
- package/dist/component/connect/mutations.js +4 -27
- package/dist/component/connect/mutations.js.map +1 -1
- package/dist/component/connect/payoutWrites.d.ts +21 -0
- package/dist/component/connect/payoutWrites.d.ts.map +1 -0
- package/dist/component/connect/payoutWrites.js +19 -0
- package/dist/component/connect/payoutWrites.js.map +1 -0
- package/dist/component/earnings/triggers.d.ts +44 -0
- package/dist/component/earnings/triggers.d.ts.map +1 -1
- package/dist/component/payoutSync/helpers.d.ts +44 -0
- package/dist/component/payoutSync/helpers.d.ts.map +1 -0
- package/dist/component/payoutSync/helpers.js +60 -0
- package/dist/component/payoutSync/helpers.js.map +1 -0
- package/dist/component/payoutSync/mutations.d.ts +51 -0
- package/dist/component/payoutSync/mutations.d.ts.map +1 -0
- package/dist/component/payoutSync/mutations.js +88 -0
- package/dist/component/payoutSync/mutations.js.map +1 -0
- package/dist/component/payoutSync/queries.d.ts +16 -0
- package/dist/component/payoutSync/queries.d.ts.map +1 -0
- package/dist/component/payoutSync/queries.js +11 -0
- package/dist/component/payoutSync/queries.js.map +1 -0
- package/dist/component/payoutSync/schema.d.ts +46 -0
- package/dist/component/payoutSync/schema.d.ts.map +1 -0
- package/dist/component/payoutSync/schema.js +22 -0
- package/dist/component/payoutSync/schema.js.map +1 -0
- package/dist/component/payoutSync/validators.d.ts +137 -0
- package/dist/component/payoutSync/validators.d.ts.map +1 -0
- package/dist/component/payoutSync/validators.js +19 -0
- package/dist/component/payoutSync/validators.js.map +1 -0
- package/dist/component/schema.d.ts +21 -0
- package/dist/component/schema.js +2 -0
- package/dist/component/schema.js.map +1 -1
- package/docs/read-contracts.md +40 -5
- package/docs/settlement.md +6 -0
- package/package.json +1 -1
- package/src/client/connect/payoutAdoption.test.ts +6 -241
- package/src/client/connect/payoutAdoption.ts +4 -3
- package/src/client/connect/payoutAdoptionTestHelpers.ts +253 -0
- package/src/client/connect/payoutReturnedAdoption.test.ts +157 -0
- package/src/client/connect/payoutSync.test.ts +313 -0
- package/src/client/connect/payoutSync.ts +93 -0
- package/src/client/connect/payoutSyncTestHelpers.ts +82 -0
- package/src/client/connect/payoutSyncValidation.ts +74 -0
- package/src/client/index.ts +14 -11
- package/src/client/readContracts.ts +12 -0
- package/src/component/_generated/api.ts +10 -0
- package/src/component/_generated/component.ts +82 -0
- package/src/component/connect/mutations.ts +4 -30
- package/src/component/connect/payoutWrites.ts +25 -0
- package/src/component/payout-sync.test.ts +117 -0
- package/src/component/payoutSync/helpers.ts +86 -0
- package/src/component/payoutSync/mutations.ts +102 -0
- package/src/component/payoutSync/queries.ts +10 -0
- package/src/component/payoutSync/schema.ts +25 -0
- package/src/component/payoutSync/validators.ts +21 -0
- 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;
|
package/dist/component/schema.js
CHANGED
|
@@ -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"}
|
package/docs/read-contracts.md
CHANGED
|
@@ -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`.
|
|
133
|
-
|
|
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
|
package/docs/settlement.md
CHANGED
|
@@ -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,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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
307
|
-
|
|
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
|
-
|
|
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
|
+
}
|