4 Commits

Author SHA1 Message Date
Ovidiu U
8fe3461adf Update legal pages with ICO registration, query logging, push notifications, and automated recommendations disclosure
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 ICO registration reference (00014395133) to privacy policy, terms of service, and refund policy
- Document search/query logging (hashed IP, location, fuel type, result count) with 24-month retention under legitimate interests
- Add push notification data section (OneSignal endpoints, encryption keys, preferences) to privacy policy
- Add new section on automated recommendations explaining fill-up timing algorithm is informational only without legal effects
- Clarify IP address collection context: security/abuse/fraud only, not individual profiling
- Update retention periods: security logs 12 months, query logs 24 months, push subscriptions until unsubscribe
- Expand data processor descriptions (OneSignal push data, Umami cookieless commitment)
- Add commercial use restrictions to terms: no scraping, mirroring, or republishing compiled data/rankings without permission
- Clarify downstream data aggregator role and upstream data feed limitations
- Add prohibition on using service while operating motor vehicle
- Remove annual billing references (monthly only)
- Add VAT status notice and downgrade-to-free-tier behavior on cancellation
- Add data controller contact details to privacy policy footer
2026-06-10 13:15:43 +01:00
Ovidiu U
ad2230728c Disable Daily and Smart pricing CTAs until features ship
Replace the basic/plus checkout links on /pricing with disabled
"Coming soon" buttons, gated by a COMING_SOON list so each tier's CTA
goes live again by removing it from that list. Free is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:15:16 +01:00
Ovidiu U
5bd909d227 Show site footer on dashboard pages and trim footer links
Render the shared SiteFooter in DashboardLayout so it appears on every
dashboard page (overview, saved stations, preferences, settings) from a
single source. Content area grows to pin the footer to the bottom on
short pages.

Trim the footer Product column to real anchors and drop the dead-link
Resources column.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 12:40:51 +01:00
Ovidiu U
a841a7c900 Add dedicated /pricing page and lock launch tiers to Free/Daily/Smart
Consolidate pricing onto a single source. Pro is deferred from launch
(left dormant: no Stripe price, no card), so the offered set is 3 tiers.

- Extract the pricing grid and footer into shared components
  (PricingGrid.vue, landing/SiteFooter.vue); add a /pricing route
  rendering Pricing.vue; remove the pricing section from Home
- Repoint every upgrade link to the /pricing route (LandingNav and
  SiteFooter via RouterLink, UpsellBanner CTA) — no more #pricing anchors
- Bump Smart (plus) SMS daily limit 1 -> 3 (PlanSeeder + PlanFactory),
  update PlanFeaturesTest assertion
- Rewrite /pricing card bullets to match real entitlements (drop
  unbuilt promises: multi-location tracking, 14-day trend, supermarket anchor)
- Fix stale "1/day" SMS references in notifications.md, tiers.md, docs/tiers.md
- Delete unused resources/views/components/pricing-card.blade.php

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 12:22:19 +01:00
18 changed files with 339 additions and 290 deletions

View File

@@ -6,7 +6,7 @@
|------------|--------|-------|------------|----------|-----------|
| Free | £0 | ✓ weekly digest | ✗ | ✗ | ✗ |
| Basic | £0.99 | ✓ daily | ✓ daily | ✓ daily | ✗ |
| Plus | £2.49 | ✓ | ✓ | ✓ | ✓ max 1/day triggered |
| Plus | £2.49 | ✓ | ✓ | ✓ | ✓ max 3/day triggered |
| Pro | £3.99 | ✓ | ✓ | ✓ | ✓ max 3/day triggered |
## NotificationDispatchService
@@ -41,8 +41,8 @@ Subject line / message copy adapts based on `recommendation` and `confidence`.
- Same Vonage client, SMS channel
- Triggered only (not daily) — fires when signal strength ≥ 2 AND price event warrants it
- Plus: max 8 SMS/month (enforced via alerts table count)
- Pro: max 30 SMS/month
- Plus (Smart): max 3 SMS/day (enforced via notification_log count)
- Pro: deferred from launch — see `tiers.md`
- Cost: ~3.5p per message UK
## Email

View File

@@ -50,7 +50,7 @@ All six are available to all tiers. The restriction is quantity only:
| email | weekly digest | daily | ✓ triggered | ✓ triggered |
| push | ✗ | ✓ daily | ✓ triggered | ✓ triggered |
| whatsapp | ✗ | ✓ daily | ✓ triggered | ✓ triggered |
| sms | ✗ | ✗ | ✓ max 1/day | ✓ max 3/day |
| sms | ✗ | ✗ | ✓ max 3/day | ✓ max 3/day |
WhatsApp also supports scheduled updates (morning + evening) independent of
price triggers — available to any tier that has WhatsApp enabled.

View File

@@ -80,7 +80,7 @@ class PlanFactory extends Factory
'whatsapp_daily_limit' => 5,
'whatsapp_scheduled_updates' => 2,
'sms_enabled' => true,
'sms_daily_limit' => 1,
'sms_daily_limit' => 3,
'ai_predictions' => true,
'price_threshold' => true,
'score_alerts' => true,

View File

@@ -57,7 +57,7 @@ class PlanSeeder extends Seeder
'whatsapp_daily_limit' => 5,
'whatsapp_scheduled_updates' => 2,
'sms_enabled' => true,
'sms_daily_limit' => 1,
'sms_daily_limit' => 3,
'ai_predictions' => true,
'price_threshold' => true,
'score_alerts' => true,

View File

@@ -12,7 +12,7 @@ decision — which channels a user can receive, how often, and what features the
|-------|--------|---------------|-----------|-----------|------------|----------------|-----------------|--------------|------------|
| free | £0 | weekly digest | — | — | — | — | — | — | 1 |
| basic | £0.99 | daily | daily | daily | — | — | ✓ | ✓ | 1 |
| plus | £2.49 | triggered | triggered | triggered | max 1/day | ✓ | ✓ | ✓ | 1 |
| plus | £2.49 | triggered | triggered | triggered | max 3/day | ✓ | ✓ | ✓ | 1 |
| pro | £3.99 | triggered | triggered | triggered | max 3/day | ✓ | ✓ | ✓ | unlimited |
Tiers are stored in the `plans` table. The `features` JSON column defines every limit and flag.

View File

@@ -0,0 +1,135 @@
<template>
<!-- Pricing -->
<section id="pricing" class="py-4 md:py-24 px-6 bg-zinc-50">
<div class="max-w-7xl mx-auto">
<div class="text-center mb-16">
<h2 class="text-4xl md:text-5xl font-black font-display text-zinc-800 mb-4">Pricing for every driver</h2>
<p class="text-zinc-500 text-lg mb-8">Save hundreds for less than the cost of a coffee.</p>
<div class="inline-flex items-center gap-1 p-1 bg-white border border-zinc-300 rounded-full">
<button
:class="cadence === 'monthly' ? 'bg-accent text-white' : 'text-zinc-500'"
class="px-5 py-2 rounded-full text-sm font-bold transition-colors"
type="button"
@click="cadence = 'monthly'"
>
Monthly
</button>
<button
:class="cadence === 'annual' ? 'bg-accent text-white' : 'text-zinc-500'"
class="px-5 py-2 rounded-full text-sm font-bold transition-colors"
type="button"
@click="cadence = 'annual'"
>
Annual <span class="text-[10px] opacity-80">(save 17%)</span>
</button>
</div>
</div>
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-6 max-w-5xl mx-auto">
<!-- Free -->
<div class="bg-white border border-zinc-300 p-8 rounded-3xl flex flex-col h-full">
<div class="mb-8">
<h4 class="text-xl font-bold font-display mb-2">Free</h4>
<div class="flex items-baseline gap-1">
<span class="text-4xl font-black">£0</span>
<span class="text-zinc-500 text-sm">/mo</span>
</div>
</div>
<ul class="space-y-4 mb-8 flex-1">
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> Local station price search</li>
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> Buy-or-wait verdict</li>
<li class="text-sm flex gap-2 text-zinc-500"><iconify-icon class="text-zinc-300" icon="lucide:x"></iconify-icon> No price alerts</li>
</ul>
<a :href="ctaHref('free')" class="w-full py-3 px-4 border border-zinc-300 rounded-xl text-center font-bold hover:bg-zinc-100 transition-colors">{{ ctaLabel('free') }}</a>
</div>
<!-- Daily (backend: basic) -->
<div class="bg-white border border-zinc-300 p-8 rounded-3xl flex flex-col h-full">
<div class="mb-8">
<h4 class="text-xl font-bold font-display mb-2">Daily</h4>
<div class="flex items-baseline gap-1">
<span class="text-4xl font-black">{{ PRICES[cadence].basic }}</span>
<span class="text-zinc-500 text-sm">{{ PRICE_SUFFIX[cadence] }}</span>
</div>
</div>
<ul class="space-y-4 mb-8 flex-1">
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> Buy-or-wait score + reason</li>
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> Daily email, push &amp; WhatsApp</li>
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> Price-drop &amp; score alerts</li>
</ul>
<a v-if="!isComingSoon('basic')" :href="ctaHref('basic')" class="w-full py-3 px-4 border border-zinc-300 rounded-xl text-center font-bold hover:bg-zinc-100 transition-colors">{{ ctaLabel('basic') }}</a>
<button v-else type="button" disabled class="w-full py-3 px-4 border border-zinc-200 bg-zinc-50 rounded-xl text-center font-bold text-zinc-400 cursor-not-allowed">Coming soon</button>
</div>
<!-- Smart (backend: plus) -->
<div class="bg-white border-2 border-accent p-8 rounded-3xl flex flex-col h-full relative">
<div class="absolute -top-4 left-1/2 -translate-x-1/2 bg-accent text-white px-4 py-1 rounded-full text-[10px] font-black uppercase tracking-widest whitespace-nowrap">Most pick this</div>
<div class="mb-8">
<h4 class="text-xl font-bold font-display mb-2">Smart</h4>
<div class="flex items-baseline gap-1">
<span class="text-4xl font-black text-accent">{{ PRICES[cadence].plus }}</span>
<span class="text-zinc-500 text-sm">{{ PRICE_SUFFIX[cadence] }}</span>
</div>
</div>
<ul class="space-y-4 mb-8 flex-1">
<li class="text-sm flex gap-2 font-bold"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> AI fuel-price prediction</li>
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> Real-time email, push &amp; WhatsApp</li>
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> SMS alerts (up to 3/day)</li>
</ul>
<a v-if="!isComingSoon('plus')" :href="ctaHref('plus')" class="w-full py-3 px-4 bg-accent text-white rounded-xl text-center font-bold shadow-lg hover:bg-primary-dark transition-all">{{ ctaLabel('plus') }}</a>
<button v-else type="button" disabled class="w-full py-3 px-4 bg-zinc-100 rounded-xl text-center font-bold text-zinc-400 cursor-not-allowed">Coming soon</button>
</div>
</div>
</div>
</section>
</template>
<script setup>
import { ref } from 'vue'
import { useAuth } from '../composables/useAuth.js'
const { isAuthenticated, userTier } = useAuth()
const cadence = ref('monthly')
const PRICES = {
monthly: { basic: '£0.99', plus: '£2.49', pro: '£3.99' },
annual: { basic: '£9.90', plus: '£24.90', pro: '£39.90' },
}
const PRICE_SUFFIX = { monthly: '/mo', annual: '/yr' }
// Paid tiers whose alerting features aren't fully shipped yet — their CTAs are
// disabled until then. Remove a tier from this list to make its button live.
const COMING_SOON = ['basic', 'plus']
function isComingSoon(tier) {
return COMING_SOON.includes(tier)
}
function ctaHref(tier) {
if (tier === 'free') {
return isAuthenticated.value ? '/dashboard' : '/register'
}
if (!isAuthenticated.value) {
return '/register?tier=' + tier + '&cadence=' + cadence.value
}
if (userTier.value === tier) {
return '/billing/portal'
}
return '/billing/checkout/' + tier + '/' + cadence.value
}
function ctaLabel(tier) {
if (tier === 'free') {
return isAuthenticated.value ? 'Go to dashboard' : 'Start free'
}
if (isAuthenticated.value && userTier.value === tier) {
return 'Manage subscription'
}
return {
basic: 'Choose Daily',
plus: 'Choose Smart',
pro: 'Choose Pro',
}[tier]
}
</script>

View File

@@ -47,6 +47,6 @@ const stationCountLabel = computed(() => {
return new Intl.NumberFormat('en-GB').format(props.stationCount)
})
const ctaHref = computed(() => isAuthenticated.value ? '#pricing' : '/register?tier=plus&cadence=monthly')
const ctaHref = computed(() => isAuthenticated.value ? '/pricing' : '/register?tier=plus&cadence=monthly')
const ctaLabel = computed(() => isAuthenticated.value ? 'See plans' : 'Start saving')
</script>

View File

@@ -11,7 +11,7 @@
<div class="hidden lg:flex items-center gap-8 font-mono text-[11px] uppercase tracking-widest text-zinc-600">
<a class="hover:text-accent transition-colors" href="#how-it-works">How it works</a>
<a class="hover:text-accent transition-colors" href="#features">Why it works</a>
<a class="hover:text-accent transition-colors" href="#pricing">Pricing</a>
<RouterLink class="hover:text-accent transition-colors" to="/pricing">Pricing</RouterLink>
</div>
<div class="flex items-center gap-3 md:gap-5">

View File

@@ -0,0 +1,55 @@
<template>
<!-- Footer -->
<footer class="bg-zinc-50 border-t border-zinc-300 pt-16 pb-8 px-6">
<div class="max-w-7xl mx-auto grid grid-cols-2 md:grid-cols-4 gap-12 mb-12">
<div class="col-span-2 md:col-span-1 space-y-4">
<RouterLink class="flex items-center gap-2" to="/">
<div class="w-8 h-8 rounded bg-accent flex items-center justify-center">
<iconify-icon class="text-white" icon="lucide:fuel"></iconify-icon>
</div>
<span class="text-xl font-black font-display tracking-tighter text-accent">FuelAlert</span>
</RouterLink>
<p class="text-sm text-zinc-500 leading-relaxed">
Helping UK drivers save money at the pump. Real-time data, smarter choices.
</p>
<p class="text-sm text-zinc-500">
Questions? <a class="text-accent hover:underline" href="mailto:hello@fuel-alert.co.uk">hello@fuel-alert.co.uk</a>
</p>
<div class="flex gap-4">
<iconify-icon class="text-2xl text-zinc-500 hover:text-accent cursor-pointer transition-colors" icon="mdi:twitter"></iconify-icon>
<iconify-icon class="text-2xl text-zinc-500 hover:text-accent cursor-pointer transition-colors" icon="mdi:facebook"></iconify-icon>
<iconify-icon class="text-2xl text-zinc-500 hover:text-accent cursor-pointer transition-colors" icon="mdi:instagram"></iconify-icon>
</div>
</div>
<div class="space-y-4">
<h5 class="font-black text-xs text-zinc-800 tracking-widest">Product</h5>
<ul class="space-y-2 text-sm text-zinc-500">
<li><RouterLink class="hover:text-accent transition-colors" to="/pricing">Pricing</RouterLink></li>
<li><a class="hover:text-accent transition-colors" href="#how-it-works">How it works</a></li>
<li><a class="hover:text-accent transition-colors" href="#features">Why it works</a></li>
</ul>
</div>
<div class="space-y-4">
<h5 class="font-black text-xs text-zinc-800 tracking-widest">Legal</h5>
<ul class="space-y-2 text-sm text-zinc-500">
<li><a class="hover:text-accent transition-colors" href="/legal/privacy">Privacy Policy</a></li>
<li><a class="hover:text-accent transition-colors" href="/legal/terms">Terms of Service</a></li>
<li><a class="hover:text-accent transition-colors" href="/legal/refund">Refund &amp; Cancellation</a></li>
<li><a class="hover:text-accent transition-colors" href="/legal/cookies">Cookie Policy</a></li>
</ul>
</div>
</div>
<div class="max-w-7xl mx-auto pt-8 border-t border-zinc-300 flex flex-col md:flex-row justify-between items-center gap-4 text-[10px] tracking-widest text-zinc-500">
<p>© 2026 FuelAlert UK. FuelAlert is a trading name of Ovidiu Ungureanu, sole trader, based in Peterborough, UK.</p>
<p>Data provided by official UK retail price transparency schemes.</p>
<p>Postcode data from <a class="underline hover:text-accent" href="https://geoportal.statistics.gov.uk/datasets/ons::onspd-online-latest-centroids-1/about" rel="noopener" target="_blank">ONS Postcode Directory</a>: contains OS data © Crown copyright &amp; database right, Royal Mail data © Royal Mail copyright &amp; database right, and National Statistics data © Crown copyright &amp; database right.</p>
</div>
</footer>
</template>
<script setup>
import { RouterLink } from 'vue-router'
</script>

View File

@@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import { useAuth } from '../composables/useAuth.js'
const Pricing = () => import('../views/Pricing.vue')
const DashboardLayout = () => import('../views/dashboard/DashboardLayout.vue')
const Overview = () => import('../views/dashboard/Overview.vue')
const SavedStations = () => import('../views/dashboard/SavedStations.vue')
@@ -13,6 +14,7 @@ const Appearance = () => import('../views/dashboard/settings/Appearance.vue')
const routes = [
{ path: '/', component: Home, name: 'home' },
{ path: '/pricing', component: Pricing, name: 'pricing' },
{
path: '/logout',
name: 'logout',

View File

@@ -225,88 +225,6 @@
</div>
</section>
<!-- Pricing -->
<section id="pricing" class="py-4 md:py-24 px-6 bg-zinc-50">
<div class="max-w-7xl mx-auto">
<div class="text-center mb-16">
<h2 class="text-4xl md:text-5xl font-black font-display text-zinc-800 mb-4">Pricing for every driver</h2>
<p class="text-zinc-500 text-lg mb-8">Save hundreds for less than the cost of a coffee.</p>
<div class="inline-flex items-center gap-1 p-1 bg-white border border-zinc-300 rounded-full">
<button
:class="cadence === 'monthly' ? 'bg-accent text-white' : 'text-zinc-500'"
class="px-5 py-2 rounded-full text-sm font-bold transition-colors"
type="button"
@click="cadence = 'monthly'"
>
Monthly
</button>
<button
:class="cadence === 'annual' ? 'bg-accent text-white' : 'text-zinc-500'"
class="px-5 py-2 rounded-full text-sm font-bold transition-colors"
type="button"
@click="cadence = 'annual'"
>
Annual <span class="text-[10px] opacity-80">(save 17%)</span>
</button>
</div>
</div>
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-6 max-w-5xl mx-auto">
<!-- Free -->
<div class="bg-white border border-zinc-300 p-8 rounded-3xl flex flex-col h-full">
<div class="mb-8">
<h4 class="text-xl font-bold font-display mb-2">Free</h4>
<div class="flex items-baseline gap-1">
<span class="text-4xl font-black">£0</span>
<span class="text-zinc-500 text-sm">/mo</span>
</div>
</div>
<ul class="space-y-4 mb-8 flex-1">
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> Basic Search</li>
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> Daily Updates</li>
<li class="text-sm flex gap-2 text-zinc-500"><iconify-icon class="text-zinc-300" icon="lucide:x"></iconify-icon> No Alerts</li>
</ul>
<a :href="ctaHref('free')" class="w-full py-3 px-4 border border-zinc-300 rounded-xl text-center font-bold hover:bg-zinc-100 transition-colors">{{ ctaLabel('free') }}</a>
</div>
<!-- Daily (backend: basic) -->
<div class="bg-white border border-zinc-300 p-8 rounded-3xl flex flex-col h-full">
<div class="mb-8">
<h4 class="text-xl font-bold font-display mb-2">Daily</h4>
<div class="flex items-baseline gap-1">
<span class="text-4xl font-black">{{ PRICES[cadence].basic }}</span>
<span class="text-zinc-500 text-sm">{{ PRICE_SUFFIX[cadence] }}</span>
</div>
</div>
<ul class="space-y-4 mb-8 flex-1">
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> Buy-or-Wait Score</li>
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> 14-day Trend Data</li>
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> 3 Daily Price Alerts</li>
</ul>
<a :href="ctaHref('basic')" class="w-full py-3 px-4 border border-zinc-300 rounded-xl text-center font-bold hover:bg-zinc-100 transition-colors">{{ ctaLabel('basic') }}</a>
</div>
<!-- Smart (backend: plus) -->
<div class="bg-white border-2 border-accent p-8 rounded-3xl flex flex-col h-full relative">
<div class="absolute -top-4 left-1/2 -translate-x-1/2 bg-accent text-white px-4 py-1 rounded-full text-[10px] font-black uppercase tracking-widest whitespace-nowrap">Most pick this</div>
<div class="mb-8">
<h4 class="text-xl font-bold font-display mb-2">Smart</h4>
<div class="flex items-baseline gap-1">
<span class="text-4xl font-black text-accent">{{ PRICES[cadence].plus }}</span>
<span class="text-zinc-500 text-sm">{{ PRICE_SUFFIX[cadence] }}</span>
</div>
</div>
<ul class="space-y-4 mb-8 flex-1">
<li class="text-sm flex gap-2 font-bold"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> Supermarket Anchor</li>
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> Priority Price Alerts</li>
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> Multi-location tracking</li>
</ul>
<a :href="ctaHref('plus')" class="w-full py-3 px-4 bg-accent text-white rounded-xl text-center font-bold shadow-lg hover:bg-primary-dark transition-all">{{ ctaLabel('plus') }}</a>
</div>
</div>
</div>
</section>
<!-- Testimonials -->
<!-- <section class="py-12 md:py-24 px-6">
<div class="max-w-7xl mx-auto">
@@ -360,72 +278,14 @@
</section>
<!-- Footer -->
<footer class="bg-zinc-50 border-t border-zinc-300 pt-16 pb-8 px-6">
<div class="max-w-7xl mx-auto grid grid-cols-2 md:grid-cols-4 gap-12 mb-12">
<div class="col-span-2 md:col-span-1 space-y-4">
<RouterLink class="flex items-center gap-2" to="/">
<div class="w-8 h-8 rounded bg-accent flex items-center justify-center">
<iconify-icon class="text-white" icon="lucide:fuel"></iconify-icon>
</div>
<span class="text-xl font-black font-display tracking-tighter text-accent">FuelAlert</span>
</RouterLink>
<p class="text-sm text-zinc-500 leading-relaxed">
Helping UK drivers save money at the pump. Real-time data, smarter choices.
</p>
<p class="text-sm text-zinc-500">
Questions? <a class="text-accent hover:underline" href="mailto:hello@fuel-alert.co.uk">hello@fuel-alert.co.uk</a>
</p>
<div class="flex gap-4">
<iconify-icon class="text-2xl text-zinc-500 hover:text-accent cursor-pointer transition-colors" icon="mdi:twitter"></iconify-icon>
<iconify-icon class="text-2xl text-zinc-500 hover:text-accent cursor-pointer transition-colors" icon="mdi:facebook"></iconify-icon>
<iconify-icon class="text-2xl text-zinc-500 hover:text-accent cursor-pointer transition-colors" icon="mdi:instagram"></iconify-icon>
</div>
</div>
<div class="space-y-4">
<h5 class="font-black text-xs text-zinc-800 tracking-widest">Product</h5>
<ul class="space-y-2 text-sm text-zinc-500">
<li><a class="hover:text-accent transition-colors" href="#pricing">Pricing</a></li>
<li><a class="hover:text-accent transition-colors" href="#features">Features</a></li>
<li><a class="hover:text-accent transition-colors" href="#">FuelAlert Pro</a></li>
<li><a class="hover:text-accent transition-colors" href="#">Enterprise API</a></li>
</ul>
</div>
<div class="space-y-4">
<h5 class="font-black text-xs text-zinc-800 tracking-widest">Resources</h5>
<ul class="space-y-2 text-sm text-zinc-500">
<li><a class="hover:text-accent transition-colors" href="#">Market Insights</a></li>
<li><a class="hover:text-accent transition-colors" href="#">How We Track</a></li>
<li><a class="hover:text-accent transition-colors" href="#">Help Center</a></li>
<li><a class="hover:text-accent transition-colors" href="#">Driver Safety</a></li>
</ul>
</div>
<div class="space-y-4">
<h5 class="font-black text-xs text-zinc-800 tracking-widest">Legal</h5>
<ul class="space-y-2 text-sm text-zinc-500">
<li><a class="hover:text-accent transition-colors" href="/legal/privacy">Privacy Policy</a></li>
<li><a class="hover:text-accent transition-colors" href="/legal/terms">Terms of Service</a></li>
<li><a class="hover:text-accent transition-colors" href="/legal/refund">Refund &amp; Cancellation</a></li>
<li><a class="hover:text-accent transition-colors" href="/legal/cookies">Cookie Policy</a></li>
</ul>
</div>
</div>
<div class="max-w-7xl mx-auto pt-8 border-t border-zinc-300 flex flex-col md:flex-row justify-between items-center gap-4 text-[10px] tracking-widest text-zinc-500">
<p>© 2026 FuelAlert UK. FuelAlert is a trading name of Ovidiu Ungureanu, sole trader, based in Peterborough, UK.</p>
<p>Data provided by official UK retail price transparency schemes.</p>
<p>Postcode data from <a class="underline hover:text-accent" href="https://geoportal.statistics.gov.uk/datasets/ons::onspd-online-latest-centroids-1/about" rel="noopener" target="_blank">ONS Postcode Directory</a>: contains OS data © Crown copyright &amp; database right, Royal Mail data © Royal Mail copyright &amp; database right, and National Statistics data © Crown copyright &amp; database right.</p>
</div>
</footer>
<SiteFooter />
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted, nextTick, defineAsyncComponent } from 'vue'
import { RouterLink, useRoute, useRouter } from 'vue-router'
import { useRoute, useRouter } from 'vue-router'
import { useAuth } from '../composables/useAuth.js'
import { useStations } from '../composables/useStations.js'
import api from '../axios.js'
@@ -441,8 +301,9 @@ import LiveTicker from '../components/landing/LiveTicker.vue'
import VerdictCard from '../components/landing/VerdictCard.vue'
import HeroSearch from '../components/landing/HeroSearch.vue'
import StatsRow from '../components/landing/StatsRow.vue'
import SiteFooter from '../components/landing/SiteFooter.vue'
const { isAuthenticated, userTier } = useAuth()
const { isAuthenticated } = useAuth()
const liveStats = ref({ stationCount: null, latestPriceAt: null })
@@ -458,40 +319,6 @@ onMounted(async () => {
}
})
const cadence = ref('monthly')
function ctaHref(tier) {
if (tier === 'free') {
return isAuthenticated.value ? '/dashboard' : '/register'
}
if (!isAuthenticated.value) {
return '/register?tier=' + tier + '&cadence=' + cadence.value
}
if (userTier.value === tier) {
return '/billing/portal'
}
return '/billing/checkout/' + tier + '/' + cadence.value
}
function ctaLabel(tier) {
if (tier === 'free') {
return isAuthenticated.value ? 'Go to dashboard' : 'Start free'
}
if (isAuthenticated.value && userTier.value === tier) {
return 'Manage subscription'
}
return {
basic: 'Choose Daily',
plus: 'Choose Smart',
pro: 'Choose Pro',
}[tier]
}
const PRICES = {
monthly: { basic: '£0.99', plus: '£2.49', pro: '£3.99' },
annual: { basic: '£9.90', plus: '£24.90', pro: '£39.90' },
}
const PRICE_SUFFIX = { monthly: '/mo', annual: '/yr' }
const { stations, meta, prediction, loading, error, search, reset } = useStations()
const showFullPrediction = computed(() => Boolean(prediction.value) && !prediction.value.tier_locked)

