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:
@@ -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'])
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
Reference in New Issue
Block a user