@comfyorg/account-core 1.0.0-alpha.1 → 1.0.0-alpha.3

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.
Files changed (76) hide show
  1. package/dist/core/billing/index.d.ts +4 -3
  2. package/dist/core/billing/index.js +2 -1
  3. package/dist/core/billing/operationLifecycle.d.ts +26 -4
  4. package/dist/core/billing/operationLifecycle.js +113 -14
  5. package/dist/core/billing/operationPointer.d.ts +24 -0
  6. package/dist/core/billing/operationPointer.js +14 -1
  7. package/dist/core/billing/operationState.d.ts +135 -1
  8. package/dist/core/billing/operationState.js +15 -2
  9. package/dist/core/billing/paymentFriction.d.ts +17 -0
  10. package/dist/core/billing/paymentFriction.js +41 -0
  11. package/dist/core/billing/subscriptionCommands.d.ts +64 -2
  12. package/dist/core/billing/subscriptionCommands.js +31 -7
  13. package/dist/core/billing/subscriptionDiscount.d.ts +26 -0
  14. package/dist/core/billing/subscriptionDiscount.js +6 -0
  15. package/dist/core/billing/telemetry/billingTelemetryEvent.d.ts +25 -0
  16. package/dist/core/billing/telemetry/billingTelemetryEvent.js +3 -0
  17. package/dist/core/billing/telemetry/cancelEvent.d.ts +19 -0
  18. package/dist/core/billing/telemetry/cancelEvent.js +1 -0
  19. package/dist/core/billing/telemetry/capabilityReadEvent.d.ts +4 -0
  20. package/dist/core/billing/telemetry/capabilityReadEvent.js +1 -0
  21. package/dist/core/billing/telemetry/checkoutChallengeEvent.d.ts +25 -0
  22. package/dist/core/billing/telemetry/checkoutChallengeEvent.js +1 -0
  23. package/dist/core/billing/telemetry/checkoutJourney.d.ts +176 -0
  24. package/dist/core/billing/telemetry/checkoutJourney.js +115 -0
  25. package/dist/core/billing/telemetry/checkoutRedirectEvent.d.ts +30 -0
  26. package/dist/core/billing/telemetry/checkoutRedirectEvent.js +1 -0
  27. package/dist/core/billing/telemetry/downgradeToPersonalEvent.d.ts +14 -0
  28. package/dist/core/billing/telemetry/downgradeToPersonalEvent.js +1 -0
  29. package/dist/core/billing/telemetry/entryEvent.d.ts +16 -0
  30. package/dist/core/billing/telemetry/entryEvent.js +1 -0
  31. package/dist/core/billing/telemetry/eventNames.d.ts +36 -0
  32. package/dist/core/billing/telemetry/eventNames.js +36 -0
  33. package/dist/core/billing/telemetry/index.d.ts +17 -0
  34. package/dist/core/billing/telemetry/index.js +6 -0
  35. package/dist/core/billing/telemetry/operationEvent.d.ts +25 -0
  36. package/dist/core/billing/telemetry/operationEvent.js +1 -0
  37. package/dist/core/billing/telemetry/operationLifecycleEvent.d.ts +9 -0
  38. package/dist/core/billing/telemetry/operationLifecycleEvent.js +79 -0
  39. package/dist/core/billing/telemetry/payload.d.ts +55 -0
  40. package/dist/core/billing/telemetry/payload.js +75 -0
  41. package/dist/core/billing/telemetry/portalEvent.d.ts +23 -0
  42. package/dist/core/billing/telemetry/portalEvent.js +1 -0
  43. package/dist/core/billing/telemetry/resubscribeEvent.d.ts +13 -0
  44. package/dist/core/billing/telemetry/resubscribeEvent.js +1 -0
  45. package/dist/core/billing/telemetry/stages.d.ts +45 -0
  46. package/dist/core/billing/telemetry/stages.js +1 -0
  47. package/dist/core/billing/telemetry/subscriptionCheckoutEvent.d.ts +23 -0
  48. package/dist/core/billing/telemetry/subscriptionCheckoutEvent.js +1 -0
  49. package/dist/core/billing/telemetry/topupEvent.d.ts +30 -0
  50. package/dist/core/billing/telemetry/topupEvent.js +6 -0
  51. package/dist/core/billing/telemetry/vocabulary.d.ts +9 -0
  52. package/dist/core/billing/telemetry/vocabulary.js +1 -0
  53. package/dist/core/billing/telemetry/webEntryEvent.d.ts +27 -0
  54. package/dist/core/billing/telemetry/webEntryEvent.js +1 -0
  55. package/dist/core/billing/telemetry/webHandoffEvent.d.ts +11 -0
  56. package/dist/core/billing/telemetry/webHandoffEvent.js +1 -0
  57. package/dist/core/billing/telemetry/webReturnEvent.d.ts +8 -0
  58. package/dist/core/billing/telemetry/webReturnEvent.js +1 -0
  59. package/dist/core/billing/telemetry/webSessionEvent.d.ts +17 -0
  60. package/dist/core/billing/telemetry/webSessionEvent.js +1 -0
  61. package/dist/core/requestTimeout.d.ts +7 -0
  62. package/dist/core/requestTimeout.js +13 -0
  63. package/dist/core/sessionTokenMint.d.ts +3 -1
  64. package/dist/core/sessionTokenMint.js +28 -11
  65. package/dist/core/webSession.d.ts +6 -3
  66. package/dist/core/webSession.js +31 -23
  67. package/dist/core/webSessionFlag.d.ts +1 -2
  68. package/dist/core/webSessionFlag.js +1 -2
  69. package/dist/core/webSessionIdentity.d.ts +12 -6
  70. package/dist/core/webSessionIdentity.js +17 -4
  71. package/dist/signInSchemas.d.ts +1 -1
  72. package/dist/telemetry.d.ts +26 -0
  73. package/dist/telemetry.js +28 -7
  74. package/dist/testing.d.ts +2 -1
  75. package/dist/testing.js +5 -4
  76. package/package.json +3 -1
