Adds refund function, adds tests, makes sure that refunds are obeyed elsewhere in the codebase

This commit is contained in:
Christopher Neugebauer 2016-03-27 19:25:24 +11:00
parent b65223aaa1
commit cf85af7719
11 changed files with 175 additions and 8 deletions

View file

@ -139,6 +139,7 @@ class CartController(object):
# It's not valid for users to re-enter a voucher they already have
user_carts_with_voucher = rego.Cart.objects.filter(
user=self.cart.user,
released=False,
vouchers=voucher,
)
if len(user_carts_with_voucher) > 0:

View file

@ -44,7 +44,7 @@ class CategoryConditionController(ConditionController):
''' returns True if the user has a product from a category that invokes
this condition in one of their carts '''
carts = rego.Cart.objects.filter(user=user)
carts = rego.Cart.objects.filter(user=user, released=False)
enabling_products = rego.Product.objects.filter(
category=self.condition.enabling_category)
products = rego.ProductItem.objects.filter(
@ -64,7 +64,7 @@ class ProductConditionController(ConditionController):
''' returns True if the user has a product that invokes this
condition in one of their carts '''
carts = rego.Cart.objects.filter(user=user)
carts = rego.Cart.objects.filter(user=user, released=False)
products = rego.ProductItem.objects.filter(
cart=carts,
product=self.condition.enabling_products.all())

View file

@ -12,6 +12,11 @@ class DiscountAndQuantity(object):
self.clause = clause
self.quantity = quantity
def __repr__(self):
print "(discount=%s, clause=%s, quantity=%d)" % (
self.discount, self.clause, self.quantity,
)
def available_discounts(user, categories, products):
''' Returns all discounts available to this user for the given categories
@ -57,6 +62,7 @@ def available_discounts(user, categories, products):
past_uses = rego.DiscountItem.objects.filter(
cart__user=user,
cart__active=False, # Only past carts count
cart__released=False, # You can reuse refunded discounts
discount=discount.discount,
)
agg = past_uses.aggregate(Sum("quantity"))

View file

@ -1,6 +1,7 @@
from decimal import Decimal
from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import Sum
from registrasion import models as rego
@ -115,12 +116,13 @@ class InvoiceController(object):
self.invoice.void = True
self.invoice.save()
@transaction.atomic
def pay(self, reference, amount):
''' Pays the invoice by the given amount. If the payment
equals the total on the invoice, finalise the invoice.
(NB should be transactional.)
'''
if self.invoice.cart is not None:
if self.invoice.cart:
cart = CartController(self.invoice.cart)
cart.validate_cart() # Raises ValidationError if invalid
@ -145,8 +147,36 @@ class InvoiceController(object):
if total == self.invoice.value:
self.invoice.paid = True
if self.invoice.cart:
cart = self.invoice.cart
cart.active = False
cart.save()
self.invoice.save()
@transaction.atomic
def refund(self, reference, amount):
''' Refunds the invoice by the given amount. The invoice is
marked as unpaid, and the underlying cart is marked as released.
'''
if self.invoice.void:
raise ValidationError("Void invoices cannot be refunded")
''' Adds a payment '''
payment = rego.Payment.objects.create(
invoice=self.invoice,
reference=reference,
amount=0 - amount,
)
payment.save()
self.invoice.paid = False
self.invoice.void = True
if self.invoice.cart:
cart = self.invoice.cart
cart.released = True
cart.save()
self.invoice.save()

View file

@ -423,7 +423,7 @@ class Cart(models.Model):
Q(time_last_updated__gt=(
timezone.now()-F('reservation_duration')
))) |
Q(active=False)
(Q(active=False) & Q(released=False))
)

View file

@ -52,7 +52,7 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
cls.products = []
for i in xrange(4):
prod = rego.Product.objects.create(
name="Product 1",
name="Product " + str(i + 1),
description="This is a test product.",
category=cls.categories[i / 2], # 2 products per category
price=Decimal("10.00"),

View file

@ -132,3 +132,21 @@ class CeilingsTestCases(RegistrationCartTestCase):
self.add_timedelta(self.RESERVATION + datetime.timedelta(seconds=1))
with self.assertRaises(ValidationError):
first_cart.validate_cart()
def test_items_released_from_ceiling_by_refund(self):
self.make_ceiling("Limit ceiling", limit=1)
first_cart = CartController.for_user(self.USER_1)
first_cart.add_to_cart(self.PROD_1, 1)
first_cart.cart.active = False
first_cart.cart.save()
second_cart = CartController.for_user(self.USER_2)
with self.assertRaises(ValidationError):
second_cart.add_to_cart(self.PROD_1, 1)
first_cart.cart.released = True
first_cart.cart.save()
second_cart.add_to_cart(self.PROD_1, 1)

View file

@ -377,3 +377,27 @@ class DiscountTestCase(RegistrationCartTestCase):
[self.PROD_3, self.PROD_4],
)
self.assertEqual(2, len(discounts))
def test_discounts_are_released_by_refunds(self):
self.add_discount_prod_1_includes_prod_2(quantity=2)
cart = CartController.for_user(self.USER_1)
cart.add_to_cart(self.PROD_1, 1) # Enable the discount
discounts = discount.available_discounts(self.USER_1, [], [self.PROD_2])
self.assertEqual(1, len(discounts))
cart.cart.active = False # Keep discount enabled
cart.cart.save()
cart = CartController.for_user(self.USER_1)
cart.add_to_cart(self.PROD_2, 2) # The discount will be exhausted
cart.cart.active = False
cart.cart.save()
discounts = discount.available_discounts(self.USER_1, [], [self.PROD_2])
self.assertEqual(0, len(discounts))
cart.cart.released = True
cart.cart.save()
discounts = discount.available_discounts(self.USER_1, [], [self.PROD_2])
self.assertEqual(1, len(discounts))

View file

@ -233,3 +233,41 @@ class EnablingConditionTestCases(RegistrationCartTestCase):
self.assertTrue(self.PROD_1 in prods)
self.assertTrue(self.PROD_2 in prods)
def test_category_enabling_condition_fails_if_cart_refunded(self):
self.add_category_enabling_condition(mandatory=False)
cart = CartController.for_user(self.USER_1)
cart.add_to_cart(self.PROD_3, 1)
cart.cart.active = False
cart.cart.save()
cart_2 = CartController.for_user(self.USER_1)
cart_2.add_to_cart(self.PROD_1, 1)
cart_2.set_quantity(self.PROD_1, 0)
cart.cart.released = True
cart.cart.save()
with self.assertRaises(ValidationError):
cart_2.set_quantity(self.PROD_1, 1)
def test_product_enabling_condition_fails_if_cart_refunded(self):
self.add_product_enabling_condition(mandatory=False)
cart = CartController.for_user(self.USER_1)
cart.add_to_cart(self.PROD_2, 1)
cart.cart.active = False
cart.cart.save()
cart_2 = CartController.for_user(self.USER_1)
cart_2.add_to_cart(self.PROD_1, 1)
cart_2.set_quantity(self.PROD_1, 0)
cart.cart.released = True
cart.cart.save()
with self.assertRaises(ValidationError):
cart_2.set_quantity(self.PROD_1, 1)

View file

@ -0,0 +1,33 @@
import datetime
import pytz
from decimal import Decimal
from django.core.exceptions import ValidationError
from registrasion import models as rego
from registrasion.controllers.cart import CartController
from registrasion.controllers.invoice import InvoiceController
from test_cart import RegistrationCartTestCase
UTC = pytz.timezone('UTC')
class RefundTestCase(RegistrationCartTestCase):
def test_refund_marks_void_and_unpaid_and_cart_released(self):
current_cart = CartController.for_user(self.USER_1)
# Should be able to create an invoice after the product is added
current_cart.add_to_cart(self.PROD_1, 1)
invoice = InvoiceController.for_cart(current_cart.cart)
invoice.pay("A Payment!", invoice.invoice.value)
self.assertFalse(invoice.invoice.void)
self.assertTrue(invoice.invoice.paid)
self.assertFalse(invoice.invoice.cart.released)
invoice.refund("A Refund!", invoice.invoice.value)
self.assertTrue(invoice.invoice.void)
self.assertFalse(invoice.invoice.paid)
self.assertTrue(invoice.invoice.cart.released)

View file

@ -126,3 +126,20 @@ class VoucherTestCases(RegistrationCartTestCase):
with self.assertRaises(ValidationError):
current_cart.apply_voucher(voucher.code)
return current_cart
def test_refund_releases_used_vouchers(self):
voucher = self.new_voucher(limit=2)
current_cart = CartController.for_user(self.USER_1)
current_cart.apply_voucher(voucher.code)
inv = InvoiceController.for_cart(current_cart.cart)
inv.pay("Hello!", inv.invoice.value)
current_cart = CartController.for_user(self.USER_1)
with self.assertRaises(ValidationError):
current_cart.apply_voucher(voucher.code)
inv.refund("Hello!", inv.invoice.value)
current_cart.apply_voucher(voucher.code)