feat(crm): route billing docs to billing_email when set

Wires customer_accounts.billing_email into the invoice, Storno, and
payment-reminder send paths. Previously the column existed on the
schema and the customer-detail page rendered an input for it, but no
send path read it — every outbound email landed on customer_accounts.email
regardless. That mismatch is the failure mode flagged in
feedback_data_driven_completeness: a UI field that promises behavior
the backend silently doesn't deliver.

Routing matrix:
  - invoice / Storno / payment reminder
      To: billing_email (fallback email when unset)
      CC: email (when billing_email took the To slot) + per-doc cc_pdf_email
  - quote / contract / event reminder / gallery share
      To: email (unchanged — decision-maker address)
  - payment-check / paid-notification
      To: admin contact (unchanged — internal flow)

A new resolveBillingRecipients helper centralises the rules:
prefer billing_email, dedupe addresses case-insensitively, keep
per-doc cc_pdf_email as a supplemental CC. Lives in its own file
(_billingRecipients.js) to match the _renderContext.js convention.
This commit is contained in:
Luca
2026-05-27 14:04:15 +02:00
parent 3d37324080
commit 09c5110d2b
3 changed files with 183 additions and 6 deletions
+10 -6
View File
@@ -29,6 +29,7 @@ const { claimNextSequence } = require('../utils/documentSequences');
const { formatShortDate } = require('../utils/dateFormatter');
const businessProfileService = require('./businessProfileService');
const { buildIssuerBlock, buildRecipientBlock } = require('./_renderContext');
const { resolveBillingRecipients } = require('./_billingRecipients');
const pdfService = require('./pdfService');
const emailProcessor = require('./emailProcessor');
// Migration 119 line-item hierarchy helpers, shared with quoteService.
@@ -1880,7 +1881,8 @@ async function sendInvoice(id, adminId) {
status: newStatus, sent_at: new Date(), pdf_path: pdfPath, updated_at: new Date(),
});
await emailProcessor.queueEmail(invoice.event_id || null, customer.email, 'invoice_sent', {
const { to: invoiceTo, cc: invoiceCc } = resolveBillingRecipients(customer, invoice.cc_pdf_email);
await emailProcessor.queueEmail(invoice.event_id || null, invoiceTo, 'invoice_sent', {
invoice_number: invoice.invoice_number,
customer_name: customer.display_name || customer.first_name || customer.email.split('@')[0],
event_name: invoice.event_name || '',
@@ -1889,7 +1891,7 @@ async function sendInvoice(id, adminId) {
installment_label: invoice.installment_label || '',
installment_index: invoice.installment_index + 1,
installment_total: invoice.installment_total,
cc: invoice.cc_pdf_email || undefined,
cc: invoiceCc,
attachments: [{
filename: `${invoice.invoice_number}.pdf`,
contentPath: pdfPath,
@@ -2200,13 +2202,14 @@ async function sendStorno(stornoId, adminId) {
.select('invoice_number', 'issue_date').first()
: null;
await emailProcessor.queueEmail(storno.event_id || null, customer.email, 'storno_issued', {
const { to: stornoTo, cc: stornoCc } = resolveBillingRecipients(customer, storno.cc_pdf_email);
await emailProcessor.queueEmail(storno.event_id || null, stornoTo, 'storno_issued', {
storno_number: storno.invoice_number,
original_invoice_number: originalRow?.invoice_number || '',
original_issue_date: originalRow?.issue_date ? formatShortDate(originalRow.issue_date) : '',
customer_name: customer.display_name || customer.first_name || customer.email.split('@')[0],
total_amount: formatMajor(Math.abs(storno.total_amount_minor), storno.currency, ctx.locale),
cc: storno.cc_pdf_email || undefined,
cc: stornoCc,
attachments: [{
filename: `${storno.invoice_number}.pdf`,
contentPath: pdfPath,
@@ -2511,7 +2514,8 @@ async function applyReminder(invoice, lineItems, level, adminId) {
+ Number(lateFeeMinor || 0)
- Number(invoice.paid_amount_minor || 0));
await emailProcessor.queueEmail(invoice.event_id || null, customer.email, templateKey, {
const { to: reminderTo, cc: reminderCc } = resolveBillingRecipients(customer, invoice.cc_pdf_email);
await emailProcessor.queueEmail(invoice.event_id || null, reminderTo, templateKey, {
invoice_number: invoice.invoice_number,
customer_name: customer.display_name || customer.first_name || customer.email.split('@')[0],
total_amount: formatMajor(invoice.total_amount_minor, invoice.currency, ctx.locale),
@@ -2523,7 +2527,7 @@ async function applyReminder(invoice, lineItems, level, adminId) {
// (matches the quote_sent + invoice_sent templates).
due_date: formatShortDate(invoice.due_date),
days_overdue: daysOverdue,
cc: invoice.cc_pdf_email || undefined,
cc: reminderCc,
attachments: [{
filename: `${invoice.invoice_number}.pdf`,
contentPath: pdfPath,