@fantastic.dev/better-stripe 0.23.1 → 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 +6 -0
- package/README.md +1 -1
- package/dist/client/connect/payoutAdoptionTestHelpers.d.ts +42 -0
- package/dist/client/connect/payoutAdoptionTestHelpers.d.ts.map +1 -1
- 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/package.json +1 -1
- 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/package.json
CHANGED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
// @vitest-environment edge-runtime
|
|
2
|
+
import { expect, it, vi } from "vitest";
|
|
3
|
+
import type Stripe from "stripe";
|
|
4
|
+
import { makeFunctionReference } from "convex/server";
|
|
5
|
+
import type { RunCtx } from "../helpers.js";
|
|
6
|
+
import {
|
|
7
|
+
component,
|
|
8
|
+
setup,
|
|
9
|
+
operation,
|
|
10
|
+
type Scope,
|
|
11
|
+
} from "./payoutSyncTestHelpers.js";
|
|
12
|
+
const symbol = Symbol.for("toReferencePath");
|
|
13
|
+
it("persists the current automatic payout under the connected scope without requiring mature funding", async () => {
|
|
14
|
+
const s = await setup();
|
|
15
|
+
expect(await s.run()).toMatchObject({ status: "accepted", changed: true });
|
|
16
|
+
expect(s.retrieve).toHaveBeenCalledWith(
|
|
17
|
+
"po_one",
|
|
18
|
+
{},
|
|
19
|
+
{ stripeAccount: "acct_owner" },
|
|
20
|
+
);
|
|
21
|
+
expect(await s.row()).toMatchObject({
|
|
22
|
+
stripePayoutId: "po_one",
|
|
23
|
+
accountId: "acct_owner",
|
|
24
|
+
amount: 500,
|
|
25
|
+
currency: "jpy",
|
|
26
|
+
status: "paid",
|
|
27
|
+
arrivalDate: "1970-01-01T00:03:20.000Z",
|
|
28
|
+
});
|
|
29
|
+
expect(await s.run()).toMatchObject({ status: "accepted", changed: false });
|
|
30
|
+
expect(
|
|
31
|
+
await s.t.run((ctx) => ctx.db.query("payouts").collect()),
|
|
32
|
+
).toHaveLength(1);
|
|
33
|
+
expect(
|
|
34
|
+
await s.t.run((ctx) => ctx.db.query("currencyTotals").unique()),
|
|
35
|
+
).toMatchObject({
|
|
36
|
+
accountId: "acct_owner",
|
|
37
|
+
currency: "jpy",
|
|
38
|
+
paidOut: 500,
|
|
39
|
+
count: 1,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
it("allows a later authoritative paid-to-failed transition and clears obsolete failure details", async () => {
|
|
43
|
+
const s = await setup();
|
|
44
|
+
await s.run();
|
|
45
|
+
s.set({
|
|
46
|
+
status: "failed",
|
|
47
|
+
failure_code: "account_closed",
|
|
48
|
+
failure_message: "Bank account closed",
|
|
49
|
+
});
|
|
50
|
+
await s.run();
|
|
51
|
+
expect(await s.row()).toMatchObject({
|
|
52
|
+
status: "failed",
|
|
53
|
+
failureCode: "account_closed",
|
|
54
|
+
});
|
|
55
|
+
expect(
|
|
56
|
+
await s.t.run((ctx) => ctx.db.query("currencyTotals").unique()),
|
|
57
|
+
).toMatchObject({ paidOut: 0, count: 1 });
|
|
58
|
+
s.set({ status: "pending", failure_code: null, failure_message: null });
|
|
59
|
+
await s.run();
|
|
60
|
+
expect((await s.row())?.failureCode).toBeUndefined();
|
|
61
|
+
expect((await s.row())?.failureMessage).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
it("keeps platform scope explicit and ignores provider metadata as account authority", async () => {
|
|
64
|
+
const s = await setup({ type: "platform" });
|
|
65
|
+
s.set({ automatic: false });
|
|
66
|
+
await s.run();
|
|
67
|
+
expect(s.retrieve).toHaveBeenCalledWith("po_one", {}, {});
|
|
68
|
+
expect(await s.row()).toMatchObject({
|
|
69
|
+
accountId: "platform",
|
|
70
|
+
currency: "jpy",
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
function deferred<T>() {
|
|
74
|
+
let resolve!: (value: T) => void;
|
|
75
|
+
const promise = new Promise<T>((done) => {
|
|
76
|
+
resolve = done;
|
|
77
|
+
});
|
|
78
|
+
return { promise, resolve };
|
|
79
|
+
}
|
|
80
|
+
it("supersedes a delayed old pending read after a newer paid observation commits", async () => {
|
|
81
|
+
const s = await setup();
|
|
82
|
+
const old = { ...s.read(), status: "pending" as const };
|
|
83
|
+
const response = deferred<Stripe.Payout>();
|
|
84
|
+
s.retrieve.mockImplementationOnce(() => response.promise);
|
|
85
|
+
const first = s.run();
|
|
86
|
+
await vi.waitFor(() => expect(s.retrieve).toHaveBeenCalledTimes(1));
|
|
87
|
+
expect(await s.run()).toMatchObject({ status: "accepted", generation: 2 });
|
|
88
|
+
response.resolve(old);
|
|
89
|
+
expect(await first).toMatchObject({ status: "superseded", generation: 1 });
|
|
90
|
+
expect(await s.row()).toMatchObject({ status: "paid" });
|
|
91
|
+
expect(
|
|
92
|
+
await s.t.run((ctx) => ctx.db.query("payoutSyncClaims").unique()),
|
|
93
|
+
).toMatchObject({ status: "accepted", generation: 2, acceptedGeneration: 2 });
|
|
94
|
+
});
|
|
95
|
+
it("a newer failed read holds its generation instead of accepting an older in-flight snapshot", async () => {
|
|
96
|
+
const s = await setup();
|
|
97
|
+
await s.run();
|
|
98
|
+
const response = deferred<Stripe.Payout>();
|
|
99
|
+
s.retrieve.mockImplementationOnce(() => response.promise);
|
|
100
|
+
const first = s.run();
|
|
101
|
+
await vi.waitFor(() => expect(s.retrieve).toHaveBeenCalledTimes(2));
|
|
102
|
+
s.retrieve.mockRejectedValueOnce(new Error("Provider unavailable"));
|
|
103
|
+
await expect(s.run()).rejects.toThrow("Provider unavailable");
|
|
104
|
+
response.resolve({ ...s.read(), status: "pending" });
|
|
105
|
+
expect(await first).toMatchObject({ status: "superseded" });
|
|
106
|
+
expect(await s.row()).toMatchObject({ status: "paid" });
|
|
107
|
+
expect(
|
|
108
|
+
await s.t.run((ctx) => ctx.db.query("payoutSyncClaims").unique()),
|
|
109
|
+
).toMatchObject({ status: "unknown", generation: 3, acceptedGeneration: 1 });
|
|
110
|
+
expect(await s.run()).toMatchObject({
|
|
111
|
+
status: "accepted",
|
|
112
|
+
generation: 4,
|
|
113
|
+
changed: false,
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
it.each(["begin", "finish"] as const)(
|
|
117
|
+
"recovers a lost %s response through a fresh durable generation",
|
|
118
|
+
async (stage) => {
|
|
119
|
+
const s = await setup();
|
|
120
|
+
let isLost = false;
|
|
121
|
+
const ctx = {
|
|
122
|
+
...s.ctx,
|
|
123
|
+
runMutation: async (ref: Record<symbol, string>, args: never) => {
|
|
124
|
+
const saved = await s.mutate(ref, args);
|
|
125
|
+
if (!isLost && ref[symbol].endsWith(`/payoutSync/mutations/${stage}`)) {
|
|
126
|
+
isLost = true;
|
|
127
|
+
throw new Error("Lost component response");
|
|
128
|
+
}
|
|
129
|
+
return saved;
|
|
130
|
+
},
|
|
131
|
+
} as unknown as RunCtx;
|
|
132
|
+
const sync = await operation();
|
|
133
|
+
await expect(
|
|
134
|
+
sync(s.provider, component, ctx, {
|
|
135
|
+
stripePayoutId: "po_one",
|
|
136
|
+
scope: s.scope,
|
|
137
|
+
}),
|
|
138
|
+
).rejects.toThrow("Lost component response");
|
|
139
|
+
expect(
|
|
140
|
+
await s.t.run((ctx) => ctx.db.query("payoutSyncClaims").unique()),
|
|
141
|
+
).toMatchObject({
|
|
142
|
+
generation: 1,
|
|
143
|
+
status: stage === "finish" ? "accepted" : "pending",
|
|
144
|
+
});
|
|
145
|
+
expect(await s.run()).toMatchObject({
|
|
146
|
+
status: "accepted",
|
|
147
|
+
generation: 2,
|
|
148
|
+
changed: stage === "begin",
|
|
149
|
+
});
|
|
150
|
+
expect(
|
|
151
|
+
await s.t.run((ctx) => ctx.db.query("payouts").collect()),
|
|
152
|
+
).toHaveLength(1);
|
|
153
|
+
expect(
|
|
154
|
+
await s.t.run((ctx) => ctx.db.query("currencyTotals").unique()),
|
|
155
|
+
).toMatchObject({ paidOut: 500, count: 1 });
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
it("rejects a foreign existing payout before requesting provider state", async () => {
|
|
159
|
+
const s = await setup();
|
|
160
|
+
await s.t.mutation(
|
|
161
|
+
makeFunctionReference<"mutation">("connect/mutations:upsertPayout"),
|
|
162
|
+
{
|
|
163
|
+
stripePayoutId: "po_one",
|
|
164
|
+
accountId: "acct_foreign",
|
|
165
|
+
amount: 500,
|
|
166
|
+
currency: "jpy",
|
|
167
|
+
status: "paid",
|
|
168
|
+
},
|
|
169
|
+
);
|
|
170
|
+
await expect(s.run()).rejects.toThrow(
|
|
171
|
+
"Existing payout account does not match.",
|
|
172
|
+
);
|
|
173
|
+
expect(s.retrieve).not.toHaveBeenCalled();
|
|
174
|
+
expect(await s.row()).toMatchObject({ accountId: "acct_foreign" });
|
|
175
|
+
});
|
|
176
|
+
it("rejects changing a payout's durable account even if no financial row was saved", async () => {
|
|
177
|
+
const s = await setup();
|
|
178
|
+
s.retrieve.mockRejectedValueOnce(new Error("Unavailable"));
|
|
179
|
+
await expect(s.run()).rejects.toThrow("Unavailable");
|
|
180
|
+
s.retrieve.mockClear();
|
|
181
|
+
const sync = await operation();
|
|
182
|
+
await expect(
|
|
183
|
+
sync(s.provider, component, s.ctx, {
|
|
184
|
+
stripePayoutId: "po_one",
|
|
185
|
+
scope: { type: "connected", accountId: "acct_foreign" },
|
|
186
|
+
}),
|
|
187
|
+
).rejects.toThrow("Payout synchronization account does not match.");
|
|
188
|
+
expect(s.retrieve).not.toHaveBeenCalled();
|
|
189
|
+
expect(await s.row()).toBeNull();
|
|
190
|
+
});
|
|
191
|
+
it.each([{ amount: 501 }, { currency: "kwd" }])(
|
|
192
|
+
"rejects changed original money %j without changing summaries",
|
|
193
|
+
async (patch) => {
|
|
194
|
+
const s = await setup();
|
|
195
|
+
await s.run();
|
|
196
|
+
s.set(patch);
|
|
197
|
+
await expect(s.run()).rejects.toThrow(
|
|
198
|
+
"Existing payout original money does not match.",
|
|
199
|
+
);
|
|
200
|
+
expect(await s.row()).toMatchObject({ amount: 500, currency: "jpy" });
|
|
201
|
+
expect(
|
|
202
|
+
await s.t.run((ctx) => ctx.db.query("currencyTotals").unique()),
|
|
203
|
+
).toMatchObject({ paidOut: 500, count: 1 });
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
it.each([0, -1, 0.5, Number.MAX_SAFE_INTEGER + 1, NaN])(
|
|
207
|
+
"rejects invalid provider amount %s without a financial record",
|
|
208
|
+
async (amount) => {
|
|
209
|
+
const s = await setup();
|
|
210
|
+
s.set({ amount });
|
|
211
|
+
await expect(s.run()).rejects.toThrow(
|
|
212
|
+
"Invalid payout synchronization money or method.",
|
|
213
|
+
);
|
|
214
|
+
expect(await s.row()).toBeNull();
|
|
215
|
+
expect(
|
|
216
|
+
await s.t.run((ctx) => ctx.db.query("payoutSyncClaims").unique()),
|
|
217
|
+
).toMatchObject({ status: "unknown" });
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
it("rejects an unavailable provider response and preserves retryability", async () => {
|
|
221
|
+
const s = await setup();
|
|
222
|
+
s.retrieve.mockResolvedValueOnce(null as unknown as Stripe.Payout);
|
|
223
|
+
await expect(s.run()).rejects.toThrow(
|
|
224
|
+
"Provider payout response is unavailable.",
|
|
225
|
+
);
|
|
226
|
+
expect(await s.row()).toBeNull();
|
|
227
|
+
expect(await s.run()).toMatchObject({ status: "accepted", generation: 2 });
|
|
228
|
+
});
|
|
229
|
+
it("rejects a foreign provider payout ID", async () => {
|
|
230
|
+
const s = await setup();
|
|
231
|
+
s.set({ id: "po_foreign" });
|
|
232
|
+
await expect(s.run()).rejects.toThrow(
|
|
233
|
+
"Provider payout identity does not match.",
|
|
234
|
+
);
|
|
235
|
+
expect(await s.row()).toBeNull();
|
|
236
|
+
});
|
|
237
|
+
it.each([NaN, -1, 8640000000001])(
|
|
238
|
+
"rejects invalid arrival seconds %s",
|
|
239
|
+
async (arrival_date) => {
|
|
240
|
+
const s = await setup();
|
|
241
|
+
s.set({ arrival_date });
|
|
242
|
+
await expect(s.run()).rejects.toThrow(
|
|
243
|
+
"Invalid provider payout arrival date.",
|
|
244
|
+
);
|
|
245
|
+
expect(await s.row()).toBeNull();
|
|
246
|
+
},
|
|
247
|
+
);
|
|
248
|
+
it.each(["jpy", "kwd"])(
|
|
249
|
+
"preserves exact safe original %s units at the maximum integer",
|
|
250
|
+
async (currency) => {
|
|
251
|
+
const s = await setup();
|
|
252
|
+
s.set({ currency, amount: Number.MAX_SAFE_INTEGER });
|
|
253
|
+
await s.run();
|
|
254
|
+
expect(await s.row()).toMatchObject({
|
|
255
|
+
currency,
|
|
256
|
+
amount: Number.MAX_SAFE_INTEGER,
|
|
257
|
+
});
|
|
258
|
+
expect(
|
|
259
|
+
await s.t.run((ctx) => ctx.db.query("currencyTotals").unique()),
|
|
260
|
+
).toMatchObject({ currency, paidOut: Number.MAX_SAFE_INTEGER });
|
|
261
|
+
},
|
|
262
|
+
);
|
|
263
|
+
it("does not reinterpret a connected account named platform as platform authority", async () => {
|
|
264
|
+
const s = await setup({ type: "connected", accountId: "platform" });
|
|
265
|
+
await expect(s.run()).rejects.toThrow(
|
|
266
|
+
"Payout synchronization scope must be explicit.",
|
|
267
|
+
);
|
|
268
|
+
expect(s.retrieve).not.toHaveBeenCalled();
|
|
269
|
+
expect(await s.row()).toBeNull();
|
|
270
|
+
});
|
|
271
|
+
it("deduplicates equivalent provider metadata order without a financial update", async () => {
|
|
272
|
+
const s = await setup();
|
|
273
|
+
s.set({ metadata: { b: "two", a: "one" } });
|
|
274
|
+
await s.run();
|
|
275
|
+
s.set({ metadata: { a: "one", b: "two" } });
|
|
276
|
+
expect(await s.run()).toMatchObject({ status: "accepted", changed: false });
|
|
277
|
+
});
|
|
278
|
+
it("publishes authoritative synchronization through the actual BetterStripe client", async () => {
|
|
279
|
+
const s = await setup();
|
|
280
|
+
const { BetterStripe } = await import("../index.js");
|
|
281
|
+
const spy = vi
|
|
282
|
+
.spyOn(
|
|
283
|
+
BetterStripe.prototype as unknown as { stripe: () => Stripe },
|
|
284
|
+
"stripe",
|
|
285
|
+
)
|
|
286
|
+
.mockReturnValue(s.provider);
|
|
287
|
+
try {
|
|
288
|
+
const client = new BetterStripe(component) as InstanceType<
|
|
289
|
+
typeof BetterStripe
|
|
290
|
+
> & {
|
|
291
|
+
syncPayoutFromStripe: (
|
|
292
|
+
ctx: RunCtx,
|
|
293
|
+
opts: { stripePayoutId: string; scope: Scope },
|
|
294
|
+
) => Promise<unknown>;
|
|
295
|
+
};
|
|
296
|
+
expect(
|
|
297
|
+
client.syncPayoutFromStripe,
|
|
298
|
+
"public authoritative payout method is missing",
|
|
299
|
+
).toBeTypeOf("function");
|
|
300
|
+
expect(
|
|
301
|
+
await client.syncPayoutFromStripe(s.ctx, {
|
|
302
|
+
stripePayoutId: "po_one",
|
|
303
|
+
scope: s.scope,
|
|
304
|
+
}),
|
|
305
|
+
).toMatchObject({ status: "accepted" });
|
|
306
|
+
expect(await s.row()).toMatchObject({
|
|
307
|
+
accountId: "acct_owner",
|
|
308
|
+
status: "paid",
|
|
309
|
+
});
|
|
310
|
+
} finally {
|
|
311
|
+
spy.mockRestore();
|
|
312
|
+
}
|
|
313
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type Stripe from "stripe";
|
|
2
|
+
import type { Infer } from "convex/values";
|
|
3
|
+
import type { Component, RunCtx } from "../helpers.js";
|
|
4
|
+
import { runMutationOrThrow } from "../helpers.js";
|
|
5
|
+
import { componentRef } from "../webhooks/helpers.js";
|
|
6
|
+
import { validateIdentity } from "../../component/payoutSync/helpers.js";
|
|
7
|
+
import type { result } from "../../component/payoutSync/validators.js";
|
|
8
|
+
import {
|
|
9
|
+
payoutAccount,
|
|
10
|
+
payoutSnapshot,
|
|
11
|
+
type PayoutSyncScope,
|
|
12
|
+
} from "./payoutSyncValidation.js";
|
|
13
|
+
export type PayoutSyncOptions = {
|
|
14
|
+
stripePayoutId: string;
|
|
15
|
+
scope: PayoutSyncScope;
|
|
16
|
+
};
|
|
17
|
+
export type PayoutSyncResult = Infer<typeof result>;
|
|
18
|
+
export type PreparedPayoutSync = Awaited<ReturnType<typeof preparePayoutSync>>;
|
|
19
|
+
async function markUnknown(
|
|
20
|
+
component: Component,
|
|
21
|
+
ctx: RunCtx,
|
|
22
|
+
observation: {
|
|
23
|
+
stripePayoutId: string;
|
|
24
|
+
accountId: string;
|
|
25
|
+
generation: number;
|
|
26
|
+
},
|
|
27
|
+
) {
|
|
28
|
+
await runMutationOrThrow(
|
|
29
|
+
ctx,
|
|
30
|
+
componentRef(component, "payoutSync/mutations/markUnknown"),
|
|
31
|
+
observation,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
/** Package-internal preparation for transactional host webhook dispatch. No financial record write. */
|
|
35
|
+
export async function preparePayoutSync(
|
|
36
|
+
stripe: Stripe,
|
|
37
|
+
component: Component,
|
|
38
|
+
ctx: RunCtx,
|
|
39
|
+
opts: PayoutSyncOptions,
|
|
40
|
+
) {
|
|
41
|
+
const identity = {
|
|
42
|
+
stripePayoutId: opts.stripePayoutId,
|
|
43
|
+
accountId: payoutAccount(opts.scope),
|
|
44
|
+
};
|
|
45
|
+
validateIdentity(identity);
|
|
46
|
+
if (!ctx.runMutation)
|
|
47
|
+
throw new Error("Payout synchronization requires runMutation.");
|
|
48
|
+
const generation = (await runMutationOrThrow(
|
|
49
|
+
ctx,
|
|
50
|
+
componentRef(component, "payoutSync/mutations/begin"),
|
|
51
|
+
identity,
|
|
52
|
+
)) as number;
|
|
53
|
+
try {
|
|
54
|
+
const payout = await stripe.payouts.retrieve(
|
|
55
|
+
identity.stripePayoutId,
|
|
56
|
+
{},
|
|
57
|
+
identity.accountId === "platform"
|
|
58
|
+
? {}
|
|
59
|
+
: { stripeAccount: identity.accountId },
|
|
60
|
+
);
|
|
61
|
+
return {
|
|
62
|
+
...identity,
|
|
63
|
+
generation,
|
|
64
|
+
payout: payoutSnapshot(payout, identity),
|
|
65
|
+
};
|
|
66
|
+
} catch (error) {
|
|
67
|
+
await markUnknown(component, ctx, { ...identity, generation });
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Host must authorize scope. This only reads Stripe and synchronizes the existing financial record. */
|
|
72
|
+
export async function syncPayoutFromStripe(
|
|
73
|
+
stripe: Stripe,
|
|
74
|
+
component: Component,
|
|
75
|
+
ctx: RunCtx,
|
|
76
|
+
opts: PayoutSyncOptions,
|
|
77
|
+
): Promise<PayoutSyncResult> {
|
|
78
|
+
const observation = await preparePayoutSync(stripe, component, ctx, opts);
|
|
79
|
+
try {
|
|
80
|
+
return (await runMutationOrThrow(
|
|
81
|
+
ctx,
|
|
82
|
+
componentRef(component, "payoutSync/mutations/finish"),
|
|
83
|
+
observation,
|
|
84
|
+
)) as PayoutSyncResult;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
await markUnknown(component, ctx, {
|
|
87
|
+
stripePayoutId: observation.stripePayoutId,
|
|
88
|
+
accountId: observation.accountId,
|
|
89
|
+
generation: observation.generation,
|
|
90
|
+
});
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// @vitest-environment edge-runtime
|
|
2
|
+
import { convexTest } from "convex-test";
|
|
3
|
+
import { makeFunctionReference } from "convex/server";
|
|
4
|
+
import { expect, vi } from "vitest";
|
|
5
|
+
import type Stripe from "stripe";
|
|
6
|
+
import type { Component, RunCtx } from "../helpers.js";
|
|
7
|
+
import schema from "../../component/schema.js";
|
|
8
|
+
const modules = import.meta.glob("../../component/**/*.*s");
|
|
9
|
+
const symbol = Symbol.for("toReferencePath");
|
|
10
|
+
export const component = {
|
|
11
|
+
payoutPolicy: {
|
|
12
|
+
queries: { get: { [symbol]: "betterStripe/payoutPolicy/queries/get" } },
|
|
13
|
+
},
|
|
14
|
+
} as unknown as Component;
|
|
15
|
+
export type Scope =
|
|
16
|
+
{ type: "connected"; accountId: string } | { type: "platform" };
|
|
17
|
+
type Sync = (
|
|
18
|
+
stripe: Stripe,
|
|
19
|
+
component: Component,
|
|
20
|
+
ctx: RunCtx,
|
|
21
|
+
opts: { stripePayoutId: string; scope: Scope },
|
|
22
|
+
) => Promise<unknown>;
|
|
23
|
+
export async function operation() {
|
|
24
|
+
const path = "./payoutSync.js";
|
|
25
|
+
const mod = (await import(path).catch(() => ({}))) as {
|
|
26
|
+
syncPayoutFromStripe?: Sync;
|
|
27
|
+
};
|
|
28
|
+
expect(
|
|
29
|
+
mod.syncPayoutFromStripe,
|
|
30
|
+
"authoritative payout synchronization is missing",
|
|
31
|
+
).toBeTypeOf("function");
|
|
32
|
+
return mod.syncPayoutFromStripe!;
|
|
33
|
+
}
|
|
34
|
+
export async function setup(
|
|
35
|
+
scope: Scope = { type: "connected", accountId: "acct_owner" },
|
|
36
|
+
) {
|
|
37
|
+
const t = convexTest(schema, modules);
|
|
38
|
+
const path = (ref: Record<symbol, string>) =>
|
|
39
|
+
ref[symbol].replace("betterStripe/", "").replace(/\/(\w+)$/, ":$1");
|
|
40
|
+
const mutate = (ref: Record<symbol, string>, args: never) =>
|
|
41
|
+
t.mutation(makeFunctionReference<"mutation">(path(ref)), args);
|
|
42
|
+
const ctx = {
|
|
43
|
+
runQuery: (ref: Record<symbol, string>, args: never) =>
|
|
44
|
+
t.query(makeFunctionReference<"query">(path(ref)), args),
|
|
45
|
+
runMutation: mutate,
|
|
46
|
+
} as unknown as RunCtx;
|
|
47
|
+
let payout = {
|
|
48
|
+
id: "po_one",
|
|
49
|
+
amount: 500,
|
|
50
|
+
currency: "jpy",
|
|
51
|
+
status: "paid",
|
|
52
|
+
arrival_date: 200,
|
|
53
|
+
created: 100,
|
|
54
|
+
automatic: true,
|
|
55
|
+
method: "standard",
|
|
56
|
+
type: "bank_account",
|
|
57
|
+
source_type: "card",
|
|
58
|
+
destination: "ba_one",
|
|
59
|
+
balance_transaction: "txn_pending",
|
|
60
|
+
failure_code: null,
|
|
61
|
+
failure_message: null,
|
|
62
|
+
metadata: { accountId: "acct_metadata_is_not_authority" },
|
|
63
|
+
} as unknown as Stripe.Payout;
|
|
64
|
+
const retrieve = vi.fn(async () => ({ ...payout }));
|
|
65
|
+
const provider = { payouts: { retrieve } } as unknown as Stripe;
|
|
66
|
+
const sync = await operation();
|
|
67
|
+
return {
|
|
68
|
+
t,
|
|
69
|
+
ctx,
|
|
70
|
+
mutate,
|
|
71
|
+
retrieve,
|
|
72
|
+
provider,
|
|
73
|
+
scope,
|
|
74
|
+
set: (patch: Partial<Stripe.Payout>) => {
|
|
75
|
+
payout = { ...payout, ...patch };
|
|
76
|
+
},
|
|
77
|
+
read: () => ({ ...payout }),
|
|
78
|
+
run: () =>
|
|
79
|
+
sync(provider, component, ctx, { stripePayoutId: "po_one", scope }),
|
|
80
|
+
row: () => t.run((ctx) => ctx.db.query("payouts").unique()),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type Stripe from "stripe";
|
|
2
|
+
import type { Infer } from "convex/values";
|
|
3
|
+
import type { snapshot } from "../../component/payoutSync/validators.js";
|
|
4
|
+
import {
|
|
5
|
+
validateIdentity,
|
|
6
|
+
validateSnapshot,
|
|
7
|
+
} from "../../component/payoutSync/helpers.js";
|
|
8
|
+
export type PayoutSyncScope =
|
|
9
|
+
{ type: "connected"; accountId: string } | { type: "platform" };
|
|
10
|
+
export function payoutAccount(scope: PayoutSyncScope) {
|
|
11
|
+
if (scope?.type === "platform" && Object.keys(scope).length === 1)
|
|
12
|
+
return "platform";
|
|
13
|
+
if (
|
|
14
|
+
scope?.type === "connected" &&
|
|
15
|
+
Object.keys(scope).length === 2 &&
|
|
16
|
+
typeof scope.accountId === "string" &&
|
|
17
|
+
/^acct_[A-Za-z0-9_]+$/.test(scope.accountId)
|
|
18
|
+
)
|
|
19
|
+
return scope.accountId;
|
|
20
|
+
throw new Error("Payout synchronization scope must be explicit.");
|
|
21
|
+
}
|
|
22
|
+
/** Status observations do not establish funding maturity or payout eligibility. */
|
|
23
|
+
export function payoutSnapshot(
|
|
24
|
+
p: Stripe.Payout,
|
|
25
|
+
args: { stripePayoutId: string; accountId: string },
|
|
26
|
+
): Infer<typeof snapshot> {
|
|
27
|
+
validateIdentity(args);
|
|
28
|
+
if (!p || typeof p !== "object")
|
|
29
|
+
throw new Error("Provider payout response is unavailable.");
|
|
30
|
+
if (p.id !== args.stripePayoutId)
|
|
31
|
+
throw new Error("Provider payout identity does not match.");
|
|
32
|
+
if (
|
|
33
|
+
(p.status !== "pending" &&
|
|
34
|
+
p.status !== "paid" &&
|
|
35
|
+
p.status !== "failed" &&
|
|
36
|
+
p.status !== "canceled" &&
|
|
37
|
+
p.status !== "in_transit") ||
|
|
38
|
+
typeof p.automatic !== "boolean" ||
|
|
39
|
+
!["standard", "instant"].includes(p.method)
|
|
40
|
+
)
|
|
41
|
+
throw new Error("Provider payout status or method is unsupported.");
|
|
42
|
+
if (
|
|
43
|
+
!Number.isSafeInteger(p.arrival_date) ||
|
|
44
|
+
p.arrival_date < 0 ||
|
|
45
|
+
!Number.isSafeInteger(p.arrival_date * 1000) ||
|
|
46
|
+
p.arrival_date * 1000 > 8640000000000000
|
|
47
|
+
)
|
|
48
|
+
throw new Error("Invalid provider payout arrival date.");
|
|
49
|
+
if (
|
|
50
|
+
(p.failure_code != null && typeof p.failure_code !== "string") ||
|
|
51
|
+
(p.failure_message != null && typeof p.failure_message !== "string")
|
|
52
|
+
)
|
|
53
|
+
throw new Error("Invalid provider payout failure detail.");
|
|
54
|
+
const metadata = p.metadata ?? {};
|
|
55
|
+
if (
|
|
56
|
+
typeof metadata !== "object" ||
|
|
57
|
+
Array.isArray(metadata) ||
|
|
58
|
+
Object.values(metadata).some((v) => typeof v !== "string")
|
|
59
|
+
)
|
|
60
|
+
throw new Error("Invalid provider payout metadata.");
|
|
61
|
+
const result: Infer<typeof snapshot> = {
|
|
62
|
+
...args,
|
|
63
|
+
amount: p.amount,
|
|
64
|
+
currency: p.currency,
|
|
65
|
+
status: p.status,
|
|
66
|
+
arrivalDate: new Date(p.arrival_date * 1000).toISOString(),
|
|
67
|
+
method: p.method,
|
|
68
|
+
failureCode: p.failure_code ?? undefined,
|
|
69
|
+
failureMessage: p.failure_message ?? undefined,
|
|
70
|
+
metadata,
|
|
71
|
+
};
|
|
72
|
+
validateSnapshot(result);
|
|
73
|
+
return result;
|
|
74
|
+
}
|