Billing works end to end against the Stripe sandbox for seed, checkout, portal, cancel-now with refund, and workspace deletion. Two authorization bugs blocked it first. One I changed in code and one I only worked around. The payer-removal and payer-claim flow (Task 10) was not tested.
Environment
- Docker was not up as described. Only the billing Postgres was running, so Traefik, Redpanda, billing Redis and the note and authorization Postgres and Redis were all down. I ran docker start on those six containers, with no compose up/down and nothing stopped.
- Webhook 502s. Those containers were down when I first seeded, so your stripe listen got the three 502s you saw. Those Stripe events were not redelivered. The seed-time price.created and billing_portal.configuration.created events are lost to billing, and nothing depended on them.
- Seed env. cmd/billingseed/.env.local is filled from the billing key by script (tmp/billing/fill.sh), with no secret printed.
- Services left running:
┌───────────────┬────────────────────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Service │ PID │ Started with │ ├───────────────┼────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ billing │ 159649 (nx wrapper 158274) │ nx run billing:run, log tmp/billing/e2e-billing.log │ ├───────────────┼────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ note │ 160637 │ bash tmp/billing/run-note.sh (go run ./cmd/note/ with env files sourced), log tmp/billing/e2e-note.log │ ├───────────────┼────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ authorization │ 165940 │ bash tmp/billing/run-auth.sh (go run, APP_ENV=production), log tmp/billing/e2e-authorization.log │ └───────────────┴────────────────────────────┴────────────────────────────────────────────────────────────────────────────────────────────────────────┘
- Helpers. tmp/billing/lib.sh has the as curl helper, and tmp/billing/pw/pay.mjs is the headless Playwright payment script.
- Test data. Test workspace is 01a13526-71fe-74e0-928a-63f27211212e (now deleted).
Results
- Seed (Task 7): PASS.
- The first run created 5 prices, matching 49,000, 470,400, 99,000, 950,400 and 2,490,000 VND.
- The second run created nothing, and stripe prices list shows exactly 5.
- GET /plans returns the prices.
- Workspace and permissions: PASS after a workaround. See Issues 1 and 2.
- Checkout (Task 8): PASS.
- The Plus monthly checkout session was paid headlessly on the first attempt, and redirected to ?checkout=success.
- All 13 webhook deliveries returned 204.
- Entitlements show plus/active, and one subscription row exists (sub_1UQ4Y37EQBrtVjFjEnpXpmcx).
- Idempotency: PASS. stripe events resend evt_1UQ4Y57EQBrtVjFjpfnsl1lt was handled a second time with still exactly 1 subscription row and 3 processed events.
- Portal: PASS. POST portal-session returns 200 with a billing.stripe.com URL. ntgnguyen (111, no billing role on this workspace) gets 403 forbidden.
- Webhook signature: PASS. A bad signature with Content-Type: application/json returns 400 webhookSignatureInvalid. The Stripe-Signature header was not checked as a separate case.
- Cancel now (Task 9): PASS.
- Preview returned amount 48999 of 49000.
- cancel-now returned 200 with subscription null and the payer still set, and entitlements went to free.
- Stripe shows the subscription canceled and refund re_3UQ4Y27EQBrtVjFj0vqouVn3 for 48999, succeeded.
- The DB refund row is succeeded.
- Workspace deletion (Task 10, partial): PASS.
- I subscribed again (sub_1UQ4ZF7EQBrtVjFj4Rfa272X) and deleted the workspace with DELETE /note/workspaces/{id} (204).
- Within seconds the subscription was canceled and refund re_3UQ4ZD7EQBrtVjFj1M0DjyUj (48999) succeeded.
- Deleting a workspace with no subscription also worked (204), with no billing errors.
Issues
- Authorization bootstrap omits the billing policies.
- BootStrapPolicies in internal/authorization/app/app.go has a hardcoded policy list with no billing_read or billing_manage rows. The rows exist only in policy.csv, which is embedded for tests.
- Every billing call returned 403 user 112 lacks billing permission.
- Even with the rows added, AddPolicies drops the whole batch if any one policy already exists in the DB. So on an existing dev DB the new rows are never added.
- Not asked: you said to add the policies to "the files csv". policy.csv already has them and policy_fixtures.csv holds only g lines, so I added the four rows to the hardcoded list in app.go instead (a 4-line change, uncommitted).
- I also inserted the four rows directly into the dev authorization DB, because the batch drop meant the app.go change alone did not take effect.
- In development mode, LoadFixtures makes DB-persisted memberships ineffective.
- Authorization runs LoadFixtures when APP_ENV=development. After a restart, workspaces created at runtime were denied even for plain read: the new workspace and user 101's old workspace both returned 403 on GET /note/workspaces/{id}/tree.
- With fixtures disabled (APP_ENV=production), the same users were allowed.
- My guess is LoadPoliciesFromString in internal/authorization/app/policyloader.go:13-17 builds a temp enforcer on enforcer.GetModel(), the same model object, so it overwrites the real enforcer's loaded policies. I did not confirm this.
- Left in place: I am running authorization as APP_ENV=production to avoid it.
- Note does not build through nx. nx run note:run regenerated pkg/api/note/*.gen.go with oapi-codegen v2.7.0, although mise exec -- oapi-codegen --version reports v2.8.0. That produced internal/note/controller/http/workspace.go:486: undefined: note.GetWorkspaceTreeParamsSortUpdatedAtDesc.
- I ran git checkout on those two generated files to undo the rewrite, so they are clean again.
- I started note with a plain go run and sourced env files instead.
- Running nx run note:run will rewrite those files again.
- Billing seed logs the Stripe secret key in plaintext. The second just seed-billing run printed the full sk_test_… key in its startup msg=configuration config=… line (Stripe:{SecretKey:…}) at INFO level. I did not include it here. The log is only in my terminal output, not saved to a file. Note logs the same config line, so the config printing should mask secrets.
- Minor issues.
- The webhook route rejects a bad-signature POST sent without Content-Type: application/json with 400 invalid instead of webhookSignatureInvalid. Real Stripe sends JSON, so this only affects hand-made requests.
- Note and authorization spam Post "https://localhost:4318/v1/logs": connection refused because no OTLP collector is running.
- Billing returned authorizationServiceError (503) and Unavailable warnings while I restarted authorization.
- subscriptions.provider_payment_id is empty for subscription payments. That may be intended.
Not tested
- Payer removal and claim (Task 10). Demoting the payer and POST payer/claim need a second owner and setup-mode checkout.
- Plan change through the portal. I did not complete a plan change in the portal.
- Extra test cards. The 3D Secure, declined and failed-charge cards were not tried.
- Duplicate-checkout guard. The optional guard was not tried.
- Hourly reconcile and daily prune crons. Not observed.
Nothing was committed or pushed. The only tracked-file changes are your AGENTS.md edit and my app.go edit.