diff --git a/registrasion/controllers/cart.py b/registrasion/controllers/cart.py index 06eef3ef..45796781 100644 --- a/registrasion/controllers/cart.py +++ b/registrasion/controllers/cart.py @@ -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: diff --git a/registrasion/controllers/conditions.py b/registrasion/controllers/conditions.py index 914a2092..20320218 100644 --- a/registrasion/controllers/conditions.py +++ b/registrasion/controllers/conditions.py @@ -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()) diff --git a/registrasion/controllers/discount.py b/registrasion/controllers/discount.py index dcb83dfd..7d6a959a 100644 --- a/registrasion/controllers/discount.py +++ b/registrasion/controllers/discount.py @@ -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")) diff --git a/registrasion/controllers/invoice.py b/registrasion/controllers/invoice.py index 19da7525..fdea324b 100644 --- a/registrasion/controllers/invoice.py +++ b/registrasion/controllers/invoice.py @@ -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 - cart = self.invoice.cart - cart.active = False - cart.save() + 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() diff --git a/registrasion/models.py b/registrasion/models.py index 2421adf9..2e1f95d1 100644 --- a/registrasion/models.py +++ b/registrasion/models.py @@ -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)) ) diff --git a/registrasion/tests/test_cart.py b/registrasion/tests/test_cart.py index 5989f6e2..03d31b54 100644 --- a/registrasion/tests/test_cart.py +++ b/registrasion/tests/test_cart.py @@ -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"), diff --git a/registrasion/tests/test_ceilings.py b/registrasion/tests/test_ceilings.py index 0bbd6e9e..c39df39a 100644 --- a/registrasion/tests/test_ceilings.py +++ b/registrasion/tests/test_ceilings.py @@ -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) diff --git a/registrasion/tests/test_discount.py b/registrasion/tests/test_discount.py index 222afc09..5f536e9b 100644 --- a/registrasion/tests/test_discount.py +++ b/registrasion/tests/test_discount.py @@ -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)) diff --git a/registrasion/tests/test_enabling_condition.py b/registrasion/tests/test_enabling_condition.py index e7155dea..2861e8db 100644 --- a/registrasion/tests/test_enabling_condition.py +++ b/registrasion/tests/test_enabling_condition.py @@ -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) diff --git a/registrasion/tests/test_refund.py b/registrasion/tests/test_refund.py new file mode 100644 index 00000000..14811b78 --- /dev/null +++ b/registrasion/tests/test_refund.py @@ -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) diff --git a/registrasion/tests/test_voucher.py b/registrasion/tests/test_voucher.py index db59d094..abc7c8c3 100644 --- a/registrasion/tests/test_voucher.py +++ b/registrasion/tests/test_voucher.py @@ -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)