Files
waggle-os/docs/OPS/stripe-smoke.md
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

186 lines
7.7 KiB
Markdown

# Stripe Smoke Test — Local End-to-End Verification
**Purpose:** Verify Waggle's Stripe integration works end-to-end without a production Stripe account. Uses the `stripe-cli` (https://github.com/stripe/stripe-cli) + Marko's Egzakta sandbox to simulate real webhook events against our local server.
**Scope:** Closes H-33 in `docs/plans/BACKLOG-MASTER-2026-04-18.md`.
**Status (2026-04-18):****EXECUTED — 7/7 green.** See [Executed results](#executed-results-2026-04-18) at the bottom. The canonical automated replay is `packages/server/tests/stripe/smoke-e2e.test.ts` — run the one-liner in that section to re-verify at any time.
---
## Prerequisites
- Stripe CLI installed: `stripe --version` should report ≥ 1.40.0. Already present at `C:/Users/MarkoMarkovic/bin/stripe`.
- Stripe CLI logged in: `stripe config --list` should show `account_id` (Egzakta sandbox is fine).
- `STRIPE_SECRET_KEY` and `STRIPE_WEBHOOK_SECRET` set in the local environment or `.env`. The webhook secret is printed by `stripe listen` — copy it per the workflow below.
- Waggle sidecar running locally on `http://127.0.0.1:3333`.
## Test-mode price IDs
For local smoke you need two test-mode prices. Create once per sandbox:
```sh
# Pro tier — $19/mo
stripe prices create \
--unit-amount=1900 \
--currency=usd \
--recurring="interval=month" \
--product-data="name=Waggle Pro"
# Teams tier — $49/mo/seat
stripe prices create \
--unit-amount=4900 \
--currency=usd \
--recurring="interval=month" \
--product-data="name=Waggle Teams"
```
Set the returned `price_*` IDs as `STRIPE_PRICE_PRO` and `STRIPE_PRICE_TEAMS` env vars before starting the sidecar. These match the canonical tier names in `packages/shared/src/tiers.ts`.
## Smoke flow
### 1 · Start the webhook forwarder
In one terminal, forward live test events to your local server:
```sh
stripe listen --forward-to localhost:3333/api/stripe/webhook
```
Stripe CLI prints a webhook signing secret like `whsec_abc...` — copy it to `STRIPE_WEBHOOK_SECRET` in the sidecar's environment and restart the sidecar. The signing secret is stable across reconnects of the same CLI session.
### 2 · Trigger a checkout-completed event
In a second terminal:
```sh
stripe trigger checkout.session.completed
```
Expected: sidecar logs `event: checkout_completed`, and `config.json` at the data directory gains (or updates) `tier: 'PRO'` (default for the trigger template) plus `stripe_customer_id`. Verify with:
```sh
cat "$WAGGLE_DATA_DIR/config.json"
```
### 3 · Trigger a subscription-updated event
```sh
stripe trigger customer.subscription.updated
```
Expected: sidecar logs `event: subscription_updated`. If the triggered price matches `STRIPE_PRICE_PRO` or `STRIPE_PRICE_TEAMS`, `config.json.tier` updates accordingly. Otherwise the tier field stays unchanged (we only update on known price IDs).
### 4 · Trigger a subscription-cancelled event
```sh
stripe trigger customer.subscription.deleted
```
Expected: sidecar logs `event: subscription_cancelled`; `config.json.tier` becomes `FREE`.
### 5 · Idempotency check
Re-fire the same event:
```sh
stripe trigger checkout.session.completed
```
Expected: sidecar logs `event: webhook_duplicate_skipped` with the event ID. No config mutation on the second run (the `.stripe-processed-events.json` dedup file ensures this).
### 6 · Checkout session creation
From the web app or via curl:
```sh
curl -X POST http://127.0.0.1:3333/api/stripe/create-checkout-session \
-H 'Content-Type: application/json' \
-d '{"tier": "PRO"}'
```
Expected: `{ "url": "https://checkout.stripe.com/..." }`. Opening that URL should render Stripe's hosted checkout for the Pro test price.
### 7 · Billing portal
```sh
curl -X POST http://127.0.0.1:3333/api/stripe/create-portal-session
```
Expected: `{ "url": "https://billing.stripe.com/..." }`. Requires the user to have a `stripe_customer_id` in their config — set by flow 2 above.
## Checklist
- [ ] `stripe listen` active and forwarding to `/api/stripe/webhook`
- [ ] `checkout.session.completed` → tier updated + customer ID stored
- [ ] `customer.subscription.updated` → tier reflects new price
- [ ] `customer.subscription.deleted` → tier reverts to `FREE`
- [ ] Duplicate event ID is skipped, config unchanged
- [ ] `/api/stripe/create-checkout-session` returns a valid Stripe URL
- [ ] `/api/stripe/create-portal-session` returns a valid portal URL
- [ ] Existing Vitest suite green: `npx vitest run packages/server/tests/stripe`
## Next steps after smoke passes
- Promote to production: Marko creates production products + prices in Stripe dashboard ([M]-01).
- Swap sandbox → production price IDs in the production environment.
- Run the same smoke flow against production once.
- Wire the upgrade CTA in SettingsApp.tsx (already integrated with `useBilling.startCheckout`).
## Known gaps
- Production Stripe products don't exist yet ([M]-01 blocker). The sandbox smoke covers signature validation, tier mapping, idempotency, and routing. Production price IDs must be plugged in for real customer checkouts.
- Windows code-signing cert ([M]-08) needed before distribution; unrelated to Stripe smoke.
## Executed results (2026-04-18)
Egzakta sandbox `acct_1SzHlbC0mmjh4oEM` (test mode, CLI key expires 2026-05-11).
**Test-mode price IDs** — created via `stripe prices create` during the H-33 execution:
| Tier | Amount | Price ID |
|---|---|---|
| PRO | $19 USD/mo | `price_1TNZfkC0mmjh4oEMGAZ2PDbc` |
| TEAMS | $49 USD/mo | `price_1TNZfpC0mmjh4oEMH10c02YB` |
These are sandbox-only. Production price IDs will be separate ([M]-01).
**Execution transcript** (from `packages/server/tests/stripe/smoke-e2e.test.ts`):
```
=== H-33 Stripe smoke checklist ===
[x] 1. checkout.session.completed — tier=PRO + customer=cus_smoke_1
[x] 2. customer.subscription.updated — PRICE_TEAMS → tier=TEAMS
[x] 3. customer.subscription.deleted — tier reverted to FREE
[x] 4. duplicate event dedup — resending evt_smoke_checkout_1 left tier on TEAMS (duplicate skipped)
[x] 5. signature validation — bogus signature → 400 INVALID_SIGNATURE
[x] 6. create-checkout-session — URL = https://checkout.stripe.com/c/pay/cs_test_b1...
[x] 7. create-portal-session — customer=cus_<live> · URL = https://billing.stripe.com/p/session/test_...
```
**Coverage:** Signature validation, tier mapping (checkout-metadata + price-id paths), FREE revert on cancellation, dedup via `.stripe-processed-events.json`, 403 tier gate before checkout/portal, real Stripe API reachability (both checkout.sessions.create and billingPortal.sessions.create round-tripped cleanly).
### Re-running the smoke
```sh
# Required env — never commit these values.
STRIPE_KEY=$(stripe config --list | awk -F"'" '/test_mode_api_key/{print $2}')
cd D:/Projects/waggle-os
STRIPE_SECRET_KEY="$STRIPE_KEY" \
STRIPE_PRICE_PRO="price_1TNZfkC0mmjh4oEMGAZ2PDbc" \
STRIPE_PRICE_TEAMS="price_1TNZfpC0mmjh4oEMH10c02YB" \
WAGGLE_STRIPE_SMOKE=1 \
npx vitest run packages/server/tests/stripe/smoke-e2e.test.ts
```
Without `WAGGLE_STRIPE_SMOKE=1` + the env vars the suite self-skips so CI stays green on developer machines that don't have a Stripe sandbox wired up.
### Production cutover checklist ([M]-01)
1. Create production products + prices in the Stripe dashboard. Use the canonical tier names — `Waggle Pro` ($19) and `Waggle Teams` ($49).
2. Replace `STRIPE_PRICE_PRO` / `STRIPE_PRICE_TEAMS` in the production env with the new `price_...` IDs.
3. Configure a production webhook endpoint at `https://<prod-host>/api/stripe/webhook` with signing secret piped into `STRIPE_WEBHOOK_SECRET`.
4. Re-run this smoke against production **exactly once**, then archive the price IDs in `docs/OPS/stripe-production.md` (new).
5. Wire the upgrade CTA in `SettingsApp.tsx` — already integrated via `useBilling.startCheckout`.