// 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 ( "encoding/json" "time" "github.com/google/uuid" "github.com/lib/pq" "mgit.msbls.de/m/paliad/pkg/litigationplanner" ) // NullableJSON is a jsonb column that may be NULL. Canonical definition // (with sql.Scanner / driver.Valuer / json.Marshaler / json.Unmarshaler) // lives in pkg/litigationplanner — kept here as a type alias so every // existing models.NullableJSON reference continues to compile. type NullableJSON = litigationplanner.NullableJSON // 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"` // OurSide is which side the firm represents on this project. Used // by the Fristenrechner Determinator to predefine the perspective // chip from the project context (t-paliad-164). NULL = unknown / // not set; Determinator falls back to free-pick. // // Allowed sub-roles (mig 112, t-paliad-222): // Active : claimant, applicant, appellant // Reactive : defendant, respondent // Other : third_party, other // // The DB column name stays as `our_side`; the UI label has moved // to "Client Role" / "Mandantenrolle" on case projects and is // hidden on every other project type. OurSide *string `db:"our_side" json:"our_side,omitempty"` // OpponentCode is the short slug for the opposing party on a // litigation project (uppercase letters / digits / dashes, max 16 // chars). Used as the middle segment when services.BuildProjectCode // assembles an auto-derived project code from the ancestor tree — // e.g. EXMPL.OPNT.567.INF.CFI (t-paliad-222 / m/paliad#50). NULL // → segment skipped silently. Only meaningful on type='litigation' // rows; CHECK constraint (mig 113) enforces the pairing. OpponentCode *string `db:"opponent_code" json:"opponent_code,omitempty"` // Code is the auto-derived (or override) project code, computed at // projection time by services.BuildProjectCode. Not a DB column — // no `db:` tag — populated by service-layer projection helpers // after the row is loaded. Empty on rows for which the helper has // not run (e.g. raw fixtures in tests, internal projection paths // that don't call the helper). Code string `db:"-" json:"code,omitempty"` // CounterclaimOf is the parent project this row is a counterclaim // (CCR) against (t-paliad-174 SmartTimeline Slice 3). NULL on // regular projects; non-NULL rows are CCR sub-projects rendered as // the parallel right-track on the parent's SmartTimeline. parent_id // keeps governing the project tree — the CCR child is placed as a // sibling under the same patent (§4.4 of the design doc). CounterclaimOf *uuid.UUID `db:"counterclaim_of" json:"counterclaim_of,omitempty"` // InstanceLevel is the procedural instance the project sits at: // 'first' (default) | 'appeal' | 'cassation'. Combined with the // proceeding code + jurisdiction by FristenrechnerService to pick // the effective proceeding (de.inf.lg + appeal → de.inf.olg, etc.). // NULL = unset / not applicable; the calculator treats NULL as // 'first'. Backfill happens via the project-detail picker UI // (Phase 3 Slice 8); this column ships in Slice 1 ahead of the // service rewrite (mig 080, t-paliad-182). InstanceLevel *string `db:"instance_level" json:"instance_level,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 " () — " 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"` // CustomRuleText holds the lawyer's free-text rule label when the // deadline form is in Custom mode (t-paliad-258 / m/paliad#89). // Mutually exclusive with RuleID at the application layer: the Auto // path sets RuleID and leaves this NULL; the Custom path sets this // and leaves RuleID NULL. Display surfaces prefer the joined // deadline_rules.name when RuleID is set, else fall back to this // text + a "Custom" badge. CustomRuleText *string `db:"custom_rule_text" json:"custom_rule_text,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 " 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 checklist template // (static catalog in internal/checklists OR authored row in // paliad.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. // // TemplateSnapshot captures the template body at instance create time so // subsequent template edits / visibility narrowing don't affect existing // instances (t-paliad-225 Slice A). NULL on pre-mig-114 rows; the // service layer falls back to live catalog lookup in that case. 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"` TemplateSnapshot NullableJSON `db:"template_snapshot" json:"template_snapshot,omitempty"` // TemplateVersion is the checklists.version at instance create time. // NULL on pre-Slice-C rows where versioning wasn't captured; the // "outdated" badge stays off in that case. TemplateVersion *int `db:"template_version" json:"template_version,omitempty"` } // 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"` } // Checklist is one authored template row in paliad.checklists. Augments // the static Go catalog (internal/checklists/templates.go) at read time // via ChecklistCatalogService. Body holds the groups + items as JSONB. type Checklist struct { ID uuid.UUID `db:"id" json:"id"` Slug string `db:"slug" json:"slug"` OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` Title string `db:"title" json:"title"` Description string `db:"description" json:"description"` Regime string `db:"regime" json:"regime"` Court string `db:"court" json:"court"` Reference string `db:"reference" json:"reference"` Deadline string `db:"deadline" json:"deadline"` Lang string `db:"lang" json:"lang"` Body json.RawMessage `db:"body" json:"body"` Visibility string `db:"visibility" json:"visibility"` PromotedAt *time.Time `db:"promoted_at" json:"promoted_at,omitempty"` PromotedBy *uuid.UUID `db:"promoted_by" json:"promoted_by,omitempty"` Version int `db:"version" json:"version"` CreatedAt time.Time `db:"created_at" json:"created_at"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"` } // ChecklistWithOwner enriches a Checklist row with author display fields // for list views (Meine Vorlagen, Gallery). type ChecklistWithOwner struct { Checklist OwnerEmail string `db:"owner_email" json:"owner_email"` OwnerDisplayName string `db:"owner_display_name" json:"owner_display_name"` } // 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"` // MKCALENDAR-capability tri-state (mig 108, Slice 2c). NULL = unprobed. SupportsMKCalendar *bool `db:"supports_mkcalendar" json:"supports_mkcalendar,omitempty"` MKCalendarProbedAt *time.Time `db:"mkcalendar_probed_at" json:"mkcalendar_probed_at,omitempty"` } // CalDAVSyncLogEntry is one historical sync record. BindingID is populated // for per-binding sync entries written by the post-Slice-2a sync engine; // older rows have it NULL and the entry covers the user's default binding. 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"` BindingID *uuid.UUID `db:"binding_id" json:"binding_id,omitempty"` } // UserCalendarBinding is one of N (calendar, scope) bindings a user can // configure on top of their single CalDAV server connection. The same // Appointment can land in multiple bindings (e.g. master + per-project), // with per-binding push state living in AppointmentCalDAVTarget. type UserCalendarBinding struct { ID uuid.UUID `db:"id" json:"id"` UserID uuid.UUID `db:"user_id" json:"user_id"` CalendarPath string `db:"calendar_path" json:"calendar_path"` DisplayName string `db:"display_name" json:"display_name"` ScopeKind string `db:"scope_kind" json:"scope_kind"` ScopeID *uuid.UUID `db:"scope_id" json:"scope_id,omitempty"` IncludePersonal bool `db:"include_personal" json:"include_personal"` 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"` } // Scope-kind enum mirrored from paliad.user_calendar_bindings_scope_kind_chk. const ( BindingScopeAllVisible = "all_visible" BindingScopePersonalOnly = "personal_only" BindingScopeProject = "project" BindingScopeClient = "client" BindingScopeLitigation = "litigation" BindingScopePatent = "patent" BindingScopeCase = "case" ) // AppointmentCalDAVTarget is the per-(appointment, binding) push state. // The caldav_uid is canonical per Appointment (same value across all of // an appointment's targets); caldav_etag varies per binding. type AppointmentCalDAVTarget struct { AppointmentID uuid.UUID `db:"appointment_id" json:"appointment_id"` BindingID uuid.UUID `db:"binding_id" json:"binding_id"` CalDAVUID string `db:"caldav_uid" json:"caldav_uid"` CalDAVEtag *string `db:"caldav_etag" json:"caldav_etag,omitempty"` LastPushedAt time.Time `db:"last_pushed_at" json:"last_pushed_at"` } // 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.). // Canonical definition lives in pkg/litigationplanner.Rule — kept here // as a type alias so every existing models.DeadlineRule reference (sqlx // scans, hydration, projection service) continues to compile. type DeadlineRule = litigationplanner.Rule // DeadlineRuleAudit is one row of paliad.deadline_rule_audit — the // append-only audit log for every change to paliad.deadline_rules. // Written by the AFTER-trigger (raw create / update / delete) and by // the Go rule-editor service (semantic publish / archive / restore). // See migration 079 and design-fristen-phase2-2026-05-15.md §2.8. type DeadlineRuleAudit struct { ID uuid.UUID `db:"id" json:"id"` RuleID uuid.UUID `db:"rule_id" json:"rule_id"` ChangedBy *uuid.UUID `db:"changed_by" json:"changed_by,omitempty"` ChangedAt time.Time `db:"changed_at" json:"changed_at"` // Action is one of: create | update | delete (trigger-written) | // publish | archive | restore (Go-written by the rule editor). Action string `db:"action" json:"action"` // BeforeJSON is the row state pre-change (NULL on 'create'). // AfterJSON is the row state post-change (NULL on 'delete'). BeforeJSON NullableJSON `db:"before_json" json:"before_json,omitempty"` AfterJSON NullableJSON `db:"after_json" json:"after_json,omitempty"` // Reason is required on update / delete (the trigger raises if // paliad.audit_reason is unset). On create the trigger defaults // to 'create' so seed migrations don't need to bother. Reason string `db:"reason" json:"reason"` // MigrationExported flips to true once the Slice 11b export // endpoint folds this delta into a checked-in .up.sql. MigrationExported bool `db:"migration_exported" json:"migration_exported"` } // ProceedingType is one of the litigation conceptual codes (INF / REV / // CCR / APM / APP / AMD / ZPO_CIVIL) or the lowercase dot-separated // fristenrechner codes (upc.*.*, de.*.*, epa.*.*, dpma.*.*) — see // docs/design-proceeding-code-taxonomy-2026-05-18.md. Canonical // definition lives in pkg/litigationplanner.ProceedingType — kept here // as a type alias so every existing models.ProceedingType reference // continues to compile. type ProceedingType = litigationplanner.ProceedingType // TriggerEvent is a UPC procedural event referenced by deadline rules // whose semantic anchor is an event rather than a parent rule. // Canonical definition lives in pkg/litigationplanner.TriggerEvent. type TriggerEvent = litigationplanner.TriggerEvent // 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"` // CounterPayload carries the approver's edited values on a // changes_requested row (mig 103, t-paliad-216). NULL for every // other status. Frontend renders it as a diff against the OLD // payload to show "approver suggested X→Y on the following fields". CounterPayload NullableJSON `db:"counter_payload" json:"counter_payload,omitempty"` // PreviousRequestID is the back-pointer from a row spawned by // SuggestChanges to the prior changes_requested row that birthed it // (mig 103, t-paliad-216). NULL on first-attempt rows. PreviousRequestID *uuid.UUID `db:"previous_request_id" json:"previous_request_id,omitempty"` CreatedAt time.Time `db:"created_at" json:"created_at"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"` } // ProjectEventChoice is one per-event-card pick scoped to a project // (t-paliad-265 / m/paliad#96). The join key SubmissionCode matches // paliad.deadline_rules.submission_code — the same identifier the // AnchorOverrides plumbing in fristenrechner.go already uses. // // ChoiceKind ∈ {appellant, include_ccr, skip}. ChoiceValue namespace // per kind: appellant=claimant|defendant|both|none; include_ccr=true|false; // skip=true|false. UNIQUE(project_id, submission_code, choice_kind) // makes re-picks idempotent (Upsert path). type ProjectEventChoice struct { ID uuid.UUID `db:"id" json:"id"` ProjectID uuid.UUID `db:"project_id" json:"project_id"` SubmissionCode string `db:"submission_code" json:"submission_code"` ChoiceKind string `db:"choice_kind" json:"choice_kind"` ChoiceValue string `db:"choice_value" json:"choice_value"` CreatedBy *uuid.UUID `db:"created_by" json:"created_by,omitempty"` CreatedAt time.Time `db:"created_at" json:"created_at"` UpdatedBy *uuid.UUID `db:"updated_by" json:"updated_by,omitempty"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"` }