From 0a719450f4f1da412da1e7616d2f110dd6722708 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Fri, 1 Mar 2019 14:09:54 +0300 Subject: [PATCH 01/35] unittest coverage --- src/momoapi/errors.py | 136 ++----------------------------- src/momoapi/utils.py | 145 ++++++++++++++++++++++++++++++++++ tests/unitests/test_client.py | 32 ++++++++ tests/unitests/utils.py | 47 ++++++++--- 4 files changed, 222 insertions(+), 138 deletions(-) create mode 100644 src/momoapi/utils.py diff --git a/src/momoapi/errors.py b/src/momoapi/errors.py index 42dde61..0efa3e8 100644 --- a/src/momoapi/errors.py +++ b/src/momoapi/errors.py @@ -1,4 +1,6 @@ +from .utils import ERROR_CODES + class MomoError(Exception): def __init__(self, message=None, http_body=None, http_status=None, @@ -40,13 +42,14 @@ def __repr__(self): self.request_id) - class APIError(MomoError): pass + class APIConnectionError(MomoError): pass + class AuthenticationError(MomoError): pass @@ -54,141 +57,18 @@ class AuthenticationError(MomoError): class PermissionError(MomoError): pass + class PreapprovalError(MomoError): pass + class RequestToPayError(MomoError): pass + class TransferError(MomoError): pass + class GeneralError(MomoError): pass - - -ERROR_CODES= [ - { - "http_code": 409, - "response_code":None, - "description": "Duplicated Reference Id. Cannot create new resource", - "error_type": "generic" - - }, - { - "http_code": 404, - "response_code":None, - "description": "Reference Id not found. Requested resource does not exist", - "error_type": "generic" - - }, - { - "http_code": 400, - "response_code":None, - "description": "Bad request. Request does not follow the specification.", - "error_type": "generic" - - }, - { - "http_code": 401, - "response_code":None, - "description": "Authentication failed. Credentials not valid", - "error_type": "generic" - - }, - - { - "http_code": 500, - "response_code":"NOT_ALLOWED", - "description": "Authorization failed. User does not have permission.", - "error_type": "generic" - - }, - - { - "http_code": 500, - "response_code":"NOT_ALLOWED_TARGET_ENVIRONMENT", - "description": "Not allowed target environment", - "error_type": "generic" - - }, - - { - "http_code": 500, - "response_code":"INVALID_CALLBACK_URL_HOST", - "description": "Callback URL with different host name then configured for API User", - "error_type": "generic" - - }, - - { - "http_code": 500, - "response_code":"INVALID_CURRENCY", - "description": "Currency not supported on the requested account", - "error_type": "generic" - - }, - - { - "http_code": 500, - "response_code":"INTERNAL_PROCESSING_ERROR", - "description": "Default error code used when there is no specific error mapping.", - "error_type": "generic" - - }, - - { - "http_code": 500, - "response_code":"SERVICE_UNAVAILABLE", - "description": "Service temporary unavailable, try again later", - "error_type": "generic" - - }, - - { - "http_code": 500, - "response_code":"PAYER_NOT_FOUND", - "description": "Payer not found", - "error_type": "preapproval" - - }, - - { - "http_code": 500, - "response_code":"PAYEE_NOT_ALLOWED_TO_RECEIVE", - "description": "Payee cannot receive funds due to e.g. transfer limit.", - "error_type": "request_to_pay" - - }, - - { - "http_code": 500, - "response_code":"NOT_ENOUGH_FUNDS", - "description": "Not enough funds on payer account", - "error_type": "transfer" - - }, - { - "http_code": 500, - "response_code":"PAYER_LIMIT_REACHED", - "description": "Not allowed to end due to Payer limit reached", - "error_type": "transfer" - - }, - - { - "http_code": 500, - "response_code":"PAYEE_NOT_FOUND", - "description": "Payee not found. Account holder is not registered", - "error_type": "transfer" - - }, - - { - "http_code": 404, - "response_code":None, - "description": "Account holder is not found", - "error_type": "account" - - } -] diff --git a/src/momoapi/utils.py b/src/momoapi/utils.py new file mode 100644 index 0000000..4651c38 --- /dev/null +++ b/src/momoapi/utils.py @@ -0,0 +1,145 @@ +ERROR_CODES = [ + { + "http_code": 409, + "response_code": None, + "description": "Duplicated Reference Id. Cannot create new resource", + "error_type": "generic" + + }, + { + "http_code": 404, + "response_code": None, + "description": "Reference Id not found. Requested resource does not exist", + "error_type": "generic" + + }, + { + "http_code": 400, + "response_code": None, + "description": "Bad request. Request does not follow the specification.", + "error_type": "generic" + + }, + { + "http_code": 401, + "response_code": None, + "description": "Authentication failed. Credentials not valid", + "error_type": "generic" + + }, + + { + "http_code": 500, + "response_code": "NOT_ALLOWED", + "description": "Authorization failed. User does not have permission.", + "error_type": "generic" + + }, + + { + "http_code": 500, + "response_code": "NOT_ALLOWED_TARGET_ENVIRONMENT", + "description": "Not allowed target environment", + "error_type": "generic" + + }, + + { + "http_code": 500, + "response_code": "INVALID_CALLBACK_URL_HOST", + "description": "Callback URL with different host name then configured for API User", + "error_type": "generic" + + }, + + { + "http_code": 500, + "response_code": "INVALID_CURRENCY", + "description": "Currency not supported on the requested account", + "error_type": "generic" + + }, + + { + "http_code": 500, + "response_code": "INTERNAL_PROCESSING_ERROR", + "description": "Default error code used when there is no specific error mapping.", + "error_type": "generic" + + }, + + { + "http_code": 500, + "response_code": "SERVICE_UNAVAILABLE", + "description": "Service temporary unavailable, try again later", + "error_type": "generic" + + }, + + { + "http_code": 500, + "response_code": "PAYER_NOT_FOUND", + "description": "Payer not found", + "error_type": "preapproval" + + }, + + { + "http_code": 500, + "response_code": "PAYEE_NOT_ALLOWED_TO_RECEIVE", + "description": "Payee cannot receive funds due to e.g. transfer limit.", + "error_type": "request_to_pay" + + }, + + { + "http_code": 500, + "response_code": "NOT_ENOUGH_FUNDS", + "description": "Not enough funds on payer account", + "error_type": "transfer" + + }, + { + "http_code": 500, + "response_code": "PAYER_LIMIT_REACHED", + "description": "Not allowed to end due to Payer limit reached", + "error_type": "transfer" + + }, + + { + "http_code": 500, + "response_code": "PAYEE_NOT_FOUND", + "description": "Payee not found. Account holder is not registered", + "error_type": "transfer" + + }, + + { + "http_code": 404, + "response_code": None, + "description": "Account holder is not found", + "error_type": "account" + + } +] + + +def requests_retry_session( + retries=3, + backoff_factor=0.3, + status_forcelist=(502, 504), + session=None, +): + session = session or requests.Session() + retry = Retry( + total=retries, + read=retries, + connect=retries, + backoff_factor=backoff_factor, + status_forcelist=status_forcelist, + ) + adapter = HTTPAdapter(max_retries=retry) + session.mount('http://', adapter) + session.mount('https://', adapter) + return session diff --git a/tests/unitests/test_client.py b/tests/unitests/test_client.py index 97bb8e9..7b1ceb0 100644 --- a/tests/unitests/test_client.py +++ b/tests/unitests/test_client.py @@ -1,6 +1,7 @@ import unittest import pytest from momoapi.client import MomoApi +import types try: from unittest import mock except ImportError: @@ -34,3 +35,34 @@ def test_request_to_pay(self, mock_get): ref = self.client.requestToPay("256772123456", "600", "123456789", note="dd", message="dd", currency="EUR", environment="sandbox") + + assert isinstance(ref, dict) + assert "transaction_ref" in ref.keys() + + @mock.patch.object(MomoApi, "request", side_effect=mocked_requests_session) + def test_get_balance(self, mock_get): + balance = self.client.getBalance() + assert isinstance(balance, dict) + assert "availableBalance" in balance.keys() + assert "currency" in balance.keys() + + # @mock.patch('requests.get', side_effect=mocked_requests_get) + @mock.patch.object(MomoApi, "request", side_effect=mocked_requests_session) + def test_get_transaction_status(self, mock_get): + status = self.client.getTransactionStatus("dummy") + assert isinstance(status, dict) + assert "amount" in status.keys() + assert "currency" in status.keys() + + @mock.patch.object(MomoApi, "request", side_effect=mocked_requests_session) + def test_transfer(self, mock): + ref = self.client.transfer("600", "256772123456", note="dd", + message="dd", currency="EUR", environment="sandbox") + assert isinstance(ref, dict) + assert "transaction_ref" in ref.keys() + + @mock.patch('requests.post', side_effect=mocked_requests_post) + def test_generate_token(self, mock): + res = MomoApi.generateToken("dummy_host", "dummy_user", "dummy_key", "dummy_base") + assert isinstance(res, dict) + assert "apiKey" in res.keys() diff --git a/tests/unitests/utils.py b/tests/unitests/utils.py index f8b0b71..59dd1f4 100644 --- a/tests/unitests/utils.py +++ b/tests/unitests/utils.py @@ -17,14 +17,18 @@ def json(self): def mocked_requests_get(*args, **kwargs): - - import pdb - pdb.set_trace() - - if args[0] == 'http://someurl.com/test.json': - return MockResponse({"key1": "value1"}, 200) - elif args[0] == 'http://someotherurl.com/anothertest.json': - return MockResponse({"key2": "value2"}, 200) + if "/requesttopay" in args[0]: + return MockResponse({ + "amount": 100, + "currency": "UGX", + "financialTransactionId": 23503452, + "externalId": 947354, + "payer": { + "partyIdType": "MSISDN", + "partyId": 4656473839 + }, + "status": "SUCCESSFUL" + }, 200) return MockResponse(None, 404) @@ -35,6 +39,10 @@ def mocked_requests_post(*args, **kwargs): return MockResponse({"access_token": "token"}, 200) elif "/collection/v1_0/requesttopay" in args[0]: return MockResponse({"key2": "value2"}, 200) + elif "apiuser" in args[0] and "apikey" in args[0]: + return MockResponse({ + "apiKey": "dummykey" + }, 200) return MockResponse(None, 404) @@ -42,5 +50,24 @@ def mocked_requests_post(*args, **kwargs): def mocked_requests_session(*args, **kwargs): if '/collection/token/' in args[1]: return MockResponse({"access_token": "token"}, 200) - elif "/collection/v1_0/requesttopay" in args[1]: - return MockResponse({"key2": "value2"}, 200) + elif '/account/balance' in args[1]: + return MockResponse({ + "availableBalance": "500", + "currency": "UGX" + }, 200) + elif "/requesttopay" in args[1] and args[0] == 'POST': + return MockResponse({}, 200) + elif "transfer" in args[1]: + return MockResponse({}, 200) + elif "/requesttopay" in args[1] and args[0] == 'GET': + return MockResponse({ + "amount": 100, + "currency": "UGX", + "financialTransactionId": 23503452, + "externalId": 947354, + "payer": { + "partyIdType": "MSISDN", + "partyId": 4656473839 + }, + "status": "SUCCESSFUL" + }, 200) From 2c9c522f6fe54dbede2465455fce50cc813f4506 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Fri, 1 Mar 2019 14:16:23 +0300 Subject: [PATCH 02/35] [minor] add support for retries --- src/momoapi/client.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/momoapi/client.py b/src/momoapi/client.py index 143fc41..cbcb9be 100644 --- a/src/momoapi/client.py +++ b/src/momoapi/client.py @@ -14,6 +14,9 @@ except ImportError: JSONDecodeError = ValueError +from requests.packages.urllib3.util.retry import Retry +from .utils import requests_retry_session + class Response: @@ -34,7 +37,7 @@ def __init__(self, token): def __call__(self, r): # modify and return the request - r.headers['Authorization'] = "Bearer "+to_native_string(self.token) + r.headers['Authorization'] = "Bearer " + to_native_string(self.token) return r @@ -50,13 +53,14 @@ def __init__(self, auth_key, user_id, api_secret, base_url="https://ericssonbasi def request(self, method, url, headers, post_data=None): self.authToken = self.getAuthToken().json()["access_token"] - request = Request(method, url, data=json.dumps(post_data), headers=headers, auth=MoMoAuth("%s" % self.authToken)) + request = Request(method, url, data=json.dumps(post_data), + headers=headers, auth=MoMoAuth("%s" % self.authToken)) prepped = self._session.prepare_request(request) - resp = self._session.send(prepped, - verify=False - ) + resp = requests_retry_session(sesssion=self._session).send(prepped, + verify=False + ) return self.interpret_response(resp) def interpret_response(self, resp): @@ -99,11 +103,11 @@ def getAuthToken(self): "Ocp-Apim-Subscription-Key": "%s" % self.auth_key } - r = requests.post(self.base_url+"/collection/token/", + r = requests.post(self.base_url + "/collection/token/", auth=HTTPBasicAuth(self.user_id, self.api_secret), data=data, headers=headers) return r - def requestToPay(self, mobile, amount, product_id, note="", message="", currency="EUR", environment="sandbox"): + def requestToPay(self, mobile, amount, product_id, note="", message="", currency="EUR", environment="sandbox"): ref = str(uuid.uuid4()) data = {"payer": {"partyIdType": "MSISDN", "partyId": mobile}, "payeeNote": note, "payerMessage": message, "externalId": product_id, "currency": currency, "amount": amount} @@ -115,7 +119,7 @@ def requestToPay(self, mobile, amount, product_id, note="", message="", currency } - url = self.base_url+"/collection/v1_0/requesttopay" + url = self.base_url + "/collection/v1_0/requesttopay" res = self.request("POST", url, headers, data) return {"transaction_ref": ref} @@ -125,18 +129,18 @@ def getBalance(self, environment="sandbox"): "Content-Type": "application/json", "Ocp-Apim-Subscription-Key": self.auth_key } - url = self.base_url+"/collection/v1_0/account/balance" + url = self.base_url + "/collection/v1_0/account/balance" res = self.request("GET", url, headers) return res.json() - def getTransactionStatus(self, transaction_id, environment="sandbox"): + def getTransactionStatus(self, transaction_id, environment="sandbox"): headers = { "X-Target-Environment": environment, "Content-Type": "application/json", "Ocp-Apim-Subscription-Key": self.auth_key } - url = self.base_url+"/collection/v1_0/requesttopay/"+transaction_id + url = self.base_url + "/collection/v1_0/requesttopay/" + transaction_id res = self.request("GET", url, headers) return res.json() @@ -158,7 +162,7 @@ def transfer(self, amount, mobile, note="", message="", currency="EUR", environm "Content-Type": "application/json", "Ocp-Apim-Subscription-Key": self.auth_key } - url = self.base_url+"/v1_0/transfer" + url = self.base_url + "/v1_0/transfer" res = self.request("POST", url, headers, data) return {"transaction_ref": external_ref} @@ -172,7 +176,7 @@ def generateToken(self, host, api_user, api_key, base_url, environment="sandbox" "X-Target-Environment": environment, } - url = base_url+"/v1_0/apiuser/%s/apikey" % api_user + url = base_url + "/v1_0/apiuser/%s/apikey" % api_user res = requests.post(url, data=json.dumps({}), headers=headers) print(res) From ed7f027e7f7f97c9a8aa8578c2ad5fc8de5f7914 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Fri, 1 Mar 2019 15:33:58 +0300 Subject: [PATCH 03/35] configure mypy for static checks --- .coveralls.yml | 2 ++ .flake8 | 7 +++++ src/momoapi/client.py | 14 +++++++--- src/momoapi/utils.py | 28 +++++++++++++++++++ tox.ini | 63 ++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 .coveralls.yml create mode 100644 .flake8 diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 0000000..49800a0 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1,2 @@ +service_name: travis-pro +repo_token: eYYZeUsJmXfN0lkaFJYh7waOrHLWMSJht diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..b730b16 --- /dev/null +++ b/.flake8 @@ -0,0 +1,7 @@ +[flake8] +# E501 is the "Line too long" error. We disable it because we use Black for +# code formatting. Black makes a best effort to keep lines under the max +# length, but can go over in some cases. +# W503 goes against PEP8 rules. It's disabled by default, but must be disabled +# explicitly when using `ignore`. +ignore = E501, W503 \ No newline at end of file diff --git a/src/momoapi/client.py b/src/momoapi/client.py index cbcb9be..45171e6 100644 --- a/src/momoapi/client.py +++ b/src/momoapi/client.py @@ -1,3 +1,9 @@ +""" +Base implementation of the MTN API client + +@author: Moses Mugisha +""" + import requests import textwrap import json @@ -107,7 +113,7 @@ def getAuthToken(self): auth=HTTPBasicAuth(self.user_id, self.api_secret), data=data, headers=headers) return r - def requestToPay(self, mobile, amount, product_id, note="", message="", currency="EUR", environment="sandbox"): + def requestToPay(self, mobile, amount, product_id, note="", message="", currency="EUR", environment="sandbox", **kwargs): ref = str(uuid.uuid4()) data = {"payer": {"partyIdType": "MSISDN", "partyId": mobile}, "payeeNote": note, "payerMessage": message, "externalId": product_id, "currency": currency, "amount": amount} @@ -133,7 +139,7 @@ def getBalance(self, environment="sandbox"): res = self.request("GET", url, headers) return res.json() - def getTransactionStatus(self, transaction_id, environment="sandbox"): + def getTransactionStatus(self, transaction_id, environment="sandbox", **kwargs): headers = { "X-Target-Environment": environment, @@ -144,7 +150,7 @@ def getTransactionStatus(self, transaction_id, environment="sandbox"): res = self.request("GET", url, headers) return res.json() - def transfer(self, amount, mobile, note="", message="", currency="EUR", environment="sandbox"): + def transfer(self, amount, mobile, note="", message="", currency="EUR", environment="sandbox", **kwargs): external_ref = str(uuid.uuid4()) data = { "amount": amount, @@ -167,7 +173,7 @@ def transfer(self, amount, mobile, note="", message="", currency="EUR", environm return {"transaction_ref": external_ref} @classmethod - def generateToken(self, host, api_user, api_key, base_url, environment="sandbox"): + def generateToken(self, host, api_user, api_key, base_url, environment="sandbox", **kwargs): data = {"providerCallbackHost": host} headers = { diff --git a/src/momoapi/utils.py b/src/momoapi/utils.py index 4651c38..6a0da84 100644 --- a/src/momoapi/utils.py +++ b/src/momoapi/utils.py @@ -1,3 +1,7 @@ + +from phonenumbers import carrier +import phonenumbers + ERROR_CODES = [ { "http_code": 409, @@ -143,3 +147,27 @@ def requests_retry_session( session.mount('http://', adapter) session.mount('https://', adapter) return session + + +def validate_phone_number(number): + obj = phonenumbers.parse(number, "UG") + if (phonenumbers.is_valid_numbe(obj) == False): + raise Exception("Invalid Phone number %s" % number) + if (carrier.name_for_number(obj, "en") != "MTN"): + raise Exception("%s: Only MTN is supported at the moment" % number) + return "256" + obj.national_number + + +def validate_number(number): + number_types = (int, float) + if sys.version_info < (3, 0, 0): + number_types += (long,) + if not type(number) in number_types: + raise Exception("%s: Must be a number" % number) + return number + + +def validate_string(_string): + if not type(_string) == str: + raise Exception("%s: Must be a string" % _string) + return string diff --git a/tox.ini b/tox.ini index 1d7dd55..67ffec0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,8 @@ [tox] envlist = clean, + mypy, + flake8, py27-cover, py27-nocov, py34-cover, @@ -12,7 +14,7 @@ envlist = py37-cover, py37-nocov, report, - flake8 + skip_missing_interpreters = {env:TOX_SKIP_MISSING_INTERPRETERS:True} @@ -155,6 +157,30 @@ deps = [testenv:py37-nocov] basepython = {env:TOXPYTHON:python3.7} + +## +# Flake8 linting +## + +[testenv:flake8] + +skip_install = True + +deps = + flake8==3.6.0 + flake8-bugbear==18.8.0 + flake8-docstrings==1.3.0 + flake8-import-order==0.18 + flake8-mutable==1.2.0 + flake8-pep3101==1.2.1 + pep8-naming==0.7.0 + mccabe==0.6.1 + +basepython = python3.7 + +commands = + flake8 {posargs:src/momoapi} + [flake8] select = B,C,E,F,I,N,S,W @@ -196,3 +222,38 @@ ignore = W504, + +## +# Mypy linting +## + +[testenv:mypy] + +basepython = python3.7 + +skip_install = True + +deps = + mypy==0.650 + +commands = + + "mypy" --config-file="{toxinidir}/tox.ini" {posargs:src} + + +[mypy] + +# Global settings + +warn_redundant_casts = True +warn_unused_ignores = True +strict_optional = True +show_column_numbers = True + +# Module default settings +# disallow_untyped_calls = True +disallow_untyped_defs = True +# warn_return_any = True + +# Need some stub files to get rid of this +ignore_missing_imports = True From 7732ace7e3f1fed54ef1409e8006f6e6bc6fb450 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Fri, 1 Mar 2019 16:41:32 +0300 Subject: [PATCH 04/35] configure flake8 travis & coveralls --- .flake8 | 7 --- .travis.yml | 14 +++-- setup.py | 3 +- src/momoapi/cli.py | 37 ++++++++----- src/momoapi/client.py | 125 ++++++++++++++++++++++++++++-------------- src/momoapi/errors.py | 4 +- src/momoapi/utils.py | 24 ++++---- tox.ini | 4 +- 8 files changed, 136 insertions(+), 82 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index b730b16..0000000 --- a/.flake8 +++ /dev/null @@ -1,7 +0,0 @@ -[flake8] -# E501 is the "Line too long" error. We disable it because we use Black for -# code formatting. Black makes a best effort to keep lines under the max -# length, but can go over in some cases. -# W503 goes against PEP8 rules. It's disabled by default, but must be disabled -# explicitly when using `ignore`. -ignore = E501, W503 \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 179ecc3..0aaf548 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,23 +8,29 @@ matrix: - os: linux dist: trusty sudo: false - env: TOXENV=py27-cover + env: TOXENV=flake8 + python: "3.7" + + - os: linux + dist: trusty + sudo: false + env: TOXENV=py27-cover,coveralls python: "2.7" - os: linux dist: trusty sudo: false python: "3.5" - env: TOXENV=py35-cover + env: TOXENV=py35-cover,coveralls - os: linux dist: trusty sudo: false python: "3.6" - env: TOXENV=py36-cover + env: TOXENV=py36-cover,coveralls - os: linux dist: xenial sudo: required python: "3.7" - env: TOXENV=py37-cover + env: TOXENV=py37-cover,coveralls script: - pip install tox - tox diff --git a/setup.py b/setup.py index 7d171af..a3592aa 100644 --- a/setup.py +++ b/setup.py @@ -71,7 +71,8 @@ def read(*names, **kwargs): ], install_requires=[ 'requests == 2.21.0', - 'Click==7.0' + 'Click==7.0', + 'phonenumbers' # eg: 'aspectlib==1.1.1', 'six>=1.7', ], diff --git a/src/momoapi/cli.py b/src/momoapi/cli.py index 09f14d2..6ee24a2 100644 --- a/src/momoapi/cli.py +++ b/src/momoapi/cli.py @@ -14,28 +14,33 @@ Also see (1) from http://click.pocoo.org/5/setuptools/#setuptools-integration """ +import json +import time +import uuid + import click + import requests -import uuid -import time -import json -def generateToken(host, key): +def generate_token(host, key): data = {"providerCallbackHost": host} - token = "%s" % uuid.uuid4() - + token = str(uuid.uuid4()) headers = { - "X-Reference-Id": "%s" % token, + "X-Reference-Id": token, "Content-Type": "application/json", "Ocp-Apim-Subscription-Key": key } - r = requests.post("https://ericssonbasicapi2.azure-api.net/v1_0/apiuser", data=json.dumps(data), headers=headers) + requests.post( + "https://ericssonbasicapi2.azure-api.net/v1_0/apiuser", + data=json.dumps(data), + headers=headers) time.sleep(5) del headers["X-Reference-Id"] - url = "https://ericssonbasicapi2.azure-api.net/v1_0/apiuser/%s/apikey" % token + url = "https://ericssonbasicapi2.azure-api.net/v1_0/apiuser/{0}/apikey".format( + token) res = requests.post(url, data=json.dumps({}), headers=headers) @@ -46,11 +51,17 @@ def generateToken(host, key): ret["UserId"] = token ret["APISecret"] = rr["apiKey"] - return "Here is your User Id and API secret : %s" % ret + return "Here is your User Id and API secret : {0}".format(ret) @click.command() -@click.option('--provider', prompt="providerCallBackHost", help='providerCallBackHost') -@click.option('--key', prompt="Ocp-Apim-Subscription-Key", help='Ocp-Apim-Subscription-Key') +@click.option( + '--provider', + prompt="providerCallBackHost", + help='providerCallBackHost') +@click.option( + '--key', + prompt="Ocp-Apim-Subscription-Key", + help='Ocp-Apim-Subscription-Key') def main(provider, key): - click.echo(generateToken(provider, key)) + click.echo(generate_token(provider, key)) diff --git a/src/momoapi/client.py b/src/momoapi/client.py index 45171e6..717a537 100644 --- a/src/momoapi/client.py +++ b/src/momoapi/client.py @@ -4,23 +4,22 @@ @author: Moses Mugisha """ -import requests -import textwrap + import json import uuid -from .errors import APIError -from requests import Request, Session -from requests.auth import AuthBase -import base64 -from requests.auth import HTTPBasicAuth -from requests._internal_utils import to_native_string - try: from json.decoder import JSONDecodeError except ImportError: JSONDecodeError = ValueError -from requests.packages.urllib3.util.retry import Retry +import requests +from requests import Request, Session +from requests._internal_utils import to_native_string +from requests.auth import AuthBase +from requests.auth import HTTPBasicAuth + + +from .errors import APIError from .utils import requests_retry_session @@ -49,7 +48,13 @@ def __call__(self, r): class MomoApi(object): - def __init__(self, auth_key, user_id, api_secret, base_url="https://ericssonbasicapi2.azure-api.net", ** kwargs): + def __init__( + self, + auth_key, + user_id, + api_secret, + base_url="https://ericssonbasicapi2.azure-api.net", + ** kwargs): super(MomoApi, self).__init__(**kwargs) self._session = Session() self.api_secret = api_secret @@ -59,8 +64,12 @@ def __init__(self, auth_key, user_id, api_secret, base_url="https://ericssonbasi def request(self, method, url, headers, post_data=None): self.authToken = self.getAuthToken().json()["access_token"] - request = Request(method, url, data=json.dumps(post_data), - headers=headers, auth=MoMoAuth("%s" % self.authToken)) + request = Request( + method, + url, + data=json.dumps(post_data), + headers=headers, + auth=MoMoAuth(self.authToken)) prepped = self._session.prepare_request(request) @@ -85,15 +94,11 @@ def interpret_response(self, resp): return resp def handle_error_response(self, rbody, rcode, resp, rheaders): - try: - error_data = resp['error'] - except (KeyError, TypeError): - raise APIError( - "Invalid response object from API: %r (HTTP response code " - "was %d)" % (rbody, rcode), - rbody, rcode, resp) - raise "" + raise APIError( + "Invalid response object from API: {0} (HTTP response code " + "was {1})".format(rbody, rcode), + rbody, rcode, resp) def request_headers(self, api_key, method): headers = {} @@ -102,21 +107,40 @@ def request_headers(self, api_key, method): def getAuthToken(self): data = json.dumps({}) - auth = "%s:%s" % (self.user_id, self.api_secret) - bs64 = base64.b64encode(auth.encode()) headers = { "Content-Type": "application/json", - - "Ocp-Apim-Subscription-Key": "%s" % self.auth_key + "Ocp-Apim-Subscription-Key": self.auth_key } - r = requests.post(self.base_url + "/collection/token/", - auth=HTTPBasicAuth(self.user_id, self.api_secret), data=data, headers=headers) - return r - - def requestToPay(self, mobile, amount, product_id, note="", message="", currency="EUR", environment="sandbox", **kwargs): + response = requests.post( + + "{0}/collection/token/".format(self.base_url), + auth=HTTPBasicAuth( + self.user_id, + self.api_secret), + data=data, + headers=headers) + return response + + def requestToPay( + self, + mobile: str, + amount, + product_id, + note="", + message="", + currency="EUR", + environment="sandbox", + **kwargs): ref = str(uuid.uuid4()) - data = {"payer": {"partyIdType": "MSISDN", "partyId": mobile}, "payeeNote": note, - "payerMessage": message, "externalId": product_id, "currency": currency, "amount": amount} + data = { + "payer": { + "partyIdType": "MSISDN", + "partyId": mobile}, + "payeeNote": note, + "payerMessage": message, + "externalId": product_id, + "currency": currency, + "amount": amount} headers = { "X-Target-Environment": environment, "Content-Type": "application/json", @@ -125,8 +149,8 @@ def requestToPay(self, mobile, amount, product_id, note="", message="", currency } - url = self.base_url + "/collection/v1_0/requesttopay" - res = self.request("POST", url, headers, data) + url = "{0}/collection/v1_0/requesttopay".format(self.base_url) + self.request("POST", url, headers, data) return {"transaction_ref": ref} def getBalance(self, environment="sandbox"): @@ -135,11 +159,15 @@ def getBalance(self, environment="sandbox"): "Content-Type": "application/json", "Ocp-Apim-Subscription-Key": self.auth_key } - url = self.base_url + "/collection/v1_0/account/balance" + url = "{0}/collection/v1_0/account/balance".format(self.base_url) res = self.request("GET", url, headers) return res.json() - def getTransactionStatus(self, transaction_id, environment="sandbox", **kwargs): + def getTransactionStatus( + self, + transaction_id, + environment="sandbox", + **kwargs): headers = { "X-Target-Environment": environment, @@ -150,7 +178,15 @@ def getTransactionStatus(self, transaction_id, environment="sandbox", **kwargs): res = self.request("GET", url, headers) return res.json() - def transfer(self, amount, mobile, note="", message="", currency="EUR", environment="sandbox", **kwargs): + def transfer( + self, + amount, + mobile, + note="", + message="", + currency="EUR", + environment="sandbox", + **kwargs): external_ref = str(uuid.uuid4()) data = { "amount": amount, @@ -169,11 +205,18 @@ def transfer(self, amount, mobile, note="", message="", currency="EUR", environm "Ocp-Apim-Subscription-Key": self.auth_key } url = self.base_url + "/v1_0/transfer" - res = self.request("POST", url, headers, data) + self.request("POST", url, headers, data) return {"transaction_ref": external_ref} @classmethod - def generateToken(self, host, api_user, api_key, base_url, environment="sandbox", **kwargs): + def generateToken( + cls, + host, + api_user, + api_key, + base_url, + environment="sandbox", + **kwargs): data = {"providerCallbackHost": host} headers = { @@ -182,9 +225,9 @@ def generateToken(self, host, api_user, api_key, base_url, environment="sandbox" "X-Target-Environment": environment, } - url = base_url + "/v1_0/apiuser/%s/apikey" % api_user + url = base_url + "/v1_0/apiuser/{0}/apikey".format(api_user) - res = requests.post(url, data=json.dumps({}), headers=headers) + res = requests.post(url, data=json.dumps(data), headers=headers) print(res) return res.json() diff --git a/src/momoapi/errors.py b/src/momoapi/errors.py index 0efa3e8..23ffa4e 100644 --- a/src/momoapi/errors.py +++ b/src/momoapi/errors.py @@ -1,6 +1,4 @@ -from .utils import ERROR_CODES - class MomoError(Exception): def __init__(self, message=None, http_body=None, http_status=None, @@ -35,7 +33,7 @@ def user_message(self): return self._message def __repr__(self): - return '%s(message=%r, http_status=%r, request_id=%r)' % ( + return '{0}(message={1}, http_status={2}, request_id={3})'.format( self.__class__.__name__, self._message, self.http_status, diff --git a/src/momoapi/utils.py b/src/momoapi/utils.py index 6a0da84..b454764 100644 --- a/src/momoapi/utils.py +++ b/src/momoapi/utils.py @@ -1,6 +1,9 @@ - -from phonenumbers import carrier import phonenumbers +from phonenumbers import carrier + +import requests +from requests.adapters import HTTPAdapter +from requests.packages.urllib3.util.retry import Retry ERROR_CODES = [ { @@ -151,23 +154,22 @@ def requests_retry_session( def validate_phone_number(number): obj = phonenumbers.parse(number, "UG") - if (phonenumbers.is_valid_numbe(obj) == False): - raise Exception("Invalid Phone number %s" % number) + if not phonenumbers.is_valid_numbe(obj): + raise Exception("Invalid Phone number {0}".format(number)) if (carrier.name_for_number(obj, "en") != "MTN"): - raise Exception("%s: Only MTN is supported at the moment" % number) + raise Exception( + "{0}: Only MTN is supported at the moment".format(number)) return "256" + obj.national_number def validate_number(number): number_types = (int, float) - if sys.version_info < (3, 0, 0): - number_types += (long,) if not type(number) in number_types: - raise Exception("%s: Must be a number" % number) + raise Exception("{0}: Must be a number".format(number)) return number def validate_string(_string): - if not type(_string) == str: - raise Exception("%s: Must be a string" % _string) - return string + if not isinstance(_string, str): + raise Exception("{0}: Must be a string".format(_string)) + return _string diff --git a/tox.ini b/tox.ini index 67ffec0..816370c 100644 --- a/tox.ini +++ b/tox.ini @@ -194,8 +194,8 @@ ignore = # multiple spaces before operator E221, - # too many blank lines - E302, + #camel case + N802, # too many blank lines E303, From d04262ac2864cf31baab57d4dc152d5ba033539c Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Fri, 1 Mar 2019 16:52:32 +0300 Subject: [PATCH 05/35] cleanu[ --- .coveralls.yml | 2 -- src/momoapi/cli.py | 4 ++-- src/momoapi/client.py | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) delete mode 100644 .coveralls.yml diff --git a/.coveralls.yml b/.coveralls.yml deleted file mode 100644 index 49800a0..0000000 --- a/.coveralls.yml +++ /dev/null @@ -1,2 +0,0 @@ -service_name: travis-pro -repo_token: eYYZeUsJmXfN0lkaFJYh7waOrHLWMSJht diff --git a/src/momoapi/cli.py b/src/momoapi/cli.py index 6ee24a2..c493f79 100644 --- a/src/momoapi/cli.py +++ b/src/momoapi/cli.py @@ -23,7 +23,7 @@ import requests -def generate_token(host, key): +def generateToken(host, key): data = {"providerCallbackHost": host} token = str(uuid.uuid4()) headers = { @@ -64,4 +64,4 @@ def generate_token(host, key): prompt="Ocp-Apim-Subscription-Key", help='Ocp-Apim-Subscription-Key') def main(provider, key): - click.echo(generate_token(provider, key)) + click.echo(generateToken(provider, key)) diff --git a/src/momoapi/client.py b/src/momoapi/client.py index 717a537..0df6158 100644 --- a/src/momoapi/client.py +++ b/src/momoapi/client.py @@ -123,7 +123,7 @@ def getAuthToken(self): def requestToPay( self, - mobile: str, + mobile, amount, product_id, note="", From 9f3e5a2274bbee01e17adf54db5c40c758394c3f Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Fri, 1 Mar 2019 19:01:53 +0300 Subject: [PATCH 06/35] coveralls fix --- .travis.yml | 10 +++++----- README.md | 1 + src/momoapi/client.py | 1 + tox.ini | 7 +++++-- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0aaf548..af1eb51 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,29 +8,29 @@ matrix: - os: linux dist: trusty sudo: false - env: TOXENV=flake8 + env: TOXENV=flake8,coveralls python: "3.7" - os: linux dist: trusty sudo: false - env: TOXENV=py27-cover,coveralls + env: TOXENV=py27-cover python: "2.7" - os: linux dist: trusty sudo: false python: "3.5" - env: TOXENV=py35-cover,coveralls + env: TOXENV=py35-cover - os: linux dist: trusty sudo: false python: "3.6" - env: TOXENV=py36-cover,coveralls + env: TOXENV=py36-cover - os: linux dist: xenial sudo: required python: "3.7" - env: TOXENV=py37-cover,coveralls + env: TOXENV=py37-cover script: - pip install tox - tox diff --git a/README.md b/README.md index 88e1c76..6aeb0d1 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ MTN MoMo API Client for Python. [![Latest Version](https://img.shields.io/pypi/v/tox-travis.svg)](https://badge.fury.io/js/mtn-momo) [![Coverage Status](https://coveralls.io/repos/github/sparkplug/momoapi-python/badge.svg?branch=coverage)](https://coveralls.io/github/sparkplug/momoapi-python?branch=master) [![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/momo-api-developers/) +[![Coverage Status](https://coveralls.io/repos/github/sparkplug/momoapi-python/badge.svg?branch=validations)](https://coveralls.io/github/sparkplug/momoapi-python?branch=validations) # Installing diff --git a/src/momoapi/client.py b/src/momoapi/client.py index 0df6158..01097f0 100644 --- a/src/momoapi/client.py +++ b/src/momoapi/client.py @@ -131,6 +131,7 @@ def requestToPay( currency="EUR", environment="sandbox", **kwargs): + # type: (String,String,String,String,String,String,String) -> json ref = str(uuid.uuid4()) data = { "payer": { diff --git a/tox.ini b/tox.ini index 816370c..f8ec37a 100644 --- a/tox.ini +++ b/tox.ini @@ -31,6 +31,7 @@ deps = mock commands = {posargs:pytest -vv --ignore=src} + passenv = @@ -42,10 +43,12 @@ passenv = [testenv:coveralls] deps = - coveralls + coveralls + PyYAML skip_install = true commands = - coveralls [] + coveralls +passenv = TRAVIS TRAVIS_* [testenv:lint] skip_install = true From f047e4a484dd17c7fab487011f548e504b703823 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Fri, 1 Mar 2019 19:08:24 +0300 Subject: [PATCH 07/35] use python 3.6 for flake8 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index af1eb51..3f1ea7d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ matrix: dist: trusty sudo: false env: TOXENV=flake8,coveralls - python: "3.7" + python: "3.6" - os: linux dist: trusty From 50a58eef0385115c0b47b945d4fcfeb31eb5bf33 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Tue, 5 Mar 2019 11:24:06 +0300 Subject: [PATCH 08/35] validation tests --- src/momoapi/client.py | 12 ++++++------ src/momoapi/errors.py | 4 ++++ src/momoapi/utils.py | 22 ++++++++++++++++------ tests/unitests/test_client.py | 21 +++++++++++++++++++-- 4 files changed, 45 insertions(+), 14 deletions(-) diff --git a/src/momoapi/client.py b/src/momoapi/client.py index 01097f0..bb9cfab 100644 --- a/src/momoapi/client.py +++ b/src/momoapi/client.py @@ -20,7 +20,7 @@ from .errors import APIError -from .utils import requests_retry_session +from .utils import requests_retry_session, validate_phone_number, validate_uuid class Response: @@ -58,7 +58,7 @@ def __init__( super(MomoApi, self).__init__(**kwargs) self._session = Session() self.api_secret = api_secret - self.user_id = user_id + self.user_id = validate_uuid(user_id) self.auth_key = auth_key self.base_url = base_url @@ -136,12 +136,12 @@ def requestToPay( data = { "payer": { "partyIdType": "MSISDN", - "partyId": mobile}, + "partyId": validate_phone_number(mobile)}, "payeeNote": note, "payerMessage": message, "externalId": product_id, "currency": currency, - "amount": amount} + "amount": str(amount)} headers = { "X-Target-Environment": environment, "Content-Type": "application/json", @@ -190,12 +190,12 @@ def transfer( **kwargs): external_ref = str(uuid.uuid4()) data = { - "amount": amount, + "amount": str(amount), "currency": currency, "externalId": external_ref, "payee": { "partyIdType": "MSISDN", - "partyId": mobile + "partyId": validate_phone_number(mobile) }, "payerMessage": message, "payeeNote": note diff --git a/src/momoapi/errors.py b/src/momoapi/errors.py index 23ffa4e..b9cd63f 100644 --- a/src/momoapi/errors.py +++ b/src/momoapi/errors.py @@ -70,3 +70,7 @@ class TransferError(MomoError): class GeneralError(MomoError): pass + + +class ValidationError(Exception): + pass diff --git a/src/momoapi/utils.py b/src/momoapi/utils.py index b454764..1493a65 100644 --- a/src/momoapi/utils.py +++ b/src/momoapi/utils.py @@ -1,9 +1,11 @@ +from uuid import UUID import phonenumbers from phonenumbers import carrier import requests from requests.adapters import HTTPAdapter from requests.packages.urllib3.util.retry import Retry +from .errors import ValidationError ERROR_CODES = [ { @@ -154,22 +156,30 @@ def requests_retry_session( def validate_phone_number(number): obj = phonenumbers.parse(number, "UG") - if not phonenumbers.is_valid_numbe(obj): - raise Exception("Invalid Phone number {0}".format(number)) + if not phonenumbers.is_valid_number(obj): + raise ValidationError("Invalid Phone number {0}".format(number)) if (carrier.name_for_number(obj, "en") != "MTN"): - raise Exception( + raise ValidationError( "{0}: Only MTN is supported at the moment".format(number)) - return "256" + obj.national_number + return "256{0}".format(obj.national_number) def validate_number(number): number_types = (int, float) if not type(number) in number_types: - raise Exception("{0}: Must be a number".format(number)) + raise ValidationError("{0}: Must be a number".format(number)) return number def validate_string(_string): if not isinstance(_string, str): - raise Exception("{0}: Must be a string".format(_string)) + raise ValidationError("{0}: Must be a string".format(_string)) + return _string + + +def validate_uuid(_string): + try: + _val = UUID(_string, version=4) + except ValueError: + raise ValidationError("{0}: Must be a valid uuid4 string".format(_string)) return _string diff --git a/tests/unitests/test_client.py b/tests/unitests/test_client.py index 7b1ceb0..ccdca9a 100644 --- a/tests/unitests/test_client.py +++ b/tests/unitests/test_client.py @@ -9,13 +9,14 @@ from requests import Request, Session from .utils import mocked_requests_get, mocked_requests_post, mocked_requests_session +from momoapi.errors import ValidationError class TestClient(unittest.TestCase): @mock.patch('requests.post', side_effect=mocked_requests_post) def setUp(self, mock_get): - client = MomoApi("APIKEY", "USERID", "APISECRET") + client = MomoApi("APIKEY", "0555e303-ae5b-4052-a77b-6d284cfc669c", "APISECRET") self.client = client def tearDown(self): @@ -25,11 +26,27 @@ def tearDown(self): @mock.patch('requests.get', side_effect=mocked_requests_get) def test_client_instantiate(self, mock_get): - client = MomoApi("APIKEY", "USERID", "APISECRET") + client = MomoApi("APIKEY", "0555e303-ae5b-4052-a77b-6d284cfc669c", "APISECRET") #request_mock.assert_requested("post", "/v1/accounts") assert isinstance(client, MomoApi) + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_invalid_uuid(self, mock_get): + #client = MomoApi("APIKEY", "USERID", "APISECRET") + with self.assertRaises(ValidationError): + client = MomoApi("APIKEY", "USERID", "APISECRET") + + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_invalid_mobile(self, mock_get): + #client = MomoApi("APIKEY", "USERID", "APISECRET") + with self.assertRaises(ValidationError): + ref = self.client.requestToPay("256712123456", "600", "123456789", note="dd", + message="dd", currency="EUR", environment="sandbox") + with self.assertRaises(ValidationError): + ref = self.client.requestToPay("254712123456", "600", "123456789", note="dd", + message="dd", currency="EUR", environment="sandbox") + @mock.patch.object(MomoApi, "request", side_effect=mocked_requests_session) def test_request_to_pay(self, mock_get): From ee3845702105cacaa0b5d7a7a21f7421fbdd84b9 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Thu, 7 Mar 2019 12:04:02 +0300 Subject: [PATCH 09/35] [major] support all products --- README.md | 155 ++++++++++++++---- src/momoapi/client.py | 139 ++++++---------- src/momoapi/collection.py | 63 +++++++ src/momoapi/config.py | 100 +++++++++++ src/momoapi/disbursement.py | 63 +++++++ src/momoapi/errors.py | 4 + src/momoapi/remittance.py | 88 ++++++++++ src/momoapi/resources/account.py | 0 src/momoapi/utils.py | 6 +- tests/integration/features/account.feature | 0 .../integration/features/collections.feature | 36 +++- .../features/disbursements.feature | 9 + .../integration/features/remittances.feature | 9 + .../integration/features/remittences.feature | 0 tests/integration/test_account.py | 0 tests/integration/test_collection.py | 49 ++++++ tests/integration/test_collections.py | 0 tests/integration/test_disbursement.py | 50 ++++++ tests/integration/test_disbursements.py | 0 tests/integration/test_remittance.py | 50 ++++++ tests/integration/test_remittences.py | 0 tests/integration/test_sandbox.py | 2 +- .../{test_client.py => test_collection.py} | 46 ++++-- tests/unitests/test_disbursement.py | 84 ++++++++++ tests/unitests/test_remittance.py | 85 ++++++++++ tests/unitests/utils.py | 10 +- tox.ini | 12 +- 27 files changed, 910 insertions(+), 150 deletions(-) create mode 100644 src/momoapi/collection.py create mode 100644 src/momoapi/config.py create mode 100644 src/momoapi/disbursement.py create mode 100644 src/momoapi/remittance.py delete mode 100644 src/momoapi/resources/account.py delete mode 100644 tests/integration/features/account.feature create mode 100644 tests/integration/features/remittances.feature delete mode 100644 tests/integration/features/remittences.feature delete mode 100644 tests/integration/test_account.py create mode 100644 tests/integration/test_collection.py delete mode 100644 tests/integration/test_collections.py create mode 100644 tests/integration/test_disbursement.py delete mode 100644 tests/integration/test_disbursements.py create mode 100644 tests/integration/test_remittance.py delete mode 100644 tests/integration/test_remittences.py rename tests/unitests/{test_client.py => test_collection.py} (58%) create mode 100644 tests/unitests/test_disbursement.py create mode 100644 tests/unitests/test_remittance.py diff --git a/README.md b/README.md index 6aeb0d1..ebe92e3 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,8 @@ MTN MoMo API Client for Python. [![Build Status](https://travis-ci.com/sparkplug/momoapi-python.svg?branch=master)](https://travis-ci.com/sparkplug/momoapi-node) [![Latest Version](https://img.shields.io/pypi/v/tox-travis.svg)](https://badge.fury.io/js/mtn-momo) -[![Coverage Status](https://coveralls.io/repos/github/sparkplug/momoapi-python/badge.svg?branch=coverage)](https://coveralls.io/github/sparkplug/momoapi-python?branch=master) +[![Coverage Status](https://coveralls.io/repos/github/sparkplug/momoapi-python/badge.svg?branch=master)](https://coveralls.io/github/sparkplug/momoapi-python?branch=master) [![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/momo-api-developers/) -[![Coverage Status](https://coveralls.io/repos/github/sparkplug/momoapi-python/badge.svg?branch=validations)](https://coveralls.io/github/sparkplug/momoapi-python?branch=validations) # Installing @@ -26,19 +25,35 @@ Additional instructions for installing this software are in `the installation in momo-api has a comprehensive test suite, which can be run by ``tox``:: - $ tox -l # to view all test environments - $ tox -e py27-cover # to run the tests for Python 2.7 - $ tox -e py34-cover # to run the tests for Python 3.4 +## to view all test environments + + ```bash + $ tox -l + ``` + ## to run the tests for Python 2.7 + + ```bash + $ tox -e py27-cover + + ``` + + ## to run the tests for Python 3.4 + + ```bash + $ tox -e py34-cover + ``` + ## Requirements * Python 2.7+ or Python 3.4+ (PyPy supported) +## Sandbox Environment -# Usage +# Creating a sandbox environment API user -Time to use the library. The goal is to create a `User ID` and `API Secret`. To do this, the API key from your profile on the MTN MoMo dashboard is needed. The library has a commandline app that helps you do just that when you enter the details as prompted on the commandline. +The library ships with a commandline application that helps to create sandbox credentials. It assumes you have created an account on `https://momodeveloper.mtn.com` and have your `Ocp-Apim-Subscription-Key` ```bash $ momoapi @@ -50,46 +65,128 @@ where `providerCallBackHost` is your callback host and `Ocp-Apim-Subscription-Ke ```bash Here is your User Id and API secret : {'apiKey': 'b0431db58a9b41faa8f5860230xxxxxx', 'UserId': '053c6dea-dd68-xxxx-xxxx-c830dac9f401'} - ``` -## Let's make calls. +## Configuration + +Each MOMO API product requires its own authentication details. i.e its own separate subscription key, user_id and api_secret. As such, we have to configure subscription keys for each product you will be using. + +In addition to this, In you may also need to specify your `BASE_URL`, `ENVIRONMENT` and `CALLBACK_HOST` if you are not using the defaults. +here is the full list of configuration options + + ```python + config={ + + "ENVIRONMENT": os.environ.get("ENVIRONMENT"),#Optional enviroment, either "sandbox" or "production". Default is 'sandbox' + "BASE_URL": os.environ.get("BASE_URL"),#An optional base url to the MTN Momo API. By default the staging base url will be used + "CALLBACK_HOST": os.environ.get("CALLBACK_HOST"),#The domain where you webhooks urls are hosted. + "COLLECTION_PRIMARY_KEY": os.environ.get("COLLECTION_PRIMARY_KEY"),# Primary Key for the `Collection` product. + "COLLECTION_USER_ID": os.environ.get("COLLECTION_USER_ID"),# User id of the collection product + "COLLECTION_API_SECRET": os.environ.get("COLLECTION_API_SECRET"),# API secret of the collection product + "REMITTANCE_USER_ID": os.environ.get("REMITTANCE_USER_ID"), # User id of the remittance product + "REMITTANCE_API_SECRET": os.environ.get("REMITTANCE_API_SECRET"),# API secret of the remittance product + "REMITTANCE_PRIMARY_KEY": os.envieon.get("REMITTANCE_PRIMARY_KEY"), #Primary Key for the 'Remittance' product. + "DISBURSEMENT_USER_ID": os.environ.get("DISBURSEMENT_USER_ID"), # User id of the disbursement product + "DISBURSEMENT_API_SECRET": os.environ.get("DISBURSEMENTS_API_SECRET"),# API secret of the Disbursemnet product + "DISBURSEMENT_PRIMARY_KEY": os.environ.get("DISBURSEMENT_PRIMARY_KEY"), #Primary Key for the 'Disbursement' product. + } + ``` + +You will only need to configure the product(s) you will be using. + + +## Collections + +The collections client can be created with the following paramaters. Note that the `COLLECTION_USER_ID` and `COLLECTION_API_SECRET` for production are provided on the MTN OVA dashboard; + +- `COLLECTION_PRIMARY_KEY`: Primary Key for the `Collection` product. +- `COLLECTION_USER_ID`: For sandbox, use the one generated with the `momoapi` command. +- `COLLECTION_API_SECRET`: For sandbox, use the one generated with the `momoapi` command. -We shall now import the library onto the commandline. Let us try to make a collection request. +You can create a collection client with the following ```python -from momoapi.client import MomoApi -client = MomoApi(APIKEY,USERID,APISECRET) -ref=client.requestToPay("256772123456", "600", "123456789", note="dd", message="dd", currency="EUR", environment="sandbox") +from momoapi.collection import Collection +import os +client = Collection({ + "COLLECTION_USER_ID": os.environ.get("COLLECTION_USER_ID"), + "COLLECTION_API_SECRET": os.environ.get("COLLECTION_API_SECRET"), + "COLLECTION_PRIMARY_KEY": os.environ.get("COLLECTION_PRIMARY_KEY"), + }) ``` -So, what just happened? We create a client on the commandline, and made a `requestToPay` transaction. How do we know this happened? Still on the commandline, input `ref` +#### Methods + +1. `requestToPay: This operation is used to request a payment from a consumer (Payer). The payer will be asked to authorize the payment. The transaction is executed once the payer has authorized the payment. The transaction will be in status PENDING until it is authorized or declined by the payer or it is timed out by the system. Status of the transaction can be validated by using `getTransactionStatus` + +2. `getTransaction`: Retrieve transaction information using the `transactionId` returned by `requestToPay`. You can invoke it at intervals until the transaction fails or succeeds. If the transaction has failed, it will throw an appropriate error. + +3. `getBalance()`: Get the balance of the account. + +4. `isPayerActive: check if an account holder is registered and active in the system. + +#### Sample Code ```python ->>> ref +from momoapi.collection import Collection +import os +client = Collection({ + "COLLECTION_USER_ID": os.environ.get("COLLECTION_USER_ID"), + "COLLECTION_API_SECRET": os.environ.get("COLLECTION_API_SECRET"), + "COLLECTION_PRIMARY_KEY": os.environ.get("COLLECTION_PRIMARY_KEY"), + }) + +client.requestToPay( + mobile="256772123456", amount="600", external_id="123456789", payee_note="dd", payer_message="dd", + currency="EUR") ``` -You should see a response similar to this: +## Disbursement + +The disbursements client can be created with the following paramaters. Note that the `DISBURSEMENT_USER_ID` and `DISBURSEMENT_API_SECRET` for production are provided on the MTN OVA dashboard; + +- `DISBURSEMENT_PRIMARY_KEY`: Primary Key for the `Disbursement` product. +- `DISBURSEMENT_USER_ID`: For sandbox, use the one generated with the `momoapi` command. +- `DISBURSEMENT_API_SECRET`: For sandbox, use the one generated with the `momoapi` command. + +You can create a disbursements client with the following ```python ->>> ref -{'transaction_ref': '33a9d94b-6828-4879-xxxx-e0ecb946d465'} +from momoapi.collection import Disbursement +import os +client = Disbursement({ + "DISBURSEMENT_USER_ID": os.environ.get("DISBURSEMENT_USER_ID"), + "DISBURSEMENT_API_SECRET": os.environ.get("DISBURSEMENT_API_SECRET"), + "DISBURSEMENT_PRIMARY_KEY": os.environ.get("DISBURSEMENT_PRIMARY_KEY"), + }) ``` -We can then use this `Transaction Reference` to get the status of the `Transaction` + +#### Methods + +1. `transfer` + +Used to transfer an amount from the owner’s account to a payee account. Status of the transaction can be validated by using the + +1. `getTransactionStatus`: Retrieve transaction information using the `transactionId` returned by `transfer`. You can invoke it at intervals until the transaction fails or succeeds. + +2. `getBalance()`: Get your account balance. + +3. `isPayerActive`: This method is used to check if an account holder is registered and active in the system. + +#### Sample Code ```python ->>> client.getTransactionStatus('33a9d94b-6828-4879-xxxx-e0ecb946d465') -{'financialTransactionId': '1854386795', 'externalId': '123456789', 'amount': '600', 'currency': 'EUR', 'payer': {'partyIdType': 'MSISDN', 'partyId': '256794631873'}, 'payerMessage': 'dd', 'payeeNote': 'dd', 'status': 'SUCCESSFUL'} +from momoapi.collection import Disbursement +import os +client = Disbursement({ + "DISBURSEMENT_USER_ID": os.environ.get("DISBURSEMENT_USER_ID"), + "DISBURSEMENT_API_SECRET": os.environ.get("DISBURSEMENT_API_SECRET"), + "DISBURSEMENT_PRIMARY_KEY": os.environ.get("DISBURSEMENT_PRIMARY_KEY"), + }) + client.transfer(amount="600", mobile="256772123456", external_id="123456789", payee_note="dd", + payer_message="dd", currency="EUR") ``` -Voila! - readme - installation - usage - reference/index - contributing - authors - changelog diff --git a/src/momoapi/client.py b/src/momoapi/client.py index bb9cfab..97c08a0 100644 --- a/src/momoapi/client.py +++ b/src/momoapi/client.py @@ -6,7 +6,6 @@ import json -import uuid try: from json.decoder import JSONDecodeError except ImportError: @@ -19,8 +18,9 @@ from requests.auth import HTTPBasicAuth +from .config import MomoConfig from .errors import APIError -from .utils import requests_retry_session, validate_phone_number, validate_uuid +from .utils import requests_retry_session class Response: @@ -46,21 +46,41 @@ def __call__(self, r): return r -class MomoApi(object): +class ClientInterface(): + def getAuthToken(self): + raise NotImplementedError + + def getBalance(self): + raise NotImplementedError + + def getTransactionStatus(self): + raise NotImplementedError + + +class Client(ClientInterface): + def getAuthToken(self): + return super().getAuthToken() + + def getBalance(self): + return super().getBalance() + + def getTransactionStatus(self): + return super().getTransactionStatus() + + +class MomoApi(ClientInterface): def __init__( self, - auth_key, - user_id, - api_secret, - base_url="https://ericssonbasicapi2.azure-api.net", + config, ** kwargs): super(MomoApi, self).__init__(**kwargs) self._session = Session() - self.api_secret = api_secret - self.user_id = validate_uuid(user_id) - self.auth_key = auth_key - self.base_url = base_url + self._config = MomoConfig(config) + + @property + def config(self): + return self._config def request(self, method, url, headers, post_data=None): self.authToken = self.getAuthToken().json()["access_token"] @@ -81,6 +101,7 @@ def request(self, method, url, headers, post_data=None): def interpret_response(self, resp): rcode = resp.status_code rheaders = resp.headers + print(resp) try: rbody = resp.json() @@ -105,110 +126,49 @@ def request_headers(self, api_key, method): return headers - def getAuthToken(self): + def getAuthToken(self, product, url, subscription_key): data = json.dumps({}) headers = { "Content-Type": "application/json", - "Ocp-Apim-Subscription-Key": self.auth_key + "Ocp-Apim-Subscription-Key": subscription_key } response = requests.post( - "{0}/collection/token/".format(self.base_url), + "{0}{1}".format(self.config.baseUrl, url), auth=HTTPBasicAuth( - self.user_id, - self.api_secret), + self.config.userId(product), + self.config.APISecret(product)), data=data, headers=headers) return response - def requestToPay( - self, - mobile, - amount, - product_id, - note="", - message="", - currency="EUR", - environment="sandbox", - **kwargs): - # type: (String,String,String,String,String,String,String) -> json - ref = str(uuid.uuid4()) - data = { - "payer": { - "partyIdType": "MSISDN", - "partyId": validate_phone_number(mobile)}, - "payeeNote": note, - "payerMessage": message, - "externalId": product_id, - "currency": currency, - "amount": str(amount)} - headers = { - "X-Target-Environment": environment, - "Content-Type": "application/json", - "X-Reference-Id": ref, - "Ocp-Apim-Subscription-Key": self.auth_key - - - } - url = "{0}/collection/v1_0/requesttopay".format(self.base_url) - self.request("POST", url, headers, data) - return {"transaction_ref": ref} - - def getBalance(self, environment="sandbox"): + def getBalance(self, url, subscription_key): headers = { - "X-Target-Environment": environment, + "X-Target-Environment": self.config.environment, "Content-Type": "application/json", - "Ocp-Apim-Subscription-Key": self.auth_key + "Ocp-Apim-Subscription-Key": subscription_key } - url = "{0}/collection/v1_0/account/balance".format(self.base_url) + url = "{0}{1}".format(self.config.baseUrl, url) res = self.request("GET", url, headers) return res.json() def getTransactionStatus( self, transaction_id, - environment="sandbox", - **kwargs): + url, + subscription_key, + ** kwargs): headers = { - "X-Target-Environment": environment, + "X-Target-Environment": self.config.environment, "Content-Type": "application/json", - "Ocp-Apim-Subscription-Key": self.auth_key + "Ocp-Apim-Subscription-Key": subscription_key } - url = self.base_url + "/collection/v1_0/requesttopay/" + transaction_id - res = self.request("GET", url, headers) + _url = self.config.baseUrl + url + transaction_id + print(_url) + res = self.request("GET", _url, headers) return res.json() - def transfer( - self, - amount, - mobile, - note="", - message="", - currency="EUR", - environment="sandbox", - **kwargs): - external_ref = str(uuid.uuid4()) - data = { - "amount": str(amount), - "currency": currency, - "externalId": external_ref, - "payee": { - "partyIdType": "MSISDN", - "partyId": validate_phone_number(mobile) - }, - "payerMessage": message, - "payeeNote": note - } - headers = { - "X-Target-Environment": environment, - "Content-Type": "application/json", - "Ocp-Apim-Subscription-Key": self.auth_key - } - url = self.base_url + "/v1_0/transfer" - self.request("POST", url, headers, data) - return {"transaction_ref": external_ref} - @classmethod def generateToken( cls, @@ -229,7 +189,6 @@ def generateToken( url = base_url + "/v1_0/apiuser/{0}/apikey".format(api_user) res = requests.post(url, data=json.dumps(data), headers=headers) - print(res) return res.json() diff --git a/src/momoapi/collection.py b/src/momoapi/collection.py new file mode 100644 index 0000000..efaa17e --- /dev/null +++ b/src/momoapi/collection.py @@ -0,0 +1,63 @@ +import uuid + +from .client import MomoApi +from .utils import validate_phone_number + + +class Collection(MomoApi): + def getAuthToken(self): + """ + Create an access token which can then be used + to authorize and authenticate towards the other end-points of the API + """ + url = "/collection/token/" + response = super().getAuthToken("COLLECTION", url, super().config.collectionsKey) + return response + + def getBalance(self): + url = "/collection/v1_0/account/balance" + + return super().getBalance(url, super().config.collectionsKey) + + def getTransactionStatus( + self, + transaction_id, + **kwargs): + url = "/collection/v1_0/requesttopay/" + + return super().getTransactionStatus( + transaction_id, url, super().config.collectionsKey) + + def requestToPay( + self, + mobile, + amount, + external_id, + payee_note="", + payer_message="", + currency="EUR", + **kwargs): + # type: (String,String,String,String,String,String,String) -> json + ref = str(uuid.uuid4()) + data = { + "payer": { + "partyIdType": "MSISDN", + "partyId": validate_phone_number(mobile)}, + "payeeNote": payee_note, + "payerMessage": payer_message, + "externalId": external_id, + "currency": currency, + "amount": str(amount)} + headers = { + "X-Target-Environment": super().config.environment, + "Content-Type": "application/json", + "X-Reference-Id": ref, + "Ocp-Apim-Subscription-Key": super().config.collectionsKey + + + } + if kwargs.get("callback_url"): + headers["X-Callback-Url"] = kwargs.get("callback_url") + url = "{0}/collection/v1_0/requesttopay".format(super().config.baseUrl) + self.request("POST", url, headers, data) + return {"transaction_ref": ref} diff --git a/src/momoapi/config.py b/src/momoapi/config.py new file mode 100644 index 0000000..7376814 --- /dev/null +++ b/src/momoapi/config.py @@ -0,0 +1,100 @@ +from .utils import validate_uuid +from .errors import ConfigurationError + + +class MomoConfig(object): + + def __init__(self, conf): + """ + + config={ + + ENVIRONMENT: os.environ.get("ENVIRONMENT"), + BASE_URL: os.environ.get("BASE_URL"), + CALLBACK_HOST: os.environ.get("CALLBACK_HOST"), + COLLECTION_PRIMARY_KEY: os.environ.get("COLLECTION_PRIMARY_KEY"), + COLLECTION_USER_ID: os.environ.get("COLLECTION_USER_ID"), + COLLECTION_API_SECRET: os.environ.get("COLLECTION_API_SECRET"), + + REMITTANCE_USER_ID: os.environ.get("REMITTANCE_USER_ID"), + REMITTANCE_API_SECRET: os.environ.get("REMITTANCE_API_SECRET"), + REMITTANCE_PRIMARY_KEY: os.envieon.get("REMITTANCE_PRIMARY_KEY") + + DISBURSEMENT_USER_ID: os.environ.get("DISBURSEMENT_USER_ID"), + DISBURSEMENT_API_SECRET: os.environ.get("DISBURSEMENTS_API_SECRET"), + DISBURSEMENT_PRIMARY_KEY: os.environ.get("DISBURSEMENT_PRIMARY_KEY"), + + + } + + + """ + self._config = conf + + def get_property(self, property_name): + if property_name not in self._config.keys(): + return None + return self._config[property_name] + + def userId(self, product): + key = self.get_property('{0}_USER_ID'.format(product)) + + if not key: + raise ConfigurationError( + "{0}_USER_ID is missing in the configuration".format(product)) + else: + return validate_uuid(key) + + def APISecret(self, product): + key = self.get_property('{0}_API_SECRET'.format(product)) + + if not key: + raise ConfigurationError( + "{0}_API_SECRET is missing in the configuration".format(product)) + else: + return key + + @property + def environment(self): + return self.get_property('ENVIRONMENT') or "sandbox" + + @property + def baseUrl(self): + return self.get_property( + 'BASE_URL') or "https://ericssonbasicapi2.azure-api.net" + + @property + def callbackHost(self): + key = self.get_property('CALLBACK_HOST') + if not key: + raise ConfigurationError( + "CALLBACK_HOST is missing in the configuration") + else: + return key + + @property + def collectionsKey(self): + key = self.get_property('COLLECTION_PRIMARY_KEY') + if not key: + raise ConfigurationError( + "COLLECTION_PRIMARY_KEY is missing in the configuration") + else: + return validate_uuid(key) + + @property + def disbursementsKey(self): + key = self.get_property('DISBURSEMENT_PRIMARY_KEY') + if not key: + raise ConfigurationError( + "DISBURSEMENT_PRIMARY_KEY is missing in the configuration") + else: + return validate_uuid(key) + + @property + def remittencesKey(self): + key = self.get_property('REMITTANCE_PRIMARY_KEY') + if not key: + raise ConfigurationError( + "REMITTANCE_PRIMARY_KEY is missing in the configuration") + else: + return validate_uuid(key) diff --git a/src/momoapi/disbursement.py b/src/momoapi/disbursement.py new file mode 100644 index 0000000..b427a0b --- /dev/null +++ b/src/momoapi/disbursement.py @@ -0,0 +1,63 @@ +from .client import MomoApi +import uuid +from .utils import validate_phone_number + + +class Disbursement(MomoApi): + + def getAuthToken(self): + """ + Create an access token which can then be used to authorize and authenticate towards the other end-points of the API. + """ + url = "/disbursement/token/" + response = super().getAuthToken( + "DISBURSEMENT", url, super().config.disbursementsKey) + return response + + def getBalance(self): + url = "/disbursement/v1_0/account/balance" + + return super().getBalance(url, super().config.disbursementsKey) + + def getTransactionStatus( + self, + transaction_id, + **kwargs): + url = "/disbursement/v1_0/transfer/" + + return super().getTransactionStatus( + transaction_id, url, super().config.disbursementsKey) + + def transfer( + self, + amount, + mobile, + external_id, + payee_note="", + payer_message="", + currency="EUR", + **kwargs): + ref = str(uuid.uuid4()) + data = { + "amount": str(amount), + "currency": currency, + "externalId": external_id, + "payee": { + "partyIdType": "MSISDN", + "partyId": validate_phone_number(mobile) + }, + "payerMessage": payer_message, + "payeeNote": payee_note + } + headers = { + "X-Target-Environment": super().config.environment, + "Content-Type": "application/json", + "Ocp-Apim-Subscription-Key": super().config.disbursementsKey, + "X-Reference-Id": ref, + } + if kwargs.get("callback_url"): + headers["X-Callback-Url"] = kwargs.get("callback_url") + url = super().config.baseUrl + "/disbursement/v1_0/transfer" + print(url) + self.request("POST", url, headers, data) + return {"transaction_ref": ref} diff --git a/src/momoapi/errors.py b/src/momoapi/errors.py index b9cd63f..19a7dc7 100644 --- a/src/momoapi/errors.py +++ b/src/momoapi/errors.py @@ -74,3 +74,7 @@ class GeneralError(MomoError): class ValidationError(Exception): pass + + +class ConfigurationError(Exception): + pass diff --git a/src/momoapi/remittance.py b/src/momoapi/remittance.py new file mode 100644 index 0000000..0648f1b --- /dev/null +++ b/src/momoapi/remittance.py @@ -0,0 +1,88 @@ +from .client import MomoApi +import uuid +from .utils import validate_phone_number + + +class Remittance(MomoApi): + def getAuthToken(self): + """Generate access token which can then be use to authorize and authenticate towards the other end-points of the API""" + + url = "/remittance/token/" + response = super().getAuthToken("REMITTANCE", url, super().config.remittencesKey) + return response + + def getBalance(self): + url = "/remittance/v1_0/account/balance" + return super().getBalance(url, super().config.remittencesKey) + + def getTransactionStatus( + self, + transaction_id, + **kwargs): + """ + get the status of a transfer + """ + url = "/remittance/v1_0/transfer/" + + return super().getTransactionStatus( + transaction_id, url, super().config.remittencesKey) + + def transfer( + self, + amount, + mobile, + external_id, + payer_message, + payee_note, + currency="EUR", + **kwargs): + """ + Transfer operation is used to transfer an amount from the own account to + a payee account + + """ + + ref = str(uuid.uuid4()) + data = { + "amount": str(amount), + "currency": currency, + "externalId": external_id, + "payee": { + "partyIdType": "MSISDN", + "partyId": validate_phone_number(mobile)}, + + "payerMessage": payer_message, + "payeeNote": payee_note + } + + headers = { + "X-Target-Environment": super().config.environment, + "Content-Type": "application/json", + "X-Reference-Id": ref, + "Ocp-Apim-Subscription-Key": super().config.remittencesKey + + } + + if kwargs.get("callback_url"): + headers["X-Callback-Url"] = kwargs.get("callback_url") + + url = "{0}/remittance/v1_0/transfer".format(super().config.baseUrl) + self.request("POST", url, headers, data) + return {"transaction_ref": ref} + + def isActive(self, mobile): + """ + Operation is used to check if an account holder is registered and + active in the system + + """ + + headers = { + "X-Target-Environment": self.config.environment, + "Content-Type": "application/json", + "Ocp-Apim-Subscription-Key": super().config.remittencesKey + } + url = "{0}/remittance/v1_0/accountholder/MSISDN/{1}/active".format( + super().config.baseUrl, mobile) + res = self.request("GET", url, headers) + return res.json() diff --git a/src/momoapi/resources/account.py b/src/momoapi/resources/account.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/momoapi/utils.py b/src/momoapi/utils.py index 1493a65..a34b0eb 100644 --- a/src/momoapi/utils.py +++ b/src/momoapi/utils.py @@ -139,6 +139,7 @@ def requests_retry_session( backoff_factor=0.3, status_forcelist=(502, 504), session=None, + **kwargs ): session = session or requests.Session() retry = Retry( @@ -179,7 +180,8 @@ def validate_string(_string): def validate_uuid(_string): try: - _val = UUID(_string, version=4) + UUID(_string, version=4) except ValueError: - raise ValidationError("{0}: Must be a valid uuid4 string".format(_string)) + raise ValidationError( + "{0}: Must be a valid uuid4 string".format(_string)) return _string diff --git a/tests/integration/features/account.feature b/tests/integration/features/account.feature deleted file mode 100644 index e69de29..0000000 diff --git a/tests/integration/features/collections.feature b/tests/integration/features/collections.feature index 1250d7e..c1da4b1 100644 --- a/tests/integration/features/collections.feature +++ b/tests/integration/features/collections.feature @@ -1,8 +1,40 @@ -Feature: Get Payments +Feature: Collections Scenario: Request a payment from a consumer (Payer) - Given I have a valid collection auth token + Given I have a valid user_id, auth_secret, and collections subscription key + When I request for a payment with the following payment details + | note | amount | message | mobile | product_id | + | test payment | 600 | message | 0782631873 | 0001 | + + And I check for transaction Status + Then It should be successful + + Scenario: Failed Transfer + Given I have a valid user_id, auth_secret, and collections subscription key + When I enter the following payment details + | note | amount | message | mobile | product_id | + | test payment | 600 | message | 0782631873 | 0001 | + + And I check for transaction Status + Then It should be successful + + Scenario: Wrong Currency + Given I have a valid user_id, auth_secret, and collections subscription key When I enter the following payment details | note | amount | message | mobile | product_id | | test payment | 600 | message | 0782631873 | 0001 | + And I check for transaction Status + Then It should be successful + + Scenario: Non Mtn mobile + Given I have a valid user_id, auth_secret, and collections subscription key + When I enter the following payment details + | note | amount | message | mobile | product_id | + | test payment | 600 | message | 0782631873 | 0001 | + + And I check for transaction Status + Then It should be successful + + + diff --git a/tests/integration/features/disbursements.feature b/tests/integration/features/disbursements.feature index e69de29..1610f0a 100644 --- a/tests/integration/features/disbursements.feature +++ b/tests/integration/features/disbursements.feature @@ -0,0 +1,9 @@ +Feature: Disbursements + Scenario: Transfer Money to another account + Given I have a valid user_id, auth_secret, and disbursements subscription key + When I transfer with the following payment details + | note | amount | message | mobile | product_id | + | test payment | 600 | message | 0782631873 | 0001 | + + And I check for transaction Status + Then It should be successful \ No newline at end of file diff --git a/tests/integration/features/remittances.feature b/tests/integration/features/remittances.feature new file mode 100644 index 0000000..6d8871d --- /dev/null +++ b/tests/integration/features/remittances.feature @@ -0,0 +1,9 @@ +Feature: Remmittences + Scenario: Move money from one account to another + Given I have a valid user_id, auth_secret, and remittence subscription key + When I transfer with the following details + | note | amount | message | mobile | product_id | + | test payment | 600 | message | 0782631873 | 0001 | + + And I check for transaction Status + Then It should be successful \ No newline at end of file diff --git a/tests/integration/features/remittences.feature b/tests/integration/features/remittences.feature deleted file mode 100644 index e69de29..0000000 diff --git a/tests/integration/test_account.py b/tests/integration/test_account.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/integration/test_collection.py b/tests/integration/test_collection.py new file mode 100644 index 0000000..5e0a920 --- /dev/null +++ b/tests/integration/test_collection.py @@ -0,0 +1,49 @@ +from pytest_bdd import scenario, given, when, then, parsers +import re +import os +import pytest +from click.testing import CliRunner +from momoapi.collection import Collection + + +pytest.globalDict = {} + + +@scenario('features/collections.feature', 'Request a payment from a consumer (Payer)') +def test_collections(): + pass + + +@given("I have a valid user_id, auth_secret, and collections subscription key") +def user_credentials(): + config = { + "COLLECTION_USER_ID": os.environ.get("COLLECTION_USER_ID"), + "COLLECTION_API_SECRET": os.environ.get("COLLECTION_API_SECRET"), + "COLLECTION_PRIMARY_KEY": os.environ.get("COLLECTION_PRIMARY_KEY"), + + } + client = Collection(config) + pytest.globalDict["client"] = client + + +@when("I request for a payment with the following payment details\n| note | amount | message | mobile | product_id |\n| test payment | 600 | message | 0782631873 | 0001 |") +def successful_request_to_pay(): + + ref = pytest.globalDict["client"].requestToPay( + mobile="256772123456", amount="600", external_id="123456789", payee_note="dd", payer_message="dd", + currency="EUR") + pytest.globalDict["ref"] = ref + + +@when("I check for transaction Status") +def check_transaction_status(): + status = pytest.globalDict["client"].getTransactionStatus(pytest.globalDict["ref"]["transaction_ref"]) + pytest.globalDict["status"] = status + assert isinstance(status, dict) + assert "amount" in status.keys() + assert "currency" in status.keys() + + +@then("It should be successful") +def successful_transaction(): + assert pytest.globalDict["status"]["status"] == "SUCCESSFUL" diff --git a/tests/integration/test_collections.py b/tests/integration/test_collections.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/integration/test_disbursement.py b/tests/integration/test_disbursement.py new file mode 100644 index 0000000..a9db72d --- /dev/null +++ b/tests/integration/test_disbursement.py @@ -0,0 +1,50 @@ +from momoapi.disbursement import Disbursement +from pytest_bdd import scenario, given, when, then, parsers +import re +import os +import pytest +from click.testing import CliRunner +from momoapi.cli import generateToken + +pytest.globalDict = {} + + +@scenario('features/disbursements.feature', 'Transfer Money to another account') +def test_disbursements(): + pass + + +@given("I have a valid user_id, auth_secret, and disbursements subscription key") +def user_credentials(): + config = { + "DISBURSEMENT_USER_ID": os.environ.get("DISBURSEMENT_USER_ID"), + "DISBURSEMENT_API_SECRET": os.environ.get("DISBURSEMENT_API_SECRET"), + "DISBURSEMENT_PRIMARY_KEY": os.environ.get("DISBURSEMENT_PRIMARY_KEY"), + } + client = Disbursement(config) + pytest.globalDict["client"] = client + + +@when("I transfer with the following payment details\n| note | amount | message | mobile | product_id |\n| test payment | 600 | message | 0782631873 | 0001 |") +def successful_transfer(): + ref = pytest.globalDict["client"].transfer( + amount="600", mobile="256772123456", external_id="123456789", payee_note="dd", payer_message="dd", + currency="EUR") + pytest.globalDict["ref"] = ref + + assert isinstance(ref, dict) + assert "transaction_ref" in ref.keys() + + +@when("I check for transaction Status") +def check_transaction_status(): + status = pytest.globalDict["client"].getTransactionStatus(pytest.globalDict["ref"]["transaction_ref"]) + pytest.globalDict["status"] = status + assert isinstance(status, dict) + assert "amount" in status.keys() + assert "currency" in status.keys() + + +@then("It should be successful") +def sucessful_transaction(): + assert pytest.globalDict["status"]["status"] == "SUCCESSFUL" diff --git a/tests/integration/test_disbursements.py b/tests/integration/test_disbursements.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/integration/test_remittance.py b/tests/integration/test_remittance.py new file mode 100644 index 0000000..caa14ca --- /dev/null +++ b/tests/integration/test_remittance.py @@ -0,0 +1,50 @@ +from pytest_bdd import scenario, given, when, then, parsers +import re +import os +import pytest +from click.testing import CliRunner +from momoapi.cli import generateToken +from momoapi.remittance import Remittance + +pytest.globalDict = {} + + +@scenario('features/remittances.feature', 'Move money from one account to another') +def test_remittences(): + pass + + +@given("I have a valid user_id, auth_secret, and remittence subscription key") +def user_credentials(): + config = { + "REMITTANCE_USER_ID": os.environ.get("REMITTANCE_USER_ID"), + "REMITTANCE_API_SECRET": os.environ.get("REMITTANCE_API_SECRET"), + "REMITTANCE_PRIMARY_KEY": os.environ.get("REMITTANCE_PRIMARY_KEY"), + } + client = Remittance(config) + pytest.globalDict["client"] = client + + +@when("I transfer with the following details\n| note | amount | message | mobile | product_id |\n| test payment | 600 | message | 0782631873 | 0001 |") +def successful_transfer(): + ref = pytest.globalDict["client"].transfer( + amount="600", mobile="256772123456", external_id="123456789", payee_note="dd", payer_message="dd", + currency="EUR") + pytest.globalDict["ref"] = ref + + assert isinstance(ref, dict) + assert "transaction_ref" in ref.keys() + + +@when("I check for transaction Status") +def check_transaction_status(): + status = pytest.globalDict["client"].getTransactionStatus(pytest.globalDict["ref"]["transaction_ref"]) + pytest.globalDict["status"] = status + assert isinstance(status, dict) + assert "amount" in status.keys() + assert "currency" in status.keys() + + +@then("It should be successful") +def sucessful_transaction(): + assert pytest.globalDict["status"]["status"] == "SUCCESSFUL" diff --git a/tests/integration/test_remittences.py b/tests/integration/test_remittences.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/integration/test_sandbox.py b/tests/integration/test_sandbox.py index fd8db08..f47288c 100644 --- a/tests/integration/test_sandbox.py +++ b/tests/integration/test_sandbox.py @@ -8,7 +8,7 @@ @scenario('features/sandbox.feature', 'Adding new account') -def test_sanbox_provisioning(): +def test_sandbox_provisioning(): pass diff --git a/tests/unitests/test_client.py b/tests/unitests/test_collection.py similarity index 58% rename from tests/unitests/test_client.py rename to tests/unitests/test_collection.py index ccdca9a..4af0a6c 100644 --- a/tests/unitests/test_client.py +++ b/tests/unitests/test_collection.py @@ -1,6 +1,7 @@ import unittest import pytest from momoapi.client import MomoApi +from momoapi.collection import Collection import types try: from unittest import mock @@ -12,11 +13,18 @@ from momoapi.errors import ValidationError -class TestClient(unittest.TestCase): +class TestCollections(unittest.TestCase): @mock.patch('requests.post', side_effect=mocked_requests_post) def setUp(self, mock_get): - client = MomoApi("APIKEY", "0555e303-ae5b-4052-a77b-6d284cfc669c", "APISECRET") + self.config = { + "COLLECTION_USER_ID": "0555e303-ae5b-4052-a77b-6d284cfc669c", + "COLLECTION_API_SECRET": "API_SECRET", + "COLLECTION_PRIMARY_KEY": "0555e303-ae5b-4052-a77b-6d284cfc669c", + # "DISBURSEMENTS_PRIMARY_KEY": "0555e303-ae5b-4052-a77b-6d284cfc669c", + # "REMITTENCES_PRIMARY_KEY": "0555e303-ae5b-4052-a77b-6d284cfc669c" + } + client = Collection(self.config) self.client = client def tearDown(self): @@ -26,32 +34,36 @@ def tearDown(self): @mock.patch('requests.get', side_effect=mocked_requests_get) def test_client_instantiate(self, mock_get): - client = MomoApi("APIKEY", "0555e303-ae5b-4052-a77b-6d284cfc669c", "APISECRET") + + client = Collection(self.config) #request_mock.assert_requested("post", "/v1/accounts") - assert isinstance(client, MomoApi) + assert isinstance(client, Collection) @mock.patch('requests.get', side_effect=mocked_requests_get) def test_invalid_uuid(self, mock_get): #client = MomoApi("APIKEY", "USERID", "APISECRET") with self.assertRaises(ValidationError): - client = MomoApi("APIKEY", "USERID", "APISECRET") + config = self.config + config["COLLECTION_PRIMARY_KEY"] = "invalid key" + client = Collection(config) + client.getAuthToken() @mock.patch('requests.get', side_effect=mocked_requests_get) def test_invalid_mobile(self, mock_get): #client = MomoApi("APIKEY", "USERID", "APISECRET") with self.assertRaises(ValidationError): - ref = self.client.requestToPay("256712123456", "600", "123456789", note="dd", - message="dd", currency="EUR", environment="sandbox") + ref = self.client.requestToPay(mobile="256712123456", amount="600", + external_id="123456789", payee_note="dd", payer_message="dd", currency="EUR") with self.assertRaises(ValidationError): - ref = self.client.requestToPay("254712123456", "600", "123456789", note="dd", - message="dd", currency="EUR", environment="sandbox") + ref = self.client.requestToPay(mobile="254712123456", amount="600", + external_id="123456789", payee_note="dd", payer_message="dd", currency="EUR") @mock.patch.object(MomoApi, "request", side_effect=mocked_requests_session) def test_request_to_pay(self, mock_get): - ref = self.client.requestToPay("256772123456", "600", "123456789", note="dd", - message="dd", currency="EUR", environment="sandbox") + ref = self.client.requestToPay(mobile="256772123456", amount="600", external_id="123456789", payee_note="dd", + payer_message="dd", currency="EUR") assert isinstance(ref, dict) assert "transaction_ref" in ref.keys() @@ -71,12 +83,12 @@ def test_get_transaction_status(self, mock_get): assert "amount" in status.keys() assert "currency" in status.keys() - @mock.patch.object(MomoApi, "request", side_effect=mocked_requests_session) - def test_transfer(self, mock): - ref = self.client.transfer("600", "256772123456", note="dd", - message="dd", currency="EUR", environment="sandbox") - assert isinstance(ref, dict) - assert "transaction_ref" in ref.keys() + # @mock.patch.object(MomoApi, "request", side_effect=mocked_requests_session) + # def test_transfer(self, mock): + # ref = self.client.transfer("600", "256772123456", note="dd", + # message="dd", currency="EUR", environment="sandbox") + #assert isinstance(ref, dict) + #assert "transaction_ref" in ref.keys() @mock.patch('requests.post', side_effect=mocked_requests_post) def test_generate_token(self, mock): diff --git a/tests/unitests/test_disbursement.py b/tests/unitests/test_disbursement.py new file mode 100644 index 0000000..ff4e359 --- /dev/null +++ b/tests/unitests/test_disbursement.py @@ -0,0 +1,84 @@ +import unittest +import pytest +from momoapi.client import MomoApi +from momoapi.disbursement import Disbursement +import types +try: + from unittest import mock +except ImportError: + import mock +from requests import Request, Session + +from .utils import mocked_requests_get, mocked_requests_post, mocked_requests_session +from momoapi.errors import ValidationError + + +class TestDisbursements(unittest.TestCase): + + @mock.patch('requests.post', side_effect=mocked_requests_post) + def setUp(self, mock_get): + self.config = { + "DISBURSEMENT_USER_ID": "USER_ID", + "DISBURSEMENT_API_SECRET": "API_SECRET", + # "COLLECTIONS_PRIMARY_KEY": "0555e303-ae5b-4052-a77b-6d284cfc669c", + "DISBURSEMENT_PRIMARY_KEY": "0555e303-ae5b-4052-a77b-6d284cfc669c", + # "REMITTENCES_PRIMARY_KEY": "0555e303-ae5b-4052-a77b-6d284cfc669c" + } + client = Disbursement(self.config) + self.client = client + + def tearDown(self): + pass + # self.widget.dispose() + #self.widget = None + + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_client_instantiate(self, mock_get): + + client = Disbursement(self.config) + + #request_mock.assert_requested("post", "/v1/accounts") + assert isinstance(client, Disbursement) + + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_invalid_uuid(self, mock_get): + #client = MomoApi("APIKEY", "USERID", "APISECRET") + with self.assertRaises(ValidationError): + config = self.config + config["COLLECTION_PRIMARY_KEY"] = "invalid key" + client = Disbursement(config) + client.getAuthToken() + + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_invalid_mobile(self, mock_get): + #client = MomoApi("APIKEY", "USERID", "APISECRET") + with self.assertRaises(ValidationError): + ref = self.client.transfer(amount="600", mobile="2567721234569", external_id="123456789", payee_note="dd", + payer_message="dd", currency="EUR") + with self.assertRaises(ValidationError): + ref = self.client.transfer(amount="600", mobile="256712123456", external_id="123456789", payee_note="dd", + payer_message="dd", currency="EUR") + + @mock.patch.object(MomoApi, "request", side_effect=mocked_requests_session) + def test_transfer(self, mock_get): + + ref = self.client.transfer(amount="600", mobile="256772123456", external_id="123456789", payee_note="dd", + payer_message="dd", currency="EUR") + + assert isinstance(ref, dict) + assert "transaction_ref" in ref.keys() + + @mock.patch.object(MomoApi, "request", side_effect=mocked_requests_session) + def test_get_balance(self, mock_get): + balance = self.client.getBalance() + assert isinstance(balance, dict) + assert "availableBalance" in balance.keys() + assert "currency" in balance.keys() + + # @mock.patch('requests.get', side_effect=mocked_requests_get) + @mock.patch.object(MomoApi, "request", side_effect=mocked_requests_session) + def test_get_transaction_status(self, mock_get): + status = self.client.getTransactionStatus("dummy") + assert isinstance(status, dict) + assert "amount" in status.keys() + assert "currency" in status.keys() diff --git a/tests/unitests/test_remittance.py b/tests/unitests/test_remittance.py new file mode 100644 index 0000000..4cba3a1 --- /dev/null +++ b/tests/unitests/test_remittance.py @@ -0,0 +1,85 @@ +import unittest +import pytest +from momoapi.client import MomoApi +from momoapi.remittance import Remittance +import types +try: + from unittest import mock +except ImportError: + import mock +from requests import Request, Session + +from .utils import mocked_requests_get, mocked_requests_post, mocked_requests_session +from momoapi.errors import ValidationError + + +class TestRemittences(unittest.TestCase): + + @mock.patch('requests.post', side_effect=mocked_requests_post) + def setUp(self, mock_get): + self.config = { + "REMITTANCE_USER_ID": "USER_ID", + "REMITTANCE_API_SECRET": "API_SECRET", + + # "COLLECTIONS_PRIMARY_KEY": "0555e303-ae5b-4052-a77b-6d284cfc669c", + # "DISBURSEMENTS_PRIMARY_KEY": "0555e303-ae5b-4052-a77b-6d284cfc669c", + "REMITTANCE_PRIMARY_KEY": "0555e303-ae5b-4052-a77b-6d284cfc669c" + } + client = Remittance(self.config) + self.client = client + + def tearDown(self): + pass + # self.widget.dispose() + #self.widget = None + + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_client_instantiate(self, mock_get): + + client = Remittance(self.config) + + #request_mock.assert_requested("post", "/v1/accounts") + assert isinstance(client, Remittance) + + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_invalid_uuid(self, mock_get): + #client = MomoApi("APIKEY", "USERID", "APISECRET") + with self.assertRaises(ValidationError): + config = self.config + config["REMITTANCE_PRIMARY_KEY"] = "invalid key" + client = Remittance(config) + client.getAuthToken() + + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_invalid_mobile(self, mock_get): + #client = MomoApi("APIKEY", "USERID", "APISECRET") + with self.assertRaises(ValidationError): + ref = self.client.transfer(amount="600", mobile="256712123456", external_id="123456789", payee_note="dd", + payer_message="dd", currency="EUR") + with self.assertRaises(ValidationError): + ref = self.client.transfer(amount="600", mobile="256712123456", external_id="123456789", payee_note="dd", + payer_message="dd", currency="EUR") + + @mock.patch.object(MomoApi, "request", side_effect=mocked_requests_session) + def test_transfer(self, mock_get): + + ref = self.client.transfer(amount="600", mobile="256772123456", external_id="123456789", payee_note="dd", + payer_message="dd", currency="EUR") + + assert isinstance(ref, dict) + assert "transaction_ref" in ref.keys() + + @mock.patch.object(MomoApi, "request", side_effect=mocked_requests_session) + def test_get_balance(self, mock_get): + balance = self.client.getBalance() + assert isinstance(balance, dict) + assert "availableBalance" in balance.keys() + assert "currency" in balance.keys() + + # @mock.patch('requests.get', side_effect=mocked_requests_get) + @mock.patch.object(MomoApi, "request", side_effect=mocked_requests_session) + def test_get_transaction_status(self, mock_get): + status = self.client.getTransactionStatus("dummy") + assert isinstance(status, dict) + assert "amount" in status.keys() + assert "currency" in status.keys() diff --git a/tests/unitests/utils.py b/tests/unitests/utils.py index 59dd1f4..d4eed6a 100644 --- a/tests/unitests/utils.py +++ b/tests/unitests/utils.py @@ -48,18 +48,18 @@ def mocked_requests_post(*args, **kwargs): def mocked_requests_session(*args, **kwargs): - if '/collection/token/' in args[1]: + if '/token/' in args[1]: return MockResponse({"access_token": "token"}, 200) - elif '/account/balance' in args[1]: + elif '/balance' in args[1]: return MockResponse({ "availableBalance": "500", "currency": "UGX" }, 200) elif "/requesttopay" in args[1] and args[0] == 'POST': return MockResponse({}, 200) - elif "transfer" in args[1]: + elif "transfer" in args[1] and args[0] == 'POST': return MockResponse({}, 200) - elif "/requesttopay" in args[1] and args[0] == 'GET': + elif ("/requesttopay" in args[1] or "/transfer" in args[1]) and args[0] == 'GET': return MockResponse({ "amount": 100, "currency": "UGX", @@ -71,3 +71,5 @@ def mocked_requests_session(*args, **kwargs): }, "status": "SUCCESSFUL" }, 200) + else: + return MockResponse({}, 200) diff --git a/tox.ini b/tox.ini index f8ec37a..34ca23d 100644 --- a/tox.ini +++ b/tox.ini @@ -31,14 +31,12 @@ deps = mock commands = {posargs:pytest -vv --ignore=src} - + passenv = - # See https://github.com/codecov/codecov-python/blob/5b9d539a6a09bc84501b381b563956295478651a/README.md#using-tox - codecov: TOXENV - codecov: CI - codecov: TRAVIS TRAVIS_* + COLLECTION_* REMITTANCE_* DISBURSEMENT_* TRAVIS TRAVIS_* + [testenv:coveralls] @@ -224,6 +222,10 @@ ignore = # line break after binary operator (W503 and W504 are opposites) W504, + I201, + + I100, + ## From eea0750a8ec99e86998147ee47cc0de28f287387 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Mon, 11 Mar 2019 17:55:43 +0300 Subject: [PATCH 10/35] fix python 2.7 compatibility issues --- src/momoapi/client.py | 10 ++++------ src/momoapi/collection.py | 17 +++++++++-------- src/momoapi/disbursement.py | 18 +++++++++--------- src/momoapi/remittance.py | 21 +++++++++++---------- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/momoapi/client.py b/src/momoapi/client.py index 97c08a0..fdeee8f 100644 --- a/src/momoapi/client.py +++ b/src/momoapi/client.py @@ -3,8 +3,6 @@ @author: Moses Mugisha """ - - import json try: from json.decoder import JSONDecodeError @@ -59,16 +57,16 @@ def getTransactionStatus(self): class Client(ClientInterface): def getAuthToken(self): - return super().getAuthToken() + return super(Client, self).getAuthToken() def getBalance(self): - return super().getBalance() + return super(Client, self).getBalance() def getTransactionStatus(self): - return super().getTransactionStatus() + return super(Client, self).getTransactionStatus() -class MomoApi(ClientInterface): +class MomoApi(ClientInterface, object): def __init__( self, diff --git a/src/momoapi/collection.py b/src/momoapi/collection.py index efaa17e..ee52b29 100644 --- a/src/momoapi/collection.py +++ b/src/momoapi/collection.py @@ -4,20 +4,21 @@ from .utils import validate_phone_number -class Collection(MomoApi): +class Collection(MomoApi, object): def getAuthToken(self): """ Create an access token which can then be used to authorize and authenticate towards the other end-points of the API """ url = "/collection/token/" - response = super().getAuthToken("COLLECTION", url, super().config.collectionsKey) + response = super(Collection, self).getAuthToken( + "COLLECTION", url, super(Collection, self).config.collectionsKey) return response def getBalance(self): url = "/collection/v1_0/account/balance" - return super().getBalance(url, super().config.collectionsKey) + return super(Collection, self).getBalance(url, super(Collection, self).config.collectionsKey) def getTransactionStatus( self, @@ -25,8 +26,8 @@ def getTransactionStatus( **kwargs): url = "/collection/v1_0/requesttopay/" - return super().getTransactionStatus( - transaction_id, url, super().config.collectionsKey) + return super(Collection, self).getTransactionStatus( + transaction_id, url, super(Collection, self).config.collectionsKey) def requestToPay( self, @@ -49,15 +50,15 @@ def requestToPay( "currency": currency, "amount": str(amount)} headers = { - "X-Target-Environment": super().config.environment, + "X-Target-Environment": super(Collection, self).config.environment, "Content-Type": "application/json", "X-Reference-Id": ref, - "Ocp-Apim-Subscription-Key": super().config.collectionsKey + "Ocp-Apim-Subscription-Key": super(Collection, self).config.collectionsKey } if kwargs.get("callback_url"): headers["X-Callback-Url"] = kwargs.get("callback_url") - url = "{0}/collection/v1_0/requesttopay".format(super().config.baseUrl) + url = "{0}/collection/v1_0/requesttopay".format(super(Collection, self).config.baseUrl) self.request("POST", url, headers, data) return {"transaction_ref": ref} diff --git a/src/momoapi/disbursement.py b/src/momoapi/disbursement.py index b427a0b..6d71deb 100644 --- a/src/momoapi/disbursement.py +++ b/src/momoapi/disbursement.py @@ -3,21 +3,21 @@ from .utils import validate_phone_number -class Disbursement(MomoApi): +class Disbursement(MomoApi, object): def getAuthToken(self): """ Create an access token which can then be used to authorize and authenticate towards the other end-points of the API. """ url = "/disbursement/token/" - response = super().getAuthToken( - "DISBURSEMENT", url, super().config.disbursementsKey) + response = super(Disbursement, self).getAuthToken( + "DISBURSEMENT", url, super(Disbursement, self).config.disbursementsKey) return response def getBalance(self): url = "/disbursement/v1_0/account/balance" - return super().getBalance(url, super().config.disbursementsKey) + return super(Disbursement, self).getBalance(url, super(Disbursement, self).config.disbursementsKey) def getTransactionStatus( self, @@ -25,8 +25,8 @@ def getTransactionStatus( **kwargs): url = "/disbursement/v1_0/transfer/" - return super().getTransactionStatus( - transaction_id, url, super().config.disbursementsKey) + return super(Disbursement, self).getTransactionStatus( + transaction_id, url, super(Disbursement, self).config.disbursementsKey) def transfer( self, @@ -50,14 +50,14 @@ def transfer( "payeeNote": payee_note } headers = { - "X-Target-Environment": super().config.environment, + "X-Target-Environment": super(Disbursement, self).config.environment, "Content-Type": "application/json", - "Ocp-Apim-Subscription-Key": super().config.disbursementsKey, + "Ocp-Apim-Subscription-Key": super(Disbursement, self).config.disbursementsKey, "X-Reference-Id": ref, } if kwargs.get("callback_url"): headers["X-Callback-Url"] = kwargs.get("callback_url") - url = super().config.baseUrl + "/disbursement/v1_0/transfer" + url = super(Disbursement, self).config.baseUrl + "/disbursement/v1_0/transfer" print(url) self.request("POST", url, headers, data) return {"transaction_ref": ref} diff --git a/src/momoapi/remittance.py b/src/momoapi/remittance.py index 0648f1b..c575fc8 100644 --- a/src/momoapi/remittance.py +++ b/src/momoapi/remittance.py @@ -3,17 +3,18 @@ from .utils import validate_phone_number -class Remittance(MomoApi): +class Remittance(MomoApi, object): def getAuthToken(self): """Generate access token which can then be use to authorize and authenticate towards the other end-points of the API""" url = "/remittance/token/" - response = super().getAuthToken("REMITTANCE", url, super().config.remittencesKey) + response = super(Remittance, self).getAuthToken( + "REMITTANCE", url, super(Remittance, self).config.remittencesKey) return response def getBalance(self): url = "/remittance/v1_0/account/balance" - return super().getBalance(url, super().config.remittencesKey) + return super(Remittance, self).getBalance(url, super(Remittance, self).config.remittencesKey) def getTransactionStatus( self, @@ -24,8 +25,8 @@ def getTransactionStatus( """ url = "/remittance/v1_0/transfer/" - return super().getTransactionStatus( - transaction_id, url, super().config.remittencesKey) + return super(Remittance, self).getTransactionStatus( + transaction_id, url, super(Remittance, self).config.remittencesKey) def transfer( self, @@ -56,17 +57,17 @@ def transfer( } headers = { - "X-Target-Environment": super().config.environment, + "X-Target-Environment": super(Remittance, self).config.environment, "Content-Type": "application/json", "X-Reference-Id": ref, - "Ocp-Apim-Subscription-Key": super().config.remittencesKey + "Ocp-Apim-Subscription-Key": super(Remittance, self).config.remittencesKey } if kwargs.get("callback_url"): headers["X-Callback-Url"] = kwargs.get("callback_url") - url = "{0}/remittance/v1_0/transfer".format(super().config.baseUrl) + url = "{0}/remittance/v1_0/transfer".format(super(Remittance, self).config.baseUrl) self.request("POST", url, headers, data) return {"transaction_ref": ref} @@ -80,9 +81,9 @@ def isActive(self, mobile): headers = { "X-Target-Environment": self.config.environment, "Content-Type": "application/json", - "Ocp-Apim-Subscription-Key": super().config.remittencesKey + "Ocp-Apim-Subscription-Key": super(Remittance, self).config.remittencesKey } url = "{0}/remittance/v1_0/accountholder/MSISDN/{1}/active".format( - super().config.baseUrl, mobile) + super(Remittance, self).config.baseUrl, mobile) res = self.request("GET", url, headers) return res.json() From bbbc0b1e2f5aaafc3fdc0e24211101a743e91e86 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Mon, 11 Mar 2019 17:55:43 +0300 Subject: [PATCH 11/35] [major] fix python 2.7 compatibility issues --- src/momoapi/client.py | 10 ++++------ src/momoapi/collection.py | 17 +++++++++-------- src/momoapi/disbursement.py | 18 +++++++++--------- src/momoapi/remittance.py | 21 +++++++++++---------- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/momoapi/client.py b/src/momoapi/client.py index 97c08a0..fdeee8f 100644 --- a/src/momoapi/client.py +++ b/src/momoapi/client.py @@ -3,8 +3,6 @@ @author: Moses Mugisha """ - - import json try: from json.decoder import JSONDecodeError @@ -59,16 +57,16 @@ def getTransactionStatus(self): class Client(ClientInterface): def getAuthToken(self): - return super().getAuthToken() + return super(Client, self).getAuthToken() def getBalance(self): - return super().getBalance() + return super(Client, self).getBalance() def getTransactionStatus(self): - return super().getTransactionStatus() + return super(Client, self).getTransactionStatus() -class MomoApi(ClientInterface): +class MomoApi(ClientInterface, object): def __init__( self, diff --git a/src/momoapi/collection.py b/src/momoapi/collection.py index efaa17e..ee52b29 100644 --- a/src/momoapi/collection.py +++ b/src/momoapi/collection.py @@ -4,20 +4,21 @@ from .utils import validate_phone_number -class Collection(MomoApi): +class Collection(MomoApi, object): def getAuthToken(self): """ Create an access token which can then be used to authorize and authenticate towards the other end-points of the API """ url = "/collection/token/" - response = super().getAuthToken("COLLECTION", url, super().config.collectionsKey) + response = super(Collection, self).getAuthToken( + "COLLECTION", url, super(Collection, self).config.collectionsKey) return response def getBalance(self): url = "/collection/v1_0/account/balance" - return super().getBalance(url, super().config.collectionsKey) + return super(Collection, self).getBalance(url, super(Collection, self).config.collectionsKey) def getTransactionStatus( self, @@ -25,8 +26,8 @@ def getTransactionStatus( **kwargs): url = "/collection/v1_0/requesttopay/" - return super().getTransactionStatus( - transaction_id, url, super().config.collectionsKey) + return super(Collection, self).getTransactionStatus( + transaction_id, url, super(Collection, self).config.collectionsKey) def requestToPay( self, @@ -49,15 +50,15 @@ def requestToPay( "currency": currency, "amount": str(amount)} headers = { - "X-Target-Environment": super().config.environment, + "X-Target-Environment": super(Collection, self).config.environment, "Content-Type": "application/json", "X-Reference-Id": ref, - "Ocp-Apim-Subscription-Key": super().config.collectionsKey + "Ocp-Apim-Subscription-Key": super(Collection, self).config.collectionsKey } if kwargs.get("callback_url"): headers["X-Callback-Url"] = kwargs.get("callback_url") - url = "{0}/collection/v1_0/requesttopay".format(super().config.baseUrl) + url = "{0}/collection/v1_0/requesttopay".format(super(Collection, self).config.baseUrl) self.request("POST", url, headers, data) return {"transaction_ref": ref} diff --git a/src/momoapi/disbursement.py b/src/momoapi/disbursement.py index b427a0b..6d71deb 100644 --- a/src/momoapi/disbursement.py +++ b/src/momoapi/disbursement.py @@ -3,21 +3,21 @@ from .utils import validate_phone_number -class Disbursement(MomoApi): +class Disbursement(MomoApi, object): def getAuthToken(self): """ Create an access token which can then be used to authorize and authenticate towards the other end-points of the API. """ url = "/disbursement/token/" - response = super().getAuthToken( - "DISBURSEMENT", url, super().config.disbursementsKey) + response = super(Disbursement, self).getAuthToken( + "DISBURSEMENT", url, super(Disbursement, self).config.disbursementsKey) return response def getBalance(self): url = "/disbursement/v1_0/account/balance" - return super().getBalance(url, super().config.disbursementsKey) + return super(Disbursement, self).getBalance(url, super(Disbursement, self).config.disbursementsKey) def getTransactionStatus( self, @@ -25,8 +25,8 @@ def getTransactionStatus( **kwargs): url = "/disbursement/v1_0/transfer/" - return super().getTransactionStatus( - transaction_id, url, super().config.disbursementsKey) + return super(Disbursement, self).getTransactionStatus( + transaction_id, url, super(Disbursement, self).config.disbursementsKey) def transfer( self, @@ -50,14 +50,14 @@ def transfer( "payeeNote": payee_note } headers = { - "X-Target-Environment": super().config.environment, + "X-Target-Environment": super(Disbursement, self).config.environment, "Content-Type": "application/json", - "Ocp-Apim-Subscription-Key": super().config.disbursementsKey, + "Ocp-Apim-Subscription-Key": super(Disbursement, self).config.disbursementsKey, "X-Reference-Id": ref, } if kwargs.get("callback_url"): headers["X-Callback-Url"] = kwargs.get("callback_url") - url = super().config.baseUrl + "/disbursement/v1_0/transfer" + url = super(Disbursement, self).config.baseUrl + "/disbursement/v1_0/transfer" print(url) self.request("POST", url, headers, data) return {"transaction_ref": ref} diff --git a/src/momoapi/remittance.py b/src/momoapi/remittance.py index 0648f1b..c575fc8 100644 --- a/src/momoapi/remittance.py +++ b/src/momoapi/remittance.py @@ -3,17 +3,18 @@ from .utils import validate_phone_number -class Remittance(MomoApi): +class Remittance(MomoApi, object): def getAuthToken(self): """Generate access token which can then be use to authorize and authenticate towards the other end-points of the API""" url = "/remittance/token/" - response = super().getAuthToken("REMITTANCE", url, super().config.remittencesKey) + response = super(Remittance, self).getAuthToken( + "REMITTANCE", url, super(Remittance, self).config.remittencesKey) return response def getBalance(self): url = "/remittance/v1_0/account/balance" - return super().getBalance(url, super().config.remittencesKey) + return super(Remittance, self).getBalance(url, super(Remittance, self).config.remittencesKey) def getTransactionStatus( self, @@ -24,8 +25,8 @@ def getTransactionStatus( """ url = "/remittance/v1_0/transfer/" - return super().getTransactionStatus( - transaction_id, url, super().config.remittencesKey) + return super(Remittance, self).getTransactionStatus( + transaction_id, url, super(Remittance, self).config.remittencesKey) def transfer( self, @@ -56,17 +57,17 @@ def transfer( } headers = { - "X-Target-Environment": super().config.environment, + "X-Target-Environment": super(Remittance, self).config.environment, "Content-Type": "application/json", "X-Reference-Id": ref, - "Ocp-Apim-Subscription-Key": super().config.remittencesKey + "Ocp-Apim-Subscription-Key": super(Remittance, self).config.remittencesKey } if kwargs.get("callback_url"): headers["X-Callback-Url"] = kwargs.get("callback_url") - url = "{0}/remittance/v1_0/transfer".format(super().config.baseUrl) + url = "{0}/remittance/v1_0/transfer".format(super(Remittance, self).config.baseUrl) self.request("POST", url, headers, data) return {"transaction_ref": ref} @@ -80,9 +81,9 @@ def isActive(self, mobile): headers = { "X-Target-Environment": self.config.environment, "Content-Type": "application/json", - "Ocp-Apim-Subscription-Key": super().config.remittencesKey + "Ocp-Apim-Subscription-Key": super(Remittance, self).config.remittencesKey } url = "{0}/remittance/v1_0/accountholder/MSISDN/{1}/active".format( - super().config.baseUrl, mobile) + super(Remittance, self).config.baseUrl, mobile) res = self.request("GET", url, headers) return res.json() From 54a2fd8167eb67c2dfea56a7d8dd9c083d05712d Mon Sep 17 00:00:00 2001 From: Ray Besiga Date: Mon, 18 Mar 2019 11:26:20 +0300 Subject: [PATCH 12/35] Update Coveralls badge to show coverage for validations branch --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ebe92e3..70aa924 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ MTN MoMo API Client for Python. [![Build Status](https://travis-ci.com/sparkplug/momoapi-python.svg?branch=master)](https://travis-ci.com/sparkplug/momoapi-node) [![Latest Version](https://img.shields.io/pypi/v/tox-travis.svg)](https://badge.fury.io/js/mtn-momo) -[![Coverage Status](https://coveralls.io/repos/github/sparkplug/momoapi-python/badge.svg?branch=master)](https://coveralls.io/github/sparkplug/momoapi-python?branch=master) +[![Coverage Status](https://coveralls.io/repos/github/sparkplug/momoapi-python/badge.svg?branch=validations)](https://coveralls.io/github/sparkplug/momoapi-python?branch=validations) [![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/momo-api-developers/) From 9ed7b8340d71323fa3d20ef05abaf8aeb27fed00 Mon Sep 17 00:00:00 2001 From: Ray Besiga Date: Mon, 18 Mar 2019 11:57:45 +0300 Subject: [PATCH 13/35] Update README --- README.md | 190 ++++++++++++++++++++++++++---------------------------- 1 file changed, 93 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index 70aa924..48471b8 100644 --- a/README.md +++ b/README.md @@ -10,104 +10,80 @@ MTN MoMo API Client for Python. # Installing - -To install the latest version of Momo-api using pip:: +Add the latest version of the library to your project using pip: ```bash $ pip install momoapi - ``` -Additional instructions for installing this software are in `the installation instructions `_. - - -# Unit Tests - -momo-api has a comprehensive test suite, which can be run by ``tox``:: - -## to view all test environments - - ```bash - $ tox -l - ``` - ## to run the tests for Python 2.7 - - ```bash - $ tox -e py27-cover - - ``` - - ## to run the tests for Python 3.4 - - ```bash - $ tox -e py34-cover - ``` - +This library supports Python 2.7+ or Python 3.4+ (PyPy supported) +# Sandbox Environment -## Requirements +## Creating a sandbox environment API user -* Python 2.7+ or Python 3.4+ (PyPy supported) - -## Sandbox Environment - -# Creating a sandbox environment API user - -The library ships with a commandline application that helps to create sandbox credentials. It assumes you have created an account on `https://momodeveloper.mtn.com` and have your `Ocp-Apim-Subscription-Key` +Next, we need to get the `User ID` and `User Secret` and to do this we shall need to use the Primary Key for the Product to which we are subscribed, as well as specify a host. The library ships with a commandline application that helps to create sandbox credentials. It assumes you have created an account on `https://momodeveloper.mtn.com` and have your `Ocp-Apim-Subscription-Key`. ```bash +## within the project, on the command line. In this example, our domain is akabbo.ug $ momoapi $ providerCallBackHost: https://akabbo.ug $ Ocp-Apim-Subscription-Key: f83xx8d8xx6749f19a26e2265aeadbcdeg ``` -where `providerCallBackHost` is your callback host and `Ocp-Apim-Subscription-Key` is your API key for the specific product to which you are subscribed. The `API Key` is unique to the product and you will need an `API Key` for each product you use. You should get the following response. +The `providerCallBackHost` is your callback host and `Ocp-Apim-Subscription-Key` is your API key for the specific product to which you are subscribed. The `API Key` is unique to the product and you will need an `API Key` for each product you use. You should get a response similar to the following: ```bash Here is your User Id and API secret : {'apiKey': 'b0431db58a9b41faa8f5860230xxxxxx', 'UserId': '053c6dea-dd68-xxxx-xxxx-c830dac9f401'} ``` +These are the credentials we shall use for the sandbox environment. In production, these credentials are provided for you on the MTN OVA management dashboard after KYC requirements are met. + ## Configuration -Each MOMO API product requires its own authentication details. i.e its own separate subscription key, user_id and api_secret. As such, we have to configure subscription keys for each product you will be using. +Before we can fully utilize the library, we need to specify global configurations. The global configuration must contain the following: -In addition to this, In you may also need to specify your `BASE_URL`, `ENVIRONMENT` and `CALLBACK_HOST` if you are not using the defaults. -here is the full list of configuration options +* `BASE_URL`: An optional base url to the MTN Momo API. By default the staging base url will be used +* `ENVIRONMENT`: Optional enviroment, either "sandbox" or "production". Default is 'sandbox' +* `CALLBACK_HOST`: The domain where you webhooks urls are hosted. This is mandatory. + +Once you have specified the global variables, you can now provide the product-specific variables. Each MoMo API product requires its own authentication details i.e its own `Subscription Key`, `User ID` and `User Secret`, also sometimes refered to as the `API Secret`. As such, we have to configure subscription keys for each product you will be using. + +The full list of configuration options can be seen in the example below: ```python - config={ - - "ENVIRONMENT": os.environ.get("ENVIRONMENT"),#Optional enviroment, either "sandbox" or "production". Default is 'sandbox' - "BASE_URL": os.environ.get("BASE_URL"),#An optional base url to the MTN Momo API. By default the staging base url will be used - "CALLBACK_HOST": os.environ.get("CALLBACK_HOST"),#The domain where you webhooks urls are hosted. - "COLLECTION_PRIMARY_KEY": os.environ.get("COLLECTION_PRIMARY_KEY"),# Primary Key for the `Collection` product. - "COLLECTION_USER_ID": os.environ.get("COLLECTION_USER_ID"),# User id of the collection product - "COLLECTION_API_SECRET": os.environ.get("COLLECTION_API_SECRET"),# API secret of the collection product - "REMITTANCE_USER_ID": os.environ.get("REMITTANCE_USER_ID"), # User id of the remittance product - "REMITTANCE_API_SECRET": os.environ.get("REMITTANCE_API_SECRET"),# API secret of the remittance product - "REMITTANCE_PRIMARY_KEY": os.envieon.get("REMITTANCE_PRIMARY_KEY"), #Primary Key for the 'Remittance' product. - "DISBURSEMENT_USER_ID": os.environ.get("DISBURSEMENT_USER_ID"), # User id of the disbursement product - "DISBURSEMENT_API_SECRET": os.environ.get("DISBURSEMENTS_API_SECRET"),# API secret of the Disbursemnet product - "DISBURSEMENT_PRIMARY_KEY": os.environ.get("DISBURSEMENT_PRIMARY_KEY"), #Primary Key for the 'Disbursement' product. - } - ``` - -You will only need to configure the product(s) you will be using. + config = { + "ENVIRONMENT": os.environ.get("ENVIRONMENT"), + "BASE_URL": os.environ.get("BASE_URL"), + "CALLBACK_HOST": os.environ.get("CALLBACK_HOST"), # Mandatory. + "COLLECTION_PRIMARY_KEY": os.environ.get("COLLECTION_PRIMARY_KEY"), + "COLLECTION_USER_ID": os.environ.get("COLLECTION_USER_ID"), + "COLLECTION_API_SECRET": os.environ.get("COLLECTION_API_SECRET"), + "REMITTANCE_USER_ID": os.environ.get("REMITTANCE_USER_ID"), + "REMITTANCE_API_SECRET": os.environ.get("REMITTANCE_API_SECRET"), + "REMITTANCE_PRIMARY_KEY": os.envieon.get("REMITTANCE_PRIMARY_KEY"), + "DISBURSEMENT_USER_ID": os.environ.get("DISBURSEMENT_USER_ID"), + "DISBURSEMENT_API_SECRET": os.environ.get("DISBURSEMENTS_API_SECRET"), + "DISBURSEMENT_PRIMARY_KEY": os.environ.get("DISBURSEMENT_PRIMARY_KEY"), +} +``` +You will only need to configure the variables for the product(s) you will be using. ## Collections The collections client can be created with the following paramaters. Note that the `COLLECTION_USER_ID` and `COLLECTION_API_SECRET` for production are provided on the MTN OVA dashboard; -- `COLLECTION_PRIMARY_KEY`: Primary Key for the `Collection` product. -- `COLLECTION_USER_ID`: For sandbox, use the one generated with the `momoapi` command. -- `COLLECTION_API_SECRET`: For sandbox, use the one generated with the `momoapi` command. +* `COLLECTION_PRIMARY_KEY`: Primary Key for the `Collection` product on the developer portal. +* `COLLECTION_USER_ID`: For sandbox, use the one generated with the `momoapi` command. +* `COLLECTION_API_SECRET`: For sandbox, use the one generated with the `momoapi` command. -You can create a collection client with the following +You can create a collection client with the following: ```python -from momoapi.collection import Collection import os +from momoapi.collection import Collection + client = Collection({ "COLLECTION_USER_ID": os.environ.get("COLLECTION_USER_ID"), "COLLECTION_API_SECRET": os.environ.get("COLLECTION_API_SECRET"), @@ -115,78 +91,98 @@ client = Collection({ }) ``` -#### Methods +### Methods -1. `requestToPay: This operation is used to request a payment from a consumer (Payer). The payer will be asked to authorize the payment. The transaction is executed once the payer has authorized the payment. The transaction will be in status PENDING until it is authorized or declined by the payer or it is timed out by the system. Status of the transaction can be validated by using `getTransactionStatus` +1. `requestToPay`: This operation is used to request a payment from a consumer (Payer). The payer will be asked to authorize the payment. The transaction is executed once the payer has authorized the payment. The transaction will be in status PENDING until it is authorized or declined by the payer or it is timed out by the system. Status of the transaction can be validated by using `getTransactionStatus`. 2. `getTransaction`: Retrieve transaction information using the `transactionId` returned by `requestToPay`. You can invoke it at intervals until the transaction fails or succeeds. If the transaction has failed, it will throw an appropriate error. -3. `getBalance()`: Get the balance of the account. +3. `getBalance`: Get the balance of the account. -4. `isPayerActive: check if an account holder is registered and active in the system. +4. `isPayerActive`: check if an account holder is registered and active in the system. -#### Sample Code +### Sample Code ```python -from momoapi.collection import Collection import os +from momoapi.collection import Collection + client = Collection({ - "COLLECTION_USER_ID": os.environ.get("COLLECTION_USER_ID"), - "COLLECTION_API_SECRET": os.environ.get("COLLECTION_API_SECRET"), - "COLLECTION_PRIMARY_KEY": os.environ.get("COLLECTION_PRIMARY_KEY"), - }) + "COLLECTION_USER_ID": os.environ.get("COLLECTION_USER_ID"), + "COLLECTION_API_SECRET": os.environ.get("COLLECTION_API_SECRET"), + "COLLECTION_PRIMARY_KEY": os.environ.get("COLLECTION_PRIMARY_KEY"), +}) client.requestToPay( - mobile="256772123456", amount="600", external_id="123456789", payee_note="dd", payer_message="dd", - currency="EUR") + mobile="256772123456", amount="600", external_id="123456789", payee_note="dd", payer_message="dd", currency="EUR") ``` ## Disbursement -The disbursements client can be created with the following paramaters. Note that the `DISBURSEMENT_USER_ID` and `DISBURSEMENT_API_SECRET` for production are provided on the MTN OVA dashboard; +The Disbursements client can be created with the following paramaters. Note that the `DISBURSEMENT_USER_ID` and `DISBURSEMENT_API_SECRET` for production are provided on the MTN OVA dashboard; -- `DISBURSEMENT_PRIMARY_KEY`: Primary Key for the `Disbursement` product. -- `DISBURSEMENT_USER_ID`: For sandbox, use the one generated with the `momoapi` command. -- `DISBURSEMENT_API_SECRET`: For sandbox, use the one generated with the `momoapi` command. +* `DISBURSEMENT_PRIMARY_KEY`: Primary Key for the `Disbursement` product on the developer portal. +* `DISBURSEMENT_USER_ID`: For sandbox, use the one generated with the `momoapi` command. +* `DISBURSEMENT_API_SECRET`: For sandbox, use the one generated with the `momoapi` command. You can create a disbursements client with the following ```python -from momoapi.collection import Disbursement import os +from momoapi.collection import Disbursement + client = Disbursement({ - "DISBURSEMENT_USER_ID": os.environ.get("DISBURSEMENT_USER_ID"), - "DISBURSEMENT_API_SECRET": os.environ.get("DISBURSEMENT_API_SECRET"), - "DISBURSEMENT_PRIMARY_KEY": os.environ.get("DISBURSEMENT_PRIMARY_KEY"), - }) + "DISBURSEMENT_USER_ID": os.environ.get("DISBURSEMENT_USER_ID"), + "DISBURSEMENT_API_SECRET": os.environ.get("DISBURSEMENT_API_SECRET"), + "DISBURSEMENT_PRIMARY_KEY": os.environ.get("DISBURSEMENT_PRIMARY_KEY"), +}) ``` -#### Methods - -1. `transfer` +### Methods -Used to transfer an amount from the owner’s account to a payee account. Status of the transaction can be validated by using the +1. `transfer`: Used to transfer an amount from the owner’s account to a payee account. Status of the transaction can be validated by using the `getTransactionStatus` method. -1. `getTransactionStatus`: Retrieve transaction information using the `transactionId` returned by `transfer`. You can invoke it at intervals until the transaction fails or succeeds. +2. `getTransactionStatus`: Retrieve transaction information using the `transactionId` returned by `transfer`. You can invoke it at intervals until the transaction fails or succeeds. -2. `getBalance()`: Get your account balance. +2. `getBalance`: Get your account balance. 3. `isPayerActive`: This method is used to check if an account holder is registered and active in the system. #### Sample Code ```python -from momoapi.collection import Disbursement import os +from momoapi.collection import Disbursement + client = Disbursement({ - "DISBURSEMENT_USER_ID": os.environ.get("DISBURSEMENT_USER_ID"), - "DISBURSEMENT_API_SECRET": os.environ.get("DISBURSEMENT_API_SECRET"), - "DISBURSEMENT_PRIMARY_KEY": os.environ.get("DISBURSEMENT_PRIMARY_KEY"), - }) - client.transfer(amount="600", mobile="256772123456", external_id="123456789", payee_note="dd", - payer_message="dd", currency="EUR") + "DISBURSEMENT_USER_ID": os.environ.get("DISBURSEMENT_USER_ID"), + "DISBURSEMENT_API_SECRET": os.environ.get("DISBURSEMENT_API_SECRET"), + "DISBURSEMENT_PRIMARY_KEY": os.environ.get("DISBURSEMENT_PRIMARY_KEY"), +}) + +client.transfer(amount="600", mobile="256772123456", external_id="123456789", payee_note="dd", payer_message="dd", currency="EUR") + ``` +# Unit Tests +This library has a comprehensive test suite, which can be run using the `tox` command: +## to view all test environments + +```bash +$ tox -l +``` +## to run the tests for Python 2.7 + +```bash +$ tox -e py27-cover +``` + +## to run the tests for Python 3.4 + +```bash +$ tox -e py34-cover +``` +Thank you. From 243e7223670864a2f9b8c90783eabe08567f255a Mon Sep 17 00:00:00 2001 From: Ray Besiga Date: Mon, 18 Mar 2019 12:08:33 +0300 Subject: [PATCH 14/35] Typo fixes --- src/momoapi/__main__.py | 2 +- src/momoapi/cli.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/momoapi/__main__.py b/src/momoapi/__main__.py index 572ff8d..4663176 100644 --- a/src/momoapi/__main__.py +++ b/src/momoapi/__main__.py @@ -1,5 +1,5 @@ """ -Entrypoint module, in case you use `python -mmomoapi_python`. +Entrypoint module, in case you use `python -momoapi_python`. Why does this file exist, and why __main__? For more info, read: diff --git a/src/momoapi/cli.py b/src/momoapi/cli.py index c493f79..09c9b65 100644 --- a/src/momoapi/cli.py +++ b/src/momoapi/cli.py @@ -19,7 +19,6 @@ import uuid import click - import requests From 123f6c6300bef44c71bd5efde9655ea89ead6230 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Thu, 21 Mar 2019 13:26:07 +0300 Subject: [PATCH 15/35] [major] bump up version --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3f1ea7d..8c31f70 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,6 @@ matrix: sudo: false env: TOXENV=flake8,coveralls python: "3.6" - - os: linux dist: trusty sudo: false From e0bf19cd49c42248152502985a0481dda9313fc2 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Thu, 21 Mar 2019 13:34:17 +0300 Subject: [PATCH 16/35] [minor] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a3592aa..38905a4 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ def read(*names, **kwargs): setup( name='momoapi', - version='0.1.0', + version='1.0.1', license='MIT license', description='Python wrapper for the MTN MoMo API.', long_description='%s\n%s' % ( From 114d66966c90d3d3c9aa4406c99ba952c0721829 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Tue, 26 Mar 2019 14:38:18 +0300 Subject: [PATCH 17/35] [major] rename momoapi to mtnmomo --- .travis.yml | 69 ++++++++--------- CONTRIBUTING.md | 90 ++++++++++++++++++++++ README.md | 20 ++--- setup.py | 6 +- src/{momoapi => mtnmomo}/__init__.py | 0 src/{momoapi => mtnmomo}/__main__.py | 4 +- src/{momoapi => mtnmomo}/cli.py | 0 src/{momoapi => mtnmomo}/client.py | 0 src/{momoapi => mtnmomo}/collection.py | 0 src/{momoapi => mtnmomo}/config.py | 0 src/{momoapi => mtnmomo}/disbursement.py | 0 src/{momoapi => mtnmomo}/errors.py | 0 src/{momoapi => mtnmomo}/remittance.py | 0 src/{momoapi => mtnmomo}/utils.py | 0 tests/integration/features/sandbox.feature | 4 +- tests/integration/test_collection.py | 2 +- tests/integration/test_disbursement.py | 4 +- tests/integration/test_remittance.py | 5 +- tests/integration/test_sandbox.py | 4 +- tests/unitests/test_collection.py | 7 +- tests/unitests/test_disbursement.py | 6 +- tests/unitests/test_remittance.py | 7 +- tox.ini | 2 +- 23 files changed, 161 insertions(+), 69 deletions(-) create mode 100644 CONTRIBUTING.md rename src/{momoapi => mtnmomo}/__init__.py (100%) rename src/{momoapi => mtnmomo}/__main__.py (75%) rename src/{momoapi => mtnmomo}/cli.py (100%) rename src/{momoapi => mtnmomo}/client.py (100%) rename src/{momoapi => mtnmomo}/collection.py (100%) rename src/{momoapi => mtnmomo}/config.py (100%) rename src/{momoapi => mtnmomo}/disbursement.py (100%) rename src/{momoapi => mtnmomo}/errors.py (100%) rename src/{momoapi => mtnmomo}/remittance.py (100%) rename src/{momoapi => mtnmomo}/utils.py (100%) diff --git a/.travis.yml b/.travis.yml index 8c31f70..6c076d3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,50 +1,49 @@ language: python cache: directories: - - "$HOME/.cache/pip" - - "$HOME/.pyenv" + - "$HOME/.cache/pip" + - "$HOME/.pyenv" matrix: include: - - os: linux - dist: trusty - sudo: false - env: TOXENV=flake8,coveralls - python: "3.6" - - os: linux - dist: trusty - sudo: false - env: TOXENV=py27-cover - python: "2.7" - - os: linux - dist: trusty - sudo: false - python: "3.5" - env: TOXENV=py35-cover - - os: linux - dist: trusty - sudo: false - python: "3.6" - env: TOXENV=py36-cover - - os: linux - dist: xenial - sudo: required - python: "3.7" - env: TOXENV=py37-cover + - os: linux + dist: trusty + sudo: false + env: TOXENV=flake8,coveralls + python: '3.6' + - os: linux + dist: trusty + sudo: false + env: TOXENV=py27-cover + python: '2.7' + - os: linux + dist: trusty + sudo: false + python: '3.5' + env: TOXENV=py35-cover + - os: linux + dist: trusty + sudo: false + python: '3.6' + env: TOXENV=py36-cover + - os: linux + dist: xenial + sudo: required + python: '3.7' + env: TOXENV=py37-cover script: - - pip install tox - - tox +- pip install tox +- tox install: - - python setup.py install +- python setup.py install deploy: provider: pypi - user: mossplix - password: - secure: AjbgM0S84NnaPtkAI9JptI7lqtXU3HtcfNM8BLbSSxz/vUymPdU7vXCqwQM0UQWU4Ns5nC8kx6GOfXoikouOtQr7gLSlM7UQVwojsWGBUtVVi6D73ha2glhaaJ7Mf+9G3YMBYUHIUqtf7uVr9hKz5aADH0XIgRpHV4nNNamMduV4SvY4FZuvgQLaUrfwKtUY4t169vtHiReYWIOXEJHnA2PAsxNz99ZOVEvuxAX/Z4mFGkOuLtAvZnPxg3vinE8inQXcMNpLxwICFJM/sKAnMcICChHOErEIir4onOoodPVvVAvKfgb51wIM2q4YMyD8P3aTOILsWgk68EDJNO4w5T3R14eS9/ccRV7pW96lB/heaW92/vl5A59KJxuu6OEgk5gQaacFPv1IUccvc6U6vgHofGhjrsT1X8GI9ekl/M+gApRtL9ZYtT8lyRTIHBhayOKCITxFjivgUOcYZWiTFQCODSompgGdy4aoyIvDpmW2aztLcpr+modFWG/4GcJZxt14z7414LJvPvpyQj31KVEEo8jWdidJY/FNVXNCz8LS/3ri1Tz3p1Zof0sa4Zp3w6WpFxPeEXewyIS3b/R/Yv8w5n8f4byXtRGShvQe/i6sDy3oJAwiSyyjasYseE6WZpAajgk0dDODKZ0vLHkD1ZyHzkNOmqI7wo0pLdiyrKc= + user: sparkplug on: tags: true + password: + secure: AABEEwfWWBSb91c20ceFOUnriS9q+f+j/p0gSVsI8MvRES5lPsov9MGaiA6bf9hxLt9xIThpbY4AtwW4Y3U46tosPLW9kyeTgMINRAXmb/uU7h7hH9sTqmsZsSDwzJo/IdkqUszJmTlH/Km/6Ly3EDown/Hdd0FjTj/wJ3Z3PfkIR6GItz/h5QnIw8lx37PJhjE91keQqPxh6dvf8ewNz+f7QjxrFXy+O53pFwF2fpHmGTFa0MPBuRvBhi3fdn8dxKVZ5FI4tj6FBp8fPWoY5vOz6xRSDJ6W75IZPk24r7p4qH8oLRtCOcR+8kntftyrthaUacmFuWJWqlUSCU5p65qgJs4x3M1zzMutfCGPWHLnY/jhqpuAlu2QefBRc0vmVzs+Z/ieZSFHbjVuDrem9S1rt4HgCj7+VozQvLuz1LTWlOa/jh0phCK4+2BOeN20NNegjxNrLruz6kW9kneACvra+ppum0E8BRwknFkTmUPrLtnKd53h+lKZ7My7FRK8mjzXkj6RoOTjPoBZsDbvccXBc7ta8DlB07D9HGmYXwy2jCAAV3xdJy5Dwkhv2B7ck+g2xAHo/FdqmyLkhyh8BIZI6ixFbCGbwqN5sLFQ85+NDnfWA+HTh7UC1N6FNt4KBcnvx8Ho7cE/5piUPvhxw89nAH2beicnDz0ooWqKcn4= after_success: - - test $TRAVIS_BRANCH = "master" && .travis/semver.sh - +- test $TRAVIS_BRANCH = "master" && .travis/semver.sh env: global: secure: BfFLRnrlUwkU7FJui6XIrbN/nmxWkuDaGFrXLcqKSzWQmIwq8T/xIU2yje+wpjUx3mfErWXUgbf8EK/A83OwDgwDvZ4vlBojWA/aTKFUHtW7S7Ix1rTQbuxIzhEpBg2elIQzuKcH9hccGSzpcC6TqeuIWu/Soog7bofO6LT8SXqoRRPZnXgm07pKyJ/8wbUlJ34Ktoje5056zRsmEriFVf4/Qv6YciDzXtPbTRDCy+gXSAOFnr5y7d6Z8SQ3T5eki17X80Yn12nK0fymkTwbBi1F51dHkqSVYB/J6uBI5+K3c80gIw/sqhiHpk2WhfCB6ju/xAlHa9cSPKggJSPeX/v2Ujfk/7wDbH7mlBKsSisWKlQ/8oOd51aiXuHKP4yrj85URhxUNHoehYMg2JyQILTICFcSf4/AlbuEWFYtH34TBS+fF56kMxjHAdDCEX/i3VfAfAER3xsFTCTmQgvyZprPkekQOj5Cy1U+V5oPXRs94h0DqYbsMip1E1cbvevmjCzP3B2UZ/00mlp21a2OtNioxXaptgLsZjNS2kahc1yhDu6L3k2QZwxc58KdgL2vP+t7aQIcGO7gYimmn6GvXiDOjojXAVp0FPIHLnjNVHyqbQa6cma14sANKRZfyfuaGg3z763I7sdc5Su1iufZoD/+QRR4zrtyv3itfEvUBPo= diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..28de1ce --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,90 @@ +============ +Contributing +============ + +Contributions are welcome, and they are greatly appreciated! Every +little bit helps, and credit will always be given. + +Bug reports +=========== + +When `reporting a bug `_ please include: + + * Your operating system name and version. + * Any details about your local setup that might be helpful in troubleshooting. + * Detailed steps to reproduce the bug. + +Documentation improvements +========================== + +momoapi-python could always use more documentation, whether as part of the +official momoapi-python docs, in docstrings, or even on the web in blog posts, +articles, and such. + +Feature requests and feedback +============================= + +The best way to send feedback is to file an issue at https://github.com/mossplix/python-momoapi/issues. + +If you are proposing a feature: + +* Explain in detail how it would work. +* Keep the scope as narrow as possible, to make it easier to implement. +* Remember that this is a volunteer-driven project, and that code contributions are welcome :) + +Development +=========== + +To set up `python-momoapi` for local development: + +1. Fork `python-momoapi `_ + (look for the "Fork" button). +2. Clone your fork locally:: + + git clone git@github.com:your_name_here/python-momoapi.git + +3. Create a branch for local development:: + + git checkout -b name-of-your-bugfix-or-feature + + Now you can make your changes locally. + +4. When you're done making changes, run all the checks, doc builder and spell checker with `tox `_ one command:: + + tox + +5. Commit your changes and push your branch to GitHub:: + + git add . + git commit -m "Your detailed description of your changes." + git push origin name-of-your-bugfix-or-feature + +6. Submit a pull request through the GitHub website. + +Pull Request Guidelines +----------------------- + +If you need some code review or feedback while you're developing the code just make the pull request. + +For merging, you should: + +1. Include passing tests (run ``tox``) [1]_. +2. Update documentation when there's new API, functionality etc. +3. Add a note to ``CHANGELOG.rst`` about the changes. +4. Add yourself to ``AUTHORS.rst``. + +.. [1] If you don't have all the necessary python versions available locally you can rely on Travis - it will + `run the tests `_ for each change you add in the pull request. + + It will be slower though ... + +Tips +---- + +To run a subset of tests:: + + tox -e envname -- pytest -k test_myfeature + +To run all the test environments in *parallel* (you need to ``pip install detox``):: + + detox diff --git a/README.md b/README.md index 48471b8..9bb099b 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ MTN MoMo API Client for Python. Add the latest version of the library to your project using pip: ```bash - $ pip install momoapi + $ pip install mtnmomo ``` This library supports Python 2.7+ or Python 3.4+ (PyPy supported) @@ -26,7 +26,7 @@ Next, we need to get the `User ID` and `User Secret` and to do this we shall nee ```bash ## within the project, on the command line. In this example, our domain is akabbo.ug -$ momoapi +$ mtnmomo $ providerCallBackHost: https://akabbo.ug $ Ocp-Apim-Subscription-Key: f83xx8d8xx6749f19a26e2265aeadbcdeg ``` @@ -75,14 +75,14 @@ You will only need to configure the variables for the product(s) you will be usi The collections client can be created with the following paramaters. Note that the `COLLECTION_USER_ID` and `COLLECTION_API_SECRET` for production are provided on the MTN OVA dashboard; * `COLLECTION_PRIMARY_KEY`: Primary Key for the `Collection` product on the developer portal. -* `COLLECTION_USER_ID`: For sandbox, use the one generated with the `momoapi` command. -* `COLLECTION_API_SECRET`: For sandbox, use the one generated with the `momoapi` command. +* `COLLECTION_USER_ID`: For sandbox, use the one generated with the `mtnmomo` command. +* `COLLECTION_API_SECRET`: For sandbox, use the one generated with the `mtnmomo` command. You can create a collection client with the following: ```python import os -from momoapi.collection import Collection +from mtnmomo.collection import Collection client = Collection({ "COLLECTION_USER_ID": os.environ.get("COLLECTION_USER_ID"), @@ -105,7 +105,7 @@ client = Collection({ ```python import os -from momoapi.collection import Collection +from mtnmomo.collection import Collection client = Collection({ "COLLECTION_USER_ID": os.environ.get("COLLECTION_USER_ID"), @@ -122,14 +122,14 @@ client.requestToPay( The Disbursements client can be created with the following paramaters. Note that the `DISBURSEMENT_USER_ID` and `DISBURSEMENT_API_SECRET` for production are provided on the MTN OVA dashboard; * `DISBURSEMENT_PRIMARY_KEY`: Primary Key for the `Disbursement` product on the developer portal. -* `DISBURSEMENT_USER_ID`: For sandbox, use the one generated with the `momoapi` command. -* `DISBURSEMENT_API_SECRET`: For sandbox, use the one generated with the `momoapi` command. +* `DISBURSEMENT_USER_ID`: For sandbox, use the one generated with the `mtnmomo` command. +* `DISBURSEMENT_API_SECRET`: For sandbox, use the one generated with the `mtnmomo` command. You can create a disbursements client with the following ```python import os -from momoapi.collection import Disbursement +from mtnmomo.collection import Disbursement client = Disbursement({ "DISBURSEMENT_USER_ID": os.environ.get("DISBURSEMENT_USER_ID"), @@ -152,7 +152,7 @@ client = Disbursement({ ```python import os -from momoapi.collection import Disbursement +from mtnmomo.collection import Disbursement client = Disbursement({ "DISBURSEMENT_USER_ID": os.environ.get("DISBURSEMENT_USER_ID"), diff --git a/setup.py b/setup.py index 38905a4..3ef3772 100644 --- a/setup.py +++ b/setup.py @@ -24,8 +24,8 @@ def read(*names, **kwargs): setup( - name='momoapi', - version='1.0.1', + name='mtnmomo', + version='2.0.0', license='MIT license', description='Python wrapper for the MTN MoMo API.', long_description='%s\n%s' % ( @@ -90,7 +90,7 @@ def read(*names, **kwargs): 'pytest-bdd'], "dev": ["semver"]}, entry_points={ 'console_scripts': [ - 'momoapi = momoapi.cli:main', + 'mtnmomo = mtnmomo.cli:main', ] }, ) diff --git a/src/momoapi/__init__.py b/src/mtnmomo/__init__.py similarity index 100% rename from src/momoapi/__init__.py rename to src/mtnmomo/__init__.py diff --git a/src/momoapi/__main__.py b/src/mtnmomo/__main__.py similarity index 75% rename from src/momoapi/__main__.py rename to src/mtnmomo/__main__.py index 4663176..203d83d 100644 --- a/src/momoapi/__main__.py +++ b/src/mtnmomo/__main__.py @@ -1,5 +1,5 @@ """ -Entrypoint module, in case you use `python -momoapi_python`. +Entrypoint module, in case you use `python -mtnmomo_python`. Why does this file exist, and why __main__? For more info, read: @@ -8,7 +8,7 @@ - https://docs.python.org/2/using/cmdline.html#cmdoption-m - https://docs.python.org/3/using/cmdline.html#cmdoption-m """ -from momoapi.cli import main +from mtnmomo.cli import main if __name__ == "__main__": main() diff --git a/src/momoapi/cli.py b/src/mtnmomo/cli.py similarity index 100% rename from src/momoapi/cli.py rename to src/mtnmomo/cli.py diff --git a/src/momoapi/client.py b/src/mtnmomo/client.py similarity index 100% rename from src/momoapi/client.py rename to src/mtnmomo/client.py diff --git a/src/momoapi/collection.py b/src/mtnmomo/collection.py similarity index 100% rename from src/momoapi/collection.py rename to src/mtnmomo/collection.py diff --git a/src/momoapi/config.py b/src/mtnmomo/config.py similarity index 100% rename from src/momoapi/config.py rename to src/mtnmomo/config.py diff --git a/src/momoapi/disbursement.py b/src/mtnmomo/disbursement.py similarity index 100% rename from src/momoapi/disbursement.py rename to src/mtnmomo/disbursement.py diff --git a/src/momoapi/errors.py b/src/mtnmomo/errors.py similarity index 100% rename from src/momoapi/errors.py rename to src/mtnmomo/errors.py diff --git a/src/momoapi/remittance.py b/src/mtnmomo/remittance.py similarity index 100% rename from src/momoapi/remittance.py rename to src/mtnmomo/remittance.py diff --git a/src/momoapi/utils.py b/src/mtnmomo/utils.py similarity index 100% rename from src/momoapi/utils.py rename to src/mtnmomo/utils.py diff --git a/tests/integration/features/sandbox.feature b/tests/integration/features/sandbox.feature index fd28ca9..21cd727 100644 --- a/tests/integration/features/sandbox.feature +++ b/tests/integration/features/sandbox.feature @@ -2,14 +2,14 @@ Feature: As a new library User, I should be able to create an account on the MOM Scenario: Adding new account Given a user with the domain sparkpl.ug and subscription key 99e9cb10e8c04ea0b788334dc6346f13 - When I run the command "momoapi" + When I run the command "mtnmomo" And I fill in the "providerCallBackHost" with "sparkpl.ug" And I fill in the "Ocp-Apim-Subscription-Key" with "99e9cb10e8c04ea0b788334dc6346f13" Then I should get back the apiKey Scenario: Wrong subscription Key Given a user with the domain sparkpl.ug and subscription key 99e9cb10e8c04ea0b788334dc6346f13dg - When I run the command "momoapi" + When I run the command "mtnmomo" And I fill in the "providerCallBackHost" with "sparkpl.ug" And I fill in the "Ocp-Apim-Subscription-Key" with "f83xx8d8xx6749f19a26e2265aeadg" Then I should get the message "Access denied due to invalid subscription key" diff --git a/tests/integration/test_collection.py b/tests/integration/test_collection.py index 5e0a920..0863bbc 100644 --- a/tests/integration/test_collection.py +++ b/tests/integration/test_collection.py @@ -3,7 +3,7 @@ import os import pytest from click.testing import CliRunner -from momoapi.collection import Collection +from mtnmomo.collection import Collection pytest.globalDict = {} diff --git a/tests/integration/test_disbursement.py b/tests/integration/test_disbursement.py index a9db72d..0d88508 100644 --- a/tests/integration/test_disbursement.py +++ b/tests/integration/test_disbursement.py @@ -1,10 +1,10 @@ -from momoapi.disbursement import Disbursement from pytest_bdd import scenario, given, when, then, parsers import re import os import pytest from click.testing import CliRunner -from momoapi.cli import generateToken +from mtnmomo.cli import generateToken +from mtnmomo.disbursement import Disbursement pytest.globalDict = {} diff --git a/tests/integration/test_remittance.py b/tests/integration/test_remittance.py index caa14ca..3b7c50c 100644 --- a/tests/integration/test_remittance.py +++ b/tests/integration/test_remittance.py @@ -3,8 +3,9 @@ import os import pytest from click.testing import CliRunner -from momoapi.cli import generateToken -from momoapi.remittance import Remittance + +from mtnmomo.cli import generateToken +from mtnmomo.remittance import Remittance pytest.globalDict = {} diff --git a/tests/integration/test_sandbox.py b/tests/integration/test_sandbox.py index f47288c..af93243 100644 --- a/tests/integration/test_sandbox.py +++ b/tests/integration/test_sandbox.py @@ -2,7 +2,7 @@ import re import pytest from click.testing import CliRunner -from momoapi.cli import generateToken +from mtnmomo.cli import generateToken pytest.globalDict = {} @@ -21,7 +21,7 @@ def provisioning_user(domain, api_key): return -@when('I run the command "momoapi"') +@when('I run the command "mtnmomo"') def run_command(): return diff --git a/tests/unitests/test_collection.py b/tests/unitests/test_collection.py index 4af0a6c..9da520e 100644 --- a/tests/unitests/test_collection.py +++ b/tests/unitests/test_collection.py @@ -1,16 +1,17 @@ import unittest import pytest -from momoapi.client import MomoApi -from momoapi.collection import Collection import types try: from unittest import mock except ImportError: import mock + from requests import Request, Session from .utils import mocked_requests_get, mocked_requests_post, mocked_requests_session -from momoapi.errors import ValidationError +from mtnmomo.errors import ValidationError +from mtnmomo.client import MomoApi +from mtnmomo.collection import Collection class TestCollections(unittest.TestCase): diff --git a/tests/unitests/test_disbursement.py b/tests/unitests/test_disbursement.py index ff4e359..c384b77 100644 --- a/tests/unitests/test_disbursement.py +++ b/tests/unitests/test_disbursement.py @@ -1,7 +1,5 @@ import unittest import pytest -from momoapi.client import MomoApi -from momoapi.disbursement import Disbursement import types try: from unittest import mock @@ -10,7 +8,9 @@ from requests import Request, Session from .utils import mocked_requests_get, mocked_requests_post, mocked_requests_session -from momoapi.errors import ValidationError +from mtnmomo.errors import ValidationError +from mtnmomo.client import MomoApi +from mtnmomo.disbursement import Disbursement class TestDisbursements(unittest.TestCase): diff --git a/tests/unitests/test_remittance.py b/tests/unitests/test_remittance.py index 4cba3a1..978e8a1 100644 --- a/tests/unitests/test_remittance.py +++ b/tests/unitests/test_remittance.py @@ -1,7 +1,5 @@ import unittest import pytest -from momoapi.client import MomoApi -from momoapi.remittance import Remittance import types try: from unittest import mock @@ -9,8 +7,11 @@ import mock from requests import Request, Session +from mtnmomo.client import MomoApi +from mtnmomo.remittance import Remittance +from mtnmomo.errors import ValidationError + from .utils import mocked_requests_get, mocked_requests_post, mocked_requests_session -from momoapi.errors import ValidationError class TestRemittences(unittest.TestCase): diff --git a/tox.ini b/tox.ini index 34ca23d..8583991 100644 --- a/tox.ini +++ b/tox.ini @@ -180,7 +180,7 @@ deps = basepython = python3.7 commands = - flake8 {posargs:src/momoapi} + flake8 {posargs:src/mtnmomo} [flake8] From b86a472d889a0c6d831bd244f5667e3debffa1fc Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Tue, 26 Mar 2019 15:52:18 +0300 Subject: [PATCH 18/35] [minor] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9bb099b..f5e35dc 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ MTN MoMo API Client for Python. [![Build Status](https://travis-ci.com/sparkplug/momoapi-python.svg?branch=master)](https://travis-ci.com/sparkplug/momoapi-node) [![Latest Version](https://img.shields.io/pypi/v/tox-travis.svg)](https://badge.fury.io/js/mtn-momo) -[![Coverage Status](https://coveralls.io/repos/github/sparkplug/momoapi-python/badge.svg?branch=validations)](https://coveralls.io/github/sparkplug/momoapi-python?branch=validations) +[![Coverage Status](https://coveralls.io/repos/github/sparkplug/momoapi-python/badge.svg?branch=validations)](https://coveralls.io/github/sparkplug/momoapi-python?branch=master) [![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/momo-api-developers/) From d67798760e7dc7e929ceefbd4f1486a417ff6e70 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Tue, 26 Mar 2019 15:54:31 +0300 Subject: [PATCH 19/35] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f5e35dc..4ce45fc 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ MTN MoMo API Client for Python. [![Build Status](https://travis-ci.com/sparkplug/momoapi-python.svg?branch=master)](https://travis-ci.com/sparkplug/momoapi-node) [![Latest Version](https://img.shields.io/pypi/v/tox-travis.svg)](https://badge.fury.io/js/mtn-momo) -[![Coverage Status](https://coveralls.io/repos/github/sparkplug/momoapi-python/badge.svg?branch=validations)](https://coveralls.io/github/sparkplug/momoapi-python?branch=master) +[![Coverage Status](https://coveralls.io/repos/github/sparkplug/momoapi-python/badge.svg?branch=master)](https://coveralls.io/github/sparkplug/momoapi-python?branch=master) [![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/momo-api-developers/) From c0659fcca8b9ba7bb727d0944d90da2ed40caae2 Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Tue, 26 Mar 2019 15:58:52 +0300 Subject: [PATCH 20/35] [minor] fix pypi markdown rendering --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 38905a4..fc29a32 100644 --- a/setup.py +++ b/setup.py @@ -32,6 +32,7 @@ def read(*names, **kwargs): re.compile('^.. start-badges.*^.. end-badges', re.M | re.S).sub('', read('README.md')), re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.md')) ), + long_description_content_type='text/markdown', author='Sparkplug', author_email='mugisha@sparkpl.ug', url='https://github.com/sparkplug/momoapi-python', From ba2e10451034df8aa49d45cab74833166a8c5a7d Mon Sep 17 00:00:00 2001 From: Moses Mugisha Date: Tue, 26 Mar 2019 16:04:31 +0300 Subject: [PATCH 21/35] [patch] configure markdown for pypi --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 27bd177..812252c 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ def read(*names, **kwargs): setup( name='mtnmomo', - version='2.0.0', + version='3.0.1', license='MIT license', description='Python wrapper for the MTN MoMo API.', long_description='%s\n%s' % ( From d1fc87b8dba5789d5811f69d71a7331cbdf23fa5 Mon Sep 17 00:00:00 2001 From: Ray Besiga Date: Thu, 28 Mar 2019 12:19:57 +0300 Subject: [PATCH 22/35] Move testing documentation to contributing guide --- README.md | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/README.md b/README.md index 4ce45fc..2db2bc6 100644 --- a/README.md +++ b/README.md @@ -164,25 +164,4 @@ client.transfer(amount="600", mobile="256772123456", external_id="123456789", pa ``` -# Unit Tests - -This library has a comprehensive test suite, which can be run using the `tox` command: - -## to view all test environments - -```bash -$ tox -l -``` -## to run the tests for Python 2.7 - -```bash -$ tox -e py27-cover -``` - -## to run the tests for Python 3.4 - -```bash -$ tox -e py34-cover -``` - Thank you. From 9a933f79ff8da8cd8f2b4fc908de4d9145a5f7d0 Mon Sep 17 00:00:00 2001 From: Ray Besiga Date: Thu, 28 Mar 2019 12:20:17 +0300 Subject: [PATCH 23/35] Add me to authors --- AUTHORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.md b/AUTHORS.md index 71ef76b..0da110b 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -3,3 +3,4 @@ Authors ======= * Moses Mugisha - http://sparkpl.ug +* Ray Besiga \ No newline at end of file From f22242ac96dd4b3d99f1b65d3272a5980568dfbd Mon Sep 17 00:00:00 2001 From: Ray Besiga Date: Thu, 28 Mar 2019 12:20:43 +0300 Subject: [PATCH 24/35] Add resources file for future articles and links --- RESOURCES.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 RESOURCES.md diff --git a/RESOURCES.md b/RESOURCES.md new file mode 100644 index 0000000..e2899d7 --- /dev/null +++ b/RESOURCES.md @@ -0,0 +1,11 @@ +# Resources + +Here, we keep a list of resources for use of the library + +## Official Docs + +* [Developer Portal](https://momodeveloper.mtn.com/api-documentation) + +## Articles + + From c10d1af23869d774ec6a79541aaf818eab53f3b1 Mon Sep 17 00:00:00 2001 From: Ray Besiga Date: Thu, 28 Mar 2019 12:21:35 +0300 Subject: [PATCH 25/35] Update contributing guide --- CONTRIBUTING.md | 154 ++++++++++++++++++++++++++++++------------------ 1 file changed, 97 insertions(+), 57 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 28de1ce..35792d5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,90 +1,130 @@ -============ -Contributing -============ +# Contributing -Contributions are welcome, and they are greatly appreciated! Every -little bit helps, and credit will always be given. +First off, thank you for considering contributing to this Python MTN MoMo Library. It's people like you that make it such a great tool. Contributions are welcome, and they are greatly appreciated! -Bug reports -=========== +## Where do I go from here? -When `reporting a bug `_ please include: +If you've noticed a bug or have a question that doesn't belong on the +[Spectrum](https://spectrum.chat/momo-api-developers/) or [Stack Overflow](https://stackoverflow.com/), [search the issue tracker](https://github.com/sparkplug/momoapi-python/issues) to see if +someone else in the community has already created a ticket. If not, go ahead and +[make one](https://github.com/sparkplug/momoapi-python/issues/new/choose)! - * Your operating system name and version. - * Any details about your local setup that might be helpful in troubleshooting. - * Detailed steps to reproduce the bug. -Documentation improvements -========================== -momoapi-python could always use more documentation, whether as part of the -official momoapi-python docs, in docstrings, or even on the web in blog posts, -articles, and such. +## Fork & create a branch -Feature requests and feedback -============================= +If there is something you think you can fix, then fork the [repo](https://github.com/sparkplug/momoapi-python) and create a branch with a descriptive name. -The best way to send feedback is to file an issue at https://github.com/mossplix/python-momoapi/issues. +A good branch name would be (where issue #32 is the ticket you're working on): -If you are proposing a feature: +```sh +git checkout -b 32-add-swahili-translations +``` -* Explain in detail how it would work. -* Keep the scope as narrow as possible, to make it easier to implement. -* Remember that this is a volunteer-driven project, and that code contributions are welcome :) +## Get the test suite running -Development -=========== +This library has a comprehensive test suite, which can be run using the `tox` command: -To set up `python-momoapi` for local development: +To view all test environments -1. Fork `python-momoapi `_ - (look for the "Fork" button). -2. Clone your fork locally:: +```sh +$ tox -l +``` +To run the tests for Python 2.7 - git clone git@github.com:your_name_here/python-momoapi.git +```sh +$ tox -e py27-cover +``` -3. Create a branch for local development:: +To run the tests for Python 3.4 - git checkout -b name-of-your-bugfix-or-feature +```sh +$ tox -e py34-cover +``` - Now you can make your changes locally. +To run a subset of tests:: -4. When you're done making changes, run all the checks, doc builder and spell checker with `tox `_ one command:: +```sh +tox -e envname -- pytest -k test_myfeature +``` - tox +To run all the test environments in *parallel*, you need to `pip install detox`: -5. Commit your changes and push your branch to GitHub:: +```sh +detox +``` - git add . - git commit -m "Your detailed description of your changes." - git push origin name-of-your-bugfix-or-feature +### Did you find a bug? -6. Submit a pull request through the GitHub website. +* **Ensure the bug was not already reported** by [searching all issues](https://github.com/sparkplug/momoapi-python/issues). -Pull Request Guidelines ------------------------ +* If you're unable to find an open issue addressing the problem, + [open a new one](https://github.com/sparkplug/momoapi-python/issues/new/choose). Be sure to include a **title and clear + description**, as much relevant information as possible, and a **code sample** + or an **executable test case** demonstrating the expected behavior that is not + occurring. -If you need some code review or feedback while you're developing the code just make the pull request. +* If possible, use the relevant bug report templates to create the issue. + Make the necessary changes to demonstrate the issue, and **paste the content into the + issue description** -For merging, you should: +### Implement your fix or feature -1. Include passing tests (run ``tox``) [1]_. -2. Update documentation when there's new API, functionality etc. -3. Add a note to ``CHANGELOG.rst`` about the changes. -4. Add yourself to ``AUTHORS.rst``. +At this point, you're ready to make your changes! Feel free to ask for help; +everyone is a beginner at first :smile_cat: -.. [1] If you don't have all the necessary python versions available locally you can rely on Travis - it will - `run the tests `_ for each change you add in the pull request. +If you are proposing a feature: - It will be slower though ... +* Explain in detail how it would work. +* Keep the scope as narrow as possible, to make it easier to implement. +* Remember that this is a volunteer-driven project, and that code contributions are welcome :) -Tips ----- +If you would like to send us feedback, simply [file an issue](https://github.com/sparkplug/momoapi-python/issues/new/choose). -To run a subset of tests:: +## Local Development + +To set up `python-momoapi` for local development: + +1. Fork the repo. Look for the "Fork" button in the Github UI. +2. Clone your fork locally: + +```sh +git clone https://github.com/your_name_here/momoapi-python.git +``` + +3. Create a branch for local development: +```sh +git checkout -b name-of-your-bugfix-or-feature +``` + +Now you can make your changes locally. + +4. When you're done making changes, run all the checks, doc builder and spell checker with `tox`. +```sh +tox +``` +Make sure Tox is installed by following the instructions [here](http://tox.readthedocs.io/en/latest/install.html) + +5. Commit your changes and push your branch to GitHub:: + +```sh +git add . +git commit -m "Your detailed description of your changes." +git push origin name-of-your-bugfix-or-feature +``` + +6. Submit a pull request through the GitHub website. + +## Pull Request Guidelines + +Before you make a Pull Request, make sure of the following: + +1. Make sure your tests pass. Run `tox` beforehand. +2. Update documentation where necessary. +3. Note changes to `CHANGELOG.md`. +4. Add yourself to `AUTHORS.md`. - tox -e envname -- pytest -k test_myfeature +## Improvements -To run all the test environments in *parallel* (you need to ``pip install detox``):: +This library could always use more documentation, whether as part of the official docs, in docstrings, or even in blog posts and articles. We look forward to add them to our RESOURCES file. - detox From 8ed6eae218f2883e151d9e796d10261134a4049f Mon Sep 17 00:00:00 2001 From: Ray Besiga Date: Thu, 28 Mar 2019 12:23:52 +0300 Subject: [PATCH 26/35] Add bugs and fixes header --- CONTRIBUTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 35792d5..f3a5c55 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,6 +54,8 @@ To run all the test environments in *parallel*, you need to `pip install detox`: detox ``` +## Bugs and Fixes + ### Did you find a bug? * **Ensure the bug was not already reported** by [searching all issues](https://github.com/sparkplug/momoapi-python/issues). From db36b67d7c683471cc23c0e5d2a7456408d8d631 Mon Sep 17 00:00:00 2001 From: Ray Besiga Date: Thu, 28 Mar 2019 12:40:20 +0300 Subject: [PATCH 27/35] Update guide for PR and maintainers --- CONTRIBUTING.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f3a5c55..be573c9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -119,6 +119,74 @@ git push origin name-of-your-bugfix-or-feature ## Pull Request Guidelines +### Make a Pull Request + +At this point, you should switch back to your master branch and make sure it's +up to date with `momoapi-python`'s master branch: + +```sh +git remote add upstream https://github.com/sparkplug/momoapi-python.git +git checkout master +git pull upstream master +``` + +Then update your feature branch from your local copy of master, and push it! + +```sh +git checkout 32-add-swahili-translations +git rebase master +git push --set-upstream origin 32-add-swahili-translations +``` + +Finally, go to GitHub and make a Pull Request :D + +TravisCI will run our test suite against all supported Python versions. We care +about quality, so your PR won't be merged until all tests pass. It's unlikely, +but it's possible that your changes pass tests in one Python version but fail in +another. In that case, you'll have to setup your development environment to use your Python version, and investigate what's going on! + +### Keeping your Pull Request updated + +If a maintainer asks you to "rebase" your PR, they're saying that a lot of code has changed, and that you need to update your branch so it's easier to merge. + +To learn more about rebasing in Git, there are a lot of [good](https://www.atlassian.com/git/tutorials/rewriting-history/git-rebase) [resources](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) but here's the suggested workflow: + +```sh +git checkout 32-add-swahili-translations +git pull --rebase upstream master +git push --force-with-lease 32-add-swahili-translations +``` + +### Merging a PR (maintainers only) + +A PR can only be merged into master by a maintainer if: + +* It is passing CI. +* It has been approved by at least one maintainers. If it was a maintainer who opened the PR, only one extra approval is needed. +* It has no requested changes. +* It is up to date with current master. + +Any maintainer is allowed to merge a PR if all of these conditions are met. + +### Shipping a release (maintainers only) + +Maintainers need to do the following to push out a release: + +* Make sure all pull requests are in and that changelog is current +* Update version and changelog with new version number using semver +* If it's not a patch level release, create a stable branch for that release, + otherwise switch to the stable branch corresponding to the patch release you + want to ship: + + ```sh + git checkout master + git fetch momoapi-python + git rebase momoapi-python/master + # If the release is 2.1.x then this should be: 2-1-stable + git checkout -b N-N-stable + git push momoapi-python N-N-stable:N-N-stable + ``` + Before you make a Pull Request, make sure of the following: 1. Make sure your tests pass. Run `tox` beforehand. From 439c65aa6bf538fd006424c91ccb4fbac64d49df Mon Sep 17 00:00:00 2001 From: Ray Besiga Date: Tue, 7 May 2019 12:44:03 +0300 Subject: [PATCH 28/35] Update README --- README.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2db2bc6..3394eb1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,15 @@ -# MTN Mobile Money API Client +# MTN MoMo API Python Client + +Power your apps with our MTN MoMo API + +
+ Join our active, engaged community:
+ Website + | + Spectrum +

