from payment.api import db """Payment Allocation functional tests.""" from decimal import Decimal import pytest from payment.config import Config from payment.constants.constants import ( PAYMENT_ALLOCATION_LEDGER_STATUSES, PAYMENT_ALLOCATION_STATUSES, ) from tests.utils.factories import PaymentAllocationFlowthroughFactory def test_get_flowthrough_allocations_bulk_success( fixture_client, mock_contracts, mock_jwt_auth ): """Test bulk retrieval of flowthrough allocations.""" mock_jwt_auth.identity = Config.PAYMENT_ALLOCATION_IDENTITIES[0] allocation1 = PaymentAllocationFlowthroughFactory.create( contract_id=1, amount_to_payment=Decimal('100.00') ) allocation2 = PaymentAllocationFlowthroughFactory.create( contract_id=1, amount_to_payment=Decimal('200.00') ) res = fixture_client.post( '/payment-allocations/flowthrough/bulk?limit=10&offset=0', json={'contract_ids': [1]}, headers=mock_jwt_auth.headers, ) assert res.status_code == 200 assert res.json['total_count'] == 2 assert len(res.json['items']) == 2 def test_get_flowthrough_allocations_bulk_with_pagination( fixture_client, mock_contracts, mock_jwt_auth ): """Test bulk endpoint with pagination parameters.""" mock_jwt_auth.identity = Config.PAYMENT_ALLOCATION_IDENTITIES[0] # Create 5 test records allocations = [ PaymentAllocationFlowthroughFactory.create(contract_id=1) for _ in range(5) ] # Test first page res = fixture_client.post( '/payment-allocations/flowthrough/bulk?limit=2&offset=0', json={'contract_ids': [1]}, headers=mock_jwt_auth.headers, ) assert res.status_code == 200 assert res.json['total_count'] == 5 assert len(res.json['items']) == 2 # Test second page res = fixture_client.post( '/payment-allocations/flowthrough/bulk?limit=2&offset=2', json={'contract_ids': [1]}, headers=mock_jwt_auth.headers, ) assert res.status_code == 200 assert res.json['total_count'] == 5 assert len(res.json['items']) == 2 # Test last page res = fixture_client.post( '/payment-allocations/flowthrough/bulk?limit=2&offset=4', json={'contract_ids': [1]}, headers=mock_jwt_auth.headers, ) assert res.status_code == 200 assert res.json['total_count'] == 5 assert len(res.json['items']) == 1 def test_get_flowthrough_allocations_bulk_default_pagination( fixture_client, mock_contracts, mock_jwt_auth ): """Test bulk endpoint with default pagination parameters.""" mock_jwt_auth.identity = Config.PAYMENT_ALLOCATION_IDENTITIES[0] # Create 3 test records for _ in range(3): PaymentAllocationFlowthroughFactory.create(contract_id=1) # No pagination params should use defaults res = fixture_client.post( '/payment-allocations/flowthrough/bulk', json={'contract_ids': [1]}, headers=mock_jwt_auth.headers, ) assert res.status_code == 200 assert res.json['total_count'] == 3 assert len(res.json['items']) == 3 def test_get_flowthrough_allocations_bulk_pagination_beyond_results( fixture_client, mock_contracts, mock_jwt_auth ): """Test bulk endpoint pagination when offset exceeds total count.""" mock_jwt_auth.identity = Config.PAYMENT_ALLOCATION_IDENTITIES[0] PaymentAllocationFlowthroughFactory.create(contract_id=1) PaymentAllocationFlowthroughFactory.create(contract_id=1) res = fixture_client.post( '/payment-allocations/flowthrough/bulk?limit=10&offset=100', json={'contract_ids': [1]}, headers=mock_jwt_auth.headers, ) assert res.status_code == 200 assert res.json['total_count'] == 2 assert len(res.json['items']) == 0 def test_get_flowthrough_allocations_bulk_filter_by_payment_allocation_ids( fixture_client, mock_contracts, mock_jwt_auth ): """Test bulk endpoint filtering by payment_allocation_ids.""" mock_jwt_auth.identity = Config.PAYMENT_ALLOCATION_IDENTITIES[0] allocation1 = PaymentAllocationFlowthroughFactory.create() allocation2 = PaymentAllocationFlowthroughFactory.create() allocation3 = PaymentAllocationFlowthroughFactory.create() res = fixture_client.post( '/payment-allocations/flowthrough/bulk?limit=10&offset=0', json={ 'payment_allocation_ids': [ allocation1.payment_allocation_id, allocation2.payment_allocation_id, ] }, headers=mock_jwt_auth.headers, ) assert res.status_code == 200 assert res.json['total_count'] == 2 assert len(res.json['items']) == 2 returned_ids = [item['payment_allocation_id'] for item in res.json['items']] assert allocation1.payment_allocation_id in returned_ids assert allocation2.payment_allocation_id in returned_ids assert allocation3.payment_allocation_id not in returned_ids def test_get_flowthrough_allocations_bulk_filter_by_contract_ids( fixture_client, mock_contracts, mock_jwt_auth ): """Test bulk endpoint filtering by contract_ids.""" mock_jwt_auth.identity = Config.PAYMENT_ALLOCATION_IDENTITIES[0] allocation1 = PaymentAllocationFlowthroughFactory.create(contract_id=1) allocation2 = PaymentAllocationFlowthroughFactory.create(contract_id=2) PaymentAllocationFlowthroughFactory.create(contract_id=3) res = fixture_client.post( '/payment-allocations/flowthrough/bulk?limit=10&offset=0', json={'contract_ids': [1, 2]}, headers=mock_jwt_auth.headers, ) assert res.status_code == 200 assert res.json['total_count'] == 2 assert len(res.json['items']) == 2 def test_get_flowthrough_allocations_bulk_filter_by_payment_statuses( fixture_client, mock_contracts, mock_jwt_auth ): """Test bulk endpoint filtering by payment_statuses.""" mock_jwt_auth.identity = Config.PAYMENT_ALLOCATION_IDENTITIES[0] allocation1 = PaymentAllocationFlowthroughFactory.create( payment_status=PAYMENT_ALLOCATION_STATUSES.INIT ) allocation2 = PaymentAllocationFlowthroughFactory.create( payment_status=PAYMENT_ALLOCATION_STATUSES.INIT ) allocation3 = PaymentAllocationFlowthroughFactory.create( payment_status=PAYMENT_ALLOCATION_STATUSES.INIT ) # Transition allocation3 to PAID status allocation3.payment_status = PAYMENT_ALLOCATION_STATUSES.ATTACHED_TO_PAYMENT allocation3.payment_status = PAYMENT_ALLOCATION_STATUSES.PAID allocation3.commit_changes() res = fixture_client.post( '/payment-allocations/flowthrough/bulk?limit=10&offset=0', json={'payment_statuses': [PAYMENT_ALLOCATION_STATUSES.INIT]}, headers=mock_jwt_auth.headers, ) assert res.status_code == 200 assert res.json['total_count'] == 2 assert len(res.json['items']) == 2 def test_get_flowthrough_allocations_bulk_filter_by_ledger_statuses( fixture_client, mock_contracts, mock_jwt_auth ): """Test bulk endpoint filtering by ledger_statuses.""" mock_jwt_auth.identity = Config.PAYMENT_ALLOCATION_IDENTITIES[0] allocation1 = PaymentAllocationFlowthroughFactory.create( ledger_status=PAYMENT_ALLOCATION_LEDGER_STATUSES.DEBITED ) allocation2 = PaymentAllocationFlowthroughFactory.create( ledger_status=PAYMENT_ALLOCATION_LEDGER_STATUSES.DEBITED ) PaymentAllocationFlowthroughFactory.create( ledger_status=PAYMENT_ALLOCATION_LEDGER_STATUSES.RETURNED ) res = fixture_client.post( '/payment-allocations/flowthrough/bulk?limit=10&offset=0', json={'ledger_statuses': [PAYMENT_ALLOCATION_LEDGER_STATUSES.DEBITED]}, headers=mock_jwt_auth.headers, ) assert res.status_code == 200 assert res.json['total_count'] == 2 assert len(res.json['items']) == 2 def test_get_flowthrough_allocations_bulk_multiple_filters( fixture_client, mock_contracts, mock_jwt_auth ): """Test bulk endpoint with multiple filters.""" mock_jwt_auth.identity = Config.PAYMENT_ALLOCATION_IDENTITIES[0] allocation1 = PaymentAllocationFlowthroughFactory.create( contract_id=1, payment_status=PAYMENT_ALLOCATION_STATUSES.INIT, ledger_status=PAYMENT_ALLOCATION_LEDGER_STATUSES.DEBITED, ) # Doesn't match payment status PaymentAllocationFlowthroughFactory.create( contract_id=1, payment_status=PAYMENT_ALLOCATION_STATUSES.INIT, ledger_status=PAYMENT_ALLOCATION_LEDGER_STATUSES.DEBITED, ) # Doesn't match contract PaymentAllocationFlowthroughFactory.create( contract_id=2, payment_status=PAYMENT_ALLOCATION_STATUSES.INIT, ledger_status=PAYMENT_ALLOCATION_LEDGER_STATUSES.DEBITED, ) res = fixture_client.post( '/payment-allocations/flowthrough/bulk?limit=10&offset=0', json={ 'contract_ids': [1], 'payment_statuses': [PAYMENT_ALLOCATION_STATUSES.INIT], 'ledger_statuses': [PAYMENT_ALLOCATION_LEDGER_STATUSES.DEBITED], }, headers=mock_jwt_auth.headers, ) assert res.status_code == 200 assert res.json['total_count'] == 2 assert len(res.json['items']) == 2 def test_get_flowthrough_allocations_bulk_pagination_with_filters( fixture_client, mock_contracts, mock_jwt_auth ): """Test bulk endpoint pagination combined with filters.""" mock_jwt_auth.identity = Config.PAYMENT_ALLOCATION_IDENTITIES[0] # Create 10 allocations with contract_id=1 for _ in range(10): PaymentAllocationFlowthroughFactory.create(contract_id=1) # Create 5 with contract_id=2 for _ in range(5): PaymentAllocationFlowthroughFactory.create(contract_id=2) # Get first page of contract_id=1 results res = fixture_client.post( '/payment-allocations/flowthrough/bulk?limit=3&offset=0', json={'contract_ids': [1]}, headers=mock_jwt_auth.headers, ) assert res.status_code == 200 assert res.json['total_count'] == 10 assert len(res.json['items']) == 3 # Get second page res = fixture_client.post( '/payment-allocations/flowthrough/bulk?limit=3&offset=3', json={'contract_ids': [1]}, headers=mock_jwt_auth.headers, ) assert res.status_code == 200 assert res.json['total_count'] == 10 assert len(res.json['items']) == 3 # Get last page res = fixture_client.post( '/payment-allocations/flowthrough/bulk?limit=3&offset=9', json={'contract_ids': [1]}, headers=mock_jwt_auth.headers, ) assert res.status_code == 200 assert res.json['total_count'] == 10 assert len(res.json['items']) == 1 def test_get_flowthrough_allocations_bulk_missing_filter_returns_error( fixture_client, mock_contracts, mock_jwt_auth ): """Test that missing all filters returns validation error.""" mock_jwt_auth.identity = Config.PAYMENT_ALLOCATION_IDENTITIES[0] res = fixture_client.post( '/payment-allocations/flowthrough/bulk?limit=10&offset=0', json={}, headers=mock_jwt_auth.headers, ) assert res.status_code == 400 assert 'message' in res.json def test_get_flowthrough_allocations_bulk_excludes_soft_deleted( fixture_client, mock_contracts, mock_jwt_auth ): """Test that soft-deleted allocations are excluded from results.""" mock_jwt_auth.identity = Config.PAYMENT_ALLOCATION_IDENTITIES[0] allocation1 = PaymentAllocationFlowthroughFactory.create(contract_id=1) allocation2 = PaymentAllocationFlowthroughFactory.create(contract_id=1) # Soft delete allocation2 from payment.models.payment_allocation import PaymentAllocationFlowthrough PaymentAllocationFlowthrough.delete_by_id_or_error( allocation2.payment_allocation_id, soft_delete=True ) res = fixture_client.post( '/payment-allocations/flowthrough/bulk?limit=10&offset=0', json={'contract_ids': [1]}, headers=mock_jwt_auth.headers, ) assert res.status_code == 200 assert res.json['total_count'] == 1 assert len(res.json['items']) == 1 assert ( res.json['items'][0]['payment_allocation_id'] == allocation1.payment_allocation_id ) def test_get_flowthrough_allocations_bulk_empty_result( fixture_client, mock_contracts, mock_jwt_auth ): """Test that empty result is returned when no allocations match filters.""" mock_jwt_auth.identity = Config.PAYMENT_ALLOCATION_IDENTITIES[0] PaymentAllocationFlowthroughFactory.create(contract_id=1) res = fixture_client.post( '/payment-allocations/flowthrough/bulk?limit=10&offset=0', json={'contract_ids': [999]}, headers=mock_jwt_auth.headers, ) assert res.status_code == 200 assert res.json['total_count'] == 0 assert len(res.json['items']) == 0 def test_get_flowthrough_allocations_bulk_response_structure( fixture_client, mock_contracts, mock_jwt_auth ): """Test that response has correct structure with all fields.""" mock_jwt_auth.identity = Config.PAYMENT_ALLOCATION_IDENTITIES[0] allocation = PaymentAllocationFlowthroughFactory.create( contract_id=1, amount_to_payment=Decimal('1000.00'), amount_to_ledger=Decimal('1000.00'), currency_code='USD', description='Test allocation', ) res = fixture_client.post( '/payment-allocations/flowthrough/bulk?limit=10&offset=0', json={'contract_ids': [1]}, headers=mock_jwt_auth.headers, ) assert res.status_code == 200 assert 'total_count' in res.json assert 'items' in res.json assert len(res.json['items']) == 1 item = res.json['items'][0] # Verify key fields are present assert item['payment_allocation_id'] == allocation.payment_allocation_id assert item['contract_id'] == 1 assert item['currency_code'] == 'USD' def test_get_flowthrough_allocations_bulk_large_limit( fixture_client, mock_contracts, mock_jwt_auth ): """Test bulk endpoint with large limit parameter.""" mock_jwt_auth.identity = Config.PAYMENT_ALLOCATION_IDENTITIES[0] # Create 150 allocations for _ in range(150): PaymentAllocationFlowthroughFactory.create(contract_id=1) # Request with large limit (default max is usually 300) res = fixture_client.post( '/payment-allocations/flowthrough/bulk?limit=300&offset=0', json={'contract_ids': [1]}, headers=mock_jwt_auth.headers, ) assert res.status_code == 200 assert res.json['total_count'] == 150 assert len(res.json['items']) == 150 def test_get_flowthrough_allocations_bulk_invalid_limit( fixture_client, mock_contracts, mock_jwt_auth ): """Test bulk endpoint with invalid limit parameter.""" mock_jwt_auth.identity = Config.PAYMENT_ALLOCATION_IDENTITIES[0] PaymentAllocationFlowthroughFactory.create(contract_id=1) # Negative limit should fail validation res = fixture_client.post( '/payment-allocations/flowthrough/bulk?limit=-1&offset=0', json={'contract_ids': [1]}, headers=mock_jwt_auth.headers, ) assert res.status_code == 400 def test_get_flowthrough_allocations_bulk_invalid_offset( fixture_client, mock_contracts, mock_jwt_auth ): """Test bulk endpoint with invalid offset parameter.""" mock_jwt_auth.identity = Config.PAYMENT_ALLOCATION_IDENTITIES[0] PaymentAllocationFlowthroughFactory.create(contract_id=1) # Negative offset should fail validation res = fixture_client.post( '/payment-allocations/flowthrough/bulk?limit=10&offset=-1', json={'contract_ids': [1]}, headers=mock_jwt_auth.headers, ) assert res.status_code == 400 def test_payment_allocation_flowthrough_bulk_update_success( fixture_client, mock_contracts, mock_jwt_auth ): mock_jwt_auth.identity = Config.PAYMENT_ALLOCATION_IDENTITIES[0] a1 = PaymentAllocationFlowthroughFactory.create( payment_status=PAYMENT_ALLOCATION_STATUSES.INIT, ledger_status=PAYMENT_ALLOCATION_LEDGER_STATUSES.DEBITED, payment_status_modified=None, ledger_status_modified=None, ) a2 = PaymentAllocationFlowthroughFactory.create( payment_status=PAYMENT_ALLOCATION_STATUSES.INIT, ledger_status=PAYMENT_ALLOCATION_LEDGER_STATUSES.DEBITED, payment_status_modified=None, ledger_status_modified=None, ) payload = [ { 'payment_allocation_id': a1.payment_allocation_id, 'payment_status': PAYMENT_ALLOCATION_STATUSES.ATTACHED_TO_PAYMENT, }, { 'payment_allocation_id': a2.payment_allocation_id, 'ledger_status': PAYMENT_ALLOCATION_LEDGER_STATUSES.RETURNED, }, ] res = fixture_client.put( '/payment-allocations/flowthrough', json=payload, headers=mock_jwt_auth.headers, ) assert res.status_code == 200 a1_refetched = db.session.get(a1.__class__, a1.payment_allocation_id) a2_refetched = db.session.get(a2.__class__, a2.payment_allocation_id) assert ( a1_refetched.payment_status == PAYMENT_ALLOCATION_STATUSES.ATTACHED_TO_PAYMENT ) assert a1_refetched.payment_status_modified is not None assert a2_refetched.ledger_status == PAYMENT_ALLOCATION_LEDGER_STATUSES.RETURNED assert a2_refetched.ledger_status_modified is not None def test_payment_allocation_flowthrough_bulk_update_missing_id_returns_400( fixture_client, mock_contracts, mock_jwt_auth ): mock_jwt_auth.identity = Config.PAYMENT_ALLOCATION_IDENTITIES[0] payload = [ { 'payment_allocation_id': 999999, 'payment_status': PAYMENT_ALLOCATION_STATUSES.PAID, } ] res = fixture_client.put( '/payment-allocations/flowthrough', json=payload, headers=mock_jwt_auth.headers, ) assert res.status_code == 400 assert res.json == { 'code': 'error', 'message': 'Flowthrough allocations not found for IDs: {999999}.', } def test_payment_allocation_flowthrough_bulk_update_excludes_soft_deleted( fixture_client, faker, mock_contracts, mock_jwt_auth ): mock_jwt_auth.identity = Config.PAYMENT_ALLOCATION_IDENTITIES[0] allocation = PaymentAllocationFlowthroughFactory.create( deleted_at=faker.past_datetime(), payment_status=PAYMENT_ALLOCATION_STATUSES.INIT, ledger_status=PAYMENT_ALLOCATION_LEDGER_STATUSES.DEBITED, ) payload = [ { 'payment_allocation_id': allocation.payment_allocation_id, 'payment_status': PAYMENT_ALLOCATION_STATUSES.PAID, } ] res = fixture_client.put( '/payment-allocations/flowthrough', json=payload, headers=mock_jwt_auth.headers, ) assert res.status_code == 400 assert res.json == { 'code': 'error', 'message': 'Flowthrough allocations not found for IDs: {1}.', } def test_payment_allocation_flowthrough_bulk_update_invalid_transition( fixture_client, mock_contracts, mock_jwt_auth ): mock_jwt_auth.identity = Config.PAYMENT_ALLOCATION_IDENTITIES[0] allocation = PaymentAllocationFlowthroughFactory.create( payment_status=PAYMENT_ALLOCATION_STATUSES.INIT, ledger_status=PAYMENT_ALLOCATION_LEDGER_STATUSES.DEBITED, ) payload = [ { 'payment_allocation_id': allocation.payment_allocation_id, 'payment_status': PAYMENT_ALLOCATION_STATUSES.PAID, } ] res = fixture_client.put( '/payment-allocations/flowthrough', json=payload, headers=mock_jwt_auth.headers, ) assert res.status_code == 400 assert res.json == { 'code': 'error', 'message': 'Invalid payment status transition: init >> paid', } refetched = db.session.get(allocation.__class__, allocation.payment_allocation_id) assert refetched.payment_status == PAYMENT_ALLOCATION_STATUSES.INIT def test_payment_allocation_flowthrough_bulk_update_duplicate_ids_validation( fixture_client, mock_contracts, mock_jwt_auth ): mock_jwt_auth.identity = Config.PAYMENT_ALLOCATION_IDENTITIES[0] allocation = PaymentAllocationFlowthroughFactory.create( payment_status=PAYMENT_ALLOCATION_STATUSES.INIT, ledger_status=PAYMENT_ALLOCATION_LEDGER_STATUSES.DEBITED, ) payload = [ { 'payment_allocation_id': allocation.payment_allocation_id, 'payment_status': PAYMENT_ALLOCATION_STATUSES.PAID, }, { 'payment_allocation_id': allocation.payment_allocation_id, 'ledger_status': PAYMENT_ALLOCATION_LEDGER_STATUSES.RETURNED, }, ] res = fixture_client.put( '/payment-allocations/flowthrough', json=payload, headers=mock_jwt_auth.headers, ) assert res.status_code == 400 assert res.json == { 'code': 'error', 'message': { 'json': {'_schema': ['Duplicate payment_allocation_id in ' 'request.']} }, } def test_payment_allocation_flowthrough_bulk_update_over_limit_validation( fixture_client, mock_jwt_auth ): mock_jwt_auth.identity = Config.PAYMENT_ALLOCATION_IDENTITIES[0] payload = [{'payment_allocation_id': i + 1} for i in range(301)] res = fixture_client.put( '/payment-allocations/flowthrough', json=payload, headers=mock_jwt_auth.headers, ) assert res.status_code == 400 assert res.json == { 'code': 'error', 'message': { 'json': {'_schema': ['Cannot process more than 300 items at ' 'once.']} }, } def test_payment_allocation_flowthrough_bulk_update_error_auth(fixture_client): """Test that missing auth returns 401.""" payload = [{'payment_allocation_id': 1}] res = fixture_client.put( '/payment-allocations/flowthrough', json=payload, ) assert res.status_code == 401 def test_payment_allocation_flowthrough_bulk_delete_success( fixture_client, mock_contracts, mock_jwt_auth ): mock_jwt_auth.identity = Config.PAYMENT_ALLOCATION_IDENTITIES[0] a1 = PaymentAllocationFlowthroughFactory.create(contract_id=1) a2 = PaymentAllocationFlowthroughFactory.create(contract_id=1) res = fixture_client.delete( '/payment-allocations/flowthrough', json={ 'payment_allocation_ids': [ a1.payment_allocation_id, a2.payment_allocation_id, ] }, headers=mock_jwt_auth.headers, ) assert res.status_code == 204 # Verify they are no longer returned by active-only bulk endpoint. res2 = fixture_client.post( '/payment-allocations/flowthrough/bulk?limit=10&offset=0', json={ 'payment_allocation_ids': [ a1.payment_allocation_id, a2.payment_allocation_id, ] }, headers=mock_jwt_auth.headers, ) assert res2.status_code == 200 assert res2.json['total_count'] == 0 assert len(res2.json['items']) == 0 def test_payment_allocation_flowthrough_bulk_delete_idempotent_for_missing_or_soft_deleted( fixture_client, faker, mock_contracts, mock_jwt_auth ): mock_jwt_auth.identity = Config.PAYMENT_ALLOCATION_IDENTITIES[0] active = PaymentAllocationFlowthroughFactory.create(contract_id=1) already_deleted = PaymentAllocationFlowthroughFactory.create( contract_id=1, deleted_at=faker.past_datetime() ) res = fixture_client.delete( '/payment-allocations/flowthrough', json={ 'payment_allocation_ids': [ active.payment_allocation_id, already_deleted.payment_allocation_id, # already soft-deleted 999999999, # non-existent ] }, headers=mock_jwt_auth.headers, ) assert res.status_code == 204 res2 = fixture_client.post( '/payment-allocations/flowthrough/bulk?limit=10&offset=0', json={'payment_allocation_ids': [active.payment_allocation_id]}, headers=mock_jwt_auth.headers, ) assert res2.status_code == 200 assert res2.json['total_count'] == 0 assert len(res2.json['items']) == 0 def test_payment_allocation_flowthrough_bulk_delete_over_limit_validation( fixture_client, mock_jwt_auth ): mock_jwt_auth.identity = Config.PAYMENT_ALLOCATION_IDENTITIES[0] payload = {'payment_allocation_ids': [i + 1 for i in range(301)]} res = fixture_client.delete( '/payment-allocations/flowthrough', json=payload, headers=mock_jwt_auth.headers, ) assert res.status_code == 400 assert res.json == { 'code': 'error', 'message': { 'json': {'_schema': ['Cannot process more than 300 items at once.']} }, } def test_payment_allocation_flowthrough_bulk_delete_error_auth(fixture_client): payload = {'payment_allocation_ids': [1]} res = fixture_client.delete( '/payment-allocations/flowthrough', json=payload, ) assert res.status_code == 401