Add pricing-page waitlist (name + email signup)
Replaces the disabled "Coming soon" buttons on the pricing page with a waitlist band so visitors can be notified when alerts launch — separate from registered users. - waitlist_subscribers table (name, email unique, source, referrer) - WaitlistService::subscribe — normalises email, idempotent - Public POST /api/waitlist (throttle:10,1), thin controller + form request - Read-only Filament resource with streamed CSV export - Vue: useWaitlist composable + WaitlistForm, rendered below the grid while any tier is still "coming soon"; sends source + document.referrer Announcement send mechanism deferred to a later task. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
31
tests/Feature/Admin/WaitlistSubscriberResourceTest.php
Normal file
31
tests/Feature/Admin/WaitlistSubscriberResourceTest.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use App\Filament\Resources\WaitlistSubscriberResource\Pages\ListWaitlistSubscribers;
|
||||
use App\Models\User;
|
||||
use App\Models\WaitlistSubscriber;
|
||||
use Livewire\Livewire;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->actingAs(User::factory()->admin()->create());
|
||||
});
|
||||
|
||||
it('lists waitlist subscribers', function () {
|
||||
$subscribers = WaitlistSubscriber::factory()->count(3)->create();
|
||||
|
||||
Livewire::test(ListWaitlistSubscribers::class)
|
||||
->assertOk()
|
||||
->assertCanSeeTableRecords($subscribers);
|
||||
});
|
||||
|
||||
it('exposes a CSV export header action', function () {
|
||||
Livewire::test(ListWaitlistSubscribers::class)
|
||||
->assertActionExists('export');
|
||||
});
|
||||
|
||||
it('exports a CSV download when the action runs against real subscribers', function () {
|
||||
WaitlistSubscriber::factory()->count(2)->create();
|
||||
|
||||
Livewire::test(ListWaitlistSubscribers::class)
|
||||
->callAction('export')
|
||||
->assertFileDownloaded('waitlist.csv');
|
||||
});
|
||||
64
tests/Feature/Api/WaitlistTest.php
Normal file
64
tests/Feature/Api/WaitlistTest.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
use App\Models\WaitlistSubscriber;
|
||||
|
||||
it('stores a subscriber and returns 201', function () {
|
||||
$response = $this->postJson('/api/waitlist', [
|
||||
'name' => 'Ada Lovelace',
|
||||
'email' => 'ada@example.com',
|
||||
]);
|
||||
|
||||
$response->assertCreated()
|
||||
->assertJsonStructure(['message']);
|
||||
|
||||
$this->assertDatabaseHas('waitlist_subscribers', [
|
||||
'name' => 'Ada Lovelace',
|
||||
'email' => 'ada@example.com',
|
||||
]);
|
||||
});
|
||||
|
||||
it('treats a duplicate email as success without creating a second row', function () {
|
||||
WaitlistSubscriber::factory()->create(['email' => 'ada@example.com']);
|
||||
|
||||
$this->postJson('/api/waitlist', [
|
||||
'name' => 'Someone Else',
|
||||
'email' => 'ada@example.com',
|
||||
])->assertCreated();
|
||||
|
||||
expect(WaitlistSubscriber::where('email', 'ada@example.com')->count())->toBe(1);
|
||||
});
|
||||
|
||||
it('stores the source and referrer sent with the request', function () {
|
||||
$this->postJson('/api/waitlist', [
|
||||
'name' => 'Ada Lovelace',
|
||||
'email' => 'ada@example.com',
|
||||
'source' => 'pricing',
|
||||
'referrer' => 'https://duckduckgo.com/',
|
||||
])->assertCreated();
|
||||
|
||||
$this->assertDatabaseHas('waitlist_subscribers', [
|
||||
'email' => 'ada@example.com',
|
||||
'source' => 'pricing',
|
||||
'referrer' => 'https://duckduckgo.com/',
|
||||
]);
|
||||
});
|
||||
|
||||
it('requires a name', function () {
|
||||
$this->postJson('/api/waitlist', [
|
||||
'email' => 'ada@example.com',
|
||||
])->assertStatus(422)->assertJsonValidationErrors('name');
|
||||
});
|
||||
|
||||
it('rejects an invalid email', function () {
|
||||
$this->postJson('/api/waitlist', [
|
||||
'name' => 'Ada Lovelace',
|
||||
'email' => 'not-an-email',
|
||||
])->assertStatus(422)->assertJsonValidationErrors('email');
|
||||
});
|
||||
|
||||
it('throttles the endpoint', function () {
|
||||
$route = collect(app('router')->getRoutes())
|
||||
->first(fn ($route) => $route->uri() === 'api/waitlist' && in_array('POST', $route->methods(), true));
|
||||
|
||||
expect($route?->gatherMiddleware())->toContain('throttle:10,1');
|
||||
});
|
||||
Reference in New Issue
Block a user