Skip to content

Instantly share code, notes, and snippets.

@KevinNitroG
Last active October 13, 2026 12:25
Show Gist options
  • Select an option

  • Save KevinNitroG/d3d95788e82f1b841c7b20aec0f0baab to your computer and use it in GitHub Desktop.

Select an option

Save KevinNitroG/d3d95788e82f1b841c7b20aec0f0baab to your computer and use it in GitHub Desktop.
Notopia billing sonnet subagent test out the flow of billing stripe, session chat

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

  1. 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.
  2. Workspace and permissions: PASS after a workaround. See Issues 1 and 2.
  3. 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).
  4. Idempotency: PASS. stripe events resend evt_1UQ4Y57EQBrtVjFjpfnsl1lt was handled a second time with still exactly 1 subscription row and 3 processed events.
  5. Portal: PASS. POST portal-session returns 200 with a billing.stripe.com URL. ntgnguyen (111, no billing role on this workspace) gets 403 forbidden.
  6. 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.
  7. 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.
  8. 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

All three fixes are committed, not pushed:

  • 2c307bd4, generated code going stale (3): note and billing already had the oapi-codegen version in their nx gen inputs (it's a named input in nx.json); only api/share was missing it, so that's all this adds.
    • The likely real cause: a plain shell finds an old oapi-codegen 2.7.0 on PATH, while mise pins 2.8.0, so nx gets a different version depending on how it's started. The subagent's own first run outside mise exec regenerated share.gen.go with 2.7.0, and it reverted that.
    • Run nx under mise (mise exec -- nx …, or a shell with mise activated) so the version is always 2.8.0. The oapi-codegen --version you pasted shows 2.8.0, so your shell is fine.
  • 97cdbcf7, secrets in the config log (4): a new commonconfig.Secret type prints [REDACTED] in slog text, slog JSON, every fmt format and json.Marshal. Code that needs the real value calls .Reveal().
    • Converted fields: database password and URL (the URL can contain the password), Authentik token, Meilisearch key, and the Stripe secret key and webhook secret.
    • It loaded the billingseed config through viper to confirm env values still come through, and the startup line now shows [REDACTED].
  • 24c6f124, billing permissions (1): bootstrap now reads the policies from the embedded policy.csv instead of a hardcoded list, so the two can't drift again.
    • The loader uses casbin's AddPoliciesEx, which adds only the missing rows, so existing databases pick up the billing rows on restart.
    • A test removes the billing rows, re-runs bootstrap, and checks they're back and saved. It wasn't run against the old code first to show it failing.

After the commits, the authorization and config tests pass (140) and lint is clean for all five Go services. Your AGENTS.md is untouched.

Problem 2 (development mode ignoring memberships stored in the database) is still unexplained. The first subagent blamed LoadPoliciesFromString for overwriting the live enforcer's policies, but casbin's LoadPolicy works on a copy of the model, so that's probably not it. That matters now, because bootstrap runs through the same function in every environment. The new test passing supports it being safe, but I haven't checked directly that memberships stored in the database survive a restart.

Authorization is still running in production mode (PID 165940) from the test run, and billing and note are still up. Want me to dig into problem 2 next?


Everything is committed and nothing is pushed. Branch kevinnitrog/billing-03-service, working tree clean.

Last commits:

  • 1d3267ee: your AGENTS.md edit.
  • 655433b0: a "Status log" section added to the end of docs/superpowers/plans/2026-10-11-billing-03-billing-service.md. The same text is in the superpowers progress file, .superpowers/sdd/2026-10-11-billing-03-billing-service/progress.md, but that folder is gitignored, so the plan file is the copy that lasts. I also updated my memory so the next session starts from it.

Progress:

  • Tasks 1–10 of plan 03 are done and their checkboxes ticked.
  • Since then, also committed:
    • typed Kafka handlers
    • the Stripe CLI added to mise, with stripe-listen forwarding all snapshot events
    • the oapi-codegen version as an nx gen input
    • secrets redacted in the startup config log
    • the authorization fix that adds the billing permissions on startup, including to existing databases
    • the two stories, watermill-event-gaps.md and BILLING_PAYMENT_PROVIDERS.md
  • Stripe sandbox E2E passed: seed, checkout and webhooks, resending an event, portal, bad signature rejected, cancel now with refund, and workspace deletion with refund.
  • Not tested yet: payer removal and claim, a plan change inside the portal, the failing and 3D Secure test cards, the duplicate-checkout guard, and the crons.

Open issues (all in the status log):

  1. With APP_ENV=development, authorization ignores workspace memberships stored in its database after a restart, so runtime-created workspaces get 403. The cause is unknown; the first subagent's guess is probably wrong.
  2. A plain shell finds an old oapi-codegen 2.7.0, while mise pins 2.8.0. Run nx through mise, or the generated code drifts back.
  3. A hand-made webhook request without a JSON content type gets the generic 400 instead of the signature error. Minor; real Stripe always sends JSON.
  4. Stripe events that got 502 while the infrastructure was down were not redelivered. Harmless here, but stripe events resend is the tool when debugging.
  5. The whole-branch review is still to do, after Tasks 11+.
  6. Tasks 11–12 are on hold for your research, and the OpenAPI 3.1 conversion is parked.

Billing, note and authorization (in production mode) are still running from the test, with logs in tmp/billing/. The manual guide is tmp/TEST_FLOW_BILLING.md.

Enjoy dinner.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment