reveclicat 0.1.1 → 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/CHANGELOG.md CHANGED
@@ -4,6 +4,23 @@ All notable changes to this project are documented here. Format based on [Keep a
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.3.0] — 2026-08-29
8
+
9
+ ### Added
10
+ - Stripe in the generator: `--store stripe` / `subscriber.store: stripe` — constant `si_…` transaction id, `prod_…` product id default, `renewal_number` on every event, `BILLING_ISSUE` with the period pre-extended and `grace_period_expiration_at_ms: null`, recovery `RENEWAL` that does not re-extend, Stripe money fields (commission 0, takehome 1), `country_code: null`. `UNCANCELLATION` and `TEST` are rejected for Stripe with a store-specific message (official compatibility table). All rules derived from a real captured lifecycle (`test/fixtures/events/real/*.stripe.json`). New example `scenarios/stripe-billing-issue-recovers.yaml`. (T-080–T-083)
11
+ - State machine: `legalEvents(state, store)` / `transition(..., { store })` — legality now has a store dimension. (T-082)
12
+
13
+ ### Changed
14
+ - `RENEWAL` is now legal from `cancelled_pending_expiration` (recovery after a `BILLING_ERROR` cancellation, as observed in real Stripe traffic and described for App Store in the official flows). (T-082)
15
+
16
+ ## [0.2.0] — 2026-08-29
17
+
18
+ ### Added
19
+ - Google Play in the generator: `--store play_store` / `subscriber.store: play_store` emit `store: PLAY_STORE`, `GPA.dddd-dddd-dddd-ddddd` order ids with `..N` renewal suffixes (Google's `Purchase.getOrderId` convention), a new order on resubscription, and the `<subscription_id>:<base_plan_id>` default product id. New example `scenarios/play-trial-converts.yaml` (`rcc init` now copies 7 scenarios). (T-071, T-072)
20
+
21
+ ### Changed
22
+ - `product_id` no longer has a default in the scenario schema / `--product`; the per-store default is applied by the generator. Explicit values are unchanged. (T-071)
23
+
7
24
  ## [0.1.1] — 2026-08-29
8
25
 
9
26
  ### Fixed
@@ -41,6 +58,8 @@ First release. Unofficial project — not affiliated with RevenueCat, Inc.
41
58
  - Consistent error output: every error prints `✖ message` + `→ hint`; usage errors (unknown command/option, missing argument) exit with code 2, other failures with 1; `RCC_DEBUG=1` shows stack traces; `NO_COLOR` honoured. (T-052)
42
59
  - Programmatic API (`reveclicat` package): schemas, `Subscriber`, `runScenario`, `loadScenario`, `VirtualClock`, `createRng`.
43
60
 
44
- [Unreleased]: https://github.com/RadW2020/ReveCliCat/compare/v0.1.1...HEAD
61
+ [Unreleased]: https://github.com/RadW2020/ReveCliCat/compare/v0.3.0...HEAD
62
+ [0.3.0]: https://github.com/RadW2020/ReveCliCat/compare/v0.2.0...v0.3.0
63
+ [0.2.0]: https://github.com/RadW2020/ReveCliCat/compare/v0.1.1...v0.2.0
45
64
  [0.1.1]: https://github.com/RadW2020/ReveCliCat/compare/v0.1.0...v0.1.1
46
65
  [0.1.0]: https://github.com/RadW2020/ReveCliCat/releases/tag/v0.1.0
package/README.md CHANGED
@@ -94,7 +94,7 @@ subscriber: # all optional
94
94
  period: P1M # ISO-8601 duration
95
95
  trial: P1W # omit → no trial
96
96
  grace_period: P16D # billing-retry window after BILLING_ISSUE
97
- store: app_store # v0.1: app_store only
97
+ store: app_store # app_store | play_store | stripe (store-shaped ids, defaults and legal events)
98
98
  environment: SANDBOX # SANDBOX | PRODUCTION
99
99
  steps:
100
100
  - event: INITIAL_PURCHASE # starts the trial (period_type: TRIAL, price 0)
@@ -115,7 +115,7 @@ expect:
115
115
 
116
116
  Rules: a step is exactly one of `event` or `advance`; unknown keys are errors; validation errors point at `file:line:column`. Illegal transitions stop the run with the step number and the list of legal events. `EXPIRATION` is only allowed once the virtual clock has reached `expiration_at_ms` (or the end of the grace period) — the error tells you exactly how much to `advance`.
117
117
 
118
- Shipped examples (`rcc init` copies them): `trial-converts`, `trial-churns`, `billing-issue-recovers`, `billing-issue-churns`, `cancel-then-uncancel`, `happy-year` (12 renewals).
118
+ Shipped examples (`rcc init` copies them): `trial-converts`, `trial-churns`, `billing-issue-recovers`, `billing-issue-churns`, `cancel-then-uncancel`, `happy-year` (12 renewals), `play-trial-converts` (Google Play ids), `stripe-billing-issue-recovers` (Stripe dunning flow).
119
119
 
120
120
  ## CI
121
121
 
@@ -174,7 +174,7 @@ none ──INITIAL_PURCHASE──▶ trial ──RENEWAL (conversion)──▶ a
174
174
  ## Fidelity & scope
175
175
 
176
176
  - Schemas, enums and inclusion rules come from the official docs (fetched 2026-08-29) and the official sample payloads are used as test fixtures. The `TEST` event has no published sample, so its schema is marked *provisional* — a captured real one is very welcome (see `docs/BACKLOG.md`, T-004).
177
- - v0.1 models the **App Store** only. Google Play, Stripe, Amazon and Roku stores, a built-in tunnel, a web UI and hosted mode are intentionally out of scope (see the Icebox in `docs/BACKLOG.md`).
177
+ - The generator models **App Store** (`--store app_store`, 16-digit transaction ids, original kept across resubscriptions), **Google Play** (`--store play_store`, `GPA.…` order ids with `..N` renewal suffixes, `<subscription_id>:<base_plan_id>` product ids, new order on resubscription) and **Stripe** (`--store stripe`, one `si_…` id for the whole subscription, `prod_…` product ids, `renewal_number`, billing issues with the period pre-extended and no grace field, no `UNCANCELLATION`/`TEST` — all taken from real captured events). Amazon, Roku and RevenueCat Billing, a built-in tunnel, a web UI and hosted mode are intentionally out of scope (see the Icebox in `docs/BACKLOG.md`). Receivers accept events from every store.
178
178
  - Programmatic use: `import { runScenario, Subscriber, WebhookEnvelopeSchema } from "reveclicat"`.
179
179
 
180
180
  ## How this was built
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ import { Command } from "commander";
9
9
  // package.json
10
10
  var package_default = {
11
11
  name: "reveclicat",
12
- version: "0.1.1",
12
+ version: "0.3.0",
13
13
  description: "Unofficial CLI to simulate RevenueCat subscription lifecycles and test webhooks locally and in CI. Not affiliated with RevenueCat, Inc.",
14
14
  type: "module",
15
15
  license: "MIT",
@@ -165,8 +165,17 @@ var CANCEL_REASONS = [
165
165
  "UNKNOWN"
166
166
  ];
167
167
  var EXPIRATION_REASONS = [...CANCEL_REASONS, "SUBSCRIPTION_PAUSED"];
168
- var CLI_STORES = ["app_store"];
169
- var CLI_STORE_TO_STORE = { app_store: "APP_STORE" };
168
+ var CLI_STORES = ["app_store", "play_store", "stripe"];
169
+ var CLI_STORE_TO_STORE = { app_store: "APP_STORE", play_store: "PLAY_STORE", stripe: "STRIPE" };
170
+ var DEFAULT_PRODUCT_ID = {
171
+ app_store: "com.example.premium.monthly",
172
+ play_store: "com.example.premium:monthly",
173
+ stripe: "prod_RccPremiumMonthly"
174
+ };
175
+ var UNSUPPORTED_EVENTS_BY_STORE = {
176
+ stripe: ["UNCANCELLATION", "TEST"]
177
+ };
178
+ var STORE_LABEL = { app_store: "App Store", play_store: "Google Play", stripe: "Stripe" };
170
179
 
171
180
  // src/core/config.ts
172
181
  var CONFIG_FILE = "reveclicat.config.json";
@@ -421,10 +430,11 @@ var IllegalTransitionError = class extends RccError {
421
430
  state;
422
431
  event;
423
432
  legal;
424
- constructor(state, event) {
425
- const legal = legalEvents(state);
433
+ constructor(state, event, store) {
434
+ const legal = legalEvents(state, store);
435
+ const unsupported = store !== void 0 && (UNSUPPORTED_EVENTS_BY_STORE[store] ?? []).includes(event);
426
436
  super(
427
- `Illegal transition: cannot apply ${event} while the subscription is "${state}". Legal events from "${state}": ${legal.join(", ")}.`,
437
+ 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(", ")}.`,
428
438
  { hint: "Check the order of the steps in your scenario (e.g. a RENEWAL needs an INITIAL_PURCHASE first)." }
429
439
  );
430
440
  this.name = "IllegalTransitionError";
@@ -449,6 +459,9 @@ var TABLE = {
449
459
  },
450
460
  cancelled_pending_expiration: {
451
461
  UNCANCELLATION: (ctx) => ctx.resumeState,
462
+ // Real flows: recovery after a BILLING_ERROR cancellation (Stripe capture 2026-08-29); App Store "CANCELLATION
463
+ // followed by a RENEWAL" when cancelling <24 h before a trial ends (docs S4).
464
+ RENEWAL: () => "active",
452
465
  EXPIRATION: () => "expired"
453
466
  },
454
467
  billing_issue: {
@@ -458,13 +471,17 @@ var TABLE = {
458
471
  },
459
472
  expired: { INITIAL_PURCHASE: () => "active" }
460
473
  };
461
- function legalEvents(state) {
462
- return [...Object.keys(TABLE[state]), "TEST"];
474
+ function legalEvents(state, store) {
475
+ const excluded = store === void 0 ? [] : UNSUPPORTED_EVENTS_BY_STORE[store] ?? [];
476
+ return [...Object.keys(TABLE[state]), "TEST"].filter((e) => !excluded.includes(e));
463
477
  }
464
478
  function transition(state, event, ctx) {
479
+ if (ctx.store !== void 0 && (UNSUPPORTED_EVENTS_BY_STORE[ctx.store] ?? []).includes(event)) {
480
+ throw new IllegalTransitionError(state, event, ctx.store);
481
+ }
465
482
  if (event === "TEST") return state;
466
483
  const rule = TABLE[state][event];
467
- if (!rule) throw new IllegalTransitionError(state, event);
484
+ if (!rule) throw new IllegalTransitionError(state, event, ctx.store);
468
485
  return rule(ctx);
469
486
  }
470
487
 
@@ -609,13 +626,14 @@ var Subscriber = class {
609
626
  this.period = asDuration(opts.period);
610
627
  this.trial = opts.trial === void 0 ? void 0 : asDuration(opts.trial);
611
628
  this.grace = asDuration(opts.gracePeriod ?? "P16D");
612
- this.store = CLI_STORE_TO_STORE[opts.store ?? "app_store"];
629
+ this.cliStore = opts.store ?? "app_store";
630
+ this.store = CLI_STORE_TO_STORE[this.cliStore];
613
631
  this.environment = opts.environment ?? "SANDBOX";
614
632
  this.price = opts.price ?? 9.99;
615
633
  this.currency = opts.currency ?? "USD";
616
634
  this.countryCode = opts.countryCode ?? "US";
617
635
  this.entitlementIds = opts.entitlementIds ?? ["premium"];
618
- this.productId = opts.productId;
636
+ this.productId = opts.productId ?? DEFAULT_PRODUCT_ID[this.cliStore];
619
637
  this.appUserId = opts.appUserId === void 0 || opts.appUserId === "auto" ? `$RCAnonymousID:${deps.rng.hex(32)}` : opts.appUserId;
620
638
  this.appId = opts.appId ?? `app${deps.rng.hex(12)}`;
621
639
  }
@@ -627,6 +645,11 @@ var Subscriber = class {
627
645
  trial;
628
646
  grace;
629
647
  store;
648
+ cliStore;
649
+ /** Play: number of renewals on the current order (drives the `..N` suffix). */
650
+ renewalIndex = 0;
651
+ /** A BILLING_ISSUE is open (until a RENEWAL recovers it or the subscription expires). */
652
+ billingRetry = false;
630
653
  environment;
631
654
  price;
632
655
  currency;
@@ -651,7 +674,7 @@ var Subscriber = class {
651
674
  /** Emit an event: check legality, time guards, build + validate payload, commit state. */
652
675
  emit(type, overrides = {}) {
653
676
  const from = this._state;
654
- const next = transition(from, type, { hasTrial: this.trial !== void 0, resumeState: this.resumeState });
677
+ const next = transition(from, type, { hasTrial: this.trial !== void 0, resumeState: this.resumeState, store: this.cliStore });
655
678
  const now = this.deps.clock.now();
656
679
  if (type === "EXPIRATION") {
657
680
  const due = Math.max(this.expirationAtMs ?? 0, from === "billing_issue" ? this.gracePeriodExpirationAtMs ?? 0 : 0);
@@ -673,6 +696,9 @@ var Subscriber = class {
673
696
  this.expirationAtMs = draft.expirationAtMs;
674
697
  this.periodType = draft.periodType;
675
698
  this.gracePeriodExpirationAtMs = draft.gracePeriodExpirationAtMs;
699
+ this.renewalIndex = draft.renewalIndex;
700
+ if (type === "BILLING_ISSUE") this.billingRetry = true;
701
+ if (type === "RENEWAL" || type === "EXPIRATION" || type === "INITIAL_PURCHASE") this.billingRetry = false;
676
702
  if (type === "CANCELLATION") this.resumeState = from === "trial" ? "trial" : "active";
677
703
  this._state = next;
678
704
  }
@@ -681,11 +707,31 @@ var Subscriber = class {
681
707
  return event;
682
708
  }
683
709
  /* ----------------------------------------------------------- internals */
684
- newTransactionId() {
685
- let s = String(1 + this.deps.rng.int(9));
686
- for (let i = 0; i < 15; i++) s += String(this.deps.rng.int(10));
710
+ digits(n) {
711
+ let s = "";
712
+ for (let i = 0; i < n; i++) s += String(this.deps.rng.int(10));
713
+ return s;
714
+ }
715
+ alnum(n) {
716
+ const A = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
717
+ let s = "";
718
+ for (let i = 0; i < n; i++) s += A[this.deps.rng.int(A.length)];
687
719
  return s;
688
720
  }
721
+ /** A brand-new order/transaction id in the store's format (specs/F7-google-play.md, specs/F8-stripe.md). */
722
+ newTransactionId() {
723
+ if (this.cliStore === "stripe") return `si_${this.alnum(14)}`;
724
+ if (this.cliStore === "play_store") {
725
+ return `GPA.${this.digits(4)}-${this.digits(4)}-${this.digits(4)}-${this.digits(5)}`;
726
+ }
727
+ return String(1 + this.deps.rng.int(9)) + this.digits(15);
728
+ }
729
+ /** Renewal id: Play appends `..N`; Stripe keeps the subscription item id; App Store issues a fresh transaction id. */
730
+ renewalTransactionId(originalId, index) {
731
+ if (this.cliStore === "stripe" && originalId !== void 0) return originalId;
732
+ if (this.cliStore === "play_store" && originalId !== void 0) return `${originalId}..${index}`;
733
+ return this.newTransactionId();
734
+ }
689
735
  draftFor(type, from, now) {
690
736
  const d = {
691
737
  originalTransactionId: this.originalTransactionId,
@@ -693,13 +739,16 @@ var Subscriber = class {
693
739
  purchasedAtMs: this.purchasedAtMs,
694
740
  expirationAtMs: this.expirationAtMs,
695
741
  periodType: this.periodType,
696
- gracePeriodExpirationAtMs: this.gracePeriodExpirationAtMs
742
+ gracePeriodExpirationAtMs: this.gracePeriodExpirationAtMs,
743
+ renewalIndex: this.renewalIndex
697
744
  };
698
745
  switch (type) {
699
746
  case "INITIAL_PURCHASE": {
700
747
  const startsTrial = from === "none" && this.trial !== void 0;
701
748
  d.transactionId = this.newTransactionId();
702
- d.originalTransactionId ??= d.transactionId;
749
+ if (this.cliStore !== "app_store") d.originalTransactionId = d.transactionId;
750
+ else d.originalTransactionId ??= d.transactionId;
751
+ d.renewalIndex = 0;
703
752
  d.purchasedAtMs = now;
704
753
  d.expirationAtMs = addDuration(now, startsTrial ? this.trial : this.period);
705
754
  d.periodType = startsTrial ? "TRIAL" : "NORMAL";
@@ -707,8 +756,14 @@ var Subscriber = class {
707
756
  break;
708
757
  }
709
758
  case "RENEWAL": {
759
+ d.transactionId = this.renewalTransactionId(d.originalTransactionId, d.renewalIndex);
760
+ if (this.cliStore === "stripe" && this.billingRetry) {
761
+ d.periodType = "NORMAL";
762
+ d.gracePeriodExpirationAtMs = null;
763
+ break;
764
+ }
710
765
  const start = d.expirationAtMs ?? now;
711
- d.transactionId = this.newTransactionId();
766
+ d.renewalIndex += 1;
712
767
  d.purchasedAtMs = start;
713
768
  d.expirationAtMs = addDuration(start, this.period);
714
769
  d.periodType = "NORMAL";
@@ -716,7 +771,13 @@ var Subscriber = class {
716
771
  break;
717
772
  }
718
773
  case "BILLING_ISSUE":
719
- d.gracePeriodExpirationAtMs = addDuration(now, this.grace);
774
+ if (this.cliStore === "stripe") {
775
+ d.expirationAtMs = addDuration(d.expirationAtMs ?? now, this.period);
776
+ d.renewalIndex += 1;
777
+ d.gracePeriodExpirationAtMs = null;
778
+ } else {
779
+ d.gracePeriodExpirationAtMs = addDuration(now, this.grace);
780
+ }
720
781
  break;
721
782
  case "TEST":
722
783
  if (from === "none") {
@@ -734,6 +795,7 @@ var Subscriber = class {
734
795
  buildPayload(type, from, now, d) {
735
796
  const isPurchase = type === "INITIAL_PURCHASE" || type === "RENEWAL" || type === "TEST";
736
797
  const price = isPurchase && d.periodType !== "TRIAL" ? this.price : 0;
798
+ const stripe = this.cliStore === "stripe";
737
799
  const payload = {
738
800
  type,
739
801
  id: this.deps.rng.uuid(),
@@ -754,16 +816,17 @@ var Subscriber = class {
754
816
  transaction_id: d.transactionId,
755
817
  original_transaction_id: d.originalTransactionId,
756
818
  is_family_share: false,
757
- country_code: this.countryCode,
819
+ country_code: stripe ? null : this.countryCode,
758
820
  store: this.store,
759
821
  currency: this.currency,
760
822
  price,
761
823
  price_in_purchased_currency: price,
762
824
  tax_percentage: 0,
763
- commission_percentage: 0.3,
764
- takehome_percentage: 0.7,
825
+ commission_percentage: stripe ? 0 : 0.3,
826
+ takehome_percentage: stripe ? 1 : 0.7,
765
827
  offer_code: null
766
828
  };
829
+ if (stripe) payload["renewal_number"] = d.renewalIndex + 1;
767
830
  switch (type) {
768
831
  case "RENEWAL":
769
832
  payload["is_trial_conversion"] = from === "trial";
@@ -974,8 +1037,8 @@ function parseEnvironment(input) {
974
1037
  }
975
1038
  function parseStore(input) {
976
1039
  if (CLI_STORES.includes(input)) return input;
977
- throw new RccError(`Unsupported --store "${input}". v0.1 supports: ${CLI_STORES.join(", ")}.`, {
978
- hint: "Other stores are on the roadmap (see docs/BACKLOG.md \u2192 Icebox)."
1040
+ throw new RccError(`Unsupported --store "${input}". The generator supports: ${CLI_STORES.join(", ")}.`, {
1041
+ hint: "Receivers (listen/tail/inbox) accept events from every store; only generation is limited. Others are in the Icebox."
979
1042
  });
980
1043
  }
981
1044
  function parseSeed(input) {
@@ -999,7 +1062,7 @@ function buildSingleEvent(type, opts) {
999
1062
  return { api_version: "1.0", event };
1000
1063
  }
1001
1064
  function registerSend(program, io) {
1002
- program.command("send").argument("<EVENT_TYPE>", `event to send: ${EVENT_TYPES.join(" | ")}`).description("Send a single, schema-valid RevenueCat webhook event to your endpoint.").option("--to <url>", `target URL (default: ${DEFAULT_TARGET}, or "to" in ${CONFIG_FILE})`).option("--store <store>", `store: ${CLI_STORES.join(" | ")} (default: app_store, or "store" in ${CONFIG_FILE})`).option("--user <app_user_id>", "app_user_id (default: generated $RCAnonymousID)").option("--product <product_id>", "product_id", "com.example.premium.monthly").option("--auth-header <value>", `value sent as the Authorization header (default: "authHeader" in ${CONFIG_FILE})`).option("--environment <env>", `${ENVIRONMENTS.join(" | ")} (default: SANDBOX, or "environment" in ${CONFIG_FILE})`).option("--set <key=value>", "override a payload field (repeatable, dot paths allowed)", (v, acc) => [...acc ?? [], v]).option("--seed <seed>", "deterministic ids and timestamps").option("--dry-run", "print the payload instead of sending it").addHelpText("after", `
1065
+ program.command("send").argument("<EVENT_TYPE>", `event to send: ${EVENT_TYPES.join(" | ")}`).description("Send a single, schema-valid RevenueCat webhook event to your endpoint.").option("--to <url>", `target URL (default: ${DEFAULT_TARGET}, or "to" in ${CONFIG_FILE})`).option("--store <store>", `store to simulate: ${CLI_STORES.join(" | ")} (default: app_store, or "store" in ${CONFIG_FILE})`).option("--user <app_user_id>", "app_user_id (default: generated $RCAnonymousID)").option("--product <product_id>", "product_id (default: com.example.premium.monthly, or com.example.premium:monthly for play_store)").option("--auth-header <value>", `value sent as the Authorization header (default: "authHeader" in ${CONFIG_FILE})`).option("--environment <env>", `${ENVIRONMENTS.join(" | ")} (default: SANDBOX, or "environment" in ${CONFIG_FILE})`).option("--set <key=value>", "override a payload field (repeatable, dot paths allowed)", (v, acc) => [...acc ?? [], v]).option("--seed <seed>", "deterministic ids and timestamps").option("--dry-run", "print the payload instead of sending it").addHelpText("after", `
1003
1066
  Examples:
1004
1067
  $ rcc send INITIAL_PURCHASE
1005
1068
  $ rcc send RENEWAL --to http://localhost:8787/webhook --auth-header "Bearer dev"
@@ -1211,12 +1274,13 @@ var eventType = z3.enum(EVENT_TYPES, {
1211
1274
  var httpStatus = z3.int({ error: "response_status must be an integer HTTP status (e.g. 200)." }).min(100).max(599);
1212
1275
  var SubscriberConfigSchema = z3.strictObject({
1213
1276
  app_user_id: z3.string().min(1).default("auto"),
1214
- product_id: z3.string().min(1).default("com.example.premium.monthly"),
1277
+ /** No default here: the Subscriber picks a per-store default (see specs/F7-google-play.md). */
1278
+ product_id: z3.string().min(1).optional(),
1215
1279
  period: duration.default("P1M"),
1216
1280
  trial: duration.optional(),
1217
1281
  grace_period: duration.default("P16D"),
1218
1282
  store: z3.enum(CLI_STORES, {
1219
- error: (iss) => `Unsupported store "${String(iss.input)}". v0.1 supports: ${list(CLI_STORES)}.`
1283
+ error: (iss) => `Unsupported store "${String(iss.input)}". Supported: ${list(CLI_STORES)}.`
1220
1284
  }).default("app_store"),
1221
1285
  environment: z3.enum(ENVIRONMENTS, {
1222
1286
  error: (iss) => `Invalid environment "${String(iss.input)}". Use one of: ${list(ENVIRONMENTS)}.`
@@ -1253,7 +1317,6 @@ var ScenarioSchema = z3.strictObject({
1253
1317
  description: z3.string().optional(),
1254
1318
  subscriber: SubscriberConfigSchema.default({
1255
1319
  app_user_id: "auto",
1256
- product_id: "com.example.premium.monthly",
1257
1320
  period: "P1M",
1258
1321
  grace_period: "P16D",
1259
1322
  store: "app_store",