-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from gannetson/new-api
New api
- Loading branch information
Showing
3 changed files
with
243 additions
and
87 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,100 +1,258 @@ | ||
import time | ||
import suds | ||
import hashlib | ||
import base64 | ||
|
||
import hashlib | ||
import logging | ||
import requests | ||
from datetime import datetime | ||
|
||
logging.basicConfig(level=logging.WARN) | ||
logging.getLogger('suds').setLevel(logging.WARN) | ||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class PaymentService(object): | ||
|
||
wsdl = 'https://www.safaricom.co.ke/mpesa_online/lnmo_checkout_server.php?wsdl' | ||
test_server = "https://sandbox.safaricom.co.ke" | ||
live_server = "https://api.safaricom.co.ke" | ||
|
||
demo = False | ||
demo_merchant_id = '898998' | ||
demo_timestamp = '20160510161908' | ||
demo_password = 'ZmRmZDYwYzIzZDQxZDc5ODYwMTIzYjUxNzNkZDMwMDRjNGRkZTY2ZDQ3ZTI0YjVjODc4ZTExNTNjMDA1YTcwNw==' | ||
server = live_server | ||
|
||
def __init__(self, merchant_id='demo', merchant_passkey='demo'): | ||
if merchant_id == 'demo': | ||
self.demo = True | ||
self.merchant_id = self.demo_merchant_id | ||
else: | ||
self.merchant_id = merchant_id | ||
access_token_path = '/oauth/v1/generate?grant_type=client_credentials' | ||
process_request_path = '/mpesa/stkpush/v1/processrequest' | ||
query_request_path = '/mpesa/stkpushquery/v1/query' | ||
transaction_status_path = '/mpesa/transactionstatus/v1/query' | ||
simulate_transaction_path = '/mpesa/c2b/v1/simulate' | ||
|
||
self.merchant_passkey = merchant_passkey | ||
self.client = suds.client.Client(self.wsdl) | ||
balance_request_path = '/mpesa/accountbalance/v1/query' | ||
|
||
# Make sure locations for methods are correctly set | ||
self.client.service.processCheckOut.method.location = self.wsdl | ||
self.client.service.confirmTransaction.method.location = self.wsdl | ||
self.client.service.LNMOResult.method.location = self.wsdl | ||
self.client.service.transactionStatusQuery.method.location = self.wsdl | ||
test_shortcode = '174379' | ||
test_passphrase = 'bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919' | ||
|
||
live = False | ||
|
||
access_token = None | ||
|
||
code_mapping = { | ||
'1037': 'timout', | ||
'1001': 'started', | ||
} | ||
|
||
def __init__(self, consumer_key, consumer_password, shortcode=None, passphrase=None, live=False, debug=False): | ||
self.consumer_key = consumer_key | ||
self.consumer_password = consumer_password | ||
self.live = live | ||
self.debug = debug | ||
if debug: | ||
logger.debug('Initiated M-Pesa Payment Service') | ||
if not live: | ||
self.server = self.test_server | ||
self.shortcode = self.test_shortcode | ||
self.passphrase = self.test_passphrase | ||
else: | ||
self.shortcode = shortcode | ||
self.passphrase = passphrase | ||
|
||
def get_access_token(self): | ||
url = self.server + self.access_token_path | ||
response = requests.get(url, auth=(self.consumer_key, self.consumer_password)) | ||
if response.status_code == 200: | ||
data = response.json() | ||
self.access_token = data['access_token'] | ||
return self.access_token | ||
else: | ||
return None | ||
|
||
def _generate_password(self, timestamp): | ||
if self.demo: | ||
return self.demo_password | ||
key = "{0}{1}{2}".format(self.merchant_id, self.merchant_passkey, timestamp) | ||
return base64.b64encode(hashlib.sha256(key).hexdigest().upper()) | ||
|
||
def _request_header(self, timestamp): | ||
data = self.client.factory.create('tns:CheckOutHeader') | ||
data.MERCHANT_ID = self.merchant_id | ||
data.TIMESTAMP = timestamp | ||
data.PASSWORD = self._generate_password(timestamp=timestamp) | ||
return data | ||
|
||
def confirm_transaction(self, transaction_id): | ||
if self.demo: | ||
timestamp = self.demo_timestamp | ||
string = self.shortcode + self.passphrase + timestamp | ||
return base64.b64encode(string) | ||
|
||
def process_request(self, phone_number=None, amount=None, | ||
callback_url=None, reference="", description=""): | ||
access_token = self.get_access_token() | ||
if not access_token: | ||
return { | ||
'response': {}, | ||
'status': 'failed', | ||
'error': 'Could not get access token', | ||
'request_id': '' | ||
} | ||
|
||
headers = {"Authorization": "Bearer %s" % access_token} | ||
timestamp = datetime.now().strftime('%Y%m%d%H%M%S') | ||
request = { | ||
"BusinessShortCode": self.shortcode, | ||
"Password": self._generate_password(timestamp), | ||
"Timestamp": timestamp, | ||
"TransactionType": "CustomerPayBillOnline", | ||
"Amount": str(int(amount)), | ||
"PartyA": phone_number, | ||
"PartyB": str(self.shortcode), | ||
"PhoneNumber": phone_number, | ||
"CallBackURL": callback_url, | ||
"AccountReference": reference, | ||
"TransactionDesc": description | ||
} | ||
url = self.server + self.process_request_path | ||
response = requests.post(url, json=request, headers=headers) | ||
data = response.json() | ||
if self.debug: | ||
logging.debug('URL: {}'.format(url)) | ||
logging.debug('Request payload:\n {}'.format(request)) | ||
logging.debug('Response {}:\n {}'.format(response.status_code, data)) | ||
|
||
if response.status_code == 200: | ||
return { | ||
'response': data, | ||
'status': 'started', | ||
'request_id': data['CheckoutRequestID'] | ||
} | ||
else: | ||
timestamp = time.time() | ||
return { | ||
'response': data, | ||
'status': 'failed', | ||
'error': data['errorMessage'], | ||
'request_id': data['requestId'] | ||
} | ||
|
||
def query_request(self, request_id): | ||
timestamp = datetime.now().strftime('%Y%m%d%H%M%S') | ||
access_token = self.get_access_token() | ||
if not access_token: | ||
return { | ||
'response': {}, | ||
'status': 'failed', | ||
'error': 'Could not get access token', | ||
'request_id': '' | ||
} | ||
|
||
header = self._request_header(timestamp=timestamp) | ||
self.client.set_options(soapheaders=header) | ||
headers = {"Authorization": "Bearer %s" % access_token} | ||
request = { | ||
"BusinessShortCode": self.shortcode, | ||
"Password": self._generate_password(timestamp), | ||
"Timestamp": timestamp, | ||
"CheckoutRequestID": request_id | ||
} | ||
url = self.server + self.simulate_transaction_path | ||
response = requests.post(url, json=request, headers=headers) | ||
|
||
response = self.client.service.confirmTransaction( | ||
transaction_id | ||
) | ||
return response | ||
data = response.json() | ||
if self.debug: | ||
logging.debug('URL: {}'.format(url)) | ||
logging.debug('Request payload:\n {}'.format(request)) | ||
logging.debug('Response {}:\n {}'.format(response.status_code, data)) | ||
|
||
def transaction_status_query(self, transaction_id): | ||
if self.demo: | ||
timestamp = self.demo_timestamp | ||
if response.status_code == 200: | ||
status = 'started' | ||
if data['ResultCode'] == '1001': | ||
status = 'settled' | ||
return { | ||
'response': data, | ||
'status': status, | ||
} | ||
else: | ||
timestamp = time.time() | ||
|
||
header = self._request_header(timestamp=timestamp) | ||
self.client.set_options(soapheaders=header) | ||
|
||
response = self.client.service.transactionStatusQuery( | ||
transaction_id | ||
) | ||
return response | ||
|
||
def checkout_request(self, merchant_transaction_id=None, reference_id=None, | ||
msisdn=None, amount=None, enc_params=None, | ||
callback_url=None): | ||
callback_method = 'xml' | ||
if self.demo: | ||
timestamp = self.demo_timestamp | ||
status = 'started' | ||
|
||
return { | ||
'response': data, | ||
'status': status, | ||
'error': data['errorMessage'], | ||
} | ||
|
||
def transaction_status_request(self, phone_number, reference): | ||
timestamp = datetime.now().strftime('%Y%m%d%H%M%S') | ||
access_token = self.get_access_token() | ||
|
||
if not access_token: | ||
return { | ||
'response': {}, | ||
'status': 'failed', | ||
'error': 'Could not get access token', | ||
'request_id': '' | ||
} | ||
|
||
timeout_url = 'https://api.twende.co.ke/payments/update-timeout' | ||
result_url = 'https://api.twende.co.ke/payments/update-result' | ||
|
||
headers = {"Authorization": "Bearer %s" % access_token} | ||
request = { | ||
"CommandID": 'TransactionStatusQuery', | ||
"PartyA": phone_number, | ||
"IdentifierType": 'MSISDN', | ||
"Remarks": "Payment for Twende ride", | ||
"Initiator": self.shortcode, | ||
"SecurityCredential": '', | ||
"QueueTimeOutURL": timeout_url, | ||
"ResultURL": result_url, | ||
"TransactionID": reference, | ||
"OriginalConversationID": reference, | ||
"Occasion": '', | ||
} | ||
url = self.server + self.transaction_status_path | ||
response = requests.post(url, json=request, headers=headers) | ||
data = response.json() | ||
if self.debug: | ||
logging.debug('URL: {}'.format(url)) | ||
logging.debug('Request payload:\n {}'.format(request)) | ||
logging.debug('Response {}:\n {}'.format(response.status_code, data)) | ||
|
||
if response.status_code == 200: | ||
status = 'started' | ||
if data['ResultCode'] == '1001': | ||
status = 'settled' | ||
return { | ||
'response': data, | ||
'status': status, | ||
} | ||
else: | ||
status = 'started' | ||
|
||
return { | ||
'response': data, | ||
'status': status, | ||
'error': data['Envelope']['Body']['Fault']['faultstring'], | ||
} | ||
|
||
def simulate_transaction(self, amount, phone_number, reference, shortcode=None): | ||
timestamp = datetime.now().strftime('%Y%m%d%H%M%S') | ||
access_token = self.get_access_token() | ||
if not shortcode: | ||
shortcode = self.shortcode | ||
|
||
if not access_token: | ||
return { | ||
'response': {}, | ||
'status': 'failed', | ||
'error': 'Could not get access token', | ||
'request_id': '' | ||
} | ||
|
||
headers = {"Authorization": "Bearer %s" % access_token} | ||
request = { | ||
"CommandID": 'CustomerPayBillOnline', | ||
"Amount": str(int(amount)), | ||
"Msisdn": phone_number, | ||
"BillRefNumber": reference, | ||
"ShortCode": shortcode | ||
} | ||
url = self.server + self.query_request_path | ||
response = requests.post(url, json=request, headers=headers) | ||
data = response.json() | ||
|
||
if self.debug: | ||
logging.debug('URL: {}'.format(url)) | ||
logging.debug('Request payload:\n {}'.format(request)) | ||
logging.debug('Response {}:\n {}'.format(response.status_code, data)) | ||
|
||
if response.status_code == 200: | ||
status = 'started' | ||
if data['ResultCode'] == '1001': | ||
status = 'settled' | ||
return { | ||
'response': data, | ||
'status': status, | ||
} | ||
else: | ||
timestamp = time.time() | ||
|
||
header = self._request_header(timestamp=timestamp) | ||
self.client.set_options(soapheaders=header) | ||
|
||
response = self.client.service.processCheckOut( | ||
merchant_transaction_id, | ||
reference_id, | ||
amount, | ||
msisdn, | ||
enc_params, | ||
callback_url, | ||
callback_method, | ||
timestamp | ||
) | ||
return response | ||
status = 'started' | ||
|
||
return { | ||
'response': data, | ||
'status': status, | ||
'error': data['errorMessage'], | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,9 +4,6 @@ | |
import setuptools | ||
import mpesa | ||
|
||
with open(os.path.join(os.path.dirname(__file__), 'README.md')) as readme: | ||
README = readme.read() | ||
|
||
os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) | ||
|
||
setuptools.setup( | ||
|
@@ -16,12 +13,13 @@ | |
include_package_data=True, | ||
license='BSD', | ||
description='M-Pesa API G2 Python adapter', | ||
long_description=README, | ||
long_description='Python adapter for Safaricom M-Pesa API G2.', | ||
url="http://onepercentclub.com", | ||
author="Loek van Gent", | ||
author_email="[email protected]", | ||
install_requires=[ | ||
'suds' | ||
'suds', | ||
'requests[security]' | ||
], | ||
tests_require=[ | ||
'nose' | ||
|