Add recurring transactions with monthly confirmation flow
New "Récurrences" section lets users define recurring depense/epargne models (name, category, default amount, day of month). Nothing is created automatically: from the day a recurrence is due, it appears as a pending proposal in Suivi (desktop + mobile) with an editable amount, so variable bills (electricity, etc.) can be adjusted before confirming. Dismissing a proposal skips it for the current month without creating a transaction, and it reappears the following month. - recurring_transactions table (household/category scoped, active flag, last_generated_year/month to prevent duplicate generation) - transactions.recurring_transaction_id nullable FK to trace origin - RecurringTransactionPolicy mirrors CategoryPolicy tenant scoping - 10 new Pest tests covering CRUD, tenant isolation, and the pending/confirm/dismiss lifecycle across month boundaries (travelTo) 58/58 tests pass, Pint clean, verified end-to-end against production data (test recurrence created and cleaned up). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
674c43afea
commit
b78c314569
@@ -98,6 +98,92 @@ it('resets the form to defaults after adding a transaction', function (): void {
|
||||
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('does not show another household transactions', function (): void {
|
||||
$ownerA = User::factory()->create();
|
||||
$ownerB = User::factory()->create();
|
||||
|
||||
Reference in New Issue
Block a user