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 copy
import models
from django import forms from django import forms
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db.models import F, Q
from django.forms import widgets from django.forms import widgets
from django.utils import timezone from django.utils import timezone
@ -10,6 +12,8 @@ from django_countries import countries
from django_countries.fields import LazyTypedChoiceField from django_countries.fields import LazyTypedChoiceField
from django_countries.widgets import CountrySelectWidget from django_countries.widgets import CountrySelectWidget
from pinax.stripe import models as pinax_stripe_models
class NoRenderWidget(forms.widgets.HiddenInput): 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: From stripe.js details:

View file

@ -10,5 +10,6 @@ from pinax.stripe.views import (
urlpatterns = [ urlpatterns = [
url(r"^card/([0-9]*)/$", views.card, name="registripe_card"), url(r"^card/([0-9]*)/$", views.card, name="registripe_card"),
url(r"^pubkey/$", views.pubkey_script, name="registripe_pubkey"), 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"), 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.core.exceptions import ValidationError
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import user_passes_test
from django.db import transaction from django.db import transaction
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from registrasion.controllers.credit_note import CreditNoteController
from registrasion.controllers.invoice import InvoiceController from registrasion.controllers.invoice import InvoiceController
from registrasion.models import commerce from registrasion.models import commerce
from pinax.stripe import actions from pinax.stripe import actions
from pinax.stripe.actions import refunds as pinax_stripe_actions_refunds
from stripe.error import StripeError from stripe.error import StripeError
from symposion.conference.models import Conference from symposion.conference.models import Conference
@ -20,6 +24,11 @@ CURRENCY = settings.INVOICE_CURRENCY
CONFERENCE_ID = settings.CONFERENCE_ID CONFERENCE_ID = settings.CONFERENCE_ID
def _staff_only(user):
''' Returns true if the user is staff. '''
return user.is_staff
def pubkey_script(request): def pubkey_script(request):
''' Returns a JS snippet that sets the Stripe public key for Stripe.js. ''' ''' 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() inv.update_status()
messages.success(request, "This invoice was successfully paid.") 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
)
)