gatefit 0.5.0 → 0.5.2

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
@@ -1,6 +1,25 @@
1
1
  # Changelog
2
2
 
3
- All notable Gatefit changes are listed here. The version heading matches `package.json`. Tag `v0.5.0` is not cut in this change.
3
+ All notable Gatefit changes are listed here. The version heading matches `package.json`. Tag `v0.5.2` is not cut in this change.
4
+
5
+ ## [0.5.2] - 2026-10-29
6
+
7
+ 0.5.2 is the first release that ships the free S7–S9 goldens. This change does not publish the package and does not create the tag.
8
+
9
+ ### Added
10
+
11
+ - Free fixture goldens S7–S9: `trial_no_card` (no-card trial, `customer.subscription.trial_will_end`, failed first charge, pause, and cancel), `portal_downgrade_period_end_vs_immediate` (Customer Portal downgrade at period end versus immediate), and `entitlements_active_summary` (Stripe active entitlement summary). They do not read a license key and do not call Polar. Stripe API version stays `2025-10-29.clover`.
12
+
13
+ ### Fixed
14
+
15
+ - The missing-`better-sqlite3` error no longer says the package fails to compile on every install. npm 9 still runs node-gyp and drops this optional package when that compile fails. npm 10 and newer unpack the shipped prebuild. The message says to upgrade npm to 10 or newer and reinstall, reinstall with `npm install --ignore-scripts`, or install a C++ compiler and reinstall.
16
+ - `./ci-local.sh` reads `npm pack --json` on npm 9 through 12. npm 12 returns an object keyed by package name. The same gate treats npm 12's dry-run refusal of an already published version as a non-write.
17
+
18
+ ## [0.5.1] - 2026-10-16
19
+
20
+ ### Fixed
21
+
22
+ - `better-sqlite3` is optional and loads only when the SQLite adapter opens a database. `--version`, `--help`, and `adapter-check` run when that module is missing. A SQLite command that cannot load it prints what is missing, that the package ships prebuilds and does not compile on install, and how to reinstall the prebuild or run `npm run build-release`, then exits non-zero.
4
23
 
5
24
  ## [0.5.0] - 2026-10-16
6
25
 
package/README.md CHANGED
@@ -2,14 +2,14 @@
2
2
 
3
3
  Stripe Entitlement Edge Pack — CI that asserts the access DB survives ugly billing transitions.
4
4
 
5
- Fixture-first S1–S6 goldens, an `npx gatefit` CLI, and a GitHub Action. Node 22+. Not a billing UI.
5
+ Fixture-first goldens, an `npx gatefit` CLI, and a GitHub Action. Node 22+. Not a billing UI. 0.5.2 is the first release that ships free S7–S9. This change does not publish `0.5.2`.
6
6
 
7
7
  ## Distribution
8
8
 
