"""#2508 compensation retries participate in durable fair scheduling.""" from __future__ import annotations import uuid from contextlib import asynccontextmanager from datetime import UTC, datetime, timedelta from types import SimpleNamespace import pytest from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession import app.core.distributed_lock as distributed_lock_module from app.approval.models import ApprovalSubmissionIntent from app.approval.submission_intents import schedule_submission_intent_retry from app.domains.domain_registration.service import DomainRegistrationService from app.domains.sso.service import SsoGrantService from app.identity.consumers.request_service import ConsumerRequestService from app.identity.person_models import IdentityPerson CASES = ( ("sso", "sso_grant_request", SsoGrantService), ("domain", "domain_registration_request", DomainRegistrationService), ("consumer", "identity_consumer_request", ConsumerRequestService), ) async def _person(db: AsyncSession, label: str) -> IdentityPerson: person = IdentityPerson( id=uuid.uuid4(), source_system="successfactors", hris_person_id=f"sf-{uuid.uuid4().hex[:8]}", display_name=f"Compensation fairness {label}", work_email=f"comp-fair-{label}-{uuid.uuid4().hex[:6]}@test.com", employment_status="active", ) db.add(person) await db.commit() return person def _patch_lock(monkeypatch) -> None: @asynccontextmanager async def _lock(*args, **kwargs): yield "compensation-fairness-test-lock" monkeypatch.setattr(distributed_lock_module, "distributed_lock", _lock) async def _add_intent( db: AsyncSession, *, scope: str, applicant_id: uuid.UUID, key: str, created_at: datetime, next_attempt_at: datetime | None = None, ) -> uuid.UUID: intent_id = uuid.uuid4() db.add(ApprovalSubmissionIntent( id=intent_id, scope=scope, applicant_id=applicant_id, idempotency_key=key, payload_fingerprint="f" * 64, provider_request_uuid=str(uuid.uuid4()), state="compensation_pending", payload={"provider": {}, "finalization": {}}, external_id=f"instance-{intent_id}", attempt_count=1, created_at=created_at, updated_at=created_at, next_attempt_at=next_attempt_at, )) await db.commit() return intent_id @pytest.mark.parametrize(("kind", "scope", "service_class"), CASES) @pytest.mark.asyncio async def test_compensation_batch_plus_one_progresses_after_failed_prefix( kind: str, scope: str, service_class, db_session: AsyncSession, monkeypatch, ) -> None: _patch_lock(monkeypatch) person = await _person(db_session, f"batch-{kind}") now = datetime.now(UTC) - timedelta(minutes=10) intent_ids = [ await _add_intent( db_session, scope=scope, applicant_id=person.id, key=f"{kind}-batch-{offset}", created_at=now + timedelta(seconds=offset), ) for offset in range(3) ] poison_ids = set(intent_ids[:2]) seen: list[uuid.UUID] = [] service = service_class(db_session) async def _finalize(intent_id, *, recovered): assert recovered is True seen.append(intent_id) intent = await db_session.get(ApprovalSubmissionIntent, intent_id) assert intent is not None if intent_id in poison_ids: schedule_submission_intent_retry( intent, RuntimeError("withdraw remains unavailable"), ) else: intent.state = "compensated" intent.completed_at = datetime.now(UTC) intent.next_attempt_at = None await db_session.commit() return SimpleNamespace(status="cancelled") monkeypatch.setattr(service, "_finalize_submission_intent", _finalize) first = await service.reconcile_submission_intents( min_age_minutes=0, batch_limit=2, ) assert first["checked"] == 2 assert first["still_unknown"] == 2 assert seen == intent_ids[:2] for intent_id in intent_ids[:2]: intent = await db_session.get(ApprovalSubmissionIntent, intent_id) assert intent is not None and intent.next_attempt_at is not None second = await service.reconcile_submission_intents( min_age_minutes=0, batch_limit=2, ) assert second["checked"] == 1 assert second["compensated"] == 1 assert seen == intent_ids @pytest.mark.parametrize(("kind", "scope", "service_class"), CASES) @pytest.mark.asyncio async def test_due_compensation_precedes_continuous_new_arrivals( kind: str, scope: str, service_class, db_session: AsyncSession, monkeypatch, ) -> None: _patch_lock(monkeypatch) person = await _person(db_session, f"stream-{kind}") now = datetime.now(UTC) due_id = await _add_intent( db_session, scope=scope, applicant_id=person.id, key=f"{kind}-due", created_at=now - timedelta(hours=2), next_attempt_at=now - timedelta(minutes=1), ) for offset in range(4): await _add_intent( db_session, scope=scope, applicant_id=person.id, key=f"{kind}-new-{offset}", created_at=now + timedelta(seconds=offset), ) seen: list[uuid.UUID] = [] service = service_class(db_session) async def _finalize(intent_id, *, recovered): assert recovered is True seen.append(intent_id) intent = await db_session.get(ApprovalSubmissionIntent, intent_id) assert intent is not None intent.state = "compensated" intent.completed_at = datetime.now(UTC) intent.next_attempt_at = None await db_session.commit() return SimpleNamespace(status="cancelled") monkeypatch.setattr(service, "_finalize_submission_intent", _finalize) summary = await service.reconcile_submission_intents( min_age_minutes=0, batch_limit=1, ) assert summary["checked"] == 1 assert summary["compensated"] == 1 assert seen == [due_id] terminal = await db_session.scalar(select(ApprovalSubmissionIntent).where( ApprovalSubmissionIntent.id == due_id, )) assert terminal is not None and terminal.state == "compensated"