This commit is contained in:
HotSwapp
2025-08-18 20:20:04 -05:00
parent 89b2bc0aa2
commit bac8cc4bd5
114 changed files with 30258 additions and 1341 deletions

View File

@@ -17,6 +17,15 @@ from .pensions import (
SeparationAgreement, LifeTable, NumberTable, PensionResult
)
from .templates import DocumentTemplate, DocumentTemplateVersion, TemplateKeyword
from .template_variables import (
TemplateVariable, VariableContext, VariableAuditLog,
VariableTemplate, VariableGroup, VariableType
)
from .document_workflows import (
DocumentWorkflow, WorkflowAction, WorkflowExecution, EventLog,
WorkflowTemplate, WorkflowSchedule, WorkflowTriggerType, WorkflowActionType,
ExecutionStatus, WorkflowStatus
)
from .billing import (
BillingBatch, BillingBatchFile, StatementTemplate, BillingStatement,
BillingStatementItem, StatementPayment, StatementStatus
@@ -26,8 +35,14 @@ from .timers import (
)
from .file_management import (
FileStatusHistory, FileTransferHistory, FileArchiveInfo,
FileClosureChecklist, FileAlert
FileClosureChecklist, FileAlert, FileRelationship
)
from .jobs import JobRecord
from .deadlines import (
Deadline, DeadlineReminder, DeadlineTemplate, DeadlineHistory,
CourtCalendar, DeadlineType, DeadlinePriority, DeadlineStatus, NotificationFrequency
)
from .sessions import UserSession, SessionActivity, SessionConfiguration, SessionSecurityEvent
from .lookups import (
Employee, FileType, FileStatus, TransactionType, TransactionCode,
State, GroupLookup, Footer, PlanInfo, FormIndex, FormList,
@@ -48,5 +63,14 @@ __all__ = [
"BillingStatementItem", "StatementPayment", "StatementStatus",
"Timer", "TimeEntry", "TimerSession", "TimerTemplate", "TimerStatus", "TimerType",
"FileStatusHistory", "FileTransferHistory", "FileArchiveInfo",
"FileClosureChecklist", "FileAlert"
"FileClosureChecklist", "FileAlert", "FileRelationship",
"Deadline", "DeadlineReminder", "DeadlineTemplate", "DeadlineHistory",
"CourtCalendar", "DeadlineType", "DeadlinePriority", "DeadlineStatus", "NotificationFrequency",
"JobRecord",
"UserSession", "SessionActivity", "SessionConfiguration", "SessionSecurityEvent",
"TemplateVariable", "VariableContext", "VariableAuditLog",
"VariableTemplate", "VariableGroup", "VariableType",
"DocumentWorkflow", "WorkflowAction", "WorkflowExecution", "EventLog",
"WorkflowTemplate", "WorkflowSchedule", "WorkflowTriggerType", "WorkflowActionType",
"ExecutionStatus", "WorkflowStatus"
]

View File

@@ -0,0 +1,388 @@
"""
Enhanced audit logging models for P2 security features
"""
from datetime import datetime, timezone
from typing import Optional, Dict, Any
from enum import Enum
import json
from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey, Text, Index
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import JSONB
from app.models.base import BaseModel
class SecurityEventType(str, Enum):
"""Security event types for classification"""
# Authentication events
LOGIN_SUCCESS = "login_success"
LOGIN_FAILURE = "login_failure"
LOGOUT = "logout"
SESSION_EXPIRED = "session_expired"
PASSWORD_CHANGE = "password_change"
ACCOUNT_LOCKED = "account_locked"
# Authorization events
ACCESS_DENIED = "access_denied"
PRIVILEGE_ESCALATION = "privilege_escalation"
UNAUTHORIZED_ACCESS = "unauthorized_access"
# Data access events
DATA_READ = "data_read"
DATA_WRITE = "data_write"
DATA_DELETE = "data_delete"
DATA_EXPORT = "data_export"
BULK_OPERATION = "bulk_operation"
# System events
CONFIGURATION_CHANGE = "configuration_change"
USER_CREATION = "user_creation"
USER_MODIFICATION = "user_modification"
USER_DELETION = "user_deletion"
# Security events
SUSPICIOUS_ACTIVITY = "suspicious_activity"
ATTACK_DETECTED = "attack_detected"
SECURITY_VIOLATION = "security_violation"
IP_BLOCKED = "ip_blocked"
# File events
FILE_UPLOAD = "file_upload"
FILE_DOWNLOAD = "file_download"
FILE_DELETION = "file_deletion"
FILE_MODIFICATION = "file_modification"
# Integration events
API_ACCESS = "api_access"
EXTERNAL_SERVICE = "external_service"
IMPORT_OPERATION = "import_operation"
class SecurityEventSeverity(str, Enum):
"""Security event severity levels"""
CRITICAL = "critical"
HIGH = "high"
MEDIUM = "medium"
LOW = "low"
INFO = "info"
class ComplianceStandard(str, Enum):
"""Compliance standards for reporting"""
SOX = "sox" # Sarbanes-Oxley
HIPAA = "hipaa" # Health Insurance Portability and Accountability Act
GDPR = "gdpr" # General Data Protection Regulation
SOC2 = "soc2" # Service Organization Control 2
ISO27001 = "iso27001" # Information Security Management
NIST = "nist" # National Institute of Standards and Technology
class EnhancedAuditLog(BaseModel):
"""
Enhanced audit logging for comprehensive security monitoring
"""
__tablename__ = "enhanced_audit_logs"
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
# Event identification
event_id = Column(String(64), nullable=False, unique=True, index=True)
event_type = Column(String(50), nullable=False, index=True)
event_category = Column(String(30), nullable=False, index=True) # security, audit, compliance, system
severity = Column(String(20), nullable=False, index=True)
# Event details
title = Column(String(255), nullable=False)
description = Column(Text, nullable=False)
outcome = Column(String(20), nullable=False, index=True) # success, failure, error, blocked
# User and session context
user_id = Column(Integer, ForeignKey("users.id"), nullable=True, index=True)
session_id = Column(String(128), nullable=True, index=True)
impersonated_user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
# Network context
source_ip = Column(String(45), nullable=True, index=True)
user_agent = Column(Text, nullable=True)
request_id = Column(String(64), nullable=True, index=True)
# Geographic context
country = Column(String(5), nullable=True)
region = Column(String(100), nullable=True)
city = Column(String(100), nullable=True)
# Technical context
endpoint = Column(String(255), nullable=True, index=True)
http_method = Column(String(10), nullable=True)
status_code = Column(Integer, nullable=True)
response_time_ms = Column(Integer, nullable=True)
# Resource context
resource_type = Column(String(50), nullable=True, index=True) # file, customer, document, etc.
resource_id = Column(String(100), nullable=True, index=True)
resource_name = Column(String(255), nullable=True)
# Data context
data_before = Column(Text, nullable=True) # JSON string of previous state
data_after = Column(Text, nullable=True) # JSON string of new state
data_volume = Column(Integer, nullable=True) # Bytes processed
record_count = Column(Integer, nullable=True) # Number of records affected
# Risk assessment
risk_score = Column(Integer, default=0, nullable=False, index=True) # 0-100
risk_factors = Column(Text, nullable=True) # JSON array of risk indicators
threat_indicators = Column(Text, nullable=True) # JSON array of threat patterns
# Compliance tracking
compliance_standards = Column(Text, nullable=True) # JSON array of applicable standards
retention_period_days = Column(Integer, default=2555, nullable=False) # 7 years default
# Timestamp and tracking
timestamp = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False, index=True)
processed_at = Column(DateTime(timezone=True), nullable=True)
correlation_id = Column(String(64), nullable=True, index=True) # For related events
# Additional metadata
tags = Column(Text, nullable=True) # JSON array of tags for categorization
custom_fields = Column(Text, nullable=True) # JSON object for custom data
# Relationships
user = relationship("User", foreign_keys=[user_id])
impersonated_user = relationship("User", foreign_keys=[impersonated_user_id])
def set_data_before(self, data: Dict[str, Any]) -> None:
"""Set data before change as JSON"""
self.data_before = json.dumps(data, default=str) if data else None
def set_data_after(self, data: Dict[str, Any]) -> None:
"""Set data after change as JSON"""
self.data_after = json.dumps(data, default=str) if data else None
def get_data_before(self) -> Optional[Dict[str, Any]]:
"""Get data before change from JSON"""
return json.loads(self.data_before) if self.data_before else None
def get_data_after(self) -> Optional[Dict[str, Any]]:
"""Get data after change from JSON"""
return json.loads(self.data_after) if self.data_after else None
def set_risk_factors(self, factors: list) -> None:
"""Set risk factors as JSON"""
self.risk_factors = json.dumps(factors) if factors else None
def get_risk_factors(self) -> list:
"""Get risk factors from JSON"""
return json.loads(self.risk_factors) if self.risk_factors else []
def set_threat_indicators(self, indicators: list) -> None:
"""Set threat indicators as JSON"""
self.threat_indicators = json.dumps(indicators) if indicators else None
def get_threat_indicators(self) -> list:
"""Get threat indicators from JSON"""
return json.loads(self.threat_indicators) if self.threat_indicators else []
def set_compliance_standards(self, standards: list) -> None:
"""Set compliance standards as JSON"""
self.compliance_standards = json.dumps(standards) if standards else None
def get_compliance_standards(self) -> list:
"""Get compliance standards from JSON"""
return json.loads(self.compliance_standards) if self.compliance_standards else []
def set_tags(self, tags: list) -> None:
"""Set tags as JSON"""
self.tags = json.dumps(tags) if tags else None
def get_tags(self) -> list:
"""Get tags from JSON"""
return json.loads(self.tags) if self.tags else []
def set_custom_fields(self, fields: Dict[str, Any]) -> None:
"""Set custom fields as JSON"""
self.custom_fields = json.dumps(fields, default=str) if fields else None
def get_custom_fields(self) -> Optional[Dict[str, Any]]:
"""Get custom fields from JSON"""
return json.loads(self.custom_fields) if self.custom_fields else None
# Add indexes for performance
__table_args__ = (
Index('idx_enhanced_audit_user_timestamp', 'user_id', 'timestamp'),
Index('idx_enhanced_audit_event_severity', 'event_type', 'severity'),
Index('idx_enhanced_audit_resource', 'resource_type', 'resource_id'),
Index('idx_enhanced_audit_ip_timestamp', 'source_ip', 'timestamp'),
Index('idx_enhanced_audit_correlation', 'correlation_id'),
Index('idx_enhanced_audit_risk_score', 'risk_score'),
Index('idx_enhanced_audit_compliance', 'compliance_standards'),
)
class SecurityAlert(BaseModel):
"""
Security alerts for real-time monitoring and incident response
"""
__tablename__ = "security_alerts"
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
# Alert identification
alert_id = Column(String(64), nullable=False, unique=True, index=True)
rule_id = Column(String(64), nullable=False, index=True)
rule_name = Column(String(255), nullable=False)
# Alert details
title = Column(String(255), nullable=False)
description = Column(Text, nullable=False)
severity = Column(String(20), nullable=False, index=True)
confidence = Column(Integer, default=100, nullable=False) # 0-100 confidence score
# Context
event_count = Column(Integer, default=1, nullable=False) # Number of triggering events
time_window_minutes = Column(Integer, nullable=True) # Time window for correlation
affected_users = Column(Text, nullable=True) # JSON array of user IDs
affected_resources = Column(Text, nullable=True) # JSON array of resource identifiers
# Response tracking
status = Column(String(20), default="open", nullable=False, index=True) # open, investigating, resolved, false_positive
assigned_to = Column(Integer, ForeignKey("users.id"), nullable=True)
resolved_by = Column(Integer, ForeignKey("users.id"), nullable=True)
resolution_notes = Column(Text, nullable=True)
# Timestamps
first_seen = Column(DateTime(timezone=True), nullable=False, index=True)
last_seen = Column(DateTime(timezone=True), nullable=False, index=True)
acknowledged_at = Column(DateTime(timezone=True), nullable=True)
resolved_at = Column(DateTime(timezone=True), nullable=True)
# Related audit logs
triggering_events = Column(Text, nullable=True) # JSON array of audit log IDs
# Additional metadata
tags = Column(Text, nullable=True) # JSON array of tags
custom_fields = Column(Text, nullable=True) # JSON object for custom data
# Relationships
assignee = relationship("User", foreign_keys=[assigned_to])
resolver = relationship("User", foreign_keys=[resolved_by])
class ComplianceReport(BaseModel):
"""
Compliance reporting for various standards
"""
__tablename__ = "compliance_reports"
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
# Report identification
report_id = Column(String(64), nullable=False, unique=True, index=True)
standard = Column(String(50), nullable=False, index=True)
report_type = Column(String(50), nullable=False, index=True) # periodic, on_demand, incident
# Report details
title = Column(String(255), nullable=False)
description = Column(Text, nullable=True)
# Time range
start_date = Column(DateTime(timezone=True), nullable=False, index=True)
end_date = Column(DateTime(timezone=True), nullable=False, index=True)
# Report content
summary = Column(Text, nullable=True) # JSON summary of findings
details = Column(Text, nullable=True) # JSON detailed findings
recommendations = Column(Text, nullable=True) # JSON recommendations
# Metrics
total_events = Column(Integer, default=0, nullable=False)
security_events = Column(Integer, default=0, nullable=False)
violations = Column(Integer, default=0, nullable=False)
high_risk_events = Column(Integer, default=0, nullable=False)
# Status
status = Column(String(20), default="generating", nullable=False, index=True) # generating, ready, delivered, archived
generated_by = Column(Integer, ForeignKey("users.id"), nullable=False)
generated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
# Delivery
recipients = Column(Text, nullable=True) # JSON array of recipient emails
delivered_at = Column(DateTime(timezone=True), nullable=True)
# File storage
file_path = Column(String(500), nullable=True) # Path to generated report file
file_size = Column(Integer, nullable=True) # File size in bytes
# Relationships
generator = relationship("User", foreign_keys=[generated_by])
class AuditRetentionPolicy(BaseModel):
"""
Audit log retention policies for compliance
"""
__tablename__ = "audit_retention_policies"
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
# Policy identification
policy_name = Column(String(255), nullable=False, unique=True, index=True)
event_types = Column(Text, nullable=True) # JSON array of event types to apply to
compliance_standards = Column(Text, nullable=True) # JSON array of applicable standards
# Retention settings
retention_days = Column(Integer, nullable=False) # Days to retain
archive_after_days = Column(Integer, nullable=True) # Days before archiving
# Policy details
description = Column(Text, nullable=True)
is_active = Column(Boolean, default=True, nullable=False)
priority = Column(Integer, default=100, nullable=False) # Higher priority = more specific
# Timestamps
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
created_by = Column(Integer, ForeignKey("users.id"), nullable=False)
# Relationships
creator = relationship("User")
class SIEMIntegration(BaseModel):
"""
SIEM integration configuration and status
"""
__tablename__ = "siem_integrations"
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
# Integration identification
integration_name = Column(String(255), nullable=False, unique=True, index=True)
siem_type = Column(String(50), nullable=False, index=True) # splunk, elk, qradar, etc.
# Configuration
endpoint_url = Column(String(500), nullable=True)
api_key_hash = Column(String(255), nullable=True) # Hashed API key
configuration = Column(Text, nullable=True) # JSON configuration
# Event filtering
event_types = Column(Text, nullable=True) # JSON array of event types to send
severity_threshold = Column(String(20), default="medium", nullable=False)
# Status
is_active = Column(Boolean, default=True, nullable=False)
is_healthy = Column(Boolean, default=True, nullable=False)
last_sync = Column(DateTime(timezone=True), nullable=True)
last_error = Column(Text, nullable=True)
# Statistics
events_sent = Column(Integer, default=0, nullable=False)
errors_count = Column(Integer, default=0, nullable=False)
# Timestamps
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
created_by = Column(Integer, ForeignKey("users.id"), nullable=False)
# Relationships
creator = relationship("User")

View File

@@ -8,6 +8,8 @@ from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey, U
from sqlalchemy.orm import relationship
from app.models.base import BaseModel
from sqlalchemy import Text
from app.models.audit import LoginAttempt as _AuditLoginAttempt
class RefreshToken(BaseModel):
@@ -32,3 +34,8 @@ class RefreshToken(BaseModel):
)
"""
Expose `LoginAttempt` from `app.models.audit` here for backward compatibility.
"""
LoginAttempt = _AuditLoginAttempt

