Files
famous-ly4-ev/domain.md
T
2026-08-04 21:40:49 +07:00

9.1 KiB
Raw Blame History

EV Booking System — Business Domain Reference

Reference doc for business rules and domain vocabulary. Pull this up alongside tickets.md while working any ticket — it explains why a rule exists, not how to code it (see the architecture plan for implementation shape).


1. Glossary

Term Meaning
EV Company A vehicle operator/fleet owner. Plain reference data — not a tenant (see §4).
Destination A city/town served by routes. Used as both origin and endpoint.
Pickup Location A physical point within a Destination where customers board.
Dropoff Location A physical point within a Destination where customers alight.
Departure Time Slot A shared catalog of times (e.g. "06:00 AM"); attached to routes via a pivot, not owned by one route.
EV Route Company + From Destination + To Destination + Pickup + Dropoff + round-trip flag + one or more Time Slots + pricing per Vehicle Option.
Vehicle Option What the customer books: front_seat, back_seat, or whole_vehicle. Not a numbered seat — see §2.
Booking A customer's reservation of one Vehicle Option on one Route + Date + Time Slot.
Payment One attempt to pay for a Booking through a gateway (may retry after failure).
Refund A reversal against a specific successful Payment (not against the Booking directly).

2. Vehicle Options & Inventory (the core domain quirk)

Unlike a bus-booking system, there is no seat map and no capacity tracking in v1:

  • Any number of customers can book the same Route + Date + Time Slot. The system does not check whether a "Whole Vehicle" or "Back Seat" is already taken by someone else.
  • The only rule enforced in code is: max 1 Front Seat per booking (a single booking cannot request more than one front seat — this is a per-booking constraint, not a per-trip inventory check).
  • Back Seat and Whole Vehicle availability are controlled by blunt config toggles, not database rows:
    • BOOKING_BACK_SEAT_ENABLED — whether Back Seat can be selected at all right now.
    • BOOKING_WHOLE_VEHICLE_ENABLED — whether Whole Vehicle can be selected at all right now.
    • BOOKING_FRONT_SEAT_MAX_PER_BOOKING — currently 1, expressed as config in case it ever needs to change.
  • This is a deliberate v1 simplification, not an oversight. Real per-route/date/time-slot capacity holding (e.g. "only 1 Whole Vehicle booking allowed per trip") is an explicitly deferred future phase — see §7.
  • Consequence: double-booking of "Whole Vehicle" is possible by design until that future phase ships. Admins reconcile manually via the Filament Booking list (filterable by route + date + time).

Do not build seat inventory, seat locking, or availability-checking logic against real capacity in current-phase tickets unless a ticket explicitly says so — it's out of scope until the "Future Scalability" phase.


3. Pricing

  • RoutePricing holds one price per (Route, Vehicle Option) pair.
  • When a Booking is created, the resolved price is snapshotted onto bookings.price and never re-read from RoutePricing again. If admin edits pricing later, existing bookings keep their original price. This is intentional — historical bookings must never silently reprice.

4. Tenancy

  • Single tenant. EV Companies are just rows in a table that the one admin team manages — there is no per-company admin login isolation, no data scoping by company for admin users, and no Spatie Permission "teams" feature in v1.
  • If multi-tenant admin isolation is ever needed, it layers onto the existing role/permission tables later without migrating the core booking/route schema (see architecture plan §30).

5. Booking Lifecycle (status machine)

pending_payment ──(payment succeeds)──▶ confirmed
pending_payment ──(customer/admin cancels, no payment yet)──▶ cancelled
confirmed ──(refund processed)──▶ cancelled
pending_payment ──(TTL expiry, future phase)──▶ expired
  • A Booking is created pending_payment — it exists before any money moves.
  • It only becomes confirmed once the linked Payment succeeds (via the KBZ webhook, not client-reported success — see §6).
  • Cancelling a confirmed (paid) Booking must go through the Refund flow first — never flip status to cancelled on a paid booking without a corresponding refund attempt.

6. Payment Domain (ported from bnf_event, refined)

