Email workspaceCustomer transaction statement contract
Purpose: send the customer a statement of invoice transactions from the issuing business, with a PDF attachment.
Implementation: App\Mail\customerTransactions. View: resources/views/emails/customers/transactions.blade.php. Original shared wrapper: mail::message; optional note: mail::promotion. Usage: app/Http/Controllers/General/emailsController.php:178, customertransactions. Explicit queue() call. These static references are evidence, not proof of current production delivery.
Suggested subject: Transactions Statement from {{business_name}} (existing). HTML titles do not set mail headers.
Suggested preheader: Transaction statement from {{business_name}}. {{statement_period}}.
Portable placeholder contract
| Token | Existing source / derivation | Handling |
|---|---|---|
| {{logo_url}} | Original Built app_name PNG | Optional deployment override; concrete project HTTPS PNG is already present. Approved absolute HTTPS or matching attached CID only. |
| {{preheader}} | Suggested copy: Transaction statement from business_name, followed by statement_period | Required escaped text, assembled from existing context. |
| {{business_name}} | $business->name | Required issuing business identity and sign-off, not a new recipient name. |
| {{statement_period}} | ($from && $to) ? 'From ' . $from . ' to ' . $to : 'All transactions' | Required escaped derived text. Both values must be truthy for range coverage, exactly as in the source view. |
| {{as_at}} | Carbon::now() string at original HTML rendering | Required existing render-time value in the configured app timezone. Do not invent a period-end timestamp or deadline. |
| {{transactions_count}} | $transactions_count = count($transactions) | Required total source collection count, not the number of visible email entries. Preserve zero. |
| {{total_amount}} | $currency->code . number_format($total_amount, 2) | Required preformatted business-currency summary. Preserve the existing controller calculation. |
| {{total_paid}} | $currency->code . number_format($total_paid, 2) | Required existing controller total: payment fx_amount sum plus withholding sum. Preserve supplied values, including zero. |
| {{total_owing}} | $currency->code . number_format($total_owing, 2) | Required supplied $customer->unformated_balance. It is not recomputed as worth minus paid, and may be negative or nonzero with no selected invoices. |
| {{entry_reference}} | $transaction->invoice_number | Scoped repeated-entry text. Preserve source reference; the supplier source explicitly permits an empty bill_id. |
| {{entry_amount_due}} | $transaction->fx_currency . number_format($transaction->fx_amount, 2) | Required scoped existing email row display, not an invented recalculation of remaining balance. |
| {{entry_amount_paid}} | $transaction->fx_currency . number_format(App\FX::get_base_amount($transaction->amount_paid, $transaction->fx_rate ?: 1), 2) | Required existing helper behavior: get_base_amount multiplies amount by rate and rounds via NumberHelper. Preserve it; do not assume division from the method name. |
| {{entry_issue_date}} | $transaction->start_date | Required scoped source date string. Do not add a timezone or change the covered period. |
| {{entry_due_date}} | $transaction->end_date | Required scoped source date string, not a new due date or overdue classification. |
| {{merchant_message}} | $message <- request.message ?? '' | Optional escaped multiline text; remove the entire marked message table and matching text block when empty. Existing raw HTML needs the explicit integration choice below. |
Source-only coverage values: {{period_from}} maps to $from and {{period_to}} maps to $to. They are used to derive statement_period and, for the supplier, the existing header subject. They are not independent required body tokens. If either is missing, remove the range concept and show All transactions; do not leave dangling From/to labels.
Repeating entry contract
The marked entry table in ready/customer-statement.html is one scoped repeat prototype, not a raw-HTML {{transactions_html}} field and not the entire collection serialized into one token. Repeat the complete table for the first ten $transactions in original iteration order, binding the five entry_* tokens to each transaction. Remove it completely for an empty collection and show a factual empty-list sentence. The matching .txt file marks the equivalent repeat block. The PDF retains the full collection; do not limit the source data or PDF to ten.
In Laravel prefer @foreach over $transactions with a ten-entry limit in the email view, using escaped expressions for every field. Use @if for the merchant note. A full document belongs in a dedicated view(), with a separate text() view if appropriate; do not nest it inside mail::message. Portable {{placeholder_name}} syntax does not automatically reference Blade variables. There is no need for a new templating framework. Avoid a global replacement that binds every row to the same entry.
The summary uses the full source values. Do not total the displayed rows to replace the summary or claim they reconcile. No latest-first ordering is established by the caller, so the draft does not call them the latest ten. The email's Amount due row labels preserve the legacy view even when underlying source fields or PDF labels differ.
Recipient, sender, attachment and state handling
- Recipient: customer.business_email, falling back to the first request.other_emails entry; other_emails remain BCC. Preserve the current current-business customer lookup. Queueing is explicit; Queueable trait alone does not establish delivery behavior.
- Source customer date lookup uses request()->getContent(from/to), while the supplier uses get(). This is a source parsing uncertainty: range previews represent the Blade contract, not proof that the current API route parses those filters correctly. No controller change is authorized here.
- total_owing is the overall unformated_balance. Payments are range-filtered by selected invoice start_date when dates exist, but the withholding query covers all customer invoice IDs. Do not silently change that accounting behavior or claim every summary figure is limited to the displayed period.
- States: range with note, all transactions without note, empty collection with nonzero balance, twelve entries (ten shown), mixed invoice currencies versus business-currency summary, long values and negative supplied balance. A negative balance is not relabeled as a refund promise or overdue debt.
Keep From configured mail address with business name and Reply-To business.email or owner-email fallback. Both attach Transactions_statement.pdf generated from PDF.customers.transactions with the full existing data. No action URL, payment button or account login link exists in either source view; no URL is invented. There are no legacy duplicate action-link fields. The PDF-only customer contact/address, deposit and balance fields remain in PDF generation and are not discarded by omitting them from this body.
Existing PDFs and email rows use different legacy amount/currency expressions. Customer PDF rows display amount_due/amount_paid directly with fx_currency, while the email uses fx_amount and an FX helper for paid. Supplier PDF rows can use transaction.fx_currency or business currency; email rows use business currency. Resolve these discrepancies separately before production integration. The design preserves existing email mappings and does not modify PDFs or accounting calculations.
Safe values, message handling and logo
Escape all names, identifiers, dates, amounts, period text and notes as text. Preserve newlines in notes. The original message uses raw HTML; before integration choose reviewed equivalent-text conversion that retains paragraphs and meaningful link destinations, or a narrowly allowlisted sanitized rich-text section. Do not blindly strip anchor destinations, insert untrusted raw HTML, or silently remove instructions. Supplier template-only notes must remain optional. One-pass replacement is required if portable tokens are used; never evaluate inserted token-shaped text again.
The logo defaults to https://built-storage.ams3.cdn.digitaloceanspaces.com/Built/app_name.png and retains the original assets/built-logo.png. Host the original PNG at an absolute HTTPS URL or attach it with a matching CID. Validate any deployment override against approved image hosts/CID requirements separately from attribute escaping, and preserve its aspect ratio. No application URL values are required. Remote-image blocking remains possible.
Files and testing limits
ready/customer-statement.html and ready/customer-statement.txt are portable extracts; all populated states have HTML/text alternatives under previews/. No separate project-owned text alternative was found for these Markdown views, so the new text version is a proposed equivalent. Fictional fixtures do not attach a real PDF, send emails, or execute application actions. Desktop, tablet, mobile and dark-mode browser testing is separate from Gmail, Outlook and iOS Mail client testing. See statements-batch.md, statements-validation.md and integration-notes.html.