Skip to content

Commit

Permalink
Implement Authorizenet Client Side Processor - EDLY-5792 (#133)
Browse files Browse the repository at this point in the history
Co-authored-by: Taimoor  Ahmed <taimoor.ahmed@A006-00933.local>
  • Loading branch information
taimoor-ahmed-1 and Taimoor Ahmed authored Jul 31, 2023
1 parent 4159f58 commit 87490ee
Show file tree
Hide file tree
Showing 9 changed files with 426 additions and 3 deletions.
4 changes: 4 additions & 0 deletions build.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@
{
name: 'js/views/cowpay',
exclude: ['js/common']
},
{
name: 'js/views/authorizenet',
exclude: ['js/common']
}
]
})
108 changes: 108 additions & 0 deletions ecommerce/extensions/payment/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,3 +319,111 @@ def clean_basket(self):
Applicator().apply(basket, self.request.user, self.request)

return basket


class AuthorizenetPaymentForm(forms.Form):
"""
Payment form for Authorizenet with billing details.
This form captures the data necessary to complete a payment transaction with Authorizenet.
"""
def __init__(self, user, request, *args, **kwargs):
super(AuthorizenetPaymentForm, self).__init__(*args, **kwargs)
self.request = request
self.basket_has_enrollment_code_product = any(
line.product.is_enrollment_code_product for line in self.request.basket.all_lines()
)
update_basket_queryset_filter(self, user)

self.helper = FormHelper(self)
self.helper.layout = Layout(
Div('basket'),
Div(
Div('full_name'),
HTML('<p class="help-block-name"></p>'),
css_class='form-item col-md-12'
),
Div(
Div('card_number'),
HTML('<p class="help-block-card"></p>'),
css_class='form-item col-md-12'
),
Div(
Div('card_code', css_class='form-item col-md-4'),
Div('expiry_month', css_class='form-item col-md-4'),
Div('expiry_year', css_class='form-item col-md-4'),
HTML('<p class="help-block-expiry"></p>'),
css_class='row'
),
Div(
HTML('<input type="hidden" name="data_value" id="id_data_value" />'),
css_class='form-item col-md-12'
),
Div(
HTML('<input type="hidden" name="data_descriptor" id="id_data_descriptor" />'),
HTML('<div class="authorizenet-error"></div>'),
css_class='form-item col-md-12'
),
)

for bound_field in list(self):
# https://www.w3.org/WAI/tutorials/forms/validation/#validating-required-input
if hasattr(bound_field, 'field') and bound_field.field.required:
# Translators: This is a string added next to the name of the required
# fields on the payment form. For example, the first name field is
# required, so this would read "First name (required)".
self.fields[bound_field.name].label = _('{label} (required)').format(label=bound_field.label)
bound_field.field.widget.attrs['required'] = 'required'

if self.basket_has_enrollment_code_product and 'organization' not in self.fields:
# If basket has any enrollment code items then we will add an organization
# field next to "last_name."
self.fields['organization'] = forms.CharField(max_length=60, label=_('Organization (required)'))
organization_div = Div(
Div(
Div('organization'),
HTML('<p class="help-block"></p>'),
css_class='form-item col-md-6'
),
css_class='row'
)
self.helper.layout.fields.insert(list(self.fields.keys()).index('last_name') + 1, organization_div)
# Purchased on behalf of an enterprise or for personal use
self.fields[PURCHASER_BEHALF_ATTRIBUTE] = forms.BooleanField(
required=False,
label=_('I am purchasing on behalf of my employer or other professional organization')
)
purchaser_div = Div(
Div(
Div(PURCHASER_BEHALF_ATTRIBUTE),
HTML('<p class="help-block"></p>'),
css_class='form-item col-md-12'
),
css_class='row'
)
self.helper.layout.fields.insert(list(self.fields.keys()).index('organization') + 1, purchaser_div)

basket = forms.ModelChoiceField(
queryset=Basket.objects.all(),
widget=forms.HiddenInput(),
required=False,
error_messages={
'invalid_choice': _('There was a problem retrieving your basket. Refresh the page to try again.'),
}
)
full_name = forms.CharField(max_length=60, label=_('Full Name'))
card_number = forms.CharField(max_length=16, required=False, label=_('Card Number'))
card_code = forms.CharField(max_length=4, required=False, label=_('CVV'))
expiry_month = forms.CharField(max_length=60, required=False, label=_('Expiry Month (mm)'))
expiry_year = forms.CharField(max_length=60, required=False, label=_('Expiry Year (yy)'))
data_descriptor = forms.CharField(max_length=255)
data_value = forms.CharField(max_length=255)

def clean_basket(self):
basket = self.cleaned_data['basket']

if basket:
basket.strategy = self.request.strategy
Applicator().apply(basket, self.request.user, self.request)

return basket
149 changes: 148 additions & 1 deletion ecommerce/extensions/payment/processors/authorizenet.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
""" AuthorizeNet payment processor. """
from __future__ import absolute_import, unicode_literals

