Track who paid each depense, with a "Commun" (shared) fallback
Adds a "payé par" field to transactions and recurring transactions: either a household member or null (shared/common expense). No per- member account balances — this is purely a "who paid" label, the household's overall balance is unchanged and unaffected by any filtering. - paid_by_user_id nullable FK on transactions and recurring_transactions - Suivi: "payé par" selector on both entry forms (desktop dropdown, mobile buttons), a filter above the transaction table, a "payé par" column, and a summary tile showing this month's depenses grouped by payer (including Commun) - Recurring transactions carry paid_by_user_id onto the transaction they generate when confirmed - Running balance is computed from all transactions regardless of the payer filter, so it never gets skewed by the active filter 11 new Pest tests (assignment, common fallback, non-member rejection, filter isolation from the balance calc, per-payer summary, recurring propagation). 69/69 total pass, Pint clean, verified end-to-end on production data (desktop + mobile, test row created and removed). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
5ef655d16f
commit
9114bb2e8d
@@ -65,3 +65,48 @@ it('deletes a recurring transaction', function (): void {
|
||||
|
||||
expect(RecurringTransaction::find($recurring->id))->toBeNull();
|
||||
});
|
||||
|
||||
it('assigns a recurring transaction to a household member', 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('name', 'Loyer')
|
||||
->set('categoryId', $category->id)
|
||||
->set('amount', '1200')
|
||||
->set('dayOfMonth', '5')
|
||||
->set('paidByUserId', (string) $member->id)
|
||||
->call('add');
|
||||
|
||||
$recurring = $household->recurringTransactions()->first();
|
||||
|
||||
expect($recurring->paid_by_user_id)->toBe($member->id);
|
||||
});
|
||||
|
||||
it('carries the recurring paid-by user onto the generated transaction', 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]);
|
||||
$recurring = $household->recurringTransactions()->create([
|
||||
'category_id' => $category->id, 'paid_by_user_id' => $member->id,
|
||||
'name' => 'Loyer', 'amount' => 1200, 'day_of_month' => 5,
|
||||
]);
|
||||
|
||||
Livewire::actingAs($owner)
|
||||
->test(App\Livewire\Suivi\Index::class)
|
||||
->call('confirmRecurrence', $recurring->id, '1200');
|
||||
|
||||
$transaction = $household->transactions()->where('recurring_transaction_id', $recurring->id)->first();
|
||||
|
||||
expect($transaction->paid_by_user_id)->toBe($member->id);
|
||||
});
|
||||
|
||||
@@ -236,6 +236,124 @@ it('returns no progress for a category without a budget defined', function (): v
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user