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 +10 -1
- package/README.md +3 -3
- package/dist/cli.js +39 -17
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +17 -5
- package/dist/index.js +40 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/scenarios/play-trial-converts.yaml +16 -0
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.
|
|
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 #
|
|
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
|
-
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
685
|
-
let s =
|
|
686
|
-
for (let i = 0; i <
|
|
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
|
|
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.
|
|
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}".
|
|
978
|
-
hint: "
|
|
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
|
|
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
|
-
|
|
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)}".
|
|
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",
|