Files
accounted/lib/tax/schablonavdrag.ts
T

200 lines
6.2 KiB
TypeScript

/**
* Schablonavdrag (Standard Deductions) Calculator
* Swedish tax deductions that don't require detailed receipts
*/
import type { SchablonavdragSettings, SchablonavdragSummary, MileageEntry } from '@/types'
// Housing types for hemmakontor deduction
export type HousingType = 'villa' | 'apartment' // villa = privatbostadsfastighet, apartment = hyresrätt/bostadsrätt
// Current Swedish schablonavdrag rates (2026)
export const SCHABLONAVDRAG_RATES = {
hemmakontor: {
// Different rates based on housing type
villa: 2000, // 2,000 kr/year for villa/privatbostadsfastighet
apartment: 4000, // 4,000 kr/year for hyresrätt/bostadsrätt
min_hours_per_year: 800, // Minimum 800 hours working from home required
description: 'Schablonavdrag för hemmakontor',
},
bil: {
rate_per_mil: 25, // 25 kr per Swedish mile (10 km)
rate_per_km: 2.5, // 2.50 kr per kilometer
description: 'Schablonavdrag för bilkostnader (milersättning)',
},
}
/**
* Calculate hemmakontor (home office) deduction
* Rate depends on housing type:
* - Villa/privatbostadsfastighet: 2,000 kr/year
* - Hyresrätt/Bostadsrätt: 4,000 kr/year
* Prorated by months active
*/
export function calculateHemmakontorDeduction(
monthsActive: number = 12,
housingType: HousingType = 'apartment'
): number {
const validMonths = Math.min(Math.max(monthsActive, 0), 12)
const annualAmount = housingType === 'villa'
? SCHABLONAVDRAG_RATES.hemmakontor.villa
: SCHABLONAVDRAG_RATES.hemmakontor.apartment
return Math.round((annualAmount / 12) * validMonths)
}
/**
* Calculate mileage deduction from total kilometers
* Rate: 25 kr/mil = 2.50 kr/km
*/
export function calculateMileageDeduction(totalKm: number): number {
if (totalKm <= 0) return 0
// 2.50 kr per kilometer (25 kr per Swedish mile)
return Math.round(totalKm * SCHABLONAVDRAG_RATES.bil.rate_per_km * 100) / 100
}
/**
* Calculate mileage deduction from entries
*/
export function calculateMileageDeductionFromEntries(entries: MileageEntry[]): number {
const totalKm = entries.reduce((sum, entry) => sum + Number(entry.distance_km), 0)
return calculateMileageDeduction(totalKm)
}
/**
* Get complete schablonavdrag summary for a user and year
*/
export function getSchablonavdragSummary(
settings: SchablonavdragSettings | null,
mileageEntries: MileageEntry[],
year: number = new Date().getFullYear(),
monthsInBusiness: number = 12
): SchablonavdragSummary {
// Default settings if none provided
const schablonavdragSettings: SchablonavdragSettings = settings || {
hemmakontor_enabled: false,
hemmakontor_housing_type: 'apartment',
bil_enabled: false,
}
// Get housing type (default to apartment for higher deduction)
const housingType: HousingType = schablonavdragSettings.hemmakontor_housing_type || 'apartment'
// Calculate hemmakontor deduction based on housing type
const hemmakontorDeduction = schablonavdragSettings.hemmakontor_enabled
? calculateHemmakontorDeduction(monthsInBusiness, housingType)
: 0
// Get the annual amount for this housing type
const hemmakontorAnnualAmount = housingType === 'villa'
? SCHABLONAVDRAG_RATES.hemmakontor.villa
: SCHABLONAVDRAG_RATES.hemmakontor.apartment
// Filter entries for the specified year
const yearEntries = mileageEntries.filter((entry) => {
const entryYear = new Date(entry.date).getFullYear()
return entryYear === year
})
// Calculate mileage totals
const totalKm = yearEntries.reduce((sum, entry) => sum + Number(entry.distance_km), 0)
const mileageDeduction = schablonavdragSettings.bil_enabled
? calculateMileageDeduction(totalKm)
: 0
return {
year,
hemmakontor: {
enabled: schablonavdragSettings.hemmakontor_enabled,
housing_type: housingType,
months_active: monthsInBusiness,
annual_amount: hemmakontorAnnualAmount,
deduction: hemmakontorDeduction,
},
mileage: {
enabled: schablonavdragSettings.bil_enabled,
total_km: totalKm,
rate_per_km: SCHABLONAVDRAG_RATES.bil.rate_per_km,
total_deduction: mileageDeduction,
entries_count: yearEntries.length,
},
total_deduction: hemmakontorDeduction + mileageDeduction,
}
}
/**
* Validate a mileage entry
*/
export function validateMileageEntry(entry: {
date: string
distance_km: number
purpose: string
}): { valid: boolean; errors: string[] } {
const errors: string[] = []
// Check date
if (!entry.date) {
errors.push('Datum krävs')
} else {
const entryDate = new Date(entry.date)
if (isNaN(entryDate.getTime())) {
errors.push('Ogiltigt datum')
} else if (entryDate > new Date()) {
errors.push('Datum kan inte vara i framtiden')
}
}
// Check distance
if (!entry.distance_km || entry.distance_km <= 0) {
errors.push('Avståndet måste vara större än 0 km')
} else if (entry.distance_km > 10000) {
errors.push('Orimligt avstånd (max 10 000 km per resa)')
}
// Check purpose
if (!entry.purpose || entry.purpose.trim().length < 3) {
errors.push('Ändamål krävs (minst 3 tecken)')
}
return {
valid: errors.length === 0,
errors,
}
}
/**
* Get mileage entries grouped by month
*/
export function groupMileageEntriesByMonth(
entries: MileageEntry[]
): Map<string, { entries: MileageEntry[]; totalKm: number; totalDeduction: number }> {
const grouped = new Map<string, { entries: MileageEntry[]; totalKm: number; totalDeduction: number }>()
for (const entry of entries) {
const date = new Date(entry.date)
const key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`
if (!grouped.has(key)) {
grouped.set(key, { entries: [], totalKm: 0, totalDeduction: 0 })
}
const group = grouped.get(key)!
group.entries.push(entry)
group.totalKm += Number(entry.distance_km)
group.totalDeduction += Number(entry.total_deduction)
}
return grouped
}
/**
* Format month key for display (Swedish)
*/
export function formatMonthKey(key: string): string {
const [year, month] = key.split('-')
const monthNames = [
'Januari', 'Februari', 'Mars', 'April', 'Maj', 'Juni',
'Juli', 'Augusti', 'September', 'Oktober', 'November', 'December'
]
return `${monthNames[parseInt(month) - 1]} ${year}`
}