The existing KBZ Mini App payment code at /home/marcspecta/company_projects/bnf_event (app/Strategies/Payments/KBZMiniApp.php, KBZPay.php, BasePayment.php, app/Services/PaymentService.php) is the reference implementation being ported and refined — read it before starting any Payment-module ticket.

What's being kept (business logic, still true in the new system):

  • KBZ's signing scheme: flatten the request array (excluding sign/sign_type) into sorted key=val pairs joined by &, append &key={merchant_key}, SHA-256 hash, uppercase. Every request sets sign_type = "SHA256".
  • Three KBZ API operations matter: kbz.payment.precreate (start payment), kbz.payment.queryorder (poll/verify status), kbz.payment.refund (reverse a payment). All are outbound HTTPS calls; refund additionally requires mTLS (client cert + key + CA bundle).
  • Client-driven confirmation pattern as a fallback/defense-in-depth: after the client reports payment complete, the server re-verifies by calling queryorder rather than trusting the client payload blindly.
  • Refund reason is a required, human-readable string sent to KBZ and stored for audit.

What's changing (the agreed refinements — see architecture plan §1719, §34):

  • Old: 3x duplicated switch($payment_type) at each call site to pick a gateway class. New: a PaymentGatewayFactory resolves PaymentGatewayInterface implementations by PaymentMethod enum, registered once.
  • Old: raw associative arrays passed through PaymentInterface methods. New: typed DTOs (PaymentRequestData, PaymentResultData, RefundResultData).
  • Old: KBZ credentials (merchant code, keys, URLs) lived in a DB singleton payment_setting table, editable via admin UI. New: .env + config/services.php (KBZ_* vars) — no DB table, no Filament resource for secrets.
  • Old: payment/refund state overloaded onto the orders table (payment_status int, order_status string, payment_remark text) with no structured history. New: dedicated payments + refunds tables (refunds.payment_id FK to payments.id), each with a proper status enum.
  • Old: refund amount was effectively hardcoded to full-amount (the refund_amount param was commented out in KBZMiniApp::refund). New: RefundBookingAction explicitly supports partial refunds, validated against the remaining refundable balance on the Payment.
  • Old: no real inbound webhook — only client-driven confirmation + server-side re-verification. New: add a genuine signed inbound webhook (POST /api/v1/webhooks/kbz) that verifies KBZ's signature before processing — confirm KBZ's actual inbound-webhook signature spec as part of the Phase 5 webhook ticket (flagged risk, KBZ's spec was never implemented in the old code, only referenced by a notify_url config value).
  • Old: status transitions applied inline inside BasePayment::paymentStatusChange(), tightly coupling the gateway class to booking-status logic. New: gateway strategies only return a PaymentResultData/RefundResultData — booking-status changes happen in listeners reacting to PaymentCompleted / PaymentFailed / RefundProcessed domain events, keeping the gateway strategy ignorant of the Booking module.

Webhook idempotency: KBZ may retry the notify webhook. The handler must check the Payment's current status before transitioning it — never assume a webhook call is the first/only delivery.


7. Deferred / Future (do not build yet)

  • Real per-route/date/time-slot capacity holding + availability checks (see §2).
  • DB row locking (SELECT ... FOR UPDATE) for booking concurrency — only needed once real inventory exists.
  • Multi-tenant admin isolation (Spatie Permission "teams").
  • Multiple payment gateways beyond KBZ (the factory is designed to support this later with zero call-site changes).
  • Booking auto-expiry TTL job for stale pending_payment bookings.
  • Customer notifications (booking confirmation, payment status push).

8. External Actors & Access Boundaries

  • Mini App / Mobile App: customer-facing clients, authenticate via Sanctum token, full customer ability set (create/view/cancel own bookings, initiate payment).
  • FastAPI AI Agent (external service, owns Gemini/tool-calling — Laravel has zero AI SDK code): authenticates via a Sanctum token scoped to a restricted ability setroute:read, booking:create, booking:read only. It must never be able to refund, cancel someone else's booking, or write catalog/pricing data. It never touches the database directly, only /api/v1.
  • Filament Admin: session-based, role-gated (super_admin, admin, support), the only place catalog/pricing mutation and refund initiation happen for staff.