BuiltEmail workspace

Transfer review / retry alert — internal operations

Source: App\Mail\PaymentError, app/Mail/PaymentError.php; resources/views/emails/invoice/paymentError.blade.php using mail::message and conditional mail::button. Native subject: Transfer failed (unchanged). Suggested preheader: A transfer notice needs internal review.

Portable values

TokenExisting source / derivationHandling
{{business_name}}business ? business.name : ''Required row, escaped; null Business leaves it empty rather than fabricating an identity.
{{transfer_amount}}(business ? business.currency.symbol : '') . number_format(constructor amount, 2)Required complete original display, including zero/sign and no currency prefix when business is absent. Do not infer conversion from a caller's fx_amount or replace symbol with code.
{{failure_reason}}constructor reasonRequired escaped multiline row, including empty/null/0. Preserve diagnostic meaning without treating provider/exception text as raw HTML. Do not copy real payloads, secrets or stack traces into fixtures.
{{retry_url}}original constructor link from caller/helperConditional complete validated original completion URL. Do not invent a URL from missing context, rewrite a #, or silently normalize query values.
{{app_name}}config('app.name')Original Thanks sign-off.
{{preheader}}Suggested internal-review summary aboveEscaped text, no verified failure/settlement claim.

Default Built logo/optional {{logo_url}} and shared CID/approved HTTPS assets: [shared-footer](../shared-footer.md). All plain-text values remain escaped in HTML/attributes. Missing business is an established state; missing required non-null context needs an explicit integration decision, not a guessed currency/retry link.

Two independent visibility decisions

Compute source_link_present from the original link using PHP truthiness before any mapping, prefixing or normalization. Source @if(link) controls the invoice-action instruction, even for the truthy string #. Null, empty string and literal 0 are PHP-falsy. Preserve that predicate separately from usable-link validation.

Compute retry_available as a strict boolean only when the original source link exists and the complete destination passes the consuming project's approved HTTPS host/path/query checks. The ready HTML/text mark the invoice instruction, validated retry block and unavailable-link block independently. Remove whole blocks as appropriate; these booleans are adapter state, not new printed tokens/application fields. When original link is # or otherwise unusable, retain the original-link invoice instruction, omit the button/fallback and show “No usable retry link was included in this notice.” When original link is PHP-falsy, omit all three blocks. A false original-link flag suppresses retry even if a stale URL/true retry flag was supplied.

For invoice transfers, the copy retains the source prerequisite that transfer action must be completed before the invoice can be marked as paid. It is qualified to invoice transfers rather than asserting an actual invoice state for null-business/wallet diagnostic callers. The native source warning is not proof of the current record's state or that a retry can safely execute. The neutral Transfer needs review heading avoids claiming verified failure for callbacks that reuse this failure Mailable with other statuses. The native subject is not silently changed.

Link construction and callers

paymentsController::sendFailedNotification, app/Http/Controllers/Income/paymentsController.php:458–469, uses transaction_id truthiness to build https://web.builtaccounting.com/completepayment?tx_ref={reference}&transaction_id={transaction_id}&status={status}&c={c}, or null. Preserve the complete original string and its reference/ID/status/c tuple. False c is concatenated as an empty string, true as 1; do not reconstruct it as a new numeric/status policy. Some callers pass references as transaction_id/status, so a syntactically valid URL is not proof of correct identifiers or authorization. Do not fetch live completion URLs during design validation. Button and visible fallback are one value, with no independent legacy duplicate fields.

Native invoice bank/mobile-money failure calls are at completeMomopayment :215/:243 and completeCardPayment :587/:614, with the original shouldApplyPayment guard and c distinction. Direct exception sends at :450/:800/:1291 pass null business, 0 and #. Preserve existing financial writes, idempotence, status checks, error handling and retry behavior rather than “repairing” them in HTML.

Other static helper references: WalletKEController::withdraw :300 (null transaction ID), NSANOController::confirmdebit :451 (diagnostic exception/null transaction ID), ATCallbackEvent::broadcastOn :50 and FlutterWaveWebhook::broadcastOn :80 (both guarded to non-production). AT/FlutterWave events have controller dispatch references. AT's var_export(data) lacks return=true and can return null while printing its diagnostic data; the template must not invent a reason. NSANOCrediCallBack::broadcastOn :49 invokes the helper for a found transfer without filtering code, including SUCCESS; a project dispatch was not found for this event class in the current trace. Treat that reachability as unresolved, not production delivery proof. Commented PaystackController and older completepayment helper calls are not active usage evidence.

The helper/direct paths queue PaymentError to their existing fixed internal recipients. No From/Reply-To override or business PDF/attachment is configured. Preserve subject, To policy, queue() and transport/outbox behavior. Different callback reasons/statuses remain data states of one email, not new success/refund/approval notifications.

Native integration and safety

Use the existing Mailable with a full HTML Blade view and proposed plain-text alternative. Portable {{placeholder_name}} tokens are not automatically Blade. Escape every dynamic text/attribute; validate retry URL separately for approved host/path and original query context, then use one pass if replacing tokens. Define an authorized production diagnostic-redaction policy before exposing arbitrary exception content; do not truncate important meaning silently or treat it as HTML. No invented retry guarantee, alternative endpoint, deadline or ledger/status update belongs in this redesign.

Eight fictional states cover bank/mobile-money retry, null link, # with missing business/zero, callback SUCCESS, empty reason, original link 0 and long fields. Files: public/ready/transfer-failure.html/.txt and previews/transfer-failure-*.html/.txt. No separate project-owned plain-text source found. Browser checks do not establish actual retry safety, record/route authorization, provider/collection result, email-client compatibility, CID delivery or current production sending.