View File

@@ -1,7 +1,7 @@
"""
Base model with common fields
"""
from sqlalchemy import Column, DateTime, String
from sqlalchemy import Column, DateTime, String, event
from sqlalchemy.sql import func
from app.database.base import Base
@@ -14,4 +14,41 @@ class TimestampMixin:
class BaseModel(Base, TimestampMixin):
"""Base model class"""
__abstract__ = True
__abstract__ = True
# Event listeners for adaptive cache integration
@event.listens_for(BaseModel, 'after_update', propagate=True)
def record_update(mapper, connection, target):
"""Record data updates for adaptive cache TTL calculation"""
try:
from app.services.adaptive_cache import record_data_update
table_name = target.__tablename__
record_data_update(table_name)
except Exception:
# Don't fail database operations if cache tracking fails
pass
@event.listens_for(BaseModel, 'after_insert', propagate=True)
def record_insert(mapper, connection, target):
"""Record data inserts for adaptive cache TTL calculation"""
try:
from app.services.adaptive_cache import record_data_update
table_name = target.__tablename__
record_data_update(table_name)
except Exception:
# Don't fail database operations if cache tracking fails
pass
@event.listens_for(BaseModel, 'after_delete', propagate=True)
def record_delete(mapper, connection, target):
"""Record data deletions for adaptive cache TTL calculation"""
try:
from app.services.adaptive_cache import record_data_update
table_name = target.__tablename__
record_data_update(table_name)
except Exception:
# Don't fail database operations if cache tracking fails
pass

