Skip to content

Commit 9d82304

Browse files
chore(payments): add Solana devnet test helpers, wallet-subscribe route & plans
- app/api/wallet-subscribe: standalone wallet subscribe route - public/wallet-subscribe.html: in-app Phantom subscribe test page - scripts/devnet-*: short-plan creation + renewal helpers (devnet:renew script) - scripts/mint-session-cookies, seed-solana-test-product: E2E test fixtures - plans/009-013: Solana payment hardening plans Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ed215f0 commit 9d82304

12 files changed

Lines changed: 1588 additions & 0 deletions

app/api/wallet-subscribe/route.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Dev-only: serves the desktop wallet subscription test page.
3+
*
4+
* The page lives in public/wallet-subscribe.html, but proxy.ts + next-intl
5+
* redirect bare static paths to /en/… (breaking them). /api/* is exempt from
6+
* that rewrite, so we serve the same HTML from here to keep it same-origin
7+
* (the page's fetch to /api/payments/checkout needs the session cookie).
8+
*
9+
* Open: http://code-academy.lvh.me:3005/api/wallet-subscribe?planId=10006
10+
*/
11+
12+
import { NextResponse } from 'next/server'
13+
import { readFileSync } from 'node:fs'
14+
import { join } from 'node:path'
15+
16+
export const runtime = 'nodejs'
17+
18+
export async function GET() {
19+
const html = readFileSync(join(process.cwd(), 'public', 'wallet-subscribe.html'), 'utf8')
20+
return new NextResponse(html, {
21+
headers: { 'Content-Type': 'text/html; charset=utf-8' },
22+
})
23+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"start": "next start",
1212
"lint": "eslint",
1313
"typecheck": "tsc --noEmit",
14+
"devnet:renew": "tsx scripts/devnet-renew.ts",
1415
"test": "npx playwright test",
1516
"test:unit": "vitest run",
1617
"db:reset": "supabase db reset",
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
# Plan 009: Payment schema is provably present before the payments code runs in production
2+
3+
> **Executor instructions**: Follow this plan step by step. Run every
4+
> verification command and confirm the expected result before moving to the
5+
> next step. If anything in the "STOP conditions" section occurs, stop and
6+
> report — do not improvise. When done, update the status row for this plan
7+
> in `plans/README.md`.
8+
>
9+
> **Drift check (run first)**:
10+
> `git diff --stat e768e357..HEAD -- supabase/migrations app/api/payments app/[locale]/platform/revenue`
11+
> If any in-scope file changed since this plan was written, compare the
12+
> "Current state" excerpts against the live code before proceeding; on a
13+
> mismatch, treat it as a STOP condition.
14+
15+
## Status
16+
17+
- **Priority**: P1
18+
- **Effort**: S
19+
- **Risk**: LOW (adds a guard script + CI step + comment edits; touches no runtime code path)
20+
- **Depends on**: none
21+
- **Category**: migration
22+
- **Planned at**: commit `e768e357`, 2026-06-20
23+
24+
## Why this matters
25+
26+
This branch ships payments code that **reads database objects which exist only
27+
in migrations the branch itself marks "LOCAL-ONLY … not pushed to cloud."** If
28+
the branch is deployed to production without those migrations being applied
29+
first, three things break silently or loudly:
30+
31+
1. **Checkout fails**`app/api/payments/checkout/route.ts` inserts
32+
`settlement_currency/base/mint/sol_usd`, columns added by
33+
`20260617130000_solana_settlement_lock.sql`. Missing columns → every Solana
34+
checkout INSERT errors → "Failed to create transaction".
35+
2. **Replay protection silently disappears** — the Solana double-spend guard
36+
relies on the partial-unique index `transactions_provider_charge_id_unique`
37+
from `20260615180000_consume_solana_signature.sql`. Without it,
38+
`app/api/payments/solana/verify/route.ts` can no longer reject a replayed
39+
on-chain signature (the 23505 branch never fires).
40+
3. **The platform revenue page 500s**`app/[locale]/platform/revenue/page.tsx`
41+
calls `rpc('get_platform_revenue')`, defined only in
42+
`20260617120000_get_platform_revenue.sql`.
43+
44+
The fix is **not** to push migrations from this plan (that mutates production —
45+
out of scope; it is an operator action documented in the checklist below).
46+
The fix an executor *can* deliver is a **preflight guard**: a script that asserts
47+
the required DB objects exist and fails the deploy/CI if they do not, so a
48+
half-applied schema can never reach users. Plus de-stale the two migration
49+
comments that tell a future operator "do not push."
50+
51+
## Current state
52+
53+
- The six payment migrations added by this branch (verified with
54+
`git diff --name-only --diff-filter=A $(git merge-base master HEAD)..HEAD -- supabase/migrations`):
55+
- `supabase/migrations/20260615150000_add_lemonsqueezy_solana_providers.sql`
56+
- `supabase/migrations/20260615160000_tenant_payment_wallets.sql`
57+
- `supabase/migrations/20260615170000_solana_native_subscriptions.sql`
58+
- `supabase/migrations/20260615180000_consume_solana_signature.sql`
59+
- `supabase/migrations/20260617120000_get_platform_revenue.sql`
60+
- `supabase/migrations/20260617130000_solana_settlement_lock.sql`
61+
- Two still carry an explicit "do not push" comment:
62+
- `20260617120000_get_platform_revenue.sql:26-27``-- LOCAL-ONLY: like the other 2026-06-15 payment migrations on this branch, this -- is not pushed to cloud until the operator says so.`
63+
- `20260617130000_solana_settlement_lock.sql:14``-- LOCAL-ONLY until the operator applies the #280/#334 migration set to cloud.`
64+
- The replay index (target of the guard), `20260615180000_consume_solana_signature.sql:21-23`:
65+
```sql
66+
CREATE UNIQUE INDEX IF NOT EXISTS transactions_provider_charge_id_unique
67+
ON public.transactions (payment_provider, provider_charge_id)
68+
WHERE provider_charge_id IS NOT NULL AND status = 'successful';
69+
```
70+
- The settlement columns, `20260617130000_solana_settlement_lock.sql:16-20`:
71+
`settlement_currency TEXT`, `settlement_base BIGINT`, `settlement_mint TEXT`, `settlement_sol_usd NUMERIC(20,8)`.
72+
- The repo already has a CI workflow at `.github/workflows/ci.yml` (added earlier
73+
on this branch) — the guard step is added there.
74+
- The repo's Node scripts convention: plain `.mjs`/`.ts` under `scripts/`, run
75+
with `node` or `tsx`. The Supabase service-role client is created in app code
76+
via `@supabase/supabase-js`'s `createClient(url, serviceKey)` — see
77+
`app/api/payments/checkout/route.ts` for the env var names
78+
(`NEXT_PUBLIC_SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY`).
79+
80+
## Commands you will need
81+
82+
| Purpose | Command | Expected on success |
83+
|-----------|---------------------------------|---------------------|
84+
| Typecheck | `npm run typecheck` | exit 0, no errors |
85+
| Unit tests| `npm run test:unit` | all pass |
86+
| Lint | `npm run lint` | no NEW errors (repo has a known lint baseline; see plans/005) |
87+
| Run guard | `node scripts/check-payment-schema.mjs` | prints a clear PASS/FAIL; exit 0 only when all objects present |
88+
89+
## Scope
90+
91+
**In scope** (the only files you should create/modify):
92+
- `scripts/check-payment-schema.mjs` (create)
93+
- `.github/workflows/ci.yml` (add one guarded job/step that runs the script)
94+
- `supabase/migrations/20260617120000_get_platform_revenue.sql` (comment edit only — lines 26-27)
95+
- `supabase/migrations/20260617130000_solana_settlement_lock.sql` (comment edit only — line 14)
96+
97+
**Out of scope** (do NOT touch):
98+
- Any SQL statement body inside the migrations — change only the comment lines named above. The migrations are already applied locally; altering their DDL would create drift.
99+
- Any application route or page — this plan adds a guard, it does not change runtime behavior.
100+
- **Do NOT run `supabase db push` or any command that writes to a remote/cloud database.** That is the operator checklist below, not executor work.
101+
102+
## Steps
103+
104+
### Step 1: Write the preflight schema-check script
105+
106+
Create `scripts/check-payment-schema.mjs`. It connects with the service-role key
107+
and asserts every object the shipped payments code depends on exists. It must:
108+
109+
- Read `NEXT_PUBLIC_SUPABASE_URL` (or `SUPABASE_URL`) and `SUPABASE_SERVICE_ROLE_KEY` from env.
110+
If either is missing, print `SKIP: Supabase env not set — schema check skipped` and exit **0** (so CI without DB creds does not fail; the gate is best-effort).
111+
- Assert these objects, querying Postgres catalogs via the service-role client
112+
(use `supabase.rpc` is not available for catalogs — instead run lightweight
113+
probe queries that fail iff the object is absent):
114+
- **Columns** on `public.transactions`: `settlement_currency`, `settlement_base`, `settlement_mint`, `settlement_sol_usd`, `provider_charge_id`.
115+
Probe: `select settlement_currency, settlement_base, settlement_mint, settlement_sol_usd, provider_charge_id from transactions limit 0`. A PostgREST/Postgres error mentioning a missing column = FAIL.
116+
- **Table** `public.tenant_payment_wallets`: probe `select 1 from tenant_payment_wallets limit 0`.
117+
- **RPC** `get_platform_revenue`: probe `supabase.rpc('get_platform_revenue')`; a "function does not exist" error = FAIL (any other error, e.g. permission, still means it exists → treat as PASS for presence).
118+
- **Index** `transactions_provider_charge_id_unique`: probe with a raw catalog read if available; if you cannot read `pg_indexes` through the client, SKIP this single check and print a warning rather than failing (the column probe above already covers the dependent column).
119+
- Collect failures into a list. At the end:
120+
- If empty: print `PASS: payment schema present` and exit 0.
121+
- Else: print `FAIL: missing payment schema objects:` followed by the list and the remediation line `Run: supabase db push (see plans/009)`, then exit **1**.
122+
- Wrap each probe in try/catch so one missing object does not abort the others — report ALL missing objects in one run.
123+
124+
Keep it dependency-free beyond `@supabase/supabase-js` (already a dependency).
125+
126+
**Verify**: `node scripts/check-payment-schema.mjs` against your local DB →
127+
prints `PASS: payment schema present` and exits 0 (local migrations are applied,
128+
so every object exists). If env is unset locally, it prints `SKIP …` and exits 0.
129+
130+
### Step 2: Wire the guard into CI as a non-fatal-when-skipped gate
131+
132+
In `.github/workflows/ci.yml`, add a step (in the existing test/build job, after
133+
dependencies install) that runs `node scripts/check-payment-schema.mjs`. The step
134+
inherits `NEXT_PUBLIC_SUPABASE_URL` / `SUPABASE_SERVICE_ROLE_KEY` from CI secrets
135+
if present; when absent the script self-skips (exit 0), so this never breaks PRs
136+
that lack DB creds, but it **fails the pipeline when creds are present and the
137+
schema is incomplete** (the deploy-target case).
138+
139+
**Verify**: `git diff .github/workflows/ci.yml` shows exactly one added step
140+
invoking the script; the YAML still parses (no tab characters; consistent
141+
indentation with the surrounding steps).
142+
143+
### Step 3: De-stale the two "do not push" migration comments
144+
145+
These comments will mislead the operator who applies the set. Replace the
146+
"LOCAL-ONLY … not pushed" wording with a neutral note that the migration is part
147+
of the #280/#334 payment set and must be applied before deploying the payments
148+
code (see plan 009). Edit **only** the comment lines:
149+
- `20260617120000_get_platform_revenue.sql:26-27`
150+
- `20260617130000_solana_settlement_lock.sql:14`
151+
152+
Do not alter any SQL.
153+
154+
**Verify**: `grep -rn "LOCAL-ONLY\|not pushed" supabase/migrations/` returns no
155+
matches.
156+
157+
### Step 4: Full verification
158+
159+
**Verify**:
160+
- `npm run typecheck` → exit 0
161+
- `npm run test:unit` → all pass (you added no tests; nothing regresses)
162+
- `git status` → only the four in-scope files changed
163+
164+
## Operator checklist (NOT executor work — for the human deploying this branch)
165+
166+
Before deploying this branch to any environment, the operator runs, against that
167+
environment's database:
168+
169+
1. `supabase migration list` — confirm the six `2026061515xxxx``2026061718xxxx`
170+
payment migrations show as **not yet applied** remotely.
171+
2. `supabase db push` — apply them (each uses `IF NOT EXISTS` / `create or
172+
replace`, so re-running is safe).
173+
3. `node scripts/check-payment-schema.mjs` against that DB → must print `PASS`.
174+
175+
Only after PASS should the application image be promoted.
176+
177+
## Test plan
178+
179+
- No new unit tests required (the deliverable is an ops guard script).
180+
- Manual: run `node scripts/check-payment-schema.mjs` against the local DB (expect PASS) and, to confirm the failure path, temporarily point it at a DB without the columns (expect a FAIL list + exit 1) — do **not** commit any such config.
181+
182+
## Done criteria
183+
184+
ALL must hold:
185+
186+
- [ ] `scripts/check-payment-schema.mjs` exists; running it locally prints `PASS` and exits 0
187+
- [ ] The script exits 0 with a `SKIP` message when Supabase env vars are unset
188+
- [ ] `.github/workflows/ci.yml` runs the script in CI
189+
- [ ] `grep -rn "LOCAL-ONLY\|not pushed" supabase/migrations/` → no matches
190+
- [ ] `npm run typecheck` exits 0; `npm run test:unit` passes
191+
- [ ] `git status` shows only the four in-scope files modified
192+
- [ ] `plans/README.md` status row updated
193+
194+
## STOP conditions
195+
196+
Stop and report back (do not improvise) if:
197+
198+
- The settlement columns / RPC / index do NOT actually exist in your local DB
199+
(the probe FAILs locally) — that means local migrations are not applied; report
200+
it rather than "fixing" by pushing anything.
201+
- The `@supabase/supabase-js` client cannot run the catalog/probe queries with the
202+
service-role key in your environment — report the limitation; do not switch to a
203+
direct `pg`/`psql` connection (not a repo dependency) without approval.
204+
- You discover a runtime code path already guards against the missing schema
205+
(e.g. a feature flag) — report it; the guard may be redundant.
206+
207+
## Maintenance notes
208+
209+
- When future payment migrations add columns/RPCs the code depends on, extend
210+
`scripts/check-payment-schema.mjs` with the new probes — it is the single
211+
deploy gate for payment schema presence.
212+
- A reviewer should confirm the CI step's skip-on-missing-creds behavior so the
213+
gate never blocks unrelated PRs, while still failing on an incomplete deploy DB.
214+
- Deferred: making each route degrade gracefully (e.g. checkout returning a clean
215+
503 when settlement columns are absent) was intentionally left out — the guard
216+
prevents the broken-schema deploy in the first place, which is higher leverage.

0 commit comments

Comments
 (0)