Просмотр исходного кода

Send default SMS templates in the workshop's language (#595)

* Send default SMS templates in the workshop's language

* Reset an SMS template to the workshop's language

* Say in AGENTS.md whose language customer text is in

* Say in AGENTS.md whose clock a time is on
Bernt Christian Egeland 7 часов назад
Родитель
Сommit
04c9bb04b8

+ 17 - 0
AGENTS.md

@@ -129,6 +129,23 @@ a cleanup.
 `Date` on the server. Amounts carry a currency code (`src/lib/money.ts`), countries are
 ISO 3166-1, languages ISO 639-1.
 
+**Whose clock.** Three clocks are in play and each is wrong somewhere: the server's (UTC in
+Docker), the viewer's browser, and the workshop's. Anything a customer or the workshop reads as
+a time or a day (texts, emails, PDFs, share pages, bookings, reminders, "today", "this month",
+due dates) is on the workshop's clock: read its zone with `workshopTimeZone(orgId)` from
+`src/lib/workshop-timezone.ts` and convert
+with `src/lib/timezone.ts`. The traps: `toLocaleString`, `toISOString().slice(0, 10)` and
+`new Date(y, m, d)` on the server are UTC; next-intl's `useFormatter` formats in the server's
+zone, so a calendar day built at local midnight shows as the day before (use `useDayFormatter`
+in `src/features/calendar/Components`); a date typed into a form is the workshop's wall clock,
+parsed with `parseWorkshopDateTime`. A test for anything with a time runs in a zone far from UTC.
+
+**Whose language.** Anything a customer receives (SMS, email, PDF, share page) is written in
+the workshop's language: `resolveDocumentLocale(orgId)` on the server, never `useTranslations`
+in a client component, which is the language of whoever is signed in. Defaults shown in
+settings, and what a Reset button restores, come from the same place, or saving them stores the
+viewer's language as the workshop's text.
+
 **Nothing country-specific by default.** Registries, tax rules, payment providers and parts
 sources ship as connectors or settings a workshop turns on, never as behaviour everyone sees.
 

+ 1 - 1
messages/de/settings.json

