Files
paliad/internal/models/models.go
m 0c12644563 feat(deadline-rules): expose concept's canonical event_type per rule
Add paliad.deadline_concept_event_types junction (mig 072) mapping each
deadline_concept to its canonical paliad.event_types row(s). Hydrate
DeadlineRule.ConceptDefaultEventTypeID via one IN query per List call so
/api/deadline-rules carries the autofill hint for the deadline create
form (t-paliad-165 / m/paliad#18).

Seed mapping covers the active concepts driving existing rules — 29
rows across 26 distinct concepts. Concepts without an obvious event_type
counterpart (decision, filing, grant, the DE-only Begründung family)
stay unmapped; auto-fill silently skips them.
2026-05-08 21:55:15 +02:00

703 lines
38 KiB
Go

// Package models holds the database row types for paliad.* tables.
// Names are English throughout; only user-facing i18n strings live in the
// frontend. See internal/db/migrations/ for the canonical schema definitions.
package models
import (
"database/sql/driver"
"encoding/json"
"fmt"
"time"
"github.com/google/uuid"
"github.com/lib/pq"
)
// NullableJSON is a jsonb column that may be NULL. json.RawMessage
// (and *json.RawMessage) doesn't implement sql.Scanner, so a NULL value
// from Postgres breaks the row scan with "unsupported Scan, storing
// driver.Value type <nil> into type *json.RawMessage" — exactly the
// error that hid every approval_request from the inbox when m's first
// "create" lifecycle row arrived with NULL pre_image (m's dogfood
// 2026-05-08 20:35). Using NullableJSON on every nullable jsonb column
// fixes the scan and preserves inline JSON output (no base64 cast).
type NullableJSON []byte
func (n *NullableJSON) Scan(value any) error {
if value == nil {
*n = nil
return nil
}
switch v := value.(type) {
case []byte:
*n = append((*n)[:0], v...)
return nil
case string:
*n = []byte(v)
return nil
}
return fmt.Errorf("NullableJSON: unsupported scan type %T", value)
}
func (n NullableJSON) Value() (driver.Value, error) {
if len(n) == 0 {
return nil, nil
}
return []byte(n), nil
}
func (n NullableJSON) MarshalJSON() ([]byte, error) {
if len(n) == 0 {
return []byte("null"), nil
}
return []byte(n), nil
}
func (n *NullableJSON) UnmarshalJSON(data []byte) error {
if string(data) == "null" {
*n = nil
return nil
}
*n = append((*n)[:0], data...)
return nil
}
// User extends auth.users with firm-specific profile fields. Created by the
// Phase D onboarding flow; without a row here, the user can't see any Projects.
type User struct {
ID uuid.UUID `db:"id" json:"id"`
Email string `db:"email" json:"email"`
DisplayName string `db:"display_name" json:"display_name"`
Office string `db:"office" json:"office"`
// AdditionalOffices lists secondary offices a partner works across.
// Informational only — office is not a visibility gate under the v2
// data model (t-paliad-024).
AdditionalOffices pq.StringArray `db:"additional_offices" json:"additional_offices"`
PracticeGroup *string `db:"practice_group" json:"practice_group,omitempty"`
// JobTitle is free-text display only ("Partner", "Counsel", "PA",
// "Counsel Knowledge Lawyer", …). NULL is allowed for users who never
// picked a title — typically global admins promoted via SQL.
JobTitle *string `db:"job_title" json:"job_title"`
// Profession is the structured firm-tier enum that drives the
// t-paliad-138 / t-paliad-148 approval ladder (partner / of_counsel /
// associate / senior_pa / pa / paralegal). NULL means "no firm tier"
// — external collaborators (local counsel, expert) and admin
// accounts that aren't practicing lawyers. NULL → ladder level 0,
// ineligible to approve. Distinct from JobTitle (display) and
// GlobalRole (tool admin gate). Added by migration 057.
Profession *string `db:"profession" json:"profession,omitempty"`
// GlobalRole is the global-permissions enum: 'standard' | 'global_admin'.
// Drives every permission gate that used to look at the legacy
// role='admin'. Per-project authority is on paliad.project_teams and
// users.profession; this column is the tool-admin axis, unrelated.
GlobalRole string `db:"global_role" json:"global_role"`
Lang string `db:"lang" json:"lang"`
EmailPreferences json.RawMessage `db:"email_preferences" json:"email_preferences"`
// ReminderMorningTime / ReminderEveningTime are stored as Postgres TIME and
// scanned as strings in HH:MM:SS form so we don't need a separate type and
// the JSON shape stays trivially editable from the settings page.
ReminderMorningTime string `db:"reminder_morning_time" json:"reminder_morning_time"`
ReminderEveningTime string `db:"reminder_evening_time" json:"reminder_evening_time"`
ReminderTimezone string `db:"reminder_timezone" json:"reminder_timezone"`
// ReminderWarningOffsetDays controls how many days before each pending
// deadline the heads-up section ("In einer Woche fällig") fires. Default
// 7. Range 1..30 enforced by a CHECK constraint in migration 025.
ReminderWarningOffsetDays int `db:"reminder_warning_offset_days" json:"reminder_warning_offset_days"`
// EscalationContactID is an optional override of the escalation channel
// for overdue / DRINGEND mail. NULL means "fall back to global_admins".
// Settings UI dropdown shipped 2026-04-29 (t-paliad-066).
EscalationContactID *uuid.UUID `db:"escalation_contact_id" json:"escalation_contact_id,omitempty"`
// ForumPref is the user's persisted Fristenrechner inbox-channel
// preference (#15): "cms" → UPC; "bea" → national-DE;
// "posteingang" → national-DE (slower channel, same forums). NULL =
// no preference. URL ?inbox= overrides per-visit.
ForumPref *string `db:"forum_pref" json:"forum_pref,omitempty"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
// Project is one node in the paliad.projects tree. Visibility is team-based
// (direct or inherited via the materialised path) — see paliad.can_see_project.
// Type-specific fields are nullable; the service layer enforces the subset
// that applies to each type.
type Project struct {
ID uuid.UUID `db:"id" json:"id"`
Type string `db:"type" json:"type"`
ParentID *uuid.UUID `db:"parent_id" json:"parent_id,omitempty"`
// Path is the '.'-joined UUID list from root to self (inclusive).
// Maintained by a Postgres trigger — writes from the service are ignored.
Path string `db:"path" json:"path"`
Title string `db:"title" json:"title"`
Reference *string `db:"reference" json:"reference,omitempty"`
Description *string `db:"description" json:"description,omitempty"`
Status string `db:"status" json:"status"`
CreatedBy *uuid.UUID `db:"created_by" json:"created_by,omitempty"`
// Client-specific (type='client'), nullable otherwise.
Industry *string `db:"industry" json:"industry,omitempty"`
Country *string `db:"country" json:"country,omitempty"`
BillingReference *string `db:"billing_reference" json:"billing_reference,omitempty"`
// ClientMatter numbers — external billing/DMS identifiers used by the firm.
// Child rows inherit client_number from the root by default (resolved at
// read time by the service); a child with its own client_number overrides.
// matter_number is assigned independently at any level.
ClientNumber *string `db:"client_number" json:"client_number,omitempty"`
MatterNumber *string `db:"matter_number" json:"matter_number,omitempty"`
NetDocumentsURL *string `db:"netdocuments_url" json:"netdocuments_url,omitempty"`
// Patent-specific (type='patent').
PatentNumber *string `db:"patent_number" json:"patent_number,omitempty"`
FilingDate *time.Time `db:"filing_date" json:"filing_date,omitempty"`
GrantDate *time.Time `db:"grant_date" json:"grant_date,omitempty"`
// Case-specific (type='case').
Court *string `db:"court" json:"court,omitempty"`
CaseNumber *string `db:"case_number" json:"case_number,omitempty"`
ProceedingTypeID *int `db:"proceeding_type_id" json:"proceeding_type_id,omitempty"`
Metadata json.RawMessage `db:"metadata" json:"metadata"`
AISummary *string `db:"ai_summary" json:"ai_summary,omitempty"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
// ProjectTeamMember is one row of paliad.project_teams — direct membership
// only. Inherited memberships are computed at read time by walking the path;
// services set Inherited=true on the in-memory copy when annotating a list
// result that mixes direct + inherited rows.
//
// t-paliad-148 split: Responsibility is the per-project role (lead /
// member / observer / external). The legacy Role field is kept as a
// deprecated read-only shadow until follow-up migration 058 drops the
// underlying column.
type ProjectTeamMember struct {
ID uuid.UUID `db:"id" json:"id"`
ProjectID uuid.UUID `db:"project_id" json:"project_id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
Responsibility string `db:"responsibility" json:"responsibility"`
// Role: deprecated shadow column. Reader populates it for backwards-
// compatibility with any consumer still reading `role`; new code
// should read .Responsibility instead.
Role string `db:"role" json:"role"`
Inherited bool `db:"inherited" json:"inherited"`
AddedBy *uuid.UUID `db:"added_by" json:"added_by,omitempty"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
// ProjectTeamMemberWithUser enriches a team row with display fields so the
// UI can render "<DisplayName> (<Email>) — <Responsibility>" without a
// per-row lookup. Used by TeamService.ListMembers which unions direct +
// inherited memberships.
//
// UserProfession reflects paliad.users.profession at read time — the
// firm-tier badge shown next to the responsibility column on
// /projects/{id} (t-paliad-148 §6).
type ProjectTeamMemberWithUser struct {
ProjectTeamMember
UserEmail string `db:"user_email" json:"user_email"`
UserDisplayName string `db:"user_display_name" json:"user_display_name"`
UserOffice string `db:"user_office" json:"user_office"`
UserProfession *string `db:"user_profession" json:"user_profession,omitempty"`
// InheritedFromID is the ancestor project_id the membership came from
// when Inherited=true. NULL for direct rows.
InheritedFromID *uuid.UUID `db:"inherited_from_id" json:"inherited_from_id,omitempty"`
InheritedFromTitle *string `db:"inherited_from_title" json:"inherited_from_title,omitempty"`
}
// PartnerUnit is one structural partner unit (Dezernat in legacy German).
// Membership is orthogonal to project teams — a user typically belongs to
// exactly one PartnerUnit but may work on projects across all of them.
type PartnerUnit struct {
ID uuid.UUID `db:"id" json:"id"`
Name string `db:"name" json:"name"`
LeadUserID *uuid.UUID `db:"lead_user_id" json:"lead_user_id,omitempty"`
Office string `db:"office" json:"office"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
// PartnerUnitMember is one user's membership in a PartnerUnit.
type PartnerUnitMember struct {
PartnerUnitID uuid.UUID `db:"partner_unit_id" json:"partner_unit_id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
// ProjectEvent is one row in the per-Project audit trail
// (paliad.project_events, renamed from paliad.project_events in migration 018).
//
// ProjectTitle is populated only by readers that join paliad.projects (e.g.
// ProjectService.ListEvents — Verlauf attribution for descendant events on
// /projects/{id}, t-paliad-139). Other readers leave it nil and the JSON
// serialiser omits it.
type ProjectEvent struct {
ID uuid.UUID `db:"id" json:"id"`
ProjectID uuid.UUID `db:"project_id" json:"project_id"`
EventType *string `db:"event_type" json:"event_type,omitempty"`
Title string `db:"title" json:"title"`
Description *string `db:"description" json:"description,omitempty"`
EventDate *time.Time `db:"event_date" json:"event_date,omitempty"`
CreatedBy *uuid.UUID `db:"created_by" json:"created_by,omitempty"`
Metadata json.RawMessage `db:"metadata" json:"metadata"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ProjectTitle *string `db:"project_title" json:"project_title,omitempty"`
}
// Deadline is one persistent deadline attached to a Project (typically a
// case- or patent-level node). Visibility is inherited from the parent
// Project via paliad.can_see_project.
type Deadline struct {
ID uuid.UUID `db:"id" json:"id"`
ProjectID uuid.UUID `db:"project_id" json:"project_id"`
Title string `db:"title" json:"title"`
Description *string `db:"description" json:"description,omitempty"`
DueDate time.Time `db:"due_date" json:"due_date"`
OriginalDueDate *time.Time `db:"original_due_date" json:"original_due_date,omitempty"`
WarningDate *time.Time `db:"warning_date" json:"warning_date,omitempty"`
Source string `db:"source" json:"source"`
RuleID *uuid.UUID `db:"rule_id" json:"rule_id,omitempty"`
// RuleCode is the legal citation ("RoP.023", "R.151") attached at
// save time — see migration 032. Free text by design; survives
// changes to paliad.deadline_rules and accepts citations from
// outside that table.
RuleCode *string `db:"rule_code" json:"rule_code,omitempty"`
Status string `db:"status" json:"status"`
CompletedAt *time.Time `db:"completed_at" json:"completed_at,omitempty"`
CalDAVUID *string `db:"caldav_uid" json:"caldav_uid,omitempty"`
CalDAVEtag *string `db:"caldav_etag" json:"caldav_etag,omitempty"`
Notes *string `db:"notes" json:"notes,omitempty"`
CreatedBy *uuid.UUID `db:"created_by" json:"created_by,omitempty"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
// Approval-workflow columns added by migration 054 (t-paliad-138).
// approval_status: 'approved' (default), 'pending' (a request is in
// flight; pending_request_id is set), 'legacy' (predates 4-eye).
// approved_by / approved_at: populated when a 4-eye approval flips
// the row from 'pending' back to 'approved'. NULL on legacy rows
// and rows that never went through 4-eye.
ApprovalStatus string `db:"approval_status" json:"approval_status"`
PendingRequestID *uuid.UUID `db:"pending_request_id" json:"pending_request_id,omitempty"`
ApprovedBy *uuid.UUID `db:"approved_by" json:"approved_by,omitempty"`
ApprovedAt *time.Time `db:"approved_at" json:"approved_at,omitempty"`
// EventTypeIDs lists the paliad.event_types attached to this deadline
// via the paliad.deadline_event_types junction. Always present (never
// nil) once the row has been hydrated by DeadlineService.
EventTypeIDs []uuid.UUID `db:"-" json:"event_type_ids"`
}
// DeadlineWithProject enriches a Deadline with parent-Project display fields
// (reference + title) for list views. RuleName/RuleNameEN are the
// human-readable label of the linked deadline-rule (e.g. "Replik" / "Reply"),
// pulled from the LEFT JOIN on paliad.deadline_rules.rule_id. The
// RuleCode field is inherited from the embedded Deadline (the row's own
// stored citation, see migration 032) — list views render it directly as
// REGEL.
type DeadlineWithProject struct {
Deadline
ProjectReference *string `db:"project_reference" json:"project_reference,omitempty"`
ProjectTitle string `db:"project_title" json:"project_title"`
ProjectType string `db:"project_type" json:"project_type"`
RuleName *string `db:"rule_name" json:"rule_name,omitempty"`
RuleNameEN *string `db:"rule_name_en" json:"rule_name_en,omitempty"`
// RequesterKind is the kind of the in-flight approval request (when
// approval_status='pending'): 'user' or 'agent' (Paliadin-drafted —
// t-paliad-161). NULL when the row has no pending request. Powers
// the ✨ glyph alongside the eye-pill 👀.
RequesterKind *string `db:"requester_kind" json:"requester_kind,omitempty"`
}
// Appointment is one appointment. project_id is nullable: NULL = personal
// (creator-only); set = follows the parent Project's team visibility.
type Appointment struct {
ID uuid.UUID `db:"id" json:"id"`
ProjectID *uuid.UUID `db:"project_id" json:"project_id,omitempty"`
Title string `db:"title" json:"title"`
Description *string `db:"description" json:"description,omitempty"`
StartAt time.Time `db:"start_at" json:"start_at"`
EndAt *time.Time `db:"end_at" json:"end_at,omitempty"`
Location *string `db:"location" json:"location,omitempty"`
AppointmentType *string `db:"appointment_type" json:"appointment_type,omitempty"`
CalDAVUID *string `db:"caldav_uid" json:"caldav_uid,omitempty"`
CalDAVEtag *string `db:"caldav_etag" json:"caldav_etag,omitempty"`
CreatedBy *uuid.UUID `db:"created_by" json:"created_by,omitempty"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
// CompletedAt is non-NULL once the appointment is marked done. New
// column added by migration 054 (t-paliad-138) — required to land the
// appointment:complete lifecycle event.
CompletedAt *time.Time `db:"completed_at" json:"completed_at,omitempty"`
// Approval-workflow columns (see Deadline doc above for semantics).
ApprovalStatus string `db:"approval_status" json:"approval_status"`
PendingRequestID *uuid.UUID `db:"pending_request_id" json:"pending_request_id,omitempty"`
ApprovedBy *uuid.UUID `db:"approved_by" json:"approved_by,omitempty"`
ApprovedAt *time.Time `db:"approved_at" json:"approved_at,omitempty"`
}
// AppointmentWithProject enriches an Appointment with its parent Project
// display fields for list views. All fields nullable because personal
// Appointments have no parent.
type AppointmentWithProject struct {
Appointment
ProjectReference *string `db:"project_reference" json:"project_reference,omitempty"`
ProjectTitle *string `db:"project_title" json:"project_title,omitempty"`
ProjectType *string `db:"project_type" json:"project_type,omitempty"`
// RequesterKind: see DeadlineWithProject (t-paliad-161 ✨).
RequesterKind *string `db:"requester_kind" json:"requester_kind,omitempty"`
}
// Note is one polymorphic note attached to exactly one parent row
// (Project, Deadline, Appointment, or ProjectEvent). Visibility follows the
// parent.
type Note struct {
ID uuid.UUID `db:"id" json:"id"`
ProjectID *uuid.UUID `db:"project_id" json:"project_id,omitempty"`
DeadlineID *uuid.UUID `db:"deadline_id" json:"deadline_id,omitempty"`
AppointmentID *uuid.UUID `db:"appointment_id" json:"appointment_id,omitempty"`
ProjectEventID *uuid.UUID `db:"project_event_id" json:"project_event_id,omitempty"`
Content string `db:"content" json:"content"`
CreatedBy *uuid.UUID `db:"created_by" json:"created_by,omitempty"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
// Author display fields populated by the service's LEFT JOIN to
// paliad.users so the UI can render "von <Name>" without a lookup.
AuthorName *string `db:"author_name" json:"author_name,omitempty"`
AuthorEmail *string `db:"author_email" json:"author_email,omitempty"`
}
// ChecklistInstance is one user's instantiation of a static checklist
// template (defined in internal/checklists). Checkbox state lives in the
// `state` jsonb column.
//
// Visibility mirrors Appointment: project_id nullable. Personal instances
// (project_id NULL) are creator-only; Project-linked instances follow
// paliad.can_see_project.
type ChecklistInstance struct {
ID uuid.UUID `db:"id" json:"id"`
TemplateSlug string `db:"template_slug" json:"template_slug"`
Name string `db:"name" json:"name"`
ProjectID *uuid.UUID `db:"project_id" json:"project_id,omitempty"`
State json.RawMessage `db:"state" json:"state"`
CreatedBy uuid.UUID `db:"created_by" json:"created_by"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
// ChecklistInstanceWithProject enriches an instance with its parent Project
// reference fields for list views.
type ChecklistInstanceWithProject struct {
ChecklistInstance
ProjectReference *string `db:"project_reference" json:"project_reference,omitempty"`
ProjectTitle *string `db:"project_title" json:"project_title,omitempty"`
}
// UserCalDAVConfig holds one user's external CalDAV connection. The password
// is never returned in API responses; only the public fields are exposed.
type UserCalDAVConfig struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
URL string `db:"url" json:"url"`
Username string `db:"username" json:"username"`
PasswordEncrypted []byte `db:"password_encrypted" json:"-"`
CalendarPath string `db:"calendar_path" json:"calendar_path"`
Enabled bool `db:"enabled" json:"enabled"`
LastSyncAt *time.Time `db:"last_sync_at" json:"last_sync_at,omitempty"`
LastSyncError *string `db:"last_sync_error" json:"last_sync_error,omitempty"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
// CalDAVSyncLogEntry is one historical sync record.
type CalDAVSyncLogEntry struct {
ID uuid.UUID `db:"id" json:"id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
OccurredAt time.Time `db:"occurred_at" json:"occurred_at"`
Direction string `db:"direction" json:"direction"`
ItemsPushed int `db:"items_pushed" json:"items_pushed"`
ItemsPulled int `db:"items_pulled" json:"items_pulled"`
Error *string `db:"error" json:"error,omitempty"`
DurationMS *int `db:"duration_ms" json:"duration_ms,omitempty"`
}
// Party is a party to a Project (Kläger, Beklagter, etc. — typically on
// a case-level project).
type Party struct {
ID uuid.UUID `db:"id" json:"id"`
ProjectID uuid.UUID `db:"project_id" json:"project_id"`
Name string `db:"name" json:"name"`
Role *string `db:"role" json:"role,omitempty"`
Representative *string `db:"representative" json:"representative,omitempty"`
ContactInfo json.RawMessage `db:"contact_info" json:"contact_info"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
// DeadlineRule is one rule in the proceeding-rule tree (UPC R.023, etc.).
type DeadlineRule struct {
ID uuid.UUID `db:"id" json:"id"`
ProceedingTypeID *int `db:"proceeding_type_id" json:"proceeding_type_id,omitempty"`
ParentID *uuid.UUID `db:"parent_id" json:"parent_id,omitempty"`
Code *string `db:"code" json:"code,omitempty"`
Name string `db:"name" json:"name"`
NameEN string `db:"name_en" json:"name_en"`
Description *string `db:"description" json:"description,omitempty"`
PrimaryParty *string `db:"primary_party" json:"primary_party,omitempty"`
EventType *string `db:"event_type" json:"event_type,omitempty"`
IsMandatory bool `db:"is_mandatory" json:"is_mandatory"`
DurationValue int `db:"duration_value" json:"duration_value"`
DurationUnit string `db:"duration_unit" json:"duration_unit"`
Timing *string `db:"timing" json:"timing,omitempty"`
RuleCode *string `db:"rule_code" json:"rule_code,omitempty"`
DeadlineNotes *string `db:"deadline_notes" json:"deadline_notes,omitempty"`
DeadlineNotesEn *string `db:"deadline_notes_en" json:"deadline_notes_en,omitempty"`
SequenceOrder int `db:"sequence_order" json:"sequence_order"`
ConditionRuleID *uuid.UUID `db:"condition_rule_id" json:"condition_rule_id,omitempty"`
// ConditionFlag holds zero or more flag codes that gate this rule.
// Semantics: rule renders iff every element is present in
// CalcOptions.Flags. Empty/NULL = unconditional. When all flags are
// satisfied AND alt_duration_value is non-NULL the calculator swaps
// to alt_*; when set + flags not satisfied the rule is suppressed.
ConditionFlag pq.StringArray `db:"condition_flag" json:"condition_flag,omitempty"`
AltDurationValue *int `db:"alt_duration_value" json:"alt_duration_value,omitempty"`
AltDurationUnit *string `db:"alt_duration_unit" json:"alt_duration_unit,omitempty"`
AltRuleCode *string `db:"alt_rule_code" json:"alt_rule_code,omitempty"`
AnchorAlt *string `db:"anchor_alt" json:"anchor_alt,omitempty"`
ConceptID *uuid.UUID `db:"concept_id" json:"concept_id,omitempty"`
// ConceptDefaultEventTypeID is the canonical paliad.event_types row for
// this rule's concept (joined via paliad.deadline_concept_event_types
// where is_default = true). Lets the deadline create form auto-populate
// the Typ chip when the user picks this rule. Hydrated by the service
// layer; not a column. NULL when the concept has no mapped event_type.
ConceptDefaultEventTypeID *uuid.UUID `db:"-" json:"concept_default_event_type_id,omitempty"`
LegalSource *string `db:"legal_source" json:"legal_source,omitempty"`
IsSpawn bool `db:"is_spawn" json:"is_spawn"`
SpawnLabel *string `db:"spawn_label" json:"spawn_label,omitempty"`
// IsOptional flags a rule whose deadline is conditional on a user
// act (e.g. RoP.151 cost-decision request — only fires when a
// party files for it). Save-modal pre-unchecks optional rows; the
// timeline still renders them so the user knows what could apply.
IsOptional bool `db:"is_optional" json:"is_optional"`
IsActive bool `db:"is_active" json:"is_active"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
// ProceedingType is one of INF/REV/CCR/APM/APP/AMD/ZPO_CIVIL (matter
// management) or UPC_*/DE_*/EPA_*/EP_GRANT (Fristenrechner UI).
type ProceedingType struct {
ID int `db:"id" json:"id"`
Code string `db:"code" json:"code"`
Name string `db:"name" json:"name"`
NameEN string `db:"name_en" json:"name_en"`
Description *string `db:"description" json:"description,omitempty"`
Jurisdiction *string `db:"jurisdiction" json:"jurisdiction,omitempty"`
Category *string `db:"category" json:"category,omitempty"`
DefaultColor string `db:"default_color" json:"default_color"`
SortOrder int `db:"sort_order" json:"sort_order"`
IsActive bool `db:"is_active" json:"is_active"`
}
// TriggerEvent is a UPC procedural event that can start one or more deadlines
// running. Powers the "Was kommt nach…" Fristenrechner mode (event-driven
// lookup, mirrored from youpc data.events).
type TriggerEvent struct {
ID int64 `db:"id" json:"id"`
Code string `db:"code" json:"code"`
Name string `db:"name" json:"name"`
NameDE string `db:"name_de" json:"name_de"`
Description string `db:"description" json:"description"`
IsActive bool `db:"is_active" json:"is_active"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
// EventDeadline is a single deadline that flows from a TriggerEvent. Mirrors
// youpc data.deadlines + the trigger half of data.deadline_events.
//
// Composite-rule semantics: when CombineOp is non-nil, the calculator
// computes both (DurationValue, DurationUnit) and (AltDurationValue,
// AltDurationUnit) from the trigger date and applies CombineOp ('max'/'min').
// Used for R.198/R.213 ("31d OR 20 working_days, whichever is longer").
type EventDeadline struct {
ID int64 `db:"id" json:"id"`
TriggerEventID int64 `db:"trigger_event_id" json:"trigger_event_id"`
Title string `db:"title" json:"title"`
TitleDE string `db:"title_de" json:"title_de"`
DurationValue int `db:"duration_value" json:"duration_value"`
DurationUnit string `db:"duration_unit" json:"duration_unit"`
Timing string `db:"timing" json:"timing"`
Notes string `db:"notes" json:"notes"`
AltDurationValue *int `db:"alt_duration_value" json:"alt_duration_value,omitempty"`
AltDurationUnit *string `db:"alt_duration_unit" json:"alt_duration_unit,omitempty"`
CombineOp *string `db:"combine_op" json:"combine_op,omitempty"`
IsActive bool `db:"is_active" json:"is_active"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
// EventDeadlineRuleCode is one RoP citation attached to an EventDeadline.
// A single deadline can carry multiple rule codes (e.g. R.029.a + R.030).
type EventDeadlineRuleCode struct {
EventDeadlineID int64 `db:"event_deadline_id" json:"event_deadline_id"`
RuleCode string `db:"rule_code" json:"rule_code"`
SortOrder int `db:"sort_order" json:"sort_order"`
}
// EventType is a user-facing categorization tag for a Deadline (Statement
// of Defence, Reply, Decision on the merits, EPO opposition, …). Distinct
// from TriggerEvent: TriggerEvents are calc-engine state (UPC-only,
// verbatim youpc imports), EventTypes are the broader taxonomy users
// pick from when creating a Deadline.
//
// CreatedBy NULL on system seeds; set on user-created rows. IsFirmWide
// true for seeds and any firm-wide row a user explicitly publishes;
// false for personal taxonomy. TriggerEventID is a loose linkage column
// (no FK constraint) populated only for seeded UPC rows.
type EventType struct {
ID uuid.UUID `db:"id" json:"id"`
Slug string `db:"slug" json:"slug"`
LabelDE string `db:"label_de" json:"label_de"`
LabelEN string `db:"label_en" json:"label_en"`
Category string `db:"category" json:"category"`
Jurisdiction *string `db:"jurisdiction" json:"jurisdiction,omitempty"`
Description string `db:"description" json:"description"`
TriggerEventID *int64 `db:"trigger_event_id" json:"trigger_event_id,omitempty"`
CreatedBy *uuid.UUID `db:"created_by" json:"created_by,omitempty"`
IsFirmWide bool `db:"is_firm_wide" json:"is_firm_wide"`
ArchivedAt *time.Time `db:"archived_at" json:"archived_at,omitempty"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
// EventTypeCategory enumerates the values allowed on event_types.category.
// Mirrors the CHECK constraint in migration 030.
const (
EventTypeCategorySubmission = "submission"
EventTypeCategoryDecision = "decision"
EventTypeCategoryOrder = "order"
EventTypeCategoryService = "service"
EventTypeCategoryFee = "fee"
EventTypeCategoryHearing = "hearing"
EventTypeCategoryOther = "other"
)
// EventTypeJurisdiction enumerates the values allowed on
// event_types.jurisdiction (NULL is also valid).
const (
EventTypeJurisdictionUPC = "UPC"
EventTypeJurisdictionEPO = "EPO"
EventTypeJurisdictionDPMA = "DPMA"
EventTypeJurisdictionDE = "DE"
EventTypeJurisdictionAny = "any"
)
// ApprovalPolicy is one row of paliad.approval_policies — a rule that says
// "this (entity_type, lifecycle_event) needs 4-eye sign-off at the given
// role tier or above" within a scope. The scope is either a single project
// (ProjectID set, PartnerUnitID nil) OR a single partner unit (PartnerUnitID
// set, ProjectID nil) — XOR enforced by the DB CHECK
// approval_policies_scope_xor.
//
// Project rows act as the most-specific override; partner-unit rows act as
// firm-wide defaults for projects attached to that unit (t-paliad-154).
type ApprovalPolicy struct {
ID uuid.UUID `db:"id" json:"id"`
ProjectID *uuid.UUID `db:"project_id" json:"project_id,omitempty"`
PartnerUnitID *uuid.UUID `db:"partner_unit_id" json:"partner_unit_id,omitempty"`
EntityType string `db:"entity_type" json:"entity_type"`
LifecycleEvent string `db:"lifecycle_event" json:"lifecycle_event"`
// RequiresApproval is the gate (t-paliad-160). False = lifecycle event
// auto-passes, no approval_request inserted.
RequiresApproval bool `db:"requires_approval" json:"requires_approval"`
// MinRole is the minimum profession tier qualified to approve. NULL
// (nil) when RequiresApproval=false. Constraint: the two columns are
// XOR-locked — either (false, NULL) or (true, role).
MinRole *string `db:"min_role" json:"min_role,omitempty"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
CreatedBy *uuid.UUID `db:"created_by" json:"created_by,omitempty"`
}
// EffectivePolicy is the resolved policy for one (project, entity_type,
// lifecycle_event) cell — what the gate actually does, after the
// project-row / ancestor-row / unit-default cascade in
// paliad.approval_policy_effective(). Populated by
// ApprovalService.GetEffectivePoliciesMatrix and the form-time hint
// endpoint.
//
// RequiresApproval is the gate (true iff any candidate demands approval).
// MinRole is the seniority threshold among requires_approval=true
// candidates (nil when the gate is off). Source ∈ {"project", "ancestor",
// "unit_default"} attributes which row supplied the winning value.
// SourceID is the project_id for project / ancestor; partner_unit_id for
// unit_default.
type EffectivePolicy struct {
EntityType string `json:"entity_type"`
LifecycleEvent string `json:"lifecycle_event"`
// RequiresApproval is the gate (t-paliad-160 split-grammar). True iff
// the resolver yielded a policy that demands approval.
RequiresApproval bool `json:"requires_approval"`
// MinRole is the seniority threshold (NULL when gate is off).
MinRole *string `json:"min_role,omitempty"`
Source *string `json:"source,omitempty"`
SourceID *uuid.UUID `json:"source_id,omitempty"`
SourceName *string `json:"source_name,omitempty"`
}
// PolicyAuditEntry is one row of paliad.policy_audit_log — admin-only audit
// trail for approval-policy CRUD (t-paliad-154). Surfaces on /admin/audit-log
// via AuditService union; never on per-project /verlauf.
type PolicyAuditEntry struct {
ID uuid.UUID `db:"id" json:"id"`
ActorID uuid.UUID `db:"actor_id" json:"actor_id"`
EventType string `db:"event_type" json:"event_type"`
ScopeType string `db:"scope_type" json:"scope_type"`
ProjectID *uuid.UUID `db:"project_id" json:"project_id,omitempty"`
PartnerUnitID *uuid.UUID `db:"partner_unit_id" json:"partner_unit_id,omitempty"`
ScopeName string `db:"scope_name" json:"scope_name"`
EntityType string `db:"entity_type" json:"entity_type"`
LifecycleEvent string `db:"lifecycle_event" json:"lifecycle_event"`
OldRequiredRole *string `db:"old_required_role" json:"old_required_role,omitempty"`
NewRequiredRole *string `db:"new_required_role" json:"new_required_role,omitempty"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
// ApprovalRequest is one row of paliad.approval_requests — an in-flight
// state-change awaiting 4-eye sign-off.
//
// PreImage carries the field values needed to revert on rejection (NULL for
// 'create' since there's nothing to revert to). Payload echoes the diff or
// new values that were written, for audit display. RequiredRole is a
// snapshot of the policy at request time.
//
// DecisionKind discriminates 'peer' (normal in-team sign-off) from
// 'admin_override' (global_admin used the escape-hatch path).
type ApprovalRequest struct {
ID uuid.UUID `db:"id" json:"id"`
ProjectID uuid.UUID `db:"project_id" json:"project_id"`
EntityType string `db:"entity_type" json:"entity_type"`
EntityID uuid.UUID `db:"entity_id" json:"entity_id"`
LifecycleEvent string `db:"lifecycle_event" json:"lifecycle_event"`
PreImage NullableJSON `db:"pre_image" json:"pre_image,omitempty"`
Payload NullableJSON `db:"payload" json:"payload,omitempty"`
RequestedBy uuid.UUID `db:"requested_by" json:"requested_by"`
RequestedAt time.Time `db:"requested_at" json:"requested_at"`
RequiredRole string `db:"required_role" json:"required_role"`
Status string `db:"status" json:"status"`
DecidedBy *uuid.UUID `db:"decided_by" json:"decided_by,omitempty"`
DecidedAt *time.Time `db:"decided_at" json:"decided_at,omitempty"`
DecisionKind *string `db:"decision_kind" json:"decision_kind,omitempty"`
DecisionNote *string `db:"decision_note" json:"decision_note,omitempty"`
// RequesterKind is 'user' (direct user create) or 'agent' (Paliadin
// drafted the row from a chat turn — t-paliad-161). Agent rows render
// alongside 👀 with a sparkle ✨ on the eye-pill surface.
RequesterKind string `db:"requester_kind" json:"requester_kind"`
AgentTurnID *uuid.UUID `db:"agent_turn_id" json:"agent_turn_id,omitempty"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}