Skip to content

Commit

Permalink
Topup => BillPayment (#76)
Browse files Browse the repository at this point in the history
* topups => bill_payments

* comment

* new formatting

* better formatting

* fix tests

* proxy doesn't handle multiple balances properly

* some cleanup

* exclude three lines from coverage

* bump version. breaking changes

* fetch both accounts when proxying
  • Loading branch information
matin authored and felipao-mx committed Oct 1, 2019
1 parent b84070f commit e0d2b7a
Show file tree
Hide file tree
Showing 22 changed files with 270 additions and 175 deletions.
14 changes: 9 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ SHELL := bash
PATH := ./venv/bin:${PATH}
PYTHON=python3.7
PROJECT=arcus
isort = isort -rc -ac $(PROJECT) tests setup.py
black = black -S -l 79 --target-version py37 $(PROJECT) tests setup.py


all: test
Expand All @@ -16,12 +18,14 @@ install-test:
test: clean install-test lint
python setup.py test

polish:
black -S -l 79 **/*.py
isort -rc --atomic **/*.py
format:
$(isort)
$(black)

lint:
pycodestyle setup.py $(PROJECT)/ tests/
flake8 $(PROJECT) tests setup.py
$(isort) --check-only
$(black) --check

clean:
find . -name '*.pyc' -exec rm -f {} +
Expand All @@ -34,4 +38,4 @@ release: clean
twine upload dist/*


.PHONY: all install-test release test clean-pyc
.PHONY: all install-test test format lint clean release
2 changes: 2 additions & 0 deletions arcus/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
__all__ = ['client', 'exc', 'resources', 'Client']

from . import client, exc, resources
from .client import Client
46 changes: 28 additions & 18 deletions arcus/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@
from .api_keys import ApiKey
from .auth import compute_auth_header, compute_date_header, compute_md5_header
from .exc import Forbidden, InvalidAuth, NotFound, UnprocessableEntity
from .resources import Account, Bill, Biller, Resource, Topup, Transaction
from .resources import (
Account,
Bill,
Biller,
BillPayment,
Resource,
Transaction,
)

API_VERSION = '3.1'
PRODUCTION_API_URL = 'https://api.regalii.com'
Expand All @@ -17,7 +24,7 @@ class Client:

bills = Bill
billers = Biller
topups = Topup
bill_payments = BillPayment
transactions = Transaction

def __init__(
Expand All @@ -34,28 +41,27 @@ def __init__(
self.session = requests.Session()
self.sandbox = sandbox
self.proxy = proxy
if not proxy:
self.api_key = ApiKey(
primary_user or os.environ['ARCUS_API_KEY'],
primary_secret or os.environ['ARCUS_SECRET_KEY'],
self.api_key = ApiKey(
primary_user or os.environ['ARCUS_API_KEY'],
primary_secret or os.environ['ARCUS_SECRET_KEY'],
)

try:
self.topup_key = ApiKey(
topup_user or os.environ.get('TOPUP_API_KEY'),
topup_secret or os.environ.get('TOPUP_SECRET_KEY', ''),
)
try:
self.topup_key = ApiKey(
topup_user or os.environ.get('TOPUP_API_KEY'),
topup_secret or os.environ.get('TOPUP_SECRET_KEY'),
)
except ValueError:
self.topup_key = None
except ValueError:
self.topup_key = None

if not proxy:
if sandbox:
self.base_url = SANDBOX_API_URL
else:
self.base_url = PRODUCTION_API_URL
else:
self.api_key = None
self.topup_key = None
self.base_url = proxy
self.headers['X-ARCUS-SANDBOX'] = str(sandbox).lower()

Resource._client = self

def get(self, endpoint: str, **kwargs) -> dict:
Expand All @@ -81,7 +87,11 @@ def request(
api_key = self.api_key
headers = self._build_headers(api_key, endpoint, api_version, data)
else:
headers = {} # Proxy is going to sign the request
if topup:
api_key = self.topup_key.user
else:
api_key = self.api_key.user
headers = {'X-ARCUS-API-KEY': api_key}
response = self.session.request(
method,
url,
Expand Down Expand Up @@ -130,4 +140,4 @@ def _check_response(response):
elif response.status_code == 403:
raise Forbidden
else:
response.raise_for_status()
response.raise_for_status() # pragma: no cover
11 changes: 10 additions & 1 deletion arcus/resources/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
__all__ = [
'Account',
'Resource',
'Biller',
'Bill',
'BillPayment',
'Transaction',
]

from .accounts import Account
from .base import Resource
from .bill_payments import BillPayment
from .billers import Biller
from .bills import Bill
from .topups import Topup
from .transactions import Transaction
6 changes: 4 additions & 2 deletions arcus/resources/base.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from typing import ClassVar

import iso8601


class Resource:
_client = None
_endpoint: str
_client: ClassVar['arcus.Client']
_endpoint: ClassVar[str]

def __post_init__(self):
for attr, value in self.__dict__.items():
Expand Down
50 changes: 26 additions & 24 deletions arcus/resources/topups.py → arcus/resources/bill_payments.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import datetime
from dataclasses import dataclass, field
from typing import Optional
from typing import ClassVar, Optional

from arcus.exc import InvalidAccountNumber, UnprocessableEntity

from .base import Resource

"""
Arcus API version 1.6 should be used in order to make top-up operations.
It's because newer API versions don't support this kind of operations.
https://www.arcusfi.com/api/v1_6/#pay
Some of Arcus's billers don't support the new API version 3.1, which is why we
have to use 1.6, the older version instead.
"""
TOPUP_API_VERSION = '1.6'
OLD_API_VERSION = '1.6'


@dataclass
class Topup(Resource):
_endpoint = '/bill/pay'
class BillPayment(Resource):
_endpoint: ClassVar[str] = '/bill/pay'

id: int
biller_id: int
Expand All @@ -43,31 +42,33 @@ class Topup(Resource):

@classmethod
def create(
cls,
biller_id: int,
account_number: str,
amount: float,
currency: str = 'MXN',
name_on_account: Optional[str] = None
cls,
biller_id: int,
account_number: str,
amount: float,
currency: str = 'MXN',
name_on_account: Optional[str] = None,
topup: bool = False, # if True, the topup creds will be used
):
if not isinstance(amount, float):
raise TypeError('amount must be a float')
data = dict(biller_id=biller_id,
account_number=account_number,
amount=amount,
currency=currency,
name_on_account=name_on_account)
data = dict(
biller_id=biller_id,
account_number=account_number,
amount=amount,
currency=currency,
name_on_account=name_on_account,
)
try:
topup_dict = cls._client.post(
cls._endpoint, data, api_version=TOPUP_API_VERSION,
topup=True
bill_payment_dict = cls._client.post(
cls._endpoint, data, api_version=OLD_API_VERSION, topup=topup
)
except UnprocessableEntity as ex:
if ex.code in {'R2', 'R5'}:
raise InvalidAccountNumber(ex.code, account_number, biller_id)
else:
raise
return Topup(**topup_dict)
raise # pragma: no cover
return BillPayment(**bill_payment_dict)

@classmethod
def get(cls, _):
Expand All @@ -80,4 +81,5 @@ def get(cls, _):
def list(cls):
raise NotImplementedError(
f"{cls.__name__}.list() hasn't been implemented or isn't "
f'supported by the API.')
f'supported by the API.'
)
5 changes: 3 additions & 2 deletions arcus/resources/billers.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ def list(cls, **filters):
billers = []
for endpoint in ENDPOINTS:
billers.extend(cls._list_for_endpoint(endpoint))
filtered_billers = [Biller(**biller_dict)
for biller_dict in filter_(billers, filters)]
filtered_billers = [
Biller(**biller_dict) for biller_dict in filter_(billers, filters)
]
return filtered_billers

@classmethod
Expand Down
20 changes: 14 additions & 6 deletions arcus/resources/bills.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import datetime
from dataclasses import dataclass, field
from typing import Optional, Union
from typing import ClassVar, Optional, Union

from arcus.exc import (
AlreadyPaid, DuplicatedPayment, IncompleteAmount,
InvalidAccountNumber, InvalidAmount, InvalidBiller,
NotFound, RecurrentPayments, UnprocessableEntity)
AlreadyPaid,
DuplicatedPayment,
IncompleteAmount,
InvalidAccountNumber,
InvalidAmount,
InvalidBiller,
NotFound,
RecurrentPayments,
UnprocessableEntity,
)

from .base import Resource
from .transactions import Transaction


@dataclass
class Bill(Resource):
_endpoint = '/bills'
_endpoint: ClassVar[str] = '/bills'

id: int
biller_id: int
Expand Down Expand Up @@ -58,7 +65,8 @@ def pay(self, amount: Optional[float] = None) -> Transaction:
data = dict(amount=amount, currency=self.balance_currency)
try:
transaction_dict = self._client.post(
f'{self._endpoint}/{self.id}/pay', data)
f'{self._endpoint}/{self.id}/pay', data
)
except UnprocessableEntity as ex:
if ex.code == 'R102':
raise InvalidAmount(ex.code, ex.message, amount=amount)
Expand Down
11 changes: 6 additions & 5 deletions arcus/resources/transactions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import datetime
from dataclasses import dataclass, field
from typing import Union
from typing import ClassVar, Union

from arcus.exc import InvalidOperation, UnprocessableEntity

Expand All @@ -9,7 +9,7 @@

@dataclass
class Transaction(Resource):
_endpoint = '/transactions'
_endpoint: ClassVar[str] = '/transactions'

id: int
amount: float
Expand All @@ -25,8 +25,9 @@ class Transaction(Resource):

@classmethod
def get(cls, transaction_id: Union[int, str]):
transaction_dict = (
cls._client.get(f'{cls._endpoint}?q[id_eq]={transaction_id}'))
transaction_dict = cls._client.get(
f'{cls._endpoint}?q[id_eq]={transaction_id}'
)
return Transaction(**transaction_dict['transactions'][0])

def refresh(self):
Expand All @@ -41,6 +42,6 @@ def cancel(self) -> dict:
if ex.code in ['R26', 'R103']:
raise InvalidOperation(ex.code, self.id)
else:
raise
raise # pragma: no cover
self.refresh()
return resp
2 changes: 1 addition & 1 deletion arcus/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.1.2' # pragma: no cover
__version__ = '1.2.0' # pragma: no cover
10 changes: 10 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,13 @@ test=pytest

[tool:pytest]
addopts = -p no:warnings -v --cov=arcus

[flake8]
inline-quotes = '
multiline-quotes = """
[isort]
multi_line_output=3
include_trailing_comma=True
force_grid_wrap=0
combine_as_imports=True
15 changes: 11 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,15 @@

version = SourceFileLoader('version', 'arcus/version.py').load_module()

test_requires = ['pytest', 'pytest-vcr', 'pycodestyle', 'pytest-cov',
'black', 'isort[pipfile]', 'requests-mock']
test_requires = [
'pytest',
'pytest-vcr',
'pytest-cov',
'black',
'isort[pipfile]',
'flake8',
'requests-mock',
]

with open('README.md', 'r') as f:
long_description = f.read()
Expand All @@ -27,7 +34,7 @@
'pytz==2018.9',
'iso8601>=0.1.12,<0.2.0',
'pydash>=4.7.4,<4.8.0',
'dataclasses>=0.6;python_version<"3.7"'
'dataclasses>=0.6;python_version<"3.7"',
],
setup_requires=['pytest-runner'],
tests_require=test_requires,
Expand All @@ -38,5 +45,5 @@
'Programming Language :: Python :: 3.7',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
]
],
)
Loading

0 comments on commit e0d2b7a

Please sign in to comment.