Skip to content

Phase 1: billing usage visibility, Stripe portal, tenant-auth binding - #4

Closed
crewcricle wants to merge 2 commits into
vorflux/phase0-foundations-queue-schedulerfrom
vorflux/phase1-billing-tenant-auth
Closed

Phase 1: billing usage visibility, Stripe portal, tenant-auth binding#4
crewcricle wants to merge 2 commits into
vorflux/phase0-foundations-queue-schedulerfrom
vorflux/phase1-billing-tenant-auth

Conversation

@crewcricle

@crewcricle crewcricle commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Billing usage visibility, Stripe Billing Portal, tenant-auth binding, and durable trial email sends.

Changes

  • Tenant-auth binding (C8/D20): new require_client_id dependency derives client_id from JWT identity via user_client_map, never from request input. Strict verify_supabase_jwt_strict rejects with 401 when SUPABASE_JWT_SECRET is unset (no anonymous fallback). Migration 013_user_client_map.sql with RLS service_role_all.
  • GET /billing/usage: returns per-job-type {used, cap, remaining} from TRIAL_CAPS (single source in trial_gate.py). Active subscriptions report unlimited (cap/remaining = null). Handles missing keys (0), missing client (404), cross-tenant rejection.
  • POST /billing/portal: creates Stripe Billing Portal session, parametrized with configuration (not configuration_id), return URL → dashboard settings. Missing customer → 422, Stripe error → 502.
  • Durable trial emails (C4): jobs/trial_emails.py Resend sends now enqueue via arq send_email_task. Idempotency recorded before enqueue, rolled back on enqueue failure (no phantom records blocking future sends).
  • Config/docs: STRIPE_PORTAL_CONFIG_ID and DASHBOARD_URL documented in DEPLOY.md and backend/.env.example.

Testing

Test Command Result
Unit suite (117 tests) cd backend && uv run pytest tests/ -q 117 passed, 2 pre-existing warnings
Phase 1 tests (30 tests) uv run pytest tests/unit/test_{tenant_auth,billing_usage,billing_portal,trial_emails_enqueue}.py -q 30 passed
Migration apply (scratch Postgres) Apply 001013 on fresh postgres:15 All 13 applied cleanly; user_client_map PK, FK, RLS verified

Cross-tenant denial, empty-secret rejection, active-sub unlimited, idempotency rollback all verified in unit tests. Live Stripe portal and real JWT deferred to post-deploy per agreed workflow.

Artifacts


Attached Images

[phase1_test_results.log]

View in Vorflux Session


Session Details

  • Session: View Session
  • Requested by: Unknown
  • Address comments on this PR. Add (aside) to your comment to have me ignore it.

* Phase 0: arq task queue, durable webhooks, single-active scheduler, migrations

- Add arq>=0.26.0 + config settings (redis_url, worker_role, dashboard_url,
  stripe_portal_config_id)
- New backend/task_queue.py (arq WorkerSettings, get_arq_pool, record_dead_letter,
  inbound processing tasks, durable outbound wrappers for all 5 integrations
  Twilio/Resend/GBP/Square/DataForSEO, cron entrypoints) + backend/worker.py
  (module named task_queue, not queue, to avoid shadowing stdlib queue that
  redis imports)
- Migrations 010_webhook_events, 011_dead_letter (both +RLS service_role_all),
  012_booking_credentials (moved to Phase 0 per C1)
- Refactor routers/webhooks.py: persist inbound events to webhook_events,
  dedupe by idempotency key, return 200 fast, enqueue processing. GBP inbound
  decodes Pub/Sub push envelope, uses messageId as key, fetches review resource
  before drafting (C3)
- Refactor scheduler.py to enqueue-only; main.py lifespan branches on worker_role
  (web = arq pool only; scheduler = APScheduler enqueue-only + arq pool)
- Mandatory utils/reconcile.py reconcile_pending_webhooks (every 5 min, C4)
- Deploy: localmate-redis (AOF, noeviction, 128MB), localmate-worker,
  localmate-scheduler on deploy_default; WORKER_ROLE=web on backend; update
  deploy_backend.sh, DEPLOY.md, Dockerfile, .env.example (REDIS_URL, WORKER_ROLE,
  STRIPE_PORTAL_CONFIG_ID, DASHBOARD_URL)