View File

@@ -0,0 +1,15 @@
<template>
<div class="min-h-screen bg-zinc-50">
<LandingNav />
<main class="pt-20 md:pt-24">
<PricingGrid />
</main>
<SiteFooter />
</div>
</template>
<script setup>
import LandingNav from '../components/landing/LandingNav.vue'
import PricingGrid from '../components/PricingGrid.vue'
import SiteFooter from '../components/landing/SiteFooter.vue'
</script>

View File

@@ -64,7 +64,7 @@
</div>
</nav>
<div class="flex pt-20 max-w-7xl mx-auto w-full px-6 py-8 gap-8">
<div class="flex flex-1 pt-20 max-w-7xl mx-auto w-full px-6 py-8 gap-8">
<!-- Sidebar -->
<aside class="w-56 flex-shrink-0 hidden md:block">
<nav class="space-y-1">
@@ -88,6 +88,8 @@
<RouterView />
</main>
</div>
<SiteFooter />
</div>
</template>
@@ -95,6 +97,7 @@
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { RouterLink, RouterView, useRoute, useRouter } from 'vue-router'
import { useAuth } from '../../composables/useAuth.js'
import SiteFooter from '../../components/landing/SiteFooter.vue'
const { user } = useAuth()
const route = useRoute()

View File

@@ -1,55 +0,0 @@
@props([
'name',
'price',
'buttonText',
'perks' => [],
'featured' => false,
'dark' => false,
])
@php
$cardClass = match(true) {
$dark => 'bg-primary border border-primary text-white',
$featured => 'bg-white border-2 border-primary',
default => 'bg-white border border-zinc-300',
};
$buttonClass = $dark
? 'bg-white text-primary hover:bg-zinc-100'
: 'bg-primary text-white hover:bg-primary-dark';
@endphp
<div {{ $attributes->merge(['class' => "p-8 rounded-3xl flex flex-col relative $cardClass"]) }}>
@if ($featured)
<div class="absolute -top-4 left-1/2 -translate-x-1/2 bg-primary text-white px-4 py-1 rounded-full text-[10px] font-black uppercase tracking-widest whitespace-nowrap">
Most Popular
</div>
@endif
<div class="flex items-baseline justify-between mb-6">
<h4 class="text-xl font-bold">{{ $name }}</h4>
<div class="flex items-baseline gap-1">
<span @class(['text-3xl font-black', 'text-primary' => $featured])>{{ $price }}</span>
<span @class(['text-sm', 'text-zinc-500' => !$dark, 'text-zinc-400' => $dark])>/mo</span>
</div>
</div>
<ul class="space-y-2 mb-6 flex-1">
@foreach ($perks as $perk)
<li @class(['text-sm flex gap-2 items-center', 'text-zinc-500' => !$perk['included'] && !$dark])>
@if ($perk['included'])
<iconify-icon icon="lucide:check" class="text-primary shrink-0"></iconify-icon>
@else
<iconify-icon icon="lucide:x" class="text-zinc-300 shrink-0"></iconify-icon>
@endif
{{ $perk['text'] }}
</li>
@endforeach
</ul>
<a href="{{ route('register') }}" class="w-full py-2.5 px-6 {{ $buttonClass }} rounded-full text-sm text-center font-bold shadow-lg transition-all hover:scale-105 active:scale-95">
{{ $buttonText }}
</a>
</div>

View File

@@ -13,8 +13,8 @@
is the <strong>data controller</strong> for personal data collected through this service.
</p>
<p>
As the data controller, Ovidiu Ungureanu is registering with the UK Information
Commissioner's Office (ICO). Our registration number will be published here once issued.
Ovidiu Ungureanu is registered with the UK Information Commissioner's Office (ICO) as a
data controller. <strong>ICO registration reference: 00014395133.</strong>
</p>
<p>
If you have any questions about this policy or how we handle your personal data, contact us at
@@ -31,8 +31,8 @@
<h3 class="font-semibold text-zinc-900">Contact data for alerts</h3>
<p>
If you opt in to WhatsApp or SMS alerts, your mobile phone number. We collect it only to
send the alerts you have requested, and only after you confirm the number through an
opt-in step.
send the alerts you have requested, and only after you verify the number through a
one-time passcode (OTP) sent to that number.
</p>
<h3 class="font-semibold text-zinc-900">Location data</h3>
@@ -64,6 +64,15 @@
</li>
</ul>
<h3 class="font-semibold text-zinc-900">Search and query logs</h3>
<p>
When you search for stations or prices, we log the approximate search location, fuel
type selected, result count, timestamp, a one-way hashed IP address, and basic device
information (browser type, device type). We use these logs for abuse prevention,
troubleshooting, and aggregate service statistics. We do not use them to build a profile
of your individual behaviour. Logs are retained for a maximum of 24 months.
</p>
<h3 class="font-semibold text-zinc-900">Payment data</h3>
<p>
Payment card details are collected and processed by <strong>Stripe</strong>, our payment
@@ -72,16 +81,25 @@
renewal date).
</p>
<h3 class="font-semibold text-zinc-900">Push notification data</h3>
<p>
If you opt in to push notifications via OneSignal, we store your push subscription
endpoint (a browser-specific URL), the encryption keys needed for secure message
delivery, and your notification preferences. This data is retained until you unsubscribe,
revoke browser permission, or your subscription becomes stale.
</p>
<h3 class="font-semibold text-zinc-900">Usage data</h3>
<p>
Features you use, queries you make, and alerts you configure used to deliver the
service and improve it.
Features you use and alerts you configure used to deliver the service and improve it.
</p>
<h3 class="font-semibold text-zinc-900">Technical data</h3>
<p>
IP address, browser type and version, device type, and operating system used for
security, fraud prevention, and basic analytics.
IP address, browser type and version, device type, and operating system. IP address is
collected alongside account actions and searches for security, abuse prevention, and
fraud detection (lawful basis: legitimate interests, Art. 6(1)(f)). We do not use IP
addresses to identify you as an individual in any other context.
</p>
<h3 class="font-semibold text-zinc-900">Marketing preferences</h3>
@@ -97,7 +115,8 @@
<li><strong>Finding stations near you on request (device location)</strong> &mdash; consent (Art. 6(1)(a)), given through your browser's location permission and withdrawable at any time.</li>
<li><strong>Storing your saved location as a registered user</strong> &mdash; contract (Art. 6(1)(b)).</li>
<li><strong>Payment processing</strong> &mdash; contract (Art. 6(1)(b)).</li>
<li><strong>Security and fraud prevention</strong> &mdash; legitimate interests (Art. 6(1)(f)).</li>
<li><strong>Security, abuse prevention, and fraud detection (including IP address logging)</strong> &mdash; legitimate interests (Art. 6(1)(f)).</li>
<li><strong>Search and query logging for aggregate statistics and troubleshooting</strong> &mdash; legitimate interests (Art. 6(1)(f)).</li>
<li><strong>Aggregated, non-identifying analytics and product improvement</strong> &mdash; legitimate interests (Art. 6(1)(f)).</li>
<li><strong>Marketing emails</strong> &mdash; consent (Art. 6(1)(a)). You can withdraw consent at any time.</li>
</ul>
@@ -118,7 +137,19 @@
</section>
<section class="space-y-3">
<h2 class="font-display text-2xl font-bold text-zinc-900">5. Who we share data with</h2>
<h2 class="font-display text-2xl font-bold text-zinc-900">5. Automated recommendations</h2>
<p>
FuelAlert generates fill-up timing recommendations (for example, "fill up now" or "wait")
using an algorithm that analyses local price trends, historical patterns, and market
signals. These recommendations are <strong>informational only</strong> and are produced
automatically without human review. They do not have legal or similarly significant
effects on you, and we do not use them to make decisions that affect your rights or
interests in any material way.
</p>
</section>
<section class="space-y-3">
<h2 class="font-display text-2xl font-bold text-zinc-900">6. Who we share data with</h2>
<p>We use the following processors to deliver the service. We do not sell your data to anyone.</p>
<ul class="list-disc space-y-2 pl-6">
<li>
@@ -131,15 +162,17 @@
<strong>Umami Analytics</strong> &mdash; we run our own self-hosted Umami instance to
collect aggregated, cookieless usage metrics (pages viewed, referrer, country, device
type). It does not store data that identifies you as an individual, and no analytics
data is shared with third parties.
data is shared with third parties. We periodically review our analytics setup to
confirm it remains cookieless; if this changes we will update our Cookie Policy and
request consent before setting any non-essential cookies.
</li>
<li><strong>Vonage</strong> &mdash; delivers WhatsApp and SMS alerts if you opt in to those channels. Your phone number is shared only to send messages you have requested. See <a class="text-accent underline" href="https://www.vonage.co.uk/legal/privacy-policy/" target="_blank" rel="noopener">Vonage's privacy policy</a>.</li>
<li><strong>OneSignal</strong> &mdash; delivers web push notifications if you opt in to push alerts. See <a class="text-accent underline" href="https://onesignal.com/privacy_policy" target="_blank" rel="noopener">OneSignal's privacy policy</a>.</li>
<li><strong>OneSignal</strong> &mdash; delivers web push notifications if you opt in to push alerts. Push subscription data (endpoint, encryption keys, device type) is processed by OneSignal on our behalf. See <a class="text-accent underline" href="https://onesignal.com/privacy_policy" target="_blank" rel="noopener">OneSignal's privacy policy</a>.</li>
</ul>
</section>
<section class="space-y-3">
<h2 class="font-display text-2xl font-bold text-zinc-900">6. International transfers</h2>
<h2 class="font-display text-2xl font-bold text-zinc-900">7. International transfers</h2>
<p>
Some of our processors &mdash; including Stripe, Vonage and OneSignal &mdash; operate
outside the UK and EEA, including in the United States. Where personal data is transferred
@@ -150,18 +183,22 @@
</section>
<section class="space-y-3">
<h2 class="font-display text-2xl font-bold text-zinc-900">7. How long we keep data</h2>
<h2 class="font-display text-2xl font-bold text-zinc-900">8. How long we keep data</h2>
<ul class="list-disc space-y-1 pl-6">
<li><strong>Active account data:</strong> for as long as your account is open, plus 12 months after closure.</li>
<li><strong>Saved location (registered users):</strong> while your account is active; deleted when you delete your account.</li>
<li><strong>Alert and notification preferences:</strong> while your account is active; deleted when you close your account or remove the preference.</li>
<li><strong>Push notification subscriptions:</strong> until you unsubscribe, revoke browser permission, or the subscription becomes stale.</li>
<li><strong>Payment records:</strong> 6 years, to meet HMRC requirements for self-employed traders.</li>
<li><strong>Marketing data:</strong> until you withdraw consent.</li>
<li><strong>Logs and analytics:</strong> a maximum of 24 months.</li>
<li><strong>Security and fraud logs (including IP records):</strong> a maximum of 12 months.</li>
<li><strong>Search and query logs:</strong> a maximum of 24 months.</li>
<li><strong>Aggregated analytics:</strong> retained indefinitely in anonymised, non-identifiable form only.</li>
</ul>
</section>
<section class="space-y-3">
<h2 class="font-display text-2xl font-bold text-zinc-900">8. Your rights under UK GDPR</h2>
<h2 class="font-display text-2xl font-bold text-zinc-900">9. Your rights under UK GDPR</h2>
<p>You have the following rights in relation to your personal data:</p>
<ul class="list-disc space-y-1 pl-6">
<li><strong>Right of access</strong> &mdash; ask for a copy of the data we hold about you.</li>
@@ -170,7 +207,7 @@
<li><strong>Right to restrict processing</strong> &mdash; ask us to pause processing in certain circumstances.</li>
<li><strong>Right to data portability</strong> &mdash; receive your data in a machine-readable format.</li>
<li><strong>Right to object</strong> &mdash; object to processing based on legitimate interests.</li>
<li><strong>Rights related to automated decision-making</strong> &mdash; we do <strong>not</strong> make solely automated decisions with legal or similarly significant effects on you.</li>
<li><strong>Rights related to automated decision-making</strong> &mdash; our fill-up timing recommendations are generated algorithmically but are informational only and do not have legal or similarly significant effects on you.</li>
<li><strong>Right to withdraw consent</strong> &mdash; where we rely on consent (for example, device location or marketing).</li>
</ul>
<p>
@@ -181,7 +218,7 @@
</section>
<section class="space-y-3">
<h2 class="font-display text-2xl font-bold text-zinc-900">9. Cookies</h2>
<h2 class="font-display text-2xl font-bold text-zinc-900">10. Cookies</h2>
<p>
We use only a small number of essential cookies to operate the service, and self-hosted,
cookieless analytics. Full details are in our
@@ -190,7 +227,7 @@
</section>
<section class="space-y-3">
<h2 class="font-display text-2xl font-bold text-zinc-900">10. Security</h2>
<h2 class="font-display text-2xl font-bold text-zinc-900">11. Security</h2>
<p>
All traffic between your device and our service is encrypted with HTTPS. Passwords are
stored as one-way hashes &mdash; we never see your plaintext password. Sensitive fields in
@@ -201,7 +238,7 @@
</section>
<section class="space-y-3">
<h2 class="font-display text-2xl font-bold text-zinc-900">11. Children</h2>
<h2 class="font-display text-2xl font-bold text-zinc-900">12. Children</h2>
<p>
FuelAlert is not directed at children. We do not knowingly collect data from anyone under
16. If you believe a child has provided us with personal data, contact us and we will
@@ -210,7 +247,7 @@
</section>
<section class="space-y-3">
<h2 class="font-display text-2xl font-bold text-zinc-900">12. Complaints</h2>
<h2 class="font-display text-2xl font-bold text-zinc-900">13. Complaints</h2>
<p>
We hope you'll contact us first if you have a complaint, so we can try to put it right.
You also have the right to lodge a complaint with the UK Information Commissioner's Office
@@ -223,7 +260,7 @@
</section>
<section class="space-y-3">
<h2 class="font-display text-2xl font-bold text-zinc-900">13. Changes to this policy</h2>
<h2 class="font-display text-2xl font-bold text-zinc-900">14. Changes to this policy</h2>
<p>
We may update this policy from time to time. If we make material changes we will notify
registered users by email. Non-material changes will be shown by an updated "Last updated"
@@ -232,10 +269,14 @@
</section>
<section class="space-y-3">
<h2 class="font-display text-2xl font-bold text-zinc-900">14. Contact</h2>
<h2 class="font-display text-2xl font-bold text-zinc-900">15. Contact</h2>
<p>
For any privacy queries, email
For any privacy or data protection queries, email
<a href="mailto:hello@fuel-alert.co.uk" class="text-accent underline">hello@fuel-alert.co.uk</a>.
</p>
<p class="text-sm text-zinc-600">
Data controller: Ovidiu Ungureanu trading as FuelAlert, Peterborough, United Kingdom.
ICO registration reference: 00014395133.
</p>
</section>
</x-layouts.legal>

View File

@@ -1,7 +1,7 @@
{{-- DRAFT: Generated {{ date('Y-m-d') }}. Review by UK-qualified solicitor recommended before launch. --}}
<x-layouts.legal
title="Refund & Cancellation Policy"
heading="Refund &amp; Cancellation Policy"
heading="Refund & Cancellation Policy"
lastUpdated="{{ now()->format('j F Y') }}"
metaDescription="Your right to cancel a FuelAlert subscription, including the 14-day cooling-off period under UK law.">

View File

@@ -1,9 +1,9 @@
{{-- DRAFT: Generated {{ date('Y-m-d') }}. Review by UK-qualified solicitor recommended before launch. --}}
<x-layouts.legal
title="Terms of Service"
heading="Terms of Service"
lastUpdated="{{ now()->format('j F Y') }}"
metaDescription="The terms that govern your use of FuelAlert's subscription service.">
title="Terms of Service"
heading="Terms of Service"
lastUpdated="{{ now()->format('j F Y') }}"
metaDescription="The terms that govern your use of FuelAlert's subscription service.">
<section class="space-y-3">
<h2 class="font-display text-2xl font-bold text-zinc-900">1. About these terms</h2>
@@ -11,6 +11,7 @@
FuelAlert is a trading name of <strong>Ovidiu Ungureanu</strong>, a sole trader based in
Peterborough, United Kingdom ("we", "us", "our"). These terms form a legally binding
contract between you and Ovidiu Ungureanu trading as FuelAlert.
ICO registration reference: 00014395133.
</p>
<p>
By creating an account or using the service, you confirm that you have read, understood
@@ -22,8 +23,12 @@
<section class="space-y-3">
<h2 class="font-display text-2xl font-bold text-zinc-900">2. The service</h2>
<p>
FuelAlert provides UK fuel price comparison and forecasting. We aggregate publicly
available UK fuel price data and surface it through a web app, alerts and forecasts.
FuelAlert provides UK fuel price comparison and fill-up timing recommendations. We act
as a downstream consumer of publicly available UK government fuel price data feeds
(including the UK Fuel Finder / Pump Watch transparency scheme) and surface that data
through a web app, alerts, and forecasts. We do not control the prices submitted by fuel
retailers to those upstream schemes and are not responsible for errors or delays in
that data.
</p>
<p>
We offer a free tier and one or more paid subscription plans. The current list of plans
@@ -49,8 +54,8 @@
<section class="space-y-3">
<h2 class="font-display text-2xl font-bold text-zinc-900">4. Subscriptions, billing and payment</h2>
<p>
Paid plans are billed in advance, either monthly or annually. The current price for each
plan is shown on the <a class="text-accent underline" href="/#pricing">pricing page</a> at the time you subscribe.
Paid plans are billed monthly in advance. The current price for each plan is shown on
the <a class="text-accent underline" href="/#pricing">pricing page</a> at the time you subscribe.
</p>
<p>
<strong>Auto-renewal.</strong> Subscriptions renew automatically at the end of each
@@ -71,7 +76,13 @@
<p>
<strong>VAT.</strong> FuelAlert is currently below the UK VAT registration threshold and is
not VAT-registered, so no VAT is charged on your subscription. The price shown is the total
amount you pay.
amount you pay. If our VAT status changes, we will update these terms and notify you before
any price change takes effect.
</p>
<p>
<strong>Downgrade on cancellation.</strong> When a paid subscription ends or is cancelled,
your account reverts to the free tier. Paid alert channels (WhatsApp, SMS) are deactivated,
but your alert settings are retained and will reactivate if you resubscribe.
</p>
</section>
@@ -94,11 +105,14 @@
<h2 class="font-display text-2xl font-bold text-zinc-900">6. Acceptable use</h2>
<p>You agree not to:</p>
<ul class="list-disc space-y-1 pl-6">
<li>Scrape, reverse-engineer, or bulk-extract data from the service.</li>
<li>Scrape, reverse-engineer, or bulk-extract data from the service without our written permission.</li>
<li>Mirror, republish, or systematically reproduce our compiled price data, station rankings, scoring outputs, or any other value-added data derived from the service.</li>
<li>Use the service or its outputs for commercial exploitation, resale, or competitor monitoring without our written consent.</li>
<li>Resell or redistribute fuel price data taken from FuelAlert.</li>
<li>Use the service for any unlawful purpose.</li>
<li>Attempt to circumvent or compromise our security measures.</li>
<li>Use automated tools to make queries beyond what a single human user would reasonably make.</li>
<li>Use the service while operating a motor vehicle. You must not interact with the service while a vehicle is in motion. Compliance with the Road Traffic Act 1988, the Highway Code, and all applicable road traffic laws is your sole responsibility.</li>
</ul>
</section>
@@ -107,12 +121,14 @@
<p>
Fuel prices shown on FuelAlert are sourced from official UK government data feeds
(including the Pump Watch / Fuel Finder transparency schemes) and refreshed
periodically. Stations can change prices at any time, and there is usually a delay
between a forecourt change and the feed update.
periodically. FuelAlert acts as a downstream consumer of those feeds and does not
control the data submitted by fuel retailers to the central aggregator. Stations can
change prices at any time, and there is usually a delay between a forecourt change
and the feed update.
</p>
<p>
We make reasonable efforts to display accurate prices but <strong>we cannot guarantee
that the price shown will match the price at the pump</strong> when you arrive.
that the price shown will match the price at the pump</strong> when you arrive.
<strong>Always confirm the price at the pump before fuelling.</strong>
</p>
<p>
@@ -123,21 +139,22 @@
</section>
<section class="space-y-3">
<h2 class="font-display text-2xl font-bold text-zinc-900">8. Forecasts and predictions</h2>
<h2 class="font-display text-2xl font-bold text-zinc-900">8. Forecasts and recommendations</h2>
<p>
FuelAlert may show forecasts and recommendations (e.g. "fill up now" or "wait"). These
are <strong>informational only</strong>, are not financial advice, and should not be
relied upon as a guarantee of future prices. Past trends do not guarantee future prices.
We do not warrant the accuracy of any forecast.
are generated algorithmically based on local price trends, historical patterns, and
market signals. They are <strong>informational only</strong>, are not financial advice,
and should not be relied upon as a guarantee of future prices. Past trends do not
guarantee future prices. We do not warrant the accuracy of any forecast or recommendation.
</p>
</section>
<section class="space-y-3">
<h2 class="font-display text-2xl font-bold text-zinc-900">9. Intellectual property</h2>
<p>
The FuelAlert name, logo, software, and original content are owned by Ovidiu Ungureanu.
You receive a limited, non-exclusive, revocable licence to use the service for personal,
non-commercial purposes.
The FuelAlert name, logo, software, scoring algorithms, and original content are owned
by Ovidiu Ungureanu. You receive a limited, non-exclusive, revocable licence to use the
service for personal, non-commercial purposes.
</p>
<p>
Underlying fuel price data is owned by the respective fuel retailers and published under
@@ -176,6 +193,11 @@
including but not limited to outages or errors at our payment processor, hosting
provider, or upstream data sources.
</li>
<li>
We are not responsible for the accuracy, completeness, or timeliness of data
submitted by fuel retailers to the UK Fuel Finder scheme or any other upstream
source we consume as a downstream aggregator.
</li>
</ul>
</section>
@@ -218,5 +240,9 @@
For questions about these terms, email
<a href="mailto:hello@fuel-alert.co.uk" class="text-accent underline">hello@fuel-alert.co.uk</a>.
</p>
<p class="text-sm text-zinc-600">
Ovidiu Ungureanu trading as FuelAlert, Peterborough, United Kingdom.
ICO registration reference: 00014395133.
</p>
</section>
</x-layouts.legal>
</x-layouts.legal>

View File

@@ -53,7 +53,7 @@ it('canSendNow returns false when tier does not allow the channel', function ():
});
it('canSendNow returns false when daily limit is reached', function (): void {
$plan = Plan::where('name', 'plus')->first(); // sms_daily_limit = 1
$plan = Plan::where('name', 'plus')->first(); // sms_daily_limit = 3
$user = User::factory()->create();
UserNotificationPreference::factory()->create([
@@ -70,7 +70,7 @@ it('canSendNow returns false when daily limit is reached', function (): void {
'created_at' => now(),
]);
expect($plan->sms_daily_limit)->toBe(1);
expect($plan->sms_daily_limit)->toBe(3);
$sentCount = NotificationLog::where('user_id', $user->id)
->where('channel', 'sms')