Skip to content

[IP-32]: implement payments options on invoice list#684

Open
nielsdrost7 wants to merge 8 commits into
InvoicePlane:developfrom
underdogg-forks:feature/32-invoice-list-enter-payment
Open

[IP-32]: implement payments options on invoice list#684
nielsdrost7 wants to merge 8 commits into
InvoicePlane:developfrom
underdogg-forks:feature/32-invoice-list-enter-payment

Conversation

@nielsdrost7

@nielsdrost7 nielsdrost7 commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

Closes #32

Summary by CodeRabbit

  • New Features

    • Added an inline action to record payments directly from the invoice list.
    • Payment amounts are prefilled with the invoice’s outstanding balance.
    • Invoice statuses now update automatically to Partially Paid or Paid after payment.
    • Payment selection only includes invoices with an outstanding balance and supports invoice number or customer searches.
    • Added a success message confirming that a payment was recorded.
  • Bug Fixes

    • Prevented fully paid and draft invoices from being selected for payment.

Ran vendor/bin/pint on develop: import ordering, binary-operator
spacing, and empty-body brace style. Purely cosmetic, no behavior
change.
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@nielsdrost7, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 59 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 71a37eee-0fa1-4b2b-8538-4770f5515e1c

📥 Commits

Reviewing files that changed from the base of the PR and between 08f53a6 and 80e7422.

📒 Files selected for processing (32)
  • .github/DOCKER.md
  • .gitignore
  • AGENTS.md
  • CLAUDE.md
  • Makefile
  • Modules/Core/Database/Factories/CompanyUserFactory.php
  • Modules/Core/Database/Factories/CustomFieldValueFactory.php
  • Modules/Core/Database/Factories/NoteFactory.php
  • Modules/Core/Tests/Feature/Seeders/RoleHasPermissionsSeederTest.php
  • Modules/Expenses/Filament/Company/Widgets/RecentExpensesWidget.php
  • Modules/Expenses/Observers/ExpenseObserver.php
  • Modules/Invoices/Enums/InvoiceStatus.php
  • Modules/Invoices/Filament/Company/Resources/Invoices/Tables/InvoicesTable.php
  • Modules/Invoices/Models/Invoice.php
  • Modules/Invoices/Tests/Feature/EnterPaymentActionTest.php
  • Modules/Invoices/Tests/Feature/InvoiceDuplicateNumberPreventionTest.php
  • Modules/Invoices/Tests/Feature/RecurringInvoicesTest.php
  • Modules/Payments/Filament/Company/Resources/Payments/Schemas/PaymentForm.php
  • Modules/Payments/Filament/Company/Widgets/RecentPaymentsWidget.php
  • Modules/Payments/Observers/PaymentObserver.php
  • Modules/Payments/Services/PaymentService.php
  • Modules/Payments/Tests/Feature/PaymentInvoiceSelectorTest.php
  • Modules/Projects/Filament/Company/Widgets/RecentProjectsWidget.php
  • Modules/Projects/Filament/Company/Widgets/RecentTasksWidget.php
  • Modules/Quotes/Tests/Feature/QuoteDuplicateNumberPreventionTest.php
  • Modules/Quotes/Tests/Unit/QuoteModelTest.php
  • README.md
  • composer.json
  • docker-compose.yml
  • docker-resources/mariadb/init/01-create-test-db.sql
  • docker-resources/php-cli/Dockerfile
  • resources/lang/en/ip.php
📝 Walkthrough

Walkthrough

Adds payable invoice filtering, outstanding-balance calculation, transactional payment creation, invoice status reconciliation, invoice-list payment action coverage, and payment-form selector tests.

Changes

Invoice payment workflow

Layer / File(s) Summary
Payable invoice contract
Modules/Invoices/Enums/InvoiceStatus.php, Modules/Invoices/Models/Invoice.php
Defines payable statuses, calculates outstanding balances from completed payments, and adds the payable() invoice scope.
Payment entry and reconciliation
Modules/Payments/Services/PaymentService.php, Modules/Invoices/Filament/Company/Resources/Invoices/Tables/InvoicesTable.php, Modules/Invoices/Tests/Feature/EnterPaymentActionTest.php, resources/lang/en/ip.php
Creates payments transactionally, updates invoice statuses for full or partial settlement, and tests payment entry, modal defaults, visibility, and persistence.
Payable invoice selector
Modules/Payments/Filament/Company/Resources/Payments/Schemas/PaymentForm.php, Modules/Payments/Tests/Feature/PaymentInvoiceSelectorTest.php
Limits invoice search results to payable invoices while retaining invoice number and customer company-name filtering, with feature coverage.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant InvoicesTable
  participant PaymentService
  participant Database
  User->>InvoicesTable: Open enter_payment action
  InvoicesTable->>PaymentService: createPayment(payment data)
  PaymentService->>Database: Create payment in transaction
  PaymentService->>Database: Recalculate linked invoice status
  Database-->>InvoicesTable: Payment and invoice updates
  InvoicesTable-->>User: Show successful payment entry
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive The supporting payment workflow is present, but the summary does not clearly show the required enter_payment row action in InvoicesTable. Confirm the enter_payment table action is implemented in InvoicesTable with the modal, prefilled balance, and hidden state for paid invoices.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and matches the main change: adding payment options to the invoice list.
Out of Scope Changes check ✅ Passed All changes support the invoice payment workflow and stay within the stated #32 scope.
📋 Issue Planner

Built with CodeRabbit's Coding Plans for faster development and fewer bugs.

View plan used: #32

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Modules/Payments/Filament/Company/Resources/Payments/Schemas/PaymentForm.php (1)

46-57: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Prevent N+1 query issue in the invoice selector.

By retrieving the results with ->get() and immediately calling ->pluck('invoice_number', 'id'), the loaded models and eager-loaded customer relationship data are discarded. Consequently, the subsequent mapWithKeys block iterates over the array and executes a new find($id) database query for every single returned record.

You can fix this severe N+1 issue by iterating over the collection of models directly.

⚡ Proposed fix for the N+1 query
                                                         return Invoice::with('customer')
                                                             ->payable()
                                                             ->where(fn ($q) => $q
                                                                 ->where('invoice_number', 'like', "%{$search}%")
                                                                 ->orWhereHas('customer', fn ($q) => $q->where('company_name', 'like', "%{$search}%"))
                                                             )
                                                             ->limit(50)
                                                             ->get()
