Skip to content

Commit

Permalink
Finalize mpesa express
Browse files Browse the repository at this point in the history
  • Loading branch information
enyachoke committed Sep 20, 2018
1 parent b8dced5 commit 1504833
Show file tree
Hide file tree
Showing 10 changed files with 268 additions and 18 deletions.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"python.pythonPath": "/Users/achachiez/.pyenv/versions/pretix/bin/python"
}
34 changes: 34 additions & 0 deletions pretix_mpesa/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 2.1.1 on 2018-09-19 18:17

from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='OnlineCheckout',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('phone', models.BigIntegerField()),
('amount', models.DecimalField(decimal_places=2, max_digits=20)),
('checkout_request_id', models.CharField(default='', max_length=50)),
('account_reference', models.CharField(default='', max_length=50)),
('transaction_description', models.CharField(blank=True, max_length=50, null=True)),
('customer_message', models.CharField(blank=True, max_length=100, null=True)),
('merchant_request_id', models.CharField(blank=True, max_length=50, null=True)),
('response_code', models.CharField(blank=True, max_length=5, null=True)),
('response_description', models.CharField(blank=True, max_length=100, null=True)),
('date_added', models.DateTimeField(auto_now_add=True)),
],
options={
'verbose_name_plural': 'Online Checkout Requests',
'db_table': 'tbl_online_checkout_requests',
},
),
]
23 changes: 23 additions & 0 deletions pretix_mpesa/migrations/0002_auto_20180919_1853.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 2.1.1 on 2018-09-19 18:53

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('pretix_mpesa', '0001_initial'),
]

operations = [
migrations.AddField(
model_name='onlinecheckout',
name='mpesa_receipt_number',
field=models.CharField(default='', max_length=50),
),
migrations.AddField(
model_name='onlinecheckout',
name='received',
field=models.BooleanField(default=False),
),
]
40 changes: 40 additions & 0 deletions pretix_mpesa/migrations/0003_auto_20180919_1911.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Generated by Django 2.1.1 on 2018-09-19 19:11

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('pretix_mpesa', '0002_auto_20180919_1853'),
]

operations = [
migrations.CreateModel(
name='OnlineCheckoutResponse',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('merchant_request_id', models.CharField(blank=True, max_length=50, null=True)),
('checkout_request_id', models.CharField(default='', max_length=50)),
('result_code', models.CharField(blank=True, max_length=5, null=True)),
('result_description', models.CharField(blank=True, max_length=100, null=True)),
('mpesa_receipt_number', models.CharField(blank=True, max_length=50, null=True)),
('transaction_date', models.DateTimeField(blank=True, null=True)),
('phone', models.BigIntegerField(blank=True, null=True)),
('amount', models.DecimalField(blank=True, decimal_places=2, max_digits=20, null=True)),
('date_added', models.DateTimeField(auto_now_add=True)),
],
options={
'verbose_name_plural': 'Online Checkout Responses',
'db_table': 'tbl_online_checkout_responses',
},
),
migrations.RemoveField(
model_name='onlinecheckout',
name='mpesa_receipt_number',
),
migrations.RemoveField(
model_name='onlinecheckout',
name='received',
),
]
Empty file.
45 changes: 45 additions & 0 deletions pretix_mpesa/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@

from django.db import models
class OnlineCheckout(models.Model):
"""
Handles Online Checkout
"""
id = models.BigAutoField(primary_key=True)
phone = models.BigIntegerField()
amount = models.DecimalField(max_digits=20, decimal_places=2)
checkout_request_id = models.CharField(max_length=50, default='')
account_reference = models.CharField(max_length=50, default='')
transaction_description = models.CharField(max_length=50, blank=True, null=True)
customer_message = models.CharField(max_length=100, blank=True, null=True)
merchant_request_id = models.CharField(max_length=50, blank=True, null=True)
response_code = models.CharField(max_length=5, blank=True, null=True)
response_description = models.CharField(max_length=100, blank=True, null=True)
date_added = models.DateTimeField(auto_now_add=True)
def __str__(self):
return str(self.phone)

class Meta:
db_table = 'tbl_online_checkout_requests'
verbose_name_plural = 'Online Checkout Requests'

class OnlineCheckoutResponse(models.Model):
"""
Handles Online Checkout Response
"""
id = models.BigAutoField(primary_key=True)
merchant_request_id = models.CharField(max_length=50, blank=True, null=True)
checkout_request_id = models.CharField(max_length=50, default='')
result_code = models.CharField(max_length=5, blank=True, null=True)
result_description = models.CharField(max_length=100, blank=True, null=True)
mpesa_receipt_number = models.CharField(max_length=50, blank=True, null=True)
transaction_date = models.DateTimeField(blank=True, null=True)
phone = models.BigIntegerField(blank=True, null=True)
amount = models.DecimalField(max_digits=20, decimal_places=2, blank=True, null=True)
date_added = models.DateTimeField(auto_now_add=True)