9
9
  The CLI and GitHub Action in this repository stay free. The docs site is live. npm is live: `npx gatefit` ([gatefit](https://www.npmjs.com/package/gatefit)). Polar checkout is.
10
10
 
11
- - **Docs site:** live at https://gatefit.io. Cloudflare Pages project `gatefit`, hostname `gatefit-bdu.pages.dev`. From `site/`: `npm run build`, then `npx wrangler@4 pages deploy dist --project-name gatefit`. `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` come from the environment. GitHub Pages needs a public repository or a paid plan and is not the live host. `scripts/deploy-site.sh` can copy `site/dist` locally or deploy that Cloudflare target.
12
- - **npm:** live. `npx gatefit` is the last published tarball (`gatefit@0.4.0` until tag `v0.5.0`) at https://www.npmjs.com/package/gatefit. This repo is `0.5.0`. The named gate runs `npm publish --dry-run` only. A `v*` tag publishes with OIDC. See [Releasing](#releasing).
11
+ - **Docs site:** live at https://gatefit.io. Cloudflare Pages project `gatefit`, hostname `gatefit-bdu.pages.dev`. From `site/`: `npm run build`, then `npx wrangler@4 pages deploy dist --project-name gatefit`. `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` come from the environment. GitHub Pages needs a public repository or a paid plan and is not the live host. `.github/workflows/pages.yml` builds the site on pushes to `main` and does not deploy. `scripts/deploy-site.sh` can copy `site/dist` locally or deploy that Cloudflare target.
12
+ - **npm:** live. `npx gatefit` today is the published tarball `gatefit@0.5.1` at https://www.npmjs.com/package/gatefit. This repo is `0.5.2`. This change does not publish `0.5.2`. The named gate runs `npm publish --dry-run` only. A `v*` tag publishes with OIDC. See [Releasing](#releasing).
13
13
  - **Polar checkout:** live. Gatefit Indie License is **$49** one-time at `https://buy.polar.sh/polar_cl_biyvx7HYtN2u19tUJCCkIuGOc98uVv7IG3gcB04eIJ0`. A plain `site` build and `scripts/deploy-site.sh` use that committed link. `PUBLIC_POLAR_CHECKOUT_URL` may override it. Any other value fails closed.
14
14
 
15
15
  ## Install / run
@@ -20,7 +20,9 @@ npx gatefit adapter-check
20
20
  npx gatefit run --suite paid_grants_access --adapter ./src/your-access-adapter.ts
21
21
  ```
22
22
 
23
- This repo is `0.5.0`. Registry: https://www.npmjs.com/package/gatefit
23
+ This repo is `0.5.2`. It is not published yet. Registry: https://www.npmjs.com/package/gatefit
24
+
25
+ `better-sqlite3` is optional and ships prebuilds for common platforms. npm 9 still runs node-gyp during install and drops the package when that compile fails. npm 10 and newer unpack the prebuild. `--version`, `--help`, and `adapter-check` run when the native module is missing. The reference SQLite adapter loads it only when that adapter runs. If the module is missing, that command tells you to upgrade npm to 10 or newer, reinstall with `npm install --ignore-scripts`, or install a C++ compiler, then exits non-zero.
24
26
 
25
27
  From a clone:
26
28
 
@@ -41,13 +43,13 @@ Bring your own AccessAdapter in ≤30 minutes: [docs/bring-your-adapter.md](docs
41
43
 
42
44
  ## GitHub Action
43
45
 
44
- Marketplace-ready Action lives at the repo root (`action.yml`). Tag `v0.5.0` is not cut yet. After a human pushes the annotated tag:
46
+ Marketplace-ready Action lives at the repo root (`action.yml`). Snippets use `@v0.5.2`. That tag is not cut in this change. Tag `v0.5.1` is the last release.
45
47
 
46
48
  ```yaml
47
49
  - uses: actions/setup-node@v4
48
50
  with:
49
51
  node-version: "22"
50
- - uses: gatefit/gatefit@v0.5.0
52
+ - uses: gatefit/gatefit@v0.5.2
51
53
  with:
52
54
  adapter: src/your-access-adapter.ts
53
55
  ```
@@ -55,12 +57,12 @@ Marketplace-ready Action lives at the repo root (`action.yml`). Tag `v0.5.0` is
55
57
  Nested path still works:
56
58
 
57
59
  ```yaml
58
- - uses: gatefit/gatefit/.github/actions/gatefit@v0.5.0
60
+ - uses: gatefit/gatefit/.github/actions/gatefit@v0.5.2
59
61
  with:
60
62
  adapter: src/your-access-adapter.ts
61
63
  ```
62
64
 
63
- The Action wraps `gatefit run`. Adapter path is in *your* repo; goldens default to Gatefit’s S1–S6 pack. See [docs/publish.md](docs/publish.md) and the release notes [docs/release-v0.5.0.md](docs/release-v0.5.0.md).
65
+ The Action wraps `gatefit run`. Adapter path is in *your* repo. `uses: gatefit/gatefit@v0.5.2` runs this repo’s goldens, including free S7–S9. 0.5.2 is the first release that ships them. This change does not publish that tag. See [docs/publish.md](docs/publish.md) and the release notes [docs/release-v0.5.2.md](docs/release-v0.5.2.md).
64
66
 
65
67
  Do not set `GATEFIT_LIVE_CLOCK` for the named gate. Do not use `sk_live`. Never treat Stripe HTTP 200 as proof of access.
66
68
 
@@ -83,7 +85,7 @@ One-time setup on npmjs.com for package `gatefit`: open Package settings, Truste
83
85
 
84
86
  ## License
85
87
 
86
- S1–S6, seats/races, dispute/unpaid, and live Test Clocks stay free. Those packs do not read `GATEFIT_LICENSE_KEY` and do not call Polar.
88
+ S1–S6, S7–S9 (no-card trial, portal downgrade, active entitlement summary), seats/races, dispute/unpaid, and live Test Clocks stay free. Those packs do not read `GATEFIT_LICENSE_KEY` and do not call Polar. 0.5.2 is the first release that ships S7–S9. This change does not publish `0.5.2`.
87
89
 
88
90
  The $49 Gatefit Indie License unlocks Pro packs. Chaos mode is live. Provider packs are not in this release. A missing, invalid, or revoked key exits non-zero and points at https://gatefit.io/#pricing.
89
91
 
package/action.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  name: Gatefit
2
- description: Run Gatefit S1–S6 entitlement goldens against an AccessAdapter (access DB, not Stripe HTTP 200)
2
+ description: Run Gatefit entitlement goldens against an AccessAdapter (access DB, not Stripe HTTP 200)
3
3
  author: gatefit
4
4
  branding:
5
5
  icon: shield
@@ -9,7 +9,7 @@ inputs:
9
9
  description: Path to the AccessAdapter module, relative to the caller workspace
10
10
  required: true
11
11
  suite:
12
- description: Golden file, directory, or id. Defaults to bundled S1–S6 goldens
12
+ description: Golden file, directory, or id. Defaults to the bundled goldens (S1–S9 and Phase 2 fixtures)
13
13
  required: false
14
14
  default: ""
15
15
  runs:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gatefit",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Stripe Entitlement Edge Pack — CI that asserts the access DB survives ugly billing transitions",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -46,10 +46,12 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "better-sqlite3": "^13.0.3",
50
49
  "tsx": "^4.20.5",
51
50
  "yaml": "^2.8.1"
52
51
  },
52
+ "optionalDependencies": {
53
+ "better-sqlite3": "^13.0.3"
54
+ },
53
55
  "devDependencies": {
54
56
  "@types/better-sqlite3": "^9.6.0",
55
57
  "@types/node": "^22.18.0",
@@ -23,6 +23,10 @@ export type BillingEvent = {
23
23
  httpStatus?: number;
24
24
  stripeHttpStatus?: number;
25
25
  cancel_at_period_end?: boolean;
26
+ /** Portal downgrade held until now >= period_end. Not applied while the flag is set early. */
27
+ downgrade_at_period_end?: boolean;
28
+ /** Plan to apply when a scheduled portal downgrade is due. Ignored until then. */
29
+ pending_plan?: string;
26
30
  period_end?: number;
27
31
  now?: number;
28
32
  /** Original timeline position. Chaos folds on this so arrival order is not access. */
@@ -18,6 +18,7 @@ export function isPaid(event: BillingEvent): boolean {
18
18
  export function isImmediateCancel(event: BillingEvent): boolean {
19
19
  return (
20
20
  event.canceled === true ||
21
+ event.status === "canceled" ||
21
22
  event.type === "customer.subscription.deleted" ||
22
23
  event.type === "customer.subscription.canceled"
23
24
  );
@@ -59,6 +60,31 @@ export function isTrialing(event: BillingEvent): boolean {
59
60
  return event.status === "trialing";
60
61
  }
61
62
 
63
+ export function isPaused(event: BillingEvent): boolean {
64
+ return event.status === "paused" || event.type === "customer.subscription.paused";
65
+ }
66
+
67
+ export function isTrialWillEnd(event: BillingEvent): boolean {
68
+ return event.type === "customer.subscription.trial_will_end";
69
+ }
70
+
71
+ export function isActiveEntitlementSummary(event: BillingEvent): boolean {
72
+ return event.type === "entitlements.active_entitlement_summary.updated";
73
+ }
74
+
75
+ export function isScheduledDowngrade(event: BillingEvent): boolean {
76
+ return event.downgrade_at_period_end === true && event.pending_plan !== undefined;
77
+ }
78
+
79
+ export function isScheduledDowngradeDue(event: BillingEvent): boolean {
80
+ return (
81
+ isScheduledDowngrade(event) &&
82
+ typeof event.now === "number" &&
83
+ typeof event.period_end === "number" &&
84
+ event.now >= event.period_end
85
+ );
86
+ }
87
+
62
88
  function withEntitlements(base: AccessSnapshot, event: BillingEvent): AccessSnapshot {
63
89
  const next: AccessSnapshot = { ...base };
64
90
  if (event.plan !== undefined) next.plan = event.plan;
@@ -71,18 +97,37 @@ export function nextSnapshot(current: AccessSnapshot, event: BillingEvent): Acce
71
97
  if (isDispute(event)) {
72
98
  return { ...current, locked: true, features: [] };
73
99
  }
100
+ // A trial-ending warning neither grants nor revokes, even with HTTP 200
101
+ // or status trialing on the same event.
102
+ if (isTrialWillEnd(event)) {
103
+ return withEntitlements(current, event);
104
+ }
105
+ // Active entitlement summaries replace features. They are not a payment.
106
+ if (isActiveEntitlementSummary(event)) {
107
+ return { ...current, features: event.features ?? [] };
108
+ }
109
+ if (isTrialing(event) || isPaused(event)) {
110
+ return withEntitlements({ ...current, locked: true }, event);
111
+ }
74
112
  if (
75
113
  isRefund(event) ||
76
114
  isImmediateCancel(event) ||
77
115
  isPastDue(event) ||
78
- isUnpaidExhaust(event) ||
79
- isTrialing(event)
116
+ isUnpaidExhaust(event)
80
117
  ) {
81
118
  return { ...current, locked: true };
82
119
  }
83
120
  if (isCapePeriodEnded(event)) {
84
121
  return { ...current, locked: true };
85
122
  }
123
+ if (isScheduledDowngrade(event) && !isScheduledDowngradeDue(event)) {
124
+ return current;
125
+ }
126
+ if (isScheduledDowngradeDue(event)) {
127
+ const next = withEntitlements({ ...current }, event);
128
+ next.plan = event.pending_plan;
129
+ return next;
130
+ }
86
131
  if (isPaid(event)) {
87
132
  return withEntitlements({ ...current, locked: false }, event);
88
133
  }
@@ -3,7 +3,31 @@ import type { AccessAdapter, AccessSnapshot, BillingEvent } from "../access-adap
3
3
  import { nextSnapshot } from "../access-rules.ts";
4
4
 
5
5
  const require = createRequire(import.meta.url);
6
- const Database = require("better-sqlite3") as typeof import("better-sqlite3");
6
+
7
+ function sqliteUnavailable(err: unknown): Error {
8
+ const detail = err instanceof Error ? err.message : String(err);
9
+ return new Error(
10
+ [
11
+ "gatefit: SQLite adapter cannot load better-sqlite3.",
12
+ "The native module is missing. better-sqlite3 is optional and ships prebuilds for linux, darwin, and win32 (x64 and arm64).",
13
+ "npm 9 still runs node-gyp during install and drops this optional package when that compile fails. npm 10 and newer unpack the shipped prebuild.",
14
+ "Fix: upgrade npm to 10 or newer and reinstall, or reinstall with npm install --ignore-scripts to use the shipped prebuild, or install a C++ compiler (Xcode Command Line Tools on macOS, or build-essential on Linux) and reinstall.",
15
+ "--version, --help, and adapter-check do not need this module.",
16
+ `Detail: ${detail}`,
17
+ ].join("\n"),
18
+ );
19
+ }
20
+
21
+ function loadDatabase(): typeof import("better-sqlite3") {
22
+ try {
23
+ const Database = require("better-sqlite3") as typeof import("better-sqlite3");
24
+ const probe = new Database(":memory:");
25
+ probe.close();
26
+ return Database;
27
+ } catch (err) {
28
+ throw sqliteUnavailable(err);
29
+ }
30
+ }
7
31
 
8
32
  type AccessRow = {
9
33
  customer_id: string;
@@ -24,6 +48,7 @@ function rowToSnapshot(row: AccessRow | undefined): AccessSnapshot {
24
48
 
25
49
  export function createAdapter(): AccessAdapter {
26
50
  const filename = process.env.GATEFIT_SQLITE_PATH ?? ":memory:";
51
+ const Database = loadDatabase();
27
52
  const db = new Database(filename);
28
53
  db.exec(`
29
54
  CREATE TABLE IF NOT EXISTS access (
package/src/cli.ts CHANGED
@@ -25,6 +25,7 @@ function usage(): string {
25
25
  " gatefit run --suite chaos --adapter <path> [--seed <value>]",
26
26
  " gatefit adapter-check",
27
27
  " gatefit live --adapter <path> [--scenario <id>]",
28
+ " gatefit --help",
28
29
  " gatefit --version",
29
30
  ].join("\n");
30
31
  }
@@ -96,11 +97,19 @@ function isVersionFlag(arg: string | undefined): boolean {
96
97
  return arg === "--version" || arg === "-v";
97
98
  }
98
99
 
100
+ function isHelpFlag(arg: string | undefined): boolean {
101
+ return arg === "--help" || arg === "-h";
102
+ }
103
+
99
104
  async function main(argv: string[]): Promise<number> {
100
105
  if (isVersionFlag(argv[0])) {
101
106
  console.log(`gatefit ${readPackageVersion()}`);
102
107
  return 0;
103
108
  }
109
+ if (isHelpFlag(argv[0])) {
110
+ console.log(usage());
111
+ return 0;
112
+ }
104
113
  const command = argv[0];
105
114
  if (command === "adapter-check") {
106
115
  return cmdAdapterCheck();
@@ -0,0 +1,26 @@
1
+ # Goldens
2
+
3
+ Fixture-first access checks. Each file is one customer and a list of `event` / `expect` steps. The runner is `src/fixture-driver.ts`. Time is the fixture `now` and `period_end` (unix seconds), not the wall clock. `GATEFIT_LIVE_CLOCK` is refused.
4
+
5
+ The Stripe API version is the pin in `src/stripe-clock.ts`: `2025-10-29.clover`. These files do not call Stripe. `sk_live` is refused. An event `httpStatus` of 200 is not access; the `expect` block is the access row.
6
+
7
+ S1–S6, the Phase 2 packs, and S7–S9 are free. They do not read `GATEFIT_LICENSE_KEY` and do not call Polar. The $49 Indie License unlocks Pro packs. Chaos mode is live. Provider packs are not in this release. 0.5.2 is the first release that ships S7–S9. This change does not publish `0.5.2`.
8
+
9
+ | Id | Pack | Access row |
10
+ | --- | --- | --- |
11
+ | `paid_grants_access` | S1 | `invoice.paid` unlocks. |
12
+ | `cancel_revokes_access` | S2 | Immediate cancel locks. |
13
+ | `refund_revokes_access` | S3 | Refund locks. |
14
+ | `trial_to_paid_grants` | S4 | `trialing` stays locked until `invoice.paid`. |
15
+ | `past_due_denies_until_recovery` | S5 | `past_due` locks until a later `invoice.paid`. |
16
+ | `duplicate_webhook_idempotent` | S6 | The same event id does not apply twice. |
17
+ | `seat_qty_vs_db_cap` | H6 | Seat quantity replaces the stored cap. |
18
+ | `cancel_at_period_end_keeps_access_until_end` | H6 | Cancel-at-period-end stays unlocked until `now >= period_end`. |
19
+ | `upgrade_plus_cape_race` | H6 | An upgrade during cancel-at-period-end changes plan and seats, then period end locks. |
20
+ | `dispute_revokes_or_flags` | H7 | `charge.dispute.created` locks and clears features. |
21
+ | `unpaid_after_retries_revokes` | H7 | `past_due`, then `unpaid`, then cancel, stays locked. |
22
+ | `trial_no_card` | S7 | No-card trial. `customer.subscription.trial_will_end` does not grant and does not revoke. A failed first charge, `paused`, and cancel stay locked. A later `invoice.paid` grants; a repeated `trial_will_end` does not revoke it. |
23
+ | `portal_downgrade_period_end_vs_immediate` | S8 | Customer Portal downgrade. `downgrade_at_period_end` keeps the current plan until `now >= period_end`, including one second early. The period-end event applies `pending_plan`. An immediate downgrade applies on that event while `now` is still before `period_end`. |
24
+ | `entitlements_active_summary` | S9 | `entitlements.active_entitlement_summary.updated` sets `features` to the active entitlement lookup keys, including an empty summary. It does not lock or unlock. |
25
+
26
+ S7–S9 are not live Test Clock ids. H6 and H7 are not either. The live lane ports only `paid_grants_access`, `cancel_revokes_access`, and `trial_to_paid_grants`.
@@ -0,0 +1,52 @@
1
+ # S9. Stripe Entitlements API active entitlement summary.
2
+ # Stripe API version 2025-10-29.clover (src/stripe-clock.ts).
3
+ # features on the event are the summary lookup keys. The access row features
4
+ # must match that active set. The summary does not grant or revoke access.
5
+ # HTTP 200 is not access. Fixture-only; not a live Test Clock id. No license key.
6
+ id: entitlements_active_summary
7
+ subject: cus_s9
8
+ steps:
9
+ - event:
10
+ id: evt_s9_summary_unpaid
11
+ type: entitlements.active_entitlement_summary.updated
12
+ customerId: cus_s9
13
+ features:
14
+ - priority
15
+ httpStatus: 200
16
+ expect:
17
+ locked: true
18
+ features:
19
+ - priority
20
+ - event:
21
+ id: evt_s9_paid
22
+ type: invoice.paid
23
+ customerId: cus_s9
24
+ plan: pro
25
+ expect:
26
+ locked: false
27
+ plan: pro
28
+ features:
29
+ - priority
30
+ - event:
31
+ id: evt_s9_summary_active
32
+ type: entitlements.active_entitlement_summary.updated
33
+ customerId: cus_s9
34
+ features:
35
+ - priority
36
+ - exports
37
+ httpStatus: 200
38
+ expect:
39
+ locked: false
40
+ plan: pro
41
+ features:
42
+ - priority
43
+ - exports
44
+ - event:
45
+ id: evt_s9_summary_empty
46
+ type: entitlements.active_entitlement_summary.updated
47
+ customerId: cus_s9
48
+ features: []
49
+ expect:
50
+ locked: false
51
+ plan: pro
52
+ features: []
@@ -0,0 +1,104 @@
1
+ # S8. Customer Portal downgrade. Stripe API version 2025-10-29.clover
2
+ # (src/stripe-clock.ts). Fixture now/period_end, not the wall clock.
3
+ # Scheduled downgrade waits until now >= period_end. Immediate downgrade
4
+ # changes the plan on that event. Never early, never late. HTTP 200 is not
5
+ # access. Fixture-only; not a live Test Clock id. No license key.
6
+ id: portal_downgrade_period_end_vs_immediate
7
+ subject: cus_s8
8
+ steps:
9
+ - event:
10
+ id: evt_s8_paid
11
+ type: invoice.paid
12
+ customerId: cus_s8
13
+ plan: pro
14
+ seats: 5
15
+ features:
16
+ - priority
17
+ expect:
18
+ locked: false
19
+ plan: pro
20
+ seats: 5
21
+ features:
22
+ - priority
23
+ - event:
24
+ id: evt_s8_scheduled
25
+ type: customer.subscription.updated
26
+ customerId: cus_s8
27
+ plan: starter
28
+ pending_plan: starter
29
+ downgrade_at_period_end: true
30
+ period_end: 1577923200
31
+ now: 1577836800
32
+ seats: 1
33
+ features:
34
+ - starter_support
35
+ httpStatus: 200
36
+ expect:
37
+ locked: false
38
+ plan: pro
39
+ seats: 5
40
+ features:
41
+ - priority
42
+ - event:
43
+ id: evt_s8_one_second_early
44
+ type: customer.subscription.updated
45
+ customerId: cus_s8
46
+ plan: starter
47
+ pending_plan: starter
48
+ downgrade_at_period_end: true
49
+ period_end: 1577923200
50
+ now: 1577923199
51
+ seats: 1
52
+ expect:
53
+ locked: false
54
+ plan: pro
55
+ seats: 5
56
+ features:
57
+ - priority
58
+ - event:
59
+ id: evt_s8_period_end
60
+ type: customer.subscription.updated
61
+ customerId: cus_s8
62
+ pending_plan: starter
63
+ downgrade_at_period_end: true
64
+ period_end: 1577923200
65
+ now: 1577923200
66
+ seats: 1
67
+ features:
68
+ - starter_support
69
+ expect:
70
+ locked: false
71
+ plan: starter
72
+ seats: 1
73
+ features:
74
+ - starter_support
75
+ - event:
76
+ id: evt_s8_reupgrade
77
+ type: invoice.paid
78
+ customerId: cus_s8
79
+ plan: pro
80
+ seats: 5
81
+ features:
82
+ - priority
83
+ expect:
84
+ locked: false
85
+ plan: pro
86
+ seats: 5
87
+ features:
88
+ - priority
89
+ - event:
90
+ id: evt_s8_immediate
91
+ type: customer.subscription.updated
92
+ customerId: cus_s8
93
+ plan: starter
94
+ seats: 1
95
+ features:
96
+ - starter_support
97
+ period_end: 1578009600
98
+ now: 1577923200
99
+ expect:
100
+ locked: false
101
+ plan: starter
102
+ seats: 1
103
+ features:
104
+ - starter_support
@@ -0,0 +1,71 @@
1
+ # S7. Trial started with no card. Stripe API version 2025-10-29.clover
2
+ # (src/stripe-clock.ts). Fixture-only; not a live Test Clock id.
3
+ # trial_will_end must not grant and must not revoke. A failed first charge,
4
+ # pause, and cancel stay locked. HTTP 200 is not access. No license key.
5
+ id: trial_no_card
6
+ subject: cus_s7
7
+ steps:
8
+ - event:
9
+ id: evt_s7_trialing
10
+ type: customer.subscription.updated
11
+ customerId: cus_s7
12
+ status: trialing
13
+ plan: pro
14
+ expect:
15
+ locked: true
16
+ plan: pro
17
+ - event:
18
+ id: evt_s7_will_end
19
+ type: customer.subscription.trial_will_end
20
+ customerId: cus_s7
21
+ status: trialing
22
+ plan: pro
23
+ httpStatus: 200
24
+ expect:
25
+ locked: true
26
+ plan: pro
27
+ - event:
28
+ id: evt_s7_first_charge_failed
29
+ type: invoice.payment_failed
30
+ customerId: cus_s7
31
+ status: past_due
32
+ plan: pro
33
+ expect:
34
+ locked: true
35
+ plan: pro
36
+ - event:
37
+ id: evt_s7_paused
38
+ type: customer.subscription.updated
39
+ customerId: cus_s7
40
+ status: paused
41
+ plan: pro
42
+ expect:
43
+ locked: true
44
+ plan: pro
45
+ - event:
46
+ id: evt_s7_canceled
47
+ type: customer.subscription.deleted
48
+ customerId: cus_s7
49
+ status: canceled
50
+ canceled: true
51
+ expect:
52
+ locked: true
53
+ plan: pro
54
+ - event:
55
+ id: evt_s7_paid_later
56
+ type: invoice.paid
57
+ customerId: cus_s7
58
+ plan: pro
59
+ expect:
60
+ locked: false
61
+ plan: pro
62
+ - event:
63
+ id: evt_s7_will_end_after_paid
64
+ type: customer.subscription.trial_will_end
65
+ customerId: cus_s7
66
+ status: trialing
67
+ plan: pro
68
+ httpStatus: 200
69
+ expect:
70
+ locked: false
71
+ plan: pro