"""Migration 179 — UNIQUE (process_id, reminder_kind) on ``offboarding_reminder_log``. The application has always treated the pair as unique (cron does a SELECT-then-add dedup, and the single-flight cron lock closes the in-process race). Migration 179 promotes that contract to a DB-level constraint so any future writer outside the cron (manual admin re-send, parallel handover trigger, backfill script) cannot silently produce duplicates. """ from __future__ import annotations import uuid from datetime import UTC, date, datetime, timedelta import pytest from sqlalchemy.exc import IntegrityError from app.domains.offboarding.models import ( PROC_SCANNING, OffboardingProcess, OffboardingReminderLog, ) from app.identity.person_models import IdentityPerson @pytest.mark.asyncio async def test_reminder_log_duplicate_pair_raises_integrity_error( db_session, ): person = IdentityPerson( id=uuid.uuid4(), source_system="test", hris_person_id=f"P{uuid.uuid4().hex[:8]}", employee_no=f"E{uuid.uuid4().hex[:8]}", work_email=f"u{uuid.uuid4().hex[:8]}@x.test", display_name="m179 dup tester", employment_status="leaver_pending", ) db_session.add(person) await db_session.flush() proc = OffboardingProcess( id=uuid.uuid4(), person_id=person.id, leaver_date=date.today() + timedelta(days=7), status=PROC_SCANNING, ready_at=None, sso_suspended_at=None, completed_at=None, ) db_session.add(proc) await db_session.flush() # First row — accepted. db_session.add(OffboardingReminderLog( process_id=proc.id, reminder_kind="reminder_7d", sent_at=datetime.now(UTC), channels=["feishu"], detail={"first": True}, )) await db_session.flush() # Second row with the same (process_id, reminder_kind) — must fail # at flush time on the UNIQUE constraint. The previous schema let # this silently land as a duplicate row. db_session.add(OffboardingReminderLog( process_id=proc.id, reminder_kind="reminder_7d", sent_at=datetime.now(UTC), channels=["feishu"], detail={"second": True}, )) with pytest.raises(IntegrityError): await db_session.flush() await db_session.rollback() @pytest.mark.asyncio async def test_reminder_log_different_kinds_same_process_allowed( db_session, ): """The constraint must NOT block legitimate distinct-kind rows for the same process — the leaver lifecycle fires T-7, T-3, T-1, escalation, DNS handover, etc. all keyed on the same process_id. """ person = IdentityPerson( id=uuid.uuid4(), source_system="test", hris_person_id=f"P{uuid.uuid4().hex[:8]}", employee_no=f"E{uuid.uuid4().hex[:8]}", work_email=f"u{uuid.uuid4().hex[:8]}@x.test", display_name="m179 multi-kind tester", employment_status="leaver_pending", ) db_session.add(person) await db_session.flush() proc = OffboardingProcess( id=uuid.uuid4(), person_id=person.id, leaver_date=date.today() + timedelta(days=7), status=PROC_SCANNING, ready_at=None, sso_suspended_at=None, completed_at=None, ) db_session.add(proc) await db_session.flush() for kind in ("reminder_7d", "reminder_3d", "reminder_1d", "escalation"): db_session.add(OffboardingReminderLog( process_id=proc.id, reminder_kind=kind, sent_at=datetime.now(UTC), channels=["feishu"], detail={"kind": kind}, )) await db_session.flush() # No raise — all four rows persisted.