"""Unit tests for models.""" from datetime import date from decimal import Decimal from pydantic import ValidationError import pytest from src.constants import TaxCorrectionTypes, TaxFormType from src.models import ( AbacusState, AccountPayee, AccountPayeeDataloaderAccount, AccountPayeeDataloaderAccountData, AccountTaxInfo, AccountTaxInfoBulk, Address, Company, Contact, NewTaxCorrection, NewTaxCorrectionVAT, NewTaxDetails, PaymentHold, PaymentHoldResult, PayoneerPayoutMethod, PostGermanTaxDetailsInput, PostNorwayTaxDetailsInput, PostSpanishTaxDetailsInput, PostTaxFormsInput, PostUkTaxDetailsInput, PullBankingDetailsInputRow, TaxDetails, TaxDetailsDataloaderItem, TaxDetailsDataloaderItemAddress, TaxFormInfoBulk, TaxFormInfoItem, UpdateAccountTaxInfo, USTaxFormW8BEN, USTaxFormW8BENE, USTaxFormW8ECI, USTaxFormW8IMY, USTaxFormW9, ) from tests.unit.factories import AccountTaxInfoFactory, TaxFormInfoItemFactory def test_new_tax_correction_model_validation_success() -> None: """Test NewTaxCorrection model.""" assert NewTaxCorrection.model_validate( { 'account_id': '36034', 'contract_id': '505338', 'correction_statement_period_id': '305', 'correction_type': 'wht', 'amount': '13.00', 'currency_code': 'USD', 'note': 'note', } ) == NewTaxCorrection( account_id=36034, contract_id=505338, correction_statement_period_id=305, correction_type=TaxCorrectionTypes.wht, amount=Decimal('13.00'), currency_code='USD', note='note', ) assert NewTaxCorrection.list_validate( [ { 'account_id': '36034', 'contract_id': '505338', 'correction_statement_period_id': '305', 'correction_type': 'wht', 'amount': '13.00', 'currency_code': 'USD', 'note': 'note', }, { 'account_id': '36035', 'contract_id': '505339', 'correction_statement_period_id': '306', 'correction_type': 'vat', 'amount': '15.00', 'currency_code': 'GBP', 'note': 'note2', }, ] ) == [ NewTaxCorrection( account_id=36034, contract_id=505338, correction_statement_period_id=305, correction_type=TaxCorrectionTypes.wht, amount=Decimal('13.00'), currency_code='USD', note='note', ), NewTaxCorrection( account_id=36035, contract_id=505339, correction_statement_period_id=306, correction_type=TaxCorrectionTypes.vat, amount=Decimal('15.00'), currency_code='GBP', note='note2', ), ] def test_new_tax_correction_model_validation_failure() -> None: """Test NewTaxCorrection model.""" with pytest.raises(ValidationError) as exc_info: NewTaxCorrection.model_validate( { 'account_id': '36034a', 'contract_id': '505338b', 'correction_statement_period_id': '305c', 'correction_type': '4d', 'amount': '13.00e', 'currency_code': 'USD', 'note': 'note', } ) assert [e['loc'] for e in exc_info.value.errors()] == [ ('account_id',), ('contract_id',), ('correction_statement_period_id',), ('correction_type',), ('amount',), ] with pytest.raises(ValidationError) as exc_info: NewTaxCorrection.list_validate( [ { 'account_id': '36034a', 'contract_id': '505338b', 'correction_statement_period_id': '305c', 'correction_type': '4d', 'amount': '13.00e', 'currency_code': 'USD', 'note': 'note', }, {'note': 'note2'}, ] ) assert [e['loc'] for e in exc_info.value.errors()] == [ (0, 'account_id'), (0, 'contract_id'), (0, 'correction_statement_period_id'), (0, 'correction_type'), (0, 'amount'), (1, 'account_id'), (1, 'contract_id'), (1, 'correction_statement_period_id'), (1, 'correction_type'), (1, 'amount'), (1, 'currency_code'), ] def test_new_tax_correction_vat_model_validation_success() -> None: """Test NewTaxCorrectionVAT model.""" assert NewTaxCorrectionVAT.model_validate( { 'statement_period_id': '520', 'account_id': '101', 'contract_id': '326', 'vat_category': 'cat1', 'payee_currency_code': 'EUR', 'vat_currency_code': 'GBP', 'base_amount_payee_currency': '401.61', 'vat_rate': '23.72', 'vat_amount_payee_currency': '46.56', 'vat_amount_vat_currency': '37', 'net_amount_payee_currency': '24.06', 'wht_rate': '14.99', 'wht_amount_payee_currency': '84.52', 'wht_amount_vat_currency': '72.22', 'note': 'testNote1', } ) == NewTaxCorrectionVAT( statement_period_id=520, account_id=101, contract_id=326, vat_category='cat1', payee_currency_code='EUR', vat_currency_code='GBP', base_amount_payee_currency=401.61, vat_rate=23.72, vat_amount_payee_currency=46.56, vat_amount_vat_currency=37, net_amount_payee_currency=24.06, wht_rate=14.99, wht_amount_payee_currency=84.52, wht_amount_vat_currency=72.22, note='testNote1', ) assert NewTaxCorrectionVAT.list_validate( [ { 'statement_period_id': '520', 'account_id': '101', 'contract_id': '326', 'vat_category': 'cat1', 'payee_currency_code': 'EUR', 'vat_currency_code': 'GBP', 'base_amount_payee_currency': '401.61', 'vat_rate': '23.72', 'vat_amount_payee_currency': '46.56', 'vat_amount_vat_currency': '37', 'net_amount_payee_currency': '24.06', 'wht_rate': '14.99', 'wht_amount_payee_currency': '84.52', 'wht_amount_vat_currency': '72.22', 'note': None, }, { 'statement_period_id': '623', 'account_id': '252', 'contract_id': '941', 'vat_category': 'cat2', 'payee_currency_code': 'USD', 'vat_currency_code': 'EUR', 'base_amount_payee_currency': '383.47', 'vat_rate': '48.28', 'vat_amount_payee_currency': '38', 'vat_amount_vat_currency': '63.1', 'net_amount_payee_currency': '42.02', 'note': '', }, ] ) == [ NewTaxCorrectionVAT( statement_period_id=520, account_id=101, contract_id=326, vat_category='cat1', payee_currency_code='EUR', vat_currency_code='GBP', base_amount_payee_currency=401.61, vat_rate=23.72, vat_amount_payee_currency=46.56, vat_amount_vat_currency=37, net_amount_payee_currency=24.06, wht_rate=14.99, wht_amount_payee_currency=84.52, wht_amount_vat_currency=72.22, note=None, ), NewTaxCorrectionVAT( statement_period_id=623, account_id=252, contract_id=941, vat_category='cat2', payee_currency_code='USD', vat_currency_code='EUR', base_amount_payee_currency=383.47, vat_rate=48.28, vat_amount_payee_currency=38, vat_amount_vat_currency=63.1, net_amount_payee_currency=42.02, note='', ), ] def test_new_tax_correction_vat_model_validation_failure() -> None: """Test NewTaxCorrectionVAT model.""" with pytest.raises(ValidationError) as exc_info: NewTaxCorrectionVAT.list_validate( [ { 'statement_period_id': 'aa', 'contract_id': 'bb', 'vat_category': 321, 'payee_currency_code': '111', 'vat_currency_code': '222', 'base_amount_payee_currency': 'ccc', 'vat_amount_payee_currency': 'dd', 'net_amount_payee_currency': '', 'wht_rate': 'ee', 'wht_amount_payee_currency': 'ff', 'wht_amount_vat_currency': '', 'note': 123, }, ] ) assert [e['loc'] for e in exc_info.value.errors()] == [ (0, 'statement_period_id'), (0, 'account_id'), (0, 'contract_id'), (0, 'vat_category'), (0, 'base_amount_payee_currency'), (0, 'vat_rate'), (0, 'vat_amount_payee_currency'), (0, 'vat_amount_vat_currency'), (0, 'net_amount_payee_currency'), (0, 'wht_rate'), (0, 'wht_amount_payee_currency'), (0, 'note'), ] def test_payment_hold_model_validation_success() -> None: """Test PaymentHoldResult model.""" assert PaymentHoldResult.model_validate( { 'payment_hold_id': 105, 'account_id': 41, 'is_on_hold': True, 'start_date': date(2024, 11, 1), 'reason': 'some reason', 'created_at': date(2024, 10, 30), 'created_by': 'some job', 'last_modified': date(2024, 10, 30), 'last_modified_by': 'the same job', } ) == PaymentHoldResult( payment_hold_id=105, account_id=41, is_on_hold=True, start_date=date(2024, 11, 1), reason='some reason', created_at=date(2024, 10, 30), created_by='some job', last_modified=date(2024, 10, 30), last_modified_by='the same job', ) def test_payment_hold_model_validation_failure() -> None: """Test PaymentHoldResult model.""" with pytest.raises(ValidationError) as exc_info: PaymentHold.model_validate( { 'account_id': '41b', 'is_on_hold': 41, 'start_date': 'some date', 'reason': 'some reason', 'created_by': 'some job', 'last_modified_by': 12, } ) assert [e['loc'] for e in exc_info.value.errors()] == [ ('account_id',), ('is_on_hold',), ('start_date',), ('last_modified_by',), ] def test_payment_hold_result_model_validation_failure() -> None: """Test PaymentHoldResult model.""" with pytest.raises(ValidationError) as exc_info: PaymentHoldResult.model_validate( { 'payment_hold_id': '105a', 'account_id': '41b', 'is_on_hold': 41, 'start_date': 'some date', 'reason': 'some reason', 'created_by': 'some job', 'created_at': 11, 'last_modified_by': 12, 'last_modified': 'aa', } ) assert [e['loc'] for e in exc_info.value.errors()] == [ ('account_id',), ('is_on_hold',), ('start_date',), ('last_modified_by',), ('payment_hold_id',), ('created_at',), ('last_modified',), ] def test_address_model_validation_success() -> None: """Test Address model.""" assert Address.model_validate( { 'country': 'US', 'address1': '123 Main St', 'address2': 'Apt 4B', 'city': 'Metropolis', 'state': 'MetroState', 'postal_code': '12345', } ) == Address( country_code='US', address_1='123 Main St', address_2='Apt 4B', city='Metropolis', province='MetroState', zip='12345', ) def test_contact_model_validation_success() -> None: """Test Contact model.""" assert Contact.model_validate( { 'firstName': 'John', 'lastName': 'Doe', 'dateOfBirth': '1990-01-01', 'email': 'john.doe@example.com', } ) == Contact( first_name='John', last_name='Doe', date_of_birth='1990-01-01', email='john.doe@example.com', ) def test_company_model_validation_success() -> None: """Test Company model.""" assert Company.model_validate({'companyName': 'TechCorp'}) == Company( name='TechCorp' ) def test_abacus_state_model_validation_success() -> None: """Test AbacusState model.""" assert AbacusState.model_validate( { 'abacus_state_id': 51, 'action_name': 'action_name', 'action_status': 'status', } ) == AbacusState( abacus_state_id=51, action_name='action_name', action_status='status', ) def test_abacus_state_model_validation_failure() -> None: """Test AbacusState model.""" with pytest.raises(ValidationError) as exc_info: AbacusState.model_validate( { 'abacus_state_id': 'abc', 'action_name': 10, } ) assert [e['loc'] for e in exc_info.value.errors()] == [ ('abacus_state_id',), ('action_name',), ('action_status',), ] def test_abacus_payee_dataloader_account_model_validation_success() -> None: """Test AccountPayeeDataloaderAccount model.""" assert AccountPayeeDataloaderAccount.model_validate( { 'items': [ {'data': {'account_payee_id': 101, 'account_id': 11}}, {'data': {'account_payee_id': 202, 'account_id': 12}}, ], } ) == AccountPayeeDataloaderAccount( items=[ AccountPayeeDataloaderAccountData( data=AccountPayee(account_payee_id=101, account_id=11) ), AccountPayeeDataloaderAccountData( data=AccountPayee(account_payee_id=202, account_id=12) ), ] ) def test_abacus_payee_dataloader_account_model_validation_failure() -> None: """Test AccountPayeeDataloaderAccount model.""" with pytest.raises(ValidationError) as exc_info: AccountPayeeDataloaderAccount.model_validate( { 'items': [ {'data1': 'abc'}, {'data': {'account_payee_id': 'aa', 'account_id': 'bb'}}, ] } ) assert [e['loc'] for e in exc_info.value.errors()] == [ ('items', 0, 'data'), ('items', 1, 'data', 'account_payee_id'), ('items', 1, 'data', 'account_id'), ] @pytest.mark.parametrize('dates_value', (True, False, None)) def test_tax_form_info_model_validation_success(dates_value: bool | None) -> None: """Test TaxFormInfoItem model.""" data = { 'account_payee_id': 17, 'tax_form_type': 'type1', 'account_payee_tax_form_info_id': 105, } expiration_date = 'exp date' if dates_value else None signed_date = 'sig date' if dates_value else None last_modified = 'last date' if dates_value else None if dates_value or dates_value is None: data.update( { 'expiration_date': expiration_date, 'signed_date': signed_date, 'last_modified': last_modified, } ) assert TaxFormInfoItem.model_validate(data) == TaxFormInfoItem( account_payee_tax_form_info_id=105, account_payee_id=17, tax_form_type='type1', expiration_date=expiration_date, signed_date=signed_date, last_modified=last_modified, ) def test_tax_form_info_model_validation_failure() -> None: """Test TaxFormInfoItem model.""" with pytest.raises(ValidationError) as exc_info: TaxFormInfoItem.model_validate( { 'account_payee_tax_form_info_id': 'def', 'account_payee_id': 'abc', 'expiration_date': 100, 'signed_date': 200, } ) assert [e['loc'] for e in exc_info.value.errors()] == [ ('account_payee_tax_form_info_id',), ('account_payee_id',), ('tax_form_type',), ('expiration_date',), ('signed_date',), ] @pytest.mark.parametrize('is_empty', (True, False)) def test_tax_form_info_bulk_model_validation_success(is_empty: bool) -> None: """Test TaxFormInfoBulk model.""" data = [] if is_empty else TaxFormInfoItemFactory.batch(4) assert TaxFormInfoBulk.model_validate( {'items': data, 'total_count': len(data)} ) == TaxFormInfoBulk(items=data, total_count=len(data)) def test_tax_form_info_bulk_model_validation_failure() -> None: """Test TaxFormInfoBulk model.""" with pytest.raises(ValidationError) as exc_info: TaxFormInfoBulk.model_validate({'items': [1, 5]}) assert [e['loc'] for e in exc_info.value.errors()] == [ ('items', 0), ('items', 1), ('total_count',), ] def test_us_tax_form_w9_model_validation_success() -> None: """Test USTaxFormW9 model.""" assert USTaxFormW9.model_validate( { 'tax_form_type': 'W9', 'tax_id_country': 'country', 'tin_type': None, 'tin': 'tn', 'tax_name': 'nm', 'tax_classification': 'class', } ) == USTaxFormW9( tax_form_type=TaxFormType.W9, tax_id_country='country', tin_type=None, tin='tn', tax_name='nm', tax_classification='class', ) def test_us_tax_form_w9_model_validation_failure() -> None: """Test USTaxFormW9 model.""" with pytest.raises(ValidationError) as exc_info: USTaxFormW9.model_validate( { 'tax_form_type': 'abc', 'tin_type': 100, 'tax_name': 200, } ) assert [e['loc'] for e in exc_info.value.errors()] == [ ('tax_form_type',), ('tax_id_country',), ('tin_type',), ('tin',), ('tax_name',), ('tax_classification',), ] def test_us_tax_form_w8_ben_model_validation_success() -> None: """Test USTaxFormW8BEN model.""" assert USTaxFormW8BEN.model_validate( { 'tax_form_type': 'W8BEN', 'tax_id_country': 'country', 'tin_type': None, 'tin': 'tn', 'tax_name': 'nm', 'tax_treaty_claim': False, 'signed_date': '05/27/2025', } ) == USTaxFormW8BEN( tax_form_type=TaxFormType.W8BEN, tax_id_country='country', tin_type=None, tin='tn', tax_name='nm', tax_treaty_claim=False, signed_date=date(2025, 5, 27), ) def test_us_tax_form_w8_ben_model_validation_failure() -> None: """Test USTaxFormW8BEN model.""" with pytest.raises(ValidationError) as exc_info: USTaxFormW8BEN.model_validate( { 'tax_form_type': 'abc', 'tin_type': 100, 'tax_name': 200, 'tax_treaty_claim': 'test', } ) assert [e['loc'] for e in exc_info.value.errors()] == [ ('tax_form_type',), ('tax_id_country',), ('tin_type',), ('tin',), ('tax_name',), ('tax_treaty_claim',), ('signed_date',), ] def test_us_tax_form_w8_bene_model_validation_success() -> None: """Test USTaxFormW8BENE model.""" assert USTaxFormW8BENE.model_validate( { 'tax_form_type': 'W8BEN-E', 'tax_id_country': 'USA', 'tin_type': 'type1', 'tin': 'tn', 'tax_name': 'nm', 'tax_treaty_claim': 'true', 'type_of_entity': 'ent1', 'lob': 'l1', 'signed_date': '05/27/2025', } ) == USTaxFormW8BENE( tax_form_type=TaxFormType.W8BENE, tax_id_country='USA', tin_type='type1', tin='tn', tax_name='nm', tax_treaty_claim=True, lob='l1', type_of_entity='ent1', signed_date=date(2025, 5, 27), ) def test_us_tax_form_w8_bene_model_validation_failure() -> None: """Test USTaxFormW8BENE model.""" with pytest.raises(ValidationError) as exc_info: USTaxFormW8BENE.model_validate( { 'tax_form_type': 10, 'tax_id_country': 15, 'tax_name': 200, 'tax_treaty_claim': 500, 'lob': 11, 'signed_date': '05/29/2025', } ) assert [e['loc'] for e in exc_info.value.errors()] == [ ('tax_form_type',), ('tax_id_country',), ('tin_type',), ('tin',), ('tax_name',), ('tax_treaty_claim',), ('type_of_entity',), ('lob',), ] def test_us_tax_form_w8_bene_model_validation_failure_lob() -> None: """Test USTaxFormW8BENE model.""" with pytest.raises(ValidationError): USTaxFormW8BENE.model_validate( { 'tax_form_type': 'W8BEN-E', 'tax_id_country': 'USA', 'tin_type': 'type1', 'tin': 'tn', 'tax_name': 'nm', 'tax_treaty_claim': 'true', 'type_of_entity': 'ent1', 'lob': None, 'signed_date': '05/27/2025', } ) def test_us_tax_form_w8_imy_model_validation_success() -> None: """Test USTaxFormW8IMY model.""" assert USTaxFormW8IMY.model_validate( { 'tax_form_type': 'W8IMY', 'tax_id_country': 'CAD', 'tin_type': None, 'tin': None, 'tax_name': 'nm', 'tax_treaty_claim': 'false', 'type_of_entity': 'ent1', 'signed_date': '05/27/2025', } ) == USTaxFormW8IMY( tax_form_type=TaxFormType.W8IMY, tax_id_country='CAD', tin_type=None, tin=None, tax_name='nm', tax_treaty_claim=False, type_of_entity='ent1', signed_date=date(2025, 5, 27), ) def test_us_tax_form_w8_imy_model_validation_failure() -> None: """Test USTaxFormW8BENE model.""" with pytest.raises(ValidationError) as exc_info: USTaxFormW8IMY.model_validate( { 'tax_form_type': 10, 'tax_id_country': 15, 'tax_name': 200, 'tax_treaty_claim': 500, 'lob': 11, } ) assert [e['loc'] for e in exc_info.value.errors()] == [ ('tax_form_type',), ('tax_id_country',), ('tin_type',), ('tin',), ('tax_name',), ('tax_treaty_claim',), ('signed_date',), ('type_of_entity',), ] def test_us_tax_form_w8_eci_model_validation_success() -> None: """Test USTaxFormW8ECI model.""" assert USTaxFormW8ECI.model_validate( { 'tax_form_type': 'W8ECI', 'tax_id_country': 'AUD', 'tin_type': None, 'tin': None, 'tax_name': 'nm', 'tax_treaty_claim': 'false', 'type_of_entity': 'ent1', 'signed_date': '2025-05-27', } ) == USTaxFormW8ECI( tax_form_type=TaxFormType.W8ECI, tax_id_country='AUD', tin_type=None, tin=None, tax_name='nm', tax_treaty_claim=False, type_of_entity='ent1', signed_date=date(2025, 5, 27), ) def test_us_tax_form_w8_eci_model_validation_failure() -> None: """Test USTaxFormW8ECI model.""" with pytest.raises(ValidationError) as exc_info: USTaxFormW8ECI.model_validate({}) assert [e['loc'] for e in exc_info.value.errors()] == [ ('tax_form_type',), ('tax_id_country',), ('tin_type',), ('tin',), ('tax_name',), ('tax_treaty_claim',), ('signed_date',), ('type_of_entity',), ] def test_post_tax_forms_input_model_validation_success() -> None: """Test PostTaxFormsInput model.""" assert PostTaxFormsInput.model_validate( dict( vendor_id='104', tax_residence_country='ABC', tax_form_type='W9', tax_id_country='country', tin_type='', tin='tn', tax_name='nm', tax_classification='class', ) ) == PostTaxFormsInput( account_id=104, country_of_tax_residence='ABC', tax_form_type=TaxFormType.W9, override=None, account_payee_id=None, account_tax_info_id=None, account_payee_tax_form_info_id=None, tax_form=USTaxFormW9( tax_form_type=TaxFormType.W9, tax_id_country='country', tin_type='', tin='tn', tax_name='nm', tax_classification='class', ), ) def test_post_tax_forms_input_model_validation_failure() -> None: """Test PostTaxFormsInput model.""" with pytest.raises(ValidationError) as exc_info: PostTaxFormsInput.model_validate({}) assert [e['loc'] for e in exc_info.value.errors()] == [ ('account_id',), ('tax_form_type',), ('country_of_tax_residence',), ('tax_form',), ] def test_account_tax_info_item_model_validation_success() -> None: """Test AccountTaxInfo model.""" assert AccountTaxInfo.model_validate( { 'country_of_tax_residence': 'USA', 'is_sba_signed': None, 'is_vat_exempt': 'true', 'is_tax_treaty_claimed': False, 'certificate_of_residence_expiration_date': '2025-11-01', 'is_resident_of_spanish_islands': 'false', 'account_tax_info_id': 1004, 'account_id': 26, } ) == AccountTaxInfo( country_of_tax_residence='USA', is_sba_signed=None, is_vat_exempt=True, is_tax_treaty_claimed=False, tax_employment_type=None, certificate_of_residence_expiration_date=date(2025, 11, 1), is_wht_applicable=None, is_resident_of_spanish_islands=False, account_tax_info_id=1004, account_id=26, ) def test_account_tax_info_item_model_validation_failure() -> None: """Test AccountTaxInfo model.""" with pytest.raises(ValidationError) as exc_info: AccountTaxInfo.model_validate( { 'country_of_tax_residence': 'aaaa', 'is_sba_signed': 'bbb', 'is_tax_treaty_claimed': 'ccc', 'tax_employment_type': 100, 'certificate_of_residence_expiration_date': 'incorrect date', 'is_wht_applicable': 200, 'is_resident_of_spanish_islands': 'bool', 'account_tax_info_id': 'abc', 'wht_rate_override': 'test', } ) assert [e['loc'] for e in exc_info.value.errors()] == [ ('country_of_tax_residence',), ('is_sba_signed',), ('is_vat_exempt',), ('is_tax_treaty_claimed',), ('tax_employment_type',), ('certificate_of_residence_expiration_date',), ('is_wht_applicable',), ('is_resident_of_spanish_islands',), ('wht_rate_override',), ('account_tax_info_id',), ('account_id',), ] @pytest.mark.parametrize('is_empty', (True, False)) def test_account_tax_info_bulk_model_validation_success(is_empty: bool) -> None: """Test AccountTaxInfoBulk model.""" data = [] if is_empty else AccountTaxInfoFactory.batch(5) assert AccountTaxInfoBulk.model_validate( {'items': data, 'total_count': len(data)} ) == AccountTaxInfoBulk(items=data, total_count=len(data)) def test_account_tax_info_bulk_model_validation_failure() -> None: """Test AccountTaxInfoBulk model.""" with pytest.raises(ValidationError) as exc_info: AccountTaxInfoBulk.model_validate({'items': [1, 'a']}) assert [e['loc'] for e in exc_info.value.errors()] == [ ('items', 0), ('items', 1), ('total_count',), ] def test_post_spanish_tax_details_input_model_validation_success_no_optional_fields() -> ( None ): """Test PostSpanishTaxDetailsInput model.""" assert PostSpanishTaxDetailsInput.model_validate( dict( vendor_id='5', first_name='first name', last_name='last name', is_vat_registered='TRUE', local_tax_id='X12345678', is_resident_of_spanish_islands='', country_code='USA', country_of_tax_residence_code='CAN', expiration_date='12/11/2025', override='TRUE', ) ) == PostSpanishTaxDetailsInput( vendor_id=5, business_name='first name last name', first_name='first name', last_name='last name', is_vat_registered=True, local_tax_id='X12345678', country_code='USA', country_of_tax_residence_code='CAN', expiration_date=date(2025, 12, 11), override=True, ) @pytest.mark.parametrize('tax_type_name', ('tax_employment_type', 'tax_entity_type')) def test_post_spanish_tax_details_input_model_validation_success_all_fields( tax_type_name: str, ) -> None: """Test PostSpanishTaxDetailsInput model.""" assert PostSpanishTaxDetailsInput.model_validate( dict( vendor_id='5', business_name='business name', first_name='', last_name=None, is_vat_registered='TRUE', local_tax_id='X12345678', is_resident_of_spanish_islands='FALSE', address1='123 Baldwin Ave.', address2='apt. 321', province='NY', city='New york', zip='10001', country_code='USA', country_of_tax_residence_code='ESP', expiration_date='12/11/2025', account_payee_id=1, account_tax_info_id=2, **{tax_type_name: 'Employed Individual'}, ) ) == PostSpanishTaxDetailsInput( vendor_id=5, business_name='business name', first_name='', is_vat_registered=True, local_tax_id='X12345678', tax_employment_type='Employed Individual', is_resident_of_spanish_islands=False, address1='123 Baldwin Ave.', address2='apt. 321', province='NY', city='New york', zip='10001', country_code='USA', country_of_tax_residence_code='ESP', expiration_date=date(2025, 12, 11), account_payee_id=1, account_tax_info_id=2, ) def test_post_spanish_tax_details_input_model_validation_tax_details() -> None: """Test PostSpanishTaxDetailsInput model getting account payee tax details.""" assert PostSpanishTaxDetailsInput( vendor_id=5, business_name='business name', is_vat_registered=True, local_tax_id='X12345678', tax_employment_type='Employed Individual', is_resident_of_spanish_islands=False, address1='123 Baldwin Ave.', address2='apt. 321', province='NY', city='New york', zip='10001', country_code='USA', country_of_tax_residence_code='ESP', expiration_date=date(2025, 12, 11), ).get_account_payee_tax_details() == NewTaxDetails( local_tax_id='X12345678', is_vat_registered=True, business_name='business name', country_of_tax_residency_code='ESP', address=Address( country='USA', address1='123 Baldwin Ave.', address2='apt. 321', city='New york', postal_code='10001', state='NY', ), ) def test_post_spanish_tax_details_input_model_validation_tax_info() -> None: """Test PostSpanishTaxDetailsInput model getting account tax info.""" assert PostSpanishTaxDetailsInput( vendor_id=5, business_name='business name', is_vat_registered=True, local_tax_id='X12345678', tax_employment_type='Employed Individual', is_resident_of_spanish_islands=False, address1='123 Baldwin Ave.', address2='apt. 321', province='NY', city='New york', zip='10001', country_code='USA', country_of_tax_residence_code='ESP', expiration_date=date(2025, 12, 11), ).get_account_tax_info() == UpdateAccountTaxInfo( country_of_tax_residence='ESP', is_resident_of_spanish_islands=False, tax_employment_type='Employed Individual', certificate_of_residence_expiration_date=date(2025, 12, 11), is_sba_signed=True, ) def test_post_spanish_tax_details_input_model_validation_failure() -> None: """Test PostSpanishTaxDetailsInput model.""" with pytest.raises(ValidationError) as exc_info: PostSpanishTaxDetailsInput.model_validate( dict( vendor_id='abc', business_name=111, last_name=222, is_vat_registered='abc', tax_employment_type=333, is_resident_of_spanish_islands='FALSE', province=444, zip=5555, expiration_date='def', account_payee_id='id', account_tax_info_id=2, ) ) assert [e['loc'] for e in exc_info.value.errors()] == [ ('vendor_id',), ('business_name',), ('last_name',), ('is_vat_registered',), ('province',), ('zip',), ('country_of_tax_residence_code',), ('account_payee_id',), ('local_tax_id',), ('tax_employment_type',), ('expiration_date',), ] def test_post_norway_tax_details_input_model_validation_success() -> None: """Test PostNorwayTaxDetailsInput model.""" assert PostNorwayTaxDetailsInput.model_validate( dict( vendor_id='15', business_name='business name', first_name=None, last_name='', is_vat_registered='TRUE', vat_number='AB12345678', address1='123 Baldwin Ave.', address2='apt. 321', province='NY', city='New york', zip='10001', country_code='USA', country_of_tax_residence_code='ESP', account_payee_id=10, account_tax_info_id=20, ) ) == PostNorwayTaxDetailsInput( vendor_id=15, business_name='business name', last_name='', is_vat_registered=True, vat_number='AB12345678', address1='123 Baldwin Ave.', address2='apt. 321', province='NY', city='New york', zip='10001', country_code='USA', country_of_tax_residence_code='ESP', account_payee_id=10, account_tax_info_id=20, ) def test_post_norway_tax_details_input_model_validation_tax_details() -> None: """Test PostNorwayTaxDetailsInput model getting account payee tax details.""" assert PostNorwayTaxDetailsInput( vendor_id=15, business_name='business name', is_vat_registered=True, vat_number='AB12345678', address1='123 Baldwin Ave.', address2='apt. 321', province='NY', city='New york', zip='10001', country_code='USA', country_of_tax_residence_code='NOR', ).get_account_payee_tax_details() == NewTaxDetails( vat_number='AB12345678', business_name='business name', country_of_tax_residency_code='NOR', address=Address( country='USA', address1='123 Baldwin Ave.', address2='apt. 321', city='New york', postal_code='10001', state='NY', ), ) def test_post_norway_tax_details_input_model_validation_tax_info() -> None: """Test PostNorwayTaxDetailsInput model getting account tax info.""" assert PostNorwayTaxDetailsInput( vendor_id=15, business_name='business name', is_vat_registered=True, vat_number='AB12345678', country_code='USA', country_of_tax_residence_code='NOR', ).get_account_tax_info() == UpdateAccountTaxInfo( country_of_tax_residence='NOR', is_sba_signed=True ) def test_post_norway_tax_details_input_model_validation_failure() -> None: """Test PostNorwayTaxDetailsInput model.""" with pytest.raises(ValidationError) as exc_info: PostNorwayTaxDetailsInput.model_validate( dict( vendor_id='abc', business_name=111, last_name=222, is_vat_registered='abc', vat_number=333, is_resident_of_spanish_islands='FALSE', province=444, zip=5555, expiration_date='def', account_payee_id='id', account_tax_info_id=2, ) ) assert [e['loc'] for e in exc_info.value.errors()] == [ ('vendor_id',), ('business_name',), ('last_name',), ('is_vat_registered',), ('province',), ('zip',), ('country_of_tax_residence_code',), ('account_payee_id',), ('vat_number',), ] def test_post_uk_tax_details_input_model_validation_success() -> None: """Test PostUkTaxDetailsInput model.""" assert PostUkTaxDetailsInput.model_validate( dict( vendor_id='22', first_name='1st name', last_name='2nd name', business_number='BN1234', is_vat_registered='TRUE', vat_number='AB12345678', address1='123 Baldwin Ave.', address2='apt. 321', city='York', zip='12340', country_code='GBR', country_of_tax_residence_code='GBR', account_payee_id=15, account_tax_info_id=26, ) ) == PostUkTaxDetailsInput( vendor_id=22, business_name='1st name 2nd name', first_name='1st name', last_name='2nd name', business_number='BN1234', is_vat_registered=True, vat_number='AB12345678', address1='123 Baldwin Ave.', address2='apt. 321', city='York', zip='12340', country_code='GBR', country_of_tax_residence_code='GBR', account_payee_id=15, account_tax_info_id=26, ) def test_post_uk_tax_details_input_model_validation_tax_details() -> None: """Test PostUkTaxDetailsInput model getting account payee tax details.""" assert PostUkTaxDetailsInput( vendor_id=15, business_name='business name', business_number='BN1234', is_vat_registered=True, vat_number='AB12345678', address1='123 Baldwin Ave.', address2='apt. 321', province='ON', city='New york', zip='20002', country_code='CAN', country_of_tax_residence_code='GBR', ).get_account_payee_tax_details() == NewTaxDetails( vat_number='AB12345678', business_name='business name', business_number='BN1234', country_of_tax_residency_code='GBR', address=Address( country='CAN', address1='123 Baldwin Ave.', address2='apt. 321', city='New york', postal_code='20002', state='ON', ), ) def test_post_uk_tax_details_input_model_validation_tax_info() -> None: """Test PostUkTaxDetailsInput model getting account tax info.""" assert PostUkTaxDetailsInput( vendor_id=15, business_name='business name', is_vat_registered=True, vat_number='', country_code='USA', country_of_tax_residence_code='GBR', ).get_account_tax_info() == UpdateAccountTaxInfo( country_of_tax_residence='GBR', is_sba_signed=False ) def test_post_uk_tax_details_input_model_validation_failure() -> None: """Test PostUkTaxDetailsInput model.""" with pytest.raises(ValidationError) as exc_info: PostUkTaxDetailsInput.model_validate( dict( vendor_id='abc', business_name=111, business_number=11111, last_name=222, is_vat_registered='abc', vat_number=333, is_resident_of_spanish_islands='FALSE', province=444, zip=5555, expiration_date='def', account_payee_id='id', account_tax_info_id=2, ) ) assert [e['loc'] for e in exc_info.value.errors()] == [ ('vendor_id',), ('business_name',), ('last_name',), ('is_vat_registered',), ('province',), ('zip',), ('country_of_tax_residence_code',), ('account_payee_id',), ('vat_number',), ('business_number',), ] def test_post_german_tax_details_input_model_validation_success() -> None: """Test PostGermanTaxDetailsInput model.""" assert PostGermanTaxDetailsInput.model_validate( dict( vendor_id='33', first_name='1st name', last_name='2nd name', business_number='BN1234', is_vat_registered='TRUE', vat_number='GE12345678', local_tax_id='GE12345', tax_entity_type='BUSINESS', address1='112 Some Ave', address2='apt 221', city='Munich', zip='3210', country_code='DEU', country_of_tax_residence_code='DEU', account_payee_id=15, account_tax_info_id=26, ) ) == PostGermanTaxDetailsInput( vendor_id=33, business_name='1st name 2nd name', first_name='1st name', last_name='2nd name', is_vat_registered=True, vat_number='GE12345678', local_tax_id='GE12345', tax_employment_type='BUSINESS', address1='112 Some Ave', address2='apt 221', city='Munich', zip='3210', country_code='DEU', country_of_tax_residence_code='DEU', account_payee_id=15, account_tax_info_id=26, ) def test_post_german_tax_details_input_model_validation_tax_details() -> None: """Test PostGermanTaxDetailsInput model getting account payee tax details.""" assert PostGermanTaxDetailsInput( vendor_id=15, business_name='business name', is_vat_registered=False, vat_number='GE12345678', local_tax_id='GE12345', tax_employment_type='BUSINESS', address1='112 Some Ave', address2='apt 221', city='Munich', zip='3210', province='Region1', country_code='DEU', country_of_tax_residence_code='DEU', ).get_account_payee_tax_details() == NewTaxDetails( vat_number='GE12345678', local_tax_id='GE12345', business_name='business name', country_of_tax_residency_code='DEU', address=Address( address1='112 Some Ave', address2='apt 221', city='Munich', postal_code='3210', country='DEU', state='Region1', ), ) def test_post_german_tax_details_input_model_validation_tax_info() -> None: """Test PostGermanTaxDetailsInput model getting account tax info.""" assert PostGermanTaxDetailsInput( vendor_id=15, business_name='business name', is_vat_registered=True, vat_number='', local_tax_id='ABC', country_code='USA', country_of_tax_residence_code='DEU', tax_employment_type='IE', expiration_date=date(2025, 11, 5), wht_rate_override=22.51, ).get_account_tax_info() == UpdateAccountTaxInfo( country_of_tax_residence='DEU', is_sba_signed=False, tax_employment_type='IE', certificate_of_residence_expiration_date=date(2025, 11, 5), wht_rate_override=22.51, ) def test_post_german_tax_details_input_model_validation_failure() -> None: """Test PostGermanTaxDetailsInput model.""" with pytest.raises(ValidationError) as exc_info: PostGermanTaxDetailsInput.model_validate( dict( vendor_id='abc', business_name=111, last_name=222, is_vat_registered='abc', vat_number=333, is_resident_of_spanish_islands='FALSE', province=444, zip=5555, wht_rate_override='abc', expiration_date='def', account_payee_id='id', account_tax_info_id=2, tax_entity_type=123, ) ) assert [e['loc'] for e in exc_info.value.errors()] == [ ('vendor_id',), ('business_name',), ('last_name',), ('is_vat_registered',), ('province',), ('zip',), ('country_of_tax_residence_code',), ('account_payee_id',), ('vat_number',), ('local_tax_id',), ('tax_entity_type',), ('wht_rate_override',), ('expiration_date',), ] def test_new_tax_details_model_validation_success() -> None: """Test NewTaxDetails model.""" assert NewTaxDetails.model_validate( dict( local_tax_id='tax_id_1', vat_number='AB123', is_vat_registered=True, country_of_tax_residency_code='CAN', address=dict( country='US', address2='Apt 4B', city='Metropolis', state='MetroState', postal_code='12345', ), ) ) == NewTaxDetails( local_tax_id='tax_id_1', vat_number='AB123', is_vat_registered=True, business_name=None, country_of_tax_residency_code='CAN', address=Address( country='US', address1=None, address2='Apt 4B', city='Metropolis', state='MetroState', postal_code='12345', ), ) def test_new_tax_details_model_validation_failure() -> None: """Test NewTaxDetails model.""" with pytest.raises(ValidationError) as exc_info: NewTaxDetails.model_validate( dict( local_tax_id=123, is_vat_registered='abc', business_name=111, address=dict( country=111, address1='Apt 4B', address2=333, city=None, postal_code=444, ), ) ) assert [e['loc'] for e in exc_info.value.errors()] == [ ('business_name',), ('address', 'country'), ('address', 'address2'), ('address', 'postal_code'), ('local_tax_id',), ('is_vat_registered',), ] def test_tax_details_model_validation_success() -> None: """Test TaxDetails model.""" assert TaxDetails.model_validate( dict( account_payee_id=123, vat_number='vat_1', local_tax_id='tax_id_2', business_name='bus_1', country_of_tax_residency_code='CAN', address=dict( country='US', address1='City', address2='Apt 4B', city='Metropolis', state='MetroState', postal_code='12345', ), ) ) == TaxDetails( account_payee_id=123, vat_number='vat_1', local_tax_id='tax_id_2', business_name='bus_1', country_of_tax_residency_code='CAN', address=Address( country='US', address1='City', address2='Apt 4B', city='Metropolis', state='MetroState', postal_code='12345', ), ) def test_tax_details_model_validation_failure() -> None: """Test TaxDetails model.""" with pytest.raises(ValidationError) as exc_info: TaxDetails.model_validate( dict( account_payee_id='abc', vat_number=222, country_of_tax_residency_code='abc', address=dict( country=111, address1='Apt 4B', address2=333, city=None, ), ) ) assert [e['loc'] for e in exc_info.value.errors()] == [ ('address', 'country'), ('address', 'address2'), ('vat_number',), ('account_payee_id',), ] @pytest.mark.parametrize( 'data', ( {'country_of_tax_residence': 'AUS'}, { 'country_of_tax_residence': 'CAN', 'is_resident_of_spanish_islands': False, 'tax_employment_type': 't1', 'certificate_of_residence_expiration_date': date(2025, 1, 5), }, { 'country_of_tax_residence': 'CAN', 'is_resident_of_spanish_islands': None, 'certificate_of_residence_expiration_date': '05/01/2025', }, { 'is_resident_of_spanish_islands': True, 'tax_employment_type': 't2', 'certificate_of_residence_expiration_date': '2025-05-01', }, { 'country_of_tax_residence': 'GER', 'tax_employment_type': 't1', 'certificate_of_residence_expiration_date': date(2025, 2, 10), 'is_sba_signed': True, 'wht_rate_override': 15.47, }, ), ) def test_update_account_tax_info_model_validation_success(data: dict[str, str]) -> None: """Test UpdateAccountTaxInfo model.""" assert UpdateAccountTaxInfo.model_validate(data) == UpdateAccountTaxInfo(**data) def test_update_account_tax_info_model_validation_failure() -> None: """Test UpdateAccountTaxInfo model.""" with pytest.raises(ValidationError) as exc_info: UpdateAccountTaxInfo.model_validate( dict( country_of_tax_residence=111, is_resident_of_spanish_islands='abc', certificate_of_residence_expiration_date='date1', ) ) assert [e['loc'] for e in exc_info.value.errors()] == [ ('country_of_tax_residence',), ('is_resident_of_spanish_islands',), ('certificate_of_residence_expiration_date',), ] def test_tax_details_dataloader_item_model_empty() -> None: """Test TaxDetailsDataloaderItem model.""" assert TaxDetailsDataloaderItem.model_validate( {'account_payee_id': 105} ) == TaxDetailsDataloaderItem(account_payee_id=105) def test_tax_details_dataloader_item_model_full() -> None: """Test TaxDetailsDataloaderItem model.""" assert TaxDetailsDataloaderItem.model_validate( { 'account_payee_id': 105, 'vat_number': '12345', 'local_tax_id': '67890', 'business_name': 'Name1', 'business_number': 'number1', 'address': { 'address_1': 'street, apt', 'city': 'city1', 'zip': '0111', 'country_code': 'CAN', }, } ) == TaxDetailsDataloaderItem( account_payee_id=105, vat_number='12345', local_tax_id='67890', business_name='Name1', business_number='number1', address=TaxDetailsDataloaderItemAddress( address_1='street, apt', city='city1', zip='0111', country_code='CAN', ), ) def test_tax_details_dataloader_item_model_validation_failure() -> None: """Test TaxDetailsDataloaderItem model.""" with pytest.raises(ValidationError) as exc_info: TaxDetailsDataloaderItem.model_validate( { 'account_payee_id': 'aaa', 'vat_number': 123, 'local_tax_id': 456, 'business_name': 7, 'business_number': 89, 'address': { 'address_1': 12, 'zip': 11, }, } ) assert [e['loc'] for e in exc_info.value.errors()] == [ ('account_payee_id',), ('vat_number',), ('local_tax_id',), ('business_name',), ('business_number',), ('address', 'address_1'), ('address', 'zip'), ] class TestPayoneerPayoutMethod: """Tests for PayoneerPayoutMethod field validators.""" @pytest.mark.parametrize( ('raw', 'expected'), [ (1, '1'), (2, '2'), ('1', '1'), ('', ''), (None, ''), ], ) def test_bank_account_type_coerced_to_string( self, raw: object, expected: str ) -> None: """Payoneer occasionally sends the bank_account_type field as an int. The `_stringify` validator must normalise to str so the downstream lookup against `_PAYONEER_BANK_ACCOUNT_TYPE` ('1'/'2') matches. """ method = PayoneerPayoutMethod.model_validate({'bank_account_type': raw}) assert method.bank_account_type == expected def test_bank_field_details_none_coerced_to_empty_list(self) -> None: """Payoneer returns `bank_field_details: null` for some payees; the `_null_to_list` validator must coerce so iteration in the mapper does not crash on None. """ method = PayoneerPayoutMethod.model_validate({'bank_field_details': None}) assert method.bank_field_details == [] class TestPullBankingDetailsInputRow: """Tests for the PullBankingDetailsProcessor input-row contract.""" def test_date_of_birth_absent_is_none(self) -> None: row = PullBankingDetailsInputRow.model_validate( {'program_id': '1', 'payee_id': '2', 'vendor_id': '3'} ) assert row.date_of_birth is None def test_date_of_birth_blank_is_none(self) -> None: row = PullBankingDetailsInputRow.model_validate( { 'program_id': '1', 'payee_id': '2', 'vendor_id': '3', 'date_of_birth': '', } ) assert row.date_of_birth is None def test_date_of_birth_parses_yyyy_mm_dd(self) -> None: row = PullBankingDetailsInputRow.model_validate( { 'program_id': '1', 'payee_id': '2', 'vendor_id': '3', 'date_of_birth': '1985-04-12', } ) assert row.date_of_birth == date(1985, 4, 12) @pytest.mark.parametrize( 'bad', ['04/12/1985', '1985/04/12', '12-04-1985', 'not-a-date', '1985-13-01'], ) def test_date_of_birth_rejects_other_formats(self, bad: str) -> None: with pytest.raises(ValidationError) as exc_info: PullBankingDetailsInputRow.model_validate( { 'program_id': '1', 'payee_id': '2', 'vendor_id': '3', 'date_of_birth': bad, } ) assert ('date_of_birth',) in [e['loc'] for e in exc_info.value.errors()]