Files
paliad/internal/db/migrations/011_feedback_tables.up.sql
m 1b2ef28334 feat(db): Phase A — paliad schema, RLS, migrations, golang-migrate
Implements docs/design-kanzlai-integration.md §8 Phase A.

Schema (paliad.*):
- users (extends auth.users) with office, practice_group, role
- akten with visibility columns: owning_office, collaborators uuid[],
  firm_wide_visible (per design §2)
- parteien, fristen, termine, dokumente, akten_events, notizen
  (polymorphic notes; notizen_exactly_one_parent CHECK)
- proceeding_types, deadline_rules, holidays (reference data)
- 4 feedback tables re-namespaced from public.* into paliad.*
  (handler swap to direct DB is a follow-up; old public tables stay
  intact for now and continue serving via PostgREST)

Visibility (paliad.can_see_akte):
- single SQL function, used by every RLS policy
- predicate: firm_wide_visible OR owning_office matches user's office
  OR auth.uid() ∈ collaborators OR user is admin
- mirrored at app layer in Phase B (defense in depth)

RLS (real, not permissive):
- akten: visibility predicate; insert restricted to own office or admin;
  delete restricted to partners + admins
- parteien/fristen/dokumente/akten_events: inherit via can_see_akte(akte_id)
- termine: personal (akte_id NULL) visible only to creator; Akte-linked
  follow visibility predicate
- notizen: paliad.notiz_is_visible() resolves polymorphic parent
- reference tables: SELECT for any authenticated user
- users: SELECT all; UPDATE/INSERT only self
- feedback tables: INSERT for any authenticated user (write-only)

Seed data (ported from KanzlAI seed_upc_timeline.sql):
- 7 proceeding_types (INF, REV, CCR, APM, APP, AMD, ZPO_CIVIL)
- 40 deadline_rules (32 UPC + 4 ZPO + 4 cross-type appeal spawns)
  including conditional logic: Reply rule code (RoP.029b → 029a) and
  Rejoinder duration (1mo → 2mo) flip when CCR active
- 55 holidays (DE federal 2026/2027 + UPC summer 2026 + UPC winter 26/27)

Indexes per audit §3.3 + visibility-predicate hot paths:
- akten: (status, owning_office), (owning_office), partial on
  firm_wide_visible, GIN on collaborators
- fristen: (status, due_date), (akte_id)
- termine: (start_at), (akte_id)
- akten_events: (akte_id, created_at DESC)
- notizen: 4 partial indexes per parent type
- users: (office), (role)

Migration tooling:
- golang-migrate/migrate/v4 with embed.FS source
- Migrations live in internal/db/migrations/ (Go embed can't reach
  outside the package; this is the conventional Go layout for embedded
  migrations)
- Applied at server startup before HTTP listener binds
- DATABASE_URL is optional today (existing knowledge tools work without
  DB); becomes required once Phase B services land
- Mock Supabase auth schema for local testing in
  internal/db/migrations/_dev/mock_supabase_auth.sql (excluded from
  embed pattern by the underscore prefix)

Other changes:
- Dockerfile: bump golang to 1.24, copy go.sum (audit §2.9), rename
  binary patholo → paliad
- docker-compose.yml: add DATABASE_URL passthrough
- README.md: rewritten to reflect Paliad brand + Phase A migration system

Verified locally:
- 11 migrations applied cleanly against postgres:16-alpine
- RLS enabled on all 15 paliad.* tables (verified via pg_class.relrowsecurity)
- Visibility predicate verified with 4-case scenario:
  - Alice (Munich associate): sees Munich + firm-wide + collab-on (t f t t)
  - Bob (Düsseldorf associate): sees Düsseldorf + firm-wide + collab-on (f t t t)
  - Carol (Munich partner): sees Munich + firm-wide only (t f t f)
  - Anonymous: sees firm-wide only (f f t f)
- migrate down + re-up cycle clean (initial 007 down had ordering bug,
  fixed: drop policies before referenced function)
- Existing endpoints (/, /login) return 302 + 200 — no regressions
2026-04-16 13:54:19 +02:00

72 lines
3.3 KiB
SQL

-- Phase A: feedback tables re-namespaced into paliad.*.
-- Replaces docs/migrations/00{1,2,3}.sql which put them in public schema.
--
-- Old tables (public.patholo_link_suggestions, public.patholo_link_feedback,
-- public.checklisten_feedback, public.gerichte_feedback) continue to exist
-- and are still referenced by the current handlers via PostgREST.
--
-- Phase A creates the paliad versions fresh (no data copy) so the rest of
-- the platform can adopt them. A follow-on phase (P1 handler refactor) will:
-- (1) swap handlers from PostgREST to the direct DB connection,
-- (2) copy any meaningful rows from the public tables,
-- (3) drop the public tables and this duplication.
--
-- This is flagged in the Phase A completion report.
-- Link suggestions (user-submitted via "Nützlichen Link vorschlagen")
CREATE TABLE paliad.link_suggestions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
title text NOT NULL,
url text NOT NULL,
category text NOT NULL,
description text NOT NULL DEFAULT '',
suggested_by text NOT NULL DEFAULT '',
status text NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'approved', 'rejected')),
created_at timestamptz NOT NULL DEFAULT now()
);
-- Link feedback (per-card feedback icon)
CREATE TABLE paliad.link_feedback (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
link_id text NOT NULL,
feedback_type text NOT NULL,
message text NOT NULL DEFAULT '',
submitted_by text NOT NULL DEFAULT '',
created_at timestamptz NOT NULL DEFAULT now()
);
-- Checklisten feedback
CREATE TABLE paliad.checklisten_feedback (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
feedback_type text NOT NULL,
checklist text NOT NULL DEFAULT '',
message text NOT NULL,
submitted_by text NOT NULL DEFAULT '',
created_at timestamptz NOT NULL DEFAULT now()
);
-- Gerichtsverzeichnis feedback
CREATE TABLE paliad.gerichte_feedback (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
court_id text NOT NULL DEFAULT '',
feedback_type text NOT NULL,
message text NOT NULL,
submitted_by text NOT NULL DEFAULT '',
created_at timestamptz NOT NULL DEFAULT now()
);
-- RLS: authenticated users can insert; no general read (admin-only, via service role).
ALTER TABLE paliad.link_suggestions ENABLE ROW LEVEL SECURITY;
ALTER TABLE paliad.link_feedback ENABLE ROW LEVEL SECURITY;
ALTER TABLE paliad.checklisten_feedback ENABLE ROW LEVEL SECURITY;
ALTER TABLE paliad.gerichte_feedback ENABLE ROW LEVEL SECURITY;
CREATE POLICY link_suggestions_insert ON paliad.link_suggestions FOR INSERT TO authenticated WITH CHECK (true);
CREATE POLICY link_feedback_insert ON paliad.link_feedback FOR INSERT TO authenticated WITH CHECK (true);
CREATE POLICY checklisten_feedback_insert ON paliad.checklisten_feedback FOR INSERT TO authenticated WITH CHECK (true);
CREATE POLICY gerichte_feedback_insert ON paliad.gerichte_feedback FOR INSERT TO authenticated WITH CHECK (true);
-- Indexes: suggestion-count widget filters on status; others are append-mostly.
CREATE INDEX link_suggestions_status_idx ON paliad.link_suggestions (status);