"""Tests for bank details schema.""" import copy from unittest.mock import patch from marshmallow.exceptions import ValidationError import pytest from payee.constants.constants import COUNTRIES_REQUIRED_FIELD_EMAIL from payee.schemas.bank_details import ( BankDetailsDetailedSchema, BankDetailsInputSchema, ContactSchema, ) from tests.utils.factories import ( BankDetailsCompanyTypeNestedFactory, BankDetailsIndividualTypeNestedFactory, BankDetailsIndividualTypeResponseFactory, ) def test_bank_details_schema(): """Test bank details schema.""" bank_details = BankDetailsIndividualTypeResponseFactory.build() result = BankDetailsDetailedSchema().dump(bank_details) assert result == bank_details def test_bank_details_schema_sanitization(): """Test bank details schema.""" spaced = lambda v: f' {v} ' bank_details = BankDetailsIndividualTypeResponseFactory.build() expected = copy.deepcopy(bank_details) payout_method = bank_details['payout_method'] payout_method['country'] = spaced(payout_method['country']) payout_method['currency'] = spaced(payout_method['currency']) for field in payout_method['bank_field_details']: field['value'] = spaced(field['value']) result = BankDetailsDetailedSchema().dump(bank_details) assert result == expected def test_bank_details_personal_type_create_schema(): """Test bank details create schema for personal type""" bank_details = BankDetailsIndividualTypeNestedFactory.build() result = BankDetailsInputSchema().load(bank_details) assert result == { 'address': { 'address_1': 'asdf', 'address_2': 'dfgh', 'city': 'odesa', 'country_code': 'US', 'province': 'AZ', 'zip': '65000', }, 'contact': { 'date_of_birth': '2000-10-10', 'first_name': 'firstName', 'last_name': 'lastName', 'email': 'test@example.com', 'phone': '4567890', 'phone_country': 'US', }, 'payout_method': { 'bank_account_type': 'PERSONAL', 'bank_field_details': [ {'name': 'AccountNumber', 'value': '345234552345'}, {'name': 'AccountName', 'value': 'John Smith'}, {'name': 'BankName', 'value': 'Bank of Hope'}, {'name': 'RoutingNumber', 'value': '122105155'}, {'name': 'AccountType', 'value': 'S'}, ], 'country': 'UA', 'currency': 'UAH', }, 'type': 'INDIVIDUAL', } def test_bank_details_personal_type_create_schema_required_individual_fields(): """Test bank details create schema for personal type""" bank_details = BankDetailsIndividualTypeNestedFactory.build() bank_details['contact']['date_of_birth'] = None bank_details['contact']['first_name'] = None bank_details['contact']['last_name'] = None result = BankDetailsInputSchema().validate(bank_details) assert result == { '_schema': [ 'Contact first_name, last_name, date_of_birth fields are required for ' 'individual type', ], } def test_bank_details_personal_type_create_schema_non_latin(): """Test bank details create schema for personal type""" bank_details = BankDetailsIndividualTypeNestedFactory.build() bank_details['contact']['first_name'] = 'fi*":&\'ÁÀÂÃÄÇÉÊÍÑÓÔÕÖÚÜÆØÅ' bank_details['contact']['last_name'] = 'lastNaáàâãäçéêíñóôõöúüßæøå' result = BankDetailsInputSchema().load(bank_details) assert result == { 'address': { 'address_1': 'asdf', 'address_2': 'dfgh', 'city': 'odesa', 'country_code': 'US', 'province': 'AZ', 'zip': '65000', }, 'contact': { 'date_of_birth': '2000-10-10', 'first_name': 'fi*":&\'ÁÀÂÃÄÇÉÊÍÑÓÔÕÖÚÜÆØÅ', 'last_name': 'lastNaáàâãäçéêíñóôõöúüßæøå', 'email': 'test@example.com', 'phone': '4567890', 'phone_country': 'US', }, 'payout_method': { 'bank_account_type': 'PERSONAL', 'bank_field_details': [ {'name': 'AccountNumber', 'value': '345234552345'}, {'name': 'AccountName', 'value': 'John Smith'}, {'name': 'BankName', 'value': 'Bank of Hope'}, {'name': 'RoutingNumber', 'value': '122105155'}, {'name': 'AccountType', 'value': 'S'}, ], 'country': 'UA', 'currency': 'UAH', }, 'type': 'INDIVIDUAL', } def test_validation_province_create_schema(): """Test bank details validation schema""" bank_details = BankDetailsIndividualTypeNestedFactory.build() bank_details['address'] = { 'address_1': 'some address', 'city': 'odesa', 'country_code': 'FR', 'zip': '65000', } result = BankDetailsInputSchema().validate(bank_details) assert result == {} bank_details['address'] = { 'address_1': 'some address', 'city': 'odesa', 'country_code': 'US', 'zip': '65000', } result = BankDetailsInputSchema().validate(bank_details) assert result == {'_schema': ['Province (state) field is required for US country']} bank_details['address'] = { 'address_1': 'some address', 'city': 'odesa', 'country_code': 'US', 'province': 'LA', 'zip': '65000', } result = BankDetailsInputSchema().validate(bank_details) assert result == {} @pytest.mark.parametrize('country', COUNTRIES_REQUIRED_FIELD_EMAIL) @patch('payee.schemas.bank_details.is_account_payee_flow', return_value=False) def test_validation_email_create_schema(mock_account_payee_flow, country): """Test country-based email requirement for the generic (non-account) payee flow. For the account payee flow email/phone are always required (see test_validation_email_phone_required_for_account_payee_flow); this covers the generic payee flow where only COUNTRIES_REQUIRED_FIELD_EMAIL applies. """ bank_details = BankDetailsIndividualTypeNestedFactory.build() bank_details['address']['country_code'] = country result = BankDetailsInputSchema().validate(bank_details) assert result == {} bank_details = BankDetailsIndividualTypeNestedFactory.build(contact__email=None) bank_details['address']['country_code'] = country result = BankDetailsInputSchema().validate(bank_details) assert result == { '_schema': [f'Contact email field is required for {country} country'] } bank_details = BankDetailsIndividualTypeNestedFactory.build(contact__email=None) bank_details['address'] = { 'address_1': 'some address', 'city': 'odesa', 'country_code': 'CA', 'zip': '65000', } result = BankDetailsInputSchema().validate(bank_details) assert result == {} assert 'CA' not in COUNTRIES_REQUIRED_FIELD_EMAIL @patch('payee.schemas.bank_details.is_account_payee_flow', return_value=True) def test_validation_email_phone_required_for_account_payee_flow( mock_account_payee_flow, ): """Email/phone are required for the account payee flow regardless of country.""" # CA is not in COUNTRIES_REQUIRED_FIELD_EMAIL, but account payees must still # provide contact email/phone now that the feature flag has been torn down. bank_details = BankDetailsIndividualTypeNestedFactory.build(contact__email=None) bank_details['address']['country_code'] = 'CA' result = BankDetailsInputSchema().validate(bank_details) assert 'CA' not in COUNTRIES_REQUIRED_FIELD_EMAIL assert 'contact' in result assert 'email' in result['contact'] bank_details = BankDetailsIndividualTypeNestedFactory.build( contact__phone=None, contact__phone_country=None ) bank_details['address']['country_code'] = 'CA' result = BankDetailsInputSchema().validate(bank_details) assert 'contact' in result assert 'phone' in result['contact'] assert 'phone_country' in result['contact'] def test_validation_payee_type_create_schema(): """Test bank details validation schema""" bank_details = BankDetailsIndividualTypeNestedFactory.build() result = BankDetailsInputSchema().validate(bank_details) assert result == {} bank_details['type'] = 'WRONG_TYPE' result = BankDetailsInputSchema().validate(bank_details) assert result == {'type': ['Must be one of: INDIVIDUAL, COMPANY.']} bank_details['type'] = 'COMPANY' result = BankDetailsInputSchema().validate(bank_details) assert result == { '_schema': ["Missing required fields for type 'COMPANY': company"] } bank_details = BankDetailsCompanyTypeNestedFactory.build() bank_details['type'] = 'INDIVIDUAL' result = BankDetailsInputSchema().validate(bank_details) assert result == { '_schema': ["Missing required fields for type 'INDIVIDUAL': contact"] } def test_bank_details_company_type_create_schema(): """Test bank details create schema for company.""" bank_details = BankDetailsCompanyTypeNestedFactory.build() result = BankDetailsInputSchema().dump(bank_details) assert result == { 'address': { 'address_1': 'asdf', 'address_2': 'dfgh', 'city': 'odesa', 'country_code': 'US', 'province': 'AZ', 'zip': '65000', }, 'company': {'name': 'company name'}, 'payout_method': { 'bank_account_type': 'PERSONAL', 'bank_field_details': [ {'name': 'AccountNumber', 'value': '345234552345'}, {'name': 'AccountName', 'value': 'John Smith'}, {'name': 'BankName', 'value': 'Bank of Hope'}, {'name': 'RoutingNumber', 'value': '122105155'}, {'name': 'AccountType', 'value': 'S'}, ], 'country': 'UA', 'currency': 'UAH', }, 'type': 'COMPANY', } def test_bank_details_company_type_create_schema_non_latin(): """Test bank details create schema for company.""" bank_details = BankDetailsCompanyTypeNestedFactory.build() bank_details['company']['name'] = 'company*":&\'ÁÀÂÃÄÇÉÊÍÑÓÔÕÖÚÜÆØÅ' result = BankDetailsInputSchema().dump(bank_details) assert result == { 'address': { 'address_1': 'asdf', 'address_2': 'dfgh', 'city': 'odesa', 'country_code': 'US', 'province': 'AZ', 'zip': '65000', }, 'company': {'name': 'company*":&\'ÁÀÂÃÄÇÉÊÍÑÓÔÕÖÚÜÆØÅ'}, 'payout_method': { 'bank_account_type': 'PERSONAL', 'bank_field_details': [ {'name': 'AccountNumber', 'value': '345234552345'}, {'name': 'AccountName', 'value': 'John Smith'}, {'name': 'BankName', 'value': 'Bank of Hope'}, {'name': 'RoutingNumber', 'value': '122105155'}, {'name': 'AccountType', 'value': 'S'}, ], 'country': 'UA', 'currency': 'UAH', }, 'type': 'COMPANY', } def test_validation_first_name_for_individual_type(): """Test bank details validation schema""" bank_details = BankDetailsIndividualTypeNestedFactory.build() bank_details['contact']['first_name'] = 'John @#$%!!!' result = BankDetailsInputSchema().validate(bank_details) assert result == { 'contact': {'first_name': ['String does not match expected pattern.']} } bank_details['contact']['first_name'] = 'John 2' result = BankDetailsInputSchema().validate(bank_details) assert result == {} def test_validation_name_for_company_type(): """Test bank details validation schema""" bank_details = BankDetailsCompanyTypeNestedFactory.build() bank_details['company']['name'] = 'COMPANY @#$%!!!НО~' result = BankDetailsInputSchema().validate(bank_details) assert result == {'company': {'name': ['String does not match expected pattern.']}} bank_details['company']['name'] = 'COMPANY FROM ODESA "QUICKLY GIVE YOUR MONEY"' result = BankDetailsInputSchema().validate(bank_details) assert result == {} @pytest.mark.parametrize( 'email,phone,phone_country,expect_error,missing_field', [ (None, None, None, True, ['email', 'phone', 'phone_country']), ('test@example.com', None, None, True, ['phone', 'phone_country']), (None, '4567890', None, True, ['email', 'phone_country']), ('test@example.com', '4567890', None, True, ['phone_country']), ('test@example.com', '4567890', 'US', False, []), ( 'test@example.com', '4567890', 'U', True, ['phone_country'], ), # invalid country code ( 'test@example.com', '4567890', 'USA', True, ['phone_country'], ), # invalid country code ], ) @patch('payee.schemas.bank_details.is_account_payee_flow') def test_contact_schema_email_phone_country_required( mock_feature, email, phone, phone_country, expect_error, missing_field ): mock_feature.return_value = True schema = ContactSchema() data = { 'first_name': 'John', 'last_name': 'Smith', 'date_of_birth': '2000-01-01', 'email': email, 'phone': phone, 'phone_country': phone_country, } # Remove None values to simulate missing fields data = {k: v for k, v in data.items() if v is not None} if expect_error: with pytest.raises(ValidationError) as exc: schema.load(data) for field in missing_field: assert field in exc.value.messages else: assert schema.load(data) == data @pytest.mark.parametrize( 'phone,phone_country,expect_error', [ ('4567890', 'US', False), ('invalid-phone', 'US', True), ('4567890', 'U', True), ('4567890', None, True), ], ) @patch('payee.schemas.bank_details.is_account_payee_flow') def test_contact_schema_phone_and_country_format( mock_feature, phone, phone_country, expect_error ): mock_feature.return_value = True schema = ContactSchema() data = { 'first_name': 'John', 'last_name': 'Smith', 'date_of_birth': '2000-01-01', 'email': 'test@example.com', 'phone': phone, 'phone_country': phone_country, } data = {k: v for k, v in data.items() if v is not None} if expect_error: with pytest.raises(ValidationError) as exc: schema.load(data) assert ( 'phone' in exc.value.messages or 'phone_country' in exc.value.messages ) else: assert schema.load(data) == data # Update the not-required test to include phone_country @patch('payee.schemas.bank_details.is_account_payee_flow') def test_contact_schema_email_phone_country_not_required(mock_feature): mock_feature.return_value = False schema = ContactSchema() data = { 'first_name': 'John', 'last_name': 'Smith', 'date_of_birth': '2000-01-01', } assert schema.load(data) == data