# Implementation State — Chowman Durga Puja Quiz Backend

**Snapshot date:** 25 Sep 2026
**Plan doc:** `docs/plans/chowman-durga-puja-quiz-backend.md`
**Branch:** `main` (uncommitted working changes)
**Last upstream commit:** `703ef70 Configure Boost post-update script`

Use this file to resume work later. It records exactly what has been built,
where each task stands, and how to pick up the next task cleanly.

---

## Progress Overview

| #   | Task                                                                        | Status                                                                              |
| --- | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| 1   | MySQL switch + Sanctum + maatwebsite/excel install                          | ✅ done                                                                             |
| 2   | Admin roles on users + HasApiTokens + seed admin user                       | ✅ done                                                                             |
| 3   | Guest model + Sanctum guest provider                                        | ✅ done                                                                             |
| 4   | OTP service + SmsGateway abstraction (Log + Bluewaves stub)                 | ✅ done                                                                             |
| 5   | Question and Offer domain (models, migrations, factories, seeders)          | ✅ done                                                                             |
| 6   | Campaign settings (settings table, repository, middleware, public endpoint) | ✅ code + Task 6 tests green; final full-suite + Pint + PHPStan pass NOT yet re-run |
| 7   | Guest registration + OTP send/verify endpoints                              | ⏳ next                                                                             |
| 8   | Quiz session start + question serving                                       | ⏳                                                                                  |
| 9   | Answer submission + timeout                                                 | ⏳                                                                                  |
| 10  | Finalize session + unlock offer + SMS                                       | ⏳                                                                                  |
| 11  | Analytics events pipeline                                                   | ⏳                                                                                  |
| 12  | Admin auth (login/logout/me)                                                | ⏳                                                                                  |
| 13  | Admin question management + XLSX import                                     | ⏳                                                                                  |
| 14  | Admin offer + settings management                                           | ⏳                                                                                  |
| 15  | Admin participants list + dashboard + export                                | ⏳                                                                                  |
| 16  | Admin user management                                                       | ⏳                                                                                  |
| 17  | Integration test — full happy path + hostile inputs                         | ⏳                                                                                  |

**Latest confirmed test count:** 95 tests / 255 assertions green after Task 5.
**Task 6 tests:** 16 additional tests written and locally green
(`SettingsRepositoryTest`, `CampaignStatusResolverTest`,
`EnsureCampaignActiveMiddlewareTest`, `CampaignStatusEndpointTest`).
Full-suite rerun + Pint + PHPStan across all Task 6 files was interrupted
before completion — first thing to run on resume.

---

## What Exists on Disk (uncommitted)

**Modified starter files:**

- `.env.example` — MySQL defaults
- `composer.json` / `composer.lock` — Sanctum ^4.3, maatwebsite/excel ^4.0
- `config/auth.php` — added `guests` provider
- `config/sanctum.php` — cast env() to string for PHPStan
- `bootstrap/app.php` — registered Sanctum ability aliases + `campaign.active` alias
- `app/Providers/AppServiceProvider.php` — SmsGateway singleton binding + rate limiters
- `app/Models/User.php` — HasApiTokens + role enum cast + isAdmin/isViewer
- `database/factories/UserFactory.php` — admin() / viewer() states, default viewer
- `database/seeders/DatabaseSeeder.php` — admin upsert + SettingsSeeder + OfferSeeder + QuestionSeeder (non-prod only)

**New enums (`app/Enums/`):**

- `UserRole` (Admin, Viewer)
- `CorrectOption` (A, B, C, D)
- `CampaignStatus` (Active, NotStarted, Ended)

**New models (`app/Models/`):**

- `Guest` — HasApiTokens, verified_at cast, isVerified(), routeNotificationForSms()
- `Question` — active() scope, optionText(CorrectOption)
- `Offer` — active() + forSlot(int) scopes
- `Setting` — string PK on `key`, no casts (repository handles JSON)

**New support layer (`app/Support/`):**

- `Sms/SmsGateway` (interface) + `LogSmsDriver` + `BluewavesSmsDriver` (stub) + `FakeSmsGateway` (test helper)
- `Otp/OtpService` + `OtpSendResult` + `OtpVerifyResult` enums
- `Offers/OfferMapper::forScore(int)` (0→1, 1→1, else N→N)
- `Settings/SettingsKey` (constants + defaults) + `SettingsRepository` (cached) + `CampaignStatusResolver`

