Repository navigation
Description
Many businesses generate recurring invoices (subscriptions, retainers, maintenance, scheduled billing). Users need a reliable, predictable system for automating invoice creation with flexible schedules and overrides.
Specific
As a user, I want to create recurring invoice profiles so that invoices are automatically generated on a defined schedule with correct client, line items, taxes, and due dates.
Measurable
- Recurring invoice creation errors reduced by ≥70%.
- Scheduled tasks generate invoices with ≥99% success rate.
- User can create a recurring invoice profile in < 1 minute.
Achievable
Implement a recurring profile engine storing schedule rules, last-run timestamp, next-run timestamp, and generation preferences. Use a dedicated scheduler task (cron/queue) to generate invoices automatically.
Relevant
This feature reduces manual effort and prevents missed billing cycles, directly increasing reliability and revenue.
Time-bound
Design, develop, and test by end of Q1 2026.
High-level Suggestions
Recurring Profile Structure
- Core fields: frequency (weekly, monthly, quarterly, yearly), start date, end date optional, client, line items, taxes, discounts.
- Support custom frequencies: every X days/weeks/months.
- Support anchor dates (e.g., 1st of month).
Generation Pipeline
- RecurringProfile → InvoiceDraft → InvoiceFinalization → Notifications.
- Store last-run and next-run timestamps to avoid duplicate creation.
- Use safe idempotence rules to prevent double invoicing.
Editing Behavior
- Editing profile updates next-run date accordingly.
- Pausing stops generation without deleting history.
User UX
- Calendar-like overview of upcoming invoices.
- Status indicators: active, paused, ended, needs review.
- Logs showing each automated invoice creation.
Notifications
- Email/SMS/Slack-style alerts when a recurring invoice is created.
- Error alerts if generation fails.
Edge Cases
- Months with fewer days (e.g., Feb 30th rules).
- Client inactive.
- Past due next-run timestamps (catch-up mode).
AAA Scenarios
Scenario 1 — Creating a Recurring Profile
Arrange
- User is logged in and has at least one client and one product.
- No recurring profiles exist.
Act
- User creates a recurring invoice profile with monthly frequency.
Assert
- Profile is saved with correct schedule.
- Next-run date is calculated immediately.
- Preview of upcoming invoices shows correct dates.
Scenario 2 — Automatic Generation at Schedule
Arrange
- Profile exists with next-run = today.
- Scheduler/cron is operational.
Act
- Scheduler triggers the recurring engine.
Assert
- A new invoice is created.
- last-run and next-run timestamps update correctly.
- No duplicate invoices are created.
- A log entry is recorded.
Scenario 3 — Editing a Recurring Profile
Arrange
- Profile exists with monthly schedule.
- User wants to change frequency to quarterly.
Act
- User updates the schedule settings.
Assert
- New next-run date is recalculated.
- Existing history remains intact.
- Future invoices follow new schedule.
Scenario 4 — Pausing a Profile
Arrange
- Active profile exists generating monthly invoices.
Act
- User pauses the profile.
Assert
- No future invoices are generated until resumed.
- Profile remains visible but marked “Paused”.
- Next-run date is suspended or marked inactive.
Scenario 5 — Error During Generation
Arrange
- Profile should run today.
- Client is deactivated or missing critical data.
Act
- Scheduler attempts generation.
Assert
- No invoice is created.
- Error logged.
- User is notified of failure.
- System remains ready for next-run after fix.
Scenario 6 — End Date Reached
Arrange
- Profile includes an end date which is today.
Act
- Scheduler runs.
Assert
- Last invoice is created if rules allow.
- Profile is marked as completed/expired.
- No further invoices are queued.
Scenario 7 — Manual Trigger
Arrange
- Profile is active.
- User wants to generate the next invoice early.
Act
- User clicks “Generate Now”.
Assert
- Invoice is generated immediately.
- next-run updates respecting schedule.
- No duplicate creation on next scheduled trigger.
Activity
Module Invoices: Turned off Recurring Invoices refs #52
Module Invoices: Turned off Recurring Invoices refs #52
[-][Invoices]: Recurring-Invoices[/-][+][Invoices]: Epic Recurring-Invoices[/+]nielsdrost7 commentedon Jul 16, 2026
SMART Plan — Recurring invoice profiles
Specific
Automate invoice creation from a recurring profile (client, line items, taxes, cadence) so invoices are generated on schedule without manual re-entry. Current state:
RecurringInvoice/RecurringInvoiceItemmodels, migration (recurring_invoices:company_id,invoice_id,numbering_id,frequency,start_at,end_at), factory, and the full Filament CRUD resource (Modules/Invoices/Filament/Company/Resources/RecurringInvoices/) already exist. The gap:Modules\Invoices\Services\RecurringInvoiceServiceis an empty stub class — there is no scheduled job that actually generates invoices from due profiles. The existingRecurringInvoicesTest::it_lists_recurring_invoicesis also stubbed out (markTestIncomplete(), references an undefined$invoicevariable) and needs fixing alongside the new work.Measurable
RecurringInvoiceServicegains agenerateDueInvoices(): Collectionmethod that creates anInvoicefor every profile whosestart_athas passed and (end_atis null or in the future).it_generates_an_invoice_for_a_due_recurring_profile()#[Test] public function it_generates_an_invoice_for_a_due_recurring_profile(): void { /* Arrange */ $recurring = RecurringInvoice::factory()->for($this->company)->create([ 'frequency' => RecurringFrequency::MONTHLY->value, 'start_at' => now()->subDay()->toDateString(), 'end_at' => null, ]); /* Act */ app(RecurringInvoiceService::class)->generateDueInvoices(); /* Assert */ $this->assertDatabaseHas('invoices', ['recurring_invoice_id' => $recurring->id]); }end_atis in the past is not picked up.it_skips_recurring_profiles_past_their_end_date()#[Test] public function it_skips_recurring_profiles_past_their_end_date(): void { /* Arrange */ $recurring = RecurringInvoice::factory()->for($this->company)->create([ 'start_at' => now()->subMonth()->toDateString(), 'end_at' => now()->subDay()->toDateString(), ]); /* Act */ app(RecurringInvoiceService::class)->generateDueInvoices(); /* Assert */ $this->assertDatabaseMissing('invoices', ['recurring_invoice_id' => $recurring->id]); }recurringInvoiceItems.it_copies_recurring_line_items_onto_the_generated_invoice()#[Test] public function it_copies_recurring_line_items_onto_the_generated_invoice(): void { /* Arrange */ $recurring = RecurringInvoice::factory()->for($this->company)->create([ 'start_at' => now()->subDay()->toDateString(), ]); RecurringInvoiceItem::factory()->for($recurring, 'recurringInvoice')->create(['name' => 'Subscription A']); /* Act */ $invoice = app(RecurringInvoiceService::class)->generateDueInvoices()->first(); /* Assert */ $this->assertTrue($invoice->items()->where('name', 'Subscription A')->exists()); }generateDueInvoices()twice in the same cycle does not create duplicate invoices for the same profile.it_does_not_duplicate_invoices_when_generation_runs_twice_in_one_cycle()#[Test] public function it_does_not_duplicate_invoices_when_generation_runs_twice_in_one_cycle(): void { /* Arrange */ $recurring = RecurringInvoice::factory()->for($this->company)->create([ 'start_at' => now()->subDay()->toDateString(), ]); /* Act */ app(RecurringInvoiceService::class)->generateDueInvoices(); app(RecurringInvoiceService::class)->generateDueInvoices(); /* Assert */ $this->assertSame(1, Invoice::query()->where('recurring_invoice_id', $recurring->id)->count()); }markTestIncomplete().it_lists_recurring_invoices()(repair existing test inRecurringInvoicesTest)#[Test] #[Group('smoke')] public function it_lists_recurring_invoices(): void { /* Arrange */ $company = $this->user->companies()->first(); $recurring = RecurringInvoice::factory()->for($company)->create(); /* Act */ $component = Livewire::actingAs($this->user) ->test(ListRecurringInvoices::class, ['tenant' => Str::lower($company->search_code)]); /* Assert */ $component->assertSuccessful()->assertCanSeeTableRecords([$recurring]); }Achievable
RecurringInvoiceService::generateDueInvoices(): queryRecurringInvoice::where('start_at', '<=', today())->where(fn ($q) => $q->whereNull('end_at')->orWhere('end_at', '>=', today())), build anInvoice(viaInvoiceService) from each profile'srecurringInvoiceItems, and recordrecurring_invoice_idon the created invoice (add the column via migration if it doesn't already exist oninvoices).last_generated_atcolumn or a check against invoices already created for the current cycle) so re-running the job doesn't duplicate invoices.Modules/Invoices/Providers/) that calls the service daily.RecurringInvoicesTest::it_lists_recurring_invoices— removemarkTestIncomplete(), remove the undefined$invoicereference, and align the payload with the real factory fields (frequency,start_at,end_at,invoice_id).RecurringInvoiceServicetests toModules/Invoices/Tests/Feature/.Relevant
Recurring billing (subscriptions, retainers, maintenance contracts) currently has full CRUD scaffolding but no automation — the core value of "recurring" invoicing (not manually re-creating them) is unimplemented.
Time-bound
Sprint 6.