ProofedMesh/db/trust/migrations/0001_per_diem_rates.up.sql

96 lines
4.8 KiB
MySQL
Raw Permalink Normal View History

-- Migration: 0001_per_diem_rates
-- Legt die Tabelle für Verpflegungsmehraufwands-Pauschalen (BMF-Sätze etc.) an.
-- Siehe docs/architecture/v1.1.md §6.2 und schemas/ds/per_diem/v1.0.0.json.
--
-- Design-Entscheidungen:
-- * rate_id ist eine stabile textuelle ID "PDR-<n>", vergeben über eine Sequence.
-- * Kernfelder (Beträge, country, valid_from, source_regulation) sind unveränderlich,
-- erzwungen per BEFORE UPDATE Trigger. Nur valid_to und rate_change_attestation_ref
-- dürfen nachträglich gesetzt werden (für kontrolliertes Ablaufen bzw. das Nachtragen
-- der Attestations-Referenz).
-- * Keine Überlappung von Gültigkeitsbereichen pro (country, city) — EXCLUDE-Constraint
-- mit GiST + daterange, benötigt die btree_gist Extension.
-- * Betrage als NUMERIC(10,2) — Geld, keine Floats.
CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE SEQUENCE IF NOT EXISTS per_diem_rates_seq START 1;
CREATE TABLE IF NOT EXISTS per_diem_rates (
rate_id TEXT PRIMARY KEY
DEFAULT ('PDR-' || nextval('per_diem_rates_seq')),
country TEXT NOT NULL CHECK (country ~ '^[A-Z]{2}$'),
city TEXT,
currency TEXT NOT NULL CHECK (currency ~ '^[A-Z]{3}$'),
full_day NUMERIC(10, 2) NOT NULL CHECK (full_day >= 0),
partial_day_min8h NUMERIC(10, 2) NOT NULL CHECK (partial_day_min8h >= 0),
arrival_day NUMERIC(10, 2) NOT NULL CHECK (arrival_day >= 0),
departure_day NUMERIC(10, 2) NOT NULL CHECK (departure_day >= 0),
valid_from DATE NOT NULL,
valid_to DATE,
source_regulation TEXT NOT NULL CHECK (length(source_regulation) BETWEEN 1 AND 500),
created_by TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
rate_change_attestation_ref TEXT CHECK (rate_change_attestation_ref ~ '^sha256:[0-9a-f]{64}$'),
CONSTRAINT per_diem_rates_valid_range CHECK (valid_to IS NULL OR valid_to >= valid_from)
);
CREATE INDEX idx_pdr_country_valid_from
ON per_diem_rates (country, valid_from);
CREATE INDEX idx_pdr_lookup_active
ON per_diem_rates (country, valid_from)
WHERE valid_to IS NULL;
-- Prevent overlapping validity windows per (country, city).
-- COALESCE(city, '') normalises NULL to a sentinel so NULL-city overlaps are also caught.
ALTER TABLE per_diem_rates
ADD CONSTRAINT per_diem_rates_no_overlap EXCLUDE USING gist (
country WITH =,
COALESCE(city, '') WITH =,
daterange(valid_from, COALESCE(valid_to, 'infinity'::date), '[]') WITH &&
);
-- Immutability trigger: only valid_to and rate_change_attestation_ref may be updated.
CREATE OR REPLACE FUNCTION per_diem_rates_prevent_rewrite() RETURNS TRIGGER AS $$
BEGIN
IF NEW.rate_id IS DISTINCT FROM OLD.rate_id
OR NEW.country IS DISTINCT FROM OLD.country
OR NEW.city IS DISTINCT FROM OLD.city
OR NEW.currency IS DISTINCT FROM OLD.currency
OR NEW.full_day IS DISTINCT FROM OLD.full_day
OR NEW.partial_day_min8h IS DISTINCT FROM OLD.partial_day_min8h
OR NEW.arrival_day IS DISTINCT FROM OLD.arrival_day
OR NEW.departure_day IS DISTINCT FROM OLD.departure_day
OR NEW.valid_from IS DISTINCT FROM OLD.valid_from
OR NEW.source_regulation IS DISTINCT FROM OLD.source_regulation
OR NEW.created_by IS DISTINCT FROM OLD.created_by
OR NEW.created_at IS DISTINCT FROM OLD.created_at
THEN
RAISE EXCEPTION 'per_diem_rates: only valid_to and rate_change_attestation_ref may be updated (rate_id=%)', OLD.rate_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER per_diem_rates_prevent_rewrite_trg
BEFORE UPDATE ON per_diem_rates
FOR EACH ROW
EXECUTE FUNCTION per_diem_rates_prevent_rewrite();
-- Audit log for admin actions on per_diem_rates. The authoritative audit lives
-- in the Transparency Log (rate_change attestations); this table is a local
-- convenience mirror for operators to query without hitting Trillian.
CREATE TABLE IF NOT EXISTS per_diem_rate_changes (
id BIGSERIAL PRIMARY KEY,
rate_id TEXT NOT NULL REFERENCES per_diem_rates(rate_id),
change_kind TEXT NOT NULL CHECK (change_kind IN ('create', 'retire')),
previous_valid_to DATE,
new_valid_to DATE,
performed_by TEXT NOT NULL,
performed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
rate_change_attestation_ref TEXT NOT NULL CHECK (rate_change_attestation_ref ~ '^sha256:[0-9a-f]{64}$')
);
CREATE INDEX idx_pdr_changes_rate_id ON per_diem_rate_changes (rate_id, performed_at DESC);