Skip to content

Commit

Permalink
First commit
Browse files Browse the repository at this point in the history
  • Loading branch information
enyachoke committed Jul 8, 2018
0 parents commit b8dced5
Show file tree
Hide file tree
Showing 22 changed files with 478 additions and 0 deletions.
62 changes: 62 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
.ropeproject/

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/

# Translations
*.mo

# Django stuff:
*.log

# Sphinx documentation
docs/_build/

# PyBuilder
target/

#Ipython Notebook
.ipynb_checkpoints
13 changes: 13 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Copyright Current year Emmanuel Nyachoke

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
3 changes: 3 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
recursive-include pretix_mpesa/static *
recursive-include pretix_mpesa/templates *
recursive-include pretix_mpesa/locale *
10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
all: localecompile
LNGS:=`find pretix_mpesa/locale/ -mindepth 1 -maxdepth 1 -type d -printf "-l %f "`

localecompile:
django-admin compilemessages

localegen:
django-admin makemessages --keep-pot -i build -i dist -i "*egg*" $(LNGS)

.PHONY: all localecompile localegen
32 changes: 32 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
Mpesa mobile payments
==========================

This is a plugin for `pretix`_.

Development setup
-----------------

1. Make sure that you have a working `pretix development setup`_.

2. Clone this repository, eg to ``local/pretix-mpesa``.

3. Activate the virtual environment you use for pretix development.

4. Execute ``python setup.py develop`` within this directory to register this application with pretix's plugin registry.

5. Execute ``make`` within this directory to compile translations.

6. Restart your local pretix server. You can now use the plugin from this repository for your events by enabling it in
the 'plugins' tab in the settings.


License
-------

Copyright Current year Emmanuel Nyachoke

Released under the terms of the Apache License 2.0


.. _pretix: https://github.com/pretix/pretix
.. _pretix development setup: https://docs.pretix.eu/en/latest/development/setup.html
20 changes: 20 additions & 0 deletions pretix_mpesa/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy


class PluginApp(AppConfig):
name = 'pretix_mpesa'
verbose_name = 'Mpesa mobile payments'

class PretixPluginMeta:
name = ugettext_lazy('Mpesa mobile payments')
author = 'Emmanuel Nyachoke'
description = ugettext_lazy('Adds safaricom mpesa payments to pretix')
visible = True
version = '1.0.0'

def ready(self):
from . import signals # NOQA


default_app_config = 'pretix_mpesa.PluginApp'
12 changes: 12 additions & 0 deletions pretix_mpesa/locale/de/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-03-07 19:01+0100\n"
"PO-Revision-Date: \n"
"Last-Translator: Emmanuel Nyachoke\n"
"Language-Team: \n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
Empty file.
12 changes: 12 additions & 0 deletions pretix_mpesa/locale/de_Informal/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-03-07 19:01+0100\n"
"PO-Revision-Date: \n"
"Last-Translator: Emmanuel Nyachoke\n"
"Language-Team: \n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
157 changes: 157 additions & 0 deletions pretix_mpesa/payment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import json
import logging
import urllib.parse
from collections import OrderedDict
import phonenumbers

from pympesa import Pympesa
from django import forms
from django.contrib import messages
from django.core import signing
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 pretix.base.decimal import round_decimal
from pretix.base.models import Order, Quota, RequiredAction
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
from pretix.helpers.urls import build_absolute_uri as build_global_uri
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.plugins.paypal.models import ReferencedPayPalObject
from pretix.presale.views.cart import (
cart_session, create_empty_cart_id, get_or_create_cart_id,
)
from .tasks import send_stk

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


class Mpesa(BasePaymentProvider):
identifier = 'mpesa'
verbose_name = _('Mpesa')
payment_form_fields = OrderedDict([
])

@cached_property
def cart_session(self):
return cart_session(self.request)

@property
def settings_form_fields(self):
d = OrderedDict(
[
('endpoint',
forms.ChoiceField(
label=_('Endpoint'),
initial='sandbox',
choices=(
('production', 'Live'),
('sandbox', 'Sandbox'),
),
)),
('safaricom_consumer_key',
forms.CharField(
label=_('Safaricom Consumer Key'),
required=True,
help_text=_('<a target="_blank" rel="noopener" href="{docs_url}">{text}</a>').format(
text=_('Go to the safaricom developer portal to obtain developer keys a get guidance on going live'),
docs_url='https://developer.safaricom.co.ke'
)
)),
('safaricom_consumer_secret',
forms.CharField(
label=_('Safaricom Consumer Secret'),
required=True,
)),
('mpesa_shortcode',
forms.CharField(
label=_('Lipa na Mpesa Online shortcode'),
required=True,
help_text=_('Apply for this from safaricom')
)),
('encryption_password',
forms.CharField(
label=_('Encription Password'),
required=True,
help_text=_('The password for encrypting the request')
)),
('mpesa_phone_number_field_required',
forms.BooleanField(
label=_('Will the mpesa phone number be required to place an order'),
help_text=_("If this is not checked, entering a mpesa phone number is optional and the mpesa payment my not work."),
required=False,
)),

] + list(super().settings_form_fields.items())
)
return d

