Email workspaceInvoice link visit notification contract
Implementation: App\Mail\LinkVisitedNotification. Source view: resources/views/emails/invoice/URLVisitedNotification.blade.php. Usage: app/Listeners/URLVisted.php:77 (ShortURLVisited listener). These static references are evidence, not proof of current production delivery. Shared original layout: mail::message; original optional message uses mail::promotion. The drafts use the existing design renderer's standalone table shell instead of nesting a full document inside Laravel Markdown wrappers.
Suggested subject: Invoice notification-{{invoice_number}} (existing)
Suggested preheader: An invoice link was visited. Review the invoice details and current payment status.
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. Use approved absolute HTTPS or matching attached CID. |
| {{preheader}} | Suggested copy above, assembled from existing context | Required escaped text; no new application field or expiry. |
| {{first_name}} | $user->first_name (business owner) | Required escaped text. Preserve the source value. |
| {{notification_time}} | Carbon::now()->toDateTimeString() evaluated while rendering the original Blade view | Required render-time notification value in the existing app timezone. This is NOT a persisted visit time. Do not map to created_at or claim a verified read timestamp. |
| {{customer_name}} | $customer->business_name | Required invoice customer context; not proof that this customer visited the link. |
| {{payment_status}} | $invoice->paid_status | Required raw stored status; do not silently normalize status vocabulary or claim a new payment. |
| {{invoice_number}} | $invoice->invoice_number | Required escaped text. Preserve the source value. |
| {{issue_date}} | $invoice->start_date | Required escaped text. Preserve the source value. |
| {{due_date}} | $invoice->end_date | Required escaped text. Preserve the source value. |
| {{total_amount}} | $invoice->fx_currency . number_format($invoice->amount_due, 2) | Required existing display. Preserve original Total Amount semantics even though the underlying field is named amount_due. |
| {{amount_paid}} | $invoice->fx_currency . number_format($invoice->amount_paid, 2) | Required source amount paid; preserve zero. |
| {{outstanding_amount}} | $invoice->fx_currency . number_format($invoice->amount_due - $invoice->amount_paid, 2) | Required existing calculated display. Calculate using current application logic, not string subtraction on formatted tokens. Do not silently fix financial semantics in a design change. |
| {{business_name}} | $business->name | Required escaped text. Preserve the source value. |
| {{business_phone}} | $business->phone_name (exact original expression) | Optional legacy source; explicit escaped em dash when absent. This appears inconsistent with phone_number elsewhere. Resolve deliberately before production; no silent field substitution here. |
| {{invoice_url}} | config('app.url') . '/viewinvoice/' . $invoice->uuid | Required server-generated complete HTTPS URL; validate approved host and path separately, then escape the attribute. Preserve signed/encrypted parameters. |
Content, states and integration decisions
- Audience: business.email or owner email fallback. Source filters device_type=bot, finds matching invoice link, requires subscription guard invoice_notification and notify_on_invoice_view/notify_email settings. This supports a link visit notification; it does not establish verified human/customer readership. The wording deliberately avoids that claim.
- Synchronous send(); Mailable does not implement ShouldQueue. Preserve configured From address/name and existing invoice-specific subject. No attachment is created in this Mailable. The SMS branch is separate and is not redesigned as email.
- Keep every original business/invoice field. Notification time labels the source render time honestly. No visitor name/IP, count of views, visit duration or read receipt is fabricated.
- States: unpaid, part-paid, paid/zero outstanding, unavailable legacy business phone, and long identities/amounts/URL. Fixtures illustrate supplied values; payment status remains raw and is not inferred from the preview amounts.
Rich text, escaping and safe integration
Portable {{placeholder_name}} tokens are not automatically Laravel Blade syntax. Prefer a dedicated full Blade HTML view and text alternative mapped explicitly to the existing Mailable values. Use view()/text() as appropriate during later authorized integration; no new templating framework is required. Keep existing constructor data and PDF-only bindings even when the body does not duplicate them.
All dynamic values in the draft are escaped text, including multiline merchant content and numbered failure details. Preserve newline structure. For existing raw-HTML merchant messages, a deliberate adapter must either convert reviewed rich text to equivalent text, retaining paragraphs and meaningful URL destinations, or render a narrowly allowlisted sanitized rich-text section. Simple strip_tags is not sufficient to preserve anchor destinations. Do not treat untrusted text as raw HTML, remove instructions silently, or repeatedly substitute inserted strings. If portable string replacement is chosen, use one pass after escaping text and separately validating URL values.
Validate destinations against intended HTTPS hosts/paths and preserve complete existing links. Optional blocks must be removed as whole marked sections in both HTML and text, not left with blank headings or unresolved tokens. Button/fallback links share the same value where present; there are no additional legacy duplicate link fields in these four views. No button is invented for the manual reminder.
The concrete default logo is https://built-storage.ams3.cdn.digitaloceanspaces.com/Built/app_name.png. Retain assets/built-logo.png as the unchanged original. Use absolute HTTPS hosting or a real attachment with a matching CID for deployment; preserve the image aspect ratio. Remote images may be blocked. Native dark CSS is progressive enhancement and requires actual client testing.
Files and text alternatives
ready/invoice-link-visited.html and ready/invoice-link-visited.txt are standalone extracts. Each state has populated HTML and plain text under previews/. No separate project-owned plain-text alternative was found for these four Markdown views; the new .txt files are proposed equivalents. All preview identities and references are fictional; all application actions are inert local explanations. No email, form, booking, approval or payment is executed. See invoice-followups-batch.md and integration-notes.html for scope and shared requirements.