Adds support for refunds
This commit is contained in:
parent
6c87b9d08a
commit
abee9e3c62
3 changed files with 122 additions and 0 deletions
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in a new issue