Skip to content

Commit 66706b7

Browse files
authored
Payment card (#13)
* support card number as account number * bump version * update README with installation and configuration instructions * remove random space * add example on how to process a batch transfers file [untested] * Transfer.create_many * public release version
1 parent fe958c0 commit 66706b7

8 files changed

Lines changed: 209 additions & 11 deletions

File tree

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ SHELL := bash
22
PATH := ./venv/bin:${PATH}
33
PYTHON = python3.7
44
PROJECT = cuenca
5-
isort = isort -rc -ac $(PROJECT) tests setup.py
6-
black = black -S -l 79 --target-version py38 $(PROJECT) tests setup.py
5+
isort = isort -rc -ac $(PROJECT) tests setup.py examples
6+
black = black -S -l 79 --target-version py38 $(PROJECT) tests setup.py examples
77

88

99
all: test

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,23 @@
44
[![codecov](https://codecov.io/gh/cuenca-mx/cuenca-python/branch/master/graph/badge.svg)](https://codecov.io/gh/cuenca-mx/cuenca-python)
55
[![PyPI](https://img.shields.io/pypi/v/cuenca.svg)](https://pypi.org/project/cuenca/)
66

7+
# Installation
8+
9+
`pip install cuenca`
10+
11+
# Authentication
12+
13+
The preferred way to configure the credentials for the client is to set the
14+
`CUENCA_API_KEY` and `CUENCA_API_SECRET` environment variables. The client
15+
library will automatically configure based on the values of those variables.
16+
17+
To configure manually:
18+
```python
19+
import cuenca
20+
21+
cuenca.configure(api_key='PKxxxx', api_secret='yyyyyy')
22+
```
23+
724
## Transfers
825

926
### Create transfer
@@ -17,7 +34,7 @@ local_transfer_id = '078efdc20bab456285437309c4b75673'
1734

1835
transfer = cuenca.Transfer.create(
1936
recipient_name='Benito Juárez',
20-
account_number='646180157042875763',
37+
account_number='646180157042875763', # CLABE or card number
2138
amount=12345, # Mx$123.45
2239
descriptor='sending money', # As it'll appear for the customer
2340
idempotency_key=local_transfer_id

cuenca/resources/transfers.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import datetime as dt
2-
from typing import ClassVar, Optional, cast
2+
from typing import ClassVar, List, Optional, Union, cast
33

44
from clabe import Clabe
55
from pydantic import BaseModel, StrictStr
66
from pydantic.dataclasses import dataclass
77

88
from ..types import Network, Status
9-
from ..validators import StrictPositiveInt, TransferQuery
9+
from ..validators import PaymentCardNumber, StrictPositiveInt, TransferQuery
1010
from .base import Creatable, Queryable, Retrievable
1111

1212

1313
class TransferRequest(BaseModel):
1414
recipient_name: StrictStr
15-
account_number: Clabe
15+
account_number: Union[Clabe, PaymentCardNumber]
1616
amount: StrictPositiveInt # in centavos
1717
descriptor: StrictStr # how it'll appear for the recipient
1818
idempotency_key: str # must be unique for each transfer
@@ -72,6 +72,12 @@ def create(
7272
)
7373
return cast('Transfer', cls._create(**req.dict()))
7474

75+
@classmethod
76+
def create_many(cls, requests: List[TransferRequest]) -> List['Transfer']:
77+
return [
78+
cast('Transfer', cls._create(**req.dict())) for req in requests
79+
]
80+
7581
@staticmethod
7682
def _gen_idempotency_key(account_number: str, amount: int) -> str:
7783
"""

cuenca/validators.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import datetime as dt
2-
from typing import Optional
2+
from typing import ClassVar, Optional
33

44
from pydantic import BaseModel, Extra, PositiveInt, StrictInt
5-
from pydantic.types import ConstrainedInt
5+
from pydantic.types import (
6+
ConstrainedInt,
7+
PaymentCardNumber as PydanticPaymentCardNumber,
8+
)
69

710
from .types import sanitize_dict
811
from .typing import DictStrAny
@@ -48,3 +51,8 @@ class TransferQuery(QueryParams):
4851

4952
class ApiKeyQuery(QueryParams):
5053
active: Optional[bool] = None
54+
55+
56+
class PaymentCardNumber(PydanticPaymentCardNumber):
57+
min_length: ClassVar[int] = 16
58+
max_length: ClassVar[int] = 16

cuenca/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
__version__ = '0.1.4'
1+
__version__ = '0.1.5'
22
CLIENT_VERSION = __version__
33
API_VERSION = '2020-03-19'

examples/batch_transfers.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import argparse
2+
import csv
3+
from dataclasses import fields
4+
5+
from cuenca.resources.transfers import Transfer, TransferRequest
6+
7+
8+
def main():
9+
parser = argparse.ArgumentParser(
10+
description='Process batch transfers file'
11+
)
12+
parser.add_argument(
13+
'input', type=str, help='Path to CSV batch transfers file',
14+
)
15+
parser.add_argument('output', type=str, help='Path to CSV output file')
16+
args = parser.parse_args()
17+
with open(args.input) as f:
18+
reader = csv.DictReader(f)
19+
# offline validation of transfers
20+
transfer_requests = [TransferRequest(**line) for line in reader]
21+
transfers = Transfer.create_many(transfer_requests)
22+
with open(args.output, 'w') as f:
23+
fieldnames = [field.name for field in fields(Transfer)]
24+
writer = csv.DictWriter(f, fieldnames)
25+
writer.writeheader()
26+
for tr in transfers:
27+
writer.writerow(tr.to_dict())
28+
29+
30+
if __name__ == '__main__':
31+
main()
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
interactions:
2+
- request:
3+
body: '{"recipient_name": "Rogelio Lopez", "account_number": "646180157034181180",
4+
"amount": 10000, "descriptor": "Mi primer transferencia", "idempotency_key":
5+
"081e71c19f8640048090b7cd740205b1"}'
6+
headers:
7+
Accept:
8+
- '*/*'
9+
Accept-Encoding:
10+
- gzip, deflate
11+
Authorization:
12+
- DUMMY
13+
Connection:
14+
- keep-alive
15+
Content-Length:
16+
- '188'
17+
Content-Type:
18+
- application/json
19+
User-Agent:
20+
- cuenca-python/0.1.5.rc0
21+
X-Cuenca-Api-Version:
22+
- '2020-03-19'
23+
method: POST
24+
uri: https://sandbox.cuenca.com/transfers
25+
response:
26+
body:
27+
string: '{"id":"TRK2s-yr17S_iAdawNSW0Rjw==","created_at":"2020-05-28T16:22:48.909272","updated_at":"2020-05-28T16:22:50.170473","account_number":"646180157034181180","recipient_name":"Rogelio
28+
Lopez","amount":10000,"descriptor":"Mi primer transferencia","idempotency_key":"081e71c19f8640048090b7cd740205b1","status":"submitted","network":"internal","tracking_key":null}'
29+
headers:
30+
Connection:
31+
- keep-alive
32+
Content-Length:
33+
- '359'
34+
Content-Type:
35+
- application/json
36+
Date:
37+
- Thu, 28 May 2020 16:22:50 GMT
38+
Via:
39+
- 1.1 c90de501a57b4f0090a033690602dc10.cloudfront.net (CloudFront)
40+
X-Amz-Cf-Id:
41+
- SkktvJVvta9Xgv5LTOvIgxWFKRpjZ3wvwkF5Uwx995yaTdHJZ-R86A==
42+
X-Amz-Cf-Pop:
43+
- DFW55-C2
44+
X-Amzn-Trace-Id:
45+
- Root=1-5ecfe555-0835695eb1f7c70c0991840e;Sampled=0
46+
X-Cache:
47+
- Miss from cloudfront
48+
x-amz-apigw-id:
49+
- NQDFUFjdoAMFeUw=
50+
x-amzn-RequestId:
51+
- 4ca5296b-be89-4818-a675-49d66a45d667
52+
status:
53+
code: 201
54+
message: Created
55+
- request:
56+
body: '{"recipient_name": "Rogelio Lopez", "account_number": "646180157034181180",
57+
"amount": 10001, "descriptor": "Mi primer transferencia", "idempotency_key":
58+
"64850742e0c14bc1b93e4b762f072592"}'
59+
headers:
60+
Accept:
61+
- '*/*'
62+
Accept-Encoding:
63+
- gzip, deflate
64+
Authorization:
65+
- DUMMY
66+
Connection:
67+
- keep-alive
68+
Content-Length:
69+
- '188'
70+
Content-Type:
71+
- application/json
72+
User-Agent:
73+
- cuenca-python/0.1.5.rc0
74+
X-Cuenca-Api-Version:
75+
- '2020-03-19'
76+
method: POST
77+
uri: https://sandbox.cuenca.com/transfers
78+
response:
79+
body:
80+
string: '{"id":"TRnaWQjdgQRs2CrrwQnTPfFg==","created_at":"2020-05-28T16:22:50.351720","updated_at":"2020-05-28T16:22:50.674677","account_number":"646180157034181180","recipient_name":"Rogelio
81+
Lopez","amount":10001,"descriptor":"Mi primer transferencia","idempotency_key":"64850742e0c14bc1b93e4b762f072592","status":"submitted","network":"internal","tracking_key":null}'
82+
headers:
83+
Connection:
84+
- keep-alive
85+
Content-Length:
86+
- '359'
87+
Content-Type:
88+
- application/json
89+
Date:
90+
- Thu, 28 May 2020 16:22:50 GMT
91+
Via:
92+
- 1.1 c90de501a57b4f0090a033690602dc10.cloudfront.net (CloudFront)
93+
X-Amz-Cf-Id:
94+
- Zqd0hwjKpKizNqgSz_JjguVJlvQq-K0PTSH7dqu-2bnQxv8b-UWCYQ==
95+
X-Amz-Cf-Pop:
96+
- DFW55-C2
97+
X-Amzn-Trace-Id:
98+
- Root=1-5ecfe55a-2457598049a260809ab5b300;Sampled=0
99+
X-Cache:
100+
- Miss from cloudfront
101+
x-amz-apigw-id:
102+
- NQDGIGiQIAMFsxw=
103+
x-amzn-RequestId:
104+
- b32aceaa-c62f-446d-a984-09d997426ae1
105+
status:
106+
code: 201
107+
message: Created
108+
version: 1

tests/resources/test_transfers.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from cuenca import Transfer
55
from cuenca.exc import MultipleResultsFound, NoResultFound
6+
from cuenca.resources.transfers import TransferRequest
67
from cuenca.types import Network, Status
78

89

@@ -18,10 +19,37 @@ def test_transfers_create():
1819
assert transfer.idempotency_key is not None
1920
assert transfer.status
2021
assert transfer.status == Status.submitted
21-
# Some seconds latter
22+
assert transfer.network == Network.internal
23+
# Some seconds later
2224
transfer.refresh()
2325
assert transfer.status == Status.succeeded
24-
assert transfer.network == Network.internal
26+
27+
28+
@pytest.mark.vcr
29+
def test_transfers_create_many():
30+
transfer_requests = [
31+
TransferRequest(
32+
account_number='646180157034181180',
33+
amount=10000,
34+
descriptor='Mi primer transferencia',
35+
recipient_name='Rogelio Lopez',
36+
idempotency_key='081e71c19f8640048090b7cd740205b1',
37+
),
38+
TransferRequest(
39+
account_number='646180157034181180',
40+
amount=10001,
41+
descriptor='Mi primer transferencia',
42+
recipient_name='Rogelio Lopez',
43+
idempotency_key='64850742e0c14bc1b93e4b762f072592',
44+
),
45+
]
46+
transfers = Transfer.create_many(transfer_requests)
47+
for transfer in transfers:
48+
assert transfer.id is not None
49+
assert transfer.idempotency_key is not None
50+
assert transfer.status
51+
assert transfer.status == Status.submitted
52+
assert transfer.network == Network.internal
2553

2654

2755
@pytest.mark.vcr

0 commit comments

Comments
 (0)