- 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)
27 KiB
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) forcatalog,routing,booking,payment,identity,shared. Each getscomposer.json(PSR-4Modules\{Module}\→src/,require: filament/filament,extra.laravel.providerspointing at its{Module}ServiceProvider), thesrc/Providers/{Module}ServiceProvider.phpstub, and emptyroutes/,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 rootcomposer.jsonrepositories, thencomposer 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'scomposer.jsonname). Confirmcomposer dump-autoloadpicks up each{Module}ServiceProvidervia package discovery — no manualbootstrap/providers.phpedits needed per module. - Domain reference: — (structural only)
T0.3 — Install & configure Sanctum, Spatie Permission, Filament
- Module: Shared/Identity
- Depends on: T0.2
- Description:
composer requireSanctum (bundled w/ Laravel 12 but confirm published config),spatie/laravel-permission, Filament panel package (root-level, not per-module). Publish configs/migrations. Create the centralapp/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.exampleandconfig/services.php/config/booking.phpskeletons per architecture plan §27 (Redis cache/queue/session,BOOKING_*toggles,KBZ_*placeholders — values empty, just the keys). SwitchCACHE_STORE,QUEUE_CONNECTION,SESSION_DRIVERtoredisin.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).RolePermissionSeederwired intoDatabaseSeeder. - 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.EnsureFastApiAgentmiddleware (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,RoutePolicyclasses 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.phpimplementingFilament\Contracts\Plugin—getId(),register(Panel $panel)callingdiscoverResources/discoverPages/discoverWidgetsagainstsrc/Filament/{Resources,Pages,Widgets},boot(Panel $panel)empty for now. AddCatalogPlugin::make()toAdminPanelProvider'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).EvCompanyResourceinapp-modules/catalog/src/Filament/Resources/(simple CRUD form) — auto-discovered byCatalogPlugin(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).DestinationResourceinapp-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).DepartureTimeSlotResourceinapp-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/destinationswithEvCompanyResource/DestinationResourceAPI resources,auth:sanctum+ throttle. Feature tests for both. - Domain reference: domain.md §8 (agent needs
route:readwhich 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:belongsTocompany/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_slotspivot migration +belongsToManyrelation onEvRoute/DepartureTimeSlot, withis_activeon 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_optionenum,price).VehicleOptionenum (front_seat,back_seat,whole_vehicle) — shared with Booking module, define once inapp/Routing/Enums/VehicleOption.php(orapp/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(samePlugincontract shape asCatalogPlugin, T2.0), added toAdminPanelProvider's->plugins([...]).EvRouteResourceinapp-modules/routing/src/Filament/Resources/— form with relation selects (company, from/to destination), multi-select for time slots, nestedRoutePricingrelation 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.EvRouteResourceincludes 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:readability 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 onEvRoute/RoutePricingsave. - 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_codeunique,user_idnullable FK,ev_route_id,departure_time_slot_id,travel_date,vehicle_optionenum,passenger_name,passenger_phone,pickup_address,pickup_latnullable,pickup_lngnullable,dropoff_address,dropoff_latnullable,dropoff_lngnullable,pricedecimal snapshot,statusenum,is_round_trip,return_travel_datenullable,created_by_channelenum).BookingStatusenum (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
RoutePricingedit), §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_ENABLEDconfig 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— callsBookingService::validateSelection,PricingService::quotefor the snapshot price, persists bookingpending_paymentinsideDB::transaction(), dispatchesBookingCreated.BookingCodegeneration 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 inBookingPolicy::view/viewAnyfor real (was stubbed in T1.4). - Domain reference: domain.md §8
T4.6 — CancelBookingAction (unpaid path only)
- Module: Booking
- Depends on: T4.4
- Description:
CancelBookingActionforpending_paymentbookings only (setsstatus = cancelleddirectly, no payment involved). Explicitly guard against cancelling aconfirmedbooking 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 asCatalogPlugin, T2.0), added toAdminPanelProvider's->plugins([...]).BookingResourceinapp-modules/booking/src/Filament/Resources/— read-mostly list (status badge, filters by status/date/route/company), aCanceltable action gated byBookingPolicy::canceland only enabled forpending_paymentrows (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), DTOsPaymentRequestData/PaymentResultData/RefundResultData, enumsPaymentMethod/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_idFK,gateway,status,amount,currency,gateway_transaction_id,gateway_payloadjsonb,initiated_at,completed_at) andrefunds(payment_idFK,status,amount,reason,gateway_refund_id,gateway_payloadjsonb,requested_bynullable 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 thekbz.payment.precreatecall frombnf_event'sKBZMiniApp::preparePaymentData/save, adapted to returnPaymentResultDataand read credentials fromconfig('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.queryordercall (KBZPay::queryOrder) asKbzMiniAppGateway::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 fromKBZMiniApp::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_PASSWORDread 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 inPaymentServiceProvider. 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:
InitiatePaymentActionpersists apaymentsrow (status = pending) and returns the gateway payload.POST /api/v1/payments/{booking}/initiate. Feature test using a fakePaymentGatewayInterfacebinding (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, noauth:sanctum(verified by signature instead). Confirm KBZ's actual inbound webhook signature spec first (flagged risk — old code never implemented one, only referencednotify_url) — spike this before writing the verifier. Invalid signature →400+warninglog, never500. 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 currentpayments.statusbefore transitioning, since KBZ may retry the webhook), callsverify()for defense-in-depth re-check, updatespayments.status, dispatchesPaymentCompleted/PaymentFailed(queued listeners).MarkBookingPaidlistener flipsbookings.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, callsrefund(), persistsrefundsrow inside a transaction, dispatchesRefundProcessed(listenerMarkBookingRefundedsetsbookings.status = cancelled).POST /api/v1/bookings/{booking}/refund,BookingPolicy::refund. Failure path:refunds.status = failedpersisted, 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 aconfirmedbooking now delegates toRefundBookingActionfirst, 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 asCatalogPlugin, T2.0), added toAdminPanelProvider's->plugins([...]). Both resources live inapp-modules/payment/src/Filament/Resources/:PaymentResource(read-only, filters by status/gateway,gateway_payloadvisible admin-only) andRefundResource(list + aProcessaction with amount/reason form, callingRefundBookingAction), only actionable from a successfulPayment. - 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 onbookings/payments/webhooksthan 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). FilamentAuditLogResource(read-only) gated byview_audit_logpermission. - Domain reference: —
T6.3 — Global API error handling
- Module: Shared
- Depends on: Phase 4/5
- Description: JSON exception envelope for
api/*inbootstrap/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,RecentBookingsTableWidgetinapp-modules/booking/src/Filament/Widgets/(discovered byBookingPlugin);RevenueChartWidget(30d, cached),PaymentFailureRateWidgetinapp-modules/payment/src/Filament/Widgets/(discovered byPaymentPlugin). Register widgets on the Filament dashboard page via each plugin'sboot(). - 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:cachein 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_idFK,name,address,lat,lng,is_active). Two Filament resources (or one resource with a type toggle — prefer two for clarity), both underapp-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_idnullable FKs (route-level default checkpoint) or put them directly onBooking(per-booking checkpoint choice — more likely, since door-to-door is also per-booking) alongside the existingpickup_address/dropoff_addressfree-text fields from T4.1, so a booking can be either a free-text address or a checkpoint reference. UpdateEvRouteResource(T3.5) and the routes read API (T3.6) if checkpoints end up route-scoped. - Domain reference: domain.md §2a, §7