create(); $category = $user->currentHousehold->categories()->create(['name' => 'Salaire', 'type' => 'revenu', 'position' => 1]); Livewire::actingAs($user) ->test(Index::class) ->set('categoryId', $category->id) ->set('amount', '2900') ->set('details', 'Paie juillet') ->call('addTransaction') ->assertHasNoErrors(); expect($user->currentHousehold->transactions()->count())->toBe(1); expect((float) $user->currentHousehold->transactions()->first()->amount)->toBe(2900.0); }); it('computes a running balance that subtracts depenses and adds revenus', 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' => 2900, ]); $household->transactions()->create([ 'category_id' => $depense->id, 'user_id' => $user->id, 'date' => '2026-01-02', 'date_effective' => '2026-01-02', 'amount' => 1200, ]); $component = Livewire::actingAs($user)->test(Index::class); $running = $component->viewData('running'); $transactions = $component->viewData('transactions'); $ordered = $transactions->sortBy('date')->values(); expect($running[$ordered[0]->id])->toBe(2900.0); expect($running[$ordered[1]->id])->toBe(1700.0); }); it('subtracts epargne contributions from the running balance', function (): void { $user = User::factory()->create(); $household = $user->currentHousehold; $revenu = $household->categories()->create(['name' => 'Salaire', 'type' => 'revenu', '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' => 2900, ]); $household->transactions()->create([ 'category_id' => $epargne->id, 'user_id' => $user->id, 'date' => '2026-01-02', 'date_effective' => '2026-01-02', 'amount' => 200, ]); $component = Livewire::actingAs($user)->test(Index::class); $running = $component->viewData('running'); $transactions = $component->viewData('transactions'); $ordered = $transactions->sortBy('date')->values(); expect($running[$ordered[0]->id])->toBe(2900.0); expect($running[$ordered[1]->id])->toBe(2700.0); }); it('dispatches a transaction-added event after successfully adding a transaction', function (): void { $user = User::factory()->create(); $category = $user->currentHousehold->categories()->create(['name' => 'Loyer', 'type' => 'depense', 'position' => 1]); Livewire::actingAs($user) ->test(Index::class) ->set('categoryId', $category->id) ->set('amount', '1200') ->call('addTransaction') ->assertDispatched('transaction-added'); }); it('resets the form to defaults after adding a transaction', function (): void { $user = User::factory()->create(); $category = $user->currentHousehold->categories()->create(['name' => 'Loyer', 'type' => 'depense', 'position' => 1]); $component = Livewire::actingAs($user) ->test(Index::class) ->set('categoryId', $category->id) ->set('amount', '1200') ->set('details', 'Août') ->call('addTransaction'); expect($component->get('categoryId'))->toBeNull(); expect($component->get('amount'))->toBe(''); expect($component->get('details'))->toBe(''); }); it('lists active recurring transactions due this month as pending', function (): void { $this->travelTo(now()->setDate(2026, 3, 10)); $user = User::factory()->create(); $household = $user->currentHousehold; $category = $household->categories()->create(['name' => 'Loyer', 'type' => 'depense', 'position' => 1]); $due = $household->recurringTransactions()->create([ 'category_id' => $category->id, 'name' => 'Loyer', 'amount' => 1200, 'day_of_month' => 5, ]); $notYetDue = $household->recurringTransactions()->create([ 'category_id' => $category->id, 'name' => 'Internet', 'amount' => 40, 'day_of_month' => 20, ]); $inactive = $household->recurringTransactions()->create([ 'category_id' => $category->id, 'name' => 'Ancien', 'amount' => 10, 'day_of_month' => 1, 'active' => false, ]); $component = Livewire::actingAs($user)->test(Index::class); $pending = $component->viewData('pendingRecurrences'); expect($pending->pluck('id'))->toContain($due->id); expect($pending->pluck('id'))->not->toContain($notYetDue->id); expect($pending->pluck('id'))->not->toContain($inactive->id); }); it('confirming a recurring transaction creates a transaction and stops it reappearing', function (): void { $this->travelTo(now()->setDate(2026, 3, 10)); $user = User::factory()->create(); $household = $user->currentHousehold; $category = $household->categories()->create(['name' => 'Loyer', 'type' => 'depense', 'position' => 1]); $recurring = $household->recurringTransactions()->create([ 'category_id' => $category->id, 'name' => 'Loyer', 'amount' => 1200, 'day_of_month' => 5, ]); Livewire::actingAs($user) ->test(Index::class) ->call('confirmRecurrence', $recurring->id, '1250') ->assertDispatched('transaction-added'); $transaction = $household->transactions()->where('recurring_transaction_id', $recurring->id)->first(); expect($transaction)->not->toBeNull(); expect((float) $transaction->amount)->toBe(1250.0); expect($transaction->details)->toBe('Loyer'); $component = Livewire::actingAs($user)->test(Index::class); expect($component->viewData('pendingRecurrences'))->toHaveCount(0); }); it('dismissing a recurring transaction hides it without creating a transaction', function (): void { $this->travelTo(now()->setDate(2026, 3, 10)); $user = User::factory()->create(); $household = $user->currentHousehold; $category = $household->categories()->create(['name' => 'Loyer', 'type' => 'depense', 'position' => 1]); $recurring = $household->recurringTransactions()->create([ 'category_id' => $category->id, 'name' => 'Loyer', 'amount' => 1200, 'day_of_month' => 5, ]); Livewire::actingAs($user)->test(Index::class)->call('dismissRecurrence', $recurring->id); expect($household->transactions()->count())->toBe(0); $component = Livewire::actingAs($user)->test(Index::class); expect($component->viewData('pendingRecurrences'))->toHaveCount(0); }); it('shows the recurrence again the following month after being confirmed', function (): void { $this->travelTo(now()->setDate(2026, 3, 10)); $user = User::factory()->create(); $household = $user->currentHousehold; $category = $household->categories()->create(['name' => 'Loyer', 'type' => 'depense', 'position' => 1]); $recurring = $household->recurringTransactions()->create([ 'category_id' => $category->id, 'name' => 'Loyer', 'amount' => 1200, 'day_of_month' => 5, ]); Livewire::actingAs($user)->test(Index::class)->call('confirmRecurrence', $recurring->id, '1200'); $this->travelTo(now()->setDate(2026, 4, 10)); $component = Livewire::actingAs($user)->test(Index::class); expect($component->viewData('pendingRecurrences')->pluck('id'))->toContain($recurring->id); }); it('computes the percentage and remaining budget already tracked for a category this month', function (): void { $this->travelTo(now()->setDate(2026, 3, 10)); $user = User::factory()->create(); $household = $user->currentHousehold; $category = $household->categories()->create(['name' => 'Loyer', 'type' => 'depense', 'position' => 1]); $category->budgetLines()->create(['household_id' => $household->id, 'year' => 2026, 'month' => null, 'amount' => 1000]); $household->transactions()->create([ 'category_id' => $category->id, 'user_id' => $user->id, 'date' => '2026-03-05', 'date_effective' => '2026-03-05', 'amount' => 600, ]); $component = Livewire::actingAs($user)->test(Index::class); $progress = $component->viewData('categoryProgress')[$category->id]; expect($progress['pct'])->toBe(60.0); expect($progress['remaining'])->toBe(400.0); }); it('caps the category progress percentage at 100 and floors remaining at 0', function (): void { $this->travelTo(now()->setDate(2026, 3, 10)); $user = User::factory()->create(); $household = $user->currentHousehold; $category = $household->categories()->create(['name' => 'Loyer', 'type' => 'depense', 'position' => 1]); $category->budgetLines()->create(['household_id' => $household->id, 'year' => 2026, 'month' => null, 'amount' => 1000]); $household->transactions()->create([ 'category_id' => $category->id, 'user_id' => $user->id, 'date' => '2026-03-05', 'date_effective' => '2026-03-05', 'amount' => 1500, ]); $component = Livewire::actingAs($user)->test(Index::class); $progress = $component->viewData('categoryProgress')[$category->id]; expect($progress['pct'])->toBe(100); expect($progress['remaining'])->toBe(0); }); it('returns no progress for a category without a budget defined', function (): void { $this->travelTo(now()->setDate(2026, 3, 10)); $user = User::factory()->create(); $household = $user->currentHousehold; $category = $household->categories()->create(['name' => 'Loyer', 'type' => 'depense', 'position' => 1]); $component = Livewire::actingAs($user)->test(Index::class); expect($component->viewData('categoryProgress')[$category->id])->toBeNull(); }); it('records who paid a transaction and defaults to common when none is selected', function (): void { $user = User::factory()->create(); $household = $user->currentHousehold; $category = $household->categories()->create(['name' => 'Loyer', 'type' => 'depense', 'position' => 1]); Livewire::actingAs($user) ->test(Index::class) ->set('categoryId', $category->id) ->set('amount', '1200') ->call('addTransaction'); $transaction = $household->transactions()->first(); expect($transaction->paid_by_user_id)->toBeNull(); }); it('assigns a transaction to a household member when selected', function (): void { $owner = User::factory()->create(); $member = User::factory()->create(); $household = $owner->currentHousehold; $member->households()->attach($household->id, ['role' => 'member']); $category = $household->categories()->create(['name' => 'Loyer', 'type' => 'depense', 'position' => 1]); Livewire::actingAs($owner) ->test(Index::class) ->set('categoryId', $category->id) ->set('amount', '1200') ->set('paidByUserId', (string) $member->id) ->call('addTransaction'); $transaction = $household->transactions()->first(); expect($transaction->paid_by_user_id)->toBe($member->id); }); it('ignores a paid-by user id that does not belong to the household', function (): void { $owner = User::factory()->create(); $outsider = User::factory()->create(); $household = $owner->currentHousehold; $category = $household->categories()->create(['name' => 'Loyer', 'type' => 'depense', 'position' => 1]); Livewire::actingAs($owner) ->test(Index::class) ->set('categoryId', $category->id) ->set('amount', '1200') ->set('paidByUserId', (string) $outsider->id) ->call('addTransaction'); $transaction = $household->transactions()->first(); expect($transaction->paid_by_user_id)->toBeNull(); }); it('filters transactions by who paid without affecting the running balance', function (): void { $owner = User::factory()->create(); $member = User::factory()->create(); $household = $owner->currentHousehold; $member->households()->attach($household->id, ['role' => 'member']); $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' => $owner->id, 'date' => '2026-01-01', 'date_effective' => '2026-01-01', 'amount' => 2000, ]); $ownerTx = $household->transactions()->create([ 'category_id' => $depense->id, 'user_id' => $owner->id, 'paid_by_user_id' => $owner->id, 'date' => '2026-01-02', 'date_effective' => '2026-01-02', 'amount' => 500, ]); $memberTx = $household->transactions()->create([ 'category_id' => $depense->id, 'user_id' => $owner->id, 'paid_by_user_id' => $member->id, 'date' => '2026-01-03', 'date_effective' => '2026-01-03', 'amount' => 300, ]); $component = Livewire::actingAs($owner)->test(Index::class)->set('filterPaidBy', (string) $member->id); $shown = $component->viewData('transactions'); expect($shown->pluck('id'))->toContain($memberTx->id); expect($shown->pluck('id'))->not->toContain($ownerTx->id); // Le solde reste calculé sur toutes les transactions malgré le filtre d'affichage. $running = $component->viewData('running'); expect($running[$memberTx->id])->toBe(1200.0); // 2000 - 500 - 300 }); it('summarizes this month depenses by payer including common', function (): void { $this->travelTo(now()->setDate(2026, 3, 10)); $owner = User::factory()->create(); $member = User::factory()->create(); $household = $owner->currentHousehold; $member->households()->attach($household->id, ['role' => 'member']); $category = $household->categories()->create(['name' => 'Loyer', 'type' => 'depense', 'position' => 1]); $household->transactions()->create([ 'category_id' => $category->id, 'user_id' => $owner->id, 'paid_by_user_id' => $owner->id, 'date' => '2026-03-05', 'date_effective' => '2026-03-05', 'amount' => 500, ]); $household->transactions()->create([ 'category_id' => $category->id, 'user_id' => $owner->id, 'paid_by_user_id' => $member->id, 'date' => '2026-03-06', 'date_effective' => '2026-03-06', 'amount' => 300, ]); $household->transactions()->create([ 'category_id' => $category->id, 'user_id' => $owner->id, 'paid_by_user_id' => null, 'date' => '2026-03-07', 'date_effective' => '2026-03-07', 'amount' => 100, ]); $component = Livewire::actingAs($owner)->test(Index::class); $summary = $component->viewData('paidBySummary')->keyBy('label'); expect($summary[$owner->name]['total'])->toBe(500.0); expect($summary[$member->name]['total'])->toBe(300.0); expect($summary['Commun']['total'])->toBe(100.0); }); it('does not show another household transactions', function (): void { $ownerA = User::factory()->create(); $ownerB = User::factory()->create(); $categoryA = $ownerA->currentHousehold->categories()->create(['name' => 'Salaire', 'type' => 'revenu', 'position' => 1]); $ownerA->currentHousehold->transactions()->create([ 'category_id' => $categoryA->id, 'user_id' => $ownerA->id, 'date' => now(), 'date_effective' => now(), 'amount' => 1000, ]); $component = Livewire::actingAs($ownerB)->test(Index::class); expect($component->viewData('transactions'))->toHaveCount(0); });