# Launch checklist — math app (2026-05-27)

> Tick items as you go. Reorder if priorities shift. See `docs/SESSION_HANDOFF.md` for the underlying narrative + per-feature commit map.

## Status as of 2026-05-27 morning

- **BE branch:** `feat/cascade-stripe-filament-2026-05-23` @ `7027462` — pushed to origin.
- **Prod BE:** ✅ deployed at `7027462` (feature branch checked out directly on prod; PR-to-master deferred). All 4 new routes verified 401 publicly. Queue worker `laravel-queue.service` installed + running.
- **Prod FE (`quiz.allgifted.com`):** live, PWA-installable, 200 OK. Source-on-prod is stale but artifact is current.
- **Prod `.env` Stripe state (verified 2026-05-27 via fingerprint):** ❌ STRIPE_SECRET_KEY is **TEST mode** (`sk_test_...lBb3`), STRIPE_WEBHOOK_SECRET length 38 (origin unverified). Live keys + webhook secret not yet installed — this is the blocker.
- **Prod `.env` Anthropic state:** ✅ ANTHROPIC_API_KEY present (`sk-ant...6AAA`, length 108). `AI_TUTOR_ENABLED` unset (defaults to off).
- **Local `.env`:** swapped to **test** Stripe keys (backup at `.env.backup-pre-test-swap-20260526-160153`).

---

## Critical path (blockers — must finish before any real customer can pay)

- [x] **1. BE deploy** — ✅ DONE 2026-05-27. Took the feature-branch-direct-on-prod path (PR-to-master deferred — open via web UI when convenient; deploy.sh now hardcodes `BRANCH="master"` so the next deploy via that script needs the merge first). Sequence:
  - `git config core.fileMode false` on prod (suppresses ~2873 PNG mode-diff noise from earlier `chmod -R 755 public/images`).
  - Backed up prod `deploy.sh` to `/tmp`, stashed it, `git fetch && git checkout feat/cascade-stripe-filament-2026-05-23`. 33 pre-existing working-tree deletions (phpinfo.php, QA_Reviewer_Manual.pdf, ~30 missing question PNGs) preserved.
  - First migrate run failed: `ai_diagnoses_question_id_foreign` FK incompatible — `questions.id` is legacy `int unsigned`, migration used `unsignedBigInteger`. Fix committed as `7027462` (4 columns across 3 migration files retyped to `unsignedInteger`). Partial `ai_diagnoses` table dropped, migrations re-ran cleanly.
  - All caches rebuilt, queue:restart, chown www-data, apache2 restart.
  - External smoke: `/api/lives/checkout-session`, `/api/subscription/checkout-session`, `/api/subscription/verify-session`, `/api/questions/{id}/solution/vote` all return **401** (route exists, auth required). `/api/stripe/webhook` returns **400** (signature missing). Prod HEAD now `7027462`.
- [ ] **2. Register live-mode Stripe webhook endpoint** in Stripe Dashboard:
  - URL: `https://mathapi.allgifted.com/api/stripe/webhook`
  - Mode: **live** (not test)
  - Subscribe to these 7 events:
    - `payment_intent.succeeded`
    - `checkout.session.completed`
    - `customer.subscription.created`
    - `customer.subscription.updated`
    - `customer.subscription.deleted`
    - `invoice.payment_succeeded`
    - `invoice.payment_failed`
- [ ] **3. Put live webhook signing secret on prod `.env`** as `STRIPE_WEBHOOK_SECRET=whsec_...`. After editing: `php artisan config:cache && systemctl restart apache2`. Verify by clicking "Send test webhook" → `payment_intent.succeeded` in the Stripe Dashboard → expect `200 OK`. If 400/401, the secret doesn't match.
- [ ] **4. Confirm prod `.env` Stripe keys are `sk_live_` / `pk_live_`** (NOT `sk_test_` / `pk_test_`). Use fingerprint-only check per `feedback_never_view_env_directly.md`:
  ```bash
  ssh root@mathapi.allgifted.com 'grep ^STRIPE_SECRET_KEY= /var/www/html/mathapi/.env | sed -E "s/^.{8}.*(.{4})$/prefix=\\1.../" | head -c 50'
  ```
- [x] **5. `php artisan migrate --force` on prod** — ✅ DONE 2026-05-27 as part of item 1. Applied 4 pending migrations after the FK type fix: `2026_05_25_120000_create_ai_diagnoses_table`, `2026_05_25_140000_add_ags_tutor_status_and_solution_source`, `2026_05_25_150000_add_ags_tutor_input_to_questions_qa_status`, `2026_05_26_120000_add_student_vetting_to_solutions`.
- [ ] **6. Real test purchase end-to-end** (live mode rejects test cards — use a small real amount with a card you control):
  - [ ] Buy 5 lives → Stripe Checkout opens → redirects to `/payment-success?session_id=…` → verify-session succeeds → lives credit lands → webhook also fires (Dashboard event log shows `200`).
  - [ ] Subscribe monthly ($20 SGD) → same flow → Premium unlocks → unlimited hearts in UI.
- [ ] **7. 99k overflow regression check** — `Math.tex(textStyle: inherit:false)` fix from 2026-05-26 PM. Open `quiz.allgifted.com`, login, do a track test, hit Submit/Next ~5-10 times with hard refresh. If RenderFlex 99k warning still appears, patch `ags_tutor_solution_modal.dart::_buildSteps` (next call site that inherits AppFontStyles directly).

## Should finish before announcing (degradation, not blockers)

- [x] **8. AI Math Tutor enablement on prod** — ✅ DONE 2026-05-27 per Pam's call:
  - [x] `ANTHROPIC_API_KEY` fingerprint verified on prod — present, `sk-ant...6AAA`, length 108.
  - [x] `AI_TUTOR_ENABLED=true` appended to prod `.env`.
  - [x] `php artisan config:cache && route:cache && systemctl restart apache2` — all done.
  - External smoke: `/api/questions/{id}/diagnose` flipped from 404 (disabled) to 401 (auth required, route live). `/solve` and `/solution` likewise 401. AI tutor will now fire on wrong-answer submissions and cost Anthropic tokens per call.
- [x] **8a. Queue worker running on prod** — ✅ DONE 2026-05-27. There was no worker at all (8-month stale `GenerateQuestionImagesJob` sitting in `jobs` table from 2025-09-27). Installed `/etc/systemd/system/laravel-queue.service` (User=www-data, `artisan queue:work --queue=default --sleep=3 --tries=3 --max-time=3600 --backoff=30`, logs to `/var/log/laravel-queue.log`). `systemctl enable` + `start`, status `active (running)`. Stale job processed cleanly, `jobs=0 failed=0`.
- [x] **9. flutterquiz prod deploy key** — ✅ DONE 2026-05-27. The existing `github_deploy.pub` was already a deploy key on capstoneapi; GitHub disallows the same key on multiple repos, so generated a new `~/.ssh/flutterquiz_deploy` keypair on prod with a `Host github-flutterquiz` alias in `~/.ssh/config`. Pamela pasted the new pubkey on flutterquiz Settings → Deploy keys (read-only). Switched `/var/www/html/quiz` remote to `git@github-flutterquiz:2ppaamm/flutterquiz.git`, fetched, checked out `rename/ags-math-brand`, pulled. Prod source-on-disk HEAD now `18f7150` (matches the latest FE we pushed).
- [x] **10. Sentry DSN sanity check on FE prod build** — ✅ DONE 2026-05-27 as a side-effect of the SSO FE redeploy. New build at `/var/www/html/quiz/build/web/` was built without `--dart-define=SENTRY_DSN=`, so the default DSN baked in. Grep of `main.dart.js` confirms `https://a698d618c88770a692b6419107d08e42@o4511358339645440.ingest.us.sentry.io` is present.
- [ ] **11. Install Stripe CLI on local** — `stripe listen --forward-to localhost:8000/api/stripe/webhook` for proper local webhook testing. Not a launch blocker but the whole `verify-session` detour exists because we lacked this.
- [ ] **12. Restore local `.env` to test keys** — `cp .env.backup-pre-test-swap-20260526-160153 .env && php artisan config:clear` so future `flutter run` against local doesn't hit live Stripe by accident.

## Nice-to-have / first-week monitoring (post-launch)

- [ ] **13. Tail prod logs for ~30 min post-deploy** — `tail -f /var/www/html/mathapi/storage/logs/laravel.log`. Watch for: 500s, `SignatureVerificationException`, missed-credit log lines.
- [ ] **14. Stripe Dashboard event log — first 24h** — each `payment_intent.succeeded` and `checkout.session.completed` should pair with a `200` from our endpoint. Failed deliveries get auto-retried but persistent failures should be investigated.
- [ ] **15. Student vetting threshold review** — currently 1 helpful vote = student-vetted. Revisit after a week; raise to 3 if low-effort first-clicks are noisy.
- [ ] **16. Polish `chmod` in `deploy.sh`** — current `chmod -R 755 public/images` marks PNGs as +x (cosmetic git dirt). Should be:
  ```bash
  find public/images -type d -exec chmod 755 {} \;
  find public/images -type f -exec chmod 644 {} \;
  ```
  Cleans up `git status` noise on prod.
- [ ] **17. Comms** — SIMBA partner notification, announce email/list, social.

---

## Notes on prod-side state discovered 2026-05-27

