Skip to content

refactor: remove dead code and fix latent bugs found in a full codebase review#640

Merged
nielsdrost7 merged 6 commits into
InvoicePlane:developfrom
underdogg-forks:fix/dead-code-review-and-test-hygiene
Jul 18, 2026
Merged

refactor: remove dead code and fix latent bugs found in a full codebase review#640
nielsdrost7 merged 6 commits into
InvoicePlane:developfrom
underdogg-forks:fix/dead-code-review-and-test-hygiene

Conversation

@nielsdrost7

@nielsdrost7 nielsdrost7 commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

Full review of develop across all 8 modules (Core, Clients, Invoices, Quotes, Payments, Products, Projects, Expenses) for dead code, SOLID/architecture violations, and test quality.

  • Removed ~50 dead-code files: unused services superseded by their real replacements, orphaned models, unused traits, and event/listener pairs that were declared but never dispatched.
  • Fixed several genuine production bugs surfaced along the way, notably RecurringInvoiceService being an empty stub (recurring invoice generation was silently a no-op), and multiple factories producing schema-invalid rows or referencing a stale DocumentGroupNumbering rename.
  • Consolidated a duplicate CompanyService/CompaniesService pair onto one service; converted RolesService/TaskService from app()-in-method-body calls to constructor injection.
  • Fixed below-par tests (a vacuous assertTrue(true) placeholder, a test asserting on an unreachable code path) and tagged genuinely environment-dependent tests as #[Group('failing')] / #[Group('flaky')] (missing Vite manifest, missing dompdf vendor tree in the CI image, one timing-sensitive timezone test) instead of leaving them to fail every run.
  • phpunit.xml now excludes failing/flaky/troubleshooting groups natively; Makefile's --exclude-group flags updated to match everywhere, plus a pre-existing missing line-continuation backslash in the ci target fixed.
  • Also reconciles this fork's develop with a few gitignore/pint-only commits that had drifted onto origin/develop but were never in this upstream repo.

Test plan

  • Full suite via ip2-test-php:8.4: 478 tests, 0 failures, 0 errors, 9 incomplete (deliberate markTestIncomplete()).
  • Targeted re-run of tests touched by the fork-reconciliation merge (InvoicesTest, UsersTest, LoginResponseTest, UserProfileTest): 50 tests, 0 failures.

Summary by CodeRabbit

  • New Features

    • Invoice, quote, and payment forms now prefill their relevant dates with today’s date.
    • Recurring invoices now support invoice and numbering selections with streamlined required fields.
    • Administrators can delete companies through the company management interface.
    • Module configuration is loaded consistently across supported areas.
  • Bug Fixes

    • Improved invoice, quote, expense, payment, and audit test data generation for more reliable related records.
    • Corrected expense rollback behavior and tax-rate handling in quote and invoice item creation.
  • Chores

    • Removed unused legacy components and streamlined automated test execution.

nielsdrost7 and others added 6 commits July 17, 2026 13:47
…se review

Full sweep across all 8 modules for dead code, SOLID violations, and
below-par tests, per the senior-laravel-developer-code-reviewer /
phpunit-test-naming / test-honesty skills.

Dead code removed (~50 files): unused services (CompaniesService,
InvoiceNumberService, MailerService, PdfGenerationService, and others
superseded by their real replacements), orphaned models (Addon, Import,
ImportDetail, LineItem, ClientCustom, Addressable, CustomerNote,
MerchantPayment), unused traits (FormatsCurrency, WithAdminUser,
WithUserCompany), unreferenced event/listener pairs in Products and
Projects that were never dispatched, and assorted dead support classes.

Production bugs fixed:
- RecurringInvoiceService was an empty stub; recurring invoice
  generation was silently a no-op.
- Several factories (Invoice/Quote items, Expense items, Payments,
  Core support models) produced schema-invalid rows or referenced
  columns/relations that no longer exist (stale DocumentGroup ->
  Numbering rename).
- CompanyService/CompaniesService duplication consolidated onto one
  service; RolesService and TaskService converted from app()-in-method
  calls to constructor injection.

Tests: fixed below-par tests (vacuous assertTrue(true) placeholder,
tests asserting on dead/unreachable code paths) and tagged genuinely
environment-dependent tests as #[Group('failing')] or
#[Group('flaky')] (missing Vite manifest, missing dompdf vendor tree
in the CI image, and one timing-sensitive timezone test) rather than
leaving them to fail every run. phpunit.xml now excludes
failing/flaky/troubleshooting natively; Makefile's exclude-group flags
updated to match everywhere (_phpunit, _artisan, docker-test, ci), and
a pre-existing missing line-continuation backslash in the `ci` target
is fixed.

Full suite: 478 tests, 0 failures, 0 errors, 9 incomplete (Docker,
ip2-test-php:8.4 image).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
origin/develop carried 4 commits (two .gitignore tweaks, a pint run,
and a merge of upstream/develop) that were never in true
upstream/develop and were therefore missing from this branch's base.

# Conflicts:
#	.gitignore
#	Modules/Invoices/Tests/Feature/InvoicesTest.php
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The pull request removes unused module artifacts, updates recurring invoice and factory implementations, adjusts service/provider wiring, corrects several model and migration mappings, and excludes selected PHPUnit groups from standard test commands.

Changes

Application and test updates

Layer / File(s) Summary
Test execution and validation
Makefile, phpunit.xml, Modules/.../Tests/*
Adds flaky to PHPUnit exclusions, groups environment-dependent tests, tightens date assertions, and updates recurring invoice and payment fixtures.
Recurring invoice flow
Modules/Invoices/...
Reworks recurring invoice relationships, factories, forms, services, provider registration, and feature-test payloads around invoice_id and numbering_id.
Relational factories and defaults
Modules/Core/Database/Factories/*, Modules/Invoices/..., Modules/Quotes/..., Modules/Payments/...
Factories now create linked company, invoice, relation, tax, and user records; invoice, quote, payment, and expense forms receive date defaults.
Service and provider wiring
Modules/Core/Services/*, Modules/Projects/Services/*, Modules/*/Providers/*, Modules/Core/Filament/*
Adds transactional company deletion, injects service dependencies, registers module configuration, updates observer imports, and changes admin actions.
Module cleanup and corrections
Modules/Clients/*, Modules/Payments/*, Modules/Products/*, Modules/Projects/*, Modules/Expenses/*
Removes unused models, events, observers, repositories, helper APIs, scopes, and service shells while correcting expense and project behavior.
Repository metadata
.gitignore, CLAUDE.md
Adds ignored issue markdown files and removes obsolete model documentation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested labels: codex

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.51% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the PR’s main theme: a broad refactor removing dead code and fixing bugs discovered during review.
✨ 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: 6

🧹 Nitpick comments (2)
Modules/Clients/Listeners/CustomerWasCreatedListener.php (1)

5-11: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Clean up the no-op event listeners.

Both listeners now have correct event type-hints but empty handle() methods. Remove each obsolete event/listener pair and its registrations, or implement the intended event handling:

  • Modules/Clients/Listeners/CustomerWasCreatedListener.php#L5-L11: remove or implement the CustomerWasCreated listener.
  • Modules/Clients/Listeners/CustomerWasUpdatedListener.php#L5-L11: remove or implement the CustomerWasUpdated listener.
🤖 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/Clients/Listeners/CustomerWasCreatedListener.php` around lines 5 -
11, Remove the no-op CustomerWasCreatedListener and CustomerWasUpdatedListener
classes, along with their event/listener registrations, unless the intended
handling behavior is defined and can be implemented in their handle methods.
Apply the cleanup to Modules/Clients/Listeners/CustomerWasCreatedListener.php
lines 5-11 and Modules/Clients/Listeners/CustomerWasUpdatedListener.php lines
5-11; ensure neither obsolete listener remains registered.

Source: Linters/SAST tools

Modules/Invoices/Filament/Company/Resources/Invoices/Schemas/InvoiceForm.php (1)

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

Use a closure for dynamic default dates.

Passing now() directly evaluates the timestamp at the time the file or schema is booted. In long-running execution environments like Laravel Octane, this value can become stale for subsequent requests. Wrapping it in a closure ensures the time is evaluated dynamically exactly when the form is instantiated.

  • Modules/Invoices/Filament/Company/Resources/Invoices/Schemas/InvoiceForm.php#L111-L111: Wrap now() in a closure: ->default(fn () => now()).
  • Modules/Quotes/Filament/Company/Resources/Quotes/Schemas/QuoteForm.php#L116-L116: Wrap now() in a closure: ->default(fn () => now()).
  • Modules/Payments/Filament/Company/Resources/Payments/Schemas/PaymentForm.php#L87-L87: Wrap now() in a closure: ->default(fn () => now()).
🤖 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/Filament/Company/Resources/Invoices/Schemas/InvoiceForm.php`
at line 111, Replace the eager now() defaults with closures so timestamps are
evaluated when each form is instantiated: update InvoiceForm.php lines 111,
QuoteForm.php line 116, and PaymentForm.php line 87 to use a dynamic default
closure while preserving the existing date fields and form behavior.
🤖 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 @.gitignore:
- Line 117: Update the ignore pattern for saving some issues.md by removing the
literal double quotes and escaping the spaces so Git ignores the intended
filename.

In `@CLAUDE.md`:
- Line 28: Update the non-standard primary-key model guidance in CLAUDE.md and
the corresponding entries in AGENTS.md and
.claude/skills/non-standard-pks/SKILL.md so ClientCustom and Import are
consistently removed or reflect the current confirmed model set. Keep all three
documents synchronized.

In `@Modules/Core/Database/Factories/AuditLogFactory.php`:
- Line 15: Prevent null dereferences in the user lookup by updating the user ID
assignments in Modules/Core/Database/Factories/AuditLogFactory.php line 15 and
Modules/Core/Database/Factories/CompanyUserFactory.php line 19: use nullable
access for the random User query and create a User via User::factory() when no
existing record is found.

In `@Modules/Core/Services/CompanyService.php`:
- Around line 68-83: Update deleteCompany to return the deleted Company Eloquent
model instead of casting the delete result to a boolean, matching the return
contract used by deleteUser and deleteTask. Preserve the existing transaction,
commit, rollback, and exception behavior.

In `@Modules/Invoices/Services/RecurringInvoiceService.php`:
- Around line 29-41: Update updateRecurringInvoice so missing fields retain
their existing model values while explicitly provided null values are persisted.
Build the update payload using array_intersect_key against the allowed
invoice_id, numbering_id, frequency, start_at, and end_at fields, then merge it
with the current model values before calling update.

In `@Modules/Quotes/Database/Factories/QuoteFactory.php`:
- Around line 86-88: Update the TaxRate factory creation in QuoteFactory to use
the company relationship via ->for($company) instead of assigning company_id
through ->state(). Reuse the existing company model variable associated with the
quote, preserving the created tax rate’s company scope.

---

Nitpick comments:
In `@Modules/Clients/Listeners/CustomerWasCreatedListener.php`:
- Around line 5-11: Remove the no-op CustomerWasCreatedListener and
CustomerWasUpdatedListener classes, along with their event/listener
registrations, unless the intended handling behavior is defined and can be
implemented in their handle methods. Apply the cleanup to
Modules/Clients/Listeners/CustomerWasCreatedListener.php lines 5-11 and
Modules/Clients/Listeners/CustomerWasUpdatedListener.php lines 5-11; ensure
neither obsolete listener remains registered.

In
`@Modules/Invoices/Filament/Company/Resources/Invoices/Schemas/InvoiceForm.php`:
- Line 111: Replace the eager now() defaults with closures so timestamps are
evaluated when each form is instantiated: update InvoiceForm.php lines 111,
QuoteForm.php line 116, and PaymentForm.php line 87 to use a dynamic default
closure while preserving the existing date fields and form behavior.
🪄 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: ba90aa41-7bf1-4d10-9f90-4c41388febde

📥 Commits

Reviewing files that changed from the base of the PR and between 0259088 and da4c38c.

📒 Files selected for processing (135)
  • .gitignore
  • CLAUDE.md
  • Makefile
  • Modules/Clients/Database/Factories/AddressableFactory.php
  • Modules/Clients/Database/Factories/CustomerFactory.php
  • Modules/Clients/Events/RelationCreating.php
  • Modules/Clients/Http/Requests/API/ClientAPIRequest.php
  • Modules/Clients/Listeners/CustomerWasCreatedListener.php
  • Modules/Clients/Listeners/CustomerWasUpdatedListener.php
  • Modules/Clients/Models/Addressable.php
  • Modules/Clients/Models/ClientCustom.php
  • Modules/Clients/Models/Contact.php
  • Modules/Clients/Models/CustomerNote.php
  • Modules/Clients/Models/Relation.php
  • Modules/Clients/Observers/CustomerObserver.php
  • Modules/Clients/Observers/RelationObserver.php
  • Modules/Clients/Providers/ClientsServiceProvider.php
  • Modules/Clients/Services/CustomerService.php
  • Modules/Clients/Support/CustomerNumberGenerator.php
  • Modules/Core/Database/Factories/AuditLogFactory.php
  • Modules/Core/Database/Factories/CompanyUserFactory.php
  • Modules/Core/Database/Factories/CustomFieldFactory.php
  • Modules/Core/Database/Factories/CustomFieldValueFactory.php
  • Modules/Core/Database/Factories/MailQueueFactory.php
  • Modules/Core/Database/Factories/NoteFactory.php
  • Modules/Core/Database/Factories/UploadFactory.php
  • Modules/Core/Filament/Admin/Resources/Companies/Pages/ListCompanies.php
  • Modules/Core/Filament/Admin/Resources/Companies/Tables/CompaniesTable.php
  • Modules/Core/Filament/Admin/Resources/MailQueues/Pages/ListMailQueues.php
  • Modules/Core/Filament/Admin/Resources/Users/UserResource.php
  • Modules/Core/Filament/Company/Components/AbstractCreateDocument.php
  • Modules/Core/Filament/Company/Pages/Auth/EditProfile.php
  • Modules/Core/Models/Addon.php
  • Modules/Core/Models/Company.php
  • Modules/Core/Models/Import.php
  • Modules/Core/Models/ImportDetail.php
  • Modules/Core/Models/LineItem.php
  • Modules/Core/Models/NoteTemplate.php
  • Modules/Core/Providers/CoreServiceProvider.php
  • Modules/Core/Services/CompaniesService.php
  • Modules/Core/Services/CompanyService.php
  • Modules/Core/Services/InvoiceNumberService.php
  • Modules/Core/Services/InvoiceTaxRateService.php
  • Modules/Core/Services/MailBuilder.php
  • Modules/Core/Services/MailQueueService.php
  • Modules/Core/Services/MailerService.php
  • Modules/Core/Services/NoteService.php
  • Modules/Core/Services/PdfGenerationService.php
  • Modules/Core/Services/QrCodeGeneratorService.php
  • Modules/Core/Services/QuoteNumberService.php
  • Modules/Core/Services/RolesService.php
  • Modules/Core/Services/TemplateParserService.php
  • Modules/Core/Services/UserService.php
  • Modules/Core/Support/Calculators/Interfaces/PayableInterface.php
  • Modules/Core/Support/CustomFields.php
  • Modules/Core/Support/FileNames.php
  • Modules/Core/Support/Languages.php
  • Modules/Core/Support/Results/SourceInterface.php
  • Modules/Core/Support/Statuses/InvoiceStatuses.php
  • Modules/Core/Support/Statuses/QuoteStatuses.php
  • Modules/Core/Tests/AbstractCompanyPanelTestCase.php
  • Modules/Core/Tests/Concerns/AssertsDatabaseRecords.php
  • Modules/Core/Tests/Feature/CompanyPanelAdminLinkTest.php
  • Modules/Core/Tests/Feature/LoginResponseTest.php
  • Modules/Core/Tests/Feature/NumberingPanelAccessTest.php
  • Modules/Core/Tests/Feature/SidebarQuickCreateItemAdminPanelRegressionTest.php
  • Modules/Core/Tests/Feature/SidebarQuickCreateItemTest.php
  • Modules/Core/Tests/Feature/UserProfileTest.php
  • Modules/Core/Tests/Feature/UserServiceAvatarTest.php
  • Modules/Core/Tests/Feature/UsersTest.php
  • Modules/Core/Tests/Unit/DateFieldAutoPopulationTest.php
  • Modules/Core/Traits/FormatsCurrency.php
  • Modules/Core/Traits/WithAdminUser.php
  • Modules/Core/Traits/WithUserCompany.php
  • Modules/Expenses/Database/Factories/ExpenseItemFactory.php
  • Modules/Expenses/Database/Migrations/2013_01_01_000036_create_expense_items_table.php
  • Modules/Expenses/Filament/Company/Resources/Expenses/Schemas/ExpenseForm.php
  • Modules/Expenses/Models/ExpenseItem.php
  • Modules/Expenses/Observers/ExpenseObserver.php
  • Modules/Expenses/Providers/ExpensesServiceProvider.php
  • Modules/Expenses/Services/ExpenseCategoryService.php
  • Modules/Invoices/Database/Factories/InvoiceItemFactory.php
  • Modules/Invoices/Database/Factories/RecurringInvoiceFactory.php
  • Modules/Invoices/Enums/Frequency.php
  • Modules/Invoices/Filament/Company/Resources/Invoices/Schemas/InvoiceForm.php
  • Modules/Invoices/Filament/Company/Resources/RecurringInvoices/Schemas/RecurringInvoiceForm.php
  • Modules/Invoices/Models/InvoiceItem.php
  • Modules/Invoices/Models/RecurringInvoice.php
  • Modules/Invoices/Observers/RecurringInvoiceItemObserver.php
  • Modules/Invoices/Observers/RecurringInvoiceObserver.php
  • Modules/Invoices/Providers/InvoicesServiceProvider.php
  • Modules/Invoices/Services/CreditInvoiceService.php
  • Modules/Invoices/Services/InvoiceCustomerSwitchService.php
  • Modules/Invoices/Services/InvoiceTemplateService.php
  • Modules/Invoices/Services/RecurringInvoiceService.php
  • Modules/Invoices/Services/SumexService.php
  • Modules/Invoices/Tests/Feature/InvoicePdfAndCreditNoteTest.php
  • Modules/Invoices/Tests/Feature/RecurringInvoicesTest.php
  • Modules/Payments/Database/Factories/PaymentFactory.php
  • Modules/Payments/Enums/PayableType.php
  • Modules/Payments/Filament/Company/Resources/Payments/Schemas/PaymentForm.php
  • Modules/Payments/Models/MerchantClient.php
  • Modules/Payments/Models/MerchantPayment.php
  • Modules/Payments/Observers/PaymentObserver.php
  • Modules/Payments/Providers/PaymentsServiceProvider.php
  • Modules/Payments/Services/PaymentService.php
  • Modules/Payments/Support/PaymentNumberGenerator.php
  • Modules/Payments/Tests/Feature/PaymentsTest.php
  • Modules/Products/Events/ProductWasCreated.php
  • Modules/Products/Events/ProductWasUpdated.php
  • Modules/Products/Listeners/ProductWasCreatedListener.php
  • Modules/Products/Listeners/ProductWasUpdatedListener.php
  • Modules/Products/Models/LineItem.php
  • Modules/Products/Providers/ProductsServiceProvider.php
  • Modules/Products/Services/ProductService.php
  • Modules/Projects/Events/ProjectWasCreated.php
  • Modules/Projects/Events/ProjectWasUpdated.php
  • Modules/Projects/Events/TaskWasCreated.php
  • Modules/Projects/Events/TaskWasUpdated.php
  • Modules/Projects/Listeners/ProjectWasCreatedListener.php
  • Modules/Projects/Listeners/ProjectWasUpdatedListener.php
  • Modules/Projects/Listeners/TaskWasCreatedListener.php
  • Modules/Projects/Listeners/TaskWasUpdatedListener.php
  • Modules/Projects/Models/Project.php
  • Modules/Projects/Providers/ProjectsServiceProvider.php
  • Modules/Projects/Repositories/ProjectRepository.php
  • Modules/Projects/Services/ProjectService.php
  • Modules/Projects/Services/TaskService.php
  • Modules/Quotes/Database/Factories/QuoteFactory.php
  • Modules/Quotes/Database/Factories/QuoteItemFactory.php
  • Modules/Quotes/Filament/Company/Resources/Quotes/Schemas/QuoteForm.php
  • Modules/Quotes/Observers/QuoteItemObserver.php
  • Modules/Quotes/Tests/Unit/QuoteModelTest.php
  • audit-report.json
  • phpunit.xml
💤 Files with no reviewable changes (80)
  • Modules/Core/Traits/WithUserCompany.php
  • Modules/Products/Events/ProductWasUpdated.php
  • Modules/Core/Traits/FormatsCurrency.php
  • Modules/Products/Events/ProductWasCreated.php
  • Modules/Core/Services/QrCodeGeneratorService.php
  • Modules/Core/Support/Languages.php
  • Modules/Core/Services/MailBuilder.php
  • Modules/Core/Filament/Company/Components/AbstractCreateDocument.php
  • Modules/Projects/Listeners/ProjectWasUpdatedListener.php
  • Modules/Products/Services/ProductService.php
  • Modules/Core/Support/Results/SourceInterface.php
  • Modules/Invoices/Enums/Frequency.php
  • Modules/Clients/Http/Requests/API/ClientAPIRequest.php
  • Modules/Clients/Database/Factories/AddressableFactory.php
  • Modules/Core/Support/Calculators/Interfaces/PayableInterface.php
  • Modules/Core/Services/TemplateParserService.php
  • Modules/Clients/Database/Factories/CustomerFactory.php
  • Modules/Core/Services/CompaniesService.php
  • Modules/Core/Models/Import.php
  • Modules/Core/Services/PdfGenerationService.php
  • Modules/Clients/Models/ClientCustom.php
  • Modules/Invoices/Services/InvoiceTemplateService.php
  • Modules/Invoices/Services/CreditInvoiceService.php
  • Modules/Products/Listeners/ProductWasCreatedListener.php
  • Modules/Payments/Support/PaymentNumberGenerator.php
  • Modules/Core/Traits/WithAdminUser.php
  • Modules/Products/Models/LineItem.php
  • Modules/Expenses/Filament/Company/Resources/Expenses/Schemas/ExpenseForm.php
  • Modules/Core/Services/InvoiceTaxRateService.php
  • Modules/Projects/Services/ProjectService.php
  • Modules/Core/Services/NoteService.php
  • Modules/Projects/Listeners/ProjectWasCreatedListener.php
  • Modules/Payments/Enums/PayableType.php
  • Modules/Projects/Events/ProjectWasCreated.php
  • Modules/Core/Models/LineItem.php
  • Modules/Payments/Models/MerchantPayment.php
  • Modules/Payments/Observers/PaymentObserver.php
  • Modules/Clients/Models/Addressable.php
  • Modules/Core/Support/Statuses/QuoteStatuses.php
  • Modules/Products/Listeners/ProductWasUpdatedListener.php
  • Modules/Core/Services/InvoiceNumberService.php
  • Modules/Core/Models/Company.php
  • Modules/Projects/Models/Project.php
  • Modules/Clients/Observers/CustomerObserver.php
  • Modules/Core/Services/QuoteNumberService.php
  • Modules/Core/Services/MailerService.php
  • Modules/Clients/Support/CustomerNumberGenerator.php
  • Modules/Core/Support/CustomFields.php
  • Modules/Invoices/Services/InvoiceCustomerSwitchService.php
  • Modules/Core/Support/Statuses/InvoiceStatuses.php
  • Modules/Invoices/Services/SumexService.php
  • Modules/Projects/Events/ProjectWasUpdated.php
  • audit-report.json
  • Modules/Quotes/Observers/QuoteItemObserver.php
  • Modules/Products/Providers/ProductsServiceProvider.php
  • Modules/Payments/Services/PaymentService.php
  • Modules/Projects/Listeners/TaskWasUpdatedListener.php
  • Modules/Core/Services/MailQueueService.php
  • Modules/Projects/Events/TaskWasUpdated.php
  • Modules/Clients/Services/CustomerService.php
  • Modules/Projects/Providers/ProjectsServiceProvider.php
  • Modules/Clients/Models/Relation.php
  • Modules/Clients/Events/RelationCreating.php
  • Modules/Projects/Listeners/TaskWasCreatedListener.php
  • Modules/Invoices/Observers/RecurringInvoiceItemObserver.php
  • Modules/Core/Support/FileNames.php
  • Modules/Core/Models/ImportDetail.php
  • Modules/Invoices/Observers/RecurringInvoiceObserver.php
  • Modules/Clients/Models/CustomerNote.php
  • Modules/Invoices/Models/InvoiceItem.php
  • Modules/Expenses/Observers/ExpenseObserver.php
  • Modules/Clients/Observers/RelationObserver.php
  • Modules/Projects/Repositories/ProjectRepository.php
  • Modules/Payments/Models/MerchantClient.php
  • Modules/Core/Models/Addon.php
  • Modules/Payments/Providers/PaymentsServiceProvider.php
  • Modules/Clients/Models/Contact.php
  • Modules/Core/Tests/Concerns/AssertsDatabaseRecords.php
  • Modules/Projects/Events/TaskWasCreated.php
  • Modules/Clients/Providers/ClientsServiceProvider.php

Comment thread .gitignore
actual-real-resolved-issues.md
current-issues.md
merge-order.md
"saving some issues.md"

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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== .gitignore context =="
nl -ba .gitignore | sed -n '108,124p'

echo
echo "== gitignore test =="
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
mkdir -p "$tmpdir/subdir"
: > "$tmpdir/saving some issues.md"
: > "$tmpdir/\"saving some issues.md\""
: > "$tmpdir/subdir/saving some issues.md"

git -C "$PWD" check-ignore -v -- "$tmpdir/saving some issues.md" "$tmpdir/\"saving some issues.md\"" "$tmpdir/subdir/saving some issues.md" || true

Repository: InvoicePlane/InvoicePlane-v2

Length of output: 231


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== .gitignore context =="
awk 'NR>=108 && NR<=124 { printf "%d:%s\n", NR, $0 }' .gitignore

echo
echo "== gitignore test =="
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
mkdir -p "$tmpdir/subdir"
: > "$tmpdir/saving some issues.md"
: > "$tmpdir/\"saving some issues.md\""
: > "$tmpdir/subdir/saving some issues.md"

# Show how Git matches the pattern from the repo root
git check-ignore -v -- "$tmpdir/saving some issues.md" "$tmpdir/\"saving some issues.md\"" "$tmpdir/subdir/saving some issues.md" || true

Repository: InvoicePlane/InvoicePlane-v2

Length of output: 613


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== .gitignore line =="
awk 'NR==117 { printf "%d:%s\n", NR, $0 }' .gitignore

echo
echo "== gitignore matching =="
printf '%s\n' \
  'saving some issues.md' \
  '"saving some issues.md"' \
  'subdir/saving some issues.md' \
| git check-ignore -v --stdin || true

echo
echo "== files with matching basename in repo =="
git ls-files | grep -F 'saving some issues.md' || true

Repository: InvoicePlane/InvoicePlane-v2

Length of output: 286


🌐 Web query:

gitignore double quotes literal characters pattern spaces quoted filenames documentation

💡 Result:

In gitignore files, double quotes do not have special meaning and are treated as literal characters [1]. Git patterns do not support enclosing filenames in double quotes to handle spaces or special characters [1]. To include spaces or special characters in a gitignore pattern, use a backslash () to escape them [2][3][4]. Specific rules regarding characters and spaces: 1. Spaces: Trailing spaces in a pattern are ignored unless they are escaped with a backslash [2][4]. To match a file or directory with a space, such as "my folder", you should write the pattern as "my\ folder" [1]. 2. Escaping: A backslash () can be used to escape any character to treat it as a literal [2][3]. For example, if you need to match a literal asterisk or exclamation mark, use * or ! [2][4]. 3. Double Quotes: If you place double quotes around a pattern, such as "file name", Git will look for a file that literally contains those double quotes in its name, rather than interpreting them as shell-style quotes for the filename [1].

Citations:


Remove the quotes from this ignore pattern. .gitignore treats double quotes as literal characters, so this only matches a filename that actually contains quotes. Use saving\ some\ issues.md instead.

🤖 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 @.gitignore at line 117, Update the ignore pattern for saving some issues.md
by removing the literal double quotes and escaping the spaces so Git ignores the
intended filename.

Comment thread CLAUDE.md
|--------|-----------|
| Core | User, Company, CompanyUser, TaxRate, Numbering, EmailTemplate, CustomField, Upload, Note, AuditLog, Setting, MailQueue |
| Clients | Relation (table: `relations`), Contact, Address, Communication, ClientCustom (`PK: client_custom_id`) |
| Clients | Relation (table: `relations`), Contact, Address, Communication |

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== CLAUDE.md ==\n'
nl -ba CLAUDE.md | sed -n '1,80p'

printf '\n== AGENTS.md ==\n'
nl -ba AGENTS.md | sed -n '120,150p'

printf '\n== .claude/skills/non-standard-pks/SKILL.md ==\n'
nl -ba .claude/skills/non-standard-pks/SKILL.md | sed -n '1,120p'

Repository: InvoicePlane/InvoicePlane-v2

Length of output: 223


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== CLAUDE.md ==\n'
sed -n '1,80p' CLAUDE.md | cat -n

printf '\n== AGENTS.md ==\n'
sed -n '120,150p' AGENTS.md | cat -n

printf '\n== .claude/skills/non-standard-pks/SKILL.md ==\n'
sed -n '1,120p' .claude/skills/non-standard-pks/SKILL.md | cat -n

printf '\n== exact string search ==\n'
rg -n "ClientCustom::\\$primaryKey = 'client_custom_id'|Import::\\$primaryKey = 'import_id'|non-standard-pks|primaryKey" .

Repository: InvoicePlane/InvoicePlane-v2

Length of output: 7990


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== CLAUDE.md around line 248 ==\n'
sed -n '236,256p' CLAUDE.md | cat -n

printf '\n== literal PK search ==\n'
rg -n -F "ClientCustom::$primaryKey = 'client_custom_id'" AGENTS.md .claude/skills/non-standard-pks/SKILL.md CLAUDE.md || true
rg -n -F "Import::$primaryKey = 'import_id'" AGENTS.md .claude/skills/non-standard-pks/SKILL.md CLAUDE.md || true

Repository: InvoicePlane/InvoicePlane-v2

Length of output: 1028


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== model definitions ==\n'
rg -n -F "class ClientCustom" Modules app database . || true
rg -n -F "class Import" Modules app database . || true

printf '\n== primary key declarations ==\n'
rg -n 'protected\s+\$primaryKey\s*=\s*'\''client_custom_id'\''' Modules app database . || true
rg -n 'protected\s+\$primaryKey\s*=\s*'\''import_id'\''' Modules app database . || true

Repository: InvoicePlane/InvoicePlane-v2

Length of output: 223


Keep the non-standard PK docs in sync. CLAUDE.md no longer lists ClientCustom/Import, but AGENTS.md and .claude/skills/non-standard-pks/SKILL.md still treat them as confirmed custom-PK models. Remove or update those entries so the guidance matches the current model set.

🤖 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 `@CLAUDE.md` at line 28, Update the non-standard primary-key model guidance in
CLAUDE.md and the corresponding entries in AGENTS.md and
.claude/skills/non-standard-pks/SKILL.md so ClientCustom and Import are
consistently removed or reflect the current confirmed model set. Keep all three
documents synchronized.

public function definition(): array
{
return [
'audit_id' => User::query()->inRandomOrder()->first()->id,

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Prevent potential Null Pointer Exception when querying random records.

Directly accessing ->id on the result of first() will cause a fatal error if the users table happens to be empty (e.g., during tests). Provide a safe fallback by creating a new User record on the fly when no existing records are found.

  • Modules/Core/Database/Factories/AuditLogFactory.php#L15-L15: Update to User::query()->inRandomOrder()->first()?->id ?? User::factory()->create()->id
  • Modules/Core/Database/Factories/CompanyUserFactory.php#L19-L19: Update to User::query()->inRandomOrder()->first()?->id ?? User::factory()->create()->id
📍 Affects 2 files
  • Modules/Core/Database/Factories/AuditLogFactory.php#L15-L15 (this comment)
  • Modules/Core/Database/Factories/CompanyUserFactory.php#L19-L19
🤖 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/Core/Database/Factories/AuditLogFactory.php` at line 15, Prevent null
dereferences in the user lookup by updating the user ID assignments in
Modules/Core/Database/Factories/AuditLogFactory.php line 15 and
Modules/Core/Database/Factories/CompanyUserFactory.php line 19: use nullable
access for the random User query and create a User via User::factory() when no
existing record is found.

Comment on lines +68 to +83

public function deleteCompany(Company $company): bool
{
DB::beginTransaction();

try {
$deleted = (bool) $company->delete();

DB::commit();

return $deleted;
} catch (Throwable $e) {
DB::rollBack();
throw $e;
}
}

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Return the Eloquent model instead of a boolean.

The service method returns a boolean, which violates the coding guidelines for services: "All services must extend Modules\Core\Services\BaseService... and return Eloquent models where applicable". For consistency with other services (e.g., deleteUser, deleteTask), this method should return the deleted Company model.

♻️ Proposed fix
-    public function deleteCompany(Company $company): bool
+    public function deleteCompany(Company $company): Company
     {
         DB::beginTransaction();
 
         try {
-            $deleted = (bool) $company->delete();
+            $company->delete();
 
             DB::commit();
 
-            return $deleted;
+            return $company;
         } catch (Throwable $e) {
             DB::rollBack();
             throw $e;
         }
     }
📝 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
public function deleteCompany(Company $company): bool
{
DB::beginTransaction();
try {
$deleted = (bool) $company->delete();
DB::commit();
return $deleted;
} catch (Throwable $e) {
DB::rollBack();
throw $e;
}
}
public function deleteCompany(Company $company): Company
{
DB::beginTransaction();
try {
$company->delete();
DB::commit();
return $company;
} catch (Throwable $e) {
DB::rollBack();
throw $e;
}
}
🤖 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/Core/Services/CompanyService.php` around lines 68 - 83, Update
deleteCompany to return the deleted Company Eloquent model instead of casting
the delete result to a boolean, matching the return contract used by deleteUser
and deleteTask. Preserve the existing transaction, commit, rollback, and
exception behavior.

Source: Coding guidelines

Comment on lines +29 to +41
public function updateRecurringInvoice(RecurringInvoice $recurringInvoice, array $data): RecurringInvoice
{
$recurringInvoice->update([
'invoice_id' => $data['invoice_id'] ?? $recurringInvoice->invoice_id,
'numbering_id' => $data['numbering_id'] ?? $recurringInvoice->numbering_id,
'frequency' => $data['frequency'] ?? $recurringInvoice->frequency,
'start_at' => $data['start_at'] ?? $recurringInvoice->start_at,
'end_at' => $data['end_at'] ?? $recurringInvoice->end_at,
]);

return $recurringInvoice;
}
}

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

Allow nullification of optional fields during updates.

Using the null coalescing operator (??) to fall back to existing model values prevents the caller from explicitly clearing nullable fields. If $data['end_at'] is passed as null to remove the end date, the ?? operator ignores it and incorrectly retains the old date.

To safely fall back to existing values for missing keys while still accepting explicit null assignments, only include the keys that exist in the $data array using array_intersect_key.

🐛 Proposed fix
     public function updateRecurringInvoice(RecurringInvoice $recurringInvoice, array $data): RecurringInvoice
     {
-        $recurringInvoice->update([
-            'invoice_id'   => $data['invoice_id'] ?? $recurringInvoice->invoice_id,
-            'numbering_id' => $data['numbering_id'] ?? $recurringInvoice->numbering_id,
-            'frequency'    => $data['frequency'] ?? $recurringInvoice->frequency,
-            'start_at'     => $data['start_at'] ?? $recurringInvoice->start_at,
-            'end_at'       => $data['end_at'] ?? $recurringInvoice->end_at,
-        ]);
+        $recurringInvoice->update(
+            array_intersect_key($data, array_flip(['invoice_id', 'numbering_id', 'frequency', 'start_at', 'end_at']))
+        );

         return $recurringInvoice;
     }
📝 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
public function updateRecurringInvoice(RecurringInvoice $recurringInvoice, array $data): RecurringInvoice
{
$recurringInvoice->update([
'invoice_id' => $data['invoice_id'] ?? $recurringInvoice->invoice_id,
'numbering_id' => $data['numbering_id'] ?? $recurringInvoice->numbering_id,
'frequency' => $data['frequency'] ?? $recurringInvoice->frequency,
'start_at' => $data['start_at'] ?? $recurringInvoice->start_at,
'end_at' => $data['end_at'] ?? $recurringInvoice->end_at,
]);
return $recurringInvoice;
}
}
public function updateRecurringInvoice(RecurringInvoice $recurringInvoice, array $data): RecurringInvoice
{
$recurringInvoice->update(
array_intersect_key($data, array_flip(['invoice_id', 'numbering_id', 'frequency', 'start_at', 'end_at']))
);
return $recurringInvoice;
}
}
🤖 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/Services/RecurringInvoiceService.php` around lines 29 - 41,
Update updateRecurringInvoice so missing fields retain their existing model
values while explicitly provided null values are persisted. Build the update
payload using array_intersect_key against the allowed invoice_id, numbering_id,
frequency, start_at, and end_at fields, then merge it with the current model
values before calling update.

Comment on lines +86 to 88
$taxRate = TaxRate::factory()
->state(['company_id' => $quote->company_id])
->create();

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use ->for() instead of ->state() for company-scoped models.

The coding guidelines strictly mandate using ->for($company) when creating company-scoped models in factories.

♻️ Proposed fix
-                $taxRate = TaxRate::factory()
-                    ->state(['company_id' => $quote->company_id])
-                    ->create();
+                $taxRate = TaxRate::factory()
+                    ->for($quote->company)
+                    ->create();
📝 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
$taxRate = TaxRate::factory()
->state(['company_id' => $quote->company_id])
->create();
$taxRate = TaxRate::factory()
->for($quote->company)
->create();
🤖 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/Quotes/Database/Factories/QuoteFactory.php` around lines 86 - 88,
Update the TaxRate factory creation in QuoteFactory to use the company
relationship via ->for($company) instead of assigning company_id through
->state(). Reuse the existing company model variable associated with the quote,
preserving the created tax rate’s company scope.

Source: Coding guidelines

@nielsdrost7
nielsdrost7 merged commit 16c779b into InvoicePlane:develop Jul 18, 2026
1 of 10 checks passed
@nielsdrost7
nielsdrost7 deleted the fix/dead-code-review-and-test-hygiene branch July 18, 2026 02:00
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.

1 participant