chore: clean up migration files and update CLAUDE.md documentation

- Consolidate migration numbering (move full_bas_2026 to slot 044)
- Remove dead/superseded migrations (document_matching, reversal columns)
- Update CLAUDE.md with placeholder migration notes and corrected descriptions
- Fix migration SQL for invoice_inbox, extension_data, supplier_invoices

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-02-26 20:53:57 +01:00
parent 9cd950de88
commit 0c422fcd25
11 changed files with 110 additions and 389 deletions
+9 -3
View File
@@ -799,6 +799,12 @@ mockResult({ data: makeTransaction(), error: null })
`YYYYMMDD00NNNN_descriptive_name.sql` — next migration: `20240101000046_*.sql`
### Placeholder Migrations
Some migrations are no-op placeholders to preserve the numbering sequence:
- **012** (`tax_codes`) — Planned but never deployed. The system operates without the `tax_codes` table.
- **023** (`document_version_chain`) — Planned but never deployed. Document versioning columns/functions do not exist in production.
### Migration Rules
1. **Always enable RLS** on new tables: `ALTER TABLE public.tablename ENABLE ROW LEVEL SECURITY;`
@@ -833,12 +839,12 @@ mockResult({ data: makeTransaction(), error: null })
### Recent Migrations
- **Migration 039 (`invoice_inbox`)** — Invoice inbox table for supplier invoice intake via email/upload.
- **Migration 039 (`invoice_inbox`)** — Invoice inbox table with document type classification, AI extraction, supplier/transaction matching, and receipt linking.
- **Migration 040 (`booking_template_embeddings`)** — Booking templates with AI embeddings for suggestion matching.
- **Migration 041 (`user_description_matching`)** — User description matching for transaction categorization.
- **Migration 042 (`full_bas_2026` + `prevent_overlapping_fiscal_periods`)** — Full BAS 2026 account catalog and fiscal period overlap prevention.
- **Migration 042 (`prevent_overlapping_fiscal_periods`)** — Exclusion constraint preventing overlapping fiscal periods per user.
- **Migration 043 (`enforce_fiscal_period_month_boundaries`)** — Ensures fiscal periods start/end on month boundaries.
- **Migration 044 (`document_matching`)** — Document-to-transaction matching support.
- **Migration 044 (`full_bas_2026`)** — Full BAS 2026 account catalog, K2-excluded flag, and SRU code backfill.
- **Migration 045 (`expand_account_type_untaxed_reserves`)** — Adds `untaxed_reserves` to `chart_of_accounts.account_type` CHECK constraint for BAS 21xx accounts (obeskattade reserver).
---
@@ -63,9 +63,10 @@ ALTER TABLE public.journal_entry_lines
ALTER TABLE public.journal_entry_lines
ADD COLUMN IF NOT EXISTS project text;
CREATE INDEX IF NOT EXISTS idx_journal_entry_lines_tax_code ON public.journal_entry_lines (tax_code);
CREATE INDEX IF NOT EXISTS idx_journal_entry_lines_cost_center ON public.journal_entry_lines (cost_center);
CREATE INDEX IF NOT EXISTS idx_journal_entry_lines_project ON public.journal_entry_lines (project);
-- Note: idx_journal_entry_lines_cost_center and idx_journal_entry_lines_project
-- are created by migration 015 (dimensions) on the UUID FK columns (cost_center_id, project_id).
-- The text dimension columns (cost_center, project) are supplementary and don't need separate indexes.
CREATE INDEX IF NOT EXISTS idx_journal_entry_lines_tax_code ON public.journal_entry_lines (tax_code) WHERE (tax_code IS NOT NULL);
-- =============================================================================
-- 4. fiscal_periods: Add lock and retention columns
@@ -1,123 +1,5 @@
-- Migration 12: Tax Code Engine
-- Decoupled tax codes for momsdeklaration mapping
-- =============================================================================
-- 1. tax_codes table
-- =============================================================================
CREATE TABLE public.tax_codes (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id uuid REFERENCES auth.users ON DELETE CASCADE,
code text NOT NULL,
description text NOT NULL,
rate numeric NOT NULL DEFAULT 0,
-- Momsdeklaration ruta mapping
moms_basis_boxes text[] DEFAULT '{}', -- e.g. {'10'} for 25% basis
moms_tax_boxes text[] DEFAULT '{}', -- e.g. {'05'} for 25% output VAT
moms_input_boxes text[] DEFAULT '{}', -- e.g. {'48'} for input VAT
-- Classification flags
is_output_vat boolean DEFAULT false,
is_reverse_charge boolean DEFAULT false,
is_eu boolean DEFAULT false,
is_export boolean DEFAULT false,
is_oss boolean DEFAULT false,
is_system boolean DEFAULT false,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
-- System codes have NULL user_id; user codes have unique code per user
UNIQUE (user_id, code)
);
ALTER TABLE public.tax_codes ENABLE ROW LEVEL SECURITY;
-- Users can see their own + system (user_id IS NULL) codes
CREATE POLICY "tax_codes_select" ON public.tax_codes
FOR SELECT USING (auth.uid() = user_id OR user_id IS NULL);
CREATE POLICY "tax_codes_insert" ON public.tax_codes
FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "tax_codes_update" ON public.tax_codes
FOR UPDATE USING (auth.uid() = user_id);
CREATE POLICY "tax_codes_delete" ON public.tax_codes
FOR DELETE USING (auth.uid() = user_id);
CREATE INDEX idx_tax_codes_user_id ON public.tax_codes (user_id);
CREATE INDEX idx_tax_codes_code ON public.tax_codes (code);
CREATE TRIGGER tax_codes_updated_at
BEFORE UPDATE ON public.tax_codes
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
-- =============================================================================
-- 2. Seed system tax codes (12 standard Swedish tax codes)
-- =============================================================================
INSERT INTO public.tax_codes (user_id, code, description, rate, moms_basis_boxes, moms_tax_boxes, moms_input_boxes, is_output_vat, is_reverse_charge, is_eu, is_export, is_oss, is_system)
VALUES
-- Output VAT (utgående moms)
(NULL, 'MP1', 'Utgående moms 25%', 0.25, '{10}', '{05}', '{}', true, false, false, false, false, true),
(NULL, 'MP2', 'Utgående moms 12%', 0.12, '{11}', '{06}', '{}', true, false, false, false, false, true),
(NULL, 'MP3', 'Utgående moms 6%', 0.06, '{12}', '{07}', '{}', true, false, false, false, false, true),
-- Input VAT (ingående moms)
(NULL, 'MPI', 'Ingående moms 25%', 0.25, '{}', '{}', '{48}', false, false, false, false, false, true),
(NULL, 'MPI12', 'Ingående moms 12%', 0.12, '{}', '{}', '{48}', false, false, false, false, false, true),
(NULL, 'MPI6', 'Ingående moms 6%', 0.06, '{}', '{}', '{48}', false, false, false, false, false, true),
-- EU / International
(NULL, 'IV', 'Intra-EU förvärv (omvänd moms)', 0.25, '{20,21}', '{30,31}', '{48}', false, true, true, false, false, true),
(NULL, 'EUS', 'EU försäljning (omvänd moms)', 0, '{39}', '{}', '{}', false, true, true, false, false, true),
(NULL, 'IP', 'Import (tull/moms)', 0.25, '{22}', '{32}', '{48}', false, false, false, false, false, true),
(NULL, 'EXP', 'Export utanför EU', 0, '{40}', '{}', '{}', false, false, false, true, false, true),
-- OSS (One Stop Shop)
(NULL, 'OSS', 'OSS försäljning EU konsument', 0, '{}', '{}', '{}', false, false, true, false, true, true),
-- Exempt
(NULL, 'NONE', 'Momsfritt', 0, '{}', '{}', '{}', false, false, false, false, false, true);
-- =============================================================================
-- 3. Function to copy system tax codes to user scope
-- =============================================================================
CREATE OR REPLACE FUNCTION public.seed_tax_codes_for_user(p_user_id uuid)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
v_count integer;
BEGIN
-- Only seed if user has no existing tax codes
SELECT count(*) INTO v_count
FROM public.tax_codes
WHERE user_id = p_user_id;
IF v_count > 0 THEN
RETURN;
END IF;
INSERT INTO public.tax_codes (user_id, code, description, rate, moms_basis_boxes, moms_tax_boxes, moms_input_boxes, is_output_vat, is_reverse_charge, is_eu, is_export, is_oss, is_system)
SELECT
p_user_id,
code,
description,
rate,
moms_basis_boxes,
moms_tax_boxes,
moms_input_boxes,
is_output_vat,
is_reverse_charge,
is_eu,
is_export,
is_oss,
false -- user copies are NOT system
FROM public.tax_codes
WHERE user_id IS NULL AND is_system = true;
END;
$$;
GRANT EXECUTE ON FUNCTION public.seed_tax_codes_for_user(uuid) TO authenticated;
-- Migration 12: Tax Code Engine (placeholder)
-- This migration was planned but never deployed to production.
-- The tax_codes table does not exist in the production database.
-- The system operates without it — tax code logic is handled in application code.
-- Kept as a numbered placeholder to preserve migration sequence.
@@ -1,9 +1,8 @@
-- ============================================================
-- Extension Data & Event Log Tables
-- Part 3: Event Bus & Extension Registry
-- Extension Data Table
-- Generic key-value store for extensions
-- ============================================================
-- Generic key-value store for extensions
create table if not exists extension_data (
id uuid primary key default gen_random_uuid(),
user_id uuid references auth.users not null,
@@ -34,31 +33,16 @@ create policy "Users can delete own extension data"
on extension_data for delete
using (auth.uid() = user_id);
-- Indexes
create index if not exists idx_extension_data_user_id on extension_data (user_id);
create index if not exists idx_extension_data_user_ext_key on extension_data (user_id, extension_id, key);
-- Auto-update updated_at
create trigger extension_data_updated_at
create trigger set_updated_at_extension_data
before update on extension_data
for each row execute function update_updated_at();
for each row execute function update_updated_at_column();
-- Append-only event log for observability
create table if not exists event_log (
id uuid primary key default gen_random_uuid(),
user_id uuid references auth.users not null,
event_type text not null,
payload jsonb not null default '{}',
created_at timestamptz default now()
);
-- RLS: users can select and insert only (no update, no delete)
alter table event_log enable row level security;
create policy "Users can select own event log"
on event_log for select
using (auth.uid() = user_id);
create policy "Users can insert own event log"
on event_log for insert
with check (auth.uid() = user_id);
-- Index for querying by event type
create index if not exists idx_event_log_user_type on event_log (user_id, event_type);
create index if not exists idx_event_log_created_at on event_log (created_at);
-- Audit trigger
create trigger audit_extension_data
after insert or update or delete on extension_data
for each row execute function write_audit_log();
@@ -1,163 +1,7 @@
-- Migration 23: Document Version Chain Hardening
-- Adds cryptographic hash chain + atomic versioning via RPC
-- Resolves: Gap 4 (no hash-chain) + Gap 5 (race condition in versioning)
-- =============================================================================
-- 1. New columns on document_attachments
-- =============================================================================
ALTER TABLE public.document_attachments
ADD COLUMN IF NOT EXISTS prev_version_hash text,
ADD COLUMN IF NOT EXISTS last_integrity_check_at timestamptz;
COMMENT ON COLUMN public.document_attachments.prev_version_hash IS
'SHA-256 hash of the previous version. NULL for version 1 (genesis). Creates tamper-evident chain.';
COMMENT ON COLUMN public.document_attachments.last_integrity_check_at IS
'Timestamp of the last batch integrity verification (set by cron job).';
-- Index for cron job: prioritize documents never checked or least recently checked
CREATE INDEX IF NOT EXISTS idx_doc_attachments_integrity_check
ON public.document_attachments (last_integrity_check_at NULLS FIRST)
WHERE is_current_version = true;
-- =============================================================================
-- 2. RPC: create_document_version (atomic versioning with row-level lock)
-- Pattern follows next_voucher_number() from migration 016
-- =============================================================================
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,
p_upload_source text DEFAULT NULL
)
RETURNS uuid
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
v_current RECORD;
v_root_original_id uuid;
v_new_version integer;
v_new_id uuid;
BEGIN
-- Lock the current version row (prevents concurrent versioning)
SELECT id, version, sha256_hash, original_id, journal_entry_id,
journal_entry_line_id, upload_source
INTO v_current
FROM public.document_attachments
WHERE id = p_original_doc_id
AND user_id = p_user_id
AND is_current_version = true
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION 'Document not found, not owned by user, or not the current version';
END IF;
v_root_original_id := COALESCE(v_current.original_id, v_current.id);
v_new_version := v_current.version + 1;
-- Insert new version with hash chain link
INSERT INTO public.document_attachments (
user_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,
p_storage_path,
p_file_name,
p_file_size_bytes,
p_mime_type,
p_sha256_hash,
v_new_version,
v_root_original_id,
true,
p_user_id,
COALESCE(p_upload_source, 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 (within same transaction = atomic)
UPDATE public.document_attachments
SET is_current_version = false,
superseded_by_id = v_new_id
WHERE id = v_current.id;
RETURN v_new_id;
END;
$$;
GRANT EXECUTE ON FUNCTION public.create_document_version(uuid, uuid, text, text, bigint, text, text, text) TO authenticated;
-- =============================================================================
-- 3. RPC: validate_version_chain
-- Verifies that prev_version_hash matches sha256_hash of the preceding version
-- =============================================================================
CREATE OR REPLACE FUNCTION public.validate_version_chain(
p_user_id uuid,
p_original_doc_id uuid
)
RETURNS TABLE (
doc_id uuid,
version integer,
sha256_hash text,
prev_version_hash text,
expected_prev_hash text,
chain_valid boolean
)
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
BEGIN
RETURN QUERY
WITH chain AS (
SELECT
da.id,
da.version,
da.sha256_hash AS current_hash,
da.prev_version_hash AS stored_prev_hash,
LAG(da.sha256_hash) OVER (ORDER BY da.version) AS computed_prev_hash
FROM public.document_attachments da
WHERE da.user_id = p_user_id
AND (da.id = p_original_doc_id OR da.original_id = p_original_doc_id)
ORDER BY da.version
)
SELECT
chain.id AS doc_id,
chain.version,
chain.current_hash AS sha256_hash,
chain.stored_prev_hash AS prev_version_hash,
chain.computed_prev_hash AS expected_prev_hash,
CASE
WHEN chain.version = 1 THEN (chain.stored_prev_hash IS NULL)
ELSE (chain.stored_prev_hash IS NOT NULL AND chain.stored_prev_hash = chain.computed_prev_hash)
END AS chain_valid
FROM chain
ORDER BY chain.version;
END;
$$;
GRANT EXECUTE ON FUNCTION public.validate_version_chain(uuid, uuid) TO authenticated;
-- Migration 23: Document Version Chain Hardening (placeholder)
-- This migration was planned but never deployed to production.
-- The columns prev_version_hash and last_integrity_check_at do not exist
-- in the production document_attachments table.
-- The functions create_document_version() and validate_version_chain()
-- do not exist in the production database.
-- Kept as a numbered placeholder to preserve migration sequence.
@@ -25,7 +25,7 @@ CREATE TABLE IF NOT EXISTS public.suppliers (
address_line2 text,
postal_code text,
city text,
country text NOT NULL DEFAULT 'SE',
country text DEFAULT 'SE',
-- Payment details
bankgiro text,
@@ -33,12 +33,18 @@ CREATE TABLE IF NOT EXISTS public.suppliers (
bank_account text,
iban text,
bic text,
clearing_number text,
account_number text,
-- Defaults
default_expense_account text, -- e.g. '5010'
default_payment_terms integer NOT NULL DEFAULT 30,
default_payment_terms integer DEFAULT 30,
default_currency text NOT NULL DEFAULT 'SEK',
-- Classification
category text,
is_active boolean DEFAULT true,
-- Notes
notes text,
@@ -48,24 +54,25 @@ CREATE TABLE IF NOT EXISTS public.suppliers (
ALTER TABLE public.suppliers ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own suppliers"
CREATE POLICY "suppliers_select"
ON public.suppliers FOR SELECT
USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own suppliers"
CREATE POLICY "suppliers_insert"
ON public.suppliers FOR INSERT
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own suppliers"
CREATE POLICY "suppliers_update"
ON public.suppliers FOR UPDATE
USING (auth.uid() = user_id);
CREATE POLICY "Users can delete own suppliers"
CREATE POLICY "suppliers_delete"
ON public.suppliers FOR DELETE
USING (auth.uid() = user_id);
CREATE INDEX idx_suppliers_user_id ON public.suppliers (user_id);
CREATE INDEX idx_suppliers_name ON public.suppliers (user_id, name);
CREATE INDEX idx_suppliers_is_active ON public.suppliers (user_id, is_active);
-- =============================================================================
-- 2. supplier_invoices table
@@ -1,19 +1,18 @@
-- Migration 034: Fix extension_data updated_at trigger
-- The original trigger references update_updated_at() which does not exist.
-- The correct function is public.update_updated_at_column().
-- Wrapped in DO block in case extension_data table does not yet exist.
-- Originally fixed a wrong function reference (update_updated_at vs update_updated_at_column).
-- The issue is now fixed directly in migration 020, making this a no-op for fresh deployments.
-- Kept for migration numbering sequence. Safe to re-run: just drops a trigger that may not exist.
do $$
begin
if exists (
select 1 from information_schema.tables
where table_schema = 'public' and table_name = 'extension_data'
) then
drop trigger if exists extension_data_updated_at on public.extension_data;
create trigger extension_data_updated_at
before update on public.extension_data
for each row execute function public.update_updated_at_column();
end if;
end;
DO $$
BEGIN
-- Drop the incorrectly-named trigger if it exists from an older migration version
IF EXISTS (
SELECT 1 FROM information_schema.triggers
WHERE trigger_schema = 'public'
AND event_object_table = 'extension_data'
AND trigger_name = 'extension_data_updated_at'
) THEN
DROP TRIGGER extension_data_updated_at ON public.extension_data;
END IF;
END;
$$;
@@ -1,5 +1,5 @@
-- Invoice Inbox: table for incoming supplier invoices (email + upload)
-- Supports AI extraction, supplier matching, and confirm-to-create workflow
-- Invoice Inbox: table for incoming documents (supplier invoices, receipts, etc.)
-- Supports AI extraction, supplier matching, transaction matching, and confirm-to-create workflow
CREATE TABLE public.invoice_inbox_items (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
@@ -8,17 +8,48 @@ CREATE TABLE public.invoice_inbox_items (
CHECK (status IN ('pending','processing','ready','confirmed','rejected','error')),
source text NOT NULL DEFAULT 'upload'
CHECK (source IN ('email','upload')),
-- Email metadata
email_from text,
email_subject text,
email_received_at timestamptz,
-- Document link
document_id uuid REFERENCES public.document_attachments(id) ON DELETE SET NULL,
-- AI extraction
extracted_data jsonb,
confidence numeric,
-- Supplier matching
matched_supplier_id uuid REFERENCES public.suppliers(id) ON DELETE SET NULL,
created_supplier_invoice_id uuid REFERENCES public.supplier_invoices(id) ON DELETE SET NULL,
-- Error tracking
error_message text,
-- Timestamps
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
updated_at timestamptz NOT NULL DEFAULT now(),
-- Document type classification (supplier_invoice, receipt, government_letter, unknown)
document_type text NOT NULL DEFAULT 'supplier_invoice'
CHECK (document_type IN ('supplier_invoice','receipt','government_letter','unknown')),
-- Receipt linking (when document_type = 'receipt')
linked_receipt_id uuid REFERENCES public.receipts(id) ON DELETE SET NULL,
-- Raw email payload for reprocessing
raw_email_payload jsonb,
-- AI booking template suggestion
suggested_template_id text,
suggested_template_confidence numeric,
-- Transaction matching
matched_transaction_id uuid REFERENCES public.transactions(id) ON DELETE SET NULL,
match_confidence numeric,
match_method text CHECK (match_method IN ('payment_reference','amount_date','amount_merchant','receipt_match'))
);
-- RLS
@@ -50,6 +81,17 @@ CREATE INDEX idx_invoice_inbox_items_user_status
CREATE INDEX idx_invoice_inbox_items_user_created
ON public.invoice_inbox_items(user_id, created_at DESC);
CREATE INDEX idx_inbox_items_document_type
ON public.invoice_inbox_items(user_id, document_type, status);
CREATE INDEX idx_inbox_items_matched_transaction
ON public.invoice_inbox_items(user_id, matched_transaction_id)
WHERE matched_transaction_id IS NOT NULL;
CREATE INDEX idx_inbox_items_unmatched_ready
ON public.invoice_inbox_items(user_id, status)
WHERE matched_transaction_id IS NULL AND status IN ('ready', 'processing');
-- updated_at trigger
CREATE TRIGGER invoice_inbox_items_updated_at
BEFORE UPDATE ON public.invoice_inbox_items
@@ -1,23 +0,0 @@
-- Document matching: add columns to invoice_inbox_items for transaction matching
-- and AI template suggestions.
-- Suggested booking template from AI extraction
ALTER TABLE public.invoice_inbox_items
ADD COLUMN suggested_template_id TEXT,
ADD COLUMN suggested_template_confidence NUMERIC;
-- Matched bank transaction
ALTER TABLE public.invoice_inbox_items
ADD COLUMN matched_transaction_id UUID REFERENCES public.transactions(id) ON DELETE SET NULL,
ADD COLUMN match_confidence NUMERIC,
ADD COLUMN match_method TEXT CHECK (match_method IN ('payment_reference', 'amount_date', 'amount_merchant', 'receipt_match'));
-- Index for looking up which inbox item is matched to a transaction
CREATE INDEX idx_inbox_items_matched_transaction
ON public.invoice_inbox_items (user_id, matched_transaction_id)
WHERE matched_transaction_id IS NOT NULL;
-- Index for finding unmatched ready items for sweep
CREATE INDEX idx_inbox_items_unmatched_ready
ON public.invoice_inbox_items (user_id, status)
WHERE matched_transaction_id IS NULL AND status IN ('ready', 'processing');
@@ -1,21 +0,0 @@
-- Add storno/correction link columns to journal_entries
-- These are required for ändringsverifikationer (correction entries) per BFL 5 kap.
-- The reverseEntry() and correctEntry() functions in engine.ts / storno-service.ts
-- depend on these columns to create bidirectional links between entries.
-- Link to storno entry that reversed this entry
ALTER TABLE public.journal_entries
ADD COLUMN IF NOT EXISTS reversed_by_id uuid REFERENCES public.journal_entries(id) ON DELETE SET NULL;
-- Link to the original entry that this storno reverses
ALTER TABLE public.journal_entries
ADD COLUMN IF NOT EXISTS reverses_id uuid REFERENCES public.journal_entries(id) ON DELETE SET NULL;
-- Link to the original entry in a correction chain (storno + new correct entry)
ALTER TABLE public.journal_entries
ADD COLUMN IF NOT EXISTS correction_of_id uuid REFERENCES public.journal_entries(id) ON DELETE SET NULL;
-- Indexes for FK lookups
CREATE INDEX IF NOT EXISTS idx_journal_entries_reversed_by_id ON public.journal_entries (reversed_by_id);
CREATE INDEX IF NOT EXISTS idx_journal_entries_reverses_id ON public.journal_entries (reverses_id);
CREATE INDEX IF NOT EXISTS idx_journal_entries_correction_of_id ON public.journal_entries (correction_of_id);