-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds the payments app (APIs for checkout), adds cart test mule app
- Loading branch information
Showing
25 changed files
with
2,929 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
"""App initialization for cart""" | ||
|
||
from django.apps import AppConfig | ||
|
||
|
||
class CartConfig(AppConfig): | ||
"""Config for the cart app""" | ||
|
||
default_auto_field = "django.db.models.BigAutoField" | ||
name = "cart" |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<!DOCTYPE html> | ||
<head> | ||
<title>MIT ODL Ecommerce - {% block title %}{% endblock title %}</title> | ||
|
||
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous"><!-- pragma: allowlist secret --> | ||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script><!-- pragma: allowlist secret --> | ||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> | ||
</head> | ||
<body class="bg-body-secondary"> | ||
<div id="body-container" class="w-75 mx-auto flex container mt-5 bg-light p-4"> | ||
<div class="row mb-3"> | ||
<div class="col-12"> | ||
<h1 class="border-bottom border-5 border-black">MIT ODL Ecommerce - {% block innertitle %}{% endblock innertitle %}</h1> | ||
</div> | ||
</div> | ||
{% block body %}{% endblock body %} | ||
</div> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
{% extends "base.html" %} | ||
|
||
{% block title %}Cart{% endblock %} | ||
{% block innertitle %}Cart{% endblock %} | ||
|
||
{% block body %} | ||
<div class="row"> | ||
<div class="col-12"> | ||
<p> | ||
This is the current cart information. | ||
<button type="button" id="clear-button" class="btn btn-secondary float-right">Clear Cart</button> | ||
</p> | ||
</div> | ||
</div> | ||
{% if basket %} | ||
<div class="row"> | ||
<div class="col-12"> | ||
<table class="table"> | ||
<thead> | ||
<tr> | ||
<th scope="col">Product</th> | ||
<th scope="col">Price</th> | ||
<th scope="col">Quantity</th> | ||
<th scope="col">Total</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
{% if basket_items|length == 0 %} | ||
<tr> | ||
<td colspan="4">No items in the basket.</td> | ||
</tr> | ||
{% endif %} | ||
{% for item in basket_items %} | ||
<tr> | ||
<td>{{ item.product }}</td> | ||
<td>{{ item.product.price }}</td> | ||
<td>{{ item.quantity }}</td> | ||
<td>{{ item.product.price }}</td> | ||
</tr> | ||
{% endfor %} | ||
</tbody> | ||
<tfoot> | ||
<tr> | ||
<td colspan="4"> | ||
<a href="{% url 'checkout_interstitial_page' %}" class="btn btn-primary float-right">Check Out</a> | ||
</td> | ||
</tr> | ||
</tfoot> | ||
</table> | ||
</div> | ||
</div> | ||
{% endif %} | ||
<div class="row"> | ||
<div class="col-12"> | ||
<p>Add a product to the basket:</p> | ||
|
||
<form method="post" id="cartform"> | ||
{% csrf_token %} | ||
<input type="hidden" name="checkout" value="1" /> | ||
<div class="form-group"> | ||
<label for="product">Product</label> | ||
<select name="product" id="product"> | ||
<option></option> | ||
{% for product in products %} | ||
<option value="{{ product.system.slug }}/{{ product.sku }}">{{ product.system.name }} - {{ product.name }} ${{ product.price }}</option> | ||
{% endfor %} | ||
</select> | ||
</div> | ||
|
||
<div class="form-group d-flex align-items-end"> | ||
<button type="submit" class="wl-auto btn btn-primary">Add Item</button> | ||
<button type="submit" id="add-and-checkout" class="wl-auto btn btn-success">Add Item & Check Out</button> | ||
</div> | ||
</form> | ||
</div> | ||
</div> | ||
|
||
<script type="text/javascript"> | ||
function add_to_cart(event) { | ||
console.log("adding to cart"); | ||
|
||
var product_raw = document.getElementById("product").value; | ||
var csrfmiddlewaretoken = document.querySelector("input[name='csrfmiddlewaretoken']").value | ||
|
||
if (event.submitter.id === "add-and-checkout") { | ||
var form = event.target; | ||
|
||
form.setAttribute("action", `http://ue.odl.local/api/v0/payments/baskets/create_from_product/${product_raw}/`) | ||
return true; | ||
} | ||
|
||
event.preventDefault(); | ||
|
||
axios.post(`http://ue.odl.local/api/v0/payments/baskets/create_from_product/${product_raw}/`, {}, { headers: { "X-CSRFToken": csrfmiddlewaretoken }}) | ||
.then(function (response) { | ||
window.location.reload(); | ||
}) | ||
.catch(function (error) { | ||
alert(error); | ||
}); | ||
|
||
return false; | ||
} | ||
|
||
function clear_cart(event) { | ||
event.preventDefault(); | ||
|
||
var slug = "{{ basket.integrated_system.slug }}"; | ||
var csrfmiddlewaretoken = document.querySelector("input[name='csrfmiddlewaretoken']").value | ||
|
||
axios.delete(`http://ue.odl.local/api/v0/payments/baskets/clear/`, { headers: { "X-CSRFToken": csrfmiddlewaretoken } }) | ||
.then(function (response) { | ||
window.location.reload(); | ||
}) | ||
.catch(function (error) { | ||
alert(error); | ||
}); | ||
|
||
return false; | ||
} | ||
|
||
document.getElementById("cartform").addEventListener("submit", add_to_cart); | ||
document.getElementById("clear-button").addEventListener("click", clear_cart); | ||
</script> | ||
{% endblock body %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
{% extends "base.html" %} | ||
{% load i18n static %} | ||
|
||
{% block title %}{% trans "Complete Payment" %}{% endblock %} | ||
{% block innertitle %}{% trans "Complete Payment" %}{% endblock %} | ||
|
||
{% block body %} | ||
<section id="main" class="info-block"> | ||
<div class="container"> | ||
<div class="content-row"> | ||
<div class="content-col"> | ||
<div class="text"> | ||
<h1 class="mt-4">Redirecting to the payment processor...</h1> | ||
|
||
<form id="checkout_form" method="post" action="{{ checkout_payload.url }}"> | ||
{% for key, value in form.items %} | ||
<div><label for="{{ key }}">{{ key }}</label> | ||
<input type="text" readonly="readonly" name="{{ key }}" value="{{ value }}" /></div> | ||
{% endfor %} | ||
|
||
<div> | ||
<button class="btn btn-primary" type="submit">CyberSource It</button> | ||
</div> | ||
</form> | ||
|
||
<script type="text/javascript"> | ||
//document.getElementById('checkout_form').submit(); | ||
</script> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</section> | ||
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
"""Routes for the cart app.""" | ||
|
||
from django.urls import path | ||
|
||
from cart.views import CartView, CheckoutCallbackView, CheckoutInterstitialView | ||
|
||
urlpatterns = [ | ||
path( | ||
r"checkout/result/", | ||
CheckoutCallbackView.as_view(), | ||
name="checkout-result-callback", | ||
), | ||
path( | ||
"checkout/to_payment", | ||
CheckoutInterstitialView.as_view(), | ||
name="checkout_interstitial_page", | ||
), | ||
path("", CartView.as_view(), name="cart"), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
"""Views for the cart app.""" | ||
|
||
import logging | ||
|
||
from django.conf import settings | ||
from django.contrib.auth.mixins import LoginRequiredMixin | ||
from django.core.exceptions import ObjectDoesNotExist | ||
from django.db import transaction | ||
from django.http import HttpResponse | ||
from django.http.request import HttpRequest | ||
from django.shortcuts import render | ||
from django.urls import reverse | ||
from django.utils.decorators import method_decorator | ||
from django.views.decorators.csrf import csrf_exempt | ||
from django.views.generic import TemplateView, View | ||
from mitol.payment_gateway.api import PaymentGateway | ||
|
||
from payments import api | ||
from payments.models import Basket, Order | ||
from system_meta.models import Product | ||
from unified_ecommerce.constants import ( | ||
USER_MSG_TYPE_PAYMENT_ACCEPTED, | ||
USER_MSG_TYPE_PAYMENT_CANCELLED, | ||
USER_MSG_TYPE_PAYMENT_DECLINED, | ||
USER_MSG_TYPE_PAYMENT_ERROR, | ||
USER_MSG_TYPE_PAYMENT_ERROR_UNKNOWN, | ||
) | ||
from unified_ecommerce.utils import redirect_with_user_message | ||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
||
class CartView(LoginRequiredMixin, TemplateView): | ||
"""View for the cart page.""" | ||
|
||
template_name = "cart.html" | ||
extra_context = {"title": "Cart", "innertitle": "Cart"} | ||
|
||
def get(self, request: HttpRequest) -> HttpResponse: | ||
"""Render the cart page.""" | ||
basket = Basket.establish_basket(request) | ||
products = Product.objects.all() | ||
|
||
if not request.user.is_authenticated: | ||
msg = "User is not authenticated" | ||
raise ValueError(msg) | ||
|
||
return render( | ||
request, | ||
self.template_name, | ||
{ | ||
**self.extra_context, | ||
"basket": basket, | ||
"basket_items": basket.basket_items.all(), | ||
"products": products, | ||
}, | ||
) | ||
|
||
|
||
@method_decorator(csrf_exempt, name="dispatch") | ||
class CheckoutCallbackView(View): | ||
""" | ||
Handles the redirect from the payment gateway after the user has completed | ||
checkout. This may not always happen as the redirect back to the app | ||
occasionally fails. If it does, then the payment gateway should trigger | ||
things via the backoffice webhook. | ||
""" | ||
|
||
def post_checkout_redirect(self, order_state, request): | ||
""" | ||
Redirect the user with a message depending on the provided state. | ||
Args: | ||
- order_state (str): the order state to consider | ||
- order (Order): the order itself | ||
- request (HttpRequest): the request | ||
Returns: HttpResponse | ||
""" | ||
if order_state == Order.STATE.CANCELED: | ||
return redirect_with_user_message( | ||
reverse("cart"), {"type": USER_MSG_TYPE_PAYMENT_CANCELLED} | ||
) | ||
elif order_state == Order.STATE.ERRORED: | ||
return redirect_with_user_message( | ||
reverse("cart"), {"type": USER_MSG_TYPE_PAYMENT_ERROR} | ||
) | ||
elif order_state == Order.STATE.DECLINED: | ||
return redirect_with_user_message( | ||
reverse("cart"), {"type": USER_MSG_TYPE_PAYMENT_DECLINED} | ||
) | ||
elif order_state == Order.STATE.FULFILLED: | ||
return redirect_with_user_message( | ||
reverse("cart"), | ||
{ | ||
"type": USER_MSG_TYPE_PAYMENT_ACCEPTED, | ||
}, | ||
) | ||
else: | ||
if not PaymentGateway.validate_processor_response( | ||
settings.ECOMMERCE_DEFAULT_PAYMENT_GATEWAY, request | ||
): | ||
log.info("Could not validate payment response for order") | ||
else: | ||
processor_response = PaymentGateway.get_formatted_response( | ||
settings.ECOMMERCE_DEFAULT_PAYMENT_GATEWAY, request | ||
) | ||
log.error( | ||
( | ||
"Checkout callback unknown error for transaction_id %s, state" | ||
" %s, reason_code %s, message %s, and ProcessorResponse %s" | ||
), | ||
processor_response.transaction_id, | ||
order_state, | ||
processor_response.response_code, | ||
processor_response.message, | ||
processor_response, | ||
) | ||
return redirect_with_user_message( | ||
reverse("cart"), | ||
{"type": USER_MSG_TYPE_PAYMENT_ERROR_UNKNOWN}, | ||
) | ||
|
||
def post(self, request): | ||
""" | ||
Handle successfully completed transactions. | ||
This does a handful of things: | ||
1. Verifies the incoming payload, which should be signed by the | ||
processor | ||
2. Finds and fulfills the order in the system (which should also then | ||
clear out the stored basket) | ||
3. Perform any enrollments, account status changes, etc. | ||
""" | ||
|
||
with transaction.atomic(): | ||
order = api.get_order_from_cybersource_payment_response(request) | ||
if order is None: | ||
return HttpResponse("Order not found") | ||
|
||
# Only process the response if the database record in pending status | ||
# If it is, then we can process the response as per usual. | ||
# If it isn't, then we just need to redirect the user with the | ||
# proper message. | ||
|
||
if order.state == Order.STATE.PENDING: | ||
processed_order_state = api.process_cybersource_payment_response( | ||
request, order | ||
) | ||
|
||
return self.post_checkout_redirect(processed_order_state, request) | ||
else: | ||
return self.post_checkout_redirect(order.state, request) | ||
|
||
|
||
class CheckoutInterstitialView(LoginRequiredMixin, TemplateView): | ||
""" | ||
Redirects the user to the payment gateway. | ||
This is a simple page that just includes the checkout payload, renders a | ||
form and then submits the form so the user gets thrown to the payment | ||
gateway. They can then complete the payment process. | ||
""" | ||
|
||
template_name = "checkout_interstitial.html" | ||
|
||
def get(self, request): | ||
"""Render the checkout interstitial page.""" | ||
try: | ||
checkout_payload = api.generate_checkout_payload(request) | ||
except ObjectDoesNotExist: | ||
return HttpResponse("No basket") | ||
if ( | ||
"country_blocked" in checkout_payload | ||
or "no_checkout" in checkout_payload | ||
or "purchased_same_courserun" in checkout_payload | ||
or "purchased_non_upgradeable_courserun" in checkout_payload | ||
or "invalid_discounts" in checkout_payload | ||
): | ||
return checkout_payload["response"] | ||
|
||
return render( | ||
request, | ||
self.template_name, | ||
{"checkout_payload": checkout_payload, "form": checkout_payload["payload"]}, | ||
) |
Empty file.
Oops, something went wrong.