* feat: add INK2 declaration improvements, invoice delivery date, and Swedish compliance skills Expand INK2 engine with full INK2S/INK2R support and improved SRU generation. Add delivery_date field to invoices and corresponding PDF/migration support. Add Claude skills for Swedish asset accounting, invoice compliance, SIE import/export, SRU filing, and tax planning. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review — map BAS 4500–4899, strip CRLF in SRU, document P3 - Map BAS accounts 4500–4599 (legoarbeten), 4700–4899 (diverse varuinköpskostnader) to SRU 7512 so they are not silently dropped from INK2R declarations - Strip \r\n in sanitizeString to prevent CRLF injection in SRU fields - Document P3 period suffix limitation for brutet räkenskapsår Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: correct BAS 4500-4599, 4700-4899 mapping from 7512 to 7511 Per the official BAS-to-SRU mapping, these account ranges are cost of goods (legoarbeten, inkurans, svinn) and belong under 7511 (Råvaror och förnödenheter), not 7512 (Handelsvaror). 7512 remains 4600-4699. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Swedish VAT compliance — representation VAT, domestic RC, full BAS 26xx mapping, SIE encoding - Representation expenses now default to reduced_12 VAT (ML 13 kap 24-25 §§); income tax deduction was abolished 2017 but VAT deduction at 12% remains - Domestic reverse charge (byggtjänster etc.) uses 2647 instead of 2645, with distinct line descriptions for Swedish vs EU/non-EU RC - VAT declaration maps all BAS 26xx variant accounts (egna uttag 2612/2622/2632, uthyrning 2613/2623/2633, VMB 2616/2626/2636, import 2615/2625/2635, domestic RC 2647, frivillig skattskyldighet 2642) and revenue variants (3108/3105/3004/3100) to correct momsdeklaration rutor - SIE parser: remove unreliable #FORMAT PC8 encoding detection (most software exports UTF-8 with PC8 header), parse #FLAGGA for import-already-done warning, default SIE type to 1 when absent, fix RTRANS/BTRANS documentation - SIE export: add #RAR -1 (previous fiscal year), fix UB = IB + movements - Error messages: add pattern matching for locked period trigger errors Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address Greptile review — update ruta49 JSDoc, use null sentinel in error map Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: harden storno CAS guard, document integrity, and BFNAR archive compliance - Storno: defer original→reversed until both entries succeed, add CAS guard for concurrent reversals, use cancelEntry() instead of delete - Document: add document.accessed event, enrich archive manifest with metadata, add BFNAR 2013:2 systemdokumentation to full archive export - Verify cron: run daily, configurable batch size, include company_id in audit - Migrations: integrity audit actions, document version chain, metadata immutability, audit deletions, fix immutability for posted/cancelled Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address Greptile review — allow is_current_version in immutability trigger, log cancelEntry errors - Remove is_current_version from blocked fields in enforce_document_metadata_immutability trigger so create_document_version RPC can supersede documents linked to posted entries - Add error logging to cancelEntry for observability on cleanup failures Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
117 lines
3.6 KiB
PL/PgSQL
117 lines
3.6 KiB
PL/PgSQL
-- Document version chain: prev_version_hash column + create_document_version RPC
|
|
-- Fixes the non-functional document versioning (migration 023 was a placeholder).
|
|
-- The createNewVersion() in lib/core/documents/document-service.ts calls this RPC.
|
|
|
|
-- 1. Add prev_version_hash column for cryptographic version chain
|
|
ALTER TABLE public.document_attachments
|
|
ADD COLUMN IF NOT EXISTS prev_version_hash text;
|
|
|
|
-- 2. Atomic version creation RPC
|
|
-- Row-locks the current version, inserts a new version with hash chain,
|
|
-- and marks the old version as superseded — all in one transaction.
|
|
CREATE OR REPLACE FUNCTION public.create_document_version(
|
|
p_user_id uuid,
|
|
p_original_doc_id uuid,
|
|
p_storage_path text,
|
|
p_file_name text,
|
|
p_file_size_bytes bigint,
|
|
p_mime_type text,
|
|
p_sha256_hash text
|
|
)
|
|
RETURNS uuid
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
DECLARE
|
|
v_current document_attachments%ROWTYPE;
|
|
v_new_id uuid;
|
|
v_root_id uuid;
|
|
v_next_version integer;
|
|
BEGIN
|
|
-- Lock the current version row to prevent concurrent versioning
|
|
SELECT * INTO v_current
|
|
FROM public.document_attachments
|
|
WHERE id = p_original_doc_id
|
|
AND is_current_version = true
|
|
FOR UPDATE;
|
|
|
|
IF v_current IS NULL THEN
|
|
RAISE EXCEPTION 'Document % not found or is not the current version', p_original_doc_id;
|
|
END IF;
|
|
|
|
-- Determine root document and next version number
|
|
v_root_id := COALESCE(v_current.original_id, v_current.id);
|
|
v_next_version := v_current.version + 1;
|
|
|
|
-- Insert new version with hash chain link
|
|
INSERT INTO public.document_attachments (
|
|
user_id, company_id, storage_path, file_name, file_size_bytes,
|
|
mime_type, sha256_hash, version, original_id, is_current_version,
|
|
uploaded_by, upload_source, digitization_date,
|
|
journal_entry_id, journal_entry_line_id, prev_version_hash
|
|
) VALUES (
|
|
p_user_id, v_current.company_id, p_storage_path, p_file_name,
|
|
p_file_size_bytes, p_mime_type, p_sha256_hash, v_next_version,
|
|
v_root_id, true, p_user_id, v_current.upload_source, now(),
|
|
v_current.journal_entry_id, v_current.journal_entry_line_id,
|
|
v_current.sha256_hash -- cryptographic link to previous version
|
|
)
|
|
RETURNING id INTO v_new_id;
|
|
|
|
-- Mark old version as superseded
|
|
UPDATE public.document_attachments
|
|
SET is_current_version = false,
|
|
superseded_by_id = v_new_id
|
|
WHERE id = p_original_doc_id;
|
|
|
|
RETURN v_new_id;
|
|
END;
|
|
$$;
|
|
|
|
-- 3. Version chain validation function
|
|
-- Walks the version chain from newest to oldest and verifies each
|
|
-- prev_version_hash matches the prior version's sha256_hash.
|
|
CREATE OR REPLACE FUNCTION public.validate_version_chain(p_document_id uuid)
|
|
RETURNS TABLE(version integer, document_id uuid, hash_valid boolean)
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
DECLARE
|
|
v_root_id uuid;
|
|
BEGIN
|
|
-- Find root document
|
|
SELECT COALESCE(da.original_id, da.id) INTO v_root_id
|
|
FROM public.document_attachments da
|
|
WHERE da.id = p_document_id;
|
|
|
|
IF v_root_id IS NULL THEN
|
|
RAISE EXCEPTION 'Document % not found', p_document_id;
|
|
END IF;
|
|
|
|
-- Walk chain and verify hashes
|
|
RETURN QUERY
|
|
WITH chain AS (
|
|
SELECT
|
|
da.id AS doc_id,
|
|
da.version AS ver,
|
|
da.sha256_hash,
|
|
da.prev_version_hash,
|
|
LAG(da.sha256_hash) OVER (ORDER BY da.version) AS expected_prev_hash
|
|
FROM public.document_attachments da
|
|
WHERE da.id = v_root_id OR da.original_id = v_root_id
|
|
ORDER BY da.version
|
|
)
|
|
SELECT
|
|
chain.ver,
|
|
chain.doc_id,
|
|
CASE
|
|
WHEN chain.ver = 1 THEN chain.prev_version_hash IS NULL
|
|
ELSE chain.prev_version_hash IS NOT DISTINCT FROM chain.expected_prev_hash
|
|
END AS hash_valid
|
|
FROM chain
|
|
ORDER BY chain.ver;
|
|
END;
|
|
$$;
|