BuiltEmail workspace

Supplier transaction statement contract

Purpose: send the supplier a statement of bill transactions from the issuing business, with a PDF attachment.

Implementation: App\Mail\supplierTransactions. View: resources/views/emails/suppliers/transactions.blade.php. Original shared wrapper: mail::message; optional note: mail::promotion. Usage: app/Http/Controllers/General/emailsController.php:258, suppliertransactions. Explicit queue() call. These static references are evidence, not proof of current production delivery.

Suggested subject: Transactions Statement from {{period_from}} to {{period_to}} (existing raw from/to concatenation). HTML titles do not set mail headers.

Suggested preheader: Transaction statement from {{business_name}}. {{statement_period}}.

Portable placeholder contract

TokenExisting source / derivationHandling
{{logo_url}}Original Built app_name PNGOptional 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_periodRequired escaped text, assembled from existing context.
{{business_name}}$business->nameRequired 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 renderingRequired 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 BillPayment amount sum, rounded by NumberHelper. Preserve supplied values, including zero.
{{total_owing}}$currency->code . number_format($total_owing, 2)Required rounded controller total_worth minus total_paid; do not recompute from the ten displayed entries.
{{entry_reference}}$transaction->bill_id ?? ''Scoped repeated-entry text. Preserve source reference; the supplier source explicitly permits an empty bill_id.
{{entry_amount_due}}$currency->code . number_format($transaction->amount_due, 2)Required scoped existing email row display, not an invented recalculation of remaining balance.
{{entry_amount_paid}}$currency->code . number_format($transaction->amount_paid, 2)Required existing business-currency row display. Do not silently substitute a bill fx_currency from the PDF view.
{{entry_issue_date}}$transaction->purchase_dateRequired scoped source date string. Do not add a timezone or change the covered period.
{{entry_due_date}}$transaction->due_dateRequired scoped source date string, not a new due date or overdue classification.
{{merchant_message}}$message <- '' in current callerOptional 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/supplier-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

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.suppliers.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 supplier contact/address 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/supplier-statement.html and ready/supplier-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.