"""Tests for abacus statement period logic.""" from datetime import datetime from decimal import Decimal from http import HTTPStatus from types import SimpleNamespace from unittest.mock import ANY, MagicMock, patch import pytest from collaborator.constants.abacus_statement_period import PAYONEER_PAYMENT_DESCRIPTION from collaborator.logic import abacus_statement_period from collaborator.models.rds.dp_payment import DpPayment from collaborator.schemas.dp_payment import DpPaymentSchema from collaborator.utils.error import OwsError from tests.testutils.seed.dp_payment_seed import dp_payment_seed_data @patch("collaborator.logic.abacus_statement_period.AbacusStatementPeriodPersister") def test_abacus_statement_period_dataloader( AbacusStatementPeriodPersister_mock, ): """Test abacus statement period dataloader.""" abacus_statement_period_ids = [1, 2, 3] mock_period_1 = MagicMock() mock_period_1.abacus_statement_period_id = 1 mock_period_1._mapping = { "abacus_statement_period_id": 1, "report_run_id": 101, "payment_fee_transactions_count": 10, "payment_fee_transactions_sum": 100.0, "payment_approvals_count": 5, "payment_approvals_sum": 500.0, } mock_period_2 = MagicMock() mock_period_2.abacus_statement_period_id = 2 mock_period_2._mapping = { "abacus_statement_period_id": 2, "report_run_id": 102, "payment_fee_transactions_count": 20, "payment_fee_transactions_sum": 200.0, "payment_approvals_count": None, "payment_approvals_sum": None, } mock_period_3 = MagicMock() mock_period_3.abacus_statement_period_id = 3 mock_period_3._mapping = { "abacus_statement_period_id": 3, "report_run_id": 103, "payment_fee_transactions_count": 30, "payment_fee_transactions_sum": 300.0, "payment_approvals_count": 3, "payment_approvals_sum": 750.0, } AbacusStatementPeriodPersister_mock.get_abacus_statement_periods.return_value = [ mock_period_1, mock_period_2, mock_period_3, ] result = abacus_statement_period.abacus_statement_period_dataloader( abacus_statement_period_ids ) AbacusStatementPeriodPersister_mock.get_abacus_statement_periods.assert_called_once_with( abacus_statement_period_ids ) expected_result = { 1: { "abacus_statement_period_id": 1, "report_run_id": 101, "payment_fee_transactions_count": 10, "payment_fee_transactions_sum": 100.0, "payment_approvals_count": 5, "payment_approvals_sum": 500.0, }, 2: { "abacus_statement_period_id": 2, "report_run_id": 102, "payment_fee_transactions_count": 20, "payment_fee_transactions_sum": 200.0, "payment_approvals_count": None, "payment_approvals_sum": None, }, 3: { "abacus_statement_period_id": 3, "report_run_id": 103, "payment_fee_transactions_count": 30, "payment_fee_transactions_sum": 300.0, "payment_approvals_count": 3, "payment_approvals_sum": 750.0, }, } assert result == expected_result @patch("collaborator.logic.abacus_statement_period.AbacusStatementPeriodPersister") def test_abacus_statement_period_dataloader_empty( AbacusStatementPeriodPersister_mock, ): """Test abacus statement period dataloader with no results.""" abacus_statement_period_ids = [999, 1000] AbacusStatementPeriodPersister_mock.get_abacus_statement_periods.return_value = [] result = abacus_statement_period.abacus_statement_period_dataloader( abacus_statement_period_ids ) AbacusStatementPeriodPersister_mock.get_abacus_statement_periods.assert_called_once_with( abacus_statement_period_ids ) assert result == {} @patch("collaborator.logic.abacus_statement_period.AbacusStatementPeriodPersisterSF") def test_direct_payment_balances( AbacusStatementPeriodPersister_mock, ): """Test abacus statement period dataloader.""" abacus_statement_period_id = 1 AbacusStatementPeriodPersister_mock.get_direct_payment_balances.return_value = [ SimpleNamespace( agreement_type="Orchard", account_name="Orchard Account", account_id=1234, currency_code="USD", current_statement_period_name="Statement Period", payoneer_program_id=1234, payoneer_program_name="Orchard Program", payoneer_client_reference_id="e5f6a7b8-c9d0-4ef1-abcd-456789012345", balance_after_tax=Decimal(56.78), collaborator_name="Orchard Collab", collaborator_id=1234, ) ] result = abacus_statement_period.direct_payment_balances(abacus_statement_period_id) AbacusStatementPeriodPersister_mock.get_direct_payment_balances.assert_called_once_with( abacus_statement_period_id ) assert result == { "currency_agnostic_total_amount": 56.78, "total_count": 1, "balances": [ { "agreement_type": "Orchard", "account_name": "Orchard Account", "account_id": 1234, "currency_code": "USD", "current_balance": 56.78, "current_statement_period_name": "Statement Period", "payoneer_program_id": 1234, "payoneer_program_name": "Orchard Program", "payoneer_client_reference_id": "e5f6a7b8-c9d0-4ef1-abcd-456789012345", "balance_after_tax": 56.78, "collaborator_name": "Orchard Collab", "collaborator_id": 1234, } ], } @patch("collaborator.logic.abacus_statement_period.AbacusStatementPeriodPersisterSF") @patch("collaborator.logic.abacus_statement_period.DpPaymentPersister") @patch("collaborator.logic.dp_payment.DpPaymentPersister") def test_replace_dp_payments(mock_dp_payment_logic, mock_dp_payment, mock_abacus_sp): """Test that DP payments are replaced correctly for a statement period.""" abacus_statement_period_id = 1 user = SimpleNamespace(id="test-user") mock_balance = SimpleNamespace( current_statement_period_id=abacus_statement_period_id, current_statement_period_name="Jan 2026", collaborator_id=1, collaborator_name="John Doe", balance_after_tax=1500.00, currency_code="USD", payee_id=123, payoneer_program_id=10000, payoneer_program_name="Standard Program", payoneer_client_reference_id="f6a7b8c9-d0e1-4fab-bcde-567890123456", account_id=12345, account_name="Acme Music", agreement_type="Orchard", ) mock_abacus_sp.get_direct_payment_balances.return_value = [mock_balance] expected_payoneer_payment_id = ( f"split:{abacus_statement_period_id}:{mock_balance.collaborator_id}" ) mock_payment = DpPayment( dp_payment_id=1, created_date=datetime(2026, 9, 8, 7, 6, 5), abacus_statement_period_id=mock_balance.current_statement_period_id, abacus_statement_period_name=mock_balance.current_statement_period_name, collaborator_id=mock_balance.collaborator_id, collaborator_name=mock_balance.collaborator_name, amount=mock_balance.balance_after_tax, currency=mock_balance.currency_code, payee_id=mock_balance.payee_id, payoneer_program_id=mock_balance.payoneer_program_id, payoneer_program_name=mock_balance.payoneer_program_name, payoneer_client_reference_id=mock_balance.payoneer_client_reference_id, account_id=mock_balance.account_id, account_name=mock_balance.account_name, approved_date=None, collaborator_transaction_id=None, agreement_type="Orchard", payoneer_payment_id=expected_payoneer_payment_id, ) mock_dp_payment_logic.get_by_filters.return_value = [] mock_dp_payment.replace_by_abacus_statement_period_id.return_value = [mock_payment] result_payments, result_amount = abacus_statement_period.replace_dp_payments( abacus_statement_period_id=abacus_statement_period_id, user=user, ) assert result_amount == mock_balance.balance_after_tax assert [payment.dump() for payment in result_payments] == [ DpPaymentSchema.parse(mock_payment).dump() ] mock_dp_payment.replace_by_abacus_statement_period_id.assert_called_once_with( abacus_statement_period_id=abacus_statement_period_id, payments=[ { "abacus_statement_period_id": abacus_statement_period_id, "abacus_statement_period_name": mock_balance.current_statement_period_name, "collaborator_id": mock_balance.collaborator_id, "collaborator_name": mock_balance.collaborator_name, "amount": mock_balance.balance_after_tax, "currency": mock_balance.currency_code, "payoneer_program_id": mock_balance.payoneer_program_id, "payoneer_program_name": mock_balance.payoneer_program_name, "payoneer_client_reference_id": mock_balance.payoneer_client_reference_id, "created_date": ANY, "account_id": mock_balance.account_id, "account_name": mock_balance.account_name, "payee_id": mock_balance.payee_id, "agreement_type": mock_balance.agreement_type, "payoneer_payment_id": expected_payoneer_payment_id, "created_by": user.id, } ], ) @patch( "collaborator.models.rds.dp_payment_persister.DpPaymentPersister.update_payoneer_status_bulk" ) @patch("collaborator.logic.abacus_statement_period.ows_payee.create_mass_payouts") @patch("collaborator.models.rds.dp_payment_persister.DpPaymentPersister.get_by_filters") def test_submit_dp_payments_success( mock_get, mock_create_mass_payouts, mock_bulk_update ): """Test that approved DP payments are submitted grouped by program_id.""" user = SimpleNamespace(id="test-user") seed = dict(dp_payment_seed_data[0]) seed["approved_date"] = datetime(2026, 11, 9, 10, 0, 0) seed["payoneer_payment_status"] = None approved_payment = DpPayment(**seed) mock_get.return_value = [(approved_payment, 1000.0)] mock_create_mass_payouts.return_value = {} result = abacus_statement_period.submit_dp_payments( abacus_statement_period_id=1, user=user, ) program_id = int(approved_payment.payoneer_program_id) mock_create_mass_payouts.assert_called_once_with( program_id, [ { "client_reference_id": approved_payment.payoneer_payment_id, "payee_id": approved_payment.payoneer_client_reference_id, "amount": float(approved_payment.amount), "currency": approved_payment.currency, "description": PAYONEER_PAYMENT_DESCRIPTION, } ], ) mock_bulk_update.assert_called_once_with( [approved_payment.dp_payment_id], ANY, event_type=None, reason=None, user=user, ) assert result == {"submitted_count": 1} @patch( "collaborator.models.rds.dp_payment_persister.DpPaymentPersister.update_payoneer_status_bulk" ) @patch("collaborator.logic.abacus_statement_period.ows_payee.create_mass_payouts") @patch("collaborator.models.rds.dp_payment_persister.DpPaymentPersister.get_by_filters") def test_submit_dp_payments_multiple_programs( mock_get, mock_create_mass_payouts, mock_bulk_update ): """Test that payments across multiple programs each trigger a separate call.""" user = SimpleNamespace(id="test-user") seed_a = dict(dp_payment_seed_data[0]) seed_a["approved_date"] = datetime(2026, 11, 9, 10, 0, 0) seed_a["payoneer_program_id"] = 100000 seed_a["payoneer_payment_status"] = None payment_a = DpPayment(**seed_a) seed_b = dict(dp_payment_seed_data[1]) seed_b["approved_date"] = datetime(2026, 11, 9, 10, 0, 0) seed_b["payoneer_program_id"] = 200000 payment_b = DpPayment(**seed_b) mock_get.return_value = [(payment_a, 1000.0), (payment_b, 2000.0)] mock_create_mass_payouts.return_value = {} result = abacus_statement_period.submit_dp_payments( abacus_statement_period_id=1, user=user, ) assert mock_create_mass_payouts.call_count == 2 assert result["submitted_count"] == 2 @patch("collaborator.models.rds.dp_payment_persister.DpPaymentPersister.get_by_filters") def test_submit_dp_payments_no_approved_raises_422(mock_get): """Test that a 422 OwsError is raised when no payments are approved.""" user = SimpleNamespace(id="test-user") mock_get.return_value = [(DpPayment(**dp_payment_seed_data[0]), 1500.0)] with pytest.raises(OwsError) as exc_info: abacus_statement_period.submit_dp_payments( abacus_statement_period_id=1, user=user, ) assert exc_info.value.status == HTTPStatus.UNPROCESSABLE_ENTITY @patch( "collaborator.models.rds.dp_payment_persister.DpPaymentPersister.update_payoneer_status_bulk" ) @patch("collaborator.logic.abacus_statement_period.ows_payee.create_mass_payouts") @patch("collaborator.models.rds.dp_payment_persister.DpPaymentPersister.get_by_filters") def test_submit_dp_payments_skips_unapproved( mock_get, mock_create_mass_payouts, mock_bulk_update ): """Test that unapproved payments are excluded from the submission.""" user = SimpleNamespace(id="test-user") seed_approved = dict(dp_payment_seed_data[0]) seed_approved["approved_date"] = datetime(2026, 11, 9, 10, 0, 0) seed_approved["payoneer_payment_status"] = None approved_payment = DpPayment(**seed_approved) unapproved_payment = DpPayment(**dp_payment_seed_data[1]) mock_get.return_value = [ (approved_payment, 1000.0), (unapproved_payment, 2750.5), ] mock_create_mass_payouts.return_value = {} result = abacus_statement_period.submit_dp_payments( abacus_statement_period_id=1, user=user, ) assert result["submitted_count"] == 1 mock_create_mass_payouts.assert_called_once() @patch( "collaborator.models.rds.dp_payment_persister.DpPaymentPersister.approve_by_abacus_statement_period_id" ) def test_approve_dp_payments(mock_approve): """Test that DP payments are approved for a statement period.""" abacus_statement_period_id = 1 user = SimpleNamespace(id="test-user") mock_payment = DpPayment(**dp_payment_seed_data[0]) mock_approve.return_value = [mock_payment] result = abacus_statement_period.approve_dp_payments( abacus_statement_period_id=abacus_statement_period_id, user=user, ) mock_approve.assert_called_once_with( abacus_statement_period_id=abacus_statement_period_id, user=user, ) assert [payment.dump() for payment in result] == [ DpPaymentSchema.parse(mock_payment).dump() ]