Itemized Payment Receipts on Both Portal and Manual Routes
Every payment on DeedStack now emails an itemized receipt, whether it came through the card portal or was recorded manually as a check, Venmo, cash, or money order.
What the Receipt Shows
The receipt breaks down where the money went: property tax escrow, HOA, late fees, notice fees, postal fees, the loan payment split into principal and interest, and extra principal on its own line. It shows previous and new balance, next due date and amount, any shortfall on a partial payment, and what extra principal bought in payments and interest saved. A payoff is stated plainly.
Branding and Toggle
I built server/utils/emailBranding.js to resolve per-tenant branding through the same chain the Branding page uses: site_settings tenant row, then global row, then tenants row, then siteConfig. It fails open to platform defaults if the settings query errors.
The receipt has its own mobile-first email layout with a media query at 620px. No wide tables, no mailto: links. I kept it as a second layout instead of a parameter on getEmailTemplate, so none of the forty templates sharing that function changed.
A per-tenant paymentReceiptsEnabled toggle lives in site_settings under the email category and defaults ON. The UI is on the Email Templates settings page. The gate sits inside sendPaymentReceipt so no future route can forget it. This is separate from emailNotificationsEnabled in admin_settings, which is a platform-wide kill switch.
One Function for Both Routes
One sendPaymentReceipt() in server.js serves both the portal and manual routes. It renders only figures already present in allocation and effect. Nothing is recomputed in the template.
The Card Processing Fee Problem
The portal charges more than it allocates. POST /api/payments allocates on breakdown.subtotal but charges the card breakdown.total. The first version of the receipt showed total as "Total paid" over itemized lines summing to subtotal, an unexplained $18.43 gap. Card processing and convenience fees now get their own lines.
After adding those lines the receipt read $470.99 against a $471.00 charge. This happens because annual_tax / 12 rarely divides evenly and each line rounds on its own. I fixed it by taking the processing fee as the residual: total − subtotal − convenience. On any itemized document one line must be the residual, or rounding will make it fail to add up. The residual is also the truer figure. It is what the card was charged less everything that reached the loan.
Dropped the Duplicate Payoff Email
A payoff sent two congratulations emails. Worse, loanPaidOff is branded from module-level siteConfig constants while the receipt is branded as the tenant, so a Dev Testing customer received one email headed "Dev Testing" and one headed "DeedStack™" simultaneously. I dropped both calls to loanPaidOff in commit 0498a10.
I kept the loanPaidOff template instead of deleting it. The copy is worth preserving. I annotated it with two defects to fix before reuse: it brands from module-level constants instead of the tenant, and its "Total Paid" is purchase_price instead of what the customer actually paid over the loan's life.
What I Corrected
The task stated the manual route sent the customer nothing. Both routes had called sendPaymentConfirmation since commit 12251af in November 2025. I checked with git blame. The gap was content, not existence. The old email had seven fields, no escrow or fee breakdown, no extra principal, no previous balance, no partial shortfall, no per-tenant branding. Both routes sent the identical thin payload.
Verified
I drove six payment shapes through the real HTTP routes against tenant 7: plain with escrow, overdue with late fee, extra principal, principal-only, partial, payoff. All returned 200. Six emails, six email_logs rows, every one carrying tenant_id = 7, zero loanPaidOff rows. With the toggle off the payment still succeeded, no email sent, no log row written, reason logged.
Green Acres (tenant 21) fingerprint was identical before and after: 836122b3f7c837faf5039bb451c4d436, 135 rows across loans, payments, properties, email_logs, users and site_settings.