feat: UPC deadline determination — event-driven model with proceeding timeline

Full event-driven deadline determination system ported from youpc.org:

Backend:
- DetermineService: walks proceeding event tree, calculates cascading
  dates with holiday adjustment and conditional logic
- GET /api/proceeding-types/{code}/timeline — full event tree structure
- POST /api/deadlines/determine — calculate timeline with conditions
- POST /api/cases/{caseID}/deadlines/batch — batch-create deadlines
- DeadlineRule model: added is_spawn, spawn_label fields
- GetFullTimeline: recursive CTE following cross-type spawn branches
- Conditional deadlines: condition_rule_id toggles alt_duration/rule_code
  (e.g. Reply changes from RoP.029b to RoP.029a when CCR is filed)
- Seed SQL with full UPC event trees (INF, REV, CCR, APM, APP, AMD)

Frontend:
- DeadlineWizard: interactive proceeding timeline with step-by-step flow
  1. Select proceeding type (visual cards)
  2. Enter trigger event date
  3. Toggle conditional branches (CCR, Appeal, Amend)
  4. See full calculated timeline with color-coded urgency
  5. Batch-create all deadlines on a selected case
- Visual timeline tree with party icons, rule codes, duration badges
- Kept existing DeadlineCalculator as "Schnell" quick mode

Also resolved merge conflicts across 6 files (auth, router, handlers)
merging role-based permissions + audit trail features.
This commit is contained in:
m
2026-03-30 11:33:59 +02:00
parent 8e65463130
commit a89ef26ebd
14 changed files with 1642 additions and 171 deletions

View File

@@ -10,57 +10,32 @@ import (
)
type mockTenantLookup struct {
<<<<<<< HEAD
tenantID *uuid.UUID
err error
hasAccess bool
accessErr error
||||||| 82878df
tenantID *uuid.UUID
err error
=======
tenantID *uuid.UUID
role string
err error
>>>>>>> mai/pike/p0-role-based
}
func (m *mockTenantLookup) FirstTenantForUser(ctx context.Context, userID uuid.UUID) (*uuid.UUID, error) {
return m.tenantID, m.err
}
<<<<<<< HEAD
func (m *mockTenantLookup) VerifyAccess(ctx context.Context, userID, tenantID uuid.UUID) (bool, error) {
return m.hasAccess, m.accessErr
}
||||||| 82878df
=======
func (m *mockTenantLookup) GetUserRole(ctx context.Context, userID, tenantID uuid.UUID) (string, error) {
if m.role != "" {
return m.role, m.err
}
return "associate", m.err
return m.role, m.err
}
>>>>>>> mai/pike/p0-role-based
func TestTenantResolver_FromHeader(t *testing.T) {
tenantID := uuid.New()
<<<<<<< HEAD
tr := NewTenantResolver(&mockTenantLookup{hasAccess: true})
||||||| 82878df
tr := NewTenantResolver(&mockTenantLookup{})
=======
tr := NewTenantResolver(&mockTenantLookup{role: "partner"})
>>>>>>> mai/pike/p0-role-based
var gotTenantID uuid.UUID
var gotRole string
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id, ok := TenantFromContext(r.Context())
if !ok {
t.Fatal("tenant ID not in context")
}
gotTenantID = id
gotRole = UserRoleFromContext(r.Context())
w.WriteHeader(http.StatusOK)
})
@@ -77,11 +52,14 @@ func TestTenantResolver_FromHeader(t *testing.T) {
if gotTenantID != tenantID {
t.Errorf("expected tenant %s, got %s", tenantID, gotTenantID)
}
if gotRole != "partner" {
t.Errorf("expected role partner, got %s", gotRole)
}
}
func TestTenantResolver_FromHeader_NoAccess(t *testing.T) {
tenantID := uuid.New()
tr := NewTenantResolver(&mockTenantLookup{hasAccess: false})
tr := NewTenantResolver(&mockTenantLookup{role: ""})
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("next should not be called")
@@ -101,7 +79,7 @@ func TestTenantResolver_FromHeader_NoAccess(t *testing.T) {
func TestTenantResolver_DefaultsToFirst(t *testing.T) {
tenantID := uuid.New()
tr := NewTenantResolver(&mockTenantLookup{tenantID: &tenantID})
tr := NewTenantResolver(&mockTenantLookup{tenantID: &tenantID, role: "associate"})
var gotTenantID uuid.UUID
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {