Files
CRM_AGENTES_CARGA/backend/tests/test_opportunities.py
Aduanasoft 3b8da8b4cc fix(crm): correcciones de revisión adversarial en servicios de dominio
- convert_lead valida embudo/etapa en el scope tenant/company (evita fuga multi-tenant y 500)
- oportunidades: estado/probabilidad/cierre se derivan de la etapa también en create/update (no solo move)
- oportunidades: valida que la etapa pertenezca al embudo indicado
- cuentas: country por defecto 'MX' (el server_default no aplicaba con NULL explícito)
- convert_lead: contact_name se normaliza (evita first_name vacío)
- +4 tests que fijan el comportamiento corregido (28 en verde)

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

110 lines
4.5 KiB
Python

import pytest
from fastapi import HTTPException
from api.v1.modules.crm.opportunities import service
from api.v1.modules.crm.opportunities.dto import OpportunityCreate, OpportunityUpdate
from api.v1.modules.crm.pipelines import service as pipelines_service
from api.v1.modules.crm.pipelines.dto import PipelineCreate, StageCreate
T, C = 1, 1
def _pipeline_with_stages(db):
pipeline = pipelines_service.create_pipeline(db, PipelineCreate(name="Ventas", is_default=True), T, C)
s_open = pipelines_service.create_stage(
db, StageCreate(pipeline_id=pipeline.id, name="Prospecto", position=0, probability=10), T, C
)
s_won = pipelines_service.create_stage(
db, StageCreate(pipeline_id=pipeline.id, name="Ganada", position=1, probability=100, is_won=True), T, C
)
s_lost = pipelines_service.create_stage(
db, StageCreate(pipeline_id=pipeline.id, name="Perdida", position=2, is_lost=True), T, C
)
return pipeline, s_open, s_won, s_lost
def test_create_opportunity(db):
pipeline, s_open, _, _ = _pipeline_with_stages(db)
opp = service.create_opportunity(
db,
OpportunityCreate(name="Licencia Aduanasoft", pipeline_id=pipeline.id, stage_id=s_open.id, amount=15000),
T, C,
)
assert opp.status == "open"
assert opp.currency == "MXN"
def test_move_to_won_closes_and_sets_probability(db):
pipeline, s_open, s_won, _ = _pipeline_with_stages(db)
opp = service.create_opportunity(db, OpportunityCreate(name="Deal", pipeline_id=pipeline.id, stage_id=s_open.id), T, C)
moved = service.move_opportunity(db, opp.id, s_won.id, T, C)
assert moved.status == "won"
assert moved.probability == 100
assert moved.closed_at is not None
assert moved.stage_id == s_won.id
def test_move_to_lost(db):
pipeline, s_open, _, s_lost = _pipeline_with_stages(db)
opp = service.create_opportunity(db, OpportunityCreate(name="Deal", pipeline_id=pipeline.id, stage_id=s_open.id), T, C)
moved = service.move_opportunity(db, opp.id, s_lost.id, T, C)
assert moved.status == "lost"
assert moved.probability == 0
assert moved.closed_at is not None
def test_move_back_to_open_reopens(db):
pipeline, s_open, s_won, _ = _pipeline_with_stages(db)
opp = service.create_opportunity(db, OpportunityCreate(name="Deal", pipeline_id=pipeline.id, stage_id=s_open.id), T, C)
service.move_opportunity(db, opp.id, s_won.id, T, C)
reopened = service.move_opportunity(db, opp.id, s_open.id, T, C)
assert reopened.status == "open"
assert reopened.probability == 10
assert reopened.closed_at is None
def test_create_rejects_unknown_account(db):
with pytest.raises(HTTPException) as exc:
service.create_opportunity(db, OpportunityCreate(name="X", account_id=999), T, C)
assert exc.value.status_code == 422
def test_only_one_default_pipeline(db):
pipelines_service.create_pipeline(db, PipelineCreate(name="P1", is_default=True), T, C)
pipelines_service.create_pipeline(db, PipelineCreate(name="P2", is_default=True), T, C)
pipelines = pipelines_service.get_pipelines(db, T, C)
defaults = [p for p in pipelines if p.is_default]
assert len(defaults) == 1 and defaults[0].name == "P2"
def test_create_in_won_stage_derives_state(db):
pipeline, _s_open, s_won, _ = _pipeline_with_stages(db)
opp = service.create_opportunity(
db,
OpportunityCreate(name="Directo a ganada", pipeline_id=pipeline.id, stage_id=s_won.id, amount=500),
T, C,
)
assert opp.status == "won"
assert opp.probability == 100
assert opp.closed_at is not None
def test_update_to_lost_stage_derives_state(db):
pipeline, s_open, _s_won, s_lost = _pipeline_with_stages(db)
opp = service.create_opportunity(db, OpportunityCreate(name="Z", pipeline_id=pipeline.id, stage_id=s_open.id), T, C)
updated = service.update_opportunity(db, opp.id, OpportunityUpdate(stage_id=s_lost.id), T, C)
assert updated.status == "lost"
assert updated.probability == 0
assert updated.closed_at is not None
def test_create_rejects_stage_from_other_pipeline(db):
pipeline, _s_open, _s_won, _s_lost = _pipeline_with_stages(db)
other = pipelines_service.create_pipeline(db, PipelineCreate(name="Otro"), T, C)
other_stage = pipelines_service.create_stage(db, StageCreate(pipeline_id=other.id, name="X"), T, C)
with pytest.raises(HTTPException) as exc:
service.create_opportunity(
db, OpportunityCreate(name="Y", pipeline_id=pipeline.id, stage_id=other_stage.id), T, C
)
assert exc.value.status_code == 422