-- ============================================================================ -- Migration 009: Whistleblower / Hinweisgebersystem (HinSchG) -- Implements the German Whistleblower Protection Act (Hinweisgeberschutzgesetz) -- ============================================================================ -- Whistleblower reports table CREATE TABLE IF NOT EXISTS whistleblower_reports ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES compliance_tenants(id) ON DELETE CASCADE, -- Identification reference_number VARCHAR(20) NOT NULL UNIQUE, -- e.g. "WB-2026-0001" access_key VARCHAR(50) NOT NULL UNIQUE, -- for anonymous reporter access -- Report content category VARCHAR(50) NOT NULL, -- corruption, fraud, data_protection, discrimination, environment, competition, product_safety, tax_evasion, other status VARCHAR(50) NOT NULL DEFAULT 'new', -- new, acknowledged, under_review, investigation, measures_taken, closed, rejected title VARCHAR(500) NOT NULL, description TEXT NOT NULL, -- Reporter info (nullable for anonymous reports) is_anonymous BOOLEAN NOT NULL DEFAULT TRUE, reporter_name VARCHAR(255), reporter_email VARCHAR(255), reporter_phone VARCHAR(100), -- HinSchG deadlines received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deadline_acknowledgment TIMESTAMPTZ NOT NULL, -- 7 days from received_at per HinSchG deadline_feedback TIMESTAMPTZ NOT NULL, -- 3 months from received_at per HinSchG -- Status timestamps acknowledged_at TIMESTAMPTZ, closed_at TIMESTAMPTZ, -- Assignment assigned_to UUID, -- user responsible for handling -- Audit trail (JSONB array of {timestamp, action, user_id, details}) audit_trail JSONB NOT NULL DEFAULT '[]', -- Resolution resolution TEXT, -- Timestamps created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Whistleblower messages table (anonymous communication channel) CREATE TABLE IF NOT EXISTS whistleblower_messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), report_id UUID NOT NULL REFERENCES whistleblower_reports(id) ON DELETE CASCADE, -- Message direction VARCHAR(30) NOT NULL, -- reporter_to_admin, admin_to_reporter content TEXT NOT NULL, sent_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), read_at TIMESTAMPTZ ); -- Whistleblower measures table (corrective measures) CREATE TABLE IF NOT EXISTS whistleblower_measures ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), report_id UUID NOT NULL REFERENCES whistleblower_reports(id) ON DELETE CASCADE, -- Measure details title VARCHAR(500) NOT NULL, description TEXT, status VARCHAR(50) NOT NULL DEFAULT 'planned', -- planned, in_progress, completed responsible VARCHAR(255), due_date TIMESTAMPTZ, completed_at TIMESTAMPTZ, -- Timestamps created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Sequence table for reference number generation CREATE TABLE IF NOT EXISTS whistleblower_sequences ( tenant_id UUID NOT NULL REFERENCES compliance_tenants(id) ON DELETE CASCADE, year INT NOT NULL, last_sequence INT NOT NULL DEFAULT 0, PRIMARY KEY (tenant_id, year) ); -- ============================================================================ -- Indexes -- ============================================================================ -- Report indexes CREATE INDEX IF NOT EXISTS idx_whistleblower_reports_tenant ON whistleblower_reports(tenant_id); CREATE INDEX IF NOT EXISTS idx_whistleblower_reports_access_key ON whistleblower_reports(access_key); CREATE INDEX IF NOT EXISTS idx_whistleblower_reports_reference ON whistleblower_reports(reference_number); CREATE INDEX IF NOT EXISTS idx_whistleblower_reports_status ON whistleblower_reports(tenant_id, status); CREATE INDEX IF NOT EXISTS idx_whistleblower_reports_category ON whistleblower_reports(tenant_id, category); CREATE INDEX IF NOT EXISTS idx_whistleblower_reports_received ON whistleblower_reports(tenant_id, received_at); CREATE INDEX IF NOT EXISTS idx_whistleblower_reports_deadlines ON whistleblower_reports(deadline_acknowledgment, deadline_feedback) WHERE acknowledged_at IS NULL OR closed_at IS NULL; -- Message indexes CREATE INDEX IF NOT EXISTS idx_whistleblower_messages_report ON whistleblower_messages(report_id); CREATE INDEX IF NOT EXISTS idx_whistleblower_messages_sent ON whistleblower_messages(report_id, sent_at); -- Measure indexes CREATE INDEX IF NOT EXISTS idx_whistleblower_measures_report ON whistleblower_measures(report_id); CREATE INDEX IF NOT EXISTS idx_whistleblower_measures_status ON whistleblower_measures(report_id, status); -- ============================================================================ -- Triggers -- ============================================================================ -- Reuse existing update_updated_at_column function -- Reports trigger DROP TRIGGER IF EXISTS update_whistleblower_reports_updated_at ON whistleblower_reports; CREATE TRIGGER update_whistleblower_reports_updated_at BEFORE UPDATE ON whistleblower_reports FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- Measures trigger DROP TRIGGER IF EXISTS update_whistleblower_measures_updated_at ON whistleblower_measures; CREATE TRIGGER update_whistleblower_measures_updated_at BEFORE UPDATE ON whistleblower_measures FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- ============================================================================ -- Comments -- ============================================================================ COMMENT ON TABLE whistleblower_reports IS 'Whistleblower reports per HinSchG (Hinweisgeberschutzgesetz)'; COMMENT ON TABLE whistleblower_messages IS 'Anonymous communication channel between reporter and admin'; COMMENT ON TABLE whistleblower_measures IS 'Corrective measures taken for whistleblower reports'; COMMENT ON TABLE whistleblower_sequences IS 'Sequence numbers for reference number generation per tenant and year'; COMMENT ON COLUMN whistleblower_reports.reference_number IS 'Human-readable reference number (e.g. WB-2026-0001)'; COMMENT ON COLUMN whistleblower_reports.access_key IS 'Secret key for anonymous reporter to access their report'; COMMENT ON COLUMN whistleblower_reports.deadline_acknowledgment IS '7-day deadline per HinSchG §17 Abs. 1'; COMMENT ON COLUMN whistleblower_reports.deadline_feedback IS '3-month deadline per HinSchG §17 Abs. 2'; COMMENT ON COLUMN whistleblower_reports.audit_trail IS 'JSON array of audit entries tracking all actions on the report';