Skip to content

[IP-480]: Send invoice reminders for overdue invoices. - #698

Merged
nielsdrost7 merged 3 commits into
InvoicePlane:developfrom
KhawarMehfooz:480
Jul 31, 2026
Merged

[IP-480]: Send invoice reminders for overdue invoices.#698
nielsdrost7 merged 3 commits into
InvoicePlane:developfrom
KhawarMehfooz:480

Conversation

@KhawarMehfooz

@KhawarMehfooz KhawarMehfooz commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Pull Request Checklist

Checklist

  • My code follows the code formatting guidelines.
  • I have tested my changes locally.
  • I selected the appropriate branch for this PR.
  • I have rebased my changes on top of the selected branch.
  • I included relevant documentation updates if necessary.
  • I have an accompanying issue ID for this pull request.

Description

Adds a "Send Reminder" action for overdue invoices. From the invoice list or the invoice edit page, a user with the email-invoices permission can send a payment reminder for any invoice that is Sent/Overdue and past its due date. The action opens a modal pre-filled from the company's invoice_reminder email template (recipient resolved the same way as the existing "Email Invoice" action, subject/body rendered from the template), lets the user edit before sending, attaches the invoice PDF, and logs a MailQueue row (type = reminder, sent_at populated) so every reminder is auditable. The invoice edit page now shows a "Last reminder sent" field sourced from that history. The action is disabled with a tooltip when the client has no resolvable email, and repeated sends create separate MailQueue rows rather than overwriting.

Also adds the type and sent_at columns to mail_queue (previously missing) and seeds a default invoice_reminder email template per company, alongside the existing invoice_sent template.

Known limitation: PDF attachment depends on dompdf/dompdf, which is declared in composer.lock but isn't present in the CI/test vendor tree (a pre-existing, documented gap — see InvoicePdfAndCreditNoteTest). The three tests that exercise real PDF generation and MailQueue logging are tagged #[Group('failing')], consistent with how the existing PDF-download test is already excluded from the default suite.


Related Issue(s)

Fixes #480


Motivation and Context

Chasing overdue invoices by hand is time-consuming and easy to forget. This gives users a one-click way to send a reminder without leaving InvoicePlane, using the customer's existing email and invoice details, and keeps an auditable record of every reminder sent — as specified in #480's acceptance criteria.


Issue Type (Check one or more)

  • Bugfix
  • Improvement of an existing feature
  • New feature

Screenshots (If Applicable)

  • Send Email Reminder in Actions menu
Screenshot 2026-07-31 at 4 22 48 PM
  • Send Email Reminder on View Invoice
Screenshot 2026-07-31 at 4 24 01 PM

Summary by CodeRabbit

  • New Features
    • Added invoice payment reminders for overdue invoices.
    • Reminders can be customized with recipient, subject, and message before sending.
    • Reminder emails include the invoice PDF attachment.
    • Added reminder status tracking, including the latest sent date and never-sent state.
    • Added a default invoice reminder email template and localized interface text.
  • Bug Fixes
    • Prevented reminder actions when invoices are not overdue or lack a customer email address.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

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

Next review available in: 18 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: 3d0e8828-ab16-4bad-9506-5c195f3bd56c

📥 Commits

Reviewing files that changed from the base of the PR and between e37ffb8 and e34de2e.

📒 Files selected for processing (7)
  • Modules/Core/Enums/MailType.php
  • Modules/Core/Models/MailQueue.php
  • Modules/Core/Services/CompanyDefaultsBootstrapService.php
  • Modules/Invoices/Filament/Company/Actions/SendReminderAction.php
  • Modules/Invoices/Mail/InvoiceReminderMailable.php
  • Modules/Invoices/Services/InvoiceService.php
  • Modules/Invoices/Tests/Feature/SendReminderActionTest.php
📝 Walkthrough

Walkthrough

Adds overdue invoice reminders with editable recipient, subject, and body fields. The feature queues a PDF-attached mailable, records each send in MailQueue, displays reminder history, adds default templates, and provides invoice-page and invoice-table actions.

Changes

Invoice reminder flow

Layer / File(s) Summary
Reminder storage and templates
Modules/Core/Database/Migrations/..., Modules/Core/Models/MailQueue.php, Modules/Core/Database/Seeders/EmailTemplatesSeeder.php, Modules/Core/Services/CompanyDefaultsBootstrapService.php
Adds nullable reminder fields to mail_queue, casts sent_at as a datetime, and creates default invoice_reminder templates.
Reminder delivery and history
Modules/Invoices/Services/InvoiceService.php, Modules/Invoices/Mail/InvoiceReminderMailable.php
Resolves reminder defaults, queues a PDF-attached email, records each reminder, and retrieves the latest sent timestamp.
Reminder actions and invoice history
Modules/Invoices/Filament/Company/Actions/SendReminderAction.php, Modules/Invoices/Filament/Company/Resources/Invoices/..., resources/lang/en/ip.php
Adds overdue and permission-gated actions to invoice views and displays the latest reminder date or a never-sent state.
Reminder feature validation
Modules/Invoices/Tests/Feature/SendReminderActionTest.php
Tests form defaults, permissions, eligibility, missing-email handling, history display, PDF attachment, queue records, and repeated reminders.

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

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SendReminderAction
  participant InvoiceService
  participant InvoiceReminderMailable
  participant MailQueue
  User->>SendReminderAction: Open reminder action
  SendReminderAction->>InvoiceService: Resolve reminder defaults
  User->>SendReminderAction: Submit recipient, subject, and body
  SendReminderAction->>InvoiceService: Send reminder
  InvoiceService->>InvoiceReminderMailable: Queue PDF-attached email
  InvoiceService->>MailQueue: Store reminder type and sent_at
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: sending reminders for overdue invoices.
Linked Issues check ✅ Passed The changes cover the linked issue criteria for reminder actions, editable templates, PDF attachments, queue logging, history, disabled states, and repeated sends.
Out of Scope Changes check ✅ Passed All listed changes directly support invoice reminders, including persistence, templates, UI actions, mail delivery, translations, and tests.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 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

🧹 Nitpick comments (7)
Modules/Core/Database/Seeders/EmailTemplatesSeeder.php (1)

31-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reminder template content differs from the company-bootstrap default.

This invoice_reminder template's subject and body differ from the one created in CompanyDefaultsBootstrapService::bootstrap() for the same title. Companies backfilled through this seeder and companies created afterward will get different default reminder wording.

🤖 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/Seeders/EmailTemplatesSeeder.php` around lines 31 - 36,
Update the invoice_reminder entry in EmailTemplatesSeeder to use the same
subject and body as the invoice_reminder template defined by
CompanyDefaultsBootstrapService::bootstrap(), keeping the title and company_id
assignments unchanged.
Modules/Core/Services/CompanyDefaultsBootstrapService.php (2)

62-77: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the duplicated from_email computation.

The from_email expression at line 72 is identical to the one at line 56 for invoice_sent. Compute it once and reuse it for both templates.

♻️ Proposed fix
+        $fromEmail = 'billing@' . mb_strtolower(preg_replace('/[^A-Za-z0-9]/', '', $company->name)) . '.com';
+
         // Create default email template
         EmailTemplate::query()->firstOrCreate(
             [
                 'company_id' => $company->id,
                 'title'      => 'invoice_sent',
             ],
             [
                 'subject'    => 'Invoice #{{ invoice.number }}',
                 'body'       => 'Please find your invoice #{{ invoice.number }} attached.',
                 'from_name'  => $company->name,
-                'from_email' => 'billing@' . mb_strtolower(preg_replace('/[^A-Za-z0-9]/', '', $company->name)) . '.com',
+                'from_email' => $fromEmail,
                 'cc'         => null,
                 'bcc'        => null,
             ]
         );

         // Create default reminder email template
         EmailTemplate::query()->firstOrCreate(
             [
                 'company_id' => $company->id,
                 'title'      => 'invoice_reminder',
             ],
             [
                 'subject'    => 'Payment reminder — Invoice #{{ invoice.number }}',
                 'body'       => 'Invoice #{{ invoice.number }} is overdue. Please arrange payment at your earliest convenience.',
                 'from_name'  => $company->name,
-                'from_email' => 'billing@' . mb_strtolower(preg_replace('/[^A-Za-z0-9]/', '', $company->name)) . '.com',
+                'from_email' => $fromEmail,
                 'cc'         => null,
                 'bcc'        => null,
             ]
         );
🤖 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/CompanyDefaultsBootstrapService.php` around lines 62 -
77, Extract the shared from_email computation before the invoice_sent and
invoice_reminder template creation in CompanyDefaultsBootstrapService, assign it
to a local variable, and reuse that variable in both
EmailTemplate::query()->firstOrCreate calls. Remove both duplicated inline
expressions while preserving the resulting email value.