**New HTTP layer (`app/Http/`):**

- `Middleware/EnsureCampaignActive` — 200 `{status: campaign_closed, reason}` outside window
- `Controllers/Api/V1/Quiz/CampaignStatusController` — public discovery endpoint

**Migrations (`database/migrations/`):**

- `create_personal_access_tokens_table` (Sanctum)
- `add_role_to_users_table` (enum admin/viewer, default viewer)
- `create_guests_table` (mobile unique 10 chars, consent bool, verified_at)
- `create_questions_table` (text + option_a..d + correct_option enum + active + index)
- `create_offers_table` (score_slot unique tinyint 1–10)
- `create_settings_table` (`key` string PK, `value` JSON)

**Seeders (`database/seeders/`):**

- `SettingsSeeder` — writes defaults from `SettingsKey::defaults()` (idempotent)
- `OfferSeeder` — 10 rows updateOrCreate keyed by score_slot
- `QuestionSeeder` — 30 rows, skips if any exist (dev only)

**Config (`config/`):**

- `sms.php` — driver selection + drivers map + bluewaves creds placeholder
- `chowman.php` — seed credentials + otp block (length/ttl/cooldown/lockout/max/hourly caps)

**Routes (`routes/api.php`):**

- Existing `/api/user` (auth:sanctum)
- New `GET /api/v1/quiz/campaign` → `CampaignStatusController`

**Tests (`tests/`):**

- 95 pre-Task-6 tests green
- 16 new Task 6 tests written and locally passing
- Unit: `OfferMapperTest`
- All others: `Feature/*Test.php`

---

## Environment / Setup

- **PHP:** 8.4.12 (Laragon), zip extension enabled
- **Composer:** 2.9.3
- **DB:** MySQL 8.4.3 on 127.0.0.1:3306, database `chowman` created and migrated
- **Cache/Session driver:** database
- **Test cache:** `array` (via `phpunit.xml`)
- **Test DB:** SQLite `:memory:`
- **SMS driver:** `log` (default), stub `bluewaves` throws NotImplemented
- **Seeded admin:** `admin@chowman.local` / `password` (override via SEED_ADMIN_* env)
- **Sanctum middleware aliases in `bootstrap/app.php`:** `abilities`, `ability`, `campaign.active`

---

## Resume Playbook

### 1. Verify the checkpoint (2 min)

```powershell
cd c:\laragon\www\Herd\chowman
php artisan test --compact
vendor\bin\pint --dirty --format agent
vendor\bin\phpstan analyse --no-progress
```

Expected: 111 tests green (95 pre-Task-6 + 16 new). Pint + PHPStan clean.
If anything breaks, the Task 6 handover files below cover what to look at.

### 2. Seed defaults into MySQL

```powershell
php artisan migrate --no-interaction
php artisan db:seed --no-interaction
```

Should produce: 1 admin user, 7 settings rows, 10 offer rows, 30 questions.

### 3. Smoke the endpoint

```powershell
curl http://chowman.test/api/v1/quiz/campaign
# or
php artisan serve
```

Expected JSON with `status`, `is_active`, `questions_per_quiz`, `timer_seconds`, `app_smartlink_url`.

### 4. Start Task 7

See `docs/plans/chowman-durga-puja-quiz-backend.md` §Task 7.

Task 7 depends on Tasks 3, 4, 6 — all done. Key inputs:

- `Guest` model with `HasApiTokens` and `verified_at`
- `OtpService::send()` / `verify()`
- `SettingsRepository` (for `attempts_per_number`)
- `EnsureCampaignActive` middleware alias `campaign.active`
- Rate limiters registered: `otp_send`, `otp_verify`, `admin_login`
- `FakeSmsGateway` for asserting sends in tests

**Task 7 checklist:**

- [ ] `App\Actions\Quiz\ResolveGuestState` — returns `new` | `in_progress` | `completed` | `unlocked`
      (relies on `QuizSession` which does NOT exist yet — decide whether to add a
      temporary placeholder or defer this until Task 8)
