Files
paliad/internal/db/migrations/076_smart_timeline_slice_2.up.sql
m 85d7dd497c feat(t-paliad-173): SmartTimeline Slice 2 backend — projection + anchor + skip + sequence guard
Slice 2 of the SmartTimeline (docs/design-smart-timeline-2026-05-08.md
§6 + §9 + §10) bundled with m/paliad#31's layered requirements:

Migration 076:
- appointments.deadline_rule_id nullable FK to deadline_rules + partial idx
- deadlines.source CHECK widened to include 'anchor' (alongside existing
  'manual','fristenrechner','rule','import').

ProjectionService (extended):
- Wires FristenrechnerService + DeadlineRuleService.
- For() now emits Kind="projected" rows for any rule lacking a matching
  paliad.deadlines.rule_id / appointments.deadline_rule_id row, with
  Status in {predicted | predicted_overdue | court_set}.
- Lookahead cap (default 7, override via ?lookahead=N, max 50): future
  predicted rows beyond N are dropped; predicted_overdue + court_set
  rows are exempt from the cap (#31 layer 1).
- Dependency annotations DependsOnRuleCode/Date/Name on every row that
  carries a DeadlineRuleID, walked from the rule's parent_id chain
  (#31 layer 2). Date prefers actuals over projections.
- AnchorOverrides built from completed deadlines (completed_at /
  status='completed') + appointments tied via deadline_rule_id.
- triggerDate derives from the proceeding's root rule's anchor when
  present, else today() as placeholder.

Anchor write path (POST /api/projects/{id}/timeline/anchor):
- Sequence guard: if rule.parent_id has no anchored actual, return
  409 predecessor_missing with the missing rule's code/name DE+EN +
  pre-formatted bilingual messages so the frontend can render an
  inline error with a "Stattdessen <predecessor> erfassen" link
  (#31 layer 3, no confirm-and-write override in v1).
- kind dispatch: rules with event_type IN ('hearing','decision','order')
  write paliad.appointments with deadline_rule_id; everything else
  writes paliad.deadlines with source='anchor', status='completed',
  completed_at=actual_date.
- Idempotent: existing (project_id, rule_id) row PATCHes instead of
  inserting (race-safe per design §13).

Skip write path (POST /api/projects/{id}/timeline/skip):
- Writes paliad.project_events with event_type='rule_skipped' +
  metadata.rule_code; subsequent reads drop the matching projected
  row from the cascade (§6.4).

Handlers expose projection meta via X-Projection-{Has,Total,Shown,Overdue,Lookahead}
headers so the wire shape stays []TimelineEvent (frozen since Slice 1).
2026-05-09 15:33:20 +02:00

58 lines
2.4 KiB
SQL

-- t-paliad-173 — SmartTimeline Slice 2.
-- Two structural additions for click-to-anchor (§6 of
-- docs/design-smart-timeline-2026-05-08.md) + the layered SoC→SoD
-- sequence enforcement from m/paliad#31:
--
-- 1. paliad.appointments.deadline_rule_id — nullable FK to
-- paliad.deadline_rules. Court-set rules (Hauptverhandlung,
-- Decision, Order) anchor as appointments rather than deadlines
-- and need to remember which rule they came from so downstream
-- reflow has the parent_id chain.
--
-- 2. paliad.deadlines.source CHECK — adds 'anchor' alongside
-- the existing 'manual' / 'fristenrechner' values + the two
-- legacy values the design doc mentions ('rule', 'import') for
-- forward-compat. 'anchor' separates a click-to-anchor write from
-- a user-typed-it-in 'manual' write so analytics + a future
-- Outlook-import path can tell them apart.
--
-- paliad.project_events.event_type is intentionally NOT constrained —
-- the column is free-text in prod (every event_type today lives in
-- code, not in a CHECK). Slice 2 needs to write 'rule_skipped' rows
-- (§6.4); no schema change is required for that.
--
-- Idempotent: re-applying is a no-op. Tracker advances 75 → 76.
-- 1. paliad.appointments.deadline_rule_id ----------------------------------
ALTER TABLE paliad.appointments
ADD COLUMN IF NOT EXISTS deadline_rule_id uuid NULL
REFERENCES paliad.deadline_rules(id) ON DELETE SET NULL;
COMMENT ON COLUMN paliad.appointments.deadline_rule_id IS
'When non-NULL, this appointment is the actual occurrence of a '
'standard-course rule (Hauptverhandlung, Decision, Order). '
'Anchors downstream re-projection via FristenrechnerService '
'AnchorOverrides. See docs/design-smart-timeline-2026-05-08.md §6.';
CREATE INDEX IF NOT EXISTS appointments_deadline_rule_id_idx
ON paliad.appointments (deadline_rule_id)
WHERE deadline_rule_id IS NOT NULL;
-- 2. paliad.deadlines.source CHECK -----------------------------------------
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'deadlines_source_check'
AND conrelid = 'paliad.deadlines'::regclass
) THEN
ALTER TABLE paliad.deadlines DROP CONSTRAINT deadlines_source_check;
END IF;
END $$;
ALTER TABLE paliad.deadlines
ADD CONSTRAINT deadlines_source_check
CHECK (source IN ('manual', 'fristenrechner', 'rule', 'import', 'anchor'));