272
app/models/deadlines.py Normal file
View File

@@ -0,0 +1,272 @@
"""
Deadline management models for legal practice deadlines and court dates
"""
from sqlalchemy import Column, Integer, String, DateTime, Date, Text, ForeignKey, Boolean, Enum
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from enum import Enum as PyEnum
from app.models.base import BaseModel
class DeadlineType(PyEnum):
"""Types of deadlines in legal practice"""
COURT_FILING = "court_filing"
COURT_HEARING = "court_hearing"
DISCOVERY = "discovery"
STATUTE_OF_LIMITATIONS = "statute_of_limitations"
CONTRACT = "contract"
ADMINISTRATIVE = "administrative"
CLIENT_MEETING = "client_meeting"
INTERNAL = "internal"
OTHER = "other"
class DeadlinePriority(PyEnum):
"""Priority levels for deadlines"""
CRITICAL = "critical" # Statute of limitations, court filings
HIGH = "high" # Court hearings, important discovery
MEDIUM = "medium" # Client meetings, administrative
LOW = "low" # Internal deadlines, optional items
class DeadlineStatus(PyEnum):
"""Status of deadline completion"""
PENDING = "pending"
COMPLETED = "completed"
MISSED = "missed"
CANCELLED = "cancelled"
EXTENDED = "extended"
class NotificationFrequency(PyEnum):
"""How often to send deadline reminders"""
NONE = "none"
DAILY = "daily"
WEEKLY = "weekly"
MONTHLY = "monthly"
CUSTOM = "custom"
class Deadline(BaseModel):
"""
Legal deadlines and important dates
Tracks court dates, filing deadlines, statute of limitations, etc.
"""
__tablename__ = "deadlines"
id = Column(Integer, primary_key=True, autoincrement=True)
# File association
file_no = Column(String(45), ForeignKey("files.file_no"), nullable=True, index=True)
client_id = Column(String(80), ForeignKey("rolodex.id"), nullable=True, index=True)
# Deadline details
title = Column(String(200), nullable=False)
description = Column(Text)
deadline_date = Column(Date, nullable=False, index=True)
deadline_time = Column(DateTime(timezone=True), nullable=True) # For specific times
# Classification
deadline_type = Column(Enum(DeadlineType), nullable=False, default=DeadlineType.OTHER)
priority = Column(Enum(DeadlinePriority), nullable=False, default=DeadlinePriority.MEDIUM)
status = Column(Enum(DeadlineStatus), nullable=False, default=DeadlineStatus.PENDING)
# Assignment and ownership
assigned_to_user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
assigned_to_employee_id = Column(String(10), ForeignKey("employees.empl_num"), nullable=True)
created_by_user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
# Court/external details
court_name = Column(String(200)) # Which court if applicable
case_number = Column(String(100)) # Court case number
judge_name = Column(String(100))
opposing_counsel = Column(String(200))
# Notification settings
notification_frequency = Column(Enum(NotificationFrequency), default=NotificationFrequency.WEEKLY)
advance_notice_days = Column(Integer, default=7) # Days before deadline to start notifications
last_notification_sent = Column(DateTime(timezone=True))
# Completion tracking
completed_date = Column(DateTime(timezone=True))
completed_by_user_id = Column(Integer, ForeignKey("users.id"))
completion_notes = Column(Text)
# Extension tracking
original_deadline_date = Column(Date) # Track if deadline was extended
extension_reason = Column(Text)
extension_granted_by = Column(String(100)) # Court, opposing counsel, etc.
# Metadata
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
# Relationships
file = relationship("File", back_populates="deadlines")
client = relationship("Rolodex")
assigned_to_user = relationship("User", foreign_keys=[assigned_to_user_id])
assigned_to_employee = relationship("Employee")
created_by = relationship("User", foreign_keys=[created_by_user_id])
completed_by = relationship("User", foreign_keys=[completed_by_user_id])
reminders = relationship("DeadlineReminder", back_populates="deadline", cascade="all, delete-orphan")
def __repr__(self):
return f"<Deadline(id={self.id}, title='{self.title}', date='{self.deadline_date}', status='{self.status.value}')>"
@property
def is_overdue(self) -> bool:
"""Check if deadline is overdue"""
from datetime import date
return self.status == DeadlineStatus.PENDING and self.deadline_date < date.today()
@property
def days_until_deadline(self) -> int:
"""Calculate days until deadline (negative if overdue)"""
from datetime import date
return (self.deadline_date - date.today()).days
class DeadlineReminder(BaseModel):
"""
Automatic reminders for deadlines
Tracks when notifications were sent and their status
"""
__tablename__ = "deadline_reminders"
id = Column(Integer, primary_key=True, autoincrement=True)
deadline_id = Column(Integer, ForeignKey("deadlines.id"), nullable=False, index=True)
# Reminder scheduling
reminder_date = Column(Date, nullable=False, index=True)
reminder_time = Column(DateTime(timezone=True))
days_before_deadline = Column(Integer, nullable=False) # How many days before deadline
# Notification details
notification_sent = Column(Boolean, default=False)
sent_at = Column(DateTime(timezone=True))
notification_method = Column(String(50), default="email") # email, sms, in_app
recipient_user_id = Column(Integer, ForeignKey("users.id"))
recipient_email = Column(String(255))
# Message content
subject = Column(String(200))
message = Column(Text)
# Status tracking
delivery_status = Column(String(50), default="pending") # pending, sent, delivered, failed
error_message = Column(Text)
# Metadata
created_at = Column(DateTime(timezone=True), default=func.now())
# Relationships
deadline = relationship("Deadline", back_populates="reminders")
recipient = relationship("User")
def __repr__(self):
return f"<DeadlineReminder(id={self.id}, deadline_id={self.deadline_id}, date='{self.reminder_date}', sent={self.notification_sent})>"
class DeadlineTemplate(BaseModel):
"""
Templates for common deadline types
Helps standardize deadline creation for common legal processes
"""
__tablename__ = "deadline_templates"
id = Column(Integer, primary_key=True, autoincrement=True)
# Template details
name = Column(String(200), nullable=False, unique=True)
description = Column(Text)
deadline_type = Column(Enum(DeadlineType), nullable=False)
priority = Column(Enum(DeadlinePriority), nullable=False, default=DeadlinePriority.MEDIUM)
# Default settings
default_title_template = Column(String(200)) # Template with placeholders like {file_no}, {client_name}
default_description_template = Column(Text)
default_advance_notice_days = Column(Integer, default=7)
default_notification_frequency = Column(Enum(NotificationFrequency), default=NotificationFrequency.WEEKLY)
# Timing defaults
days_from_file_open = Column(Integer) # Auto-calculate deadline based on file open date
days_from_event = Column(Integer) # Days from some triggering event
# Status and metadata
active = Column(Boolean, default=True)
created_by_user_id = Column(Integer, ForeignKey("users.id"))
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
# Relationships
created_by = relationship("User")
def __repr__(self):
return f"<DeadlineTemplate(id={self.id}, name='{self.name}', type='{self.deadline_type.value}')>"
class DeadlineHistory(BaseModel):
"""
History of deadline changes and updates
Maintains audit trail for deadline modifications
"""
__tablename__ = "deadline_history"
id = Column(Integer, primary_key=True, autoincrement=True)
deadline_id = Column(Integer, ForeignKey("deadlines.id"), nullable=False, index=True)
# Change details
change_type = Column(String(50), nullable=False) # created, updated, completed, extended, cancelled
field_changed = Column(String(100)) # Which field was changed
old_value = Column(Text)
new_value = Column(Text)
# Change context
change_reason = Column(Text)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
change_date = Column(DateTime(timezone=True), default=func.now())
# Relationships
deadline = relationship("Deadline")
user = relationship("User")
def __repr__(self):
return f"<DeadlineHistory(id={self.id}, deadline_id={self.deadline_id}, change='{self.change_type}')>"
class CourtCalendar(BaseModel):
"""
Court calendar entries and hearing schedules
Specialized deadline type for court appearances
"""
__tablename__ = "court_calendar"
id = Column(Integer, primary_key=True, autoincrement=True)
deadline_id = Column(Integer, ForeignKey("deadlines.id"), nullable=False, unique=True)
# Court details
court_name = Column(String(200), nullable=False)
courtroom = Column(String(50))
judge_name = Column(String(100))
case_number = Column(String(100))
# Hearing details
hearing_type = Column(String(100)) # Motion hearing, trial, conference, etc.
estimated_duration = Column(Integer) # Minutes
appearance_required = Column(Boolean, default=True)
# Preparation tracking
preparation_deadline = Column(Date) # When prep should be completed
documents_filed = Column(Boolean, default=False)
client_notified = Column(Boolean, default=False)
# Outcome tracking
hearing_completed = Column(Boolean, default=False)
outcome = Column(Text)
next_hearing_date = Column(Date)
# Relationships
deadline = relationship("Deadline")
def __repr__(self):
return f"<CourtCalendar(id={self.id}, court='{self.court_name}', hearing='{self.hearing_type}')>"

