Skip to content

Commit b53ca81

Browse files
committed
feat: adds invaliteToken endpoint
1 parent 6f65ea9 commit b53ca81

File tree

8 files changed

+87
-10
lines changed

8 files changed

+87
-10
lines changed

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.

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: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from typing import Dict
22

33
from checkout.contracts.carrier import Carrier
4-
from checkout.entities.instrument import Instrument
54
from checkout.entities.settings import Settings
65
from checkout.entities.status import Status
76
from checkout.messages.requests.collect import CollectRequest
7+
from checkout.messages.requests.invalidate_token import InvalidateToKenRequest
88
from checkout.messages.requests.redirect import RedirectRequest
99
from checkout.messages.responses.information import InformationResponse
1010
from checkout.messages.responses.redirect import RedirectResponse
@@ -59,9 +59,10 @@ def reverse(self, transaction_id: str) -> ReverseResponse:
5959
result = self._post("api/reverse", {"internalReference": transaction_id})
6060
return ReverseResponse(**result)
6161

62-
def invalidateToken(self, instrument: Instrument) -> Status:
62+
def invalidateToken(self, invalidate_token_request: InvalidateToKenRequest) -> Status:
6363
"""
6464
Invalidate a token.
6565
"""
66-
result = self._post("/api/instrument/invalidate", {"instrument": instrument.to_dict()})
67-
return Status(**result)
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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from typing import Protocol
22

3-
from checkout.entities.instrument import Instrument
43
from checkout.entities.status import Status
54
from checkout.messages.requests.collect import CollectRequest
5+
from checkout.messages.requests.invalidate_token import InvalidateToKenRequest
66
from checkout.messages.requests.redirect import RedirectRequest
77
from checkout.messages.responses.information import InformationResponse
88
from checkout.messages.responses.redirect import RedirectResponse
@@ -30,7 +30,7 @@ def reverse(self, transaction_id: str) -> ReverseResponse:
3030
Reverse a transaction by its ID.
3131
"""
3232

33-
def invalidateToken(self, instrument: Instrument) -> Status:
33+
def invalidateToken(self, invalidate_token_request: InvalidateToKenRequest) -> Status:
3434
"""
3535
invalidate a token.
3636
"""

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/tests/feature/test_checkout.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,23 @@ def test_reverse_valid(self, mock_post, mock_response):
216216
self.assertEqual(payment.processor_fields[-1].value, "00")
217217
self.assertEqual(payment.processor_fields[-1].keyword, "b24")
218218

219+
@patch("requests.post")
220+
@RedirectResponseMock.mock_response_decorator("invalidate_token_response_successful")
221+
def test_invalidate_token_succssful(self, mock_post, mock_response):
222+
mock_post.return_value = mock_response
223+
224+
instrument = Instrument(
225+
token=Token(
226+
token="5caef08ecd1230088a12e8f7d9ce20e9134dc6fc049c8a4857c9ba6e942b16b2", subtoken="test_subtoken"
227+
)
228+
)
229+
invalidate_token_request = {"locale": "en_US", "instrument": instrument}
230+
result = self.checkout.invalidateToken(invalidate_token_request)
231+
232+
self.assertEqual(result.status, "APPROVED")
233+
self.assertEqual(result.reason, "00")
234+
self.assertEqual(result.message, "The petition has been successfully approved")
235+
219236
@patch("requests.post")
220237
@RedirectResponseMock.mock_response_decorator("redirect_response_fail_authentication", 401)
221238
def test_request_fails_bad_request(self, mock_post, mock_response):
@@ -283,3 +300,19 @@ def test_reverse_fails_when_transaction_not_found(self, mock_post, mock_response
283300
self.assertEqual("FAILED", error_details["status"]["status"])
284301
self.assertEqual("request_not_valid", error_details["status"]["reason"])
285302
self.assertEqual("No existe la transacción que busca", error_details["status"]["message"])
303+
304+
@patch("requests.post")
305+
@RedirectResponseMock.mock_response_decorator("invalidate_token_response_fails_token_not_valid", 400)
306+
def test_invalidate_fails_when_token_is_not_valid(self, mock_post, mock_response):
307+
mock_post.return_value = mock_response
308+
309+
instrument = Instrument(token=Token(token="not_valid_token", subtoken="test_subtoken"))
310+
invalidate_token_request = {"locale": "en_US", "instrument": instrument}
311+
312+
with self.assertRaises(ClientErrorException) as context:
313+
self.checkout.invalidateToken(invalidate_token_request)
314+
315+
error_details = json.loads(str(context.exception))["error_details"]
316+
self.assertEqual("FAILED", error_details["status"]["status"])
317+
self.assertEqual("XN", error_details["status"]["reason"])
318+
self.assertEqual("The token used is invalid", error_details["status"]["message"])
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"status": {
3+
"status": "FAILED",
4+
"reason": "XN",
5+
"message": "The token used is invalid",
6+
"date": "2022-07-27T14:51:27-05:00"
7+
}
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"status": {
3+
"status": "APPROVED",
4+
"reason": "00",
5+
"message": "The petition has been successfully approved",
6+
"date": "2022-07-27T14:51:27-05:00"
7+
}
8+
}

0 commit comments

Comments
 (0)