def checkout_confirm_render(self, request) -> str:
"""
Returns the HTML that should be displayed when the user selected this provider
on the 'confirm order' page.
"""
template = get_template('pretix_mpesa/checkout_payment_confirm.html')
ctx = {'request': request, 'event': self.event, 'settings': self.settings}
return template.render(ctx)

def order_pending_render(self, request, order) -> str:
template = get_template('pretix_mpesa/pending.html')
ctx = {'request': request, 'event': self.event, 'settings': self.settings, 'order': order}
return template.render(ctx)

def payment_form_render(self, request) -> str:
template = get_template('pretix_mpesa/checkout_payment_form.html')
ctx = {'request': request, 'event': self.event, 'settings': self.settings}
return template.render(ctx)

def checkout_prepare(self, request, cart):
self.request = request
mpesa_phone_number = self.cart_session.get('contact_form_data', {}).get('mpesa_phone_number', '')
try:
parsed_num = phonenumbers.parse(mpesa_phone_number, 'KE')
except phonenumbers.NumberParseException:
messages.error(request, _('Please check to confirm that you entered the mpesa phone number and that it was a valid phone number'))
return False
else:
if phonenumbers.is_valid_number(parsed_num):
return True
else:
messages.error(request, _('The Mpesa number is not a valid phone number'))
return False

def payment_is_valid_session(self, request):
return True

def order_can_retry(self, order):
return self._is_still_available(order=order)

def payment_perform(self, request, order) -> str:
"""
Will be called if the user submitted his order successfully to initiate the
payment process.
It should return a custom redirct URL, if you need special behavior, or None to
continue with default behavior.
On errors, it should use Django's message framework to display an error message
to the user (or the normal form validation error messages).
:param order: The order object
"""
kwargs = {}
if request.resolver_match and 'cart_namespace' in request.resolver_match.kwargs:
kwargs['cart_namespace'] = request.resolver_match.kwargs['cart_namespace']
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)),
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',
'callback_url': callback_url, 'mode': mode})
return None
24 changes: 24 additions & 0 deletions pretix_mpesa/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Register your receivers here
from django.dispatch import receiver
from i18nfield.strings import LazyI18nString

from pretix.base.signals import register_payment_providers
from pretix.presale.signals import contact_form_fields
from django.utils.translation import ugettext_lazy as _
from django import forms



@receiver(register_payment_providers, dispatch_uid="payment_mpesa")
def register_payment_provider(sender, **kwargs):
from .payment import Mpesa
return Mpesa

@receiver(contact_form_fields, dispatch_uid="pretix_mpesa_phone_question")
def add_telephone_question(sender, **kwargs):
return {'mpesa_phone_number': forms.CharField(
label=_('Mpesa Phone number'),
required=sender.settings.telephone_field_required,
help_text='The payment request will be made to this number by the mpesa payment provider',
widget=forms.TextInput(attrs={'placeholder': _('Mpesa Phone number')}),
)}
Empty file.
36 changes: 36 additions & 0 deletions pretix_mpesa/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@

from pretix.base.services.async import ProfiledTask
import pympesa
from pretix.celery_app import app
import logging
logger = logging.getLogger('pretix.plugins.mpesa')
import time

@app.task(base=ProfiledTask)
def send_stk(consumer_key, consumer_secret, business_short_code, password, amount, phone, order_number,callback_url,mode) -> None:
response = pympesa.oauth_generate_token(consumer_key, consumer_secret,'client_credentials',mode).json()
access_token = response.get("access_token")
from pympesa import Pympesa
mpesa_client = Pympesa(access_token,mode)
response = mpesa_client.lipa_na_mpesa_online_payment(
BusinessShortCode=business_short_code,
Password=password,
Timestamp=time.strftime('%Y%m%d%H%M%S'),
TransactionType="CustomerPayBillOnline",
Amount="100",
PartyA="254708374149",
PartyB=business_short_code,
PhoneNumber=phone,
CallBackURL='https://nyachoke.localtunnel.me/bigevents/2019/mpesa/callback',
AccountReference=order_number,
TransactionDesc=order_number
)
logger.debug(response.json())

@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()
Empty file.
Loading

0 comments on commit b8dced5

Please sign in to comment.