# -*- encoding: utf-8 -*-
import json
import logging
from braces.views import LoginRequiredMixin, StaffuserRequiredMixin
from datetime import date
from django.apps import apps
from django.conf import settings
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.http import Http404, HttpResponseRedirect
from django.urls import reverse
from django.utils import timezone
from django.views.generic import (
CreateView,
DetailView,
ListView,
UpdateView,
TemplateView,
)
from base.url_utils import url_with_querystring
from base.view_utils import BaseMixin
from mail.service import queue_mail_template
from mail.tasks import process_mail
from .forms import (
# CheckoutTokenForm,
CustomerCheckoutForm,
CustomerEmptyForm,
CustomerPaymentForm,
ObjectPaymentPlanEmptyForm,
ObjectPaymentPlanInstalmentEmptyForm,
PaymentPlanEmptyForm,
PaymentPlanForm,
)
from .models import (
as_pennies,
Checkout,
CheckoutAction,
CheckoutAdditional,
CheckoutError,
CheckoutSettings,
CURRENCY,
Customer,
CustomerPayment,
ObjectPaymentPlan,
ObjectPaymentPlanInstalment,
PaymentPlan,
PaymentRun,
PaymentRunItem,
)
CONTENT_OBJECT_PK = "content_object_pk"
logger = logging.getLogger(__name__)
def _check_perm(request, content_object):
"""Check the session variable to make sure it was set."""
checkout_actions = content_object.checkout_actions
pk = request.session.get(CONTENT_OBJECT_PK, None)
if pk:
pk = int(pk)
if pk == content_object.pk:
pass
else:
logger.critical(
"content object check: invalid: {} != {}".format(
pk, content_object.pk
)
)
raise PermissionDenied("content check failed")
elif checkout_actions == [CheckoutAction.CARD_REFRESH]:
# allow card refresh without setting content object in the session
pass
else:
logger.critical("content object check: invalid")
raise PermissionDenied("content check failed")
def _check_perm_success(request, payment):
"""Check the permissions for the checkout success (thank you) page.
We check the user (who might be anonymous) is not viewing an old
transaction.
"""
_check_perm(request, payment.content_object)
td = timezone.now() - payment.created
diff = td.days * 1440 + td.seconds / 60
if abs(diff) > 10:
raise CheckoutError(
"Cannot view this checkout transaction. It is too old "
"(or has travelled in time, {} {} {}).".format(
payment.created.strftime("%d/%m/%Y %H:%M"),
timezone.now().strftime("%d/%m/%Y %H:%M"),
abs(diff),
)
)
[docs]def payment_plan_example(total):
checkout_settings = CheckoutSettings.objects.settings
payment_plan = checkout_settings.default_payment_plan
return payment_plan.example(date.today(), total)
[docs]class CheckoutAuditListView(
LoginRequiredMixin, StaffuserRequiredMixin, BaseMixin, ListView
):
paginate_by = 10
[docs] def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update(dict(audit=True))
return context
[docs] def get_queryset(self):
return Checkout.objects.audit()
[docs]class CheckoutCardRefreshListView(
LoginRequiredMixin, StaffuserRequiredMixin, BaseMixin, ListView
):
"""Customers with expiring cards."""
paginate_by = 10
template_name = "checkout/card_refresh_list.html"
[docs] def get_queryset(self):
return Customer.objects.refresh
[docs]class CheckoutDashTemplateView(
StaffuserRequiredMixin, LoginRequiredMixin, BaseMixin, TemplateView
):
template_name = "checkout/dash.html"
[docs]class CheckoutListView(
LoginRequiredMixin, StaffuserRequiredMixin, BaseMixin, ListView
):
paginate_by = 10
[docs] def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update(dict(audit=False))
return context
[docs] def get_queryset(self):
return Checkout.objects.success()
[docs]class CheckoutBaseMixin:
"""Some methods for use in creating a checkout
.. note: if using checkout in a standalone view use CheckoutMixin
"""
def _add_cost_info_to_context(self, context):
context.update({"email": self.object.checkout_email})
if CheckoutAction.PAYMENT_PLAN in self._checkout_actions():
context.update(
{"example": payment_plan_example(self.object.checkout_total)}
)
def _action_data(self):
"""the action data for the javascript on the page."""
actions = self._checkout_actions()
result = {}
for slug in actions:
obj = CheckoutAction.objects.get(slug=slug)
result[slug] = dict(name=obj.name, payment=obj.payment)
return json.dumps(result)
def _check_permissions(self):
if not self.request.user.is_staff:
_check_perm(self.request, self.object)
if not self.object.checkout_can_charge:
raise CheckoutError(
"Cannot charge '{}': {}".format(
self.object.__class__.__name__, self.object.pk
)
)
def _checkout_actions(self):
return self.object.checkout_actions
def _form_valid_checkout(self, obj, form):
"""Process the payment.
.. note: We check ``checkout_can_charge`` again here. It will already
have been called in ``get_context_data``, but the user might
have continued in another tab (or something):
https://www.kbsoftware.co.uk/crm/ticket/2554/
"""
token = form.cleaned_data["token"]
slug = form.cleaned_data["action"]
action = CheckoutAction.objects.get(slug=slug)
checkout = None
if not self.object.checkout_can_charge:
raise CheckoutError(
"Cannot charge '{}': {}".format(obj.__class__.__name__, obj.pk)
)
try:
with transaction.atomic():
checkout = Checkout.objects.create_checkout(
action, obj, self.request.user
)
# do stripe stuff outside of a transaction so we have some chance
# of diagnosing progress if an exception is thrown.
if not action.invoice:
if not token:
raise CheckoutError(
"No checkout 'token' for: '{}'".format(obj)
)
self._form_valid_stripe(checkout, token)
with transaction.atomic():
if action.invoice or action.payment_plan:
self._form_valid_invoice(checkout, form)
checkout.success()
checkout.notify(self.request)
# obj.checkout_mail(action)
url = obj.checkout_success_url(checkout.pk)
transaction.on_commit(lambda: process_mail.delay())
except CheckoutError as e:
logger.error(e)
if checkout:
with transaction.atomic():
checkout.fail()
checkout.notify(self.request)
url = obj.checkout_fail_url(checkout.pk)
# PJK TODO remove temp
# raise
return url
def _form_valid_invoice(self, checkout, form):
"""Use the forms's additional_data method to create the
CheckoutAdditional record.
"""
data = form.additional_data()
CheckoutAdditional.objects.create_checkout_additional(checkout, **data)
def _form_valid_stripe(self, checkout, token):
"""Create a stripe customer object for a checkout.
"""
customer = Customer.objects.init_customer(self.object, token)
checkout.customer = customer
checkout.save()
checkout.charge_user(self.request.user)
[docs] def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
self._check_permissions()
allow_remember_me = not self.request.user.is_staff
checkout_actions = self._checkout_actions()
context.update(
dict(
action_data=self._action_data(),
allow_pay_by_invoice=CheckoutAction.INVOICE in checkout_actions,
allow_remember_me=allow_remember_me,
currency=CURRENCY,
key=settings.STRIPE_PUBLISH_KEY,
name=settings.STRIPE_CAPTION,
)
)
self._add_cost_info_to_context(context)
return context
[docs]class CheckoutMixin(CheckoutBaseMixin):
"""Checkout using a standalone view.
Use with an ``UpdateView`` e.g::
class ShopCheckoutUpdateView(CheckoutMixin, BaseMixin, UpdateView):
"""
[docs] def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update(
dict(
description=self.object.checkout_description,
total=as_pennies(self.object.checkout_total), # pennies
)
)
return context
[docs]class CheckoutSuccessMixin(object):
"""Thank you for your payment (etc).
Use with a ``DetailView`` e.g::
class ShopCheckoutSuccessView(
CheckoutSuccessMixin, BaseMixin, DetailView):
"""
model = Checkout
[docs] def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
_check_perm_success(self.request, self.object)
if self.object.action == CheckoutAction.objects.payment_plan:
context.update(example=payment_plan_example(self.object.total))
return context
[docs]class CustomerCheckoutRefreshUpdateView(
StaffuserRequiredMixin,
LoginRequiredMixin,
CheckoutMixin,
BaseMixin,
UpdateView,
):
form_class = CustomerCheckoutForm
model = Customer
template_name = "checkout/customer_checkout_refresh.html"
# def _email(self):
# if self.request.method == 'GET':
# email = self.request.GET.get('email')
# else:
# email = self.request.POST.get('next')
# if not email:
# raise Http404(
# "Customer view needs an email address parameter."
# )
# return email
# def get_object(self, queryset=None):
# customer = Customer.objects.get_customer(self._email())
# self.request.session[CONTENT_OBJECT_PK] = customer.pk
# return customer
[docs]class CustomerChargeCreateView(
LoginRequiredMixin, StaffuserRequiredMixin, BaseMixin, CreateView
):
form_class = CustomerPaymentForm
model = CustomerPayment
def _customer(self):
pk = int(self.kwargs.get("pk"))
return Customer.objects.get(pk=pk)
[docs] def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update(dict(customer=self._customer()))
return context
[docs] def get_success_url(self):
return url_with_querystring(
reverse("checkout.customer"), email=self.object.customer.email
)
[docs]class CustomerTemplateView(
StaffuserRequiredMixin, LoginRequiredMixin, BaseMixin, TemplateView
):
template_name = "checkout/customer.html"
def _contacts(self, email):
contact_model = apps.get_model(settings.CONTACT_MODEL)
return contact_model.objects.filter(user__email__iexact=email)
def _customers(self, email):
return Customer.objects.get_customers(email)
def _email(self):
email = self.request.GET.get("email")
if not email:
raise Http404("Customer view needs an email address parameter.")
return email
def _payment_plans(self, customers):
emails = [o.email.lower() for o in customers]
payment_plans = (
ObjectPaymentPlan.objects.all()
.prefetch_related("content_object")
.order_by("-pk")
)
result = []
for obj in payment_plans:
if obj.content_object.checkout_email.lower() in emails:
result.append(obj)
return result
[docs] def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
email = self._email()
customers = self._customers(email)
context.update(
dict(
audit=Checkout.objects.audit_customers(customers),
contacts=self._contacts(email),
customers=customers,
email=email,
payment_plans=self._payment_plans(customers),
)
)
return context
[docs]class ObjectPaymentPlanDeleteView(
LoginRequiredMixin, StaffuserRequiredMixin, BaseMixin, UpdateView
):
form_class = ObjectPaymentPlanEmptyForm
model = ObjectPaymentPlan
template_name = "checkout/object_paymentplan_delete.html"
[docs] def get_success_url(self):
return reverse("checkout.object.payment.plan.list")
[docs]class ObjectPaymentPlanListView(
LoginRequiredMixin, StaffuserRequiredMixin, BaseMixin, ListView
):
model = ObjectPaymentPlan
paginate_by = 10
[docs] def get_queryset(self):
qs = ObjectPaymentPlan.objects.outstanding_payment_plans
return qs.order_by("-pk")
[docs]class ObjectPaymentPlanCardFailListView(
LoginRequiredMixin, StaffuserRequiredMixin, BaseMixin, ListView
):
""""""
paginate_by = 10
[docs] def get_queryset(self):
return ObjectPaymentPlan.objects.fail_or_request
[docs]class ObjectPaymentPlanInstalmentAuditListView(
LoginRequiredMixin, StaffuserRequiredMixin, BaseMixin, ListView
):
paginate_by = 15
[docs] def get_queryset(self):
return Checkout.objects.audit(
ContentType.objects.get_for_model(ObjectPaymentPlanInstalment)
)
[docs]class ObjectPaymentPlanDetailView(
StaffuserRequiredMixin, LoginRequiredMixin, BaseMixin, DetailView
):
model = ObjectPaymentPlan
[docs]class ObjectPaymentPlanInstalmentDetailView(
StaffuserRequiredMixin, LoginRequiredMixin, BaseMixin, DetailView
):
model = ObjectPaymentPlanInstalment
[docs]class ObjectPaymentPlanInstalmentChargeUpdateView(
StaffuserRequiredMixin, LoginRequiredMixin, BaseMixin, UpdateView
):
"""Charge the customers card for this instalment."""
model = ObjectPaymentPlanInstalment
form_class = ObjectPaymentPlanInstalmentEmptyForm
template_name = "checkout/objectpaymentplaninstalment_charge_form.html"
[docs] def get_success_url(self):
return reverse(
"checkout.object.payment.plan",
args=[self.object.object_payment_plan.pk],
)
[docs]class ObjectPaymentPlanInstalmentPaidUpdateView(
StaffuserRequiredMixin, LoginRequiredMixin, BaseMixin, UpdateView
):
"""Mark this instalment as paid."""
model = ObjectPaymentPlanInstalment
form_class = ObjectPaymentPlanInstalmentEmptyForm
template_name = "checkout/objectpaymentplaninstalment_paid_form.html"
[docs] def get_success_url(self):
return reverse(
"checkout.object.payment.plan",
args=[self.object.object_payment_plan.pk],
)
[docs]class PaymentPlanCreateView(
LoginRequiredMixin, StaffuserRequiredMixin, BaseMixin, CreateView
):
form_class = PaymentPlanForm
model = PaymentPlan
[docs] def get_success_url(self):
return reverse("checkout.payment.plan.list")
[docs]class PaymentPlanDeleteView(
LoginRequiredMixin, StaffuserRequiredMixin, BaseMixin, UpdateView
):
form_class = PaymentPlanEmptyForm
model = PaymentPlan
template_name = "checkout/paymentplan_delete.html"
[docs] def get_success_url(self):
return reverse("checkout.payment.plan.list")
[docs]class PaymentPlanListView(
LoginRequiredMixin, StaffuserRequiredMixin, BaseMixin, ListView
):
paginate_by = 5
[docs] def get_queryset(self):
return PaymentPlan.objects.current()
[docs]class PaymentPlanUpdateView(
LoginRequiredMixin, StaffuserRequiredMixin, BaseMixin, UpdateView
):
form_class = PaymentPlanForm
model = PaymentPlan
[docs] def get_success_url(self):
return reverse("checkout.payment.plan.list")
[docs]class PaymentRunItemListView(
LoginRequiredMixin, StaffuserRequiredMixin, BaseMixin, ListView
):
paginate_by = 20
def _payment_run(self):
pk = int(self.kwargs.get("pk"))
return PaymentRun.objects.get(pk=pk)
[docs] def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update(dict(payment_run=self._payment_run()))
return context
[docs] def get_queryset(self):
return PaymentRunItem.objects.filter(
payment_run=self._payment_run()
).order_by("created")
[docs]class PaymentRunListView(
LoginRequiredMixin, StaffuserRequiredMixin, BaseMixin, ListView
):
paginate_by = 20
[docs] def get_queryset(self):
return PaymentRun.objects.all().order_by("-created")