Email workspaceCard transaction alert
Source: App\Mail\cardTransactionNotification, app/Mail/cardTransactionNotification.php; resources/views/mails/cardTransactionNotification.blade.php. Caller: MapleradController::handleWebhook, app/Http/Controllers/Wallet/MapleradController.php:64.
Subject: Card: {{masked_card_number}} transaction (unchanged).
Suggested preheader: Debit on virtual card {{masked_card_number}}: {{debit_amount}}.
Required portable values
| Token | Existing source | Handling |
|---|---|---|
| {{masked_card_number}} | card.card_pan_masked | Escaped masked display only, including subject. Never full PAN, CVV or expiry. |
| {{debit_amount}} | transaction.currency . transaction.amount | Preserve the original raw display, including zero/sign. Webhook stores provider amount / 100 before constructing the Mailable; do not divide again or add number_format silently. |
| {{transaction_description}} | transaction.description | Escaped text, even if supplied by provider; empty/0 remain visible row values. |
| {{login_url}} | Fixed https://app.built.africa | Validated original app-root destination; new label Log in to Built. Not a direct classification, dispute or reversal URL. |
| {{preheader}} | Suggested source-derived summary above | Escaped text with masked card only. |
Default logo/optional {{logo_url}} and shared CID/HTTPS assets: [shared-footer](../shared-footer.md). No optional business sections, legacy duplicate link, PDF or business attachment exists.
Trigger, recipient and unresolved source meaning
The issuing.transaction branch requires card_id and strtolower(status) == 'success', then resolves local card, subtracts amount / 100 from local balance and creates a VirtualCardRealtimeTransaction. With wallet and user found, it queues this email to wallet.user_id's email. Preserve those guards, queue(), balance/transaction writes and recipient lookup. From explicitly uses configured mail address with Built display; no Reply-To override. Underlying transport/outbox remains separate.
The original body says debit was successful. The webhook stores provider type but does not filter it before subtracting/sending, so the body is not independent proof that every incoming type is a debit. Retain the source wording and document this gap; changing event filtering/arithmetic/copy based on type needs separate application review. Do not invent credit/refund/declined variants unsupported by this body path.
The original amount is raw transaction.currency plus transaction.amount, unlike formatted receipt amounts. Keep the instruction to log in and classify this expense in the books inside the email. No provider reference, time, fee, balance, merchant field or resolution claim is added.
Integration and safety
Use the existing Mailable with a complete HTML Blade view and proposed text alternative. Portable {{placeholder_name}} tokens are not automatically Blade. Escape values/attributes, validate approved app-root and image URLs separately and use one pass for replacements. Only fictional masked displays are used in fixtures. Shared PNG footer badges need matching CID attachments or approved absolute HTTPS URLs, independently of business attachments.
Three states cover successful-debit body, supplied zero and long values. Files: public/ready/card-transaction.html/.txt and previews/card-transaction-*.html/.txt. No separate project-owned plain-text counterpart found. Browser rendering does not validate financial/provider correctness, actual image delivery, Gmail/Outlook/iOS Mail or current production sending.