diff --git a/cuenca_validations/types/__init__.py b/cuenca_validations/types/__init__.py index 045fc9b6..e8c7b8aa 100644 --- a/cuenca_validations/types/__init__.py +++ b/cuenca_validations/types/__init__.py @@ -62,6 +62,13 @@ 'MonthlySpendingType', 'LegalPersonRequest', 'LegalPersonUpdateRequest', + 'OperatorLoginRequest', + 'OperatorLoginResponse', + 'OperatorQuery', + 'OperatorRequest', + 'OperatorRole', + 'OperatorStatus', + 'OperatorUpdateRequest', 'PartnerRequest', 'PartnerUpdateRequest', 'PasswordResetRequest', @@ -158,6 +165,8 @@ Language, MonthlyMovementsType, MonthlySpendingType, + OperatorRole, + OperatorStatus, PlatformType, PosCapability, Profession, @@ -216,6 +225,7 @@ EventQuery, FileQuery, IdentityQuery, + OperatorQuery, PostalCodeQuery, QueryParams, SessionQuery, @@ -244,6 +254,10 @@ LegalPersonRequest, LegalPersonUpdateRequest, LimitedWalletRequest, + OperatorLoginRequest, + OperatorLoginResponse, + OperatorRequest, + OperatorUpdateRequest, PartnerRequest, PartnerUpdateRequest, PasswordResetRequest, diff --git a/cuenca_validations/types/enums.py b/cuenca_validations/types/enums.py index 6da7bc8b..af5242cb 100644 --- a/cuenca_validations/types/enums.py +++ b/cuenca_validations/types/enums.py @@ -745,3 +745,13 @@ class RequiredAction(str, Enum): level_up_required = 'level_up_required' level_up_invitation = 'level_up_invitation' fix_documents = 'fix_documents' + + +class OperatorRole(str, Enum): + operator = 'operator' + authorizer = 'authorizer' + + +class OperatorStatus(str, Enum): + active = 'active' + disabled = 'disabled' diff --git a/cuenca_validations/types/queries.py b/cuenca_validations/types/queries.py index 5981c269..1e5780ac 100644 --- a/cuenca_validations/types/queries.py +++ b/cuenca_validations/types/queries.py @@ -153,6 +153,10 @@ class AccountQuery(QueryParams): account_number: Optional[str] = None +class OperatorQuery(QueryParams): + email: Optional[EmailStr] = None + + class BalanceEntryQuery(QueryParams): funding_instrument_uri: Optional[str] = None wallet_id: str = 'default' diff --git a/cuenca_validations/types/requests.py b/cuenca_validations/types/requests.py index ffbf867b..92d284c5 100644 --- a/cuenca_validations/types/requests.py +++ b/cuenca_validations/types/requests.py @@ -36,6 +36,8 @@ KYCValidationSource, MonthlyMovementsType, MonthlySpendingType, + OperatorRole, + OperatorStatus, PlatformType, PosCapability, Profession, @@ -899,5 +901,82 @@ class LegalPersonUpdateRequest(BaseRequest): legal_representatives: Optional[list[LegalRepresentative]] = None +class OperatorRequest(BaseRequest): + name: str + email: EmailStr + phone: PhoneNumber + company_user_id: str + role: OperatorRole + status: OperatorStatus = OperatorStatus.active + + model_config = ConfigDict( + json_schema_extra={ + 'example': { + 'name': 'Maria Lopez', + 'email': 'maria.lopez@aceros.com', + 'phone': '+525512345678', + 'company_user_id': 'USWqY5cvkISJOxHyEKjAKf8w', + 'role': 'operator', + } + }, + ) + + @field_validator('email', mode='before') + @classmethod + def validate_email(cls, email: str) -> str: + return normalize_email(email) + + +class OperatorUpdateRequest(BaseRequest): + name: Optional[str] = None + phone: Optional[PhoneNumber] = None + role: Optional[OperatorRole] = None + status: Optional[OperatorStatus] = None + + @model_validator(mode="before") + @classmethod + def check_at_least_one_param(cls, values: DictStrAny) -> DictStrAny: + if not values: + raise ValueError('At least one parameter must be provided') + return values + + +class OperatorLoginRequest(BaseRequest): + email: EmailStr + password: Annotated[Password, LogConfig(masked=True)] + + model_config = ConfigDict( + json_schema_extra={ + 'example': { + 'email': 'maria.lopez@aceros.com', + 'password': 'supersecret', + } + }, + ) + + @field_validator('email', mode='before') + @classmethod + def validate_email(cls, email: str) -> str: + return normalize_email(email) + + +class OperatorLoginResponse(BaseModel): + session_token: str + operator_id: str + role: OperatorRole + company_user_id: str + + model_config = ConfigDict( + json_schema_extra={ + 'example': { + 'session_token': 'SEWqY5cvkISJOxHyEKjAKf8w', + 'operator_id': 'OPWqY5cvkISJOxHyEKjAKf8w', + 'role': 'authorizer', + 'company_user_id': 'USWqY5cvkISJOxHyEKjAKf8w', + } + }, + ) + + class PhoneVerificationAssociationRequest(BaseRequest): verification_id: str diff --git a/cuenca_validations/version.py b/cuenca_validations/version.py index 9f552eb9..d13b5181 100644 --- a/cuenca_validations/version.py +++ b/cuenca_validations/version.py @@ -1 +1 @@ -__version__ = '2.1.42' +__version__ = '2.1.43' diff --git a/tests/test_requests.py b/tests/test_requests.py index d756be59..b788657e 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -2,8 +2,18 @@ from pydantic import ValidationError from pydantic_extra_types.phone_numbers import PhoneNumber -from cuenca_validations.types.enums import Country, VerificationType +from cuenca_validations.types.enums import ( + Country, + OperatorRole, + OperatorStatus, + VerificationType, +) +from cuenca_validations.types.queries import OperatorQuery from cuenca_validations.types.requests import ( + OperatorLoginRequest, + OperatorLoginResponse, + OperatorRequest, + OperatorUpdateRequest, PasswordResetRequest, UpdateTransferRequest, UserTOSAgreementRequest, @@ -13,6 +23,102 @@ from cuenca_validations.typing import DictStrAny +def test_operator_request_valid() -> None: + req = OperatorRequest( + name='Maria Lopez', + email='Maria+Tag@Aceros.com', + phone=PhoneNumber('+525512345678'), + company_user_id='USWqY5cvkISJOxHyEKjAKf8w', + role=OperatorRole.operator, + ) + assert req.email == 'maria@aceros.com' + assert req.status == OperatorStatus.active + + +def test_operator_request_rejects_invalid_role() -> None: + with pytest.raises(ValidationError) as ex: + OperatorRequest.model_validate( + { + 'name': 'Maria Lopez', + 'email': 'maria@aceros.com', + 'phone': '+525512345678', + 'company_user_id': 'USWqY5cvkISJOxHyEKjAKf8w', + 'role': 'admin', + } + ) + assert 'role' in str(ex.value) + + +def test_operator_update_requires_at_least_one_param() -> None: + with pytest.raises(ValueError) as ex: + OperatorUpdateRequest() + assert 'At least one parameter must be provided' in str(ex.value) + + +def test_operator_update_valid() -> None: + req = OperatorUpdateRequest.model_validate({'name': 'New name'}) + assert req.name == 'New name' + + +def test_operator_login_request_valid() -> None: + req = OperatorLoginRequest.model_validate( + { + 'email': 'Operator+Tag@Aceros.com', + 'password': 'supersecret', + } + ) + assert req.email == 'operator@aceros.com' + + +def test_operator_login_request_rejects_short_password() -> None: + with pytest.raises(ValidationError) as ex: + OperatorLoginRequest.model_validate( + { + 'email': 'operator@aceros.com', + 'password': 'short', + } + ) + assert 'password' in str(ex.value) + + +def test_operator_login_request_forbids_extra() -> None: + with pytest.raises(ValidationError) as ex: + OperatorLoginRequest.model_validate( + { + 'email': 'operator@aceros.com', + 'password': 'supersecret', + 'foo': 'bar', + } + ) + assert 'Extra inputs are not permitted' in str(ex.value) + + +def test_operator_login_response_valid() -> None: + resp = OperatorLoginResponse( + session_token='SEWqY5cvkISJOxHyEKjAKf8w', + operator_id='OPWqY5cvkISJOxHyEKjAKf8w', + role=OperatorRole.authorizer, + company_user_id='USWqY5cvkISJOxHyEKjAKf8w', + ) + assert resp.session_token == 'SEWqY5cvkISJOxHyEKjAKf8w' + assert resp.operator_id == 'OPWqY5cvkISJOxHyEKjAKf8w' + assert resp.role == OperatorRole.authorizer + assert resp.company_user_id == 'USWqY5cvkISJOxHyEKjAKf8w' + + +def test_operator_query_valid() -> None: + query = OperatorQuery.model_validate({'email': 'maria.lopez@aceros.com'}) + assert str(query.email) == 'maria.lopez@aceros.com' + + +def test_operator_query_forbids_extra() -> None: + with pytest.raises(ValidationError) as ex: + OperatorQuery.model_validate( + {'email': 'maria@aceros.com', 'foo': 'bar'} + ) + assert 'Extra inputs are not permitted' in str(ex.value) + + @pytest.mark.parametrize('environment', ['api.stage', 'api.sandbox', 'api']) def test_file_cuenca_url(environment: str) -> None: request_data: DictStrAny = dict(