Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma strängvärde byggs med konkatenering i stället för att matcha mönstret.
980 lines
38 KiB
PL/PgSQL
980 lines
38 KiB
PL/PgSQL
-- Schema sync: consolidates all changes applied to production after migration 97
|
||
-- (20260402120000_inbox_classification). This single migration reproduces the exact
|
||
-- production schema delta when applied on top of the first 97 base migrations.
|
||
--
|
||
-- Changes consolidated from 20 remote migrations (20260407091315 – 20260414191533).
|
||
-- Production database is the source of truth — no schema changes are introduced.
|
||
|
||
|
||
-- ============================================================================
|
||
-- 1. NEW TABLES
|
||
-- ============================================================================
|
||
|
||
-- 1a. bankid_identities
|
||
CREATE TABLE IF NOT EXISTS public.bankid_identities (
|
||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||
user_id uuid REFERENCES auth.users ON DELETE CASCADE UNIQUE NOT NULL,
|
||
personal_number_hash text NOT NULL,
|
||
personal_number_enc bytea NOT NULL,
|
||
given_name text,
|
||
surname text,
|
||
linked_at timestamptz NOT NULL DEFAULT now(),
|
||
created_at timestamptz NOT NULL DEFAULT now(),
|
||
updated_at timestamptz NOT NULL DEFAULT now()
|
||
);
|
||
ALTER TABLE public.bankid_identities ENABLE ROW LEVEL SECURITY;
|
||
|
||
CREATE UNIQUE INDEX IF NOT EXISTS idx_bankid_identities_pnr_hash
|
||
ON public.bankid_identities (personal_number_hash);
|
||
CREATE INDEX IF NOT EXISTS idx_bankid_identities_user_id
|
||
ON public.bankid_identities (user_id);
|
||
|
||
DO $$ BEGIN
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='bankid_identities' AND policyname='bankid_identities_select') THEN
|
||
CREATE POLICY bankid_identities_select ON public.bankid_identities FOR SELECT USING (auth.uid() = user_id);
|
||
END IF;
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='bankid_identities' AND policyname='bankid_identities_insert') THEN
|
||
CREATE POLICY bankid_identities_insert ON public.bankid_identities FOR INSERT WITH CHECK (auth.uid() = user_id);
|
||
END IF;
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='bankid_identities' AND policyname='bankid_identities_delete') THEN
|
||
CREATE POLICY bankid_identities_delete ON public.bankid_identities FOR DELETE USING (auth.uid() = user_id);
|
||
END IF;
|
||
END $$;
|
||
|
||
CREATE OR REPLACE TRIGGER bankid_identities_updated_at
|
||
BEFORE UPDATE ON public.bankid_identities
|
||
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
|
||
|
||
|
||
-- 1b. automation_webhooks
|
||
CREATE TABLE IF NOT EXISTS public.automation_webhooks (
|
||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||
company_id uuid NOT NULL REFERENCES public.companies ON DELETE CASCADE,
|
||
event_type text NOT NULL,
|
||
webhook_url text NOT NULL,
|
||
active boolean NOT NULL DEFAULT true,
|
||
created_at timestamptz NOT NULL DEFAULT now(),
|
||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||
UNIQUE (company_id, event_type)
|
||
);
|
||
ALTER TABLE public.automation_webhooks ENABLE ROW LEVEL SECURITY;
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_automation_webhooks_company_event
|
||
ON public.automation_webhooks (company_id, event_type) WHERE (active = true);
|
||
|
||
DO $$ BEGIN
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='automation_webhooks' AND policyname='Members can view company webhooks') THEN
|
||
CREATE POLICY "Members can view company webhooks" ON public.automation_webhooks FOR SELECT
|
||
USING (company_id IN (SELECT company_id FROM company_members WHERE user_id = auth.uid()));
|
||
END IF;
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='automation_webhooks' AND policyname='Members can insert company webhooks') THEN
|
||
CREATE POLICY "Members can insert company webhooks" ON public.automation_webhooks FOR INSERT
|
||
WITH CHECK (company_id IN (SELECT company_id FROM company_members WHERE user_id = auth.uid()));
|
||
END IF;
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='automation_webhooks' AND policyname='Members can update company webhooks') THEN
|
||
CREATE POLICY "Members can update company webhooks" ON public.automation_webhooks FOR UPDATE
|
||
USING (company_id IN (SELECT company_id FROM company_members WHERE user_id = auth.uid()));
|
||
END IF;
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='automation_webhooks' AND policyname='Members can delete company webhooks') THEN
|
||
CREATE POLICY "Members can delete company webhooks" ON public.automation_webhooks FOR DELETE
|
||
USING (company_id IN (SELECT company_id FROM company_members WHERE user_id = auth.uid()));
|
||
END IF;
|
||
END $$;
|
||
|
||
CREATE OR REPLACE TRIGGER set_updated_at
|
||
BEFORE UPDATE ON public.automation_webhooks
|
||
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
|
||
|
||
|
||
-- 1c. booking_template_library
|
||
CREATE TABLE IF NOT EXISTS public.booking_template_library (
|
||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||
company_id uuid REFERENCES public.companies ON DELETE CASCADE,
|
||
team_id uuid REFERENCES public.teams ON DELETE CASCADE,
|
||
created_by uuid,
|
||
name text NOT NULL,
|
||
description text NOT NULL DEFAULT '',
|
||
category text NOT NULL DEFAULT 'other'
|
||
CHECK (category IN ('eu_trade','tax_account','private_transfer','salary','representation','year_end','vat','financial','other')),
|
||
entity_type text NOT NULL DEFAULT 'all'
|
||
CHECK (entity_type IN ('all','enskild_firma','aktiebolag')),
|
||
lines jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||
is_system boolean NOT NULL DEFAULT false,
|
||
is_active boolean NOT NULL DEFAULT true,
|
||
created_at timestamptz NOT NULL DEFAULT now(),
|
||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||
CHECK (NOT is_system OR (company_id IS NULL AND team_id IS NULL)),
|
||
CHECK (team_id IS NULL OR company_id IS NULL),
|
||
CHECK (company_id IS NOT NULL OR team_id IS NOT NULL OR is_system)
|
||
);
|
||
ALTER TABLE public.booking_template_library ENABLE ROW LEVEL SECURITY;
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_btl_active ON public.booking_template_library (is_active) WHERE (is_active = true);
|
||
CREATE INDEX IF NOT EXISTS idx_btl_category ON public.booking_template_library (category);
|
||
CREATE INDEX IF NOT EXISTS idx_btl_company ON public.booking_template_library (company_id) WHERE (company_id IS NOT NULL);
|
||
CREATE INDEX IF NOT EXISTS idx_btl_system ON public.booking_template_library (is_system) WHERE (is_system = true);
|
||
CREATE INDEX IF NOT EXISTS idx_btl_team ON public.booking_template_library (team_id) WHERE (team_id IS NOT NULL);
|
||
|
||
DO $$ BEGIN
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='booking_template_library' AND policyname='btl_select') THEN
|
||
CREATE POLICY btl_select ON public.booking_template_library FOR SELECT
|
||
USING (is_system OR company_id IN (SELECT user_company_ids()) OR team_id IN (SELECT user_team_ids()));
|
||
END IF;
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='booking_template_library' AND policyname='btl_insert') THEN
|
||
CREATE POLICY btl_insert ON public.booking_template_library FOR INSERT
|
||
WITH CHECK (NOT is_system AND (company_id IN (SELECT user_company_ids()) OR (company_id IS NULL AND team_id IN (SELECT user_team_ids()))));
|
||
END IF;
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='booking_template_library' AND policyname='btl_update') THEN
|
||
CREATE POLICY btl_update ON public.booking_template_library FOR UPDATE
|
||
USING (NOT is_system AND (company_id IN (SELECT user_company_ids()) OR (company_id IS NULL AND team_id IN (SELECT user_team_ids()))));
|
||
END IF;
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='booking_template_library' AND policyname='btl_delete') THEN
|
||
CREATE POLICY btl_delete ON public.booking_template_library FOR DELETE
|
||
USING (NOT is_system AND (company_id IN (SELECT user_company_ids()) OR (company_id IS NULL AND team_id IN (SELECT user_team_ids()))));
|
||
END IF;
|
||
END $$;
|
||
|
||
CREATE OR REPLACE TRIGGER btl_updated_at
|
||
BEFORE UPDATE ON public.booking_template_library
|
||
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
|
||
|
||
|
||
-- ============================================================================
|
||
-- 2. ALTER EXISTING TABLES — new columns
|
||
-- ============================================================================
|
||
|
||
ALTER TABLE public.companies ADD COLUMN IF NOT EXISTS archived_by uuid;
|
||
ALTER TABLE public.invoices ADD COLUMN IF NOT EXISTS delivery_date date;
|
||
ALTER TABLE public.journal_entries ADD COLUMN IF NOT EXISTS notes text;
|
||
ALTER TABLE public.sie_imports ADD COLUMN IF NOT EXISTS replaced_at timestamptz;
|
||
ALTER TABLE public.document_attachments ADD COLUMN IF NOT EXISTS prev_version_hash text;
|
||
|
||
|
||
-- ============================================================================
|
||
-- 3. MODIFIED CHECK CONSTRAINTS
|
||
-- ============================================================================
|
||
|
||
-- sie_imports: add 'replaced' status
|
||
ALTER TABLE public.sie_imports DROP CONSTRAINT IF EXISTS sie_imports_status_check;
|
||
ALTER TABLE public.sie_imports ADD CONSTRAINT sie_imports_status_check
|
||
CHECK (status = ANY (ARRAY['pending','mapped','completed','failed','replaced']));
|
||
|
||
-- audit_log: add integrity/security action types
|
||
ALTER TABLE public.audit_log DROP CONSTRAINT IF EXISTS audit_log_action_check;
|
||
ALTER TABLE public.audit_log ADD CONSTRAINT audit_log_action_check
|
||
CHECK (action = ANY (ARRAY[
|
||
'INSERT','UPDATE','DELETE','COMMIT','REVERSE','CORRECT',
|
||
'LOCK_PERIOD','CLOSE_PERIOD','DOCUMENT_DELETE_BLOCKED',
|
||
'RETENTION_BLOCK','SECURITY_EVENT','INTEGRITY_FAILURE'
|
||
]));
|
||
|
||
|
||
-- ============================================================================
|
||
-- 4. FUNCTIONS — CREATE OR REPLACE
|
||
-- ============================================================================
|
||
|
||
-- 4a. user_company_ids — filters out archived companies
|
||
CREATE OR REPLACE FUNCTION public.user_company_ids()
|
||
RETURNS SETOF uuid
|
||
LANGUAGE sql
|
||
STABLE SECURITY DEFINER
|
||
SET search_path TO 'public'
|
||
AS $function$
|
||
SELECT cm.company_id
|
||
FROM public.company_members cm
|
||
JOIN public.companies c ON c.id = cm.company_id
|
||
WHERE cm.user_id = auth.uid()
|
||
AND c.archived_at IS NULL;
|
||
$function$;
|
||
|
||
-- 4b. ensure_user_team — get-or-create personal team
|
||
CREATE OR REPLACE FUNCTION public.ensure_user_team()
|
||
RETURNS uuid
|
||
LANGUAGE plpgsql
|
||
SECURITY DEFINER
|
||
SET search_path TO 'public'
|
||
AS $function$
|
||
DECLARE
|
||
v_user_id uuid;
|
||
v_team_id uuid;
|
||
BEGIN
|
||
v_user_id := auth.uid();
|
||
IF v_user_id IS NULL THEN
|
||
RAISE EXCEPTION 'Not authenticated';
|
||
END IF;
|
||
|
||
SELECT team_id INTO v_team_id
|
||
FROM public.team_members
|
||
WHERE user_id = v_user_id
|
||
LIMIT 1;
|
||
|
||
IF v_team_id IS NOT NULL THEN
|
||
RETURN v_team_id;
|
||
END IF;
|
||
|
||
INSERT INTO public.teams (name, created_by)
|
||
VALUES ('Personal', v_user_id)
|
||
RETURNING id INTO v_team_id;
|
||
|
||
INSERT INTO public.team_members (team_id, user_id, role)
|
||
VALUES (v_team_id, v_user_id, 'owner');
|
||
|
||
RETURN v_team_id;
|
||
END;
|
||
$function$;
|
||
|
||
-- 4c. enforce_journal_entry_immutability — supports gnubok.allow_delete + cancelled
|
||
CREATE OR REPLACE FUNCTION public.enforce_journal_entry_immutability()
|
||
RETURNS trigger
|
||
LANGUAGE plpgsql
|
||
AS $function$
|
||
BEGIN
|
||
IF current_setting('gnubok.allow_delete', true) = 'true' THEN
|
||
IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
|
||
RETURN NEW;
|
||
END IF;
|
||
|
||
IF TG_OP = 'DELETE' THEN
|
||
RAISE EXCEPTION 'Cannot delete journal entries (id: %, status: %). Use cancelled status instead.',
|
||
OLD.id, OLD.status;
|
||
END IF;
|
||
|
||
IF OLD.status = 'draft' AND NEW.status IN ('draft', 'posted', 'cancelled') THEN
|
||
RETURN NEW;
|
||
END IF;
|
||
|
||
IF OLD.status = 'posted' AND NEW.status = 'posted' THEN
|
||
IF NEW.description = OLD.description
|
||
AND NEW.entry_date = OLD.entry_date
|
||
AND NEW.fiscal_period_id = OLD.fiscal_period_id
|
||
AND NEW.voucher_number = OLD.voucher_number
|
||
AND NEW.voucher_series = OLD.voucher_series
|
||
AND NEW.source_type = OLD.source_type
|
||
AND COALESCE(NEW.source_id::text, '') = COALESCE(OLD.source_id::text, '')
|
||
AND NEW.user_id = OLD.user_id
|
||
AND COALESCE(NEW.reversed_by_id::text, '') = COALESCE(OLD.reversed_by_id::text, '')
|
||
AND COALESCE(NEW.reverses_id::text, '') = COALESCE(OLD.reverses_id::text, '')
|
||
AND COALESCE(NEW.correction_of_id::text, '') = COALESCE(OLD.correction_of_id::text, '')
|
||
AND NEW.committed_at IS NOT DISTINCT FROM OLD.committed_at
|
||
THEN
|
||
RETURN NEW;
|
||
END IF;
|
||
END IF;
|
||
|
||
IF OLD.status = 'posted' AND NEW.status IN ('reversed', 'cancelled') THEN
|
||
IF NEW.status = 'reversed' THEN
|
||
IF NEW.description != OLD.description OR NEW.entry_date != OLD.entry_date
|
||
OR NEW.fiscal_period_id != OLD.fiscal_period_id
|
||
OR NEW.voucher_number != OLD.voucher_number THEN
|
||
RAISE EXCEPTION 'Cannot modify fields of a posted entry during reversal (id: %)', OLD.id;
|
||
END IF;
|
||
END IF;
|
||
RETURN NEW;
|
||
END IF;
|
||
|
||
IF OLD.status = 'reversed' AND NEW.status = 'posted'
|
||
AND current_setting('gnubok.allow_delete', true) = 'true' THEN
|
||
RETURN NEW;
|
||
END IF;
|
||
|
||
RAISE EXCEPTION 'Cannot modify a % journal entry (id: %). Committed entries are immutable per Bokforingslagen.',
|
||
OLD.status, OLD.id;
|
||
END; $function$;
|
||
|
||
-- 4d. enforce_journal_entry_line_immutability — supports gnubok.allow_delete + cancelled
|
||
CREATE OR REPLACE FUNCTION public.enforce_journal_entry_line_immutability()
|
||
RETURNS trigger
|
||
LANGUAGE plpgsql
|
||
AS $function$
|
||
DECLARE v_status text;
|
||
BEGIN
|
||
IF current_setting('gnubok.allow_delete', true) = 'true' THEN
|
||
IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
|
||
RETURN NEW;
|
||
END IF;
|
||
|
||
SELECT status INTO v_status FROM public.journal_entries
|
||
WHERE id = COALESCE(OLD.journal_entry_id, NEW.journal_entry_id);
|
||
|
||
IF v_status = 'draft' THEN
|
||
IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
|
||
RETURN NEW;
|
||
END IF;
|
||
|
||
IF v_status = 'cancelled' THEN
|
||
IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
|
||
RAISE EXCEPTION 'Cannot % lines of a cancelled journal entry.', TG_OP;
|
||
END IF;
|
||
|
||
RAISE EXCEPTION 'Cannot % lines of a % journal entry.', TG_OP, v_status;
|
||
END; $function$;
|
||
|
||
-- 4e. enforce_retention_journal_entries — supports gnubok.allow_delete
|
||
CREATE OR REPLACE FUNCTION public.enforce_retention_journal_entries()
|
||
RETURNS trigger
|
||
LANGUAGE plpgsql
|
||
SECURITY DEFINER
|
||
AS $function$
|
||
DECLARE
|
||
v_retention_expires date;
|
||
BEGIN
|
||
IF current_setting('gnubok.allow_delete', true) = 'true' THEN
|
||
RETURN OLD;
|
||
END IF;
|
||
|
||
SELECT fp.retention_expires_at INTO v_retention_expires
|
||
FROM public.fiscal_periods fp
|
||
WHERE fp.id = OLD.fiscal_period_id;
|
||
|
||
IF v_retention_expires IS NOT NULL AND v_retention_expires > CURRENT_DATE THEN
|
||
INSERT INTO public.audit_log (user_id, action, table_name, record_id, description)
|
||
VALUES (OLD.user_id, 'RETENTION_BLOCK', 'journal_entries', OLD.id,
|
||
'Attempted deletion within retention period (expires ' || v_retention_expires || ')');
|
||
|
||
RAISE EXCEPTION 'Cannot delete journal entry within 7-year retention period (expires %)',
|
||
v_retention_expires;
|
||
END IF;
|
||
|
||
RETURN OLD;
|
||
END; $function$;
|
||
|
||
-- 4f. enforce_document_metadata_immutability — new trigger function
|
||
CREATE OR REPLACE FUNCTION public.enforce_document_metadata_immutability()
|
||
RETURNS trigger
|
||
LANGUAGE plpgsql
|
||
SECURITY DEFINER
|
||
SET search_path TO 'public'
|
||
AS $function$
|
||
DECLARE
|
||
v_entry_status text;
|
||
BEGIN
|
||
IF OLD.journal_entry_id IS NULL THEN
|
||
RETURN NEW;
|
||
END IF;
|
||
|
||
SELECT status INTO v_entry_status
|
||
FROM public.journal_entries
|
||
WHERE id = OLD.journal_entry_id;
|
||
|
||
IF v_entry_status IS NULL OR v_entry_status NOT IN ('posted', 'reversed') THEN
|
||
RETURN NEW;
|
||
END IF;
|
||
|
||
IF NEW.file_name IS DISTINCT FROM OLD.file_name
|
||
OR NEW.storage_path IS DISTINCT FROM OLD.storage_path
|
||
OR NEW.file_size_bytes IS DISTINCT FROM OLD.file_size_bytes
|
||
OR NEW.mime_type IS DISTINCT FROM OLD.mime_type
|
||
OR NEW.sha256_hash IS DISTINCT FROM OLD.sha256_hash
|
||
OR NEW.upload_source IS DISTINCT FROM OLD.upload_source
|
||
OR NEW.digitization_date IS DISTINCT FROM OLD.digitization_date
|
||
OR NEW.uploaded_by IS DISTINCT FROM OLD.uploaded_by
|
||
OR NEW.version IS DISTINCT FROM OLD.version
|
||
OR NEW.original_id IS DISTINCT FROM OLD.original_id
|
||
OR NEW.is_current_version IS DISTINCT FROM OLD.is_current_version
|
||
THEN
|
||
INSERT INTO public.audit_log (user_id, company_id, action, table_name, record_id, description)
|
||
VALUES (OLD.user_id, OLD.company_id, 'SECURITY_EVENT', 'document_attachments', OLD.id,
|
||
'Blocked metadata modification of document linked to ' || v_entry_status || ' entry ' || OLD.journal_entry_id);
|
||
|
||
RAISE EXCEPTION 'Cannot modify metadata of document linked to a % journal entry (BFL 7 kap)', v_entry_status;
|
||
END IF;
|
||
|
||
RETURN NEW;
|
||
END;
|
||
$function$;
|
||
|
||
-- 4g. create_document_version — document versioning with hash chain
|
||
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 TO 'public'
|
||
AS $function$
|
||
DECLARE
|
||
v_current document_attachments%ROWTYPE;
|
||
v_new_id uuid;
|
||
v_root_id uuid;
|
||
v_next_version integer;
|
||
BEGIN
|
||
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;
|
||
|
||
v_root_id := COALESCE(v_current.original_id, v_current.id);
|
||
v_next_version := v_current.version + 1;
|
||
|
||
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
|
||
)
|
||
RETURNING id INTO v_new_id;
|
||
|
||
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;
|
||
$function$;
|
||
|
||
-- 4h. validate_version_chain — validates document version hash chain
|
||
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 TO 'public'
|
||
AS $function$
|
||
DECLARE
|
||
v_root_id uuid;
|
||
BEGIN
|
||
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;
|
||
|
||
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;
|
||
$function$;
|
||
|
||
-- 4i. write_audit_log — comprehensive audit logging
|
||
CREATE OR REPLACE FUNCTION public.write_audit_log()
|
||
RETURNS trigger
|
||
LANGUAGE plpgsql
|
||
SECURITY DEFINER
|
||
AS $function$
|
||
DECLARE
|
||
v_user_id uuid;
|
||
v_company_id uuid;
|
||
v_action text;
|
||
v_old_state jsonb;
|
||
v_new_state jsonb;
|
||
v_record_id uuid;
|
||
v_desc text;
|
||
BEGIN
|
||
IF TG_OP = 'DELETE' THEN
|
||
v_old_state := to_jsonb(OLD);
|
||
v_new_state := NULL;
|
||
v_record_id := OLD.id;
|
||
v_user_id := (v_old_state->>'user_id')::uuid;
|
||
v_company_id := (v_old_state->>'company_id')::uuid;
|
||
v_action := 'DELETE';
|
||
v_desc := 'Deleted ' || TG_TABLE_NAME || ' record';
|
||
ELSIF TG_OP = 'INSERT' THEN
|
||
v_old_state := NULL;
|
||
v_new_state := to_jsonb(NEW);
|
||
v_record_id := NEW.id;
|
||
v_user_id := (v_new_state->>'user_id')::uuid;
|
||
v_company_id := (v_new_state->>'company_id')::uuid;
|
||
v_action := 'INSERT';
|
||
v_desc := 'Created ' || TG_TABLE_NAME || ' record';
|
||
ELSIF TG_OP = 'UPDATE' THEN
|
||
v_old_state := to_jsonb(OLD);
|
||
v_new_state := to_jsonb(NEW);
|
||
v_record_id := COALESCE(NEW.id, OLD.id);
|
||
v_user_id := COALESCE((v_new_state->>'user_id')::uuid, (v_old_state->>'user_id')::uuid);
|
||
v_company_id := COALESCE((v_new_state->>'company_id')::uuid, (v_old_state->>'company_id')::uuid);
|
||
v_action := 'UPDATE';
|
||
v_desc := 'Updated ' || TG_TABLE_NAME || ' record';
|
||
|
||
IF TG_TABLE_NAME = 'journal_entries' THEN
|
||
IF OLD.status = 'draft' AND NEW.status = 'posted' THEN
|
||
v_action := 'COMMIT';
|
||
v_desc := 'Committed journal entry ' || NEW.voucher_series || NEW.voucher_number;
|
||
ELSIF OLD.status = 'posted' AND NEW.status = 'reversed' THEN
|
||
v_action := 'REVERSE';
|
||
v_desc := 'Reversed journal entry ' || OLD.voucher_series || OLD.voucher_number;
|
||
END IF;
|
||
END IF;
|
||
|
||
IF TG_TABLE_NAME = 'fiscal_periods' THEN
|
||
IF (OLD.locked_at IS NULL AND NEW.locked_at IS NOT NULL) THEN
|
||
v_action := 'LOCK_PERIOD';
|
||
v_desc := 'Locked fiscal period "' || NEW.name || '"';
|
||
ELSIF (NOT OLD.is_closed AND NEW.is_closed) THEN
|
||
v_action := 'CLOSE_PERIOD';
|
||
v_desc := 'Closed fiscal period "' || NEW.name || '"';
|
||
END IF;
|
||
END IF;
|
||
END IF;
|
||
|
||
v_user_id := COALESCE(v_user_id, auth.uid());
|
||
|
||
INSERT INTO public.audit_log (user_id, company_id, action, table_name, record_id, actor_id, old_state, new_state, description)
|
||
VALUES (v_user_id, v_company_id, v_action, TG_TABLE_NAME, v_record_id, v_user_id, v_old_state, v_new_state, v_desc);
|
||
|
||
IF TG_OP = 'DELETE' THEN
|
||
RETURN OLD;
|
||
END IF;
|
||
RETURN NEW;
|
||
END;
|
||
$function$;
|
||
|
||
-- 4j. delete_last_voucher — deletes only the last voucher in a series
|
||
CREATE OR REPLACE FUNCTION public.delete_last_voucher(p_company_id uuid, p_entry_id uuid)
|
||
RETURNS jsonb
|
||
LANGUAGE plpgsql
|
||
SECURITY DEFINER
|
||
SET search_path TO 'public'
|
||
AS $function$
|
||
DECLARE
|
||
v_entry record;
|
||
v_period record;
|
||
v_max_voucher integer;
|
||
v_ref_count integer;
|
||
v_caller_role text;
|
||
v_snapshot jsonb;
|
||
v_lines_snapshot jsonb;
|
||
BEGIN
|
||
SELECT cm.role INTO v_caller_role
|
||
FROM company_members cm
|
||
WHERE cm.company_id = p_company_id
|
||
AND cm.user_id = auth.uid();
|
||
|
||
IF v_caller_role IS NULL OR v_caller_role NOT IN ('owner', 'admin') THEN
|
||
RAISE EXCEPTION 'Only company owners and admins can delete vouchers';
|
||
END IF;
|
||
|
||
SELECT * INTO v_entry
|
||
FROM journal_entries
|
||
WHERE id = p_entry_id
|
||
AND company_id = p_company_id
|
||
FOR UPDATE;
|
||
|
||
IF v_entry IS NULL THEN
|
||
RAISE EXCEPTION 'Journal entry not found';
|
||
END IF;
|
||
|
||
IF v_entry.status != 'posted' THEN
|
||
RAISE EXCEPTION 'Only posted entries can be deleted (current status: %)', v_entry.status;
|
||
END IF;
|
||
|
||
SELECT * INTO v_period
|
||
FROM fiscal_periods
|
||
WHERE id = v_entry.fiscal_period_id
|
||
FOR UPDATE;
|
||
|
||
IF v_period.is_closed THEN
|
||
RAISE EXCEPTION 'Cannot delete voucher in a closed fiscal period';
|
||
END IF;
|
||
|
||
IF v_period.locked_at IS NOT NULL THEN
|
||
RAISE EXCEPTION 'Cannot delete voucher in a locked fiscal period';
|
||
END IF;
|
||
|
||
PERFORM 1 FROM voucher_sequences
|
||
WHERE company_id = p_company_id
|
||
AND fiscal_period_id = v_entry.fiscal_period_id
|
||
AND voucher_series = v_entry.voucher_series
|
||
FOR UPDATE;
|
||
|
||
SELECT MAX(voucher_number) INTO v_max_voucher
|
||
FROM journal_entries
|
||
WHERE company_id = p_company_id
|
||
AND fiscal_period_id = v_entry.fiscal_period_id
|
||
AND voucher_series = v_entry.voucher_series
|
||
AND status NOT IN ('cancelled', 'draft');
|
||
|
||
IF v_entry.voucher_number != v_max_voucher THEN
|
||
RAISE EXCEPTION 'Kan bara radera det sista verifikatet i serien. % har nummer % men senaste är %',
|
||
v_entry.voucher_series, v_entry.voucher_number, v_max_voucher;
|
||
END IF;
|
||
|
||
SELECT COUNT(*) INTO v_ref_count
|
||
FROM journal_entries
|
||
WHERE company_id = p_company_id
|
||
AND status != 'cancelled'
|
||
AND (reverses_id = p_entry_id OR correction_of_id = p_entry_id);
|
||
|
||
IF v_ref_count > 0 THEN
|
||
RAISE EXCEPTION 'Cannot delete: other entries reference this voucher (% references)',
|
||
v_ref_count;
|
||
END IF;
|
||
|
||
SELECT jsonb_agg(to_jsonb(l)) INTO v_lines_snapshot
|
||
FROM journal_entry_lines l
|
||
WHERE l.journal_entry_id = p_entry_id;
|
||
|
||
v_snapshot := to_jsonb(v_entry) || jsonb_build_object('lines', COALESCE(v_lines_snapshot, '[]'::jsonb));
|
||
|
||
IF v_entry.reverses_id IS NOT NULL THEN
|
||
PERFORM set_config('gnubok.allow_delete', 'true', true);
|
||
UPDATE journal_entries
|
||
SET status = 'posted', reversed_by_id = NULL
|
||
WHERE id = v_entry.reverses_id
|
||
AND company_id = p_company_id;
|
||
END IF;
|
||
|
||
PERFORM set_config('gnubok.allow_delete', 'true', true);
|
||
|
||
UPDATE document_attachments
|
||
SET journal_entry_id = NULL
|
||
WHERE journal_entry_id = p_entry_id;
|
||
|
||
DELETE FROM journal_entries WHERE id = p_entry_id;
|
||
|
||
UPDATE voucher_sequences
|
||
SET last_number = GREATEST(last_number - 1, 0)
|
||
WHERE company_id = p_company_id
|
||
AND fiscal_period_id = v_entry.fiscal_period_id
|
||
AND voucher_series = v_entry.voucher_series;
|
||
|
||
INSERT INTO audit_log (user_id, action, table_name, record_id, actor_id, old_state, description)
|
||
VALUES (
|
||
v_entry.user_id,
|
||
'DELETE',
|
||
'journal_entries',
|
||
p_entry_id,
|
||
auth.uid(),
|
||
v_snapshot,
|
||
'Deleted voucher ' || v_entry.voucher_series || v_entry.voucher_number ||
|
||
' (delete_last_voucher RPC, caller: ' || auth.uid() || ')'
|
||
);
|
||
|
||
RETURN jsonb_build_object(
|
||
'deleted', true,
|
||
'voucher_series', v_entry.voucher_series,
|
||
'voucher_number', v_entry.voucher_number
|
||
);
|
||
END;
|
||
$function$;
|
||
|
||
-- 4k. replace_sie_import — cancels entries from a SIE import
|
||
CREATE OR REPLACE FUNCTION public.replace_sie_import(p_company_id uuid, p_import_id uuid)
|
||
RETURNS integer
|
||
LANGUAGE plpgsql
|
||
SECURITY DEFINER
|
||
SET search_path TO 'public'
|
||
AS $function$
|
||
DECLARE
|
||
v_cancelled integer;
|
||
v_fiscal_period_id uuid;
|
||
v_opening_balance_entry_id uuid;
|
||
BEGIN
|
||
SELECT fiscal_period_id, opening_balance_entry_id
|
||
INTO v_fiscal_period_id, v_opening_balance_entry_id
|
||
FROM public.sie_imports
|
||
WHERE id = p_import_id AND company_id = p_company_id AND status = 'completed';
|
||
|
||
IF NOT FOUND THEN
|
||
RAISE EXCEPTION 'Import % not found or not in completed status', p_import_id;
|
||
END IF;
|
||
|
||
UPDATE public.journal_entries
|
||
SET status = 'cancelled'
|
||
WHERE company_id = p_company_id
|
||
AND status = 'posted'
|
||
AND id IN (
|
||
SELECT v_opening_balance_entry_id WHERE v_opening_balance_entry_id IS NOT NULL
|
||
UNION ALL
|
||
SELECT je.id FROM public.journal_entries je
|
||
WHERE je.company_id = p_company_id
|
||
AND je.fiscal_period_id = v_fiscal_period_id
|
||
AND je.source_type = 'import'
|
||
AND je.status = 'posted'
|
||
);
|
||
GET DIAGNOSTICS v_cancelled = ROW_COUNT;
|
||
|
||
UPDATE public.sie_imports
|
||
SET status = 'replaced', replaced_at = now()
|
||
WHERE id = p_import_id AND company_id = p_company_id;
|
||
|
||
RETURN v_cancelled;
|
||
END;
|
||
$function$;
|
||
|
||
-- 4l. get_unlinked_1930_lines — bank reconciliation helper
|
||
CREATE OR REPLACE FUNCTION public.get_unlinked_1930_lines(
|
||
p_company_id uuid,
|
||
p_date_from date DEFAULT NULL,
|
||
p_date_to date DEFAULT NULL
|
||
)
|
||
RETURNS TABLE(
|
||
line_id uuid, journal_entry_id uuid, debit_amount numeric,
|
||
credit_amount numeric, line_description text, entry_date date,
|
||
voucher_number integer, voucher_series text, entry_description text,
|
||
source_type text
|
||
)
|
||
LANGUAGE sql
|
||
STABLE SECURITY DEFINER
|
||
SET search_path TO 'public'
|
||
AS $function$
|
||
SELECT
|
||
jel.id AS line_id,
|
||
je.id AS journal_entry_id,
|
||
jel.debit_amount,
|
||
jel.credit_amount,
|
||
jel.line_description,
|
||
je.entry_date,
|
||
je.voucher_number,
|
||
je.voucher_series,
|
||
je.description AS entry_description,
|
||
je.source_type
|
||
FROM public.journal_entry_lines jel
|
||
JOIN public.journal_entries je ON je.id = jel.journal_entry_id
|
||
WHERE jel.account_number = '1930'
|
||
AND je.company_id = p_company_id
|
||
AND je.status = 'posted'
|
||
AND (p_date_from IS NULL OR je.entry_date >= p_date_from)
|
||
AND (p_date_to IS NULL OR je.entry_date <= p_date_to)
|
||
AND NOT EXISTS (
|
||
SELECT 1
|
||
FROM public.transactions t
|
||
WHERE t.journal_entry_id = je.id
|
||
AND t.company_id = p_company_id
|
||
)
|
||
ORDER BY je.entry_date, je.voucher_number;
|
||
$function$;
|
||
|
||
-- 4m. delete_user_account — full account deletion with cascade blockers
|
||
CREATE OR REPLACE FUNCTION public.delete_user_account(target_user_id uuid)
|
||
RETURNS void
|
||
LANGUAGE plpgsql
|
||
SECURITY DEFINER
|
||
SET search_path TO 'public'
|
||
SET statement_timeout TO '60s'
|
||
SET lock_timeout TO '10s'
|
||
AS $function$
|
||
DECLARE
|
||
v_company_ids uuid[];
|
||
BEGIN
|
||
IF auth.uid() IS DISTINCT FROM target_user_id THEN
|
||
RAISE EXCEPTION 'Can only delete your own account';
|
||
END IF;
|
||
|
||
SELECT array_agg(id) INTO v_company_ids
|
||
FROM public.companies
|
||
WHERE created_by = target_user_id;
|
||
|
||
DELETE FROM public.user_preferences WHERE user_id = target_user_id;
|
||
DELETE FROM public.extension_data WHERE user_id = target_user_id;
|
||
|
||
ALTER TABLE audit_log DISABLE TRIGGER audit_log_no_delete;
|
||
ALTER TABLE payment_match_log DISABLE TRIGGER payment_match_log_no_delete;
|
||
ALTER TABLE document_attachments DISABLE TRIGGER block_document_deletion;
|
||
ALTER TABLE journal_entries DISABLE TRIGGER enforce_journal_entry_immutability;
|
||
ALTER TABLE journal_entries DISABLE TRIGGER enforce_retention_journal_entries;
|
||
ALTER TABLE journal_entry_lines DISABLE TRIGGER enforce_journal_entry_line_immutability;
|
||
|
||
ALTER TABLE api_keys DISABLE TRIGGER audit_api_keys;
|
||
ALTER TABLE chart_of_accounts DISABLE TRIGGER audit_chart_of_accounts;
|
||
ALTER TABLE company_settings DISABLE TRIGGER audit_company_settings;
|
||
ALTER TABLE document_attachments DISABLE TRIGGER audit_document_attachments;
|
||
ALTER TABLE extension_data DISABLE TRIGGER audit_extension_data;
|
||
ALTER TABLE fiscal_periods DISABLE TRIGGER audit_fiscal_periods;
|
||
ALTER TABLE journal_entries DISABLE TRIGGER audit_journal_entries;
|
||
ALTER TABLE supplier_invoices DISABLE TRIGGER audit_supplier_invoices;
|
||
|
||
IF v_company_ids IS NOT NULL THEN
|
||
DELETE FROM public.audit_log
|
||
WHERE company_id = ANY(v_company_ids);
|
||
|
||
UPDATE public.fiscal_periods
|
||
SET previous_period_id = NULL,
|
||
closing_entry_id = NULL,
|
||
opening_balance_entry_id = NULL
|
||
WHERE company_id = ANY(v_company_ids);
|
||
END IF;
|
||
|
||
DELETE FROM auth.users WHERE id = target_user_id;
|
||
|
||
ALTER TABLE audit_log ENABLE TRIGGER audit_log_no_delete;
|
||
ALTER TABLE payment_match_log ENABLE TRIGGER payment_match_log_no_delete;
|
||
ALTER TABLE document_attachments ENABLE TRIGGER block_document_deletion;
|
||
ALTER TABLE journal_entries ENABLE TRIGGER enforce_journal_entry_immutability;
|
||
ALTER TABLE journal_entries ENABLE TRIGGER enforce_retention_journal_entries;
|
||
ALTER TABLE journal_entry_lines ENABLE TRIGGER enforce_journal_entry_line_immutability;
|
||
ALTER TABLE api_keys ENABLE TRIGGER audit_api_keys;
|
||
ALTER TABLE chart_of_accounts ENABLE TRIGGER audit_chart_of_accounts;
|
||
ALTER TABLE company_settings ENABLE TRIGGER audit_company_settings;
|
||
ALTER TABLE document_attachments ENABLE TRIGGER audit_document_attachments;
|
||
ALTER TABLE extension_data ENABLE TRIGGER audit_extension_data;
|
||
ALTER TABLE fiscal_periods ENABLE TRIGGER audit_fiscal_periods;
|
||
ALTER TABLE journal_entries ENABLE TRIGGER audit_journal_entries;
|
||
ALTER TABLE supplier_invoices ENABLE TRIGGER audit_supplier_invoices;
|
||
END;
|
||
$function$;
|
||
|
||
|
||
-- ============================================================================
|
||
-- 5. TRIGGERS on existing tables
|
||
-- ============================================================================
|
||
|
||
-- Document metadata immutability enforcement
|
||
DROP TRIGGER IF EXISTS enforce_document_metadata_immutability ON public.document_attachments;
|
||
CREATE TRIGGER enforce_document_metadata_immutability
|
||
BEFORE UPDATE ON public.document_attachments
|
||
FOR EACH ROW EXECUTE FUNCTION public.enforce_document_metadata_immutability();
|
||
|
||
|
||
-- ============================================================================
|
||
-- 6. MISSING DELETE POLICIES on existing tables
|
||
-- ============================================================================
|
||
|
||
DO $$
|
||
DECLARE
|
||
r record;
|
||
BEGIN
|
||
-- Tables with a direct company_id column
|
||
FOR r IN
|
||
SELECT unnest(ARRAY[
|
||
'api_keys', 'bank_connections', 'bank_file_imports', 'calendar_feeds',
|
||
'categorization_templates', 'chart_of_accounts', 'company_settings',
|
||
'cost_centers', 'customers', 'deadlines', 'document_attachments',
|
||
'extension_data', 'fiscal_periods',
|
||
'invoice_inbox_items', 'invoice_payments',
|
||
'invoice_reminders', 'invoices', 'journal_entries',
|
||
'mapping_rules', 'projects',
|
||
'receipts',
|
||
'sie_account_mappings', 'sie_imports', 'skatteverket_tokens',
|
||
'supplier_invoice_payments',
|
||
'supplier_invoices', 'suppliers', 'transactions'
|
||
]) AS tbl
|
||
LOOP
|
||
IF NOT EXISTS (
|
||
SELECT 1 FROM pg_policies
|
||
WHERE schemaname = 'public' AND tablename = r.tbl AND cmd = 'DELETE'
|
||
) THEN
|
||
EXECUTE format(
|
||
'CREATE POLICY %I ON public.%I FOR DELETE USING (company_id IN (SELECT user_company_ids()))',
|
||
r.tbl || '_delete', r.tbl
|
||
);
|
||
END IF;
|
||
END LOOP;
|
||
|
||
-- Tables that join through a parent to reach company_id
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='invoice_items' AND cmd='DELETE') THEN
|
||
CREATE POLICY invoice_items_delete ON public.invoice_items FOR DELETE
|
||
USING (invoice_id IN (SELECT id FROM invoices WHERE company_id IN (SELECT user_company_ids())));
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='journal_entry_lines' AND cmd='DELETE') THEN
|
||
CREATE POLICY journal_entry_lines_delete ON public.journal_entry_lines FOR DELETE
|
||
USING (journal_entry_id IN (SELECT id FROM journal_entries WHERE company_id IN (SELECT user_company_ids())));
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='receipt_line_items' AND cmd='DELETE') THEN
|
||
CREATE POLICY receipt_line_items_delete ON public.receipt_line_items FOR DELETE
|
||
USING (receipt_id IN (SELECT id FROM receipts WHERE company_id IN (SELECT user_company_ids())));
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='supplier_invoice_items' AND cmd='DELETE') THEN
|
||
CREATE POLICY supplier_invoice_items_delete ON public.supplier_invoice_items FOR DELETE
|
||
USING (supplier_invoice_id IN (SELECT id FROM supplier_invoices WHERE company_id IN (SELECT user_company_ids())));
|
||
END IF;
|
||
|
||
-- User-scoped tables (no company_id — use auth.uid())
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='extension_toggles' AND cmd='DELETE') THEN
|
||
CREATE POLICY extension_toggles_delete ON public.extension_toggles FOR DELETE
|
||
USING (auth.uid() = user_id);
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='notification_settings' AND cmd='DELETE') THEN
|
||
CREATE POLICY notification_settings_delete ON public.notification_settings FOR DELETE
|
||
USING (auth.uid() = user_id);
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='push_subscriptions' AND cmd='DELETE') THEN
|
||
CREATE POLICY push_subscriptions_delete ON public.push_subscriptions FOR DELETE
|
||
USING (auth.uid() = user_id);
|
||
END IF;
|
||
|
||
-- Tables that use other policy patterns
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='chat_sessions' AND cmd='DELETE') THEN
|
||
CREATE POLICY chat_sessions_delete ON public.chat_sessions FOR DELETE
|
||
USING (company_id IN (SELECT user_company_ids()));
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='chat_messages' AND cmd='DELETE') THEN
|
||
CREATE POLICY chat_messages_delete ON public.chat_messages FOR DELETE
|
||
USING (company_id IN (SELECT user_company_ids()));
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='company_members' AND cmd='DELETE') THEN
|
||
CREATE POLICY company_members_delete ON public.company_members FOR DELETE
|
||
USING (company_id IN (SELECT user_company_ids()));
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='company_invitations' AND cmd='DELETE') THEN
|
||
CREATE POLICY company_invitations_delete ON public.company_invitations FOR DELETE
|
||
USING (company_id IN (SELECT user_company_ids()));
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='team_invitations' AND cmd='DELETE') THEN
|
||
CREATE POLICY team_invitations_delete ON public.team_invitations FOR DELETE
|
||
USING (team_id IN (SELECT user_team_ids()));
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='team_members' AND cmd='DELETE') THEN
|
||
CREATE POLICY team_members_delete ON public.team_members FOR DELETE
|
||
USING (team_id IN (SELECT user_team_ids()));
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='provider_consents' AND cmd='DELETE') THEN
|
||
CREATE POLICY provider_consents_delete ON public.provider_consents FOR DELETE
|
||
USING (company_id IN (SELECT user_company_ids()));
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='provider_consent_tokens' AND cmd='DELETE') THEN
|
||
CREATE POLICY provider_consent_tokens_delete ON public.provider_consent_tokens FOR DELETE
|
||
USING (consent_id IN (SELECT id FROM provider_consents WHERE company_id IN (SELECT user_company_ids())));
|
||
END IF;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='provider_otc' AND cmd='DELETE') THEN
|
||
CREATE POLICY provider_otc_delete ON public.provider_otc FOR DELETE
|
||
USING (consent_id IN (SELECT id FROM provider_consents WHERE company_id IN (SELECT user_company_ids())));
|
||
END IF;
|
||
END $$;
|
||
|
||
|
||
-- ============================================================================
|
||
-- 7. NOTIFY PostgREST to reload schema
|
||
-- ============================================================================
|
||
|
||
NOTIFY pgrst, 'reload schema';
|