Now that only the outbound leg gets a row, its Price column showed just
that leg's price, silently missing the return leg's half. It now sums both
legs (bcadd, 2dp) for a round trip, with a breakdown shown underneath;
unchanged for one-way bookings.
MarkBookingRefunded (queued) is still the source of truth for every other
caller (API, ProcessRefundAction) — left untouched. Scoped here only: after
a successful refund from the booking list/detail action specifically, flip
the status in the same request instead of waiting on the queue worker, so
the row/page reflects Cancelled the moment this request finishes.
Showing both legs as separate rows read as two unrelated bookings rather
than one round trip. The list now only queries outbound/primary legs; the
return leg is still reachable via the outbound leg's existing "Linked Leg"
link on the detail page.
MarkBookingPaid/MarkBookingRefunded (and anything else ShouldQueue) had
nothing consuming the jobs table locally — no queue service existed in
compose.yaml, so a booking's status only flipped once someone happened to
run `sail artisan queue:work` by hand. Refund actions in Filament looked
like they hadn't taken effect until that ran.
It was ShouldQueue, so on QUEUE_CONNECTION=database with no worker running
(or just queue lag), a booking stayed Confirmed in the UI until a worker
processed the job — refund actions in Filament looked like they hadn't
taken effect without a page reload. A refund is always initiated from a
request already waiting on it (the Filament action or the API endpoint),
so there's no reason to defer the status flip.
ImageColumn::make('logo') had no explicit disk, so it defaulted to
Filament's local disk while uploads write to the public disk,
causing the file-existence check to fail and the src to render
empty. Align the column's disk with the upload field's disk.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015HaT8a5ZWh45JkjTLPWeaP
Logos were being written to the default filesystem disk, which
resolves to storage/app/private and isn't web-servable, so the
frontend couldn't load the image. Store logos on the public disk
(storage/app/public, symlinked to public/storage) instead, and
resolve logoUrl() against that disk explicitly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015HaT8a5ZWh45JkjTLPWeaP
- Modules\Shared\Bnfexpress\BnfexpressAdminClient: HMAC-signed HTTP client
for bnfexpress's admin API (EV FAQs, agent instructions, chat history),
with a bnfexpress:smoke-test command and full unit coverage.
- New ai-agent module: Filament pages to manage EV FAQs, publish/roll back
agent instruction versions, and browse EV chat history + transcripts.
- New manage_ai_agent permission (super_admin/admin).
- Recorded .ai/rules for the client's auth scheme and non-Resource
Filament page/table testing gotchas.
Groups fields into Payment/Timeline sections with icons, copyable IDs, and
a money-formatted amount. Drops the permission-gated Gateway Response
section (raw gateway_payload) and its two visibility tests along with it.
Front Seat previously had a hardcoded max-per-booking check with no enable
toggle; now every Vehicle Option (front_seat/back_seat/whole_vehicle) gets
the same config-driven pair — an on/off toggle and a max passenger_count —
surfaced via new BOOKING_*_ENABLED/BOOKING_*_MAX_PER_BOOKING env vars,
editable from ManageAppSettings, and exposed on route pricing as
max_per_booking.
Two-step flow: request a one-time code by email/phone (RegistrationVerification,
mailed via RegistrationCodeMail, rate-limited by the new api-otp limiter keyed
to the identifier), then verify the code and register with RegistrationController.
User gains a phone column/fillable.
Curated from/to destination pairs for marketing content, editable via a
Filament resource and served publicly (no auth, IP-throttled like CMS
pages) through GET /api/v1/popular-routes. The API response trims each
nested destination down to id/name/mm_name rather than the full catalog
resource.
Static content pages (CmsPage) manageable from a new Filament resource and
readable via a public API endpoint. Wired into the admin panel with its own
CmsPlugin and "CMS" navigation group.
- New RefundBookingTableAction on the booking list row and detail page,
refunding a Confirmed booking directly via RefundBookingAction — no need
to hunt up its Payment on the Refunds resource first.
- Payment::refundableBalance() extracted from RefundBookingAction's private
balance check so both refund forms can display and cap against it.
- RefundBookingAction::resolveRefundablePayment() made public for the same
reason (round-trip leg resolution reused by the UI).
- Both refund forms (ProcessRefundAction and the new booking action) gain a
"Full refund" toggle, on by default, which refunds the payment's whole
remaining balance without requiring a manually typed amount. Turning it
off reveals an amount field capped at the refundable balance.
KBZ_CERT_PATH/KBZ_CERT_KEY_PATH/KBZ_CA_PATH now store paths relative to
storage/, wrapped in storage_path() here instead of requiring an absolute
path in .env.
EvCompanyResource returned the raw disk-relative path stored by
Filament's FileUpload (e.g. "logos/xxx.png"), not something API
consumers can render directly.
- EvCompany::logoUrl() builds an absolute URL from the configured
filesystem disk, guarding against a disk (e.g. s3) that already
returns an absolute URL so it isn't double-prefixed.
- EvCompanyResource now exposes that as 'logo' instead of the raw path.
Staging terminates SSL at a reverse proxy in front of the app, but
Laravel had no trustProxies() configured, so it never saw the request
as HTTPS and generated http:// asset URLs on the https:// page.
Browsers block that as mixed content, which silently broke every
JS-enhanced Filament field (FileUpload, Textarea, etc.) — e.g. the
Ev Company logo field falling back to a bare native file input.
- bootstrap/app.php: trust the proxy via X-Forwarded-* headers.
- AppServiceProvider: force the https scheme when APP_URL is https,
as a fallback in case the forwarded header is ever missing.
New modules/reporting Filament page: filterable bookings table (travel
date range, status, route, channel) with CSV/Excel export. Report
columns include booking ref, route, passenger name/count, price,
best-payment status/amount, and driver info, plus a TOTAL row summing
passenger count, price, and payment amount in both export formats.
- BookingsRevenueExport backs both CSV and XLSX via maatwebsite/excel
^4.0 (the only version compatible with PHP 8.5; 3.1.x caps
phpoffice/phpspreadsheet below 8.5).
- CSV export writes a UTF-8 BOM so non-Latin passenger names (Burmese)
open correctly in Excel.
- New view_reports permission (super_admin/admin/support) gates the
page; new indexes on bookings.travel_date/status/created_by_channel
and payments.completed_at support the report's filters.
- BookingsTodayWidget, RecentBookingsTableWidget (Booking module) and
RevenueChartWidget, PaymentFailureRateWidget (Payment module) dashboard
widgets, auto-registered via each plugin's existing discoverWidgets().
- .gitea/workflows/tests.yml: Postgres-backed Pest run on push/PR.
- Landing page (resources/views/welcome.blade.php) now shows the Famous
Linnyone4 EV logo with a single admin login link, and the Filament admin
panel uses the same logo as its brand logo.
Access group (Filament):
- StaffResource: manage users with an admin-tier role, gated by manage_staff
- CustomerResource: read-only view of role-less users, gated by view_customers
- RoleResource: edit permissions per role (fixed role set), gated by manage_roles
- ManageAppSettings: tabbed General/Booking settings page that reads/writes
real .env keys via new EnvFileWriter (no parallel DB settings table, so
BookingService/config('booking.*') stay unchanged)
- Moved Access above Catalog in the nav group order
- New permissions: manage_staff, manage_roles, view_customers, manage_settings
Booking soft deletes:
- bookings.deleted_at + SoftDeletes on the Booking model
- BookingPolicy::delete (manage_bookings, cancelled/expired only) and
::restore (manage_bookings)
- DeleteBookingTableAction/RestoreBookingTableAction + TrashedFilter on
BookingsTable, using authorize() so the policy is enforced at call time,
not just cosmetically hidden
Refund crash fix:
- ProcessRefundAction passed a nullable $payment->booking into
RefundBookingAction's non-nullable Booking param — a soft-deleted
booking's payment reaching the refund picker was an uncaught TypeError.
Excluded such payments from the picker and added a defensive guard.
- Same unguarded $event->payment->booking / $event->refund->payment->booking
pattern fixed in the MarkBookingPaid/MarkBookingRefunded queued listeners.
289 tests passing.
- 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.
Implements Phase 3 in full: EvRoute model with company/destination relations
and a from/to-must-differ guard; the ev_route_time_slots pivot; RoutePricing
with a per-route is_blocked flag (every route auto-manages exactly one price
row per vehicle option via a fixed-row Filament repeater on both create and
edit); PricingService::quote() with a shared VehicleOption enum; RoutingPlugin
with the EvRouteResource admin UI (activation gated on non-blocked options
being priced); the /api/v1/routes read API (list/show/pricing/time-slots,
AI-agent-friendly nested shape); and Redis-tag-based response caching
invalidated via EvRoute/RoutePricing observers.
Adds the shared DepartureTimeSlot catalog entity with its Filament
resource, and public GET /api/v1/companies + /api/v1/destinations
endpoints (auth:sanctum + throttled) for the mini app/mobile/agent
clients to consume.
Registers CatalogPlugin with the admin panel so catalog Filament
resources auto-discover, and adds the EvCompany model/migration/
factory plus its Filament resource (auto-generated slug on create).