Ports KanzlAI document upload + AI extraction into paliad. PDFs are stored
in Supabase Storage (bucket paliad-documents); Claude Sonnet extracts
deadlines with tool-forced structured output; the user reviews candidates
and picks which to persist as Fristen.
Backend
- internal/services/ai_service.go — Anthropic SDK wrapper. Uses native PDF
content blocks, forced tool_use for structured output, ephemeral prompt
caching on the system prompt. Sonnet 4.6.
- internal/services/storage.go — Supabase Storage REST client (upload,
download, delete). Nil when SUPABASE_SERVICE_KEY is unset.
- internal/services/dokument_service.go — upload (PDF magic-number check,
20 MB cap), list, download, extract, persist-confirmed-as-Fristen. All
visibility-checked through AkteService.GetByID.
- internal/handlers/dokumente.go — five endpoints plus /api/config/features
so the UI can hide disabled buttons.
- internal/handlers/ratelimit.go — in-memory per-user cap of 20 extractions
per UTC day (design §9.7).
- Both optional services (storage, AI) degrade to 501 with friendly German
messages when their env vars are unset.
Schema
- migration 013 adds fristen.source_document_id (FK to dokumente) and
dokumente.ai_extraction_count + ai_extracted_at for the UI badge.
Frontend
- Dokumente tab in /akten/{id}/dokumente replaces the Phase D placeholder:
drag-drop upload zone with live progress bar (XHR), document table with
download + extract actions, extraction-review modal with per-row
checkboxes, confidence chips, expandable source-quote, editable title +
due date + rule code, POST to the from-extraction endpoint.
- Upload + extract buttons hide automatically when the server reports the
feature is disabled.
- Full DE/EN i18n. CSS for the upload zone, extraction modal, and
confidence chips.
Env vars (not set here — flag to head):
- ANTHROPIC_API_KEY (enables extraction)
- SUPABASE_SERVICE_KEY (enables upload/download)
Branch: mai/ritchie/phase-h-ai-deadline
168 lines
9.9 KiB
Go
168 lines
9.9 KiB
Go
// Package models holds the database row types for paliad.* tables.
|
|
// Names mirror the German schema (Akte, Frist, Termin, Notiz, …).
|
|
// 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 Akten.
|
|
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"`
|
|
PracticeGroup *string `db:"practice_group" json:"practice_group,omitempty"`
|
|
Role string `db:"role" json:"role"`
|
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
|
}
|
|
|
|
// Akte is a matter (case file). Office-scoped visibility: see paliad.can_see_akte.
|
|
type Akte struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
Aktenzeichen string `db:"aktenzeichen" json:"aktenzeichen"`
|
|
Title string `db:"title" json:"title"`
|
|
AkteType *string `db:"akte_type" json:"akte_type,omitempty"`
|
|
Court *string `db:"court" json:"court,omitempty"`
|
|
CourtRef *string `db:"court_ref" json:"court_ref,omitempty"`
|
|
Status string `db:"status" json:"status"`
|
|
AISummary *string `db:"ai_summary" json:"ai_summary,omitempty"`
|
|
OwningOffice string `db:"owning_office" json:"owning_office"`
|
|
Collaborators pq.StringArray `db:"collaborators" json:"collaborators"`
|
|
FirmWideVisible bool `db:"firm_wide_visible" json:"firm_wide_visible"`
|
|
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"`
|
|
}
|
|
|
|
// AkteEvent is one row in the per-Akte audit trail.
|
|
type AkteEvent struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
AkteID uuid.UUID `db:"akte_id" json:"akte_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"`
|
|
}
|
|
|
|
// Frist is one persistent deadline attached to an Akte.
|
|
// Visibility is inherited from the parent Akte (see paliad.can_see_akte).
|
|
type Frist struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
AkteID uuid.UUID `db:"akte_id" json:"akte_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"`
|
|
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"`
|
|
SourceDocumentID *uuid.UUID `db:"source_document_id" json:"source_document_id,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"`
|
|
}
|
|
|
|
// FristWithAkte enriches a Frist with parent Akte fields needed by the
|
|
// /fristen list page (Akten ref + title + office) without requiring a
|
|
// per-row /api/akten/{id} fetch.
|
|
type FristWithAkte struct {
|
|
Frist
|
|
AkteAktenzeichen string `db:"akte_aktenzeichen" json:"akte_aktenzeichen"`
|
|
AkteTitle string `db:"akte_title" json:"akte_title"`
|
|
AkteOffice string `db:"akte_office" json:"akte_office"`
|
|
RuleCode *string `db:"rule_code" json:"rule_code,omitempty"`
|
|
}
|
|
|
|
// Dokument is a file uploaded against an Akte. Blob lives in Supabase
|
|
// Storage (bucket paliad-documents); only metadata is stored here.
|
|
// Visibility is inherited from the parent Akte.
|
|
type Dokument struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
AkteID uuid.UUID `db:"akte_id" json:"akte_id"`
|
|
Title string `db:"title" json:"title"`
|
|
DocType *string `db:"doc_type" json:"doc_type,omitempty"`
|
|
FilePath *string `db:"file_path" json:"file_path,omitempty"`
|
|
FileSize *int64 `db:"file_size" json:"file_size,omitempty"`
|
|
MimeType *string `db:"mime_type" json:"mime_type,omitempty"`
|
|
AIExtracted json.RawMessage `db:"ai_extracted" json:"ai_extracted,omitempty"`
|
|
AIExtractionCount int `db:"ai_extraction_count" json:"ai_extraction_count"`
|
|
AIExtractedAt *time.Time `db:"ai_extracted_at" json:"ai_extracted_at,omitempty"`
|
|
UploadedBy *uuid.UUID `db:"uploaded_by" json:"uploaded_by,omitempty"`
|
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
|
}
|
|
|
|
// Partei is a party to an Akte (Kläger, Beklagter, etc.).
|
|
type Partei struct {
|
|
ID uuid.UUID `db:"id" json:"id"`
|
|
AkteID uuid.UUID `db:"akte_id" json:"akte_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"`
|
|
SequenceOrder int `db:"sequence_order" json:"sequence_order"`
|
|
ConditionRuleID *uuid.UUID `db:"condition_rule_id" json:"condition_rule_id,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"`
|
|
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"`
|
|
}
|