Skip to content

Commit

Permalink
Merge pull request #1 from gannetson/new-api
Browse files Browse the repository at this point in the history
New api
  • Loading branch information
gannetson authored Nov 15, 2017
2 parents 8e3f8ea + 2a57a1f commit e601c2e
Show file tree
Hide file tree
Showing 3 changed files with 243 additions and 87 deletions.
2 changes: 1 addition & 1 deletion mpesa/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
VERSION = [0, 0, 2, 'final']
VERSION = [0, 1, 4, 'final']

def get_version():
version = '%s.%s' % (VERSION[0], VERSION[1])
Expand Down
320 changes: 239 additions & 81 deletions mpesa/services.py
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'],
}
8 changes: 3 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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'
Expand Down

0 comments on commit e601c2e

Please sign in to comment.