Commit 7ab3e44b by Renzo Lucioni

Automatically approve refunds corresponding to a total credit of $0

Also prevent completed and denied refunds from appearing in the default refund list view, and allow filtering of refunds by multiple statuses.
parent 5a6509c5
......@@ -2,10 +2,20 @@ from django import forms
from django.utils.translation import ugettext_lazy as _
from oscar.core.loading import get_model
from ecommerce.extensions.refund.status import REFUND
Refund = get_model('refund', 'Refund')
class RefundSearchForm(forms.Form):
id = forms.IntegerField(required=False, label=_('Refund ID'))
status_choices = (('', '---------'),) + tuple([(status, status) for status in Refund.all_statuses()])
status = forms.ChoiceField(choices=status_choices, label=_("Status"), required=False)
status_choices = tuple([(status, status) for status in Refund.all_statuses()])
status = forms.MultipleChoiceField(choices=status_choices, label=_("Status"), required=False)
def clean(self):
cleaned_data = super(RefundSearchForm, self).clean()
if not cleaned_data.get('status'):
# If no statuses are specified, default to displaying all those refunds requiring action.
cleaned_data['status'] = list(set(Refund.all_statuses()) - set((REFUND.COMPLETE, REFUND.DENIED)))
......@@ -52,21 +52,30 @@ class RefundListViewTests(RefundViewTestMixin, TestCase):
refund = RefundFactory()
open_refund = RefundFactory(status=REFUND.OPEN)
complete_refund = RefundFactory(status=REFUND.COMPLETE)
denied_refund = RefundFactory(status=REFUND.DENIED)
self.client.login(username=self.user.username, password=self.password)
# Sanity check for an unfiltered query
# Sanity check for an unfiltered query. Completed and denied refunds should be excluded.
response = self.client.get(self.path)
self.assert_successful_response(response, [refund, open_refund, complete_refund])
self.assert_successful_response(response, [refund, open_refund])
# ID filtering
response = self.client.get('{path}?id={id}'.format(path=self.path, id=open_refund.id))
self.assert_successful_response(response, [open_refund])
# Status filtering
# Single-choice status filtering
response = self.client.get('{path}?status={status}'.format(path=self.path, status=REFUND.COMPLETE))
self.assert_successful_response(response, [complete_refund])
# Multiple-choice status filtering
response = self.client.get('{path}?status={complete_status}&status={denied_status}'.format(
path=self.path,
complete_status=REFUND.COMPLETE,
denied_status=REFUND.DENIED
))
self.assert_successful_response(response, [complete_refund, denied_refund])
def test_sorting(self):
""" The view should allow sorting by ID. """
refunds = [RefundFactory(), RefundFactory(), RefundFactory()]
......
......@@ -25,7 +25,9 @@ class RefundListView(ListView):
if self.form.is_valid():
for field, value in self.form.cleaned_data.iteritems():
if value:
if field == 'status' and value:
queryset = queryset.filter(status__in=value)
elif value:
queryset = queryset.filter(**{field: value})
return queryset
......
......@@ -75,7 +75,8 @@ class Refund(StatusMixin, TimeStampedModel):
def create_with_lines(cls, order, lines):
"""Given an order and order lines, creates a Refund with corresponding RefundLines.
Only creates RefundLines for unrefunded order lines.
Only creates RefundLines for unrefunded order lines. Refunds corresponding to a total
credit of $0 are approved upon creation.
Arguments:
order (order.Order): The order to which the newly-created refund corresponds.
......@@ -106,6 +107,9 @@ class Refund(StatusMixin, TimeStampedModel):
status=status
)
if total_credit_excl_tax == 0:
refund.approve()
return refund
@property
......@@ -128,11 +132,15 @@ class Refund(StatusMixin, TimeStampedModel):
def _issue_credit(self):
"""Issue a credit to the purchaser via the payment processor used for the original order."""
# TODO Update this if we ever support multiple payment sources for a single order.
source = self.order.sources.first()
processor = get_processor_class_by_name(source.source_type.name)()
processor.issue_credit(source, self.total_credit_excl_tax, self.currency)
try:
# TODO Update this if we ever support multiple payment sources for a single order.
source = self.order.sources.first()
processor = get_processor_class_by_name(source.source_type.name)()
processor.issue_credit(source, self.total_credit_excl_tax, self.currency)
except AttributeError:
# Order has no sources, resulting in an exception when trying to access `source_type`.
# This occurs when attempting to refund free orders.
logger.info("No payments to credit for Refund [%d]", self.id)
def _revoke_lines(self):
"""Revoke fulfillment for the lines in this Refund."""
......
......@@ -26,13 +26,17 @@ class RefundTestMixin(object):
self.honor_product = self.course.add_mode('honor', 0)
self.verified_product = self.course.add_mode('verified', Decimal(10.00), id_verification_required=True)
def create_order(self, user=None, multiple_lines=False):
def create_order(self, user=None, multiple_lines=False, free=False):
user = user or self.user
basket = BasketFactory(owner=user)
basket.add_product(self.verified_product)
if multiple_lines:
basket.add_product(self.verified_product)
basket.add_product(self.honor_product)
elif free:
basket.add_product(self.honor_product)
else:
basket.add_product(self.verified_product)
order = create_order(basket=basket, user=user)
order.status = ORDER.COMPLETE
......
import ddt
from django.conf import settings
from django.test import TestCase
import httpretty
import mock
from oscar.apps.payment.exceptions import PaymentError
from oscar.core.loading import get_model, get_class
......@@ -96,6 +97,32 @@ class RefundTests(RefundTestMixin, StatusTestsMixin, TestCase):
self.assertEqual(refund, None)
@httpretty.activate
def test_zero_dollar_refund(self):
"""
Given an order and order lines which total $0 and are not refunded, Refund.create_with_lines
should create and approve a Refund with corresponding RefundLines.
"""
httpretty.register_uri(
httpretty.POST,
settings.ENROLLMENT_API_URL,
status=200,
body='{}',
content_type='application/json'
)
order = self.create_order(user=UserFactory(), free=True)
# Verify that the order totals $0.
self.assertEqual(order.total_excl_tax, 0)
refund = Refund.create_with_lines(order, list(order.lines.all()))
# Verify that the refund has been successfully approved.
self.assertEqual(refund.status, REFUND.COMPLETE)
for line in refund.lines.all():
self.assertEqual(line.status, REFUND_LINE.COMPLETE)
@ddt.unpack
@ddt.data(
(REFUND.OPEN, True),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment