diff --git a/.claude/rules/notifications.md b/.claude/rules/notifications.md index 2ed93c9..1a05486 100644 --- a/.claude/rules/notifications.md +++ b/.claude/rules/notifications.md @@ -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 diff --git a/.claude/rules/tiers.md b/.claude/rules/tiers.md index 9b8511d..701844d 100644 --- a/.claude/rules/tiers.md +++ b/.claude/rules/tiers.md @@ -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. diff --git a/database/factories/PlanFactory.php b/database/factories/PlanFactory.php index d771cd5..222053e 100644 --- a/database/factories/PlanFactory.php +++ b/database/factories/PlanFactory.php @@ -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, diff --git a/database/seeders/PlanSeeder.php b/database/seeders/PlanSeeder.php index 7e50ae4..beacbf9 100644 --- a/database/seeders/PlanSeeder.php +++ b/database/seeders/PlanSeeder.php @@ -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, diff --git a/docs/tiers.md b/docs/tiers.md index cc66f31..f2d39c1 100644 --- a/docs/tiers.md +++ b/docs/tiers.md @@ -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. diff --git a/resources/js/components/PricingGrid.vue b/resources/js/components/PricingGrid.vue new file mode 100644 index 0000000..6382b96 --- /dev/null +++ b/resources/js/components/PricingGrid.vue @@ -0,0 +1,125 @@ + + + diff --git a/resources/js/components/UpsellBanner.vue b/resources/js/components/UpsellBanner.vue index 4f5c1e9..d13adfa 100644 --- a/resources/js/components/UpsellBanner.vue +++ b/resources/js/components/UpsellBanner.vue @@ -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') diff --git a/resources/js/components/landing/LandingNav.vue b/resources/js/components/landing/LandingNav.vue index 2fb1c79..0f384ac 100644 --- a/resources/js/components/landing/LandingNav.vue +++ b/resources/js/components/landing/LandingNav.vue @@ -11,7 +11,7 @@
diff --git a/resources/js/components/landing/SiteFooter.vue b/resources/js/components/landing/SiteFooter.vue new file mode 100644 index 0000000..379a857 --- /dev/null +++ b/resources/js/components/landing/SiteFooter.vue @@ -0,0 +1,66 @@ + + + diff --git a/resources/js/router/index.js b/resources/js/router/index.js index 3d80650..69d91ac 100644 --- a/resources/js/router/index.js +++ b/resources/js/router/index.js @@ -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', diff --git a/resources/js/views/Home.vue b/resources/js/views/Home.vue index 3f0c6bc..e730349 100644 --- a/resources/js/views/Home.vue +++ b/resources/js/views/Home.vue @@ -225,88 +225,6 @@
- -
-
-
-

Pricing for every driver

-

Save hundreds for less than the cost of a coffee.

-
- - -
-
- -
- -
-
-

Free

-
- £0 - /mo -
-
-
    -
  • Basic Search
  • -
  • Daily Updates
  • -
  • No Alerts
  • -
- {{ ctaLabel('free') }} -
- - -
-
-

Daily

-
- {{ PRICES[cadence].basic }} - {{ PRICE_SUFFIX[cadence] }} -
-
-
    -
  • Buy-or-Wait Score
  • -
  • 14-day Trend Data
  • -
  • 3 Daily Price Alerts
  • -
- {{ ctaLabel('basic') }} -
- - -
-
Most pick this
-
-

Smart

-
- {{ PRICES[cadence].plus }} - {{ PRICE_SUFFIX[cadence] }} -
-
-
    -
  • Supermarket Anchor
  • -
  • Priority Price Alerts
  • -
  • Multi-location tracking
  • -
- {{ ctaLabel('plus') }} -
-
-
-
- - + diff --git a/resources/views/components/pricing-card.blade.php b/resources/views/components/pricing-card.blade.php deleted file mode 100644 index 8a05332..0000000 --- a/resources/views/components/pricing-card.blade.php +++ /dev/null @@ -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 - -
merge(['class' => "p-8 rounded-3xl flex flex-col relative $cardClass"]) }}> - - @if ($featured) -
- Most Popular -
- @endif - -
-

{{ $name }}

-
- $featured])>{{ $price }} - !$dark, 'text-zinc-400' => $dark])>/mo -
-
- - - - - {{ $buttonText }} - - -
diff --git a/tests/Feature/Tiers/PlanFeaturesTest.php b/tests/Feature/Tiers/PlanFeaturesTest.php index 1c40e20..8abd27f 100644 --- a/tests/Feature/Tiers/PlanFeaturesTest.php +++ b/tests/Feature/Tiers/PlanFeaturesTest.php @@ -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')