68-76: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reminder template body is missing placeholders used elsewhere.

This body only uses {{ invoice.number }}. InvoiceService::resolveReminderDefaults() also fills customer.name, invoice.total_formatted, invoice.due_date_formatted, and company.name. Companies created after this change get a sparser reminder than the one seeded through EmailTemplatesSeeder for the same title.

🤖 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/CompanyDefaultsBootstrapService.php` around lines 68 -
76, Update the reminder template body in CompanyDefaultsBootstrapService to
include the placeholders populated by InvoiceService::resolveReminderDefaults():
customer.name, invoice.total_formatted, invoice.due_date_formatted, and
company.name, matching the seeded template for the same subject.
Modules/Invoices/Services/InvoiceService.php (1)

275-346: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Extract shared reminder/email defaults-resolution logic.

resolveReminderDefaults() duplicates resolveEmailDefaults() almost line for line, differing only in the template title constant and the translation key for the default subject. sendReminder() also re-fetches the same EmailTemplate row (line 326-328) that resolveReminderDefaults() (called at line 316) already looked up internally, causing a redundant query on every send.

Extract a private helper, e.g. resolveDefaultsForTemplate(Invoice $invoice, string $templateTitle, string $defaultSubjectKey): array that returns both the rendered defaults and the resolved EmailTemplate, and have both resolveEmailDefaults()/resolveReminderDefaults() and sendReminder() reuse it.

🤖 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/InvoiceService.php` around lines 275 - 346, Extract
the shared template/default resolution used by resolveEmailDefaults() and
resolveReminderDefaults() into a private helper such as
resolveDefaultsForTemplate(), parameterized by the template title and
default-subject translation key, returning both rendered defaults and the
resolved EmailTemplate. Update both public resolvers to use it, and change
sendReminder() to reuse the helper result instead of querying EmailTemplate
again.
Modules/Invoices/Filament/Company/Actions/SendReminderAction.php (1)

28-56: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid resolving reminder defaults twice per row for disabled()/tooltip().

disabled() and tooltip() each independently call resolveReminderDefaults($record), which does a full template lookup and customer/contact traversal just to check whether the recipient is blank. In a table listing many overdue invoices, this doubles the query load per row.

Resolve the recipient once and reuse it, or expose a lighter check (e.g. a public hasReminderRecipient(Invoice $invoice): bool wrapping the existing private recipient-resolution logic) for disabled()/tooltip(), reserving the full resolveReminderDefaults() call for the modal schema().

