Files
accounted/extensions/general/skatteverket/lib/mappers.ts
T
MattssonandClaude Opus 4.7 980f29dae8 Bug/momsdeklaration skv (#449)
* fix(salary): show birthdate in masked personnummer, hide the 4-digit suffix

Flip the personnummer display format from XXXXXXXX-NNNN to YYYYMMDD-XXXX so
the sensitive 4-digit suffix is hidden while the (public) birthdate stays
visible. Affects the employees list/detail, salary run, payslip PDF, payslip
email, and the MCP server tools (list_employees, get_salary_run). Each call
site now decrypts the stored personnummer before masking.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(transactions): allow deleting unbooked transactions from "Alla transaktioner"

The history list only let users delete via the inbox card; once a category or
mall was picked but the verifikation hadn't been created, the row showed
"Ej bokförd" with no way to remove it. The API already permits delete while
journal_entry_id is null, so the gap was purely a missing UI affordance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(vat): populate ruta 20-24 for reverse charge + dishonest "Validera OK"

Three connected issues caused Skatteverket to reject momsdeklarationer
with FK004 even after our local "Validera"-knapp returned OK.

1. supplier-invoice-entries booked fiktiv moms (2614/2624/2634 + 2645/2647)
   on reverse-charge invoices but never the underlying basbelopp on 44xx/45xx.
   Ruta 30-32 filled up at SKV while ruta 20-24 stayed at 0 — SKV's FK004
   ("silent netting prohibited", ML 13 kap kräver båda sidor).

   Fix: generateReverseChargeBasisLines in vat-entries.ts emits parallel
   45xx/44xx debit + 4598 motkonto credit per rate group. Engine calls it
   from registration, cash, and credit-note paths. Skipped when the user
   booked the expense directly on a basis account to avoid double-counting.
   4598 added to BAS reference (no migration needed; account_number is
   plain text on journal_entry_lines).

2. rutorToMomsuppgift rounded each ruta independently but computed
   summaMoms from the unrounded ruta49. SKV recomputes the sum from
   integer rutor on their side, so fractional öres caused ±1 SEK drift
   and SKV rejected with FK009.

   Fix: derive summaMoms from the already-rounded VAT-amount rutor.

3. "Validera"-knappen only confirmed SKV's internal arithmetic — a
   declaration with ruta 30-32 populated and ruta 20-24 empty validated
   fine until /utkast hit FK004. Users got a false green light.

   Fix: vat-declaration-checks.ts runs locally before the SKV call,
   blocks Validera/Spara when ERROR-level findings exist, and surfaces
   them in a separate "Lokala kontroller"-section. Success message
   reworded so SKV's OK is no longer presented as filing-ready.

Tests: 4535/4536/4531/4425 lines + 4598 motkonto on EU/non-EU/byggtjänster
RC, credit-note reversal, fractional-öres summaMoms, all four pre-flight
codes (RC_BASIS_MISSING, RC_OUTPUT_MISSING, RC_INPUT_VAT_MISMATCH,
SUMMA_MOMS_DRIFT).

Backfill for already-posted entries follows in the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: add skattekonto matching functionality

- Enhance TransactionInboxCard to display a warning for potential 1930↔1630 transfers.
- Implement match suggestions for skattekonto transactions in the backend.
- Create SkattekontoMatchDialog component for linking skattekonto rows to existing journal entries.
- Develop SkattekontoInboxCard component to handle skattekonto transactions in the inbox.
- Introduce skattekonto-match utility functions for candidate matching and linking.
- Update types to include match suggestions and enriched transaction responses.

* refactor: reorganize skattekonto types and implement bank counterpart matching logic

* docs: update CLAUDE.md to streamline integrations and clarify architecture details

* refactor: enhance reverse charge logic to handle non-basis accounts and prevent double-counting

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:04:48 +02:00

89 lines
3.4 KiB
TypeScript

import type { VatDeclarationRutor } from '@/types'
import type { SkatteverketMomsuppgift } from '../types'
// Re-export shared formatting utilities
export { formatRedovisare, formatRedovisningsperiod } from '@/lib/skatteverket/format'
/**
* Convert gnubok VatDeclarationRutor to Skatteverket's momsuppgift payload.
*
* Fields with value 0 are omitted (Skatteverket treats absent fields as 0).
* This keeps the payload clean and avoids sending unnecessary data.
*
* Rounding: every ruta is rounded to whole kronor (SKV's schema is integers).
* summaMoms is recomputed from the rounded VAT-amount rutor — not from the
* pre-rounding ruta49 — so the payload is internally consistent with how
* Skatteverket recomputes the sum on their side. Rounding ruta49 separately
* from the components causes ±1 SEK drift on fractional-öres inputs and
* triggers SKV's FK009 ("summaMoms stämmer inte överens med övriga
* momsuppgifter") even when the underlying ledger arithmetic is correct.
*/
export function rutorToMomsuppgift(rutor: VatDeclarationRutor): SkatteverketMomsuppgift {
const result: SkatteverketMomsuppgift = {}
// Helper: only set non-zero values, rounded to whole kronor (SKV expects integers)
const set = (key: keyof SkatteverketMomsuppgift, value: number) => {
if (value !== 0) result[key] = Math.round(value)
}
// Taxable sales basis
set('momspliktigForsaljning', rutor.ruta05)
set('momspliktigaUttag', rutor.ruta06)
set('vinstmarginal', rutor.ruta07)
set('hyresInkomst', rutor.ruta08)
// Output VAT on sales
set('momsForsaljningUtgaendeHog', rutor.ruta10)
set('momsForsaljningUtgaendeMedel', rutor.ruta11)
set('momsForsaljningUtgaendeLag', rutor.ruta12)
// Reverse charge purchase bases
set('inkopVarorEU', rutor.ruta20)
set('inkopTjansterEU', rutor.ruta21)
set('inkopTjansterUtanforEU', rutor.ruta22)
set('inkopVarorSE', rutor.ruta23)
set('inkopTjansterSE', rutor.ruta24)
// Output VAT on reverse charge purchases
set('momsInkopUtgaendeHog', rutor.ruta30)
set('momsInkopUtgaendeMedel', rutor.ruta31)
set('momsInkopUtgaendeLag', rutor.ruta32)
// EU/export sales
set('forsaljningVarorEU', rutor.ruta35)
set('forsaljningVarorUtanforEU', rutor.ruta36)
set('inkopVaror3pHandel', rutor.ruta37)
set('forsaljningVaror3pHandel', rutor.ruta38)
set('forsaljningTjansterEU', rutor.ruta39)
set('ovrigForsaljningTjansterUtanforSE', rutor.ruta40)
set('forsaljningBskKopareSE', rutor.ruta41)
set('momsfriForsaljning', rutor.ruta42)
// Input VAT
set('ingaendeMomsAvdrag', rutor.ruta48)
// Import
set('import', rutor.ruta50)
set('momsImportUtgaendeHog', rutor.ruta60)
set('momsImportUtgaendeMedel', rutor.ruta61)
set('momsImportUtgaendeLag', rutor.ruta62)
// Net VAT must always be present, whole kronor. Compute from the already-
// rounded VAT-amount rutor so SKV's reconciliation (sum of integer rutor)
// never disagrees with our summaMoms by ±1 SEK.
result.summaMoms =
(result.momsForsaljningUtgaendeHog ?? 0) +
(result.momsForsaljningUtgaendeMedel ?? 0) +
(result.momsForsaljningUtgaendeLag ?? 0) +
(result.momsInkopUtgaendeHog ?? 0) +
(result.momsInkopUtgaendeMedel ?? 0) +
(result.momsInkopUtgaendeLag ?? 0) +
(result.momsImportUtgaendeHog ?? 0) +
(result.momsImportUtgaendeMedel ?? 0) +
(result.momsImportUtgaendeLag ?? 0) -
(result.ingaendeMomsAvdrag ?? 0)
return result
}