from __future__ import annotations import uuid from contextlib import asynccontextmanager from datetime import UTC, datetime import pytest from sqlalchemy.dialects.postgresql import JSONB from app.crm import CrmOutcome, CrmResult from app.engine import INSERT_CRM_COMMAND, UPDATE_CRM_COMMAND, WorkflowEngine from app.repository import Profile class Result: rowcount = 1 def __init__(self, *, row=None, scalar=None) -> None: self.row = row self.scalar = scalar def mappings(self): return self def one(self): return self.row def scalar_one_or_none(self): return self.scalar class Connection: def __init__(self, statements: list[str]) -> None: self.statements = statements async def execute(self, statement, params=None): sql = str(statement) self.statements.append(sql) if "INSERT INTO bitrix_sync.business_alerts" in sql: now = datetime.now(UTC) return Result( row={ "id": uuid.uuid4(), "alert_number": 1, "fingerprint": "fingerprint", "alert_type": params["type"], "severity": "warning", "app_user_id": params["user_id"], "selected_external_id": params["selected_external_id"], "candidate_external_ids": params["candidates"], "remote_item_id": None, "occurrence_count": 1, "first_occurred_at": now, "last_occurred_at": now, } ) if "SELECT value_json FROM bitrix_sync.settings" in sql: return Result( scalar={ "entity_type_id": 178, "category_id": 5, "stage_new": "DT178_5:NEW", "field_ids": {"candidate_external_ids": "ufCrm_999"}, } ) return Result() class FakeRepository: def __init__(self) -> None: self.mapping = None self.statements: list[str] = [] @asynccontextmanager async def transaction(self): yield Connection(self.statements) async def active_mapping(self, user_id): return self.mapping async def reserve_limiter_token(self, refill_per_second, burst): return 0 class FakeCrm: def __init__( self, user_id: uuid.UUID, *, existing_identity: bool = False, foreign_owner: bool = False, ) -> None: self.user_id = user_id self.existing_identity = existing_identity self.foreign_owner = foreign_owner self.calls: list[tuple[str, dict]] = [] async def call(self, method, params, *, mutating): self.calls.append((method, params)) if method == "crm.duplicate.findbycomm": return CrmResult(CrmOutcome.SUCCEEDED, {"CONTACT": ["9", "10"]}) if method == "crm.contact.get": contact_id = str(params["id"]) return CrmResult( CrmOutcome.SUCCEEDED, { "ID": contact_id, "CREATED_TIME": "2026-08-06T10:00:00Z", "UF_CRM_100": ( str(self.user_id) if self.existing_identity and contact_id == "10" else str(uuid.UUID(int=1)) if self.foreign_owner and contact_id == "10" else None ), }, ) if method == "crm.contact.add": return CrmResult(CrmOutcome.SUCCEEDED, "11") if method == "crm.item.add": return CrmResult(CrmOutcome.SUCCEEDED, {"item": {"id": "500"}}) return CrmResult(CrmOutcome.SUCCEEDED, True) def test_crm_command_json_payloads_have_explicit_jsonb_types() -> None: assert isinstance(INSERT_CRM_COMMAND._bindparams["safe_request"].type, JSONB) assert isinstance(UPDATE_CRM_COMMAND._bindparams["safe_response"].type, JSONB) @pytest.mark.asyncio async def test_multiple_contacts_choose_numeric_newest(full_settings) -> None: user_id = uuid.uuid4() repository = FakeRepository() crm = FakeCrm(user_id) engine = WorkflowEngine(repository, crm, full_settings) await engine._map_or_create( uuid.uuid4(), Profile(user_id=user_id, phone="+79001234567", identity_status="A", profile_status="A"), ) updates = [params for method, params in crm.calls if method == "crm.contact.update"] assert updates[0]["id"] == "10" assert not any(method == "crm.contact.add" for method, _ in crm.calls) assert any(method == "crm.item.add" for method, _ in crm.calls) alert_add = next(params for method, params in crm.calls if method == "crm.item.add") assert alert_add["fields"]["contactIds"] == [9, 10] assert alert_add["fields"]["sourceId"] == "WEB" assert "ufCrm_999" not in alert_add["fields"] assert any("entity_external_mapping" in statement for statement in repository.statements) assert not any("digest(" in statement for statement in repository.statements) @pytest.mark.asyncio async def test_recovered_duplicate_creates_alert_before_mapping(full_settings) -> None: user_id = uuid.uuid4() repository = FakeRepository() crm = FakeCrm(user_id, existing_identity=True) engine = WorkflowEngine(repository, crm, full_settings) await engine._map_or_create( uuid.uuid4(), Profile(user_id=user_id, phone="+79001234567", identity_status="A", profile_status="A"), ) methods = [method for method, _ in crm.calls] assert "crm.item.add" in methods assert "crm.contact.update" not in methods assert any("entity_external_mapping" in statement for statement in repository.statements) @pytest.mark.asyncio async def test_foreign_owned_contact_alert_includes_new_contact(full_settings) -> None: user_id = uuid.uuid4() repository = FakeRepository() crm = FakeCrm(user_id, foreign_owner=True) engine = WorkflowEngine(repository, crm, full_settings) await engine._map_or_create( uuid.uuid4(), Profile(user_id=user_id, phone="+79001234567", identity_status="A", profile_status="A"), ) methods = [method for method, _ in crm.calls] alert_add = next(params for method, params in crm.calls if method == "crm.item.add") assert methods.index("crm.contact.add") < methods.index("crm.item.add") assert alert_add["fields"]["contactIds"] == [9, 10, 11]