From fcc3e9195d6f63b2dffddfa72a867a3e32325e81 Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Mon, 6 Jul 2026 19:15:24 +0200 Subject: [PATCH] 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) --- backend/src/services/emailProcessor.js | 11 +++++++---- backend/src/services/invoice/reminders.js | 4 +++- frontend/src/i18n/locales/de.json | 2 +- frontend/src/i18n/locales/en.json | 2 +- frontend/src/pages/admin/CustomerDetailPage.tsx | 2 +- frontend/src/pages/public/PaymentCheckPage.tsx | 11 +++++++---- 6 files changed, 20 insertions(+), 12 deletions(-) diff --git a/backend/src/services/emailProcessor.js b/backend/src/services/emailProcessor.js index 65be426d..4de4ab27 100644 --- a/backend/src/services/emailProcessor.js +++ b/backend/src/services/emailProcessor.js @@ -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 }; } diff --git a/backend/src/services/invoice/reminders.js b/backend/src/services/invoice/reminders.js index c8fecef4..3bf2edd4 100644 --- a/backend/src/services/invoice/reminders.js +++ b/backend/src/services/invoice/reminders.js @@ -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), diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 971feefe..1903b87b 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -4074,7 +4074,7 @@ "noEvents": "Noch keinem Event zugewiesen. Fügen Sie diesen Kunden über das Event-Formular hinzu.", "email": "E-Mail", "preferredLanguage": "Bevorzugte Sprache", - "preferredLanguageHint": "Steuert die Portal-Sprache sowie die Sprache von Angebots- und Rechnungs-PDFs. Neue Kunden erben standardmässig die Sprache aus dem Geschäftsprofil ({{lang}}); hier kann pro Kunde überschrieben werden.", + "preferredLanguageHint": "Steuert die Portal-Sprache, Angebots-/Rechnungs-PDFs sowie Rechnungs-E-Mails (Erinnerungen/Mahnungen). Neue Kunden erben standardmässig die Sprache aus dem Geschäftsprofil ({{lang}}); hier kann pro Kunde überschrieben werden.", "salutation": "Anrede", "salutationNone": "—", "firstName": "Vorname", diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 93df80e8..694eb762 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -4074,7 +4074,7 @@ "noEvents": "Not assigned to any events yet. Add this customer to an event from the event form.", "email": "Email", "preferredLanguage": "Preferred language", - "preferredLanguageHint": "Drives portal UI and quote/invoice PDF locale. New customers default to the business-profile language ({{lang}}); override here per customer.", + "preferredLanguageHint": "Drives portal UI, quote/invoice PDFs, and billing emails (reminders/dunning). New customers default to the business-profile language ({{lang}}); override here per customer.", "salutation": "Salutation", "salutationNone": "—", "firstName": "First name", diff --git a/frontend/src/pages/admin/CustomerDetailPage.tsx b/frontend/src/pages/admin/CustomerDetailPage.tsx index a9d0c678..899f05a2 100644 --- a/frontend/src/pages/admin/CustomerDetailPage.tsx +++ b/frontend/src/pages/admin/CustomerDetailPage.tsx @@ -360,7 +360,7 @@ export const CustomerDetailPage: React.FC = () => {

{t('customers.detail.preferredLanguageHint', - 'Drives portal UI and quote/invoice PDF locale. New customers default to the business-profile language ({{lang}}); override here per customer.', + 'Drives portal UI, quote/invoice PDFs, and billing emails (reminders/dunning). New customers default to the business-profile language ({{lang}}); override here per customer.', { lang: LOCALE_LABELS[profileDefaultLocale] || profileDefaultLocale.toUpperCase() })}

diff --git a/frontend/src/pages/public/PaymentCheckPage.tsx b/frontend/src/pages/public/PaymentCheckPage.tsx index 2a3a376a..f3ccbb9a 100644 --- a/frontend/src/pages/public/PaymentCheckPage.tsx +++ b/frontend/src/pages/public/PaymentCheckPage.tsx @@ -373,15 +373,18 @@ const ResultBox: React.FC<{
-

+

{t('paymentCheck.result.title', 'Action recorded')}

-

+

{result.applied === 'paid_full' && t('paymentCheck.result.paid', 'Invoice {{n}} marked as paid in full.', { n: inv.invoiceNumber })} {result.applied === 'paid_with_skonto' && t('paymentCheck.result.paidSkonto',