Adds support for refunds

This commit is contained in:
Christopher Neugebauer 2016-09-22 11:04:43 +10:00
parent 6c87b9d08a
commit abee9e3c62
3 changed files with 122 additions and 0 deletions

View file

@ -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:

View file

@ -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"),
]

View file

@ -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
)
)