Files
accounted/supabase/migrations/20260803227200_enforce_tax_depreciation_snapshot_integrity.sql
T
MattssonandClaude Fable 5 5ca64bde30 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>
2026-08-04 11:43:58 +02:00

137 lines
5.0 KiB
PL/PgSQL

-- Issue #324 follow-up: make annual tax-depreciation snapshots coherent and
-- preserve their fiscal-period chain even for direct RLS-authorized writes.
ALTER TABLE public.fiscal_periods
ADD CONSTRAINT fiscal_periods_tax_depreciation_arithmetic_check
CHECK (
tax_depreciation_method IS NULL
OR (
-- Completeness first: SQL NULL semantics would let a partially
-- populated snapshot slip past the arithmetic comparisons below
-- (NULL operands make the whole expression NULL, which passes CHECK).
tax_depreciation_opening_value IS NOT NULL
AND tax_depreciation_base IS NOT NULL
AND tax_depreciation_deduction IS NOT NULL
AND tax_depreciation_closing_value IS NOT NULL
AND tax_depreciation_calculation IS NOT NULL
AND (
tax_depreciation_method <> 'rakenskapsenlig'
OR tax_depreciation_rule IS NOT NULL
)
AND tax_depreciation_deduction <= tax_depreciation_base
AND tax_depreciation_closing_value
= tax_depreciation_base - tax_depreciation_deduction
AND (tax_depreciation_calculation ->> 'version')::integer >= 2
AND (tax_depreciation_calculation ->> 'elected_deduction')::numeric
= tax_depreciation_deduction
AND (tax_depreciation_calculation ->> 'maximum_deduction')::numeric
>= tax_depreciation_deduction
AND (
tax_depreciation_method <> 'rakenskapsenlig'
OR tax_depreciation_calculation ->> 'book_conformity_confirmed' = 'true'
)
)
);
CREATE OR REPLACE FUNCTION public.guard_fiscal_period_tax_depreciation()
RETURNS TRIGGER
LANGUAGE plpgsql
SET search_path = ''
AS $$
DECLARE
previous_period public.fiscal_periods%ROWTYPE;
tax_snapshot_changed boolean;
BEGIN
tax_snapshot_changed := (
NEW.tax_depreciation_method IS DISTINCT FROM OLD.tax_depreciation_method
OR NEW.tax_depreciation_rule IS DISTINCT FROM OLD.tax_depreciation_rule
OR NEW.tax_depreciation_opening_value IS DISTINCT FROM OLD.tax_depreciation_opening_value
OR NEW.tax_depreciation_base IS DISTINCT FROM OLD.tax_depreciation_base
OR NEW.tax_depreciation_deduction IS DISTINCT FROM OLD.tax_depreciation_deduction
OR NEW.tax_depreciation_closing_value IS DISTINCT FROM OLD.tax_depreciation_closing_value
OR NEW.tax_depreciation_calculation IS DISTINCT FROM OLD.tax_depreciation_calculation
);
IF NOT tax_snapshot_changed THEN
RETURN NEW;
END IF;
IF OLD.is_closed = true OR OLD.locked_at IS NOT NULL OR OLD.closing_entry_id IS NOT NULL THEN
RAISE EXCEPTION 'Fiscal period is locked for tax depreciation elections'
USING ERRCODE = 'check_violation';
END IF;
IF EXISTS (
SELECT 1
FROM public.fiscal_periods successor
WHERE successor.company_id = NEW.company_id
AND successor.period_start > NEW.period_start
AND successor.tax_depreciation_method IS NOT NULL
) THEN
RAISE EXCEPTION 'A later fiscal period already has a tax depreciation snapshot'
USING ERRCODE = 'check_violation';
END IF;
IF NEW.tax_depreciation_method IS NULL THEN
RETURN NEW;
END IF;
IF NEW.previous_period_id IS NOT NULL THEN
SELECT *
INTO previous_period
FROM public.fiscal_periods candidate
WHERE candidate.id = NEW.previous_period_id
AND candidate.company_id = NEW.company_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Previous fiscal period is missing or belongs to another company'
USING ERRCODE = 'check_violation';
END IF;
IF previous_period.period_end + 1 <> NEW.period_start THEN
RAISE EXCEPTION 'Previous fiscal period is not date-adjacent'
USING ERRCODE = 'check_violation';
END IF;
ELSE
SELECT *
INTO previous_period
FROM public.fiscal_periods candidate
WHERE candidate.company_id = NEW.company_id
AND candidate.period_end + 1 = NEW.period_start
ORDER BY candidate.period_end DESC
LIMIT 1;
END IF;
IF previous_period.id IS NOT NULL THEN
IF previous_period.tax_depreciation_method IS NULL
OR previous_period.tax_depreciation_closing_value IS NULL THEN
RAISE EXCEPTION 'Previous fiscal period has no tax depreciation snapshot'
USING ERRCODE = 'check_violation';
END IF;
IF NEW.tax_depreciation_method <> previous_period.tax_depreciation_method THEN
RAISE EXCEPTION 'Tax depreciation method must match the previous fiscal period'
USING ERRCODE = 'check_violation';
END IF;
IF NEW.tax_depreciation_opening_value
IS DISTINCT FROM previous_period.tax_depreciation_closing_value THEN
RAISE EXCEPTION 'Tax depreciation opening value must equal the previous closing value'
USING ERRCODE = 'check_violation';
END IF;
END IF;
RETURN NEW;
END;
$$;
-- The annual snapshot chain is authoritative. Keeping a second company-level
-- method introduced an admin/member RLS mismatch and a non-atomic second write.
ALTER TABLE public.company_settings
DROP CONSTRAINT IF EXISTS company_settings_tax_depreciation_method_check;
ALTER TABLE public.company_settings
DROP COLUMN IF EXISTS tax_depreciation_method;
NOTIFY pgrst, 'reload schema';