def __str__(self):
return str(self.phone)

class Meta:
db_table = 'tbl_online_checkout_responses'
verbose_name_plural = 'Online Checkout Responses'
28 changes: 22 additions & 6 deletions pretix_mpesa/payment.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import json
import logging
import urllib.parse
from collections import OrderedDict
import phonenumbers
import math

from pympesa import Pympesa
from django import forms
Expand All @@ -11,9 +11,11 @@
from django.template.loader import get_template
from django.utils.translation import ugettext as __, ugettext_lazy as _
from django.utils.functional import cached_property
from collections import OrderedDict
from django.http import HttpRequest

from pretix.base.decimal import round_decimal
from pretix.base.models import Order, Quota, RequiredAction
from pretix.base.models import Order, Quota, RequiredAction,OrderPayment, OrderRefund
from pretix.base.payment import BasePaymentProvider, PaymentException
from pretix.base.services.mail import SendMailException
from pretix.base.services.orders import mark_order_paid, mark_order_refunded
Expand All @@ -33,7 +35,9 @@ class Mpesa(BasePaymentProvider):
verbose_name = _('Mpesa')
payment_form_fields = OrderedDict([
])

@property
def abort_pending_allowed(self):
return False
@cached_property
def cart_session(self):
return cart_session(self.request)
Expand Down Expand Up @@ -77,6 +81,12 @@ def settings_form_fields(self):
required=True,
help_text=_('The password for encrypting the request')
)),
('stk_callback_url',
forms.CharField(
label=_('Mpesa STK Callback'),
required=True,
help_text=_('This is the callback url for mpesa stk')
)),
('mpesa_phone_number_field_required',
forms.BooleanField(
label=_('Will the mpesa phone number be required to place an order'),
Expand Down Expand Up @@ -117,6 +127,7 @@ def checkout_prepare(self, request, cart):
return False
else:
if phonenumbers.is_valid_number(parsed_num):
request.session['mpesa_phone_number'] = '254' + str(parsed_num.national_number)
return True
else:
messages.error(request, _('The Mpesa number is not a valid phone number'))
Expand All @@ -128,7 +139,7 @@ def payment_is_valid_session(self, request):
def order_can_retry(self, order):
return self._is_still_available(order=order)

def payment_perform(self, request, order) -> str:
def execute_payment(self, request: HttpRequest, payment: OrderPayment):
"""
Will be called if the user submitted his order successfully to initiate the
payment process.
Expand All @@ -144,14 +155,19 @@ def payment_perform(self, request, order) -> str:
kwargs = {}
if request.resolver_match and 'cart_namespace' in request.resolver_match.kwargs:
kwargs['cart_namespace'] = request.resolver_match.kwargs['cart_namespace']
parsed_num = request.session.get('mpesa_phone_number', '')
logger.debug(parsed_num)
mode = self.settings.get('endpoint')
consumer_key = self.settings.get('safaricom_consumer_key')
consumer_secret = self.settings.get('safaricom_consumer_secret')
business_short_code = self.settings.get('mpesa_shortcode')
password = self.settings.get('encryption_password')
callback_url = ''.join(build_absolute_uri(request.event, 'plugins:pretix_mpesa:callback', kwargs=kwargs)),
amount = math.ceil(payment.amount)
callback_url = self.settings.get('stk_callback_url')
logger.debug(amount)
logger.debug(callback_url)
send_stk.apply_async(kwargs={'consumer_key': consumer_key, 'consumer_secret': consumer_secret,
'business_short_code': business_short_code,
'password': password, 'amount': 10, 'phone': '254700247286', 'order_number': 'Test Order',
'password': password, 'amount': str(amount), 'phone': parsed_num, 'order_number': str(payment.id),
'callback_url': callback_url, 'mode': mode})
return None
40 changes: 32 additions & 8 deletions pretix_mpesa/tasks.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@

from pretix.base.services.async import ProfiledTask
from pretix.base.services.tasks import ProfiledTask
from .models import OnlineCheckout
import base64
import pympesa
import uuid
from pretix.celery_app import app
from decimal import Decimal
import uuid
import logging
logger = logging.getLogger('pretix.plugins.mpesa')
import time
Expand All @@ -12,25 +17,44 @@ def send_stk(consumer_key, consumer_secret, business_short_code, password, amoun
access_token = response.get("access_token")
from pympesa import Pympesa
mpesa_client = Pympesa(access_token,mode)
time_stamp = pympesa.generate_timestamp()
encoded_password = encode_password(shortcode=business_short_code,passkey=password,timestamp=time_stamp)
response = mpesa_client.lipa_na_mpesa_online_payment(
BusinessShortCode=business_short_code,
Password=password,
Timestamp=time.strftime('%Y%m%d%H%M%S'),
Password=encoded_password ,
Timestamp=time_stamp,
TransactionType="CustomerPayBillOnline",
Amount="100",
PartyA="254708374149",
Amount=amount,
PartyA=phone,
PartyB=business_short_code,
PhoneNumber=phone,
CallBackURL='https://nyachoke.localtunnel.me/bigevents/2019/mpesa/callback',
CallBackURL=callback_url,
AccountReference=order_number,
TransactionDesc=order_number
)
logger.debug(response.json())
json_response = response.json()
checkout_id = json_response.get('CheckoutRequestID')
result_code = json_response.get('ResponseCode')
logger.debug(json_response)
if result_code == '0':
OnlineCheckout.objects.create(phone=int(phone),
amount=Decimal(str(amount)),
account_reference=order_number,
checkout_request_id=checkout_id,
transaction_description=order_number)
logger.debug('Created request')

@app.task(base=ProfiledTask)
def simulate_C2B(consumer_key,consumer_secret):
response = pympesa.oauth_generate_token(consumer_key, consumer_secret).json()
access_token = response.get("access_token")
from pympesa import Pympesa
mpesa_client = Pympesa(access_token)
mpesa_client.c2b_simulate_transaction()
mpesa_client.c2b_simulate_transaction()

def encode_password(shortcode, passkey, timestamp):
"""Generate and return a base64 encoded password for online access.
"""
data = shortcode + passkey + timestamp
data_bytes = data.encode('utf-8')
return base64.b64encode(data_bytes).decode()
4 changes: 2 additions & 2 deletions pretix_mpesa/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

from pretix.multidomain import event_url

from .views import confirm , validate
from .views import confirm , validate , stk_callback
event_patterns = [
url(r'^mpesa/', include([
url(r'^confirm/$', confirm, name='confirm'),
url(r'^validate/$', validate, name='validate'),
url(r'^callback/$', validate, name='callback')
url(r'^callback/$', stk_callback, name='callback')
])),
]
69 changes: 67 additions & 2 deletions pretix_mpesa/views.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,75 @@
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse, JsonResponse
import logging
import json
from .models import OnlineCheckoutResponse, OnlineCheckout
from pretix.base.models import Order, Quota, RequiredAction,OrderPayment, OrderRefund
from decimal import Decimal

logger = logging.getLogger('pretix.plugins.mpesa')


@csrf_exempt
def confirm(request, *args, **kwargs):
message = ''


@csrf_exempt
def validate(request, *args, **kwargs):
message = ''


def callback(request, *args, **kwargs):
message = ''
@csrf_exempt
def stk_callback(request, *args, **kwargs):
json_data = json.loads(request.body)
logger.info(json_data)

try:
data = json_data.get('Body', {}).get('stkCallback', {})
update_data = dict()
update_data['result_code'] = data.get('ResultCode', '')
update_data['result_description'] = data.get('ResultDesc', '')
update_data['checkout_request_id'] = data.get('CheckoutRequestID', '')
update_data['merchant_request_id'] = data.get('MerchantRequestID', '')

meta_data = data.get('CallbackMetadata', {}).get('Item', {})
if len(meta_data) > 0:
# handle the meta data
for item in meta_data:
if len(item.values()) > 1:
key, value = item.values()
if key == 'MpesaReceiptNumber':
update_data['mpesa_receipt_number'] = value
if key == 'Amount':
update_data['amount'] = Decimal(value)
if key == 'PhoneNumber':
update_data['phone'] = int(value)
if key == 'TransactionDate':
date = str(value)
year, month, day, hour, min, sec = date[:4], date[4:-8], date[6:-6], date[8:-4], date[10:-2], date[12:]
update_data['transaction_date'] = '{}-{}-{} {}:{}:{}'.format(year, month, day, hour, min, sec)

# save
checkout_request_id = data.get('CheckoutRequestID', '')
logger.info(dict(updated_data=update_data))
try:
online_checkout = OnlineCheckout.objects.get(checkout_request_id=checkout_request_id)
logger.info('Checkout')
logger.info(online_checkout.account_reference)
try:
payment = OrderPayment.objects.get(id=online_checkout.account_reference)
logger.info('Payment Found')
try:
payment.confirm()
except Quota.QuotaExceededException:
pass
except OrderPayment.DoesNotExist:
logger.info('Payment Not found')
pass
except OnlineCheckout.DoesNotExist:
logger.info('Not found')
pass
except Exception as ex:
logger.error(ex)
raise ValueError(str(ex))
return HttpResponse(request)

0 comments on commit 1504833

Please sign in to comment.