BuiltEmail workspace

Payment request contract

Implementation: App\Mail\SendLinkEmail, app/Mail/SendLinkEmail.php. Original Markdown body: resources/views/emails/SendLinkEmail.blade.php using mail::message, mail::panel and optional mail::promotion. Caller: app/Http/Controllers/General/emailsController.php:344–371, sendLinkToEmails. Static references are usage evidence, not proof of current production delivery.

Subject: Payment request from {{wallet_business_name}} (existing exact pattern).

Suggested preheader: Payment request from {{wallet_business_name}}: {{request_title}}.

Read docs/integration/shared-footer.md for the common PNG/CID assets and medium action policy.

Portable placeholders

TokenExisting source / derivationHandling
{{logo_url}}Original approved Built app_name PNGOptional deployment override. Concrete HTTPS logo already exists; use approved HTTPS or an actual matching CID.
{{preheader}}Suggested existing wallet-label/title summary aboveRequired escaped text; not a deadline or fixed-charge promise.
{{wallet_business_name}}$wallet->business_nameRequired escaped body, subject and Regards sign-off identity. It is separate from Business.name used for From/Reply-To display.
{{request_title}}$link->titleRequired escaped source payment-request title; not an invoice number or customer name.
{{requested_amount}}$wallet->currency . number_format($link->amount, 2)Required complete existing formatted display, retaining prefix, two decimals, zero and sign. Do not substitute business.currency, fx_currency, transaction amount, minimum/maximum or a fee-inclusive total.
{{request_description}}$link->descriptionOptional escaped multiline text under the original PHP truthiness predicate. Original Blade already escapes this field; preserve literal markup if present rather than treating it as raw HTML or applying a lossy rich-text converter.
{{payment_url}}$link->short_linkRequired complete stored HTTPS payment link. Validate the approved short-link/direct secure hosts and paths separately, then escape attributes. Button and visible fallback use this exact one value. Do not rebuild from invoice UUID or an unused variable.

Trigger, routing and headers

routes/api.php:1775 POST link/{uuid}/send/email calls sendLinkToEmails inside the existing wallet routing. The method requires emails, splits request.emails by comma, finds PaymentLink by uuid and Wallet by link.wallet_id, then loops Mail::to(email)->send(new SendLinkEmail(link, wallet)). Preserve the current lookup/routing and individual sends. It does not establish a customer-only recipient, per-address validation, deduplication or trimming inside this method. Do not add/remove security or validation behavior through HTML; assess the target project’s authorization separately during integration.

The Mailable receives the existing PaymentLink and Wallet. It loads Business through wallet.business_id and User through business.owner_id without events. From uses config(mail.from.address) with business.name. Reply-To uses business.email ?: owner.email with business.name display. Body/subject/sign-off use wallet.business_name instead, which can differ from business.name. Do not collapse these sources or use wallet.user_id for this request’s owner fallback.

The caller explicitly uses send(), not queue(). Queueable/SerializesModels alone do not change the native call. Preserve this behavior; the configured transport may have asynchronous outbox behavior underneath Laravel, so this is not proof of immediate provider delivery. No source business attachment/PDF is configured. Shared footer PNGs remain separate inline-image requirements.

Payment destination and action meaning

WalletController::createLink (around :2554–2567) builds config(app.secure_URL) + /bw/pay/{link.uuid}. Production uses ShortURLFacade with redirectStatusCode(302), stores config(app.short_url) + /p/{url_key}, and non-production stores the direct secure URL. The WhatsApp link-creation path uses the same destination scheme, but it is a separate channel. Preserve whichever full value is stored in short_link, including its key/UUID and any existing parameters; do not rewrite to a guessed checkout-provider URL.

The source email prints Pay Now followed by short_link. The draft turns that existing destination into one Open payment link action, explains that it opens the merchant’s payment page and keeps a visible fallback. It does not review invoice details, automatically charge the recipient, confirm payment or promise a particular external provider. There are no separate legacy duplicate email link fields.

The original local $base_url = config(app.url) + /viewinvoice/ is unused and is not an action to preserve or invent. Keep application/invoice review separate from payment-page navigation.

Conditions and amount semantics

The complete marked description table is present in the ready extract. Retain it only when the original link.description is PHP-truthy; null, empty string and literal string 0 omit it. Remove its entire wrapper/spacing in HTML and the corresponding text block. No HTML normalization is needed for this already-escaped field, unlike raw merchant-message Mailables. Preserve all nonempty literal text and line breaks.

The source displays link.amount regardless of can_change_amount. Link models also store min_amount, max_amount, limit, expires_at, one_time, is_active and archived, but none is rendered as an instruction in this email. Keep those application controls in their current checkout/lifecycle paths. Do not add an expiry, deadline, fixed-charge promise, free-payment claim or guessed min/max. Requested amount is the stored display, not proof of the eventual amount paid. Missing/invalid required link/title/amount/URL needs an explicit target-project decision rather than an invented value.

Six states cover description, absence, literal-0 absence, editable zero amount, direct versus short destination, and long values. Zero/editable fixtures are rendering/source-data examples, not a claim that checkout is free or every zero-amount request can complete.

Native integration and safety

Portable {{placeholder_name}} tokens are not automatically Laravel Blade expressions. Prefer the existing Mailable with a complete HTML view and separate text view, mapped explicitly to its existing objects. Do not nest the complete document in mail::message or introduce a new framework. Keep native subject, sender/Reply-To, recipient iteration and send() behavior.

Escape every dynamic title, wallet label, formatted amount, description and attribute. Validate payment_url separately for approved HTTPS short-link or secure payment-page destinations, preserving complete values. If using string replacement, use one pass so inserted token-looking text remains literal and is never re-evaluated as markup. Image hosting/CID overrides require the shared contract’s separate validation and actual embedded assets. Relative local preview URLs are not production endpoints.

Files: public/ready/payment-request.html and .txt; public/previews/payment-request-*.html/.txt. No separate project-owned plain-text file was found for this Markdown source; the supplied text is proposed equivalent content. No real request, link, checkout, charge or email is created/opened/sent here. Browser rendering does not establish Gmail/Outlook/iOS Mail, actual CID delivery, checkout availability or production delivery.