diff --git a/cuenca_validations/types/__init__.py b/cuenca_validations/types/__init__.py index 4fbd542b..7c89b9a4 100644 --- a/cuenca_validations/types/__init__.py +++ b/cuenca_validations/types/__init__.py @@ -44,6 +44,10 @@ 'FileBatchUploadRequest', 'FileRequest', 'FileUploadRequest', + 'FraudFundsTransferAcceptedResponse', + 'FraudFundsTransferQuery', + 'FraudFundsTransferRequest', + 'FraudFundsTransferResultEvent', 'Gender', 'IncomeType', 'IssuerNetwork', @@ -207,6 +211,7 @@ DepositQuery, EventQuery, FileQuery, + FraudFundsTransferQuery, IdentityQuery, PostalCodeQuery, QueryParams, @@ -230,6 +235,9 @@ FileBatchUploadRequest, FileRequest, FileUploadRequest, + FraudFundsTransferAcceptedResponse, + FraudFundsTransferRequest, + FraudFundsTransferResultEvent, KYCValidationRequest, LimitedWalletRequest, PartnerRequest, diff --git a/cuenca_validations/types/queries.py b/cuenca_validations/types/queries.py index 15fd8680..a3617183 100644 --- a/cuenca_validations/types/queries.py +++ b/cuenca_validations/types/queries.py @@ -24,6 +24,7 @@ KYCFileType, SessionType, TermsOfService, + TransactionStatus, TransferNetwork, UserStatus, ) @@ -153,6 +154,11 @@ class WalletTransactionQuery(QueryParams): wallet_uri: Optional[str] = None +class FraudFundsTransferQuery(QueryParams): + request_id: Optional[str] = None + status: Optional[TransactionStatus] = None + + class UserQuery(QueryParams): phone_number: Optional[str] = None email_address: Optional[EmailStr] = None diff --git a/cuenca_validations/types/requests.py b/cuenca_validations/types/requests.py index 93c215b6..7adb3c57 100644 --- a/cuenca_validations/types/requests.py +++ b/cuenca_validations/types/requests.py @@ -1,5 +1,5 @@ import datetime as dt -from typing import Annotated, Any, Optional, Union +from typing import Annotated, Any, Literal, Optional, Union from clabe import Clabe from pydantic import ( @@ -309,6 +309,61 @@ class WalletTransactionRequest(BaseRequest): amount: StrictPositiveInt +class FraudFundsTransferRequest(BaseRequest): + user_id: NonEmptyStr + clabe: Clabe + concepto: NonEmptyStr + amount: Optional[StrictPositiveInt] = None + reason: NonEmptyStr + request_id: NonEmptyStr + + +class FraudFundsTransferAcceptedResponse(BaseRequest): + request_id: NonEmptyStr + status: Literal['queued'] + + +class FraudFundsTransferResultEvent(BaseRequest): + schema_version: NonEmptyStr + event_type: Literal[ + 'fraud_funds_transfer.succeeded', + 'fraud_funds_transfer.failed', + ] + request_id: NonEmptyStr + user_id: NonEmptyStr + transaction_id: Optional[NonEmptyStr] = None + amount: Optional[StrictPositiveInt] = None + clave_rastreo: Optional[NonEmptyStr] = None + reason_code: Optional[NonEmptyStr] = None + message: Optional[NonEmptyStr] = None + completed_at: dt.datetime + + @model_validator(mode='after') + def validate_payload(self) -> 'FraudFundsTransferResultEvent': + if self.event_type == 'fraud_funds_transfer.succeeded': + required = { + 'transaction_id': self.transaction_id, + 'amount': self.amount, + } + missing = [ + field for field, value in required.items() if value is None + ] + if missing: + raise ValueError( + f'{", ".join(missing)} required for succeeded event' + ) + return self + + required = { + 'reason_code': self.reason_code, + 'message': self.message, + } + missing = [field for field, value in required.items() if value is None] + if missing: + raise ValueError(f'{", ".join(missing)} required for failed event') + return self + + class FraudValidationRequest(BaseModel): amount: StrictPositiveInt merchant_name: str diff --git a/cuenca_validations/version.py b/cuenca_validations/version.py index 2d93b16b..f8b2d782 100644 --- a/cuenca_validations/version.py +++ b/cuenca_validations/version.py @@ -1 +1 @@ -__version__ = '2.1.32' +__version__ = '2.1.33.dev0' diff --git a/tests/test_types.py b/tests/test_types.py index 3282e59b..9a23aee3 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -12,6 +12,9 @@ from cuenca_validations.types import ( CardQuery, + FraudFundsTransferAcceptedResponse, + FraudFundsTransferRequest, + FraudFundsTransferResultEvent, JSONEncoder, QueryParams, SantizedDict, @@ -636,6 +639,87 @@ def test_bank_account_validation_clabe_request(): assert BankAccountValidationRequest(account_number='646180157098510917') +def test_fraud_funds_transfer_models(): + request = FraudFundsTransferRequest( + user_id='US123', + clabe='646180157098510917', + concepto=' fondos fraude ', + amount=100, + reason='fraud_report', + request_id='REQ123', + ) + + assert request.concepto == 'fondos fraude' + assert request.model_dump() == { + 'user_id': 'US123', + 'clabe': '646180157098510917', + 'concepto': 'fondos fraude', + 'amount': 100, + 'reason': 'fraud_report', + 'request_id': 'REQ123', + } + + response = FraudFundsTransferAcceptedResponse( + request_id='REQ123', + status='queued', + ) + + assert response.status == 'queued' + + succeeded_event = FraudFundsTransferResultEvent( + schema_version='1.0', + event_type='fraud_funds_transfer.succeeded', + request_id='REQ123', + user_id='US123', + transaction_id='TR123', + amount=100, + clave_rastreo='RASTREO123', + completed_at=now, + ) + + assert succeeded_event.transaction_id == 'TR123' + + failed_event = FraudFundsTransferResultEvent( + schema_version='1.0', + event_type='fraud_funds_transfer.failed', + request_id='REQ123', + user_id='US123', + reason_code='insufficient_funds', + message='Insufficient funds', + completed_at=now, + ) + + assert failed_event.reason_code == 'insufficient_funds' + + +@pytest.mark.parametrize( + 'event_type,expected_error', + [ + ( + 'fraud_funds_transfer.succeeded', + 'transaction_id, amount required for succeeded event', + ), + ( + 'fraud_funds_transfer.failed', + 'reason_code, message required for failed event', + ), + ], +) +def test_fraud_funds_transfer_result_event_requires_payload( + event_type, expected_error +): + with pytest.raises(ValidationError) as ex: + FraudFundsTransferResultEvent( + schema_version='1.0', + event_type=event_type, + request_id='REQ123', + user_id='US123', + completed_at=now, + ) + + assert expected_error in str(ex.value) + + @pytest.mark.parametrize( 'input_data', [