from __future__ import annotations import unittest from datetime import date from monthly_reports.contracts import ErrorCode from monthly_reports.repository import DerivedMonthlyRequest from monthly_reports.service import RunResult from monthly_reports.worker import ( CLAIM_EVENT_SQL, FAIL_EVENT_SQL, PUBLISH_EVENT_SQL, MonthlyOutboxWorker, OutboxEvent, ) class FakeOutbox: def __init__(self, event: OutboxEvent | None) -> None: self.event = event self.published: list[tuple[int, int]] = [] self.failed: list[tuple[OutboxEvent, str, bool]] = [] def claim_next(self): value, self.event = self.event, None return value def mark_published(self, event_id: int, report_id: int) -> None: self.published.append((event_id, report_id)) def mark_failed(self, event: OutboxEvent, code: str, *, retryable: bool) -> str: self.failed.append((event, code, retryable)) return "pending" if retryable else "dead" class FakeRequests: def __init__(self, derived: DerivedMonthlyRequest | None) -> None: self.derived = derived self.calls: list[int] = [] def derive_monthly_request(self, daily_version_id: int): self.calls.append(daily_version_id) return self.derived class FakeService: def __init__(self, result_factory) -> None: self.result_factory = result_factory self.requests = [] def run(self, request): self.requests.append(request) return self.result_factory(request) def event(payload=None, attempts: int = 1) -> OutboxEvent: return OutboxEvent( event_id=9, event_key="processing-run:27:accepted", payload=payload or {"daily_version_id": 4, "business_date": "1999-01-01"}, publish_attempts=attempts, ) class MonthlyReportsWorkerTests(unittest.TestCase): def test_worker_uses_derived_arrival_scope_and_publishes_only_real_report(self): outbox = FakeOutbox(event()) requests = FakeRequests(DerivedMonthlyRequest(2026, 7, date(2026, 7, 27))) service = FakeService( lambda request: RunResult( request=request, status="success", report_version_id=44, version_no=1, artifact={"sha256": "a" * 64}, ) ) outcome = MonthlyOutboxWorker(outbox, requests, service).process_next() self.assertEqual(outcome.status, "published") self.assertEqual(outcome.report_id, 44) self.assertEqual(requests.calls, [4]) self.assertEqual(service.requests[0].as_of_date, date(2026, 7, 27)) self.assertEqual(outbox.published, [(9, 44)]) self.assertEqual(outbox.failed, []) def test_empty_arrival_scope_is_dead_lettered_without_generation(self): outbox = FakeOutbox(event()) service = FakeService(lambda request: None) outcome = MonthlyOutboxWorker( outbox, FakeRequests(None), service, ).process_next() self.assertEqual(outcome.status, "dead") self.assertEqual(outcome.error_code, "MONTHLY_WORKER_ARRIVAL_SCOPE_EMPTY") self.assertEqual(service.requests, []) self.assertFalse(outbox.failed[0][2]) def test_retryable_report_failure_returns_event_to_pending(self): outbox = FakeOutbox(event()) requests = FakeRequests(DerivedMonthlyRequest(2026, 7, date(2026, 7, 27))) service = FakeService( lambda request: RunResult( request=request, status="failed", error_code=ErrorCode.PUBLISH_FAILED, error_stage="publish", ) ) outcome = MonthlyOutboxWorker(outbox, requests, service).process_next() self.assertEqual(outcome.status, "pending") self.assertEqual(outcome.error_code, ErrorCode.PUBLISH_FAILED) self.assertTrue(outbox.failed[0][2]) self.assertEqual(outbox.published, []) def test_claim_and_ack_sql_are_leased_locked_and_downloadable(self): self.assertIn("FOR UPDATE SKIP LOCKED", CLAIM_EVENT_SQL) self.assertIn("publish_status IN ('pending', 'publishing')", CLAIM_EVENT_SQL) self.assertIn("available_at <= now()", CLAIM_EVENT_SQL) self.assertIn("reporting.monthly_runs", PUBLISH_EVENT_SQL) self.assertIn("monthly_xlsx", PUBLISH_EVENT_SQL) self.assertIn("result_json", PUBLISH_EVENT_SQL) self.assertIn("publish_status = %s", FAIL_EVENT_SQL) if __name__ == "__main__": unittest.main()