Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LUMSX Payment System - Challan Voucher #1

Merged
merged 1 commit into from
Jan 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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',
},
),
]
8 changes: 8 additions & 0 deletions ecommerce/extensions/basket/models.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
11 changes: 11 additions & 0 deletions ecommerce/extensions/basket/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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()
Expand Down
31 changes: 31 additions & 0 deletions ecommerce/extensions/payment/processors/lumsxpay.py
Original file line number Diff line number Diff line change
@@ -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)
8 changes: 7 additions & 1 deletion ecommerce/extensions/payment/urls.py
Original file line number Diff line number Diff line change
@@ -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'),
Expand Down Expand Up @@ -30,11 +30,17 @@
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'),
url(r'^paypal/', include(PAYPAL_URLS, namespace='paypal')),
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'))
]

167 changes: 167 additions & 0 deletions ecommerce/extensions/payment/views/lumsxpay.py
Original file line number Diff line number Diff line change
@@ -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()
8 changes: 7 additions & 1 deletion ecommerce/settings/_oscar.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/'
Expand Down Expand Up @@ -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,
}
},
}

Expand Down
Loading