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:
co-authored by
Claude Opus 4.7
parent
7a0214c053
commit
11621bb79f
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user