diff --git a/docker/extensions.hosted.json b/docker/extensions.hosted.json
index 26879b79..1337411a 100644
--- a/docker/extensions.hosted.json
+++ b/docker/extensions.hosted.json
@@ -1 +1 @@
-{"extensions": ["enable-banking", "email", "arcim-migration", "tic", "mcp-server"]}
+{"extensions": ["enable-banking", "email", "arcim-migration", "tic", "mcp-server", "cloud-backup", "skatteverket", "invoice-inbox", "document-extraction"]}
diff --git a/extensions/general/email/lib/invoice-templates.ts b/extensions/general/email/lib/invoice-templates.ts
deleted file mode 100644
index 45cc0280..00000000
--- a/extensions/general/email/lib/invoice-templates.ts
+++ /dev/null
@@ -1,220 +0,0 @@
-import type { Invoice, Customer, CompanySettings, InvoiceDocumentType } from '@/types'
-import { formatCurrency, formatDate, getCompanyDisplayName, getCompanyPrimaryName } from '@/lib/utils'
-
-function getDocumentLabel(invoice: Invoice): string {
- if (invoice.credited_invoice_id) return 'Kreditfaktura'
- const docType = (invoice as Invoice & { document_type?: InvoiceDocumentType }).document_type || 'invoice'
- if (docType === 'proforma') return 'Proformafaktura'
- if (docType === 'delivery_note') return 'Följesedel'
- return 'Faktura'
-}
-
-export interface InvoiceEmailData {
- invoice: Invoice
- customer: Customer
- company: CompanySettings
-}
-
-/**
- * Generate HTML email for sending an invoice
- */
-export function generateInvoiceEmailHtml(data: InvoiceEmailData): string {
- const { invoice, customer, company } = data
-
- const documentType = getDocumentLabel(invoice)
- const isCreditNote = !!invoice.credited_invoice_id
- const docType = (invoice as Invoice & { document_type?: InvoiceDocumentType }).document_type || 'invoice'
- const isDeliveryNote = docType === 'delivery_note'
- const isProforma = docType === 'proforma'
- const hidePayment = isCreditNote || isDeliveryNote || isProforma
-
- return `
-
-
-
-
-
- ${documentType} ${invoice.invoice_number}
-
-
-
-
-
-
- ${documentType} från ${getCompanyPrimaryName(company)}
-
-
- ${documentType}nummer: ${invoice.invoice_number}
-
-
-
-
-
-
- Hej${customer.name ? ` ${customer.name.split(' ')[0]}` : ''},
-
-
- ${isCreditNote
- ? `Bifogat hittar du en kreditfaktura som korrigerar en tidigare faktura.`
- : `Tack för ditt förtroende! Bifogat hittar du din faktura.`
- }
-
-
-
-
-
-
-
- | ${documentType}nummer: |
- ${invoice.invoice_number} |
-
-
- | ${documentType}datum: |
- ${formatDate(invoice.invoice_date)} |
-
-
- | Förfallodatum: |
-
- ${formatDate(invoice.due_date)}
- |
-
-
- |
-
-
- | Att betala: |
-
- ${formatCurrency(invoice.total, invoice.currency)}
- |
-
-
-
-
-
- ${!hidePayment ? `
-
-
- Betalningsinformation
-
-
- ${company.bank_name ? `
-
- | Bank: |
- ${company.bank_name} |
-
- ` : ''}
- ${company.clearing_number && company.account_number ? `
-
- | Kontonummer: |
- ${company.clearing_number}-${company.account_number} |
-
- ` : ''}
- ${company.iban ? `
-
- | IBAN: |
- ${company.iban} |
-
- ` : ''}
- ${company.bic ? `
-
- | BIC/SWIFT: |
- ${company.bic} |
-
- ` : ''}
-
- | Meddelande: |
- ${invoice.invoice_number} |
-
-
-
- ` : ''}
-
-
-
-
- Har du frågor om fakturan? Svara direkt på detta mejl så hjälper vi dig.
-
-
- Med vänliga hälsningar,
- ${getCompanyPrimaryName(company)}
-
- ${company.org_number ? `
-
- Org.nr: ${company.org_number}
- ${company.vat_number ? ` | VAT: ${company.vat_number}` : ''}
- ${company.f_skatt ? ' | Innehar F-skattsedel' : ''}
-
- ` : ''}
-
-
-
-
-`
-}
-
-/**
- * Generate plain text email for sending an invoice
- */
-export function generateInvoiceEmailText(data: InvoiceEmailData): string {
- const { invoice, customer, company } = data
-
- const documentType = getDocumentLabel(invoice)
- const isCreditNote = !!invoice.credited_invoice_id
- const docType = (invoice as Invoice & { document_type?: InvoiceDocumentType }).document_type || 'invoice'
- const isDeliveryNote = docType === 'delivery_note'
- const isProforma = docType === 'proforma'
- const hidePayment = isCreditNote || isDeliveryNote || isProforma
-
- let text = `${documentType} från ${getCompanyPrimaryName(company)}\n`
- text += `${documentType}nummer: ${invoice.invoice_number}\n\n`
-
- text += `Hej${customer.name ? ` ${customer.name.split(' ')[0]}` : ''},\n\n`
-
- if (isCreditNote) {
- text += `Bifogat hittar du en kreditfaktura som korrigerar en tidigare faktura.\n\n`
- } else {
- text += `Tack för ditt förtroende! Bifogat hittar du din faktura.\n\n`
- }
-
- text += `${documentType}sammanfattning:\n`
- text += `---\n`
- text += `${documentType}nummer: ${invoice.invoice_number}\n`
- text += `${documentType}datum: ${formatDate(invoice.invoice_date)}\n`
- text += `Förfallodatum: ${formatDate(invoice.due_date)}\n`
- text += `Att betala: ${formatCurrency(invoice.total, invoice.currency)}\n`
- text += `---\n\n`
-
- if (!hidePayment) {
- text += `Betalningsinformation:\n`
- if (company.bank_name) text += `Bank: ${company.bank_name}\n`
- if (company.clearing_number && company.account_number) {
- text += `Kontonummer: ${company.clearing_number}-${company.account_number}\n`
- }
- if (company.iban) text += `IBAN: ${company.iban}\n`
- if (company.bic) text += `BIC/SWIFT: ${company.bic}\n`
- text += `Meddelande: ${invoice.invoice_number}\n\n`
- }
-
- text += `Har du frågor om fakturan? Svara direkt på detta mejl så hjälper vi dig.\n\n`
- text += `Med vänliga hälsningar,\n`
- text += `${getCompanyDisplayName(company)}\n`
-
- if (company.org_number) {
- text += `\nOrg.nr: ${company.org_number}`
- if (company.vat_number) text += ` | VAT: ${company.vat_number}`
- if (company.f_skatt) text += ` | Innehar F-skattsedel`
- text += `\n`
- }
-
- return text
-}
-
-/**
- * Generate email subject for an invoice
- */
-export function generateInvoiceEmailSubject(data: InvoiceEmailData): string {
- const { invoice, company } = data
- const documentType = getDocumentLabel(invoice)
-
- return `${documentType} ${invoice.invoice_number} från ${getCompanyPrimaryName(company)}`
-}
diff --git a/extensions/general/email/lib/reminder-templates.ts b/extensions/general/email/lib/reminder-templates.ts
deleted file mode 100644
index 392a386b..00000000
--- a/extensions/general/email/lib/reminder-templates.ts
+++ /dev/null
@@ -1,280 +0,0 @@
-import type { Invoice, Customer, CompanySettings } from '@/types'
-import { formatCurrency, formatDate, getCompanyDisplayName, getCompanyPrimaryName } from '@/lib/utils'
-
-export interface ReminderEmailData {
- invoice: Invoice
- customer: Customer
- company: CompanySettings
- reminderLevel: 1 | 2 | 3
- daysOverdue: number
- actionUrl: string // URL for customer to mark as paid or dispute
-}
-
-// Reminder level configurations
-const REMINDER_CONFIG = {
- 1: {
- title: 'Vänlig påminnelse',
- tone: 'friendly',
- daysAfterDue: 15
- },
- 2: {
- title: 'Andra påminnelsen',
- tone: 'firm',
- daysAfterDue: 30
- },
- 3: {
- title: 'Slutlig påminnelse',
- tone: 'urgent',
- daysAfterDue: 45
- }
-} as const
-
-/**
- * Generate HTML email for payment reminder
- */
-export function generateReminderEmailHtml(data: ReminderEmailData): string {
- const { invoice, customer, company, reminderLevel, daysOverdue, actionUrl } = data
- const config = REMINDER_CONFIG[reminderLevel]
-
- // Different styling based on urgency
- const headerColor = reminderLevel === 3 ? '#dc2626' : reminderLevel === 2 ? '#ea580c' : '#2563eb'
- const buttonColor = reminderLevel === 3 ? '#dc2626' : reminderLevel === 2 ? '#ea580c' : '#2563eb'
-
- return `
-
-
-
-
-
- ${config.title} - Faktura ${invoice.invoice_number}
-
-
-
-
-
-
-
-
-
- Faktura ${invoice.invoice_number} förföll för ${daysOverdue} dagar sedan
-
-
-
-
-
- Hej${customer.name ? ` ${customer.name.split(' ')[0]}` : ''},
-
-
- ${reminderLevel === 1 ? `
-
- Vi vill påminna dig om att faktura ${invoice.invoice_number} förföll till betalning den ${formatDate(invoice.due_date)}.
- Om du redan har betalat kan du bortse från denna påminnelse.
-
- ` : reminderLevel === 2 ? `
-
- Trots vår tidigare påminnelse har vi ännu inte mottagit betalning för faktura ${invoice.invoice_number}
- som förföll den ${formatDate(invoice.due_date)}.
-
-
- Vi ber dig vänligen att omgående reglera detta belopp för att undvika ytterligare åtgärder.
-
- ` : `
-
- Detta är vår slutliga påminnelse gällande faktura ${invoice.invoice_number}.
-
-
- Fakturan förföll till betalning den ${formatDate(invoice.due_date)} och vi har ännu inte mottagit betalning
- trots tidigare påminnelser.
-
-
- Om betalning inte inkommer inom 7 dagar kommer ärendet att överlämnas för vidare hantering.
-
- `}
-
-
-
-
-
-
- | Fakturanummer: |
- ${invoice.invoice_number} |
-
-
- | Fakturadatum: |
- ${formatDate(invoice.invoice_date)} |
-
-
- | Förfallodatum: |
-
- ${formatDate(invoice.due_date)}
- |
-
-
- |
-
-
- | Belopp att betala: |
-
- ${formatCurrency(invoice.total, invoice.currency)}
- |
-
-
-
-
-
-
-
- Betalningsinformation
-
-
- ${company.bank_name ? `
-
- | Bank: |
- ${company.bank_name} |
-
- ` : ''}
- ${company.clearing_number && company.account_number ? `
-
- | Kontonummer: |
- ${company.clearing_number}-${company.account_number} |
-
- ` : ''}
- ${company.iban ? `
-
- | IBAN: |
- ${company.iban} |
-
- ` : ''}
- ${company.bic ? `
-
- | BIC/SWIFT: |
- ${company.bic} |
-
- ` : ''}
-
- | Meddelande: |
- ${invoice.invoice_number} |
-
-
-
-
-
-
-
-
-
-
- Har du frågor? Svara direkt på detta mejl så hjälper vi dig.
-
-
- Med vänliga hälsningar,
- ${getCompanyPrimaryName(company)}
-
- ${company.org_number ? `
-
- Org.nr: ${company.org_number}
- ${company.vat_number ? ` | VAT: ${company.vat_number}` : ''}
-
- ` : ''}
-
-
-
-
-
- Detta är ett automatiskt meddelande angående en obetald faktura.
-
-
-
-
-`
-}
-
-/**
- * Generate plain text email for payment reminder
- */
-export function generateReminderEmailText(data: ReminderEmailData): string {
- const { invoice, customer, company, reminderLevel, daysOverdue, actionUrl } = data
- const config = REMINDER_CONFIG[reminderLevel]
-
- let text = `${config.title.toUpperCase()}\n`
- text += `Faktura ${invoice.invoice_number} förföll för ${daysOverdue} dagar sedan\n`
- text += `=`.repeat(50) + `\n\n`
-
- text += `Hej${customer.name ? ` ${customer.name.split(' ')[0]}` : ''},\n\n`
-
- if (reminderLevel === 1) {
- text += `Vi vill påminna dig om att faktura ${invoice.invoice_number} förföll till betalning den ${formatDate(invoice.due_date)}.\n`
- text += `Om du redan har betalat kan du bortse från denna påminnelse.\n\n`
- } else if (reminderLevel === 2) {
- text += `Trots vår tidigare påminnelse har vi ännu inte mottagit betalning för faktura ${invoice.invoice_number} som förföll den ${formatDate(invoice.due_date)}.\n\n`
- text += `Vi ber dig vänligen att omgående reglera detta belopp för att undvika ytterligare åtgärder.\n\n`
- } else {
- text += `DETTA ÄR VÅR SLUTLIGA PÅMINNELSE\n\n`
- text += `Fakturan förföll till betalning den ${formatDate(invoice.due_date)} och vi har ännu inte mottagit betalning trots tidigare påminnelser.\n\n`
- text += `Om betalning inte inkommer inom 7 dagar kommer ärendet att överlämnas för vidare hantering.\n\n`
- }
-
- text += `Fakturasammanfattning:\n`
- text += `-`.repeat(30) + `\n`
- text += `Fakturanummer: ${invoice.invoice_number}\n`
- text += `Fakturadatum: ${formatDate(invoice.invoice_date)}\n`
- text += `Förfallodatum: ${formatDate(invoice.due_date)}\n`
- text += `Belopp att betala: ${formatCurrency(invoice.total, invoice.currency)}\n`
- text += `-`.repeat(30) + `\n\n`
-
- text += `Betalningsinformation:\n`
- if (company.bank_name) text += `Bank: ${company.bank_name}\n`
- if (company.clearing_number && company.account_number) {
- text += `Kontonummer: ${company.clearing_number}-${company.account_number}\n`
- }
- if (company.iban) text += `IBAN: ${company.iban}\n`
- if (company.bic) text += `BIC/SWIFT: ${company.bic}\n`
- text += `Meddelande: ${invoice.invoice_number}\n\n`
-
- text += `Har du redan betalat eller har frågor om fakturan?\n`
- text += `Hantera faktura: ${actionUrl}\n\n`
-
- text += `Har du frågor? Svara direkt på detta mejl så hjälper vi dig.\n\n`
- text += `Med vänliga hälsningar,\n`
- text += `${getCompanyDisplayName(company)}\n`
-
- if (company.org_number) {
- text += `\nOrg.nr: ${company.org_number}`
- if (company.vat_number) text += ` | VAT: ${company.vat_number}`
- text += `\n`
- }
-
- return text
-}
-
-/**
- * Generate email subject for payment reminder
- */
-export function generateReminderEmailSubject(data: ReminderEmailData): string {
- const { invoice, reminderLevel } = data
- const config = REMINDER_CONFIG[reminderLevel]
-
- return `${config.title}: Faktura ${invoice.invoice_number} - ${formatCurrency(invoice.total, invoice.currency)}`
-}
-
-/**
- * Get the number of days after due date for each reminder level
- */
-export function getReminderDaysConfig(): Record<1 | 2 | 3, number> {
- return {
- 1: REMINDER_CONFIG[1].daysAfterDue,
- 2: REMINDER_CONFIG[2].daysAfterDue,
- 3: REMINDER_CONFIG[3].daysAfterDue
- }
-}
diff --git a/lib/errors/__tests__/get-error-message.test.ts b/lib/errors/__tests__/get-error-message.test.ts
index 22015f4b..f7b5d845 100644
--- a/lib/errors/__tests__/get-error-message.test.ts
+++ b/lib/errors/__tests__/get-error-message.test.ts
@@ -95,6 +95,25 @@ describe('getErrorMessage — typed bookkeeping error codes', () => {
})
})
+describe('getErrorMessage — English locale uses registry English (C9)', () => {
+ it('returns the registry English message for a known structured code instead of Swedish', () => {
+ const code = 'FISCAL_PERIOD_NOT_FOUND'
+ const sv = getErrorMessage({ error: { code, message: '...' } })
+ const en = getErrorMessage({ error: { code, message: '...' } }, { locale: 'en' })
+
+ expect(sv).toMatch(/[åäö]/i) // default (Swedish) path is unchanged
+ expect(en).not.toBe(sv) // English locale now differs
+ expect(en).not.toMatch(/[åäö]/i) // …and is no longer Swedish prose
+ expect(en.toLowerCase()).toContain('fiscal period')
+ })
+
+ it('leaves the Swedish (default-locale) message identical to before', () => {
+ expect(getErrorMessage({ error: { code: 'CANNOT_REVERSE_NON_POSTED', message: '...' } })).toBe(
+ 'Endast bokförda verifikationer kan stornas.',
+ )
+ })
+})
+
describe('getErrorMessage — existing patterns still work', () => {
it('regex match for "Entry date ... outside fiscal period" on plain string', () => {
const msg = getErrorMessage('Entry date 2024-06-15 is outside fiscal period "FY 2025"')
diff --git a/lib/errors/get-error-message.ts b/lib/errors/get-error-message.ts
index b953d1b1..71c21c14 100644
--- a/lib/errors/get-error-message.ts
+++ b/lib/errors/get-error-message.ts
@@ -18,6 +18,7 @@
*/
import { formatCurrency } from '@/lib/utils'
+import { getErrorEntry } from './structured-errors'
type ErrorContext =
| 'invoice'
@@ -279,6 +280,17 @@ export function getErrorMessage(
details?: unknown
}
+ // For English UI, return the registry's English message for any known
+ // code instead of falling through to the Swedish branches below (which
+ // ignored locale — English users were shown Swedish prose). The Swedish
+ // path is left entirely unchanged; codes absent from the registry still
+ // fall through. The dynamic branches (amounts / lock date / reason) keep
+ // owning Swedish display.
+ if (locale === 'en' && typeof structured.code === 'string') {
+ const entry = getErrorEntry(structured.code)
+ if (entry?.message_en) return entry.message_en
+ }
+
if (structured.code === 'ACCOUNTS_NOT_IN_CHART' && Array.isArray(structured.account_numbers)) {
const numbers = structured.account_numbers as string[]
return `Följande konton behöver aktiveras: ${numbers.join(', ')}`