reveclicat 0.2.0 → 0.3.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/dist/index.d.ts CHANGED
@@ -41,11 +41,14 @@ type CancelReason = (typeof CANCEL_REASONS)[number];
41
41
  declare const EXPIRATION_REASONS: readonly ["UNSUBSCRIBE", "BILLING_ERROR", "DEVELOPER_INITIATED", "PRICE_INCREASE", "CUSTOMER_SUPPORT", "UNKNOWN", "SUBSCRIPTION_PAUSED"];
42
42
  type ExpirationReason = (typeof EXPIRATION_REASONS)[number];
43
43
  /** CLI-facing store names (lowercase) → RevenueCat `store` values. Generator supports these; receivers accept every store. */
44
- declare const CLI_STORES: readonly ["app_store", "play_store"];
44
+ declare const CLI_STORES: readonly ["app_store", "play_store", "stripe"];
45
45
  type CliStore = (typeof CLI_STORES)[number];
46
46
  declare const CLI_STORE_TO_STORE: Record<CliStore, Store>;
47
47
  /** Default product id per store (Play uses RevenueCat's `<subscription_id>:<base_plan_id>` format). */
48
48
  declare const DEFAULT_PRODUCT_ID: Record<CliStore, string>;
49
+ /** Events a store never emits (official store-compatibility table, docs/payload-sources.md). */
50
+ declare const UNSUPPORTED_EVENTS_BY_STORE: Partial<Record<CliStore, readonly EventType[]>>;
51
+ declare const STORE_LABEL: Record<CliStore, string>;
49
52
 
50
53
  /**
51
54
  * Zod schemas for RevenueCat webhook events (v0.1: 7 event types).
@@ -2040,6 +2043,7 @@ declare const ScenarioSchema: z.ZodObject<{
2040
2043
  store: z.ZodDefault<z.ZodEnum<{
2041
2044
  app_store: "app_store";
2042
2045
  play_store: "play_store";
2046
+ stripe: "stripe";
2043
2047
  }>>;
2044
2048
  environment: z.ZodDefault<z.ZodEnum<{
2045
2049
  SANDBOX: "SANDBOX";
@@ -2108,15 +2112,17 @@ interface TransitionContext {
2108
2112
  hasTrial: boolean;
2109
2113
  /** State to return to on UNCANCELLATION (the state before the CANCELLATION). */
2110
2114
  resumeState: "trial" | "active";
2115
+ /** Store being simulated; some stores never emit some events. */
2116
+ store?: CliStore | undefined;
2111
2117
  }
2112
2118
  declare class IllegalTransitionError extends RccError {
2113
2119
  readonly state: SubscriptionState;
2114
2120
  readonly event: EventType;
2115
2121
  readonly legal: readonly EventType[];
2116
- constructor(state: SubscriptionState, event: EventType);
2122
+ constructor(state: SubscriptionState, event: EventType, store?: CliStore);
2117
2123
  }
2118
- /** Events that may legally follow `state`, in display order. */
2119
- declare function legalEvents(state: SubscriptionState): EventType[];
2124
+ /** Events that may legally follow `state`, in display order; `store` removes events that store never emits. */
2125
+ declare function legalEvents(state: SubscriptionState, store?: CliStore): EventType[];
2120
2126
  /** Pure transition. Throws IllegalTransitionError; never returns an incoherent state. */
2121
2127
  declare function transition(state: SubscriptionState, event: EventType, ctx: TransitionContext): SubscriptionState;
2122
2128
 
