Adds refund function, adds tests, makes sure that refunds are obeyed elsewhere in the codebase
This commit is contained in:
		
							parent
							
								
									b65223aaa1
								
							
						
					
					
						commit
						cf85af7719
					
				
					 11 changed files with 175 additions and 8 deletions
				
			
		|  | @ -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: | ||||
|  |  | |||
|  | @ -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()) | ||||
|  |  | |||
|  | @ -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")) | ||||
|  |  | |||
|  | @ -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() | ||||
|  |  | |||
|  | @ -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)) | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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"), | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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)) | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
							
								
								
									
										33
									
								
								registrasion/tests/test_refund.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								registrasion/tests/test_refund.py
									
										
									
									
									
										Normal 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) | ||||
|  | @ -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) | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 Christopher Neugebauer
						Christopher Neugebauer