fix(notifications,mcp): carry the record's currency in amount strings (#1179)
Fixes #1171. Three currency-blind format sites: - Invoice due/overdue push notifications rendered every total as kr; the scheduler query did not even select currency. Builders now take the invoice currency ("kr" only for SEK, ISO code otherwise). - The receipt-matcher MCP widget hardcoded ' kr' although the tool handler passes each transaction's currency through. - The duplicate-booking warning in gnubok_categorize_transaction interpolated "N kr" for a transaction whose currency was already selected; the string reaches the agent, so a mislabeled currency can mislead the model, not just the user. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
bb78f8fce8
commit
9b3e344796
@@ -3482,9 +3482,10 @@ export const tools: McpTool[] = [
|
||||
})
|
||||
if (dup) {
|
||||
const amountAbs = roundOre(Math.abs(Number(tx.amount)))
|
||||
const amountUnit = !tx.currency || tx.currency === 'SEK' ? 'kr' : tx.currency
|
||||
const voucher = dup.voucher_label ? `verifikat ${dup.voucher_label}` : 'en befintlig verifikation'
|
||||
throw new Error(
|
||||
`Möjlig dubblettbokföring: ${voucher} (${dup.entry_date}) bokför redan ${amountAbs} kr på bankkontot. ` +
|
||||
`Möjlig dubblettbokföring: ${voucher} (${dup.entry_date}) bokför redan ${amountAbs} ${amountUnit} på bankkontot. ` +
|
||||
`Den här affärshändelsen ser redan ut att vara bokförd: länka transaktionen till den befintliga ` +
|
||||
`verifikationen i stället. Anropa igen med allow_duplicate=true först om det är en genuint separat affärshändelse.`,
|
||||
)
|
||||
|
||||
@@ -199,7 +199,9 @@ export const RECEIPT_MATCHER_HTML = `<!DOCTYPE html>
|
||||
const cls = isBooked ? 'booked' : (hasError ? 'error-row' : '');
|
||||
const amt = Number(tx.amount);
|
||||
const amtClass = amt < 0 ? 'negative' : 'positive';
|
||||
const formatted = amt.toLocaleString('sv-SE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' kr';
|
||||
// The tool handler passes each transaction's own currency through.
|
||||
const suffix = !tx.currency || tx.currency === 'SEK' ? 'kr' : tx.currency;
|
||||
const formatted = amt.toLocaleString('sv-SE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' ' + esc(suffix);
|
||||
|
||||
html += '<tr class="' + cls + '" data-idx="' + i + '">';
|
||||
html += '<td>' + esc(tx.date || '') + '</td>';
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { createInvoiceOverduePayload, createInvoiceDuePayload } from '../payload-builders'
|
||||
|
||||
describe('invoice notification payloads', () => {
|
||||
// sv-SE grouping uses a non-breaking space: build expectations via the same
|
||||
// formatter instead of typing literals with a plain space.
|
||||
it('labels SEK amounts with kr', () => {
|
||||
const payload = createInvoiceDuePayload('1042', 'Acme AB', 12500, 'SEK', '2026-08-01', 'inv-1')
|
||||
expect(payload.body).toContain(`${(12500).toLocaleString('sv-SE')} kr`)
|
||||
})
|
||||
|
||||
it('labels non-SEK amounts with their ISO code instead of kr', () => {
|
||||
const payload = createInvoiceOverduePayload('1043', 'Odin Aero GmbH', 9800, 'EUR', '2026-07-01', 'inv-2')
|
||||
expect(payload.body).toContain(`${(9800).toLocaleString('sv-SE')} EUR`)
|
||||
expect(payload.body).not.toContain('kr (')
|
||||
})
|
||||
|
||||
it('falls back to kr when currency is missing on legacy rows', () => {
|
||||
const payload = createInvoiceDuePayload('1044', 'Acme AB', 100, '', '2026-08-01', 'inv-3')
|
||||
expect(payload.body).toContain('100 kr')
|
||||
})
|
||||
})
|
||||
@@ -135,7 +135,7 @@ export async function sendInvoiceNotifications(
|
||||
|
||||
const { data: invoices } = await supabase
|
||||
.from('invoices')
|
||||
.select('id, user_id, invoice_number, total, due_date, customer:customers(name)')
|
||||
.select('id, user_id, invoice_number, total, currency, due_date, customer:customers(name)')
|
||||
.in('status', ['sent', 'overdue'])
|
||||
.in('due_date', [in3DaysStr, todayStr, daysAgo3Str, daysAgo7Str])
|
||||
|
||||
@@ -180,6 +180,7 @@ export async function sendInvoiceNotifications(
|
||||
invoice.invoice_number,
|
||||
customerName,
|
||||
invoice.total,
|
||||
invoice.currency,
|
||||
invoice.due_date,
|
||||
invoice.id
|
||||
)
|
||||
@@ -187,6 +188,7 @@ export async function sendInvoiceNotifications(
|
||||
invoice.invoice_number,
|
||||
customerName,
|
||||
invoice.total,
|
||||
invoice.currency,
|
||||
invoice.due_date,
|
||||
invoice.id
|
||||
)
|
||||
|
||||
@@ -163,16 +163,22 @@ export function createTaxDeadlinePayload(
|
||||
}
|
||||
}
|
||||
|
||||
// Invoices carry their own currency; "kr" is only correct for SEK.
|
||||
function formatInvoiceAmount(amount: number, currency: string): string {
|
||||
return `${amount.toLocaleString('sv-SE')} ${!currency || currency === 'SEK' ? 'kr' : currency}`
|
||||
}
|
||||
|
||||
export function createInvoiceOverduePayload(
|
||||
invoiceNumber: string,
|
||||
customerName: string,
|
||||
amount: number,
|
||||
currency: string,
|
||||
dueDate: string,
|
||||
invoiceId: string
|
||||
): NotificationPayload {
|
||||
return {
|
||||
title: `Obetald faktura #${invoiceNumber}`,
|
||||
body: `${customerName} - ${amount.toLocaleString('sv-SE')} kr (förföll ${formatDate(dueDate)})`,
|
||||
body: `${customerName} - ${formatInvoiceAmount(amount, currency)} (förföll ${formatDate(dueDate)})`,
|
||||
icon: '/icons/icon-192.png',
|
||||
badge: '/icons/badge-72.png',
|
||||
tag: `invoice-${invoiceId}`,
|
||||
@@ -189,12 +195,13 @@ export function createInvoiceDuePayload(
|
||||
invoiceNumber: string,
|
||||
customerName: string,
|
||||
amount: number,
|
||||
currency: string,
|
||||
dueDate: string,
|
||||
invoiceId: string
|
||||
): NotificationPayload {
|
||||
return {
|
||||
title: `Faktura #${invoiceNumber} förfaller`,
|
||||
body: `${customerName} - ${amount.toLocaleString('sv-SE')} kr (${formatDate(dueDate)})`,
|
||||
body: `${customerName} - ${formatInvoiceAmount(amount, currency)} (${formatDate(dueDate)})`,
|
||||
icon: '/icons/icon-192.png',
|
||||
badge: '/icons/badge-72.png',
|
||||
tag: `invoice-${invoiceId}`,
|
||||
|
||||
Reference in New Issue
Block a user