* chore: repo-wide bloat sweep, remove dead code and fold duplicate helpers Remove 33 dead files, ~270 unreferenced exports/types, 13 dead i18n namespaces and 4 unused dependencies; fold byte-identical helper copies into one canonical home each (lib/utils chunk/sleep/utcDateStamp, lib/dates/iso, lib/invariants/uuid, lib/xml/escape, lib/reports/sru/format, lib/pdf/number-text, lib/browser/panel-request, lib/api/v1/body + v1ValidationError rolled out to ~55 v1 routes, booking-template schemas). No behaviour change: v1 bodies and status codes, MCP tool schemas, DB writes and money math are untouched. Naive ore rounding was deliberately not swapped for roundOre; see DECISIONS.md 2026-09-02 for the full list of things left alone on purpose. tsc, lint, 19588 unit tests and check:guards green; antipattern baseline ratcheted (naive-ore-round 622 -> 620, hand-rolled-invariant 115 -> 113). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(transactions): import RawTransaction from @/types after the ingest re-export removal CI's type ratchet (check:types, full tsconfig) caught the one test file that still imported the type through lib/transactions/ingest. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
216 lines
8.3 KiB
TypeScript
216 lines
8.3 KiB
TypeScript
import type { ProposedDisposition } from '../types'
|
||
import { roundOre } from '@/lib/money'
|
||
|
||
/** 30-rule (huvudregel, IL 18 kap 13 §): restvärde minst 70 % av ingående
|
||
* bokfört värde plus årets anskaffningar minus årets försäljningar. */
|
||
export const OVERAVSKRIVNING_30_RULE = 0.7
|
||
|
||
export interface Compute30RuleInput {
|
||
/** IB skattemässigt värde maskiner & inventarier. */
|
||
openingBookValue: number
|
||
/** Årets anskaffningar (debet på anskaffningskonto, t.ex. 1220). */
|
||
additions: number
|
||
/** Försäljningsvärde och utrangering av tillgångar (kredit på anskaffningskonto). */
|
||
disposals: number
|
||
/** Räkenskapsperiodens längd. 30 % proportioneras för perioder som inte är 12 månader. */
|
||
fiscalPeriodMonths?: number
|
||
}
|
||
|
||
/** 20-rule (kompletteringsregel, IL 18 kap 17 §): restvärde minst 0 % efter
|
||
* 5 år (20 % avskrivning per år, raklinje), hence the five-slot input. */
|
||
export interface Compute20RuleInput {
|
||
/** Anskaffningskostnad per anskaffningsår, från innevarande år minus 4 till
|
||
* innevarande år. Index 0 = innevarande år. */
|
||
acquisitionCostByYearOffset: [number, number, number, number, number]
|
||
}
|
||
|
||
/**
|
||
* 30-regeln: skattemässigt lägsta restvärde = 70 % × (IB + årets anskaffningar − årets avyttringar).
|
||
* Maximalt skattemässigt avskrivningsbart belopp = avskrivningsunderlag − restvärde.
|
||
*/
|
||
export function compute30Rule(input: Compute30RuleInput): {
|
||
base: number
|
||
minimumResidual: number
|
||
maxAllowedAccumulated: number
|
||
} {
|
||
const base = Math.max(0, input.openingBookValue + input.additions - input.disposals)
|
||
const periodMonths = input.fiscalPeriodMonths ?? 12
|
||
const depreciationRate = Math.min(1, 0.3 * (periodMonths / 12))
|
||
const minimumResidual = roundOre(base * (1 - depreciationRate))
|
||
return {
|
||
base,
|
||
minimumResidual,
|
||
maxAllowedAccumulated: roundOre(base - minimumResidual),
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 20-regeln: varje årsanskaffning får skrivas av med 20 % under 5 år. Lägsta
|
||
* skattemässigt restvärde är anskaffningskostnaden minus 20 % per tolvmånadersperiod,
|
||
* inklusive anskaffningsperioden.
|
||
*
|
||
* Returns the allowed depreciation if 20-rule is used as the sole basis,
|
||
* computed against ALL still-active 20-rule cohorts.
|
||
*/
|
||
export function compute20Rule(input: Compute20RuleInput): {
|
||
minimumResidual: number
|
||
} {
|
||
return compute20RuleForFiscalPeriods({
|
||
acquisitionCostByPeriod: input.acquisitionCostByYearOffset,
|
||
fiscalPeriodMonths: [12, 12, 12, 12, 12],
|
||
})
|
||
}
|
||
|
||
export interface Compute20RuleForFiscalPeriodsInput {
|
||
/** Anskaffningskostnad per period. Index 0 är innevarande period. */
|
||
acquisitionCostByPeriod: number[]
|
||
/** Periodlängd per motsvarande period, nyaste först. */
|
||
fiscalPeriodMonths: number[]
|
||
}
|
||
|
||
/**
|
||
* Kompletteringsregeln writes off 20 % per twelve fiscal months, including
|
||
* the acquisition period. Supplying actual period lengths handles shortened
|
||
* and extended fiscal years without shifting the acquisition cohort by a year.
|
||
*/
|
||
export function compute20RuleForFiscalPeriods(
|
||
input: Compute20RuleForFiscalPeriodsInput,
|
||
): { minimumResidual: number } {
|
||
let residual = 0
|
||
for (let offset = 0; offset < input.acquisitionCostByPeriod.length; offset++) {
|
||
const cost = input.acquisitionCostByPeriod[offset] ?? 0
|
||
const elapsedMonths = input.fiscalPeriodMonths
|
||
.slice(0, offset + 1)
|
||
.reduce((sum, months) => sum + months, 0)
|
||
const depreciatedFraction = Math.min(1, elapsedMonths / 60)
|
||
residual += cost * (1 - depreciatedFraction)
|
||
}
|
||
return { minimumResidual: roundOre(residual) }
|
||
}
|
||
|
||
/**
|
||
* Pick the rule that gives the lowest restvärde (highest allowed deduction)
|
||
* per IL 18 kap 13-17 §§: företaget får välja den fördelaktigaste regeln
|
||
* varje år.
|
||
*/
|
||
export function pickLowerResidual(
|
||
rule30: ReturnType<typeof compute30Rule>,
|
||
rule20: ReturnType<typeof compute20Rule>,
|
||
): { residual: number; rule: '30-regeln' | '20-regeln' } {
|
||
if (rule20.minimumResidual < rule30.minimumResidual) {
|
||
return { residual: rule20.minimumResidual, rule: '20-regeln' }
|
||
}
|
||
return { residual: rule30.minimumResidual, rule: '30-regeln' }
|
||
}
|
||
|
||
/**
|
||
* BAS account pairs for överavskrivningar by asset category. The 88xx
|
||
* "förändring" account always pairs with its matching 21xx "ackumulerade"
|
||
* account so the verifikation stays balanced and flows into the right INK2R
|
||
* field via the SRU mapping.
|
||
*
|
||
* - 8853 / 2153: maskiner & inventarier (IL 18 kap, dominant K2 case)
|
||
* - 8852 / 2152: byggnader (IL 19 kap, rare in SME)
|
||
* - 8851 / 2151: immateriella tillgångar (IL 16 kap, even rarer)
|
||
* - 8850 / 2150: samlingskonto för grupp
|
||
*/
|
||
export const OVERAVSKRIVNING_ACCOUNTS = {
|
||
machinery_equipment: { expense: '8853', accumulated: '2153' },
|
||
building: { expense: '8852', accumulated: '2152' },
|
||
immaterial: { expense: '8851', accumulated: '2151' },
|
||
group: { expense: '8850', accumulated: '2150' },
|
||
} as const
|
||
|
||
export type OveravskrivningCategory = keyof typeof OVERAVSKRIVNING_ACCOUNTS
|
||
|
||
export interface OveravskrivningarInput {
|
||
/** Föreslagen ökning av ackumulerade överavskrivningar. Positivt belopp
|
||
* ökar ackumulerade-kontot (debet 88xx), negativt minskar (kredit 88xx). */
|
||
additionalAmount: number
|
||
/** Account pair to use. Defaults to maskiner & inventarier (8853 / 2153),
|
||
* the only category where överavskrivningar is common in K2 SME. Override
|
||
* for buildings or immateriella tillgångar when relevant. */
|
||
category?: OveravskrivningCategory
|
||
/** Visa beräkningens bakgrund i UI:t. Helt fritt format. */
|
||
computation?: Record<string, unknown>
|
||
}
|
||
|
||
/**
|
||
* Propose an överavskrivningar entry. Caller computes the desired delta
|
||
* using compute30Rule / compute20Rule (or enters a manual amount) and the
|
||
* service emits the verifikation. Uses BAS 8853 / 2153 (maskiner & inventarier),
|
||
* which covers the vast majority of cases for small AB.
|
||
*
|
||
* For Phase 2 this is a thin wrapper. Phase 3 (anläggningsregister) will
|
||
* compute the delta automatically from per-asset planenlig vs skattemässig
|
||
* schedules and pre-fill `additionalAmount`.
|
||
*/
|
||
const CATEGORY_LABELS: Record<OveravskrivningCategory, string> = {
|
||
machinery_equipment: 'maskiner & inventarier',
|
||
building: 'byggnader',
|
||
immaterial: 'immateriella tillgångar',
|
||
group: 'samlingskonto',
|
||
}
|
||
|
||
export function proposeOveravskrivningar(input: OveravskrivningarInput): ProposedDisposition | null {
|
||
const amount = roundOre(input.additionalAmount)
|
||
if (amount === 0) return null
|
||
|
||
const category = input.category ?? 'machinery_equipment'
|
||
const accounts = OVERAVSKRIVNING_ACCOUNTS[category]
|
||
const categoryLabel = CATEGORY_LABELS[category]
|
||
|
||
if (amount > 0) {
|
||
return {
|
||
kind: 'overavskrivningar',
|
||
label: `Ökning av överavskrivningar (${categoryLabel})`,
|
||
description: `Debet ${accounts.expense}, kredit ${accounts.accumulated}. Bokför skattemässig avskrivning utöver planenlig.`,
|
||
amount,
|
||
signedAmount: amount,
|
||
lines: [
|
||
{
|
||
account_number: accounts.expense,
|
||
debit_amount: amount,
|
||
credit_amount: 0,
|
||
line_description: 'Förändring av överavskrivningar',
|
||
},
|
||
{
|
||
account_number: accounts.accumulated,
|
||
debit_amount: 0,
|
||
credit_amount: amount,
|
||
line_description: 'Ackumulerade överavskrivningar',
|
||
},
|
||
],
|
||
warnings: [],
|
||
computation: { ...input.computation, additionalAmount: amount },
|
||
}
|
||
}
|
||
|
||
// Negative = upplösning av tidigare överavskrivning
|
||
const absAmount = Math.abs(amount)
|
||
return {
|
||
kind: 'overavskrivningar',
|
||
label: `Upplösning av överavskrivningar (${categoryLabel})`,
|
||
description: `Debet ${accounts.accumulated}, kredit ${accounts.expense}. Återför tidigare gjord överavskrivning.`,
|
||
amount: absAmount,
|
||
signedAmount: -absAmount,
|
||
lines: [
|
||
{
|
||
account_number: accounts.accumulated,
|
||
debit_amount: absAmount,
|
||
credit_amount: 0,
|
||
line_description: 'Upplösning ackumulerade överavskrivningar',
|
||
},
|
||
{
|
||
account_number: accounts.expense,
|
||
debit_amount: 0,
|
||
credit_amount: absAmount,
|
||
line_description: 'Förändring av överavskrivningar',
|
||
},
|
||
],
|
||
warnings: ['Negativ förändring återför tidigare överavskrivning och ökar skattepliktigt resultat.'],
|
||
computation: { ...input.computation, additionalAmount: -absAmount },
|
||
required: true,
|
||
}
|
||
}
|