import crum
import json
import logging
from urllib.parse import quote
Expand All @@ -14,7 +15,7 @@
from authorizenet.constants import constants
from django.urls import reverse
from oscar.apps.payment.exceptions import GatewayError
from oscar.core.loading import get_model
from oscar.core.loading import get_class, get_model

from ecommerce.core.url_utils import get_ecommerce_url
from ecommerce.extensions.payment.exceptions import (
Expand All @@ -23,10 +24,12 @@
PaymentProcessorResponseNotFound,
RefundError
)
from ecommerce.extensions.payment.forms import AuthorizenetPaymentForm
from ecommerce.extensions.payment.processors import BaseClientSidePaymentProcessor, HandledProcessorResponse
from ecommerce.extensions.payment.utils import LxmlObjectJsonEncoder

logger = logging.getLogger(__name__)
Applicator = get_class('offer.applicator', 'Applicator')
PaymentProcessorResponse = get_model('payment', 'PaymentProcessorResponse')

AUTH_CAPTURE_TRANSACTION_TYPE = "authCaptureTransaction"
Expand Down Expand Up @@ -353,3 +356,147 @@ def issue_credit(self, order_number, basket, reference_number, amount, currency)
order_number)
logger.exception(msg)
raise RefundError(msg)


class AuthorizenetClient(BaseClientSidePaymentProcessor):
NAME = 'authorizenetclient'
template_name = 'payment/authorizenet.html'

def __init__(self, site):
"""
Constructs a new instance of the Authorizenet Client processor.
Raises:
KeyError: If no settings configured for this payment processor.
"""
super(AuthorizenetClient, self).__init__(site)
self.request = crum.get_current_request()
configuration = self.configuration
self.client_key = configuration['client_key']
self.base_url = configuration['base_url']
self.api_login_id = configuration['api_login_id']
self.transaction_key = configuration['transaction_key']
self.authorizenet_production_mode = configuration['production_mode']

@property
def authorizenet_form(self):
return AuthorizenetPaymentForm(
user=self.request.user,
request=self.request,
initial={'basket': self.request.basket},
label_suffix=''
)

def get_transaction_parameters(self, basket, request=None, **kwargs):
basket.strategy = self.request.strategy
Applicator().apply(basket, self.request.user, self.request)

merchant_auth = apicontractsv1.merchantAuthenticationType()
merchant_auth.name = self.api_login_id
merchant_auth.transactionKey = self.transaction_key

# Create the payment object for a payment nonce
opaque_data = apicontractsv1.opaqueDataType()
opaque_data.dataDescriptor = kwargs['data_descriptor']
opaque_data.dataValue = kwargs['data_value']

# Add the payment data to a paymentType object
payment_one = apicontractsv1.paymentType()
payment_one.opaqueData = opaque_data

# Create order information
order = apicontractsv1.orderType()
order.invoiceNumber = basket.order_number
order.description = '{} - {}: {}'.format(
basket.order_number,
basket.site.partner.name,
basket.all_lines().first().product.title
)

# Set the customer's Bill To address
owner = basket.owner
customer_address = apicontractsv1.customerAddressType()
customer_address.firstName = owner.first_name
customer_address.lastName = owner.last_name

# Set the customer's identifying information
customer_data = apicontractsv1.customerDataType()
customer_data.type = "individual"
customer_data.id = str(owner.id)
customer_data.email = owner.email

# Add values for transaction settings
duplicate_window_setting = apicontractsv1.settingType()
duplicate_window_setting.settingName = "duplicateWindow"
duplicate_window_setting.settingValue = "600"
settings = apicontractsv1.ArrayOfSetting()
settings.setting.append(duplicate_window_setting)

# Create a transactionRequestType object and add the previous objects to it
transaction_request = apicontractsv1.transactionRequestType()
transaction_request.transactionType = "authCaptureTransaction"
transaction_request.amount = basket.total_incl_tax
transaction_request.order = order
transaction_request.payment = payment_one
transaction_request.billTo = customer_address
transaction_request.customer = customer_data
transaction_request.transactionSettings = settings

# Assemble the complete transaction request
create_transaction_request = apicontractsv1.createTransactionRequest()
create_transaction_request.merchantAuthentication = merchant_auth
create_transaction_request.refId = basket.order_number
create_transaction_request.transactionRequest = transaction_request

# Create the controller and get response
create_transaction_controller = createTransactionController(create_transaction_request)
if self.authorizenet_production_mode:
create_transaction_controller.setenvironment(constants.PRODUCTION)

create_transaction_controller.execute()

response = create_transaction_controller.getresponse()

if response is not None:
if response.messages.resultCode == 'Ok':
if hasattr(response.transactionResponse, 'messages'):
logger.info('Successfully created transaction with Transaction ID: %s' % response.transactionResponse.transId)
logger.info('Transaction Response Code: %s' % response.transactionResponse.responseCode)
logger.info('Message Code: %s' % response.transactionResponse.messages.message[0].code)
logger.info('Auth Code: %s' % response.transactionResponse.authCode)
logger.info('Description: %s' % response.transactionResponse.messages.message[0].description)
else:
if hasattr(response.transactionResponse, 'errors'):
logger.info('Error Code: %s' % str(response.transactionResponse.errors.error[0].errorCode))
logger.info('Error Message: %s' % response.transactionResponse.errors.error[0].errorText)
else:
if hasattr(response, 'transactionResponse') and hasattr(response.transactionResponse, 'errors'):
logger.info('Error Code: %s' % str(response.transactionResponse.errors.error[0].errorCode))
logger.info('Error Message: %s' % response.transactionResponse.errors.error[0].errorText)
else:
logger.info('Error Code: %s' % response.messages.message[0]['code'].text)
logger.info('Error Message: %s' % response.messages.message[0]['text'].text)

return response

def handle_processor_response(self, response, basket=None):
currency = basket.currency
transaction_id = response.transactionResponse.transId if hasattr(response.transactionResponse, 'transId') else None
transaction_dict = LxmlObjectJsonEncoder().encode(response)

self.record_processor_response(transaction_dict, transaction_id=transaction_id, basket=basket)
logger.info('Successfully created Authorizenet charge [%s] for basket [%d].', 'merchant_id', basket.id)

total = basket.total_incl_tax
card_type = self.NAME

return HandledProcessorResponse(
transaction_id=transaction_id,
total=total,
currency=currency,
card_number='XXXX',
card_type=card_type
)

def issue_credit(self, order_number, basket, reference_number, amount, currency):
raise NotImplementedError('Authorizenet payment processor does not support refunds.')
1 change: 1 addition & 0 deletions ecommerce/extensions/payment/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
AUTHORIZENET_URLS = [
url(r'^notification/$', authorizenet.AuthorizeNetNotificationView.as_view(), name='authorizenet_notifications'),
url(r'^redirect/$', authorizenet.handle_redirection, name='redirect'),
url(r'^submit/$', authorizenet.AuthorizenetClientView.as_view(), name='submit'),
]

COWPAY_URLS = [
Expand Down
52 changes: 50 additions & 2 deletions ecommerce/extensions/payment/views/authorizenet.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,22 @@
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from django.http import HttpResponse
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import redirect
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from oscar.apps.partner import strategy
from oscar.core.loading import get_class, get_model
from rest_framework.views import APIView

from ecommerce.extensions.basket.utils import basket_add_organization_attribute
from ecommerce.core.url_utils import get_lms_dashboard_url
from ecommerce.extensions.checkout.mixins import EdxOrderPlacementMixin
from ecommerce.extensions.checkout.utils import get_receipt_page_url
from ecommerce.extensions.payment.exceptions import InvalidBasketError
from ecommerce.extensions.payment.processors.authorizenet import AuthorizeNet
from ecommerce.extensions.payment.forms import AuthorizenetPaymentForm
from ecommerce.extensions.payment.processors.authorizenet import AuthorizeNet, AuthorizenetClient
from ecommerce.extensions.payment.views import BasePaymentSubmitView
from ecommerce.notifications.notifications import send_notification

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -252,3 +256,47 @@ def handle_redirection(request):
response.set_cookie('pendingTransactionCourse', course_id_hash, domain=domain)

return response


class AuthorizenetClientView(EdxOrderPlacementMixin, BasePaymentSubmitView):


form_class = AuthorizenetPaymentForm

@property
def payment_processor(self):
return AuthorizenetClient(self.request.site)

def form_valid(self, form):
form_data = form.cleaned_data
basket = form_data['basket']
order_number = basket.order_number
data_descriptor = form_data['data_descriptor']
data_value = form_data['data_value']

basket_add_organization_attribute(basket, self.request.POST)

response = self.payment_processor.get_transaction_parameters(
basket, data_descriptor=data_descriptor, data_value=data_value
)
try:
self.handle_payment(response, basket)
except Exception: # pylint: disable=broad-except
logger.exception('An error occurred while processing the Authorizent payment for basket [%d].', basket.id)
return JsonResponse({}, status=400)

try:
order = self.create_order(self.request, basket)
except Exception: # pylint: disable=broad-except
logger.exception('An error occurred while processing the Authorizenet payment for basket [%d].', basket.id)
return JsonResponse({}, status=400)

self.handle_post_order(order)

receipt_url = get_receipt_page_url(
site_configuration=self.request.site.siteconfiguration,
order_number=order_number,
disable_back_button=True,
)

return HttpResponseRedirect(receipt_url)
1 change: 1 addition & 0 deletions ecommerce/settings/_oscar.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@
'ecommerce.extensions.payment.processors.paypal.Paypal',
'ecommerce.extensions.payment.processors.stripe.Stripe',
'ecommerce.extensions.payment.processors.authorizenet.AuthorizeNet',
'ecommerce.extensions.payment.processors.authorizenet.AuthorizenetClient',
'ecommerce.extensions.payment.processors.cowpay.Cowpay',
)

Expand Down
Loading

0 comments on commit 87490ee

Please sign in to comment.