diff --git a/config/settings.py b/config/settings.py index 6b507e0..39d1fb4 100644 --- a/config/settings.py +++ b/config/settings.py @@ -42,6 +42,7 @@ 'django.contrib.staticfiles', 'crispy_forms', + 'widget_tweaks', ] MIDDLEWARE = [ @@ -140,3 +141,8 @@ STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static'),] MEDIA_ROOT = os.path.join(BASE_DIR, 'media') + +#M-PESA PAYMENT CONFIGURATIONS +PUBLIC_KEY ='MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArv9yxA69XQKBo24BaF/D+fvlqmGdYjqLQ5WtNBb5tquqGvAvG3WMFETVUSow/LizQalxj2ElMVrUmzu5mGGkxK08bWEXF7a1DEvtVJs6nppIlFJc2SnrU14AOrIrB28ogm58JjAl5BOQawOXD5dfSk7MaAA82pVHoIqEu0FxA8BOKU+RGTihRU+ptw1j4bsAJYiPbSX6i71gfPvwHPYamM0bfI4CmlsUUR3KvCG24rB6FNPcRBhM3jDuv8ae2kC33w9hEq8qNB55uw51vK7hyXoAa+U7IqP1y6nBdlN25gkxEA8yrsl1678cspeXr+3ciRyqoRgj9RD/ONbJhhxFvt1cLBh+qwK2eqISfBb06eRnNeC71oBokDm3zyCnkOtMDGl7IvnMfZfEPFCfg5QgJVk1msPpRvQxmEsrX9MQRyFVzgy2CWNIb7c+jPapyrNwoUbANlN8adU1m6yOuoX7F49x+OjiG2se0EJ6nafeKUXw/+hiJZvELUYgzKUtMAZVTNZfT8jjb58j8GVtuS+6TM2AutbejaCV84ZK58E2CRJqhmjQibEUO6KPdD7oTlEkFy52Y1uOOBXgYpqMzufNPmfdqqqSM4dU70PO8ogyKGiLAIxCetMjjm6FCMEA3Kc8K0Ig7/XtFm9By6VxTJK1Mg36TlHaZKP6VzVLXMtesJECAwEAAQ==' + +API_KEY = 'e0dlIJMW6qrit7c9XpP1olfhqgHQ05sw' \ No newline at end of file diff --git a/membership/__init__.py b/membership/__init__.py index e69de29..e5a7af4 100644 --- a/membership/__init__.py +++ b/membership/__init__.py @@ -0,0 +1 @@ +default_app_config = 'membership.apps.MembershipConfig' \ No newline at end of file diff --git a/membership/apps.py b/membership/apps.py index 537f521..2eefe56 100644 --- a/membership/apps.py +++ b/membership/apps.py @@ -1,5 +1,9 @@ from django.apps import AppConfig +from django.utils.translation import ugettext_lazy as _ #added class MembershipConfig(AppConfig): name = 'membership' + + def ready(self): + import membership.signals #added diff --git a/membership/forms.py b/membership/forms.py index 21150dd..f811c95 100644 --- a/membership/forms.py +++ b/membership/forms.py @@ -1,3 +1,4 @@ +import re from django import forms from .models import Payment @@ -6,4 +7,12 @@ class PaymentForm(forms.ModelForm): class Meta: model = Payment - fields = ['phone'] \ No newline at end of file + fields = ['phone'] + + def clean_phone(self): + phone = self.cleaned_data.get('phone') + x = re.search("^2557[0-9]{8}$", phone) + + if not x: + raise forms.ValidationError("Phone number must in format 2557xxxxxxxx") + return phone \ No newline at end of file diff --git a/membership/migrations/0003_auto_20201015_2212.py b/membership/migrations/0003_auto_20201015_2212.py new file mode 100644 index 0000000..b31a944 --- /dev/null +++ b/membership/migrations/0003_auto_20201015_2212.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2020-10-15 19:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('membership', '0002_business_user'), + ] + + operations = [ + migrations.AlterField( + model_name='payment', + name='phone', + field=models.CharField(max_length=12, verbose_name='Enter your M-PESA mobile number'), + ), + ] diff --git a/membership/migrations/0004_auto_20201015_2227.py b/membership/migrations/0004_auto_20201015_2227.py new file mode 100644 index 0000000..7ce556f --- /dev/null +++ b/membership/migrations/0004_auto_20201015_2227.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2020-10-15 19:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('membership', '0003_auto_20201015_2212'), + ] + + operations = [ + migrations.AlterField( + model_name='payment', + name='phone', + field=models.CharField(help_text='example. 255700000000', max_length=12, verbose_name='Enter your M-PESA mobile number'), + ), + ] diff --git a/membership/migrations/0005_auto_20201015_2239.py b/membership/migrations/0005_auto_20201015_2239.py new file mode 100644 index 0000000..9a5c929 --- /dev/null +++ b/membership/migrations/0005_auto_20201015_2239.py @@ -0,0 +1,25 @@ +# Generated by Django 3.1 on 2020-10-15 19:39 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('membership', '0004_auto_20201015_2227'), + ] + + operations = [ + migrations.AddField( + model_name='business', + name='created_at', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='business', + name='modified_at', + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/membership/migrations/0006_auto_20201015_2336.py b/membership/migrations/0006_auto_20201015_2336.py new file mode 100644 index 0000000..25494f0 --- /dev/null +++ b/membership/migrations/0006_auto_20201015_2336.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1 on 2020-10-15 20:36 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('membership', '0005_auto_20201015_2239'), + ] + + operations = [ + migrations.AlterField( + model_name='subscription', + name='plan', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='membership.plan'), + ), + ] diff --git a/membership/models.py b/membership/models.py index 5725350..94d4950 100644 --- a/membership/models.py +++ b/membership/models.py @@ -6,13 +6,15 @@ class Business(models.Model): + created_at = models.DateTimeField(auto_now_add=True) + modified_at = models.DateTimeField(auto_now=True) user = models.OneToOneField(User, on_delete=models.PROTECT) name = models.CharField(max_length=100) location = models.CharField(max_length=100) reference_no = models.CharField(max_length=150, blank=True, null=True) #payment gateway def __str__(self): - return self.name + return self.user.email class Plan(models.Model): @@ -32,7 +34,7 @@ def __str__(self): class Subscription(models.Model): created_at = models.DateTimeField(auto_now_add=True) modified_at = models.DateTimeField(auto_now=True) - plan = models.OneToOneField(Plan, on_delete=models.PROTECT) + plan = models.ForeignKey(Plan, on_delete=models.PROTECT) business = models.OneToOneField(Business, on_delete=models.PROTECT) start_time = models.DateTimeField() ends_time = models.DateTimeField() @@ -55,7 +57,7 @@ class Payment(models.Model): created_at = models.DateTimeField(auto_now_add=True) modified_at = models.DateTimeField(auto_now=True) subscription = models.OneToOneField(Subscription, on_delete=models.PROTECT) - phone = models.CharField(max_length=12) + phone = models.CharField(max_length=12, verbose_name='Enter your M-PESA mobile number', help_text='example. 255700000000') transactionID = models.CharField(max_length=100, blank=True, null=True) conversationID = models.CharField(max_length=100, blank=True, null=True) diff --git a/membership/signals.py b/membership/signals.py new file mode 100644 index 0000000..6a42e61 --- /dev/null +++ b/membership/signals.py @@ -0,0 +1,12 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver +from .models import Business + + +@receiver(post_save, sender=Business) +def business_reference_no(sender, instance, created, **kwargs): + if created: + reference_no = str(instance.id) + str(instance.created_at.hour) + str(instance.created_at.minute) + str(instance.created_at.year)[-2:] + + Business.objects.filter(pk=instance.pk).update(reference_no=reference_no) + print("reference_no:", reference_no) \ No newline at end of file diff --git a/membership/templates/membership/payment.html b/membership/templates/membership/payment.html index 19bd144..bd42521 100644 --- a/membership/templates/membership/payment.html +++ b/membership/templates/membership/payment.html @@ -1,6 +1,6 @@ {% extends 'base.html' %} {% load crispy_forms_tags %} - +{% load widget_tweaks %} {% block content %}
@@ -17,10 +17,22 @@ {% endif %}
-
+ {% csrf_token %} - {{form|crispy}} + {% for hidden in form.hidden_fields %} + {{ hidden }} + {% endfor %} + + {% for field in form.visible_fields %} +
+ + {{form.phone|add_class:'form-control'|attr:'placeholder:Phone (eg. 255700000000)'}} + {% for error in field.errors %} + {{ error }} + {% endfor %} +
+ {% endfor %}
diff --git a/membership/views.py b/membership/views.py index 575e909..3441fef 100644 --- a/membership/views.py +++ b/membership/views.py @@ -1,11 +1,20 @@ +import datetime +from django.utils import timezone from django.shortcuts import render from django.http import HttpResponseRedirect from django.urls import reverse from django.views.generic import TemplateView, ListView from django.contrib import messages -from .models import Plan, Subscription +from .models import Plan, Subscription, Business from .forms import PaymentForm +#Vodacom mpesa intergrations +from django.conf import settings +from portalsdk import APIContext, APIMethodType, APIRequest +from time import sleep + +from datetime import datetime, timedelta, date + def get_user_plan(request): user_plan_qs = Subscription.objects.filter(business=request.user.business) @@ -61,21 +70,133 @@ def post(self, request, *args, **kwargs): def paymentView(request): selected_plan = get_selected_plan(request) - plans = Plan.objects.get(name=selected_plan) - - + business = Business.objects.get(user=request.user) + reference_no = business.reference_no + print('reference_no:', reference_no) if request.method == 'POST': form = PaymentForm(request.POST) if form.is_valid(): - form.save() - return HttpResponse('Payment successfully') + #Begin payment processing + public_key = settings.PUBLIC_KEY + + # Create Context with API to request a Session ID + api_context = APIContext() + + # Api key + api_context.api_key = settings.API_KEY + + # Public key + api_context.public_key = public_key + + # Use ssl/https + api_context.ssl = True + + # Method type (can be GET/POST/PUT) + api_context.method_type = APIMethodType.GET + + # API address + api_context.address = 'openapi.m-pesa.com' + + # API Port + api_context.port = 443 + + # API Path + api_context.path = '/sandbox/ipg/v2/vodacomTZN/getSession/' + + # Add/update headers + api_context.add_header('Origin', '*') + + # Parameters can be added to the call as well that on POST will be in JSON format and on GET will be URL parameters + # api_context.add_parameter('key', 'value') + + #Do the API call and put result in a response packet + api_request = APIRequest(api_context) + + # Do the API call and put result in a response packet + result = None + try: + result = api_request.execute() + except Exception as e: + print('Call Failed: ' + e) + + if result is None: + raise Exception('SessionKey call failed to get result. Please check.') + + # Display results + print(result.status_code) + print(result.headers) + print(result.body) + + # The above call issued a sessionID + api_context = APIContext() + api_context.api_key = result.body['output_SessionID'] + api_context.public_key = public_key + api_context.ssl = True + api_context.method_type = APIMethodType.POST + api_context.address = 'openapi.m-pesa.com' + api_context.port = 443 + api_context.path = '/sandbox/ipg/v2/vodacomTZN/c2bPayment/singleStage/' + api_context.add_header('Origin', '*') + + #Input Variables + amount = plans.price + phone = request.POST.get('phone') + desc = plans.name + + api_context.add_parameter('input_Amount', amount) + api_context.add_parameter('input_Country', 'TZN') + api_context.add_parameter('input_Currency', 'TZS') + api_context.add_parameter('input_CustomerMSISDN', '000000000001') #phone number from customer + api_context.add_parameter('input_ServiceProviderCode', '000000') + api_context.add_parameter('input_ThirdPartyConversationID', 'asv02e5958774f7ba228d83d0d689761') + api_context.add_parameter('input_TransactionReference', reference_no) + api_context.add_parameter('input_PurchasedItemsDesc', desc) + + api_request = APIRequest(api_context) + + sleep(30) + + result = None + + try: + result = api_request.execute() + except Exception as e: + print('Call Failed: ' + e) + + if result is None: + raise Exception('API call failed to get result. Please check.') + + print(result.status_code) + print(result.headers) + print(result.body) + + if result.body['output_ResponseCode'] == 'INS-0': + + ends_time = timezone.now() + timedelta(days=plans.duration_days) + Subscription.objects.filter(business=request.user.business).update( + plan=plans.id, + start_time = timezone.now(), + ends_time = ends_time, + paid_status = True, + ) + subscription = Subscription.objects.filter(business=request.user.business) + + #save transactionID,transactionID + payment = form.save(commit=False) + payment.subscription = subscription + payment.transactionID = result.body['output_TransactionID'] + payment.conversationID = result.body['output_ConversationID'] + payment.save() + + return HttpResponse('Your Payment was Successfully sent!') + else: form = PaymentForm() - context = {'form':form, 'plans':plans} + context = {'form':form, 'plans':plans} return render(request, 'membership/payment.html', context)