-
Notifications
You must be signed in to change notification settings - Fork 253
feat: add SDN endpoints #3985
feat: add SDN endpoints #3985
Changes from all commits
8461466
e1a74b4
9777d5c
80ef10b
bd63daa
5407eba
8552678
4d4eb11
02abebd
8ab0494
6bf700c
856a6ae
69a1d9d
d459a95
a7dc37d
3bb8b5a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
"""Payment Extension Serializers. """ | ||
|
||
from rest_framework import serializers | ||
|
||
from ecommerce.extensions.payment.models import SDNCheckFailure | ||
|
||
|
||
class SDNCheckFailureSerializer(serializers.ModelSerializer): | ||
""" | ||
Serializer for SDNCheckFailure model. | ||
""" | ||
|
||
class Meta: | ||
model = SDNCheckFailure | ||
fields = '__all__' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
import logging | ||
|
||
from django.conf import settings | ||
from django.contrib.auth.decorators import login_required | ||
from django.http import JsonResponse | ||
from django.utils.decorators import method_decorator | ||
from django.views.generic import TemplateView, View | ||
from requests.exceptions import HTTPError, Timeout | ||
from rest_framework import status, views | ||
from rest_framework.permissions import IsAdminUser, IsAuthenticated | ||
|
||
from ecommerce.extensions.payment.core.sdn import SDNClient, checkSDNFallback | ||
from ecommerce.extensions.payment.models import SDNCheckFailure | ||
from ecommerce.extensions.payment.serializers import SDNCheckFailureSerializer | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class SDNCheckFailureView(views.APIView): | ||
""" | ||
REST API for SDNCheckFailure class. | ||
""" | ||
http_method_names = ['post', 'options'] | ||
permission_classes = [IsAuthenticated, IsAdminUser] | ||
serializer_class = SDNCheckFailureSerializer | ||
|
||
def _validate_arguments(self, payload): | ||
|
||
invalid = False | ||
reasons = [] | ||
# Check for presence of required variables | ||
for arg in ['full_name', 'username', 'city', 'country', 'sdn_check_response']: | ||
if not payload.get(arg): | ||
reason = f'{arg} is missing or blank.' | ||
reasons.append(reason) | ||
if reasons: | ||
invalid = True | ||
return invalid, reasons | ||
|
||
return invalid, reasons | ||
|
||
def post(self, request, *args, **kwargs): # pylint: disable=unused-argument | ||
payload = request.data | ||
invalid, reasons = self._validate_arguments(payload) | ||
if invalid is True: | ||
logger.warning( | ||
'Invalid payload for request user %s against SDNCheckFailureView endpoint. Reasons: %s', | ||
request.user, | ||
reasons, | ||
) | ||
return JsonResponse( | ||
{'error': ' '.join(reasons)}, | ||
status=400, | ||
) | ||
|
||
sdn_check_failure = SDNCheckFailure.objects.create( | ||
full_name=payload['full_name'], | ||
username=payload['username'], | ||
city=payload['city'], | ||
country=payload['country'], | ||
site=request.site, | ||
sdn_check_response=payload['sdn_check_response'], | ||
) | ||
|
||
# This is the point where we would add the products to the SDNCheckFailure obj. | ||
# We, however, do not know whether the products themselves are relevant to the flow | ||
# calling this endpoint. If you wanted to attach products to the failure record, you | ||
# can use skus handed to this endpoint to filter Products using their stockrecords: | ||
# Product.objects.filter(stockrecords__partner_sku__in=['C92A142','ABC123']) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this a TODO? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good question! this is more of an [inform] to compare how the previous SDN flow works. nothing to do at the moment, unless we have a requirement to attach product objects to the metadata record (which for our case, i don't think we do, because most of the purchasing happens outside of ecommerce) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see, thanks! |
||
|
||
# Return a response | ||
data = self.serializer_class(sdn_check_failure, context={'request': request}).data | ||
return JsonResponse(data, status=status.HTTP_201_CREATED) | ||
|
||
|
||
class SDNFailure(TemplateView): | ||
""" Display an error page when the SDN check fails at checkout. """ | ||
template_name = 'oscar/checkout/sdn_failure.html' | ||
|
||
def get_context_data(self, **kwargs): | ||
context = super(SDNFailure, self).get_context_data(**kwargs) | ||
context['logout_url'] = self.request.site.siteconfiguration.build_lms_url('/logout') | ||
return context | ||
|
||
|
||
class SDNCheckView(View): | ||
""" | ||
View for external services to use to run SDN checks against. | ||
|
||
While this endpoint uses a lot of logic from sdn.py, this endpoint is | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it worth moving some of the same code to an utils function to be used in both files? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's really only a few lines that are truly shared, because we do not use the deactivate or sitconfiguration logic here. in case our use case evolves at all in the short term, i'd like to keep these separate |
||
not called during a normal checkout flow (as of 6/8/2023). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe worth specifying what "normal" is? Or have an example (like "non-subscriptions", "B2C one time payment", etc) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i like "B2C one time payment" -- i'll use that. thanks |
||
""" | ||
http_method_names = ['post', 'options'] | ||
|
||
@method_decorator(login_required) | ||
def post(self, request): | ||
""" | ||
Use data provided to check against SDN list. | ||
|
||
Return a count of hits. | ||
""" | ||
payload = request.POST | ||
|
||
# Make sure we have the values needed to carry out the request | ||
missing_args = [] | ||
for expected_arg in ['lms_user_id', 'name', 'city', 'country']: | ||
if not payload.get(expected_arg): | ||
missing_args.append(expected_arg) | ||
|
||
if missing_args: | ||
return JsonResponse({ | ||
'missing_args': ', '.join(missing_args) | ||
}, status=400) | ||
|
||
# Begin the check logic | ||
lms_user_id = payload.get('lms_user_id') | ||
name = payload.get('name') | ||
city = payload.get('city') | ||
country = payload.get('country') | ||
sdn_list = payload.get('sdn_list', 'ISN,SDN') # Set SDN lists to a sane default | ||
|
||
sdn_check = SDNClient( | ||
api_url=settings.SDN_CHECK_API_URL, | ||
api_key=settings.SDN_CHECK_API_KEY, | ||
sdn_list=sdn_list | ||
) | ||
try: | ||
response = sdn_check.search(name, city, country) | ||
except (HTTPError, Timeout) as e: | ||
logger.info( | ||
'SDNCheck: SDN API call received an error: %s. SDNFallback function called for user %s.', | ||
str(e), | ||
lms_user_id | ||
) | ||
sdn_fallback_hit_count = checkSDNFallback( | ||
name, | ||
city, | ||
country | ||
) | ||
response = {'total': sdn_fallback_hit_count} | ||
|
||
hit_count = response['total'] | ||
if hit_count > 0: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At this point we would deactivate the user - but since we will be doing this logic from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. correct. i have split up the logic from the one time checkout flow into 3 parts to make their use more modular/convenient for external services: 1) endpoint for getting SDN hit counts 2) endpoint for creating SDNfailure metadata records 3) separate place to deactivate user (subs) |
||
logger.info( | ||
'SDNCheck Endpoint called for lms user [%s]. It received %d hit(s).', | ||
lms_user_id, | ||
hit_count, | ||
) | ||
else: | ||
logger.info( | ||
'SDNCheck function called for lms user [%s]. It did not receive a hit.', | ||
lms_user_id, | ||
) | ||
json_data = { | ||
'hit_count': hit_count, | ||
'sdn_response': response, | ||
} | ||
return JsonResponse(json_data, status=200) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you need this return here if you're returning the same in the next line?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch. i'll simplify. previously i had more code here