@fantastic.dev/better-stripe 0.28.3 → 0.28.5
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 +5 -1
- package/dist/client/connect/payoutAdoptionTestHelpers.d.ts +102 -4
- package/dist/client/connect/payoutAdoptionTestHelpers.d.ts.map +1 -1
- package/dist/client/connect/payoutExecutionTestHelpers.d.ts +51 -2
- package/dist/client/connect/payoutExecutionTestHelpers.d.ts.map +1 -1
- package/dist/client/connect/payoutExpiry.d.ts +42 -0
- package/dist/client/connect/payoutExpiry.d.ts.map +1 -0
- package/dist/client/connect/payoutExpiry.js +93 -0
- package/dist/client/connect/payoutExpiry.js.map +1 -0
- package/dist/client/connect/payoutSyncTestHelpers.d.ts +41 -2
- package/dist/client/connect/payoutSyncTestHelpers.d.ts.map +1 -1
- package/dist/client/connect/payoutWorker.d.ts +4 -1
- package/dist/client/connect/payoutWorker.d.ts.map +1 -1
- package/dist/client/connect/payoutWorker.js +35 -1
- package/dist/client/connect/payoutWorker.js.map +1 -1
- package/dist/client/payoutPolicy.d.ts +20 -2
- package/dist/client/payoutPolicy.d.ts.map +1 -1
- package/dist/client/payoutPolicy.js +5 -25
- package/dist/client/payoutPolicy.js.map +1 -1
- package/dist/component/_generated/component.d.ts +120 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/core/resetSafety.d.ts.map +1 -1
- package/dist/component/core/resetSafety.js +7 -1
- package/dist/component/core/resetSafety.js.map +1 -1
- package/dist/component/earnings/triggers.d.ts +26 -4
- package/dist/component/earnings/triggers.d.ts.map +1 -1
- package/dist/component/payoutPolicy/expiry.d.ts +78 -0
- package/dist/component/payoutPolicy/expiry.d.ts.map +1 -0
- package/dist/component/payoutPolicy/expiry.js +57 -0
- package/dist/component/payoutPolicy/expiry.js.map +1 -0
- package/dist/component/payoutPolicy/helpers.d.ts +10 -0
- package/dist/component/payoutPolicy/helpers.d.ts.map +1 -1
- package/dist/component/payoutPolicy/mutations.d.ts +77 -0
- package/dist/component/payoutPolicy/mutations.d.ts.map +1 -1
- package/dist/component/payoutPolicy/mutations.js +36 -3
- package/dist/component/payoutPolicy/mutations.js.map +1 -1
- package/dist/component/payoutPolicy/queries.d.ts +20 -0
- package/dist/component/payoutPolicy/queries.d.ts.map +1 -1
- package/dist/component/payoutPolicy/schema.d.ts +139 -2
- package/dist/component/payoutPolicy/schema.d.ts.map +1 -1
- package/dist/component/payoutPolicy/schema.js +12 -0
- package/dist/component/payoutPolicy/schema.js.map +1 -1
- package/dist/component/payoutRelease/helpers.d.ts +10 -0
- package/dist/component/payoutRelease/helpers.d.ts.map +1 -1
- package/dist/component/payoutWorker/helpers.d.ts +1 -0
- package/dist/component/payoutWorker/helpers.d.ts.map +1 -1
- package/dist/component/payoutWorker/mutations.d.ts +2 -0
- package/dist/component/payoutWorker/mutations.d.ts.map +1 -1
- package/dist/component/payoutWorker/mutations.js +3 -0
- package/dist/component/payoutWorker/mutations.js.map +1 -1
- package/dist/component/payoutWorker/queries.d.ts +22 -0
- package/dist/component/payoutWorker/queries.d.ts.map +1 -1
- package/dist/component/payoutWorker/schema.d.ts +18 -2
- package/dist/component/payoutWorker/schema.d.ts.map +1 -1
- package/dist/component/payoutWorker/schema.js +7 -0
- package/dist/component/payoutWorker/schema.js.map +1 -1
- package/dist/component/schema.d.ts +41 -2
- package/dist/react/workflows/PayoutAttemptStatus.d.ts +14 -0
- package/dist/react/workflows/PayoutAttemptStatus.d.ts.map +1 -0
- package/dist/react/workflows/PayoutAttemptStatus.js +13 -0
- package/dist/react/workflows/PayoutAttemptStatus.js.map +1 -0
- package/dist/react/workflows/PayoutProgress.d.ts.map +1 -1
- package/dist/react/workflows/PayoutProgress.js +3 -2
- package/dist/react/workflows/PayoutProgress.js.map +1 -1
- package/dist/react/workflows/index.d.ts +2 -0
- package/dist/react/workflows/index.d.ts.map +1 -1
- package/dist/react/workflows/index.js +1 -0
- package/dist/react/workflows/index.js.map +1 -1
- package/dist/react/workflows/payout-attempt-state.d.ts +9 -0
- package/dist/react/workflows/payout-attempt-state.d.ts.map +1 -0
- package/dist/react/workflows/payout-attempt-state.js +47 -0
- package/dist/react/workflows/payout-attempt-state.js.map +1 -0
- package/dist/react/workflows/payout-progress-state.d.ts +16 -2
- package/dist/react/workflows/payout-progress-state.d.ts.map +1 -1
- package/dist/react/workflows/payout-progress-state.js +37 -2
- package/dist/react/workflows/payout-progress-state.js.map +1 -1
- package/docs/react-workflows.md +30 -0
- package/docs/settlement.md +47 -3
- package/package.json +1 -1
- package/src/client/connect/payoutDeadline.test.ts +70 -0
- package/src/client/connect/payoutExpiry.ts +132 -0
- package/src/client/connect/payoutWorker.test.ts +57 -1
- package/src/client/connect/payoutWorker.ts +46 -2
- package/src/client/payoutPolicy.test.ts +196 -0
- package/src/client/payoutPolicy.ts +11 -23
- package/src/component/_generated/api.ts +2 -0
- package/src/component/_generated/component.ts +147 -0
- package/src/component/core/resetSafety.ts +49 -33
- package/src/component/payout-reset-safety.test.ts +28 -0
- package/src/component/payoutPolicy/expiry.ts +80 -0
- package/src/component/payoutPolicy/mutations.ts +47 -2
- package/src/component/payoutPolicy/schema.ts +17 -0
- package/src/component/payoutWorker/mutations.ts +3 -0
- package/src/component/payoutWorker/schema.ts +15 -0
- package/src/react/workflows/PayoutAttemptStatus.tsx +44 -0
- package/src/react/workflows/PayoutProgress.test.tsx +90 -0
- package/src/react/workflows/PayoutProgress.tsx +31 -1
- package/src/react/workflows/index.ts +6 -0
- package/src/react/workflows/payout-attempt-state.ts +70 -0
- package/src/react/workflows/payout-progress-state.ts +62 -2
|
@@ -2790,6 +2790,44 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
2790
2790
|
},
|
|
2791
2791
|
Name
|
|
2792
2792
|
>;
|
|
2793
|
+
beginHoldingExpiry: FunctionReference<
|
|
2794
|
+
"mutation",
|
|
2795
|
+
"internal",
|
|
2796
|
+
{ accountId: string; ownerKey: string },
|
|
2797
|
+
{
|
|
2798
|
+
_creationTime: number;
|
|
2799
|
+
_id: string;
|
|
2800
|
+
accountId: string;
|
|
2801
|
+
country: string;
|
|
2802
|
+
holdDeadline?: number;
|
|
2803
|
+
holdingExpiry?: {
|
|
2804
|
+
balances?: Array<{
|
|
2805
|
+
available: number;
|
|
2806
|
+
currency: string;
|
|
2807
|
+
pending: number;
|
|
2808
|
+
}>;
|
|
2809
|
+
deadline: number;
|
|
2810
|
+
observedAt?: number;
|
|
2811
|
+
status:
|
|
2812
|
+
| "unverified"
|
|
2813
|
+
| "balance_remaining"
|
|
2814
|
+
| "claims_unresolved"
|
|
2815
|
+
| "no_held_balance";
|
|
2816
|
+
};
|
|
2817
|
+
mode: "stripe_managed" | "custom";
|
|
2818
|
+
operationKey: string;
|
|
2819
|
+
ownerKey: string;
|
|
2820
|
+
previousSchedule?: {
|
|
2821
|
+
interval: "daily" | "weekly" | "monthly" | "manual";
|
|
2822
|
+
monthlyDays?: Array<number>;
|
|
2823
|
+
weeklyDays?: Array<string>;
|
|
2824
|
+
};
|
|
2825
|
+
revision: number;
|
|
2826
|
+
status: "changing" | "ready";
|
|
2827
|
+
thresholds: Array<{ amount: number; currency: string }>;
|
|
2828
|
+
},
|
|
2829
|
+
Name
|
|
2830
|
+
>;
|
|
2793
2831
|
cancelUnattempted: FunctionReference<
|
|
2794
2832
|
"mutation",
|
|
2795
2833
|
"internal",
|
|
@@ -2906,6 +2944,55 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
2906
2944
|
},
|
|
2907
2945
|
Name
|
|
2908
2946
|
>;
|
|
2947
|
+
observeHoldingExpiry: FunctionReference<
|
|
2948
|
+
"mutation",
|
|
2949
|
+
"internal",
|
|
2950
|
+
{
|
|
2951
|
+
accountId: string;
|
|
2952
|
+
balances: Array<{
|
|
2953
|
+
available: number;
|
|
2954
|
+
currency: string;
|
|
2955
|
+
pending: number;
|
|
2956
|
+
}>;
|
|
2957
|
+
deadline: number;
|
|
2958
|
+
expectedRevision: number;
|
|
2959
|
+
observedAt: number;
|
|
2960
|
+
ownerKey: string;
|
|
2961
|
+
},
|
|
2962
|
+
{
|
|
2963
|
+
_creationTime: number;
|
|
2964
|
+
_id: string;
|
|
2965
|
+
accountId: string;
|
|
2966
|
+
country: string;
|
|
2967
|
+
holdDeadline?: number;
|
|
2968
|
+
holdingExpiry?: {
|
|
2969
|
+
balances?: Array<{
|
|
2970
|
+
available: number;
|
|
2971
|
+
currency: string;
|
|
2972
|
+
pending: number;
|
|
2973
|
+
}>;
|
|
2974
|
+
deadline: number;
|
|
2975
|
+
observedAt?: number;
|
|
2976
|
+
status:
|
|
2977
|
+
| "unverified"
|
|
2978
|
+
| "balance_remaining"
|
|
2979
|
+
| "claims_unresolved"
|
|
2980
|
+
| "no_held_balance";
|
|
2981
|
+
};
|
|
2982
|
+
mode: "stripe_managed" | "custom";
|
|
2983
|
+
operationKey: string;
|
|
2984
|
+
ownerKey: string;
|
|
2985
|
+
previousSchedule?: {
|
|
2986
|
+
interval: "daily" | "weekly" | "monthly" | "manual";
|
|
2987
|
+
monthlyDays?: Array<number>;
|
|
2988
|
+
weeklyDays?: Array<string>;
|
|
2989
|
+
};
|
|
2990
|
+
revision: number;
|
|
2991
|
+
status: "changing" | "ready";
|
|
2992
|
+
thresholds: Array<{ amount: number; currency: string }>;
|
|
2993
|
+
},
|
|
2994
|
+
Name
|
|
2995
|
+
>;
|
|
2909
2996
|
prepare: FunctionReference<
|
|
2910
2997
|
"mutation",
|
|
2911
2998
|
"internal",
|
|
@@ -3007,6 +3094,20 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
3007
3094
|
accountId: string;
|
|
3008
3095
|
country: string;
|
|
3009
3096
|
holdDeadline?: number;
|
|
3097
|
+
holdingExpiry?: {
|
|
3098
|
+
balances?: Array<{
|
|
3099
|
+
available: number;
|
|
3100
|
+
currency: string;
|
|
3101
|
+
pending: number;
|
|
3102
|
+
}>;
|
|
3103
|
+
deadline: number;
|
|
3104
|
+
observedAt?: number;
|
|
3105
|
+
status:
|
|
3106
|
+
| "unverified"
|
|
3107
|
+
| "balance_remaining"
|
|
3108
|
+
| "claims_unresolved"
|
|
3109
|
+
| "no_held_balance";
|
|
3110
|
+
};
|
|
3010
3111
|
mode: "stripe_managed" | "custom";
|
|
3011
3112
|
operationKey: string;
|
|
3012
3113
|
ownerKey: string;
|
|
@@ -3032,6 +3133,20 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
3032
3133
|
accountId: string;
|
|
3033
3134
|
country: string;
|
|
3034
3135
|
holdDeadline?: number;
|
|
3136
|
+
holdingExpiry?: {
|
|
3137
|
+
balances?: Array<{
|
|
3138
|
+
available: number;
|
|
3139
|
+
currency: string;
|
|
3140
|
+
pending: number;
|
|
3141
|
+
}>;
|
|
3142
|
+
deadline: number;
|
|
3143
|
+
observedAt?: number;
|
|
3144
|
+
status:
|
|
3145
|
+
| "unverified"
|
|
3146
|
+
| "balance_remaining"
|
|
3147
|
+
| "claims_unresolved"
|
|
3148
|
+
| "no_held_balance";
|
|
3149
|
+
};
|
|
3035
3150
|
mode: "stripe_managed" | "custom";
|
|
3036
3151
|
operationKey: string;
|
|
3037
3152
|
ownerKey: string;
|
|
@@ -3997,6 +4112,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
3997
4112
|
hasOperation: boolean;
|
|
3998
4113
|
lastAttemptAt?: number;
|
|
3999
4114
|
lastReason?: string;
|
|
4115
|
+
lastResultAt?: number;
|
|
4000
4116
|
leaseUntil?: number;
|
|
4001
4117
|
nextAttemptAt: number;
|
|
4002
4118
|
operationKey?: string;
|
|
@@ -4031,6 +4147,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
4031
4147
|
hasOperation: boolean;
|
|
4032
4148
|
lastAttemptAt?: number;
|
|
4033
4149
|
lastReason?: string;
|
|
4150
|
+
lastResultAt?: number;
|
|
4034
4151
|
leaseUntil?: number;
|
|
4035
4152
|
nextAttemptAt: number;
|
|
4036
4153
|
operationKey?: string;
|
|
@@ -4063,6 +4180,20 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
4063
4180
|
accountId: string;
|
|
4064
4181
|
country: string;
|
|
4065
4182
|
holdDeadline?: number;
|
|
4183
|
+
holdingExpiry?: {
|
|
4184
|
+
balances?: Array<{
|
|
4185
|
+
available: number;
|
|
4186
|
+
currency: string;
|
|
4187
|
+
pending: number;
|
|
4188
|
+
}>;
|
|
4189
|
+
deadline: number;
|
|
4190
|
+
observedAt?: number;
|
|
4191
|
+
status:
|
|
4192
|
+
| "unverified"
|
|
4193
|
+
| "balance_remaining"
|
|
4194
|
+
| "claims_unresolved"
|
|
4195
|
+
| "no_held_balance";
|
|
4196
|
+
};
|
|
4066
4197
|
mode: "stripe_managed" | "custom";
|
|
4067
4198
|
operationKey: string;
|
|
4068
4199
|
ownerKey: string;
|
|
@@ -4094,6 +4225,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
4094
4225
|
hasOperation: boolean;
|
|
4095
4226
|
lastAttemptAt?: number;
|
|
4096
4227
|
lastReason?: string;
|
|
4228
|
+
lastResultAt?: number;
|
|
4097
4229
|
leaseUntil?: number;
|
|
4098
4230
|
nextAttemptAt: number;
|
|
4099
4231
|
operationKey?: string;
|
|
@@ -4124,6 +4256,20 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
4124
4256
|
accountId: string;
|
|
4125
4257
|
country: string;
|
|
4126
4258
|
holdDeadline?: number;
|
|
4259
|
+
holdingExpiry?: {
|
|
4260
|
+
balances?: Array<{
|
|
4261
|
+
available: number;
|
|
4262
|
+
currency: string;
|
|
4263
|
+
pending: number;
|
|
4264
|
+
}>;
|
|
4265
|
+
deadline: number;
|
|
4266
|
+
observedAt?: number;
|
|
4267
|
+
status:
|
|
4268
|
+
| "unverified"
|
|
4269
|
+
| "balance_remaining"
|
|
4270
|
+
| "claims_unresolved"
|
|
4271
|
+
| "no_held_balance";
|
|
4272
|
+
};
|
|
4127
4273
|
mode: "stripe_managed" | "custom";
|
|
4128
4274
|
operationKey: string;
|
|
4129
4275
|
ownerKey: string;
|
|
@@ -4167,6 +4313,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
4167
4313
|
hasOperation: boolean;
|
|
4168
4314
|
lastAttemptAt?: number;
|
|
4169
4315
|
lastReason?: string;
|
|
4316
|
+
lastResultAt?: number;
|
|
4170
4317
|
leaseUntil?: number;
|
|
4171
4318
|
nextAttemptAt: number;
|
|
4172
4319
|
operationKey?: string;
|
|
@@ -1,39 +1,51 @@
|
|
|
1
1
|
import type { MutationCtx } from "../_generated/server.js";
|
|
2
2
|
/** A database reset cannot erase the only restoration or unresolved-payout record. */
|
|
3
3
|
export async function assertPayoutResetSafe(ctx: MutationCtx) {
|
|
4
|
-
const [
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
4
|
+
const [
|
|
5
|
+
bound,
|
|
6
|
+
custom,
|
|
7
|
+
changing,
|
|
8
|
+
reserved,
|
|
9
|
+
submitted,
|
|
10
|
+
pending,
|
|
11
|
+
transit,
|
|
12
|
+
deadline,
|
|
13
|
+
] = await Promise.all([
|
|
14
|
+
ctx.db
|
|
15
|
+
.query("payoutSnapshots")
|
|
16
|
+
.withIndex("by_read_only", (q) => q.eq("readOnly", false))
|
|
17
|
+
.first(),
|
|
18
|
+
ctx.db
|
|
19
|
+
.query("payoutPolicies")
|
|
20
|
+
.withIndex("by_mode_status", (q) => q.eq("mode", "custom"))
|
|
21
|
+
.first(),
|
|
22
|
+
ctx.db
|
|
23
|
+
.query("payoutPolicies")
|
|
24
|
+
.withIndex("by_mode_status", (q) =>
|
|
25
|
+
q.eq("mode", "stripe_managed").eq("status", "changing"),
|
|
26
|
+
)
|
|
27
|
+
.first(),
|
|
28
|
+
ctx.db
|
|
29
|
+
.query("payoutReleaseClaims")
|
|
30
|
+
.withIndex("by_status", (q) => q.eq("status", "reserved"))
|
|
31
|
+
.first(),
|
|
32
|
+
ctx.db
|
|
33
|
+
.query("payoutReleaseClaims")
|
|
34
|
+
.withIndex("by_status", (q) => q.eq("status", "submitted"))
|
|
35
|
+
.first(),
|
|
36
|
+
ctx.db
|
|
37
|
+
.query("payouts")
|
|
38
|
+
.withIndex("by_status", (q) => q.eq("status", "pending"))
|
|
39
|
+
.first(),
|
|
40
|
+
ctx.db
|
|
41
|
+
.query("payouts")
|
|
42
|
+
.withIndex("by_status", (q) => q.eq("status", "in_transit"))
|
|
43
|
+
.first(),
|
|
44
|
+
ctx.db
|
|
45
|
+
.query("payoutPolicies")
|
|
46
|
+
.withIndex("by_deadline", (q) => q.gt("holdDeadline", 0))
|
|
47
|
+
.first(),
|
|
48
|
+
]);
|
|
37
49
|
if (bound)
|
|
38
50
|
throw new Error(
|
|
39
51
|
"A financially bound payout collection must be resolved before reset.",
|
|
@@ -42,6 +54,10 @@ export async function assertPayoutResetSafe(ctx: MutationCtx) {
|
|
|
42
54
|
throw new Error(
|
|
43
55
|
"Restore and verify payout schedules before reset; restoration records cannot be erased.",
|
|
44
56
|
);
|
|
57
|
+
if (deadline)
|
|
58
|
+
throw new Error(
|
|
59
|
+
"Resolve holding expiry recovery before reset; its deadline and balance record cannot be erased.",
|
|
60
|
+
);
|
|
45
61
|
if (reserved || submitted)
|
|
46
62
|
throw new Error(
|
|
47
63
|
"Resolve payout claims before reset; unresolved reservations cannot be erased.",
|
|
@@ -146,3 +146,31 @@ it.each(["pending", "unknown"] as const)(
|
|
|
146
146
|
).toHaveLength(1);
|
|
147
147
|
},
|
|
148
148
|
);
|
|
149
|
+
|
|
150
|
+
it("retains an unresolved holding-expiry record after automatic scheduling was restored", async () => {
|
|
151
|
+
const t = convexTest(schema, modules);
|
|
152
|
+
await t.run((ctx) =>
|
|
153
|
+
ctx.db.insert("payoutPolicies", {
|
|
154
|
+
...scope,
|
|
155
|
+
revision: 1,
|
|
156
|
+
mode: "stripe_managed",
|
|
157
|
+
status: "ready",
|
|
158
|
+
operationKey: "restore",
|
|
159
|
+
country: "US",
|
|
160
|
+
thresholds: [],
|
|
161
|
+
holdDeadline: Date.now() - 1,
|
|
162
|
+
holdingExpiry: {
|
|
163
|
+
deadline: Date.now() - 1,
|
|
164
|
+
status: "balance_remaining",
|
|
165
|
+
balances: [{ currency: "usd", available: 999, pending: 0 }],
|
|
166
|
+
observedAt: Date.now(),
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
await expect(t.mutation(reset, {})).rejects.toThrow(
|
|
171
|
+
/holding expiry.*recovery/i,
|
|
172
|
+
);
|
|
173
|
+
expect(
|
|
174
|
+
await t.run((ctx) => ctx.db.query("payoutPolicies").first()),
|
|
175
|
+
).not.toBeNull();
|
|
176
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { MutationCtx } from "../_generated/server.js";
|
|
2
|
+
import { readPolicy } from "./helpers.js";
|
|
3
|
+
type Scope = { accountId: string; ownerKey: string };
|
|
4
|
+
export async function beginHoldingExpiry(ctx: MutationCtx, args: Scope) {
|
|
5
|
+
const p = await readPolicy(ctx, args);
|
|
6
|
+
if (
|
|
7
|
+
!p ||
|
|
8
|
+
p.mode !== "custom" ||
|
|
9
|
+
!p.holdDeadline ||
|
|
10
|
+
p.holdDeadline > Date.now()
|
|
11
|
+
)
|
|
12
|
+
throw new Error("A due custom holding deadline is required.");
|
|
13
|
+
if (p.holdingExpiry?.deadline === p.holdDeadline) return p;
|
|
14
|
+
if (p.holdingExpiry && p.holdingExpiry.status !== "no_held_balance")
|
|
15
|
+
throw new Error("Previous holding expiry recovery is unresolved.");
|
|
16
|
+
await ctx.db.patch("payoutPolicies", p._id, {
|
|
17
|
+
holdingExpiry: { deadline: p.holdDeadline, status: "unverified" },
|
|
18
|
+
});
|
|
19
|
+
return (await ctx.db.get("payoutPolicies", p._id))!;
|
|
20
|
+
}
|
|
21
|
+
export async function observeHoldingExpiry(
|
|
22
|
+
ctx: MutationCtx,
|
|
23
|
+
args: Scope & {
|
|
24
|
+
expectedRevision: number;
|
|
25
|
+
deadline: number;
|
|
26
|
+
observedAt: number;
|
|
27
|
+
balances: { currency: string; available: number; pending: number }[];
|
|
28
|
+
},
|
|
29
|
+
) {
|
|
30
|
+
const p = await readPolicy(ctx, args);
|
|
31
|
+
if (
|
|
32
|
+
!p ||
|
|
33
|
+
p.revision !== args.expectedRevision ||
|
|
34
|
+
p.mode !== "stripe_managed" ||
|
|
35
|
+
p.status !== "ready" ||
|
|
36
|
+
p.holdingExpiry?.deadline !== args.deadline ||
|
|
37
|
+
p.holdDeadline !== args.deadline
|
|
38
|
+
)
|
|
39
|
+
throw new Error("Holding expiry recovery changed.");
|
|
40
|
+
if (
|
|
41
|
+
!Number.isSafeInteger(args.observedAt) ||
|
|
42
|
+
args.observedAt > Date.now() ||
|
|
43
|
+
args.observedAt < Date.now() - 30000 ||
|
|
44
|
+
new Set(args.balances.map((b) => b.currency)).size !==
|
|
45
|
+
args.balances.length ||
|
|
46
|
+
args.balances.some(
|
|
47
|
+
(b) =>
|
|
48
|
+
!/^[a-z]{3}$/.test(b.currency) ||
|
|
49
|
+
!Number.isSafeInteger(b.available) ||
|
|
50
|
+
!Number.isSafeInteger(b.pending),
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
throw new Error("Invalid or stale holding expiry balance observation.");
|
|
54
|
+
const unresolved = await Promise.all(
|
|
55
|
+
(["reserved", "submitted"] as const).map((status) =>
|
|
56
|
+
ctx.db
|
|
57
|
+
.query("payoutReleaseClaims")
|
|
58
|
+
.withIndex("by_account_status", (q) =>
|
|
59
|
+
q.eq("accountId", args.accountId).eq("status", status),
|
|
60
|
+
)
|
|
61
|
+
.first(),
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
const status = unresolved.some(Boolean)
|
|
65
|
+
? ("claims_unresolved" as const)
|
|
66
|
+
: args.balances.some((b) => b.available !== 0 || b.pending !== 0)
|
|
67
|
+
? ("balance_remaining" as const)
|
|
68
|
+
: ("no_held_balance" as const);
|
|
69
|
+
await ctx.db.patch("payoutPolicies", p._id, {
|
|
70
|
+
holdingExpiry: {
|
|
71
|
+
deadline: args.deadline,
|
|
72
|
+
observedAt: args.observedAt,
|
|
73
|
+
balances: args.balances,
|
|
74
|
+
status,
|
|
75
|
+
},
|
|
76
|
+
// Retain due discovery until both provider balances and durable claims clear.
|
|
77
|
+
holdDeadline: status === "no_held_balance" ? undefined : args.deadline,
|
|
78
|
+
});
|
|
79
|
+
return (await ctx.db.get("payoutPolicies", p._id))!;
|
|
80
|
+
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
beginHoldingExpiry as beginExpiry,
|
|
3
|
+
observeHoldingExpiry as observeExpiry,
|
|
4
|
+
} from "./expiry.js";
|
|
1
5
|
import { validateAdoption } from "./adoption.js";
|
|
2
6
|
import { v } from "convex/values";
|
|
3
7
|
import { mutation } from "../_generated/server.js";
|
|
4
8
|
import {
|
|
5
9
|
adoptionProof,
|
|
10
|
+
expiryBalance,
|
|
11
|
+
policyDoc,
|
|
6
12
|
prepareFields,
|
|
7
13
|
operationDoc,
|
|
8
14
|
scopeFields,
|
|
@@ -69,6 +75,14 @@ export const prepare = mutation({
|
|
|
69
75
|
)
|
|
70
76
|
throw new Error("Payout policy observation is stale or in the future.");
|
|
71
77
|
const policy = await readPolicy(ctx, args);
|
|
78
|
+
if (
|
|
79
|
+
args.mode === "custom" &&
|
|
80
|
+
policy?.holdingExpiry &&
|
|
81
|
+
policy.holdingExpiry.status !== "no_held_balance"
|
|
82
|
+
)
|
|
83
|
+
throw new Error(
|
|
84
|
+
"Holding expiry requires recovery before custom activation.",
|
|
85
|
+
);
|
|
72
86
|
if (policy?.status === "changing")
|
|
73
87
|
throw new Error("A payout policy operation is pending.");
|
|
74
88
|
if ((policy?.revision ?? -1) !== args.expectedRevision)
|
|
@@ -177,7 +191,14 @@ export const confirm = mutation({
|
|
|
177
191
|
thresholds: op.thresholds,
|
|
178
192
|
status: "ready",
|
|
179
193
|
...(op.mode === "stripe_managed"
|
|
180
|
-
? {
|
|
194
|
+
? {
|
|
195
|
+
previousSchedule: undefined,
|
|
196
|
+
holdDeadline:
|
|
197
|
+
policy.holdingExpiry &&
|
|
198
|
+
policy.holdingExpiry.status !== "no_held_balance"
|
|
199
|
+
? policy.holdDeadline
|
|
200
|
+
: undefined,
|
|
201
|
+
}
|
|
181
202
|
: {}),
|
|
182
203
|
});
|
|
183
204
|
return (await ctx.db.get("payoutPolicyOperations", op._id))!;
|
|
@@ -263,9 +284,33 @@ export const cancelUnattempted = mutation({
|
|
|
263
284
|
await ctx.db.patch("payoutPolicies", policy._id, {
|
|
264
285
|
status: "ready",
|
|
265
286
|
...(policy.mode === "stripe_managed"
|
|
266
|
-
? {
|
|
287
|
+
? {
|
|
288
|
+
previousSchedule: undefined,
|
|
289
|
+
holdDeadline:
|
|
290
|
+
policy.holdingExpiry &&
|
|
291
|
+
policy.holdingExpiry.status !== "no_held_balance"
|
|
292
|
+
? policy.holdDeadline
|
|
293
|
+
: undefined,
|
|
294
|
+
}
|
|
267
295
|
: {}),
|
|
268
296
|
});
|
|
269
297
|
return (await ctx.db.get("payoutPolicyOperations", op._id))!;
|
|
270
298
|
},
|
|
271
299
|
});
|
|
300
|
+
|
|
301
|
+
export const beginHoldingExpiry = mutation({
|
|
302
|
+
args: scopeFields,
|
|
303
|
+
returns: policyDoc,
|
|
304
|
+
handler: beginExpiry,
|
|
305
|
+
});
|
|
306
|
+
export const observeHoldingExpiry = mutation({
|
|
307
|
+
args: {
|
|
308
|
+
...scopeFields,
|
|
309
|
+
expectedRevision: v.number(),
|
|
310
|
+
deadline: v.number(),
|
|
311
|
+
observedAt: v.number(),
|
|
312
|
+
balances: v.array(expiryBalance),
|
|
313
|
+
},
|
|
314
|
+
returns: policyDoc,
|
|
315
|
+
handler: observeExpiry,
|
|
316
|
+
});
|
|
@@ -34,6 +34,22 @@ export const prepareFields = {
|
|
|
34
34
|
observedSchedule: schedule,
|
|
35
35
|
observedAt: v.optional(v.number()),
|
|
36
36
|
};
|
|
37
|
+
export const expiryBalance = v.object({
|
|
38
|
+
currency: v.string(),
|
|
39
|
+
available: v.number(),
|
|
40
|
+
pending: v.number(),
|
|
41
|
+
});
|
|
42
|
+
export const holdingExpiry = v.object({
|
|
43
|
+
deadline: v.number(),
|
|
44
|
+
status: v.union(
|
|
45
|
+
v.literal("unverified"),
|
|
46
|
+
v.literal("balance_remaining"),
|
|
47
|
+
v.literal("claims_unresolved"),
|
|
48
|
+
v.literal("no_held_balance"),
|
|
49
|
+
),
|
|
50
|
+
observedAt: v.optional(v.number()),
|
|
51
|
+
balances: v.optional(v.array(expiryBalance)),
|
|
52
|
+
});
|
|
37
53
|
export const policyFields = {
|
|
38
54
|
...scopeFields,
|
|
39
55
|
revision: v.number(),
|
|
@@ -44,6 +60,7 @@ export const policyFields = {
|
|
|
44
60
|
country: v.string(),
|
|
45
61
|
previousSchedule: v.optional(schedule),
|
|
46
62
|
holdDeadline: v.optional(v.number()),
|
|
63
|
+
holdingExpiry: v.optional(holdingExpiry),
|
|
47
64
|
};
|
|
48
65
|
export const operationFields = {
|
|
49
66
|
...prepareFields,
|
|
@@ -77,6 +77,7 @@ export const begin = mutation({
|
|
|
77
77
|
hasOperation: false,
|
|
78
78
|
leaseUntil: undefined,
|
|
79
79
|
lastReason: "policy_inactive",
|
|
80
|
+
lastResultAt: now,
|
|
80
81
|
nextAttemptAt: nextPayoutAttempt(policy, now, args.cadenceMs),
|
|
81
82
|
});
|
|
82
83
|
return { status: "held" as const, reason: "policy_inactive" };
|
|
@@ -113,6 +114,7 @@ export const begin = mutation({
|
|
|
113
114
|
hasOperation: false,
|
|
114
115
|
leaseUntil: undefined,
|
|
115
116
|
lastReason: "affiliate_recovery_pending",
|
|
117
|
+
lastResultAt: now,
|
|
116
118
|
nextAttemptAt,
|
|
117
119
|
});
|
|
118
120
|
return {
|
|
@@ -189,6 +191,7 @@ export const finish = mutation({
|
|
|
189
191
|
job.cadenceMs,
|
|
190
192
|
),
|
|
191
193
|
lastReason: args.reason,
|
|
194
|
+
lastResultAt: Date.now(),
|
|
192
195
|
completedGeneration: args.generation,
|
|
193
196
|
completedOperationKey: args.operationKey,
|
|
194
197
|
});
|
|
@@ -13,6 +13,7 @@ export const fields = {
|
|
|
13
13
|
hasOperation: v.boolean(),
|
|
14
14
|
lastReason: v.optional(v.string()),
|
|
15
15
|
lastAttemptAt: v.optional(v.number()),
|
|
16
|
+
lastResultAt: v.optional(v.number()),
|
|
16
17
|
completedGeneration: v.optional(v.number()),
|
|
17
18
|
completedOperationKey: v.optional(v.string()),
|
|
18
19
|
};
|
|
@@ -24,3 +25,17 @@ export const jobDoc = v.object({
|
|
|
24
25
|
export const jobsTable = defineTable(fields)
|
|
25
26
|
.index("by_account_currency", ["accountId", "currency"])
|
|
26
27
|
.index("by_operation_pending", ["hasOperation", "nextAttemptAt"]);
|
|
28
|
+
|
|
29
|
+
export const attemptStatus = v.object({
|
|
30
|
+
currency: v.string(),
|
|
31
|
+
status: v.union(
|
|
32
|
+
v.literal("running"),
|
|
33
|
+
v.literal("reconciling"),
|
|
34
|
+
v.literal("rejected"),
|
|
35
|
+
v.literal("failed"),
|
|
36
|
+
v.literal("paid"),
|
|
37
|
+
v.literal("held"),
|
|
38
|
+
),
|
|
39
|
+
reason: v.string(),
|
|
40
|
+
at: v.optional(v.number()),
|
|
41
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import type { PayoutAttempt } from "../../client/connect/payoutWorker.js";
|
|
4
|
+
import {
|
|
5
|
+
payoutAttemptState,
|
|
6
|
+
type PayoutAttemptState,
|
|
7
|
+
} from "./payout-attempt-state.js";
|
|
8
|
+
export type { PayoutAttemptState } from "./payout-attempt-state.js";
|
|
9
|
+
export type PayoutAttemptStatusProps = {
|
|
10
|
+
attempt?: PayoutAttempt | null;
|
|
11
|
+
locale?: string;
|
|
12
|
+
timeZone?: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
children?: (state: PayoutAttemptState) => ReactNode;
|
|
15
|
+
};
|
|
16
|
+
/** Unstyled worker status. Result times are not bank arrival estimates. */
|
|
17
|
+
export function PayoutAttemptStatus({
|
|
18
|
+
attempt,
|
|
19
|
+
locale,
|
|
20
|
+
timeZone,
|
|
21
|
+
className,
|
|
22
|
+
children,
|
|
23
|
+
}: PayoutAttemptStatusProps) {
|
|
24
|
+
const state = payoutAttemptState(attempt, locale, timeZone);
|
|
25
|
+
if (!state) return null;
|
|
26
|
+
if (children) return <>{children(state)}</>;
|
|
27
|
+
return (
|
|
28
|
+
<aside
|
|
29
|
+
className={className}
|
|
30
|
+
data-better-stripe="payout-attempt"
|
|
31
|
+
data-tone={state.tone}
|
|
32
|
+
>
|
|
33
|
+
<h3>Latest automatic payout check</h3>
|
|
34
|
+
<p role="status" aria-label="Latest automatic payout attempt">
|
|
35
|
+
{state.message}
|
|
36
|
+
</p>
|
|
37
|
+
{state.formattedAt && (
|
|
38
|
+
<p>
|
|
39
|
+
{state.timeLabel}: {state.formattedAt}
|
|
40
|
+
</p>
|
|
41
|
+
)}
|
|
42
|
+
</aside>
|
|
43
|
+
);
|
|
44
|
+
}
|