Files
paliad/internal/db/migrations/018_projects_v2.down.sql
m 3faec6c526 refactor(rename): German→English for backend (tables, types, services, handler files)
t-paliad-025 — Phase 1: backend rename.

Migrations 018+019 rewritten from scratch with English table/column
names throughout. Since v2 schema (018/019) has never been applied to
youpc prod DB, this is a clean replacement — not an ALTER RENAME chain.
Pre-existing German tables (parteien, fristen, termine, dokumente,
akten_events, notizen) are renamed inline in 018 via ALTER TABLE … RENAME
TO alongside the akte_id → project_id column rewrite.

Renames applied:
  projekte            → projects
  projekt_teams       → project_teams
  projekt_events      → project_events (via akten_events → project_events)
  fristen             → deadlines
  termine             → appointments
  parteien            → parties
  notizen             → notes
  dezernate           → departments
  dezernat_mitglieder → department_members
  dokumente           → documents
  can_see_projekt     → can_see_project
  notiz_is_visible    → note_is_visible
  akte_id  / frist_id / termin_id / akten_event_id → project_id /
    deadline_id / appointment_id / project_event_id
  termin_type → appointment_type

Go types + services renamed:
  Projekt / ProjektService / ProjektEvent / ProjektTeamMember
  Frist / FristService / FristWithProjekt
  Termin / TerminService / TerminWithProjekt / TerminType
  Notiz / NotizService / ChecklistInstanceWithProjekt
  Dezernat / DezernatService / DezernatMitglied
  Partei / Parteien / ParteienService

Files renamed (git mv):
  internal/services/{projekt,frist,termin,notiz,dezernat,parteien}_service.go
    → {project,deadline,appointment,note,department,party}_service.go
  internal/handlers/{projekte,fristen,fristen_pages,termine,termine_pages,
    notizen,dezernate,akten_pages,gerichte,glossar,checklisten}.go
    → {projects,deadlines,deadlines_pages,appointments,appointments_pages,
       notes,departments,projects_pages,courts,glossary,checklists}.go
  internal/checklisten/ → internal/checklists/
  internal/db/migrations/018_projekte_v2.* → 018_projects_v2.*
  internal/db/migrations/019_seed_dezernate_from_user_text.*
    → 019_seed_departments_from_user_text.*

User-facing i18n strings (DE/EN labels) stay untouched. Product names
Fristenrechner / Kostenrechner / Gebührentabellen stay German.

Build + vet + tests clean.
2026-04-20 17:35:38 +02:00

361 lines
17 KiB
PL/PgSQL

