zlh-grind/SCRATCH/billing-stripe-handover-apr11-2026.md

197 lines
4.1 KiB
Markdown

# Billing + Stripe Handover (Apr 11, 2026)
## Context
This document captures the current billing integration state between:
- zpack-api
- zpack-portal
- Stripe (sandbox/test mode)
This was developed during a developer-mode session where chat history is not persisted, so this doc acts as the continuity handoff.
---
## Current Status
### Stripe
- Stripe sandbox/test mode configured
- Products + recurring prices created
- Checkout sessions successfully redirect to Stripe
- Test checkout completed using Stripe test card
- Stripe dashboard confirms:
- checkout.session.completed
- customer created
- subscription created
- invoice/payment succeeded (test)
### API (zpack-api)
Working:
- `ensureStripeCustomer()` persists `stripeCustomerId`
- `/api/billing/checkout` successfully creates Stripe Checkout session
- Billing foundation fields exist in Prisma:
- `stripeCustomerId`
- `subscriptionStatus`
- `plan`
Not yet complete:
- Webhook delivery not working (API not publicly reachable)
- As a result:
- `subscriptionStatus` is NOT being updated
- `plan` is NOT being updated
Root cause:
- Stripe cannot reach `/api/billing/webhook`
- This is NOT a checkout issue
- This is a webhook reachability issue
### Portal (zpack-portal)
Working:
- Billing page renders correctly for unsubscribed users
- Plan selection UI present
- Checkout button calls API
- Redirect to Stripe works
Partially complete:
- Needs final alignment for:
- `trialing` state
- `active` state
- billing-exempt/admin display
---
## Key Architectural Decisions
### Billing Model
- Users can register without paying
- Subscription is optional until resource enforcement is implemented
- Future plan: soft gate (likely 1 free server, then require subscription)
### Stripe Integration Strategy
- Use Stripe Checkout (NOT custom payment form)
- Use Stripe Billing Portal for management
- Use webhooks to update DB state
- DB remains source of truth for access decisions
### Metadata Usage
Stripe objects should include:
- `metadata.userId`
- optionally `metadata.plan`
This is used for safer webhook reconciliation.
---
## Current Blocker
### Webhook Delivery
Problem:
- API is not publicly reachable
- Stripe cannot POST webhook events
Observed behavior:
- `stripeCustomerId` set (checkout path)
- `subscriptionStatus` remains null
- `plan` remains null
---
## Immediate Next Steps
### Option A (Dev Testing - Recommended)
Use Stripe CLI forwarding:
```bash
stripe listen --forward-to http://10.60.0.18:4000/api/billing/webhook
```
Then:
- copy CLI-provided `whsec_...`
- set as `STRIPE_WEBHOOK_SECRET`
- restart API
- re-run test checkout
Expected result:
- webhook hits API
- DB updates correctly
---
### Option B (Staging/Prod Path)
Expose only webhook endpoint via public domain:
Example:
```
https://billing.zerolaghub.com/api/billing/webhook
```
Requirements:
- real reverse proxy or tunnel (NOT DNS redirect)
- HTTPS
- Stripe signature verification enabled
---
## Important Notes
### Namecheap Redirect
- simple DNS/URL redirect is NOT sufficient
- Stripe requires direct POST endpoint
- must use proxy/tunnel instead
### Stripe CLI
- does NOT need to run on API VM
- but easiest if it does
- must be able to reach internal API IP
### Test Cards
Use Stripe test card:
```
4242 4242 4242 4242
```
---
## Known Good
- Checkout flow: WORKING
- Customer creation: WORKING
- Portal integration: WORKING enough for testing
## Known Broken
- Webhook delivery: NOT working
- Subscription state persistence: NOT working
---
## Next Session Starting Point
1. Get Stripe CLI installed and running
2. Verify webhook hits API
3. Confirm DB updates:
- `subscriptionStatus`
- `plan`
4. Refresh portal billing page
5. Verify UI reflects new state
---
## Future Work (Post-Webhook)
- Implement trial handling (`trialing` state)
- Add quota enforcement (soft gate)
- Improve billing UI states
- Add billing portal button behavior
- Consider exposing webhook endpoint cleanly for production
---
## Summary
The system is effectively complete except for webhook reachability.
Once webhooks are flowing, billing becomes fully functional end-to-end.