reveclicat 0.1.1 → 0.2.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,14 @@ All notable changes to this project are documented here. Format based on [Keep a
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.2.0] — 2026-08-29
8
+
9
+ ### Added
10
+ - 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)
11
+
12
+ ### Changed
13
+ - `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)
14
+
7
15
  ## [0.1.1] — 2026-08-29
8
16
 
9
17
  ### Fixed
@@ -41,6 +49,7 @@ First release. Unofficial project — not affiliated with RevenueCat, Inc.
41
49
  - 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
50
  - Programmatic API (`reveclicat` package): schemas, `Subscriber`, `runScenario`, `loadScenario`, `VirtualClock`, `createRng`.
43
51
 
44
- [Unreleased]: https://github.com/RadW2020/ReveCliCat/compare/v0.1.1...HEAD
52
+ [Unreleased]: https://github.com/RadW2020/ReveCliCat/compare/v0.2.0...HEAD
53
+ [0.2.0]: https://github.com/RadW2020/ReveCliCat/compare/v0.1.1...v0.2.0
45
54
  [0.1.1]: https://github.com/RadW2020/ReveCliCat/compare/v0.1.0...v0.1.1
46
55
  [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 (Google-shaped ids and product_id format)
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).
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) and **Google Play** (`--store play_store`, `GPA.…` order ids with `..N` renewal suffixes, `<subscription_id>:<base_plan_id>` product ids, new order on resubscription). Stripe, Amazon and Roku, 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.2.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,9 @@ 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"];
169
+ var CLI_STORE_TO_STORE = { app_store: "APP_STORE", play_store: "PLAY_STORE" };
170
+ var DEFAULT_PRODUCT_ID = { app_store: "com.example.premium.monthly", play_store: "com.example.premium:monthly" };
170
171
 
171
172
  // src/core/config.ts
172
173
  var CONFIG_FILE = "reveclicat.config.json";
@@ -609,13 +610,14 @@ var Subscriber = class {
609
610
  this.period = asDuration(opts.period);
610
611
  this.trial = opts.trial === void 0 ? void 0 : asDuration(opts.trial);
611
612
  this.grace = asDuration(opts.gracePeriod ?? "P16D");
612
- this.store = CLI_STORE_TO_STORE[opts.store ?? "app_store"];
613
+ this.cliStore = opts.store ?? "app_store";
614
+ this.store = CLI_STORE_TO_STORE[this.cliStore];
613
615
  this.environment = opts.environment ?? "SANDBOX";
614
616
  this.price = opts.price ?? 9.99;
615
617
  this.currency = opts.currency ?? "USD";
616
618
  this.countryCode = opts.countryCode ?? "US";
617
619
  this.entitlementIds = opts.entitlementIds ?? ["premium"];
618
- this.productId = opts.productId;
620
+ this.productId = opts.productId ?? DEFAULT_PRODUCT_ID[this.cliStore];
619
621
  this.appUserId = opts.appUserId === void 0 || opts.appUserId === "auto" ? `$RCAnonymousID:${deps.rng.hex(32)}` : opts.appUserId;
620
622
  this.appId = opts.appId ?? `app${deps.rng.hex(12)}`;
621
623
  }
@@ -627,6 +629,9 @@ var Subscriber = class {
627
629
  trial;
628
630
  grace;
629
631
  store;
632
+ cliStore;
633
+ /** Play: number of renewals on the current order (drives the `..N` suffix). */
634
+ renewalIndex = 0;
630
635
  environment;
631
636
  price;
632
637
  currency;
@@ -673,6 +678,7 @@ var Subscriber = class {
673
678
  this.expirationAtMs = draft.expirationAtMs;
674
679
  this.periodType = draft.periodType;
675
680
  this.gracePeriodExpirationAtMs = draft.gracePeriodExpirationAtMs;
681
+ this.renewalIndex = draft.renewalIndex;
676
682
  if (type === "CANCELLATION") this.resumeState = from === "trial" ? "trial" : "active";
677
683
  this._state = next;
678
684
  }
@@ -681,11 +687,23 @@ var Subscriber = class {
681
687
  return event;
682
688
  }
683
689
  /* ----------------------------------------------------------- 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));
690
+ digits(n) {
691
+ let s = "";
692
+ for (let i = 0; i < n; i++) s += String(this.deps.rng.int(10));
687
693
  return s;
688
694
  }
695
+ /** A brand-new order/transaction id in the store's format (see specs/F7-google-play.md). */
696
+ newTransactionId() {
697
+ if (this.cliStore === "play_store") {
698
+ return `GPA.${this.digits(4)}-${this.digits(4)}-${this.digits(4)}-${this.digits(5)}`;
699
+ }
700
+ return String(1 + this.deps.rng.int(9)) + this.digits(15);
701
+ }
702
+ /** Renewal id: Play appends `..N` to the original order id; App Store issues a fresh transaction id. */
703
+ renewalTransactionId(originalId, index) {
704
+ if (this.cliStore === "play_store" && originalId !== void 0) return `${originalId}..${index}`;
705
+ return this.newTransactionId();
706
+ }
689
707
  draftFor(type, from, now) {
690
708
  const d = {
691
709
  originalTransactionId: this.originalTransactionId,
@@ -693,13 +711,16 @@ var Subscriber = class {
693
711
  purchasedAtMs: this.purchasedAtMs,
694
712
  expirationAtMs: this.expirationAtMs,
695
713
  periodType: this.periodType,
696
- gracePeriodExpirationAtMs: this.gracePeriodExpirationAtMs
714
+ gracePeriodExpirationAtMs: this.gracePeriodExpirationAtMs,
715
+ renewalIndex: this.renewalIndex
697
716
  };
698
717
  switch (type) {
699
718
  case "INITIAL_PURCHASE": {
700
719
  const startsTrial = from === "none" && this.trial !== void 0;
701
720
  d.transactionId = this.newTransactionId();
702
- d.originalTransactionId ??= d.transactionId;
721
+ if (this.cliStore === "play_store") d.originalTransactionId = d.transactionId;
722
+ else d.originalTransactionId ??= d.transactionId;
723
+ d.renewalIndex = 0;
703
724
  d.purchasedAtMs = now;
704
725
  d.expirationAtMs = addDuration(now, startsTrial ? this.trial : this.period);
705
726
  d.periodType = startsTrial ? "TRIAL" : "NORMAL";
@@ -708,7 +729,8 @@ var Subscriber = class {
708
729
  }
709
730
  case "RENEWAL": {
710
731
  const start = d.expirationAtMs ?? now;
711
- d.transactionId = this.newTransactionId();
732
+ d.transactionId = this.renewalTransactionId(d.originalTransactionId, d.renewalIndex);
733
+ d.renewalIndex += 1;
712
734
  d.purchasedAtMs = start;
713
735
  d.expirationAtMs = addDuration(start, this.period);
714
736
  d.periodType = "NORMAL";
@@ -974,8 +996,8 @@ function parseEnvironment(input) {
974
996
  }
975
997
  function parseStore(input) {
976
998
  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)."
999
+ throw new RccError(`Unsupported --store "${input}". The generator supports: ${CLI_STORES.join(", ")}.`, {
1000
+ hint: "Receivers (listen/tail/inbox) accept events from every store; only generation is limited. Others are in the Icebox."
979
1001
  });
980
1002
  }
981
1003
  function parseSeed(input) {
@@ -999,7 +1021,7 @@ function buildSingleEvent(type, opts) {
999
1021
  return { api_version: "1.0", event };
1000
1022
  }
1001
1023
  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", `