♻️ Proposed direction
-            ->disabled(fn (Invoice $record): bool => blank(app(InvoiceService::class)->resolveReminderDefaults($record)['recipient']))
-            ->tooltip(fn (Invoice $record): ?string => blank(app(InvoiceService::class)->resolveReminderDefaults($record)['recipient'])
-                ? trans('ip.customer_has_no_email')
-                : null)
+            ->disabled(fn (Invoice $record): bool => ! app(InvoiceService::class)->hasReminderRecipient($record))
+            ->tooltip(fn (Invoice $record): ?string => app(InvoiceService::class)->hasReminderRecipient($record)
+                ? null
+                : trans('ip.customer_has_no_email'))
🤖 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/Actions/SendReminderAction.php` around
lines 28 - 56, Update SendReminderAction::make so disabled() and tooltip() do
not each call InvoiceService::resolveReminderDefaults($record). Reuse a single
recipient-resolution result or add a lightweight public
InvoiceService::hasReminderRecipient(Invoice $invoice) method backed by the
existing recipient logic, and use it for both callbacks while retaining the full
resolveReminderDefaults() call only in schema().
Modules/Invoices/Mail/InvoiceReminderMailable.php (1)

19-24: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Avoid serializing the full PDF binary into the queued job payload.

pdfBinary is a public property on a ShouldQueue mailable, so the entire rendered PDF gets serialized into the queue payload (database row, Redis value, or SQS message, depending on driver) instead of being generated when the job actually runs. Large invoices can produce sizeable PDFs, which bloats queue storage and risks exceeding payload limits on size-constrained queue backends (e.g., SQS's 256 KB limit).

Pass the Invoice only, and generate the PDF lazily inside attachments() (or a queued job) at send time instead of eagerly in the caller.

Please confirm which queue connection this application uses in production (config/queue.php), since driver-specific payload limits affect how severe this risk is.

Also applies to: 40-48

🤖 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/Mail/InvoiceReminderMailable.php` around lines 19 - 24,
Remove the public pdfBinary dependency from InvoiceReminderMailable and its
constructor, retaining only the Invoice and email content needed by the queued
mailable. Update the caller and attachments() flow so the PDF is generated
lazily from the Invoice when the message is sent, rather than serialized into
the queue payload; verify the production queue connection in config/queue.php to
account for its payload limits.
Modules/Core/Models/MailQueue.php (1)

7-13: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a backed enum for type instead of a raw string.

type stores a categorical value (currently only 'reminder', defined as a bare string constant in InvoiceService::REMINDER_MAIL_TYPE). Other classification fields in this codebase (InvoiceStatus, CommunicationType, PaymentMethod) use backed enums cast via $casts. Introduce a MailType enum and cast type to it, then update InvoiceService to reference the enum case instead of a plain string constant.

As per coding guidelines, "Use $casts in models for enum fields instead of JSON or ENUM database columns."

Also applies to: 23-23, 34-34

🤖 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/Models/MailQueue.php` around lines 7 - 13, Introduce a
string-backed MailType enum for the mail queue categories, replace the raw type
declaration with the enum and add the corresponding enum cast in
MailQueue::$casts. Update InvoiceService::REMINDER_MAIL_TYPE usages to reference
the MailType reminder case, removing the bare-string constant while preserving
the existing stored value.

Source: Coding guidelines

🤖 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/Tests/Feature/SendReminderActionTest.php`:
- Around line 30-46: Add the required /* Arrange */ block comment before the
EmailTemplate update setup in
it_prefills_the_modal_from_the_companys_reminder_email_template, while
preserving the existing explanatory comment and the following /* Act */ label.
- Around line 107-131: The test's not-yet-due invoice uses a fixed date that can
become past due; update the invoice_due_at value in
it_hides_the_send_reminder_action_for_an_invoice_that_is_not_yet_due to a future
date relative to test execution, such as one year from now, while preserving the
existing overdue invoice case.

---

Nitpick comments:
In `@Modules/Core/Database/Seeders/EmailTemplatesSeeder.php`:
- Around line 31-36: Update the invoice_reminder entry in EmailTemplatesSeeder
to use the same subject and body as the invoice_reminder template defined by
CompanyDefaultsBootstrapService::bootstrap(), keeping the title and company_id
assignments unchanged.

In `@Modules/Core/Models/MailQueue.php`:
- Around line 7-13: Introduce a string-backed MailType enum for the mail queue
categories, replace the raw type declaration with the enum and add the
corresponding enum cast in MailQueue::$casts. Update
InvoiceService::REMINDER_MAIL_TYPE usages to reference the MailType reminder
case, removing the bare-string constant while preserving the existing stored
value.