+
-MTN MoMo API Client for Python. [![Build Status](https://travis-ci.com/sparkplug/momoapi-python.svg?branch=master)](https://travis-ci.com/sparkplug/momoapi-node) [![Latest Version](https://img.shields.io/pypi/v/tox-travis.svg)](https://badge.fury.io/js/mtn-momo) @@ -8,7 +17,9 @@ MTN MoMo API Client for Python. [![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/momo-api-developers/) -# Installing +# Usage + +## Installation Add the latest version of the library to your project using pip: From 8b93092f03594dfa3012e9d7c73cf7f15e075122 Mon Sep 17 00:00:00 2001 From: Ray Besiga Date: Tue, 7 May 2019 13:07:12 +0300 Subject: [PATCH 29/35] Update Authors --- AUTHORS.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index 0da110b..0343a4e 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -2,5 +2,7 @@ Authors ======= -* Moses Mugisha - http://sparkpl.ug -* Ray Besiga \ No newline at end of file +* Moses Mugisha +* Ray Besiga + +For [Sparkplug](http://sparkpl.ug) \ No newline at end of file From a5fa464e36ff5c34e163722e57ffa1de53acfaac Mon Sep 17 00:00:00 2001 From: Phillip Ahereza Date: Sat, 22 Jun 2019 08:32:06 +0300 Subject: [PATCH 30/35] fixed typo in config.py --- src/mtnmomo/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mtnmomo/config.py b/src/mtnmomo/config.py index 7376814..60c6912 100644 --- a/src/mtnmomo/config.py +++ b/src/mtnmomo/config.py @@ -18,7 +18,7 @@ def __init__(self, conf): REMITTANCE_USER_ID: os.environ.get("REMITTANCE_USER_ID"), REMITTANCE_API_SECRET: os.environ.get("REMITTANCE_API_SECRET"), - REMITTANCE_PRIMARY_KEY: os.envieon.get("REMITTANCE_PRIMARY_KEY") + REMITTANCE_PRIMARY_KEY: os.environ.get("REMITTANCE_PRIMARY_KEY") DISBURSEMENT_USER_ID: os.environ.get("DISBURSEMENT_USER_ID"), DISBURSEMENT_API_SECRET: os.environ.get("DISBURSEMENTS_API_SECRET"), From 9728556bd6beb55b407d4c694dc048bec33ff72c Mon Sep 17 00:00:00 2001 From: okellogabrielinnocent Date: Tue, 10 Sep 2019 13:57:51 +0300 Subject: [PATCH 31/35] update README.md to match implemented function - change getTransaction to getTransactionStatus on readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3394eb1..fc92547 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ client = Collection({ 1. `requestToPay`: This operation is used to request a payment from a consumer (Payer). The payer will be asked to authorize the payment. The transaction is executed once the payer has authorized the payment. The transaction will be in status PENDING until it is authorized or declined by the payer or it is timed out by the system. Status of the transaction can be validated by using `getTransactionStatus`. -2. `getTransaction`: Retrieve transaction information using the `transactionId` returned by `requestToPay`. You can invoke it at intervals until the transaction fails or succeeds. If the transaction has failed, it will throw an appropriate error. +2. `getTransactionStatus`: Retrieve transaction information using the `transactionId` returned by `requestToPay`. You can invoke it at intervals until the transaction fails or succeeds. If the transaction has failed, it will throw an appropriate error. 3. `getBalance`: Get the balance of the account. From 70100d9e280291e98f3d084e1ebf12f9ac4e446c Mon Sep 17 00:00:00 2001 From: arthurarty Date: Tue, 14 Jan 2020 12:09:45 +0300 Subject: [PATCH 32/35] ch(README) - Correct importation in sample code. - Change import for Disbursement class in sample code. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3394eb1..8e192a6 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ You can create a disbursements client with the following ```python import os -from mtnmomo.collection import Disbursement +from mtnmomo.disbursement import Disbursement client = Disbursement({ "DISBURSEMENT_USER_ID": os.environ.get("DISBURSEMENT_USER_ID"), @@ -163,7 +163,7 @@ client = Disbursement({ ```python import os -from mtnmomo.collection import Disbursement +from mtnmomo.disbursement import Disbursement client = Disbursement({ "DISBURSEMENT_USER_ID": os.environ.get("DISBURSEMENT_USER_ID"), From c978d594131fb770905d972d97113282d9510c76 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Wed, 27 May 2020 13:28:42 +0300 Subject: [PATCH 33/35] remove print statements --- src/mtnmomo/client.py | 3 --- src/mtnmomo/disbursement.py | 1 - 2 files changed, 4 deletions(-) diff --git a/src/mtnmomo/client.py b/src/mtnmomo/client.py index fdeee8f..2740009 100644 --- a/src/mtnmomo/client.py +++ b/src/mtnmomo/client.py @@ -99,7 +99,6 @@ def request(self, method, url, headers, post_data=None): def interpret_response(self, resp): rcode = resp.status_code rheaders = resp.headers - print(resp) try: rbody = resp.json() @@ -163,7 +162,6 @@ def getTransactionStatus( "Ocp-Apim-Subscription-Key": subscription_key } _url = self.config.baseUrl + url + transaction_id - print(_url) res = self.request("GET", _url, headers) return res.json() @@ -192,5 +190,4 @@ def generateToken( def close(self): if self._session is not None: - print("closing!") self._session.close() diff --git a/src/mtnmomo/disbursement.py b/src/mtnmomo/disbursement.py index 6d71deb..4472b95 100644 --- a/src/mtnmomo/disbursement.py +++ b/src/mtnmomo/disbursement.py @@ -58,6 +58,5 @@ def transfer( if kwargs.get("callback_url"): headers["X-Callback-Url"] = kwargs.get("callback_url") url = super(Disbursement, self).config.baseUrl + "/disbursement/v1_0/transfer" - print(url) self.request("POST", url, headers, data) return {"transaction_ref": ref} From 06e2abd2ef5c4e8377c0337182ea93a3254781dc Mon Sep 17 00:00:00 2001 From: Peter Thaleikis Date: Wed, 25 Nov 2020 16:50:35 +0400 Subject: [PATCH 34/35] Typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 36deeef..1a89e95 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ These are the credentials we shall use for the sandbox environment. In productio Before we can fully utilize the library, we need to specify global configurations. The global configuration must contain the following: * `BASE_URL`: An optional base url to the MTN Momo API. By default the staging base url will be used -* `ENVIRONMENT`: Optional enviroment, either "sandbox" or "production". Default is 'sandbox' +* `ENVIRONMENT`: Optional environment, either "sandbox" or "production". Default is 'sandbox' * `CALLBACK_HOST`: The domain where you webhooks urls are hosted. This is mandatory. Once you have specified the global variables, you can now provide the product-specific variables. Each MoMo API product requires its own authentication details i.e its own `Subscription Key`, `User ID` and `User Secret`, also sometimes refered to as the `API Secret`. As such, we have to configure subscription keys for each product you will be using. From 5914667d6e393d2879315190a1862fa715e9567f Mon Sep 17 00:00:00 2001 From: Ray Besiga Date: Mon, 14 Jul 2025 14:34:56 +0300 Subject: [PATCH 35/35] Update README.md --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1a89e95..5e46eed 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,21 @@ # MTN MoMo API Python Client -Power your apps with our MTN MoMo API + +**🛑 This repository is no longer actively maintained.** + +As of July 14, 2025, this project is no longer under active development. This means: +* No new features will be added. +* Bugs will not be fixed. +* Pull requests will not be reviewed or merged. +* Issues will not be addressed. + +We appreciate your interest and contributions. +**Thank you.** + +
+ +Power your apps with our MTN MoMo API
Join our active, engaged community:
Website