Files
CRM_AGENTES_CARGA/backend/api/v1/modules/crm/pipelines/service.py
Aduanasoft 088a8fc4df feat(crm): dominio backend (cuentas, contactos, prospectos, embudos, oportunidades, actividades)
- Nuevo schema `crm` con 7 tablas multi-tenant (TenantScopedMixin + soft delete)
- Módulos FastAPI por dominio: models/dto/service/routes (patrón example)
- Métricas del dashboard (KPIs + embudo por etapa)
- Conversión de prospecto → cuenta/contacto/oportunidad (idempotente)
- Movimiento de oportunidad entre etapas (Kanban) con estado/probabilidad derivados
- 25 permisos registrados en PermissionRegistry
- Migración Alembic con upgrade/downgrade completos
- 24 tests de servicios (pytest) en verde

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 09:32:05 -06:00

134 lines
4.4 KiB
Python

from datetime import datetime, timezone
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from .dto import PipelineCreate, PipelineUpdate, StageCreate, StageUpdate
from .models import Pipeline, PipelineStage
# ----- Pipelines -----
def get_pipelines(db: Session, tenant_id: int, company_id: int) -> list[Pipeline]:
return (
db.query(Pipeline)
.filter(
Pipeline.tenant_id == tenant_id,
Pipeline.company_id == company_id,
Pipeline.deleted_at.is_(None),
)
.order_by(Pipeline.is_default.desc(), Pipeline.name.asc())
.all()
)
def get_pipeline(db: Session, pipeline_id: int, tenant_id: int, company_id: int) -> Pipeline:
pipeline = (
db.query(Pipeline)
.filter(
Pipeline.id == pipeline_id,
Pipeline.tenant_id == tenant_id,
Pipeline.company_id == company_id,
Pipeline.deleted_at.is_(None),
)
.first()
)
if not pipeline:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Embudo no encontrado")
return pipeline
def _clear_default(db: Session, tenant_id: int, company_id: int) -> None:
"""Solo un embudo puede ser el predeterminado por company."""
db.query(Pipeline).filter(
Pipeline.tenant_id == tenant_id,
Pipeline.company_id == company_id,
Pipeline.is_default.is_(True),
).update({Pipeline.is_default: False})
def create_pipeline(db: Session, payload: PipelineCreate, tenant_id: int, company_id: int) -> Pipeline:
if payload.is_default:
_clear_default(db, tenant_id, company_id)
pipeline = Pipeline(**payload.model_dump(), tenant_id=tenant_id, company_id=company_id)
db.add(pipeline)
db.commit()
db.refresh(pipeline)
return pipeline
def update_pipeline(
db: Session, pipeline_id: int, payload: PipelineUpdate, tenant_id: int, company_id: int
) -> Pipeline:
pipeline = get_pipeline(db, pipeline_id, tenant_id, company_id)
data = payload.model_dump(exclude_unset=True)
if data.get("is_default"):
_clear_default(db, tenant_id, company_id)
for field, value in data.items():
setattr(pipeline, field, value)
db.commit()
db.refresh(pipeline)
return pipeline
def delete_pipeline(db: Session, pipeline_id: int, tenant_id: int, company_id: int) -> None:
pipeline = get_pipeline(db, pipeline_id, tenant_id, company_id)
pipeline.deleted_at = datetime.now(timezone.utc)
db.commit()
# ----- Stages -----
def get_stages(db: Session, tenant_id: int, company_id: int, pipeline_id: int | None = None) -> list[PipelineStage]:
query = db.query(PipelineStage).filter(
PipelineStage.tenant_id == tenant_id,
PipelineStage.company_id == company_id,
PipelineStage.deleted_at.is_(None),
)
if pipeline_id is not None:
query = query.filter(PipelineStage.pipeline_id == pipeline_id)
return query.order_by(PipelineStage.position.asc()).all()
def get_stage(db: Session, stage_id: int, tenant_id: int, company_id: int) -> PipelineStage:
stage = (
db.query(PipelineStage)
.filter(
PipelineStage.id == stage_id,
PipelineStage.tenant_id == tenant_id,
PipelineStage.company_id == company_id,
PipelineStage.deleted_at.is_(None),
)
.first()
)
if not stage:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Etapa no encontrada")
return stage
def create_stage(db: Session, payload: StageCreate, tenant_id: int, company_id: int) -> PipelineStage:
# La etapa debe pertenecer a un embudo del mismo tenant/company
get_pipeline(db, payload.pipeline_id, tenant_id, company_id)
stage = PipelineStage(**payload.model_dump(), tenant_id=tenant_id, company_id=company_id)
db.add(stage)
db.commit()
db.refresh(stage)
return stage
def update_stage(
db: Session, stage_id: int, payload: StageUpdate, tenant_id: int, company_id: int
) -> PipelineStage:
stage = get_stage(db, stage_id, tenant_id, company_id)
for field, value in payload.model_dump(exclude_unset=True).items():
setattr(stage, field, value)
db.commit()
db.refresh(stage)
return stage
def delete_stage(db: Session, stage_id: int, tenant_id: int, company_id: int) -> None:
stage = get_stage(db, stage_id, tenant_id, company_id)
stage.deleted_at = datetime.now(timezone.utc)
db.commit()