# Code Style

## PHP

- PHP 8.2+ features: constructor property promotion, readonly properties, enums, match expressions
- Type declarations on all method parameters and return types — no untyped signatures
- Named arguments for clarity on calls with 3+ parameters
- Enums over string constants: `FuelType::E10`, `Recommendation::Wait`, `NotificationChannel::WhatsApp`
- `final` classes for Services and Jobs — they are not designed for extension

## Laravel conventions

- Route model binding over manual `findOrFail()` in controllers
- Form Requests for any validated input
- `config()` helper over `env()` outside of config files — never call `env()` in app code
- Events + Listeners for side effects (e.g. `PricesUpdatedEvent` triggers scoring)
- Do not use `DB::transaction()` manually — wrap in Service methods where needed

## Naming

- Services: `{Noun}Service` — e.g. `AlertScoringService`
- Jobs: `{Verb}{Noun}Job` — e.g. `SendFuelAlertJob`, `PollFuelPricesJob`
- Events: `{Noun}{PastTense}Event` — e.g. `PricesUpdatedEvent`
- Livewire components: `{Context}/{Feature}` — e.g. `Dashboard/FuelRecommendation`
- Migrations: descriptive, e.g. `create_station_prices_table`, `add_whatsapp_fields_to_users_table`

## Formatting

- 4-space indentation (PSR-12)
- Max line length: 120 characters
- Run `./vendor/bin/pint` before committing (Laravel Pint, default ruleset)
- No commented-out code committed — use git to recover old code

## Security

- Never store raw passwords — Bcrypt via Laravel's Hash facade only
- Validate and sanitise all user input — postcodes, phone numbers, fuel type selections
- Phone numbers: strip non-numeric characters, enforce UK format (07xxx or +447xxx)
- OTP codes: 6 digits, expire in 10 minutes, single use only
- All Stripe webhook payloads verified via Cashier's built-in signature check

## Environment

- Local: Laravel Valet or `php artisan serve`
- Production: IONOS VPS, Nginx + PHP-FPM, MySQL, Redis
- Never commit `.env` — `.env.example` must stay up to date with all required keys
- Queue driver: `redis` in production, `sync` in testing only
