project initialize
This commit is contained in:
+349
@@ -0,0 +1,349 @@
|
||||
# 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, PickupLocation, DropoffLocation, DepartureTimeSlot)
|
||||
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 — Pickup & Dropoff 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 given the architecture plan's separate tables), both under `app-modules/catalog/src/Filament/Resources/`.
|
||||
- **Domain reference**: domain.md §1
|
||||
|
||||
### 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, T2.3
|
||||
- **Description**: Migration, model, factory for `ev_routes` (`ev_company_id`, `from_destination_id`, `to_destination_id`, `pickup_location_id`, `dropoff_location_id`, `is_round_trip`, `is_active`). Eloquent relations: `belongsTo` company/fromDestination/toDestination/pickup/dropoff.
|
||||
- **Domain reference**: domain.md §1
|
||||
|
||||
### 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, pickup/dropoff), 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 pickup/dropoff/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`, `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 §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)
|
||||
Reference in New Issue
Block a user