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:
jeremy bayse
2026-08-01 08:24:14 +02:00
co-authored by Claude Sonnet 5
parent 5ef655d16f
commit 9114bb2e8d
10 changed files with 414 additions and 19 deletions
@@ -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);
});