Commit Graph

55 Commits

Author SHA1 Message Date
Ovidiu U
73de53994f fix: prevent sensitive field leaks in /me, add retry logic to Brent price sources
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled
- Made `/api/auth/me` public and return explicit allowlist (name, email,
  two_factor_confirmed_at, tier, subscription fields) instead of spreading
  `$user->toArray()` which leaked is_admin, stripe_id, pm_type, pm_last_four,
  postcode. Returns `null` when unauthenticated rather than 401.
- Moved `/auth/logout` to remain behind auth:sanctum gate.
- Added 3×200ms retry with exponential backoff to EiaBrentPriceSource and
  FredBrentPriceSource on ConnectionException or 5xx responses. Timeout
  raised from 10s to 30s.
- Both sources now throw typed BrentPriceFetchException on exhausted retries
  instead of silently returning null + logging. Updated tests to assert
  exception message includes HTTP status or "connection failed".
2026-05-01 13:22:36 +01:00
Ovidiu U
5369b4a5a0 feat: build FuelPriceAlert notification with multi-channel adapters
The DispatchUserNotificationJob has been logging sent=true for every
allowed channel without actually sending anything — a TODO marker covered
by the existing test suite, which only asserted log rows. The downstream
"missed today" widget read those rows and reported falsely. This commit
makes the telemetry truthful by wiring the real send.

- App\Notifications\FuelPriceAlert — Notification class with via() that
  returns the per-tier-filtered channel list passed in by the dispatcher.
  Implements toMail / toOneSignal / toVonageWhatsApp / toVonageSms.
  ShouldQueue on the 'notifications' queue.
- App\Notifications\Channels\OneSignalChannel — raw HTTP to OneSignal
  REST API, gated on services.onesignal.{app_id,api_key} + user
  push_token. Logs every call to api_logs via ApiLogger.
- App\Notifications\Channels\VonageWhatsAppChannel — raw HTTP to Vonage
  Messages API, gated on whatsapp_verified_at + whatsapp_number.
- App\Notifications\Channels\VonageSmsChannel — raw HTTP to Vonage SMS
  API, gated on whatsapp_number.
- DispatchUserNotificationJob now calls $user->notify(new
  FuelPriceAlert(...)) before logging.
- New tests: assert the notification IS dispatched with the right
  channels, and that nothing is dispatched when no channels are allowed.

Channels gracefully no-op when their credentials are unset (logging at
info level), so existing tests without a Notification::fake() still
pass — the channels just early-return on missing config.

No new composer dependencies — Vonage SDK avoided in favour of raw HTTP
through the existing ApiLogger pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 19:48:10 +01:00
Ovidiu U
48af2083f3 feat: add fuel:archive command and monthly scheduler entry
Completes Tasks 12 + 13 from docs/superpowers/plans/2026-04-03-fuel-api-ingestion.md.

- ArchiveOldPricesCommand moves station_prices rows older than 12 months
  into station_prices_archive in chunks of 1000, wrapping each chunk's
  insert + delete in a DB::transaction so a partial failure can't
  duplicate rows.
- StationPriceArchive: add $table = 'station_prices_archive'; without it
  Eloquent infers 'station_price_archives' and the insert would fail.
- routes/console.php: register fuel:archive monthly on the 1st at 04:00
  UTC, alongside the other fuel/oil scheduler entries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 18:33:05 +01:00
Ovidiu U
8695d5ec95 refactor: flatten plans.features JSON to typed columns
The features JSON column required defensive fallback stubs in three
places (Plan::resolveForUser, PlanFeatures::__construct, PlanSeeder)
and silently swallowed misspelled keys. Typed columns give Eloquent
type-safe reads, simplify the Filament form (no more dotted JSON
paths), and let resolveForUser fail loud when the free row is
missing.

PlanFeatures public API is unchanged so consumers (jobs, middleware)
need no rewrites — one missed JSON read in SendScheduledWhatsAppJob
was caught and converted to a typed where() query.

