"""Tax forms models.""" from abc import ABC, abstractmethod from dataclasses import dataclass, field import datetime import typing from abacus_common_logic.models.base import BaseSoftDeleteModel, db from payee.connectors.secure_data.document import SecureDocument from payee.connectors.secure_data.fields.boolean_field import BooleanField from payee.connectors.secure_data.fields.text_field import TextField from payee.constants.constants import TAX_FORM_TYPES class TaxFormInfo(BaseSoftDeleteModel): """Account payee tax form information model.""" __tablename__ = 'account_payee_tax_form_info' account_payee_tax_form_info_id = db.Column(db.Integer, primary_key=True) revision_id = db.Column(db.String(36), nullable=True) tax_form_type = db.Column(db.Enum(*TAX_FORM_TYPES), nullable=False) account_payee_id = db.Column( db.Integer, db.ForeignKey('account_payee.account_payee_id'), nullable=False ) signed_date = db.Column(db.Date, nullable=True) expiration_date = db.Column(db.Date) @property def tax_form_document_class(self): return next( ( cls for cls in BaseUSTaxFormDocument.__subclasses__() if cls._document_type == self.tax_form_type ), None, ) @classmethod def get_by_id(cls, obj_id): """Get object from DB by ID property.""" return ( cls.query.filter(cls.account_payee_tax_form_info_id == obj_id) .filter(cls.deleted_at.is_(None)) .first() ) @classmethod def get_by_account_payee_id(cls, account_payee_id): """Get tax form info from DB by account payee ID property.""" return ( cls.query.filter(cls.account_payee_id == account_payee_id) .filter(cls.deleted_at.is_(None)) .first() ) @classmethod def delete_with_revision( cls, obj: typing.Self, revision_id: str, commit=True ) -> None: """Load an object by id, then soft delete it. Aborts if the object is not found. Commit by default. """ obj._soft_delete() obj.revision_id = revision_id if commit: db.session.commit() @dataclass class TaxFormInfoDetails: """Utility dataclass to present full tax form data including secure details.""" account_payee_tax_form_info_id: int tax_form_type: str account_payee_id: int signed_date: typing.Optional[datetime.date] expiration_date: typing.Optional[datetime.date] created_at: datetime.datetime last_modified: datetime.datetime created_by: str last_modified_by: str deleted_at: typing.Optional[datetime.datetime] deleted_by: typing.Optional[str] # no repr as this data should not be exposed somewhere in logs details: dict[str, typing.Any] = field(repr=False) def __getattr__(self, attr): if attr not in self.details_fields: raise AttributeError(attr) return self.details.get(attr) if self.details else None @property def details_fields(self): return [ key for cls in BaseUSTaxFormDocument.__subclasses__() for key in cls._fields.keys() if cls._document_type == self.tax_form_type ] class BaseUSTaxFormDocument(SecureDocument, ABC): """Base Tax Form Document.""" _fields = { 'tax_id_country': TextField('tax_id_country', encrypt=True, required=True), 'tin_type': TextField('tin_type', encrypt=True, required=False), 'tin': TextField('tin', encrypt=True, required=False), 'tax_name': TextField('tax_name', encrypt=True, required=True), 'modified_by_profile_type': TextField( 'modified_by_profile_type', encrypt=True, required=True ), 'modified_by_profile_id': TextField( 'modified_by_profile_id', encrypt=True, required=True ), 'modified_by_identity': TextField( 'modified_by_identity', encrypt=True, required=True ), 'modified_at': TextField('modified_at', encrypt=True, required=False), 'revision': TextField('revision', encrypt=False, required=False), } @property @abstractmethod def _document_type(self): pass @classmethod def build_response( cls, payee_id, secure_document_item: SecureDocument, obscure_pii=False ) -> dict | None: """Format the response given the raw data.""" # Added to prevent failure when DynamoDB is not in sync with prod data if secure_document_item is None: return None revision = ( secure_document_item.get_value('revision') if secure_document_item.field_exists('revision') else None ) message = { 'account_payee_id': payee_id, 'tax_form_type': cls.document_type(), 'modified_by_profile_type': secure_document_item.get_value( 'modified_by_profile_type' ) if secure_document_item.field_exists('modified_by_profile_type') else None, 'modified_by_profile_id': secure_document_item.get_value( 'modified_by_profile_id' ) if secure_document_item.field_exists('modified_by_profile_id') else None, 'modified_by_identity': secure_document_item.get_value( 'modified_by_identity' ) if secure_document_item.field_exists('modified_by_identity') else None, 'modified_at': secure_document_item.get_value('modified_at'), 'revision': secure_document_item.get_value('revision').split('#')[1] if revision else '0', 'tax_id_country': secure_document_item.get_value('tax_id_country'), 'tin_type': secure_document_item.get_value('tin_type'), 'tin': secure_document_item.get_value('tin', is_obscured=obscure_pii), 'tax_name': secure_document_item.get_value('tax_name'), } return message class USTaxFormW9Document(BaseUSTaxFormDocument): """W-9 Tax Form.""" _document_type = TAX_FORM_TYPES.W9 _fields = { **BaseUSTaxFormDocument._fields, 'tax_classification': TextField( 'tax_classification', encrypt=True, required=True ), } @classmethod def build_response( cls, payee_id, secure_document_item: SecureDocument, obscure_pii=False ) -> dict | None: # Added to prevent failure when DynamoDB is not in sync with prod data if secure_document_item is None: return None message = { **super().build_response(payee_id, secure_document_item, obscure_pii), 'tax_classification': secure_document_item.get_value('tax_classification'), } return message class USTaxFormW8BENDocument(BaseUSTaxFormDocument): """W-8BEN Tax Form.""" _document_type = TAX_FORM_TYPES.W8BEN _fields = { **BaseUSTaxFormDocument._fields, 'tax_treaty_claim': BooleanField( 'tax_treaty_claim', encrypt=True, required=True ), } @classmethod def build_response( cls, payee_id, secure_document_item: SecureDocument, obscure_pii=False ) -> dict | None: # Added to prevent failure when DynamoDB is not in sync with prod data if secure_document_item is None: return None message = { **super().build_response(payee_id, secure_document_item, obscure_pii), 'tax_treaty_claim': secure_document_item.get_value('tax_treaty_claim') or False, } return message class USTaxFormW8BENEDocument(BaseUSTaxFormDocument): """W-8BEN-E Tax Form.""" _document_type = TAX_FORM_TYPES.W8BENE _fields = { **BaseUSTaxFormDocument._fields, 'tax_treaty_claim': BooleanField( 'tax_treaty_claim', encrypt=True, required=True ), 'type_of_entity': TextField('type_of_entity', encrypt=True, required=True), 'lob': TextField('lob', encrypt=True, required=False), } @classmethod def build_response( cls, payee_id, secure_document_item: SecureDocument, obscure_pii=False ) -> dict | None: # Added to prevent failure when DynamoDB is not in sync with prod data if secure_document_item is None: return None message = { **super().build_response(payee_id, secure_document_item, obscure_pii), 'tax_treaty_claim': secure_document_item.get_value('tax_treaty_claim') or False, 'type_of_entity': secure_document_item.get_value('type_of_entity'), 'lob': secure_document_item.get_value('lob'), } return message class USTaxFormW8IMYDocument(BaseUSTaxFormDocument): """W-8IMY Tax Form.""" _document_type = TAX_FORM_TYPES.W8IMY _fields = { **BaseUSTaxFormDocument._fields, 'tax_treaty_claim': BooleanField( 'tax_treaty_claim', encrypt=True, required=True ), 'type_of_entity': TextField('type_of_entity', encrypt=True, required=True), } @classmethod def build_response( cls, payee_id, secure_document_item: SecureDocument, obscure_pii=False ) -> dict | None: # Added to prevent failure when DynamoDB is not in sync with prod data if secure_document_item is None: return None message = { **super().build_response(payee_id, secure_document_item, obscure_pii), 'tax_treaty_claim': secure_document_item.get_value('tax_treaty_claim') or False, 'type_of_entity': secure_document_item.get_value('type_of_entity'), } return message class USTaxFormW8ECIDocument(BaseUSTaxFormDocument): """W-8ECI Tax Form.""" _document_type = TAX_FORM_TYPES.W8ECI _fields = { **BaseUSTaxFormDocument._fields, 'tax_treaty_claim': BooleanField( 'tax_treaty_claim', encrypt=True, required=True ), 'type_of_entity': TextField('type_of_entity', encrypt=True, required=True), } @classmethod def build_response( cls, payee_id, secure_document_item: SecureDocument, obscure_pii=False ) -> dict | None: # Added to prevent failure when DynamoDB is not in sync with prod data if secure_document_item is None: return None message = { **super().build_response(payee_id, secure_document_item, obscure_pii), 'tax_treaty_claim': secure_document_item.get_value('tax_treaty_claim') or False, 'type_of_entity': secure_document_item.get_value('type_of_entity'), } return message