From abee9e3c6231d77d76b7d8b7ccce83704405055d Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Thu, 22 Sep 2016 11:04:43 +1000 Subject: [PATCH] Adds support for refunds --- registripe/forms.py | 51 +++++++++++++++++++++++++++++++++ registripe/urls.py | 1 + registripe/views.py | 70 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+) diff --git a/registripe/forms.py b/registripe/forms.py index 4f8915fb..0f4a4db0 100644 --- a/registripe/forms.py +++ b/registripe/forms.py @@ -1,8 +1,10 @@ import copy +import models from django import forms from django.core.urlresolvers import reverse from django.core.exceptions import ValidationError +from django.db.models import F, Q from django.forms import widgets from django.utils import timezone @@ -10,6 +12,8 @@ from django_countries import countries from django_countries.fields import LazyTypedChoiceField from django_countries.widgets import CountrySelectWidget +from pinax.stripe import models as pinax_stripe_models + class NoRenderWidget(forms.widgets.HiddenInput): @@ -140,6 +144,53 @@ class CreditCardForm(forms.Form): )) +class StripeRefundForm(forms.Form): + + def __init__(self, *args, **kwargs): + ''' + + Arguments: + user (User): The user whose charges we should filter to. + min_value (Decimal): The minimum value of the charges we should + show (currently, credit notes can only be cashed out in full.) + + ''' + user = kwargs.pop('user', None) + min_value = kwargs.pop('min_value', None) + super(StripeRefundForm, self).__init__(*args, **kwargs) + + payment_field = self.fields['payment'] + qs = payment_field.queryset + + if user: + qs = qs.filter( + charge__customer__user=user, + ) + + if min_value is not None: + # amount >= amount_to_refund + amount_refunded + # No refunds yet + q1 = ( + Q(charge__amount_refunded__isnull=True) & + Q(charge__amount__gte=min_value) + ) + # There are some refunds + q2 = ( + Q(charge__amount_refunded__isnull=False) & + Q(charge__amount__gte=( + F("charge__amount_refunded") + min_value) + ) + ) + qs = qs.filter(q1 | q2) + + payment_field.queryset = qs + + payment = forms.ModelChoiceField( + required=True, + queryset=models.StripePayment.objects.all(), + ) + + '''{ From stripe.js details: diff --git a/registripe/urls.py b/registripe/urls.py index e5958902..04a249ba 100644 --- a/registripe/urls.py +++ b/registripe/urls.py @@ -10,5 +10,6 @@ from pinax.stripe.views import ( urlpatterns = [ url(r"^card/([0-9]*)/$", views.card, name="registripe_card"), url(r"^pubkey/$", views.pubkey_script, name="registripe_pubkey"), + url(r"^refund/([0-9]*)/$", views.refund, name="registripe_refund"), url(r"^webhook/$", Webhook.as_view(), name="pinax_stripe_webhook"), ] diff --git a/registripe/views.py b/registripe/views.py index 8207e2b7..bd54a528 100644 --- a/registripe/views.py +++ b/registripe/views.py @@ -4,14 +4,18 @@ import models from django.core.exceptions import ValidationError from django.conf import settings from django.contrib import messages +from django.contrib.auth.decorators import user_passes_test from django.db import transaction from django.http import HttpResponse from django.shortcuts import redirect, render +from registrasion.controllers.credit_note import CreditNoteController from registrasion.controllers.invoice import InvoiceController from registrasion.models import commerce from pinax.stripe import actions +from pinax.stripe.actions import refunds as pinax_stripe_actions_refunds + from stripe.error import StripeError from symposion.conference.models import Conference @@ -20,6 +24,11 @@ CURRENCY = settings.INVOICE_CURRENCY CONFERENCE_ID = settings.CONFERENCE_ID +def _staff_only(user): + ''' Returns true if the user is staff. ''' + return user.is_staff + + def pubkey_script(request): ''' Returns a JS snippet that sets the Stripe public key for Stripe.js. ''' @@ -126,3 +135,64 @@ def process_card(request, form, inv): inv.update_status() messages.success(request, "This invoice was successfully paid.") + + +@user_passes_test(_staff_only) +def refund(request, credit_note_id): + ''' Allows staff to select a Stripe charge for the owner of the credit + note, and refund the credit note into stripe. ''' + + cn = CreditNoteController.for_id_or_404(str(credit_note_id)) + + to_credit_note = redirect("credit_note", cn.credit_note.id) + + if not cn.credit_note.is_unclaimed: + return to_credit_note + + form = forms.StripeRefundForm( + request.POST or None, + user=cn.credit_note.invoice.user, + min_value=cn.credit_note.value, + ) + + if request.POST and form.is_valid(): + try: + process_refund(cn, form) + return to_credit_note + except StripeError as se: + form.add_error(None, ValidationError(se)) + + data = { + "credit_note": cn.credit_note, + "form": form, + } + + return render( + request, "registrasion/stripe/refund.html", data + ) + + +def process_refund(cn, form): + payment = form.cleaned_data["payment"] + charge = payment.charge + + to_refund = cn.credit_note.value + stripe_charge_id = charge.stripe_charge.id + + # Test that the given charge is allowed to be refunded. + max_refund = actions.charges.calculate_refund_amount(charge) + + if max_refund < to_refund: + raise ValidationError( + "You must select a payment holding greater value than " + "the credit note." + ) + + refund = actions.refunds.create(charge, to_refund) + + commerce.CreditNoteRefund.objects.create( + parent=cn.credit_note, + reference="Refunded %s to Stripe charge %s" % ( + to_refund, stripe_charge_id + ) + )