from unittest import mock from payoneer_status_sync.service import run BASE_EVENT = { "dp_payment_ids": [], "abacus_statement_period_ids": [], "limit": 500, "dry_run": True, "request_timeout_seconds": 10, } PAYMENT = { "dp_payment_id": 1, "payoneer_program_id": "prog1", "payoneer_payment_id": "pay1", "payoneer_payment_status": None, } TRANSFERRED_RESULT = {"status": "Transferred", "reason_code": None, "reason_description": None} @mock.patch("payoneer_status_sync.service.payoneer") @mock.patch("payoneer_status_sync.service.db") @mock.patch("payoneer_status_sync.service.config") def test_run_dry_run_returns_stats_without_updating_db( mock_config: mock.MagicMock, mock_db: mock.MagicMock, mock_payoneer: mock.MagicMock, ) -> None: mock_config.get_payoneer_auth_token.return_value = "token" mock_config.PAYONEER_API_URL = "https://api.example.com" mock_db.fetch_target_payments.return_value = [PAYMENT] mock_payoneer.fetch_payout_status.return_value = TRANSFERRED_RESULT result = run(BASE_EVENT) assert result["dry_run"] is True assert result["scanned"] == 1 assert result["changed"] == 1 assert result["unchanged"] == 0 assert result["pending"] == 0 assert result["failed"] == 0 assert len(result["updates"]) == 1 assert result["updates"][0]["payoneer_status"] == "complete" mock_db.update_payoneer_status_bulk.assert_not_called() @mock.patch("payoneer_status_sync.service.payoneer") @mock.patch("payoneer_status_sync.service.db") @mock.patch("payoneer_status_sync.service.config") def test_run_applies_updates_when_not_dry_run( mock_config: mock.MagicMock, mock_db: mock.MagicMock, mock_payoneer: mock.MagicMock, ) -> None: mock_config.get_payoneer_auth_token.return_value = "token" mock_config.PAYONEER_API_URL = "https://api.example.com" mock_db.fetch_target_payments.return_value = [PAYMENT] mock_payoneer.fetch_payout_status.return_value = TRANSFERRED_RESULT mock_db.update_payoneer_status_bulk.return_value = 1 result = run({**BASE_EVENT, "dry_run": False}) mock_db.update_payoneer_status_bulk.assert_called_once_with([(1, "complete", None)]) assert result["changed"] == 1 assert result["dry_run"] is False @mock.patch("payoneer_status_sync.service.payoneer") @mock.patch("payoneer_status_sync.service.db") @mock.patch("payoneer_status_sync.service.config") def test_run_persists_cancellation_reason( mock_config: mock.MagicMock, mock_db: mock.MagicMock, mock_payoneer: mock.MagicMock, ) -> None: mock_config.get_payoneer_auth_token.return_value = "token" mock_config.PAYONEER_API_URL = "https://api.example.com" mock_db.fetch_target_payments.return_value = [PAYMENT] mock_payoneer.fetch_payout_status.return_value = { "status": "Cancelled", "reason_code": "30013", "reason_description": "Bank details - Invalid Branch Code", } mock_db.update_payoneer_status_bulk.return_value = 1 result = run({**BASE_EVENT, "dry_run": False}) mock_db.update_payoneer_status_bulk.assert_called_once_with( [(1, "rejected", "Bank details - Invalid Branch Code")] ) assert result["updates"][0]["reason"] == "Bank details - Invalid Branch Code" @mock.patch("payoneer_status_sync.service.payoneer") @mock.patch("payoneer_status_sync.service.db") @mock.patch("payoneer_status_sync.service.config") def test_run_records_failure_on_payoneer_error( mock_config: mock.MagicMock, mock_db: mock.MagicMock, mock_payoneer: mock.MagicMock, ) -> None: mock_config.get_payoneer_auth_token.return_value = "token" mock_config.PAYONEER_API_URL = "https://api.example.com" mock_db.fetch_target_payments.return_value = [PAYMENT] mock_payoneer.fetch_payout_status.side_effect = ValueError("timeout") result = run(BASE_EVENT) assert result["scanned"] == 1 assert result["failed"] == 1 assert result["changed"] == 0 assert result["failures"][0]["dp_payment_id"] == 1 assert "timeout" in result["failures"][0]["reason"] @mock.patch("payoneer_status_sync.service.payoneer") @mock.patch("payoneer_status_sync.service.db") @mock.patch("payoneer_status_sync.service.config") def test_run_marks_payment_as_unchanged_when_status_already_matches( mock_config: mock.MagicMock, mock_db: mock.MagicMock, mock_payoneer: mock.MagicMock, ) -> None: mock_config.get_payoneer_auth_token.return_value = "token" mock_config.PAYONEER_API_URL = "https://api.example.com" already_synced = {**PAYMENT, "payoneer_payment_status": "complete"} mock_db.fetch_target_payments.return_value = [already_synced] mock_payoneer.fetch_payout_status.return_value = TRANSFERRED_RESULT result = run(BASE_EVENT) assert result["unchanged"] == 1 assert result["changed"] == 0 assert result["updates"] == [] @mock.patch("payoneer_status_sync.service.payoneer") @mock.patch("payoneer_status_sync.service.db") @mock.patch("payoneer_status_sync.service.config") def test_run_returns_empty_response_when_no_payments( mock_config: mock.MagicMock, mock_db: mock.MagicMock, mock_payoneer: mock.MagicMock, ) -> None: mock_config.get_payoneer_auth_token.return_value = "token" mock_config.PAYONEER_API_URL = "https://api.example.com" mock_db.fetch_target_payments.return_value = [] result = run(BASE_EVENT) assert result["scanned"] == 0 assert result["changed"] == 0 assert result["unchanged"] == 0 assert result["pending"] == 0 assert result["failed"] == 0 assert result["updates"] == [] assert "pending_payments" not in result assert "failures" not in result mock_payoneer.fetch_payout_status.assert_not_called() @mock.patch("payoneer_status_sync.service.payoneer") @mock.patch("payoneer_status_sync.service.db") @mock.patch("payoneer_status_sync.service.config") def test_run_records_failure_on_unknown_payoneer_status( mock_config: mock.MagicMock, mock_db: mock.MagicMock, mock_payoneer: mock.MagicMock, ) -> None: mock_config.get_payoneer_auth_token.return_value = "token" mock_config.PAYONEER_API_URL = "https://api.example.com" mock_db.fetch_target_payments.return_value = [PAYMENT] mock_payoneer.fetch_payout_status.return_value = { "status": "UnknownStatus", "reason_code": None, "reason_description": None, } result = run(BASE_EVENT) assert result["scanned"] == 1 assert result["failed"] == 1 assert result["changed"] == 0 assert result["failures"][0]["dp_payment_id"] == 1 assert "UnknownStatus" in result["failures"][0]["reason"] @mock.patch("payoneer_status_sync.service.payoneer") @mock.patch("payoneer_status_sync.service.db") @mock.patch("payoneer_status_sync.service.config") def test_run_tracks_pending_payments_without_updating_db( mock_config: mock.MagicMock, mock_db: mock.MagicMock, mock_payoneer: mock.MagicMock, ) -> None: mock_config.get_payoneer_auth_token.return_value = "token" mock_config.PAYONEER_API_URL = "https://api.example.com" mock_db.fetch_target_payments.return_value = [PAYMENT] mock_payoneer.fetch_payout_status.return_value = { "status": "Pending", "reason_code": "13", "reason_description": "Bank cannot process", } result = run(BASE_EVENT) assert result["scanned"] == 1 assert result["pending"] == 1 assert result["changed"] == 0 assert result["unchanged"] == 0 assert result["failed"] == 0 assert result["pending_payments"][0]["dp_payment_id"] == 1 assert result["pending_payments"][0]["reason_code"] == "13" assert result["pending_payments"][0]["reason_description"] == "Bank cannot process" mock_db.update_payoneer_status_bulk.assert_not_called()