- 6 notification classes: PaymentSucceeded, PaymentFailed, SubscriptionCreated, SubscriptionCancelled, ServiceProvisioned, InvoiceGenerated (mail + database) - Wire notifications to existing event listeners + new subscription listeners - NotificationBell component in Account and Admin layouts - NotificationController with index, markAsRead, markAllAsRead endpoints - 62 new Pest tests: AdminPanelTest (admin CRUD) + CustomerAccountTest (account features) - Add Legal links column to marketing footer - 114 tests passing (623 assertions) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
285 lines
10 KiB
PHP
285 lines
10 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\Invoice;
|
|
use App\Models\Plan;
|
|
use App\Models\Service;
|
|
use App\Models\User;
|
|
use Database\Seeders\RoleAndPermissionSeeder;
|
|
|
|
beforeEach(function (): void {
|
|
$this->seed(RoleAndPermissionSeeder::class);
|
|
$this->accountUrl = 'http://'.config('app.domains.account');
|
|
});
|
|
|
|
describe('Dashboard', function (): void {
|
|
it('allows a customer to view the dashboard', function (): void {
|
|
$customer = User::factory()->customer()->create();
|
|
|
|
$this->actingAs($customer)
|
|
->get($this->accountUrl.'/dashboard')
|
|
->assertOk()
|
|
->assertInertia(fn ($page) => $page
|
|
->component('Dashboard')
|
|
->has('activeServicesCount')
|
|
->has('activeSubscriptionsCount')
|
|
->has('pendingInvoicesAmount')
|
|
->has('latestInvoices')
|
|
);
|
|
});
|
|
|
|
it('redirects guests to login', function (): void {
|
|
$this->get($this->accountUrl.'/dashboard')
|
|
->assertRedirect();
|
|
});
|
|
|
|
it('shows correct active services count', function (): void {
|
|
$customer = User::factory()->customer()->create();
|
|
Service::factory()->count(3)->create(['user_id' => $customer->id, 'status' => 'active']);
|
|
Service::factory()->create(['user_id' => $customer->id, 'status' => 'suspended']);
|
|
|
|
$this->actingAs($customer)
|
|
->get($this->accountUrl.'/dashboard')
|
|
->assertOk()
|
|
->assertInertia(fn ($page) => $page
|
|
->component('Dashboard')
|
|
->where('activeServicesCount', 3)
|
|
);
|
|
});
|
|
|
|
it('shows correct pending invoices amount', function (): void {
|
|
$customer = User::factory()->customer()->create();
|
|
Invoice::factory()->create(['user_id' => $customer->id, 'status' => 'pending', 'total' => 50.00]);
|
|
Invoice::factory()->create(['user_id' => $customer->id, 'status' => 'overdue', 'total' => 25.50]);
|
|
Invoice::factory()->create(['user_id' => $customer->id, 'status' => 'paid', 'total' => 100.00]);
|
|
|
|
$this->actingAs($customer)
|
|
->get($this->accountUrl.'/dashboard')
|
|
->assertOk()
|
|
->assertInertia(fn ($page) => $page
|
|
->component('Dashboard')
|
|
->where('pendingInvoicesAmount', '75.50')
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Profile', function (): void {
|
|
it('allows a customer to view their profile', function (): void {
|
|
$customer = User::factory()->customer()->create();
|
|
|
|
$this->actingAs($customer)
|
|
->get($this->accountUrl.'/profile')
|
|
->assertOk()
|
|
->assertInertia(fn ($page) => $page
|
|
->component('Profile/Show')
|
|
->has('user')
|
|
->has('twoFactorEnabled')
|
|
);
|
|
});
|
|
|
|
it('allows a customer to update their profile', function (): void {
|
|
$customer = User::factory()->customer()->create();
|
|
|
|
$this->actingAs($customer)
|
|
->put($this->accountUrl.'/profile', [
|
|
'first_name' => 'Jane',
|
|
'last_name' => 'Smith',
|
|
'phone' => '+1234567890',
|
|
'company' => 'Acme Inc',
|
|
])
|
|
->assertRedirect()
|
|
->assertSessionHas('success', 'Profile updated successfully.');
|
|
|
|
$customer->refresh();
|
|
expect($customer->name)->toBe('Jane Smith');
|
|
expect($customer->phone)->toBe('+1234567890');
|
|
expect($customer->company)->toBe('Acme Inc');
|
|
});
|
|
|
|
it('validates profile update requires first and last name', function (): void {
|
|
$customer = User::factory()->customer()->create();
|
|
|
|
$this->actingAs($customer)
|
|
->put($this->accountUrl.'/profile', [
|
|
'first_name' => '',
|
|
'last_name' => '',
|
|
])
|
|
->assertSessionHasErrors(['first_name', 'last_name']);
|
|
});
|
|
|
|
it('allows a customer to update their password', function (): void {
|
|
$customer = User::factory()->customer()->create();
|
|
|
|
$this->actingAs($customer)
|
|
->put($this->accountUrl.'/profile/password', [
|
|
'current_password' => 'password',
|
|
'password' => 'NewSecurePass1!',
|
|
'password_confirmation' => 'NewSecurePass1!',
|
|
])
|
|
->assertRedirect()
|
|
->assertSessionHas('success', 'Password updated successfully.');
|
|
});
|
|
|
|
it('rejects password update with wrong current password', function (): void {
|
|
$customer = User::factory()->customer()->create();
|
|
|
|
$this->actingAs($customer)
|
|
->put($this->accountUrl.'/profile/password', [
|
|
'current_password' => 'wrong-password',
|
|
'password' => 'NewSecurePass1!',
|
|
'password_confirmation' => 'NewSecurePass1!',
|
|
])
|
|
->assertSessionHasErrors('current_password');
|
|
});
|
|
|
|
it('rejects password update when confirmation does not match', function (): void {
|
|
$customer = User::factory()->customer()->create();
|
|
|
|
$this->actingAs($customer)
|
|
->put($this->accountUrl.'/profile/password', [
|
|
'current_password' => 'password',
|
|
'password' => 'NewSecurePass1!',
|
|
'password_confirmation' => 'DifferentPass2!',
|
|
])
|
|
->assertSessionHasErrors('password');
|
|
});
|
|
});
|
|
|
|
describe('Plans', function (): void {
|
|
it('displays active plans to a customer', function (): void {
|
|
$customer = User::factory()->customer()->create();
|
|
Plan::factory()->count(3)->create(['status' => 'active']);
|
|
Plan::factory()->create(['status' => 'inactive']);
|
|
|
|
$this->actingAs($customer)
|
|
->get($this->accountUrl.'/plans')
|
|
->assertOk()
|
|
->assertInertia(fn ($page) => $page
|
|
->component('Plans/Index')
|
|
->has('plansByType')
|
|
);
|
|
});
|
|
|
|
it('displays a single plan detail page', function (): void {
|
|
$customer = User::factory()->customer()->create();
|
|
$plan = Plan::factory()->create(['status' => 'active']);
|
|
|
|
$this->actingAs($customer)
|
|
->get($this->accountUrl.'/plans/'.$plan->id)
|
|
->assertOk()
|
|
->assertInertia(fn ($page) => $page
|
|
->component('Plans/Show')
|
|
->has('plan')
|
|
->where('plan.id', $plan->id)
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Services', function (): void {
|
|
it('displays only the customers own services', function (): void {
|
|
$customer = User::factory()->customer()->create();
|
|
$otherUser = User::factory()->customer()->create();
|
|
|
|
Service::factory()->count(2)->create(['user_id' => $customer->id]);
|
|
Service::factory()->count(3)->create(['user_id' => $otherUser->id]);
|
|
|
|
$this->actingAs($customer)
|
|
->get($this->accountUrl.'/services')
|
|
->assertOk()
|
|
->assertInertia(fn ($page) => $page
|
|
->component('Services/Index')
|
|
->has('services', 2)
|
|
);
|
|
});
|
|
|
|
it('allows a customer to view their own service detail', function (): void {
|
|
$customer = User::factory()->customer()->create();
|
|
$service = Service::factory()->create(['user_id' => $customer->id]);
|
|
|
|
$this->actingAs($customer)
|
|
->get($this->accountUrl.'/services/'.$service->id)
|
|
->assertOk()
|
|
->assertInertia(fn ($page) => $page
|
|
->component('Services/Show')
|
|
->has('service')
|
|
->where('service.id', $service->id)
|
|
);
|
|
});
|
|
|
|
it('forbids a customer from viewing another users service', function (): void {
|
|
$customer = User::factory()->customer()->create();
|
|
$otherUser = User::factory()->customer()->create();
|
|
$service = Service::factory()->create(['user_id' => $otherUser->id]);
|
|
|
|
$this->actingAs($customer)
|
|
->get($this->accountUrl.'/services/'.$service->id)
|
|
->assertForbidden();
|
|
});
|
|
});
|
|
|
|
describe('Subscriptions', function (): void {
|
|
it('displays the subscriptions list page', function (): void {
|
|
$customer = User::factory()->customer()->create();
|
|
|
|
$this->actingAs($customer)
|
|
->get($this->accountUrl.'/subscriptions')
|
|
->assertOk()
|
|
->assertInertia(fn ($page) => $page
|
|
->component('Subscriptions/Index')
|
|
->has('subscriptions')
|
|
);
|
|
});
|
|
|
|
it('requires authentication to view subscriptions', function (): void {
|
|
$this->get($this->accountUrl.'/subscriptions')
|
|
->assertRedirect();
|
|
});
|
|
});
|
|
|
|
describe('Billing', function (): void {
|
|
it('displays the billing index page', function (): void {
|
|
$customer = User::factory()->customer()->create();
|
|
|
|
$this->actingAs($customer)
|
|
->get($this->accountUrl.'/billing')
|
|
->assertOk()
|
|
->assertInertia(fn ($page) => $page
|
|
->component('Billing/Index')
|
|
->has('paymentMethods')
|
|
->has('invoices')
|
|
->has('transactions')
|
|
);
|
|
});
|
|
|
|
it('displays the invoices page with customer invoices', function (): void {
|
|
$customer = User::factory()->customer()->create();
|
|
Invoice::factory()->count(3)->create(['user_id' => $customer->id]);
|
|
|
|
$this->actingAs($customer)
|
|
->get($this->accountUrl.'/billing/invoices')
|
|
->assertOk()
|
|
->assertInertia(fn ($page) => $page
|
|
->component('Billing/Invoices')
|
|
->has('invoices.data', 3)
|
|
);
|
|
});
|
|
|
|
it('displays the transactions page', function (): void {
|
|
$customer = User::factory()->customer()->create();
|
|
|
|
$this->actingAs($customer)
|
|
->get($this->accountUrl.'/billing/transactions')
|
|
->assertOk()
|
|
->assertInertia(fn ($page) => $page
|
|
->component('Billing/Transactions')
|
|
->has('transactions')
|
|
);
|
|
});
|
|
|
|
it('requires authentication to view billing', function (): void {
|
|
$this->get($this->accountUrl.'/billing')
|
|
->assertRedirect();
|
|
});
|
|
});
|