# EV Booking System — Implementation Tickets

Companion to `domain.md` (business rules) and the approved architecture plan. Work tickets top-to-bottom within a phase; phases are mostly sequential (Booking depends on Catalog+Routing, Payment depends on Booking).

**Note — folder structure supersedes architecture-plan §3.** Per Filament's own modular pattern, each bounded context is a **local Composer package** under `app-modules/{module}/`, not a folder under `app/`. Each module has its own `composer.json`, PSR-4 root (`Modules\{Module}\`), service provider, migrations/routes/tests, and — if it has admin UI — its own `{Module}Plugin` implementing Filament's `Plugin` contract, which the central admin Panel Provider registers.

```
app-modules/
  catalog/
    composer.json                          # requires filament/filament, registers provider
    src/
      Providers/CatalogServiceProvider.php  # repository bindings, event/listener registration
      Models/            (EvCompany, Destination, DepartureTimeSlot — PickupLocation/DropoffLocation deferred, see "Deferred Tickets")
      Http/Controllers/   Http/Requests/   Http/Resources/
      Filament/
        Resources/        Pages/        Widgets/
      CatalogPlugin.php                     # implements Filament\Contracts\Plugin, discovers the above
    routes/
    resources/
    database/{migrations,factories,seeders}
    tests/
  routing/      (EvRoute, RoutePricing, PricingService, RoutingPlugin)
  booking/      (Booking, BookingService, booking Actions/Events, BookingPlugin)
  payment/      (Payment, Refund, gateway Strategy Pattern, Payment Actions/Events, PaymentPlugin)
  identity/     (roles/permissions wiring, Sanctum token issuance, agent middleware — no Filament plugin, or a thin Access-management one)
  shared/       (base DTO/Action contracts, shared enums, shared exceptions — only if genuinely cross-module; no Filament plugin)
```

Root `composer.json` declares each module as a path repository (`{"type": "path", "url": "app-modules/*"}`) and `require`s each module package (`"my-app/catalog": "@dev"`, etc.), so modules autoload and their service providers register through normal Composer/Laravel package discovery — not manual `bootstrap/providers.php` edits per module.

The central Filament `AdminPanelProvider` (`app/Providers/Filament/AdminPanelProvider.php`) registers every module's plugin: `->plugins([CatalogPlugin::make(), RoutingPlugin::make(), BookingPlugin::make(), PaymentPlugin::make()])` — this is the **only** place that wires modules into the panel; modules never reference the panel provider or each other's Filament classes directly.

Cross-module domain code (e.g. Booking module calling Payment module's `RefundBookingAction`) goes through each module's public contracts/service class, not deep imports into another module's internals — keep the same layering discipline (Actions → Services → Repositories → Models) inside each module's `src/`.

---

## Phase 0 — Project Setup & Module Scaffolding

### T0.1 — Scaffold module packages
- **Module**: Shared
- **Depends on**: —
- **Description**: Run `php artisan make:module` (or hand-scaffold if the command isn't available yet) for `catalog`, `routing`, `booking`, `payment`, `identity`, `shared`. Each gets `composer.json` (PSR-4 `Modules\{Module}\` → `src/`, `require: filament/filament`, `extra.laravel.providers` pointing at its `{Module}ServiceProvider`), the `src/Providers/{Module}ServiceProvider.php` stub, and empty `routes/`, `resources/`, `database/{migrations,factories,seeders}`, `tests/` directories. No business code yet — just the shape the rest of the tickets build into.
- **Domain reference**: — (structural only)

### T0.2 — Wire module packages into root composer.json
- **Module**: Shared
- **Depends on**: T0.1
- **Description**: Add `{"type": "path", "url": "app-modules/*"}` to root `composer.json` `repositories`, then `composer require my-app/catalog:@dev my-app/routing:@dev my-app/booking:@dev my-app/payment:@dev my-app/identity:@dev my-app/shared:@dev` (adjust vendor/package names to match each module's `composer.json` `name`). Confirm `composer dump-autoload` picks up each `{Module}ServiceProvider` via package discovery — no manual `bootstrap/providers.php` edits needed per module.
- **Domain reference**: — (structural only)

### T0.3 — Install & configure Sanctum, Spatie Permission, Filament
- **Module**: Shared/Identity
- **Depends on**: T0.2
- **Description**: `composer require` Sanctum (bundled w/ Laravel 12 but confirm published config), `spatie/laravel-permission`, Filament panel package (root-level, not per-module). Publish configs/migrations. Create the central `app/Providers/Filament/AdminPanelProvider.php` (no `->plugins([...])` entries yet — modules register themselves in later tickets).
- **Domain reference**: domain.md §8 (access boundaries)

### T0.4 — Base infra config
- **Module**: Shared
- **Depends on**: —
- **Description**: Wire `.env`/`.env.example` and `config/services.php`/`config/booking.php` skeletons per architecture plan §27 (Redis cache/queue/session, `BOOKING_*` toggles, `KBZ_*` placeholders — values empty, just the keys). Switch `CACHE_STORE`, `QUEUE_CONNECTION`, `SESSION_DRIVER` to `redis` in `.env.example`.
- **Domain reference**: domain.md §2 (config toggles), §6 (KBZ env vars)

---

## Phase 1 — Identity & Access

### T1.1 — Roles & permissions seed
- **Module**: Identity
- **Depends on**: T0.3
- **Description**: Migration/seeder for roles (`super_admin`, `admin`, `support`) and permissions (`manage_catalog`, `manage_routes`, `manage_pricing`, `view_bookings`, `manage_bookings`, `view_payments`, `process_refunds`, `view_audit_log`). `RolePermissionSeeder` wired into `DatabaseSeeder`.
- **Domain reference**: domain.md §4 (single tenant, no teams)

### T1.2 — Sanctum token issuance + ability scopes
- **Module**: Identity
- **Depends on**: T1.1
- **Description**: Token issuance endpoint(s) for customer channels (mini app/mobile) and a separate provisioning path for the FastAPI agent token, restricted to abilities `route:read`, `booking:create`, `booking:read`. `EnsureFastApiAgent` middleware (checks token ability, not role).
- **Domain reference**: domain.md §8 (external actors)

### T1.3 — Filament admin auth + navigation shell
- **Module**: Identity/Filament (central, `app/Providers/Filament/AdminPanelProvider.php` — not a module package)
- **Depends on**: T0.3, T1.1
- **Description**: Filament panel login restricted to users with an admin-tier role; navigation group order defined (Catalog, Routing, Operations, Access) so later module plugins slot their resources into the right group. `->plugins([])` left empty here — each module ticket (T2.0, T3.5, T4.7, T5.13) appends its own `{Module}Plugin::make()` to this call as it's built.
- **Domain reference**: domain.md §4, §8

### T1.4 — Policy skeletons
- **Module**: Identity
- **Depends on**: T1.1
- **Description**: `BookingPolicy`, `RoutePolicy` classes registered, methods stubbed (`view`, `create`, `cancel`, `refund` / `viewAny`, `create`, `update`, `delete`) returning role-based checks per architecture plan §12. Booking-specific ownership logic filled in during Phase 4 tickets — this ticket just establishes the class + registration + basic role gates.
- **Domain reference**: domain.md §8

---

## Phase 2 — Catalog Module

### T2.0 — CatalogPlugin scaffold + registration
- **Module**: Catalog
- **Depends on**: T0.2, T1.3
- **Description**: `app-modules/catalog/src/CatalogPlugin.php` implementing `Filament\Contracts\Plugin` — `getId()`, `register(Panel $panel)` calling `discoverResources`/`discoverPages`/`discoverWidgets` against `src/Filament/{Resources,Pages,Widgets}`, `boot(Panel $panel)` empty for now. Add `CatalogPlugin::make()` to `AdminPanelProvider`'s `->plugins([...])` array. No resources exist yet — this just makes the wiring live so T2.1–T2.4 only need to drop files in the right folder.
- **Domain reference**: — (structural only)

### T2.1 — EvCompany
- **Module**: Catalog
- **Depends on**: T0.2
- **Description**: Migration, model, factory for `ev_companies` (`name`, `slug`, `is_active`). `EvCompanyResource` in `app-modules/catalog/src/Filament/Resources/` (simple CRUD form) — auto-discovered by `CatalogPlugin` (T2.0), no manual registration needed.
- **Domain reference**: domain.md §1, §4

### T2.2 — Destination
- **Module**: Catalog
- **Depends on**: T0.2
- **Description**: Migration, model, factory for `destinations` (`name`, `region`, `is_active`). `DestinationResource` in `app-modules/catalog/src/Filament/Resources/`.
- **Domain reference**: domain.md §1

### T2.3 — Deferred (see "Deferred Tickets" at bottom)

Pickup & Dropoff Locations was originally scoped here. The real business model is door-to-door (customer supplies a free-text pickup/dropoff address on `Booking`, see domain.md §2a), so a fixed checkpoint catalog isn't needed for v1. The original ticket is kept at the bottom of this file for when checkpoint meet-ups get built.

### T2.4 — Departure Time Slot
- **Module**: Catalog
- **Depends on**: T0.2
- **Description**: Migration, model, factory for `departure_time_slots` (`label`, `time`, `is_active`). `DepartureTimeSlotResource` in `app-modules/catalog/src/Filament/Resources/`. Emphasize in code/tests that this is a **shared catalog** entity, not owned by one route (attached via pivot in Phase 3).
- **Domain reference**: domain.md §1

### T2.5 — Catalog read API
- **Module**: Catalog
- **Depends on**: T2.1–T2.4
- **Description**: `GET /api/v1/companies`, `GET /api/v1/destinations` with `EvCompanyResource`/`DestinationResource` API resources, `auth:sanctum` + throttle. Feature tests for both.
- **Domain reference**: domain.md §8 (agent needs `route:read` which implies these being world-readable to authenticated clients)

---

## Phase 3 — Routing Module

### T3.1 — EvRoute
- **Module**: Routing
- **Depends on**: T2.1, T2.2
- **Description**: Migration, model, factory for `ev_routes` (`ev_company_id`, `from_destination_id`, `to_destination_id`, `is_round_trip`, `is_active`). Eloquent relations: `belongsTo` company/fromDestination/toDestination. No pickup/dropoff FK on the route — those are captured per-booking as free-text addresses (domain.md §2a).
- **Domain reference**: domain.md §1, §2a

### T3.2 — Route ↔ Time Slot pivot
- **Module**: Routing
- **Depends on**: T3.1, T2.4
- **Description**: `ev_route_time_slots` pivot migration + `belongsToMany` relation on `EvRoute`/`DepartureTimeSlot`, with `is_active` on the pivot row (a route can later disable a slot without deleting the catalog entry).
- **Domain reference**: domain.md §1

### T3.3 — RoutePricing
- **Module**: Routing
- **Depends on**: T3.1
- **Description**: Migration, model, factory for `route_pricing` (`ev_route_id`, `vehicle_option` enum, `price`). `VehicleOption` enum (`front_seat`, `back_seat`, `whole_vehicle`) — shared with Booking module, define once in `app/Routing/Enums/VehicleOption.php` (or `app/Shared/Enums/` if Booking needs to reuse it — decide when writing T4.1, keep it in one place).
- **Domain reference**: domain.md §3 (pricing), §2 (vehicle options)

### T3.4 — PricingService
- **Module**: Routing
- **Depends on**: T3.3
- **Description**: `PricingService::quote(EvRoute $route, VehicleOption $option): PriceQuoteData`. Unit test covering a route with all three vehicle options priced.
- **Domain reference**: domain.md §3

### T3.5 — RoutingPlugin + Filament EvRouteResource
- **Module**: Routing/Filament
- **Depends on**: T3.1–T3.3, T1.3
- **Description**: `app-modules/routing/src/RoutingPlugin.php` (same `Plugin` contract shape as `CatalogPlugin`, T2.0), added to `AdminPanelProvider`'s `->plugins([...])`. `EvRouteResource` in `app-modules/routing/src/Filament/Resources/` — form with relation selects (company, from/to destination), multi-select for time slots, nested `RoutePricing` relation manager (one row per vehicle option, enforce all 3 present before route can be activated — validation, not a DB constraint).
- **Domain reference**: domain.md §1, §3

### T3.6 — Routes read API
- **Module**: Routing
- **Depends on**: T3.1–T3.4
- **Description**: `GET /api/v1/routes` (filters: `from`, `to`, `date`, `company`), `GET /api/v1/routes/{route}`, `GET /api/v1/routes/{route}/pricing`, `GET /api/v1/routes/{route}/time-slots`. `EvRouteResource` includes nested company/timeSlots/pricing per architecture plan §11 (AI-agent-friendly shape). Feature tests including the filter combinations.
- **Domain reference**: domain.md §8 (this is what the AI agent's `route:read` ability consumes)

### T3.7 — Route/pricing caching
- **Module**: Routing
- **Depends on**: T3.6
- **Description**: Redis cache (tagged `routes`) on the three read endpoints, TTL ~5 min, invalidated via a model observer on `EvRoute`/`RoutePricing` save.
- **Domain reference**: — (perf, no business rule)

---

## Phase 4 — Booking Module (core, no payment yet)

### T4.1 — Booking model
- **Module**: Booking
- **Depends on**: T3.1, T3.2, T3.3
- **Description**: Migration, model, factory for `bookings` (`booking_code` unique, `user_id` nullable FK, `ev_route_id`, `departure_time_slot_id`, `travel_date`, `vehicle_option` enum, `passenger_name`, `passenger_phone`, `pickup_address`, `pickup_lat` nullable, `pickup_lng` nullable, `dropoff_address`, `dropoff_lat` nullable, `dropoff_lng` nullable, `price` decimal snapshot, `status` enum, `is_round_trip`, `return_travel_date` nullable, `created_by_channel` enum). `BookingStatus` enum (`pending_payment`, `confirmed`, `cancelled`, `expired`).
- **Domain reference**: domain.md §2a (door-to-door pickup/dropoff addresses live here, not on the route), §3 (price snapshot — critical, write a test asserting price doesn't change after a later `RoutePricing` edit), §5 (status machine)

### T4.2 — BookingService::validateSelection
- **Module**: Booking
- **Depends on**: T4.1
- **Description**: Enforces the **only** v1 inventory rule — max 1 Front Seat per booking — plus reads `BOOKING_BACK_SEAT_ENABLED`/`BOOKING_WHOLE_VEHICLE_ENABLED` config toggles to reject disabled vehicle options. Unit tests: front seat over-limit rejected, disabled option rejected, normal case passes. **Do not add any real availability/capacity check here** — out of scope (domain.md §2, §7).
- **Domain reference**: domain.md §2 (read this whole section before starting)

### T4.3 — CreateBookingAction + BookingCreated event
- **Module**: Booking
- **Depends on**: T4.2, T3.4
- **Description**: `CreateBookingAction` — calls `BookingService::validateSelection`, `PricingService::quote` for the snapshot price, persists booking `pending_payment` inside `DB::transaction()`, dispatches `BookingCreated`. `BookingCode` generation strategy (e.g. `EVB-{date}-{random}`) — deterministic + unique, tested.
- **Domain reference**: domain.md §5

### T4.4 — Booking create API
- **Module**: Booking
- **Depends on**: T4.3
- **Description**: `StoreBookingRequest` (shape validation only, per architecture plan §10 — business rules stay in the Service), `BookingController@store`, `BookingResource`, `POST /api/v1/bookings`. Feature tests: happy path, front-seat-limit rejection surfaces as 422, disabled-vehicle-option rejection surfaces as 422.
- **Domain reference**: domain.md §2, §5

### T4.5 — Booking read API
- **Module**: Booking
- **Depends on**: T4.4, T1.4
- **Description**: `GET /api/v1/bookings` (current user's own, paginated), `GET /api/v1/bookings/{booking}` (policy: owner or admin). Fill in `BookingPolicy::view`/`viewAny` for real (was stubbed in T1.4).
- **Domain reference**: domain.md §8

### T4.6 — CancelBookingAction (unpaid path only)
- **Module**: Booking
- **Depends on**: T4.4
- **Description**: `CancelBookingAction` for `pending_payment` bookings only (sets `status = cancelled` directly, no payment involved). Explicitly **guard against** cancelling a `confirmed` booking here — throw/reject, since that path requires a refund first (wired in T5.12). `POST /api/v1/bookings/{booking}/cancel`, `BookingPolicy::cancel`.
- **Domain reference**: domain.md §5 (do not let this ticket bypass the refund requirement for paid bookings)

### T4.7 — BookingPlugin + Filament BookingResource
- **Module**: Booking/Filament
- **Depends on**: T4.4, T1.3
- **Description**: `app-modules/booking/src/BookingPlugin.php` (same shape as `CatalogPlugin`, T2.0), added to `AdminPanelProvider`'s `->plugins([...])`. `BookingResource` in `app-modules/booking/src/Filament/Resources/` — read-mostly list (status badge, filters by status/date/route/company), a `Cancel` table action gated by `BookingPolicy::cancel` and only enabled for `pending_payment` rows (paid-booking cancellation UI comes with Refund resource in Phase 5).
- **Domain reference**: domain.md §5

---

## Phase 5 — Payment Module

**Before starting T5.x, read the whole payment section of `domain.md` (§6) and skim `/home/marcspecta/company_projects/bnf_event`'s `app/Strategies/Payments/*.php` and `app/Services/PaymentService.php` — these tickets port and refine that code, they don't design from scratch.**

### T5.1 — Payment contracts
- **Module**: Payment
- **Depends on**: T0.2
- **Description**: `PaymentGatewayInterface` (`initiate`, `verify`, `refund`), DTOs `PaymentRequestData`/`PaymentResultData`/`RefundResultData`, enums `PaymentMethod`/`PaymentStatus`/`RefundStatus`. No implementation yet — pure contract layer.
- **Domain reference**: domain.md §6

### T5.2 — payments & refunds tables
- **Module**: Payment
- **Depends on**: T4.1, T5.1
- **Description**: Migrations/models/factories for `payments` (`booking_id` FK, `gateway`, `status`, `amount`, `currency`, `gateway_transaction_id`, `gateway_payload` jsonb, `initiated_at`, `completed_at`) and `refunds` (`payment_id` FK, `status`, `amount`, `reason`, `gateway_refund_id`, `gateway_payload` jsonb, `requested_by` nullable FK, `requested_at`, `completed_at`). Relations: `Booking hasMany Payment`, `Payment hasMany Refund`.
- **Domain reference**: domain.md §6 (separate-tables decision), architecture plan §4

### T5.3 — KbzMiniAppGateway: initiate()
- **Module**: Payment
- **Depends on**: T5.1
- **Description**: Port the signing scheme (`joinKeyVal`/`signature`, SHA-256 flatten) and the `kbz.payment.precreate` call from `bnf_event`'s `KBZMiniApp::preparePaymentData`/`save`, adapted to return `PaymentResultData` and read credentials from `config('services.kbz')`. Unit test the signature function against a known fixture derived from the old code.
- **Domain reference**: domain.md §6

### T5.4 — KbzMiniAppGateway: verify()
- **Module**: Payment
- **Depends on**: T5.3
- **Description**: Port `kbz.payment.queryorder` call (`KBZPay::queryOrder`) as `KbzMiniAppGateway::verify()`, used both for the client-driven re-verification fallback and inside webhook processing (T5.10).
- **Domain reference**: domain.md §6

### T5.5 — KbzMiniAppGateway: refund()
- **Module**: Payment
- **Depends on**: T5.3
- **Description**: Port `kbz.payment.refund` + mTLS cert handling from `KBZMiniApp::refund`. Unlike the old code (refund amount commented out / full-refund-only), **wire the amount parameter through** to support partial refunds. `KBZ_CERT_PATH`/`KBZ_CERT_KEY_PATH`/`KBZ_CA_PATH`/`KBZ_CERT_PASSWORD` read from config.
- **Domain reference**: domain.md §6 (partial refund is an explicit improvement, not matching old behavior)

### T5.6 — PaymentGatewayFactory
- **Module**: Payment
- **Depends on**: T5.3–T5.5
- **Description**: `PaymentGatewayFactory::make(PaymentMethod $method): PaymentGatewayInterface`, registered via bindings in `PaymentServiceProvider`. This replaces the old code's 3x duplicated switch — test that adding a fake second gateway for tests requires only a factory registration, no controller/action changes.
- **Domain reference**: domain.md §6

### T5.7 — PaymentService (orchestrator)
- **Module**: Payment
- **Depends on**: T5.6
- **Description**: Thin context class delegating to the resolved gateway — the single call site every Action goes through.
- **Domain reference**: —

### T5.8 — InitiatePaymentAction + API
- **Module**: Payment
- **Depends on**: T5.7, T5.2
- **Description**: `InitiatePaymentAction` persists a `payments` row (`status = pending`) and returns the gateway payload. `POST /api/v1/payments/{booking}/initiate`. Feature test using a fake `PaymentGatewayInterface` binding (never call real KBZ in tests).
- **Domain reference**: domain.md §6

### T5.9 — KBZ webhook + signature verification
- **Module**: Payment
- **Depends on**: T5.2
- **Description**: `POST /api/v1/webhooks/kbz`, no `auth:sanctum` (verified by signature instead). Confirm KBZ's actual inbound webhook signature spec first (flagged risk — old code never implemented one, only referenced `notify_url`) — spike this before writing the verifier. Invalid signature → `400` + `warning` log, never `500`. Raw payload always persisted before/regardless of processing outcome.
- **Domain reference**: domain.md §6 (webhook idempotency, signature spec risk)

### T5.10 — ConfirmPaymentAction + payment events/listeners
- **Module**: Payment/Booking
- **Depends on**: T5.9, T5.4
- **Description**: `ConfirmPaymentAction` — **idempotent** (checks current `payments.status` before transitioning, since KBZ may retry the webhook), calls `verify()` for defense-in-depth re-check, updates `payments.status`, dispatches `PaymentCompleted`/`PaymentFailed` (queued listeners). `MarkBookingPaid` listener flips `bookings.status = confirmed`. Feature tests: double-delivery of the same webhook only transitions once.
- **Domain reference**: domain.md §5, §6 (idempotency is the critical thing to test here)

### T5.11 — RefundBookingAction + API
- **Module**: Payment/Booking
- **Depends on**: T5.6, T5.2
- **Description**: `RefundBookingAction` (usable from both API and Filament) — resolves gateway via factory, calls `refund()`, persists `refunds` row inside a transaction, dispatches `RefundProcessed` (listener `MarkBookingRefunded` sets `bookings.status = cancelled`). `POST /api/v1/bookings/{booking}/refund`, `BookingPolicy::refund`. Failure path: `refunds.status = failed` persisted, booking status untouched, 422 with gateway message surfaced.
- **Domain reference**: domain.md §5, §6

### T5.12 — Wire CancelBookingAction to refunds for paid bookings
- **Module**: Booking/Payment
- **Depends on**: T4.6, T5.11
- **Description**: Extend `CancelBookingAction` (from T4.6) so that cancelling a `confirmed` booking now delegates to `RefundBookingAction` first, instead of the T4.6 guard-reject. Update the T4.6 test that asserted rejection.
- **Domain reference**: domain.md §5

### T5.13 — PaymentPlugin + Filament PaymentResource/RefundResource
- **Module**: Payment/Filament
- **Depends on**: T5.2, T5.11, T1.3
- **Description**: `app-modules/payment/src/PaymentPlugin.php` (same shape as `CatalogPlugin`, T2.0), added to `AdminPanelProvider`'s `->plugins([...])`. Both resources live in `app-modules/payment/src/Filament/Resources/`: `PaymentResource` (read-only, filters by status/gateway, `gateway_payload` visible admin-only) and `RefundResource` (list + a `Process` action with amount/reason form, calling `RefundBookingAction`), only actionable from a successful `Payment`.
- **Domain reference**: domain.md §6

---

## Phase 6 — Security & Ops Hardening

### T6.1 — API rate limiting
- **Module**: Shared
- **Depends on**: Phase 4/5 endpoints existing
- **Description**: Throttle middleware config for `/api/v1/*`, tighter limits on `bookings`/`payments`/`webhooks` than read-only catalog/routing endpoints.
- **Domain reference**: domain.md §8

### T6.2 — Audit logging
- **Module**: Shared
- **Depends on**: Phase 2–5 modules
- **Description**: Install `spatie/laravel-activitylog`, attach to Booking/Payment/Refund status transitions and catalog/pricing admin CRUD (Filament). Filament `AuditLogResource` (read-only) gated by `view_audit_log` permission.
- **Domain reference**: —

### T6.3 — Global API error handling
- **Module**: Shared
- **Depends on**: Phase 4/5
- **Description**: JSON exception envelope for `api/*` in `bootstrap/app.php`. `PaymentGatewayException` (carries gateway error code/message) mapped to 422/502, never a raw 500.
- **Domain reference**: domain.md §6

### T6.4 — Full policy + agent-ability audit
- **Module**: Identity
- **Depends on**: all prior policy tickets
- **Description**: Review every endpoint against domain.md §8's access boundaries; add feature tests proving the FastAPI agent token **cannot** hit refund/catalog-write endpoints (expect 403), and that catalog/pricing writes have no customer-facing route at all.
- **Domain reference**: domain.md §8 (re-read before writing these tests)

### T6.5 — Filament log viewer
- **Module**: Shared
- **Depends on**: T6.2
- **Description**: Install `gboquizosanchez/filament-log-viewer` to browse application/error log files (`storage/logs/laravel-*.log`) from the admin panel, on top of the `daily` log channel. This is a dev/ops convenience for reading raw log output in-browser — distinct from T6.2's structured, per-model audit trail (`spatie/laravel-activitylog`), which this does not replace. Gate visibility to admin-tier roles only (same access boundary as the rest of the Filament panel, domain.md §8).
- **Domain reference**: — (ops convenience, no business rule)

---

## Phase 7 — Dashboard & Polish

### T7.1 — Dashboard widgets
- **Module**: Booking/Payment
- **Depends on**: Phase 4/5
- **Description**: `BookingsTodayWidget`, `RecentBookingsTableWidget` in `app-modules/booking/src/Filament/Widgets/` (discovered by `BookingPlugin`); `RevenueChartWidget` (30d, cached), `PaymentFailureRateWidget` in `app-modules/payment/src/Filament/Widgets/` (discovered by `PaymentPlugin`). Register widgets on the Filament dashboard page via each plugin's `boot()`.
- **Domain reference**: —

### T7.2 — Deployment pipeline
- **Module**: Shared
- **Depends on**: all above
- **Description**: Production compose file (queue worker service, scheduler cron), `config:cache`/`route:cache`/`view:cache` in deploy steps, mTLS certs delivered via deployment secrets (not committed).
- **Domain reference**: domain.md §6 (cert handling)

---

## Deferred Tickets (not scheduled — pick up if the business need reappears)

### T2.3 (deferred) — Pickup & Dropoff Checkpoint Locations
- **Module**: Catalog
- **Depends on**: T2.2
- **Description**: Migrations, models, factories for `pickup_locations` / `dropoff_locations` (`destination_id` FK, `name`, `address`, `lat`, `lng`, `is_active`). Two Filament resources (or one resource with a type toggle — prefer two for clarity), both under `app-modules/catalog/src/Filament/Resources/`.
- **Why deferred**: the actual business model is door-to-door — the EV goes to whatever address the customer gives at booking time (see domain.md §2a), not a fixed catalog point. This ticket models the *exception* case (customer too far, so they and the car meet at a fixed checkpoint instead), which isn't built for v1.
- **To revive this later**: re-add `pickup_location_id`/`dropoff_location_id` nullable FKs (route-level default checkpoint) or put them directly on `Booking` (per-booking checkpoint choice — more likely, since door-to-door is also per-booking) alongside the existing `pickup_address`/`dropoff_address` free-text fields from T4.1, so a booking can be *either* a free-text address *or* a checkpoint reference. Update `EvRouteResource` (T3.5) and the routes read API (T3.6) if checkpoints end up route-scoped.
- **Domain reference**: domain.md §2a, §7
