@fantastic.dev/better-stripe 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +7 -1
- package/dist/client/billing/checkout.d.ts +3 -43
- package/dist/client/billing/checkout.d.ts.map +1 -1
- package/dist/client/billing/checkout.js +5 -260
- package/dist/client/billing/checkout.js.map +1 -1
- package/dist/client/billing/checkoutRequest.d.ts +129 -0
- package/dist/client/billing/checkoutRequest.d.ts.map +1 -0
- package/dist/client/billing/checkoutRequest.js +263 -0
- package/dist/client/billing/checkoutRequest.js.map +1 -0
- package/dist/client/billing/preparedCheckout.d.ts +22 -0
- package/dist/client/billing/preparedCheckout.d.ts.map +1 -0
- package/dist/client/billing/preparedCheckout.js +86 -0
- package/dist/client/billing/preparedCheckout.js.map +1 -0
- package/dist/client/checkoutRequests.d.ts +80 -0
- package/dist/client/checkoutRequests.d.ts.map +1 -0
- package/dist/client/checkoutRequests.js +28 -0
- package/dist/client/checkoutRequests.js.map +1 -0
- package/dist/client/connect/payoutAdoptionTestHelpers.d.ts +464 -0
- package/dist/client/connect/payoutAdoptionTestHelpers.d.ts.map +1 -1
- package/dist/client/connect/payoutCollection.d.ts +21 -0
- package/dist/client/connect/payoutCollection.d.ts.map +1 -0
- package/dist/client/connect/payoutCollection.js +47 -0
- package/dist/client/connect/payoutCollection.js.map +1 -0
- package/dist/client/connect/payoutExecutionTestHelpers.d.ts +1805 -0
- package/dist/client/connect/payoutExecutionTestHelpers.d.ts.map +1 -0
- package/dist/client/connect/payoutExecutionTestHelpers.js +194 -0
- package/dist/client/connect/payoutExecutionTestHelpers.js.map +1 -0
- package/dist/client/connect/payoutSyncTestHelpers.d.ts +232 -0
- package/dist/client/connect/payoutSyncTestHelpers.d.ts.map +1 -1
- package/dist/client/connect/payoutWorker.d.ts +48 -0
- package/dist/client/connect/payoutWorker.d.ts.map +1 -0
- package/dist/client/connect/payoutWorker.js +78 -0
- package/dist/client/connect/payoutWorker.js.map +1 -0
- package/dist/client/index.d.ts +4 -11
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +4 -9
- package/dist/client/index.js.map +1 -1
- package/dist/client/payoutPolicy.d.ts +67 -0
- package/dist/client/payoutPolicy.d.ts.map +1 -1
- package/dist/client/payoutPolicy.js +10 -0
- package/dist/client/payoutPolicy.js.map +1 -1
- package/dist/client/readContracts.d.ts +11 -1
- package/dist/client/readContracts.d.ts.map +1 -1
- package/dist/client/readContracts.js +10 -0
- package/dist/client/readContracts.js.map +1 -1
- package/dist/component/_generated/component.d.ts +293 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/core/mutations.d.ts.map +1 -1
- package/dist/component/core/mutations.js +2 -0
- package/dist/component/core/mutations.js.map +1 -1
- package/dist/component/core/resetSafety.d.ts +4 -0
- package/dist/component/core/resetSafety.d.ts.map +1 -0
- package/dist/component/core/resetSafety.js +54 -0
- package/dist/component/core/resetSafety.js.map +1 -0
- package/dist/component/earnings/triggers.d.ts +418 -0
- package/dist/component/earnings/triggers.d.ts.map +1 -1
- package/dist/component/earnings/triggers.js +2 -0
- package/dist/component/earnings/triggers.js.map +1 -1
- package/dist/component/payoutPolicy/schema.d.ts +2 -0
- package/dist/component/payoutPolicy/schema.d.ts.map +1 -1
- package/dist/component/payoutPolicy/schema.js +3 -1
- package/dist/component/payoutPolicy/schema.js.map +1 -1
- package/dist/component/payoutRelease/mutations.js +1 -1
- package/dist/component/payoutRelease/schema.d.ts +1 -0
- package/dist/component/payoutRelease/schema.d.ts.map +1 -1
- package/dist/component/payoutRelease/schema.js +1 -0
- package/dist/component/payoutRelease/schema.js.map +1 -1
- package/dist/component/payoutSnapshots/localState.d.ts +89 -0
- package/dist/component/payoutSnapshots/localState.d.ts.map +1 -0
- package/dist/component/payoutSnapshots/localState.js +186 -0
- package/dist/component/payoutSnapshots/localState.js.map +1 -0
- package/dist/component/payoutSnapshots/mutations.d.ts +108 -0
- package/dist/component/payoutSnapshots/mutations.d.ts.map +1 -0
- package/dist/component/payoutSnapshots/mutations.js +263 -0
- package/dist/component/payoutSnapshots/mutations.js.map +1 -0
- package/dist/component/payoutSnapshots/revisions.d.ts +12 -0
- package/dist/component/payoutSnapshots/revisions.d.ts.map +1 -0
- package/dist/component/payoutSnapshots/revisions.js +173 -0
- package/dist/component/payoutSnapshots/revisions.js.map +1 -0
- package/dist/component/payoutSnapshots/schema.d.ts +285 -0
- package/dist/component/payoutSnapshots/schema.d.ts.map +1 -0
- package/dist/component/payoutSnapshots/schema.js +108 -0
- package/dist/component/payoutSnapshots/schema.js.map +1 -0
- package/dist/component/payoutSnapshots/scope.d.ts +7 -0
- package/dist/component/payoutSnapshots/scope.d.ts.map +1 -0
- package/dist/component/payoutSnapshots/scope.js +8 -0
- package/dist/component/payoutSnapshots/scope.js.map +1 -0
- package/dist/component/payoutWorker/helpers.d.ts +30 -0
- package/dist/component/payoutWorker/helpers.d.ts.map +1 -0
- package/dist/component/payoutWorker/helpers.js +21 -0
- package/dist/component/payoutWorker/helpers.js.map +1 -0
- package/dist/component/payoutWorker/mutations.d.ts +68 -0
- package/dist/component/payoutWorker/mutations.d.ts.map +1 -0
- package/dist/component/payoutWorker/mutations.js +161 -0
- package/dist/component/payoutWorker/mutations.js.map +1 -0
- package/dist/component/payoutWorker/queries.d.ts +123 -0
- package/dist/component/payoutWorker/queries.d.ts.map +1 -0
- package/dist/component/payoutWorker/queries.js +61 -0
- package/dist/component/payoutWorker/queries.js.map +1 -0
- package/dist/component/payoutWorker/schema.d.ts +91 -0
- package/dist/component/payoutWorker/schema.d.ts.map +1 -0
- package/dist/component/payoutWorker/schema.js +27 -0
- package/dist/component/payoutWorker/schema.js.map +1 -0
- package/dist/component/schema.d.ts +232 -0
- package/dist/component/schema.js +10 -0
- package/dist/component/schema.js.map +1 -1
- package/docs/read-contracts.md +48 -0
- package/docs/settlement.md +128 -1
- package/package.json +1 -1
- package/src/client/billing/checkout.ts +11 -359
- package/src/client/billing/checkoutRequest.ts +362 -0
- package/src/client/billing/prepared-checkout.test.ts +430 -0
- package/src/client/billing/preparedCheckout.ts +143 -0
- package/src/client/checkoutRequests.ts +40 -0
- package/src/client/connect/payoutCollection.test.ts +74 -0
- package/src/client/connect/payoutCollection.ts +82 -0
- package/src/client/connect/payoutExecution.test.ts +1 -211
- package/src/client/connect/payoutExecutionTestHelpers.ts +213 -0
- package/src/client/connect/payoutWorker.test.ts +145 -0
- package/src/client/connect/payoutWorker.ts +144 -0
- package/src/client/index.ts +10 -21
- package/src/client/payoutPolicy.ts +26 -1
- package/src/client/readContracts.ts +23 -0
- package/src/component/_generated/api.ts +16 -0
- package/src/component/_generated/component.ts +361 -0
- package/src/component/core/mutations.ts +2 -0
- package/src/component/core/resetSafety.ts +67 -0
- package/src/component/earnings/triggers.ts +2 -0
- package/src/component/payout-reset-safety.test.ts +148 -0
- package/src/component/payout-revisions.test.ts +125 -0
- package/src/component/payout-snapshots.test.ts +395 -0
- package/src/component/payout-worker.test.ts +165 -0
- package/src/component/payoutPolicy/schema.ts +3 -1
- package/src/component/payoutRelease/mutations.ts +1 -1
- package/src/component/payoutRelease/schema.ts +1 -0
- package/src/component/payoutSnapshots/localState.ts +238 -0
- package/src/component/payoutSnapshots/mutations.ts +275 -0
- package/src/component/payoutSnapshots/revisions.ts +247 -0
- package/src/component/payoutSnapshots/schema.ts +125 -0
- package/src/component/payoutSnapshots/scope.ts +7 -0
- package/src/component/payoutWorker/helpers.ts +30 -0
- package/src/component/payoutWorker/mutations.ts +181 -0
- package/src/component/payoutWorker/queries.ts +64 -0
- package/src/component/payoutWorker/schema.ts +26 -0
- package/src/component/public.test.ts +2 -2
- package/src/component/schema.ts +18 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertPayoutResetSafe } from "./resetSafety.js";
|
|
1
2
|
import { v } from "convex/values";
|
|
2
3
|
|
|
3
4
|
import type { TableNames } from "../_generated/dataModel";
|
|
@@ -155,6 +156,7 @@ export const clearAllTables = mutation({
|
|
|
155
156
|
tables: v.array(v.string()),
|
|
156
157
|
}),
|
|
157
158
|
handler: async (ctx) => {
|
|
159
|
+
await assertPayoutResetSafe(ctx);
|
|
158
160
|
// Derive table names from the schema definition so the clear list can never
|
|
159
161
|
// drift out of sync when new tables are added.
|
|
160
162
|
const tables = Object.keys(schema.tables) as TableNames[];
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { MutationCtx } from "../_generated/server.js";
|
|
2
|
+
/** A database reset cannot erase the only restoration or unresolved-payout record. */
|
|
3
|
+
export async function assertPayoutResetSafe(ctx: MutationCtx) {
|
|
4
|
+
const [bound, custom, changing, reserved, submitted, pending, transit] =
|
|
5
|
+
await Promise.all([
|
|
6
|
+
ctx.db
|
|
7
|
+
.query("payoutSnapshots")
|
|
8
|
+
.withIndex("by_read_only", (q) => q.eq("readOnly", false))
|
|
9
|
+
.first(),
|
|
10
|
+
ctx.db
|
|
11
|
+
.query("payoutPolicies")
|
|
12
|
+
.withIndex("by_mode_status", (q) => q.eq("mode", "custom"))
|
|
13
|
+
.first(),
|
|
14
|
+
ctx.db
|
|
15
|
+
.query("payoutPolicies")
|
|
16
|
+
.withIndex("by_mode_status", (q) =>
|
|
17
|
+
q.eq("mode", "stripe_managed").eq("status", "changing"),
|
|
18
|
+
)
|
|
19
|
+
.first(),
|
|
20
|
+
ctx.db
|
|
21
|
+
.query("payoutReleaseClaims")
|
|
22
|
+
.withIndex("by_status", (q) => q.eq("status", "reserved"))
|
|
23
|
+
.first(),
|
|
24
|
+
ctx.db
|
|
25
|
+
.query("payoutReleaseClaims")
|
|
26
|
+
.withIndex("by_status", (q) => q.eq("status", "submitted"))
|
|
27
|
+
.first(),
|
|
28
|
+
ctx.db
|
|
29
|
+
.query("payouts")
|
|
30
|
+
.withIndex("by_status", (q) => q.eq("status", "pending"))
|
|
31
|
+
.first(),
|
|
32
|
+
ctx.db
|
|
33
|
+
.query("payouts")
|
|
34
|
+
.withIndex("by_status", (q) => q.eq("status", "in_transit"))
|
|
35
|
+
.first(),
|
|
36
|
+
]);
|
|
37
|
+
if (bound)
|
|
38
|
+
throw new Error(
|
|
39
|
+
"A financially bound payout collection must be resolved before reset.",
|
|
40
|
+
);
|
|
41
|
+
if (custom || changing)
|
|
42
|
+
throw new Error(
|
|
43
|
+
"Restore and verify payout schedules before reset; restoration records cannot be erased.",
|
|
44
|
+
);
|
|
45
|
+
if (reserved || submitted)
|
|
46
|
+
throw new Error(
|
|
47
|
+
"Resolve payout claims before reset; unresolved reservations cannot be erased.",
|
|
48
|
+
);
|
|
49
|
+
if (pending || transit)
|
|
50
|
+
throw new Error("Verify in-flight provider payouts before reset.");
|
|
51
|
+
const affiliateOperations = await Promise.all(
|
|
52
|
+
(["pending", "unknown"] as const).flatMap((state) => [
|
|
53
|
+
ctx.db
|
|
54
|
+
.query("affiliatePayoutReleases")
|
|
55
|
+
.withIndex("by_state", (q) => q.eq("state", state))
|
|
56
|
+
.first(),
|
|
57
|
+
ctx.db
|
|
58
|
+
.query("affiliateRecoveryOperations")
|
|
59
|
+
.withIndex("by_state", (q) => q.eq("state", state))
|
|
60
|
+
.first(),
|
|
61
|
+
]),
|
|
62
|
+
);
|
|
63
|
+
if (affiliateOperations.some(Boolean))
|
|
64
|
+
throw new Error(
|
|
65
|
+
"Resolve pending or unknown affiliate operations before reset.",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { registerPayoutRevisions } from "../payoutSnapshots/revisions.js";
|
|
1
2
|
import { Triggers } from "convex-helpers/server/triggers";
|
|
2
3
|
import {
|
|
3
4
|
customCtx,
|
|
@@ -93,4 +94,5 @@ triggers.register("payouts", async (ctx, change) => {
|
|
|
93
94
|
await ctx.innerDb.patch("payouts", change.id, { totalsRecorded: true });
|
|
94
95
|
}
|
|
95
96
|
});
|
|
97
|
+
registerPayoutRevisions(triggers);
|
|
96
98
|
export const mutation = customMutation(rawMutation, customCtx(triggers.wrapDB));
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// @vitest-environment edge-runtime
|
|
2
|
+
import { convexTest } from "convex-test";
|
|
3
|
+
import { makeFunctionReference } from "convex/server";
|
|
4
|
+
import { expect, it } from "vitest";
|
|
5
|
+
import schema from "./schema.js";
|
|
6
|
+
const modules = import.meta.glob("./**/*.*s");
|
|
7
|
+
const reset = makeFunctionReference<"mutation">(
|
|
8
|
+
"core/mutations:clearAllTables",
|
|
9
|
+
);
|
|
10
|
+
const scope = { accountId: "acct_one", ownerKey: "owner" };
|
|
11
|
+
it.each(["custom", "changing"])(
|
|
12
|
+
"retains restoration records for %s payout policy",
|
|
13
|
+
async (kind) => {
|
|
14
|
+
const t = convexTest(schema, modules);
|
|
15
|
+
await t.run((ctx) =>
|
|
16
|
+
ctx.db.insert("payoutPolicies", {
|
|
17
|
+
...scope,
|
|
18
|
+
revision: 0,
|
|
19
|
+
mode: kind === "custom" ? "custom" : "stripe_managed",
|
|
20
|
+
status: kind === "changing" ? "changing" : "ready",
|
|
21
|
+
operationKey: "control",
|
|
22
|
+
country: "US",
|
|
23
|
+
thresholds: [],
|
|
24
|
+
previousSchedule: { interval: "weekly", weeklyDays: ["monday"] },
|
|
25
|
+
holdDeadline: Date.now() + 86400000,
|
|
26
|
+
}),
|
|
27
|
+
);
|
|
28
|
+
await expect(t.mutation(reset, {})).rejects.toThrow(/payout.*schedule/i);
|
|
29
|
+
expect(
|
|
30
|
+
await t.run((ctx) => ctx.db.query("payoutPolicies").collect()),
|
|
31
|
+
).toHaveLength(1);
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
it.each(["reserved", "submitted"])(
|
|
35
|
+
"retains an unresolved %s payout claim",
|
|
36
|
+
async (status) => {
|
|
37
|
+
const t = convexTest(schema, modules);
|
|
38
|
+
await t.run((ctx) =>
|
|
39
|
+
ctx.db.insert("payoutReleaseClaims", {
|
|
40
|
+
...scope,
|
|
41
|
+
operationKey: "money",
|
|
42
|
+
expectedRevision: 0,
|
|
43
|
+
currency: "usd",
|
|
44
|
+
amount: 1000,
|
|
45
|
+
status: status as "reserved" | "submitted",
|
|
46
|
+
allocations: [{ stripeTransferId: "tr_one", amount: 1000 }],
|
|
47
|
+
observations: [],
|
|
48
|
+
attemptedAt: Date.now(),
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
await expect(t.mutation(reset, {})).rejects.toThrow(/payout.*claim/i);
|
|
52
|
+
expect(
|
|
53
|
+
await t.run((ctx) => ctx.db.query("payoutReleaseClaims").collect()),
|
|
54
|
+
).toHaveLength(1);
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
it("retains known in-flight provider payouts", async () => {
|
|
58
|
+
const t = convexTest(schema, modules);
|
|
59
|
+
await t.mutation(
|
|
60
|
+
makeFunctionReference<"mutation">("connect/mutations:upsertPayout"),
|
|
61
|
+
{
|
|
62
|
+
stripePayoutId: "po_one",
|
|
63
|
+
accountId: scope.accountId,
|
|
64
|
+
currency: "usd",
|
|
65
|
+
amount: 1000,
|
|
66
|
+
status: "pending",
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
await expect(t.mutation(reset, {})).rejects.toThrow(/in.flight.*payout/i);
|
|
70
|
+
expect(await t.run((ctx) => ctx.db.query("payouts").collect())).toHaveLength(
|
|
71
|
+
1,
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
it("cannot bypass a bound collection through wholesale reset", async () => {
|
|
75
|
+
const t = convexTest(schema, modules);
|
|
76
|
+
const h = await t.mutation(
|
|
77
|
+
makeFunctionReference<"mutation">("payoutSnapshots/mutations:begin"),
|
|
78
|
+
{ ...scope, operationKey: "collection", expectedRevision: -1 },
|
|
79
|
+
);
|
|
80
|
+
await t.run((ctx) =>
|
|
81
|
+
ctx.db.patch("payoutSnapshots", h._id, { readOnly: false }),
|
|
82
|
+
);
|
|
83
|
+
await expect(t.mutation(reset, {})).rejects.toThrow(/bound.*collection/i);
|
|
84
|
+
expect(
|
|
85
|
+
await t.run((ctx) => ctx.db.query("payoutSnapshots").collect()),
|
|
86
|
+
).toHaveLength(1);
|
|
87
|
+
});
|
|
88
|
+
it.each(["pending", "unknown"] as const)(
|
|
89
|
+
"retains %s direct affiliate payout attempts",
|
|
90
|
+
async (state) => {
|
|
91
|
+
const t = convexTest(schema, modules);
|
|
92
|
+
await t.run((ctx) =>
|
|
93
|
+
ctx.db.insert("affiliatePayoutReleases", {
|
|
94
|
+
affiliateAccountId: scope.accountId,
|
|
95
|
+
currency: "usd",
|
|
96
|
+
operationKey: "direct-affiliate",
|
|
97
|
+
amount: 1000,
|
|
98
|
+
minimumAmount: 1,
|
|
99
|
+
createdAt: Date.now(),
|
|
100
|
+
state,
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
await expect(t.mutation(reset, {})).rejects.toThrow(
|
|
104
|
+
/affiliate.*operation/i,
|
|
105
|
+
);
|
|
106
|
+
expect(
|
|
107
|
+
await t.run((ctx) => ctx.db.query("affiliatePayoutReleases").collect()),
|
|
108
|
+
).toHaveLength(1);
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
it.each(["pending", "unknown"] as const)(
|
|
112
|
+
"retains %s affiliate recovery attempts",
|
|
113
|
+
async (state) => {
|
|
114
|
+
const t = convexTest(schema, modules);
|
|
115
|
+
await t.run(async (ctx) => {
|
|
116
|
+
const obligationId = await ctx.db.insert("affiliateObligations", {
|
|
117
|
+
sourceChargeId: "ch_one",
|
|
118
|
+
affiliateAccountId: scope.accountId,
|
|
119
|
+
currency: "usd",
|
|
120
|
+
chargeAmount: 1000,
|
|
121
|
+
originalCommission: 100,
|
|
122
|
+
refundedPrincipal: 1000,
|
|
123
|
+
disputedPrincipal: 0,
|
|
124
|
+
lostPrincipal: 0,
|
|
125
|
+
finalizedAmount: 100,
|
|
126
|
+
provisionalAmount: 0,
|
|
127
|
+
recoveredAmount: 0,
|
|
128
|
+
needsReconciliation: false,
|
|
129
|
+
});
|
|
130
|
+
await ctx.db.insert("affiliateRecoveryOperations", {
|
|
131
|
+
obligationId,
|
|
132
|
+
kind: "recovery",
|
|
133
|
+
amount: 100,
|
|
134
|
+
state,
|
|
135
|
+
attempted: true,
|
|
136
|
+
createdAt: Date.now(),
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
await expect(t.mutation(reset, {})).rejects.toThrow(
|
|
140
|
+
/affiliate.*operation/i,
|
|
141
|
+
);
|
|
142
|
+
expect(
|
|
143
|
+
await t.run((ctx) =>
|
|
144
|
+
ctx.db.query("affiliateRecoveryOperations").collect(),
|
|
145
|
+
),
|
|
146
|
+
).toHaveLength(1);
|
|
147
|
+
},
|
|
148
|
+
);
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// @vitest-environment edge-runtime
|
|
2
|
+
import { convexTest } from "convex-test";
|
|
3
|
+
import { makeFunctionReference } from "convex/server";
|
|
4
|
+
import { expect, it } from "vitest";
|
|
5
|
+
import schema from "./schema.js";
|
|
6
|
+
const modules = import.meta.glob("./**/*.*s");
|
|
7
|
+
const m = (name: string) =>
|
|
8
|
+
makeFunctionReference<"mutation">(`connect/mutations:${name}`);
|
|
9
|
+
const transfer = (id = "tr_one", extra = {}) => ({
|
|
10
|
+
stripeTransferId: id,
|
|
11
|
+
destinationAccountId: "acct_one",
|
|
12
|
+
amount: 1000,
|
|
13
|
+
currency: "usd",
|
|
14
|
+
role: "affiliate",
|
|
15
|
+
status: "paid",
|
|
16
|
+
sourceChargeId: "ch_one",
|
|
17
|
+
...extra,
|
|
18
|
+
});
|
|
19
|
+
const test = () => convexTest(schema, modules);
|
|
20
|
+
const revision = (t: ReturnType<typeof test>, accountId = "acct_one") =>
|
|
21
|
+
t.run((ctx) =>
|
|
22
|
+
ctx.db
|
|
23
|
+
.query("payoutFinancialRevisions")
|
|
24
|
+
.withIndex("by_account", (q) => q.eq("accountId", accountId))
|
|
25
|
+
.unique(),
|
|
26
|
+
);
|
|
27
|
+
it("records new positive funding without invalidating older risk-fenced snapshots", async () => {
|
|
28
|
+
const t = test();
|
|
29
|
+
await t.mutation(m("upsertTransfer"), transfer());
|
|
30
|
+
expect(await revision(t)).toMatchObject({ revision: 1, riskRevision: 0 });
|
|
31
|
+
await t.mutation(m("upsertTransfer"), transfer("tr_two"));
|
|
32
|
+
expect(await revision(t)).toMatchObject({ revision: 2, riskRevision: 0 });
|
|
33
|
+
});
|
|
34
|
+
it("does not invalidate snapshots for replay or metadata-only changes", async () => {
|
|
35
|
+
const t = test();
|
|
36
|
+
await t.mutation(m("upsertTransfer"), transfer());
|
|
37
|
+
await t.mutation(
|
|
38
|
+
m("upsertTransfer"),
|
|
39
|
+
transfer("tr_one", { metadata: { note: "cosmetic" } }),
|
|
40
|
+
);
|
|
41
|
+
expect(await revision(t)).toMatchObject({ revision: 1, riskRevision: 0 });
|
|
42
|
+
});
|
|
43
|
+
it("invalidates risk when a refund/reversal reduces source funding", async () => {
|
|
44
|
+
const t = test();
|
|
45
|
+
await t.mutation(m("upsertTransfer"), transfer());
|
|
46
|
+
await t.mutation(m("recordTransferReversal"), {
|
|
47
|
+
stripeTransferId: "tr_one",
|
|
48
|
+
reversedAmount: 100,
|
|
49
|
+
});
|
|
50
|
+
expect(await revision(t)).toMatchObject({ revision: 2, riskRevision: 1 });
|
|
51
|
+
await t.mutation(m("recordTransferReversal"), {
|
|
52
|
+
stripeTransferId: "tr_one",
|
|
53
|
+
reversedAmount: 100,
|
|
54
|
+
});
|
|
55
|
+
expect(await revision(t)).toMatchObject({ revision: 2, riskRevision: 1 });
|
|
56
|
+
});
|
|
57
|
+
it("reserved reversals invalidate risk before Stripe I/O", async () => {
|
|
58
|
+
const t = test();
|
|
59
|
+
await t.mutation(m("upsertTransfer"), transfer());
|
|
60
|
+
await t.mutation(m("claimReversalSlices"), {
|
|
61
|
+
operationId: "refund-one",
|
|
62
|
+
sourceChargeId: "ch_one",
|
|
63
|
+
mode: { kind: "fraction", chargeAmount: 1000, amountRefunded: 500 },
|
|
64
|
+
});
|
|
65
|
+
expect(await revision(t)).toMatchObject({ revision: 2, riskRevision: 1 });
|
|
66
|
+
});
|
|
67
|
+
it("a moved source invalidates both account scopes", async () => {
|
|
68
|
+
const t = test();
|
|
69
|
+
await t.mutation(m("upsertTransfer"), transfer());
|
|
70
|
+
await t.mutation(
|
|
71
|
+
m("upsertTransfer"),
|
|
72
|
+
transfer("tr_one", { destinationAccountId: "acct_other" }),
|
|
73
|
+
);
|
|
74
|
+
expect(await revision(t)).toMatchObject({ revision: 2, riskRevision: 1 });
|
|
75
|
+
expect(await revision(t, "acct_other")).toMatchObject({
|
|
76
|
+
revision: 1,
|
|
77
|
+
riskRevision: 1,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
it("concurrent independent source inserts cannot lose revision updates", async () => {
|
|
81
|
+
const t = test();
|
|
82
|
+
await Promise.all([
|
|
83
|
+
t.mutation(m("upsertTransfer"), transfer("tr_one")),
|
|
84
|
+
t.mutation(m("upsertTransfer"), transfer("tr_two")),
|
|
85
|
+
]);
|
|
86
|
+
expect(await revision(t)).toMatchObject({ revision: 2, riskRevision: 0 });
|
|
87
|
+
});
|
|
88
|
+
it("provider payout outcomes invalidate account cash risk", async () => {
|
|
89
|
+
const t = test();
|
|
90
|
+
await t.mutation(m("upsertPayout"), {
|
|
91
|
+
stripePayoutId: "po_one",
|
|
92
|
+
accountId: "acct_one",
|
|
93
|
+
amount: 1000,
|
|
94
|
+
currency: "usd",
|
|
95
|
+
status: "pending",
|
|
96
|
+
arrivalDate: new Date().toISOString(),
|
|
97
|
+
});
|
|
98
|
+
expect(await revision(t)).toMatchObject({ revision: 1, riskRevision: 1 });
|
|
99
|
+
});
|
|
100
|
+
it("records an ordered account risk journal for incremental repair", async () => {
|
|
101
|
+
const t = test();
|
|
102
|
+
await t.mutation(m("upsertTransfer"), transfer());
|
|
103
|
+
await t.mutation(m("recordTransferReversal"), {
|
|
104
|
+
stripeTransferId: "tr_one",
|
|
105
|
+
reversedAmount: 100,
|
|
106
|
+
});
|
|
107
|
+
const events = await t.run((ctx) =>
|
|
108
|
+
ctx.db.query("payoutRiskEvents").collect(),
|
|
109
|
+
);
|
|
110
|
+
expect(events).toMatchObject([
|
|
111
|
+
{ accountId: "acct_one", riskRevision: 1, kind: "transfer", key: "tr_one" },
|
|
112
|
+
]);
|
|
113
|
+
});
|
|
114
|
+
it("positive writes leave the isolated risk fence unchanged", async () => {
|
|
115
|
+
const t = test();
|
|
116
|
+
await t.mutation(m("upsertTransfer"), transfer());
|
|
117
|
+
const first = await t.run((ctx) =>
|
|
118
|
+
ctx.db.query("payoutRiskRevisions").collect(),
|
|
119
|
+
);
|
|
120
|
+
expect(first).toMatchObject([{ accountId: "acct_one", riskRevision: 0 }]);
|
|
121
|
+
await t.mutation(m("upsertTransfer"), transfer("tr_two"));
|
|
122
|
+
expect(
|
|
123
|
+
await t.run((ctx) => ctx.db.query("payoutRiskRevisions").collect()),
|
|
124
|
+
).toEqual(first);
|
|
125
|
+
});
|