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:
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user