- Tests: webhook_events, dead_letter, queue_tasks, scheduler_enqueue,
  outbound_durable, reconcile, gbp_envelope_decode; new stub env in conftest
- pytest pythonpath config so tests collect without PYTHONPATH

* refactor(phase0): remove dead code, dedupe imports, fix stale docstring

- task_queue: drop unused _mark_event bump param; hoist datetime import
- reconcile: fix stale queue.WorkerSettings docstring ref -> task_queue
- main: dedupe get_arq_pool import across lifespan branches; cache logger
- tests: remove unused imports/params (json, insert_id, redundant webhooks imports)

* fix(phase0): address code-review blocking issues (C4 reliability, C10 gate)

Item 1 (CRITICAL): arq retries + dead-lettering — RETRY_BACKOFF, _retry_defer,
  raise arq.Retry on non-final failures, dead-letter on MAX_TRIES; drop
  reliance on ctx["max_tries"] (not present in arq ctx).
Item 2: GBP fetch decrypts access+refresh tokens, refreshes empty, retries once
  on 401; raises on transport/auth failure instead of silently returning {}.
Item 3: dedup webhook_events SELECT before provider fetch in inbound_review.
Item 4: keep secrets out of Redis/AOF — tasks take client_id, decrypt in-worker
  (post_gbp_reply_task, square_sync_task, _load_client, _resolve_gbp_access_token).
Item 5: single reconcile trigger (arq cron only); deterministic _job_id dedup,
  stale-processing recovery, atomic pending claim.
Item 6: only ack duplicate on unique-violation (23505); re-raise other DB errors.
Item 7: propagate outbound failures — strict email/dataforseo variants, menu
  dispatch inspects sync result, holiday-skip treated as success.
Item 8 (C10): real Postgres+Redis staging gate (scripts/phase0_staging_gate.*),
  asyncpg dev dep, DEPLOY.md acceptance checks.
Add test_lifespan.py for main.py worker_role branching; update affected tests.

* fix(phase0): durable outbound setup failures, lease-based stale recovery, real staging gate

Fix 1 (REVIEW BLOCKER): move client loading + credential resolution INSIDE the
coroutine passed to _run_outbound for post_gbp_reply_task and square_sync_task,
so DB outages / missing clients / decrypt / token-refresh failures get arq
retry + dead-letter instead of failing bare. Add retry-then-dead-letter tests for
client-load and decrypt failures on both tasks.

Fix 2 (REVIEW BLOCKER): add processing_started_at lease column to webhook_events
(migration 010, amended in place). Record it on the atomic pending->processing
claim, clear it on retry reset, and base stale-processing recovery on it instead
of created_at so an actively-running event is never reclaimed. Add test that a
row with old created_at but fresh lease stays processing.

Fix 3 (REVIEW BLOCKER): rewrite phase0_staging_gate to import and run the real
task_queue.FUNCTIONS (process_stripe_event, post_gbp_reply_task) and the real
reconcile_pending_webhooks against real Redis/Postgres via a psycopg Supabase
shim, instead of substitute tasks / replicated SQL. Add the single-scheduler
enqueue-exactly-once + restart/resume check using the real create_scheduler.

Fix 4 (TESTING FIND): change FOR INSERT USING to WITH CHECK in 003_reviews.sql
so the INSERT policy is valid Postgres and a clean-DB migration apply works.

Tests: 88 passed.

---------

Co-authored-by: crewcricle <280911048+crewcricle@users.noreply.github.com>
@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
localmate Error Error Jul 22, 2026 6:38pm

Copy link
Copy Markdown
Contributor Author

Closing: replaced by #8 (merged). This PR targets the stale vorflux/phase0-foundations-queue-scheduler base branch.

@crewcricle crewcricle closed this Jul 22, 2026
@crewcricle
crewcricle deleted the vorflux/phase1-billing-tenant-auth branch July 26, 2026 13:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant