"""Regression tests for the Leaver-Blocked gate fix. Before migration 032 / PR #XXX, ``transition_overdue_leavers`` would flip any overdue ``leaver_pending`` person to ``left`` regardless of whether their OffboardingProcess was still BLOCKED. Post-fix, a person with a BLOCKED process is held in ``leaver_pending`` — the manager escalation worker is expected to unblock them by auto-transferring pending must-transfer resources to the manager. """ from __future__ import annotations from datetime import date, timedelta from uuid import uuid4 import pytest from sqlalchemy import select from app.domains.hr.service import SuccessFactorsLifecycleService from app.domains.offboarding.models import ( PROC_BLOCKED, PROC_READY, RES_HARDWARE, RESO_PENDING, OffboardingBlocker, OffboardingProcess, ) from app.identity.person_models import IdentityPerson async def _make_leaver( db_session, *, display_name: str, termination: date, ) -> IdentityPerson: person = IdentityPerson( id=uuid4(), source_system="successfactors", hris_person_id=f"sf-{uuid4().hex[:8]}", work_email=f"{display_name.lower().replace(' ', '.')}@example.com", display_name=display_name, employment_status="leaver_pending", termination_date=termination, ) db_session.add(person) await db_session.flush() return person @pytest.mark.asyncio async def test_overdue_leaver_with_blocked_process_is_held(db_session): """Person past termination_date but process=BLOCKED → stays leaver_pending.""" past = date.today() - timedelta(days=2) person = await _make_leaver(db_session, display_name="Blocked Bob", termination=past) proc = OffboardingProcess( id=uuid4(), person_id=person.id, leaver_date=past, status=PROC_BLOCKED, ) db_session.add(proc) await db_session.flush() db_session.add(OffboardingBlocker( process_id=proc.id, resource_type=RES_HARDWARE, resource_id=uuid4(), label="MacBook Pro", resolution=RESO_PENDING, )) await db_session.flush() svc = SuccessFactorsLifecycleService(db_session) summary = await svc.transition_overdue_leavers() assert summary["transitioned_count"] == 0 assert str(person.id) in summary["held_user_ids"] fresh = ( await db_session.execute( select(IdentityPerson).where(IdentityPerson.id == person.id) ) ).scalar_one() assert fresh.employment_status == "leaver_pending" @pytest.mark.asyncio async def test_overdue_leaver_with_ready_process_transitions(db_session): """Person past termination_date and process=READY → transitions to left.""" past = date.today() - timedelta(days=1) person = await _make_leaver(db_session, display_name="Ready Rita", termination=past) db_session.add(OffboardingProcess( id=uuid4(), person_id=person.id, leaver_date=past, status=PROC_READY, )) await db_session.flush() svc = SuccessFactorsLifecycleService(db_session) summary = await svc.transition_overdue_leavers() assert summary["transitioned_count"] == 1 assert str(person.id) in summary["transitioned_user_ids"] assert summary["held_user_ids"] == [] @pytest.mark.asyncio async def test_overdue_leaver_without_process_transitions(db_session): """Person past termination_date and no OffboardingProcess at all — transitions. Covers the edge case of a user manually set to leaver_pending without a scan having run. No blocker ⇒ no reason to hold.""" past = date.today() - timedelta(days=5) person = await _make_leaver(db_session, display_name="Orphan Oscar", termination=past) await db_session.flush() svc = SuccessFactorsLifecycleService(db_session) summary = await svc.transition_overdue_leavers() assert summary["transitioned_count"] == 1 assert str(person.id) in summary["transitioned_user_ids"]