View File

@@ -0,0 +1,303 @@
"""
Document Workflow Automation Models
This module provides automated document generation workflows triggered by case events,
deadlines, file status changes, and other system events.
"""
from sqlalchemy import Column, Integer, String, Text, ForeignKey, Boolean, JSON, DateTime, Date, Enum
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from enum import Enum as PyEnum
from typing import Dict, Any, List, Optional
import json
from app.models.base import BaseModel
class WorkflowTriggerType(PyEnum):
"""Types of events that can trigger workflows"""
FILE_STATUS_CHANGE = "file_status_change"
DEADLINE_APPROACHING = "deadline_approaching"
DEADLINE_OVERDUE = "deadline_overdue"
DEADLINE_COMPLETED = "deadline_completed"
PAYMENT_RECEIVED = "payment_received"
PAYMENT_OVERDUE = "payment_overdue"
FILE_OPENED = "file_opened"
FILE_CLOSED = "file_closed"
DOCUMENT_UPLOADED = "document_uploaded"
QDRO_STATUS_CHANGE = "qdro_status_change"
TIME_BASED = "time_based"
MANUAL_TRIGGER = "manual_trigger"
CUSTOM_EVENT = "custom_event"
class WorkflowStatus(PyEnum):
"""Workflow execution status"""
ACTIVE = "active"
INACTIVE = "inactive"
PAUSED = "paused"
ARCHIVED = "archived"
class WorkflowActionType(PyEnum):
"""Types of actions a workflow can perform"""
GENERATE_DOCUMENT = "generate_document"
SEND_EMAIL = "send_email"
CREATE_DEADLINE = "create_deadline"
UPDATE_FILE_STATUS = "update_file_status"
CREATE_LEDGER_ENTRY = "create_ledger_entry"
SEND_NOTIFICATION = "send_notification"
EXECUTE_CUSTOM = "execute_custom"
class ExecutionStatus(PyEnum):
"""Status of workflow execution"""
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
CANCELLED = "cancelled"
RETRYING = "retrying"
class DocumentWorkflow(BaseModel):
"""
Defines automated workflows for document generation and case management
"""
__tablename__ = "document_workflows"
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
# Basic workflow information
name = Column(String(200), nullable=False, index=True)
description = Column(Text, nullable=True)
status = Column(Enum(WorkflowStatus), default=WorkflowStatus.ACTIVE, nullable=False)
# Trigger configuration
trigger_type = Column(Enum(WorkflowTriggerType), nullable=False, index=True)
trigger_conditions = Column(JSON, nullable=True) # JSON conditions for when to trigger
# Execution settings
delay_minutes = Column(Integer, default=0) # Delay before execution
max_retries = Column(Integer, default=3)
retry_delay_minutes = Column(Integer, default=30)
timeout_minutes = Column(Integer, default=60)
# Filtering conditions
file_type_filter = Column(JSON, nullable=True) # Array of file types to include
status_filter = Column(JSON, nullable=True) # Array of file statuses to include
attorney_filter = Column(JSON, nullable=True) # Array of attorney IDs to include
client_filter = Column(JSON, nullable=True) # Array of client IDs to include
# Schedule settings (for time-based triggers)
schedule_cron = Column(String(100), nullable=True) # Cron expression for scheduling
schedule_timezone = Column(String(50), default="UTC")
next_run_time = Column(DateTime(timezone=True), nullable=True)
# Priority and organization
priority = Column(Integer, default=5) # 1-10, higher = more important
category = Column(String(100), nullable=True, index=True)
tags = Column(JSON, nullable=True) # Array of tags for organization
# Metadata
created_by = Column(String(150), ForeignKey("users.username"), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now(), nullable=False)
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
last_triggered_at = Column(DateTime(timezone=True), nullable=True)
# Statistics
execution_count = Column(Integer, default=0)
success_count = Column(Integer, default=0)
failure_count = Column(Integer, default=0)
# Relationships
actions = relationship("WorkflowAction", back_populates="workflow", cascade="all, delete-orphan")
executions = relationship("WorkflowExecution", back_populates="workflow", cascade="all, delete-orphan")
def __repr__(self):
return f"<DocumentWorkflow(id={self.id}, name='{self.name}', trigger='{self.trigger_type.value}')>"
class WorkflowAction(BaseModel):
"""
Individual actions within a workflow
"""
__tablename__ = "workflow_actions"
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
workflow_id = Column(Integer, ForeignKey("document_workflows.id"), nullable=False, index=True)
# Action configuration
action_type = Column(Enum(WorkflowActionType), nullable=False)
action_order = Column(Integer, default=1) # Order of execution within workflow
action_name = Column(String(200), nullable=True) # Optional descriptive name
# Action parameters (specific to action type)
parameters = Column(JSON, nullable=True)
# Document generation specific fields
template_id = Column(Integer, ForeignKey("document_templates.id"), nullable=True)
output_format = Column(String(50), default="DOCX") # DOCX, PDF, HTML
custom_filename_template = Column(String(500), nullable=True) # Template for filename
# Email action specific fields
email_template_id = Column(Integer, nullable=True) # Reference to email template
email_recipients = Column(JSON, nullable=True) # Array of recipient types/addresses
email_subject_template = Column(String(500), nullable=True)
# Conditional execution
condition = Column(JSON, nullable=True) # Conditions for this action to execute
continue_on_failure = Column(Boolean, default=False) # Whether to continue if this action fails
# Relationships
workflow = relationship("DocumentWorkflow", back_populates="actions")
template = relationship("DocumentTemplate")
def __repr__(self):
return f"<WorkflowAction(id={self.id}, workflow_id={self.workflow_id}, type='{self.action_type.value}')>"
class WorkflowExecution(BaseModel):
"""
Tracks individual workflow executions
"""
__tablename__ = "workflow_executions"
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
workflow_id = Column(Integer, ForeignKey("document_workflows.id"), nullable=False, index=True)
# Execution context
triggered_by_event_id = Column(String(100), nullable=True, index=True) # Reference to triggering event
triggered_by_event_type = Column(String(50), nullable=True)
context_file_no = Column(String(45), nullable=True, index=True)
context_client_id = Column(String(80), nullable=True)
context_user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
# Execution details
status = Column(Enum(ExecutionStatus), default=ExecutionStatus.PENDING, nullable=False)
started_at = Column(DateTime(timezone=True), nullable=True)
completed_at = Column(DateTime(timezone=True), nullable=True)
# Input data and context
trigger_data = Column(JSON, nullable=True) # Data from the triggering event
execution_context = Column(JSON, nullable=True) # Variables and context for execution
# Results and outputs
generated_documents = Column(JSON, nullable=True) # Array of generated document info
action_results = Column(JSON, nullable=True) # Results from each action
error_message = Column(Text, nullable=True)
error_details = Column(JSON, nullable=True)
# Performance metrics
execution_duration_seconds = Column(Integer, nullable=True)
retry_count = Column(Integer, default=0)
next_retry_at = Column(DateTime(timezone=True), nullable=True)
# Relationships
workflow = relationship("DocumentWorkflow", back_populates="executions")
user = relationship("User")
def __repr__(self):
return f"<WorkflowExecution(id={self.id}, workflow_id={self.workflow_id}, status='{self.status.value}')>"
class WorkflowTemplate(BaseModel):
"""
Pre-defined workflow templates for common scenarios
"""
__tablename__ = "workflow_templates"
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
# Template information
name = Column(String(200), nullable=False, unique=True)
description = Column(Text, nullable=True)
category = Column(String(100), nullable=True, index=True)
# Workflow definition
workflow_definition = Column(JSON, nullable=False) # Complete workflow configuration
# Metadata
created_by = Column(String(150), ForeignKey("users.username"), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now(), nullable=False)
is_system_template = Column(Boolean, default=False) # Built-in vs user-created
usage_count = Column(Integer, default=0) # How many times this template has been used
# Version control
version = Column(String(20), default="1.0.0")
template_tags = Column(JSON, nullable=True) # Tags for categorization
def __repr__(self):
return f"<WorkflowTemplate(id={self.id}, name='{self.name}', version='{self.version}')>"
class EventLog(BaseModel):
"""
Unified event log for workflow triggering
"""
__tablename__ = "event_log"
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
# Event identification
event_id = Column(String(100), nullable=False, unique=True, index=True) # UUID
event_type = Column(String(50), nullable=False, index=True)
event_source = Column(String(100), nullable=False) # Which system/module generated the event
# Event context
file_no = Column(String(45), nullable=True, index=True)
client_id = Column(String(80), nullable=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
resource_type = Column(String(50), nullable=True) # deadline, file, payment, etc.
resource_id = Column(String(100), nullable=True)
# Event data
event_data = Column(JSON, nullable=True) # Event-specific data
previous_state = Column(JSON, nullable=True) # Previous state before event
new_state = Column(JSON, nullable=True) # New state after event
# Workflow processing
processed = Column(Boolean, default=False, index=True)
processed_at = Column(DateTime(timezone=True), nullable=True)
triggered_workflows = Column(JSON, nullable=True) # Array of workflow IDs triggered
processing_errors = Column(JSON, nullable=True)
# Timing
occurred_at = Column(DateTime(timezone=True), default=func.now(), nullable=False, index=True)
# Relationships
user = relationship("User")
def __repr__(self):
return f"<EventLog(id={self.id}, event_type='{self.event_type}', file_no='{self.file_no}')>"
class WorkflowSchedule(BaseModel):
"""
Scheduled workflow executions (for time-based triggers)
"""
__tablename__ = "workflow_schedules"
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
workflow_id = Column(Integer, ForeignKey("document_workflows.id"), nullable=False, index=True)
# Schedule configuration
schedule_name = Column(String(200), nullable=True)
cron_expression = Column(String(100), nullable=False)
timezone = Column(String(50), default="UTC")
# Execution tracking
next_run_time = Column(DateTime(timezone=True), nullable=False, index=True)
last_run_time = Column(DateTime(timezone=True), nullable=True)
# Status
active = Column(Boolean, default=True, nullable=False)
# Metadata
created_at = Column(DateTime(timezone=True), default=func.now(), nullable=False)
# Relationships
workflow = relationship("DocumentWorkflow")
def __repr__(self):
return f"<WorkflowSchedule(id={self.id}, workflow_id={self.workflow_id}, next_run='{self.next_run_time}')>"

View File

@@ -188,4 +188,33 @@ class FileAlert(BaseModel):
def __repr__(self):
status = "🔔" if self.is_active and not self.is_acknowledged else ""
return f"<FileAlert({status} {self.alert_type} - {self.file_no} on {self.alert_date})>"
return f"<FileAlert({status} {self.alert_type} - {self.file_no} on {self.alert_date})>"
class FileRelationship(BaseModel):
"""
Track relationships between files (e.g., related, parent/child, duplicate).
Enables cross-referencing and conflict checks.
"""
__tablename__ = "file_relationships"
id = Column(Integer, primary_key=True, autoincrement=True)
source_file_no = Column(String(45), ForeignKey("files.file_no"), nullable=False, index=True)
target_file_no = Column(String(45), ForeignKey("files.file_no"), nullable=False, index=True)
# Relationship metadata
relationship_type = Column(String(45), nullable=False) # related, parent, child, duplicate, conflict, referral
notes = Column(Text)
# Who created it (cached for reporting)
created_by_user_id = Column(Integer, ForeignKey("users.id"))
created_by_name = Column(String(100))
# Relationships
source_file = relationship("File", foreign_keys=[source_file_no])
target_file = relationship("File", foreign_keys=[target_file_no])
def __repr__(self):
return (
f"<FileRelationship({self.source_file_no} -[{self.relationship_type}]-> {self.target_file_no})>"
)

View File

@@ -68,4 +68,5 @@ class File(BaseModel):
documents = relationship("Document", back_populates="file", cascade="all, delete-orphan")
billing_statements = relationship("BillingStatement", back_populates="file", cascade="all, delete-orphan")
timers = relationship("Timer", back_populates="file", cascade="all, delete-orphan")
time_entries = relationship("TimeEntry", back_populates="file", cascade="all, delete-orphan")
time_entries = relationship("TimeEntry", back_populates="file", cascade="all, delete-orphan")
deadlines = relationship("Deadline", back_populates="file", cascade="all, delete-orphan")

55
app/models/jobs.py Normal file
View File

@@ -0,0 +1,55 @@
"""
Simple job record schema for tracking synchronous batch operations.
"""
from sqlalchemy import Column, Integer, String, DateTime, Text, JSON, Index
from sqlalchemy.sql import func
from app.models.base import BaseModel
class JobRecord(BaseModel):
"""
Minimal job tracking record (no worker/queue yet).
Used to record outcomes and downloadable bundle info for synchronous jobs
such as batch document generation.
"""
__tablename__ = "jobs"
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
job_id = Column(String(100), unique=True, nullable=False, index=True)
job_type = Column(String(64), nullable=False, index=True) # e.g., documents_batch
status = Column(String(32), nullable=False, index=True) # running|completed|failed
# Request/identity
requested_by_username = Column(String(150), nullable=True, index=True)
# Timing
started_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)
completed_at = Column(DateTime(timezone=True), nullable=True, index=True)
# Metrics
total_requested = Column(Integer, nullable=False, default=0)
total_success = Column(Integer, nullable=False, default=0)
total_failed = Column(Integer, nullable=False, default=0)
# Result bundle (if any)
result_storage_path = Column(String(512), nullable=True)
result_mime_type = Column(String(100), nullable=True)
result_size = Column(Integer, nullable=True)
# Arbitrary details/metadata for easy querying
details = Column(JSON, nullable=True)
__table_args__ = (
Index("ix_jobs_type_status", "job_type", "status"),
{},
)
def __repr__(self):
return (
f"<JobRecord(job_id={self.job_id}, type='{self.job_type}', status='{self.status}', "
f"success={self.total_success}/{self.total_requested})>"
)