- [ ] `App\Http\Requests\Api\V1\Quiz\RegisterGuestRequest` — name (2–50, letters/spaces), mobile `/^[6-9]\d{9}$/`, consent accepted
- [ ] `App\Http\Requests\Api\V1\Quiz\VerifyOtpRequest`
- [ ] `App\Http\Requests\Api\V1\Quiz\ResendOtpRequest`
- [ ] Controllers under `app/Http/Controllers/Api/V1/Quiz/`:
    - `RegisterController` — POST /register
    - `OtpController@verify`, `@resend`
- [ ] Route group under `/api/v1/quiz` with `campaign.active` middleware
- [ ] `throttle:otp_send` on register + resend, `throttle:otp_verify` on verify
- [ ] 30-second resend cooldown check (already in OtpService via cache)
- [ ] Feature tests using `FakeSmsGateway` bound in the container
- [ ] Analytics events: none yet — deferred to Task 11 (record via action later)

**Important design note for Task 7:**
`ResolveGuestState` needs to check the existence of a `QuizSession`. Since
that model lands in Task 8, the pragmatic approach is:

1. Return only `new` in verify for now.
2. Add a `TODO: extend after Task 8` marker.
3. Update this action in Task 8 or Task 9 when the session model exists.

Alternatively, land the `quiz_sessions` migration + skeletal model early
(in Task 7) so `ResolveGuestState` can be complete from day one. This is
recommended — it front-loads a small piece of Task 8 but avoids reopening
Task 7.

---

## Known Gotchas / Decisions Recorded

- **Unit vs Feature tests:** any test using a factory needs to run as a
  Feature test because factories call `Hash::make` / `now()` which need
  the Laravel bootstrap. `OfferMapperTest` is the only Unit test so far.
- **Sanctum ability middleware aliases** are NOT auto-registered by
  `install:api` on Laravel 11+ streamlined bootstrap — explicitly aliased
  in `bootstrap/app.php`.
- **PHPStan generic scopes:** model scope methods need
  `@param Builder<ModelName> $query` docblocks or PHPStan flags them.
- **Faker types:** `fake()->words(3, true)` is typed `array|string` in
  Faker's stubs — use `fake()->sentence()` when concatenating into a
  string.
- **JSON storage in `settings`:** the model has no cast; the repository
  handles `json_encode`/`json_decode` explicitly so scalars round-trip.
- **Cache invalidation in tests:** `Cache::store('array')` is the default
  test cache. `Setting::query()->update()` bypasses the repository, so
  cache warming has to be explicit before testing invalidation.
- **Campaign closed = 200, not 4xx:** the middleware returns a 200 body
  because "closed" is a UX state, not an error.

---

## Commit Recommendation

Before resuming, consider committing the current state as a stable
checkpoint. Suggested commit boundaries (one PR per task is overkill —
group them):

```
feat(auth): add Guest + admin roles + Sanctum ability separation

- Sanctum 4.3 + maatwebsite/excel 4.0
- User: role enum (admin/viewer) + HasApiTokens + helpers
- Guest: HasApiTokens, verified_at, mobile unique 10-char
- Sanctum ability middleware aliases wired in bootstrap
- Admin user seeder + role factory states

feat(otp): SMS gateway abstraction + OTP service + rate limiters

- SmsGateway interface + LogSmsDriver + BluewavesSmsDriver stub
- FakeSmsGateway test helper
- OtpService with HMAC-hashed cache-backed state
- 5-attempt lockout, 30s cooldown, 5-min TTL
- Rate limiters: otp_send, otp_verify, admin_login

feat(content): Question + Offer models with seeders and mapper

- CorrectOption enum (A/B/C/D)
- Question with active scope, Offer with score_slot 1-10 unique
- OfferMapper::forScore (0→1, else N)
- Seeders: 30 questions + 10 offer slots (idempotent)

feat(campaign): settings repository + middleware + status endpoint

- SettingsRepository (cached key/value, JSON round-trip)
- CampaignStatusResolver + CampaignStatus enum
- EnsureCampaignActive middleware alias `campaign.active`
- GET /api/v1/quiz/campaign public discovery endpoint
- SettingsSeeder with campaign.* defaults
```

Or squash all six tasks into one commit for the checkpoint — up to you.
