Skip to content

Commit 7a69375

Browse files
authored
Merge pull request #43 from andrextor/feature/endpoint-invalidate-token
Feature/endpoint invalidate token
2 parents 5249f37 + 5a4b216 commit 7a69375

18 files changed

+234
-55
lines changed

.github/workflows/python-app.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535

3636
- name: Run Tests
3737
run: |
38-
poetry run pytest --cov=checkout --cov-report=xml
38+
poetry run pytest --cov-report=xml
3939
4040
lint:
4141
name: Lint and Type Check

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- Initial release of Checkout P2P version 1.0.0.
13-
- Support for **query**, **single payments**, **subscriptions**, **payments using subscription tokens** and **reverse**.
13+
- Support for **query**, **single payments**, **subscriptions**, **payments using subscription tokens**, **Invalidate token** and **reverse**.
1414
- Added Pydantic-based validation for request and response models.
1515
- Decorators to format booleans and clean dictionaries for API compatibility.

README.md

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
# **Checkout-P2P Python Integration Library**
32

43
[![pypi](https://img.shields.io/pypi/v/checkout-p2p.svg)](https://pypi.org/project/checkout-p2p/)
@@ -59,9 +58,9 @@ checkout = Checkout({
5958
from checkout import RedirectRequest
6059

6160
redirect_request = RedirectRequest(
62-
returnUrl="https://example.com/return",
63-
ipAddress="192.168.1.1",
64-
userAgent="Test User Agent",
61+
return_url="https://example.com/return",
62+
ip_address="192.168.1.1",
63+
user_agent="Test User Agent",
6564
payment={"reference": "TEST _q", "description": "Test Payment", "amount": {"currency": "COP", "total": 10000}}
6665
)
6766

@@ -73,23 +72,66 @@ print("Redirect to:", response.process_url)
7372
3.Query a Payment Request
7473

7574
```python
76-
77-
7875
query_response = checkout.query(123456) # Replace with your request ID
7976

8077
print("Request Status:", query_response.status)
8178
```
8279

83-
4.Reverse a Payment
80+
4 Charge using token
8481

8582
```python
83+
from checkout import CollectRequest
84+
85+
collect_request = CollectRequest(
86+
return_url="https://example.com/return",
87+
ip_address="192.168.1.1",
88+
user_agent="Test User Agent",
89+
instrument={"token": {"token" : "your_token_c5583922eccd6d2061c1b0592b099f04e352a894f37ae51cf1a"}},
90+
payer={
91+
"email": "[email protected]",
92+
"name" : "Andres",
93+
"surname": "López",
94+
"document": "111111111",
95+
"documentType": "CC",
96+
"mobile": "+573111111111"
97+
},
98+
payment={
99+
"reference": "TEST_COllECT",
100+
"description": "Test Payment",
101+
"amount": {"currency": "COP", "total": 15000}
102+
}
103+
)
104+
105+
# Collect. Returns a `InformationResponse` object.
106+
collect_response = checkout.collect(collect_request)
107+
108+
print("Collect Status :", collect_response.status)
109+
```
110+
111+
5.Reverse a Payment
86112

113+
```python
87114
# Reverse a transaction. Returns a `ReverseResponse` object.
88115
reverse_response = checkout.reverse("internal_reference")
89116

90117
print("Reverse Status:", reverse_response.status)
91118
```
92119

120+
6.Invalidate token
121+
122+
```python
123+
invalidate_token_request = {
124+
"locale": "en_US",
125+
"instrument": {"token" : {"token" : "your_token_c5583922eccd6d2061c1b0592b099f04e352a894f37ae51cf1a"}}
126+
}
127+
128+
# invalite token. Returns a `Status` object.
129+
invalidate_response = checkout.invalidate_token(invalidate_token_request)
130+
131+
print("Invalidate Status:", invalidate_response.status)
132+
print("Message:", invalidate_response.message)
133+
```
134+
93135
## **License**
94136

95137
This project is licensed under the MIT License. See the [LICENSE](LICENSE.txt) file for details.

checkout/checkout.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22

33
from checkout.contracts.carrier import Carrier
44
from checkout.entities.settings import Settings
5+
from checkout.entities.status import Status
56
from checkout.exceptions.checkout_exception import CheckoutException
67
from checkout.messages.requests.collect import CollectRequest
8+
from checkout.messages.requests.invalidate_token import InvalidateToKenRequest
79
from checkout.messages.requests.redirect import RedirectRequest
810
from checkout.messages.responses.information import InformationResponse
911
from checkout.messages.responses.redirect import RedirectResponse
1012
from checkout.messages.responses.reverse import ReverseResponse
1113

12-
T = TypeVar("T", RedirectRequest, CollectRequest)
14+
T = TypeVar("T", RedirectRequest, CollectRequest, InvalidateToKenRequest)
1315

1416

1517
class Checkout:
@@ -26,7 +28,9 @@ def __init__(self, data: Dict[str, Any]) -> None:
2628
self.settings: Settings = Settings(**data)
2729
self.logger = self.settings.logger()
2830

29-
def _validate_request(self, request: Union[RedirectRequest, CollectRequest, Dict], expected_class: Type[T]) -> T:
31+
def _validate_request(
32+
self, request: Union[RedirectRequest, CollectRequest, InvalidateToKenRequest, Dict], expected_class: Type[T]
33+
) -> T:
3034
"""
3135
Validate the request object and convert it to the expected class if necessary.
3236
@@ -98,3 +102,13 @@ def reverse(self, internal_reference: str) -> ReverseResponse:
98102
"""
99103
self.logger.info(f"Reversing transaction with reference: {internal_reference}.")
100104
return self.carrier.reverse(internal_reference)
105+
106+
def invalidateToken(self, invalidate_token_request: Union[InvalidateToKenRequest, Dict]) -> Status:
107+
"""
108+
Reverse a transaction.
109+
110+
:param invalidate_token_request: InvalidateToKenRequest instance or dictionary with request data.
111+
:return: Status object.
112+
"""
113+
invalidate_token_request = self._validate_request(invalidate_token_request, InvalidateToKenRequest)
114+
return self.carrier.invalidateToken(invalidate_token_request)

checkout/clients/rest_client.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
from checkout.contracts.carrier import Carrier
44
from checkout.entities.settings import Settings
5+
from checkout.entities.status import Status
56
from checkout.messages.requests.collect import CollectRequest
7+
from checkout.messages.requests.invalidate_token import InvalidateToKenRequest
68
from checkout.messages.requests.redirect import RedirectRequest
79
from checkout.messages.responses.information import InformationResponse
810
from checkout.messages.responses.redirect import RedirectResponse
@@ -56,3 +58,11 @@ def reverse(self, transaction_id: str) -> ReverseResponse:
5658
"""
5759
result = self._post("api/reverse", {"internalReference": transaction_id})
5860
return ReverseResponse(**result)
61+
62+
def invalidateToken(self, invalidate_token_request: InvalidateToKenRequest) -> Status:
63+
"""
64+
Invalidate a token.
65+
"""
66+
result = self._post("/api/instrument/invalidate", invalidate_token_request.to_dic())
67+
68+
return Status.from_dict(result)

checkout/contracts/carrier.py

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

3+
from checkout.entities.status import Status
34
from checkout.messages.requests.collect import CollectRequest
5+
from checkout.messages.requests.invalidate_token import InvalidateToKenRequest
46
from checkout.messages.requests.redirect import RedirectRequest
57
from checkout.messages.responses.information import InformationResponse
68
from checkout.messages.responses.redirect import RedirectResponse
@@ -27,3 +29,8 @@ def reverse(self, transaction_id: str) -> ReverseResponse:
2729
"""
2830
Reverse a transaction by its ID.
2931
"""
32+
33+
def invalidateToken(self, invalidate_token_request: InvalidateToKenRequest) -> Status:
34+
"""
35+
invalidate a token.
36+
"""

checkout/entities/status.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from datetime import datetime
2-
from typing import Optional
2+
from typing import Dict, Optional
33

44
from pydantic import BaseModel, Field
55

@@ -34,6 +34,19 @@ def quick(
3434
) -> "Status":
3535
return cls(status=status, reason=reason, message=message, date=date)
3636

37+
@classmethod
38+
def from_dict(cls, data: Dict) -> "Status":
39+
"""
40+
Create a Status instance from a dictionary.
41+
"""
42+
status_data = data.get("status", {})
43+
return cls(
44+
status=StatusEnum(status_data["status"]),
45+
reason=status_data["reason"],
46+
message=status_data.get("message", ""),
47+
date=status_data.get("date", datetime.now().isoformat()),
48+
)
49+
3750
def to_dict(self) -> dict:
3851
"""
3952
Convert the Status object to a dictionary.

checkout/entities/subscription_information.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,17 +68,7 @@ def parse_instrument(self) -> Optional[Union[Account, Token]]:
6868
data[nvp.keyword] = nvp.value
6969

7070
if self.type == "token":
71-
return Token(
72-
token=data.get("token", ""),
73-
subtoken=data.get("subtoken", ""),
74-
franchise=data.get("franchise", ""),
75-
franchiseName=data.get("franchiseName", ""),
76-
issuerName=data.get("issuerName", ""),
77-
lastDigits=data.get("lastDigits", ""),
78-
validUntil=data.get("validUntil", ""),
79-
cvv=data.get("cvv", ""),
80-
installments=data.get("installments", 0),
81-
)
71+
return Token(**data)
8272

8373
elif self.type == "account":
8474
return Account(

checkout/entities/token.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,28 @@
11
from datetime import datetime
2+
from typing import Optional
23

3-
from pydantic import BaseModel, Field
4+
from pydantic import BaseModel, ConfigDict, Field
45

56

67
class Token(BaseModel):
78
token: str = Field(default="", description="Unique token identifier")
89
subtoken: str = Field(default="", description="Secondary token identifier")
910
franchise: str = Field(default="", description="Franchise associated with the token")
10-
franchiseName: str = Field(default="", description="Name of the franchise")
11-
issuerName: str = Field(default="", description="Name of the issuer")
12-
lastDigits: str = Field(default="", description="Last digits of the card/token")
13-
validUntil: str = Field(default="", description="Expiration date in ISO format")
11+
franchise_name: str = Field(default="", description="Name of the franchise", alias="franchiseName")
12+
issuer_name: str = Field(default="", description="Name of the issuer", alias="issuerName")
13+
last_digits: str = Field(default="", description="Last digits of the card/token", alias="lastDigits")
14+
valid_until: str = Field(default="", description="Expiration date in ISO format", alias="validUntil")
1415
cvv: str = Field(default="", description="CVV associated with the token")
15-
installments: int = Field(default=0, description="Number of installments")
16+
installments: Optional[int] = Field(default=None, description="Number of installments")
17+
18+
model_config = ConfigDict(populate_by_name=True, arbitrary_types_allowed=True)
1619

1720
def expiration(self) -> str:
1821
"""
1922
Convert valid_until to 'mm/yy' format for expiration date.
2023
"""
2124
try:
22-
expiration_date = datetime.strptime(self.validUntil, "%Y-%m-%d")
25+
expiration_date = datetime.strptime(self.valid_until, "%Y-%m-%d")
2326
return expiration_date.strftime("%m/%y")
2427
except ValueError:
2528
return "Invalid date"
@@ -28,4 +31,14 @@ def to_dict(self) -> dict:
2831
"""
2932
Convert the Token object to a dictionary using the Pydantic model_dump method.
3033
"""
31-
return self.model_dump()
34+
return {
35+
"token": self.token,
36+
"subtoken": self.subtoken,
37+
"franchise": self.franchise,
38+
"franchiseName": self.franchise_name,
39+
"issuerName": self.issuer_name,
40+
"lastDigits": self.last_digits,
41+
"validUntil": self.valid_until,
42+
"cvv": self.cvv,
43+
"installments": self.installments,
44+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from pydantic import BaseModel, Field
2+
3+
from checkout.decorators.convert_to_boolean import convert_booleans_to_strings
4+
from checkout.decorators.filter_empty_values import filter_empty_values
5+
from checkout.entities.instrument import Instrument
6+
7+
8+
class InvalidateToKenRequest(BaseModel):
9+
locale: str = Field(default="es_CO", description="Locale of the request")
10+
instrument: Instrument = Field(..., description="Instrument details")
11+
12+
@filter_empty_values
13+
@convert_booleans_to_strings
14+
def to_dic(self) -> dict:
15+
return {"locale": self.locale, "instrument": self.instrument.to_dict()}

0 commit comments

Comments
 (0)