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/CHANGELOG.md +11 -1
- package/README.md +3 -3
- package/dist/cli.js +60 -19
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +17 -7
- package/dist/index.js +62 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/scenarios/stripe-billing-issue-recovers.yaml +19 -0
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
|
-
|
|
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
|
|
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.
|
|
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 = {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|