View File

@@ -71,6 +71,7 @@ class TransactionType(BaseModel):
t_type = Column(String(1), primary_key=True, index=True) # Transaction type code
description = Column(String(100), nullable=False) # Description
debit_credit = Column(String(1)) # D=Debit, C=Credit
footer_code = Column(String(45), ForeignKey("footers.footer_code")) # Default footer for statements (legacy TRNSTYPE Footer)
active = Column(Boolean, default=True) # Is type active
def __repr__(self):
@@ -118,6 +119,7 @@ class GroupLookup(BaseModel):
group_code = Column(String(45), primary_key=True, index=True) # Group code
description = Column(String(200), nullable=False) # Description
title = Column(String(200)) # Legacy GRUPLKUP Title
active = Column(Boolean, default=True) # Is group active
def __repr__(self):

189
app/models/sessions.py Normal file
View File

@@ -0,0 +1,189 @@
"""
Session management models for advanced security
"""
from datetime import datetime, timezone, timedelta
from typing import Optional
from enum import Enum
from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey, Text, func
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import UUID
import uuid
from app.models.base import BaseModel
class SessionStatus(str, Enum):
"""Session status enumeration"""
ACTIVE = "active"
EXPIRED = "expired"
REVOKED = "revoked"
LOCKED = "locked"
class UserSession(BaseModel):
"""
Enhanced user session tracking for security monitoring
"""
__tablename__ = "user_sessions"
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
session_id = Column(String(128), nullable=False, unique=True, index=True) # Secure session identifier
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
# Session metadata
ip_address = Column(String(45), nullable=True, index=True)
user_agent = Column(Text, nullable=True)
device_fingerprint = Column(String(255), nullable=True) # For device tracking
# Geographic and security info
country = Column(String(5), nullable=True) # ISO country code
city = Column(String(100), nullable=True)
is_suspicious = Column(Boolean, default=False, nullable=False, index=True)
risk_score = Column(Integer, default=0, nullable=False) # 0-100 risk assessment
# Session lifecycle
status = Column(String(20), default=SessionStatus.ACTIVE, nullable=False, index=True)
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
last_activity = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False, index=True)
expires_at = Column(DateTime(timezone=True), nullable=False, index=True)
# Security tracking
login_method = Column(String(50), nullable=True) # password, 2fa, sso, etc.
locked_at = Column(DateTime(timezone=True), nullable=True)
revoked_at = Column(DateTime(timezone=True), nullable=True)
revocation_reason = Column(String(100), nullable=True)
# Relationships
user = relationship("User", back_populates="sessions")
activities = relationship("SessionActivity", back_populates="session", cascade="all, delete-orphan")
def is_expired(self) -> bool:
"""Check if session is expired"""
return datetime.now(timezone.utc) >= self.expires_at
def is_active(self) -> bool:
"""Check if session is currently active"""
return self.status == SessionStatus.ACTIVE and not self.is_expired()
def extend_session(self, duration: timedelta = timedelta(hours=8)) -> None:
"""Extend session expiration time"""
self.expires_at = datetime.now(timezone.utc) + duration
self.last_activity = datetime.now(timezone.utc)
def revoke_session(self, reason: str = "user_logout") -> None:
"""Revoke the session"""
self.status = SessionStatus.REVOKED
self.revoked_at = datetime.now(timezone.utc)
self.revocation_reason = reason
def lock_session(self, reason: str = "suspicious_activity") -> None:
"""Lock the session for security reasons"""
self.status = SessionStatus.LOCKED
self.locked_at = datetime.now(timezone.utc)
self.revocation_reason = reason
class SessionActivity(BaseModel):
"""
Track user activities within sessions for security analysis
"""
__tablename__ = "session_activities"
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
session_id = Column(Integer, ForeignKey("user_sessions.id"), nullable=False, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
# Activity details
activity_type = Column(String(50), nullable=False, index=True) # login, logout, api_call, admin_action, etc.
endpoint = Column(String(255), nullable=True) # API endpoint accessed
method = Column(String(10), nullable=True) # HTTP method
status_code = Column(Integer, nullable=True) # Response status
# Request details
ip_address = Column(String(45), nullable=True, index=True)
user_agent = Column(Text, nullable=True)
referer = Column(String(255), nullable=True)
# Security analysis
is_suspicious = Column(Boolean, default=False, nullable=False, index=True)
risk_factors = Column(Text, nullable=True) # JSON string of detected risks
# Timing
timestamp = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False, index=True)
duration_ms = Column(Integer, nullable=True) # Request processing time
# Additional metadata
resource_accessed = Column(String(255), nullable=True) # File, customer, etc.
data_volume = Column(Integer, nullable=True) # Bytes transferred
# Relationships
session = relationship("UserSession", back_populates="activities")
user = relationship("User")
class SessionConfiguration(BaseModel):
"""
Configurable session policies and limits
"""
__tablename__ = "session_configurations"
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=True, index=True) # Null for global config
# Session limits
max_concurrent_sessions = Column(Integer, default=3, nullable=False)
session_timeout_minutes = Column(Integer, default=480, nullable=False) # 8 hours default
idle_timeout_minutes = Column(Integer, default=60, nullable=False) # 1 hour idle
# Security policies
require_session_renewal = Column(Boolean, default=True, nullable=False)
renewal_interval_hours = Column(Integer, default=24, nullable=False)
force_logout_on_ip_change = Column(Boolean, default=False, nullable=False)
suspicious_activity_threshold = Column(Integer, default=5, nullable=False)
# Geographic restrictions
allowed_countries = Column(Text, nullable=True) # JSON array of ISO codes
blocked_countries = Column(Text, nullable=True) # JSON array of ISO codes
# Timestamps
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=func.now(), nullable=False)
# Relationships
user = relationship("User") # If user-specific config
class SessionSecurityEvent(BaseModel):
"""
Track security events related to sessions
"""
__tablename__ = "session_security_events"
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
session_id = Column(Integer, ForeignKey("user_sessions.id"), nullable=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
# Event details
event_type = Column(String(50), nullable=False, index=True) # session_fixation, concurrent_limit, suspicious_login, etc.
severity = Column(String(20), nullable=False, index=True) # low, medium, high, critical
description = Column(Text, nullable=False)
# Context
ip_address = Column(String(45), nullable=True, index=True)
user_agent = Column(Text, nullable=True)
country = Column(String(5), nullable=True)
# Response actions
action_taken = Column(String(100), nullable=True) # session_locked, user_notified, admin_alerted, etc.
resolved = Column(Boolean, default=False, nullable=False, index=True)
resolved_at = Column(DateTime(timezone=True), nullable=True)
resolved_by = Column(Integer, ForeignKey("users.id"), nullable=True)
# Timestamps
timestamp = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False, index=True)
# Relationships
session = relationship("UserSession")
user = relationship("User", foreign_keys=[user_id])
resolver = relationship("User", foreign_keys=[resolved_by])

