diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6da9ad7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.pyc +*.pyc +venv +backup +*db.sqlite3 +start_app.sh \ No newline at end of file diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..2a05d17 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: gunicorn config.wsgi --log-file - \ No newline at end of file diff --git a/app.json b/app.json new file mode 100644 index 0000000..cdcae6a --- /dev/null +++ b/app.json @@ -0,0 +1,21 @@ +{ + "name": "Ussd App", + "description": "A barebones Python app, which can easily be deployed to Heroku.", + "image": "heroku/python", + "repository": "https://github.com/peterolayinka/USSDCodeChallenge", + "keywords": ["python", "django"], + "env": { + "SECRET_KEY": { + "description": "The secret key for the Django application.", + "generator": "secret" + } + }, + "environments": { + "test": { + "scripts": { + "test-setup": "python manage.py collectstatic --noinput", + "test": "python manage.py test" + } + } + } +} \ No newline at end of file diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/settings.py b/config/settings.py new file mode 100644 index 0000000..b847786 --- /dev/null +++ b/config/settings.py @@ -0,0 +1,121 @@ +""" +Django settings for py_2_africaTalkingUSSD. + +Generated by 'django-admin startproject' using Django 1.11.13. + +For more information on this file, see +https://docs.djangoproject.com/en/1.11/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.11/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = os.environ.get('SECRET_KEY', 'something-secret') + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ['*'] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'ussd_app' +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'config.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'config.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.11/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/1.11/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.11/howto/static-files/ + +STATIC_URL = '/static/' diff --git a/config/urls.py b/config/urls.py new file mode 100644 index 0000000..8de0110 --- /dev/null +++ b/config/urls.py @@ -0,0 +1,24 @@ +"""py_2_africaTalkingUSSD URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.11/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.conf.urls import url, include + 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +""" +from django.conf.urls import url +from django.contrib import admin +from ussd_app import views + +urlpatterns = [ + url(r'^$', views.process_ussd, name="process_ussd"), + url(r'^process-voice/$', views.process_voice, name="process_voice"), + url(r'^admin/', admin.site.urls), +] diff --git a/config/wsgi.py b/config/wsgi.py new file mode 100644 index 0000000..1d014de --- /dev/null +++ b/config/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for py_2_africaTalkingUSSD project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") + +application = get_wsgi_application() diff --git a/db.sqlite3 b/db.sqlite3 new file mode 100644 index 0000000..3fc4828 Binary files /dev/null and b/db.sqlite3 differ diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..68141ee --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") + try: + from django.core.management import execute_from_command_line + except ImportError: + # The above import may fail for some other reason. Ensure that the + # issue is really that Django is missing to avoid masking other + # exceptions on Python 2. + try: + import django + except ImportError: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) + raise + execute_from_command_line(sys.argv) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4682192 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +certifi==2018.4.16 +Django==1.11.13 +gunicorn==19.8.1 +pipenv==2018.5.18 +pytz==2018.4 +virtualenv==16.0.0 +virtualenv-clone==0.3.0 diff --git a/runtime.txt b/runtime.txt new file mode 100644 index 0000000..5b66454 --- /dev/null +++ b/runtime.txt @@ -0,0 +1 @@ +python-2.7.15 \ No newline at end of file diff --git a/ussd_app/AfricasTalkingGateway.py b/ussd_app/AfricasTalkingGateway.py new file mode 100644 index 0000000..ec48322 --- /dev/null +++ b/ussd_app/AfricasTalkingGateway.py @@ -0,0 +1,692 @@ +""" + COPYRIGHT (C) 2014 AFRICASTALKING LTD # + + AFRICAStALKING SMS GATEWAY CLASS IS A FREE SOFTWARE IE. CAN BE MODIFIED AND/OR REDISTRIBUTED + UNDER THER TERMS OF GNU GENERAL PUBLIC LICENCES AS PUBLISHED BY THE + FREE SOFTWARE FOUNDATION VERSION 3 OR ANY LATER VERSION + + THE CLASS IS DISTRIBUTED ON 'AS IS' BASIS WITHOUT ANY WARRANTY, INCLUDING BUT NOT LIMITED TO + THE IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +""" + +import urllib +import urllib2 +import json + +class AfricasTalkingGatewayException(Exception): + pass + +class AfricasTalkingGateway: + + def __init__(self, username_, apiKey_): + self.username = username_ + self.apiKey = apiKey_ + self.environment = 'sandbox' if username_ is 'sandbox' else 'prod' + + self.HTTP_RESPONSE_OK = 200 + self.HTTP_RESPONSE_CREATED = 201 + + # Turn this on if you run into problems. It will print the raw HTTP response from our server + self.Debug = True + + def generateAuthToken(self): + parameters = {'username': self.username} + url = self.getGenerateAuthTokenUrl() + response = self.sendJSONRequest(url, json.dumps(parameters)) + if self.responseCode == self.HTTP_RESPONSE_CREATED: + return json.loads(response) + raise AfricasTalkingGatewayException(response) + + # Messaging methods + def sendMessage(self, to_, message_, from_ = None, bulkSMSMode_ = 1, enqueue_ = 0, keyword_ = None, linkId_ = None, retryDurationInHours_ = None, authToken_ = None): + if len(to_) == 0 or len(message_) == 0: + raise AfricasTalkingGatewayException("Please provide both to_ and message_ parameters") + + parameters = {'username' : self.username, + 'to': to_, + 'message': message_, + 'bulkSMSMode':bulkSMSMode_} + + if not from_ is None : + parameters["from"] = from_ + + if enqueue_ > 0: + parameters["enqueue"] = enqueue_ + + if not keyword_ is None: + parameters["keyword"] = keyword_ + + if not linkId_ is None: + parameters["linkId"] = linkId_ + + if not retryDurationInHours_ is None: + parameters["retryDurationInHours"] = retryDurationInHours_ + + response = self.sendRequest(self.getSmsUrl(), parameters, authToken_) + + if self.responseCode == self.HTTP_RESPONSE_CREATED: + decoded = json.loads(response) + recipients = decoded['SMSMessageData']['Recipients'] + + if len(recipients) > 0: + return recipients + + raise AfricasTalkingGatewayException(decoded['SMSMessageData']['Message']) + + raise AfricasTalkingGatewayException(response) + + + def fetchMessages(self, lastReceivedId_ = 0): + url = "%s?username=%s&lastReceivedId=%s" % (self.getSmsUrl(), self.username, lastReceivedId_) + response = self.sendRequest(url) + + if self.responseCode == self.HTTP_RESPONSE_OK: + decoded = json.loads(response) + return decoded['SMSMessageData']['Messages'] + raise AfricasTalkingGatewayException(response) + + + # Subscription methods + def createSubscription(self, phoneNumber_, shortCode_, keyword_, checkoutToken_): + if len(phoneNumber_) == 0 or len(shortCode_) == 0 or len(keyword_) == 0: + raise AfricasTalkingGatewayException("Please supply phone number, short code and keyword") + + url = "%s/create" %(self.getSmsSubscriptionUrl()) + parameters = { + 'username' : self.username, + 'phoneNumber' : phoneNumber_, + 'shortCode' : shortCode_, + 'keyword' : keyword_, + "checkoutToken" : checkoutToken_ + } + + response = self.sendRequest (url, parameters) + if self.responseCode == self.HTTP_RESPONSE_CREATED: + decoded = json.loads(response) + return decoded + raise AfricasTalkingGatewayException(response) + + + def deleteSubscription(self, phoneNumber_, shortCode_, keyword_): + if len(phoneNumber_) == 0 or len(shortCode_) == 0 or len(keyword_) == 0: + raise AfricasTalkingGatewayException("Please supply phone number, short code and keyword") + + url = "%s/delete" %(self.getSmsSubscriptionUrl()) + parameters = { + 'username' :self.username, + 'phoneNumber' :phoneNumber_, + 'shortCode' :shortCode_, + 'keyword' :keyword_ + } + response = self.sendRequest(url, parameters) + if self.responseCode == self.HTTP_RESPONSE_CREATED: + decoded = json.loads(response) + return decoded + raise AfricasTalkingGatewayException(response) + + + def fetchPremiumSubscriptions(self,shortCode_, keyword_, lastReceivedId_ = 0): + if len(shortCode_) == 0 or len(keyword_) == 0: + raise AfricasTalkingGatewayException("Please supply the short code and keyword") + + url = "%s?username=%s&shortCode=%s&keyword=%s&lastReceivedId=%s" % (self.getSmsSubscriptionUrl(), + self.username, + shortCode_, + keyword_, + lastReceivedId_) + result = self.sendRequest(url) + if self.responseCode == self.HTTP_RESPONSE_OK: + decoded = json.loads(result) + return decoded['responses'] + + raise AfricasTalkingGatewayException(response) + + + # Voice methods + def call(self, from_, to_): + parameters = { + 'username' : self.username, + 'from' : from_, + 'to': to_ + } + + url = "%s/call" %(self.getVoiceUrl()) + response = self.sendRequest(url, parameters) + decoded = json.loads(response) + if decoded['errorMessage'] == "None": + return decoded['entries']; + raise AfricasTalkingGatewayException(decoded['errorMessage']) + + def getNumQueuedCalls(self, phoneNumber_, queueName_ = None): + parameters = { + 'username' :self.username, + 'phoneNumbers' :phoneNumber_ + } + + if queueName_ is not None: + parameters['queueName'] = queueName_ + + url = "%s/queueStatus" %(self.getVoiceUrl()) + response = self.sendRequest(url, parameters) + decoded = json.loads(response) + if decoded['errorMessage'] == "None": + return decoded['entries'] + + raise AfricasTalkingGatewayException(decoded['errorMessage']) + + def uploadMediaFile(self, urlString_): + parameters = { + 'username' :self.username, + 'url' :urlString_ + } + url = "%s/mediaUpload" %(self.getVoiceUrl()) + response = self.sendRequest(url, parameters) + decoded = json.loads(response) + if decoded['errorMessage'] != "None": + raise AfricasTalkingGatewayException(decoded['errorMessage']) + + #Airtime method + def sendAirtime(self, recipients_): + parameters = { + 'username' : self.username, + 'recipients' : json.dumps(recipients_) + } + + url = "%s/send" %(self.getAirtimeUrl()) + response = self.sendRequest(url, parameters) + decoded = json.loads(response) + responses = decoded['responses'] + if self.responseCode == self.HTTP_RESPONSE_CREATED: + if len(responses) > 0: + return responses + raise AfricasTalkingGatewayException(decoded["errorMessage"]) + raise AfricasTalkingGatewayException(response) + + #USSD Push method + def sendUssdPush(self, phoneNumber_, menu_, checkoutToken_): + parameters = { + 'username' : self.username, + 'phoneNumber' : phoneNumber_, + 'menu' : menu_, + 'checkoutToken' : checkoutToken_ + } + + url = self.getUssdPushUrl() + response = self.sendRequest(url, parameters) + if self.responseCode == self.HTTP_RESPONSE_CREATED: + decoded = json.loads(response) + if decoded['status'] == 'Queued': + return decoded['sessionId'] + raise AfricasTalkingGatewayException(decoded["errorMessage"]) + raise AfricasTalkingGatewayException(response) + + #Checkout Token Request + def createCheckoutToken(self, phoneNumber_): + parameters = { + 'phoneNumber' : phoneNumber_ + } + + url = "%s/checkout/token/create" %(self.getApiHost()) + response = self.sendRequest(url, parameters) + if self.responseCode == self.HTTP_RESPONSE_CREATED: + decoded = json.loads(response) + if decoded['token'] == 'None': + raise AfricasTalkingGatewayException(decoded['token']) + return decoded['description'] + raise AfricasTalkingGatewayException(response) + + #Payment Methods + def bankPaymentCheckoutCharge(self, + productName_, + bankAccount_, + currencyCode_, + amount_, + narration_, + metadata_ = None): + + parameters = { + 'username' : self.username, + 'productName' : productName_, + 'bankAccount' : bankAccount_, + 'currencyCode' : currencyCode_, + 'amount' : amount_, + 'narration' : narration_ + } + + if metadata_: + parameters['metadata'] = metadata_ + + url = self.getBankPaymentCheckoutChargeUrl() + response = self.sendJSONRequest(url, json.dumps(parameters)) + if self.responseCode == self.HTTP_RESPONSE_CREATED: + responseObj = json.loads(response) + if responseObj['status'] == 'PendingValidation': + return responseObj['transactionId'] + raise AfricasTalkingGatewayException(responseObj['description']) + raise AfricasTalkingGatewayException(response) + + def bankPaymentCheckoutValidation(self, + transactionId_, + otp_): + + parameters = { + 'username' : self.username, + 'transactionId' : transactionId_, + 'otp' : otp_ + } + + url = self.getBankPaymentCheckoutValidationUrl() + response = self.sendJSONRequest(url, json.dumps(parameters)) + if self.responseCode == self.HTTP_RESPONSE_CREATED: + responseObj = json.loads(response) + if responseObj['status'] == 'Success': return + raise AfricasTalkingGatewayException(responseObj['description']) + raise AfricasTalkingGatewayException(response) + + def bankPaymentTransfer(self, + productName_, + recipients_): + + parameters = { + 'username' : self.username, + 'productName' : productName_, + 'recipients' : recipients_ + } + + url = self.getBankPaymentTransferUrl() + response = self.sendJSONRequest(url, json.dumps(parameters)) + if self.responseCode == self.HTTP_RESPONSE_CREATED: + responseObj = json.loads(response) + if len(responseObj['entries']): + return responseObj['entries'] + raise AfricasTalkingGatewayException(responseObj['errorMessage']) + raise AfricasTalkingGatewayException(response) + + def cardPaymentCheckoutCharge(self, + productName_, + paymentCard_, + currencyCode_, + amount_, + narration_, + metadata_ = None): + parameters = { + 'username' : self.username, + 'productName' : productName_, + 'paymentCard' : paymentCard_, + 'currencyCode' : currencyCode_, + 'amount' : amount_, + 'narration' : narration_ + } + + if metadata_: + parameters['metadata'] = metadata_ + + url = self.getCardPaymentCheckoutChargeUrl() + response = self.sendJSONRequest(url, json.dumps(parameters)) + if self.responseCode == self.HTTP_RESPONSE_CREATED: + responseObj = json.loads(response) + if responseObj['status'] == 'PendingValidation': + return responseObj['transactionId'] + raise AfricasTalkingGatewayException(responseObj['description']) + raise AfricasTalkingGatewayException(response) + + def cardPaymentCheckoutChargeWithToken(self, + productName_, + checkoutToken_, + currencyCode_, + amount_, + narration_, + metadata_ = None): + parameters = { + 'username' : self.username, + 'productName' : productName_, + 'checkoutToken' : checkoutToken_, + 'currencyCode' : currencyCode_, + 'amount' : amount_, + 'narration' : narration_ + } + + if metadata_: + parameters['metadata'] = metadata_ + + url = self.getCardPaymentCheckoutChargeUrl() + response = self.sendJSONRequest(url, json.dumps(parameters)) + if self.responseCode == self.HTTP_RESPONSE_CREATED: + responseObj = json.loads(response) + if responseObj['status'] == 'Success': return + raise AfricasTalkingGatewayException(responseObj['description']) + raise AfricasTalkingGatewayException(response) + + def cardPaymentCheckoutValidation(self, + transactionId_, + otp_): + + parameters = { + 'username' : self.username, + 'transactionId' : transactionId_, + 'otp' : otp_ + } + + url = self.getCardPaymentCheckoutValidationUrl() + response = self.sendJSONRequest(url, json.dumps(parameters)) + if self.responseCode == self.HTTP_RESPONSE_CREATED: + responseObj = json.loads(response) + if responseObj['status'] == 'Success': + return responseObj['checkoutToken'] + raise AfricasTalkingGatewayException(responseObj['description']) + raise AfricasTalkingGatewayException(response) + + def paymentStashTopup(self, + productName_, + currencyCode_, + amount_, + metadata_): + parameters = { + 'username' : self.username, + 'productName' : productName_, + 'currencyCode' : currencyCode_, + 'amount' : amount_, + 'metadata' : metadata_ + } + url = self.getPaymentStashTopupUrl() + response = self.sendJSONRequest(url, json.dumps(parameters)) + if self.responseCode == self.HTTP_RESPONSE_CREATED: + return json.loads(response) + raise AfricasTalkingGatewayException(response) + + def paymentWalletTransfer(self, + productName_, + targetUsername_, + targetProductName_, + currencyCode_, + amount_, + metadata_): + parameters = { + 'username' : self.username, + 'productName' : productName_, + 'targetUsername' : targetUsername_, + 'targetProductName' : targetProductName_, + 'currencyCode' : currencyCode_, + 'amount' : amount_, + 'metadata' : metadata_ + } + url = self.getPaymentWalletTransferUrl() + response = self.sendJSONRequest(url, json.dumps(parameters)) + if self.responseCode == self.HTTP_RESPONSE_CREATED: + return json.loads(response) + raise AfricasTalkingGatewayException(response) + + def paymentWalletBalanceQuery(self): + url = self.getPaymentWalletBalanceQueryUrl() + response = self.sendRequest(url + "?username=%s" % self.username) + if self.responseCode == self.HTTP_RESPONSE_CREATED: + return json.loads(response) + raise AfricasTalkingGatewayException(response) + + def paymentTransactionFindQuery(self, + transactionId_): + url = self.getPaymentTransactionFindQueryUrl() + response = self.sendRequest(url + "?username=%s&transactionId=%s" % (self.username, transactionId_)) + if self.responseCode == self.HTTP_RESPONSE_OK: + return json.loads(response) + raise AfricasTalkingGatewayException(response) + + def initiateMobilePaymentCheckout(self, + productName_, + phoneNumber_, + currencyCode_, + amount_, + metadata_, + providerChannel_): + parameters = { + 'username' : self.username, + 'productName' : productName_, + 'phoneNumber' : phoneNumber_, + 'currencyCode' : currencyCode_, + 'amount' : amount_, + 'metadata' : metadata_ + } + + if providerChannel_ is not None: + parameters['providerChannel'] = providerChannel_ + + url = self.getMobilePaymentCheckoutUrl() + response = self.sendJSONRequest(url, json.dumps(parameters)) + if self.responseCode == self.HTTP_RESPONSE_CREATED: + decoded = json.loads(response) + if decoded['status'] == 'PendingConfirmation': + return decoded['transactionId'] + raise AfricasTalkingGatewayException(decoded['description']) + raise AfricasTalkingGatewayException(response) + + def mobilePaymentB2CRequest(self, productName_, recipients_): + parameters = { + 'username' : self.username, + 'productName' : productName_, + 'recipients' : recipients_ + } + url = self.getMobilePaymentB2CUrl() + response = self.sendJSONRequest(url, json.dumps(parameters)) + if self.responseCode == self.HTTP_RESPONSE_CREATED: + decoded = json.loads(response) + if len(decoded['entries']) > 0: + return decoded['entries'] + raise AfricasTalkingGatewayException(decoded['errorMessage']) + raise AfricasTalkingGatewayException(response) + + def mobilePaymentB2BRequest(self, productName_, providerData_, currencyCode_, amount_, metadata_): + if "provider" not in providerData_: + raise AfricasTalkingGatewayException("Missing field provider") + + if "destinationChannel" not in providerData_: + raise AfricasTalkingGatewayException("Missing field destinationChannel") + + if "transferType" not in providerData_: + raise AfricasTalkingGatewayException("Missing field transferType") + + parameters = { + 'username' : self.username, + 'productName' : productName_, + 'provider' : providerData_['provider'], + 'destinationChannel' : providerData_['destinationChannel'], + 'transferType' : providerData_['transferType'], + 'currencyCode' : currencyCode_, + 'amount' : amount_, + 'metadata' : metadata_ + } + if "destinationAccount" in providerData_: + parameters['destinationAccount'] = providerData_['destinationAccount'] + + url = self.getMobilePaymentB2BUrl() + response = self.sendJSONRequest(url, json.dumps(parameters)) + if self.responseCode == self.HTTP_RESPONSE_CREATED: + decoded = json.loads(response) + return decoded + raise AfricasTalkingGatewayException(response) + + def mobilePaymentB2BRequest(self, + productName_, + provider_, + transferType_, + currencyCode_, + amount_, + metadata_, + destinationChannel_, + destinationAccount_): + parameters = { + 'username' : self.username, + 'productName' : productName_, + 'provider' : provider_, + 'transferType' : transferType_, + 'currencyCode' : currencyCode_, + 'amount' : amount_, + 'destinationChannel' : destinationChannel_, + } + if metadata_ is not None: + parameters['metadata'] = metadata_ + + if destinationAccount_ is not None: + parameters['destinationAccount'] = destinationAccount_ + + url = self.getMobilePaymentB2BUrl() + response = self.sendJSONRequest(url, json.dumps(parameters)) + if self.responseCode == self.HTTP_RESPONSE_CREATED: + return json.loads(response) + raise AfricasTalkingGatewayException(response) + + def paymentBankWithdrawalRequest(self, + productName_, + bankAccountName_, + currencyCode_, + amount_, + metadata_): + parameters = { + 'username' : self.username, + 'bankAccountName' : bankAccountName_, + 'productName' : productName_, + 'currencyCode' : currencyCode_, + 'amount' : amount_ + } + if metadata_ is not None: + parameters['metadata'] = metadata_ + + url = self.getPaymentBankWithdrawalUrl() + response = self.sendJSONRequest(url, json.dumps(parameters)) + if self.responseCode == self.HTTP_RESPONSE_CREATED: + return json.loads(response) + raise AfricasTalkingGatewayException(response) + + # Userdata method + def getUserData(self): + url = "%s?username=%s" %(self.getUserDataUrl(), self.username) + result = self.sendRequest(url) + if self.responseCode == self.HTTP_RESPONSE_OK: + decoded = json.loads(result) + return decoded['UserData'] + raise AfricasTalkingGatewayException(response) + + # HTTP access method + def sendRequest(self, urlString, data_ = None, authToken_ = None): + try: + headers = {'Accept' : 'application/json'} + if authToken_ is None: + headers['apikey'] = self.apiKey + else: + headers['authToken'] = authToken_ + + if data_ is not None: + data = urllib.urlencode(data_) + request = urllib2.Request(urlString, data, headers = headers) + else: + request = urllib2.Request(urlString, headers = headers) + response = urllib2.urlopen(request) + except urllib2.HTTPError as e: + raise AfricasTalkingGatewayException(e.read()) + else: + self.responseCode = response.getcode() + response = ''.join(response.readlines()) + if self.Debug: + print "Raw response: " + response + + return response + + def sendJSONRequest(self, urlString, data_): + try: + headers = {'Accept' : 'application/json', + 'Content-Type' : 'application/json', + 'apikey' : self.apiKey} + request = urllib2.Request(urlString, + data_, + headers = headers) + response = urllib2.urlopen(request) + except urllib2.HTTPError as e: + raise AfricasTalkingGatewayException(e.read()) + else: + self.responseCode = response.getcode() + response = ''.join(response.readlines()) + if self.Debug: + print "Raw response: " + response + + return response + + def getApiHost(self): + if self.environment == 'sandbox': + return 'https://api.sandbox.africastalking.com' + else: + return 'https://api.africastalking.com' + + def getPaymentHost(self): + if self.environment == 'sandbox': + return 'https://payments.sandbox.africastalking.com' + else: + return 'https://payments.africastalking.com' + + def getVoiceHost(self): + if self.environment == 'sandbox': + return 'https://voice.sandbox.africastalking.com' + else: + return 'https://voice.africastalking.com' + + def getGenerateAuthTokenUrl(self): + return self.getApiHost() + "/auth-token/generate" + + def getSmsUrl(self): + return self.getApiHost() + "/version1/messaging" + + def getVoiceUrl(self): + return self.getVoiceHost() + + def getSmsSubscriptionUrl(self): + return self.getApiHost() + "/version1/subscription" + + def getUserDataUrl(self): + return self.getApiHost() + "/version1/user" + + def getAirtimeUrl(self): + return self.getApiHost() + "/version1/airtime" + + def getUssdPushUrl(self): + return self.getApiHost() + "/ussd/push/request" + + def getMobilePaymentCheckoutUrl(self): + return self.getPaymentHost() + "/mobile/checkout/request" + + def getMobilePaymentB2CUrl(self): + return self.getPaymentHost() + "/mobile/b2c/request" + + def getMobilePaymentB2BUrl(self): + return self.getPaymentHost() + "/mobile/b2b/request" + + def getPaymentBankWithdrawalUrl(self): + return self.getPaymentHost() + "/bank-withdrawal" + + def getBankPaymentCheckoutChargeUrl(self): + return self.getPaymentHost() + "/bank/checkout/charge" + + def getBankPaymentCheckoutValidationUrl(self): + return self.getPaymentHost() + "/bank/checkout/validate" + + def getBankPaymentTransferUrl(self): + return self.getPaymentHost() + "/bank/transfer" + + def getCardPaymentCheckoutChargeUrl(self): + return self.getPaymentHost() + "/card/checkout/charge" + + def getCardPaymentCheckoutValidationUrl(self): + return self.getPaymentHost() + "/card/checkout/validate" + + def getPaymentStashTopupUrl(self): + return self.getPaymentHost() + "/topup/stash" + + def getPaymentWalletTransferUrl(self): + return self.getPaymentHost() + "/transfer/wallet" + + def getPaymentWalletBalanceQueryUrl(self): + return self.getPaymentHost() + "/query/wallet/balance" + + def getPaymentTransactionFindQueryUrl(self): + return self.getPaymentHost() + "/query/transaction/find" + diff --git a/ussd_app/__init__.py b/ussd_app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ussd_app/admin.py b/ussd_app/admin.py new file mode 100644 index 0000000..543ed74 --- /dev/null +++ b/ussd_app/admin.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.contrib import admin +from . import models +# Register your models here. + +class AccountAdmin(admin.ModelAdmin): + list_display = ['phonenumber', 'balance', 'loan'] +admin.site.register(models.Account, AccountAdmin) + +class TransactionAdmin(admin.ModelAdmin): + list_display = ['account', 'type', 'amount', 'status', 'trans_id'] +admin.site.register(models.Transaction, TransactionAdmin) diff --git a/ussd_app/apps.py b/ussd_app/apps.py new file mode 100644 index 0000000..f3eb1b7 --- /dev/null +++ b/ussd_app/apps.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class UssdAppConfig(AppConfig): + name = 'ussd_app' diff --git a/ussd_app/migrations/0001_initial.py b/ussd_app/migrations/0001_initial.py new file mode 100644 index 0000000..786e90b --- /dev/null +++ b/ussd_app/migrations/0001_initial.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.13 on 2018-06-15 11:34 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Account', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('phonenumber', models.CharField(max_length=25, null=True)), + ('balance', models.IntegerField(null=True)), + ('loan', models.IntegerField(null=True)), + ('reg_date', models.DateField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name='Coperative', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=30, null=True)), + ('phonenumber', models.CharField(max_length=20, null=True)), + ('city', models.CharField(max_length=30, null=True)), + ('reg_date', models.DateField(auto_now_add=True)), + ('level', models.IntegerField(null=True)), + ], + ), + migrations.CreateModel( + name='SessionLevels', + fields=[ + ('session_id', models.CharField(max_length=25, primary_key=True, serialize=False)), + ('level', models.IntegerField(null=True)), + ('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ussd_app.Account')), + ], + ), + migrations.CreateModel( + name='Transaction', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(max_length=30, null=True)), + ('type', models.CharField(max_length=30, null=True)), + ('amount', models.IntegerField(null=True)), + ('date', models.DateField(auto_now_add=True)), + ('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ussd_app.Account')), + ], + ), + migrations.CreateModel( + name='VoiceCall', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(max_length=30, null=True)), + ('amount', models.IntegerField(null=True)), + ('reg_date', models.DateField(auto_now_add=True)), + ('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ussd_app.Account')), + ], + ), + ] diff --git a/ussd_app/migrations/0002_auto_20180615_1306.py b/ussd_app/migrations/0002_auto_20180615_1306.py new file mode 100644 index 0000000..bcd062b --- /dev/null +++ b/ussd_app/migrations/0002_auto_20180615_1306.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.13 on 2018-06-15 13:06 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ussd_app', '0001_initial'), + ] + + operations = [ + migrations.DeleteModel( + name='Coperative', + ), + migrations.RemoveField( + model_name='sessionlevels', + name='account', + ), + migrations.RemoveField( + model_name='voicecall', + name='account', + ), + migrations.AlterField( + model_name='account', + name='balance', + field=models.DecimalField(decimal_places=2, default=0, max_digits=10, null=True), + ), + migrations.AlterField( + model_name='account', + name='loan', + field=models.DecimalField(decimal_places=2, default=0, max_digits=10, null=True), + ), + migrations.AlterField( + model_name='transaction', + name='amount', + field=models.DecimalField(decimal_places=2, max_digits=10, null=True), + ), + migrations.DeleteModel( + name='SessionLevels', + ), + migrations.DeleteModel( + name='VoiceCall', + ), + ] diff --git a/ussd_app/migrations/0003_auto_20180615_1517.py b/ussd_app/migrations/0003_auto_20180615_1517.py new file mode 100644 index 0000000..af7ee29 --- /dev/null +++ b/ussd_app/migrations/0003_auto_20180615_1517.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.13 on 2018-06-15 15:17 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ussd_app', '0002_auto_20180615_1306'), + ] + + operations = [ + migrations.AddField( + model_name='account', + name='account_name', + field=models.CharField(max_length=25, null=True), + ), + migrations.AddField( + model_name='account', + name='account_number', + field=models.CharField(max_length=25, null=True), + ), + migrations.AddField( + model_name='account', + name='bank', + field=models.CharField(max_length=25, null=True), + ), + migrations.AddField( + model_name='account', + name='bank_code', + field=models.IntegerField(max_length=25, null=True), + ), + ] diff --git a/ussd_app/migrations/0004_auto_20180615_1518.py b/ussd_app/migrations/0004_auto_20180615_1518.py new file mode 100644 index 0000000..771da46 --- /dev/null +++ b/ussd_app/migrations/0004_auto_20180615_1518.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.13 on 2018-06-15 15:18 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ussd_app', '0003_auto_20180615_1517'), + ] + + operations = [ + migrations.AlterField( + model_name='account', + name='bank_code', + field=models.IntegerField(null=True), + ), + ] diff --git a/ussd_app/migrations/0005_auto_20180615_1640.py b/ussd_app/migrations/0005_auto_20180615_1640.py new file mode 100644 index 0000000..c8b88ea --- /dev/null +++ b/ussd_app/migrations/0005_auto_20180615_1640.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.13 on 2018-06-15 16:40 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ussd_app', '0004_auto_20180615_1518'), + ] + + operations = [ + migrations.AddField( + model_name='transaction', + name='trans_id', + field=models.CharField(default=1234, max_length=255), + preserve_default=False, + ), + migrations.AlterField( + model_name='transaction', + name='status', + field=models.CharField(choices=[('pending', 'Pending'), ('concluded', 'Concluded')], max_length=30, null=True), + ), + migrations.AlterField( + model_name='transaction', + name='type', + field=models.CharField(choices=[('loan', 'Loan'), ('deposit', 'Deposit')], max_length=30, null=True), + ), + ] diff --git a/ussd_app/migrations/0006_auto_20180615_1658.py b/ussd_app/migrations/0006_auto_20180615_1658.py new file mode 100644 index 0000000..973b84c --- /dev/null +++ b/ussd_app/migrations/0006_auto_20180615_1658.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.13 on 2018-06-15 16:58 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('ussd_app', '0005_auto_20180615_1640'), + ] + + operations = [ + migrations.AlterField( + model_name='transaction', + name='account', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transaction', to='ussd_app.Account'), + ), + ] diff --git a/ussd_app/migrations/0007_auto_20180615_1751.py b/ussd_app/migrations/0007_auto_20180615_1751.py new file mode 100644 index 0000000..ed4daf3 --- /dev/null +++ b/ussd_app/migrations/0007_auto_20180615_1751.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.13 on 2018-06-15 17:51 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ussd_app', '0006_auto_20180615_1658'), + ] + + operations = [ + migrations.AlterField( + model_name='transaction', + name='status', + field=models.CharField(choices=[('pending', 'Pending'), ('concluded', 'Concluded'), ('declined', 'Payed')], max_length=30, null=True), + ), + migrations.AlterField( + model_name='transaction', + name='trans_id', + field=models.CharField(max_length=255, null=True), + ), + ] diff --git a/ussd_app/migrations/__init__.py b/ussd_app/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ussd_app/models.py b/ussd_app/models.py new file mode 100644 index 0000000..739dfbf --- /dev/null +++ b/ussd_app/models.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models + +# Create your models here. +class Account(models.Model): + phonenumber= models.CharField(max_length=25,null=True) + balance= models.DecimalField(null=True, default=0, max_digits=10, decimal_places=2) + bank = models.CharField(max_length=25, null=True) + bank_code = models.IntegerField(null=True) + account_name = models.CharField(max_length=25, null=True) + account_number = models.CharField(max_length=25, null=True) + loan = models.DecimalField(null=True, default=0, max_digits=10, decimal_places=2) + reg_date= models.DateField(auto_now_add=True) + + def __unicode__(self): + return "{} with balance {}".format(self.phonenumber, self.balance) + + @classmethod + def get_current_customer(cls, phonenumber): + try: + customer = cls.objects.get(phonenumber=phonenumber) + except: + customer = None + return customer + + @classmethod + def create_account(cls, phonenumber, sort_code=None, account_number=None, check_status=True): + check = cls.objects.filter(phonenumber=phonenumber).exists() + if check: + return 'Account already exist. \n', False + else: + if not check_status: + cls.objects.create(phonenumber=phonenumber, bank_code=sort_code, account_number=account_number) + return 'Your phone number number has been successfully registerd', True + return 'Does not exist', True + + def get_last_trans(self, **kwargs): + if kwargs['status'] == Transaction.PENDING: + trans = self.transaction.filter(status=Transaction.PENDING).last() + else: + trans = self.transaction.last() + return trans + + def settle_loan(self): + if self.balance > self.loan and self.loan > 0: + self.balance -= self.loan + self.loan = 0 + self.save() + response = "END Loan Repaid was successful,\n" + response += "New Balance: {}\n".format(self.balance) + response += "Loan: {}".format(self.loan) + elif self.loan == 0: + response = "END You have no loan to pay back,\n" + response += "Balance: {}\n".format(self.balance) + response += "Loan: {}".format(self.loan) + elif self.balance > 0 and self.loan > self.balance: + self.loan -= self.balance + self.balance = 0 + self.save() + response = "END Loan Repaid was successful,\n" + response += "New Balance: {}\n".format(self.balance) + response += "Loan: {}".format(self.loan) + else: + response = "END You current balance can not settle your debt,\n" + response += "Please Deposit in your account \n" + response += "New Balance: {}\n".format(self.balance) + response += "Loan: {}".format(self.loan) + return response + +class Transaction(models.Model): + LOAN = 'loan' + DEPOSIT = 'deposit' + PENDING = 'pending' + APPROVED = 'approved' + CONCLUDED = 'concluded' + PAYED = 'payed' + NOT_PAYED = 'not_paid' + DECLINED = 'declined' + + TRANSACTION_TYPES = ( + (LOAN, 'Loan'), + (DEPOSIT, 'Deposit') + ) + STATUS_TYPE = ( + (PENDING, 'Pending'), + (CONCLUDED, 'Concluded'), + (PAYED, 'Payed'), + (NOT_PAYED, 'Not Payed'), + (DECLINED, 'Declined'), + (APPROVED, 'Approved') + ) + trans_id = models.CharField(max_length=255, null=True, default='12345') + status=models.CharField(max_length=30,null=True, choices=STATUS_TYPE) + account = models.ForeignKey(Account, related_name="transaction") + type = models.CharField(max_length=30, null=True, choices=TRANSACTION_TYPES) + amount = models.DecimalField(null=True, max_digits=10, decimal_places=2) + date=models.DateField(auto_now_add=True) + + + def __unicode__(self): + return "{}".format(self.account.phonenumber) + + @classmethod + def record(cls, **kwargs): + if kwargs['type'] == cls.LOAN: + cls.objects.create( + account=kwargs['account'], + type=kwargs['type'], + status=kwargs['status'], + amount=kwargs['amount'] + ) + elif kwargs['type'] == cls.DEPOSIT: + cls.objects.create( + account=kwargs['account'], + type=kwargs['type'], + trans_id=kwargs['trans_id'], + status=kwargs['status'], + amount=kwargs['amount'] + ) + + @classmethod + def check_loan_validity(cls, **kwargs): + try: + trans = cls.objects.get(trans_id=kwargs['access_code'], + account__phonenumber=kwargs['phonenumber'], + status=cls.CONCLUDED) + except: + trans = False + return trans + + def mark_as_paid(self, **kwargs): + self.status = self.CONCLUDED + self.save() + self.account.balance += self.amount + self.account.save() + return self.account.balance, self.account.loan + + def received_loan(self, **kwargs): + self.status = self.APPROVED + self.save() + self.account.loan += self.amount + self.account.save() + return self.account.balance, self.account.loan \ No newline at end of file diff --git a/ussd_app/tests.py b/ussd_app/tests.py new file mode 100644 index 0000000..5982e6b --- /dev/null +++ b/ussd_app/tests.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.test import TestCase + +# Create your tests here. diff --git a/ussd_app/utils.py b/ussd_app/utils.py new file mode 100644 index 0000000..7bab52a --- /dev/null +++ b/ussd_app/utils.py @@ -0,0 +1,305 @@ +import os + +from . import models + +from AfricasTalkingGateway import AfricasTalkingGateway, AfricasTalkingGatewayException + +## CONSTANT +PRODUCT_NAME = 'wazobia' +CURRENCY = 'NGN' + +## MENU +MY_COPERATIVE = '1' +WAZOBIA_LOANS = '2' +JOIN_AGBETUNTU = '3' +REQUEST_A_CALL = '4' + +## SUB MENU +# MY_COPERATIVE SUBMENU +CHECK_BALANCE = '1*1' +REQUEST_LOAN = '2*4' +MAKE_DEPOSIT = '1*3' +# WAZOBIA SUBMENU +REGISTER = '2*1' +REPAY_LOAN = '2*2' +MAKE_DEPOSIT_2 = '2*3' +REQUEST_LOAN_2 = '1*2' +REQUEST_A_CALL_2 = '2*5' +PROCESS_DEPOSIT = "231" + +# REGISTER SUBMENU +WELCOME_USER = '2*1*1' + +class AfricasTalkingUtils: + def __init__(self, **kwargs): + #Specify your credentials + self.username = "sandbox" + self.apiKey = os.environ.get('API_KEY') + self.phonenumber = kwargs.get('phoneNumber', [None])[0] + self.caller_number = kwargs.get('callerNumber', [None])[0] + self.is_active = kwargs.get('isActive', [None])[0] + self.duration_in_seconds = kwargs.get('du=rationInSeconds', [None])[0] + self.currency_code = kwargs.get('currencyCode', [None])[0] + self.amount = kwargs.get('amount', [None])[0] + self.session_id = kwargs.get('sessionId', [None])[0] + self.service_code = kwargs.get('serviceCode', [None])[0] + self.text = kwargs.get('text')[0] + + self.customer = models.Account.get_current_customer(self.phonenumber) + #Create an instance of our awesome gateway class and pass your credentials + self.gateway = AfricasTalkingGateway(self.username, self.apiKey) + + def bank_checkout(self, **kwargs): + # receive fund from customer + try: + # Send the request to the gateway. If successful, we will respond with + # a transactionId that you can then use to validate the transaction + transactionId = self.gateway.bankPaymentCheckoutCharge( + productName_ = PRODUCT_NAME, + currencyCode_ = CURRENCY, + amount_ = kwargs['amount'], + narration_ = kwargs['narration'], + bankAccount_ = { + 'accountName' : kwargs['account_name'], + 'accountNumber' : kwargs['account_num'], + 'bankCode' : kwargs['bank_code'] + }, + metadata_ = { + 'Reason' : 'To Test The Gateways' + } + ) + return transactionId + except AfricasTalkingGatewayException, e: + print 'Encountered an error while sending: %s' % str(e) + + def validate_payment(self, **kwargs): + try: + # Initiate the request with the transacitonId that was returned + # by the charge request. If there are no exceptions, that means + # the transaction was completed successfully + self.gateway.bankPaymentCheckoutValidation( + transactionId_ = kwargs['trans_id'], + otp_ = kwargs['otp'] + ) + return True + + except AfricasTalkingGatewayException, e: + print 'Encountered an error while sending: %s' % str(e) + + def pay_customers(self, **kwargs): + recipients = [dict(bankAccount=dict(accountName=x['account_name'], + accountNumber=x['account_num'], + bankCode=x['bank_code']), + currencyCode='NGN', amount=x['amount'], narration=x['narration'], + metadata=dict(referenceId=x['reference_id'],officeBranch=x['office_branch']) + ) for x in kwargs['recipients_list']] + try: + responses = self.gateway.bankPaymentTransfer( + productName_ = 'wazobia', + recipients_ = recipients + ) + for response in responses: + print "accountNumber=%s;status=%s;" % (response['accountNumber'], + response['status']) + if response['status'] == 'Queued': + print "transactionId=%s;transactionFee=%s;" % (response['transactionId'], + response['transactionFee']) + return True + else: + print "errorMessage=%s;" % response['errorMessage'] + except AfricasTalkingGatewayException, e: + print 'Encountered an error while sending: %s' % str(e) + pass + + def handle_calls(self, **kwargs): + duration = self.duration_in_seconds + try: + if self.is_active == '1': #make the call when isActive is 1 + + caller_number = self.caller_number or self.phonenumber + + # Compose the response + response = '' + response += '' + response += 'Thank you for calling Good bye!' + response += '' + return response + else: + # # Read in call details (duration, cost)+ This flag is set once the call is completed+ + # # Note that the gateway does not expect a response in thie case + currencyCode = self.currency_code + amount = self.amount + # You can then store this information in the database for your records + + except: + print 'exception',duration + + def get_ussd_response(self, **kwargs): + # main menu + if self.text == "": + response = "CON WELCOME, What would you want to check. \r\n" + response += "1. My Cooperative \r\n" + response += "2. Wazobia Loans \r\n" + response += "3. Join Agbetuntun \r\n" + response += "4. Request a Call \r\n" + elif self.text == MY_COPERATIVE: + response = "CON What would you like to do in You Cooperative account. \r\n" + response += "1. Check Balance \r\n" + response += "2. Accept Loan \r\n" + response += "3. Make Deposit \r\n" + elif self.text == WAZOBIA_LOANS: + response = "CON WELCOME to Wazobia Loans \r\n" + response += "1. Register \r\n" + response += "2. Repay Loan \r\n" + response += "3. Make Deposit \r\n" + response += "4. Request Loan \r\n" + response += "5. Request a call \r\n" + + elif self.text.startswith(REQUEST_LOAN): + # request a loan + if self.customer: + if len(self.text.split('*')) == 3: + amount=self.text.split('*')[2] + response = "END An Agent will process your request and get back to you by text, \r\n" + response += "Enjoy Yourself!" + models.Transaction.record( + account=self.customer, + type=models.Transaction.LOAN, + status=models.Transaction.PENDING, + amount=amount + ) + else: + response = "CON Please specify the amount for the loan: \r\n" + else: + response = "END You are not a registered user, please register and try again.\r\n" + + elif self.text.startswith(REQUEST_LOAN_2): + if self.customer: + if len(self.text.split('*')) == 3: + narration = 'Loan from Wazobia group' + access_code=self.text.split('*')[2] + trans = models.Transaction.check_loan_validity( + access_code=access_code, + phonenumber=self.phonenumber) + if trans: + recipients_list = [ + dict(account_name=self.customer.account_name, account_num=self.customer.account_number, + bank_code=self.customer.bank_code, amount=int(trans.amount), narration=narration, reference_id=access_code, office_branch='201'), + ] + pay_status = self.pay_customers(recipients_list=recipients_list) + if pay_status: + balance, loan = trans.received_loan() + response = "END Your Loan Deposit is being processed. You will receive it shortly. \r\n" + response += "Balance: {} \r\n".format(balance) + response += "Loan: {} \r\n".format(loan) + response += "Enjoy Yourself! \r\n" + else: + response = "END Your Request was not successful \r\n" + response += "Please try again later {} \r\n".format(balance) + else: + response = "END Your Access Code is invalid.\r\n" + response += "Please try again.\r\n" + + else: + response = "CON Please enter your loan access code to accept the loan requested.\r\n" + else: + response = "END You are not a registered user, please register and try again.\r\n" + + # sub menu + elif self.text == CHECK_BALANCE : + # return user balance + if self.customer: + response = "END Balance: {}\r\n".format(self.customer.balance) + response += "Loan: {}\r\n".format(self.customer.loan) + else: + response = "END You are not a registered user, please register and try again.\r\n" + + elif self.text.startswith(MAKE_DEPOSIT) or self.text.startswith(MAKE_DEPOSIT_2): + if self.customer: + if len(self.text.split('*')) == 3: + amount=self.text.split('*')[2] + if self.customer: + narration = 'Deposit by {}'.format(self.customer.phonenumber) + deposit = self.bank_checkout( + amount=amount, + narration=narration, + account_name=self.customer.account_name, + account_num=self.customer.account_number, + bank_code=self.customer.bank_code) + if deposit: + models.Transaction.record( + account=self.customer, + type=models.Transaction.DEPOSIT, + trans_id=deposit, + status=models.Transaction.PENDING, + amount=amount + ) + response = "CON Please enter OTP sent to your phone,\r\n"; + else: + response = "END Your Deposite was not successful, please try again \r\n" + + elif len(self.text.split('*')) == 4: + otp = self.text.split('*')[3] + amount = self.text.split('*')[2] + trans = self.customer.get_last_trans( + status=models.Transaction.PENDING) + validate = self.validate_payment(otp=otp, trans_id=trans.trans_id) + if validate: + balance, loan = trans.mark_as_paid(amount=amount) + response = "END Deposit was successful,\r\n" + response += "New Balance: {}\r\n".format(balance) + response += "Loan: {} \r\n".format(loan) + else: + response = "END Your Deposite was not successful, please try again \r\n" + else: + response = "CON Please enter amount: \r\n" + else: + response = "END You are not a registered user, please register and try again.\r\n" + + elif self.text.startswith(REGISTER) or self.text.startswith(JOIN_AGBETUNTU): + balance = '0.00' + resp, check_status = models.Account.create_account(phonenumber=self.phonenumber, check_status=True) + if check_status == False: + balance = self.customer.balance + response = "END {} \r\n".format(resp) + response += "Balance: {} {}\r\n".format(CURRENCY, self.customer.balance) + response += "Loan: {} {}\r\n".format(CURRENCY, self.customer.loan) + else: + if (len(self.text.split('*')) == 1) and self.text[0] == JOIN_AGBETUNTU: + response = "CON Please enter your bank sort code \r\n" + elif (len(self.text.split('*')) == 2) and self.text[0] == JOIN_AGBETUNTU: + response = "CON Please enter your account number: \r\n" + elif(len(self.text.split('*')) == 3) and self.text[0] == JOIN_AGBETUNTU: + sort_code = self.text.split('*')[1] + account_number = self.text.split('*')[2] + resp, check_status = models.Account.create_account(self.phonenumber, + sort_code, account_number, check_status=False) + response = "END Welcome to Agbetuntu \r\n" + response += "{}\r\n".format(resp) + response += "Balance: 0.00\r\n".format(CURRENCY) + response += "Loan: 0.00\r\n".format(CURRENCY) + elif(len(self.text.split('*')) == 3) and self.text[0] == REGISTER.split('*')[0]: + response = "CON Please enter your account number: \r\n" + elif(len(self.text.split('*')) == 2) and self.text[0] == REGISTER.split('*')[0]: + response = "CON Please enter your bank sort code \r\n" + elif (len(self.text.split('*')) == 4) and self.text[0] == REGISTER.split('*')[0]: + sort_code = self.text.split('*')[2] + account_number = self.text.split('*')[3] + resp, check_status = models.Account.create_account(self.phonenumber, + sort_code, account_number, check_status=False) + response = "END Welcome to Agbetuntu \r\n" + response += "{}\r\n".format(resp) + response += "Balance: 0.00\r\n".format(CURRENCY) + response += "Loan: 0.00\r\n".format(CURRENCY) + else: + response = "END Registration failed, \r\n" + response += "Try again later \r\n" + + elif self.text == REQUEST_A_CALL or self.text == REQUEST_A_CALL_2: + response = self.handle_calls() + elif self.text == REPAY_LOAN: + response = self.customer.settle_loan() + else: + response = "CON You selected a wrong option, please try again\r\n" + response += "Your Last Input was {} \r\n".format(self.text) + return response diff --git a/ussd_app/views.py b/ussd_app/views.py new file mode 100644 index 0000000..4d87ad7 --- /dev/null +++ b/ussd_app/views.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +# from django.shortcuts import render +from django.http import HttpResponse +from django.views.decorators.csrf import csrf_exempt +from utils import AfricasTalkingUtils + + +# Create your views here. +@csrf_exempt +def process_ussd(request): + if request.method == 'POST': + africa_talking = AfricasTalkingUtils(**request.POST) + response = africa_talking.get_ussd_response() + else: + response = "Ooops, Sorry... #wink" + + return HttpResponse(response) + +@csrf_exempt +def process_voice(request): + if request.method == 'POST': + africa_talking = AfricasTalkingUtils(**request.POST) + response = africa_talking.handle_calls() + else: + response = 'Ooops, sorry... #wink' + + return HttpResponse(response, content_type='application/xml') \ No newline at end of file