1024
+ 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
1025
  Examples:
1004
1026
  $ rcc send INITIAL_PURCHASE
1005
1027
  $ rcc send RENEWAL --to http://localhost:8787/webhook --auth-header "Bearer dev"
@@ -1211,12 +1233,13 @@ var eventType = z3.enum(EVENT_TYPES, {
1211
1233
  var httpStatus = z3.int({ error: "response_status must be an integer HTTP status (e.g. 200)." }).min(100).max(599);
1212
1234
  var SubscriberConfigSchema = z3.strictObject({
1213
1235
  app_user_id: z3.string().min(1).default("auto"),
1214
- product_id: z3.string().min(1).default("com.example.premium.monthly"),
1236
+ /** No default here: the Subscriber picks a per-store default (see specs/F7-google-play.md). */
1237
+ product_id: z3.string().min(1).optional(),
1215
1238
  period: duration.default("P1M"),
1216
1239
  trial: duration.optional(),
1217
1240
  grace_period: duration.default("P16D"),
1218
1241
  store: z3.enum(CLI_STORES, {
1219
- error: (iss) => `Unsupported store "${String(iss.input)}". v0.1 supports: ${list(CLI_STORES)}.`
1242
+ error: (iss) => `Unsupported store "${String(iss.input)}". Supported: ${list(CLI_STORES)}.`
1220
1243
  }).default("app_store"),
1221
1244
  environment: z3.enum(ENVIRONMENTS, {
1222
1245
  error: (iss) => `Invalid environment "${String(iss.input)}". Use one of: ${list(ENVIRONMENTS)}.`
@@ -1253,7 +1276,6 @@ var ScenarioSchema = z3.strictObject({
1253
1276
  description: z3.string().optional(),
1254
1277
  subscriber: SubscriberConfigSchema.default({
1255
1278
  app_user_id: "auto",
1256
- product_id: "com.example.premium.monthly",
1257
1279
  period: "P1M",
1258
1280
  grace_period: "P16D",
1259
1281
  store: "app_store",