# 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) --- ## 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