from contextlib import nullcontext from datetime import date import pytest from fastapi import HTTPException from pydantic import ValidationError from app.contacts.backend import lifecycle as api from app.contacts.backend import lifecycle_service as service USER = {'id': 5, 'is_superadmin': True} def event(**overrides): return dict(id=9, contact_id=1, customer_id=2, event_type='onboarding', event_date=date(2026, 11, 17), lead_days=14, auto_create=False, template_id=None, responsible_user_id=None, parent_case_id=None, case_id=None, status='draft', last_error=None, **overrides) @pytest.fixture def setup_api(monkeypatch): monkeypatch.setattr(api, '_ready', lambda: None) monkeypatch.setattr(api, '_contact', lambda _: None) monkeypatch.setattr(api, '_membership', lambda *args: None) monkeypatch.setattr(api, 'atomic_transaction', nullcontext) monkeypatch.setattr(api, 'hidden_case_ids', lambda _: set()) monkeypatch.setattr(service, 'find_parent_candidates', lambda *args: []) def test_default_automation_is_disabled_and_lead_is_fourteen(): request = api.LifecycleCreate(customer_id=2, event_type='onboarding', event_date='2026-11-17') assert request.auto_create is False assert request.lead_days == 14 with pytest.raises(ValidationError): api.LifecycleCreate(customer_id=2, event_type='offboarding', event_date='2026-11-17', lead_days=-1) def test_missing_migration_is_explicit_503(monkeypatch): monkeypatch.setattr(api, 'execute_query_single', lambda *args: {'relation': None}) with pytest.raises(HTTPException) as error: api._ready() assert error.value.status_code == 503 def test_customer_must_belong_to_contact(monkeypatch): monkeypatch.setattr(api, 'execute_query_single', lambda *args: None) with pytest.raises(HTTPException) as error: api._membership(1, 90) assert error.value.status_code == 400 def test_enabling_automation_requires_case_creation(setup_api): row = event() row['auto_create'] = True with pytest.raises(HTTPException) as error: api._validate(row, {'id': 5, 'permissions': ['cases.edit']}) assert error.value.status_code == 403 def test_template_requires_run_permission(setup_api): row = event() row['template_id'] = 4 with pytest.raises(HTTPException) as error: api._validate(row, {'id': 5, 'permissions': ['cases.edit']}) assert error.value.status_code == 403 def test_ambiguous_parent_saves_waiting_for_choice(setup_api, monkeypatch): monkeypatch.setattr(service, 'find_parent_candidates', lambda *args: [{'id': 10}, {'id': 11}]) writes = [] def query(sql, params): writes.append((sql, params)) row = event() row.update(auto_create=True, status='needs_parent') return row monkeypatch.setattr(api, 'execute_query_single', query) result = api.create_lifecycle(1, api.LifecycleCreate( customer_id=2, event_type='onboarding', event_date='2026-11-17', auto_create=True), USER) assert result['status'] == 'needs_parent' assert writes[0][1][-2] == 'needs_parent' def test_preview_excludes_confidential_candidates(setup_api, monkeypatch): monkeypatch.setattr(service, 'find_parent_candidates', lambda *args: [{'id': 10}, {'id': 11}]) monkeypatch.setattr(api, 'hidden_case_ids', lambda _: {11}) result = api.preview_lifecycle(1, 2, 'onboarding', date(2026, 11, 17), 14, USER) assert result['candidates'] == [{'id': 10}] assert result['parent_case_id'] == 10 assert result['needs_parent'] is False def test_date_change_updates_same_case_without_insert(setup_api, monkeypatch): row = event() row.update(case_id=45, status='created') monkeypatch.setattr(api, '_find_event', lambda *args, **kwargs: row) monkeypatch.setattr(api, 'require_case', lambda *args: None) calls = [] monkeypatch.setattr(service, 'update_created_case_date', lambda *args: calls.append(args) or {'status': 'created', 'last_error': None}) writes = [] def query(sql, params): writes.append(sql) return dict(row, event_date=date(2026, 11, 20)) monkeypatch.setattr(api, 'execute_query_single', query) result = api.update_lifecycle(1, 9, api.LifecycleUpdate(event_date='2026-11-20'), USER) assert result['case_id'] == 45 assert calls[0][1] == date(2026, 11, 17) assert all('INSERT' not in sql for sql in writes) def test_cancel_preserves_case_and_disables_automation(setup_api, monkeypatch): monkeypatch.setattr(api, '_find_event', lambda *args, **kwargs: event()) writes = [] def query(sql, params): writes.append(sql) return event() monkeypatch.setattr(api, 'execute_query_single', query) api.cancel_lifecycle(1, 9, USER) assert 'auto_create=FALSE' in writes[0] assert 'DELETE' not in writes[0] assert 'case_id=' not in writes[0]