diff --git a/cuenca/__init__.py b/cuenca/__init__.py index 15afeb18..5bd6d5bd 100644 --- a/cuenca/__init__.py +++ b/cuenca/__init__.py @@ -2,11 +2,13 @@ '__version__', 'ApiKey', 'Account', + 'Arpc', 'BalanceEntry', 'BillPayment', 'Card', 'CardActivation', 'CardTransaction', + 'CardValidation', 'Commission', 'Deposit', 'LoginToken', @@ -24,11 +26,13 @@ from .resources import ( Account, ApiKey, + Arpc, BalanceEntry, BillPayment, Card, CardActivation, CardTransaction, + CardValidation, Commission, Deposit, LoginToken, diff --git a/cuenca/resources/__init__.py b/cuenca/resources/__init__.py index c0055cfe..70b07b56 100644 --- a/cuenca/resources/__init__.py +++ b/cuenca/resources/__init__.py @@ -1,11 +1,13 @@ __all__ = [ 'ApiKey', 'Account', + 'Arpc', 'BalanceEntry', 'BillPayment', 'Card', 'CardActivation', 'CardTransaction', + 'CardValidation', 'Commission', 'Deposit', 'LoginToken', @@ -18,10 +20,12 @@ from .accounts import Account from .api_keys import ApiKey +from .arpc import Arpc from .balance_entries import BalanceEntry from .bill_payments import BillPayment from .card_activations import CardActivation from .card_transactions import CardTransaction +from .card_validations import CardValidation from .cards import Card from .commissions import Commission from .deposits import Deposit @@ -38,11 +42,13 @@ resource_classes = [ ApiKey, Account, + Arpc, BalanceEntry, BillPayment, Card, CardActivation, CardTransaction, + CardValidation, Commission, Deposit, LoginToken, diff --git a/cuenca/resources/arpc.py b/cuenca/resources/arpc.py new file mode 100644 index 00000000..73237231 --- /dev/null +++ b/cuenca/resources/arpc.py @@ -0,0 +1,58 @@ +import datetime as dt +from typing import ClassVar, Optional, cast + +from cuenca_validations.types.requests import ARPCRequest +from pydantic.dataclasses import dataclass + +from ..http import Session, session as global_session +from .base import Creatable + + +@dataclass +class Arpc(Creatable): + """ + An ARPC (Authorisation Response Cryptogram) is generated by the issuer + to authorize EMV transactions (Chip, Contactless) + + The point of sales terminal or ATM generate an ARQC (Authorization Request + Cryptogram) to obtain authorization for transactions. + After, the issuer has to verified the ARQC and generate an ARPC. + Finally the ARPC is sent back to the point of sales terminal to authorize + the transaction and validate the issuer as authentic + """ + + _resource: ClassVar = 'arpc' + + created_at: dt.datetime + card_uri: str + is_valid_arqc: Optional[bool] + arpc: Optional[str] + err: Optional[str] + + @classmethod + def create( + cls, + number: str, + arqc: str, + arpc_method: str, + transaction_data: str, + response_code: str, + transaction_counter: str, + pan_sequence: str, + unique_number: str, + track_data_method: str, + *, + session: Session = global_session, + ) -> 'Arpc': + req = ARPCRequest( + number=number, + arqc=arqc, + arpc_method=arpc_method, + transaction_data=transaction_data, + response_code=response_code, + transaction_counter=transaction_counter, + pan_sequence=pan_sequence, + unique_number=unique_number, + track_data_method=track_data_method, + ) + return cast('Arpc', cls._create(session=session, **req.dict())) diff --git a/cuenca/resources/card_validations.py b/cuenca/resources/card_validations.py new file mode 100644 index 00000000..33f8a628 --- /dev/null +++ b/cuenca/resources/card_validations.py @@ -0,0 +1,66 @@ +import datetime as dt +from typing import ClassVar, Optional, cast + +from cuenca_validations.types import CardStatus, CardType +from cuenca_validations.types.requests import CardValidationRequest +from pydantic.dataclasses import dataclass + +from ..http import Session, session as global_session +from .base import Creatable +from .cards import Card +from .resources import retrieve_uri + + +@dataclass +class CardValidation(Creatable): + _resource: ClassVar = 'card_validations' + + created_at: dt.datetime + card_uri: str + user_id: str + card_status: CardStatus + card_type: CardType + is_valid_cvv: Optional[bool] + is_valid_cvv2: Optional[bool] + is_valid_icvv: Optional[bool] + is_valid_pin_block: Optional[bool] + is_valid_exp_date: Optional[bool] + is_expired: bool + + @classmethod + def create( + cls, + number: str, + cvv: Optional[str] = None, + cvv2: Optional[str] = None, + icvv: Optional[str] = None, + exp_month: Optional[int] = None, + exp_year: Optional[int] = None, + pin_block: Optional[str] = None, + *, + session: Session = global_session, + ) -> 'CardValidation': + req = CardValidationRequest( + number=number, + cvv=cvv, + cvv2=cvv2, + icvv=icvv, + exp_month=exp_month, + exp_year=exp_year, + pin_block=pin_block, + ) + return cast( + 'CardValidation', cls._create(session=session, **req.dict()) + ) + + @property + def card(self) -> Card: + return cast(Card, retrieve_uri(self.card_uri)) + + @property + def card_id(self) -> str: + return self.card_uri.split('/')[-1] + + @property + def is_active(self): + return self.card_status == CardStatus.active diff --git a/cuenca/resources/cards.py b/cuenca/resources/cards.py index ff602d1b..0eff1162 100644 --- a/cuenca/resources/cards.py +++ b/cuenca/resources/cards.py @@ -35,6 +35,10 @@ class Card(Retrievable, Queryable, Creatable, Updateable): def last_4_digits(self): return self.number[-4:] + @property + def bin(self): + return self.number[:6] + @classmethod def create( cls, @@ -64,6 +68,7 @@ def update( cls, card_id: str, status: Optional[CardStatus] = None, + pin_block: Optional[str] = None, *, session: Session = global_session, ) -> 'Card': @@ -73,9 +78,11 @@ def update( :param card_id: existing card_id :param status: + :param pin_block + :param session: :return: Updated card object """ - req = CardUpdateRequest(status=status) + req = CardUpdateRequest(status=status, pin_block=pin_block) resp = cls._update(card_id, session=session, **req.dict()) return cast('Card', resp) diff --git a/cuenca/version.py b/cuenca/version.py index 7cab8a4e..8187c3d7 100644 --- a/cuenca/version.py +++ b/cuenca/version.py @@ -1,3 +1,3 @@ -__version__ = '0.7.4' +__version__ = '0.7.5' CLIENT_VERSION = __version__ API_VERSION = '2020-03-19' diff --git a/requirements.txt b/requirements.txt index b6ea7db1..7b6a9c46 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ requests==2.25.1 -cuenca-validations==0.9.2 +cuenca-validations==0.9.3 dataclasses>=0.7;python_version<"3.7" diff --git a/tests/resources/cassettes/test_arpc.yaml b/tests/resources/cassettes/test_arpc.yaml new file mode 100644 index 00000000..647e4f1e --- /dev/null +++ b/tests/resources/cassettes/test_arpc.yaml @@ -0,0 +1,59 @@ +interactions: +- request: + body: '{"number": "1234567890123403", "arqc": "DB3C77D5469C53C6", "arpc_method": + "1", "transaction_data": "somerandomtransactiondata", + "response_code": "0010", "transaction_counter": "001D", "pan_sequence": "01", + "unique_number": "42D6A016", "track_data_method": "terminal"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - DUMMY + Connection: + - keep-alive + Content-Length: + - '320' + Content-Type: + - application/json + User-Agent: + - cuenca-python/0.7.5 + X-Cuenca-Api-Version: + - '2020-03-19' + method: POST + uri: https://sandbox.cuenca.com/arpc + response: + body: + string: '{"id":"ARfuiaeihfusbcibec","created_at":"2021-04-14T20:27:45.485000","card_uri":"/cards/CAoawhiursbcbeac","is_valid_arqc":true,"arpc":"16404ADAFB227B4D","err":null}' + headers: + Connection: + - keep-alive + Content-Length: + - '178' + Content-Type: + - application/json + Date: + - Wed, 14 Apr 2021 20:27:45 GMT + X-Amzn-Trace-Id: + - Root=1-60775040-62cf8efc0574bee15063442b;Sampled=0 + X-Request-Time: + - 'value: 1.001' + x-amz-apigw-id: + - dyl6HE7DiYcFdVg= + x-amzn-Remapped-Connection: + - keep-alive + x-amzn-Remapped-Content-Length: + - '178' + x-amzn-Remapped-Date: + - Wed, 14 Apr 2021 20:27:45 GMT + x-amzn-Remapped-Server: + - nginx/1.18.0 + x-amzn-Remapped-x-amzn-RequestId: + - fc51600d-4747-4443-89f1-a3c4040831b1 + x-amzn-RequestId: + - 342cac75-f28e-486d-9c8d-51923caf0624 + status: + code: 201 + message: Created +version: 1 diff --git a/tests/resources/cassettes/test_card_update_pin.yaml b/tests/resources/cassettes/test_card_update_pin.yaml new file mode 100644 index 00000000..40f1e20a --- /dev/null +++ b/tests/resources/cassettes/test_card_update_pin.yaml @@ -0,0 +1,56 @@ +interactions: +- request: + body: '{"pin_block": "7AC814A636D901BE"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - DUMMY + Connection: + - keep-alive + Content-Length: + - '33' + Content-Type: + - application/json + User-Agent: + - cuenca-python/0.7.5 + X-Cuenca-Api-Version: + - '2020-03-19' + method: PATCH + uri: https://sandbox.cuenca.com/cards/CAycvo_X9TQoKOKsaAvdqn3w + response: + body: + string: '{"id":"CAycvo_X9TQoKOKsaAvdqn3w","created_at":"2021-03-11T22:54:35.337000","updated_at":"2021-03-11T23:45:23.266000","user_id":"USfeauhf3873g85","number":"1234567890123403","exp_month":2,"exp_year":25,"cvv2":"150","type":"physical","status":"active","pin":"6725","issuer":"cuenca","funding_type":"debit","pin_block":"461F248CA285A5CB","batch":null,"manufacturer":null,"cvv":"685","icvv":"399","pin_block_switch":"7AC814A636D901BE","pin_block_embosser":"26641FB1CB03B667"}' + headers: + Connection: + - keep-alive + Content-Length: + - '480' + Content-Type: + - application/json + Date: + - Wed, 14 Apr 2021 23:05:36 GMT + X-Amzn-Trace-Id: + - Root=1-60777538-1d5098837edce6774d61ecb7;Sampled=0 + X-Request-Time: + - 'value: 7.757' + x-amz-apigw-id: + - dy9A0EDaCYcFWzw= + x-amzn-Remapped-Connection: + - keep-alive + x-amzn-Remapped-Content-Length: + - '480' + x-amzn-Remapped-Date: + - Wed, 14 Apr 2021 23:05:36 GMT + x-amzn-Remapped-Server: + - nginx/1.18.0 + x-amzn-Remapped-x-amzn-RequestId: + - 85b2184b-d132-46d8-9fc3-2abb4167eb0f + x-amzn-RequestId: + - 07367a9e-1066-419a-8048-07dabe32595d + status: + code: 200 + message: OK +version: 1 diff --git a/tests/resources/cassettes/test_card_validations.yaml b/tests/resources/cassettes/test_card_validations.yaml new file mode 100644 index 00000000..eda3c0ec --- /dev/null +++ b/tests/resources/cassettes/test_card_validations.yaml @@ -0,0 +1,107 @@ +interactions: +- request: + body: '{"number": "1234567890123403", "exp_month": 2, "exp_year": 25, "cvv": "685", + "cvv2": "150", "icvv": "399", "pin_block": "BDIEHA38457W"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - DUMMY + Connection: + - keep-alive + Content-Length: + - '139' + Content-Type: + - application/json + User-Agent: + - cuenca-python/0.7.5 + X-Cuenca-Api-Version: + - '2020-03-19' + method: POST + uri: https://sandbox.cuenca.com/card_validations + response: + body: + string: '{"id":"CVlYj-hNIfQpmJH7mcc8GpoA","created_at":"2021-04-14T23:19:53.928000","card_uri":"/cards/CA7pET83WM25VXO3IQ8BwYO3","user_id":"US3zGWi1n852bTyqD1wCZ1ft","card_status":"active","card_type":"physical","is_active":true,"is_valid_cvv":true,"is_valid_cvv2":true,"is_valid_icvv":true,"is_valid_pin_block":true,"is_valid_exp_date":true,"is_expired":false}' + headers: + Connection: + - keep-alive + Content-Length: + - '353' + Content-Type: + - application/json + Date: + - Wed, 14 Apr 2021 23:19:54 GMT + X-Amzn-Trace-Id: + - Root=1-60777894-7b1b1c365ab7d3e818fd9b89;Sampled=0 + X-Request-Time: + - 'value: 5.099' + x-amz-apigw-id: + - dy_HSH_miYcF2Yw= + x-amzn-Remapped-Connection: + - keep-alive + x-amzn-Remapped-Content-Length: + - '353' + x-amzn-Remapped-Date: + - Wed, 14 Apr 2021 23:19:54 GMT + x-amzn-Remapped-Server: + - nginx/1.18.0 + x-amzn-Remapped-x-amzn-RequestId: + - 1b3475f9-24d3-4d08-bad5-de61287be5e4 + x-amzn-RequestId: + - c0f8ddfb-88d8-4d11-b3cf-7a4048abdea5 + status: + code: 201 + message: Created +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - DUMMY + Connection: + - keep-alive + User-Agent: + - cuenca-python/0.7.5 + X-Cuenca-Api-Version: + - '2020-03-19' + method: GET + uri: https://sandbox.cuenca.com/cards/CA7pET83WM25VXO3IQ8BwYO3 + response: + body: + string: '{"id":"CA7pET83WM25VXO3IQ8BwYO3","created_at":"2021-03-11T22:54:35.337000","updated_at":"2021-03-11T23:45:23.266000","user_id":"US3zGWi1n852bTyqD1wCZ1ft","number":"5448750129965637","exp_month":2,"exp_year":25,"cvv2":"150","type":"physical","status":"active","pin":"6725","issuer":"cuenca","funding_type":"debit","pin_block":"461F248CA285A5CB","batch":null,"manufacturer":null,"cvv":"685","icvv":"399","pin_block_switch":"3B241739AB05D290","pin_block_embosser":"26641FB1CB03B667"}' + headers: + Connection: + - keep-alive + Content-Length: + - '480' + Content-Type: + - application/json + Date: + - Wed, 14 Apr 2021 23:19:54 GMT + X-Amzn-Trace-Id: + - Root=1-6077789a-1895180e4d53b05655b53c24;Sampled=0 + X-Request-Time: + - 'value: 0.708' + x-amz-apigw-id: + - dy_IHEEZiYcFQsw= + x-amzn-Remapped-Connection: + - keep-alive + x-amzn-Remapped-Content-Length: + - '480' + x-amzn-Remapped-Date: + - Wed, 14 Apr 2021 23:19:54 GMT + x-amzn-Remapped-Server: + - nginx/1.18.0 + x-amzn-Remapped-x-amzn-RequestId: + - fbde3e85-a233-481e-b0c0-33b0e9fc92d1 + x-amzn-RequestId: + - 624ac3a8-9c57-4b4e-83b8-a7f21722e4ff + status: + code: 200 + message: OK +version: 1 diff --git a/tests/resources/test_arpc.py b/tests/resources/test_arpc.py new file mode 100644 index 00000000..aad649ae --- /dev/null +++ b/tests/resources/test_arpc.py @@ -0,0 +1,20 @@ +import pytest + +from cuenca.resources import Arpc + + +@pytest.mark.vcr +def test_arpc(): + arpc_req = dict( + number='1234567890123403', + arqc='DB3C77D5469C53C6', + arpc_method='1', + transaction_data='somerandomtransactiondata', + response_code='0010', + pan_sequence='01', + unique_number='42D6A016', + transaction_counter='001D', + track_data_method='terminal', + ) + arpc = Arpc.create(**arpc_req) + assert arpc.is_valid_arqc diff --git a/tests/resources/test_card_validations.py b/tests/resources/test_card_validations.py new file mode 100644 index 00000000..948e7efa --- /dev/null +++ b/tests/resources/test_card_validations.py @@ -0,0 +1,28 @@ +import pytest + +from cuenca.resources import CardValidation + + +@pytest.mark.vcr +def test_card_validations(): + card_data = dict( + number='1234567890123403', + cvv='685', + cvv2='150', + icvv='399', + exp_month=2, + exp_year=25, + pin_block='BDIEHA38457W', + ) + validation = CardValidation.create(**card_data) + assert validation.is_active + assert validation.card_uri is not None + assert validation.is_valid_cvv + assert validation.is_valid_cvv2 + assert validation.is_valid_icvv + assert validation.is_valid_pin_block + assert validation.is_valid_exp_date + assert not validation.is_expired + c = validation.card + assert validation.card_id == c.id + assert validation.is_active diff --git a/tests/resources/test_cards.py b/tests/resources/test_cards.py index ce1d1ec7..19b4a42c 100644 --- a/tests/resources/test_cards.py +++ b/tests/resources/test_cards.py @@ -35,6 +35,7 @@ def test_card_retrieve(): assert card.id == card_id assert len(card.number) == 16 assert card.last_4_digits == '9849' + assert card.bin == '544875' assert card.type == CardType.virtual @@ -74,6 +75,13 @@ def test_card_update(): assert card.status == CardStatus.active +@pytest.mark.vcr +def test_card_update_pin(): + new_pin = '7AC814A636D901BE' + card = Card.update(card_id, pin_block=new_pin) + assert card + + @pytest.mark.vcr def test_deactivate_card(): card = Card.deactivate(card_id)