Files
paliad/internal/db/migrations/135_primary_party_check.up.sql
mAi 989941c648
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
feat(litigationplanner): primary_party CHECK constraint + IsValidPrimaryParty helper (Slice B3, m/paliad#124 §18.3)
Tightens paliad.deadline_rules.primary_party from free-text to a CHECK
constraint over the canonical four-value vocab (claimant / defendant /
court / both). NULL stays valid for the 78 cross-cutting orphan
concept seeds (Wiedereinsetzung, Versäumnisurteil-Einspruch,
Schriftsatznachreichung, Weiterbehandlung) — they have no
proceeding_type_id binding so they're outside the calculator's path;
loosening the CHECK to "IS NULL OR IN (…)" keeps them valid without
backfill gymnastics.

Migration 135 (audit-first):
  - DO block RAISEs NOTICE for every non-conforming row + RAISEs
    EXCEPTION if any dirty rows exist (manual cleanup required).
    Live audit (Supabase, 2026-05-26 §18.0) confirmed zero dirty rows
    on the current corpus; the audit pass stays in the migration as
    safety against future drift.
  - ALTER TABLE … ADD CONSTRAINT deadline_rules_primary_party_chk
    CHECK (primary_party IS NULL OR primary_party IN
           ('claimant', 'defendant', 'court', 'both'))
  - Post-migration distribution NOTICE so the operator sees the
    final per-value count.
  - Down = DROP CONSTRAINT. No data revert needed.

Package additions (pkg/litigationplanner):
  - PrimaryParty* constants (PrimaryPartyClaimant / Defendant / Court
    / Both) + PrimaryParties[] ordered list + IsValidPrimaryParty(s)
    predicate. Empty string is "no value supplied" = valid (NULL maps
    to empty on the wire); non-empty must match one of the four
    canonical values.
  - Sibling unit tests (primary_party_test.go) pin the four-value
    vocab + the chip order + IsValidAppealTarget's matching shape.

Rule-editor validation hook (rule_editor_service.go):
  - Create() validates input.PrimaryParty before INSERT.
  - UpdateDraft() validates patch.PrimaryParty before UPDATE.
  - Both surface a user-friendly 400 with the canonical vocab listed
    instead of leaking the raw PG CHECK constraint-violation message.
  - Uses errors.Is(err, ErrInvalidInput) so handler 400 routing
    continues to work.

services/fristenrechner.go cleanup:
  - The B2-inlined isValidPartyForLookup helper is replaced with the
    canonical lp.IsValidPrimaryParty. No behaviour change.

No frontend changes — the rule-editor's primary_party UI already
constrains to the four values via a select; the validation hook is
defense-in-depth.

Audit:
  - go build + go test (incl. new lp unit tests) all green
  - Pre-migration audit confirmed: 26 claimant + 26 defendant + 38
    court + 63 both + 78 NULL = 231 total, all in canonical vocab
  - event_categories.party (text[] array, narrower semantic) is
    NOT touched in this migration per the design doc's
    "out of scope, separate follow-up" decision
2026-05-26 13:58:33 +02:00

93 lines
3.9 KiB
SQL

-- 135_primary_party_check — Slice B3, m/paliad#124 §18.3
--
-- Tightens paliad.deadline_rules.primary_party from free-text to a
-- CHECK constraint over the canonical four-value vocabulary
-- (claimant / defendant / court / both). NULL stays valid for the
-- 78 cross-cutting orphan concept seeds (Wiedereinsetzung,
-- Versäumnisurteil-Einspruch, Schriftsatznachreichung,
-- Weiterbehandlung) — they have no proceeding_type_id binding so
-- they're outside the calculator's path; loosening the CHECK to
-- "IS NULL OR IN (…)" keeps them valid without backfill gymnastics.
--
-- Audit-first: the DO block RAISEs NOTICE for every non-conforming
-- row before adding the CHECK, and RAISEs EXCEPTION if any dirty
-- rows are found so the operator can decide a manual cleanup path.
-- Live audit (Supabase, 2026-05-26 §18.0) confirmed zero dirty rows
-- on the current corpus: 26 claimant + 26 defendant + 38 court +
-- 63 both + 78 NULL = 231 total, all in the canonical vocab. The
-- audit pass stays in the migration for safety against future drift
-- (e.g. a rule editor write that bypassed the application-layer
-- validation hook this slice also adds).
DO $$
DECLARE
rec record;
dirty_count int := 0;
BEGIN
RAISE NOTICE '[mig 135] primary_party audit pass — non-conforming rows:';
FOR rec IN
SELECT dr.id, dr.name, dr.primary_party,
pt.code AS proceeding_code
FROM paliad.deadline_rules dr
LEFT JOIN paliad.proceeding_types pt ON pt.id = dr.proceeding_type_id
WHERE dr.is_active = true
AND dr.primary_party IS NOT NULL
AND dr.primary_party NOT IN ('claimant', 'defendant', 'court', 'both')
ORDER BY pt.code NULLS LAST, dr.name
LOOP
RAISE NOTICE '[mig 135] % % primary_party=% (rule=%)',
COALESCE(rec.proceeding_code, '<orphan>'),
rec.name,
rec.primary_party,
rec.id;
dirty_count := dirty_count + 1;
END LOOP;
IF dirty_count > 0 THEN
RAISE EXCEPTION '[mig 135] FAILED — % rule(s) carry non-canonical primary_party values. '
'Manual cleanup required: update each row to one of '
'''claimant'', ''defendant'', ''court'', ''both'', or NULL. '
'See the NOTICE lines above for the offending rows.', dirty_count;
END IF;
RAISE NOTICE '[mig 135] audit clean — proceeding with CHECK constraint';
END $$;
-- ---------------------------------------------------------------
-- Add the CHECK constraint. NULL stays valid; the four canonical
-- values are the only allowed non-NULL forms.
-- ---------------------------------------------------------------
ALTER TABLE paliad.deadline_rules
ADD CONSTRAINT deadline_rules_primary_party_chk
CHECK (
primary_party IS NULL
OR primary_party IN ('claimant', 'defendant', 'court', 'both')
);
COMMENT ON CONSTRAINT deadline_rules_primary_party_chk
ON paliad.deadline_rules IS
'Slice B3 (mig 135, m/paliad#124 §18.3) — canonical four-value '
'vocab for primary_party (claimant / defendant / court / both). '
'NULL allowed for cross-cutting orphan concept seeds (78 rows in '
'live corpus as of mig 135). See pkg/litigationplanner.PrimaryParties '
'for the in-code vocabulary.';
-- ---------------------------------------------------------------
-- Post-migration distribution check — informational NOTICE only.
-- ---------------------------------------------------------------
DO $$
DECLARE
rec record;
BEGIN
RAISE NOTICE '[mig 135] post: primary_party distribution after constraint add:';
FOR rec IN
SELECT COALESCE(primary_party, '<NULL>') AS party, COUNT(*) AS n
FROM paliad.deadline_rules
WHERE is_active = true
GROUP BY primary_party
ORDER BY party
LOOP
RAISE NOTICE '[mig 135] % count=%', rec.party, rec.n;
END LOOP;
END $$;