Initial commit
This commit is contained in:
28
backend/app/models/category.py
Normal file
28
backend/app/models/category.py
Normal 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}')>"
|
||||
25
backend/app/models/system.py
Normal file
25
backend/app/models/system.py
Normal 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}')>"
|
||||
68
backend/app/models/tenant.py
Normal file
68
backend/app/models/tenant.py
Normal 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
|
||||
65
backend/app/models/ticket.py
Normal file
65
backend/app/models/ticket.py
Normal 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
135
backend/app/models/user.py
Normal 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
|
||||
Reference in New Issue
Block a user