gatefit 0.0.0-stage → 0.4.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 +20 -0
- package/LICENSE +21 -0
- package/README.md +78 -2
- package/action.yml +23 -0
- package/bin/gatefit.js +32 -0
- package/package.json +56 -4
- package/scripts/action-run.sh +18 -0
- package/src/access-adapter.ts +88 -0
- package/src/access-rules.ts +104 -0
- package/src/adapters/hardened.ts +20 -0
- package/src/adapters/naive.ts +25 -0
- package/src/adapters/sqlite.ts +83 -0
- package/src/cli.ts +163 -0
- package/src/clock-driver.ts +52 -0
- package/src/fixture-driver.ts +84 -0
- package/src/license.ts +160 -0
- package/src/live-scenarios.ts +191 -0
- package/src/packs.ts +27 -0
- package/src/paths.ts +31 -0
- package/src/stripe-clock.ts +153 -0
- package/tests/entitlements/goldens/cancel_at_period_end_keeps_access_until_end.yaml +36 -0
- package/tests/entitlements/goldens/cancel_revokes_access.yaml +18 -0
- package/tests/entitlements/goldens/dispute_revokes_or_flags.yaml +29 -0
- package/tests/entitlements/goldens/duplicate_webhook_idempotent.yaml +33 -0
- package/tests/entitlements/goldens/paid_grants_access.yaml +11 -0
- package/tests/entitlements/goldens/past_due_denies_until_recovery.yaml +26 -0
- package/tests/entitlements/goldens/refund_revokes_access.yaml +18 -0
- package/tests/entitlements/goldens/seat_qty_vs_db_cap.yaml +29 -0
- package/tests/entitlements/goldens/trial_to_paid_grants.yaml +19 -0
- package/tests/entitlements/goldens/unpaid_after_retries_revokes.yaml +40 -0
- package/tests/entitlements/goldens/upgrade_plus_cape_race.yaml +46 -0
package/src/paths.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
export function packageRoot(): string {
|
|
6
|
+
return process.env.GATEFIT_PACKAGE_ROOT ?? path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function userCwd(): string {
|
|
10
|
+
return process.env.INIT_CWD ?? process.cwd();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function goldenDir(): string {
|
|
14
|
+
return path.join(packageRoot(), "tests/entitlements/goldens");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function readPackageVersion(): string {
|
|
18
|
+
const raw = fs.readFileSync(path.join(packageRoot(), "package.json"), "utf8");
|
|
19
|
+
const pkg = JSON.parse(raw) as { version?: string };
|
|
20
|
+
if (!pkg.version) throw new Error("package.json missing version");
|
|
21
|
+
return pkg.version;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveUserOrPackage(spec: string): string {
|
|
25
|
+
if (path.isAbsolute(spec) && fs.existsSync(spec)) return spec;
|
|
26
|
+
const fromUser = path.resolve(userCwd(), spec);
|
|
27
|
+
if (fs.existsSync(fromUser)) return fromUser;
|
|
28
|
+
const fromPkg = path.resolve(packageRoot(), spec);
|
|
29
|
+
if (fs.existsSync(fromPkg)) return fromPkg;
|
|
30
|
+
throw new Error(`path not found: ${spec}`);
|
|
31
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { assertNoLiveStripeSecret } from "./access-adapter.ts";
|
|
2
|
+
|
|
3
|
+
export const STRIPE_API_VERSION = "2025-10-29.clover";
|
|
4
|
+
export const TEST_PAYMENT_METHOD = "pm_card_visa";
|
|
5
|
+
export const MONTH_SECONDS = 30 * 24 * 60 * 60;
|
|
6
|
+
export const MAX_ADVANCE_INTERVALS = 2;
|
|
7
|
+
export const MAX_CUSTOMERS_PER_CLOCK = 3;
|
|
8
|
+
export const MAX_SUBSCRIPTIONS_PER_CLOCK = 3;
|
|
9
|
+
|
|
10
|
+
export type TestClock = {
|
|
11
|
+
id: string;
|
|
12
|
+
status: string;
|
|
13
|
+
frozen_time: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type StripeCustomer = { id: string };
|
|
17
|
+
export type StripeSubscription = {
|
|
18
|
+
id: string;
|
|
19
|
+
status: string;
|
|
20
|
+
customer: string;
|
|
21
|
+
cancel_at_period_end?: boolean;
|
|
22
|
+
current_period_end?: number;
|
|
23
|
+
items?: { data?: Array<{ quantity?: number }> };
|
|
24
|
+
};
|
|
25
|
+
export type StripeInvoice = { id: string; status: string; paid: boolean; customer: string };
|
|
26
|
+
|
|
27
|
+
export type StripeClockClient = {
|
|
28
|
+
createClock(input: { frozenTime: number; name?: string }): Promise<TestClock>;
|
|
29
|
+
retrieveClock(id: string): Promise<TestClock>;
|
|
30
|
+
advanceClock(id: string, frozenTime: number): Promise<TestClock>;
|
|
31
|
+
deleteClock(id: string): Promise<{ id: string; deleted: boolean }>;
|
|
32
|
+
createCustomer(input: { testClock: string; paymentMethod?: string }): Promise<StripeCustomer>;
|
|
33
|
+
createSubscription(input: { customer: string; trialPeriodDays?: number }): Promise<StripeSubscription>;
|
|
34
|
+
retrieveSubscription(id: string): Promise<StripeSubscription>;
|
|
35
|
+
cancelSubscription(id: string): Promise<StripeSubscription>;
|
|
36
|
+
listInvoices(customerId: string): Promise<StripeInvoice[]>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function requireLiveClockEnabled(env: NodeJS.ProcessEnv = process.env): void {
|
|
40
|
+
if (env.GATEFIT_LIVE_CLOCK !== "1") {
|
|
41
|
+
throw new Error("live lane is opt-in: set GATEFIT_LIVE_CLOCK=1");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function requireTestModeSecret(env: NodeJS.ProcessEnv = process.env): string {
|
|
46
|
+
assertNoLiveStripeSecret(JSON.stringify(env));
|
|
47
|
+
const secret = env.STRIPE_SECRET_KEY ?? env.STRIPE_API_KEY ?? "";
|
|
48
|
+
assertNoLiveStripeSecret(secret);
|
|
49
|
+
if (!secret) {
|
|
50
|
+
throw new Error("live lane requires STRIPE_SECRET_KEY or STRIPE_API_KEY (sk_test_ only)");
|
|
51
|
+
}
|
|
52
|
+
if (!secret.startsWith("sk_test_")) {
|
|
53
|
+
throw new Error("refuse non-test Stripe secret: live lane requires sk_test_");
|
|
54
|
+
}
|
|
55
|
+
return secret;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function assertAdvanceWithinTwoIntervals(
|
|
59
|
+
from: number,
|
|
60
|
+
to: number,
|
|
61
|
+
intervalSeconds: number = MONTH_SECONDS,
|
|
62
|
+
): void {
|
|
63
|
+
if (to <= from) {
|
|
64
|
+
throw new Error("advance frozen_time must be after current frozen_time");
|
|
65
|
+
}
|
|
66
|
+
if (to > from + MAX_ADVANCE_INTERVALS * intervalSeconds) {
|
|
67
|
+
throw new Error("advance exceeds two billing intervals");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function createStripeClockClient(opts: {
|
|
72
|
+
secret: string;
|
|
73
|
+
fetchImpl?: typeof fetch;
|
|
74
|
+
apiVersion?: string;
|
|
75
|
+
baseUrl?: string;
|
|
76
|
+
}): StripeClockClient {
|
|
77
|
+
requireTestModeSecret({ STRIPE_SECRET_KEY: opts.secret });
|
|
78
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
79
|
+
const apiVersion = opts.apiVersion ?? STRIPE_API_VERSION;
|
|
80
|
+
const baseUrl = (opts.baseUrl ?? "https://api.stripe.com").replace(/\/$/, "");
|
|
81
|
+
|
|
82
|
+
async function request<T>(method: string, path: string, form?: Record<string, string>): Promise<T> {
|
|
83
|
+
const headers: Record<string, string> = {
|
|
84
|
+
Authorization: `Bearer ${opts.secret}`,
|
|
85
|
+
"Stripe-Version": apiVersion,
|
|
86
|
+
};
|
|
87
|
+
const init: RequestInit = { method, headers };
|
|
88
|
+
if (form) {
|
|
89
|
+
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
90
|
+
init.body = new URLSearchParams(form).toString();
|
|
91
|
+
}
|
|
92
|
+
const res = await fetchImpl(`${baseUrl}${path}`, init);
|
|
93
|
+
const json = (await res.json()) as T & { error?: { message?: string } };
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
const message = json.error?.message ?? `stripe ${method} ${path} failed`;
|
|
96
|
+
throw new Error(message);
|
|
97
|
+
}
|
|
98
|
+
return json;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
createClock(input) {
|
|
103
|
+
const form: Record<string, string> = { frozen_time: String(input.frozenTime) };
|
|
104
|
+
if (input.name) form.name = input.name;
|
|
105
|
+
return request<TestClock>("POST", "/v1/test_helpers/test_clocks", form);
|
|
106
|
+
},
|
|
107
|
+
retrieveClock(id) {
|
|
108
|
+
return request<TestClock>("GET", `/v1/test_helpers/test_clocks/${id}`);
|
|
109
|
+
},
|
|
110
|
+
advanceClock(id, frozenTime) {
|
|
111
|
+
return request<TestClock>("POST", `/v1/test_helpers/test_clocks/${id}/advance`, {
|
|
112
|
+
frozen_time: String(frozenTime),
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
deleteClock(id) {
|
|
116
|
+
return request<{ id: string; deleted: boolean }>("DELETE", `/v1/test_helpers/test_clocks/${id}`);
|
|
117
|
+
},
|
|
118
|
+
createCustomer(input) {
|
|
119
|
+
const form: Record<string, string> = {
|
|
120
|
+
test_clock: input.testClock,
|
|
121
|
+
payment_method: input.paymentMethod ?? TEST_PAYMENT_METHOD,
|
|
122
|
+
"invoice_settings[default_payment_method]": input.paymentMethod ?? TEST_PAYMENT_METHOD,
|
|
123
|
+
};
|
|
124
|
+
return request<StripeCustomer>("POST", "/v1/customers", form);
|
|
125
|
+
},
|
|
126
|
+
createSubscription(input) {
|
|
127
|
+
const form: Record<string, string> = {
|
|
128
|
+
customer: input.customer,
|
|
129
|
+
"items[0][price_data][currency]": "usd",
|
|
130
|
+
"items[0][price_data][product_data][name]": "gatefit-pro",
|
|
131
|
+
"items[0][price_data][recurring][interval]": "month",
|
|
132
|
+
"items[0][price_data][unit_amount]": "2000",
|
|
133
|
+
};
|
|
134
|
+
if (input.trialPeriodDays !== undefined) {
|
|
135
|
+
form.trial_period_days = String(input.trialPeriodDays);
|
|
136
|
+
}
|
|
137
|
+
return request<StripeSubscription>("POST", "/v1/subscriptions", form);
|
|
138
|
+
},
|
|
139
|
+
retrieveSubscription(id) {
|
|
140
|
+
return request<StripeSubscription>("GET", `/v1/subscriptions/${id}`);
|
|
141
|
+
},
|
|
142
|
+
cancelSubscription(id) {
|
|
143
|
+
return request<StripeSubscription>("DELETE", `/v1/subscriptions/${id}`);
|
|
144
|
+
},
|
|
145
|
+
async listInvoices(customerId) {
|
|
146
|
+
const payload = await request<{ data: StripeInvoice[] }>(
|
|
147
|
+
"GET",
|
|
148
|
+
`/v1/invoices?customer=${encodeURIComponent(customerId)}`,
|
|
149
|
+
);
|
|
150
|
+
return payload.data ?? [];
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
id: cancel_at_period_end_keeps_access_until_end
|
|
2
|
+
subject: cus_h6_cape
|
|
3
|
+
steps:
|
|
4
|
+
- event:
|
|
5
|
+
id: evt_h6_cape_paid
|
|
6
|
+
type: invoice.paid
|
|
7
|
+
customerId: cus_h6_cape
|
|
8
|
+
plan: pro
|
|
9
|
+
seats: 1
|
|
10
|
+
expect:
|
|
11
|
+
locked: false
|
|
12
|
+
plan: pro
|
|
13
|
+
seats: 1
|
|
14
|
+
- event:
|
|
15
|
+
id: evt_h6_cape_set
|
|
16
|
+
type: customer.subscription.updated
|
|
17
|
+
customerId: cus_h6_cape
|
|
18
|
+
cancel_at_period_end: true
|
|
19
|
+
period_end: 1577923200
|
|
20
|
+
now: 1577836800
|
|
21
|
+
plan: pro
|
|
22
|
+
seats: 1
|
|
23
|
+
expect:
|
|
24
|
+
locked: false
|
|
25
|
+
plan: pro
|
|
26
|
+
seats: 1
|
|
27
|
+
- event:
|
|
28
|
+
id: evt_h6_cape_period_end
|
|
29
|
+
type: customer.subscription.updated
|
|
30
|
+
customerId: cus_h6_cape
|
|
31
|
+
cancel_at_period_end: true
|
|
32
|
+
period_end: 1577923200
|
|
33
|
+
now: 1577923200
|
|
34
|
+
plan: pro
|
|
35
|
+
expect:
|
|
36
|
+
locked: true
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
id: cancel_revokes_access
|
|
2
|
+
subject: cus_s2
|
|
3
|
+
steps:
|
|
4
|
+
- event:
|
|
5
|
+
id: evt_s2_invoice_paid
|
|
6
|
+
type: invoice.paid
|
|
7
|
+
customerId: cus_s2
|
|
8
|
+
plan: pro
|
|
9
|
+
expect:
|
|
10
|
+
locked: false
|
|
11
|
+
plan: pro
|
|
12
|
+
- event:
|
|
13
|
+
id: evt_s2_subscription_deleted
|
|
14
|
+
type: customer.subscription.deleted
|
|
15
|
+
customerId: cus_s2
|
|
16
|
+
canceled: true
|
|
17
|
+
expect:
|
|
18
|
+
locked: true
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Assumed policy: charge.dispute.created revokes access (locked: true)
|
|
2
|
+
# and flags the row by clearing features. Plan/seats stay. Fixture-only;
|
|
3
|
+
# dispute + Stripe Test Clocks flake (auto-cancel delay).
|
|
4
|
+
id: dispute_revokes_or_flags
|
|
5
|
+
subject: cus_h7_dispute
|
|
6
|
+
steps:
|
|
7
|
+
- event:
|
|
8
|
+
id: evt_h7_dispute_paid
|
|
9
|
+
type: invoice.paid
|
|
10
|
+
customerId: cus_h7_dispute
|
|
11
|
+
plan: pro
|
|
12
|
+
seats: 1
|
|
13
|
+
features:
|
|
14
|
+
- team
|
|
15
|
+
expect:
|
|
16
|
+
locked: false
|
|
17
|
+
plan: pro
|
|
18
|
+
seats: 1
|
|
19
|
+
features:
|
|
20
|
+
- team
|
|
21
|
+
- event:
|
|
22
|
+
id: evt_h7_dispute_created
|
|
23
|
+
type: charge.dispute.created
|
|
24
|
+
customerId: cus_h7_dispute
|
|
25
|
+
expect:
|
|
26
|
+
locked: true
|
|
27
|
+
plan: pro
|
|
28
|
+
seats: 1
|
|
29
|
+
features: []
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
id: duplicate_webhook_idempotent
|
|
2
|
+
subject: cus_s6
|
|
3
|
+
steps:
|
|
4
|
+
- event:
|
|
5
|
+
id: evt_s6_invoice_paid
|
|
6
|
+
type: invoice.paid
|
|
7
|
+
customerId: cus_s6
|
|
8
|
+
plan: pro
|
|
9
|
+
expect:
|
|
10
|
+
locked: false
|
|
11
|
+
plan: pro
|
|
12
|
+
- event:
|
|
13
|
+
id: evt_s6_invoice_paid
|
|
14
|
+
type: invoice.paid
|
|
15
|
+
customerId: cus_s6
|
|
16
|
+
plan: pro
|
|
17
|
+
expect:
|
|
18
|
+
locked: false
|
|
19
|
+
plan: pro
|
|
20
|
+
- event:
|
|
21
|
+
id: evt_s6_subscription_deleted
|
|
22
|
+
type: customer.subscription.deleted
|
|
23
|
+
customerId: cus_s6
|
|
24
|
+
canceled: true
|
|
25
|
+
expect:
|
|
26
|
+
locked: true
|
|
27
|
+
- event:
|
|
28
|
+
id: evt_s6_invoice_paid
|
|
29
|
+
type: invoice.paid
|
|
30
|
+
customerId: cus_s6
|
|
31
|
+
plan: pro
|
|
32
|
+
expect:
|
|
33
|
+
locked: true
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
id: past_due_denies_until_recovery
|
|
2
|
+
subject: cus_s5
|
|
3
|
+
steps:
|
|
4
|
+
- event:
|
|
5
|
+
id: evt_s5_invoice_paid
|
|
6
|
+
type: invoice.paid
|
|
7
|
+
customerId: cus_s5
|
|
8
|
+
plan: pro
|
|
9
|
+
expect:
|
|
10
|
+
locked: false
|
|
11
|
+
plan: pro
|
|
12
|
+
- event:
|
|
13
|
+
id: evt_s5_payment_failed
|
|
14
|
+
type: invoice.payment_failed
|
|
15
|
+
customerId: cus_s5
|
|
16
|
+
status: past_due
|
|
17
|
+
expect:
|
|
18
|
+
locked: true
|
|
19
|
+
- event:
|
|
20
|
+
id: evt_s5_recovered
|
|
21
|
+
type: invoice.paid
|
|
22
|
+
customerId: cus_s5
|
|
23
|
+
plan: pro
|
|
24
|
+
expect:
|
|
25
|
+
locked: false
|
|
26
|
+
plan: pro
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
id: refund_revokes_access
|
|
2
|
+
subject: cus_s3
|
|
3
|
+
steps:
|
|
4
|
+
- event:
|
|
5
|
+
id: evt_s3_invoice_paid
|
|
6
|
+
type: invoice.paid
|
|
7
|
+
customerId: cus_s3
|
|
8
|
+
plan: pro
|
|
9
|
+
expect:
|
|
10
|
+
locked: false
|
|
11
|
+
plan: pro
|
|
12
|
+
- event:
|
|
13
|
+
id: evt_s3_charge_refunded
|
|
14
|
+
type: charge.refunded
|
|
15
|
+
customerId: cus_s3
|
|
16
|
+
refunded: true
|
|
17
|
+
expect:
|
|
18
|
+
locked: true
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
id: seat_qty_vs_db_cap
|
|
2
|
+
subject: cus_h6_seats
|
|
3
|
+
steps:
|
|
4
|
+
- event:
|
|
5
|
+
id: evt_h6_seats_paid_10
|
|
6
|
+
type: invoice.paid
|
|
7
|
+
customerId: cus_h6_seats
|
|
8
|
+
plan: pro
|
|
9
|
+
seats: 10
|
|
10
|
+
features:
|
|
11
|
+
- team
|
|
12
|
+
expect:
|
|
13
|
+
locked: false
|
|
14
|
+
plan: pro
|
|
15
|
+
seats: 10
|
|
16
|
+
features:
|
|
17
|
+
- team
|
|
18
|
+
- event:
|
|
19
|
+
id: evt_h6_seats_qty_3
|
|
20
|
+
type: customer.subscription.updated
|
|
21
|
+
customerId: cus_h6_seats
|
|
22
|
+
plan: pro
|
|
23
|
+
seats: 3
|
|
24
|
+
expect:
|
|
25
|
+
locked: false
|
|
26
|
+
plan: pro
|
|
27
|
+
seats: 3
|
|
28
|
+
features:
|
|
29
|
+
- team
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
id: trial_to_paid_grants
|
|
2
|
+
subject: cus_s4
|
|
3
|
+
steps:
|
|
4
|
+
- event:
|
|
5
|
+
id: evt_s4_trialing
|
|
6
|
+
type: customer.subscription.updated
|
|
7
|
+
customerId: cus_s4
|
|
8
|
+
status: trialing
|
|
9
|
+
plan: pro
|
|
10
|
+
expect:
|
|
11
|
+
locked: true
|
|
12
|
+
- event:
|
|
13
|
+
id: evt_s4_invoice_paid
|
|
14
|
+
type: invoice.paid
|
|
15
|
+
customerId: cus_s4
|
|
16
|
+
plan: pro
|
|
17
|
+
expect:
|
|
18
|
+
locked: false
|
|
19
|
+
plan: pro
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Assumed policy: past_due denies, then unpaid after retries exhaust
|
|
2
|
+
# revokes (locked: true). Immediate cancel stays locked. Fixture-only.
|
|
3
|
+
id: unpaid_after_retries_revokes
|
|
4
|
+
subject: cus_h7_unpaid
|
|
5
|
+
steps:
|
|
6
|
+
- event:
|
|
7
|
+
id: evt_h7_unpaid_paid
|
|
8
|
+
type: invoice.paid
|
|
9
|
+
customerId: cus_h7_unpaid
|
|
10
|
+
plan: pro
|
|
11
|
+
seats: 1
|
|
12
|
+
expect:
|
|
13
|
+
locked: false
|
|
14
|
+
plan: pro
|
|
15
|
+
seats: 1
|
|
16
|
+
- event:
|
|
17
|
+
id: evt_h7_unpaid_past_due
|
|
18
|
+
type: invoice.payment_failed
|
|
19
|
+
customerId: cus_h7_unpaid
|
|
20
|
+
status: past_due
|
|
21
|
+
expect:
|
|
22
|
+
locked: true
|
|
23
|
+
- event:
|
|
24
|
+
id: evt_h7_unpaid_exhaust
|
|
25
|
+
type: customer.subscription.updated
|
|
26
|
+
customerId: cus_h7_unpaid
|
|
27
|
+
status: unpaid
|
|
28
|
+
plan: pro
|
|
29
|
+
seats: 1
|
|
30
|
+
expect:
|
|
31
|
+
locked: true
|
|
32
|
+
plan: pro
|
|
33
|
+
seats: 1
|
|
34
|
+
- event:
|
|
35
|
+
id: evt_h7_unpaid_canceled
|
|
36
|
+
type: customer.subscription.deleted
|
|
37
|
+
customerId: cus_h7_unpaid
|
|
38
|
+
canceled: true
|
|
39
|
+
expect:
|
|
40
|
+
locked: true
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
id: upgrade_plus_cape_race
|
|
2
|
+
subject: cus_h6_race
|
|
3
|
+
steps:
|
|
4
|
+
- event:
|
|
5
|
+
id: evt_h6_race_starter
|
|
6
|
+
type: invoice.paid
|
|
7
|
+
customerId: cus_h6_race
|
|
8
|
+
plan: starter
|
|
9
|
+
seats: 1
|
|
10
|
+
expect:
|
|
11
|
+
locked: false
|
|
12
|
+
plan: starter
|
|
13
|
+
seats: 1
|
|
14
|
+
- event:
|
|
15
|
+
id: evt_h6_race_cape
|
|
16
|
+
type: customer.subscription.updated
|
|
17
|
+
customerId: cus_h6_race
|
|
18
|
+
cancel_at_period_end: true
|
|
19
|
+
period_end: 1577923200
|
|
20
|
+
now: 1577836800
|
|
21
|
+
plan: starter
|
|
22
|
+
seats: 1
|
|
23
|
+
expect:
|
|
24
|
+
locked: false
|
|
25
|
+
plan: starter
|
|
26
|
+
seats: 1
|
|
27
|
+
- event:
|
|
28
|
+
id: evt_h6_race_upgrade
|
|
29
|
+
type: invoice.paid
|
|
30
|
+
customerId: cus_h6_race
|
|
31
|
+
plan: pro
|
|
32
|
+
seats: 5
|
|
33
|
+
paid: true
|
|
34
|
+
expect:
|
|
35
|
+
locked: false
|
|
36
|
+
plan: pro
|
|
37
|
+
seats: 5
|
|
38
|
+
- event:
|
|
39
|
+
id: evt_h6_race_period_end
|
|
40
|
+
type: customer.subscription.updated
|
|
41
|
+
customerId: cus_h6_race
|
|
42
|
+
cancel_at_period_end: true
|
|
43
|
+
period_end: 1577923200
|
|
44
|
+
now: 1577923200
|
|
45
|
+
expect:
|
|
46
|
+
locked: true
|