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:
@@ -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);
|
||||
Reference in New Issue
Block a user