Bug/skv konto numbers (#498)

* feat(skattekonto): add overdue transactions handling and split logic

* feat: enhance transaction handling and loading states

- Update BalanceHero component to display last synced date and additional information about Skatteverket updates.
- Refactor BookDirectlyDialog to simplify transaction linking logic and improve UI for transaction selection.
- Revamp InvoiceInboxWorkspace layout for better responsiveness and user experience, including improved skeleton loading states.
- Introduce new loading states for ExtensionWorkspace to match the live layout and improve user feedback during data fetching.
- Implement exchange rate fetching in QuickReviewDialog, ensuring transactions are always processed in SEK with error handling for rate fetching.
- Add structured error handling for unavailable exchange rates in the transaction API.

* feat(skattekonto): add 'Skattekonto – saldo & transaktioner' scope and update authorization checks

* feat: implement reverse charge handling in supplier invoice calculations and UI
This commit is contained in:
Mattsson
2026-05-15 16:25:05 +02:00
committed by GitHub
parent 3912c74a7b
commit 7175daee87
16 changed files with 780 additions and 623 deletions
@@ -0,0 +1,102 @@
import { describe, it, expect } from 'vitest'
import { splitTransactions } from '../lib/skattekonto-buckets'
import type { StoredSkattekontoTransaction } from '../types'
function makeRow(
overrides: Partial<StoredSkattekontoTransaction> = {},
): StoredSkattekontoTransaction {
return {
id: 'row-1',
company_id: 'co-1',
transaktionsidentitet: null,
dedup_key: 'h:abc',
transaktionsdatum: '2026-05-12',
forfallodatum: null,
ranteberakningsdatum: null,
transaktionstext: 'Test',
belopp_skatteverket: 100,
belopp_kronofogden: 0,
status: 'upcoming',
journal_entry_id: null,
imported_at: '2026-05-15T10:00:00Z',
updated_at: '2026-05-15T10:00:00Z',
...overrides,
}
}
describe('splitTransactions', () => {
const today = '2026-05-15'
it('routes booked rows to booked regardless of date', () => {
const rows = [
makeRow({ id: 'a', status: 'booked', transaktionsdatum: '2026-05-12' }),
makeRow({ id: 'b', status: 'booked', transaktionsdatum: '2026-06-01' }),
]
const out = splitTransactions(rows, today)
expect(out.booked.map(r => r.id)).toEqual(['a', 'b'])
expect(out.overdue).toEqual([])
expect(out.upcoming).toEqual([])
})
it('routes upcoming with past forfallodatum to overdue', () => {
// The reported bug: SKV still has it in kommande on 2026-05-15
// even though forfallodatum was 2026-05-12.
const rows = [
makeRow({ id: 'a', status: 'upcoming', forfallodatum: '2026-05-12' }),
]
const out = splitTransactions(rows, today)
expect(out.overdue.map(r => r.id)).toEqual(['a'])
expect(out.upcoming).toEqual([])
})
it('routes upcoming with future forfallodatum to upcoming', () => {
const rows = [
makeRow({ id: 'a', status: 'upcoming', forfallodatum: '2026-06-12' }),
]
const out = splitTransactions(rows, today)
expect(out.upcoming.map(r => r.id)).toEqual(['a'])
expect(out.overdue).toEqual([])
})
it('treats today as upcoming, not overdue', () => {
const rows = [
makeRow({ id: 'a', status: 'upcoming', forfallodatum: today }),
]
const out = splitTransactions(rows, today)
expect(out.upcoming.map(r => r.id)).toEqual(['a'])
expect(out.overdue).toEqual([])
})
it('falls back to transaktionsdatum when forfallodatum is null', () => {
const rows = [
makeRow({
id: 'past',
status: 'upcoming',
forfallodatum: null,
transaktionsdatum: '2026-05-10',
}),
makeRow({
id: 'future',
status: 'upcoming',
forfallodatum: null,
transaktionsdatum: '2026-05-20',
}),
]
const out = splitTransactions(rows, today)
expect(out.overdue.map(r => r.id)).toEqual(['past'])
expect(out.upcoming.map(r => r.id)).toEqual(['future'])
})
it('handles a mixed input', () => {
const rows = [
makeRow({ id: 'b1', status: 'booked' }),
makeRow({ id: 'o1', status: 'upcoming', forfallodatum: '2026-05-01' }),
makeRow({ id: 'u1', status: 'upcoming', forfallodatum: '2026-05-31' }),
makeRow({ id: 'o2', status: 'upcoming', forfallodatum: '2026-05-14' }),
]
const out = splitTransactions(rows, today)
expect(out.booked.map(r => r.id)).toEqual(['b1'])
expect(out.overdue.map(r => r.id)).toEqual(['o1', 'o2'])
expect(out.upcoming.map(r => r.id)).toEqual(['u1'])
})
})
+5 -2
View File
@@ -26,6 +26,7 @@ import {
matchSkattekontoToEntry,
SkattekontoMatchError,
} from './lib/skattekonto-match'
import { splitTransactions } from './lib/skattekonto-buckets'
import type { SkattekontoBalanceSnapshot } from './types'
import type { VatPeriodType } from '@/types'
@@ -1387,7 +1388,8 @@ export const skatteverketExtension: Extension = {
}
const rows = data ?? []
const booked = rows.filter(r => r.status === 'booked')
const today = new Date().toISOString().slice(0, 10)
const { booked, overdue, upcoming } = splitTransactions(rows, today)
// Enrich obokförda rader with a single-best-candidate suggestion.
// Only attached when there's exactly one match — avoids the UI
@@ -1411,7 +1413,8 @@ export const skatteverketExtension: Extension = {
return NextResponse.json({
data: {
booked: bookedEnriched,
upcoming: rows.filter(r => r.status === 'upcoming'),
overdue,
upcoming,
},
})
},
+1 -1
View File
@@ -23,7 +23,7 @@ const DEFAULT_OAUTH_BASE_URL = 'https://peroauth2.test.skatteverket.se/oauth2/v1
// section 4.1.2.2 — the 403 "Felaktigt access scope" example shows
// `"description": "The required scope agd has been requested for that access token."`
// The other tokens match the path segments of their respective APIs.
const DEFAULT_SCOPES = 'momsdeklaration inkforetag skattekonto agd'
const DEFAULT_SCOPES = 'momsdeklaration inkforetag skahmst skattekonto agd'
function getOAuthBaseUrl(): string {
return process.env.SKATTEVERKET_OAUTH_BASE_URL || DEFAULT_OAUTH_BASE_URL
@@ -0,0 +1,42 @@
import type { StoredSkattekontoTransaction } from '../types'
export interface SkattekontoBuckets<T extends StoredSkattekontoTransaction> {
booked: T[]
overdue: T[]
upcoming: T[]
}
/**
* Split skattekonto rows into UI buckets.
*
* SKV's `kommandeTransaktioner` includes rows whose due date has passed but
* haven't settled yet — labelling them "Kommande" misleads the user. We pull
* those into a separate "Förfallna" bucket here. Stored `status` keeps
* mirroring SKV.
*
* `today` is an ISO date string ('YYYY-MM-DD'). Lexicographic compare on
* ISO dates is chronological.
*/
export function splitTransactions<T extends StoredSkattekontoTransaction>(
rows: T[],
today: string,
): SkattekontoBuckets<T> {
const booked: T[] = []
const overdue: T[] = []
const upcoming: T[] = []
for (const row of rows) {
if (row.status === 'booked') {
booked.push(row)
continue
}
const dueDate = row.forfallodatum ?? row.transaktionsdatum
if (dueDate < today) {
overdue.push(row)
} else {
upcoming.push(row)
}
}
return { booked, overdue, upcoming }
}