tests/Pest.php seeds PlanSeeder in beforeEach so any feature test
that resolves a plan finds the free row, mirroring production where
plans always exist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 18:13:26 +01:00
Ovidiu U
088fd11058 Remove prediction API endpoint and integrate into stations search
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled
Consolidate prediction functionality by merging /api/prediction endpoint into /api/stations response. Move prediction logic from PredictionController into StationController, returning prediction data alongside station results. Replace usePrediction composable with unified useStations that returns {stations, meta, prediction}. Remove PredictionRequest, related tests, and unused Vue components (FuelFinderTest, MapTest, RecommendationTest, StationListTest). Add PredictionFull component and UpsellBanner. Extend NationalFuelPredictionService to include weekly_summary (7-day series, yesterday/today averages, cheapest/priciest days) and oil signal from price_predictions table. Update Home.vue to consume prediction from stations response. Add Plan::resolveCadenceForUser helper and configure Cashier to use custom Subscription model.
2026-04-29 13:28:33 +01:00
Ovidiu U
ee6de23709 feat: gate full prediction by ai_predictions feature flag
Add a prediction box above filter results on the homepage.
Server returns the full payload only when PlanFeatures::can(
'ai_predictions') — currently plus and pro. Other tiers and
guests get a trimmed {fuel_type, predicted_direction,
tier_locked: true} response so the gate is enforced server-side.

Frontend renders a compact one-liner with the national trend
direction for trimmed responses, full card for unlocked.

Hide the Pro plan card from the pricing section (pro plan
disabled in DB pending real Stripe price ids), and only show
the bottom signup CTA when the visitor is a guest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 09:29:29 +01:00
Ovidiu U
b7175169f0 feat: handle invoice.payment_failed — set grace period and queue reminders
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 10:56:26 +01:00
Ovidiu U
5b17f4cae4 feat: add SendPaymentFailedReminderJob with grace guard 2026-04-23 10:51:13 +01:00
Ovidiu U
2078c4b83e feat: clear grace period on invoice.payment_succeeded
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 10:37:50 +01:00
Ovidiu U
b9d457578c feat: fold subscription deletion handling into HandleStripeWebhook 2026-04-23 10:34:05 +01:00
Ovidiu U
25b79f095b feat: bust plan cache on customer.subscription.updated 2026-04-23 10:31:07 +01:00
Ovidiu U
a39d4b1b94 feat: consolidate stripe webhook handling into HandleStripeWebhook listener 2026-04-23 10:27:23 +01:00
Ovidiu U
19fc61a0a3 feat: accept ArcGIS ONSPD column aliases (PCD7/PCD8/PCDS) in postcodes:import 2026-04-22 13:31:27 +01:00
Ovidiu U
3ec7cda790 feat: derive outcode centroids from postcodes during import 2026-04-22 12:36:39 +01:00
Ovidiu U
d01a634f0b test: cover terminated + blank-coord skip paths for postcodes:import 2026-04-22 12:34:19 +01:00
Ovidiu U
9ad62538b9 fix: harden postcodes:import against duplicate headers and test collisions 2026-04-22 12:33:10 +01:00
Ovidiu U
4a60298606 feat: add postcodes:import command for loading ONSPD CSV 2026-04-22 12:28:08 +01:00
Ovidiu U
c2466e5a61 feat(tiers): add display-name layer, push.frequency entitlement, and rename pricing cards
Reconciles tier docs with `PlanSeeder` reality (basic has price_threshold
and score_alerts; schema is stripe_price_id_monthly + stripe_price_id_annual)
and introduces the display-name layer from pricing-plan.md v2.

- PlanTier::label() + Plan::displayName() + PlanFeatures::displayName()
  expose user-facing names (Free/Daily/Smart/Pro); backend identifiers stay
  basic/plus/pro so every call site, Stripe mapping, and test keeps working.
