Files
famous-ly4-ev/tickets.md
T
Nyan Lin Paing 46f9b8d5a3 Phase 6: security & ops hardening (T6.1-T6.5)
- T6.1: named rate limiters (api-read/api-write/api-auth/api-webhooks),
  applied per module route group with tighter limits on booking/payment
  writes and the KBZ webhook than read-only catalog/routing endpoints.
- T6.2: install spatie/laravel-activitylog; LogsActivity on Booking/
  Payment/Refund status transitions and catalog/pricing admin CRUD
  (EvCompany, Destination, DepartureTimeSlot, EvRoute, RoutePricing).
  New IdentityPlugin with a read-only AuditLogResource gated by
  view_audit_log.
- T6.3: JSON error envelope for api/* in bootstrap/app.php (401/403/404/
  405/429/500 fallback), plus PaymentGatewayException (422 declined /
  502 unavailable).
- T6.4: feature tests proving the FastAPI agent token gets 403 on
  refund/cancel-not-owned and 405 (no write handler) on catalog/routing
  writes.
- T6.5: install gboquizosanchez/filament-log-viewer with a custom
  Filament admin theme (required for its views' Tailwind classes to
  compile), LOG_CHANNEL/FILAMENT_LOG_VIEWER_DRIVER=daily, registered
  under Operations in the sidebar.

252 tests passing.
2026-08-09 20:59:00 +07:00

27 KiB
Raw Blame History

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 requires 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\PlugingetId(), 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: ConfirmPaymentActionidempotent (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)

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