feat(skatteverket): expose filed VAT declarations and decisions via the v1 API (#1773)
* feat(skatteverket): expose filed VAT declarations and decisions via the v1 API Add GET /api/v1/companies/:companyId/skatteverket/vat-declarations, returning a period's momsdeklaration as Skatteverket has it on file: the submitted declaration (SKV /inlamnat) and Skatteverket's beslut (SKV /beslutat), either individually via ?state= or both. - Auth: compliance:read scope; member-visibility read model per #1673 (resolveReadAuth: caller's token, any member's active token, or system credentials with a verified ombud grant). - Architecture: core reaches the Skatteverket extension through the registry-resolved services channel (contract in lib/skatteverket/declaration-status.ts), so core never imports from @/extensions/. - New structured error SKATTEVERKET_API_ERROR (502) for upstream SKV failures; 404 from SKV maps to submitted/decided = null with HTTP 200. - 19 new tests (route: auth, validation, extension-disabled, happy path; extension service: auth resolution, state filtering, SKV error mapping). Fixes #1663 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skatteverket): address review findings on the vat-declarations read API Consolidated fixes for PR #1773 review round: - apiskill sync (core-build Checks): map the new skatteverket endpoint group into the periods.md reference and regenerate skills/accounted-api (124 -> 125 operations). - CodeRabbit: parse the SKV 2xx body before writing the audit row, so an unreadable body is audited as skv_error and returns the structured SKATTEVERKET_API_ERROR 502 instead of escaping as an internal 500; regression test added. - Compliance swarm (ISO A.8.12 / SOC2 CC6.1): stop forwarding the raw upstream SKV response body to API consumers; the caller now gets the status code and a generic Swedish message, the body is logged server-side only. - Compliance swarm (GDPR Art.30): add the moms.declaration_status_read processing activity to .compliance/ropa.yaml (live read, no payload persisted, audit-log metadata only). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
0de766c6a4
commit
60920ec794
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* Tests for the registry-exposed read service fetchVatDeclarationStatus
|
||||
* (issue #1663): the SKATTEVERKET_ENABLED gate, the #1673 company-scoped
|
||||
* read-auth model, 404 → null semantics for inlamnat/beslutat, and the
|
||||
* structured error mapping the v1 route depends on.
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
|
||||
const mockSkvRequestWithAuth = vi.fn()
|
||||
vi.mock('../lib/api-client', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as Record<string, unknown>
|
||||
return { ...actual, skvRequestWithAuth: (...a: unknown[]) => mockSkvRequestWithAuth(...a) }
|
||||
})
|
||||
|
||||
const mockResolveReadAuth = vi.fn()
|
||||
vi.mock('../lib/resolve-auth', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as Record<string, unknown>
|
||||
return { ...actual, resolveReadAuth: (...a: unknown[]) => mockResolveReadAuth(...a) }
|
||||
})
|
||||
|
||||
const mockResolveRedovisare = vi.fn()
|
||||
vi.mock('../lib/declaration-prep', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as Record<string, unknown>
|
||||
return { ...actual, resolveRedovisare: (...a: unknown[]) => mockResolveRedovisare(...a) }
|
||||
})
|
||||
|
||||
const mockResolvePeriodDates = vi.fn()
|
||||
vi.mock('@/lib/reports/vat-declaration', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as Record<string, unknown>
|
||||
return { ...actual, resolvePeriodDates: (...a: unknown[]) => mockResolvePeriodDates(...a) }
|
||||
})
|
||||
|
||||
const mockWriteAudit = vi.fn()
|
||||
vi.mock('../lib/audit', () => ({
|
||||
writeSkatteverketAudit: (...a: unknown[]) => mockWriteAudit(...a),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/extensions/context-factory', () => ({
|
||||
createExtensionContext: () => ({
|
||||
supabase: {},
|
||||
companyId: 'company-1',
|
||||
userId: 'user-1',
|
||||
settings: { set: vi.fn().mockResolvedValue(undefined) },
|
||||
log: { error: vi.fn(), warn: vi.fn(), info: vi.fn() },
|
||||
}),
|
||||
}))
|
||||
|
||||
import type { SupabaseClient } from '@supabase/supabase-js'
|
||||
import { fetchVatDeclarationStatus } from '../lib/declaration-status'
|
||||
import { SkatteverketAuthError } from '../lib/api-client'
|
||||
|
||||
const supabase = {} as SupabaseClient
|
||||
const USER_AUTH = { mode: 'user', supabase, userId: 'user-2', companyId: 'company-1' }
|
||||
|
||||
function skvJson(status: number, body: unknown) {
|
||||
return {
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
json: async () => body,
|
||||
text: async () => JSON.stringify(body),
|
||||
}
|
||||
}
|
||||
|
||||
let prevEnv: string | undefined
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
prevEnv = process.env.SKATTEVERKET_ENABLED
|
||||
process.env.SKATTEVERKET_ENABLED = 'true'
|
||||
mockResolveRedovisare.mockResolvedValue('165560000167')
|
||||
mockResolveReadAuth.mockResolvedValue({
|
||||
ok: true,
|
||||
auth: USER_AUTH,
|
||||
source: 'user',
|
||||
tokenUserId: 'user-2',
|
||||
})
|
||||
})
|
||||
afterEach(() => {
|
||||
if (prevEnv === undefined) delete process.env.SKATTEVERKET_ENABLED
|
||||
else process.env.SKATTEVERKET_ENABLED = prevEnv
|
||||
})
|
||||
|
||||
describe('fetchVatDeclarationStatus', () => {
|
||||
it('flag off → EXTENSION_DISABLED, zero SKV calls', async () => {
|
||||
delete process.env.SKATTEVERKET_ENABLED
|
||||
const result = await fetchVatDeclarationStatus(supabase, 'user-1', 'company-1', {
|
||||
periodType: 'monthly', year: 2026, period: 3,
|
||||
})
|
||||
expect(result).toMatchObject({ ok: false, code: 'EXTENSION_DISABLED', http_status: 503 })
|
||||
expect(mockSkvRequestWithAuth).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('missing org number → VALIDATION_ERROR 400', async () => {
|
||||
mockResolveRedovisare.mockRejectedValue(
|
||||
new Error('Organisationsnummer saknas i företagsinställningar'),
|
||||
)
|
||||
const result = await fetchVatDeclarationStatus(supabase, 'user-1', 'company-1', {
|
||||
periodType: 'monthly', year: 2026, period: 3,
|
||||
})
|
||||
expect(result).toMatchObject({ ok: false, code: 'VALIDATION_ERROR', http_status: 400 })
|
||||
expect(mockSkvRequestWithAuth).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('no company token → SKATTEVERKET_NOT_CONNECTED 401', async () => {
|
||||
mockResolveReadAuth.mockResolvedValue({ ok: false, reason: 'no_token' })
|
||||
const result = await fetchVatDeclarationStatus(supabase, 'user-1', 'company-1', {
|
||||
periodType: 'monthly', year: 2026, period: 3,
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
ok: false, code: 'SKATTEVERKET_NOT_CONNECTED', http_status: 401,
|
||||
})
|
||||
expect(mockSkvRequestWithAuth).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('needs_reconsent → SKATTEVERKET_NOT_CONNECTED with reconnect message', async () => {
|
||||
mockResolveReadAuth.mockResolvedValue({ ok: false, reason: 'needs_reconsent' })
|
||||
const result = await fetchVatDeclarationStatus(supabase, 'user-1', 'company-1', {
|
||||
periodType: 'monthly', year: 2026, period: 3,
|
||||
})
|
||||
expect(result).toMatchObject({ ok: false, code: 'SKATTEVERKET_NOT_CONNECTED' })
|
||||
expect((result as { error: string }).error).toContain('förnyas')
|
||||
})
|
||||
|
||||
it('happy path both: inlamnat + beslutat via the company-resolved auth (#1673)', async () => {
|
||||
mockSkvRequestWithAuth
|
||||
.mockResolvedValueOnce(skvJson(200, { skatt: 12500 }))
|
||||
.mockResolvedValueOnce(skvJson(200, { beslut: 'FASTSTALLT' }))
|
||||
|
||||
const result = await fetchVatDeclarationStatus(supabase, 'user-1', 'company-1', {
|
||||
periodType: 'quarterly', year: 2026, period: 1,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
redovisare: '165560000167',
|
||||
redovisningsperiod: '202603',
|
||||
submitted: { skatt: 12500 },
|
||||
decided: { beslut: 'FASTSTALLT' },
|
||||
})
|
||||
// The resolved (possibly another member's) auth is what hits SKV: the
|
||||
// caller's own uid is only a preference inside resolveReadAuth.
|
||||
expect(mockResolveReadAuth).toHaveBeenCalledWith(supabase, 'company-1', {
|
||||
requires: 'moms_ombud', userId: 'user-1',
|
||||
})
|
||||
expect(mockSkvRequestWithAuth.mock.calls[0]).toEqual([
|
||||
USER_AUTH, 'GET', '/inlamnat/165560000167/202603',
|
||||
])
|
||||
expect(mockSkvRequestWithAuth.mock.calls[1]).toEqual([
|
||||
USER_AUTH, 'GET', '/beslutat/165560000167/202603',
|
||||
])
|
||||
// One regulator audit row per SKV view.
|
||||
expect(mockWriteAudit).toHaveBeenCalledTimes(2)
|
||||
expect(mockWriteAudit.mock.calls[0][1]).toMatchObject({ endpoint: 'inlamnat', outcome: 'ok' })
|
||||
expect(mockWriteAudit.mock.calls[1][1]).toMatchObject({ endpoint: 'beslutat', outcome: 'ok' })
|
||||
})
|
||||
|
||||
it('404 from SKV means nothing on file: null sections, ok audit outcome', async () => {
|
||||
mockSkvRequestWithAuth
|
||||
.mockResolvedValueOnce(skvJson(404, {}))
|
||||
.mockResolvedValueOnce(skvJson(404, {}))
|
||||
const result = await fetchVatDeclarationStatus(supabase, 'user-1', 'company-1', {
|
||||
periodType: 'monthly', year: 2026, period: 7,
|
||||
})
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
redovisare: '165560000167',
|
||||
redovisningsperiod: '202607',
|
||||
submitted: null,
|
||||
decided: null,
|
||||
})
|
||||
expect(mockWriteAudit.mock.calls[0][1]).toMatchObject({ outcome: 'ok', responseStatus: 404 })
|
||||
})
|
||||
|
||||
it("state='submitted' only calls inlamnat and leaves decided null", async () => {
|
||||
mockSkvRequestWithAuth.mockResolvedValueOnce(skvJson(200, { skatt: 1 }))
|
||||
const result = await fetchVatDeclarationStatus(supabase, 'user-1', 'company-1', {
|
||||
periodType: 'monthly', year: 2026, period: 3, state: 'submitted',
|
||||
})
|
||||
expect(result).toMatchObject({ ok: true, submitted: { skatt: 1 }, decided: null })
|
||||
expect(mockSkvRequestWithAuth).toHaveBeenCalledTimes(1)
|
||||
expect(mockSkvRequestWithAuth.mock.calls[0][2]).toBe('/inlamnat/165560000167/202603')
|
||||
})
|
||||
|
||||
it("state='decided' only calls beslutat and leaves submitted null", async () => {
|
||||
mockSkvRequestWithAuth.mockResolvedValueOnce(skvJson(200, { beslut: 'X' }))
|
||||
const result = await fetchVatDeclarationStatus(supabase, 'user-1', 'company-1', {
|
||||
periodType: 'monthly', year: 2026, period: 3, state: 'decided',
|
||||
})
|
||||
expect(result).toMatchObject({ ok: true, submitted: null, decided: { beslut: 'X' } })
|
||||
expect(mockSkvRequestWithAuth).toHaveBeenCalledTimes(1)
|
||||
expect(mockSkvRequestWithAuth.mock.calls[0][2]).toBe('/beslutat/165560000167/202603')
|
||||
})
|
||||
|
||||
it('yearly resolves the fiscal-year end month (broken räkenskapsår)', async () => {
|
||||
mockResolvePeriodDates.mockResolvedValue({ start: '2025-07-01', end: '2026-06-30' })
|
||||
mockSkvRequestWithAuth.mockResolvedValue(skvJson(404, {}))
|
||||
const result = await fetchVatDeclarationStatus(supabase, 'user-1', 'company-1', {
|
||||
periodType: 'yearly', year: 2026, period: 1,
|
||||
})
|
||||
expect(result).toMatchObject({ ok: true, redovisningsperiod: '202606' })
|
||||
})
|
||||
|
||||
it('upstream non-404 error → SKATTEVERKET_API_ERROR 502 with skv_error audit', async () => {
|
||||
mockSkvRequestWithAuth.mockResolvedValueOnce(skvJson(500, { fel: 'internt' }))
|
||||
const result = await fetchVatDeclarationStatus(supabase, 'user-1', 'company-1', {
|
||||
periodType: 'monthly', year: 2026, period: 3,
|
||||
})
|
||||
expect(result).toMatchObject({ ok: false, code: 'SKATTEVERKET_API_ERROR', http_status: 502 })
|
||||
expect((result as { error: string }).error).toContain('500')
|
||||
// The upstream body is logged server-side only, never forwarded to the
|
||||
// API consumer (it can leak Skatteverket system details).
|
||||
expect((result as { error: string }).error).not.toContain('internt')
|
||||
expect(mockWriteAudit.mock.calls[0][1]).toMatchObject({ outcome: 'skv_error' })
|
||||
})
|
||||
|
||||
it('2xx with an unparseable body → SKATTEVERKET_API_ERROR 502 with skv_error audit', async () => {
|
||||
mockSkvRequestWithAuth.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => {
|
||||
throw new SyntaxError('Unexpected end of JSON input')
|
||||
},
|
||||
text: async () => '',
|
||||
})
|
||||
const result = await fetchVatDeclarationStatus(supabase, 'user-1', 'company-1', {
|
||||
periodType: 'monthly', year: 2026, period: 3,
|
||||
})
|
||||
expect(result).toMatchObject({ ok: false, code: 'SKATTEVERKET_API_ERROR', http_status: 502 })
|
||||
expect(mockWriteAudit.mock.calls[0][1]).toMatchObject({
|
||||
outcome: 'skv_error',
|
||||
responseStatus: 200,
|
||||
})
|
||||
})
|
||||
|
||||
it('SkatteverketAuthError → structured code via skvAuthCodeToStructured', async () => {
|
||||
mockSkvRequestWithAuth.mockRejectedValueOnce(
|
||||
new SkatteverketAuthError('Behörighet saknas', 'BEHORIGHET_SAKNAS'),
|
||||
)
|
||||
const result = await fetchVatDeclarationStatus(supabase, 'user-1', 'company-1', {
|
||||
periodType: 'monthly', year: 2026, period: 3,
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
ok: false, code: 'SKATTEVERKET_ACCESS_DENIED', http_status: 403,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
agiKontrolleraIU,
|
||||
} from './lib/agi-client'
|
||||
import { syncSkattekonto, SKATTEKONTO_BALANCE_SNAPSHOT_KEY, SKATTEKONTO_LAST_SYNCED_AT_KEY } from './lib/skattekonto-sync'
|
||||
import { fetchVatDeclarationStatus } from './lib/declaration-status'
|
||||
import { runPostConnectRefresh } from './lib/post-connect-refresh'
|
||||
import { readAgiSubmissionStatus } from './lib/agi-submission-status'
|
||||
import {
|
||||
@@ -2502,6 +2503,10 @@ export const skatteverketExtension: Extension = {
|
||||
services: {
|
||||
commitSubmitVatDeclaration,
|
||||
commitSubmitAgi,
|
||||
// Read service for the v1 REST endpoint (issue #1663): filed
|
||||
// momsdeklarationer (inlamnat) and beslut (beslutat). Contract in
|
||||
// lib/skatteverket/declaration-status.ts.
|
||||
fetchVatDeclarationStatus,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
import type { SupabaseClient } from '@supabase/supabase-js'
|
||||
import { formatRedovisningsperiod } from '@/lib/skatteverket/format'
|
||||
import { resolvePeriodDates } from '@/lib/reports/vat-declaration'
|
||||
import { createExtensionContext } from '@/lib/extensions/context-factory'
|
||||
import type {
|
||||
SkvVatDeclarationStatusInput,
|
||||
SkvVatDeclarationStatusResult,
|
||||
} from '@/lib/skatteverket/declaration-status'
|
||||
import { skvRequestWithAuth, SkatteverketAuthError } from './api-client'
|
||||
import { resolveReadAuth } from './resolve-auth'
|
||||
import { resolveRedovisare } from './declaration-prep'
|
||||
import { skvAuthCodeToStructured } from './error-map'
|
||||
import { writeSkatteverketAudit } from './audit'
|
||||
|
||||
/**
|
||||
* Registry-resolved read service for filed momsdeklarationer (issue #1663).
|
||||
*
|
||||
* Fetches Skatteverket's /inlamnat (the declaration as submitted) and/or
|
||||
* /beslutat (the beslut) views for one period, so API consumers (v1 REST) can
|
||||
* compare their books against actually-filed data. Read-only on SKV's side;
|
||||
* the only local writes are the regulator audit rows.
|
||||
*
|
||||
* Auth follows the company-scoped read model (#1673, resolve-auth.ts): the
|
||||
* caller's own token when they connected, otherwise any other member's active
|
||||
* token, otherwise system credentials with a verified ombud grant. The fetched
|
||||
* declaration belongs to the company, not to whoever pressed "Anslut".
|
||||
*
|
||||
* Error contract: known failure modes return `{ ok: false, code, http_status,
|
||||
* error }` with structured codes so the v1 route maps them deterministically;
|
||||
* unexpected errors are thrown and handled by the route wrapper.
|
||||
*/
|
||||
export async function fetchVatDeclarationStatus(
|
||||
supabase: SupabaseClient,
|
||||
userId: string,
|
||||
companyId: string,
|
||||
input: SkvVatDeclarationStatusInput,
|
||||
): Promise<SkvVatDeclarationStatusResult> {
|
||||
// Direct service calls bypass the HTTP dispatcher's SKATTEVERKET_ENABLED
|
||||
// gate (app/api/extensions/ext/[...path]/route.ts), so check the flag here:
|
||||
// same reasoning as the commit services in index.ts.
|
||||
if (process.env.SKATTEVERKET_ENABLED !== 'true') {
|
||||
return {
|
||||
ok: false,
|
||||
code: 'EXTENSION_DISABLED',
|
||||
http_status: 503,
|
||||
error: 'Skatteverket-integrationen är inte aktiverad i denna miljö.',
|
||||
}
|
||||
}
|
||||
|
||||
const state = input.state ?? 'both'
|
||||
const ctx = createExtensionContext(supabase, userId, companyId, 'skatteverket')
|
||||
|
||||
let redovisare: string
|
||||
try {
|
||||
redovisare = await resolveRedovisare(supabase, companyId)
|
||||
} catch (err) {
|
||||
// Missing org number in company settings: a configuration problem the
|
||||
// caller can fix, not a server error.
|
||||
return {
|
||||
ok: false,
|
||||
code: 'VALIDATION_ERROR',
|
||||
http_status: 400,
|
||||
error:
|
||||
err instanceof Error ? err.message : 'Organisationsnummer saknas i företagsinställningar',
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Helårsmoms is filed per räkenskapsår (SFL 26 kap 10-11 §§): a broken
|
||||
// fiscal year ends in its own month, not December. Same resolution as
|
||||
// buildMomsuppgift so the period identifier matches what was filed.
|
||||
let fiscalYearEnd: { year: number; month: number } | undefined
|
||||
if (input.periodType === 'yearly') {
|
||||
const { end } = await resolvePeriodDates(
|
||||
supabase, companyId, input.periodType, input.year, input.period,
|
||||
)
|
||||
fiscalYearEnd = { year: Number(end.slice(0, 4)), month: Number(end.slice(5, 7)) }
|
||||
}
|
||||
const redovisningsperiod = formatRedovisningsperiod(
|
||||
input.periodType, input.year, input.period, fiscalYearEnd,
|
||||
)
|
||||
|
||||
const resolved = await resolveReadAuth(supabase, companyId, {
|
||||
requires: 'moms_ombud',
|
||||
userId,
|
||||
})
|
||||
if (!resolved.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
code: 'SKATTEVERKET_NOT_CONNECTED',
|
||||
http_status: 401,
|
||||
error:
|
||||
resolved.reason === 'needs_reconsent'
|
||||
? 'Anslutningen mot Skatteverket behöver förnyas. Anslut igen med BankID.'
|
||||
: 'Inte ansluten till Skatteverket.',
|
||||
}
|
||||
}
|
||||
|
||||
const fetchView = async (
|
||||
view: 'inlamnat' | 'beslutat',
|
||||
): Promise<
|
||||
| { ok: true; body: unknown }
|
||||
| { ok: false; failure: Extract<SkvVatDeclarationStatusResult, { ok: false }> }
|
||||
> => {
|
||||
const res = await skvRequestWithAuth(
|
||||
resolved.auth, 'GET', `/${view}/${redovisare}/${redovisningsperiod}`,
|
||||
)
|
||||
// Parse the 2xx body BEFORE writing the audit row, so a success status
|
||||
// with an unreadable body is recorded as skv_error, not 'ok', and maps
|
||||
// to SKATTEVERKET_API_ERROR instead of escaping as an internal 500.
|
||||
let body: unknown = null
|
||||
let bodyUnparseable = false
|
||||
if (res.ok) {
|
||||
try {
|
||||
body = await res.json()
|
||||
} catch {
|
||||
bodyUnparseable = true
|
||||
}
|
||||
}
|
||||
// 404 means "nothing on file for the period": a normal answer, not an
|
||||
// upstream failure. Same audit convention as the MCP status tool.
|
||||
const upstreamOk = (res.ok && !bodyUnparseable) || res.status === 404
|
||||
await writeSkatteverketAudit(ctx, {
|
||||
endpoint: view,
|
||||
agRegistreradId: redovisare,
|
||||
redovisningsperiod,
|
||||
outcome: upstreamOk ? 'ok' : 'skv_error',
|
||||
responseStatus: res.status,
|
||||
})
|
||||
if (res.status === 404) return { ok: true, body: null }
|
||||
if (!res.ok || bodyUnparseable) {
|
||||
// The upstream body is logged server-side only. Forwarding it verbatim
|
||||
// to API consumers would leak Skatteverket system details; the caller
|
||||
// gets the status code and a generic Swedish message.
|
||||
const text = bodyUnparseable
|
||||
? '<2xx body was not valid JSON>'
|
||||
: await res.text().catch(() => '')
|
||||
ctx.log.warn('skv declaration-status upstream error', {
|
||||
view,
|
||||
status: res.status,
|
||||
body: text.slice(0, 500),
|
||||
})
|
||||
return {
|
||||
ok: false,
|
||||
failure: {
|
||||
ok: false,
|
||||
code: 'SKATTEVERKET_API_ERROR',
|
||||
http_status: 502,
|
||||
error: bodyUnparseable
|
||||
? 'Skatteverket svarade med ett svar som inte kunde tolkas.'
|
||||
: `Skatteverket svarade med ${res.status}.`,
|
||||
},
|
||||
}
|
||||
}
|
||||
return { ok: true, body }
|
||||
}
|
||||
|
||||
let submitted: unknown = null
|
||||
let decided: unknown = null
|
||||
if (state === 'submitted' || state === 'both') {
|
||||
const result = await fetchView('inlamnat')
|
||||
if (!result.ok) return result.failure
|
||||
submitted = result.body
|
||||
}
|
||||
if (state === 'decided' || state === 'both') {
|
||||
const result = await fetchView('beslutat')
|
||||
if (!result.ok) return result.failure
|
||||
decided = result.body
|
||||
}
|
||||
|
||||
return { ok: true, redovisare, redovisningsperiod, submitted, decided }
|
||||
} catch (err) {
|
||||
if (err instanceof SkatteverketAuthError) {
|
||||
const mapped = skvAuthCodeToStructured(err.code)
|
||||
return { ok: false, code: mapped.code, http_status: mapped.httpStatus, error: err.message }
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user