Laravel 11 + Livewire 3/Volt + Alpine.js + Tailwind app for household budget planning, transaction tracking, and financial dashboards. - Multi-tenant households with member invites - Plan: monthly/yearly budget per category with overrides - Suivi: transaction log with running balance (desktop + mobile quick-entry) - Dashboard: budget vs tracked breakdown + Chart.js donuts - Flux: Sankey diagram of income allocation - WCAG 2.1 AA color tokens, dark mode, accessible tables/forms - 50 Pest tests Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
94 lines
3.8 KiB
PHP
94 lines
3.8 KiB
PHP
<?php
|
|
|
|
use App\Livewire\Sankey\Index;
|
|
use App\Models\User;
|
|
use Livewire\Livewire;
|
|
|
|
it('builds flows from revenue to depense and epargne categories', function (): void {
|
|
$user = User::factory()->create();
|
|
$household = $user->currentHousehold;
|
|
|
|
$revenu = $household->categories()->create(['name' => 'Salaire', 'type' => 'revenu', 'position' => 1]);
|
|
$depense = $household->categories()->create(['name' => 'Loyer', 'type' => 'depense', 'position' => 1]);
|
|
$epargne = $household->categories()->create(['name' => 'Vacances', 'type' => 'epargne', 'position' => 1]);
|
|
|
|
$household->transactions()->create([
|
|
'category_id' => $revenu->id, 'user_id' => $user->id,
|
|
'date' => '2026-01-01', 'date_effective' => '2026-01-01', 'amount' => 2000,
|
|
]);
|
|
$household->transactions()->create([
|
|
'category_id' => $depense->id, 'user_id' => $user->id,
|
|
'date' => '2026-01-02', 'date_effective' => '2026-01-02', 'amount' => 1200,
|
|
]);
|
|
$household->transactions()->create([
|
|
'category_id' => $epargne->id, 'user_id' => $user->id,
|
|
'date' => '2026-01-03', 'date_effective' => '2026-01-03', 'amount' => 300,
|
|
]);
|
|
|
|
$component = Livewire::actingAs($user)->test(Index::class)
|
|
->set('year', 2026)
|
|
->set('period', 'year');
|
|
|
|
$flows = $component->viewData('flows');
|
|
|
|
expect($flows)->toHaveCount(3); // Loyer, Vacances, Non affecté
|
|
expect($flows->firstWhere('to', 'Loyer')['flow'])->toBe(1200.0);
|
|
expect($flows->firstWhere('to', 'Vacances')['flow'])->toBe(300.0);
|
|
expect($flows->firstWhere('to', 'Non affecté')['flow'])->toBe(500.0);
|
|
|
|
expect($component->viewData('totalRevenue'))->toBe(2000.0);
|
|
expect($component->viewData('hasData'))->toBeTrue();
|
|
});
|
|
|
|
it('omits the unallocated flow when all revenue is spent or saved', function (): void {
|
|
$user = User::factory()->create();
|
|
$household = $user->currentHousehold;
|
|
|
|
$revenu = $household->categories()->create(['name' => 'Salaire', 'type' => 'revenu', 'position' => 1]);
|
|
$depense = $household->categories()->create(['name' => 'Loyer', 'type' => 'depense', 'position' => 1]);
|
|
|
|
$household->transactions()->create([
|
|
'category_id' => $revenu->id, 'user_id' => $user->id,
|
|
'date' => '2026-01-01', 'date_effective' => '2026-01-01', 'amount' => 1000,
|
|
]);
|
|
$household->transactions()->create([
|
|
'category_id' => $depense->id, 'user_id' => $user->id,
|
|
'date' => '2026-01-02', 'date_effective' => '2026-01-02', 'amount' => 1000,
|
|
]);
|
|
|
|
$component = Livewire::actingAs($user)->test(Index::class)
|
|
->set('year', 2026)
|
|
->set('period', 'year');
|
|
|
|
$flows = $component->viewData('flows');
|
|
|
|
expect($flows)->toHaveCount(1);
|
|
expect($flows->firstWhere('to', 'Non affecté'))->toBeNull();
|
|
});
|
|
|
|
it('shows no data state when there is no tracked revenue', function (): void {
|
|
$user = User::factory()->create();
|
|
|
|
$component = Livewire::actingAs($user)->test(Index::class);
|
|
|
|
expect($component->viewData('hasData'))->toBeFalse();
|
|
expect($component->viewData('flows'))->toHaveCount(0);
|
|
});
|
|
|
|
it('does not leak another household categories into the flows', function (): void {
|
|
$ownerA = User::factory()->create();
|
|
$ownerB = User::factory()->create();
|
|
|
|
$revenuA = $ownerA->currentHousehold->categories()->create(['name' => 'Salaire A', 'type' => 'revenu', 'position' => 1]);
|
|
$depenseB = $ownerB->currentHousehold->categories()->create(['name' => 'Loyer B', 'type' => 'depense', 'position' => 1]);
|
|
|
|
$ownerA->currentHousehold->transactions()->create([
|
|
'category_id' => $revenuA->id, 'user_id' => $ownerA->id,
|
|
'date' => now(), 'date_effective' => now(), 'amount' => 1000,
|
|
]);
|
|
|
|
$component = Livewire::actingAs($ownerA)->test(Index::class);
|
|
|
|
expect($component->viewData('flows')->pluck('to'))->not->toContain('Loyer B');
|
|
});
|