Files
paliad/internal/models/models.go
m 4ebbf2c1af feat(t-paliad-138): ApprovalService core + tests
Commit 2 of 8 — the workflow engine for the 4-Augen-Prüfung. Wires the
service into the handlers.Services bundle so commit 3 can call into
SubmitCreate/Update/Complete/Delete from DeadlineService and
AppointmentService.

Public surface:

- Submit{Create,Update,Complete,Delete} — invoked by Deadline /
  AppointmentService inside their existing tx. Looks up policy,
  runs the deadlock check, inserts paliad.approval_requests, marks
  the entity pending, emits the *_approval_requested project_events
  audit row.
- Approve / Reject / Revoke — top-level operations (own tx). Approve
  finalises the lifecycle (clears pending markers + sets approved_by
  for non-delete; hard-deletes for delete). Reject / Revoke revert
  the entity from pre_image (delete a pending-create, restore date
  fields, NULL completed_at).
- ListPendingForApprover / ListSubmittedByUser / GetRequest /
  PendingCountForUser — read paths the inbox + bell will hit in
  commit 5.
- ListPolicies / UpsertPolicy / DeletePolicy — CRUD for the
  authoring page in commit 4.

Self-approval is blocked at three layers:
  1. canApprove() returns ErrSelfApproval when caller == requester.
  2. The DB CHECK constraint approval_requests_no_self_approval.
  3. The deadlock check excludes the requester from the pool.

Strict-ladder helper levelOf(role) mirrors the SQL function added in
migration 054. Path-walk authorization: ancestors with eligible roles
qualify for descendant requests (matches the visibility predicate).

Tests:
- Pure-Go: levelOf strict-ladder semantics, IsValidRequiredRole,
  approvalEventType. All pass under `go test`.
- Live-DB (TEST_DATABASE_URL): no-policy noop; submit→approve cycle;
  reject-create deletes; reject-update restores pre_image;
  no-qualified-approver fail; revoke flow; policy CRUD roundtrip.
  Skipped when TEST_DATABASE_URL is unset, mirroring the existing
  audit_service_test pattern.

No call sites in DeadlineService / AppointmentService yet — that's
commit 3. Paliad continues to behave identically until that lands.
2026-05-06 15:21:47 +02:00

517 lines
29 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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"
)
// 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"`
// 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.role and
// is 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"`
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.
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"`
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>) — <Role>" without a per-row lookup.
// Used by TeamService.ListMembers which unions direct + inherited memberships.
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"`
// 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).
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"`
}
// 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"`
// 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"`
}
// 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"`
}
// 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"`
}
// 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"`
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"`
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 — the per-(project,
// entity_type, lifecycle_event) rule that says "this lifecycle event needs
// 4-eye sign-off at the given role tier or above". Up to 8 rows per project
// (deadline×4 + appointment×4); missing rows = no approval needed.
type ApprovalPolicy 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"`
LifecycleEvent string `db:"lifecycle_event" json:"lifecycle_event"`
RequiredRole string `db:"required_role" json:"required_role"`
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"`
}
// 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 json.RawMessage `db:"pre_image" json:"pre_image,omitempty"`
Payload json.RawMessage `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"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}