@comfyorg/account-core 1.0.0-alpha.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/LICENSE +21 -0
- package/dist/core/billing/balanceWatch.d.ts +35 -0
- package/dist/core/billing/balanceWatch.js +83 -0
- package/dist/core/billing/billingContracts.d.ts +123 -0
- package/dist/core/billing/billingContracts.js +11 -0
- package/dist/core/billing/billingErrorBody.d.ts +8 -0
- package/dist/core/billing/billingErrorBody.js +11 -0
- package/dist/core/billing/billingScope.d.ts +39 -0
- package/dist/core/billing/billingScope.js +45 -0
- package/dist/core/billing/capabilities.d.ts +70 -0
- package/dist/core/billing/capabilities.js +137 -0
- package/dist/core/billing/capabilityDenials.d.ts +26 -0
- package/dist/core/billing/capabilityDenials.js +43 -0
- package/dist/core/billing/challengeDriver.d.ts +22 -0
- package/dist/core/billing/challengeDriver.js +38 -0
- package/dist/core/billing/credentialedTransport.d.ts +35 -0
- package/dist/core/billing/credentialedTransport.js +53 -0
- package/dist/core/billing/credits.d.ts +43 -0
- package/dist/core/billing/credits.js +29 -0
- package/dist/core/billing/httpStatus.d.ts +7 -0
- package/dist/core/billing/httpStatus.js +25 -0
- package/dist/core/billing/index.d.ts +52 -0
- package/dist/core/billing/index.js +23 -0
- package/dist/core/billing/operationLifecycle.d.ts +77 -0
- package/dist/core/billing/operationLifecycle.js +547 -0
- package/dist/core/billing/operationPointer.d.ts +48 -0
- package/dist/core/billing/operationPointer.js +89 -0
- package/dist/core/billing/operationPolicy.d.ts +26 -0
- package/dist/core/billing/operationPolicy.js +51 -0
- package/dist/core/billing/operationState.d.ts +128 -0
- package/dist/core/billing/operationState.js +187 -0
- package/dist/core/billing/paymentCopy.d.ts +18 -0
- package/dist/core/billing/paymentCopy.js +47 -0
- package/dist/core/billing/paymentMethods.d.ts +40 -0
- package/dist/core/billing/paymentMethods.js +37 -0
- package/dist/core/billing/paymentProjection.d.ts +34 -0
- package/dist/core/billing/paymentProjection.js +56 -0
- package/dist/core/billing/plans.d.ts +44 -0
- package/dist/core/billing/plans.js +29 -0
- package/dist/core/billing/presentation.d.ts +20 -0
- package/dist/core/billing/presentation.js +10 -0
- package/dist/core/billing/scopedReader.d.ts +61 -0
- package/dist/core/billing/scopedReader.js +108 -0
- package/dist/core/billing/sharedRead.d.ts +33 -0
- package/dist/core/billing/sharedRead.js +59 -0
- package/dist/core/billing/status.d.ts +27 -0
- package/dist/core/billing/status.js +21 -0
- package/dist/core/billing/subscriptionCommands.d.ts +367 -0
- package/dist/core/billing/subscriptionCommands.js +263 -0
- package/dist/core/billing/topup.d.ts +113 -0
- package/dist/core/billing/topup.js +209 -0
- package/dist/core/billing/transport.d.ts +14 -0
- package/dist/core/billing/transport.js +106 -0
- package/dist/core/billing/transportExchange.d.ts +24 -0
- package/dist/core/billing/transportExchange.js +79 -0
- package/dist/core/boundedOperation.d.ts +16 -0
- package/dist/core/boundedOperation.js +12 -0
- package/dist/core/credentialCache.d.ts +41 -0
- package/dist/core/credentialCache.js +97 -0
- package/dist/core/customerRecovery.d.ts +39 -0
- package/dist/core/customerRecovery.js +79 -0
- package/dist/core/exchange.d.ts +50 -0
- package/dist/core/exchange.js +137 -0
- package/dist/core/identity.d.ts +21 -0
- package/dist/core/identity.js +16 -0
- package/dist/core/mintCoordinator.d.ts +18 -0
- package/dist/core/mintCoordinator.js +31 -0
- package/dist/core/refreshScheduler.d.ts +46 -0
- package/dist/core/refreshScheduler.js +262 -0
- package/dist/core/session.d.ts +146 -0
- package/dist/core/session.js +312 -0
- package/dist/core/sessionContracts.d.ts +135 -0
- package/dist/core/sessionContracts.js +9 -0
- package/dist/core/sessionState.d.ts +118 -0
- package/dist/core/sessionState.js +235 -0
- package/dist/firebase/index.d.ts +53 -0
- package/dist/firebase/index.js +74 -0
- package/dist/firebaseAuthError.d.ts +52 -0
- package/dist/firebaseAuthError.js +88 -0
- package/dist/loadExternalScript.d.ts +7 -0
- package/dist/loadExternalScript.js +88 -0
- package/dist/provisioning.d.ts +53 -0
- package/dist/provisioning.js +76 -0
- package/dist/redirect.d.ts +13 -0
- package/dist/redirect.js +38 -0
- package/dist/signInSchemas.d.ts +88 -0
- package/dist/signInSchemas.js +71 -0
- package/dist/telemetry.d.ts +39 -0
- package/dist/telemetry.js +26 -0
- package/dist/testing.d.ts +7 -0
- package/dist/testing.js +3 -0
- package/dist/turnstile.d.ts +14 -0
- package/dist/turnstile.js +17 -0
- package/dist/turnstileScript.d.ts +18 -0
- package/dist/turnstileScript.js +6 -0
- package/dist/web/crossTabRefresh.d.ts +18 -0
- package/dist/web/crossTabRefresh.js +121 -0
- package/dist/webviewDetection.d.ts +5 -0
- package/dist/webviewDetection.js +62 -0
- package/package.json +114 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The subscription commands on the shared operation lifecycle: subscribe,
|
|
3
|
+
* resubscribe, cancel, and the payment portal. Each one encodes its request
|
|
4
|
+
* against the generated contract, issues it through `lifecycle.begin`, and
|
|
5
|
+
* waits for the settled operation; polling, routing, recovery, and telemetry
|
|
6
|
+
* stay in the lifecycle. `previewSubscribe` sits beside them without a
|
|
7
|
+
* lifecycle: it only quotes a change.
|
|
8
|
+
*
|
|
9
|
+
* No command decides for the server whether a transition is allowed
|
|
10
|
+
* (`../../../docs/billing-command-eligibility.md`). Server codes are matched
|
|
11
|
+
* against the closed set named here; a code outside it reaches the caller only
|
|
12
|
+
* as the transport's coded failure.
|
|
13
|
+
*/
|
|
14
|
+
import { zSubscribeRequest, zSubscribeResponse } from '@comfyorg/ingest-types/zod';
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
import type { BillingFailure, BillingTransport } from './billingContracts.js';
|
|
17
|
+
import type { CapabilitiesReader } from './capabilities.js';
|
|
18
|
+
import type { CreditsReader } from './credits.js';
|
|
19
|
+
import type { BillingOperationLifecycle } from './operationLifecycle.js';
|
|
20
|
+
import type { BillingOperationState, PendingBillingOperation } from './operationState.js';
|
|
21
|
+
import type { BillingStatusReader } from './status.js';
|
|
22
|
+
export declare const SUBSCRIBE_ROUTE = "/billing/subscribe";
|
|
23
|
+
export declare const RESUBSCRIBE_ROUTE = "/billing/subscription/resubscribe";
|
|
24
|
+
export declare const CANCEL_SUBSCRIPTION_ROUTE = "/billing/subscription/cancel";
|
|
25
|
+
export declare const PAYMENT_PORTAL_ROUTE = "/billing/payment-portal";
|
|
26
|
+
export declare const PREVIEW_SUBSCRIBE_ROUTE = "/billing/preview-subscribe";
|
|
27
|
+
export type SubscribeInput = z.infer<typeof zSubscribeRequest>;
|
|
28
|
+
export type TerminalBillingOperation = Exclude<BillingOperationState, PendingBillingOperation>;
|
|
29
|
+
/** Outcomes a subscription command adds to the shared `BillingErrorCode` set. */
|
|
30
|
+
export type SubscriptionCommandCode =
|
|
31
|
+
/** The input fails the generated request contract; nothing was sent. */
|
|
32
|
+
'INVALID_REQUEST'
|
|
33
|
+
/** `confirmation_token` and `saved_payment_method_id` were both given. */
|
|
34
|
+
| 'CONFLICTING_PAYMENT_METHOD'
|
|
35
|
+
/** The server wants the host to re-preview and resend with `confirm_reactivation`. */
|
|
36
|
+
| 'REACTIVATION_CONFIRMATION_REQUIRED' | 'NO_ACTIVE_SUBSCRIPTION'
|
|
37
|
+
/** The server asked for a hosted payment step but offered no page for it. */
|
|
38
|
+
| 'MISSING_PAYMENT_METHOD_URL';
|
|
39
|
+
export type SubscriptionCommandFailure = BillingFailure | {
|
|
40
|
+
readonly status: 'error';
|
|
41
|
+
readonly code: SubscriptionCommandCode;
|
|
42
|
+
};
|
|
43
|
+
/** What the subscribe route answered before the lifecycle drove the operation on. */
|
|
44
|
+
type SubscribeIssuedStatus = z.infer<typeof zSubscribeResponse>['status'];
|
|
45
|
+
export interface SubscriptionCommandOutcome {
|
|
46
|
+
readonly phase: TerminalBillingOperation['phase'];
|
|
47
|
+
/** Absent when the requested state already held and nothing was issued. */
|
|
48
|
+
readonly operation?: TerminalBillingOperation;
|
|
49
|
+
/**
|
|
50
|
+
* The `status` the subscribe response carried, before the lifecycle settled
|
|
51
|
+
* the operation: `subscribed` is a plan the server activated on the spot,
|
|
52
|
+
* the other two are payment steps it could not complete without the
|
|
53
|
+
* customer. Absent for every other command, and whenever this call read no
|
|
54
|
+
* subscribe response at all: `begin` adopted an operation the server was
|
|
55
|
+
* already settling, or joined one another `subscription` command had in
|
|
56
|
+
* flight. Both land on `requiredPayment: true`, the side an operation the
|
|
57
|
+
* server is mid-payment on belongs to.
|
|
58
|
+
*/
|
|
59
|
+
readonly issuedStatus?: SubscribeIssuedStatus;
|
|
60
|
+
}
|
|
61
|
+
export type SubscriptionCommandResult = {
|
|
62
|
+
readonly status: 'ok';
|
|
63
|
+
readonly value: SubscriptionCommandOutcome;
|
|
64
|
+
} | SubscriptionCommandFailure;
|
|
65
|
+
export type PaymentPortalResult = {
|
|
66
|
+
readonly status: 'ok';
|
|
67
|
+
readonly value: {
|
|
68
|
+
readonly url: string;
|
|
69
|
+
};
|
|
70
|
+
} | BillingFailure;
|
|
71
|
+
declare const PreviewSchema: z.ZodObject<{
|
|
72
|
+
allowed: z.ZodBoolean;
|
|
73
|
+
currency: z.ZodOptional<z.ZodString>;
|
|
74
|
+
effective_at: z.ZodString;
|
|
75
|
+
is_immediate: z.ZodBoolean;
|
|
76
|
+
payment_method_configuration_id: z.ZodOptional<z.ZodString>;
|
|
77
|
+
promotion_code: z.ZodOptional<z.ZodString>;
|
|
78
|
+
proration_at: z.ZodOptional<z.ZodString>;
|
|
79
|
+
quote_id: z.ZodOptional<z.ZodString>;
|
|
80
|
+
quote_version: z.ZodOptional<z.ZodNumber>;
|
|
81
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
82
|
+
renewal_at: z.ZodOptional<z.ZodString>;
|
|
83
|
+
requires_reactivation_confirmation: z.ZodOptional<z.ZodBoolean>;
|
|
84
|
+
transition_type: z.ZodEnum<["new_subscription", "upgrade", "downgrade", "duration_change"]>;
|
|
85
|
+
} & {
|
|
86
|
+
amount_due_cents: z.ZodOptional<z.ZodNumber>;
|
|
87
|
+
cost_next_period_cents: z.ZodNumber;
|
|
88
|
+
cost_today_cents: z.ZodNumber;
|
|
89
|
+
credits_next_period_cents: z.ZodNumber;
|
|
90
|
+
credits_today_cents: z.ZodNumber;
|
|
91
|
+
renewal_amount_cents: z.ZodOptional<z.ZodNumber>;
|
|
92
|
+
current_plan: z.ZodOptional<z.ZodObject<{
|
|
93
|
+
duration: z.ZodEnum<["MONTHLY", "ANNUAL"]>;
|
|
94
|
+
period_end: z.ZodOptional<z.ZodString>;
|
|
95
|
+
period_start: z.ZodOptional<z.ZodString>;
|
|
96
|
+
slug: z.ZodString;
|
|
97
|
+
tier: z.ZodEnum<["FREE", "STANDARD", "CREATOR", "PRO", "FOUNDERS_EDITION", "TEAM", "ENTERPRISE"]>;
|
|
98
|
+
} & {
|
|
99
|
+
credits_cents: z.ZodNumber;
|
|
100
|
+
price_cents: z.ZodNumber;
|
|
101
|
+
seat_summary: z.ZodObject<{
|
|
102
|
+
seat_count: z.ZodNumber;
|
|
103
|
+
} & {
|
|
104
|
+
total_cost_cents: z.ZodNumber;
|
|
105
|
+
total_credits_cents: z.ZodNumber;
|
|
106
|
+
}, "strip", z.ZodTypeAny, {
|
|
107
|
+
seat_count: number;
|
|
108
|
+
total_cost_cents: number;
|
|
109
|
+
total_credits_cents: number;
|
|
110
|
+
}, {
|
|
111
|
+
seat_count: number;
|
|
112
|
+
total_cost_cents: number;
|
|
113
|
+
total_credits_cents: number;
|
|
114
|
+
}>;
|
|
115
|
+
}, "strip", z.ZodTypeAny, {
|
|
116
|
+
price_cents: number;
|
|
117
|
+
credits_cents: number;
|
|
118
|
+
duration: "MONTHLY" | "ANNUAL";
|
|
119
|
+
seat_summary: {
|
|
120
|
+
seat_count: number;
|
|
121
|
+
total_cost_cents: number;
|
|
122
|
+
total_credits_cents: number;
|
|
123
|
+
};
|
|
124
|
+
slug: string;
|
|
125
|
+
tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
|
|
126
|
+
period_end?: string | undefined;
|
|
127
|
+
period_start?: string | undefined;
|
|
128
|
+
}, {
|
|
129
|
+
price_cents: number;
|
|
130
|
+
credits_cents: number;
|
|
131
|
+
duration: "MONTHLY" | "ANNUAL";
|
|
132
|
+
seat_summary: {
|
|
133
|
+
seat_count: number;
|
|
134
|
+
total_cost_cents: number;
|
|
135
|
+
total_credits_cents: number;
|
|
136
|
+
};
|
|
137
|
+
slug: string;
|
|
138
|
+
tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
|
|
139
|
+
period_end?: string | undefined;
|
|
140
|
+
period_start?: string | undefined;
|
|
141
|
+
}>>;
|
|
142
|
+
new_plan: z.ZodObject<{
|
|
143
|
+
duration: z.ZodEnum<["MONTHLY", "ANNUAL"]>;
|
|
144
|
+
period_end: z.ZodOptional<z.ZodString>;
|
|
145
|
+
period_start: z.ZodOptional<z.ZodString>;
|
|
146
|
+
slug: z.ZodString;
|
|
147
|
+
tier: z.ZodEnum<["FREE", "STANDARD", "CREATOR", "PRO", "FOUNDERS_EDITION", "TEAM", "ENTERPRISE"]>;
|
|
148
|
+
} & {
|
|
149
|
+
credits_cents: z.ZodNumber;
|
|
150
|
+
price_cents: z.ZodNumber;
|
|
151
|
+
seat_summary: z.ZodObject<{
|
|
152
|
+
seat_count: z.ZodNumber;
|
|
153
|
+
} & {
|
|
154
|
+
total_cost_cents: z.ZodNumber;
|
|
155
|
+
total_credits_cents: z.ZodNumber;
|
|
156
|
+
}, "strip", z.ZodTypeAny, {
|
|
157
|
+
seat_count: number;
|
|
158
|
+
total_cost_cents: number;
|
|
159
|
+
total_credits_cents: number;
|
|
160
|
+
}, {
|
|
161
|
+
seat_count: number;
|
|
162
|
+
total_cost_cents: number;
|
|
163
|
+
total_credits_cents: number;
|
|
164
|
+
}>;
|
|
165
|
+
}, "strip", z.ZodTypeAny, {
|
|
166
|
+
price_cents: number;
|
|
167
|
+
credits_cents: number;
|
|
168
|
+
duration: "MONTHLY" | "ANNUAL";
|
|
169
|
+
seat_summary: {
|
|
170
|
+
seat_count: number;
|
|
171
|
+
total_cost_cents: number;
|
|
172
|
+
total_credits_cents: number;
|
|
173
|
+
};
|
|
174
|
+
slug: string;
|
|
175
|
+
tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
|
|
176
|
+
period_end?: string | undefined;
|
|
177
|
+
period_start?: string | undefined;
|
|
178
|
+
}, {
|
|
179
|
+
price_cents: number;
|
|
180
|
+
credits_cents: number;
|
|
181
|
+
duration: "MONTHLY" | "ANNUAL";
|
|
182
|
+
seat_summary: {
|
|
183
|
+
seat_count: number;
|
|
184
|
+
total_cost_cents: number;
|
|
185
|
+
total_credits_cents: number;
|
|
186
|
+
};
|
|
187
|
+
slug: string;
|
|
188
|
+
tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
|
|
189
|
+
period_end?: string | undefined;
|
|
190
|
+
period_start?: string | undefined;
|
|
191
|
+
}>;
|
|
192
|
+
discounts: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
193
|
+
code: z.ZodString;
|
|
194
|
+
kind: z.ZodEnum<["plan", "promotion"]>;
|
|
195
|
+
name: z.ZodOptional<z.ZodString>;
|
|
196
|
+
} & {
|
|
197
|
+
amount_off_cents: z.ZodOptional<z.ZodNumber>;
|
|
198
|
+
}, "strip", z.ZodTypeAny, {
|
|
199
|
+
code: string;
|
|
200
|
+
kind: "plan" | "promotion";
|
|
201
|
+
name?: string | undefined;
|
|
202
|
+
amount_off_cents?: number | undefined;
|
|
203
|
+
}, {
|
|
204
|
+
code: string;
|
|
205
|
+
kind: "plan" | "promotion";
|
|
206
|
+
name?: string | undefined;
|
|
207
|
+
amount_off_cents?: number | undefined;
|
|
208
|
+
}>, "many">>;
|
|
209
|
+
}, "strip", z.ZodTypeAny, {
|
|
210
|
+
effective_at: string;
|
|
211
|
+
allowed: boolean;
|
|
212
|
+
cost_next_period_cents: number;
|
|
213
|
+
cost_today_cents: number;
|
|
214
|
+
credits_next_period_cents: number;
|
|
215
|
+
credits_today_cents: number;
|
|
216
|
+
is_immediate: boolean;
|
|
217
|
+
new_plan: {
|
|
218
|
+
price_cents: number;
|
|
219
|
+
credits_cents: number;
|
|
220
|
+
duration: "MONTHLY" | "ANNUAL";
|
|
221
|
+
seat_summary: {
|
|
222
|
+
seat_count: number;
|
|
223
|
+
total_cost_cents: number;
|
|
224
|
+
total_credits_cents: number;
|
|
225
|
+
};
|
|
226
|
+
slug: string;
|
|
227
|
+
tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
|
|
228
|
+
period_end?: string | undefined;
|
|
229
|
+
period_start?: string | undefined;
|
|
230
|
+
};
|
|
231
|
+
transition_type: "new_subscription" | "upgrade" | "downgrade" | "duration_change";
|
|
232
|
+
currency?: string | undefined;
|
|
233
|
+
promotion_code?: string | undefined;
|
|
234
|
+
proration_at?: string | undefined;
|
|
235
|
+
quote_id?: string | undefined;
|
|
236
|
+
quote_version?: number | undefined;
|
|
237
|
+
amount_due_cents?: number | undefined;
|
|
238
|
+
current_plan?: {
|
|
239
|
+
price_cents: number;
|
|
240
|
+
credits_cents: number;
|
|
241
|
+
duration: "MONTHLY" | "ANNUAL";
|
|
242
|
+
seat_summary: {
|
|
243
|
+
seat_count: number;
|
|
244
|
+
total_cost_cents: number;
|
|
245
|
+
total_credits_cents: number;
|
|
246
|
+
};
|
|
247
|
+
slug: string;
|
|
248
|
+
tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
|
|
249
|
+
period_end?: string | undefined;
|
|
250
|
+
period_start?: string | undefined;
|
|
251
|
+
} | undefined;
|
|
252
|
+
discounts?: {
|
|
253
|
+
code: string;
|
|
254
|
+
kind: "plan" | "promotion";
|
|
255
|
+
name?: string | undefined;
|
|
256
|
+
amount_off_cents?: number | undefined;
|
|
257
|
+
}[] | undefined;
|
|
258
|
+
payment_method_configuration_id?: string | undefined;
|
|
259
|
+
reason?: string | undefined;
|
|
260
|
+
renewal_amount_cents?: number | undefined;
|
|
261
|
+
renewal_at?: string | undefined;
|
|
262
|
+
requires_reactivation_confirmation?: boolean | undefined;
|
|
263
|
+
}, {
|
|
264
|
+
effective_at: string;
|
|
265
|
+
allowed: boolean;
|
|
266
|
+
cost_next_period_cents: number;
|
|
267
|
+
cost_today_cents: number;
|
|
268
|
+
credits_next_period_cents: number;
|
|
269
|
+
credits_today_cents: number;
|
|
270
|
+
is_immediate: boolean;
|
|
271
|
+
new_plan: {
|
|
272
|
+
price_cents: number;
|
|
273
|
+
credits_cents: number;
|
|
274
|
+
duration: "MONTHLY" | "ANNUAL";
|
|
275
|
+
seat_summary: {
|
|
276
|
+
seat_count: number;
|
|
277
|
+
total_cost_cents: number;
|
|
278
|
+
total_credits_cents: number;
|
|
279
|
+
};
|
|
280
|
+
slug: string;
|
|
281
|
+
tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
|
|
282
|
+
period_end?: string | undefined;
|
|
283
|
+
period_start?: string | undefined;
|
|
284
|
+
};
|
|
285
|
+
transition_type: "new_subscription" | "upgrade" | "downgrade" | "duration_change";
|
|
286
|
+
currency?: string | undefined;
|
|
287
|
+
promotion_code?: string | undefined;
|
|
288
|
+
proration_at?: string | undefined;
|
|
289
|
+
quote_id?: string | undefined;
|
|
290
|
+
quote_version?: number | undefined;
|
|
291
|
+
amount_due_cents?: number | undefined;
|
|
292
|
+
current_plan?: {
|
|
293
|
+
price_cents: number;
|
|
294
|
+
credits_cents: number;
|
|
295
|
+
duration: "MONTHLY" | "ANNUAL";
|
|
296
|
+
seat_summary: {
|
|
297
|
+
seat_count: number;
|
|
298
|
+
total_cost_cents: number;
|
|
299
|
+
total_credits_cents: number;
|
|
300
|
+
};
|
|
301
|
+
slug: string;
|
|
302
|
+
tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
|
|
303
|
+
period_end?: string | undefined;
|
|
304
|
+
period_start?: string | undefined;
|
|
305
|
+
} | undefined;
|
|
306
|
+
discounts?: {
|
|
307
|
+
code: string;
|
|
308
|
+
kind: "plan" | "promotion";
|
|
309
|
+
name?: string | undefined;
|
|
310
|
+
amount_off_cents?: number | undefined;
|
|
311
|
+
}[] | undefined;
|
|
312
|
+
payment_method_configuration_id?: string | undefined;
|
|
313
|
+
reason?: string | undefined;
|
|
314
|
+
renewal_amount_cents?: number | undefined;
|
|
315
|
+
renewal_at?: string | undefined;
|
|
316
|
+
requires_reactivation_confirmation?: boolean | undefined;
|
|
317
|
+
}>;
|
|
318
|
+
/**
|
|
319
|
+
* The quote the server returns for a plan change, in the generated field names.
|
|
320
|
+
*
|
|
321
|
+
* Its strings are product copy, not `serverCode`'s kind of machine identifier:
|
|
322
|
+
* a `discounts[]` entry describes a discount the server applied, and a
|
|
323
|
+
* `promotion` one carries back the very `promotionCode` the caller sent, so a
|
|
324
|
+
* host renders `code` and `name` rather than matching them.
|
|
325
|
+
*/
|
|
326
|
+
export type SubscriptionPreview = z.infer<typeof PreviewSchema>;
|
|
327
|
+
export interface PreviewSubscribeInput {
|
|
328
|
+
readonly planSlug: string;
|
|
329
|
+
readonly promotionCode?: string;
|
|
330
|
+
readonly teamCreditStopId?: string;
|
|
331
|
+
readonly checkoutAttemptId?: string;
|
|
332
|
+
}
|
|
333
|
+
export interface PreviewSubscribeOptions {
|
|
334
|
+
readonly signal?: AbortSignal;
|
|
335
|
+
readonly timeoutMs?: number;
|
|
336
|
+
}
|
|
337
|
+
export type PreviewSubscribeResult = {
|
|
338
|
+
readonly status: 'ok';
|
|
339
|
+
readonly value: SubscriptionPreview;
|
|
340
|
+
} | SubscriptionCommandFailure;
|
|
341
|
+
export interface BillingCommandsOptions {
|
|
342
|
+
readonly transport: BillingTransport;
|
|
343
|
+
readonly lifecycle: BillingOperationLifecycle;
|
|
344
|
+
readonly statusReader: BillingStatusReader;
|
|
345
|
+
readonly capabilities: CapabilitiesReader;
|
|
346
|
+
readonly credits: CreditsReader;
|
|
347
|
+
/** One key per attempt; the backend's dedupe key and the transport's replay gate. */
|
|
348
|
+
readonly idempotencyKey?: () => string;
|
|
349
|
+
}
|
|
350
|
+
export interface BillingCommands {
|
|
351
|
+
subscribe: (input: SubscribeInput) => Promise<SubscriptionCommandResult>;
|
|
352
|
+
/**
|
|
353
|
+
* Quotes a plan change without issuing an operation or touching the
|
|
354
|
+
* capability cache: a read the backend happens to shape as a POST. It sends
|
|
355
|
+
* no idempotency key, so a stale token surfaces as the transient
|
|
356
|
+
* `REQUEST_FAILED` that callers already retry rather than as a denial.
|
|
357
|
+
*/
|
|
358
|
+
previewSubscribe: (input: PreviewSubscribeInput, options?: PreviewSubscribeOptions) => Promise<PreviewSubscribeResult>;
|
|
359
|
+
resubscribe: () => Promise<SubscriptionCommandResult>;
|
|
360
|
+
cancelSubscription: () => Promise<SubscriptionCommandResult>;
|
|
361
|
+
/** Returns the portal URL; the host decides how to open it. */
|
|
362
|
+
openPaymentPortal: (input: {
|
|
363
|
+
readonly returnUrl?: string;
|
|
364
|
+
}) => Promise<PaymentPortalResult>;
|
|
365
|
+
}
|
|
366
|
+
export declare function createBillingCommands(options: BillingCommandsOptions): BillingCommands;
|
|
367
|
+
export {};
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The subscription commands on the shared operation lifecycle: subscribe,
|
|
3
|
+
* resubscribe, cancel, and the payment portal. Each one encodes its request
|
|
4
|
+
* against the generated contract, issues it through `lifecycle.begin`, and
|
|
5
|
+
* waits for the settled operation; polling, routing, recovery, and telemetry
|
|
6
|
+
* stay in the lifecycle. `previewSubscribe` sits beside them without a
|
|
7
|
+
* lifecycle: it only quotes a change.
|
|
8
|
+
*
|
|
9
|
+
* No command decides for the server whether a transition is allowed
|
|
10
|
+
* (`../../../docs/billing-command-eligibility.md`). Server codes are matched
|
|
11
|
+
* against the closed set named here; a code outside it reaches the caller only
|
|
12
|
+
* as the transport's coded failure.
|
|
13
|
+
*/
|
|
14
|
+
import { zCancelSubscriptionResponse2, zPaymentPortalResponse, zPreviewPlanInfo, zPreviewSubscribeRequest, zPreviewSubscribeResponse, zResubscribeResponse, zSubscribeRequest, zSubscribeResponse, zSubscriptionDiscount } from '@comfyorg/ingest-types/zod';
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
import { matchesServerCode } from './billingContracts.js';
|
|
17
|
+
import { validateActionUrl } from './operationState.js';
|
|
18
|
+
import { readValidatedBillingResponse } from './sharedRead.js';
|
|
19
|
+
export const SUBSCRIBE_ROUTE = '/billing/subscribe';
|
|
20
|
+
export const RESUBSCRIBE_ROUTE = '/billing/subscription/resubscribe';
|
|
21
|
+
export const CANCEL_SUBSCRIPTION_ROUTE = '/billing/subscription/cancel';
|
|
22
|
+
export const PAYMENT_PORTAL_ROUTE = '/billing/payment-portal';
|
|
23
|
+
export const PREVIEW_SUBSCRIBE_ROUTE = '/billing/preview-subscribe';
|
|
24
|
+
/** The closed set of `serverCode` values these commands act on. */
|
|
25
|
+
const NO_ACTIVE_SUBSCRIPTION_SERVER_CODE = 'NO_ACTIVE_SUBSCRIPTION';
|
|
26
|
+
const REACTIVATION_CONFIRMATION_REQUIRED_SERVER_CODE = 'REACTIVATION_CONFIRMATION_REQUIRED';
|
|
27
|
+
const NOT_SCHEDULED_FOR_CANCELLATION_SERVER_CODE = 'NOT_SCHEDULED_FOR_CANCELLATION';
|
|
28
|
+
const ALREADY_CANCELED_SERVER_CODE = 'ALREADY_CANCELED';
|
|
29
|
+
/**
|
|
30
|
+
* The generated schema coerces every int64 to a `bigint`, which no caller can
|
|
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();
|
|
39
|
+
const PlanInfoSchema = zPreviewPlanInfo.extend({
|
|
40
|
+
credits_cents: cents,
|
|
41
|
+
price_cents: cents,
|
|
42
|
+
seat_summary: zPreviewPlanInfo.shape.seat_summary.extend({
|
|
43
|
+
total_cost_cents: cents,
|
|
44
|
+
total_credits_cents: cents
|
|
45
|
+
})
|
|
46
|
+
});
|
|
47
|
+
const PreviewSchema = zPreviewSubscribeResponse.extend({
|
|
48
|
+
amount_due_cents: cents.optional(),
|
|
49
|
+
cost_next_period_cents: cents,
|
|
50
|
+
cost_today_cents: cents,
|
|
51
|
+
credits_next_period_cents: cents,
|
|
52
|
+
credits_today_cents: cents,
|
|
53
|
+
renewal_amount_cents: cents.optional(),
|
|
54
|
+
current_plan: PlanInfoSchema.optional(),
|
|
55
|
+
new_plan: PlanInfoSchema,
|
|
56
|
+
discounts: z
|
|
57
|
+
.array(zSubscriptionDiscount.extend({ amount_off_cents: cents.optional() }))
|
|
58
|
+
.optional()
|
|
59
|
+
});
|
|
60
|
+
const ALREADY_HELD = {
|
|
61
|
+
status: 'ok',
|
|
62
|
+
value: { phase: 'succeeded' }
|
|
63
|
+
};
|
|
64
|
+
function coded(code) {
|
|
65
|
+
return { status: 'error', code };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A server code the caller's request already satisfies is a success, but
|
|
69
|
+
* only from a 4xx: a 5xx echoing the code is an upstream failure that
|
|
70
|
+
* happens to carry it, and the requested state cannot be assumed to hold.
|
|
71
|
+
*/
|
|
72
|
+
function alreadyInRequestedState(failure, serverCode) {
|
|
73
|
+
return (matchesServerCode(failure, serverCode) &&
|
|
74
|
+
failure.httpStatus !== undefined &&
|
|
75
|
+
failure.httpStatus >= 400 &&
|
|
76
|
+
failure.httpStatus < 500);
|
|
77
|
+
}
|
|
78
|
+
function mapServerCode(failure, alreadyHeldCode) {
|
|
79
|
+
if (alreadyInRequestedState(failure, alreadyHeldCode)) {
|
|
80
|
+
return { status: 'already_held' };
|
|
81
|
+
}
|
|
82
|
+
return matchesServerCode(failure, NO_ACTIVE_SUBSCRIPTION_SERVER_CODE)
|
|
83
|
+
? coded('NO_ACTIVE_SUBSCRIPTION')
|
|
84
|
+
: failure;
|
|
85
|
+
}
|
|
86
|
+
function dropEmpty(value) {
|
|
87
|
+
return value === '' ? undefined : value;
|
|
88
|
+
}
|
|
89
|
+
function previewRequestBody(input) {
|
|
90
|
+
const { planSlug, promotionCode, teamCreditStopId, checkoutAttemptId } = input;
|
|
91
|
+
return {
|
|
92
|
+
plan_slug: planSlug,
|
|
93
|
+
...(promotionCode === undefined ? {} : { promotion_code: promotionCode }),
|
|
94
|
+
...(teamCreditStopId === undefined
|
|
95
|
+
? {}
|
|
96
|
+
: { team_credit_stop_id: teamCreditStopId }),
|
|
97
|
+
...(checkoutAttemptId === undefined
|
|
98
|
+
? {}
|
|
99
|
+
: { checkout_attempt_id: checkoutAttemptId })
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export function createBillingCommands(options) {
|
|
103
|
+
const { transport, lifecycle, statusReader, capabilities, credits, idempotencyKey = () => crypto.randomUUID() } = options;
|
|
104
|
+
async function refreshAfterSuccess() {
|
|
105
|
+
capabilities.invalidate();
|
|
106
|
+
await Promise.all([credits.read(), statusReader.read()]);
|
|
107
|
+
}
|
|
108
|
+
function post(route, body, parse, key) {
|
|
109
|
+
return readValidatedBillingResponse(transport, {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
route,
|
|
112
|
+
body,
|
|
113
|
+
...(key === undefined ? {} : { idempotencyKey: key })
|
|
114
|
+
}, parse);
|
|
115
|
+
}
|
|
116
|
+
async function settle(kind, issue) {
|
|
117
|
+
// The lifecycle carries only the shared codes; the command's own verdict
|
|
118
|
+
// and what it read off the issuing response wait here for `begin` to
|
|
119
|
+
// return. Both stay unset whenever `begin` never ran this caller's issue:
|
|
120
|
+
// it adopted an operation the server was already settling, or joined one
|
|
121
|
+
// another command of the same kind had in flight.
|
|
122
|
+
const verdict = {};
|
|
123
|
+
const issued = {};
|
|
124
|
+
const began = await lifecycle.begin(kind, async () => {
|
|
125
|
+
const outcome = await issue();
|
|
126
|
+
if (outcome.status === 'ok') {
|
|
127
|
+
issued.value = outcome;
|
|
128
|
+
return outcome;
|
|
129
|
+
}
|
|
130
|
+
verdict.value = outcome;
|
|
131
|
+
return { status: 'error', code: 'REQUEST_FAILED' };
|
|
132
|
+
});
|
|
133
|
+
if (verdict.value !== undefined) {
|
|
134
|
+
if (verdict.value.status !== 'already_held')
|
|
135
|
+
return verdict.value;
|
|
136
|
+
await refreshAfterSuccess();
|
|
137
|
+
return ALREADY_HELD;
|
|
138
|
+
}
|
|
139
|
+
if (began.status === 'error')
|
|
140
|
+
return began;
|
|
141
|
+
const settled = lifecycle.settled(began.value.id);
|
|
142
|
+
const operation = settled === undefined ? began.value : await settled;
|
|
143
|
+
if (operation.phase === 'pending') {
|
|
144
|
+
return { status: 'error', code: 'SUPERSEDED' };
|
|
145
|
+
}
|
|
146
|
+
if (operation.phase === 'succeeded')
|
|
147
|
+
await refreshAfterSuccess();
|
|
148
|
+
const issuedStatus = issued.value?.issuedStatus;
|
|
149
|
+
return {
|
|
150
|
+
status: 'ok',
|
|
151
|
+
value: {
|
|
152
|
+
phase: operation.phase,
|
|
153
|
+
operation,
|
|
154
|
+
...(issuedStatus === undefined ? {} : { issuedStatus })
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
async function issueSubscribe(input) {
|
|
159
|
+
const key = idempotencyKey();
|
|
160
|
+
const response = await post(SUBSCRIBE_ROUTE, { ...input, idempotency_key: key }, (body) => zSubscribeResponse.safeParse(body), key);
|
|
161
|
+
if (response.status === 'error') {
|
|
162
|
+
return matchesServerCode(response, REACTIVATION_CONFIRMATION_REQUIRED_SERVER_CODE)
|
|
163
|
+
? coded('REACTIVATION_CONFIRMATION_REQUIRED')
|
|
164
|
+
: response;
|
|
165
|
+
}
|
|
166
|
+
const { billing_op_id, status, payment_method_url } = response.value.data;
|
|
167
|
+
if (status !== 'needs_payment_method') {
|
|
168
|
+
return {
|
|
169
|
+
status: 'ok',
|
|
170
|
+
value: { operationId: billing_op_id },
|
|
171
|
+
issuedStatus: status
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (payment_method_url === undefined) {
|
|
175
|
+
return coded('MISSING_PAYMENT_METHOD_URL');
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
status: 'ok',
|
|
179
|
+
value: { operationId: billing_op_id, actionUrl: payment_method_url },
|
|
180
|
+
issuedStatus: status
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
async function issueResubscribe() {
|
|
184
|
+
const key = idempotencyKey();
|
|
185
|
+
const response = await post(RESUBSCRIBE_ROUTE, { idempotency_key: key }, (body) => zResubscribeResponse.safeParse(body), key);
|
|
186
|
+
if (response.status === 'error') {
|
|
187
|
+
return mapServerCode(response, NOT_SCHEDULED_FOR_CANCELLATION_SERVER_CODE);
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
status: 'ok',
|
|
191
|
+
value: { operationId: response.value.data.billing_op_id }
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
async function issueCancel() {
|
|
195
|
+
const key = idempotencyKey();
|
|
196
|
+
const response = await post(CANCEL_SUBSCRIPTION_ROUTE, { idempotency_key: key }, (body) => zCancelSubscriptionResponse2.safeParse(body), key);
|
|
197
|
+
if (response.status === 'error') {
|
|
198
|
+
return mapServerCode(response, ALREADY_CANCELED_SERVER_CODE);
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
status: 'ok',
|
|
202
|
+
value: { operationId: response.value.data.billing_op_id }
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const resubscribe = () => settle('subscription', issueResubscribe);
|
|
206
|
+
async function subscribe(input) {
|
|
207
|
+
const confirmationToken = dropEmpty(input.confirmation_token);
|
|
208
|
+
const savedPaymentMethodId = dropEmpty(input.saved_payment_method_id);
|
|
209
|
+
if (confirmationToken !== undefined && savedPaymentMethodId !== undefined) {
|
|
210
|
+
return coded('CONFLICTING_PAYMENT_METHOD');
|
|
211
|
+
}
|
|
212
|
+
// JSON drops `undefined` but keeps `''`, so an empty credential would
|
|
213
|
+
// reach the server as a present-but-meaningless value.
|
|
214
|
+
const request = {
|
|
215
|
+
...input,
|
|
216
|
+
confirmation_token: confirmationToken,
|
|
217
|
+
saved_payment_method_id: savedPaymentMethodId
|
|
218
|
+
};
|
|
219
|
+
if (!zSubscribeRequest.safeParse(request).success) {
|
|
220
|
+
return coded('INVALID_REQUEST');
|
|
221
|
+
}
|
|
222
|
+
return settle('subscription', () => issueSubscribe(request));
|
|
223
|
+
}
|
|
224
|
+
async function previewSubscribe(input, options = {}) {
|
|
225
|
+
const body = previewRequestBody(input);
|
|
226
|
+
if (!zPreviewSubscribeRequest.safeParse(body).success) {
|
|
227
|
+
return coded('INVALID_REQUEST');
|
|
228
|
+
}
|
|
229
|
+
const response = await readValidatedBillingResponse(transport, {
|
|
230
|
+
method: 'POST',
|
|
231
|
+
route: PREVIEW_SUBSCRIBE_ROUTE,
|
|
232
|
+
body,
|
|
233
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
234
|
+
...(options.timeoutMs === undefined
|
|
235
|
+
? {}
|
|
236
|
+
: { timeoutMs: options.timeoutMs })
|
|
237
|
+
}, (raw) => PreviewSchema.safeParse(raw));
|
|
238
|
+
return response.status === 'error'
|
|
239
|
+
? response
|
|
240
|
+
: { status: 'ok', value: response.value.data };
|
|
241
|
+
}
|
|
242
|
+
async function openPaymentPortal(input) {
|
|
243
|
+
const response = await post(PAYMENT_PORTAL_ROUTE, input.returnUrl === undefined ? {} : { return_url: input.returnUrl }, (body) => zPaymentPortalResponse.safeParse(body));
|
|
244
|
+
if (response.status === 'error')
|
|
245
|
+
return response;
|
|
246
|
+
const url = validateActionUrl(response.value.data.url);
|
|
247
|
+
if (url === undefined) {
|
|
248
|
+
return {
|
|
249
|
+
status: 'error',
|
|
250
|
+
code: 'MALFORMED_RESPONSE',
|
|
251
|
+
httpStatus: response.value.httpStatus
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return { status: 'ok', value: { url } };
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
subscribe,
|
|
258
|
+
previewSubscribe,
|
|
259
|
+
resubscribe,
|
|
260
|
+
cancelSubscription: () => settle('cancel', issueCancel),
|
|
261
|
+
openPaymentPortal
|
|
262
|
+
};
|
|
263
|
+
}
|