- Prod's `deploy.sh` had been substantially upgraded in place (`set -euo pipefail`, `--dry-run`, rollback-on-migrate-fail, logging, queue:restart, post-deploy size check). Captured back into the repo at commit `334e742`. Future deploys should use this script verbatim.
- 2873 PNGs under `public/images/` show as "modified" on prod's `git status` — confirmed pure file-mode changes (100644 → 100755) from `chmod -R 755 public/images` in the new deploy.sh. **Mitigation:** set `git config core.fileMode false` on prod's checkout once during the deploy; mode-only changes will then be invisible.
- Prod BE checkout is currently on `master` (HEAD `49b557f`). Two paths to land the feature branch:
  - **Recommended:** open PR `feat/cascade-stripe-filament-2026-05-23` → `master`, merge, then `./deploy.sh` on prod pulls master.
  - **Alternative:** `git checkout feat/cascade-stripe-filament-2026-05-23 && git pull` on prod (prod tracks a feature branch — unusual, but works).

## Added 2026-05-27 PM — SSO bridge to account.allgifted.com

Discovered during pre-launch QA that account.allgifted.com SSO had been scoped for this launch but never built on the math side. The SSO design at account.* was already complete (per-app HS256 JWT, signed with `client_apps.jwt_secret` per slug, validated locally by each downstream app). Vocab had already integrated as the reference template.

**Built and deployed end-to-end:**

- **`database/migrations/2026_05_27_120000_add_external_id_to_users.php`** — `users.external_id` string(64) nullable unique.
- **`app/Http/Controllers/API/SsoController.php`** — receives JWT, validates iss=account.allgifted.com + aud=math + signature, upserts user by external_id → email → phone_number, seeds `lives=5` via forceFill on new users (lives isn't fillable), returns OTP-shape login snapshot via LiveService::getLivesInfo.
- **`POST /api/sso/exchange`** — public route (entry point for Sanctum token mint).
- **`config/services.php sso.jwt_secret`** + prod `.env SSO_JWT_SECRET=` (96-char hex copied from account's `client_apps.jwt_secret` row for slug=math via fingerprint-safe mysql process-substitution — never echoed to chat).
- **FE (`flutter_demo` rename/ags-math-brand commit `18f7150`):** boot-time SSO handler in `main.dart` reads `Uri.base.queryParameters['sso_token']`, calls `AuthService.ssoExchange`, scrubs URL via `web.window.history.replaceState`, falls through to splash. `AuthService.ssoExchange` POSTs to `/api/sso/exchange`, persists token + OTP-shape snapshot (lives, max_lives, unlimited, kudos, maxile, first_name, is_subscriber) to SharedPreferences using the same keys OTPVerifyScreen writes — home screen renders identically whether the user came in via OTP or SSO.
- **Synthetic end-to-end test on prod:** minted JWT via account's `Firebase\JWT\JWT::encode` with the slug=math secret → POSTed to mathapi → user created with lives=5 → Sanctum token returned. Both test users (id 373, 374) cleaned up.
- **Real end-to-end test 2026-05-27 PM (confirmed by Pam):** logged into account.allgifted.com via OTP, clicked Math card on dashboard, landed on quiz.allgifted.com signed in. Two issues hit + fixed along the way: (a) Apache vhost for quiz was missing `RewriteEngine On` so `/sso/callback` returned 404 instead of falling through to Flutter's SPA `/index.html` (would have hit `/payment-success` from Stripe too); (b) `config/cors.php` default `allowed_origins` only listed `allgifted.com` + `mathapi.allgifted.com` — added `quiz.allgifted.com` + `account.allgifted.com` (and prod `.env CORS_ALLOWED_ORIGINS` set to the same list). Both fixes also unblock future Stripe redirect-back flow.

**Ready for your real test:**

1. Log into `account.allgifted.com` via OTP using your email/phone.
2. On the dashboard, click the **Math** card.
3. Browser redirects to `https://quiz.allgifted.com/sso/callback?sso_token=...`
4. Flutter web exchanges the token, you land at home screen logged in. URL scrubs to `/`.

**Items 2-4 (Stripe live mode) are now the only remaining launch blocker.** Same as before below.

---

## Next concrete action

Items 1, 5, 8a, 10 complete. **SSO bridge also complete and ready for your test.** Anthropic key verified (8 partially). Next blocker is **Stripe live mode (items 2, 3, 4)** — needs you in the Stripe Dashboard:

1. **In Stripe Dashboard → Developers → Webhooks (live mode):** click "Add endpoint", URL `https://mathapi.allgifted.com/api/stripe/webhook`, select the 7 events listed under item 2. Copy the signing secret it generates (`whsec_...`).
2. **In Stripe Dashboard → Developers → API keys (live mode):** copy `sk_live_...` and `pk_live_...`.
3. **Hand the three values to me** (or paste into a secrets manager and tell me where) — I'll update prod `.env` via fingerprint-safe rewrite, run `config:cache`, restart apache.
4. **Click "Send test webhook" → `payment_intent.succeeded`** in the Dashboard → should land 200 OK. I'll watch the prod log live.
5. **Do one real test purchase** (small amount, real card you control): buy 5 lives + subscribe monthly — confirm lives credit + Premium unlock.
6. **Optional next:** AI tutor enable (set `AI_TUTOR_ENABLED=true`), flutterquiz deploy key (paste pubkey under item 9 on GitHub).