- push.frequency key added to features JSON (none/daily/triggered), mirroring
  email.frequency so Daily's daily push is distinguishable from Smart/Pro's
  triggered push. Seeder, factory, free-tier stubs, and Filament form updated.
- Homepage pricing cards renamed: Basic→Daily, Plus→Smart; badge
  "Most Popular"→"Most pick this"; CTAs refreshed.
- docs/tiers.md change log records the full diff.

Fleet tier, 14-day trial copy, and Smart dark-card treatment deferred.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:57:24 +01:00
Ovidiu U
d29f3e6487 Add tier feature design spec, annual billing, fuel type normalization, and admin subscription management
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled
- Add comprehensive tier feature matrix spec defining Free/Basic/Plus/Pro capabilities across recommendations, predictions, history, logs, tools, and family sharing
- Add `stripe_price_id_annual` column to plans table and rename existing column to `stripe_price_id_monthly`
- Normalize legacy fuel type aliases (petrol→e10, diesel→b7_standard) in users table
- Implement BillingController with checkout, portal, success/cancel routes supporting monthly/annual cadence
- Add admin subscription assignment in Filament user edit page with admin-granted subscription support
- Add DowngradeUserOnSubscriptionDeleted listener to disable WhatsApp/SMS preferences on subscription cancellation
- Add MissedNotificationsOverview widget to Filament user detail page
- Add PollFuelPricesTest covering auto-refresh scenarios
- Add PriceReliability enum with reliability classification based on price age
- Add fuelTypes.js constants file exporting FUEL_TYPES from window global
2026-04-20 14:13:03 +01:00
Ovidiu U
5acb99c9e3 Remove obsolete Livewire fuel search components and consolidate pricing tiers
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled
- Delete unused Livewire Search test and fuel type select Blade component
- Move subscription webhook listener from EventServiceProvider to AppServiceProvider
- Add FUEL_TYPES global config to app layout for client-side use
- Add Billable trait to User model and include email_verified_at in fillable
- Implement monthly/annual cadence toggle with pricing display and smart CTA routing on homepage
- Update VerifyApiKeyMiddlewareTest to use e10 instead of petrol
- Refactor PollFuelPrices to auto-refresh stale stations based on last_seen_at
- Add incremental polling with cached timestamp and effective-start-timestamp param to FuelPriceService
- Normalize amenities/fuel_types from API objects to flat arrays, skip stations missing required fields
- Log response body on API failures in ApiLogger
- Default homepage sort to 'reliable' instead of 'price'
2026-04-20 14:12:15 +01:00
Ovidiu U
4220b1b86a Add subscription tiers, notification preferences, and logging infrastructure
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled
- Add database migrations for plans, subscriptions, notification preferences, and notification log tables
- Implement DispatchUserNotificationJob to handle channel resolution, daily limits, and logging (sent/tier_restricted/daily_limit)
- Add SendScheduledWhatsAppJob for scheduled notification delivery
- Create PlanFeatures service to resolve tier capabilities, check daily limits, and validate fuel
2026-04-14 16:20:51 +01:00
Ovidiu U
ea7a5b4f10 chore: remove Livewire settings pages — migrated to Vue SPA
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 13:14:34 +01:00
Ovidiu U
e90078d39e feat: add updateProfile, updatePassword, deleteAccount API endpoints
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 13:02:23 +01:00
Ovidiu U
580f9c6929 feat: add user preferences and saved stations API endpoints
Adds authenticated endpoints for reading/updating fuel type preferences and managing saved stations, backed by new migrations and a SavedStation model.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 18:06:31 +01:00
Ovidiu U
05b5d1f3b3 feat: add SPA Blade shell and catch-all route
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 17:57:54 +01:00
Ovidiu U
acaa791eda feat: allow Sanctum-authenticated sessions through VerifyApiKey middleware
Enables stateful API via Sanctum so the Vue SPA can call /api/* routes
using cookie auth, without requiring an X-Api-Key header.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 17:56:14 +01:00
Ovidiu U
7b6aaac661 chore: remove StationSearch, dead Volt SFCs, mobile prototype, and fix homepage CTAs 2026-04-08 09:24:40 +01:00
Ovidiu U
c935903614 feat: strip FuelFinder to layout shell, wire sub-components 2026-04-08 08:48:15 +01:00
Ovidiu U
bce5aa72c8 feat: extract fuel.map component and wire Leaflet to map-update browser event 2026-04-08 08:46:33 +01:00
Ovidiu U
0809a5340b feat: extract fuel.recommendation Livewire component 2026-04-08 08:46:16 +01:00
Ovidiu U
c4f5fd042b fix: remove dead search property, simplify empty state message 2026-04-07 22:04:15 +01:00
Ovidiu U
a576ef6b4a feat: extract fuel.station-list Livewire component
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 22:02:08 +01:00
Ovidiu U
0b289c8ec2 feat: extract fuel.search Livewire component with stations-found dispatch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 21:58:01 +01:00
Ovidiu U
6a80c11f38 feat: add LLM prediction providers with structured output support
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 14:42:44 +01:00
Ovidiu U
80ae25d98a test: add failing FuelFinder component tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 14:31:00 +01:00
Ovidiu U
4f695ca37c test: add lat/lng to faked meta fixture in StationSearchTest 2026-04-06 09:46:17 +01:00
Ovidiu U
21aea84797 test: add postcode path assertion for meta lat/lng 2026-04-06 09:39:28 +01:00
Ovidiu U
cef21a4f0f feat: include search lat/lng in station API meta response 2026-04-06 09:37:39 +01:00
Ovidiu U
fa94bb537b feat: add sort option to station search form 2026-04-05 20:48:00 +01:00
Ovidiu U
649772f65f fix: add X-Api-Key header to API feature tests and register auth routes 2026-04-05 20:32:20 +01:00
Ovidiu U
1318e3ac3b test: fix quality issues in StationSearchTest 2026-04-05 20:27:17 +01:00
Ovidiu U
55cd68fbaa test: add failing tests for StationSearch Livewire component 2026-04-05 20:24:20 +01:00
Ovidiu U
7101ed3550 feat: add postcode resolution to /api/stations and Filament SearchResource
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled
Extends NearbyStationsRequest to accept `postcode` (full or outcode) as an alternative to lat/lng. PostcodeService resolves it via postcodes.io and falls through to coordinates. Also adds SearchResource to the Filament admin panel for viewing logged search activity with fuel type filter and price/distance stats columns. Includes SQLite GREATEST/LEAST function polyfills in AppServiceProvider for test compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 19:10:25 +01:00
Ovidiu U
0bea50b843 feat: add GET /api/stats/searches endpoint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 19:28:41 +01:00
Ovidiu U
8bd43ee9e4 feat: add GET /api/stations nearby stations endpoint with haversine query and search logging
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 19:27:55 +01:00
Ovidiu U
cf6a1369d4 feat: add GET /api/prediction endpoint
Implements PredictionRequest (fuel_type validation with ValueError→ValidationException), PredictionController delegating to NationalFuelPredictionService, and 5 feature tests. Also fixes LEAST() MySQL-only function to a CASE WHEN expression for SQLite test compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 19:26:48 +01:00
Ovidiu U
1d2eb12e83 feat: add StatsOverviewWidget to admin dashboard
Four-stat overview widget (users, stations, oil prediction, API errors)
with 30s polling registered on the admin panel dashboard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 14:31:02 +01:00
Ovidiu U
52dc225b3d feat: add StationResource with poll action and view page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 14:27:22 +01:00
Ovidiu U
b2cc3ee0ff feat: add BrentPriceResource with 30-day line chart widget
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 14:23:57 +01:00
Ovidiu U
d936175090 feat: add OilPredictionResource with run-prediction header action
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 14:20:51 +01:00