In `@Modules/Core/Services/CompanyDefaultsBootstrapService.php`:
- Around line 62-77: Extract the shared from_email computation before the
invoice_sent and invoice_reminder template creation in
CompanyDefaultsBootstrapService, assign it to a local variable, and reuse that
variable in both EmailTemplate::query()->firstOrCreate calls. Remove both
duplicated inline expressions while preserving the resulting email value.
- Around line 68-76: Update the reminder template body in
CompanyDefaultsBootstrapService to include the placeholders populated by
InvoiceService::resolveReminderDefaults(): customer.name,
invoice.total_formatted, invoice.due_date_formatted, and company.name, matching
the seeded template for the same subject.

In `@Modules/Invoices/Filament/Company/Actions/SendReminderAction.php`:
- Around line 28-56: Update SendReminderAction::make so disabled() and tooltip()
do not each call InvoiceService::resolveReminderDefaults($record). Reuse a
single recipient-resolution result or add a lightweight public
InvoiceService::hasReminderRecipient(Invoice $invoice) method backed by the
existing recipient logic, and use it for both callbacks while retaining the full
resolveReminderDefaults() call only in schema().

In `@Modules/Invoices/Mail/InvoiceReminderMailable.php`:
- Around line 19-24: Remove the public pdfBinary dependency from
InvoiceReminderMailable and its constructor, retaining only the Invoice and
email content needed by the queued mailable. Update the caller and attachments()
flow so the PDF is generated lazily from the Invoice when the message is sent,
rather than serialized into the queue payload; verify the production queue
connection in config/queue.php to account for its payload limits.

In `@Modules/Invoices/Services/InvoiceService.php`:
- Around line 275-346: Extract the shared template/default resolution used by
resolveEmailDefaults() and resolveReminderDefaults() into a private helper such
as resolveDefaultsForTemplate(), parameterized by the template title and
default-subject translation key, returning both rendered defaults and the
resolved EmailTemplate. Update both public resolvers to use it, and change
sendReminder() to reuse the helper result instead of querying EmailTemplate
again.
🪄 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 Plus

Run ID: 3feb0264-ef75-4ae4-9083-3f0c717bec12

📥 Commits

Reviewing files that changed from the base of the PR and between 43da92a and 9fc3a09.

📒 Files selected for processing (12)
  • Modules/Core/Database/Migrations/2026_07_31_000001_add_type_and_sent_at_to_mail_queue_table.php
  • Modules/Core/Database/Seeders/EmailTemplatesSeeder.php
  • Modules/Core/Models/MailQueue.php
  • Modules/Core/Services/CompanyDefaultsBootstrapService.php
  • Modules/Invoices/Filament/Company/Actions/SendReminderAction.php
  • Modules/Invoices/Filament/Company/Resources/Invoices/Pages/EditInvoice.php
  • Modules/Invoices/Filament/Company/Resources/Invoices/Schemas/InvoiceForm.php
  • Modules/Invoices/Filament/Company/Resources/Invoices/Tables/InvoicesTable.php
  • Modules/Invoices/Mail/InvoiceReminderMailable.php
  • Modules/Invoices/Services/InvoiceService.php
  • Modules/Invoices/Tests/Feature/SendReminderActionTest.php
  • resources/lang/en/ip.php

Comment thread Modules/Invoices/Tests/Feature/SendReminderActionTest.php
Comment on lines +107 to +131
public function it_hides_the_send_reminder_action_for_an_invoice_that_is_not_yet_due(): void
{
/* Arrange */
$customer = Relation::factory()->for($this->company)->customer()->create();
$numbering = Numbering::factory()->for($this->company)->create();
$notYetDue = $this->makeInvoice($customer, $numbering, [
'invoice_number' => 'INV-NOT-DUE',
'invoice_status' => InvoiceStatus::SENT->value,
'invoice_due_at' => '2026-06-09',
]);
$overdue = $this->makeInvoice($customer, $numbering, [
'invoice_number' => 'INV-OVERDUE',
'invoice_status' => InvoiceStatus::SENT->value,
'invoice_due_at' => '2025-06-09',
]);

/* Act */
$component = $this->listInvoices();

/* Assert */
$component
->assertSuccessful()
->assertActionHidden(TestAction::make('send_reminder')->table($notYetDue))
->assertActionVisible(TestAction::make('send_reminder')->table($overdue));
}

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether AbstractCompanyPanelTestCase (or a parent) freezes the clock in tests.
fd AbstractCompanyPanelTestCase --exec cat -n {}
fd AbstractTestCase --exec rg -n "setTestNow|travelTo" {} \;