-- Rollback for 018_projects_v2.
--
-- Recreates paliad.akten (same UUIDs), restores akte_id FK columns on the
-- children, renames child tables back to German (notes→notizen, deadlines→
-- fristen, appointments→termine, parties→parteien, documents→dokumente,
-- project_events→akten_events), drops the new projects/team/department
-- tables. Best-effort:
-- * Child rows whose project_id points at a non-akten project row (i.e.,
-- anything created post-migration under non-'case' types) will fail
-- FK re-creation. The rollback aborts in that case — this is intentional,
-- since v2-only data has no v1 home.
-- * project_teams memberships are folded back into akten.collaborators
-- (dedup, drop role metadata); users who were only 'lead' end up in
-- the array like everyone else.
-- * owning_office is set to the creator's user.office when available,
-- else 'munich' as a last-resort fallback (documented loss).
-- 1. Recreate paliad.akten with the old shape.
CREATE TABLE paliad.akten (
id uuid PRIMARY KEY,
aktenzeichen text NOT NULL,
title text NOT NULL,
akte_type text,
court text,
court_ref text,
status text NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'pending', 'closed', 'archived')),
ai_summary text,
owning_office text NOT NULL CHECK (owning_office IN (
'munich', 'duesseldorf', 'hamburg',
'amsterdam', 'london', 'paris', 'milan'
)),
collaborators uuid[] NOT NULL DEFAULT '{}',
firm_wide_visible boolean NOT NULL DEFAULT false,
created_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
metadata jsonb NOT NULL DEFAULT '{}',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX akten_status_owning_office_idx ON paliad.akten (status, owning_office);
CREATE INDEX akten_owning_office_idx ON paliad.akten (owning_office);
CREATE INDEX akten_firm_wide_idx ON paliad.akten (firm_wide_visible) WHERE firm_wide_visible = true;
CREATE INDEX akten_collaborators_gin_idx ON paliad.akten USING GIN (collaborators);
-- 2. Backfill from projects (only type='case' — others have no v1 home).
INSERT INTO paliad.akten (
id, aktenzeichen, title, akte_type, court, court_ref, status, ai_summary,
owning_office, collaborators, firm_wide_visible,
created_by, metadata, created_at, updated_at
)
SELECT
p.id,
COALESCE(p.reference, p.id::text),
p.title,
NULL,
p.court,
p.case_number,
CASE WHEN p.status IN ('active','pending','closed','archived')
THEN p.status ELSE 'active' END,
p.ai_summary,
COALESCE(
(SELECT u.office FROM paliad.users u WHERE u.id = p.created_by),
'munich'
),
COALESCE(
(SELECT array_agg(DISTINCT pt.user_id)
FROM paliad.project_teams pt
WHERE pt.project_id = p.id
AND pt.user_id IS NOT NULL),
'{}'::uuid[]
),
false,
p.created_by,
p.metadata,
p.created_at,
p.updated_at
FROM paliad.projects p
WHERE p.type = 'case';
-- 3. Drop new RLS policies + helpers we installed in .up.
DROP POLICY IF EXISTS parties_all ON paliad.parties;
DROP POLICY IF EXISTS deadlines_all ON paliad.deadlines;
DROP POLICY IF EXISTS appointments_select ON paliad.appointments;
DROP POLICY IF EXISTS appointments_insert ON paliad.appointments;
DROP POLICY IF EXISTS appointments_update ON paliad.appointments;
DROP POLICY IF EXISTS appointments_delete ON paliad.appointments;
DROP POLICY IF EXISTS documents_all ON paliad.documents;
DROP POLICY IF EXISTS project_events_all ON paliad.project_events;
DROP POLICY IF EXISTS notes_all ON paliad.notes;
DROP POLICY IF EXISTS checklist_instances_select ON paliad.checklist_instances;
DROP POLICY IF EXISTS checklist_instances_insert ON paliad.checklist_instances;
DROP POLICY IF EXISTS checklist_instances_update ON paliad.checklist_instances;
DROP POLICY IF EXISTS checklist_instances_delete ON paliad.checklist_instances;
DROP POLICY IF EXISTS projects_select ON paliad.projects;
DROP POLICY IF EXISTS projects_insert ON paliad.projects;
DROP POLICY IF EXISTS projects_update ON paliad.projects;
DROP POLICY IF EXISTS projects_delete ON paliad.projects;
DROP POLICY IF EXISTS project_teams_select ON paliad.project_teams;
DROP POLICY IF EXISTS project_teams_insert ON paliad.project_teams;
DROP POLICY IF EXISTS project_teams_update ON paliad.project_teams;
DROP POLICY IF EXISTS project_teams_delete ON paliad.project_teams;
DROP POLICY IF EXISTS departments_select ON paliad.departments;
DROP POLICY IF EXISTS departments_write ON paliad.departments;
DROP POLICY IF EXISTS department_members_select ON paliad.department_members;
DROP POLICY IF EXISTS department_members_write ON paliad.department_members;
DROP FUNCTION IF EXISTS paliad.note_is_visible(uuid, uuid, uuid, uuid);
DROP FUNCTION IF EXISTS paliad.can_see_project(uuid);
-- 4. Rename child tables back to German + restore akte_id column.
ALTER TABLE paliad.parties DROP CONSTRAINT IF EXISTS parties_project_id_fkey;
ALTER TABLE paliad.parties RENAME COLUMN project_id TO akte_id;
ALTER TABLE paliad.parties RENAME TO parteien;
ALTER TABLE paliad.parteien
ADD CONSTRAINT parteien_akte_id_fkey FOREIGN KEY (akte_id)
REFERENCES paliad.akten(id) ON DELETE CASCADE;
DROP INDEX IF EXISTS paliad.parties_project_idx;
CREATE INDEX parteien_akte_idx ON paliad.parteien (akte_id);
ALTER TABLE paliad.deadlines DROP CONSTRAINT IF EXISTS deadlines_project_id_fkey;
ALTER TABLE paliad.deadlines RENAME COLUMN project_id TO akte_id;
ALTER TABLE paliad.deadlines RENAME TO fristen;
ALTER TABLE paliad.fristen
ADD CONSTRAINT fristen_akte_id_fkey FOREIGN KEY (akte_id)
REFERENCES paliad.akten(id) ON DELETE CASCADE;
DROP INDEX IF EXISTS paliad.deadlines_project_idx;
DROP INDEX IF EXISTS paliad.deadlines_status_due_date_idx;
DROP INDEX IF EXISTS paliad.deadlines_due_date_idx;
CREATE INDEX fristen_akte_idx ON paliad.fristen (akte_id);
CREATE INDEX fristen_status_due_date_idx ON paliad.fristen (status, due_date);
CREATE INDEX fristen_due_date_idx ON paliad.fristen (due_date);
ALTER TABLE paliad.appointments DROP CONSTRAINT IF EXISTS appointments_project_id_fkey;
ALTER TABLE paliad.appointments DROP CONSTRAINT IF EXISTS appointments_appointment_type_check;
ALTER TABLE paliad.appointments RENAME COLUMN project_id TO akte_id;
ALTER TABLE paliad.appointments RENAME COLUMN appointment_type TO termin_type;
ALTER TABLE paliad.appointments RENAME TO termine;
ALTER TABLE paliad.termine
ADD CONSTRAINT termine_akte_id_fkey FOREIGN KEY (akte_id)
REFERENCES paliad.akten(id) ON DELETE CASCADE,
ADD CONSTRAINT termine_termin_type_check
CHECK (termin_type IS NULL OR termin_type IN (
'hearing', 'meeting', 'consultation', 'deadline_hearing'
));
DROP INDEX IF EXISTS paliad.appointments_project_idx;
DROP INDEX IF EXISTS paliad.appointments_start_at_idx;
CREATE INDEX termine_akte_idx ON paliad.termine (akte_id);
CREATE INDEX termine_start_at_idx ON paliad.termine (start_at);
ALTER TABLE paliad.documents DROP CONSTRAINT IF EXISTS documents_project_id_fkey;
ALTER TABLE paliad.documents RENAME COLUMN project_id TO akte_id;
ALTER TABLE paliad.documents RENAME TO dokumente;
ALTER TABLE paliad.dokumente
ADD CONSTRAINT dokumente_akte_id_fkey FOREIGN KEY (akte_id)
REFERENCES paliad.akten(id) ON DELETE CASCADE;
DROP INDEX IF EXISTS paliad.documents_project_idx;
CREATE INDEX dokumente_akte_idx ON paliad.dokumente (akte_id);
ALTER TABLE paliad.project_events DROP CONSTRAINT IF EXISTS project_events_project_id_fkey;
ALTER TABLE paliad.project_events RENAME COLUMN project_id TO akte_id;
ALTER TABLE paliad.project_events RENAME TO akten_events;
ALTER TABLE paliad.akten_events
ADD CONSTRAINT akten_events_akte_id_fkey FOREIGN KEY (akte_id)
REFERENCES paliad.akten(id) ON DELETE CASCADE;
DROP INDEX IF EXISTS paliad.project_events_project_created_idx;
CREATE INDEX akten_events_akte_created_idx ON paliad.akten_events (akte_id, created_at DESC);
-- notes → notizen
ALTER TABLE paliad.notes DROP CONSTRAINT IF EXISTS notes_exactly_one_parent;
ALTER TABLE paliad.notes DROP CONSTRAINT IF EXISTS notes_project_id_fkey;
ALTER TABLE paliad.notes DROP CONSTRAINT IF EXISTS notes_deadline_id_fkey;
ALTER TABLE paliad.notes DROP CONSTRAINT IF EXISTS notes_appointment_id_fkey;
ALTER TABLE paliad.notes DROP CONSTRAINT IF EXISTS notes_project_event_id_fkey;
ALTER TABLE paliad.notes RENAME COLUMN project_id TO akte_id;
ALTER TABLE paliad.notes RENAME COLUMN deadline_id TO frist_id;
ALTER TABLE paliad.notes RENAME COLUMN appointment_id TO termin_id;
ALTER TABLE paliad.notes RENAME COLUMN project_event_id TO akten_event_id;
ALTER TABLE paliad.notes RENAME TO notizen;
ALTER TABLE paliad.notizen
ADD CONSTRAINT notizen_akte_id_fkey FOREIGN KEY (akte_id)
REFERENCES paliad.akten(id) ON DELETE CASCADE,
ADD CONSTRAINT notizen_frist_id_fkey FOREIGN KEY (frist_id)
REFERENCES paliad.fristen(id) ON DELETE CASCADE,
ADD CONSTRAINT notizen_termin_id_fkey FOREIGN KEY (termin_id)
REFERENCES paliad.termine(id) ON DELETE CASCADE,
ADD CONSTRAINT notizen_akten_event_id_fkey FOREIGN KEY (akten_event_id)
REFERENCES paliad.akten_events(id) ON DELETE CASCADE,
ADD CONSTRAINT notizen_exactly_one_parent CHECK (
(CASE WHEN akte_id IS NOT NULL THEN 1 ELSE 0 END) +
(CASE WHEN frist_id IS NOT NULL THEN 1 ELSE 0 END) +
(CASE WHEN termin_id IS NOT NULL THEN 1 ELSE 0 END) +
(CASE WHEN akten_event_id IS NOT NULL THEN 1 ELSE 0 END) = 1
);
DROP INDEX IF EXISTS paliad.notes_project_idx;
DROP INDEX IF EXISTS paliad.notes_deadline_idx;
DROP INDEX IF EXISTS paliad.notes_appointment_idx;
DROP INDEX IF EXISTS paliad.notes_project_event_idx;
CREATE INDEX notizen_akte_idx ON paliad.notizen (akte_id) WHERE akte_id IS NOT NULL;
CREATE INDEX notizen_frist_idx ON paliad.notizen (frist_id) WHERE frist_id IS NOT NULL;
CREATE INDEX notizen_termin_idx ON paliad.notizen (termin_id) WHERE termin_id IS NOT NULL;
CREATE INDEX notizen_akten_event_idx ON paliad.notizen (akten_event_id) WHERE akten_event_id IS NOT NULL;
-- checklist_instances: restore akte_id column.
ALTER TABLE paliad.checklist_instances DROP CONSTRAINT IF EXISTS checklist_instances_project_id_fkey;
ALTER TABLE paliad.checklist_instances RENAME COLUMN project_id TO akte_id;
ALTER TABLE paliad.checklist_instances
ADD CONSTRAINT checklist_instances_akte_id_fkey FOREIGN KEY (akte_id)
REFERENCES paliad.akten(id) ON DELETE SET NULL;
DROP INDEX IF EXISTS paliad.checklist_instances_project_idx;
CREATE INDEX checklist_instances_akte_idx ON paliad.checklist_instances (akte_id) WHERE akte_id IS NOT NULL;
-- 5. Drop new tables + triggers.
DROP TRIGGER IF EXISTS projects_rewrite_subtree_after ON paliad.projects;
DROP TRIGGER IF EXISTS projects_sync_path_before ON paliad.projects;
DROP FUNCTION IF EXISTS paliad.projects_rewrite_subtree();
DROP FUNCTION IF EXISTS paliad.projects_sync_path();
DROP TABLE IF EXISTS paliad.department_members;
DROP TABLE IF EXISTS paliad.departments;
DROP TABLE IF EXISTS paliad.project_teams;
DROP TABLE IF EXISTS paliad.projects;
-- 6. Restore the v1 visibility helpers + RLS policies.
CREATE OR REPLACE FUNCTION paliad.can_see_akte(_akte_id uuid)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = paliad, public
AS $$
SELECT EXISTS (
SELECT 1
FROM paliad.akten a
LEFT JOIN paliad.users u ON u.id = auth.uid()
WHERE a.id = _akte_id
AND (
a.firm_wide_visible
OR (u.office IS NOT NULL AND a.owning_office = u.office)
OR auth.uid() = ANY (a.collaborators)
OR (u.role = 'admin')
)
);
$$;
CREATE OR REPLACE FUNCTION paliad.notiz_is_visible(
_akte_id uuid,
_frist_id uuid,
_termin_id uuid,
_akten_event_id uuid
) RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = paliad, public
AS $$
SELECT CASE
WHEN _akte_id IS NOT NULL THEN paliad.can_see_akte(_akte_id)
WHEN _frist_id IS NOT NULL THEN paliad.can_see_akte((SELECT akte_id FROM paliad.fristen WHERE id = _frist_id))
WHEN _termin_id IS NOT NULL THEN
CASE
WHEN (SELECT akte_id FROM paliad.termine WHERE id = _termin_id) IS NULL
THEN (SELECT created_by FROM paliad.termine WHERE id = _termin_id) = auth.uid()
ELSE paliad.can_see_akte((SELECT akte_id FROM paliad.termine WHERE id = _termin_id))
END
WHEN _akten_event_id IS NOT NULL THEN paliad.can_see_akte((SELECT akte_id FROM paliad.akten_events WHERE id = _akten_event_id))
ELSE false
END;
$$;
ALTER TABLE paliad.akten ENABLE ROW LEVEL SECURITY;
CREATE POLICY akten_select ON paliad.akten
FOR SELECT TO authenticated
USING (paliad.can_see_akte(id));
CREATE POLICY akten_insert ON paliad.akten
FOR INSERT TO authenticated
WITH CHECK (
owning_office = (SELECT office FROM paliad.users WHERE id = auth.uid())
OR (SELECT role FROM paliad.users WHERE id = auth.uid()) = 'admin'
);
CREATE POLICY akten_update ON paliad.akten
FOR UPDATE TO authenticated
USING (paliad.can_see_akte(id))
WITH CHECK (paliad.can_see_akte(id));
CREATE POLICY akten_delete ON paliad.akten
FOR DELETE TO authenticated
USING (
paliad.can_see_akte(id)
AND (SELECT role FROM paliad.users WHERE id = auth.uid()) IN ('partner', 'admin')
);
CREATE POLICY parteien_all ON paliad.parteien
FOR ALL TO authenticated
USING (paliad.can_see_akte(akte_id))
WITH CHECK (paliad.can_see_akte(akte_id));
CREATE POLICY fristen_all ON paliad.fristen
FOR ALL TO authenticated
USING (paliad.can_see_akte(akte_id))
WITH CHECK (paliad.can_see_akte(akte_id));
CREATE POLICY termine_select ON paliad.termine
FOR SELECT TO authenticated
USING ((akte_id IS NULL AND created_by = auth.uid())
OR (akte_id IS NOT NULL AND paliad.can_see_akte(akte_id)));
CREATE POLICY termine_insert ON paliad.termine
FOR INSERT TO authenticated
WITH CHECK ((akte_id IS NULL AND created_by = auth.uid())
OR (akte_id IS NOT NULL AND paliad.can_see_akte(akte_id)));
CREATE POLICY termine_update ON paliad.termine
FOR UPDATE TO authenticated
USING ((akte_id IS NULL AND created_by = auth.uid())
OR (akte_id IS NOT NULL AND paliad.can_see_akte(akte_id)))
WITH CHECK ((akte_id IS NULL AND created_by = auth.uid())
OR (akte_id IS NOT NULL AND paliad.can_see_akte(akte_id)));
CREATE POLICY termine_delete ON paliad.termine
FOR DELETE TO authenticated
USING ((akte_id IS NULL AND created_by = auth.uid())
OR (akte_id IS NOT NULL AND paliad.can_see_akte(akte_id)));
CREATE POLICY dokumente_all ON paliad.dokumente
FOR ALL TO authenticated
USING (paliad.can_see_akte(akte_id))
WITH CHECK (paliad.can_see_akte(akte_id));
CREATE POLICY akten_events_all ON paliad.akten_events
FOR ALL TO authenticated
USING (paliad.can_see_akte(akte_id))
WITH CHECK (paliad.can_see_akte(akte_id));
CREATE POLICY notizen_all ON paliad.notizen
FOR ALL TO authenticated
USING (paliad.notiz_is_visible(akte_id, frist_id, termin_id, akten_event_id))
WITH CHECK (paliad.notiz_is_visible(akte_id, frist_id, termin_id, akten_event_id));
CREATE POLICY checklist_instances_select ON paliad.checklist_instances
FOR SELECT TO authenticated
USING ((akte_id IS NULL AND created_by = auth.uid())
OR (akte_id IS NOT NULL AND paliad.can_see_akte(akte_id)));
CREATE POLICY checklist_instances_insert ON paliad.checklist_instances
FOR INSERT TO authenticated
WITH CHECK (created_by = auth.uid()
AND (akte_id IS NULL OR paliad.can_see_akte(akte_id)));
CREATE POLICY checklist_instances_update ON paliad.checklist_instances
FOR UPDATE TO authenticated
USING ((akte_id IS NULL AND created_by = auth.uid())
OR (akte_id IS NOT NULL AND paliad.can_see_akte(akte_id)))
WITH CHECK ((akte_id IS NULL AND created_by = auth.uid())
OR (akte_id IS NOT NULL AND paliad.can_see_akte(akte_id)));
CREATE POLICY checklist_instances_delete ON paliad.checklist_instances
FOR DELETE TO authenticated
USING ((akte_id IS NULL AND created_by = auth.uid())
OR (akte_id IS NOT NULL AND paliad.can_see_akte(akte_id)));
-- 7. Drop the users.additional_offices column we added.
ALTER TABLE paliad.users DROP COLUMN IF EXISTS additional_offices;