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 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: | ||||
| 
 | ||||
|  |  | |||
|  | @ -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"), | ||||
| ] | ||||
|  |  | |||
|  | @ -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 | ||||
|         ) | ||||
|     ) | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 Christopher Neugebauer
						Christopher Neugebauer