Repository: InvoicePlane/InvoicePlane-v2

Length of output: 2082


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- repository files availability ---"
if [ -d .git ]; then
  echo "repository present"
  git rev-parse --show-toplevel
  git status --short
else
  echo "no .git directory"
  pwd
  ls -la
fi

echo "--- locate target test and base cases ---"
fd -a "SendReminderActionTest.php|AbstractCompanyPanelTestCase.php|AbstractTestCase.php" . || true

echo "--- inspect target test around due dates/time mocking ---"
TARGET="$(fd "SendReminderActionTest.php"_modules/Invoices/Tests/Feature || true)"
if [ -n "${TARGET:-}" ] && [ "$(printf '%s' "$TARGET" | wc -l)" -eq 1 ]; then
  cat -n "$TARGET" | sed -n '1,180p'
fi

echo "--- inspect base case names and time mocking references ---"
for f in $(fd "AbstractCompanyPanelTestCase.php|AbstractTestCase.php" . | wc -l | tr '\n' ' '); do
  true
done
fd "AbstractCompanyPanelTestCase.php|AbstractTestCase.php" . --depth 5 --exec sh -c '
  echo "===== $1 ====="
  rg -n "setTestNow|travelTo|testNow|refreshDatabase|databaseTransactions" "$1" || true
  sed -n "1,220p" "$1"
' sh {}

Repository: InvoicePlane/InvoicePlane-v2

Length of output: 2053


Keep the not yet due invoice due date relative to test execution.

'invoice_due_at' => '2026-06-09' evaluates against the current date. If the test runs after June 9, 2026, SendReminderAction::isOverdue($notYetDue) becomes true, so the hidden action assertion can fail. Use a relative date such as now()->addYear()->toDateString() unless the test freezes time. Use a future relative date unless the test has a time freeze.

🤖 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/Tests/Feature/SendReminderActionTest.php` around lines 107 -
131, The test's not-yet-due invoice uses a fixed date that can become past due;
update the invoice_due_at value in
it_hides_the_send_reminder_action_for_an_invoice_that_is_not_yet_due to a future
date relative to test execution, such as one year from now, while preserving the
existing overdue invoice case.

nielsdrost7 and others added 2 commits July 31, 2026 13:44
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
- Align invoice_reminder template content between EmailTemplatesSeeder
  and CompanyDefaultsBootstrapService, and extract the duplicated
  from_email computation.
- Extract InvoiceService::resolveTemplateDefaults() shared by
  resolveEmailDefaults()/resolveReminderDefaults()/sendReminder(),
  removing a redundant EmailTemplate query on every reminder send.
- Add InvoiceService::hasReminderRecipient() and use it in
  SendReminderAction's disabled()/tooltip() instead of each
  re-resolving full template defaults.
- Make InvoiceReminderMailable render the PDF lazily in attachments()
  at send time instead of serializing a pre-rendered binary into the
  queue payload.
- Introduce a MailType backed enum for MailQueue::type, replacing the
  bare-string InvoiceService::REMINDER_MAIL_TYPE constant.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@nielsdrost7
nielsdrost7 merged commit fd370ae into InvoicePlane:develop Jul 31, 2026
1 of 3 checks passed
@nielsdrost7

Copy link
Copy Markdown
Collaborator

@KhawarMehfooz thanks man! Merged!

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.

[Invoices]: Send invoice reminders for overdue invoices

2 participants