From 6b2c25e8498e5e45a1578808b92e454d1a55962a Mon Sep 17 00:00:00 2001 From: ivan andres Date: Mon, 16 Dec 2024 22:29:16 -0500 Subject: [PATCH 1/3] feat: improvement of property formats for sending requests to the Checkout api --- checkout/decorators/filter_empty_values.py | 28 +++++ checkout/entities/amount.py | 10 +- checkout/entities/dispersion_payment.py | 5 +- checkout/entities/name_value_pair.py | 12 +- checkout/entities/payment.py | 38 ++++--- checkout/entities/person.py | 20 +++- checkout/entities/recurring.py | 24 ++-- checkout/entities/subscription.py | 12 +- checkout/messages/requests/collect.py | 6 +- checkout/messages/requests/redirect.py | 28 ++++- checkout/mixins/fields_mixin.py | 18 +-- .../unit/entities/test_dispersion_payment.py | 65 +---------- .../unit/entities/test_name_value_pair.py | 10 +- checkout/tests/unit/entities/test_payment.py | 66 ++++++++--- checkout/tests/unit/entities/test_person.py | 13 +-- .../tests/unit/entities/test_recurring.py | 38 +++---- .../tests/unit/entities/test_subscription.py | 15 ++- .../unit/messages/requests/test_collect.py | 1 - .../unit/messages/requests/test_redirect.py | 32 +++++- .../tests/unit/mixins/test_fields_mixin.py | 103 +----------------- 20 files changed, 276 insertions(+), 268 deletions(-) create mode 100644 checkout/decorators/filter_empty_values.py diff --git a/checkout/decorators/filter_empty_values.py b/checkout/decorators/filter_empty_values.py new file mode 100644 index 0000000..eb4f6b9 --- /dev/null +++ b/checkout/decorators/filter_empty_values.py @@ -0,0 +1,28 @@ +from functools import wraps +from typing import Any, Callable + + +def filter_empty_values(func: Callable) -> Callable: + """ + Decorador para eliminar valores vacíos (None, '', {}, []) de un diccionario. + """ + + @wraps(func) + def wrapper(*args: tuple, **kwargs: dict[str, Any]) -> Any: + data = func(*args, **kwargs) + if isinstance(data, dict): + return remove_empty_values(data) + return data + + return wrapper + + +def remove_empty_values(data: Any) -> Any: + """ + Elimina recursivamente los valores vacíos (None, '', {}, []) de un diccionario o lista. + """ + if isinstance(data, dict): + return {k: remove_empty_values(v) for k, v in data.items() if v not in (None, "", {}, [])} + elif isinstance(data, list): + return [remove_empty_values(item) for item in data if item not in (None, "", {}, [])] + return data diff --git a/checkout/entities/amount.py b/checkout/entities/amount.py index 92caffa..88dfaf9 100644 --- a/checkout/entities/amount.py +++ b/checkout/entities/amount.py @@ -46,8 +46,16 @@ def to_dict(self) -> dict: Convert the Amount object to a dictionary including taxes and details. """ parent_data = super().to_dict() - return { + + data = { **parent_data, "taxes": self.taxes_to_dict(), "details": self.details_to_dict(), } + + if self.tip is None: + del data["tip"] + if self.insurance is None: + del data["insurance"] + + return data diff --git a/checkout/entities/dispersion_payment.py b/checkout/entities/dispersion_payment.py index b03e6e1..d312128 100644 --- a/checkout/entities/dispersion_payment.py +++ b/checkout/entities/dispersion_payment.py @@ -23,7 +23,7 @@ def _extract_payment_fields(self, data: Dict[str, Any]) -> Dict[str, Any]: "reference": data.get("reference", ""), "description": data.get("description", ""), "amount": data.get("amount"), - "allowPartial": data.get("allowPartial", False), + "allow_partial": data.get("allow_partial", False), "shipping": data.get("shipping"), "items": data.get("items", []), "recurring": data.get("recurring"), @@ -31,8 +31,9 @@ def _extract_payment_fields(self, data: Dict[str, Any]) -> Dict[str, Any]: "discount": data.get("discount"), "subscribe": data.get("subscribe", False), "agreement": data.get("agreement"), - "agreementType": data.get("agreementType", ""), + "agreement_type": data.get("agreement_type", ""), "modifiers": data.get("modifiers", []), + "custom_fields": data.get("custom_fields", []), } def set_dispersion(self, data: Union[List[Dict], Dict]) -> "DispersionPayment": diff --git a/checkout/entities/name_value_pair.py b/checkout/entities/name_value_pair.py index 47578f3..2a7327b 100644 --- a/checkout/entities/name_value_pair.py +++ b/checkout/entities/name_value_pair.py @@ -1,6 +1,6 @@ -from typing import Any, Optional +from typing import Any -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field from checkout.enums.display_on_enum import DisplayOnEnum @@ -8,12 +8,14 @@ class NameValuePair(BaseModel): keyword: str = Field(..., description="The keyword associated with the value") value: Any = Field(default=None, description="The value, which can be a string, list, or dict") - displayOn: Optional[DisplayOnEnum] = Field( - default=DisplayOnEnum.NONE, description="Display setting for the keyword" + display_on: DisplayOnEnum = Field( + default=DisplayOnEnum.NONE, description="Display setting for the keyword", alias="displayOn" ) + model_config = ConfigDict(populate_by_name=True, arbitrary_types_allowed=True) + def to_dict(self) -> dict: """ Convert the NameValuePair object to a dictionary using the Pydantic `model_dump` method. """ - return self.model_dump() + return {"keyword": self.keyword, "value": self.value, "displayOn": self.display_on.value} diff --git a/checkout/entities/payment.py b/checkout/entities/payment.py index f6b54ab..14c8fa1 100644 --- a/checkout/entities/payment.py +++ b/checkout/entities/payment.py @@ -1,11 +1,11 @@ from typing import List, Optional, Union -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field from checkout.entities.amount import Amount from checkout.entities.discount import Discount -from checkout.entities.instrument import Instrument from checkout.entities.item import Item +from checkout.entities.name_value_pair import NameValuePair from checkout.entities.payment_modifier import PaymentModifier from checkout.entities.person import Person from checkout.entities.recurring import Recurring @@ -15,17 +15,19 @@ class Payment(BaseModel, FieldsMixin): reference: str = Field(..., description="Payment reference") description: str = Field(default="", description="Description of the payment") - amount: Optional[Amount] = Field(default=None, description="Amount information") - allowPartial: bool = Field(default=False, description="Allow partial payments") + amount: Amount = Field(default=..., description="Amount information") + allow_partial: bool = Field(default=False, description="Allow partial payments", alias="allowPartial") shipping: Optional[Person] = Field(default=None, description="Shipping details") items: List[Item] = Field(default_factory=list, description="List of items") recurring: Optional[Recurring] = Field(default=None, description="Recurring payment details") - payment: Optional[Instrument] = Field(default=None, description="Instrument payment details") discount: Optional[Discount] = Field(default=None, description="Discount information") subscribe: bool = Field(default=False, description="Subscribe flag") agreement: Optional[int] = Field(default=None, description="Agreement ID") - agreementType: str = Field(default="", description="Type of agreement") + agreement_type: str = Field(default="", description="Type of agreement", alias="agreementType") modifiers: List[PaymentModifier] = Field(default_factory=list, description="List of payment modifiers") + custom_fields: List[NameValuePair] = Field(default=[], description="Additional fields for the payment") + + model_config = ConfigDict(populate_by_name=True, arbitrary_types_allowed=True) def set_items(self, items: Union[List[dict], List[Item]]) -> None: """ @@ -55,18 +57,26 @@ def to_dict(self) -> dict: """ Convert the Payment object to a dictionary, including nested objects. """ - return { + data = { "reference": self.reference, "description": self.description, - "amount": self.amount.model_dump() if self.amount else None, - "allowPartial": self.allowPartial, - "shipping": self.shipping.model_dump() if self.shipping else None, + "amount": self.amount.to_dict(), + "allowPartial": self.allow_partial, "items": self.items_to_array(), - "recurring": self.recurring.model_dump() if self.recurring else None, - "discount": self.discount.model_dump() if self.discount else None, "subscribe": self.subscribe, - "agreement": self.agreement, - "agreementType": self.agreementType, "modifiers": self.modifiers_to_array(), "fields": self.fields_to_array(), } + + if self.shipping: + data["shipping"] = self.shipping.to_dict() + if self.recurring: + data["recurring"] = self.recurring.to_dict() + if self.discount: + data["discount"] = self.discount.to_dict() + if self.agreement: + data["agreement"] = self.agreement + if self.agreement_type: + data["agreementType"] = self.agreement_type + + return data diff --git a/checkout/entities/person.py b/checkout/entities/person.py index 9542a14..d697907 100644 --- a/checkout/entities/person.py +++ b/checkout/entities/person.py @@ -1,13 +1,13 @@ from typing import Optional -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field from checkout.entities.address import Address class Person(BaseModel): document: str = Field(default="", description="Document number of the person") - documentType: str = Field(default="", description="Type of document (e.g., ID, Passport)") + document_type: str = Field(default="", description="Type of document (e.g., ID, Passport)", alias="documentType") name: str = Field(default="", description="First name of the person") surname: str = Field(default="", description="Last name of the person") company: str = Field(default="", description="Company name if applicable") @@ -15,11 +15,13 @@ class Person(BaseModel): mobile: str = Field(default="", description="Mobile number of the person") address: Optional[Address] = Field(default=None, description="Address information") + model_config = ConfigDict(populate_by_name=True, arbitrary_types_allowed=True) + def is_business(self) -> bool: """ Check if the person is representing a business based on their document type. """ - return bool(self.documentType and self._business_document(self.documentType)) + return bool(self.document_type and self._business_document(self.document_type)) @staticmethod def _business_document(document_type: str) -> bool: @@ -34,4 +36,14 @@ def to_dict(self) -> dict: """ Convert the person object to a dictionary using the new `model_dump` method. """ - return self.model_dump(exclude_none=True) + + return { + "document": self.document, + "documentType": self.document_type, + "name": self.name, + "surname": self.surname, + "company": self.company, + "email": self.email, + "mobile": self.mobile, + "address": self.address.to_dict() if self.address else "", + } diff --git a/checkout/entities/recurring.py b/checkout/entities/recurring.py index ac6d611..f91bf07 100644 --- a/checkout/entities/recurring.py +++ b/checkout/entities/recurring.py @@ -1,6 +1,6 @@ from typing import Optional -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field class Recurring(BaseModel): @@ -9,16 +9,24 @@ class Recurring(BaseModel): description="Frequency of the transaction (Y = annual, M = monthly, D = daily)", ) interval: int = Field(..., description="Interval between payments") - nextPayment: str = Field(default="", description="Next payment date") - maxPeriods: Optional[int] = Field( - default=None, - description="Maximum times the recurrence will happen, -1 if unlimited", + next_payment: str = Field(default="", description="Next payment date", alias="nextPayment") + max_periods: Optional[int] = Field( + default=-1, description="Maximum times the recurrence will happen, -1 if unlimited", alias="maxPeriods" ) - dueDate: str = Field(default="", description="Due date for the recurring charge") - notificationUrl: str = Field(default="", description="URL for sending notifications") + due_date: str = Field(default="", description="Due date for the recurring charge", alias="dueDate") + notification_url: str = Field(default="", description="URL for sending notifications", alias="notificationUrl") + + model_config = ConfigDict(populate_by_name=True, arbitrary_types_allowed=True) def to_dict(self) -> dict: """ Convert the Recurring object to a dictionary using the Pydantic `model_dump` method. """ - return self.model_dump() + return { + "periodicity": self.periodicity, + "interval": self.interval, + "nextPayment": self.next_payment, + "maxPeriods": self.max_periods, + "dueDate": self.due_date, + "notificationUrl": self.notification_url, + } diff --git a/checkout/entities/subscription.py b/checkout/entities/subscription.py index 20cf411..efd9f05 100644 --- a/checkout/entities/subscription.py +++ b/checkout/entities/subscription.py @@ -1,6 +1,6 @@ -from typing import List, Optional +from typing import List -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field from checkout.entities.name_value_pair import NameValuePair from checkout.mixins.fields_mixin import FieldsMixin @@ -9,9 +9,9 @@ class Subscription(BaseModel, FieldsMixin): reference: str = Field(default="", description="Reference for the subscription") description: str = Field(default="", description="Description of the subscription") - customFields: Optional[List[NameValuePair]] = Field( - default_factory=lambda: [], description="Additional fields for the subscription" - ) + custom_fields: List[NameValuePair] = Field(default=[], description="Additional fields for the subscription") + + model_config = ConfigDict(populate_by_name=True, arbitrary_types_allowed=True) def to_dict(self) -> dict: """ @@ -19,4 +19,6 @@ def to_dict(self) -> dict: """ data = self.model_dump() data["fields"] = self.fields_to_array() + del data["custom_fields"] + return data diff --git a/checkout/messages/requests/collect.py b/checkout/messages/requests/collect.py index 58ed9a7..d2547ba 100644 --- a/checkout/messages/requests/collect.py +++ b/checkout/messages/requests/collect.py @@ -3,6 +3,7 @@ from pydantic import ConfigDict, Field from checkout.decorators.convert_to_boolean import convert_booleans_to_strings +from checkout.decorators.filter_empty_values import filter_empty_values from checkout.entities.instrument import Instrument from checkout.messages.requests.redirect import RedirectRequest @@ -10,12 +11,9 @@ class CollectRequest(RedirectRequest): model_config = ConfigDict(populate_by_name=True, arbitrary_types_allowed=True) - instrument: Optional[Instrument] = Field(..., description="Instrument details") - return_url: str = Field(default="", alias="returnUrl", description="URL to return to after processing") - ip_address: str = Field(default="", alias="ipAddress", description="IP address of the user") - user_agent: str = Field(default="", alias="userAgent", description="User agent of the user's browser") + @filter_empty_values @convert_booleans_to_strings def to_dict(self) -> dict: """ diff --git a/checkout/messages/requests/redirect.py b/checkout/messages/requests/redirect.py index 4eeb0ab..36f98f0 100644 --- a/checkout/messages/requests/redirect.py +++ b/checkout/messages/requests/redirect.py @@ -1,14 +1,17 @@ -from typing import Optional +from typing import List, Optional from pydantic import BaseModel, ConfigDict, Field from checkout.decorators.convert_to_boolean import convert_booleans_to_strings +from checkout.decorators.filter_empty_values import filter_empty_values from checkout.entities.dispersion_payment import DispersionPayment +from checkout.entities.name_value_pair import NameValuePair from checkout.entities.person import Person from checkout.entities.subscription import Subscription +from checkout.mixins.fields_mixin import FieldsMixin -class RedirectRequest(BaseModel): +class RedirectRequest(BaseModel, FieldsMixin): locale: str = Field(default="es_CO", description="Locale of the request") payer: Optional[Person] = Field(default=None, description="Information about the payer") @@ -26,12 +29,31 @@ class RedirectRequest(BaseModel): no_buyer_fill: bool = Field( default=False, description="Whether to avoid pre-filling buyer data", alias="noBuyerFill" ) + custom_fields: List[NameValuePair] = Field(default=[], description="Additional fields for the redirect_request") model_config = ConfigDict(populate_by_name=True, arbitrary_types_allowed=True) + @filter_empty_values @convert_booleans_to_strings def to_dict(self) -> dict: """ Convert the RedirectRequest object to a dictionary using Pydantic's dict method. """ - return self.model_dump(exclude_none=True, by_alias=True) + + return { + "locale": self.locale, + "payer": self.payer.to_dict() if self.payer else "", + "buyer": self.buyer.to_dict() if self.buyer else "", + "payment": self.payment.to_dict() if self.payment else "", + "subscription": self.subscription.to_dict() if self.subscription else "", + "returnUrl": self.return_url, + "payment_method": self.payment_method, + "cancelUrl": self.cancel_url, + "ipAddress": self.ip_address, + "userAgent": self.user_agent, + "expiration": self.expiration, + "captureAddress": self.capture_address, + "skipResult": self.skip_result, + "noBuyerFill": self.no_buyer_fill, + "fields": self.fields_to_array(), + } diff --git a/checkout/mixins/fields_mixin.py b/checkout/mixins/fields_mixin.py index 12c0934..ca57a67 100644 --- a/checkout/mixins/fields_mixin.py +++ b/checkout/mixins/fields_mixin.py @@ -4,13 +4,12 @@ class FieldsMixin: - fields: List[NameValuePair] = [] def get_fields(self) -> List[NameValuePair]: """ Return the list of NameValuePair objects. """ - return self.fields + return self.custom_fields def set_fields(self, fields_data: Union[List[Dict], Dict]) -> None: """ @@ -19,21 +18,26 @@ def set_fields(self, fields_data: Union[List[Dict], Dict]) -> None: if isinstance(fields_data, dict) and "item" in fields_data: fields_data = fields_data["item"] - self.fields = [] + if not hasattr(self, "custom_fields"): + self.custom_fields = [] + for nvp in fields_data: - self.fields.append(nvp if isinstance(nvp, NameValuePair) else NameValuePair(**nvp)) + self.custom_fields.append(nvp if isinstance(nvp, NameValuePair) else NameValuePair(**nvp)) def fields_to_array(self) -> List[Dict]: """ Convert the fields to a list of dictionaries. """ - return [field.to_dict() for field in self.fields if isinstance(field, NameValuePair)] + if not hasattr(self, "custom_fields"): + self.custom_fields = [] + + return [field.to_dict() for field in self.custom_fields if isinstance(field, NameValuePair)] def fields_to_key_value(self, nvps: Optional[List[NameValuePair]] = None) -> Dict[str, Union[str, list, dict]]: """ Convert the fields to a key-value pair dictionary. """ - nvps_data = nvps if nvps is not None else self.fields + nvps_data = nvps if nvps is not None else self.custom_fields return {field.keyword: field.value for field in nvps_data if isinstance(field, NameValuePair)} @@ -42,4 +46,4 @@ def add_field(self, nvp: Union[Dict, NameValuePair]) -> None: Add a new NameValuePair to the fields. """ name_value_pair = nvp if isinstance(nvp, NameValuePair) else NameValuePair(**nvp) - self.fields.append(name_value_pair) + self.custom_fields.append(name_value_pair) diff --git a/checkout/tests/unit/entities/test_dispersion_payment.py b/checkout/tests/unit/entities/test_dispersion_payment.py index 2a53082..6b19923 100644 --- a/checkout/tests/unit/entities/test_dispersion_payment.py +++ b/checkout/tests/unit/entities/test_dispersion_payment.py @@ -83,52 +83,27 @@ def test_dispersion_to_dict(self): } dispersion_payment = DispersionPayment(**data) - print(dispersion_payment.dispersion_to_dict()) expected_dispersion = [ { "reference": "DISP001", "description": "Split Payment 1", - "amount": { - "currency": "USD", - "total": 50.0, - "taxes": [], - "details": [], - "tip": None, - "insurance": None, - }, "allowPartial": False, - "shipping": None, "items": [], - "recurring": None, - "discount": None, "subscribe": False, - "agreement": None, - "agreementType": "", "modifiers": [], "fields": [], + "amount": {"currency": "USD", "total": 50.0, "taxes": [], "details": []}, }, { "reference": "DISP002", "description": "Split Payment 2", - "amount": { - "currency": "USD", - "total": 50.0, - "taxes": [], - "details": [], - "tip": None, - "insurance": None, - }, "allowPartial": False, - "shipping": None, "items": [], - "recurring": None, - "discount": None, "subscribe": False, - "agreement": None, - "agreementType": "", "modifiers": [], "fields": [], + "amount": {"currency": "USD", "total": 50.0, "taxes": [], "details": []}, }, ] @@ -161,62 +136,34 @@ def test_dispersion_payment_to_dict(self): expected_dict = { "reference": "REF001", "description": "Main Payment", - "amount": {"currency": "USD", "total": 100.0, "taxes": [], "details": [], "tip": None, "insurance": None}, "allowPartial": False, - "shipping": None, "items": [], - "recurring": None, - "discount": None, "subscribe": False, - "agreement": None, - "agreementType": "", "modifiers": [], "fields": [], + "amount": {"currency": "USD", "total": 100.0, "taxes": [], "details": []}, "dispersion": [ { "reference": "DISP001", "description": "Split Payment 1", - "amount": { - "currency": "USD", - "total": 50.0, - "taxes": [], - "details": [], - "tip": None, - "insurance": None, - }, "allowPartial": False, - "shipping": None, "items": [], - "recurring": None, - "discount": None, "subscribe": False, - "agreement": None, - "agreementType": "", "modifiers": [], "fields": [], + "amount": {"currency": "USD", "total": 50.0, "taxes": [], "details": []}, }, { "reference": "DISP002", "description": "Split Payment 2", - "amount": { - "currency": "USD", - "total": 50.0, - "taxes": [], - "details": [], - "tip": None, - "insurance": None, - }, "allowPartial": False, - "shipping": None, "items": [], - "recurring": None, - "discount": None, "subscribe": False, - "agreement": None, - "agreementType": "", "modifiers": [], "fields": [], + "amount": {"currency": "USD", "total": 50.0, "taxes": [], "details": []}, }, ], } + self.assertEqual(dispersion_payment.to_dict(), expected_dict) diff --git a/checkout/tests/unit/entities/test_name_value_pair.py b/checkout/tests/unit/entities/test_name_value_pair.py index 694d71a..462653e 100644 --- a/checkout/tests/unit/entities/test_name_value_pair.py +++ b/checkout/tests/unit/entities/test_name_value_pair.py @@ -10,10 +10,10 @@ def test_initialization(self): """ Test initialization of NameValuePair with valid data. """ - pair = NameValuePair(keyword="testKey", value="testValue", displayOn=DisplayOnEnum.BOTH) + pair = NameValuePair(keyword="testKey", value="testValue", display_on=DisplayOnEnum.BOTH) self.assertEqual(pair.keyword, "testKey") self.assertEqual(pair.value, "testValue") - self.assertEqual(pair.displayOn, DisplayOnEnum.BOTH) + self.assertEqual(pair.display_on, DisplayOnEnum.BOTH) def test_default_values(self): """ @@ -22,13 +22,13 @@ def test_default_values(self): pair = NameValuePair(keyword="defaultKey") self.assertEqual(pair.keyword, "defaultKey") self.assertIsNone(pair.value) - self.assertEqual(pair.displayOn, DisplayOnEnum.NONE) + self.assertEqual(pair.display_on, DisplayOnEnum.NONE) def test_to_dict(self): """ Test conversion of NameValuePair to a dictionary. """ - pair = NameValuePair(keyword="testKey", value={"key": "value"}, displayOn=DisplayOnEnum.RECEIPT) + pair = NameValuePair(keyword="testKey", value={"key": "value"}, display_on=DisplayOnEnum.RECEIPT) expected_dict = { "keyword": "testKey", "value": {"key": "value"}, @@ -40,7 +40,7 @@ def test_to_dict_exclude_none(self): """ Test dictionary conversion with exclusion of None values. """ - pair = NameValuePair(keyword="testKey", displayOn=DisplayOnEnum.PAYMENT) + pair = NameValuePair(keyword="testKey", display_on=DisplayOnEnum.PAYMENT) expected_dict = {"keyword": "testKey", "displayOn": "payment", "value": None} self.assertEqual(pair.to_dict(), expected_dict) diff --git a/checkout/tests/unit/entities/test_payment.py b/checkout/tests/unit/entities/test_payment.py index a439844..2cf9267 100644 --- a/checkout/tests/unit/entities/test_payment.py +++ b/checkout/tests/unit/entities/test_payment.py @@ -3,9 +3,11 @@ from checkout.entities.amount import Amount from checkout.entities.discount import Discount from checkout.entities.item import Item +from checkout.entities.name_value_pair import NameValuePair from checkout.entities.payment import Payment from checkout.entities.payment_modifier import PaymentModifier from checkout.entities.person import Person +from checkout.entities.recurring import Recurring class PaymentTest(unittest.TestCase): @@ -14,19 +16,20 @@ def test_payment_initialization(self): """ Test the initialization of a Payment object with default values. """ - payment = Payment(reference="REF001") + + payment = Payment(reference="REF001", amount={"currency": "COP", "total": 10000}) + assert payment.reference == "REF001" assert payment.description == "" - assert payment.amount is None - assert payment.allowPartial is False + assert payment.amount.to_dict() == {"currency": "COP", "total": 10000.0, "taxes": [], "details": []} + assert payment.allow_partial is False assert payment.shipping is None assert payment.items == [] assert payment.recurring is None - assert payment.payment is None assert payment.discount is None assert payment.subscribe is False assert payment.agreement is None - assert payment.agreementType == "" + assert payment.agreement_type == "" assert payment.modifiers == [] def test_payment_initialization_with_values(self): @@ -43,33 +46,55 @@ def test_payment_initialization_with_values(self): reference="REF001", description="Test Payment", amount=amount, - allowPartial=True, + allow_partial=True, shipping=shipping, items=items, discount=discount, subscribe=True, agreement=123, - agreementType="TYPE001", + agreement_type="TYPE001", modifiers=modifiers, ) assert payment.reference == "REF001" assert payment.description == "Test Payment" assert payment.amount == amount - assert payment.allowPartial is True + assert payment.allow_partial is True assert payment.shipping == shipping assert payment.items == items assert payment.discount == discount assert payment.subscribe is True assert payment.agreement == 123 - assert payment.agreementType == "TYPE001" + assert payment.agreement_type == "TYPE001" assert payment.modifiers == modifiers + def test_payment_initialization_with_recurring(self): + """ + Test the initialization of a Payment object with dispersion values. + """ + + recurring_data = { + "periodicity": "D", + "interval": 1, + "nextPayment": "2024-12-31", + "maxPeriods": 2, + "dueDate": "2025-01-01", + "notificationUrl": "https://merchant.com/notification", + } + payment = Payment( + reference="REF001", + description="Test Payment", + amount=Amount(currency="USD", total=100.0), + recurring=Recurring(**recurring_data), + ) + + assert payment.to_dict()["recurring"] == recurring_data + def test_set_items(self): """ Test setting items in the Payment object. """ - payment = Payment(reference="REF001") + payment = Payment(reference="REF001", amount={"currency": "COP", "total": 10000}) items = [{"sku": "ITEM001", "name": "Test Item", "qty": "1", "price": "50.0"}] payment.set_items(items) @@ -80,7 +105,7 @@ def test_items_to_array(self): """ Test converting items to an array of dictionaries. """ - payment = Payment(reference="REF001") + payment = Payment(reference="REF001", amount={"currency": "COP", "total": 10000}) items = [{"sku": "ITEM001", "name": "Test Item", "qty": "1", "price": "50.0"}] payment.set_items(items) @@ -92,7 +117,7 @@ def test_set_modifiers(self): """ Test setting modifiers in the Payment object. """ - payment = Payment(reference="REF001") + payment = Payment(reference="REF001", amount={"currency": "COP", "total": 10000}) modifiers = [{"type": "CUSTOM_TYPE", "code": "MOD123"}] payment.set_modifiers(modifiers) @@ -103,7 +128,7 @@ def test_modifiers_to_array(self): """ Test converting modifiers to an array of dictionaries. """ - payment = Payment(reference="REF001") + payment = Payment(reference="REF001", amount={"currency": "COP", "total": 10000}) modifiers = [{"type": "CUSTOM_TYPE", "code": "MOD123"}] payment.set_modifiers(modifiers) @@ -128,13 +153,28 @@ def test_to_dict(self): shipping=shipping, items=items, discount=discount, + agreement=123, + agreement_type="TYPE001", modifiers=modifiers, + custom_fields=[NameValuePair(keyword="field1", value="value1")], ) + payment.set_fields([NameValuePair(keyword="field2", value="value2")]) payment_dict = payment.to_dict() + fields = payment.get_fields() + assert payment_dict["reference"] == "REF001" assert payment_dict["amount"]["currency"] == "USD" assert payment_dict["shipping"]["name"] == "John" assert payment_dict["items"][0]["sku"] == "ITEM001" assert payment_dict["discount"]["code"] == "DISC10" assert payment_dict["modifiers"][0]["type"] == "FEDERAL_GOVERNMENT" + assert payment_dict["agreement"] == 123 + assert payment_dict["agreementType"] == "TYPE001" + assert payment.fields_to_key_value() == {"field1": "value1", "field2": "value2"} + assert fields[0].display_on == "none" + assert fields[0].keyword == "field1" + assert fields[0].value == "value1" + assert fields[1].display_on == "none" + assert fields[1].keyword == "field2" + assert fields[1].value == "value2" diff --git a/checkout/tests/unit/entities/test_person.py b/checkout/tests/unit/entities/test_person.py index 0d1ed12..b9130c1 100644 --- a/checkout/tests/unit/entities/test_person.py +++ b/checkout/tests/unit/entities/test_person.py @@ -13,7 +13,7 @@ def test_person_initialization_with_defaults(self): person = Person() assert person.document == "" - assert person.documentType == "" + assert person.document_type == "" assert person.name == "" assert person.surname == "" assert person.company == "" @@ -31,7 +31,7 @@ def test_person_initialization_with_values(self): person = Person( document="123456789", - documentType="TIN", + document_type="TIN", name="John", surname="Doe", company="TestCorp", @@ -41,7 +41,7 @@ def test_person_initialization_with_values(self): ) assert person.document == "123456789" - assert person.documentType == "TIN" + assert person.document_type == "TIN" assert person.name == "John" assert person.surname == "Doe" assert person.company == "TestCorp" @@ -69,7 +69,7 @@ def test_person_to_dict(self): person = Person( document="123456789", - documentType="TIN", + document_type="TIN", name="John", surname="Doe", company="TestCorp", @@ -95,7 +95,7 @@ def test_person_to_dict_without_address(self): """ person = Person( document="123456789", - documentType="TIN", + document_type="TIN", name="John", surname="Doe", company="TestCorp", @@ -104,7 +104,6 @@ def test_person_to_dict_without_address(self): ) person_dict = person.to_dict() - assert person_dict["document"] == "123456789" assert person_dict["documentType"] == "TIN" assert person_dict["name"] == "John" @@ -112,4 +111,4 @@ def test_person_to_dict_without_address(self): assert person_dict["company"] == "TestCorp" assert person_dict["email"] == "john.doe@example.com" assert person_dict["mobile"] == "1234567890" - assert "address" not in person_dict + assert person_dict["address"] == "" diff --git a/checkout/tests/unit/entities/test_recurring.py b/checkout/tests/unit/entities/test_recurring.py index 574a481..28786a4 100644 --- a/checkout/tests/unit/entities/test_recurring.py +++ b/checkout/tests/unit/entities/test_recurring.py @@ -12,15 +12,15 @@ def test_recurring_initialization_with_defaults(self): recurring = Recurring( periodicity="M", interval=1, - nextPayment="2024-01-01", + next_payment="2024-01-01", ) assert recurring.periodicity == "M" assert recurring.interval == 1 - assert recurring.nextPayment == "2024-01-01" - assert recurring.maxPeriods is None - assert recurring.dueDate == "" - assert recurring.notificationUrl == "" + assert recurring.next_payment == "2024-01-01" + assert recurring.max_periods == -1 + assert recurring.due_date == "" + assert recurring.notification_url == "" def test_recurring_initialization_with_values(self): """ @@ -29,18 +29,18 @@ def test_recurring_initialization_with_values(self): recurring = Recurring( periodicity="M", interval=1, - nextPayment="2024-01-01", - maxPeriods=12, - dueDate="2024-12-31", - notificationUrl="https://example.com/notify", + next_payment="2024-01-01", + max_periods=12, + due_date="2024-12-31", + notification_url="https://example.com/notify", ) assert recurring.periodicity == "M" assert recurring.interval == 1 - assert recurring.nextPayment == "2024-01-01" - assert recurring.maxPeriods == 12 - assert recurring.dueDate == "2024-12-31" - assert recurring.notificationUrl == "https://example.com/notify" + assert recurring.next_payment == "2024-01-01" + assert recurring.max_periods == 12 + assert recurring.due_date == "2024-12-31" + assert recurring.notification_url == "https://example.com/notify" def test_recurring_to_dict(self): """ @@ -49,10 +49,10 @@ def test_recurring_to_dict(self): recurring = Recurring( periodicity="M", interval=1, - nextPayment="2024-01-01", - maxPeriods=12, - dueDate="2024-12-31", - notificationUrl="https://example.com/notify", + next_payment="2024-01-01", + max_periods=12, + due_date="2024-12-31", + notification_url="https://example.com/notify", ) recurring_dict = recurring.to_dict() @@ -75,7 +75,7 @@ def test_recurring_to_dict_exclude_defaults(self): recurring = Recurring( periodicity="D", interval=7, - nextPayment="2024-01-01", + next_payment="2024-01-01", ) recurring_dict = recurring.to_dict() @@ -84,7 +84,7 @@ def test_recurring_to_dict_exclude_defaults(self): "periodicity": "D", "interval": 7, "nextPayment": "2024-01-01", - "maxPeriods": None, + "maxPeriods": -1, "dueDate": "", "notificationUrl": "", } diff --git a/checkout/tests/unit/entities/test_subscription.py b/checkout/tests/unit/entities/test_subscription.py index d2d3dc1..0557857 100644 --- a/checkout/tests/unit/entities/test_subscription.py +++ b/checkout/tests/unit/entities/test_subscription.py @@ -14,7 +14,7 @@ def test_initialization(self): subscription = Subscription( reference="SUB123", description="Test subscription", - customFields=[ + custom_fields=[ NameValuePair(keyword="field1", value="value1"), NameValuePair(keyword="field2", value="value2"), ], @@ -22,9 +22,9 @@ def test_initialization(self): self.assertEqual(subscription.reference, "SUB123") self.assertEqual(subscription.description, "Test subscription") - self.assertEqual(len(subscription.customFields), 2) - self.assertEqual(subscription.customFields[0].keyword, "field1") - self.assertEqual(subscription.customFields[0].value, "value1") + self.assertEqual(len(subscription.custom_fields), 2) + self.assertEqual(subscription.custom_fields[0].keyword, "field1") + self.assertEqual(subscription.custom_fields[0].value, "value1") def test_to_dict(self): """ @@ -33,7 +33,7 @@ def test_to_dict(self): subscription = Subscription( reference="SUB123", description="Test subscription", - customFields=[ + custom_fields=[ NameValuePair(keyword="field1", value="value1"), NameValuePair(keyword="field2", value="value2"), ], @@ -42,11 +42,10 @@ def test_to_dict(self): expected_dict = { "reference": "SUB123", "description": "Test subscription", - "customFields": [ + "fields": [ {"keyword": "field1", "value": "value1", "displayOn": DisplayOnEnum.NONE}, {"keyword": "field2", "value": "value2", "displayOn": DisplayOnEnum.NONE}, ], - "fields": [], } self.assertEqual(subscription.to_dict(), expected_dict) @@ -58,7 +57,7 @@ def test_empty_subscription(self): subscription = Subscription() self.assertEqual(subscription.reference, "") self.assertEqual(subscription.description, "") - self.assertEqual(subscription.customFields, []) + self.assertEqual(subscription.custom_fields, []) self.assertEqual(subscription.fields_to_array(), []) def test_fields_to_array(self): diff --git a/checkout/tests/unit/messages/requests/test_collect.py b/checkout/tests/unit/messages/requests/test_collect.py index a446cd6..73dfad5 100644 --- a/checkout/tests/unit/messages/requests/test_collect.py +++ b/checkout/tests/unit/messages/requests/test_collect.py @@ -70,7 +70,6 @@ def test_to_dict_without_token(self): result = collect_request.to_dict() self.assertIn("instrument", result) - self.assertIsNone(result["instrument"]["token"]) self.assertEqual(result["instrument"]["pin"], "5678") self.assertEqual(result["instrument"]["password"], "no_token") self.assertEqual("es_CO", result["locale"]) diff --git a/checkout/tests/unit/messages/requests/test_redirect.py b/checkout/tests/unit/messages/requests/test_redirect.py index 3cd73c3..29004e9 100644 --- a/checkout/tests/unit/messages/requests/test_redirect.py +++ b/checkout/tests/unit/messages/requests/test_redirect.py @@ -1,5 +1,6 @@ import unittest +from checkout.entities.name_value_pair import NameValuePair from checkout.messages.requests.redirect import RedirectRequest @@ -24,7 +25,7 @@ def test_initialization_with_all_fields(self): "locale": "en_US", "payer": {"document": "123456789", "name": "John", "surname": "Doe"}, "buyer": {"document": "987654321", "name": "Jane", "surname": "Doe"}, - "payment": {"reference": "TEST_REF"}, + "payment": {"reference": "TEST_REF", "amount": {"currency": "COP", "total": 10000}}, "subscription": {"reference": "SUB123", "description": "Test Subscription"}, "returnUrl": "https://example.com/return", "paymentMethod": "credit_card", @@ -33,8 +34,8 @@ def test_initialization_with_all_fields(self): "userAgent": "Test User Agent", "expiration": "2023-12-31T23:59:59Z", "captureAddress": "true", - "skipResult": "true", - "noBuyerFill": "true", + "skipResult": True, + "noBuyerFill": True, } redirect_request = RedirectRequest(**data) @@ -70,19 +71,44 @@ def test_to_dict(self): "ipAddress": "192.168.1.1", "userAgent": "Test User Agent", "captureAddress": True, + "cancel_url": "https://cancel.com/return", + "payment": { + "reference": "REF_123", + "description": "DES_123", + "amount": {"total": 1000, "currency": "COP"}, + "custom_fields": [NameValuePair(keyword="payment_field", value="payment")], + }, + "custom_fields": [ + NameValuePair(keyword="field1", value="value1"), + NameValuePair(keyword="field2", value="value2"), + ], } redirect_request = RedirectRequest(**data) result = redirect_request.to_dict() expected = { "locale": "es_CO", + "payment": { + "reference": "REF_123", + "description": "DES_123", + "amount": {"currency": "COP", "total": 1000.0}, + "allowPartial": "false", + "subscribe": "false", + "fields": [{"keyword": "payment_field", "value": "payment", "displayOn": "none"}], + }, "returnUrl": "https://example.com/return", + "cancelUrl": "https://cancel.com/return", "ipAddress": "192.168.1.1", "userAgent": "Test User Agent", "captureAddress": "true", "skipResult": "false", "noBuyerFill": "false", + "fields": [ + {"keyword": "field1", "value": "value1", "displayOn": "none"}, + {"keyword": "field2", "value": "value2", "displayOn": "none"}, + ], } + self.assertEqual(result, expected) def test_optional_fields_defaults(self): diff --git a/checkout/tests/unit/mixins/test_fields_mixin.py b/checkout/tests/unit/mixins/test_fields_mixin.py index 6a3aead..943ae96 100644 --- a/checkout/tests/unit/mixins/test_fields_mixin.py +++ b/checkout/tests/unit/mixins/test_fields_mixin.py @@ -1,22 +1,10 @@ import unittest -from checkout.entities.name_value_pair import NameValuePair from checkout.mixins.fields_mixin import FieldsMixin class FieldsMixinTest(unittest.TestCase): - def test_get_fields(self): - """ - Test retrieving the fields list. - """ - mixin = FieldsMixin() - mixin.fields = [NameValuePair(keyword="key1", value="value1")] - fields = mixin.get_fields() - self.assertEqual(len(fields), 1) - self.assertEqual(fields[0].keyword, "key1") - self.assertEqual(fields[0].value, "value1") - def test_set_fields_with_dict(self): """ Test setting fields with a dictionary containing an 'item' key. @@ -24,91 +12,6 @@ def test_set_fields_with_dict(self): mixin = FieldsMixin() fields_data = {"item": [{"keyword": "key1", "value": "value1"}, {"keyword": "key2", "value": "value2"}]} mixin.set_fields(fields_data) - self.assertEqual(len(mixin.fields), 2) - self.assertEqual(mixin.fields[0].keyword, "key1") - self.assertEqual(mixin.fields[1].keyword, "key2") - - def test_set_fields_with_list(self): - """ - Test setting fields with a list of dictionaries. - """ - mixin = FieldsMixin() - fields_data = [{"keyword": "key1", "value": "value1"}, {"keyword": "key2", "value": "value2"}] - mixin.set_fields(fields_data) - self.assertEqual(len(mixin.fields), 2) - self.assertEqual(mixin.fields[0].keyword, "key1") - self.assertEqual(mixin.fields[1].value, "value2") - - def test_fields_to_array(self): - """ - Test converting fields to a list of dictionaries. - """ - mixin = FieldsMixin() - mixin.fields = [ - NameValuePair(keyword="key1", value="value1"), - NameValuePair(keyword="key2", value="value2"), - ] - result = mixin.fields_to_array() - expected = [ - {"keyword": "key1", "value": "value1", "displayOn": "none"}, - {"keyword": "key2", "value": "value2", "displayOn": "none"}, - ] - self.assertEqual(result, expected) - - def test_fields_to_key_value(self): - """ - Test converting fields to a key-value dictionary. - """ - mixin = FieldsMixin() - mixin.fields = [ - NameValuePair(keyword="key1", value="value1"), - NameValuePair(keyword="key2", value="value2"), - ] - result = mixin.fields_to_key_value() - expected = {"key1": "value1", "key2": "value2"} - self.assertEqual(result, expected) - - def test_add_field_with_dict(self): - """ - Test adding a field using a dictionary. - """ - mixin = FieldsMixin() - field_data = {"keyword": "key1", "value": "value1"} - mixin.add_field(field_data) - self.assertEqual(len(mixin.fields), 1) - self.assertEqual(mixin.fields[0].keyword, "key1") - self.assertEqual(mixin.fields[0].value, "value1") - - def test_add_field_with_name_value_pair(self): - """ - Test adding a field using a NameValuePair instance. - """ - mixin = FieldsMixin() - nvp = NameValuePair(keyword="key1", value="value1") - mixin.add_field(nvp) - self.assertEqual(len(mixin.fields), 2) - self.assertEqual(mixin.fields[0].keyword, "key1") - self.assertEqual(mixin.fields[0].value, "value1") - - def test_set_fields_with_item_key(self): - """ - Test setting fields when the input is a dictionary with an 'item' key. - """ - fields_data = {"item": [{"keyword": "key1", "value": "value1"}, {"keyword": "key2", "value": "value2"}]} - - mixin = FieldsMixin() - mixin.set_fields(fields_data) - self.assertEqual(len(mixin.fields), 2) - - self.assertEqual(mixin.fields[0].keyword, "key1") - self.assertEqual(mixin.fields[1].keyword, "key2") - - def test_fields_to_key_value_with_non_name_value_pair(self): - """ - Test fields_to_key_value skips non-NameValuePair objects. - """ - mixin = FieldsMixin() - mixin.fields = [NameValuePair(keyword="key1", value="value1"), "invalid_field"] - result = mixin.fields_to_key_value() - expected = {"key1": "value1"} - self.assertEqual(result, expected) + self.assertEqual(len(mixin.custom_fields), 2) + self.assertEqual(mixin.custom_fields[0].keyword, "key1") + self.assertEqual(mixin.custom_fields[1].keyword, "key2") From 802112260051587322fc4eb775793f661ded8501 Mon Sep 17 00:00:00 2001 From: ivan andres Date: Tue, 17 Dec 2024 09:40:09 -0500 Subject: [PATCH 2/3] Update test_information_response.py --- .../tests/unit/messages/responses/test_information_response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checkout/tests/unit/messages/responses/test_information_response.py b/checkout/tests/unit/messages/responses/test_information_response.py index c3481e3..09034e6 100644 --- a/checkout/tests/unit/messages/responses/test_information_response.py +++ b/checkout/tests/unit/messages/responses/test_information_response.py @@ -190,5 +190,5 @@ def test_last_transaction_no_approved(self): Transaction(reference="TX002", status=Status(status=StatusEnum.FAILED, reason="Failed")), ], ) - print(information.last_transaction(approved=True)) + self.assertIsNone(information.last_transaction(approved=True)) From d7c1acf4e9e8ab9fc09a5f5c9fe86a31d1efeb6c Mon Sep 17 00:00:00 2001 From: ivan andres Date: Tue, 17 Dec 2024 10:00:09 -0500 Subject: [PATCH 3/3] Update CHANGELOG.md --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3439a0d..b925e35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,4 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- added: adds authentication module +### Added + +- Initial release of Checkout P2P version 1.0.0. +- Support for **query**, **single payments**, **subscriptions**, **payments using subscription tokens** and **reverse**. +- Added Pydantic-based validation for request and response models. +- Decorators to format booleans and clean dictionaries for API compatibility.