Skip to content

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

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

[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 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)

❌ Autofix failed (check again to retry)

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
@InvoicePlane InvoicePlane deleted a comment from coderabbitai Bot Aug 1, 2026
@nielsdrost7
nielsdrost7 marked this pull request as draft August 1, 2026 06:53
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

An unexpected error occurred while generating fixes: Not Found - https://docs.github.com/rest/git/refs#get-a-reference

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