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 ? ` - - - - - ` : ''} - ${company.clearing_number && company.account_number ? ` - - - - - ` : ''} - ${company.iban ? ` - - - - - ` : ''} - ${company.bic ? ` - - - - - ` : ''} - - - - -
Bank:${company.bank_name}
Kontonummer:${company.clearing_number}-${company.account_number}
IBAN:${company.iban}
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} - - -
-
- -
-
- ${config.title} -
-
- - -

- 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 ? ` - - - - - ` : ''} - ${company.clearing_number && company.account_number ? ` - - - - - ` : ''} - ${company.iban ? ` - - - - - ` : ''} - ${company.bic ? ` - - - - - ` : ''} - - - - -
Bank:${company.bank_name}
Kontonummer:${company.clearing_number}-${company.account_number}
IBAN:${company.iban}
BIC/SWIFT:${company.bic}
Meddelande:${invoice.invoice_number}
-
- - -
-

- Har du redan betalat eller har frågor om fakturan? -

- - Hantera faktura - -
- - -
-

- 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(', ')}`