Skip to content

Commit 5249f37

Browse files
authored
Merge pull request #42 from andrextor/fix/fields-to-dict
Fix/fields to dict
2 parents 33b3b72 + d7c1acf commit 5249f37

22 files changed

+283
-270
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
- added: adds authentication module
10+
### Added
11+
12+
- Initial release of Checkout P2P version 1.0.0.
13+
- Support for **query**, **single payments**, **subscriptions**, **payments using subscription tokens** and **reverse**.
14+
- Added Pydantic-based validation for request and response models.
15+
- Decorators to format booleans and clean dictionaries for API compatibility.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from functools import wraps
2+
from typing import Any, Callable
3+
4+
5+
def filter_empty_values(func: Callable) -> Callable:
6+
"""
7+
Decorador para eliminar valores vacíos (None, '', {}, []) de un diccionario.
8+
"""
9+
10+
@wraps(func)
11+
def wrapper(*args: tuple, **kwargs: dict[str, Any]) -> Any:
12+
data = func(*args, **kwargs)
13+
if isinstance(data, dict):
14+
return remove_empty_values(data)
15+
return data
16+
17+
return wrapper
18+
19+
20+
def remove_empty_values(data: Any) -> Any:
21+
"""
22+
Elimina recursivamente los valores vacíos (None, '', {}, []) de un diccionario o lista.
23+
"""
24+
if isinstance(data, dict):
25+
return {k: remove_empty_values(v) for k, v in data.items() if v not in (None, "", {}, [])}
26+
elif isinstance(data, list):
27+
return [remove_empty_values(item) for item in data if item not in (None, "", {}, [])]
28+
return data

checkout/entities/amount.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,16 @@ def to_dict(self) -> dict:
4646
Convert the Amount object to a dictionary including taxes and details.
4747
"""
4848
parent_data = super().to_dict()
49-
return {
49+
50+
data = {
5051
**parent_data,
5152
"taxes": self.taxes_to_dict(),
5253
"details": self.details_to_dict(),
5354
}
55+
56+
if self.tip is None:
57+
del data["tip"]
58+
if self.insurance is None:
59+
del data["insurance"]
60+
61+
return data

checkout/entities/dispersion_payment.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,17 @@ def _extract_payment_fields(self, data: Dict[str, Any]) -> Dict[str, Any]:
2323
"reference": data.get("reference", ""),
2424
"description": data.get("description", ""),
2525
"amount": data.get("amount"),
26-
"allowPartial": data.get("allowPartial", False),
26+
"allow_partial": data.get("allow_partial", False),
2727
"shipping": data.get("shipping"),
2828
"items": data.get("items", []),
2929
"recurring": data.get("recurring"),
3030
"payment": data.get("payment"),
3131
"discount": data.get("discount"),
3232
"subscribe": data.get("subscribe", False),
3333
"agreement": data.get("agreement"),
34-
"agreementType": data.get("agreementType", ""),
34+
"agreement_type": data.get("agreement_type", ""),
3535
"modifiers": data.get("modifiers", []),
36+
"custom_fields": data.get("custom_fields", []),
3637
}
3738

3839
def set_dispersion(self, data: Union[List[Dict], Dict]) -> "DispersionPayment":
Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
1-
from typing import Any, Optional
1+
from typing import Any
22

3-
from pydantic import BaseModel, Field
3+
from pydantic import BaseModel, ConfigDict, Field
44

55
from checkout.enums.display_on_enum import DisplayOnEnum
66

77

88
class NameValuePair(BaseModel):
99
keyword: str = Field(..., description="The keyword associated with the value")
1010
value: Any = Field(default=None, description="The value, which can be a string, list, or dict")
11-
displayOn: Optional[DisplayOnEnum] = Field(
12-
default=DisplayOnEnum.NONE, description="Display setting for the keyword"
11+
display_on: DisplayOnEnum = Field(
12+
default=DisplayOnEnum.NONE, description="Display setting for the keyword", alias="displayOn"
1313
)
1414

15+
model_config = ConfigDict(populate_by_name=True, arbitrary_types_allowed=True)
16+
1517
def to_dict(self) -> dict:
1618
"""
1719
Convert the NameValuePair object to a dictionary using the Pydantic `model_dump` method.
1820
"""
19-
return self.model_dump()
21+
return {"keyword": self.keyword, "value": self.value, "displayOn": self.display_on.value}

checkout/entities/payment.py

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
from typing import List, Optional, Union
22

3-
from pydantic import BaseModel, Field
3+
from pydantic import BaseModel, ConfigDict, Field
44

55
from checkout.entities.amount import Amount
66
from checkout.entities.discount import Discount
7-
from checkout.entities.instrument import Instrument
87
from checkout.entities.item import Item
8+
from checkout.entities.name_value_pair import NameValuePair
99
from checkout.entities.payment_modifier import PaymentModifier
1010
from checkout.entities.person import Person
1111
from checkout.entities.recurring import Recurring
@@ -15,17 +15,19 @@
1515
class Payment(BaseModel, FieldsMixin):
1616
reference: str = Field(..., description="Payment reference")
1717
description: str = Field(default="", description="Description of the payment")
18-
amount: Optional[Amount] = Field(default=None, description="Amount information")
19-
allowPartial: bool = Field(default=False, description="Allow partial payments")
18+
amount: Amount = Field(default=..., description="Amount information")
19+
allow_partial: bool = Field(default=False, description="Allow partial payments", alias="allowPartial")
2020
shipping: Optional[Person] = Field(default=None, description="Shipping details")
2121
items: List[Item] = Field(default_factory=list, description="List of items")
2222
recurring: Optional[Recurring] = Field(default=None, description="Recurring payment details")
23-
payment: Optional[Instrument] = Field(default=None, description="Instrument payment details")
2423
discount: Optional[Discount] = Field(default=None, description="Discount information")
2524
subscribe: bool = Field(default=False, description="Subscribe flag")
2625
agreement: Optional[int] = Field(default=None, description="Agreement ID")
27-
agreementType: str = Field(default="", description="Type of agreement")
26+
agreement_type: str = Field(default="", description="Type of agreement", alias="agreementType")
2827
modifiers: List[PaymentModifier] = Field(default_factory=list, description="List of payment modifiers")
28+
custom_fields: List[NameValuePair] = Field(default=[], description="Additional fields for the payment")
29+
30+
model_config = ConfigDict(populate_by_name=True, arbitrary_types_allowed=True)
2931

3032
def set_items(self, items: Union[List[dict], List[Item]]) -> None:
3133
"""
@@ -55,18 +57,26 @@ def to_dict(self) -> dict:
5557
"""
5658
Convert the Payment object to a dictionary, including nested objects.
5759
"""
58-
return {
60+
data = {
5961
"reference": self.reference,
6062
"description": self.description,
61-
"amount": self.amount.model_dump() if self.amount else None,
62-
"allowPartial": self.allowPartial,
63-
"shipping": self.shipping.model_dump() if self.shipping else None,
63+
"amount": self.amount.to_dict(),
64+
"allowPartial": self.allow_partial,
6465
"items": self.items_to_array(),
65-
"recurring": self.recurring.model_dump() if self.recurring else None,
66-
"discount": self.discount.model_dump() if self.discount else None,
6766
"subscribe": self.subscribe,
68-
"agreement": self.agreement,
69-
"agreementType": self.agreementType,
7067
"modifiers": self.modifiers_to_array(),
7168
"fields": self.fields_to_array(),
7269
}
70+
71+
if self.shipping:
72+
data["shipping"] = self.shipping.to_dict()
73+
if self.recurring:
74+
data["recurring"] = self.recurring.to_dict()
75+
if self.discount:
76+
data["discount"] = self.discount.to_dict()
77+
if self.agreement:
78+
data["agreement"] = self.agreement
79+
if self.agreement_type:
80+
data["agreementType"] = self.agreement_type
81+
82+
return data

checkout/entities/person.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,27 @@
11
from typing import Optional
22

3-
from pydantic import BaseModel, Field
3+
from pydantic import BaseModel, ConfigDict, Field
44

55
from checkout.entities.address import Address
66

77

88
class Person(BaseModel):
99
document: str = Field(default="", description="Document number of the person")
10-
documentType: str = Field(default="", description="Type of document (e.g., ID, Passport)")
10+
document_type: str = Field(default="", description="Type of document (e.g., ID, Passport)", alias="documentType")
1111
name: str = Field(default="", description="First name of the person")
1212
surname: str = Field(default="", description="Last name of the person")
1313
company: str = Field(default="", description="Company name if applicable")
1414
email: Optional[str] = Field(default="", description="Email address of the person")
1515
mobile: str = Field(default="", description="Mobile number of the person")
1616
address: Optional[Address] = Field(default=None, description="Address information")
1717

18+
model_config = ConfigDict(populate_by_name=True, arbitrary_types_allowed=True)
19+
1820
def is_business(self) -> bool:
1921
"""
2022
Check if the person is representing a business based on their document type.
2123
"""
22-
return bool(self.documentType and self._business_document(self.documentType))
24+
return bool(self.document_type and self._business_document(self.document_type))
2325

2426
@staticmethod
2527
def _business_document(document_type: str) -> bool:
@@ -34,4 +36,14 @@ def to_dict(self) -> dict:
3436
"""
3537
Convert the person object to a dictionary using the new `model_dump` method.
3638
"""
37-
return self.model_dump(exclude_none=True)
39+
40+
return {
41+
"document": self.document,
42+
"documentType": self.document_type,
43+
"name": self.name,
44+
"surname": self.surname,
45+
"company": self.company,
46+
"email": self.email,
47+
"mobile": self.mobile,
48+
"address": self.address.to_dict() if self.address else "",
49+
}

checkout/entities/recurring.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import Optional
22

3-
from pydantic import BaseModel, Field
3+
from pydantic import BaseModel, ConfigDict, Field
44

55

66
class Recurring(BaseModel):
@@ -9,16 +9,24 @@ class Recurring(BaseModel):
99
description="Frequency of the transaction (Y = annual, M = monthly, D = daily)",
1010
)
1111
interval: int = Field(..., description="Interval between payments")
12-
nextPayment: str = Field(default="", description="Next payment date")
13-
maxPeriods: Optional[int] = Field(
14-
default=None,
15-
description="Maximum times the recurrence will happen, -1 if unlimited",
12+
next_payment: str = Field(default="", description="Next payment date", alias="nextPayment")
13+
max_periods: Optional[int] = Field(
14+
default=-1, description="Maximum times the recurrence will happen, -1 if unlimited", alias="maxPeriods"
1615
)
17-
dueDate: str = Field(default="", description="Due date for the recurring charge")
18-
notificationUrl: str = Field(default="", description="URL for sending notifications")
16+
due_date: str = Field(default="", description="Due date for the recurring charge", alias="dueDate")
17+
notification_url: str = Field(default="", description="URL for sending notifications", alias="notificationUrl")
18+
19+
model_config = ConfigDict(populate_by_name=True, arbitrary_types_allowed=True)
1920

2021
def to_dict(self) -> dict:
2122
"""
2223
Convert the Recurring object to a dictionary using the Pydantic `model_dump` method.
2324
"""
24-
return self.model_dump()
25+
return {
26+
"periodicity": self.periodicity,
27+
"interval": self.interval,
28+
"nextPayment": self.next_payment,
29+
"maxPeriods": self.max_periods,
30+
"dueDate": self.due_date,
31+
"notificationUrl": self.notification_url,
32+
}

checkout/entities/subscription.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
from typing import List, Optional
1+
from typing import List
22

3-
from pydantic import BaseModel, Field
3+
from pydantic import BaseModel, ConfigDict, Field
44

55
from checkout.entities.name_value_pair import NameValuePair
66
from checkout.mixins.fields_mixin import FieldsMixin
@@ -9,14 +9,16 @@
99
class Subscription(BaseModel, FieldsMixin):
1010
reference: str = Field(default="", description="Reference for the subscription")
1111
description: str = Field(default="", description="Description of the subscription")
12-
customFields: Optional[List[NameValuePair]] = Field(
13-
default_factory=lambda: [], description="Additional fields for the subscription"
14-
)
12+
custom_fields: List[NameValuePair] = Field(default=[], description="Additional fields for the subscription")
13+
14+
model_config = ConfigDict(populate_by_name=True, arbitrary_types_allowed=True)
1515

1616
def to_dict(self) -> dict:
1717
"""
1818
Convert the subscription object to a dictionary including fields.
1919
"""
2020
data = self.model_dump()
2121
data["fields"] = self.fields_to_array()
22+
del data["custom_fields"]
23+
2224
return data

checkout/messages/requests/collect.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,17 @@
33
from pydantic import ConfigDict, Field
44

55
from checkout.decorators.convert_to_boolean import convert_booleans_to_strings
6+
from checkout.decorators.filter_empty_values import filter_empty_values
67
from checkout.entities.instrument import Instrument
78
from checkout.messages.requests.redirect import RedirectRequest
89

910

1011
class CollectRequest(RedirectRequest):
1112

1213
model_config = ConfigDict(populate_by_name=True, arbitrary_types_allowed=True)
13-
1414
instrument: Optional[Instrument] = Field(..., description="Instrument details")
15-
return_url: str = Field(default="", alias="returnUrl", description="URL to return to after processing")
16-
ip_address: str = Field(default="", alias="ipAddress", description="IP address of the user")
17-
user_agent: str = Field(default="", alias="userAgent", description="User agent of the user's browser")
1815

16+
@filter_empty_values
1917
@convert_booleans_to_strings
2018
def to_dict(self) -> dict:
2119
"""

0 commit comments

Comments
 (0)