diff --git a/DECISIONS.md b/DECISIONS.md index fbe7f8ab..87ff11ee 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -799,3 +799,5 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-08-05] Dropped "ML 13 kap 8 §" cites for kontantmetoden VAT timing (comments/docs only): section is the old ML 1994:200 numbering; in ML 2023:200, 13 kap is input-VAT deduction. Rule stated without section cite until the current-law section is verified. [2026-08-05] The in-app assistant now reads invoice_inbox_items.channel_context (the answers a user gave in WhatsApp) as first-class underlag context, marked "uppgivna av användaren" and ranked above OCR output. Found in the field: the assistant asked for representation participants the user had typed into WhatsApp minutes earlier, because the intent's inbox query selected only document_id + extracted_data. Also backfilled by document_id, since a receipt can reach the intent through the document paths without the inbox row being matched to the transaction. [2026-08-05] The WhatsApp representation question now asks ONCE for a missing purpose instead of silently storing participants with purpose=null. Skatteverket wants participants AND purpose; accepting half and saying "Tack!" produced an undocumented deduction. Anti-loop: the follow-up fires only when no representation block exists yet, so a second incomplete answer is accepted as-is rather than nagging. +[2026-08-06] ROT/RUT payout strings were placed in the invoice_editor namespace while RotRutPayoutDialog and the invoices page read useTranslations('invoices'), so all 44 labels rendered as raw "invoices.rot_rut_*" key paths in production since #1380. Moved the keys to invoices rather than repointing the components, since the dialog belongs to the invoice list, not the editor. Message files are edited textually, never via JSON.parse/stringify: they contain duplicate keys a round trip would silently drop. Same bug class fixed in TemplateBookDialog (bookkeeping) and Correction/StrikeLines dialogs (journal_detail) by adding the strings to the namespace each component reads, matching the existing precedent that toast_posted_* is duplicated across journal_list and journal_detail. Added i18n/__tests__/message-keys.test.ts, which resolves every literal t() key against both locales: next-intl has no build-time check and fails by rendering the key path, so nothing caught this before users did. +[2026-08-06] The ROT/RUT payout button is hidden from the invoices header unless the company has an invoice with deduction_total > 0 or rot_rut_enabled is on in tax settings. ROT/RUT concerns only companies selling eligible work to consumers, and a payout can never precede the invoice that created the claim, so the derived signal cannot hide the action from someone who needs it. Read from the company_settings row the page already fetches for ore_rounding (no extra round trip); deliberately not scoped to the fiscal-year filter, since a begäran is claimed the year after payment. ?rot-rut=1 still opens the dialog, so the feature is hidden, not removed. diff --git a/app/(dashboard)/invoices/page.tsx b/app/(dashboard)/invoices/page.tsx index 7012ca49..1f60ccbd 100644 --- a/app/(dashboard)/invoices/page.tsx +++ b/app/(dashboard)/invoices/page.tsx @@ -154,6 +154,7 @@ export default function InvoicesPage() { const searchParams = useSearchParams() const [invoices, setInvoices] = useState([]) const [oreRounding, setOreRounding] = useState(true) + const [rotRutEnabled, setRotRutEnabled] = useState(false) const [isLoading, setIsLoading] = useState(true) const [searchTerm, setSearchTerm] = useState('') const [sort, setSort] = useState(null) @@ -192,6 +193,16 @@ export default function InvoicesPage() { const closeRotRutPayout = () => router.replace('/invoices', { scroll: false }) const openRotRutPayout = () => router.push('/invoices?rot-rut=1', { scroll: false }) + // Begäran om utbetalning (Lag 2009:194 8 §) only concerns companies selling + // ROT/RUT-eligible work to consumers, so the action stays out of the header + // for everyone else. It appears once the company has invoiced a deduction + // (the payout can never precede that invoice), or once ROT/RUT is opted into + // in tax settings. Not scoped to the fiscal-year filter: a payout is claimed + // the year after payment, so last year's invoices are exactly the relevant + // ones. ?rot-rut=1 keeps working regardless, so nothing is unreachable. + const showRotRutAction = + rotRutEnabled || invoices.some((invoice) => (invoice.deduction_total ?? 0) > 0) + async function fetchInvoices() { if (!company) return setIsLoading(true) @@ -209,7 +220,7 @@ export default function InvoicesPage() { ), supabase .from('company_settings') - .select('ore_rounding') + .select('ore_rounding, rot_rut_enabled') .eq('company_id', company.id) .maybeSingle(), ]) @@ -228,6 +239,11 @@ export default function InvoicesPage() { ? (settingsResult.value.data?.ore_rounding ?? true) : true, ) + setRotRutEnabled( + settingsResult.status === 'fulfilled' + ? (settingsResult.value.data?.rot_rut_enabled ?? false) + : false, + ) setIsLoading(false) } @@ -382,16 +398,18 @@ export default function InvoicesPage() {

{t('title')}

- + {showRotRutAction && ( + + )} ( + (node, part) => + node && typeof node === 'object' ? (node as Record)[part] : undefined, + messages, + ) +} + +interface Reference { + file: string + namespace: string + key: string +} + +function collectReferences(): Reference[] { + const refs: Reference[] = [] + for (const dir of SCAN_DIRS) { + for (const file of collectSourceFiles(path.join(ROOT, dir))) { + const src = stripComments(fs.readFileSync(file, 'utf8')) + + // `const t = useTranslations('invoices')` / `= await getTranslations('x')` + const namespaces: Record = {} + const declaration = + /(?:const|let)\s+(\w+)\s*=\s*(?:await\s+)?(?:useTranslations|getTranslations)\(\s*['"]([^'"]+)['"]\s*\)/g + for (const match of src.matchAll(declaration)) namespaces[match[1]] = match[2] + + for (const [variable, namespace] of Object.entries(namespaces)) { + // Literal calls only: t(dynamicKey) cannot be checked statically. + const call = new RegExp( + `\\b${variable}(?:\\.rich|\\.markup|\\.raw)?\\(\\s*['"]([A-Za-z0-9_.]+)['"]`, + 'g', + ) + for (const match of src.matchAll(call)) { + refs.push({ file: path.relative(ROOT, file), namespace, key: match[1] }) + } + } + } + } + return refs +} + +describe('message keys', () => { + const references = collectReferences() + + it('finds translation calls to check', () => { + expect(references.length).toBeGreaterThan(500) + }) + + for (const locale of ['sv', 'en'] as const) { + it(`resolves every referenced key in messages/${locale}.json`, () => { + const messages = JSON.parse( + fs.readFileSync(path.join(ROOT, 'messages', `${locale}.json`), 'utf8'), + ) + + const missing = references + .filter(({ namespace, key }) => resolveKey(messages, `${namespace}.${key}`) === undefined) + .map(({ file, namespace, key }) => `${file}: ${namespace}.${key}`) + + expect([...new Set(missing)]).toEqual([]) + }) + } +}) diff --git a/messages/en.json b/messages/en.json index 4268c6a2..bb8d2d9e 100644 --- a/messages/en.json +++ b/messages/en.json @@ -3203,51 +3203,7 @@ "to_pay_label": "Amount to pay", "total_incl_vat_label": "Total incl. VAT", "review_customer_missing_title": "Customer details could not be loaded", - "review_customer_missing_description": "Reload the page and try again. Contact support if the problem persists.", - "rot_rut_payout_action": "ROT/RUT file", - "rot_rut_payout_title": "Request a ROT/RUT payout", - "rot_rut_payout_description": "Select paid invoices and download an XML file for Skatteverket's e-service. The file is not submitted automatically: upload and sign it at Skatteverket.", - "rot_rut_type_aria": "Select deduction type", - "rot_rut_type_rot": "ROT", - "rot_rut_type_rut": "RUT", - "rot_rut_year_aria": "Select payment year", - "rot_rut_loading": "Loading ROT/RUT details", - "rot_rut_load_failed_title": "Could not load the ROT/RUT details", - "rot_rut_load_failed_description": "Reload the page and try again.", - "rot_rut_eligible_title": "Invoices to include", - "rot_rut_selected_count": "{selected} selected · {amount}", - "rot_rut_select_all": "Select all", - "rot_rut_clear_selection": "Clear selection", - "rot_rut_no_eligible_title": "No invoices are ready", - "rot_rut_no_eligible_description": "A paid ROT or RUT invoice appears here once its buyer details are complete.", - "rot_rut_paid_at": "Paid {date}", - "rot_rut_max_cases_help": "Skatteverket allows at most {count} cases in one file. Create multiple files if you need to request more invoices.", - "rot_rut_generate_file": "Create and download file", - "rot_rut_generating_file": "Creating file…", - "rot_rut_generated_title": "The ROT/RUT file was downloaded", - "rot_rut_generated_description": "Upload the file in Skatteverket's e-service and sign the request there.", - "rot_rut_generate_failed_title": "Could not create the ROT/RUT file", - "rot_rut_blocked_title": "Cannot be included ({count})", - "rot_rut_history_title": "Previous files", - "rot_rut_upload_help": "After uploading and signing at Skatteverket, mark the file as uploaded here.", - "rot_rut_history_empty": "No files have been created for this deduction type.", - "rot_rut_history_meta": "{date} · {count} cases · {amount}", - "rot_rut_status_generated": "Created", - "rot_rut_status_submitted": "Uploaded", - "rot_rut_status_paid": "Approved", - "rot_rut_status_partially_paid": "Partly approved", - "rot_rut_status_rejected": "Rejected", - "rot_rut_status_cancelled": "Cancelled", - "rot_rut_download_again": "Download again", - "rot_rut_cancel_request": "Cancel", - "rot_rut_mark_uploaded": "Mark as uploaded", - "rot_rut_uploaded_title": "The file is marked as uploaded", - "rot_rut_cancelled_title": "The request was cancelled", - "rot_rut_update_failed_title": "Could not update the request", - "rot_rut_download_failed_title": "Could not download the file", - "rot_rut_download_timeout": "The download took too long. Try again.", - "rot_rut_download_network": "The file could not be downloaded. Check your connection and try again.", - "rot_rut_skatteverket_link": "Open ROT and RUT at Skatteverket" + "review_customer_missing_description": "Reload the page and try again. Contact support if the problem persists." }, "invoice_review": { "assigned_number_prefix": "Will be assigned invoice number", @@ -4345,6 +4301,9 @@ "current": "Current" }, "journal_detail": { + "accounts_loading": "Loading chart of accounts...", + "accounts_load_failed": "The chart of accounts could not be loaded. Try again to change the account.", + "accounts_retry": "Try again", "edit_draft": "Edit", "back": "Back to bookkeeping", "loading": "Loading journal entry...", @@ -5047,6 +5006,10 @@ "footer_to_handle": "{count, plural, =0 {Nothing to handle} =1 {1 to handle} other {# to handle}}" }, "bookkeeping": { + "toast_post_failed": "Could not post", + "toast_posted_title": "Journal entry posted", + "toast_posted_description": "Journal entry {voucher} has been posted.", + "toast_post_failed_generic": "Could not post journal entry", "edit_draft_dialog_title": "Edit draft", "title": "Bookkeeping", "year_end": "Year-end (Årsbokslut)", @@ -5457,6 +5420,50 @@ "validation_min_one_row": "At least one row is required" }, "invoices": { + "rot_rut_payout_action": "ROT/RUT file", + "rot_rut_payout_title": "Request a ROT/RUT payout", + "rot_rut_payout_description": "Select paid invoices and download an XML file for Skatteverket's e-service. The file is not submitted automatically: upload and sign it at Skatteverket.", + "rot_rut_type_aria": "Select deduction type", + "rot_rut_type_rot": "ROT", + "rot_rut_type_rut": "RUT", + "rot_rut_year_aria": "Select payment year", + "rot_rut_loading": "Loading ROT/RUT details", + "rot_rut_load_failed_title": "Could not load the ROT/RUT details", + "rot_rut_load_failed_description": "Reload the page and try again.", + "rot_rut_eligible_title": "Invoices to include", + "rot_rut_selected_count": "{selected} selected · {amount}", + "rot_rut_select_all": "Select all", + "rot_rut_clear_selection": "Clear selection", + "rot_rut_no_eligible_title": "No invoices are ready", + "rot_rut_no_eligible_description": "A paid ROT or RUT invoice appears here once its buyer details are complete.", + "rot_rut_paid_at": "Paid {date}", + "rot_rut_max_cases_help": "Skatteverket allows at most {count} cases in one file. Create multiple files if you need to request more invoices.", + "rot_rut_generate_file": "Create and download file", + "rot_rut_generating_file": "Creating file…", + "rot_rut_generated_title": "The ROT/RUT file was downloaded", + "rot_rut_generated_description": "Upload the file in Skatteverket's e-service and sign the request there.", + "rot_rut_generate_failed_title": "Could not create the ROT/RUT file", + "rot_rut_blocked_title": "Cannot be included ({count})", + "rot_rut_history_title": "Previous files", + "rot_rut_upload_help": "After uploading and signing at Skatteverket, mark the file as uploaded here.", + "rot_rut_history_empty": "No files have been created for this deduction type.", + "rot_rut_history_meta": "{date} · {count} cases · {amount}", + "rot_rut_status_generated": "Created", + "rot_rut_status_submitted": "Uploaded", + "rot_rut_status_paid": "Approved", + "rot_rut_status_partially_paid": "Partly approved", + "rot_rut_status_rejected": "Rejected", + "rot_rut_status_cancelled": "Cancelled", + "rot_rut_download_again": "Download again", + "rot_rut_cancel_request": "Cancel", + "rot_rut_mark_uploaded": "Mark as uploaded", + "rot_rut_uploaded_title": "The file is marked as uploaded", + "rot_rut_cancelled_title": "The request was cancelled", + "rot_rut_update_failed_title": "Could not update the request", + "rot_rut_download_failed_title": "Could not download the file", + "rot_rut_download_timeout": "The download took too long. Try again.", + "rot_rut_download_network": "The file could not be downloaded. Check your connection and try again.", + "rot_rut_skatteverket_link": "Open ROT and RUT at Skatteverket", "title": "Customer invoices", "recurring": "Recurring", "new_invoice": "New invoice", diff --git a/messages/sv.json b/messages/sv.json index 9edaa41e..642a6167 100644 --- a/messages/sv.json +++ b/messages/sv.json @@ -3203,51 +3203,7 @@ "to_pay_label": "Att betala", "total_incl_vat_label": "Totalt inkl. moms", "review_customer_missing_title": "Kunduppgifterna kunde inte laddas", - "review_customer_missing_description": "Ladda om sidan och försök igen. Kontakta support om det inte hjälper.", - "rot_rut_payout_action": "ROT/RUT-fil", - "rot_rut_payout_title": "Begär utbetalning för ROT/RUT", - "rot_rut_payout_description": "Välj betalda fakturor och hämta en XML-fil för Skatteverkets e-tjänst. Filen skickas inte automatiskt: du laddar upp och signerar den hos Skatteverket.", - "rot_rut_type_aria": "Välj avdragstyp", - "rot_rut_type_rot": "ROT", - "rot_rut_type_rut": "RUT", - "rot_rut_year_aria": "Välj betalningsår", - "rot_rut_loading": "Laddar ROT/RUT-underlag", - "rot_rut_load_failed_title": "Kunde inte ladda ROT/RUT-underlaget", - "rot_rut_load_failed_description": "Ladda om sidan och försök igen.", - "rot_rut_eligible_title": "Fakturor att ta med", - "rot_rut_selected_count": "{selected} valda · {amount}", - "rot_rut_select_all": "Välj alla", - "rot_rut_clear_selection": "Rensa val", - "rot_rut_no_eligible_title": "Inga fakturor är redo", - "rot_rut_no_eligible_description": "När en ROT- eller RUT-faktura är betald och har fullständiga köparuppgifter visas den här.", - "rot_rut_paid_at": "Betald {date}", - "rot_rut_max_cases_help": "Skatteverket tillåter högst {count} ärenden i samma fil. Skapa flera filer om fler fakturor ska begäras.", - "rot_rut_generate_file": "Skapa och hämta fil", - "rot_rut_generating_file": "Skapar fil…", - "rot_rut_generated_title": "ROT/RUT-filen är hämtad", - "rot_rut_generated_description": "Ladda upp filen i Skatteverkets e-tjänst och signera begäran där.", - "rot_rut_generate_failed_title": "Kunde inte skapa ROT/RUT-filen", - "rot_rut_blocked_title": "Kan inte tas med ({count})", - "rot_rut_history_title": "Tidigare filer", - "rot_rut_upload_help": "Efter uppladdning och signering hos Skatteverket markerar du filen som uppladdad här.", - "rot_rut_history_empty": "Inga filer har skapats för den här avdragstypen.", - "rot_rut_history_meta": "{date} · {count} ärenden · {amount}", - "rot_rut_status_generated": "Skapad", - "rot_rut_status_submitted": "Uppladdad", - "rot_rut_status_paid": "Beviljad", - "rot_rut_status_partially_paid": "Delvis beviljad", - "rot_rut_status_rejected": "Avslagen", - "rot_rut_status_cancelled": "Avbruten", - "rot_rut_download_again": "Hämta igen", - "rot_rut_cancel_request": "Avbryt", - "rot_rut_mark_uploaded": "Markera uppladdad", - "rot_rut_uploaded_title": "Filen är markerad som uppladdad", - "rot_rut_cancelled_title": "Begäran är avbruten", - "rot_rut_update_failed_title": "Kunde inte uppdatera begäran", - "rot_rut_download_failed_title": "Kunde inte hämta filen", - "rot_rut_download_timeout": "Hämtningen tog för lång tid. Försök igen.", - "rot_rut_download_network": "Filen kunde inte hämtas. Kontrollera anslutningen och försök igen.", - "rot_rut_skatteverket_link": "Öppna ROT och RUT hos Skatteverket" + "review_customer_missing_description": "Ladda om sidan och försök igen. Kontakta support om det inte hjälper." }, "invoice_review": { "assigned_number_prefix": "Tilldelas fakturanummer", @@ -4345,6 +4301,9 @@ "current": "Aktuell" }, "journal_detail": { + "accounts_loading": "Laddar kontoplan...", + "accounts_load_failed": "Kontoplanen kunde inte laddas. Försök igen för att ändra konto.", + "accounts_retry": "Försök igen", "edit_draft": "Redigera", "back": "Tillbaka till bokföring", "loading": "Laddar verifikation...", @@ -5047,6 +5006,10 @@ "footer_to_handle": "{count, plural, =0 {Inget att hantera} =1 {1 att hantera} other {# att hantera}}" }, "bookkeeping": { + "toast_post_failed": "Kunde inte bokföra", + "toast_posted_title": "Verifikat bokfört", + "toast_posted_description": "Verifikat {voucher} har bokförts.", + "toast_post_failed_generic": "Kunde inte bokföra verifikat", "edit_draft_dialog_title": "Redigera utkast", "title": "Bokföring", "year_end": "Årsbokslut", @@ -5457,6 +5420,50 @@ "validation_min_one_row": "Minst en rad krävs" }, "invoices": { + "rot_rut_payout_action": "ROT/RUT-fil", + "rot_rut_payout_title": "Begär utbetalning för ROT/RUT", + "rot_rut_payout_description": "Välj betalda fakturor och hämta en XML-fil för Skatteverkets e-tjänst. Filen skickas inte automatiskt: du laddar upp och signerar den hos Skatteverket.", + "rot_rut_type_aria": "Välj avdragstyp", + "rot_rut_type_rot": "ROT", + "rot_rut_type_rut": "RUT", + "rot_rut_year_aria": "Välj betalningsår", + "rot_rut_loading": "Laddar ROT/RUT-underlag", + "rot_rut_load_failed_title": "Kunde inte ladda ROT/RUT-underlaget", + "rot_rut_load_failed_description": "Ladda om sidan och försök igen.", + "rot_rut_eligible_title": "Fakturor att ta med", + "rot_rut_selected_count": "{selected} valda · {amount}", + "rot_rut_select_all": "Välj alla", + "rot_rut_clear_selection": "Rensa val", + "rot_rut_no_eligible_title": "Inga fakturor är redo", + "rot_rut_no_eligible_description": "När en ROT- eller RUT-faktura är betald och har fullständiga köparuppgifter visas den här.", + "rot_rut_paid_at": "Betald {date}", + "rot_rut_max_cases_help": "Skatteverket tillåter högst {count} ärenden i samma fil. Skapa flera filer om fler fakturor ska begäras.", + "rot_rut_generate_file": "Skapa och hämta fil", + "rot_rut_generating_file": "Skapar fil…", + "rot_rut_generated_title": "ROT/RUT-filen är hämtad", + "rot_rut_generated_description": "Ladda upp filen i Skatteverkets e-tjänst och signera begäran där.", + "rot_rut_generate_failed_title": "Kunde inte skapa ROT/RUT-filen", + "rot_rut_blocked_title": "Kan inte tas med ({count})", + "rot_rut_history_title": "Tidigare filer", + "rot_rut_upload_help": "Efter uppladdning och signering hos Skatteverket markerar du filen som uppladdad här.", + "rot_rut_history_empty": "Inga filer har skapats för den här avdragstypen.", + "rot_rut_history_meta": "{date} · {count} ärenden · {amount}", + "rot_rut_status_generated": "Skapad", + "rot_rut_status_submitted": "Uppladdad", + "rot_rut_status_paid": "Beviljad", + "rot_rut_status_partially_paid": "Delvis beviljad", + "rot_rut_status_rejected": "Avslagen", + "rot_rut_status_cancelled": "Avbruten", + "rot_rut_download_again": "Hämta igen", + "rot_rut_cancel_request": "Avbryt", + "rot_rut_mark_uploaded": "Markera uppladdad", + "rot_rut_uploaded_title": "Filen är markerad som uppladdad", + "rot_rut_cancelled_title": "Begäran är avbruten", + "rot_rut_update_failed_title": "Kunde inte uppdatera begäran", + "rot_rut_download_failed_title": "Kunde inte hämta filen", + "rot_rut_download_timeout": "Hämtningen tog för lång tid. Försök igen.", + "rot_rut_download_network": "Filen kunde inte hämtas. Kontrollera anslutningen och försök igen.", + "rot_rut_skatteverket_link": "Öppna ROT och RUT hos Skatteverket", "title": "Kundfakturor", "recurring": "Återkommande", "new_invoice": "Ny faktura",