-                                                            ->pluck('invoice_number', 'id')
-                                                            ->mapWithKeys(fn ($number, $id) => [
-                                                                $id => "{$number} – " . Invoice::query()->find($id)->customer?->company_name,
+                                                            ->mapWithKeys(fn (Invoice $invoice) => [
+                                                                $invoice->id => "{$invoice->invoice_number} – " . $invoice->customer?->company_name,
                                                             ])
                                                             ->toArray();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Payments/Filament/Company/Resources/Payments/Schemas/PaymentForm.php`
around lines 46 - 57, Update the invoice selector’s collection transformation
after get() to iterate over the retrieved Invoice models directly instead of
plucking IDs and re-querying with Invoice::query()->find($id). Reuse each
model’s invoice_number and already-loaded customer relationship when building
the keyed options, preserving the existing labels and limit.
🧹 Nitpick comments (1)
Modules/Payments/Services/PaymentService.php (1)

31-34: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove commented-out code.

As a best practice for maintainability, dead or commented-out code should be removed rather than committed.

🧹 Proposed cleanup
-            /* if ($payment->merchant_client_id) {
-                dispatch(new ProcessMerchantPaymentJob($payment));
-            } */
-
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Payments/Services/PaymentService.php` around lines 31 - 34, Remove
the commented-out ProcessMerchantPaymentJob dispatch block from the payment
handling code, leaving the surrounding PaymentService logic unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Modules/Invoices/Models/Invoice.php`:
- Around line 169-180: Update getOutstandingBalanceAttribute to use the loaded
payments relationship collection, filtering it for completed PaymentStatus
values and summing payment_amount without invoking the payments() query builder.
Preserve the existing outstanding-balance calculation and non-negative result.

In `@Modules/Payments/Services/PaymentService.php`:
- Around line 52-69: Update syncInvoiceStatusFromPayments so an invoice with
current status OVERDUE is not changed to PARTIALLY_PAID when paidTotal is below
invoice_total; preserve OVERDUE until the invoice reaches the existing PAID
branch, while retaining current behavior for other statuses.

---

Outside diff comments:
In
`@Modules/Payments/Filament/Company/Resources/Payments/Schemas/PaymentForm.php`:
- Around line 46-57: Update the invoice selector’s collection transformation
after get() to iterate over the retrieved Invoice models directly instead of
plucking IDs and re-querying with Invoice::query()->find($id). Reuse each
model’s invoice_number and already-loaded customer relationship when building
the keyed options, preserving the existing labels and limit.

---

Nitpick comments:
In `@Modules/Payments/Services/PaymentService.php`:
- Around line 31-34: Remove the commented-out ProcessMerchantPaymentJob dispatch
block from the payment handling code, leaving the surrounding PaymentService
logic unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 45799a81-3c0a-4eb9-a713-ecb0f1e33c31

📥 Commits

Reviewing files that changed from the base of the PR and between 16c779b and 08f53a6.

📒 Files selected for processing (8)
  • Modules/Invoices/Enums/InvoiceStatus.php
  • Modules/Invoices/Filament/Company/Resources/Invoices/Tables/InvoicesTable.php
  • Modules/Invoices/Models/Invoice.php
  • Modules/Invoices/Tests/Feature/EnterPaymentActionTest.php
  • Modules/Payments/Filament/Company/Resources/Payments/Schemas/PaymentForm.php
  • Modules/Payments/Services/PaymentService.php
  • Modules/Payments/Tests/Feature/PaymentInvoiceSelectorTest.php
  • resources/lang/en/ip.php

Comment on lines +169 to +180
/**
* The invoice total minus all completed payments recorded against it.
*/
public function getOutstandingBalanceAttribute(): float
{
$paid = $this->payments()
->where('payment_status', \Modules\Payments\Enums\PaymentStatus::COMPLETED->value)
->sum('payment_amount');

return max(0, (float) $this->invoice_total - (float) $paid);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Prevent N+1 queries in the outstanding balance accessor.

Using the payments() query builder relationship method executes a new database query every time this accessor is accessed. If the payments relationship has been eager-loaded, you can prevent N+1 query issues by filtering the loaded collection directly instead of hitting the database again.

⚡ Proposed optimization
     public function getOutstandingBalanceAttribute(): float
     {
-        $paid = $this->payments()
-            ->where('payment_status', \Modules\Payments\Enums\PaymentStatus::COMPLETED->value)
-            ->sum('payment_amount');
+        $paid = $this->relationLoaded('payments')
+            ? $this->payments->where('payment_status', \Modules\Payments\Enums\PaymentStatus::COMPLETED->value)->sum('payment_amount')
+            : $this->payments()
+                ->where('payment_status', \Modules\Payments\Enums\PaymentStatus::COMPLETED->value)
+                ->sum('payment_amount');

         return max(0, (float) $this->invoice_total - (float) $paid);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* The invoice total minus all completed payments recorded against it.
*/
public function getOutstandingBalanceAttribute(): float
{
$paid = $this->payments()
->where('payment_status', \Modules\Payments\Enums\PaymentStatus::COMPLETED->value)
->sum('payment_amount');
return max(0, (float) $this->invoice_total - (float) $paid);
}
/**
* The invoice total minus all completed payments recorded against it.
*/
public function getOutstandingBalanceAttribute(): float
{
$paid = $this->relationLoaded('payments')
? $this->payments->where('payment_status', \Modules\Payments\Enums\PaymentStatus::COMPLETED->value)->sum('payment_amount')
: $this->payments()
->where('payment_status', \Modules\Payments\Enums\PaymentStatus::COMPLETED->value)
->sum('payment_amount');
return max(0, (float) $this->invoice_total - (float) $paid);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Invoices/Models/Invoice.php` around lines 169 - 180, Update
getOutstandingBalanceAttribute to use the loaded payments relationship
collection, filtering it for completed PaymentStatus values and summing
payment_amount without invoking the payments() query builder. Preserve the
existing outstanding-balance calculation and non-negative result.

Comment on lines +52 to 69
protected function syncInvoiceStatusFromPayments(Invoice $invoice): void
{
$paidTotal = $invoice->payments()
->where('payment_status', PaymentStatus::COMPLETED->value)
->sum('payment_amount');

if ((float) $paidTotal <= 0) {
return;
}

if ((float) $paidTotal >= (float) $invoice->invoice_total) {
$invoice->update(['invoice_status' => InvoiceStatus::PAID->value]);

return;
}

$invoice->update(['invoice_status' => InvoiceStatus::PARTIALLY_PAID->value]);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve the OVERDUE invoice status.

This method unconditionally sets the status to PARTIALLY_PAID if the invoice isn't fully paid. Based on the domain rules (and handled correctly by the existing syncInvoiceStatus method), an overdue invoice must remain OVERDUE until it is fully settled.

Please check the current status before making the downgrade to avoid corrupting the aging metrics.

🛡️ Proposed fix
         if ((float) $paidTotal >= (float) $invoice->invoice_total) {
             $invoice->update(['invoice_status' => InvoiceStatus::PAID->value]);

             return;
         }

-        $invoice->update(['invoice_status' => InvoiceStatus::PARTIALLY_PAID->value]);
+        if (in_array($invoice->invoice_status, [InvoiceStatus::SENT, InvoiceStatus::VIEWED], true)) {
+            $invoice->update(['invoice_status' => InvoiceStatus::PARTIALLY_PAID->value]);
+        }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
protected function syncInvoiceStatusFromPayments(Invoice $invoice): void
{
$paidTotal = $invoice->payments()
->where('payment_status', PaymentStatus::COMPLETED->value)
->sum('payment_amount');
if ((float) $paidTotal <= 0) {
return;
}
if ((float) $paidTotal >= (float) $invoice->invoice_total) {
$invoice->update(['invoice_status' => InvoiceStatus::PAID->value]);
return;
}
$invoice->update(['invoice_status' => InvoiceStatus::PARTIALLY_PAID->value]);
}
protected function syncInvoiceStatusFromPayments(Invoice $invoice): void
{
$paidTotal = $invoice->payments()
->where('payment_status', PaymentStatus::COMPLETED->value)
->sum('payment_amount');
if ((float) $paidTotal <= 0) {
return;
}
if ((float) $paidTotal >= (float) $invoice->invoice_total) {
$invoice->update(['invoice_status' => InvoiceStatus::PAID->value]);
return;
}
if (in_array($invoice->invoice_status, [InvoiceStatus::SENT, InvoiceStatus::VIEWED], true)) {
$invoice->update(['invoice_status' => InvoiceStatus::PARTIALLY_PAID->value]);
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Modules/Payments/Services/PaymentService.php` around lines 52 - 69, Update
syncInvoiceStatusFromPayments so an invoice with current status OVERDUE is not
changed to PARTIALLY_PAID when paidTotal is below invoice_total; preserve
OVERDUE until the invoice reaches the existing PAID branch, while retaining
current behavior for other statuses.

…ed_at

Expense, Payment, Project, and Task all set $timestamps = false and have
no created_at column, but the RecentExpensesWidget/RecentPaymentsWidget/
RecentProjectsWidget/RecentTasksWidget dashboard widgets called ->latest()
with no argument, which defaults to ordering by created_at regardless of
$timestamps. MySQL (CI) raises a hard 1054 Unknown column error; SQLite
silently reinterprets the double-quoted "created_at" identifier as a
string literal when it can't resolve it, so the bug never surfaced in
local sqlite-backed test runs.
Local tests silently diverging from CI's MariaDB (via a documented SQLite
.env.testing fallback) has repeatedly masked real bugs this session —
->latest() defaulting to a nonexistent created_at column, and identifier
quoting differences, both passed locally on SQLite and only failed on CI.

- docker-compose.yml: cli service now injects DB_CONNECTION=mysql/DB_HOST=db
  etc. itself and depends_on db, so `docker compose run --rm cli php artisan
  test` works against real MariaDB with zero per-developer .env.testing edits
- Add docker-resources/mariadb/init/01-create-test-db.sql to provision a
  dedicated invoiceplane_test database alongside the dev one on first boot
- Fix db service: the named `database` volume was declared but never
  mounted, so all local dev/test data was lost on every container recreate
- docker-resources/php-cli/Dockerfile: rebuild on Debian (php:8.4-cli) with
  the minimal proven extension set, matching the ip2-test-php:8.4 image this
  session used successfully throughout — see InvoicePlane#689 for a still-open false-
  failure issue found with a fresh cli image build, flagged in the docs
- Update AGENTS.md/CLAUDE.md/README.md/.github/DOCKER.md/Makefile to point
  at the compose db/cli path instead of the SQLite instructions
- Note throughout: use `php artisan test`, not raw vendor/bin/phpunit — the
  two were observed to behave differently for this app's Livewire form tests
@nielsdrost7
nielsdrost7 force-pushed the feature/32-invoice-list-enter-payment branch from 5e50805 to 80e7422 Compare July 24, 2026 11:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Payments]: implement payments options on invoice list

1 participant