Files
famous-ly4-ev/tickets.md
T
Nyan Lin Paing 5b68f4fa38 Add Booking module: model, create/read/cancel API, Filament resource (T4.1-T4.7)
- Booking model with booking_vehicle_options line items (supports mixing
  vehicle options like front_seat + back_seat in one booking), price
  snapshot, status machine, and driver/car assignment fields
- BookingService: front-seat max, disabled-option toggles, duplicate-option
  and whole-vehicle-exclusivity guards
- CreateBookingAction, CancelBookingAction, AssignDriverAction
- BookingRefGenerator: sequential EVB-AAAAA1-style refs via row lock
- POST/GET/cancel booking API endpoints (Sanctum, ownership + admin policy)
- BookingPlugin + Filament BookingResource: list, detail view, Cancel and
  Assign Driver actions (shared between table and detail page)
- domain.md updated for multi-vehicle-option bookings (§2) and driver/
  vehicle assignment (§5a)
2026-08-08 21:43:15 +07:00

360 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.1T2.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.1T2.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.1T3.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.1T3.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.3T5.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 25 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)
---
## 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