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
93 lines
3.9 KiB
SQL
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 $$;
|