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 +20 -1
- package/README.md +11 -9
- package/action.yml +2 -2
- package/package.json +4 -2
- package/src/access-adapter.ts +4 -0
- package/src/access-rules.ts +47 -2
- package/src/adapters/sqlite.ts +26 -1
- package/src/cli.ts +9 -0
- package/tests/entitlements/goldens/README.md +26 -0
- package/tests/entitlements/goldens/entitlements_active_summary.yaml +52 -0
- package/tests/entitlements/goldens/portal_downgrade_period_end_vs_immediate.yaml +104 -0
- package/tests/entitlements/goldens/trial_no_card.yaml +71 -0
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.
|
|
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
|
|
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
|
|
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.
|
|
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`).
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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–
|
|
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.
|
|
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",
|
package/src/access-adapter.ts
CHANGED
|
@@ -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. */
|
package/src/access-rules.ts
CHANGED
|
@@ -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
|
}
|
package/src/adapters/sqlite.ts
CHANGED
|
@@ -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
|
-
|
|
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
|