Skip to content

[Payments]: implement payments options on invoice list #32

Description

@nielsdrost7

SMART Plan — [Payments] Implement payments options on invoice list

Specific

Add an "Enter Payment" inline table action to InvoicesTable that opens a Filament modal pre-filled with the invoice's ID and its outstanding balance (total minus already-recorded payments). Submitting the modal creates a new Payment record linked to the invoice.

Measurable

  • An "Enter Payment" action appears in the row action group for each invoice in InvoicesTable.
  • Triggering the action opens a modal with the invoice_id fixed and the amount field defaulting to the invoice's outstanding balance.
  • Submitting the modal creates a new Payment record with the correct invoice_id and amount.
  • After payment entry, the invoice's status is updated (e.g. paid if fully settled, partial otherwise).
  • The action is hidden or disabled for invoices that are already fully paid.

Achievable

  1. Add Action::make('enter_payment') with a form([TextInput::make('amount')->default(fn (Invoice $record) => $record->outstanding_balance)]) and a modal to Modules/Invoices/Filament/Company/Resources/Invoices/Tables/InvoicesTable.php.
  2. In the action's action() callback, delegate to Modules/Payments/Services/PaymentService.php to create the payment and update the invoice status.
  3. Add an outstanding_balance accessor to Modules/Invoices/Models/Invoice.php if not already present.
  4. Add PHPUnit tests in Modules/Invoices/Tests/Feature/InvoicesTest.php.

Relevant

Entering payments is one of the most frequent tasks in an invoicing app. Forcing users to navigate away from the invoice list to a separate Payment creation form interrupts the workflow. An inline modal keeps the user in context and reduces clicks.

Time-bound

Sprint 5.


Arrange · Act · Assert

// Modules/Invoices/Tests/Feature/InvoicesTest.php

#[Test]
public function it_opens_enter_payment_modal_with_outstanding_balance_prefilled(): void
{
    /* Arrange */
    $invoice = Invoice::factory()->for($this->company)->create([
        'total'  => 200.00,
        'status' => 'sent',
    ]);
    // No payments yet — outstanding = 200.00

    /* Act */
    $component = Livewire::actingAs($this->user)
        ->test(ListInvoices::class, ['tenant' => Str::lower($this->company->search_code)])
        ->mountTableAction('enter_payment', $invoice);

    /* Assert */
    $component->assertSuccessful();
    $component->assertTableActionDataSet([
        'amount' => 200.00,
    ]);
}

#[Test]
public function it_creates_a_payment_record_when_enter_payment_is_submitted(): void
{
    /* Arrange */
    $invoice = Invoice::factory()->for($this->company)->create([
        'total'  => 150.00,
        'status' => 'sent',
    ]);

    /* Act */
    $component = Livewire::actingAs($this->user)
        ->test(ListInvoices::class, ['tenant' => Str::lower($this->company->search_code)])
        ->mountTableAction('enter_payment', $invoice)
        ->setTableActionData(['amount' => 150.00])
        ->callMountedTableAction();

    /* Assert */
    $component->assertSuccessful();
    $this->assertDatabaseHas('payments', [
        'invoice_id' => $invoice->id,
        'amount'     => 150.00,
    ]);
    $this->assertDatabaseHas('invoices', [
        'id'     => $invoice->id,
        'status' => 'paid',
    ]);
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    refinedRefined with SMART plan / test suggestions

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions