feat(assets): atomic asset disposal workflow (avyttring, utrangering, verksamhetsoverlatelse) (#1391)

* feat(assets): atomic asset disposal workflow (avyttring, utrangering, verksamhetsoverlatelse)

Disposal books depreciation to the disposal date, clears cost and
accumulated depreciation, books gain (3973) or loss (7973), applies
output VAT on third-party sales, honors the ML 5 kap. 38 §
verksamhetsoverlatelse exemption, and recalculates ML 15 kap. jamkning
server-side from tax years and original input VAT. The voucher, the
disposal-date depreciation schedule and the immutable register state
commit in one dedicated commit_asset_disposal RPC transaction that
delegates voucher numbering to commit_journal_entry.

Fixes #325

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(assets): harden disposal per review and pg-real findings

- commit_asset_disposal now uses the NULL-safe caller_is_company_member()
  guard (tenant-guard ratchet) and passes the allowed 'user_accept'
  commit_method instead of the unlisted 'asset_disposal' value
- disposal metadata invariants validated in the RPC (non-negative
  proceeds/VAT, VAT requires a treatment, VAT <= gross, scrap carries no
  proceeds) since the RPC is independently callable
- new FK and CHECK constraints added NOT VALID + VALIDATE CONSTRAINT so
  the migration never blocks writes on the hot journal_entries table
- disposeAsset paginates fiscal periods and depreciation schedules with
  fetchAllRows; jamkning_remaining_years keeps a valid 0 (?? not ||)
- engine imports shared AssetDisposalType/AssetJamkningDirection/
  VatTreatment unions; post-commit reload retries once and logs before
  surfacing, so a transient read cannot masquerade as a failed disposal
- dispose page parses Swedish-formatted amounts (125 000,50) and blocks
  submission on unparseable proceeds
- assets pg tests write disposal attributes in the disposal transition
  itself and gain a regression test that the register is frozen after

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-08-04 09:59:52 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 1c9d378df8
commit cb3ef45f14
22 changed files with 19644 additions and 1805 deletions
+38
View File
@@ -3115,6 +3115,44 @@ const BOLAGSVERKET: Record<string, StructuredErrorEntry> = {
}
const ASSETS: Record<string, StructuredErrorEntry> = {
ASSET_NOT_FOUND: {
httpStatus: 404,
message_sv: 'Tillgången kunde inte hittas.',
message_en: 'Asset not found.',
},
ASSET_ALREADY_DISPOSED: {
httpStatus: 409,
message_sv: 'Tillgången är redan avyttrad.',
message_en: 'The asset has already been disposed.',
},
ASSET_DISPOSAL_BLOCKED: {
httpStatus: 409,
message_sv:
'Avyttringen kan inte bokföras eftersom avskrivningar redan finns för samma eller en senare period. Återför den felaktiga avskrivningen med storno först.',
message_en:
'The disposal cannot be posted because depreciation already exists for the same or a later period. Reverse the incorrect depreciation first.',
},
ASSET_JAMKNING_DATA_REQUIRED: {
httpStatus: 422,
message_sv:
'Ange ursprunglig ingående moms och ursprunglig avdragsprocent för att bedöma justering enligt ML 15 kap.',
message_en:
'Enter the original input VAT and original deduction percentage to assess adjustment under ML chapter 15.',
},
ASSET_ADJUSTMENT_DOCUMENT_REQUIRED: {
httpStatus: 422,
message_sv:
'Bekräfta att en justeringshandling upprättas när justeringsskyldigheten överförs.',
message_en:
'Confirm that an adjustment document is prepared when the adjustment obligation is transferred.',
},
ASSET_BUSINESS_TRANSFER_CONFIRMATION_REQUIRED: {
httpStatus: 422,
message_sv:
'Bekräfta att överlåtelsen omfattar en hel verksamhet eller självständig verksamhetsgren och uppfyller villkoren i ML 5 kap. 38 §.',
message_en:
'Confirm that the transfer covers an entire business or independent branch and meets the conditions in ML chapter 5, section 38.',
},
ASSET_CORRECTION_BLOCKED: {
httpStatus: 409,
message_sv: