Initial commit

This commit is contained in:
2026-01-12 08:17:17 -07:00
commit de5b6feef4
104 changed files with 12925 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
"""
Category Model - ServiceManagerWeb
"""
from sqlalchemy import String, Text, Boolean, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID
from typing import List, Optional
import uuid
from app.core.database import Base
class Category(Base):
__tablename__ = "categories"
name: Mapped[str] = mapped_column(String(100), nullable=False)
description: Mapped[Optional[str]] = mapped_column(Text)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
# Optional: Tenant specific categories?
tenant_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=True)
# Relationships
tickets: Mapped[List["Ticket"]] = relationship("Ticket", back_populates="category")
tenant: Mapped["Tenant"] = relationship("Tenant") # Assuming Tenant model is imported
def __repr__(self) -> str:
return f"<Category(id={self.id}, name='{self.name}')>"

View File

@@ -0,0 +1,25 @@
"""
System Model - ServiceManagerWeb
"""
from sqlalchemy import String, Text, Boolean
from sqlalchemy.orm import Mapped, mapped_column, relationship
from typing import List, Optional
import uuid
from app.core.database import Base
class System(Base):
__tablename__ = "systems"
name: Mapped[str] = mapped_column(String(100), nullable=False)
description: Mapped[Optional[str]] = mapped_column(Text)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
# Relationships
# If we want tickets to link to systems, we will add relationship in Ticket later or now.
# We will assume Ticket links to System.
tickets: Mapped[List["Ticket"]] = relationship("Ticket", back_populates="system")
def __repr__(self) -> str:
return f"<System(id={self.id}, name='{self.name}')>"

View File

@@ -0,0 +1,68 @@
"""
Tenant Model - ServiceManagerWeb
Modelo para organizaciones cliente (multi-tenancy)
"""
from sqlalchemy import String, Integer, Text, Boolean, ARRAY
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID, ENUM
from typing import List, Optional
import enum
import uuid
from app.core.database import Base
class TenantStatus(str, enum.Enum):
"""Estados de un tenant."""
ACTIVE = "active"
SUSPENDED = "suspended"
INACTIVE = "inactive"
class Tenant(Base):
"""Modelo de Tenant (Organización cliente)."""
__tablename__ = "tenants"
# Información básica
name: Mapped[str] = mapped_column(String(255), nullable=False)
slug: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
domain: Mapped[Optional[str]] = mapped_column(String(255))
logo_url: Mapped[Optional[str]] = mapped_column(String(500))
# Contacto
contact_email: Mapped[Optional[str]] = mapped_column(String(320))
contact_phone: Mapped[Optional[str]] = mapped_column(String(20))
address: Mapped[Optional[str]] = mapped_column(Text)
# Configuración regional
timezone: Mapped[str] = mapped_column(String(50), default="UTC")
locale: Mapped[str] = mapped_column(String(10), default="es-ES")
# Límites y configuración
max_users: Mapped[int] = mapped_column(Integer, default=50)
max_storage_mb: Mapped[int] = mapped_column(Integer, default=1024)
allowed_file_types: Mapped[List[str]] = mapped_column(
ARRAY(String),
default=["pdf", "jpg", "jpeg", "png", "doc", "docx", "xls", "xlsx", "txt"]
)
# Estado
status: Mapped[TenantStatus] = mapped_column(
String(20),
default=TenantStatus.ACTIVE
)
# Relaciones
users: Mapped[List["User"]] = relationship("User", back_populates="tenant")
tickets: Mapped[List["Ticket"]] = relationship("Ticket", back_populates="tenant")
def __repr__(self) -> str:
return f"<Tenant(id={self.id}, name='{self.name}', slug='{self.slug}')>"
@property
def is_active(self) -> bool:
"""Check if tenant is active."""
return self.status == TenantStatus.ACTIVE

View File

@@ -0,0 +1,65 @@
"""
Ticket Model - ServiceManagerWeb
"""
from sqlalchemy import String, ForeignKey, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID, ENUM
from typing import Optional
import enum
import uuid
from app.core.database import Base
class TicketStatus(str, enum.Enum):
NEW = "NEW"
TRIAGE = "TRIAGE"
IN_PROGRESS = "IN_PROGRESS"
WAITING_FOR_CLIENT = "WAITING_FOR_CLIENT"
RESOLVED = "RESOLVED"
CLOSED = "CLOSED"
REOPENED = "REOPENED"
class TicketPriority(str, enum.Enum):
LOW = "LOW"
MEDIUM = "MEDIUM"
HIGH = "HIGH"
URGENT = "URGENT"
class Ticket(Base):
__tablename__ = "tickets"
# Note: id, created_at, updated_at are inherited from Base
tenant_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False)
ticket_number: Mapped[str] = mapped_column(String(20), nullable=False)
subject: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str] = mapped_column(Text, nullable=False)
status: Mapped[TicketStatus] = mapped_column(ENUM(TicketStatus, name="ticket_status_enum", create_type=False), default=TicketStatus.NEW)
priority: Mapped[TicketPriority] = mapped_column(ENUM(TicketPriority, name="ticket_priority_enum", create_type=False), default=TicketPriority.MEDIUM)
# Foreign Keys
created_by: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
assigned_to: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
system_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("systems.id"), nullable=True)
category_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("categories.id"), nullable=True)
# Relationships
tenant: Mapped["Tenant"] = relationship("Tenant", back_populates="tickets")
system: Mapped["System"] = relationship("System", back_populates="tickets")
category: Mapped["Category"] = relationship("Category", back_populates="tickets")
created_by_user: Mapped["User"] = relationship(
"User",
foreign_keys=[created_by],
back_populates="created_tickets"
)
assigned_to_user: Mapped[Optional["User"]] = relationship(
"User",
foreign_keys=[assigned_to],
back_populates="assigned_tickets"
)

135
backend/app/models/user.py Normal file
View File

@@ -0,0 +1,135 @@
"""
User Model - ServiceManagerWeb
Modelo para usuarios del sistema (internos y clientes)
"""
from sqlalchemy import String, Boolean, DateTime, ForeignKey, Text, ARRAY
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID, ENUM
from typing import Optional, List
import enum
import uuid
from datetime import datetime
from app.core.database import Base
class UserRole(str, enum.Enum):
"""Roles de usuario en el sistema."""
# Staff interno
ADMIN = "ADMIN" # Control total
SUPPORT_MANAGER = "SUPPORT_MANAGER" # Gestión de equipos y SLAs
AGENT = "AGENT" # Atención de tickets
AUDITOR = "AUDITOR" # Solo lectura para auditoría
# Clientes
CLIENT_ADMIN = "CLIENT_ADMIN" # Admin de organización cliente
CLIENT_USER = "CLIENT_USER" # Usuario final cliente
class User(Base):
"""Modelo de Usuario."""
__tablename__ = "users"
# Relación con tenant
tenant_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("tenants.id", ondelete="CASCADE"),
nullable=False
)
# Información básica
email: Mapped[str] = mapped_column(String(320), nullable=False)
first_name: Mapped[str] = mapped_column(String(100), nullable=False)
last_name: Mapped[str] = mapped_column(String(100), nullable=False)
avatar_url: Mapped[Optional[str]] = mapped_column(String(500))
# Autenticación
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
role: Mapped[UserRole] = mapped_column(ENUM(UserRole), nullable=False)
# 2FA (opcional para staff interno)
totp_secret: Mapped[Optional[str]] = mapped_column(String(32))
totp_enabled: Mapped[bool] = mapped_column(Boolean, default=False)
backup_codes: Mapped[Optional[List[str]]] = mapped_column(ARRAY(String))
# Estado
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
email_verified: Mapped[bool] = mapped_column(Boolean, default=False)
last_login: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
last_activity: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
# Preferencias
language: Mapped[str] = mapped_column(String(10), default="es")
timezone: Mapped[str] = mapped_column(String(50), default="UTC")
notifications_email: Mapped[bool] = mapped_column(Boolean, default=True)
# Relaciones
tenant: Mapped["Tenant"] = relationship("Tenant", back_populates="users")
created_tickets: Mapped[List["Ticket"]] = relationship(
"Ticket",
back_populates="created_by_user",
foreign_keys="Ticket.created_by"
)
assigned_tickets: Mapped[List["Ticket"]] = relationship(
"Ticket",
back_populates="assigned_to_user",
foreign_keys="Ticket.assigned_to"
)
# Unique constraint por tenant
__table_args__ = (
{"postgresql_tablespace": "users"},
)
def __repr__(self) -> str:
return f"<User(id={self.id}, email='{self.email}', role='{self.role}')>"
@property
def full_name(self) -> str:
"""Get user's full name."""
return f"{self.first_name} {self.last_name}"
@property
def is_staff(self) -> bool:
"""Check if user is internal staff."""
return self.role in [
UserRole.ADMIN,
UserRole.SUPPORT_MANAGER,
UserRole.AGENT,
UserRole.AUDITOR
]
@property
def is_client(self) -> bool:
"""Check if user is a client."""
return self.role in [
UserRole.CLIENT_ADMIN,
UserRole.CLIENT_USER
]
@property
def can_manage_users(self) -> bool:
"""Check if user can manage other users."""
return self.role in [
UserRole.ADMIN,
UserRole.SUPPORT_MANAGER,
UserRole.CLIENT_ADMIN
]
@property
def can_manage_tickets(self) -> bool:
"""Check if user can manage tickets."""
return self.role in [
UserRole.ADMIN,
UserRole.SUPPORT_MANAGER,
UserRole.AGENT
]
@property
def requires_2fa(self) -> bool:
"""Check if 2FA is required for this user."""
# 2FA opcional para staff interno, no requerido para clientes
return self.is_staff