Atomic extraction of the deadline-rule compute engine + types from
internal/services into a new pkg/litigationplanner package that paliad
+ youpc.org can both import. No behaviour change — every existing test
passes against the post-move shape.
Package contents (~1850 LoC):
- doc.go package docstring + reuse manifesto
- types.go Rule, ProceedingType, NullableJSON, AdjustmentReason,
HolidayDTO, CalcOptions, CalcRuleParams, Timeline,
TimelineEntry, RuleCalculation*, FristenrechnerType,
ProjectHint, sentinel errors
- catalog.go Catalog interface (proceeding + rule lookups)
- holidays.go HolidayCalendar interface
- courts.go CourtRegistry interface + DefaultsForJurisdiction +
country/regime constants
- expr.go EvalConditionExpr + HasConditionExpr +
ExtractFlagsFromExpr (jsonb gate evaluator)
- durations.go ApplyDuration + AddWorkingDays (pure compute)
- subtrack.go SubTrackRouting + LookupSubTrackRouting registry
- legal_source.go FormatLegalSourceDisplay + BuildLegalSourceURL
- proceeding_mapping.go MapLitigationToFristenrechner + code constants
(CodeUPCInfringement, CodeDEInfringementLG, ...)
- engine.go Calculate + CalculateRule + the trigger-event
branch + applyRuleOverrides (the big move)
paliad side (~1900 LoC net deletion):
- internal/services/fristenrechner.go shrinks from 1505 → ~290 lines
(thin paliad Catalog adapter + type aliases for back-compat).
- internal/models/models.go: DeadlineRule, ProceedingType, NullableJSON
become type aliases to litigationplanner.* — every sqlx scan and
every projection_service caller compiles unchanged.
- internal/services/holidays.go: AdjustmentReason + HolidayDTO become
aliases to lp.* (canonical definitions now in the package).
- internal/services/proceeding_mapping.go: rewritten as thin re-exports
of lp constants + helpers.
- internal/services/deadline_search_service.go: FormatLegalSourceDisplay
+ BuildLegalSourceURL replaced with delegating wrappers to lp.
Catalog interface satisfaction:
- DeadlineRuleService → paliadCatalog adapter (wraps the existing
service, replicates the original SELECT shapes).
- HolidayService → satisfies lp.HolidayCalendar directly (compile-
time assertion at end of fristenrechner.go).
- CourtService → satisfies lp.CourtRegistry directly.
Wire shape is byte-identical. JSON tags on Rule / ProceedingType /
Timeline / TimelineEntry / RuleCalculation match the historical
UIResponse / UIDeadline shape; the frontend reads the same bytes.
Slice B (Catalog interface + paliad loader cleanup) is folded into
this commit since Slice A already needs the interfaces to call
Calculate across the boundary. Slice C (embedded UPC snapshot +
generator) is the next coder shift; the Berufung unification m
called out lands in Slice B/C per head's brief.
Refs: docs/design-litigation-planner-2026-05-26.md
816 lines
45 KiB
Go
816 lines
45 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 (
|
|
"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 "<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"`
|
|
// 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 <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 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 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"`
|
|
// 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"`
|
|
}
|