Add/skv salary agi (#423)

* feat: add Bankgirot LB-fil support for salary payments and tax payments

- Implemented `generateBgLb` for salary batch payments, producing opening, payment, and closing records.
- Added tests for `generateBgLb` to ensure correct record generation and validation.
- Created `generateBankgiroPaymentBgLb` for single tax payments to Skatteverket, including validation and formatting.
- Added tests for `generateBankgiroPaymentBgLb` to verify record structure and data integrity.
- Introduced `generateSkattekontoOcr` for generating valid OCR references for Skattekonto payments, with tests for various input formats.
- Updated database schema to track payment file formats and timestamps for salary runs and AGI declarations.
- Created a new table for logging salary payslip deliveries to ensure compliance with audit requirements.

* feat: add write permission check and company ID validation for payment file generation

* feat: add write permission check for salary payment file generation
This commit is contained in:
Mattsson
2026-05-09 12:41:16 +02:00
committed by GitHub
parent c238542596
commit 7e81f661b2
24 changed files with 1870 additions and 20 deletions
+46 -8
View File
@@ -4,7 +4,7 @@ import { NextResponse } from 'next/server'
import { TimeoutError } from '@/lib/http/fetch-with-timeout'
import { buildAuthorizeUrl, exchangeCodeForTokens } from './lib/oauth'
import { storeTokens, getTokens, deleteTokens } from './lib/token-store'
import { skvRequest, SkatteverketAuthError } from './lib/api-client'
import { skvRequest, SkatteverketAuthError, getSkatteverketEnvironment } from './lib/api-client'
import { rutorToMomsuppgift, formatRedovisare, formatRedovisningsperiod } from './lib/mappers'
import { calculateVatDeclaration } from '@/lib/reports/vat-declaration'
import {
@@ -26,20 +26,53 @@ import type { VatPeriodType } from '@/types'
/**
* Skatteverket integration extension.
*
* Enables filing momsdeklaration (VAT declaration) directly to Skatteverket
* via their Momsdeklaration API 1.0. Users authenticate with BankID through
* the `per` (e-legitimation) OAuth2 flow.
* Enables filing momsdeklaration (VAT declaration) and arbetsgivardeklaration
* (AGI), plus Skattekonto saldo sync. Users authenticate with BankID via the
* `per` (e-legitimation) OAuth2 flow.
*
* Required environment variables:
* - SKATTEVERKET_OAUTH2_CLIENT_ID
* - SKATTEVERKET_OAUTH2_CLIENT_SECRET
* - SKATTEVERKET_APIGW_CLIENT_ID
* - SKATTEVERKET_APIGW_CLIENT_SECRET
* - SKATTEVERKET_TOKEN_ENCRYPTION_KEY
* - SKATTEVERKET_TOKEN_ENCRYPTION_KEY (openssl rand -base64 32; never reuse
* the test-env key in prod)
*
* Optional:
* - SKATTEVERKET_OAUTH_BASE_URL (defaults to test environment)
* - SKATTEVERKET_API_BASE_URL (defaults to test environment)
* - SKATTEVERKET_OAUTH_BASE_URL — defaults to test
* - SKATTEVERKET_API_BASE_URL — momsdeklaration; defaults to test
* - SKATTEVERKET_AGD_INLAMNING_API_BASE_URL — AGI inlämning; defaults to test
* - SKATTEVERKET_AGD_PERIOD_API_BASE_URL — AGI period mgmt; defaults to test
* - SKATTEVERKET_SKATTEKONTO_API_BASE_URL — Skattekonto; defaults to test
* - SKATTEVERKET_DISABLED=true — emergency kill switch
*
* ─── Production cutover checklist ─────────────────────────────────────────
* Before flipping the env URLs to prod, the following has to land first
* (most are external blockers):
*
* 1. Register a prod OAuth2 client in Skatteverket's developer portal
* (separate from the test client). Requires a signed integrationsavtal.
* 2. Order APIGW prod credentials (separate ärende).
* 3. Register the prod redirect URI:
* `${NEXT_PUBLIC_APP_URL}/api/extensions/ext/skatteverket/callback`.
* 4. Request scopes: agd:skicka, agd:lasa, skattekonto:lasa, moms:skicka.
* 5. Pass Skatteverket's godkännandetest (they validate a few real AGI
* submissions in their test tenant before granting prod access).
* 6. Generate a fresh SKATTEVERKET_TOKEN_ENCRYPTION_KEY (rotate from test).
* 7. Set the prod base URLs:
* SKATTEVERKET_API_BASE_URL=https://api.skatteverket.se/momsdeklaration/v1
* SKATTEVERKET_AGD_INLAMNING_API_BASE_URL=https://api.skatteverket.se/arbetsgivardeklaration/inlamning/v1
* SKATTEVERKET_AGD_PERIOD_API_BASE_URL=https://api.skatteverket.se/arbetsgivardeklaration/hanteraredovisningsperiod/v1
* SKATTEVERKET_SKATTEKONTO_API_BASE_URL=https://api.skatteverket.se/beskattning/skattekonto/v2
* SKATTEVERKET_OAUTH_BASE_URL=https://oauth2.skatteverket.se/oauth2
* 8. Verify Sentry alerts on /api/extensions/ext/skatteverket/* 5xx.
* 9. Verify 7-year retention of `agi_declarations.xml_content` +
* `kvittensnummer` (BFL 7 kap.).
* 10. Run a single AGI end-to-end against test on a real client before
* switching that client over.
*
* The /status endpoint reports which environment is active so the UI can
* surface a Testmiljö / Produktion badge.
*/
export const skatteverketExtension: Extension = {
id: 'skatteverket',
@@ -227,8 +260,11 @@ export const skatteverketExtension: Extension = {
}
const tokens = await getTokens(ctx.supabase, ctx.userId)
const environment = getSkatteverketEnvironment()
const disabled = (process.env.SKATTEVERKET_DISABLED ?? '').toLowerCase() === 'true'
if (!tokens) {
return NextResponse.json({ connected: false })
return NextResponse.json({ connected: false, environment, disabled })
}
const expired = tokens.expires_at < Date.now()
@@ -240,6 +276,8 @@ export const skatteverketExtension: Extension = {
canRefresh,
scope: tokens.scope,
expiresAt: new Date(tokens.expires_at).toISOString(),
environment,
disabled,
})
},
},
@@ -38,6 +38,31 @@ function getApiGwClientSecret(): string {
return secret
}
/**
* Kill switch: when SKATTEVERKET_DISABLED=true, all SKV API calls fail with a
* single, clear Swedish error. Useful during incidents (provider outage, key
* rotation, suspended access) to surface a graceful failure mode instead of
* letting requests hang or leak partial state.
*/
function isDisabled(): boolean {
const v = (process.env.SKATTEVERKET_DISABLED ?? '').toLowerCase()
return v === 'true' || v === '1' || v === 'yes'
}
/**
* Detect whether we're pointed at SKV's test or prod environment.
* Used by the UI to surface an obvious badge so the user knows whether their
* filings will hit Skatteverket's production system.
*/
export function getSkatteverketEnvironment(): 'test' | 'prod' {
const baseUrl =
process.env.SKATTEVERKET_API_BASE_URL ||
process.env.SKATTEVERKET_AGD_INLAMNING_API_BASE_URL ||
process.env.SKATTEVERKET_SKATTEKONTO_API_BASE_URL ||
DEFAULT_API_BASE_URL
return baseUrl.includes('api.test.skatteverket.se') ? 'test' : 'prod'
}
/**
* Ensure rate limit compliance (4 req/sec).
* Delays if the last request was too recent.
@@ -146,6 +171,12 @@ export async function skvRequest(
body?: unknown,
options?: { baseUrl?: string; contentType?: string }
): Promise<Response> {
if (isDisabled()) {
throw new SkatteverketAuthError(
'Skatteverket-integrationen är tillfälligt avstängd. Kontakta support.',
'ACCESS_DENIED'
)
}
const accessToken = await getValidToken(supabase, userId)
await enforceRateLimit()
@@ -16,7 +16,8 @@
"SKATTEVERKET_API_BASE_URL",
"SKATTEVERKET_AGD_INLAMNING_API_BASE_URL",
"SKATTEVERKET_AGD_PERIOD_API_BASE_URL",
"SKATTEVERKET_SKATTEKONTO_API_BASE_URL"
"SKATTEVERKET_SKATTEKONTO_API_BASE_URL",
"SKATTEVERKET_DISABLED"
],
"npmDependencies": [],
"definition": {