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 # It's not valid for users to re-enter a voucher they already have
user_carts_with_voucher = rego.Cart.objects.filter( user_carts_with_voucher = rego.Cart.objects.filter(
user=self.cart.user, user=self.cart.user,
released=False,
vouchers=voucher, vouchers=voucher,
) )
if len(user_carts_with_voucher) > 0: 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 ''' returns True if the user has a product from a category that invokes
this condition in one of their carts ''' 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( enabling_products = rego.Product.objects.filter(
category=self.condition.enabling_category) category=self.condition.enabling_category)
products = rego.ProductItem.objects.filter( products = rego.ProductItem.objects.filter(
@ -64,7 +64,7 @@ class ProductConditionController(ConditionController):
''' returns True if the user has a product that invokes this ''' returns True if the user has a product that invokes this
condition in one of their carts ''' 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( products = rego.ProductItem.objects.filter(
cart=carts, cart=carts,
product=self.condition.enabling_products.all()) product=self.condition.enabling_products.all())

View file

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

View file

@ -1,6 +1,7 @@
from decimal import Decimal from decimal import Decimal
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import Sum from django.db.models import Sum
from registrasion import models as rego from registrasion import models as rego
@ -115,12 +116,13 @@ class InvoiceController(object):
self.invoice.void = True self.invoice.void = True
self.invoice.save() self.invoice.save()
@transaction.atomic
def pay(self, reference, amount): def pay(self, reference, amount):
''' Pays the invoice by the given amount. If the payment ''' Pays the invoice by the given amount. If the payment
equals the total on the invoice, finalise the invoice. equals the total on the invoice, finalise the invoice.
(NB should be transactional.) (NB should be transactional.)
''' '''
if self.invoice.cart is not None: if self.invoice.cart:
cart = CartController(self.invoice.cart) cart = CartController(self.invoice.cart)
cart.validate_cart() # Raises ValidationError if invalid cart.validate_cart() # Raises ValidationError if invalid
@ -145,8 +147,36 @@ class InvoiceController(object):
if total == self.invoice.value: if total == self.invoice.value:
self.invoice.paid = True self.invoice.paid = True
if self.invoice.cart:
cart = self.invoice.cart cart = self.invoice.cart
cart.active = False cart.active = False
cart.save() cart.save()
self.invoice.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=( Q(time_last_updated__gt=(
timezone.now()-F('reservation_duration') 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 = [] cls.products = []
for i in xrange(4): for i in xrange(4):
prod = rego.Product.objects.create( prod = rego.Product.objects.create(
name="Product 1", name="Product " + str(i + 1),
description="This is a test product.", description="This is a test product.",
category=cls.categories[i / 2], # 2 products per category category=cls.categories[i / 2], # 2 products per category
price=Decimal("10.00"), price=Decimal("10.00"),

View file

@ -132,3 +132,21 @@ class CeilingsTestCases(RegistrationCartTestCase):
self.add_timedelta(self.RESERVATION + datetime.timedelta(seconds=1)) self.add_timedelta(self.RESERVATION + datetime.timedelta(seconds=1))
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
first_cart.validate_cart() 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.PROD_3, self.PROD_4],
) )
self.assertEqual(2, len(discounts)) 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_1 in prods)
self.assertTrue(self.PROD_2 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): with self.assertRaises(ValidationError):
current_cart.apply_voucher(voucher.code) 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)