diff --git a/ecommerce/extensions/basket/migrations/0012_basketchallanvoucher.py b/ecommerce/extensions/basket/migrations/0012_basketchallanvoucher.py new file mode 100644 index 00000000000..b1a57353227 --- /dev/null +++ b/ecommerce/extensions/basket/migrations/0012_basketchallanvoucher.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.29 on 2021-01-13 14:02 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import django_extensions.db.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('basket', '0011_add_email_basket_attribute_type'), + ] + + operations = [ + migrations.CreateModel( + name='BasketChallanVoucher', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')), + ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')), + ('voucher_number', models.CharField(max_length=32, unique=True)), + ('is_paid', models.BooleanField(default=False)), + ('due_date', models.DateTimeField()), + ('basket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='basket.Basket', verbose_name='Basket')), + ], + options={ + 'ordering': ('-modified', '-created'), + 'abstract': False, + 'get_latest_by': 'modified', + }, + ), + ] diff --git a/ecommerce/extensions/basket/models.py b/ecommerce/extensions/basket/models.py index 4c2a1ed5c02..3c0b9b0e712 100644 --- a/ecommerce/extensions/basket/models.py +++ b/ecommerce/extensions/basket/models.py @@ -1,5 +1,6 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ +from django_extensions.db.models import TimeStampedModel from edx_django_utils.cache import DEFAULT_REQUEST_CACHE from oscar.apps.basket.abstract_models import AbstractBasket from oscar.core.loading import get_class @@ -125,3 +126,10 @@ class Meta(object): # noinspection PyUnresolvedReferences from oscar.apps.basket.models import * # noqa isort:skip pylint: disable=wildcard-import,unused-wildcard-import,wrong-import-position,wrong-import-order,ungrouped-imports + + +class BasketChallanVoucher(TimeStampedModel): + basket = models.ForeignKey('basket.Basket', verbose_name=_("Basket"), on_delete=models.CASCADE) + voucher_number = models.CharField(max_length=32, unique=True) + is_paid = models.BooleanField(default=False, null=False) + due_date = models.DateTimeField(null=False) diff --git a/ecommerce/extensions/basket/views.py b/ecommerce/extensions/basket/views.py index d6a14746412..c446b1fe4d3 100644 --- a/ecommerce/extensions/basket/views.py +++ b/ecommerce/extensions/basket/views.py @@ -42,6 +42,7 @@ from ecommerce.extensions.partner.shortcuts import get_partner_for_site from ecommerce.extensions.payment.constants import CLIENT_SIDE_CHECKOUT_FLAG_NAME from ecommerce.extensions.payment.forms import PaymentForm +from ecommerce.extensions.payment.views.lumsxpay import LumsxpayExecutionView BasketAttribute = get_model('basket', 'BasketAttribute') BasketAttributeType = get_model('basket', 'BasketAttributeType') @@ -387,6 +388,16 @@ def get(self, request, *args, **kwargs): if has_enterprise_offer(basket) and basket.total_incl_tax == Decimal(0): return redirect('checkout:free-checkout') else: + + # lumsx is giving a thirdparty method for payment rather than a gateway so had to make a minimal + # processor and integerate the API, if client side processor matches with the site configurations + # than move forward towards API + configuration_helpers = request.site.siteconfiguration.edly_client_theme_branding_settings + custom_processor_name = configuration_helpers.get('PAYMENT_PROCESSOR_NAME') + if custom_processor_name == self.request.site.siteconfiguration.client_side_payment_processor: + # return LumsxpayExecutionView.get_voucher_api(request) + return redirect_to_referrer(self.request, 'lumsxpay:execute') + return super(BasketSummaryView, self).get(request, *args, **kwargs) @newrelic.agent.function_trace() diff --git a/ecommerce/extensions/payment/processors/lumsxpay.py b/ecommerce/extensions/payment/processors/lumsxpay.py new file mode 100644 index 00000000000..86c3822d35f --- /dev/null +++ b/ecommerce/extensions/payment/processors/lumsxpay.py @@ -0,0 +1,31 @@ +""" Paystack payment processor. """ +from __future__ import absolute_import, unicode_literals + +import logging + +import waffle +from django.conf import settings + +logger = logging.getLogger(__name__) + + +class Lumsxpay(): + """ + Just a bare structure of processor to register its name, no legacy methods added or will be used as LUMSX + payment method is thirdparty and cannot be integerated + """ + NAME = 'lumsxpay' + + def __init__(self, site): + self.site = site + + @property + def payment_processor(self): + return Lumsxpay(self.request.site) + + @classmethod + def is_enabled(cls): + """ + Returns True if this payment processor is enabled, and False otherwise. + """ + return waffle.switch_is_active(settings.PAYMENT_PROCESSOR_SWITCH_PREFIX + cls.NAME) diff --git a/ecommerce/extensions/payment/urls.py b/ecommerce/extensions/payment/urls.py index b6b1f8f6ff7..5208d601ca7 100644 --- a/ecommerce/extensions/payment/urls.py +++ b/ecommerce/extensions/payment/urls.py @@ -1,6 +1,6 @@ from django.conf.urls import include, url -from ecommerce.extensions.payment.views import PaymentFailedView, SDNFailure, authorizenet, cybersource, paypal, stripe +from ecommerce.extensions.payment.views import PaymentFailedView, SDNFailure, authorizenet, cybersource, paypal, stripe, lumsxpay CYBERSOURCE_APPLE_PAY_URLS = [ url(r'^authorize/$', cybersource.CybersourceApplePayAuthorizationView.as_view(), name='authorize'), @@ -30,6 +30,10 @@ url(r'^redirect/$', authorizenet.handle_redirection, name='redirect'), ] +LUMSXPAY_URLS = [ + url(r'^execute/$', lumsxpay.LumsxpayExecutionView.as_view(), name='execute'), +] + urlpatterns = [ url(r'^cybersource/', include(CYBERSOURCE_URLS, namespace='cybersource')), url(r'^error/$', PaymentFailedView.as_view(), name='payment_error'), @@ -37,4 +41,6 @@ url(r'^sdn/', include(SDN_URLS, namespace='sdn')), url(r'^stripe/', include(STRIPE_URLS, namespace='stripe')), url(r'^authorizenet/', include(AUTHORIZENET_URLS, namespace='authorizenet')), + url(r'^lumsxpay/', include(LUMSXPAY_URLS, namespace='lumsxpay')) ] + diff --git a/ecommerce/extensions/payment/views/lumsxpay.py b/ecommerce/extensions/payment/views/lumsxpay.py new file mode 100644 index 00000000000..449da01d8f6 --- /dev/null +++ b/ecommerce/extensions/payment/views/lumsxpay.py @@ -0,0 +1,167 @@ +""" View for interacting with the LumsxPay payment processor. """ + +from __future__ import unicode_literals + +import json +import logging +import requests +import datetime + +from django.contrib.auth.views import redirect_to_login +from django.db import transaction +from django.http import HttpResponseNotFound +from django.shortcuts import render_to_response +from django.utils.decorators import method_decorator +from django.views.generic import View +from oscar.core.loading import get_class, get_model + +from ecommerce.extensions.checkout.mixins import EdxOrderPlacementMixin +from ecommerce.extensions.payment.processors.lumsxpay import Lumsxpay +from ecommerce.extensions.basket.models import BasketChallanVoucher +from ecommerce.core.url_utils import get_lms_dashboard_url + +logger = logging.getLogger(__name__) + +Basket = get_model('basket', 'Basket') +Product = get_model('catalogue', 'Product') + + +class LumsxpayExecutionView(EdxOrderPlacementMixin, View): + @property + def payment_processor(self): + return Lumsxpay(self.request.site) + + @method_decorator(transaction.non_atomic_requests) + def dispatch(self, request, *args, **kwargs): + return super(LumsxpayExecutionView, self).dispatch(request, *args, **kwargs) + + def extract_items_from_basket(self, basket): + return [ + { + "title": l.product.title, + "amount": str(l.line_price_incl_tax), + "id": l.product.course_id} + for l in basket.all_lines() + ] + + def get_existing_basket(self, request): + courses = [i['id'] for i in self.extract_items_from_basket(request.basket)] + course_ids = [] + + for course in courses: + product = Product.objects.filter(structure='child', course_id=course).first() + if product: + course_ids.append(product.id) + + return Basket.objects.filter(owner_id=request.user, lines__product_id__in=course_ids).first() + + def get_due_date(self, configuration_helpers): + due_date_span_in_weeks = configuration_helpers.get('PAYMENT_DUE_DATE_SPAN', 52) + due_date = datetime.datetime.now() + datetime.timedelta(weeks=due_date_span_in_weeks) + return due_date.strftime("%Y-%m-%d %H:%M:%S%z") + + def fetch_context(self, request, response, configuration_helpers): + voucher_details = response.json() + voucher_data = voucher_details.get('data', {}) + url_for_online_payment = voucher_data.get("url_for_online_payment") + url_for_download_voucher = voucher_data.get("url_for_download_voucher") + return { + 'configuration_helpers': configuration_helpers, + 'url_for_online_payment': url_for_online_payment, + 'url_for_download_voucher': url_for_download_voucher, + 'items_list': voucher_data.get('items'), + "name": request.user.username, + "email": request.user.email, + "order_id": request.basket.order_number, + "user": request.user, + "lms_dashboard_url": get_lms_dashboard_url, + "is_paid": False, + "support_email": request.site.siteconfiguration.payment_support_email, + } + + def request_already_existing_challan(self, request): + challan_basket = BasketChallanVoucher.objects.filter(basket=self.get_existing_basket(request)).first() + + configuration_helpers = request.site.siteconfiguration.edly_client_theme_branding_settings + url = '{}/{}'.format(configuration_helpers.get('LUMSXPAY_VOUCHER_API_URL'), challan_basket.voucher_number) + headers = { + "Authorization": configuration_helpers.get('PAYMENT_AUTHORIZATION_KEY'), + "Content-Type": "application/json" + } + + response = requests.get(url, headers=headers) + if response.status_code == 200: + return self.fetch_context(request, response, configuration_helpers) + + return {} + + def get(self, request): + configuration_helpers = request.site.siteconfiguration.edly_client_theme_branding_settings + url = configuration_helpers.get('LUMSXPAY_VOUCHER_API_URL') + + if request.user.is_anonymous or not (url and request.basket): + msg = 'user is anonymous cannot proceed to checkout page so redirecting to login. ' + logger.info(msg) + + if not url: + msg = 'Site configuration in ecommerce does not include payment API url' + logger.info(msg) + + return redirect_to_login(get_lms_dashboard_url) + + if BasketChallanVoucher.objects.filter(basket=self.get_existing_basket(request)).exists(): + context = self.request_already_existing_challan(request) + if not context: + logger.exception('challan status API not working, no context found') + return HttpResponseNotFound() + + return render_to_response('payment/lumsxpay.html', context) + + items = self.extract_items_from_basket(request.basket) + payload = { + "name": request.user.username, + "email": request.user.email, + "order_id": request.basket.order_number, + "items": items, + "due_date": self.get_due_date(configuration_helpers) + } + + headers = { + "Authorization": configuration_helpers.get('PAYMENT_AUTHORIZATION_KEY'), + "Content-Type": "application/json" + } + + try: + response = requests.post(url, data=json.dumps(payload), headers=headers) + except: + logger.exception('Challan generation API not working and cannot be reached.') + return HttpResponseNotFound() + + if response.status_code == 200: + voucher_details = response.json() + context = self.fetch_context(request, response, configuration_helpers) + + voucher_number = voucher_details['data']['voucher_id'] + due_date = voucher_details['data']['due_date'] + + _, created = BasketChallanVoucher.objects.get_or_create( + basket=request.basket, + voucher_number=voucher_number, + due_date=due_date, + is_paid=False + ) + + if created: + logger.info('challan-basket created with voucher number %s and due date %s', voucher_number, due_date) + else: + logger.exception('could not create the challan voucher entry in DB') + return HttpResponseNotFound() + + return render_to_response('payment/lumsxpay.html', context) + + logger.exception( + 'Challan creation API status return %s status code and challan creation failed with response %s', + response.status_code, response.json() + ) + + return HttpResponseNotFound() diff --git a/ecommerce/settings/_oscar.py b/ecommerce/settings/_oscar.py index 3e5f4fc6b58..74735dcb66f 100644 --- a/ecommerce/settings/_oscar.py +++ b/ecommerce/settings/_oscar.py @@ -105,6 +105,7 @@ 'ecommerce.extensions.payment.processors.paypal.Paypal', 'ecommerce.extensions.payment.processors.stripe.Stripe', 'ecommerce.extensions.payment.processors.authorizenet.AuthorizeNet', + 'ecommerce.extensions.payment.processors.lumsxpay.Lumsxpay', ) PAYMENT_PROCESSOR_RECEIPT_PATH = '/checkout/receipt/' @@ -144,7 +145,12 @@ 'merchant_auth_name': None, 'transaction_key': None, 'redirect_url': None - } + }, + 'lumsxpay': { + 'cancel_checkout_path': PAYMENT_PROCESSOR_CANCEL_PATH, + 'public_key': None, + 'private_key': None, + } }, } diff --git a/ecommerce/templates/payment/lumsxpay.html b/ecommerce/templates/payment/lumsxpay.html new file mode 100644 index 00000000000..b81bba985d3 --- /dev/null +++ b/ecommerce/templates/payment/lumsxpay.html @@ -0,0 +1,123 @@ +{% extends "edx/base.html" %} +{% load i18n %} +{% block title %}Bootstrap Demo{% endblock title %} + + +{% block content %} + +
+
+ Payment clearance to the course might take upto {{ configuration_helpers.CRON_DELAY_TIME }} minutes. +
+ +
+
+
+
In Your Cart
+
+
+
Your purchases are the following:
+
+
+ + + + + + + + + + {% for item in items_list %} + + + {% endfor %} + + +
Product NamePrice
+ {{ item.title }} + + {{ item.amount }} +
+
+
+ +
+
+ +
+

+ + Download the Challan Voucher for payments through, Cash, Pay Order, Bank Drafts, ATM machines, + Internet banking portals at selected banks in Pakistan +

+
+
+
+ +
+

Next Steps

+
+ A confirmation email will be sent to you when you have made the payment and been enrolled in the course. + Please follow the steps in this email to access the course.

+ Alternatively, after making the payment you go to your Dashboard to view the course. Please note that it + may take up to {{ configuration_helpers.CRON_DELAY_TIME }} minutes for your course to be made available to you. Kindly contact + {{ support_email }} for further queries +
+
+
+ View my courses +
+ + +
+ + + + + + + +{% endblock %}