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…
	
	Add table
		
		Reference in a new issue
	
	 Christopher Neugebauer
						Christopher Neugebauer