Feat/skv integration full (#284)

* feat: add script to import Skatteverket monthly tax tables as fallback TypeScript module

- Implemented a new script `import-tax-tables.ts` to parse fixed-width TXT tax tables from Skatteverket (SKV 434).
- The script generates a TypeScript module for emergency fallback when the Skatteverket open-data API is unavailable.
- Supports command-line argument for specifying the year and handles parsing of B-rows only.
- Outputs a structured TypeScript file containing tax data for specified years.

* feat: gate salary module behind dev-only flag

Temporarily disable the Lön module in production while the feature is
being completed. Sidebar entries ("Löner", "Anställda") still render but
are not clickable and show a "Kommer snart" badge. Middleware redirects
/salary* to / and returns 404 on /api/salary/* so the feature can't be
reached by direct URL. All gates check NODE_ENV === 'development' so
local dev keeps full access for continued development.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: refactor bank file import wizard to streamline column mapping and enhance CSV handling

* fix: bump migration timestamp to avoid collision with logos_bucket

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: enhance AGI generation and salary entry calculations with improved status checks and error handling

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-04-20 21:03:23 +02:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 7a0214c053
commit 11621bb79f
23 changed files with 16227 additions and 340 deletions
@@ -26,6 +26,7 @@ function makeTotals(overrides: Partial<AGITotals> = {}): AGITotals {
return {
totalTax: 8000,
totalAvgifterBasis: 35000,
totalAvgifterAmount: 10997,
avgifterByCategory: {
standard: { basis: 35000, amount: 10997 },
},
@@ -171,6 +172,7 @@ describe('buildAGIPayload', () => {
const totals: AGITotals = {
totalTax: 0,
totalAvgifterBasis: 0,
totalAvgifterAmount: 0,
avgifterByCategory: {},
}
const result = buildAGIPayload([makeEmployee({ grossSalary: 0, taxWithheld: 0, avgifterBasis: 0 })], totals)
+33 -3
View File
@@ -96,6 +96,7 @@ export const skatteverketExtension: Extension = {
// Exchange code FIRST — 5-minute expiry, do this before anything else
const { createClient } = await import('@/lib/supabase/server')
const { requireCompanyId } = await import('@/lib/company/context')
const supabase = await createClient()
// Verify user session (browser should still have cookies)
@@ -106,11 +107,22 @@ export const skatteverketExtension: Extension = {
)
}
// Resolve the active company — state/redirect_uri were stored keyed on company_id
// by ctx.settings.set() in the /authorize handler.
let companyId: string
try {
companyId = await requireCompanyId(supabase, user.id)
} catch {
return NextResponse.redirect(
`${appUrl}/reports?tab=vat-declaration&skv_error=${encodeURIComponent('Inget företag valt')}`
)
}
// Validate CSRF state
const { data: settingsData } = await supabase
.from('extension_data')
.select('value')
.eq('company_id', user.id)
.eq('company_id', companyId)
.eq('extension_id', 'skatteverket')
.eq('key', 'oauth_state')
.single()
@@ -125,7 +137,7 @@ export const skatteverketExtension: Extension = {
const { data: redirectData } = await supabase
.from('extension_data')
.select('value')
.eq('company_id', user.id)
.eq('company_id', companyId)
.eq('extension_id', 'skatteverket')
.eq('key', 'oauth_redirect_uri')
.single()
@@ -141,7 +153,7 @@ export const skatteverketExtension: Extension = {
await supabase
.from('extension_data')
.delete()
.eq('company_id', user.id)
.eq('company_id', companyId)
.eq('extension_id', 'skatteverket')
.eq('key', 'oauth_state')
@@ -1082,9 +1094,27 @@ async function parseAGIRequest(
;(avgifterByCategory as Record<string, { basis: number; amount: number }>)[category] = cat
}
const totalAvgifterAmount = Object.values(avgifterByCategory).reduce(
(sum, cat) => sum + (cat?.amount ?? 0),
0
)
// FK499 — sjuklönekostnad summed from sick_day2_14 line items
let totalSjuklonekostnad = 0
for (const sre of runEmployees) {
const lineItems = (sre.line_items || []) as Array<Record<string, unknown>>
for (const li of lineItems) {
if (li.item_type === 'sick_day2_14') {
totalSjuklonekostnad += Math.abs((li.amount as number) || 0)
}
}
}
const totals: AGITotals = {
totalTax: run.total_tax,
totalAvgifterBasis: runEmployees.reduce((s: number, e: { avgifter_basis: number }) => s + e.avgifter_basis, 0),
totalAvgifterAmount: Math.round(totalAvgifterAmount * 100) / 100,
totalSjuklonekostnad: Math.round(totalSjuklonekostnad * 100) / 100,
avgifterByCategory,
}