# Architecture ## Shape: Vue SPA ⇄ REST API ⇄ fat Laravel Services The frontend is a **Vue 3 single-page app** (`resources/js/`). It talks to the backend exclusively over a **REST API** (`routes/api.php` → `app/Http/Controllers/Api/`). Controllers stay thin; all business logic lives in **Service classes** (`app/Services/`). Blade is reduced to a one-line SPA shell, server-rendered legal pages, transactional emails, and the Filament admin panel. ``` Browser └── Vue SPA (resources/js, mounted on #app via app.blade.php) └── axios (resources/js/axios.js — baseURL '/api', cookie + XSRF auth) └── REST API (routes/api.php → Http/Controllers/Api/*) └── Services (app/Services/*) ← all business logic └── Models / Jobs / Events / Notifications ``` ## Core principle: fat Services, thin everything else All business logic lives in Service classes. API controllers, console commands, jobs, listeners, and Filament resources are thin orchestrators — they call Services and return results. Never put domain logic in a controller or a Vue component. ## Directory structure (actual) ``` app/ ├── Console/Commands/ # Scheduler: PollFuelPrices, FetchOilPrices, BackfillOilPrices, │ # RunLlmOverlay, EvaluateVolatilityRegime, ResolveForecastOutcomes, │ # ArchiveOldPricesCommand, ImportBeisFuelPrices, ImportPostcodes ├── Http/ │ ├── Controllers/Api/ # AuthController, StationController, StatsController, UserController │ ├── Controllers/ # BillingController (Stripe Checkout/Portal redirects) │ ├── Middleware/ # VerifyApiKey (API-key gate), RequiresFeature (tier gate) │ ├── Requests/ # Form requests │ └── Resources/Api/ # StationResource (API output shaping) ├── Actions/Fortify/ # Fortify auth actions (CreateNewUser, …) ├── Services/ # ALL business logic — see below ├── Models/ # Eloquent models ├── Notifications/ # FuelPriceAlert + custom Channels (OneSignal, Vonage WA/SMS) ├── Jobs/ # DispatchUserNotificationJob, PollFuelPricesJob, │ # SendScheduledWhatsAppJob, SendPaymentFailedReminderJob ├── Events/ # PricesUpdatedEvent ├── Listeners/ # HandleStripeWebhook (Cashier WebhookReceived) ├── Filament/ # Admin panel (Resources, Widgets) + Providers/Filament ├── Enums/ # FuelType, PlanTier, … └── Livewire/Actions/ # Logout only — Livewire is otherwise vestigial app/Services/ ├── FuelPriceService.php # Fuel Finder API polling + storage ├── StationTaggingService.php # Supermarket brand detection ├── PostcodeService.php # postcode/outcode/place → lat/lng (+ LocationResult DTO) ├── PlanFeatures.php # Single source of tier-entitlement truth (see tiers.md) ├── HaversineQuery.php # Distance SQL helper ├── ApiLogger.php # Wraps outbound HTTP, logs to api_logs ├── StationSearch/ # StationSearchService (+ SearchCriteria, SearchResult DTOs) ├── BrentPriceSources/ # FRED + EIA Brent sources (+ BrentPriceFetcher) └── Forecasting/ # Weekly forecast subsystem — WeeklyForecastService, # LlmOverlayService, BacktestRunner, OutcomeResolver, # VolatilityRegimeService, feature/leak tooling, … resources/ ├── js/ # Vue 3 SPA — see .claude/rules/frontend.md └── views/ ├── app.blade.php # SPA shell (mounts #app) ├── legal/ # Server-rendered legal pages ├── emails/ # Mailable templates └── livewire/auth/ # Starter-kit Volt auth screens (left untouched) routes/ ├── web.php # SPA shell + catch-all, legal pages, billing redirects, server logout └── api.php # REST API consumed by the SPA (see below) ``` > **Stale service names elsewhere.** Domain rule files (`scoring.md`, > `prediction.md`, `notifications.md`) still name services that have since been > refactored away — e.g. `AlertScoringService`, `OilPriceService`, > `NationalFuelPredictionService`, `NotificationDispatchService`, > `SubscriptionService`. Those concerns now live under `Services/Forecasting/`, > `Services/StationSearch/`, and `PlanFeatures`. Always verify a service name > against `app/Services/` before trusting it from those files. ## Service class conventions - Constructor injection only — no facade usage inside Services - Each Service has one responsibility — do not merge concerns - Return typed DTOs or Eloquent collections — never raw arrays from Services - Services never dispatch jobs directly — that's the controller/command/listener's job ## The REST API `routes/api.php` is the SPA's backend. Three access tiers: - **Public** (no auth): `POST /api/auth/register`, `POST /api/auth/login`, `GET /api/auth/me`, `GET /api/fuel-types`, `GET /api/stats/live` - **API-key** (`VerifyApiKey` + `throttle:60,1`): `GET /api/stations`, `GET /api/stats/searches` - **Sanctum** (`auth:sanctum`): `POST /api/auth/logout`, `/api/user/*` (preferences, saved stations, profile, password, delete account) Conventions: - Shape responses with API Resources (`app/Http/Resources/Api/`), never raw arrays. - The fuel **prediction is embedded in the `/api/stations` response** under the `prediction` key — there is no standalone prediction endpoint (see `prediction.md`). - Tier-gate routes with the `feature` middleware (`RequiresFeature`); never inline entitlement checks (see `tiers.md`). - Auth is cookie/session via Sanctum SPA mode (axios sends XSRF + credentials); the API-key gate protects the expensive station search from scraping. ## Frontend The Vue SPA is documented in `.claude/rules/frontend.md`. Do **not** build new Livewire components — Livewire/Flux remain only for the starter-kit auth screens and the `Logout` action.