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

109 lines
9.1 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 — 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 set**`route: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.