From 8f8e7203cea139e869d7cc2df94977403997220c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Cabrera?= Date: Fri, 15 May 2026 12:34:16 -0600 Subject: [PATCH 1/5] Update version to 2.1.31.dev0 and add FraudFundsTransferRequest, FraudFundsTransferAcceptedResponse, and FraudFundsTransferResultEvent models with validation logic and tests. --- cuenca_validations/types/__init__.py | 6 ++ cuenca_validations/types/requests.py | 67 +++++++++++++++++++- cuenca_validations/version.py | 2 +- tests/test_types.py | 93 ++++++++++++++++++++++++++++ 4 files changed, 165 insertions(+), 3 deletions(-) diff --git a/cuenca_validations/types/__init__.py b/cuenca_validations/types/__init__.py index 4fbd542b..0c598f3e 100644 --- a/cuenca_validations/types/__init__.py +++ b/cuenca_validations/types/__init__.py @@ -44,6 +44,9 @@ 'FileBatchUploadRequest', 'FileRequest', 'FileUploadRequest', + 'FraudFundsTransferAcceptedResponse', + 'FraudFundsTransferRequest', + 'FraudFundsTransferResultEvent', 'Gender', 'IncomeType', 'IssuerNetwork', @@ -230,6 +233,9 @@ FileBatchUploadRequest, FileRequest, FileUploadRequest, + FraudFundsTransferAcceptedResponse, + FraudFundsTransferRequest, + FraudFundsTransferResultEvent, KYCValidationRequest, LimitedWalletRequest, PartnerRequest, diff --git a/cuenca_validations/types/requests.py b/cuenca_validations/types/requests.py index 93c215b6..d3a59dee 100644 --- a/cuenca_validations/types/requests.py +++ b/cuenca_validations/types/requests.py @@ -1,7 +1,7 @@ import datetime as dt -from typing import Annotated, Any, Optional, Union +from typing import Annotated, Any, Literal, Optional, Union -from clabe import Clabe +from clabe import Clabe, validate_clabe from pydantic import ( BaseModel, ConfigDict, @@ -309,6 +309,69 @@ class WalletTransactionRequest(BaseRequest): amount: StrictPositiveInt +class FraudFundsTransferRequest(BaseRequest): + user_id: NonEmptyStr + clabe: Clabe + concepto: NonEmptyStr + amount: Optional[StrictPositiveInt] = None + reason: Optional[NonEmptyStr] = None + request_id: Optional[NonEmptyStr] = None + requested_by: Optional[NonEmptyStr] = None + + @field_validator('clabe', mode='before') + @classmethod + def validate_clabe_format(cls, clabe: str) -> str: + if not validate_clabe(clabe): + raise ValueError('La CLABE ingresada no es valida') + return clabe + + +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..5794c504 100644 --- a/cuenca_validations/version.py +++ b/cuenca_validations/version.py @@ -1 +1 @@ -__version__ = '2.1.32' +__version__ = '2.1.32.dev0' diff --git a/tests/test_types.py b/tests/test_types.py index 3282e59b..2685433e 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,96 @@ 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, + request_id='REQ123', + ) + + assert request.concepto == 'fondos fraude' + assert request.model_dump() == { + 'user_id': 'US123', + 'clabe': '646180157098510917', + 'concepto': 'fondos fraude', + 'amount': 100, + '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' + + +def test_fraud_funds_transfer_request_invalid_clabe(): + with pytest.raises(ValidationError) as ex: + FraudFundsTransferRequest( + user_id='US123', + clabe='not-a-clabe', + concepto='fondos fraude', + ) + + assert 'La CLABE ingresada no es valida' in str(ex.value) + + +@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', [ From 025c405159a701f1a56d568ed8a57fed62656ca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Cabrera?= Date: Fri, 15 May 2026 13:12:10 -0600 Subject: [PATCH 2/5] Refactor FraudFundsTransferRequest and FraudFundsTransferResultEvent models to use union types for optional fields and update the validate_payload method to return Self. --- cuenca_validations/types/requests.py | 20 +++++++++++--------- cuenca_validations/version.py | 2 +- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/cuenca_validations/types/requests.py b/cuenca_validations/types/requests.py index d3a59dee..cbfb6fca 100644 --- a/cuenca_validations/types/requests.py +++ b/cuenca_validations/types/requests.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime as dt from typing import Annotated, Any, Literal, Optional, Union @@ -313,10 +315,10 @@ class FraudFundsTransferRequest(BaseRequest): user_id: NonEmptyStr clabe: Clabe concepto: NonEmptyStr - amount: Optional[StrictPositiveInt] = None - reason: Optional[NonEmptyStr] = None - request_id: Optional[NonEmptyStr] = None - requested_by: Optional[NonEmptyStr] = None + amount: StrictPositiveInt | None = None + reason: NonEmptyStr | None = None + request_id: NonEmptyStr | None = None + requested_by: NonEmptyStr | None = None @field_validator('clabe', mode='before') @classmethod @@ -339,11 +341,11 @@ class FraudFundsTransferResultEvent(BaseRequest): ] 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 + transaction_id: NonEmptyStr | None = None + amount: StrictPositiveInt | None = None + clave_rastreo: NonEmptyStr | None = None + reason_code: NonEmptyStr | None = None + message: NonEmptyStr | None = None completed_at: dt.datetime @model_validator(mode='after') diff --git a/cuenca_validations/version.py b/cuenca_validations/version.py index 5794c504..4acdb8c8 100644 --- a/cuenca_validations/version.py +++ b/cuenca_validations/version.py @@ -1 +1 @@ -__version__ = '2.1.32.dev0' +__version__ = '2.1.32.dev1' From c73f0553a8874cacd84ff6cab9828cb6b5ef0580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Cabrera?= Date: Fri, 15 May 2026 13:34:42 -0600 Subject: [PATCH 3/5] Update version to 2.1.32.dev2 and refactor FraudFundsTransferRequest and FraudFundsTransferResultEvent models to use Optional for fields, removing validation logic for CLABE format. --- cuenca_validations/types/requests.py | 29 ++++++++++------------------ cuenca_validations/version.py | 2 +- tests/test_types.py | 11 ----------- 3 files changed, 11 insertions(+), 31 deletions(-) diff --git a/cuenca_validations/types/requests.py b/cuenca_validations/types/requests.py index cbfb6fca..74b4e220 100644 --- a/cuenca_validations/types/requests.py +++ b/cuenca_validations/types/requests.py @@ -1,9 +1,7 @@ -from __future__ import annotations - import datetime as dt from typing import Annotated, Any, Literal, Optional, Union -from clabe import Clabe, validate_clabe +from clabe import Clabe from pydantic import ( BaseModel, ConfigDict, @@ -315,17 +313,10 @@ class FraudFundsTransferRequest(BaseRequest): user_id: NonEmptyStr clabe: Clabe concepto: NonEmptyStr - amount: StrictPositiveInt | None = None - reason: NonEmptyStr | None = None - request_id: NonEmptyStr | None = None - requested_by: NonEmptyStr | None = None - - @field_validator('clabe', mode='before') - @classmethod - def validate_clabe_format(cls, clabe: str) -> str: - if not validate_clabe(clabe): - raise ValueError('La CLABE ingresada no es valida') - return clabe + amount: Optional[StrictPositiveInt] = None + reason: Optional[NonEmptyStr] = None + request_id: Optional[NonEmptyStr] = None + requested_by: Optional[NonEmptyStr] = None class FraudFundsTransferAcceptedResponse(BaseRequest): @@ -341,11 +332,11 @@ class FraudFundsTransferResultEvent(BaseRequest): ] request_id: NonEmptyStr user_id: NonEmptyStr - transaction_id: NonEmptyStr | None = None - amount: StrictPositiveInt | None = None - clave_rastreo: NonEmptyStr | None = None - reason_code: NonEmptyStr | None = None - message: NonEmptyStr | None = None + 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') diff --git a/cuenca_validations/version.py b/cuenca_validations/version.py index 4acdb8c8..289b5147 100644 --- a/cuenca_validations/version.py +++ b/cuenca_validations/version.py @@ -1 +1 @@ -__version__ = '2.1.32.dev1' +__version__ = '2.1.32.dev2' diff --git a/tests/test_types.py b/tests/test_types.py index 2685433e..51344633 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -690,17 +690,6 @@ def test_fraud_funds_transfer_models(): assert failed_event.reason_code == 'insufficient_funds' -def test_fraud_funds_transfer_request_invalid_clabe(): - with pytest.raises(ValidationError) as ex: - FraudFundsTransferRequest( - user_id='US123', - clabe='not-a-clabe', - concepto='fondos fraude', - ) - - assert 'La CLABE ingresada no es valida' in str(ex.value) - - @pytest.mark.parametrize( 'event_type,expected_error', [ From 319379ba4aae2dd4c89dcd6800cbaca9e75175ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Cabrera?= Date: Fri, 15 May 2026 15:54:15 -0600 Subject: [PATCH 4/5] Refactor FraudFundsTransferRequest to make reason and request_id required fields, and update related tests accordingly. --- cuenca_validations/types/requests.py | 5 ++--- cuenca_validations/version.py | 2 +- tests/test_types.py | 2 ++ 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cuenca_validations/types/requests.py b/cuenca_validations/types/requests.py index 74b4e220..7adb3c57 100644 --- a/cuenca_validations/types/requests.py +++ b/cuenca_validations/types/requests.py @@ -314,9 +314,8 @@ class FraudFundsTransferRequest(BaseRequest): clabe: Clabe concepto: NonEmptyStr amount: Optional[StrictPositiveInt] = None - reason: Optional[NonEmptyStr] = None - request_id: Optional[NonEmptyStr] = None - requested_by: Optional[NonEmptyStr] = None + reason: NonEmptyStr + request_id: NonEmptyStr class FraudFundsTransferAcceptedResponse(BaseRequest): diff --git a/cuenca_validations/version.py b/cuenca_validations/version.py index 289b5147..49bbcc08 100644 --- a/cuenca_validations/version.py +++ b/cuenca_validations/version.py @@ -1 +1 @@ -__version__ = '2.1.32.dev2' +__version__ = '2.1.32.dev3' diff --git a/tests/test_types.py b/tests/test_types.py index 51344633..9a23aee3 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -645,6 +645,7 @@ def test_fraud_funds_transfer_models(): clabe='646180157098510917', concepto=' fondos fraude ', amount=100, + reason='fraud_report', request_id='REQ123', ) @@ -654,6 +655,7 @@ def test_fraud_funds_transfer_models(): 'clabe': '646180157098510917', 'concepto': 'fondos fraude', 'amount': 100, + 'reason': 'fraud_report', 'request_id': 'REQ123', } From 15736df00cc0a64862195cce8adbbfb8ded2af4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Cabrera?= Date: Wed, 20 May 2026 14:30:28 -0600 Subject: [PATCH 5/5] Add FraudFundsTransferQuery class and update imports in __init__.py and queries.py --- cuenca_validations/types/__init__.py | 2 ++ cuenca_validations/types/queries.py | 6 ++++++ cuenca_validations/version.py | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/cuenca_validations/types/__init__.py b/cuenca_validations/types/__init__.py index 0c598f3e..7c89b9a4 100644 --- a/cuenca_validations/types/__init__.py +++ b/cuenca_validations/types/__init__.py @@ -45,6 +45,7 @@ 'FileRequest', 'FileUploadRequest', 'FraudFundsTransferAcceptedResponse', + 'FraudFundsTransferQuery', 'FraudFundsTransferRequest', 'FraudFundsTransferResultEvent', 'Gender', @@ -210,6 +211,7 @@ DepositQuery, EventQuery, FileQuery, + FraudFundsTransferQuery, IdentityQuery, PostalCodeQuery, QueryParams, 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/version.py b/cuenca_validations/version.py index 49bbcc08..f8b2d782 100644 --- a/cuenca_validations/version.py +++ b/cuenca_validations/version.py @@ -1 +1 @@ -__version__ = '2.1.32.dev3' +__version__ = '2.1.33.dev0'