feat(bokslut): IL 18 kap pooled tax depreciation with method election (#1393)
* feat(bokslut): IL 18 kap pooled tax depreciation with method election Rakenskapsenlig (huvudregel 30 / kompletteringsregel 20) and restvarde 25 as a company-level annual pool separate from per-asset book depreciation. Method election persisted with immutable snapshots and book-conformity confirmation for rakenskapsenlig. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(db): move tax depreciation migrations to coordinated versions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bokslut): keep tax depreciation view loadable when a saved election goes stale A predecessor's changed closing value can push a saved elected deduction above the new statutory maximum; the view now falls back to the statutory recomputation so the snapshot is flagged stale instead of crashing. Ratchet naive-ore-round baseline down by 3. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bokslut): resolve tax-depreciation period selects statically The no-phantom-columns guard counts every select it cannot resolve toward a hard ceiling, and the PERIOD_COLUMNS join pushed the repo 4 over (364 > 360). Inline the literal column list at the four call sites so the guard verifies these columns instead of skipping them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bokslut): address review findings on tax depreciation election - DepreciationPanel: gate the saving flag on a dedicated save sequence so a successful save (which refreshes the view and bumps the request version) no longer leaves the card permanently busy - computeTaxDepreciation: refuse kompletteringsregel_20 with a positive basis and no acquisition cohorts instead of degenerating to a full write-off the cohort evidence does not support (IL 18 kap. 17 §) - migration 227000: judge the asset-method guards on NEW.disposed_at so reversing a disposal cannot reactivate a grandfathered non-linear row - migration 227200: require snapshot column completeness in the CHECK; SQL NULL semantics let partially populated snapshots pass the pure arithmetic comparisons - depreciation route: use the string issue code 'custom' like the rest of the codebase instead of the Zod 3 compat enum Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
00ae3540db
commit
5ca64bde30
+7
-54
@@ -6,7 +6,7 @@ import { validateBody } from '@/lib/api/validate'
|
||||
import { K3ComponentSchema } from '@/lib/api/schemas'
|
||||
import { createAsset, listAssets } from '@/lib/bokslut/assets/asset-service'
|
||||
import { validateComponents } from '@/lib/bokslut/assets/k3-components'
|
||||
import type { AssetCategory, DepreciationMethod } from '@/types'
|
||||
import type { AssetCategory, WritableDepreciationMethod } from '@/types'
|
||||
|
||||
const ASSET_CATEGORIES: readonly AssetCategory[] = [
|
||||
'immaterial',
|
||||
@@ -19,14 +19,8 @@ const ASSET_CATEGORIES: readonly AssetCategory[] = [
|
||||
'other_tangible',
|
||||
] as const
|
||||
|
||||
// All four depreciation methods are now implemented by the engine. The DB
|
||||
// CHECK constraint mirrors this list (see
|
||||
// 20260526120100_restvardeavskrivning.sql).
|
||||
const DEPRECIATION_METHODS: readonly DepreciationMethod[] = [
|
||||
const DEPRECIATION_METHODS: readonly WritableDepreciationMethod[] = [
|
||||
'linear',
|
||||
'declining_balance_30',
|
||||
'declining_balance_20',
|
||||
'restvardesavskrivning_25',
|
||||
] as const
|
||||
|
||||
const CreateAssetSchema = z
|
||||
@@ -40,13 +34,12 @@ const CreateAssetSchema = z
|
||||
salvage_value: z.number().nonnegative().optional(),
|
||||
useful_life_months: z.number().int().positive(),
|
||||
depreciation_method: z
|
||||
.enum(DEPRECIATION_METHODS as unknown as [DepreciationMethod, ...DepreciationMethod[]])
|
||||
.enum(DEPRECIATION_METHODS as unknown as [
|
||||
WritableDepreciationMethod,
|
||||
...WritableDepreciationMethod[],
|
||||
])
|
||||
.optional(),
|
||||
// Restvärde-target floor for restvärdeavskrivning. Required iff
|
||||
// depreciation_method = 'restvardesavskrivning_25'. The DB CHECK enforces
|
||||
// the same biconditional; we mirror it in the API for an early, Swedish
|
||||
// error message rather than a Postgres check_violation surfacing.
|
||||
restvarde_target: z.number().nonnegative().nullable().optional(),
|
||||
restvarde_target: z.null().optional(),
|
||||
bas_asset_account: z.string().regex(/^\d{4}$/).optional(),
|
||||
bas_accumulated_account: z.string().regex(/^\d{4}$/).optional(),
|
||||
bas_expense_account: z.string().regex(/^\d{4}$/).optional(),
|
||||
@@ -63,7 +56,6 @@ const CreateAssetSchema = z
|
||||
// outside the legitimate range for the asset category so the chart stays
|
||||
// BAS-aligned and INK2R mappings continue to work.
|
||||
validateBasOverrides(value, ctx)
|
||||
validateRestvardeTarget(value, ctx)
|
||||
validateK3Components(value, ctx)
|
||||
})
|
||||
|
||||
@@ -88,45 +80,6 @@ function validateK3Components(
|
||||
}
|
||||
}
|
||||
|
||||
function validateRestvardeTarget(
|
||||
value: {
|
||||
depreciation_method?: DepreciationMethod
|
||||
restvarde_target?: number | null
|
||||
acquisition_cost?: number
|
||||
},
|
||||
ctx: z.RefinementCtx,
|
||||
): void {
|
||||
const isRestvarde = value.depreciation_method === 'restvardesavskrivning_25'
|
||||
const hasTarget = value.restvarde_target !== undefined && value.restvarde_target !== null
|
||||
if (isRestvarde && !hasTarget) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['restvarde_target'],
|
||||
message: 'restvarde_target krävs när avskrivningsmetoden är restvärdeavskrivning (25 %).',
|
||||
})
|
||||
}
|
||||
if (!isRestvarde && hasTarget) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['restvarde_target'],
|
||||
message: 'restvarde_target får bara anges för restvärdeavskrivning (25 %).',
|
||||
})
|
||||
}
|
||||
if (
|
||||
isRestvarde &&
|
||||
hasTarget &&
|
||||
value.acquisition_cost !== undefined &&
|
||||
(value.restvarde_target as number) >= value.acquisition_cost
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['restvarde_target'],
|
||||
message:
|
||||
'restvarde_target måste vara lägre än anskaffningsvärdet: annars finns inget kvar att skriva av.',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function validateBasOverrides(
|
||||
value: {
|
||||
category: AssetCategory
|
||||
|
||||
Reference in New Issue
Block a user