* feat: implement salary module with personnummer encryption, salary entries, tax tables, and AGI tracking - Added personnummer encryption and decryption functions for secure storage. - Created salary entries handling for journal entries including gross salary, tax withholding, and employer contributions. - Implemented tax table lookup functionality for calculating tax amounts based on monthly income. - Developed SQL migration for salary module including tables for payroll configuration, tax rates, employees, salary runs, salary run employees, salary line items, and AGI declarations. - Established row-level security policies for all new tables to ensure company-scoped access. * feat: add salary calculation modules for 2026 - Implemented engångsskatt calculation for one-time payments with tax brackets. - Added löneväxling functionality for salary sacrifice to pension, including employer savings and warnings. - Created pain.001 generator for salary batch payments in compliance with Swedish banking standards. - Developed PDF template for payslips, including detailed breakdowns and employer costs. - Generated seed data for Swedish tax tables for 2026, including SQL insert statements. - Implemented traktamente calculations for per diem and mileage allowances, adhering to Skatteverket regulations. - Added seed script for populating tax tables in the database. * feat: Update meal reduction percentages in traktamente calculation fix: Remove obsolete seed script for 2026 tax tables feat: Extend SalaryRunStatus type to include 'corrected' status feat: Implement KU10 XML generation endpoint for annual employee income statements feat: Add endpoint for creating corrections to booked salary runs feat: Implement endpoint for sending payslip PDFs to employees feat: Create KU10 XML generator for annual reporting feat: Add salary transaction matcher for auto-linking bank transactions to salary entries chore: Add database migration for salary correction support * feat: replace select elements with custom Select component for employment and salary types * feat: enhance salary calculations with pension entry and avgifter category support
153 lines
4.9 KiB
TypeScript
153 lines
4.9 KiB
TypeScript
import type { PayrollConfig } from './payroll-config'
|
||
|
||
/**
|
||
* Swedish benefit value calculations (förmånsbeskattning).
|
||
* Implements Skatteverket's rules for taxable benefits.
|
||
*/
|
||
|
||
export interface BenefitStep {
|
||
label: string
|
||
formula: string
|
||
input: Record<string, number | string>
|
||
output: number
|
||
}
|
||
|
||
// ============================================================
|
||
// Car Benefit (Bilförmån) — Generation 3 (≥July 1, 2021)
|
||
// ============================================================
|
||
|
||
export interface CarBenefitParams {
|
||
nybilspris: number // New car price including options
|
||
fordonsskatt: number // Annual vehicle tax
|
||
isEnvironmental: boolean // Elbil/laddhybrid/gasbil
|
||
environmentalType?: 'electric' | 'plugin_hybrid' | 'gas'
|
||
highMileage: boolean // ≥30,000 km/year (25% reduction)
|
||
}
|
||
|
||
/**
|
||
* Calculate monthly car benefit value (förmånsvärde bilförmån).
|
||
*
|
||
* Gen3 formula (≥July 2021):
|
||
* annual = 0.29 × PBB + nybilspris × (0.70 × SLR + 0.01) + 0.13 × nybilspris + fordonsskatt
|
||
*
|
||
* Environmental reductions on nybilspris:
|
||
* - Elbil/vätgas: -350,000 (max 50% of nybilspris)
|
||
* - Laddhybrid: -140,000 (max 50%)
|
||
* - Gasbil: -100,000 (max 50%)
|
||
*
|
||
* High mileage (≥30,000 km/year): 25% reduction on total
|
||
*/
|
||
export function calculateCarBenefit(
|
||
params: CarBenefitParams,
|
||
config: PayrollConfig
|
||
): { monthlyValue: number; annualValue: number; steps: BenefitStep[] } {
|
||
const r = (x: number) => Math.round(x * 100) / 100
|
||
const steps: BenefitStep[] = []
|
||
|
||
let adjustedPrice = params.nybilspris
|
||
|
||
// Environmental reduction
|
||
if (params.isEnvironmental && params.environmentalType) {
|
||
const reductions: Record<string, number> = {
|
||
electric: 350000,
|
||
plugin_hybrid: 140000,
|
||
gas: 100000,
|
||
}
|
||
const maxReduction = Math.min(reductions[params.environmentalType] || 0, params.nybilspris * 0.5)
|
||
adjustedPrice = params.nybilspris - maxReduction
|
||
steps.push({
|
||
label: `Miljöbilsreduktion (${params.environmentalType})`,
|
||
formula: 'nybilspris - reduction (max 50%)',
|
||
input: { nybilspris: params.nybilspris, reduction: maxReduction },
|
||
output: adjustedPrice,
|
||
})
|
||
}
|
||
|
||
const pbbComponent = r(0.29 * config.prisbasbelopp)
|
||
const rateComponent = r(adjustedPrice * (0.70 * config.bilformanSlr + 0.01))
|
||
const percentComponent = r(0.13 * adjustedPrice)
|
||
const annualValue = r(pbbComponent + rateComponent + percentComponent + params.fordonsskatt)
|
||
|
||
steps.push({
|
||
label: 'Bilförmån (Gen3)',
|
||
formula: '0.29×PBB + pris×(0.70×SLR+0.01) + 0.13×pris + fordonsskatt',
|
||
input: {
|
||
pbb: config.prisbasbelopp,
|
||
slr: config.bilformanSlr,
|
||
adjusted_price: adjustedPrice,
|
||
fordonsskatt: params.fordonsskatt,
|
||
},
|
||
output: annualValue,
|
||
})
|
||
|
||
let finalAnnual = annualValue
|
||
if (params.highMileage) {
|
||
finalAnnual = r(annualValue * 0.75)
|
||
steps.push({
|
||
label: 'Milreduktion (≥30 000 km/år)',
|
||
formula: 'annual × 75%',
|
||
input: { annual: annualValue },
|
||
output: finalAnnual,
|
||
})
|
||
}
|
||
|
||
const monthlyValue = r(finalAnnual / 12)
|
||
steps.push({
|
||
label: 'Månatligt förmånsvärde',
|
||
formula: 'annual / 12',
|
||
input: { annual: finalAnnual },
|
||
output: monthlyValue,
|
||
})
|
||
|
||
return { monthlyValue, annualValue: finalAnnual, steps }
|
||
}
|
||
|
||
// ============================================================
|
||
// Meal Benefit (Kostförmån)
|
||
// ============================================================
|
||
|
||
export type MealType = 'full_day' | 'lunch' | 'breakfast'
|
||
|
||
/**
|
||
* Get meal benefit value (schablonvärde).
|
||
* If employee pays ≥ schablonvärde via nettolöneavdrag, benefit is eliminated.
|
||
*/
|
||
export function getMealBenefitValue(mealType: MealType, config: PayrollConfig): number {
|
||
switch (mealType) {
|
||
case 'full_day': return config.kostformanHeldag
|
||
case 'lunch': return config.kostformanLunch
|
||
case 'breakfast': return config.kostformanFrukost
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// Wellness Benefit (Friskvårdsbidrag)
|
||
// ============================================================
|
||
|
||
/**
|
||
* Check wellness benefit tax status.
|
||
* Tax-free if total ≤ cap (5,000 SEK/year).
|
||
* If exceeded, ENTIRE amount becomes taxable (not just excess).
|
||
*/
|
||
export function calculateWellnessBenefit(
|
||
amount: number,
|
||
ytdWellness: number,
|
||
config: PayrollConfig
|
||
): { taxable: boolean; taxableAmount: number; steps: BenefitStep[] } {
|
||
const totalYtd = ytdWellness + amount
|
||
const taxable = totalYtd > config.friskvardCap
|
||
|
||
return {
|
||
taxable,
|
||
taxableAmount: taxable ? totalYtd : 0, // Entire amount if exceeded
|
||
steps: [{
|
||
label: 'Friskvårdsbidrag',
|
||
formula: taxable
|
||
? `YTD ${totalYtd} > ${config.friskvardCap} — hela beloppet skattepliktigt`
|
||
: `YTD ${totalYtd} ≤ ${config.friskvardCap} — skattefritt`,
|
||
input: { amount, ytd: ytdWellness, cap: config.friskvardCap },
|
||
output: taxable ? totalYtd : 0,
|
||
}],
|
||
}
|
||
}
|