"""A hire that provisions nothing must leave an audit trail. ``execute_onboarding_for_person`` used to log only when it created ≥1 task, so a hire matching no ProvisionPolicy (no department × title × location rule and no NULL-wildcard catch-all) produced zero tasks *silently* — IT could not tell "provisioned nothing, no policy matched" apart from "this hire was never processed". At 500-person scale that's a real under-provisioning blind spot. The zero-task branch now emits a ``provisioning.onboarding_no_tasks`` audit row with the cause. """ from __future__ import annotations import uuid import pytest from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.audit.models import AuditLog from app.domains.provisioning.models import ProvisionPolicy from app.domains.provisioning.service import ProvisioningService from app.identity.person_models import IdentityPerson async def _seed_person(db: AsyncSession) -> uuid.UUID: pid = uuid.uuid4() db.add(IdentityPerson( id=pid, source_system="successfactors", hris_person_id=f"sf-{pid.hex[:8]}", work_email=f"{pid.hex[:8]}@example.com", display_name="No-Policy Hire", employment_status="active", )) await db.commit() return pid async def _no_task_rows(db: AsyncSession, pid: uuid.UUID) -> list[AuditLog]: rows = ( await db.execute( select(AuditLog).where( AuditLog.action == "provisioning.onboarding_no_tasks", AuditLog.resource_id == str(pid), ) ) ).scalars().all() return list(rows) @pytest.mark.asyncio async def test_no_matching_policy_emits_audit(db_session: AsyncSession): pid = await _seed_person(db_session) svc = ProvisioningService(db_session) created = await svc.execute_onboarding_for_person( pid, trace_id="t-nopolicy", actor_user_id=None, ) await db_session.commit() assert created == [] rows = await _no_task_rows(db_session, pid) assert len(rows) == 1, "zero-task onboarding must leave an audit row" assert rows[0].detail["reason"] == "no_matching_policy" assert rows[0].detail["matched_policy_count"] == 0 @pytest.mark.asyncio async def test_matched_policy_with_unresolvable_target_is_distinguished( db_session: AsyncSession, ): # A wildcard policy matches the hire, but its seat target points at a # plan that doesn't exist → _build_preview_items yields nothing. This is # a misconfiguration, NOT "no policy" — the reason must say so, so IT # triages the broken policy rather than assuming the hire is out of scope. pid = await _seed_person(db_session) db_session.add(ProvisionPolicy( id=uuid.uuid4(), department_name=None, match_job_title=None, match_location=None, target_type="seat", target_id=uuid.uuid4(), # no SoftwareLicensePlan with this id is_active=True, )) await db_session.commit() svc = ProvisioningService(db_session) created = await svc.execute_onboarding_for_person( pid, trace_id="t-unresolvable", actor_user_id=None, ) await db_session.commit() assert created == [] rows = await _no_task_rows(db_session, pid) assert len(rows) == 1 assert rows[0].detail["reason"] == "policy_without_resolvable_target" assert rows[0].detail["matched_policy_count"] == 1