@@ -978,7 +978,7 @@
     "saveSmsTemplates": "SMS-Vorlagen speichern",
     "smsTemplateSaved": "SMS-Vorlageneinstellungen gespeichert",
     "smsDefaults": {
-      "invoiceReady": "Ihre Rechnung ist fertig. Hier ansehen: {share_link}",
+      "invoiceReady": "Rechnung von {company_name} ist fertig. Hier ansehen: {share_link}\nViele Grüße {company_name} - {current_user}",
       "quoteReady": "Ihr Angebot steht zur Prüfung bereit. Hier ansehen: {share_link}",
       "inspectionReady": "Ihr Fahrzeuginspektionsbericht ist fertig. Hier ansehen: {share_link}",
       "statusInProgress": "Die Arbeit an Ihrem Fahrzeug hat begonnen. Wir halten Sie auf dem Laufenden.",

+ 1 - 1
messages/en/settings.json

@@ -983,7 +983,7 @@
     "layoutDocInvoice": "Invoice",
     "layoutDocQuote": "Quote",
     "smsDefaults": {
-      "invoiceReady": "Your invoice is ready. View it here: {share_link}",
+      "invoiceReady": "Invoice from {company_name} is ready. View it here: {share_link}\nRegards {company_name} - {current_user}",
       "quoteReady": "Your quote is ready for review. View it here: {share_link}",
       "inspectionReady": "Your vehicle inspection report is ready. View it here: {share_link}",
       "statusInProgress": "Work has started on your vehicle. We'll keep you updated.",

+ 1 - 1
messages/es/settings.json

@@ -978,7 +978,7 @@
     "saveSmsTemplates": "Guardar plantillas SMS",
     "smsTemplateSaved": "Configuración de plantillas SMS guardada",
     "smsDefaults": {
-      "invoiceReady": "Su factura está lista. Véala aquí: {share_link}",
+      "invoiceReady": "La factura de {company_name} está lista. Véala aquí: {share_link}\nSaludos {company_name} - {current_user}",
       "quoteReady": "Su presupuesto está listo para revisión. Véalo aquí: {share_link}",
       "inspectionReady": "Su informe de inspección vehicular está listo. Véalo aquí: {share_link}",
       "statusInProgress": "Se ha comenzado a trabajar en su vehículo. Le mantendremos informado.",

+ 1 - 1
messages/fr/settings.json

@@ -978,7 +978,7 @@
     "saveSmsTemplates": "Enregistrer les modèles SMS",
     "smsTemplateSaved": "Paramètres des modèles SMS enregistrés",
     "smsDefaults": {
-      "invoiceReady": "Votre facture est prête. Consultez-la ici : {share_link}",
+      "invoiceReady": "La facture de {company_name} est prête. Consultez-la ici : {share_link}\nCordialement {company_name} - {current_user}",
       "quoteReady": "Votre devis est prêt pour examen. Consultez-le ici : {share_link}",
       "inspectionReady": "Votre rapport d'inspection véhicule est prêt. Consultez-le ici : {share_link}",
       "statusInProgress": "Les travaux sur votre véhicule ont commencé. Nous vous tiendrons informé.",

+ 1 - 1
messages/it/settings.json

@@ -978,7 +978,7 @@
     "saveSmsTemplates": "Salva modelli SMS",
     "smsTemplateSaved": "Impostazioni modelli SMS salvate",
     "smsDefaults": {
-      "invoiceReady": "La tua fattura è pronta. Visualizzala qui: {share_link}",
+      "invoiceReady": "La fattura di {company_name} è pronta. Visualizzala qui: {share_link}\nCordiali saluti {company_name} - {current_user}",
       "quoteReady": "Il tuo preventivo è pronto per la revisione. Visualizzalo qui: {share_link}",
       "inspectionReady": "Il rapporto di ispezione del veicolo è pronto. Visualizzalo qui: {share_link}",
       "statusInProgress": "I lavori sul tuo veicolo sono iniziati. Ti terremo aggiornato.",

+ 1 - 1
messages/lt/settings.json

@@ -983,7 +983,7 @@
     "layoutDocInvoice": "Sąskaita",
     "layoutDocQuote": "Pasiūlymas",
     "smsDefaults": {
-      "invoiceReady": "Jūsų sąskaita paruošta. Peržiūrėkite čia: {share_link}",
+      "invoiceReady": "Sąskaita iš {company_name} paruošta. Peržiūrėkite čia: {share_link}\nPagarbiai {company_name} - {current_user}",
       "quoteReady": "Jūsų pasiūlymas paruoštas peržiūrai. Peržiūrėkite čia: {share_link}",
       "inspectionReady": "Jūsų transporto priemonės apžiūros ataskaita paruošta. Peržiūrėkite čia: {share_link}",
       "statusInProgress": "Darbas su jūsų transporto priemone pradėtas. Informuosime apie eigą.",

+ 1 - 1
messages/nb/settings.json

@@ -983,7 +983,7 @@
     "layoutDocInvoice": "Faktura",
     "layoutDocQuote": "Tilbud",
     "smsDefaults": {
-      "invoiceReady": "Fakturaen din er klar. Se den her: {share_link}",
+      "invoiceReady": "Faktura fra {company_name} er klar. Se den her: {share_link}\nMvh {company_name} - {current_user}",
       "quoteReady": "Tilbudet ditt er klart for gjennomgang. Se det her: {share_link}",
       "inspectionReady": "Inspeksjonsrapporten for kjøretøyet ditt er klar. Se den her: {share_link}",
       "statusInProgress": "Arbeidet med kjøretøyet ditt har startet. Vi holder deg oppdatert.",

+ 1 - 1
messages/nl/settings.json

@@ -978,7 +978,7 @@
     "saveSmsTemplates": "SMS-sjablonen opslaan",
     "smsTemplateSaved": "SMS-sjablooninstellingen opgeslagen",
     "smsDefaults": {
-      "invoiceReady": "Uw factuur is gereed. Bekijk deze hier: {share_link}",
+      "invoiceReady": "Factuur van {company_name} is gereed. Bekijk deze hier: {share_link}\nMet vriendelijke groet {company_name} - {current_user}",
       "quoteReady": "Uw offerte is klaar voor beoordeling. Bekijk deze hier: {share_link}",
       "inspectionReady": "Uw voertuiginspectierapport is gereed. Bekijk het hier: {share_link}",
       "statusInProgress": "Er wordt aan uw voertuig gewerkt. We houden u op de hoogte.",

+ 1 - 1
messages/pl/settings.json

@@ -978,7 +978,7 @@
     "saveSmsTemplates": "Zapisz szablony SMS",
     "smsTemplateSaved": "Ustawienia szablonów SMS zapisane",
     "smsDefaults": {
-      "invoiceReady": "Twoja faktura jest gotowa. Zobacz ją tutaj: {share_link}",
+      "invoiceReady": "Faktura od {company_name} jest gotowa. Zobacz ją tutaj: {share_link}\nPozdrawiamy {company_name} - {current_user}",
       "quoteReady": "Twoja wycena jest gotowa do przeglądu. Zobacz ją tutaj: {share_link}",
       "inspectionReady": "Raport z inspekcji pojazdu jest gotowy. Zobacz go tutaj: {share_link}",
       "statusInProgress": "Rozpoczęto prace nad Twoim pojazdem. Będziemy Cię informować.",

+ 1 - 1
messages/pt-BR/settings.json

@@ -978,7 +978,7 @@
     "saveSmsTemplates": "Salvar modelos SMS",
     "smsTemplateSaved": "Configurações dos modelos SMS salvas",
     "smsDefaults": {
-      "invoiceReady": "Sua fatura está pronta. Veja aqui: {share_link}",
+      "invoiceReady": "A fatura de {company_name} está pronta. Veja aqui: {share_link}\nAtenciosamente {company_name} - {current_user}",
       "quoteReady": "Seu orçamento está pronto para revisão. Veja aqui: {share_link}",
       "inspectionReady": "O relatório de inspeção do seu veículo está pronto. Veja aqui: {share_link}",
       "statusInProgress": "O trabalho no seu veículo foi iniciado. Manteremos você informado.",

+ 1 - 1
messages/pt-PT/settings.json

@@ -978,7 +978,7 @@
     "saveSmsTemplates": "Guardar modelos de SMS",
     "smsTemplateSaved": "Definições dos modelos de SMS guardadas",
     "smsDefaults": {
-      "invoiceReady": "A sua fatura está pronta. Consulte-a aqui: {share_link}",
+      "invoiceReady": "A fatura de {company_name} está pronta. Consulte-a aqui: {share_link}\nCumprimentos {company_name} - {current_user}",
       "quoteReady": "O seu orçamento está pronto para análise. Consulte-o aqui: {share_link}",
       "inspectionReady": "O relatório de inspeção da sua viatura está pronto. Consulte-o aqui: {share_link}",
       "statusInProgress": "Começámos a trabalhar na sua viatura. Vamos mantê-lo informado.",

+ 1 - 1
messages/ru/settings.json

@@ -983,7 +983,7 @@
     "layoutDocInvoice": "Счёт",
     "layoutDocQuote": "Предложение",
     "smsDefaults": {
-      "invoiceReady": "Ваш счёт готов. Просмотрите его здесь: {share_link}",
+      "invoiceReady": "Счёт от {company_name} готов. Просмотрите его здесь: {share_link}\nС уважением, {company_name} - {current_user}",
       "quoteReady": "Ваше предложение готово для рассмотрения. Просмотрите его здесь: {share_link}",
       "inspectionReady": "Отчёт осмотра вашего транспортного средства готов. Просмотрите его здесь: {share_link}",
       "statusInProgress": "Начаты работы над вашим транспортным средством. Мы будем держать вас в курсе.",

+ 1 - 1
messages/tr/settings.json

@@ -978,7 +978,7 @@
     "saveSmsTemplates": "SMS Şablonlarını Kaydet",
     "smsTemplateSaved": "SMS şablon ayarları kaydedildi",
     "smsDefaults": {
-      "invoiceReady": "Faturanız hazır. Buradan görüntüleyin: {share_link}",
+      "invoiceReady": "{company_name} faturanız hazır. Buradan görüntüleyin: {share_link}\nSaygılarımızla {company_name} - {current_user}",
       "quoteReady": "Teklifiniz incelemeye hazır. Buradan görüntüleyin: {share_link}",
       "inspectionReady": "Araç muayene raporunuz hazır. Buradan görüntüleyin: {share_link}",
       "statusInProgress": "Aracınız üzerinde çalışmaya başlandı. Sizi bilgilendirmeye devam edeceğiz.",

+ 5 - 0
src/__tests__/features/messaging/send-document-link.test.ts

@@ -47,6 +47,11 @@ vi.mock('@/lib/features', () => ({
 }))
 
 const deliverSms = vi.fn()
+// The defaults in the workshop's language; this test's workshop reads English.
+vi.mock('@/features/sms/Lib/smsTemplateDefaults.server', async () => {
+  const { SMS_TEMPLATE_DEFAULTS } = await import('@/lib/sms-templates')
+  return { smsTemplateDefaults: async () => SMS_TEMPLATE_DEFAULTS }
+})
 vi.mock('@/features/sms/Lib/deliverSms.server', () => ({
   deliverSms: (org: string, input: unknown) => deliverSms(org, input),
 }))

+ 62 - 0
src/__tests__/features/sms/sms-template-defaults.test.ts

@@ -0,0 +1,62 @@
+/**
+ * A workshop that never wrote its own SMS wording texts its customers in the
+ * workshop's language, the same wording Settings → Templates shows, and the
+ * invoice text signs off on its own line.
+ */
+
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+vi.mock('server-only', () => ({}))
+
+let workshopLocale = 'nb'
+vi.mock('@/i18n/locale-from-request', () => ({
+  resolveDocumentLocale: async () => workshopLocale,
+}))
+
+const { smsTemplateDefaults } = await import('@/features/sms/Lib/smsTemplateDefaults.server')
+const { interpolateSmsTemplate } = await import('@/lib/sms-templates')
+const { SETTING_KEYS } = await import('@/features/settings/Schema/settingsSchema')
+
+const INVOICE = SETTING_KEYS.SMS_TEMPLATE_INVOICE_READY
+const values = {
+  company_name: 'Egeland Auto',
+  share_link: 'https://app.test/share/invoice/1',
+  current_user: 'Bernt Christian Egeland',
+}
+
+beforeEach(() => {
+  workshopLocale = 'nb'
+})
+
+describe('the default SMS wording', () => {
+  it('is in the workshop’s language, with the sign-off on its own line', async () => {
+    const defaults = await smsTemplateDefaults('org')
+
+    expect(interpolateSmsTemplate(defaults[INVOICE], values)).toBe(
+      'Faktura fra Egeland Auto er klar. Se den her: https://app.test/share/invoice/1\n' +
+        'Mvh Egeland Auto - Bernt Christian Egeland'
+    )
+  })
+
+  it('follows the workshop when it reads English', async () => {
+    workshopLocale = 'en'
+
+    const defaults = await smsTemplateDefaults('org')
+
+    expect(interpolateSmsTemplate(defaults[INVOICE], values)).toBe(
+      'Invoice from Egeland Auto is ready. View it here: https://app.test/share/invoice/1\n' +
+        'Regards Egeland Auto - Bernt Christian Egeland'
+    )
+  })
+
+  it('has every template, in every language the app speaks', async () => {
+    for (const locale of ['de', 'es', 'fr', 'it', 'lt', 'nl', 'pl', 'pt-BR', 'pt-PT', 'ru', 'tr']) {
+      workshopLocale = locale
+      const defaults = await smsTemplateDefaults('org')
+
+      expect(Object.keys(defaults)).toHaveLength(8)
+      expect(defaults[INVOICE]).toContain('{share_link}')
+      expect(defaults[INVOICE]).toContain('\n')
+    }
+  })
+})

+ 5 - 12
src/app/(authenticated)/settings/templates/page.tsx

@@ -10,6 +10,7 @@ import { getChecklistLanguage, getTemplates } from '@/features/inspections/Actio
 import { listMarkTypes } from '@/features/condition-map/Actions/markTypeActions'
 import { db } from '@/lib/db'
 import { getTranslations } from 'next-intl/server'
+import { smsTemplateDefaults } from '@/features/sms/Lib/smsTemplateDefaults.server'
 import {
   getInvoiceLayoutConfig,
   getQuoteLayoutConfig,
@@ -129,18 +130,9 @@ export default async function TemplatePage() {
       | 'imperial',
   }
 
-  const smsDefaultMap: Record<string, string> = {
-    [SETTING_KEYS.SMS_TEMPLATE_INVOICE_READY]: t.raw('templates.smsDefaults.invoiceReady'),
-    [SETTING_KEYS.SMS_TEMPLATE_QUOTE_READY]: t.raw('templates.smsDefaults.quoteReady'),
-    [SETTING_KEYS.SMS_TEMPLATE_INSPECTION_READY]: t.raw('templates.smsDefaults.inspectionReady'),
-    [SETTING_KEYS.SMS_TEMPLATE_STATUS_IN_PROGRESS]: t.raw('templates.smsDefaults.statusInProgress'),
-    [SETTING_KEYS.SMS_TEMPLATE_STATUS_WAITING_PARTS]: t.raw(
-      'templates.smsDefaults.statusWaitingParts'
-    ),
-    [SETTING_KEYS.SMS_TEMPLATE_STATUS_READY]: t.raw('templates.smsDefaults.statusReady'),
-    [SETTING_KEYS.SMS_TEMPLATE_STATUS_COMPLETED]: t.raw('templates.smsDefaults.statusCompleted'),
-    [SETTING_KEYS.SMS_TEMPLATE_PAYMENT_RECEIVED]: t.raw('templates.smsDefaults.paymentReceived'),
-  }
+  // The same defaults a text falls back to when it is sent: the workshop's
+  // language, not whoever is looking at this page.
+  const smsDefaultMap = await smsTemplateDefaults(data.organizationId)
 
   const smsTemplates: Record<string, string> = {}
   for (const key of Object.keys(smsDefaultMap)) {
@@ -176,6 +168,7 @@ export default async function TemplatePage() {
       checklistLanguage={checklistLanguage.success ? (checklistLanguage.data ?? null) : null}
       smsEnabled={features.sms ?? false}
       initialSmsTemplates={smsTemplates}
+      smsDefaults={smsDefaultMap}
       logoUrl={settings[SETTING_KEYS.COMPANY_LOGO] || undefined}
       workshop={workshop}
       savedDesigns={savedDesigns}

+ 9 - 2
src/app/(authenticated)/settings/templates/template-settings.tsx

@@ -440,14 +440,18 @@ const smsTemplateFields = [
 function SmsTemplateTab({
   values,
   setValues,
+  defaults,
 }: {
   values: Record<string, string>
   setValues: (v: Record<string, string>) => void
+  /** The defaults in the workshop's language, the ones a text falls back to when sent. */
+  defaults: Record<string, string>
 }) {
   const t = useTranslations('settings')
   const handleReset = (key: string) => {
     const field = smsTemplateFields.find((f) => f.key === key)
-    const defaultVal = field ? t.raw(`templates.smsDefaults.${field.defaultKey}`) : ''
+    const defaultVal =
+      defaults[key] ?? (field ? t.raw(`templates.smsDefaults.${field.defaultKey}`) : '')
     setValues({ ...values, [key]: defaultVal })
   }
 
@@ -554,6 +558,7 @@ export function TemplateSettings({
   checklistLanguage = null,
   smsEnabled = false,
   initialSmsTemplates = {},
+  smsDefaults = {},
   logoUrl,
   workshop,
   invoiceLayoutConfig,
@@ -569,6 +574,8 @@ export function TemplateSettings({
   checklistLanguage?: string | null
   smsEnabled?: boolean
   initialSmsTemplates?: Record<string, string>
+  /** Reset restores these, in the workshop's language rather than the viewer's. */
+  smsDefaults?: Record<string, string>
   logoUrl?: string
   workshop?: WorkshopPreviewInfo
   invoiceLayoutConfig?: InvoiceLayoutConfig
@@ -785,7 +792,7 @@ export function TemplateSettings({
       ) : tab === 'sms' ? (
         <>
           <ReadOnlyWrapper>
-            <SmsTemplateTab values={smsValues} setValues={setSmsValues} />
+            <SmsTemplateTab values={smsValues} setValues={setSmsValues} defaults={smsDefaults} />
           </ReadOnlyWrapper>
           <SaveButton>
             <div className="flex justify-end">

+ 5 - 3
src/features/messaging/Actions/sendDocumentLink.ts

@@ -13,7 +13,8 @@ import { requireFeature } from '@/lib/features'
 import { PermissionAction, PermissionSubject } from '@/lib/permissions'
 import { ensureShareToken } from '@/lib/share-token.server'
 import { normalizeOrgPhone } from '@/lib/sms'
-import { interpolateSmsTemplate, SMS_TEMPLATE_DEFAULTS } from '@/lib/sms-templates'
+import { interpolateSmsTemplate } from '@/lib/sms-templates'
+import { smsTemplateDefaults } from '@/features/sms/Lib/smsTemplateDefaults.server'
 import {
   getWhatsappConfig,
   isWithinServiceWindow,
@@ -114,12 +115,13 @@ async function linkMessage(
   customerName: string
 ) {
   const key = TEMPLATE_KEY[kind]
-  const [setting, org, user] = await Promise.all([
+  const [setting, org, user, defaults] = await Promise.all([
     db.appSetting.findFirst({ where: { organizationId, key }, select: { value: true } }),
     db.organization.findUnique({ where: { id: organizationId }, select: { name: true } }),
     db.user.findUnique({ where: { id: userId }, select: { name: true } }),
+    smsTemplateDefaults(organizationId),
   ])
-  const body = interpolateSmsTemplate(setting?.value || SMS_TEMPLATE_DEFAULTS[key] || '', {
+  const body = interpolateSmsTemplate(setting?.value || defaults[key] || '', {
     share_link: link,
     customer_name: customerName,
     company_name: org?.name || '',

+ 4 - 3
src/features/sms/Actions/smsActions.ts

@@ -6,7 +6,7 @@ import { getPlatformSmsAllowance, normalizeOrgPhone } from '@/lib/sms'
 import { requireFeature } from '@/lib/features'
 import { PermissionAction, PermissionSubject } from '@/lib/permissions'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
-import { SMS_TEMPLATE_DEFAULTS } from '@/lib/sms-templates'
+import { smsTemplateDefaults } from '../Lib/smsTemplateDefaults.server'
 import { demoGuard } from '@/lib/demo'
 import { deliverSms } from '../Lib/deliverSms.server'
 
@@ -220,7 +220,7 @@ export async function getSmsTemplates() {
         SETTING_KEYS.SMS_TEMPLATE_STATUS_COMPLETED,
         SETTING_KEYS.SMS_TEMPLATE_PAYMENT_RECEIVED,
       ]
-      const [settings, org, user] = await Promise.all([
+      const [settings, org, user, defaults] = await Promise.all([
         db.appSetting.findMany({
           where: { organizationId, key: { in: smsKeys } },
         }),
@@ -232,12 +232,13 @@ export async function getSmsTemplates() {
           where: { id: userId },
           select: { name: true },
         }),
+        smsTemplateDefaults(organizationId),
       ])
 
       const templates: Record<string, string> = {}
       for (const key of smsKeys) {
         const found = settings.find((s) => s.key === key)
-        templates[key] = found?.value || SMS_TEMPLATE_DEFAULTS[key] || ''
+        templates[key] = found?.value || defaults[key] || ''
       }
       return {
         templates,

+ 40 - 0
src/features/sms/Lib/smsTemplateDefaults.server.ts

@@ -0,0 +1,40 @@
+import 'server-only'
+import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
+import { resolveDocumentLocale } from '@/i18n/locale-from-request'
+import { SMS_TEMPLATE_DEFAULTS } from '@/lib/sms-templates'
+
+/** Where each SMS template's default wording lives in settings.json. */
+const DEFAULT_KEYS: Record<string, string> = {
+  [SETTING_KEYS.SMS_TEMPLATE_INVOICE_READY]: 'invoiceReady',
+  [SETTING_KEYS.SMS_TEMPLATE_QUOTE_READY]: 'quoteReady',
+  [SETTING_KEYS.SMS_TEMPLATE_INSPECTION_READY]: 'inspectionReady',
+  [SETTING_KEYS.SMS_TEMPLATE_STATUS_IN_PROGRESS]: 'statusInProgress',
+  [SETTING_KEYS.SMS_TEMPLATE_STATUS_WAITING_PARTS]: 'statusWaitingParts',
+  [SETTING_KEYS.SMS_TEMPLATE_STATUS_READY]: 'statusReady',
+  [SETTING_KEYS.SMS_TEMPLATE_STATUS_COMPLETED]: 'statusCompleted',
+  [SETTING_KEYS.SMS_TEMPLATE_PAYMENT_RECEIVED]: 'paymentReceived',
+}
+
+/**
+ * The default SMS wording for a workshop that has not written its own, in the
+ * workshop's language: the language its customers get their invoices in.
+ *
+ * Settings → Templates always showed these translated, but the text that was
+ * sent fell back to the English constants, so a Norwegian workshop that never
+ * touched its templates texted its customers in English. Read raw from the
+ * messages file because the wording holds `{share_link}` and friends, which
+ * the app fills in itself rather than through next-intl.
+ */
+export async function smsTemplateDefaults(organizationId: string): Promise<Record<string, string>> {
+  const locale = await resolveDocumentLocale(organizationId)
+  const messages = (await import(`../../../../messages/${locale}/settings.json`)).default as {
+    templates?: { smsDefaults?: Record<string, string> }
+  }
+  const translated = messages.templates?.smsDefaults ?? {}
+
+  const defaults: Record<string, string> = {}
+  for (const [key, defaultKey] of Object.entries(DEFAULT_KEYS)) {
+    defaults[key] = translated[defaultKey] || SMS_TEMPLATE_DEFAULTS[key] || ''
+  }
+  return defaults
+}

+ 2 - 1
src/lib/sms-templates.ts

@@ -1,7 +1,8 @@
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
 
 export const SMS_TEMPLATE_DEFAULTS: Record<string, string> = {
-  [SETTING_KEYS.SMS_TEMPLATE_INVOICE_READY]: 'Your invoice is ready. View it here: {share_link}',
+  [SETTING_KEYS.SMS_TEMPLATE_INVOICE_READY]:
+    'Invoice from {company_name} is ready. View it here: {share_link}\nRegards {company_name} - {current_user}',
   [SETTING_KEYS.SMS_TEMPLATE_QUOTE_READY]:
     'Your quote is ready for review. View it here: {share_link}',
   [SETTING_KEYS.SMS_TEMPLATE_INSPECTION_READY]: