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>
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from api.v1.modules.crm.leads import service
|
||||
from api.v1.modules.crm.leads.dto import LeadConvert, LeadCreate
|
||||
from api.v1.modules.crm.pipelines import service as pipelines_service
|
||||
@@ -6,6 +9,13 @@ from api.v1.modules.crm.pipelines.dto import PipelineCreate, StageCreate
|
||||
T, C = 1, 1
|
||||
|
||||
|
||||
def test_convert_rejects_invalid_stage(db):
|
||||
lead = service.create_lead(db, LeadCreate(name="P", company_name="Empresa"), T, C)
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
service.convert_lead(db, lead.id, LeadConvert(create_opportunity=True, stage_id=999), T, C)
|
||||
assert exc.value.status_code == 422
|
||||
|
||||
|
||||
def test_create_lead_defaults_to_new(db):
|
||||
lead = service.create_lead(db, LeadCreate(name="Prospecto X", company_name="XYZ SA"), T, C)
|
||||
assert lead.status == "new"
|
||||
|
||||
@@ -2,7 +2,7 @@ import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from api.v1.modules.crm.opportunities import service
|
||||
from api.v1.modules.crm.opportunities.dto import OpportunityCreate
|
||||
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
|
||||
|
||||
@@ -75,3 +75,35 @@ def test_only_one_default_pipeline(db):
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user