fix(email,ui): billing emails follow customer language + readable payment-check confirmation

- Billing/dunning emails no longer render in the gallery event's language.
  emailProcessor now honors an explicit `__language` in the email data
  (else falls back to the event-first recipient resolution), and the invoice
  reminder passes the customer/invoice locale (customer.preferred_language
  || invoice.language || 'de'). Fixes German customers getting English
  dunning notices. (#760)
- Payment-check confirmation card ("Action recorded") is now theme-adaptive
  (green tint + readable text on both light and dark surfaces) instead of a
  hardcoded light-green mix + dark-green title that vanished in dark mode. (#759)
- The per-customer "Preferred language" field already exists
  (CustomerDetailPage) plus the business-profile default; updated the helper
  text to note billing emails now honor it too. (#761)
This commit is contained in:
Luca
2026-07-06 19:27:57 +02:00
parent a52317d8a5
commit fcc3e9195d
6 changed files with 20 additions and 12 deletions
+7 -4
View File
@@ -726,9 +726,12 @@ async function sendTemplateEmail(to, templateKey, variables) {
throw new Error('Email configuration not found');
}
// Determine recipient language (pass eventId if available in variables)
const language = await getRecipientLanguage(to, variables.eventId || null);
// Determine recipient language. An explicit `__language` in the email data
// wins (CRM/billing emails set it to the customer/invoice language so a
// gallery event's language can't override a dunning notice — see #760);
// otherwise fall back to the event-first recipient resolution.
const language = variables.__language || await getRecipientLanguage(to, variables.eventId || null);
// Process template with variables
const { subject, htmlBody, textBody } = await processTemplate(template, variables, language);
@@ -783,7 +786,7 @@ async function sendTemplateEmail(to, templateKey, variables) {
async function renderQueuedEmail(templateKey, variables = {}, to = '') {
const template = await db('email_templates').where('template_key', templateKey).first();
if (!template) return null;
const language = await getRecipientLanguage(to, variables.eventId || null);
const language = variables.__language || await getRecipientLanguage(to, variables.eventId || null);
const { subject, htmlBody } = await processTemplate(template, variables, language);
return { subject, html: htmlBody };
}
+3 -1
View File
@@ -141,7 +141,7 @@ async function applyReminder(invoice, lineItems, level, adminId) {
const rawDaysOverdue = Math.floor((Date.now() - new Date(invoice.due_date).getTime()) / 86400000);
const daysOverdue = Math.max(1, rawDaysOverdue);
const templateKey = level === 1 ? 'invoice_reminder_first' : 'invoice_reminder_second';
const locale = ctx.locale || invoice.language || 'de';
const locale = ctx.locale || customer.preferred_language || invoice.language || 'de';
const outstandingMinor = Math.max(0, newTotal - Number(invoice.paid_amount_minor || 0));
// Attach the (unchanged) original invoice PDF + the new Mahnung.
@@ -154,6 +154,8 @@ async function applyReminder(invoice, lineItems, level, adminId) {
const { to: reminderTo, cc: reminderCc } = resolveBillingRecipients(customer, invoice.cc_pdf_email);
try {
await emailProcessor.queueEmail(invoice.event_id || null, reminderTo, templateKey, {
// Render in the customer/invoice language, not the gallery event's (#760).
__language: locale,
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, locale),