Skip to content

[Invoices]: Epic Recurring-Invoices #52

Open

Description

@nielsdrost7

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

  1. added this to the v2.0.0-alpha milestone on Aug 9, 2025
  2. changed the title [-][Invoices]: Recurring-Invoices[/-] [+][Invoices]: Epic Recurring-Invoices[/+] on Jun 13, 2026
  3. nielsdrost7 commented on Jul 16, 2026

    @nielsdrost7
    CollaboratorAuthor

    SMART Plan — Recurring invoice profiles

    Context/Epic: Invoices

    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/RecurringInvoiceItem models, 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\RecurringInvoiceService is an empty stub class — there is no scheduled job that actually generates invoices from due profiles. The existing RecurringInvoicesTest::it_lists_recurring_invoices is also stubbed out (markTestIncomplete(), references an undefined $invoice variable) and needs fixing alongside the new work.

    Measurable

    • RecurringInvoiceService gains a generateDueInvoices(): Collection method that creates an Invoice for every profile whose start_at has passed and (end_at is null or in the future).
      • PHPUnit: 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]);
        }
    • A profile whose end_at is in the past is not picked up.
      • PHPUnit: 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]);
        }
    • Line items on the generated invoice match the recurring profile's recurringInvoiceItems.
      • PHPUnit: 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());
        }
    • Running generateDueInvoices() twice in the same cycle does not create duplicate invoices for the same profile.
      • PHPUnit: 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());
        }
    • The existing stubbed list test is fixed and passes without markTestIncomplete().
      • PHPUnit: it_lists_recurring_invoices() (repair existing test in RecurringInvoicesTest)
        #[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

    1. Implement RecurringInvoiceService::generateDueInvoices(): query RecurringInvoice::where('start_at', '<=', today())->where(fn ($q) => $q->whereNull('end_at')->orWhere('end_at', '>=', today())), build an Invoice (via InvoiceService) from each profile's recurringInvoiceItems, and record recurring_invoice_id on the created invoice (add the column via migration if it doesn't already exist on invoices).
    2. Add an idempotency guard (e.g. a last_generated_at column or a check against invoices already created for the current cycle) so re-running the job doesn't duplicate invoices.
    3. Register a scheduled console command in the module provider (Modules/Invoices/Providers/) that calls the service daily.
    4. Fix RecurringInvoicesTest::it_lists_recurring_invoices — remove markTestIncomplete(), remove the undefined $invoice reference, and align the payload with the real factory fields (frequency, start_at, end_at, invoice_id).
    5. Add the new RecurringInvoiceService tests to Modules/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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    refinedRefined with SMART plan / test suggestions

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions