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>
65 lines
1.9 KiB
PHP
65 lines
1.9 KiB
PHP
<?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');
|
|
});
|