@comfyorg/account-core 1.0.0-alpha.0 → 1.0.0-alpha.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/dist/core/billing/billingContracts.d.ts +29 -14
- package/dist/core/billing/billingErrorBody.d.ts +8 -4
- package/dist/core/billing/billingErrorBody.js +14 -4
- package/dist/core/billing/credentialedTransport.d.ts +23 -7
- package/dist/core/billing/credentialedTransport.js +72 -8
- package/dist/core/billing/events.d.ts +47 -0
- package/dist/core/billing/events.js +43 -0
- package/dist/core/billing/index.d.ts +16 -11
- package/dist/core/billing/index.js +8 -6
- package/dist/core/billing/operationLifecycle.d.ts +34 -1
- package/dist/core/billing/operationLifecycle.js +146 -67
- package/dist/core/billing/operationPointer.d.ts +27 -2
- package/dist/core/billing/operationPointer.js +36 -25
- package/dist/core/billing/operationPolicy.d.ts +44 -7
- package/dist/core/billing/operationPolicy.js +64 -3
- package/dist/core/billing/operationState.d.ts +225 -19
- package/dist/core/billing/operationState.js +93 -8
- package/dist/core/billing/paymentCopy.d.ts +6 -1
- package/dist/core/billing/paymentCopy.js +34 -1
- package/dist/core/billing/paymentProjection.d.ts +14 -2
- package/dist/core/billing/paymentProjection.js +37 -9
- package/dist/core/billing/scopedReader.d.ts +11 -5
- package/dist/core/billing/scopedReader.js +20 -9
- package/dist/core/billing/sharedRead.js +4 -2
- package/dist/core/billing/subscriptionCommands.d.ts +63 -9
- package/dist/core/billing/subscriptionCommands.js +46 -30
- package/dist/core/billing/topup.d.ts +26 -0
- package/dist/core/billing/topup.js +36 -2
- package/dist/core/billing/wireCents.d.ts +8 -0
- package/dist/core/billing/wireCents.js +8 -0
- package/dist/core/billing/workspaceInvites.d.ts +12 -0
- package/dist/core/billing/workspaceInvites.js +26 -0
- package/dist/core/identity.d.ts +6 -6
- package/dist/core/identity.js +4 -10
- package/dist/core/lazyIdentity.d.ts +31 -0
- package/dist/core/lazyIdentity.js +96 -0
- package/dist/core/requestAuth.d.ts +30 -0
- package/dist/core/requestAuth.js +41 -0
- package/dist/core/requestTimeout.d.ts +7 -0
- package/dist/core/requestTimeout.js +13 -0
- package/dist/core/session.d.ts +12 -18
- package/dist/core/session.js +16 -26
- package/dist/core/sessionContracts.d.ts +39 -2
- package/dist/core/sessionTokenMint.d.ts +35 -0
- package/dist/core/sessionTokenMint.js +222 -0
- package/dist/core/webSession.d.ts +27 -0
- package/dist/core/webSession.js +144 -0
- package/dist/core/webSessionFlag.d.ts +19 -0
- package/dist/core/webSessionFlag.js +61 -0
- package/dist/core/webSessionIdentity.d.ts +297 -0
- package/dist/core/webSessionIdentity.js +509 -0
- package/dist/firebase/configSource.d.ts +54 -0
- package/dist/firebase/configSource.js +111 -0
- package/dist/firebase/index.d.ts +71 -8
- package/dist/firebase/index.js +236 -23
- package/dist/firebase/popupWatch.d.ts +42 -0
- package/dist/firebase/popupWatch.js +140 -0
- package/dist/testing.d.ts +38 -0
- package/dist/testing.js +116 -0
- package/dist/web/crossTabRefresh.d.ts +4 -2
- package/dist/web/crossTabRefresh.js +13 -0
- package/dist/workspaceLink.d.ts +45 -0
- package/dist/workspaceLink.js +78 -0
- package/package.json +30 -6
|
@@ -3,9 +3,35 @@ const PROCESSING_REASONS = new Set([
|
|
|
3
3
|
'processing_error',
|
|
4
4
|
'generic'
|
|
5
5
|
]);
|
|
6
|
+
const RECOVERY_ACTIONS = {
|
|
7
|
+
retry: true,
|
|
8
|
+
replace_payment_method: true,
|
|
9
|
+
authenticate_payment: true,
|
|
10
|
+
contact_support: true
|
|
11
|
+
};
|
|
12
|
+
/** A recovery action this build cannot act on reads as none named. */
|
|
13
|
+
function knownRecoveryAction(action) {
|
|
14
|
+
return action !== undefined && isRecoveryAction(action) ? action : undefined;
|
|
15
|
+
}
|
|
16
|
+
function isRecoveryAction(action) {
|
|
17
|
+
return Object.hasOwn(RECOVERY_ACTIONS, action);
|
|
18
|
+
}
|
|
6
19
|
function stepForReason(reason) {
|
|
7
20
|
return PROCESSING_REASONS.has(reason) ? 'processing_error' : 'declined';
|
|
8
21
|
}
|
|
22
|
+
/** A completed challenge waits on the server, not on the customer. */
|
|
23
|
+
function awaitsVerification(state) {
|
|
24
|
+
const challengeOpen = state.challenge !== undefined && state.challenge.status !== 'completed';
|
|
25
|
+
return challengeOpen || state.actionUrl !== undefined;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Whether a pending payment's progress reads as a prompt rather than a
|
|
29
|
+
* spinner: only a hosted page the customer still has to visit asks them for
|
|
30
|
+
* anything. An in-page challenge is driven by the host itself.
|
|
31
|
+
*/
|
|
32
|
+
export function awaitsHostedAction(state) {
|
|
33
|
+
return state.actionUrl !== undefined;
|
|
34
|
+
}
|
|
9
35
|
function projectPending(state, hostStep) {
|
|
10
36
|
const base = { operationId: state.id, noChargeConfirmed: false };
|
|
11
37
|
if (hostStep === 'canceled')
|
|
@@ -13,17 +39,18 @@ function projectPending(state, hostStep) {
|
|
|
13
39
|
const reason = state.declineReason ??
|
|
14
40
|
(state.challenge?.status === 'failed' ? 'authentication_failed' : undefined);
|
|
15
41
|
if (reason !== undefined) {
|
|
42
|
+
const recoveryAction = knownRecoveryAction(state.recoveryAction);
|
|
16
43
|
return {
|
|
17
44
|
...base,
|
|
18
45
|
step: stepForReason(reason),
|
|
19
46
|
reasonKey: reason,
|
|
20
|
-
...(
|
|
21
|
-
? {}
|
|
22
|
-
: { recoveryAction: state.recoveryAction })
|
|
47
|
+
...(recoveryAction === undefined ? {} : { recoveryAction })
|
|
23
48
|
};
|
|
24
49
|
}
|
|
25
|
-
|
|
26
|
-
|
|
50
|
+
return {
|
|
51
|
+
...base,
|
|
52
|
+
step: awaitsVerification(state) ? 'verifying' : 'preview'
|
|
53
|
+
};
|
|
27
54
|
}
|
|
28
55
|
export function projectPaymentStep(operation, hostStep) {
|
|
29
56
|
if (operation === undefined) {
|
|
@@ -35,15 +62,16 @@ export function projectPaymentStep(operation, hostStep) {
|
|
|
35
62
|
return projectPending(operation, hostStep);
|
|
36
63
|
case 'succeeded':
|
|
37
64
|
return { ...base, step: 'success' };
|
|
38
|
-
case 'failed':
|
|
65
|
+
case 'failed': {
|
|
66
|
+
const recoveryAction = knownRecoveryAction(operation.recoveryAction) ??
|
|
67
|
+
(operation.retryable ? undefined : 'contact_support');
|
|
39
68
|
return {
|
|
40
69
|
...base,
|
|
41
70
|
step: stepForReason(operation.declineReason),
|
|
42
71
|
reasonKey: operation.declineReason,
|
|
43
|
-
...(
|
|
44
|
-
? {}
|
|
45
|
-
: { recoveryAction: operation.recoveryAction })
|
|
72
|
+
...(recoveryAction === undefined ? {} : { recoveryAction })
|
|
46
73
|
};
|
|
74
|
+
}
|
|
47
75
|
case 'reconciliation_needed':
|
|
48
76
|
return { ...base, step: 'processing_error', reasonKey: 'generic' };
|
|
49
77
|
case 'timed_out':
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* between reads, so only those live in the readers.
|
|
6
6
|
*
|
|
7
7
|
* What the skeleton owns is what a reader therefore cannot get wrong on its
|
|
8
|
-
* own: one shared request per scope, a caller's signal releasing
|
|
9
|
-
* caller, the publish fence that reports a late answer as
|
|
10
|
-
* the denial that drops a published snapshot unless it
|
|
11
|
-
* host reported.
|
|
8
|
+
* own: one shared request per scope and route, a caller's signal releasing
|
|
9
|
+
* only that caller, the publish fence that reports a late answer as
|
|
10
|
+
* `SUPERSEDED`, and the denial that drops a published snapshot unless it
|
|
11
|
+
* predates a change the host reported.
|
|
12
12
|
*/
|
|
13
13
|
import type { BillingResult, BillingTransport } from './billingContracts.js';
|
|
14
14
|
import type { BillingScope, BillingScopeSource } from './billingScope.js';
|
|
@@ -21,7 +21,13 @@ export interface ScopedReaderDefinition<TData, TSnapshot, TOptions extends Scope
|
|
|
21
21
|
readonly transport: BillingTransport;
|
|
22
22
|
/** Where the core learns which user, workspace, and role it runs as. */
|
|
23
23
|
readonly scopeSource: BillingScopeSource;
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* The route to request, as a function for a read whose query varies per
|
|
26
|
+
* call. The resolved route joins the scope in identifying an in-flight
|
|
27
|
+
* read, so two callers asking for different pages each get their own answer
|
|
28
|
+
* rather than the first one's.
|
|
29
|
+
*/
|
|
30
|
+
readonly route: string | ((options: TOptions | undefined) => string);
|
|
25
31
|
readonly parse: (body: unknown) => ParsedBillingBody<TData>;
|
|
26
32
|
/**
|
|
27
33
|
* The reader's own reading of a validated response. Returning a failure is
|
|
@@ -5,6 +5,7 @@ function publishAsRead(value) {
|
|
|
5
5
|
}
|
|
6
6
|
export function createScopedReader(definition) {
|
|
7
7
|
const { transport, scopeSource, route, parse, project, timeoutMs, cached, publish = publishAsRead } = definition;
|
|
8
|
+
const resolveRoute = typeof route === 'string' ? () => route : route;
|
|
8
9
|
let snapshot;
|
|
9
10
|
let inFlight;
|
|
10
11
|
const lifetime = { disposed: false };
|
|
@@ -12,11 +13,11 @@ export function createScopedReader(definition) {
|
|
|
12
13
|
snapshot = undefined;
|
|
13
14
|
inFlight = undefined;
|
|
14
15
|
});
|
|
15
|
-
async function request(scope, options) {
|
|
16
|
+
async function request(scope, options, resolvedRoute) {
|
|
16
17
|
const budget = timeoutMs?.(options);
|
|
17
18
|
const billingRequest = {
|
|
18
19
|
method: 'GET',
|
|
19
|
-
route,
|
|
20
|
+
route: resolvedRoute,
|
|
20
21
|
...(budget === undefined ? {} : { timeoutMs: budget })
|
|
21
22
|
};
|
|
22
23
|
const response = await readValidatedBillingResponse(transport, billingRequest, parse);
|
|
@@ -30,8 +31,8 @@ export function createScopedReader(definition) {
|
|
|
30
31
|
snapshot = published;
|
|
31
32
|
return { status: 'ok', value: published ?? value };
|
|
32
33
|
}
|
|
33
|
-
async function settle(context, pending, options) {
|
|
34
|
-
const result = await request(context.scope, options);
|
|
34
|
+
async function settle(context, pending, options, resolvedRoute) {
|
|
35
|
+
const result = await request(context.scope, options, resolvedRoute);
|
|
35
36
|
// The publish guard. Between issuing the request and settling it the host
|
|
36
37
|
// may have changed workspace or signed out, so an answer cached now would
|
|
37
38
|
// be attributed to whoever is signed in next, and a failure says nothing
|
|
@@ -60,10 +61,19 @@ export function createScopedReader(definition) {
|
|
|
60
61
|
const served = cached?.(snapshot, context.scope, options);
|
|
61
62
|
if (served !== undefined)
|
|
62
63
|
return { status: 'ok', value: served };
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
64
|
+
// Resolved once, then used both to issue the request and to identify it,
|
|
65
|
+
// so the answer a joining caller is served is the one its own route asked
|
|
66
|
+
// for.
|
|
67
|
+
const resolvedRoute = resolveRoute(options);
|
|
68
|
+
// A caller arriving mid-flight joins only a read of the same scope and
|
|
69
|
+
// route, rather than issuing a second identical request. A caller for a
|
|
70
|
+
// different scope starts its own, and the older one can no longer
|
|
71
|
+
// publish. A caller for a different route on the same scope also starts
|
|
72
|
+
// its own and takes the slot, so the displaced read still settles and
|
|
73
|
+
// publishes — the snapshot carries which page it is — but nobody can join
|
|
74
|
+
// it any more. One slot is enough for a feed read one page at a time.
|
|
75
|
+
if (matchesScopedRead(inFlight, context) &&
|
|
76
|
+
inFlight.route === resolvedRoute) {
|
|
67
77
|
return releaseOnAbort(inFlight.promise, options?.signal);
|
|
68
78
|
}
|
|
69
79
|
// No caller signal reaches the shared request: it is bounded by its own
|
|
@@ -72,7 +82,8 @@ export function createScopedReader(definition) {
|
|
|
72
82
|
const pending = { fenced: false };
|
|
73
83
|
const attempt = {
|
|
74
84
|
context,
|
|
75
|
-
|
|
85
|
+
route: resolvedRoute,
|
|
86
|
+
promise: settle(context, pending, options, resolvedRoute),
|
|
76
87
|
pending
|
|
77
88
|
};
|
|
78
89
|
inFlight = attempt;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { sameBillingScope } from './billingScope.js';
|
|
2
|
-
import { readBillingErrorCode } from './billingErrorBody.js';
|
|
2
|
+
import { readBillingErrorCode, readBillingErrorMessage } from './billingErrorBody.js';
|
|
3
3
|
import { codeForHttpStatus } from './httpStatus.js';
|
|
4
4
|
/**
|
|
5
5
|
* An abandoned wait is transient, the same bucket the transport reports an
|
|
@@ -22,11 +22,13 @@ export async function readValidatedBillingResponse(transport, request, parse) {
|
|
|
22
22
|
const { httpStatus, body } = response.value;
|
|
23
23
|
if (httpStatus < 200 || httpStatus >= 300) {
|
|
24
24
|
const serverCode = readBillingErrorCode(body);
|
|
25
|
+
const serverMessage = readBillingErrorMessage(body);
|
|
25
26
|
return {
|
|
26
27
|
status: 'error',
|
|
27
28
|
code: codeForHttpStatus(response.value),
|
|
28
29
|
httpStatus,
|
|
29
|
-
...(serverCode === undefined ? {} : { serverCode })
|
|
30
|
+
...(serverCode === undefined ? {} : { serverCode }),
|
|
31
|
+
...(serverMessage === undefined ? {} : { serverMessage })
|
|
30
32
|
};
|
|
31
33
|
}
|
|
32
34
|
const parsed = parse(body);
|
|
@@ -35,7 +35,9 @@ export type SubscriptionCommandCode =
|
|
|
35
35
|
/** The server wants the host to re-preview and resend with `confirm_reactivation`. */
|
|
36
36
|
| 'REACTIVATION_CONFIRMATION_REQUIRED' | 'NO_ACTIVE_SUBSCRIPTION'
|
|
37
37
|
/** The server asked for a hosted payment step but offered no page for it. */
|
|
38
|
-
| 'MISSING_PAYMENT_METHOD_URL'
|
|
38
|
+
| 'MISSING_PAYMENT_METHOD_URL'
|
|
39
|
+
/** The quote no longer matches what the server would charge; re-preview. */
|
|
40
|
+
| 'QUOTE_STALE';
|
|
39
41
|
export type SubscriptionCommandFailure = BillingFailure | {
|
|
40
42
|
readonly status: 'error';
|
|
41
43
|
readonly code: SubscriptionCommandCode;
|
|
@@ -89,6 +91,10 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
89
91
|
credits_next_period_cents: z.ZodNumber;
|
|
90
92
|
credits_today_cents: z.ZodNumber;
|
|
91
93
|
renewal_amount_cents: z.ZodOptional<z.ZodNumber>;
|
|
94
|
+
subtotal_cents: z.ZodOptional<z.ZodNumber>;
|
|
95
|
+
balance_applied_cents: z.ZodOptional<z.ZodNumber>;
|
|
96
|
+
proration_remaining_cents: z.ZodOptional<z.ZodNumber>;
|
|
97
|
+
proration_unused_cents: z.ZodOptional<z.ZodNumber>;
|
|
92
98
|
current_plan: z.ZodOptional<z.ZodObject<{
|
|
93
99
|
duration: z.ZodEnum<["MONTHLY", "ANNUAL"]>;
|
|
94
100
|
period_end: z.ZodOptional<z.ZodString>;
|
|
@@ -98,6 +104,9 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
98
104
|
} & {
|
|
99
105
|
credits_cents: z.ZodNumber;
|
|
100
106
|
price_cents: z.ZodNumber;
|
|
107
|
+
list_price_cents: z.ZodOptional<z.ZodNumber>;
|
|
108
|
+
monthly_list_price_cents: z.ZodOptional<z.ZodNumber>;
|
|
109
|
+
monthly_price_cents: z.ZodOptional<z.ZodNumber>;
|
|
101
110
|
seat_summary: z.ZodObject<{
|
|
102
111
|
seat_count: z.ZodNumber;
|
|
103
112
|
} & {
|
|
@@ -114,8 +123,8 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
114
123
|
}>;
|
|
115
124
|
}, "strip", z.ZodTypeAny, {
|
|
116
125
|
price_cents: number;
|
|
117
|
-
credits_cents: number;
|
|
118
126
|
duration: "MONTHLY" | "ANNUAL";
|
|
127
|
+
credits_cents: number;
|
|
119
128
|
seat_summary: {
|
|
120
129
|
seat_count: number;
|
|
121
130
|
total_cost_cents: number;
|
|
@@ -125,10 +134,13 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
125
134
|
tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
|
|
126
135
|
period_end?: string | undefined;
|
|
127
136
|
period_start?: string | undefined;
|
|
137
|
+
list_price_cents?: number | undefined;
|
|
138
|
+
monthly_list_price_cents?: number | undefined;
|
|
139
|
+
monthly_price_cents?: number | undefined;
|
|
128
140
|
}, {
|
|
129
141
|
price_cents: number;
|
|
130
|
-
credits_cents: number;
|
|
131
142
|
duration: "MONTHLY" | "ANNUAL";
|
|
143
|
+
credits_cents: number;
|
|
132
144
|
seat_summary: {
|
|
133
145
|
seat_count: number;
|
|
134
146
|
total_cost_cents: number;
|
|
@@ -138,6 +150,9 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
138
150
|
tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
|
|
139
151
|
period_end?: string | undefined;
|
|
140
152
|
period_start?: string | undefined;
|
|
153
|
+
list_price_cents?: number | undefined;
|
|
154
|
+
monthly_list_price_cents?: number | undefined;
|
|
155
|
+
monthly_price_cents?: number | undefined;
|
|
141
156
|
}>>;
|
|
142
157
|
new_plan: z.ZodObject<{
|
|
143
158
|
duration: z.ZodEnum<["MONTHLY", "ANNUAL"]>;
|
|
@@ -148,6 +163,9 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
148
163
|
} & {
|
|
149
164
|
credits_cents: z.ZodNumber;
|
|
150
165
|
price_cents: z.ZodNumber;
|
|
166
|
+
list_price_cents: z.ZodOptional<z.ZodNumber>;
|
|
167
|
+
monthly_list_price_cents: z.ZodOptional<z.ZodNumber>;
|
|
168
|
+
monthly_price_cents: z.ZodOptional<z.ZodNumber>;
|
|
151
169
|
seat_summary: z.ZodObject<{
|
|
152
170
|
seat_count: z.ZodNumber;
|
|
153
171
|
} & {
|
|
@@ -164,8 +182,8 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
164
182
|
}>;
|
|
165
183
|
}, "strip", z.ZodTypeAny, {
|
|
166
184
|
price_cents: number;
|
|
167
|
-
credits_cents: number;
|
|
168
185
|
duration: "MONTHLY" | "ANNUAL";
|
|
186
|
+
credits_cents: number;
|
|
169
187
|
seat_summary: {
|
|
170
188
|
seat_count: number;
|
|
171
189
|
total_cost_cents: number;
|
|
@@ -175,10 +193,13 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
175
193
|
tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
|
|
176
194
|
period_end?: string | undefined;
|
|
177
195
|
period_start?: string | undefined;
|
|
196
|
+
list_price_cents?: number | undefined;
|
|
197
|
+
monthly_list_price_cents?: number | undefined;
|
|
198
|
+
monthly_price_cents?: number | undefined;
|
|
178
199
|
}, {
|
|
179
200
|
price_cents: number;
|
|
180
|
-
credits_cents: number;
|
|
181
201
|
duration: "MONTHLY" | "ANNUAL";
|
|
202
|
+
credits_cents: number;
|
|
182
203
|
seat_summary: {
|
|
183
204
|
seat_count: number;
|
|
184
205
|
total_cost_cents: number;
|
|
@@ -188,23 +209,32 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
188
209
|
tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
|
|
189
210
|
period_end?: string | undefined;
|
|
190
211
|
period_start?: string | undefined;
|
|
212
|
+
list_price_cents?: number | undefined;
|
|
213
|
+
monthly_list_price_cents?: number | undefined;
|
|
214
|
+
monthly_price_cents?: number | undefined;
|
|
191
215
|
}>;
|
|
192
216
|
discounts: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
193
217
|
code: z.ZodString;
|
|
218
|
+
duration: z.ZodOptional<z.ZodEnum<["once", "repeating", "forever"]>>;
|
|
194
219
|
kind: z.ZodEnum<["plan", "promotion"]>;
|
|
195
220
|
name: z.ZodOptional<z.ZodString>;
|
|
196
221
|
} & {
|
|
197
222
|
amount_off_cents: z.ZodOptional<z.ZodNumber>;
|
|
223
|
+
duration_in_months: z.ZodOptional<z.ZodNumber>;
|
|
198
224
|
}, "strip", z.ZodTypeAny, {
|
|
199
225
|
code: string;
|
|
200
226
|
kind: "plan" | "promotion";
|
|
201
227
|
name?: string | undefined;
|
|
202
228
|
amount_off_cents?: number | undefined;
|
|
229
|
+
duration?: "once" | "repeating" | "forever" | undefined;
|
|
230
|
+
duration_in_months?: number | undefined;
|
|
203
231
|
}, {
|
|
204
232
|
code: string;
|
|
205
233
|
kind: "plan" | "promotion";
|
|
206
234
|
name?: string | undefined;
|
|
207
235
|
amount_off_cents?: number | undefined;
|
|
236
|
+
duration?: "once" | "repeating" | "forever" | undefined;
|
|
237
|
+
duration_in_months?: number | undefined;
|
|
208
238
|
}>, "many">>;
|
|
209
239
|
}, "strip", z.ZodTypeAny, {
|
|
210
240
|
effective_at: string;
|
|
@@ -216,8 +246,8 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
216
246
|
is_immediate: boolean;
|
|
217
247
|
new_plan: {
|
|
218
248
|
price_cents: number;
|
|
219
|
-
credits_cents: number;
|
|
220
249
|
duration: "MONTHLY" | "ANNUAL";
|
|
250
|
+
credits_cents: number;
|
|
221
251
|
seat_summary: {
|
|
222
252
|
seat_count: number;
|
|
223
253
|
total_cost_cents: number;
|
|
@@ -227,6 +257,9 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
227
257
|
tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
|
|
228
258
|
period_end?: string | undefined;
|
|
229
259
|
period_start?: string | undefined;
|
|
260
|
+
list_price_cents?: number | undefined;
|
|
261
|
+
monthly_list_price_cents?: number | undefined;
|
|
262
|
+
monthly_price_cents?: number | undefined;
|
|
230
263
|
};
|
|
231
264
|
transition_type: "new_subscription" | "upgrade" | "downgrade" | "duration_change";
|
|
232
265
|
currency?: string | undefined;
|
|
@@ -235,10 +268,11 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
235
268
|
quote_id?: string | undefined;
|
|
236
269
|
quote_version?: number | undefined;
|
|
237
270
|
amount_due_cents?: number | undefined;
|
|
271
|
+
balance_applied_cents?: number | undefined;
|
|
238
272
|
current_plan?: {
|
|
239
273
|
price_cents: number;
|
|
240
|
-
credits_cents: number;
|
|
241
274
|
duration: "MONTHLY" | "ANNUAL";
|
|
275
|
+
credits_cents: number;
|
|
242
276
|
seat_summary: {
|
|
243
277
|
seat_count: number;
|
|
244
278
|
total_cost_cents: number;
|
|
@@ -248,18 +282,26 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
248
282
|
tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
|
|
249
283
|
period_end?: string | undefined;
|
|
250
284
|
period_start?: string | undefined;
|
|
285
|
+
list_price_cents?: number | undefined;
|
|
286
|
+
monthly_list_price_cents?: number | undefined;
|
|
287
|
+
monthly_price_cents?: number | undefined;
|
|
251
288
|
} | undefined;
|
|
252
289
|
discounts?: {
|
|
253
290
|
code: string;
|
|
254
291
|
kind: "plan" | "promotion";
|
|
255
292
|
name?: string | undefined;
|
|
256
293
|
amount_off_cents?: number | undefined;
|
|
294
|
+
duration?: "once" | "repeating" | "forever" | undefined;
|
|
295
|
+
duration_in_months?: number | undefined;
|
|
257
296
|
}[] | undefined;
|
|
258
297
|
payment_method_configuration_id?: string | undefined;
|
|
298
|
+
proration_remaining_cents?: number | undefined;
|
|
299
|
+
proration_unused_cents?: number | undefined;
|
|
259
300
|
reason?: string | undefined;
|
|
260
301
|
renewal_amount_cents?: number | undefined;
|
|
261
302
|
renewal_at?: string | undefined;
|
|
262
303
|
requires_reactivation_confirmation?: boolean | undefined;
|
|
304
|
+
subtotal_cents?: number | undefined;
|
|
263
305
|
}, {
|
|
264
306
|
effective_at: string;
|
|
265
307
|
allowed: boolean;
|
|
@@ -270,8 +312,8 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
270
312
|
is_immediate: boolean;
|
|
271
313
|
new_plan: {
|
|
272
314
|
price_cents: number;
|
|
273
|
-
credits_cents: number;
|
|
274
315
|
duration: "MONTHLY" | "ANNUAL";
|
|
316
|
+
credits_cents: number;
|
|
275
317
|
seat_summary: {
|
|
276
318
|
seat_count: number;
|
|
277
319
|
total_cost_cents: number;
|
|
@@ -281,6 +323,9 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
281
323
|
tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
|
|
282
324
|
period_end?: string | undefined;
|
|
283
325
|
period_start?: string | undefined;
|
|
326
|
+
list_price_cents?: number | undefined;
|
|
327
|
+
monthly_list_price_cents?: number | undefined;
|
|
328
|
+
monthly_price_cents?: number | undefined;
|
|
284
329
|
};
|
|
285
330
|
transition_type: "new_subscription" | "upgrade" | "downgrade" | "duration_change";
|
|
286
331
|
currency?: string | undefined;
|
|
@@ -289,10 +334,11 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
289
334
|
quote_id?: string | undefined;
|
|
290
335
|
quote_version?: number | undefined;
|
|
291
336
|
amount_due_cents?: number | undefined;
|
|
337
|
+
balance_applied_cents?: number | undefined;
|
|
292
338
|
current_plan?: {
|
|
293
339
|
price_cents: number;
|
|
294
|
-
credits_cents: number;
|
|
295
340
|
duration: "MONTHLY" | "ANNUAL";
|
|
341
|
+
credits_cents: number;
|
|
296
342
|
seat_summary: {
|
|
297
343
|
seat_count: number;
|
|
298
344
|
total_cost_cents: number;
|
|
@@ -302,18 +348,26 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
302
348
|
tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
|
|
303
349
|
period_end?: string | undefined;
|
|
304
350
|
period_start?: string | undefined;
|
|
351
|
+
list_price_cents?: number | undefined;
|
|
352
|
+
monthly_list_price_cents?: number | undefined;
|
|
353
|
+
monthly_price_cents?: number | undefined;
|
|
305
354
|
} | undefined;
|
|
306
355
|
discounts?: {
|
|
307
356
|
code: string;
|
|
308
357
|
kind: "plan" | "promotion";
|
|
309
358
|
name?: string | undefined;
|
|
310
359
|
amount_off_cents?: number | undefined;
|
|
360
|
+
duration?: "once" | "repeating" | "forever" | undefined;
|
|
361
|
+
duration_in_months?: number | undefined;
|
|
311
362
|
}[] | undefined;
|
|
312
363
|
payment_method_configuration_id?: string | undefined;
|
|
364
|
+
proration_remaining_cents?: number | undefined;
|
|
365
|
+
proration_unused_cents?: number | undefined;
|
|
313
366
|
reason?: string | undefined;
|
|
314
367
|
renewal_amount_cents?: number | undefined;
|
|
315
368
|
renewal_at?: string | undefined;
|
|
316
369
|
requires_reactivation_confirmation?: boolean | undefined;
|
|
370
|
+
subtotal_cents?: number | undefined;
|
|
317
371
|
}>;
|
|
318
372
|
/**
|
|
319
373
|
* The quote the server returns for a plan change, in the generated field names.
|
|
@@ -16,6 +16,7 @@ import { z } from 'zod';
|
|
|
16
16
|
import { matchesServerCode } from './billingContracts.js';
|
|
17
17
|
import { validateActionUrl } from './operationState.js';
|
|
18
18
|
import { readValidatedBillingResponse } from './sharedRead.js';
|
|
19
|
+
import { wireCents } from './wireCents.js';
|
|
19
20
|
export const SUBSCRIBE_ROUTE = '/billing/subscribe';
|
|
20
21
|
export const RESUBSCRIBE_ROUTE = '/billing/subscription/resubscribe';
|
|
21
22
|
export const CANCEL_SUBSCRIPTION_ROUTE = '/billing/subscription/cancel';
|
|
@@ -26,35 +27,37 @@ const NO_ACTIVE_SUBSCRIPTION_SERVER_CODE = 'NO_ACTIVE_SUBSCRIPTION';
|
|
|
26
27
|
const REACTIVATION_CONFIRMATION_REQUIRED_SERVER_CODE = 'REACTIVATION_CONFIRMATION_REQUIRED';
|
|
27
28
|
const NOT_SCHEDULED_FOR_CANCELLATION_SERVER_CODE = 'NOT_SCHEDULED_FOR_CANCELLATION';
|
|
28
29
|
const ALREADY_CANCELED_SERVER_CODE = 'ALREADY_CANCELED';
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
* add to a price or hand to a currency formatter — and the generated *type*
|
|
32
|
-
* for the same field is a `number`. Money on this route is bounded to cents
|
|
33
|
-
* well inside the JavaScript-safe range, so the cents are read as numbers, the
|
|
34
|
-
* way `capabilities` reads `revision` — as whole units of currency that
|
|
35
|
-
* survive arithmetic, since these amounts are displayed as prices and
|
|
36
|
-
* confirmed as charges.
|
|
37
|
-
*/
|
|
38
|
-
const cents = z.number().int().safe();
|
|
30
|
+
const SUBSCRIPTION_QUOTE_STALE_SERVER_CODE = 'SUBSCRIPTION_QUOTE_STALE';
|
|
31
|
+
const SUBSCRIPTION_CHANGE_IN_PROGRESS_SERVER_CODE = 'SUBSCRIPTION_CHANGE_IN_PROGRESS';
|
|
39
32
|
const PlanInfoSchema = zPreviewPlanInfo.extend({
|
|
40
|
-
credits_cents:
|
|
41
|
-
price_cents:
|
|
33
|
+
credits_cents: wireCents,
|
|
34
|
+
price_cents: wireCents,
|
|
35
|
+
list_price_cents: wireCents.optional(),
|
|
36
|
+
monthly_list_price_cents: wireCents.optional(),
|
|
37
|
+
monthly_price_cents: wireCents.optional(),
|
|
42
38
|
seat_summary: zPreviewPlanInfo.shape.seat_summary.extend({
|
|
43
|
-
total_cost_cents:
|
|
44
|
-
total_credits_cents:
|
|
39
|
+
total_cost_cents: wireCents,
|
|
40
|
+
total_credits_cents: wireCents
|
|
45
41
|
})
|
|
46
42
|
});
|
|
47
43
|
const PreviewSchema = zPreviewSubscribeResponse.extend({
|
|
48
|
-
amount_due_cents:
|
|
49
|
-
cost_next_period_cents:
|
|
50
|
-
cost_today_cents:
|
|
51
|
-
credits_next_period_cents:
|
|
52
|
-
credits_today_cents:
|
|
53
|
-
renewal_amount_cents:
|
|
44
|
+
amount_due_cents: wireCents.optional(),
|
|
45
|
+
cost_next_period_cents: wireCents,
|
|
46
|
+
cost_today_cents: wireCents,
|
|
47
|
+
credits_next_period_cents: wireCents,
|
|
48
|
+
credits_today_cents: wireCents,
|
|
49
|
+
renewal_amount_cents: wireCents.optional(),
|
|
50
|
+
subtotal_cents: wireCents.optional(),
|
|
51
|
+
balance_applied_cents: wireCents.optional(),
|
|
52
|
+
proration_remaining_cents: wireCents.optional(),
|
|
53
|
+
proration_unused_cents: wireCents.optional(),
|
|
54
54
|
current_plan: PlanInfoSchema.optional(),
|
|
55
55
|
new_plan: PlanInfoSchema,
|
|
56
56
|
discounts: z
|
|
57
|
-
.array(zSubscriptionDiscount.extend({
|
|
57
|
+
.array(zSubscriptionDiscount.extend({
|
|
58
|
+
amount_off_cents: wireCents.optional(),
|
|
59
|
+
duration_in_months: wireCents.optional()
|
|
60
|
+
}))
|
|
58
61
|
.optional()
|
|
59
62
|
});
|
|
60
63
|
const ALREADY_HELD = {
|
|
@@ -65,23 +68,33 @@ function coded(code) {
|
|
|
65
68
|
return { status: 'error', code };
|
|
66
69
|
}
|
|
67
70
|
/**
|
|
68
|
-
* A server code
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
+
* A server code is trusted as state only from a 4xx: a 5xx echoing the code
|
|
72
|
+
* is an upstream failure that happens to carry it, and the state it names
|
|
73
|
+
* cannot be assumed to hold.
|
|
71
74
|
*/
|
|
72
|
-
function
|
|
75
|
+
function refusedWith(failure, serverCode) {
|
|
73
76
|
return (matchesServerCode(failure, serverCode) &&
|
|
74
77
|
failure.httpStatus !== undefined &&
|
|
75
78
|
failure.httpStatus >= 400 &&
|
|
76
79
|
failure.httpStatus < 500);
|
|
77
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* The server's refusal because another subscription operation is still open
|
|
83
|
+
* is the same answer the lifecycle gives when it sees that operation first.
|
|
84
|
+
*/
|
|
85
|
+
function refusedWhilePending(failure) {
|
|
86
|
+
return refusedWith(failure, SUBSCRIPTION_CHANGE_IN_PROGRESS_SERVER_CODE)
|
|
87
|
+
? { ...failure, code: 'OPERATION_ALREADY_PENDING' }
|
|
88
|
+
: failure;
|
|
89
|
+
}
|
|
78
90
|
function mapServerCode(failure, alreadyHeldCode) {
|
|
79
|
-
|
|
91
|
+
// A server code the caller's request already satisfies is a success.
|
|
92
|
+
if (refusedWith(failure, alreadyHeldCode)) {
|
|
80
93
|
return { status: 'already_held' };
|
|
81
94
|
}
|
|
82
95
|
return matchesServerCode(failure, NO_ACTIVE_SUBSCRIPTION_SERVER_CODE)
|
|
83
96
|
? coded('NO_ACTIVE_SUBSCRIPTION')
|
|
84
|
-
: failure;
|
|
97
|
+
: refusedWhilePending(failure);
|
|
85
98
|
}
|
|
86
99
|
function dropEmpty(value) {
|
|
87
100
|
return value === '' ? undefined : value;
|
|
@@ -159,9 +172,12 @@ export function createBillingCommands(options) {
|
|
|
159
172
|
const key = idempotencyKey();
|
|
160
173
|
const response = await post(SUBSCRIBE_ROUTE, { ...input, idempotency_key: key }, (body) => zSubscribeResponse.safeParse(body), key);
|
|
161
174
|
if (response.status === 'error') {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
175
|
+
if (matchesServerCode(response, REACTIVATION_CONFIRMATION_REQUIRED_SERVER_CODE)) {
|
|
176
|
+
return coded('REACTIVATION_CONFIRMATION_REQUIRED');
|
|
177
|
+
}
|
|
178
|
+
return refusedWith(response, SUBSCRIPTION_QUOTE_STALE_SERVER_CODE)
|
|
179
|
+
? coded('QUOTE_STALE')
|
|
180
|
+
: refusedWhilePending(response);
|
|
165
181
|
}
|
|
166
182
|
const { billing_op_id, status, payment_method_url } = response.value.data;
|
|
167
183
|
if (status !== 'needs_payment_method') {
|
|
@@ -11,6 +11,26 @@ export declare const TOPUP_ROUTE = "/billing/topup";
|
|
|
11
11
|
* through a webhook, so completion is the host's return plus a balance watch.
|
|
12
12
|
*/
|
|
13
13
|
export declare const TOPUP_CHECKOUT_ROUTE = "/billing/topup/checkout";
|
|
14
|
+
/** A read the backend shapes as a POST: nothing is charged or reserved. */
|
|
15
|
+
export declare const TOPUP_QUOTE_ROUTE = "/billing/topup/quote";
|
|
16
|
+
/**
|
|
17
|
+
* What the server says an amount buys. `credits` is the server's display
|
|
18
|
+
* count; `expiresAt` is when the grant would lapse, which the server may
|
|
19
|
+
* move, so a host never states the expiry policy itself.
|
|
20
|
+
*/
|
|
21
|
+
export interface TopupQuote {
|
|
22
|
+
readonly amountCents: number;
|
|
23
|
+
readonly credits: number;
|
|
24
|
+
readonly expiresAt: string;
|
|
25
|
+
}
|
|
26
|
+
export type TopupQuoteResult = {
|
|
27
|
+
readonly status: 'ok';
|
|
28
|
+
readonly value: TopupQuote;
|
|
29
|
+
} | BillingFailure | TopupNotAvailable | TopupInvalidAmount;
|
|
30
|
+
export interface QuoteTopupInput {
|
|
31
|
+
readonly amountCents: number;
|
|
32
|
+
readonly signal?: AbortSignal;
|
|
33
|
+
}
|
|
14
34
|
type OperationIn<P extends BillingOperationState['phase']> = Extract<BillingOperationState, {
|
|
15
35
|
readonly phase: P;
|
|
16
36
|
}>;
|
|
@@ -101,6 +121,12 @@ export interface TopupCommandOptions {
|
|
|
101
121
|
readonly idempotencyKey?: () => string;
|
|
102
122
|
}
|
|
103
123
|
export interface TopupCommand {
|
|
124
|
+
/**
|
|
125
|
+
* Quotes an amount without starting anything. It does not check the plan
|
|
126
|
+
* tier or the payment method, so a quote does not promise the top-up will
|
|
127
|
+
* go through.
|
|
128
|
+
*/
|
|
129
|
+
quoteTopup: (input: QuoteTopupInput) => Promise<TopupQuoteResult>;
|
|
104
130
|
createTopupCheckout: (input: CreateTopupCheckoutInput) => Promise<TopupResult>;
|
|
105
131
|
/**
|
|
106
132
|
* The route Workshop and Platform ship on today. Unlike
|