@@ -2161,6 +2167,8 @@ declare class Subscriber {
2161
2167
  private readonly cliStore;
2162
2168
  /** Play: number of renewals on the current order (drives the `..N` suffix). */
2163
2169
  private renewalIndex;
2170
+ /** A BILLING_ISSUE is open (until a RENEWAL recovers it or the subscription expires). */
2171
+ private billingRetry;
2164
2172
  private readonly environment;
2165
2173
  private readonly price;
2166
2174
  private readonly currency;
@@ -2182,9 +2190,10 @@ declare class Subscriber {
2182
2190
  /** Emit an event: check legality, time guards, build + validate payload, commit state. */
2183
2191
  emit(type: EventType, overrides?: Record<string, unknown>): Event;
2184
2192
  private digits;
2185
- /** A brand-new order/transaction id in the store's format (see specs/F7-google-play.md). */
2193
+ private alnum;
2194
+ /** A brand-new order/transaction id in the store's format (specs/F7-google-play.md, specs/F8-stripe.md). */
2186
2195
  private newTransactionId;
2187
- /** Renewal id: Play appends `..N` to the original order id; App Store issues a fresh transaction id. */
2196
+ /** Renewal id: Play appends `..N`; Stripe keeps the subscription item id; App Store issues a fresh transaction id. */
2188
2197
  private renewalTransactionId;
2189
2198
  private draftFor;
2190
2199
  private buildPayload;
@@ -2254,6 +2263,7 @@ declare const ConfigSchema: z.ZodObject<{
2254
2263
  store: z.ZodOptional<z.ZodEnum<{
2255
2264
  app_store: "app_store";
2256
2265
  play_store: "play_store";
2266
+ stripe: "stripe";
2257
2267
  }>>;
2258
2268
  environment: z.ZodOptional<z.ZodEnum<{
2259
2269
  SANDBOX: "SANDBOX";
@@ -2264,4 +2274,4 @@ type Config = z.infer<typeof ConfigSchema>;
2264
2274
  /** Read `reveclicat.config.json` from `dir` (default cwd). Missing file → {}. Invalid → RccError. */
2265
2275
  declare function loadConfig(dir?: string): Config;
2266
2276
 
2267
- export { type BillingIssueEvent, BillingIssueEventSchema, CANCEL_REASONS, CLI_STORES, CLI_STORE_TO_STORE, type CancelReason, type CancellationEvent, CancellationEventSchema, type CliStore, type Config, ConfigSchema, DEFAULT_PRODUCT_ID, type Duration, ENVIRONMENTS, EVENT_SCHEMAS, EVENT_TYPES, EXPIRATION_REASONS, type EnvelopeClassification, type Environment, type Event, type EventResult, EventSchema, type EventType, ExperimentSchema, type ExpirationEvent, ExpirationEventSchema, type ExpirationReason, IllegalTransitionError, type InitialPurchaseEvent, InitialPurchaseEventSchema, type LifecycleEvent, type LoadedScenario, PERIOD_TYPES, type PeriodType, PrematureEventError, RccError, type RenewalEvent, RenewalEventSchema, type Rng, type RunOptions, type RunResult, STORES, type Scenario, ScenarioSchema, ScenarioValidationError, type Step, type Store, Subscriber, SubscriberAttributeSchema, type SubscriberOptions, type SubscriptionState, type TestEvent, TestEventSchema, type UncancellationEvent, UncancellationEventSchema, type UnknownEvent, UnknownEventSchema, UnknownWebhookEnvelopeSchema, VirtualClock, type WebhookEnvelope, WebhookEnvelopeSchema, addDuration, buildProgram, classifyEnvelope, createRng, createSimulation, formatDuration, legalEvents, loadConfig, loadScenario, loadScenarioWithSource, parseDuration, parseScenario, postEvent, runScenario, transition };
2277
+ export { type BillingIssueEvent, BillingIssueEventSchema, CANCEL_REASONS, CLI_STORES, CLI_STORE_TO_STORE, type CancelReason, type CancellationEvent, CancellationEventSchema, type CliStore, type Config, ConfigSchema, DEFAULT_PRODUCT_ID, type Duration, ENVIRONMENTS, EVENT_SCHEMAS, EVENT_TYPES, EXPIRATION_REASONS, type EnvelopeClassification, type Environment, type Event, type EventResult, EventSchema, type EventType, ExperimentSchema, type ExpirationEvent, ExpirationEventSchema, type ExpirationReason, IllegalTransitionError, type InitialPurchaseEvent, InitialPurchaseEventSchema, type LifecycleEvent, type LoadedScenario, PERIOD_TYPES, type PeriodType, PrematureEventError, RccError, type RenewalEvent, RenewalEventSchema, type Rng, type RunOptions, type RunResult, STORES, STORE_LABEL, type Scenario, ScenarioSchema, ScenarioValidationError, type Step, type Store, Subscriber, SubscriberAttributeSchema, type SubscriberOptions, type SubscriptionState, type TestEvent, TestEventSchema, UNSUPPORTED_EVENTS_BY_STORE, type UncancellationEvent, UncancellationEventSchema, type UnknownEvent, UnknownEventSchema, UnknownWebhookEnvelopeSchema, VirtualClock, type WebhookEnvelope, WebhookEnvelopeSchema, addDuration, buildProgram, classifyEnvelope, createRng, createSimulation, formatDuration, legalEvents, loadConfig, loadScenario, loadScenarioWithSource, parseDuration, parseScenario, postEvent, runScenario, transition };
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { Command } from "commander";
4
4
  // package.json
5
5
  var package_default = {
6
6
  name: "reveclicat",
7
- version: "0.2.0",
7
+ version: "0.3.0",
8
8
  description: "Unofficial CLI to simulate RevenueCat subscription lifecycles and test webhooks locally and in CI. Not affiliated with RevenueCat, Inc.",
9
9
  type: "module",
10
10
  license: "MIT",
@@ -157,9 +157,17 @@ var CANCEL_REASONS = [
157
157
  "UNKNOWN"
158
158
  ];
159
159
  var EXPIRATION_REASONS = [...CANCEL_REASONS, "SUBSCRIPTION_PAUSED"];
160
- var CLI_STORES = ["app_store", "play_store"];
161
- var CLI_STORE_TO_STORE = { app_store: "APP_STORE", play_store: "PLAY_STORE" };
162
- var DEFAULT_PRODUCT_ID = { app_store: "com.example.premium.monthly", play_store: "com.example.premium:monthly" };
160
+ var CLI_STORES = ["app_store", "play_store", "stripe"];
161
+ var CLI_STORE_TO_STORE = { app_store: "APP_STORE", play_store: "PLAY_STORE", stripe: "STRIPE" };
162
+ var DEFAULT_PRODUCT_ID = {
163
+ app_store: "com.example.premium.monthly",
164
+ play_store: "com.example.premium:monthly",
165
+ stripe: "prod_RccPremiumMonthly"
166
+ };
167
+ var UNSUPPORTED_EVENTS_BY_STORE = {
168
+ stripe: ["UNCANCELLATION", "TEST"]
169
+ };
170
+ var STORE_LABEL = { app_store: "App Store", play_store: "Google Play", stripe: "Stripe" };
163
171
 
164
172
  // src/core/config.ts
165
173
  var CONFIG_FILE = "reveclicat.config.json";
@@ -414,10 +422,11 @@ var IllegalTransitionError = class extends RccError {
414
422
  state;
415
423
  event;
416
424
  legal;
417
- constructor(state, event) {
418
- const legal = legalEvents(state);
425
+ constructor(state, event, store) {
426
+ const legal = legalEvents(state, store);
427
+ const unsupported = store !== void 0 && (UNSUPPORTED_EVENTS_BY_STORE[store] ?? []).includes(event);
419
428
  super(
420
- `Illegal transition: cannot apply ${event} while the subscription is "${state}". Legal events from "${state}": ${legal.join(", ")}.`,
429
+ unsupported ? `${STORE_LABEL[store]} does not emit ${event} (RevenueCat store compatibility table). Legal events from "${state}" for ${STORE_LABEL[store]}: ${legal.join(", ")}.` : `Illegal transition: cannot apply ${event} while the subscription is "${state}". Legal events from "${state}": ${legal.join(", ")}.`,
421
430
  { hint: "Check the order of the steps in your scenario (e.g. a RENEWAL needs an INITIAL_PURCHASE first)." }
422
431
  );
423
432
  this.name = "IllegalTransitionError";
@@ -442,6 +451,9 @@ var TABLE = {
442
451
  },
443
452
  cancelled_pending_expiration: {
444
453
  UNCANCELLATION: (ctx) => ctx.resumeState,
454
+ // Real flows: recovery after a BILLING_ERROR cancellation (Stripe capture 2026-08-29); App Store "CANCELLATION
455
+ // followed by a RENEWAL" when cancelling <24 h before a trial ends (docs S4).
456
+ RENEWAL: () => "active",
445
457
  EXPIRATION: () => "expired"
446
458
  },
447
459
  billing_issue: {
@@ -451,13 +463,17 @@ var TABLE = {
451
463
  },
452
464
  expired: { INITIAL_PURCHASE: () => "active" }
453
465
  };
454
- function legalEvents(state) {
455
- return [...Object.keys(TABLE[state]), "TEST"];
466
+ function legalEvents(state, store) {
467
+ const excluded = store === void 0 ? [] : UNSUPPORTED_EVENTS_BY_STORE[store] ?? [];
468
+ return [...Object.keys(TABLE[state]), "TEST"].filter((e) => !excluded.includes(e));
456
469
  }
457
470
  function transition(state, event, ctx) {
471
+ if (ctx.store !== void 0 && (UNSUPPORTED_EVENTS_BY_STORE[ctx.store] ?? []).includes(event)) {
472
+ throw new IllegalTransitionError(state, event, ctx.store);
473
+ }
458
474
  if (event === "TEST") return state;
459
475
  const rule = TABLE[state][event];
460
- if (!rule) throw new IllegalTransitionError(state, event);
476
+ if (!rule) throw new IllegalTransitionError(state, event, ctx.store);
461
477
  return rule(ctx);
462
478
  }
463
479
 
@@ -624,6 +640,8 @@ var Subscriber = class {
624
640
  cliStore;
625
641
  /** Play: number of renewals on the current order (drives the `..N` suffix). */
626
642
  renewalIndex = 0;
643
+ /** A BILLING_ISSUE is open (until a RENEWAL recovers it or the subscription expires). */
644
+ billingRetry = false;
627
645
  environment;
628
646
  price;
629
647
  currency;
@@ -648,7 +666,7 @@ var Subscriber = class {
648
666
  /** Emit an event: check legality, time guards, build + validate payload, commit state. */
649
667
  emit(type, overrides = {}) {
650
668
  const from = this._state;
651
- const next = transition(from, type, { hasTrial: this.trial !== void 0, resumeState: this.resumeState });
669
+ const next = transition(from, type, { hasTrial: this.trial !== void 0, resumeState: this.resumeState, store: this.cliStore });
652
670
  const now = this.deps.clock.now();
653
671
  if (type === "EXPIRATION") {
654
672
  const due = Math.max(this.expirationAtMs ?? 0, from === "billing_issue" ? this.gracePeriodExpirationAtMs ?? 0 : 0);
@@ -671,6 +689,8 @@ var Subscriber = class {
671
689
  this.periodType = draft.periodType;
672
690
  this.gracePeriodExpirationAtMs = draft.gracePeriodExpirationAtMs;
673
691
  this.renewalIndex = draft.renewalIndex;
692
+ if (type === "BILLING_ISSUE") this.billingRetry = true;
693
+ if (type === "RENEWAL" || type === "EXPIRATION" || type === "INITIAL_PURCHASE") this.billingRetry = false;
674
694
  if (type === "CANCELLATION") this.resumeState = from === "trial" ? "trial" : "active";
675
695
  this._state = next;
676
696
  }
@@ -684,15 +704,23 @@ var Subscriber = class {
684
704
  for (let i = 0; i < n; i++) s += String(this.deps.rng.int(10));
685
705
  return s;
686
706
  }
687
- /** A brand-new order/transaction id in the store's format (see specs/F7-google-play.md). */
707
+ alnum(n) {
708
+ const A = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
709
+ let s = "";
710
+ for (let i = 0; i < n; i++) s += A[this.deps.rng.int(A.length)];
711
+ return s;
712
+ }
713
+ /** A brand-new order/transaction id in the store's format (specs/F7-google-play.md, specs/F8-stripe.md). */
688
714
  newTransactionId() {
715
+ if (this.cliStore === "stripe") return `si_${this.alnum(14)}`;
689
716
  if (this.cliStore === "play_store") {
690
717
  return `GPA.${this.digits(4)}-${this.digits(4)}-${this.digits(4)}-${this.digits(5)}`;
691
718
  }
692
719
  return String(1 + this.deps.rng.int(9)) + this.digits(15);
693
720
  }
694
- /** Renewal id: Play appends `..N` to the original order id; App Store issues a fresh transaction id. */
721
+ /** Renewal id: Play appends `..N`; Stripe keeps the subscription item id; App Store issues a fresh transaction id. */
695
722
  renewalTransactionId(originalId, index) {
723
+ if (this.cliStore === "stripe" && originalId !== void 0) return originalId;
696
724
  if (this.cliStore === "play_store" && originalId !== void 0) return `${originalId}..${index}`;
697
725
  return this.newTransactionId();
698
726
  }
@@ -710,7 +738,7 @@ var Subscriber = class {
710
738
  case "INITIAL_PURCHASE": {
711
739
  const startsTrial = from === "none" && this.trial !== void 0;
712
740
  d.transactionId = this.newTransactionId();
713
- if (this.cliStore === "play_store") d.originalTransactionId = d.transactionId;
741
+ if (this.cliStore !== "app_store") d.originalTransactionId = d.transactionId;
714
742
  else d.originalTransactionId ??= d.transactionId;
715
743
  d.renewalIndex = 0;
716
744
  d.purchasedAtMs = now;
@@ -720,8 +748,13 @@ var Subscriber = class {
720
748
  break;
721
749
  }
722
750
  case "RENEWAL": {
723
- const start = d.expirationAtMs ?? now;
724
751
  d.transactionId = this.renewalTransactionId(d.originalTransactionId, d.renewalIndex);
752
+ if (this.cliStore === "stripe" && this.billingRetry) {
753
+ d.periodType = "NORMAL";
754
+ d.gracePeriodExpirationAtMs = null;
755
+ break;
756
+ }
757
+ const start = d.expirationAtMs ?? now;
725
758
  d.renewalIndex += 1;
726
759
  d.purchasedAtMs = start;
727
760
  d.expirationAtMs = addDuration(start, this.period);
@@ -730,7 +763,13 @@ var Subscriber = class {
730
763
  break;
731
764
  }
732
765
  case "BILLING_ISSUE":
733
- d.gracePeriodExpirationAtMs = addDuration(now, this.grace);
766
+ if (this.cliStore === "stripe") {
767
+ d.expirationAtMs = addDuration(d.expirationAtMs ?? now, this.period);
768
+ d.renewalIndex += 1;
769
+ d.gracePeriodExpirationAtMs = null;
770
+ } else {
771
+ d.gracePeriodExpirationAtMs = addDuration(now, this.grace);
772
+ }
734
773
  break;
735
774
  case "TEST":
736
775
  if (from === "none") {
@@ -748,6 +787,7 @@ var Subscriber = class {
748
787
  buildPayload(type, from, now, d) {
749
788
  const isPurchase = type === "INITIAL_PURCHASE" || type === "RENEWAL" || type === "TEST";
750
789
  const price = isPurchase && d.periodType !== "TRIAL" ? this.price : 0;
790
+ const stripe = this.cliStore === "stripe";
751
791
  const payload = {
752
792
  type,
753
793
  id: this.deps.rng.uuid(),
@@ -768,16 +808,17 @@ var Subscriber = class {
768
808
  transaction_id: d.transactionId,
769
809
  original_transaction_id: d.originalTransactionId,
770
810
  is_family_share: false,
771
- country_code: this.countryCode,
811
+ country_code: stripe ? null : this.countryCode,
772
812
  store: this.store,
773
813
  currency: this.currency,
774
814
  price,
775
815
  price_in_purchased_currency: price,
776
816
  tax_percentage: 0,
777
- commission_percentage: 0.3,
778
- takehome_percentage: 0.7,
817
+ commission_percentage: stripe ? 0 : 0.3,
818
+ takehome_percentage: stripe ? 1 : 0.7,
779
819
  offer_code: null
780
820
  };
821
+ if (stripe) payload["renewal_number"] = d.renewalIndex + 1;
781
822
  switch (type) {
782
823
  case "RENEWAL":
783
824
  payload["is_trial_conversion"] = from === "trial";
@@ -1934,11 +1975,13 @@ export {
1934
1975
  RccError,
1935
1976
  RenewalEventSchema,
1936
1977
  STORES,
1978
+ STORE_LABEL,
1937
1979
  ScenarioSchema,
1938
1980
  ScenarioValidationError,
1939
1981
  Subscriber,
1940
1982
  SubscriberAttributeSchema,
1941
1983
  TestEventSchema,
1984
+ UNSUPPORTED_EVENTS_BY_STORE,
1942
1985
  UncancellationEventSchema,
1943
1986
  UnknownEventSchema,
1944
1987
  UnknownWebhookEnvelopeSchema,