@@ -31,7 +31,7 @@ export type { PaymentMethodsReadOptions, PaymentMethodsReader, PaymentMethodsRea
31
31
  export { PAYMENT_METHODS_ROUTE, createPaymentMethodsReader } from './paymentMethods.js';
32
32
  export type { BillingStatusData, BillingStatusReader, BillingStatusReaderOptions, BillingStatusReadOptions, BillingStatusScope, BillingStatusSnapshot } from './status.js';
33
33
  export { BILLING_STATUS_ROUTE, createBillingStatusReader } from './status.js';
34
- export type { BillingAuthenticationState, BillingDeclineReason, BillingOpStatus, BillingOperationEvent, BillingOperationIdentity, BillingOperationReceipt, BillingOperationKind, BillingOperationPhase, BillingOperationServerPhase, BillingOperationState, BillingPresentation, BillingPresentationState, BillingRecoveryAction, EmbeddedChallenge, FailedBillingOperation, HostedBillingDestination, PendingBillingOperation, SucceededBillingOperation } from './operationState.js';
34
+ export type { BillingAuthenticationState, BillingChargeBreakdown, BillingChargeReason, BillingDeclineReason, BillingOpStatus, BillingOperationEvent, BillingOperationIdentity, BillingOperationReceipt, BillingOperationKind, BillingOperationPhase, BillingOperationServerPhase, BillingOperationState, BillingPresentation, BillingPresentationState, BillingRecoveryAction, EmbeddedChallenge, FailedBillingOperation, HostedBillingDestination, PendingBillingOperation, SucceededBillingOperation } from './operationState.js';
35
35
  export { isBlockedOnCustomerPhase, isGrantLanding, isTerminal, reduceBillingOperation, validateActionUrl } from './operationState.js';
36
36
  export { OPERATION_POLL_BUDGET, OPERATION_POLL_TIMING, customerCanActHere, hasExhaustedPollBudget, isParkedOnCustomer, nextPollDelayMs, pendingOperationActionHold, pollBudgetMs } from './operationPolicy.js';
37
37
  export type { CustomerActionHold } from './operationPolicy.js';
@@ -41,11 +41,12 @@ export type { PresentationRoutingInput } from './presentation.js';
41
41
  export { selectBillingPresentation } from './presentation.js';
42
42
  export type { BillingOperationFailureCategory, BillingOperationLifecycle, BillingOperationLifecycleOptions, BillingOperationTelemetryEvent, BillingRecoverOptions, IssuedBillingOperation, PresentationSwitchOutcome } from './operationLifecycle.js';
43
43
  export { createBillingOperationLifecycle, operationRoute } from './operationLifecycle.js';
44
- export { BILLING_OPERATION_TELEMETRY_EVENT } from '../../telemetry.js';
44
+ export { BILLING_CHECKOUT_FRICTION_TELEMETRY_EVENT, BILLING_OPERATION_TELEMETRY_EVENT } from '../../telemetry.js';
45
+ export * from './telemetry/index.js';
45
46
  export type { EmbeddedChallengeOutcome, EmbeddedChallengePort } from './challengeDriver.js';
46
47
  export { driveEmbeddedChallenge } from './challengeDriver.js';
47
48
  export { readBillingErrorCode, readBillingErrorMessage } from './billingErrorBody.js';
48
- export type { BillingCommands, BillingCommandsOptions, PaymentPortalResult, PreviewSubscribeInput, PreviewSubscribeOptions, PreviewSubscribeResult, SubscribeInput, SubscriptionCommandCode, SubscriptionCommandFailure, SubscriptionCommandOutcome, SubscriptionCommandResult, SubscriptionPreview, TerminalBillingOperation } from './subscriptionCommands.js';
49
+ export type { BillingCommands, BillingCommandsOptions, CancelOperationResult, CancelRefusalCode, PaymentPortalResult, PreviewSubscribeInput, PreviewSubscribeOptions, PreviewSubscribeResult, SubscribeInput, SubscriptionCommandCode, SubscriptionCommandFailure, SubscriptionCommandOutcome, SubscriptionCommandResult, SubscriptionPreview, TerminalBillingOperation } from './subscriptionCommands.js';
49
50
  export { CANCEL_SUBSCRIPTION_ROUTE, PAYMENT_PORTAL_ROUTE, PREVIEW_SUBSCRIBE_ROUTE, RESUBSCRIBE_ROUTE, SUBSCRIBE_ROUTE, createBillingCommands } from './subscriptionCommands.js';
50
51
  export type { HostPaymentStep, PaymentProjection, PaymentReasonKey, PaymentStep } from './paymentProjection.js';
51
52
  export { awaitsHostedAction, projectPaymentStep } from './paymentProjection.js';
@@ -15,7 +15,8 @@ export { OPERATION_POLL_BUDGET, OPERATION_POLL_TIMING, customerCanActHere, hasEx
15
15
  export { OPERATION_POINTER_MAX_AGE_MS, createOperationPointerStore, operationPointerKey } from './operationPointer.js';
16
16
  export { selectBillingPresentation } from './presentation.js';
17
17
  export { createBillingOperationLifecycle, operationRoute } from './operationLifecycle.js';
18
- export { BILLING_OPERATION_TELEMETRY_EVENT } from '../../telemetry.js';
18
+ export { BILLING_CHECKOUT_FRICTION_TELEMETRY_EVENT, BILLING_OPERATION_TELEMETRY_EVENT } from '../../telemetry.js';
19
+ export * from './telemetry/index.js';
19
20
  export { driveEmbeddedChallenge } from './challengeDriver.js';
20
21
  export { readBillingErrorCode, readBillingErrorMessage } from './billingErrorBody.js';
21
22
  export { CANCEL_SUBSCRIPTION_ROUTE, PAYMENT_PORTAL_ROUTE, PREVIEW_SUBSCRIBE_ROUTE, RESUBSCRIBE_ROUTE, SUBSCRIBE_ROUTE, createBillingCommands } from './subscriptionCommands.js';
@@ -16,11 +16,13 @@
16
16
  * wiring onto the scope source, the scope tracker, and the generated
17
17
  * contract is new.
18
18
  */
19
- import { BILLING_OPERATION_TELEMETRY_EVENT } from '../../telemetry.js';
19
+ import { BILLING_CHECKOUT_FRICTION_TELEMETRY_EVENT, BILLING_OPERATION_TELEMETRY_EVENT } from '../../telemetry.js';
20
20
  import type { BillingResult, BillingTransport } from './billingContracts.js';
21
21
  import type { BillingScope, BillingScopeSource } from './billingScope.js';
22
22
  import type { BillingOperationPointerStorage } from './operationPointer.js';
23
23
  import type { BillingDeclineReason, BillingOperationKind, BillingOperationState, BillingPresentation, HostedBillingDestination } from './operationState.js';
24
+ import type { CheckoutHostedStep, CheckoutRedirectNavigation } from './telemetry/checkoutRedirectEvent.js';
25
+ import type { CheckoutMethodKind } from './telemetry/checkoutJourney.js';
24
26
  import type { BillingStatusReader } from './status.js';
25
27
  export declare function operationRoute(operationId: string): string;
26
28
  /** What a command learned from the backend when it issued the operation. */
@@ -32,18 +34,31 @@ export interface IssuedBillingOperation {
32
34
  readonly clientSecret?: string;
33
35
  }
34
36
  export type BillingOperationFailureCategory = 'provider_decline' | 'api_rejected' | 'poll_timeout' | 'reconciliation_needed' | 'stale_operation';
35
- export interface BillingOperationTelemetryEvent {
36
- readonly name: (typeof BILLING_OPERATION_TELEMETRY_EVENT)[keyof typeof BILLING_OPERATION_TELEMETRY_EVENT];
37
+ type HostedStepEventName = typeof BILLING_CHECKOUT_FRICTION_TELEMETRY_EVENT.redirectStarted | typeof BILLING_CHECKOUT_FRICTION_TELEMETRY_EVENT.returned;
38
+ type BillingOperationTelemetryEventName = Exclude<(typeof BILLING_OPERATION_TELEMETRY_EVENT)[keyof typeof BILLING_OPERATION_TELEMETRY_EVENT] | (typeof BILLING_CHECKOUT_FRICTION_TELEMETRY_EVENT)[keyof typeof BILLING_CHECKOUT_FRICTION_TELEMETRY_EVENT], HostedStepEventName>;
39
+ /** A hosted step this tab handed the customer to. */
40
+ interface HostedStepVisit {
41
+ readonly destination: HostedBillingDestination;
42
+ readonly step: CheckoutHostedStep;
43
+ readonly navigation: CheckoutRedirectNavigation;
44
+ readonly method_kind?: CheckoutMethodKind;
45
+ }
46
+ interface BillingOperationTelemetryBase {
37
47
  readonly billing_op_id: string;
38
48
  readonly operation_type: BillingOperationKind;
39
49
  readonly presentation: BillingPresentation;
40
50
  /** True when this tab reattached to an operation it did not issue. */
41
51
  readonly resumed: boolean;
52
+ }
53
+ export type BillingOperationTelemetryEvent = BillingOperationTelemetryBase & ({
54
+ readonly name: BillingOperationTelemetryEventName;
42
55
  readonly failure_category?: BillingOperationFailureCategory;
43
56
  readonly decline_reason?: BillingDeclineReason;
44
57
  /** From the attempt's start, so a resumed operation reports its whole life. */
45
58
  readonly duration_ms?: number;
46
- }
59
+ } | (HostedStepVisit & {
60
+ readonly name: HostedStepEventName;
61
+ }));
47
62
  export type PresentationSwitchOutcome = 'switched' | 'unchanged'
48
63
  /** The server has offered no hosted continuation yet. */
49
64
  | 'no_hosted_url'
@@ -97,6 +112,12 @@ export interface BillingOperationLifecycle {
97
112
  wake: () => void;
98
113
  /** Moves the operation between presentations under the same id. */
99
114
  switchPresentation: (operationId: string, presentation: BillingPresentation) => PresentationSwitchOutcome;
115
+ /**
116
+ * The host opened the operation's hosted step. A redirect is remembered in
117
+ * the pointer, so the page it returns to reports the return; a new tab
118
+ * reports it on the next wake.
119
+ */
120
+ reportHostedStepOpened: (operationId: string, navigation: CheckoutRedirectNavigation, methodKind?: CheckoutMethodKind) => void;
100
121
  /** The host began driving the operation's challenge; polling pauses until it settles. */
101
122
  reportChallengeStarted: (operationId: string) => void;
102
123
  reportChallengeSettled: (operationId: string, outcome: 'completed' | 'failed') => void;
@@ -108,3 +129,4 @@ export interface BillingOperationLifecycle {
108
129
  dispose: () => void;
109
130
  }
110
131
  export declare function createBillingOperationLifecycle(options: BillingOperationLifecycleOptions): BillingOperationLifecycle;
132
+ export {};
@@ -16,11 +16,12 @@
16
16
  * wiring onto the scope source, the scope tracker, and the generated
17
17
  * contract is new.
18
18
  */
19
- import { BILLING_OPERATION_TELEMETRY_EVENT } from '../../telemetry.js';
19
+ import { BILLING_CHECKOUT_FRICTION_TELEMETRY_EVENT, BILLING_OPERATION_TELEMETRY_EVENT } from '../../telemetry.js';
20
20
  import { createBillingScopeTracker } from './billingScope.js';
21
21
  import { NO_POINTER_STORE, createOperationPointerStore } from './operationPointer.js';
22
22
  import { hasExhaustedPollBudget, isWaitingOnCustomerWithoutAction, nextPollDelayMs } from './operationPolicy.js';
23
23
  import { BillingOpStatusSchema, isGrantLanding, isTerminal, reduceBillingOperation, validateActionUrl } from './operationState.js';
24
+ import { paymentFrictionBetween } from './paymentFriction.js';
24
25
  import { selectBillingPresentation } from './presentation.js';
25
26
  import { readValidatedBillingResponse } from './sharedRead.js';
26
27
  /** Settled, but a later read may still change what the operation reports. */
@@ -42,6 +43,18 @@ const OPERATION_ALREADY_PENDING = {
42
43
  status: 'error',
43
44
  code: 'OPERATION_ALREADY_PENDING'
44
45
  };
46
+ /** The attempt a pointer remembers, picked up again by this tab. */
47
+ function resumedFrom(pointer) {
48
+ return {
49
+ presentation: pointer.presentation,
50
+ attemptStartedAt: pointer.attemptStartedAt,
51
+ resumed: true,
52
+ awaitedHere: pointer.awaited === true,
53
+ ...(pointer.redirect === undefined
54
+ ? {}
55
+ : { returnedFrom: pointer.redirect })
56
+ };
57
+ }
45
58
  function continuationOf(issued) {
46
59
  return {
47
60
  ...(issued.actionUrl === undefined ? {} : { actionUrl: issued.actionUrl }),
@@ -95,6 +108,23 @@ function failureCategoryFor(state) {
95
108
  return 'stale_operation';
96
109
  }
97
110
  }
111
+ const FRICTION_EVENT_NAME = {
112
+ challenge_required: BILLING_CHECKOUT_FRICTION_TELEMETRY_EVENT.challengeRequired,
113
+ challenge_completed: BILLING_CHECKOUT_FRICTION_TELEMETRY_EVENT.challengeCompleted,
114
+ challenge_failed: BILLING_CHECKOUT_FRICTION_TELEMETRY_EVENT.challengeFailed
115
+ };
116
+ /** What the hosted page asks of the customer, from what the server waits on. */
117
+ function hostedStepOf(state) {
118
+ if (state.authenticationState === 'requires_action' ||
119
+ (state.presentation === 'embedded' && state.challenge !== undefined)) {
120
+ return 'authentication';
121
+ }
122
+ if (state.serverPhase === 'awaiting_payment_method')
123
+ return 'payment_method';
124
+ if (state.serverPhase === 'awaiting_invoice_payment')
125
+ return 'invoice_payment';
126
+ return 'checkout';
127
+ }
98
128
  /** The pause is this tab's own challenge on screen; a hosted page never pauses. */
99
129
  function isDrivingChallenge(state) {
100
130
  return (state.presentation === 'embedded' &&
@@ -155,11 +185,28 @@ export function createBillingOperationLifecycle(options) {
155
185
  duration_ms: now() - state.attemptStartedAt
156
186
  });
157
187
  }
188
+ function emitFrictionTelemetry(record, before) {
189
+ const state = record.state;
190
+ for (const signal of paymentFrictionBetween(before, state)) {
191
+ onTelemetry?.({
192
+ name: FRICTION_EVENT_NAME[signal.stage],
193
+ billing_op_id: state.id,
194
+ operation_type: state.kind,
195
+ presentation: state.presentation,
196
+ resumed: record.resumed,
197
+ ...('declineReason' in signal && signal.declineReason !== undefined
198
+ ? { decline_reason: signal.declineReason }
199
+ : {})
200
+ });
201
+ }
202
+ }
158
203
  function dispatch(record, event) {
159
- const next = reduceBillingOperation(record.state, event);
160
- if (next === record.state)
204
+ const before = record.state;
205
+ const next = reduceBillingOperation(before, event);
206
+ if (next === before)
161
207
  return;
162
208
  record.state = next;
209
+ emitFrictionTelemetry(record, before);
163
210
  if (isTerminal(next)) {
164
211
  stopTimer(record);
165
212
  pointers.settle(next);
@@ -195,13 +242,28 @@ export function createBillingOperationLifecycle(options) {
195
242
  record.inFlightPoll = request;
196
243
  return request;
197
244
  }
198
- function writePointer(scope, state) {
245
+ function savedRedirect(scope, operationId) {
246
+ const saved = pointers.read(scope);
247
+ return saved?.operationId === operationId ? saved.redirect : undefined;
248
+ }
249
+ function writePointer(scope, state, redirect) {
199
250
  pointers.write(scope, {
200
251
  operationId: state.id,
201
252
  kind: state.kind,
202
253
  presentation: state.presentation,
203
254
  attemptStartedAt: state.attemptStartedAt,
204
- ...(state.awaitedHere ? { awaited: true } : {})
255
+ ...(state.awaitedHere ? { awaited: true } : {}),
256
+ ...(redirect === undefined ? {} : { redirect })
257
+ });
258
+ }
259
+ function emitHostedStepTelemetry(record, name, visit) {
260
+ onTelemetry?.({
261
+ name,
262
+ billing_op_id: record.state.id,
263
+ operation_type: record.state.kind,
264
+ presentation: record.state.presentation,
265
+ resumed: record.resumed,
266
+ ...visit
205
267
  });
206
268
  }
207
269
  function readOperation(operationId) {
@@ -292,6 +354,7 @@ export function createBillingOperationLifecycle(options) {
292
354
  waitingWithoutActionSince: undefined,
293
355
  timer: undefined,
294
356
  inFlightPoll: undefined,
357
+ awaitingReturn: undefined,
295
358
  settled,
296
359
  resolveSettled
297
360
  };
@@ -304,6 +367,13 @@ export function createBillingOperationLifecycle(options) {
304
367
  presentation: input.presentation,
305
368
  resumed: input.resumed
306
369
  });
370
+ emitFrictionTelemetry(record, undefined);
371
+ if (input.returnedFrom !== undefined) {
372
+ emitHostedStepTelemetry(record, BILLING_CHECKOUT_FRICTION_TELEMETRY_EVENT.returned, {
373
+ ...input.returnedFrom,
374
+ navigation: 'redirect'
375
+ });
376
+ }
307
377
  startObserving(record, input, state);
308
378
  return record;
309
379
  }
@@ -450,10 +520,7 @@ export function createBillingOperationLifecycle(options) {
450
520
  id: pointer.operationId,
451
521
  kind: pointer.kind,
452
522
  context,
453
- presentation: pointer.presentation,
454
- attemptStartedAt: pointer.attemptStartedAt,
455
- resumed: true,
456
- awaitedHere: pointer.awaited === true
523
+ ...resumedFrom(pointer)
457
524
  };
458
525
  }
459
526
  // Unreachable is not "nothing pending": the pointer is the only evidence
@@ -470,10 +537,14 @@ export function createBillingOperationLifecycle(options) {
470
537
  const record = adopt({
471
538
  ...pending,
472
539
  context,
473
- presentation: known?.presentation ?? routeFor(rail, pending),
474
- attemptStartedAt: known?.attemptStartedAt ?? now(),
475
- resumed: true,
476
- awaitedHere: known?.awaited === true
540
+ ...(known === undefined
541
+ ? {
542
+ presentation: routeFor(rail, pending),
543
+ attemptStartedAt: now(),
544
+ resumed: true,
545
+ awaitedHere: false
546
+ }
547
+ : resumedFrom(known))
477
548
  }, includeSettled);
478
549
  return { status: 'ok', value: record.state };
479
550
  }
@@ -524,6 +595,7 @@ export function createBillingOperationLifecycle(options) {
524
595
  }
525
596
  function wake() {
526
597
  for (const record of operations.values()) {
598
+ reportReturnFromNewTab(record);
527
599
  if (record.state.phase !== 'pending')
528
600
  continue;
529
601
  if (isDrivingChallenge(record.state))
@@ -561,11 +633,37 @@ export function createBillingOperationLifecycle(options) {
561
633
  if (refusal !== undefined)
562
634
  return refusal;
563
635
  dispatch(record, switchEvent(presentation));
564
- writePointer(record.context.scope, record.state);
636
+ writePointer(record.context.scope, record.state, savedRedirect(record.context.scope, operationId));
565
637
  record.delayMs = undefined;
566
638
  schedule(record);
567
639
  return 'switched';
568
640
  }
641
+ function reportReturnFromNewTab(record) {
642
+ const visit = record.awaitingReturn;
643
+ if (visit === undefined)
644
+ return;
645
+ record.awaitingReturn = undefined;
646
+ emitHostedStepTelemetry(record, BILLING_CHECKOUT_FRICTION_TELEMETRY_EVENT.returned, visit);
647
+ }
648
+ function reportHostedStepOpened(operationId, navigation, methodKind) {
649
+ const record = operations.get(operationId);
650
+ if (record === undefined || record.state.phase !== 'pending')
651
+ return;
652
+ const state = record.state;
653
+ const redirect = {
654
+ destination: state.presentation === 'hosted' ? state.hostedDestination : 'stripe',
655
+ step: hostedStepOf(state),
656
+ ...(methodKind === undefined ? {} : { method_kind: methodKind })
657
+ };
658
+ const visit = { ...redirect, navigation };
659
+ if (navigation === 'redirect') {
660
+ writePointer(record.context.scope, state, redirect);
661
+ }
662
+ else {
663
+ record.awaitingReturn = visit;
664
+ }
665
+ emitHostedStepTelemetry(record, BILLING_CHECKOUT_FRICTION_TELEMETRY_EVENT.redirectStarted, visit);
666
+ }
569
667
  function reportChallengeStarted(operationId) {
570
668
  const record = operations.get(operationId);
571
669
  if (record === undefined)
@@ -594,6 +692,7 @@ export function createBillingOperationLifecycle(options) {
594
692
  recover,
595
693
  wake,
596
694
  switchPresentation,
695
+ reportHostedStepOpened,
597
696
  reportChallengeStarted,
598
697
  reportChallengeSettled,
599
698
  get: (operationId) => operations.get(operationId)?.state,
@@ -26,6 +26,20 @@ declare const PointerSchema: z.ZodObject<{
26
26
  awaited: z.ZodOptional<z.ZodLiteral<true>>;
27
27
  /** The end this tab saw it reach, kept only by a store that retains settled pointers. */
28
28
  settled: z.ZodOptional<z.ZodEnum<["succeeded", "reconciliation_needed"]>>;
29
+ /** The hosted step this page redirected to; the page it returns to reports the return. */
30
+ redirect: z.ZodOptional<z.ZodObject<{
31
+ destination: z.ZodEnum<["stripe", "billing_web"]>;
32
+ step: z.ZodEnum<["authentication", "payment_method", "invoice_payment", "checkout"]>;
33
+ method_kind: z.ZodOptional<z.ZodEnum<["card", "alipay", "other"]>>;
34
+ }, "strip", z.ZodTypeAny, {
35
+ step: "authentication" | "payment_method" | "invoice_payment" | "checkout";
36
+ destination: "stripe" | "billing_web";
37
+ method_kind?: "card" | "alipay" | "other" | undefined;
38
+ }, {
39
+ step: "authentication" | "payment_method" | "invoice_payment" | "checkout";
40
+ destination: "stripe" | "billing_web";
41
+ method_kind?: "card" | "alipay" | "other" | undefined;
42
+ }>>;
29
43
  }, "strip", z.ZodTypeAny, {
30
44
  kind: "cancel" | "subscription" | "topup";
31
45
  attemptStartedAt: number;
@@ -33,6 +47,11 @@ declare const PointerSchema: z.ZodObject<{
33
47
  operationId: string;
34
48
  awaited?: true | undefined;
35
49
  settled?: "succeeded" | "reconciliation_needed" | undefined;
50
+ redirect?: {
51
+ step: "authentication" | "payment_method" | "invoice_payment" | "checkout";
52
+ destination: "stripe" | "billing_web";
53
+ method_kind?: "card" | "alipay" | "other" | undefined;
54
+ } | undefined;
36
55
  }, {
37
56
  kind: "cancel" | "subscription" | "topup";
38
57
  attemptStartedAt: number;
@@ -40,6 +59,11 @@ declare const PointerSchema: z.ZodObject<{
40
59
  operationId: string;
41
60
  awaited?: true | undefined;
42
61
  settled?: "succeeded" | "reconciliation_needed" | undefined;
62
+ redirect?: {
63
+ step: "authentication" | "payment_method" | "invoice_payment" | "checkout";
64
+ destination: "stripe" | "billing_web";
65
+ method_kind?: "card" | "alipay" | "other" | undefined;
66
+ } | undefined;
43
67
  }>;
44
68
  export type BillingOperationPointer = z.infer<typeof PointerSchema>;
45
69
  export declare function operationPointerKey(scope: BillingScope): string;
@@ -19,7 +19,20 @@ const PointerSchema = z.object({
19
19
  /** This tab issued the operation and has not yet seen it succeed. */
20
20
  awaited: z.literal(true).optional(),
21
21
  /** The end this tab saw it reach, kept only by a store that retains settled pointers. */
22
- settled: z.enum(['succeeded', 'reconciliation_needed']).optional()
22
+ settled: z.enum(['succeeded', 'reconciliation_needed']).optional(),
23
+ /** The hosted step this page redirected to; the page it returns to reports the return. */
24
+ redirect: z
25
+ .object({
26
+ destination: z.enum(['stripe', 'billing_web']),
27
+ step: z.enum([
28
+ 'authentication',
29
+ 'payment_method',
30
+ 'invoice_payment',
31
+ 'checkout'
32
+ ]),
33
+ method_kind: z.enum(['card', 'alipay', 'other']).optional()
34
+ })
35
+ .optional()
23
36
  });
24
37
  export function operationPointerKey(scope) {
25
38
  return `${POINTER_KEY_PREFIX}:${scope.userId}:${scope.workspaceId}`;
@@ -1,4 +1,4 @@
1
- import type { z } from 'zod';
1
+ import { z } from 'zod';
2
2
  import type { BillingScope } from './billingScope.js';
3
3
  export declare const BillingOpStatusSchema: z.ZodObject<{
4
4
  action_url: z.ZodOptional<z.ZodString>;
@@ -26,6 +26,101 @@ export declare const BillingOpStatusSchema: z.ZodObject<{
26
26
  } & {
27
27
  amount_charged_cents: z.ZodOptional<z.ZodNumber>;
28
28
  credits_added: z.ZodOptional<z.ZodNumber>;
29
+ charge_breakdown: z.ZodOptional<z.ZodObject<{
30
+ currency: z.ZodString;
31
+ prorated: z.ZodBoolean;
32
+ } & {
33
+ amount_charged_cents: z.ZodNumber;
34
+ reasons: z.ZodArray<z.ZodObject<{
35
+ kind: z.ZodEnum<["promo_code", "subscription_discount", "account_balance"]>;
36
+ } & {
37
+ amount_cents: z.ZodNumber;
38
+ discount: z.ZodOptional<z.ZodObject<{
39
+ code: z.ZodString;
40
+ duration: z.ZodOptional<z.ZodEnum<["once", "repeating", "forever"]>>;
41
+ kind: z.ZodEnum<["plan", "promotion"]>;
42
+ name: z.ZodOptional<z.ZodString>;
43
+ term: z.ZodOptional<z.ZodEnum<["this_payment", "first_month", "first_year", "months", "ongoing"]>>;
44
+ } & {
45
+ amount_off_cents: z.ZodOptional<z.ZodNumber>;
46
+ duration_in_months: z.ZodOptional<z.ZodNumber>;
47
+ }, "strip", z.ZodTypeAny, {
48
+ code: string;
49
+ kind: "plan" | "promotion";
50
+ name?: string | undefined;
51
+ amount_off_cents?: number | undefined;
52
+ duration?: "once" | "repeating" | "forever" | undefined;
53
+ duration_in_months?: number | undefined;
54
+ term?: "this_payment" | "first_month" | "first_year" | "months" | "ongoing" | undefined;
55
+ }, {
56
+ code: string;
57
+ kind: "plan" | "promotion";
58
+ name?: string | undefined;
59
+ amount_off_cents?: number | undefined;
60
+ duration?: "once" | "repeating" | "forever" | undefined;
61
+ duration_in_months?: number | undefined;
62
+ term?: "this_payment" | "first_month" | "first_year" | "months" | "ongoing" | undefined;
63
+ }>>;
64
+ }, "strip", z.ZodTypeAny, {
65
+ kind: "promo_code" | "subscription_discount" | "account_balance";
66
+ amount_cents: number;
67
+ discount?: {
68
+ code: string;
69
+ kind: "plan" | "promotion";
70
+ name?: string | undefined;
71
+ amount_off_cents?: number | undefined;
72
+ duration?: "once" | "repeating" | "forever" | undefined;
73
+ duration_in_months?: number | undefined;
74
+ term?: "this_payment" | "first_month" | "first_year" | "months" | "ongoing" | undefined;
75
+ } | undefined;
76
+ }, {
77
+ kind: "promo_code" | "subscription_discount" | "account_balance";
78
+ amount_cents: number;
79
+ discount?: {
80
+ code: string;
81
+ kind: "plan" | "promotion";
82
+ name?: string | undefined;
83
+ amount_off_cents?: number | undefined;
84
+ duration?: "once" | "repeating" | "forever" | undefined;
85
+ duration_in_months?: number | undefined;
86
+ term?: "this_payment" | "first_month" | "first_year" | "months" | "ongoing" | undefined;
87
+ } | undefined;
88
+ }>, "many">;
89
+ }, "strip", z.ZodTypeAny, {
90
+ currency: string;
91
+ amount_charged_cents: number;
92
+ prorated: boolean;
93
+ reasons: {
94
+ kind: "promo_code" | "subscription_discount" | "account_balance";
95
+ amount_cents: number;
96
+ discount?: {
97
+ code: string;
98
+ kind: "plan" | "promotion";
99
+ name?: string | undefined;
100
+ amount_off_cents?: number | undefined;
101
+ duration?: "once" | "repeating" | "forever" | undefined;
102
+ duration_in_months?: number | undefined;
103
+ term?: "this_payment" | "first_month" | "first_year" | "months" | "ongoing" | undefined;
104
+ } | undefined;
105
+ }[];
106
+ }, {
107
+ currency: string;
108
+ amount_charged_cents: number;
109
+ prorated: boolean;
110
+ reasons: {
111
+ kind: "promo_code" | "subscription_discount" | "account_balance";
112
+ amount_cents: number;
113
+ discount?: {
114
+ code: string;
115
+ kind: "plan" | "promotion";
116
+ name?: string | undefined;
117
+ amount_off_cents?: number | undefined;
118
+ duration?: "once" | "repeating" | "forever" | undefined;
119
+ duration_in_months?: number | undefined;
120
+ term?: "this_payment" | "first_month" | "first_year" | "months" | "ongoing" | undefined;
121
+ } | undefined;
122
+ }[];
123
+ }>>;
29
124
  }, "strip", z.ZodTypeAny, {
30
125
  status: "failed" | "pending" | "succeeded" | "reconciliation_needed";
31
126
  id: string;
@@ -40,6 +135,24 @@ export declare const BillingOpStatusSchema: z.ZodObject<{
40
135
  payment_intent_client_secret?: string | undefined;
41
136
  amount_charged_cents?: number | undefined;
42
137
  authentication_state?: "requires_action" | "processing" | "failed_retryable" | "succeeded" | "reconciliation_needed" | undefined;
138
+ charge_breakdown?: {
139
+ currency: string;
140
+ amount_charged_cents: number;
141
+ prorated: boolean;
142
+ reasons: {
143
+ kind: "promo_code" | "subscription_discount" | "account_balance";
144
+ amount_cents: number;
145
+ discount?: {
146
+ code: string;
147
+ kind: "plan" | "promotion";
148
+ name?: string | undefined;
149
+ amount_off_cents?: number | undefined;
150
+ duration?: "once" | "repeating" | "forever" | undefined;
151
+ duration_in_months?: number | undefined;
152
+ term?: "this_payment" | "first_month" | "first_year" | "months" | "ongoing" | undefined;
153
+ } | undefined;
154
+ }[];
155
+ } | undefined;
43
156
  credits_added?: number | undefined;
44
157
  decline_reason?: "card_declined" | "insufficient_funds" | "expired_card" | "incorrect_cvc" | "authentication_required" | "authentication_failed" | "processing_error" | "payment_not_completed" | "generic" | undefined;
45
158
  phase?: "in_progress" | "awaiting_payment_method" | "awaiting_invoice_payment" | undefined;
@@ -59,6 +172,24 @@ export declare const BillingOpStatusSchema: z.ZodObject<{
59
172
  payment_intent_client_secret?: string | undefined;
60
173
  amount_charged_cents?: number | undefined;
61
174
  authentication_state?: "requires_action" | "processing" | "failed_retryable" | "succeeded" | "reconciliation_needed" | undefined;
175
+ charge_breakdown?: {
176
+ currency: string;
177
+ amount_charged_cents: number;
178
+ prorated: boolean;
179
+ reasons: {
180
+ kind: "promo_code" | "subscription_discount" | "account_balance";
181
+ amount_cents: number;
182
+ discount?: {
183
+ code: string;
184
+ kind: "plan" | "promotion";
185
+ name?: string | undefined;
186
+ amount_off_cents?: number | undefined;
187
+ duration?: "once" | "repeating" | "forever" | undefined;
188
+ duration_in_months?: number | undefined;
189
+ term?: "this_payment" | "first_month" | "first_year" | "months" | "ongoing" | undefined;
190
+ } | undefined;
191
+ }[];
192
+ } | undefined;
62
193
  credits_added?: number | undefined;
63
194
  decline_reason?: "card_declined" | "insufficient_funds" | "expired_card" | "incorrect_cvc" | "authentication_required" | "authentication_failed" | "processing_error" | "payment_not_completed" | "generic" | undefined;
64
195
  phase?: "in_progress" | "awaiting_payment_method" | "awaiting_invoice_payment" | undefined;
@@ -66,6 +197,8 @@ export declare const BillingOpStatusSchema: z.ZodObject<{
66
197
  retryable?: boolean | undefined;
67
198
  }>;
68
199
  export type BillingOpStatus = z.infer<typeof BillingOpStatusSchema>;
200
+ export type BillingChargeBreakdown = NonNullable<BillingOpStatus['charge_breakdown']>;
201
+ export type BillingChargeReason = BillingChargeBreakdown['reasons'][number];
69
202
  export type BillingOperationKind = 'subscription' | 'topup' | 'cancel';
70
203
  /**
71
204
  * Where the customer completes the operation: the challenge this tab drives
@@ -148,6 +281,7 @@ export type FailedBillingOperation = BillingOperationIdentity & {
148
281
  */
149
282
  export interface BillingOperationReceipt {
150
283
  readonly amountChargedCents?: number;
284
+ readonly chargeBreakdown?: BillingChargeBreakdown;
151
285
  readonly creditsAdded?: number;
152
286
  readonly plan?: NonNullable<BillingOpStatus['plan']>;
153
287
  }
@@ -11,11 +11,21 @@
11
11
  * so no server or payment-provider text can reach a consumer through this
12
12
  * state.
13
13
  */
14
- import { zBillingOpStatusResponse } from '@comfyorg/ingest-types/zod';
14
+ import { zBillingOpChargeBreakdown, zBillingOpChargeReason, zBillingOpStatusResponse } from '@comfyorg/ingest-types/zod';
15
+ import { z } from 'zod';
16
+ import { SubscriptionDiscountSchema } from './subscriptionDiscount.js';
15
17
  import { wireCents } from './wireCents.js';
18
+ const ChargeBreakdownSchema = zBillingOpChargeBreakdown.extend({
19
+ amount_charged_cents: wireCents,
20
+ reasons: z.array(zBillingOpChargeReason.extend({
21
+ amount_cents: wireCents,
22
+ discount: SubscriptionDiscountSchema.optional()
23
+ }))
24
+ });
16
25
  export const BillingOpStatusSchema = zBillingOpStatusResponse.extend({
17
26
  amount_charged_cents: wireCents.optional(),
18
- credits_added: wireCents.optional()
27
+ credits_added: wireCents.optional(),
28
+ charge_breakdown: ChargeBreakdownSchema.optional()
19
29
  });
20
30
  /**
21
31
  * The phases the contract defines as blocked on the customer. Neither advances
@@ -112,6 +122,9 @@ function receiptOf(status) {
112
122
  ...(status.amount_charged_cents === undefined
113
123
  ? {}
114
124
  : { amountChargedCents: status.amount_charged_cents }),
125
+ ...(status.charge_breakdown === undefined
126
+ ? {}
127
+ : { chargeBreakdown: status.charge_breakdown }),
115
128
  ...(status.credits_added === undefined
116
129
  ? {}
117
130
  : { creditsAdded: status.credits_added }),
@@ -0,0 +1,17 @@
1
+ import type { BillingDeclineReason, BillingOperationState } from './operationState.js';
2
+ export type PaymentFrictionSignal = {
3
+ readonly stage: 'challenge_required';
4
+ } | {
5
+ readonly stage: 'challenge_completed';
6
+ } | {
7
+ readonly stage: 'challenge_failed';
8
+ readonly declineReason?: BillingDeclineReason;
9
+ };
10
+ /**
11
+ * What one transition of a pending operation put in the customer's way: a
12
+ * challenge first required under a client secret, this tab's verdict on it,
13
+ * or a retryable decline with a reason the operation did not hold before.
14
+ * The first decline the server reports for a challenge this tab already saw
15
+ * fail is the same failure, so it is not reported twice.
16
+ */
17
+ export declare function paymentFrictionBetween(before: BillingOperationState | undefined, after: BillingOperationState): readonly PaymentFrictionSignal[];