Skip to content

[Invoices] Attach invoice PDF automatically when sending an invoice email #526

Open

Description

@nielsdrost7

Sub-task extracted from #169 during consolidation into #150.

Parent: #150

When the user confirms the send action, the invoice PDF must be generated and attached to the outgoing email automatically — no manual download required.

Activity

  1. added this to the v2.0.0-alpha milestone on Jul 3, 2026
  2. nielsdrost7 commented on Jul 16, 2026

    @nielsdrost7
    CollaboratorAuthor

    SMART Plan — Attach invoice PDF automatically when sending an invoice email

    Context/Epic: Core — sub-task of #150 (Invoices epic #15)

    Specific

    When the send is confirmed, the invoice PDF must be generated and attached automatically. This is currently unbuildable in develop: Modules\Core\Services\PdfGenerationService is an empty stub class here — there is no PDF rendering pipeline to call. A complete, tested implementation (PdfGenerationService::renderInvoiceHtml()/PDF byte generation, ReportTemplateStorage, etc.) exists on the unmerged feature/130-report-builder branch (verified while grounding #411 in this same batch — a clean fast-forward from develop's current HEAD, 7 commits). This sub-task is blocked on that branch landing; building a second, parallel PDF pipeline just for this issue would be wasted effort.

    Measurable

    • Confirming the send generates a PDF for the invoice and attaches it to the outgoing mailable.
      • PHPUnit: it_attaches_the_invoice_pdf_to_the_outgoing_email()
        #[Test]
        public function it_attaches_the_invoice_pdf_to_the_outgoing_email(): void
        {
            /* Arrange */
            Mail::fake();
            $invoice = Invoice::factory()->for($this->company)->create();
    
            /* Act */
            $this->testLivewire(ListInvoices::class)
                ->callTableAction('send email', $invoice, data: ['to' => 'client@example.com', 'subject' => 'Invoice', 'body' => 'See attached.']);
    
            /* Assert */
            Mail::assertSent(fn ($mail) => count($mail->attachments) === 1);
        }
    • The attached file is named after the invoice number, not a generic filename.
      • PHPUnit: it_names_the_attached_pdf_after_the_invoice_number()
        #[Test]
        public function it_names_the_attached_pdf_after_the_invoice_number(): void
        {
            /* Arrange */
            Mail::fake();
            $invoice = Invoice::factory()->for($this->company)->create(['invoice_number' => 'INV-2026-042']);
    
            /* Act */
            $this->testLivewire(ListInvoices::class)
                ->callTableAction('send email', $invoice, data: ['to' => 'client@example.com', 'subject' => 'Invoice', 'body' => 'See attached.']);
    
            /* Assert */
            Mail::assertSent(function ($mail) {
                return collect($mail->attachments)->first()->as === 'INV-2026-042.pdf';
            });
        }
        #[Test]
        public function it_renders_the_attachment_using_the_invoices_resolved_pdf_template(): void
        {
            /* Arrange */
            Mail::fake();
            $invoice = Invoice::factory()->for($this->company)->create(['template' => 'system/invoice/default']);
    
            /* Act */
            $this->testLivewire(ListInvoices::class)
                ->callTableAction('send email', $invoice, data: ['to' => 'client@example.com', 'subject' => 'Invoice', 'body' => 'See attached.']);
    
            /* Assert */
            Mail::assertSent(fn ($mail) => count($mail->attachments) === 1);
        }

    Edge cases worth covering: PDF generation failing (malformed template, missing data) should surface as a graceful send failure per #531, not a silent email with no attachment.

    Achievable

    1. Hard dependency: merge/land feature/130-report-builder ([Invoices]: Ability to select from multiple invoice templates #411/[Reports]: Report Builder — bands + bricks #130) first — PdfGenerationService is an empty stub in develop otherwise.
    2. Once merged, call PdfGenerationService::renderInvoicePdf($invoice) (or equivalent) from the send action's confirmation handler, attaching the resulting bytes via Mail::attachData().
    3. Filename convention: {$invoice->invoice_number}.pdf, falling back to a generic name for invoices without a number yet (drafts).
    4. Coordinate with [Invoices] Handle invoice email send failures gracefully — show error notification, no status change #531 so a PDF-generation exception is caught and surfaced as a friendly error rather than a fatal 500.

    Relevant

    "No manual download required" is the whole point of this sub-task — without it, sending an invoice email still requires a separate manual PDF download-and-attach step.

    Time-bound

    Sprint 7 — blocked on #411/#130 (PdfGenerationService) landing; can't start meaningfully before then.

  3. nielsdrost7 commented on Jul 18, 2026

    @nielsdrost7
    CollaboratorAuthor

    Sub-task extracted from #169 during consolidation into #150.

    Parent: #150

    When the user confirms the "Email Invoice" send action, the invoice PDF must be generated and attached to the outgoing email automatically — no manual download required — matching v1 (application/modules/mailer/helpers/phpmailer_helper.php:176-205, application/modules/mailer/controllers/Mailer.php:189-194).

    Specific

    Modules/Invoices/Mail/InvoiceMailable::attachments() returns the rendered invoice PDF (reuse InvoiceService::renderHtml() + PDFFactory, the same pipeline as generatePdf()), named <invoice_number>.pdf (fallback invoice-draft-<id>.pdf). Attachment is skipped when the company setting email_pdf_attachment is false (toggle already exists at Modules/Core/Filament/Admin/Pages/Settings.php:575 — v1 key email_pdf_attachment, default ON). Document uploads linked to the invoice (v1: Mdl_uploads::get_invoice_uploads) are attached as additional files.

    Measurable

    • Sending with email_pdf_attachment=true produces exactly one PDF attachment with the invoice-number filename.
    • Sending with email_pdf_attachment=false produces zero PDF attachments.
    • Invoice uploads are attached with their original client filenames.

    Achievable

    1. Render the PDF eagerly in InvoiceService::sendInvoiceEmail() (or lazily via Attachment::fromData) and pass bytes+filename into InvoiceMailable.
    2. Read the setting via the Setting model scoped to $invoice->company_id — the mailable runs on a queue worker with no Filament tenant, so do not rely on session/tenant helpers.
    3. Attach $invoice->attachments/uploads from storage.

    Relevant

    v1 always shipped the PDF; recipients of v2 emails currently get body text only. Blocks #480 (reminders attach PDF too).
    Dependency note: when e-invoicing lands (#487 NLCIUS/UBL, #126 Peppol), the XML must be attached alongside the PDF (v1 CII behavior, phpmailer_helper.php:181-197) — add a checklist item there, not here.

    Time-Bound

    One sprint, after #524/#522 groundwork (already merged).

    Arrange

    Mail::fake(); company with email_pdf_attachment=true; invoice INV-100 with customer email and one upload.

    Act

    Mount email_invoice action on ListInvoices for the record, submit with defaults.

    Assert

    • Mail::assertQueued(InvoiceMailable::class, fn ($m) => collect($m->attachments())->count() >= 1)
    • PDF attachment is named INV-100.pdf
    • Upload file is among the attachments
    • With email_pdf_attachment=false, queued mailable has no PDF attachment
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    refinedRefined with SMART plan / test suggestions

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions