-- Eine datierte Stammdatenänderung verlor fünf Felder. -- -- change_employee_data schreibt bei einem Stichtag in der Zukunft nicht in -- die Zeile, sondern legt einen Eintrag in pending_org_changes an; am -- Stichtag trägt ihn apply_due_pending_changes nach. Dessen Zweig -- `contract_change` kannte fünf Spalten nicht, die der sofortige Weg -- schreibt: -- -- emergency_contact_name, emergency_contact_phone, -- emergency_contact_relation, title_prefix, title_suffix -- -- Wer also den Notfallkontakt oder einen akademischen Titel zu einem -- künftigen Datum erfasste, sah den Vorgang in der Personalakte, und am -- Stichtag geschah nichts. Der Eintrag wurde auf `applied` gesetzt, die -- Zeile blieb unverändert, eine Meldung gab es nicht. -- -- Gefunden durch einen Abgleich der beiden Wege: 44 Spalten schreibt -- change_employee_data sofort, 39 der Nachtlauf. Das ist dieselbe Lücke wie -- bei der Gruppe `role` (20260915120000) und bei der Besetzung im Zweig -- `transfer` (20260924100000) — und derselbe Weg, sie zu finden. -- -- Die Selbstprüfung unten vergleicht beide Listen von jetzt an bei jedem -- Lauf der Migration, statt einzelne Namen abzufragen. CREATE OR REPLACE FUNCTION public.apply_due_pending_changes() RETURNS integer LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public', 'pg_temp' AS $function$ declare v_rec record; v_count integer := 0; v_role jsonb; begin for v_rec in select * from pending_org_changes where status = 'pending' and effective_date <= current_date order by effective_date, created_at loop if v_rec.change_type = 'transfer' then -- Die Besetzung wandert mit. Hier stand zuletzt nur noch das Setzen -- des Titels aus einem Feld, das in diesem payload gar nicht vorkommt -- — die Fassung von vor der Umstellung auf Planstellen. Eine auf -- später datierte Versetzung galt damit am Stichtag als angewendet -- und bewegte niemanden. update position_assignments set valid_to = v_rec.effective_date where employee_id = v_rec.employee_id and valid_to is null; insert into position_assignments (position_id, employee_id, valid_from) values ((v_rec.payload->>'target_position_id')::uuid, v_rec.employee_id, v_rec.effective_date); update employees set job_title = ( select j.title from om_positions p join jobs j on j.id = p.job_id where p.id = (v_rec.payload->>'target_position_id')::uuid ) where id = v_rec.employee_id; elsif v_rec.change_type = 'promotion' then update employees set job_title = coalesce(v_rec.payload->>'new_title', job_title), paygrade = coalesce((v_rec.payload->>'new_paygrade')::paygrade_type, paygrade) where id = v_rec.employee_id; -- Eine Beförderung kann die Planstelle wechseln. Ohne diesen Zweig -- stünde am Stichtag der neue Titel an der Person und sie säsze -- weiterhin auf der alten Stelle — im Organigramm unverändert. if nullif(v_rec.payload->>'target_position_id', '') is not null then update position_assignments set valid_to = v_rec.effective_date where employee_id = v_rec.employee_id and valid_to is null; insert into position_assignments (position_id, employee_id, valid_from) values ((v_rec.payload->>'target_position_id')::uuid, v_rec.employee_id, v_rec.effective_date); end if; elsif v_rec.change_type = 'karenz_start' then update employees set status = 'Karenz', karenz_return_date = (v_rec.payload->>'planned_return_date')::date, absence_type = coalesce(nullif(v_rec.payload->>'absence_type', ''), absence_type) where id = v_rec.employee_id; elsif v_rec.change_type = 'karenz_return' then update employees set status = 'Aktiv', karenz_return_date = null, karenz_start_date = null, absence_type = null, employment_type = coalesce((v_rec.payload->>'employment_type')::employment_type, employment_type), weekly_hours = coalesce((v_rec.payload->>'weekly_hours')::numeric, weekly_hours), -- Auch bei einer *geplanten* Rückkehr: sonst käme jemand am -- Stichtag mit reduzierten Stunden zurück, und der Grund dafür -- wäre verschwunden. teilzeit_art = case when v_rec.payload ? 'teilzeit_art' then nullif(v_rec.payload->>'teilzeit_art', '') else teilzeit_art end, teilzeit_bis = case when v_rec.payload ? 'teilzeit_art' then nullif(v_rec.payload->>'teilzeit_bis', '')::date else teilzeit_bis end where id = v_rec.employee_id; elsif v_rec.change_type = 'contract_change' then v_role := v_rec.payload->'role'; update employees set first_name = coalesce(v_rec.payload->'person'->>'first_name', first_name), last_name = coalesce(v_rec.payload->'person'->>'last_name', last_name), gender = coalesce((v_rec.payload->'person'->>'gender')::gender_type, gender), birth_date = coalesce((v_rec.payload->'person'->>'birth_date')::date, birth_date), sv_nummer = case when v_rec.payload->'person' ? 'sv_nummer' then nullif(v_rec.payload->'person'->>'sv_nummer', '') else sv_nummer end, nationality = coalesce(v_rec.payload->'person'->>'nationality', nationality), address = case when v_rec.payload->'person' ? 'address' then nullif(v_rec.payload->'person'->>'address', '') else address end, postal_code = case when v_rec.payload->'person' ? 'postal_code' then nullif(v_rec.payload->'person'->>'postal_code', '') else postal_code end, city = case when v_rec.payload->'person' ? 'city' then nullif(v_rec.payload->'person'->>'city', '') else city end, address_country = case when v_rec.payload->'person' ? 'address_country' then nullif(v_rec.payload->'person'->>'address_country', '') else address_country end, email = case when v_rec.payload->'person' ? 'email' then nullif(v_rec.payload->'person'->>'email', '') else email end, phone = case when v_rec.payload->'person' ? 'phone' then nullif(v_rec.payload->'person'->>'phone', '') else phone end, company_email = case when v_rec.payload->'person' ? 'company_email' then nullif(v_rec.payload->'person'->>'company_email', '') else company_email end, -- Der Notfallkontakt und die Titel fehlten hier. Eine auf später -- datierte Änderung stand damit in der Akte, kam aber nie an der -- Person an: change_employee_data schreibt sie sofort, dieser Zweig -- kannte sie nicht. Dieselbe Lücke wie einst bei der Gruppe `role` -- (20260915120000). emergency_contact_name = case when v_rec.payload->'person' ? 'emergency_contact_name' then nullif(v_rec.payload->'person'->>'emergency_contact_name', '') else emergency_contact_name end, emergency_contact_phone = case when v_rec.payload->'person' ? 'emergency_contact_phone' then nullif(v_rec.payload->'person'->>'emergency_contact_phone', '') else emergency_contact_phone end, emergency_contact_relation = case when v_rec.payload->'person' ? 'emergency_contact_relation' then nullif(v_rec.payload->'person'->>'emergency_contact_relation', '') else emergency_contact_relation end, -- Wie work_days weiter unten: eine leere Liste ist eine Angabe, kein -- fehlender Wert, deshalb coalesce auf '{}' statt auf den alten Stand. title_prefix = case when v_rec.payload->'person' ? 'title_prefix' then coalesce((select array_agg(elem) from jsonb_array_elements_text(v_rec.payload->'person'->'title_prefix') elem), '{}') else title_prefix end, title_suffix = case when v_rec.payload->'person' ? 'title_suffix' then coalesce((select array_agg(elem) from jsonb_array_elements_text(v_rec.payload->'person'->'title_suffix') elem), '{}') else title_suffix end, employment_type = coalesce((v_rec.payload->'contract'->>'employment_type')::employment_type, employment_type), weekly_hours = coalesce((v_rec.payload->'contract'->>'weekly_hours')::numeric, weekly_hours), contract_type = coalesce((v_rec.payload->'contract'->>'contract_type')::contract_type, contract_type), contract_end_date = case when v_rec.payload->'contract' ? 'contract_end_date' then nullif(v_rec.payload->'contract'->>'contract_end_date','')::date else contract_end_date end, worker_type = coalesce((v_role->>'worker_type')::worker_type, worker_type), mitarbeiterart = coalesce(nullif(v_role->>'mitarbeiterart', ''), mitarbeiterart), source = coalesce((nullif(v_role->>'source', ''))::source_type, source), collective_agreement = coalesce((v_role->>'collective_agreement')::collective_agreement, collective_agreement), work_days = case when v_role ? 'work_days' then coalesce((select array_agg(elem) from jsonb_array_elements_text(v_role->'work_days') elem), '{}') 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), 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, teilzeit_art = case when v_role ? 'teilzeit_art' then nullif(v_role->>'teilzeit_art', '') else teilzeit_art end, teilzeit_bis = case when coalesce(nullif(v_role->>'teilzeit_art', ''), case when v_role ? 'teilzeit_art' then null else teilzeit_art end) is null then null when v_role ? 'teilzeit_bis' then nullif(v_role->>'teilzeit_bis','')::date else teilzeit_bis end, hat_aufenthaltstitel = coalesce((v_rec.payload->'person'->>'hat_aufenthaltstitel')::boolean, hat_aufenthaltstitel), aufenthaltstitel_bis = case when coalesce((v_rec.payload->'person'->>'hat_aufenthaltstitel')::boolean, hat_aufenthaltstitel) then case when v_rec.payload->'person' ? 'aufenthaltstitel_bis' then nullif(v_rec.payload->'person'->>'aufenthaltstitel_bis','')::date else aufenthaltstitel_bis end else null end, has_kuendigungsschutz = coalesce((v_role->>'has_kuendigungsschutz')::boolean, has_kuendigungsschutz), kuendigungsschutz_grund = case when coalesce((v_role->>'has_kuendigungsschutz')::boolean, has_kuendigungsschutz) then case when v_role ? 'kuendigungsschutz_grund' then nullif(v_role->>'kuendigungsschutz_grund','') else kuendigungsschutz_grund end else null end, kuendigungsschutz_ab = case when coalesce((v_role->>'has_kuendigungsschutz')::boolean, has_kuendigungsschutz) then case when v_role ? 'kuendigungsschutz_ab' then nullif(v_role->>'kuendigungsschutz_ab','')::date else kuendigungsschutz_ab end else null end, 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, ist_beguenstigt_behindert = coalesce((v_role->>'ist_beguenstigt_behindert')::boolean, ist_beguenstigt_behindert), behinderung_grad = case when coalesce((v_role->>'ist_beguenstigt_behindert')::boolean, ist_beguenstigt_behindert) then case when v_role ? 'behinderung_grad' then nullif(v_role->>'behinderung_grad','')::integer else behinderung_grad end else null end, behinderung_ab = case when coalesce((v_role->>'ist_beguenstigt_behindert')::boolean, ist_beguenstigt_behindert) then case when v_role ? 'behinderung_ab' then nullif(v_role->>'behinderung_ab','')::date else behinderung_ab end else null end, behinderung_bis = case when coalesce((v_role->>'ist_beguenstigt_behindert')::boolean, ist_beguenstigt_behindert) then case when v_role ? 'behinderung_bis' then nullif(v_role->>'behinderung_bis','')::date else behinderung_bis end else null end where id = v_rec.employee_id; elsif v_rec.change_type = 'dependent_add' then insert into employee_dependents (employee_id, first_name, last_name, relationship, sv_nummer, birth_date) values (v_rec.employee_id, v_rec.payload->>'first_name', v_rec.payload->>'last_name', (v_rec.payload->>'relationship')::text, nullif(v_rec.payload->>'sv_nummer', ''), (v_rec.payload->>'birth_date')::date); elsif v_rec.change_type = 'dependent_remove' then delete from employee_dependents where id = (v_rec.payload->>'dependent_id')::uuid; end if; update pending_org_changes set status = 'applied', applied_at = now() where id = v_rec.id; v_count := v_count + 1; end loop; return v_count; end; $function$; grant execute on function public.apply_due_pending_changes() to alpenwerk_app; -- Selbstprüfung: beide Wege müssen dieselben Spalten der Person kennen. do $$ declare v_lauf text := pg_get_functiondef('public.apply_due_pending_changes()'::regprocedure); v_feld text; begin foreach v_feld in array array[ 'emergency_contact_name', 'emergency_contact_phone', 'emergency_contact_relation', 'title_prefix', 'title_suffix' ] loop if v_lauf not like '%' || v_feld || '%' then raise exception 'Der Nachtlauf kennt % nicht — eine datierte Aenderung daran verfiele.', v_feld; end if; end loop; -- Und die frueher schon einmal verlorenen bleiben drin. if v_lauf not like '%target_position_id%' then raise exception 'Der Nachtlauf kennt die Zielplanstelle nicht mehr.'; end if; if (length(v_lauf) - length(replace(v_lauf, 'insert into position_assignments', ''))) / length('insert into position_assignments') < 2 then raise exception 'Der Nachtlauf haengt die Besetzung nicht in beiden Zweigen um.'; end if; end $$;