Email workspaceInvoice payment receipt contract
Implementation: App\Mail\Receipt, app/Mail/Receipt.php. Baseline view: resources/views/emails/invoice/Receipt.blade.php, using mail::message. Referenced calls are static usage evidence, not proof of current production delivery.
Suggested subject: Receipt_#{{invoice_number}}.pdf (existing exact pattern).
Suggested preheader: Receipt from {{business_name}} for invoice {{invoice_number}}.
Placeholder contract
| Token | Existing source / derivation | Handling |
|---|---|---|
| {{logo_url}} | Original Built app_name PNG | Optional deployment override; a concrete approved HTTPS logo already exists in ready HTML. Use approved absolute HTTPS or a real matching CID. |
| {{preheader}} | Suggested existing business/reference summary above | Required escaped text; no new payment, settlement or delivery claim. |
| {{business_name}} | $business->name passed/resolved by the existing Mailable | Required escaped merchant identity; not the customer, sender email or owner name. |
| {{invoice_number}} | $invoice->invoice_number | Required escaped source reference. Used in the original header subject and PDF filename. |
| {{invoice_paid_total}} | $invoice->fx_currency . number_format($invoice->amount_paid, 2) | Required complete formatted display. This is the supplied invoice’s cumulative recorded paid amount, not constructor $amount or payment.amount. Do not use business base currency, reformat the currency prefix, recalculate payments or replace it with invoice.amount_due. Preserve supplied zero and signed amounts. |
| {{payment_method}} | $payment->method (also exposed as $data[method]) | Required escaped method for the associated payment. Do not infer it from the invoice total or expose a bank/account number. |
| {{payment_date}} | Illuminate\Support\Carbon::parse($payment->date_paid)->format("l d F, Y") | Required existing formatted date, e.g. Tuesday 03 November, 2026. Retain source timezone/locale/date semantics. It is not the invoice issue date or today. |
| {{business_phone}} | $business->phone_number | Optional; original PHP truthiness predicate. Remove the entire marked row when falsy, including string 0. Preserve supplied text, leading + and zeroes; no numeric conversion or new tel: URL. |
| {{business_address}} | $business->address | Optional; original PHP truthiness predicate. Escape multiline text and remove the complete row when falsy. |
| {{business_email}} | $business->email | Optional visible text under original PHP truthiness predicate. This is the merchant contact, not customer.business_email or the owner fallback. Remove the whole row when falsy; do not invent a mailto: link. |
| {{receipt_url}} | $data[link] = config('app.url') . '/invoicereciept/' . $invoice->uuid | Required complete server-generated HTTPS URL. Preserve the legacy invoicereciept spelling and UUID; button/fallback share this one value. Validate scheme/host/path separately and escape attributes. No new signed link, expiry or payment URL. |
Usage, recipients, values and attachments
- Caller evidence: app/Http/Controllers/Income/invoicesController.php:908 queues to invoice.customer.business_email when request.send_receipt == string true and customer email is nonempty. app/Http/Controllers/Accounting/DepositTransactionsController.php:457 queues when request.send_receipt != string false and customer email is nonempty, selecting the last Payment for the invoice. Preserve this opt-out distinction.
- app/Http/Repository/InvoiceRepository.php:1549 addPayment queues when (string) send_receipt == string true and customer email is nonempty. :1757 payFromDeposit queues under if(send_receipt), again selecting the last Payment and requiring customer email. Do not standardize these predicates as part of an email refactor. All four explicit queue() calls are usage evidence; the Mailable itself has Queueable/SerializesModels and no ShouldQueue declaration.
- The associated invoice, business, constructor amount and payment are passed into the Mailable. Keep $data[invoice], business, payment, method, amount, link and payments = invoice.payment_history for the attachment path, even where the redesigned body does not consume them. The body uses invoice.amount_paid. InvoiceRepository.php:1272/:1597 and invoicesController.php:639 add the current amount to the existing paid total. Preserve the supplied model’s recorded value, including existing caller refresh behavior; do not query/recompute another total in the template.
- Amount label Paid on invoice and its explanatory sentence make the cumulative context explicit. Payment method/date remain tied to the associated payment, not to every payment in the aggregate. Do not imply the displayed total is this one payment, a new transfer, paid-in-full status or settlement guarantee. The constructor amount remains source data, not a second portable body field.
- Keep subject Receipt_# + invoice_number + .pdf exactly, despite its filename-like appearance. Keep SnappyPdf::loadView(receipt, $data) and attachData(..., Receipt_#number.pdf). The full PDF’s payment-history/online-payment/withholding/currency calculations remain untouched. Adding the accurate PDF instruction does not create a new attachment.
- Source color invoice.invoice_color is deferred secondary merchant styling, documented but not used in this Built-led draft. Do not remove the field from the invoice/PDF data. The original body has no business sign-off; none is invented.
- routes/web.php:110 maps /invoicereciept/{id} to invoicesController::itemisedReceipt (line 1090), looking up invoice.uuid and returning invoice-receipt-itemised with payment history. It opens a receipt page, not checkout or a direct PDF download. Do not change it to /viewinvoice or a guessed corrected /invoicereceipt path.
- States: standard; cumulative paid total distinct from current payment; each phone/address/email absent; all contacts absent; supplied zero; long identities/amount/method/contact/URL. Zero is a rendering check, not evidence of a zero-payment delivery workflow. Independently remove optional rows in both HTML/text. The payment-date/method table remains when no contacts exist.
From uses config(mail.from.address) with business.name. Reply-To uses business.email ?? business owner.email, with business.name as display name. Preserve null-coalescing semantics (an empty string differs from null), owner lookup and exact source headers. The body’s optional business email is not automatically replaced by the owner’s Reply-To address. No recipient/CC/BCC, queue or attachment change is authorized by this design.
Integration and safe handling
Portable {{placeholder_name}} tokens are not automatically Laravel Blade variables. Prefer the existing Mailable with a complete HTML Blade view and a separate plain-text view using explicit mapped values; do not nest this full document inside mail::message or introduce another templating framework. Keep native header, queue, business authorization, PDF and recipient behavior. Follow docs/integration/laravel.md.
Escape all dynamic text/attributes: identifiers, names, amounts, methods, dates and contacts. Validate the complete receipt URL’s HTTPS scheme, approved application host and exact existing path separately; preserve encrypted/UUID components without decoding/rebuilding in the template. Bind the same receipt_url to the button and visible fallback. There is one source link, not separate legacy duplicate action fields. Browser preview URLs are local inert explanations and must not enter production.
If using one-pass string replacement, escape text first and validate URL values separately, then replace only original tokens once. Inserted token-looking text must remain text, not be evaluated again; no dynamic value is trusted raw HTML. Optional blocks must be controlled explicitly by the integration adapter rather than leaving empty/unresolved tokens. Required missing/invalid amount/date/reference/URL needs an explicit integration decision, not an invented fallback or silently dropped content.
Ready HTML has the approved concrete https://built-storage.ams3.cdn.digitaloceanspaces.com/Built/app_name.png logo with descriptive Built alt text and proportional white-backed rendering. Retain assets/brand/built-logo.png. Deploy on accessible approved absolute HTTPS hosting or attach the original PNG and reference a real matching CID. Preview-relative assets are local fixtures only. Remote-image blocking, rounded corners and native dark styling vary by email client.
Alternatives and open decisions
public/ready/invoice-receipt.html and .txt are portable extracts; public/previews/invoice-receipt-*.html/.txt contain fictional sample data. No separate project-owned text alternative was found for the Markdown source, so the .txt is a proposed equivalent and must be registered in the consuming Mailable explicitly.
Legacy resources/views/mails/receipt.blade.php and mails/salesreciept.blade.php are separate unresolved-dispatch surfaces with different historical bindings. Do not migrate them merely because their names resemble these referenced Mailables. This batch does not include payment-link receipts, deposit receipts, waybills, SMS or externally authored CRM content.
Open integration decisions: native Blade/text registration, image hosting/CID, client approval, treatment of invalid required source values and explicit review of the documented amount-label semantics. Existing amounts, filenames and routes are preserved. No real recipients, live tokens, emails, payment actions or PDFs are generated here. Browser rendering does not prove Gmail, Outlook, iOS Mail compatibility, PDF correctness or actual payment/delivery status.