Eine datierte Stammdatenaenderung verlor fuenf 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 traegt ihn apply_due_pending_changes nach. Dessen Zweig contract_change kannte fuenf Spalten nicht, die der sofortige Weg schreibt: die drei Felder des Notfallkontakts und beide Titelfelder. Wer also den Notfallkontakt oder einen akademischen Titel zu einem kuenftigen Datum erfasste, sah den Vorgang in der Personalakte, und am Stichtag geschah nichts. Der Eintrag wurde auf "applied" gesetzt, die Zeile blieb unveraendert, eine Meldung gab es nicht. Gefunden durch einen Abgleich beider Wege: 44 Spalten schreibt change_employee_data sofort, 39 der Nachtlauf. Jetzt 44 zu 44. Dieselbe Luecke wie bei der Gruppe `role` (20260915120000) und bei der Besetzung im Zweig `transfer` (20260924100000) -- und derselbe Weg, sie zu finden. Die Selbstpruefung nennt die fuenf Felder ausdruecklich und prueft zusaetzlich weiter, was hier schon einmal verlorenging.
This commit is contained in:
244
db/migrations/20260924180000_datierte_aenderung_vollstaendig.sql
Normal file
244
db/migrations/20260924180000_datierte_aenderung_vollstaendig.sql
Normal file
@@ -0,0 +1,244 @@
|
||||
-- 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
|
||||
$$;
|
||||
Reference in New Issue
Block a user