View File

@@ -0,0 +1,186 @@
"""
Enhanced Template Variable Models with Advanced Features
This module provides sophisticated variable management for document templates including:
- Conditional logic and calculations
- Dynamic data source integration
- Variable dependencies and validation
- Type-safe variable definitions
"""
from sqlalchemy import Column, Integer, String, Text, ForeignKey, Boolean, JSON, Enum, Float, DateTime, func
from sqlalchemy.orm import relationship
from sqlalchemy.sql import expression
from enum import Enum as PyEnum
from typing import Dict, Any, List, Optional
import json
from app.models.base import BaseModel
class VariableType(PyEnum):
"""Variable types supported in templates"""
STRING = "string"
NUMBER = "number"
DATE = "date"
BOOLEAN = "boolean"
CALCULATED = "calculated"
CONDITIONAL = "conditional"
QUERY = "query"
LOOKUP = "lookup"
class TemplateVariable(BaseModel):
"""
Enhanced template variables with support for complex logic, calculations, and data sources
"""
__tablename__ = "template_variables"
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
# Basic identification
name = Column(String(100), nullable=False, index=True)
display_name = Column(String(200), nullable=True)
description = Column(Text, nullable=True)
# Variable type and behavior
variable_type = Column(Enum(VariableType), nullable=False, default=VariableType.STRING)
required = Column(Boolean, default=False)
active = Column(Boolean, default=True, nullable=False)
# Default and static values
default_value = Column(Text, nullable=True)
static_value = Column(Text, nullable=True) # When set, always returns this value
# Advanced features
formula = Column(Text, nullable=True) # Mathematical or logical expressions
conditional_logic = Column(JSON, nullable=True) # If/then/else rules
data_source_query = Column(Text, nullable=True) # SQL query for dynamic data
lookup_table = Column(String(100), nullable=True) # Reference table name
lookup_key_field = Column(String(100), nullable=True) # Field to match on
lookup_value_field = Column(String(100), nullable=True) # Field to return
# Validation rules
validation_rules = Column(JSON, nullable=True) # JSON schema or validation rules
format_pattern = Column(String(200), nullable=True) # Regex pattern for formatting
# Dependencies and relationships
depends_on = Column(JSON, nullable=True) # List of variable names this depends on
scope = Column(String(50), default="global") # global, template, file, client
# Metadata
created_by = Column(String(150), ForeignKey("users.username"), nullable=True)
category = Column(String(100), nullable=True, index=True)
tags = Column(JSON, nullable=True) # Array of tags for organization
# Cache settings for performance
cache_duration_minutes = Column(Integer, default=0) # 0 = no cache
last_cached_at = Column(DateTime, nullable=True)
cached_value = Column(Text, nullable=True)
def __repr__(self):
return f"<TemplateVariable(name='{self.name}', type='{self.variable_type}')>"
class VariableTemplate(BaseModel):
"""
Association between variables and document templates
"""
__tablename__ = "variable_templates"
id = Column(Integer, primary_key=True, autoincrement=True)
template_id = Column(Integer, ForeignKey("document_templates.id"), nullable=False, index=True)
variable_id = Column(Integer, ForeignKey("template_variables.id"), nullable=False, index=True)
# Template-specific overrides
override_default = Column(Text, nullable=True)
override_required = Column(Boolean, nullable=True)
display_order = Column(Integer, default=0)
group_name = Column(String(100), nullable=True) # For organizing variables in UI
# Relationships
template = relationship("DocumentTemplate")
variable = relationship("TemplateVariable")
class VariableContext(BaseModel):
"""
Context-specific variable values (per file, client, case, etc.)
"""
__tablename__ = "variable_contexts"
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
variable_id = Column(Integer, ForeignKey("template_variables.id"), nullable=False, index=True)
# Context identification
context_type = Column(String(50), nullable=False, index=True) # file, client, global, session
context_id = Column(String(100), nullable=False, index=True) # The actual ID (file_no, client_id, etc.)
# Value storage
value = Column(Text, nullable=True)
computed_value = Column(Text, nullable=True) # Result after formula/logic processing
last_computed_at = Column(DateTime, nullable=True)
# Validation and metadata
is_valid = Column(Boolean, default=True)
validation_errors = Column(JSON, nullable=True)
source = Column(String(100), nullable=True) # manual, computed, imported, etc.
# Relationships
variable = relationship("TemplateVariable")
def __repr__(self):
return f"<VariableContext(variable_id={self.variable_id}, context='{self.context_type}:{self.context_id}')>"
class VariableAuditLog(BaseModel):
"""
Audit trail for variable value changes
"""
__tablename__ = "variable_audit_log"
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
variable_id = Column(Integer, ForeignKey("template_variables.id"), nullable=False, index=True)
context_type = Column(String(50), nullable=True, index=True)
context_id = Column(String(100), nullable=True, index=True)
# Change tracking
old_value = Column(Text, nullable=True)
new_value = Column(Text, nullable=True)
change_type = Column(String(50), nullable=False) # created, updated, deleted, computed
change_reason = Column(String(200), nullable=True)
# Metadata
changed_by = Column(String(150), ForeignKey("users.username"), nullable=True)
changed_at = Column(DateTime, default=func.now(), nullable=False)
source_system = Column(String(100), nullable=True) # web, api, import, etc.
# Relationships
variable = relationship("TemplateVariable")
def __repr__(self):
return f"<VariableAuditLog(variable_id={self.variable_id}, change='{self.change_type}')>"
class VariableGroup(BaseModel):
"""
Logical groupings of variables for better organization
"""
__tablename__ = "variable_groups"
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
name = Column(String(100), nullable=False, unique=True, index=True)
description = Column(Text, nullable=True)
parent_group_id = Column(Integer, ForeignKey("variable_groups.id"), nullable=True)
display_order = Column(Integer, default=0)
# UI configuration
icon = Column(String(50), nullable=True)
color = Column(String(20), nullable=True)
collapsible = Column(Boolean, default=True)
# Relationships
parent_group = relationship("VariableGroup", remote_side=[id])
child_groups = relationship("VariableGroup", back_populates="parent_group")
def __repr__(self):
return f"<VariableGroup(name='{self.name}')>"

View File

@@ -39,6 +39,7 @@ class User(BaseModel):
submitted_tickets = relationship("SupportTicket", foreign_keys="SupportTicket.user_id", back_populates="submitter")
timers = relationship("Timer", back_populates="user", cascade="all, delete-orphan")
time_entries = relationship("TimeEntry", back_populates="user", cascade="all, delete-orphan")
sessions = relationship("UserSession", back_populates="user", cascade="all, delete-orphan")
def __repr__(self):
return f"<User(username='{self.username}', email='{self.email}')>"