"""Tax Details schema.""" from typing import Any from abacus_common_logic.marshalling.custom_fields import ma from dateutil.parser import parse from flask import request from marshmallow import post_load, pre_load, validates_schema, ValidationError from payee.connectors import sentry, uk_gov from payee.connectors.vies_api import Vies from payee.constants.constants import ( FORCE_BYPASS_VAT_TRUE, KNR_DUMMY_VAT, KNR_VAT_PREFIXES_OMIT_VALIDATION, PAYMENT_ENTITIES, REQUIRED_FIELD_MESSAGE, UNKNOWN_FIELD_MESSAGE, VAT_NUMBER_REGEXPS, ) from payee.constants.error import ( ERROR_VAT_API_UNREACHABLE, ERROR_VAT_INVALID_COUNTRY_CODE, ) from payee.models.reference_payment_entity import ReferencePaymentEntity from payee.schemas.address import AddressSchema, NonUSAddressSchema from payee.utils.exception import HmrcApiException, ViesApiException from payee.utils.validations import ( validate_country_code, validate_vat_bypass_validation_role, ) class KnrAddressSchema(AddressSchema): """Knr Address schema.""" province = ma.String(required=True) def get_tax_details_schema_for_corp_entity( corp_entity_or_id: str | int, ) -> ma.Schema | None: """Get the correct tax details schema to validate against. this function returns the correct schema based on which corporate entity they user is signed to and other data (like if they are VAT registered in the UK) """ if isinstance(corp_entity_or_id, int): payment_entity = ReferencePaymentEntity.get_by_id(corp_entity_or_id) corp_entity = payment_entity.payment_entity_name if payment_entity else None else: corp_entity = corp_entity_or_id if corp_entity in (PAYMENT_ENTITIES.AWAL_UK, PAYMENT_ENTITIES.ORCHARD_UK): return UkTaxDetailsSchema if corp_entity == PAYMENT_ENTITIES.ORCHARD_NO: return NOTaxDetailsSchema if corp_entity in [ PAYMENT_ENTITIES.KNR_NL, PAYMENT_ENTITIES.KNR_UK, ]: return KnrTaxDetailsSchema if corp_entity == PAYMENT_ENTITIES.ORCHARD_ES: return ESTaxDetailsSchema if corp_entity == PAYMENT_ENTITIES.ORCHARD_DE: return DETaxDetailsSchema return None class TaxDetailsSchema(ma.Schema): """Tax Details schema.""" country_of_tax_residency_code = ma.String( required=False, allow_none=True, validate=validate_country_code ) vat_number = ma.String(required=False, allow_none=True) address = ma.Nested(AddressSchema, allow_none=True) business_name = ma.String(required=False) business_number = ma.String(required=False) @pre_load def change_empty_address_to_none(self, data, **kwargs): """Preprocess input data.""" if 'address' in data and not data['address']: data['address'] = None return data def reverse_validate(self, data, *args, **kwargs): """ Method for reverse validation to exclude None fields for fields where None is not allowed. For cases like business_name where it is not required in input data, but we get it back from secure document as None and it should be omitted. """ exclude_none = [ name for name, field in self._declared_fields.items() if not getattr(field, 'allow_none', False) ] data = {k: v for k, v in data.items() if v is not None or k not in exclude_none} return super().validate(data, *args, **kwargs) class VatTaxDetailsSchema(TaxDetailsSchema): """Tax Details with VAT schema.""" @pre_load def change_empty_address_to_none(self, data, **kwargs): """Preprocess input data.""" if 'address' in data and not data['address']: data['address'] = None return data @validates_schema def validate_non_vat(self, data, **kwargs): """Additional non-VAT data validation.""" if data.get('vat_number') is None: errors = {} if 'address' in data and data['address'] is not None: errors['address'] = UNKNOWN_FIELD_MESSAGE if 'business_number' in data: errors['business_number'] = UNKNOWN_FIELD_MESSAGE if 'business_name' in data: errors['business_name'] = UNKNOWN_FIELD_MESSAGE if errors: raise ValidationError(errors) class UkTaxDetailsSchema(VatTaxDetailsSchema): """ Tax Details schema. This schema is for clients signed to the UK entity who are VAT registered in the UK. """ address = ma.Nested(NonUSAddressSchema, allow_none=True) @validates_schema def validate_vat(self, data, **kwargs): """Additional VAT data validation.""" if data.get('vat_number') is not None: vat = data['vat_number'] force_bypass_vat = request.args.get('force_bypass_vat') errors = {} if not data.get('address'): errors['address'] = REQUIRED_FIELD_MESSAGE if 'business_name' not in data or not data['business_name']: errors['business_name'] = REQUIRED_FIELD_MESSAGE try: uk_gov.validate_vat_number(vat) except HmrcApiException: if ( force_bypass_vat and force_bypass_vat == FORCE_BYPASS_VAT_TRUE and validate_vat_bypass_validation_role(request) ): sentry.send_to_sentry( f'VAT:`{vat}` was bypassed API check.', 'Bypass VAT validation!', 'warning', f'VAT:`{vat}` was bypassed API check.', ) else: raise ValidationError( field_name='vat_number', message=ERROR_VAT_API_UNREACHABLE, ) except ValidationError as e: raise ValidationError(field_name='vat_number', message=str(e)) if errors: raise ValidationError(field_name='vat_number', message=errors) class NOTaxDetailsSchema(VatTaxDetailsSchema): """Norwegian tax details schema.""" address = ma.Nested(NonUSAddressSchema, allow_none=True) class WFormSchema(ma.Schema): """WForm schema.""" w_form_expiry = ma.String( required=False, allow_none=True, metadata={'allow_blank': True} ) w_form_type = ma.String( required=False, allow_none=True, metadata={'allow_blank': True} ) class KnrTaxDetailsSchema(TaxDetailsSchema): """ KNR accounts Tax Details schema. This schema is for KNR clients. """ address = ma.Nested(KnrAddressSchema, allow_none=False) @validates_schema def validate_vat(self, data, **kwargs): """Additional KNR VAT data validation.""" if data.get('vat_number') is not None: vat = data['vat_number'] force_bypass_vat = request.args.get('force_bypass_vat') if KNR_DUMMY_VAT == vat: return True if any( vat.upper().startswith(prefix) for prefix in KNR_VAT_PREFIXES_OMIT_VALIDATION ): return True try: vat_country_code = vat[:2] except Exception: raise ValidationError( field_name='vat_number', message=ERROR_VAT_INVALID_COUNTRY_CODE ) vat_country_code = vat_country_code.upper() try: if vat_country_code in VAT_NUMBER_REGEXPS: Vies().request(vat_number=vat) elif vat_country_code == 'GB': uk_gov.validate_vat_number(vat) else: raise ValidationError( field_name='vat_number', message=ERROR_VAT_INVALID_COUNTRY_CODE ) except (ViesApiException, HmrcApiException): if ( force_bypass_vat and force_bypass_vat == FORCE_BYPASS_VAT_TRUE and validate_vat_bypass_validation_role(request) ): sentry.send_to_sentry( f'VAT:`{vat}` was bypassed API check.', 'Bypass VAT validation!', 'warning', f'VAT:`{vat}` was bypassed API check.', ) else: raise ValidationError( field_name='vat_number', message=ERROR_VAT_API_UNREACHABLE, ) except ValidationError as e: raise ValidationError(field_name='vat_number', message=str(e)) if 'address' not in data: raise ValidationError({'address': REQUIRED_FIELD_MESSAGE}) if 'business_name' not in data or not data['business_name']: raise ValidationError({'business_name': REQUIRED_FIELD_MESSAGE}) @validates_schema def validate_non_vat(self, data, **kwargs): """Additional non-VAT data validation.""" if data.get('vat_number') is None: errors = {} if 'address' not in data or not data['address']: errors['address'] = REQUIRED_FIELD_MESSAGE if errors: raise ValidationError(errors) class KnrDetailsSchema(ma.Schema): """ KNR accounts Details schema. This schema is for KNR clients. """ @pre_load def format_date_field(self, data, **kwargs): """Preprocess input data.""" if ( 'w_form' in data and 'w_form_expiry' in data['w_form'] and data['w_form']['w_form_expiry'] ): expiry_parsed_date = parse(data['w_form']['w_form_expiry']) data['w_form']['w_form_expiry'] = expiry_parsed_date.strftime('%Y-%m-%d') return data w_form = ma.Nested( WFormSchema, required=False, allow_none=True, metadata={'allow_blank': True} ) sap_vendor_id = ma.NonNegativeInteger( required=False, allow_none=True, metadata={'allow_blank': True} ) class ESTaxDetailsSchema(TaxDetailsSchema): """ Spanish tax details schema. """ local_tax_id = ma.String(required=True) is_vat_registered = ma.Boolean(required=True) address = ma.Nested(NonUSAddressSchema, allow_none=True) @post_load def populate_values(self, data: dict[str, Any], **kwargs) -> dict[str, Any]: """Process final data.""" if data.get('is_vat_registered'): data['vat_number'] = f'ES{data["local_tax_id"]}' else: data['vat_number'] = None if not data.get('country_of_tax_residency_code'): data['country_of_tax_residency_code'] = 'ESP' return data @validates_schema def validate_is_vat_registered(self, data: dict[str, Any], **kwargs) -> None: """Additional VAT registration data validation.""" errors = {} if data.get('is_vat_registered') and not data.get('address'): errors['address'] = REQUIRED_FIELD_MESSAGE elif not data.get('is_vat_registered') and data.get('address'): errors['address'] = UNKNOWN_FIELD_MESSAGE if errors: raise ValidationError(errors) def reverse_validate(self, data, *args, **kwargs): """Add is is_vat_registered back that is not stored but used only for validation.""" data['is_vat_registered'] = bool(data.get('vat_number')) return super().reverse_validate(data, *args, **kwargs) class DETaxDetailsSchema(TaxDetailsSchema): """ German tax details schema. """ local_tax_id = ma.String(required=True) address = ma.Nested(NonUSAddressSchema, allow_none=True) class ValidateTaxDetailsParamsSchema(ma.Schema): """Validate tax details params schema.""" payment_entity_id = ma.NonNegativeInteger(required=False, allow_none=True)