Files
alpenwerk-hr/db/migrations/20260814120000_kuendigungsschutz.sql
Maximilian Stubhan b87c8ad64c
Some checks failed
CI / Lint, Typen, Tests, Build (push) Failing after 5m40s
CI / Migrationen auf leerer Datenbank (push) Has been cancelled
Remove Supabase
The database moved to a container of our own; the platform is gone.
This takes out what was left of it — and, where the leftovers were load
bearing, moves rather than deletes.

Moved, not deleted:

  supabase/migrations/  -> db/migrations/      the schema's source of truth
  supabase/build-org.ts -> scripts/build-org.ts
  lib/supabase/types.ts -> lib/types.ts        52 import sites repointed

The bookkeeping needed care. It lived in `supabase_migrations.schema_migrations`,
and simply renaming the schema would have left the runner facing an empty
table: it would have called all 67 migrations pending and replayed them
against a database that is long since current. So the runner now creates
`migrationen.schema_migrations` and, once, copies the old rows across —
guarded so a second run does nothing and a fresh database skips it entirely.
Only then does migration 20260907100000 drop the old schema.

Deleted: the CLI config, the seed, the historical schema/function dumps
(nothing read them), scripts/umzug-von-supabase.sh (the move is done), and
both Supabase packages plus the CLI. Nothing in the application imported
them — the build now succeeds with no environment variables at all, which
is the proof.

Integration tests: six of them signed in through Supabase Auth and asserted
against the anon key and the service role. That model is gone, so the tests
were not portable — they are deleted. session-context and
employee-status-filter already ran on pg and are untouched; om-reporting is
ported to a direct connection because it guards a real risk (the reporting
line rule exists twice, once in SQL and once in TypeScript).

CI: the integration job started a Supabase stack. It now runs a postgres
service, applies deploy/db-init and every migration to an empty database —
that was the valuable part, and it still holds — then checks that a second
run is a no-op, which is what proves the bookkeeping works.

Docs: security-review.md audited a service-role key, a cookie adapter and
auth.users, none of which exist. Restating findings about removed components
would suggest today's system had been reviewed; it has not. It now records
what was removed and says a fresh review is due. data-model.md was already
marked obsolete and described the pre-OM schema; azure-migration.md was a
plan for a route not taken. Both deleted.

Verified: npm ci, typecheck, lint, 445 tests, build — all clean without the
packages. Integration tests skip cleanly with no database. Migration SQL and
the runner are reviewed but NOT executed: no Docker here, and the old
instance no longer resolves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 10:43:22 +02:00

358 lines
23 KiB
PL/PgSQL

-- Besonderer Kündigungsschutz.
--
-- Betriebsratsmitglieder, Schwangere, Eltern in Karenz, begünstigte
-- Behinderte, Lehrlinge, Präsenzdiener — für sie gelten eigene Regeln, bevor
-- ein Dienstverhältnis beendet werden darf. Das Werkzeug entscheidet das
-- nicht, aber es soll niemanden einen Austritt erfassen lassen, ohne es zu
-- erwähnen.
--
-- Zwei Spalten, wie beim Dienstwagen: ein Kennzeichen und eine Angabe, die
-- nur mit ihm zusammen Sinn ergibt. Das Datum ist **freiwillig** — bei einem
-- Betriebsratsmandat steht das Ende oft fest, bei einer Schwangerschaft
-- nicht, und ein Pflichtfeld zwänge dann zu einer erfundenen Zahl.
--
-- Die Bedingung sagt nur, was ohne das Kennzeichen nicht sein darf. Sie
-- verlangt umgekehrt kein Datum.
alter table employees
add column if not exists has_kuendigungsschutz boolean not null default false,
add column if not exists kuendigungsschutz_bis date;
comment on column employees.has_kuendigungsschutz is
'Besonderer Kündigungsschutz — Betriebsrat, Mutterschutz, Karenz, begünstigte Behinderung, Lehrverhältnis. Löst beim Austritt eine Warnung aus.';
comment on column employees.kuendigungsschutz_bis is
'Ende des Schutzes, falls bekannt. Freiwillig: bei einer Schwangerschaft steht es nicht fest, bei einem Mandat schon.';
alter table employees drop constraint if exists chk_kuendigungsschutz_bis;
alter table employees add constraint chk_kuendigungsschutz_bis
check (has_kuendigungsschutz or kuendigungsschutz_bis is null);
comment on constraint chk_kuendigungsschutz_bis on employees is
'Ein Enddatum ohne Schutz wäre ein Rest, den niemand mehr deuten kann. has_kuendigungsschutz ist NOT NULL, deshalb genügt hier die einfache Oder-Form — anders als bei chk_dienstwagen_art, wo eine nullbare Spalte die Bedingung sonst durchrutschen liesse.';
CREATE OR REPLACE FUNCTION public.app_feld_karte()
RETURNS jsonb
LANGUAGE sql
IMMUTABLE
SET search_path TO 'public', 'pg_temp'
AS $function$
select jsonb_build_object(
'Vorname', jsonb_build_array('first_name', 'text', 'person'),
'Nachname', jsonb_build_array('last_name', 'text', 'person'),
'Geschlecht', jsonb_build_array('gender', 'gender_type', 'person'),
'Geburtsdatum', jsonb_build_array('birth_date', 'date', 'person'),
'SV-Nummer', jsonb_build_array('sv_nummer', 'text', 'person'),
'Staatsbürgerschaft', jsonb_build_array('nationality', 'text', 'person'),
'Adresse', jsonb_build_array('address', 'text', 'person'),
'Postleitzahl', jsonb_build_array('postal_code', 'text', 'person'),
'Ort', jsonb_build_array('city', 'text', 'person'),
'Land', jsonb_build_array('address_country', 'text', 'person'),
'E-Mail', jsonb_build_array('email', 'text', 'person'),
'Telefon', jsonb_build_array('phone', 'text', 'person'),
'Notfallkontakt', jsonb_build_array('emergency_contact_name', 'text', 'person'),
'Notfallkontakt Telefon', jsonb_build_array('emergency_contact_phone', 'text', 'person'),
'Notfallkontakt Verhältnis', jsonb_build_array('emergency_contact_relation', 'text', 'person'),
'Titel (vorangestellt)', jsonb_build_array('title_prefix', 'liste', 'person'),
'Titel (nachgestellt)', jsonb_build_array('title_suffix', 'liste', 'person'),
'Beschäftigungsausmaß', jsonb_build_array('employment_type', 'employment_type', 'contract'),
'Wochenstunden', jsonb_build_array('weekly_hours', 'numeric', 'contract'),
'Vertragsart', jsonb_build_array('contract_type', 'contract_type', 'contract'),
'Befristet bis', jsonb_build_array('contract_end_date', 'date', 'contract'),
'Angestellte:r/Arbeiter:in', jsonb_build_array('worker_type', 'worker_type', 'role'),
'Kollektivvertrag', jsonb_build_array('collective_agreement', 'collective_agreement', 'role'),
'Arbeitstage', jsonb_build_array('work_days', 'liste', 'role'),
'Betriebsrat', jsonb_build_array('is_betriebsrat', 'boolean', 'role'),
'Dienstwagen', jsonb_build_array('has_dienstwagen', 'boolean', 'role'),
'Laterale Führung', jsonb_build_array('is_laterale_fuehrung', 'boolean', 'role'),
'C-Level', jsonb_build_array('is_c_level', 'boolean', 'role'),
'Dienstwagen Antrieb', jsonb_build_array('dienstwagen_art', 'text', 'role'),
'Besonderer Kündigungsschutz', jsonb_build_array('has_kuendigungsschutz', 'boolean', 'role'),
'Kündigungsschutz bis', jsonb_build_array('kuendigungsschutz_bis', 'date', 'role')
);
$function$;
CREATE OR REPLACE FUNCTION public.hire_employee(payload jsonb)
RETURNS uuid
LANGUAGE plpgsql
SET search_path TO 'public', 'pg_temp'
AS $function$
declare
v_id uuid;
v_position_id uuid := (payload->>'position_id')::uuid;
v_entry date := (payload->>'entry_date')::date;
v_besetzt uuid;
begin
perform require_hr_admin();
if v_position_id is null then
raise exception 'Es muss eine Planstelle angegeben werden.';
end if;
if payload->>'personnel_number' is null or btrim(payload->>'personnel_number') = '' then
raise exception 'Es muss eine Personalnummer angegeben werden.';
end if;
if exists (select 1 from employees where personnel_number = (payload->>'personnel_number')::int) then
raise exception 'Die Personalnummer % ist bereits vergeben.', payload->>'personnel_number';
end if;
declare
v_ab date;
v_bis date;
begin
select valid_from, valid_to into v_ab, v_bis from om_positions where id = v_position_id;
if v_ab is null then
raise exception 'Die Planstelle existiert nicht.';
end if;
if v_entry < v_ab then
raise exception 'Die Planstelle gilt erst ab %. Ein Eintritt am % ist darauf nicht möglich.', v_ab, v_entry;
end if;
if v_bis is not null and v_entry >= v_bis then
raise exception 'Die Planstelle gilt nur bis %. Ein Eintritt am % ist darauf nicht möglich.', v_bis, v_entry;
end if;
end;
select pa.employee_id into v_besetzt
from position_assignments pa
where pa.position_id = v_position_id
and (pa.valid_to is null or pa.valid_to > v_entry);
if v_besetzt is not null then
raise exception 'Diese Planstelle ist bereits besetzt.';
end if;
insert into employees (
personnel_number, first_name, last_name, gender, birth_date, sv_nummer, nationality, email, phone,
address, postal_code, city, address_country, location_id, job_title,
employment_type, weekly_hours, contract_type, contract_end_date, paygrade,
source, status, entry_date, title_prefix, title_suffix,
worker_type, collective_agreement, work_days,
is_betriebsrat, has_dienstwagen, is_laterale_fuehrung, is_c_level,
has_kuendigungsschutz, kuendigungsschutz_bis,
dienstwagen_art, emergency_contact_name, emergency_contact_phone, emergency_contact_relation
)
values (
(payload->>'personnel_number')::int, payload->>'first_name', payload->>'last_name', (payload->>'gender')::gender_type,
(payload->>'birth_date')::date, payload->>'sv_nummer',
coalesce(payload->>'nationality', 'Österreich'), payload->>'email', payload->>'phone',
payload->>'address', payload->>'postal_code', payload->>'city',
coalesce(payload->>'address_country', 'Österreich'),
(payload->>'location_id')::uuid,
(select j.title from om_positions p join jobs j on j.id = p.job_id where p.id = v_position_id),
coalesce((payload->>'employment_type')::employment_type, 'Vollzeit'),
coalesce((payload->>'weekly_hours')::numeric, 38.5),
coalesce((payload->>'contract_type')::contract_type, 'unbefristet'),
nullif(payload->>'contract_end_date', '')::date,
coalesce((payload->>'paygrade')::paygrade_type, 'B'),
coalesce((payload->>'source')::source_type, 'Extern'),
case when v_entry > current_date then 'Geplant' else 'Aktiv' end::employment_status,
v_entry,
coalesce(array(select jsonb_array_elements_text(payload->'title_prefix')), '{}'),
coalesce(array(select jsonb_array_elements_text(payload->'title_suffix')), '{}'),
coalesce((payload->>'worker_type')::worker_type, 'Angestellte:r'),
coalesce((payload->>'collective_agreement')::collective_agreement, 'Süßwaren'),
coalesce(nullif(array(select jsonb_array_elements_text(payload->'work_days'))::text[], '{}'), '{Mo,Di,Mi,Do,Fr}'),
coalesce((payload->>'is_betriebsrat')::boolean, false),
coalesce((payload->>'has_dienstwagen')::boolean, false),
coalesce((payload->>'is_laterale_fuehrung')::boolean, false),
coalesce((payload->>'is_c_level')::boolean, false),
coalesce((payload->>'has_kuendigungsschutz')::boolean, false),
-- Das Datum nur, wenn der Schutz überhaupt gesetzt ist: sonst bliebe
-- ein Enddatum ohne Schutz stehen, und chk_kuendigungsschutz_bis
-- würde es zu Recht abweisen.
case when coalesce((payload->>'has_kuendigungsschutz')::boolean, false)
then nullif(payload->>'kuendigungsschutz_bis', '')::date else null end,
case when coalesce((payload->>'has_dienstwagen')::boolean, false) then coalesce(nullif(payload->>'dienstwagen_art', ''), 'Verbrenner') else null end,
nullif(payload->>'emergency_contact_name', ''),
nullif(payload->>'emergency_contact_phone', ''),
nullif(payload->>'emergency_contact_relation', '')
)
returning id into v_id;
insert into position_assignments (position_id, employee_id, valid_from)
values (v_position_id, v_id, v_entry);
insert into employee_history (employee_id, event_date, event_type, description)
values (v_id, v_entry, 'Eintritt', 'Eintritt auf Planstelle ' ||
(select position_number from om_positions where id = v_position_id));
insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details)
values (app_current_user_id(), current_actor_name(), 'Neueinstellung',
(payload->>'first_name') || ' ' || (payload->>'last_name'), v_id, 'Eintritt am ' || v_entry);
return v_id;
end;
$function$;
CREATE OR REPLACE FUNCTION public.change_employee_data(payload jsonb)
RETURNS void
LANGUAGE plpgsql
SET search_path TO 'public', 'pg_temp'
AS $function$
declare
v_employee_id uuid := (payload->>'employee_id')::uuid;
v_effective_date date := coalesce(nullif(payload->>'effective_date', '')::date, current_date);
v_old employees%rowtype;
v_name text;
v_person_changes jsonb := '[]'::jsonb;
v_contract_changes jsonb := '[]'::jsonb;
v_person jsonb := payload->'person';
v_contract jsonb := payload->'contract';
v_role jsonb := payload->'role';
v_immediate boolean;
v_new_work_days text[];
v_new_title_prefix text[];
v_new_title_suffix text[];
v_pending_id uuid;
begin
perform require_hr_admin();
select * into v_old from employees where id = v_employee_id;
v_name := v_old.first_name || ' ' || v_old.last_name;
v_immediate := v_effective_date <= current_date;
-- Der `?`-Test bleibt: ein fehlender Schlüssel heisst „nicht übermittelt",
-- nicht „geleert". Ohne ihn würde jedes nicht gesendete Feld als Änderung
-- auf null gemeldet.
if v_person ? 'first_name' then v_person_changes := app_aenderung(v_person_changes, 'Vorname', v_old.first_name, v_person->>'first_name'); end if;
if v_person ? 'last_name' then v_person_changes := app_aenderung(v_person_changes, 'Nachname', v_old.last_name, v_person->>'last_name'); end if;
if v_person ? 'gender' then v_person_changes := app_aenderung(v_person_changes, 'Geschlecht', v_old.gender::text, v_person->>'gender'); end if;
-- Datumswerte über ::date::text vergleichen, damit „2026-8-3" und
-- „2026-08-03" nicht als Änderung gelten.
if v_person ? 'birth_date' then v_person_changes := app_aenderung(v_person_changes, 'Geburtsdatum', v_old.birth_date::text, (nullif(v_person->>'birth_date','')::date)::text); end if;
if v_person ? 'sv_nummer' then v_person_changes := app_aenderung(v_person_changes, 'SV-Nummer', v_old.sv_nummer, v_person->>'sv_nummer'); end if;
if v_person ? 'nationality' then v_person_changes := app_aenderung(v_person_changes, 'Staatsbürgerschaft', v_old.nationality, v_person->>'nationality'); end if;
if v_person ? 'address' then v_person_changes := app_aenderung(v_person_changes, 'Adresse', v_old.address, v_person->>'address'); end if;
if v_person ? 'postal_code' then v_person_changes := app_aenderung(v_person_changes, 'Postleitzahl', v_old.postal_code, v_person->>'postal_code'); end if;
if v_person ? 'city' then v_person_changes := app_aenderung(v_person_changes, 'Ort', v_old.city, v_person->>'city'); end if;
if v_person ? 'address_country' then v_person_changes := app_aenderung(v_person_changes, 'Land', v_old.address_country, v_person->>'address_country'); end if;
if v_person ? 'email' then v_person_changes := app_aenderung(v_person_changes, 'E-Mail', v_old.email, v_person->>'email'); end if;
if v_person ? 'phone' then v_person_changes := app_aenderung(v_person_changes, 'Telefon', v_old.phone, v_person->>'phone'); end if;
if v_person ? 'emergency_contact_name' then v_person_changes := app_aenderung(v_person_changes, 'Notfallkontakt', v_old.emergency_contact_name, v_person->>'emergency_contact_name'); end if;
if v_person ? 'emergency_contact_phone' then v_person_changes := app_aenderung(v_person_changes, 'Notfallkontakt Telefon', v_old.emergency_contact_phone, v_person->>'emergency_contact_phone'); end if;
if v_person ? 'emergency_contact_relation' then v_person_changes := app_aenderung(v_person_changes, 'Notfallkontakt Verhältnis', v_old.emergency_contact_relation, v_person->>'emergency_contact_relation'); end if;
if v_person ? 'title_prefix' then
v_new_title_prefix := coalesce((select array_agg(elem) from jsonb_array_elements_text(v_person->'title_prefix') elem), '{}');
v_person_changes := app_aenderung(v_person_changes, 'Titel (vorangestellt)',
array_to_string(v_old.title_prefix, ', '), array_to_string(v_new_title_prefix, ', '));
end if;
if v_person ? 'title_suffix' then
v_new_title_suffix := coalesce((select array_agg(elem) from jsonb_array_elements_text(v_person->'title_suffix') elem), '{}');
v_person_changes := app_aenderung(v_person_changes, 'Titel (nachgestellt)',
array_to_string(v_old.title_suffix, ', '), array_to_string(v_new_title_suffix, ', '));
end if;
if v_contract ? 'employment_type' then v_contract_changes := app_aenderung(v_contract_changes, 'Beschäftigungsausmaß', v_old.employment_type::text, v_contract->>'employment_type'); end if;
-- Über ::numeric::text, damit „38.50" und „38.5" gleich zählen.
if v_contract ? 'weekly_hours' then v_contract_changes := app_aenderung(v_contract_changes, 'Wochenstunden', v_old.weekly_hours::text, (nullif(v_contract->>'weekly_hours','')::numeric)::text); end if;
if v_contract ? 'contract_type' then v_contract_changes := app_aenderung(v_contract_changes, 'Vertragsart', v_old.contract_type::text, v_contract->>'contract_type'); end if;
if v_contract ? 'contract_end_date' then v_contract_changes := app_aenderung(v_contract_changes, 'Befristet bis', v_old.contract_end_date::text, (nullif(v_contract->>'contract_end_date','')::date)::text); end if;
if v_role ? 'worker_type' then v_contract_changes := app_aenderung(v_contract_changes, 'Angestellte:r/Arbeiter:in', v_old.worker_type::text, v_role->>'worker_type'); end if;
if v_role ? 'collective_agreement' then v_contract_changes := app_aenderung(v_contract_changes, 'Kollektivvertrag', v_old.collective_agreement::text, v_role->>'collective_agreement'); end if;
if v_role ? 'work_days' then
v_new_work_days := coalesce((select array_agg(elem) from jsonb_array_elements_text(v_role->'work_days') elem), '{}');
v_contract_changes := app_aenderung(v_contract_changes, 'Arbeitstage',
array_to_string(v_old.work_days, ', '), array_to_string(v_new_work_days, ', '));
end if;
if v_role ? 'is_betriebsrat' then v_contract_changes := app_aenderung(v_contract_changes, 'Betriebsrat', v_old.is_betriebsrat::text, v_role->>'is_betriebsrat'); end if;
if v_role ? 'has_dienstwagen' then v_contract_changes := app_aenderung(v_contract_changes, 'Dienstwagen', v_old.has_dienstwagen::text, v_role->>'has_dienstwagen'); end if;
if v_role ? 'is_laterale_fuehrung' then v_contract_changes := app_aenderung(v_contract_changes, 'Laterale Führung', v_old.is_laterale_fuehrung::text, v_role->>'is_laterale_fuehrung'); end if;
if v_role ? 'is_c_level' then v_contract_changes := app_aenderung(v_contract_changes, 'C-Level', v_old.is_c_level::text, v_role->>'is_c_level'); end if;
if v_role ? 'has_kuendigungsschutz' then v_contract_changes := app_aenderung(v_contract_changes, 'Besonderer Kündigungsschutz', v_old.has_kuendigungsschutz::text, v_role->>'has_kuendigungsschutz'); end if;
if v_role ? 'kuendigungsschutz_bis' then v_contract_changes := app_aenderung(v_contract_changes, 'Kündigungsschutz bis', v_old.kuendigungsschutz_bis::text, (nullif(v_role->>'kuendigungsschutz_bis','')::date)::text); end if;
if v_role ? 'dienstwagen_art' then v_contract_changes := app_aenderung(v_contract_changes, 'Dienstwagen Antrieb', v_old.dienstwagen_art, nullif(v_role->>'dienstwagen_art', '')); end if;
if v_immediate then
update employees set
first_name = coalesce(v_person->>'first_name', first_name),
last_name = coalesce(v_person->>'last_name', last_name),
gender = coalesce((v_person->>'gender')::gender_type, gender),
birth_date = coalesce((v_person->>'birth_date')::date, birth_date),
sv_nummer = coalesce(v_person->>'sv_nummer', sv_nummer),
nationality = coalesce(v_person->>'nationality', nationality),
address = coalesce(v_person->>'address', address),
postal_code = coalesce(v_person->>'postal_code', postal_code),
city = coalesce(v_person->>'city', city),
address_country = coalesce(v_person->>'address_country', address_country),
email = coalesce(v_person->>'email', email),
phone = coalesce(v_person->>'phone', phone),
emergency_contact_name = case when v_person ? 'emergency_contact_name' then nullif(v_person->>'emergency_contact_name', '') else emergency_contact_name end,
emergency_contact_phone = case when v_person ? 'emergency_contact_phone' then nullif(v_person->>'emergency_contact_phone', '') else emergency_contact_phone end,
emergency_contact_relation = case when v_person ? 'emergency_contact_relation' then nullif(v_person->>'emergency_contact_relation', '') else emergency_contact_relation end,
title_prefix = case when v_person ? 'title_prefix' then v_new_title_prefix else title_prefix end,
title_suffix = case when v_person ? 'title_suffix' then v_new_title_suffix else title_suffix end,
employment_type = coalesce((v_contract->>'employment_type')::employment_type, employment_type),
weekly_hours = coalesce((v_contract->>'weekly_hours')::numeric, weekly_hours),
contract_type = coalesce((v_contract->>'contract_type')::contract_type, contract_type),
contract_end_date = case when v_contract ? 'contract_end_date' then nullif(v_contract->>'contract_end_date','')::date else contract_end_date end,
worker_type = coalesce((v_role->>'worker_type')::worker_type, worker_type),
collective_agreement = coalesce((v_role->>'collective_agreement')::collective_agreement, collective_agreement),
work_days = case when v_role ? 'work_days' then v_new_work_days else work_days end,
is_betriebsrat = coalesce((v_role->>'is_betriebsrat')::boolean, is_betriebsrat),
has_dienstwagen = coalesce((v_role->>'has_dienstwagen')::boolean, has_dienstwagen),
is_laterale_fuehrung = coalesce((v_role->>'is_laterale_fuehrung')::boolean, is_laterale_fuehrung),
is_c_level = coalesce((v_role->>'is_c_level')::boolean, is_c_level),
has_kuendigungsschutz = coalesce((v_role->>'has_kuendigungsschutz')::boolean, has_kuendigungsschutz),
-- Fällt der Schutz weg, fällt das Datum mit. Andernfalls bliebe ein
-- Enddatum ohne Schutz stehen — die Bedingung verbietet das, und der
-- Vorgang schlüge fehl, statt das Offensichtliche zu tun.
kuendigungsschutz_bis = case
when coalesce((v_role->>'has_kuendigungsschutz')::boolean, has_kuendigungsschutz) then
case when v_role ? 'kuendigungsschutz_bis'
then nullif(v_role->>'kuendigungsschutz_bis','')::date
else kuendigungsschutz_bis end
else null
end,
dienstwagen_art = case
when coalesce((v_role->>'has_dienstwagen')::boolean, has_dienstwagen) then
coalesce(nullif(v_role->>'dienstwagen_art', ''), dienstwagen_art, 'Verbrenner')
else null
end
where id = v_employee_id;
elsif jsonb_array_length(v_person_changes) > 0 or jsonb_array_length(v_contract_changes) > 0 then
insert into pending_org_changes (employee_id, change_type, effective_date, payload)
values (v_employee_id, 'contract_change', v_effective_date, payload)
returning id into v_pending_id;
end if;
if jsonb_array_length(v_person_changes) > 0 then
insert into employee_history (employee_id, event_date, event_type, description, changes, pending_id)
values (v_employee_id, v_effective_date, 'Stammdatenänderung',
'Geänderte Felder: ' || app_aenderungsfelder(v_person_changes) || ', wirksam ab ' || v_effective_date, v_person_changes, v_pending_id);
insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details, changes)
values (app_current_user_id(), current_actor_name(), 'Stammdatenänderung', v_name, v_employee_id,
app_aenderungsfelder(v_person_changes) || ', wirksam ab ' || v_effective_date, v_person_changes);
end if;
if jsonb_array_length(v_contract_changes) > 0 then
insert into employee_history (employee_id, event_date, event_type, description, changes, pending_id)
values (v_employee_id, v_effective_date, 'Vertragsänderung',
'Geänderte Felder: ' || app_aenderungsfelder(v_contract_changes) || ', wirksam ab ' || v_effective_date, v_contract_changes, v_pending_id);
insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details, changes)
values (app_current_user_id(), current_actor_name(), 'Vertragsänderung', v_name, v_employee_id,
app_aenderungsfelder(v_contract_changes) || ', wirksam ab ' || v_effective_date, v_contract_changes);
end if;
end;
$function$;
-- Selbstprüfung.
do $$
declare
v_hire text := pg_get_functiondef('public.hire_employee(jsonb)'::regprocedure);
v_chg text := pg_get_functiondef('public.change_employee_data(jsonb)'::regprocedure);
begin
if v_hire not like '%has_kuendigungsschutz%' then
raise exception 'hire_employee nimmt den Kündigungsschutz nicht entgegen';
end if;
if v_chg not like '%Besonderer Kündigungsschutz%' then
raise exception 'change_employee_data protokolliert den Kündigungsschutz nicht';
end if;
if not (app_feld_karte() ? 'Besonderer Kündigungsschutz' and app_feld_karte() ? 'Kündigungsschutz bis') then
raise exception 'Die Feldtabelle kennt den Kündigungsschutz nicht — dann liesse sich ein Eintrag dazu nicht berichtigen';
end if;
if app_feld_karte()->'Kündigungsschutz bis'->>2 <> 'role' then
raise exception 'Die Gruppe im payload stimmt nicht';
end if;
end
$$;