Imports code from old Symposion repo

This commit is contained in:
Christopher Neugebauer 2016-01-22 16:01:30 +11:00
parent ecd5e08263
commit d9e433659d
19 changed files with 2554 additions and 0 deletions

298
design/design.md Normal file
View file

@ -0,0 +1,298 @@
# Logic
## Definitions
- User has one 'active Cart' at a time. The Cart remains active until a paid Invoice is attached to it.
- A 'paid Cart' is a Cart with a paid Invoice attached to it, where the Invoice has not been voided.
- An unpaid Cart is 'reserved' if
- CURRENT_TIME - "Time last updated" <= max(reservation duration of Products in Cart),
- A Voucher was added and CURRENT_TIME - "Time last updated" < VOUCHER_RESERVATION_TIME (15 minutes?)
- An Item is 'reserved' if:
- it belongs to a reserved Cart
- it belongs to a paid Cart
- A Cart can have any number of Items added to it, subject to limits.
## Entering Vouchers
- Vouchers are attached to Carts
- A user can enter codes for as many different Vouchers as they like.
- A Voucher is added to the Cart if the number of paid or reserved Carts containing the Voucher is less than the "total available" for the voucher.
- A cart is invalid if it contains a voucher that has been overused
## Are products available?
- Availability is determined by the number of items we want to add to the cart: items_to_add
- If items_to_add + count(Product in their active and paid Carts) > "Limit per user" for the Product, the Product is "unavailable".
- If the Product belongs to an exhausted Ceiling, the Product is "unavailable".
- Otherwise, the product is available
## Displaying Products:
- If there is at least one mandatory EnablingCondition attached to the Product, display it only if all EnablingConditions are met
- If there is at least one EnablingCondition attached to the Product, display it only if at least one EnablingCondition is met
- If there are zero EnablingConditions attached to the Product, display it
- If the product is not available for items_to_add=0, mark it as "unavailable"
- If the Product is displayed and available, its price is the price for the Product, minus the greatest Discount available to this Cart and Product
- The product is displayed per the rendering characteristics of the Category it belongs to
## Displaying Categories
- If the Category contains only "unavailable" Products, mark it as "unavailable"
- If the Category contains no displayed Products, do not display the Category
- If the Category contains at least one EnablingCondition, display it only if at least one EnablingCondition is met
- If the Category contains no EnablingConditions, display it
## Exhausting Ceilings
- Exhaustion is determined by the number of items we want to add to the cart: items_to_add
- A ceiling is exhausted if:
- Its start date has not yet been reached
- Its end date has been exceeded
- items_to_add + sum(paid and reserved Items for each Product in the ceiling) > Total available
## Applying Discounts
- Discounts only apply to the current cart
- Discounts can be applied to multiple carts until the user has exhausted the quantity for each product attached to the discount.
- Only one discount discount can be applied to each single item. Discounts are applied as follows:
- All non-exhausted discounts for the product or its category are ordered by value
- The highest discount is applied for the lower of the quantity of the product in the cart, or the remaining quantity from this discount
- If the quantity remaining is non-zero, apply the next available discount
- Individual discount objects should not contain more than one DiscountForProduct for the same product
- Individual discount objects should not contain more than one DiscountForCategory for the same category
- Individual discount objects should not contain a discount for both a product and its category
## Adding Items to the Cart
- Products that are not displayed may not be added to a Cart
- The requested number of items must be available for those items to be added to a Cart
- If a different price applies to a Product when it is added to a cart, add at the new price, and display an alert to the user
- If a discount is used when adding a Product to the cart, add the discount as well
- Adding an item resets the "Time last updated" for the cart
- Each time carts have items added or removed, the revision number is updated
## Generating an invoice
- User can ask to 'check out' the active Cart. Doing so generates an Invoice. The invoice corresponds to a revision number of the cart.
- Checking out the active Cart resets the "Time last updated" for the cart.
- The invoice represents the current state of the cart.
- If the revision number for the cart is different to the cart's revision number for the invoice, the invoice is void.
- The invoice is void if
## Paying an invoice
- A payment can only be attached to an invoice if all of the items in it are available at the time payment is processed
### One-Shot
- Update the "Time last updated" for the cart based on the expected time it takes for a payment to complete
- Verify that all items are available, and if so:
- Proceed to make payment
- Apply payment record from amount received
### Authorization-based approach:
- Capture an authorization on the card
- Verify that all items are available, and if so:
- Apply payment record
- Take payment
# Registration workflow:
## User has not taken a guided registration yet:
User is shown two options:
1. Undertake guided registration ("for current user")
1. Purchase vouchers
## User has not purchased a ticket, and wishes to:
This gives the user a guided registration process.
1. Take list of categories, sorted by display order, and display the next lowest enabled & available category
1. Take user to category page
1. User can click "back" to go to previous screen, or "next" to go the next lowest enabled & available category
Once all categories have been seen:
1. Ask for badge information -- badge information is *not* the same as the invoicee.
1. User is taken to the "user has purchased a ticket" workflow
## User is buying vouchers
TODO: Consider separate workflow for purchasing ticket vouchers.
## User has completed a guided registration or purchased vouchers
1. Show list of products that are pending purchase.
1. Show list of categories + badge information, as well as 'checkout' button if the user has items in their current cart
## Category page
- User can enter a voucher at any time
- User is shown the list of products that have been paid for
- User has the option to add/remove products that are in the current cart
## Checkout
1. Ask for invoicing details (pre-fill from previous invoice?)
1. Ask for payment
# User Models
- Profile:
- User
- Has done guided registration?
- Badge
-
## Transaction Models
- Cart:
- User
- {Items}
- {Voucher}
- {DiscountItems}
- Time last updated
- Revision Number
- Active?
- Item
- Product
- Quantity
- DiscountItem
- Product
- Discount
- Quantity
- Invoice:
- Invoice number
- User
- Cart
- Cart Revision
- {Line Items}
- (Invoice Details)
- {Payments}
- Voided?
- LineItem
- Description
- Quantity
- Price
- Payment
- Time
- Amount
- Reference
## Inventory Model
- Product:
- Name
- Description
- Category
- Price
- Limit per user
- Reservation duration
- Display order
- {Ceilings}
- Voucher
- Description
- Code
- Total available
- Category?
- Name
- Description
- Display Order
- Rendering Style
## Product Modifiers
- Discount:
- Description
- {DiscountForProduct}
- {DiscountForCategory}
- Discount Types:
- TimeOrStockLimitDiscount:
* A discount that is available for a limited amount of time, e.g. Early Bird sales *
- Start date
- End date
- Total available
- VoucherDiscount:
* A discount that is available to a specific voucher *
- Voucher
- RoleDiscount
* A discount that is available to a specific role *
- Role
- IncludedProductDiscount:
* A discount that is available because another product has been purchased *
- {Parent Product}
- DiscountForProduct
- Product
- Amount
- Percentage
- Quantity
- DiscountForCategory
- Category
- Percentage
- Quantity
- EnablingCondition:
- Description
- Mandatory?
- {Products}
- {Categories}
- EnablingCondition Types:
- ProductEnablingCondition:
* Enabling because the user has purchased a specific product *
- {Products that enable}
- CategoryEnablingCondition:
* Enabling because the user has purchased a product in a specific category *
- {Categories that enable}
- VoucherEnablingCondition:
* Enabling because the user has entered a voucher code *
- Voucher
- RoleEnablingCondition:
* Enabling because the user has a specific role *
- Role
- TimeOrStockLimitEnablingCondition:
* Enabling because a time condition has been met, or a number of items underneath it have not been sold *
- Start date
- End date
- Total available

3
registrasion/__init__.py Normal file
View file

@ -0,0 +1,3 @@
__version__ = "0.1a1"
default_app_config = "registrasion.apps.RegistrasionConfig"

83
registrasion/admin.py Normal file
View file

@ -0,0 +1,83 @@
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
import nested_admin
from registrasion import models as rego
# Inventory admin
class ProductInline(admin.TabularInline):
model = rego.Product
@admin.register(rego.Category)
class CategoryAdmin(admin.ModelAdmin):
model = rego.Category
verbose_name_plural = _("Categories")
inlines = [
ProductInline,
]
admin.site.register(rego.Product)
# Discounts
class DiscountForProductInline(admin.TabularInline):
model = rego.DiscountForProduct
verbose_name = _("Product included in discount")
verbose_name_plural = _("Products included in discount")
class DiscountForCategoryInline(admin.TabularInline):
model = rego.DiscountForCategory
verbose_name = _("Category included in discount")
verbose_name_plural = _("Categories included in discount")
@admin.register(
rego.TimeOrStockLimitDiscount,
rego.IncludedProductDiscount,
)
class DiscountAdmin(admin.ModelAdmin):
inlines = [
DiscountForProductInline,
DiscountForCategoryInline,
]
# Vouchers
class VoucherDiscountInline(nested_admin.NestedStackedInline):
model = rego.VoucherDiscount
verbose_name = _("Discount")
# TODO work out why we're allowed to add more than one?
max_num = 1
extra = 1
inlines = [
DiscountForProductInline,
DiscountForCategoryInline,
]
class VoucherEnablingConditionInline(nested_admin.NestedStackedInline):
model = rego.VoucherEnablingCondition
verbose_name = _("Product and category enabled by voucher")
verbose_name_plural = _("Products and categories enabled by voucher")
# TODO work out why we're allowed to add more than one?
max_num = 1
extra = 1
@admin.register(rego.Voucher)
class VoucherAdmin(nested_admin.NestedAdmin):
model = rego.Voucher
inlines = [
VoucherDiscountInline,
VoucherEnablingConditionInline,
]

8
registrasion/apps.py Normal file
View file

@ -0,0 +1,8 @@
from __future__ import unicode_literals
from django.apps import AppConfig
class RegistrasionConfig(AppConfig):
name = "registrasion"
label = "registrasion"
verbose_name = "Registrasion"

210
registrasion/cart.py Normal file
View file

@ -0,0 +1,210 @@
import datetime
import itertools
from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ValidationError
from django.db.models import Avg, Min, Max, Sum
from django.utils import timezone
from registrasion import models as rego
from conditions import ConditionController
from controllers import ProductController
class CartController(object):
def __init__(self, cart):
self.cart = cart
@staticmethod
def for_user(user):
''' Returns the user's current cart, or creates a new cart
if there isn't one ready yet. '''
try:
existing = rego.Cart.objects.get(user=user, active=True)
except ObjectDoesNotExist:
existing = rego.Cart.objects.create(
user=user,
time_last_updated=timezone.now(),
reservation_duration=datetime.timedelta(),
)
existing.save()
return CartController(existing)
def extend_reservation(self):
''' Updates the cart's time last updated value, which is used to
determine whether the cart has reserved the items and discounts it
holds. '''
reservations = [datetime.timedelta()]
# If we have vouchers, we're entitled to an hour at minimum.
if len(self.cart.vouchers.all()) >= 1:
reservations.append(rego.Voucher.RESERVATION_DURATION)
# Else, it's the maximum of the included products
items = rego.ProductItem.objects.filter(cart=self.cart)
agg = items.aggregate(Max("product__reservation_duration"))
product_max = agg["product__reservation_duration__max"]
if product_max is not None:
reservations.append(product_max)
self.cart.time_last_updated = timezone.now()
self.cart.reservation_duration = max(reservations)
def add_to_cart(self, product, quantity):
''' Adds _quantity_ of the given _product_ to the cart. Raises
ValidationError if constraints are violated.'''
prod = ProductController(product)
# TODO: Check enabling conditions for product for user
if not prod.can_add_with_enabling_conditions(self.cart.user, quantity):
raise ValidationError("Not enough of that product left (ec)")
if not prod.user_can_add_within_limit(self.cart.user, quantity):
raise ValidationError("Not enough of that product left (user)")
try:
# Try to update an existing item within this cart if possible.
product_item = rego.ProductItem.objects.get(
cart=self.cart,
product=product)
product_item.quantity += quantity
except ObjectDoesNotExist:
product_item = rego.ProductItem.objects.create(
cart=self.cart,
product=product,
quantity=quantity,
)
product_item.save()
self.recalculate_discounts()
self.extend_reservation()
self.cart.revision += 1
self.cart.save()
def apply_voucher(self, voucher):
''' Applies the given voucher to this cart. '''
# TODO: is it valid for a cart to re-add a voucher that they have?
# Is voucher exhausted?
active_carts = rego.Cart.reserved_carts()
carts_with_voucher = active_carts.filter(vouchers=voucher)
if len(carts_with_voucher) >= voucher.limit:
raise ValidationError("This voucher is no longer available")
# If successful...
self.cart.vouchers.add(voucher)
self.extend_reservation()
self.cart.revision += 1
self.cart.save()
def validate_cart(self):
''' Determines whether the status of the current cart is valid;
this is normally called before generating or paying an invoice '''
is_reserved = self.cart in rego.Cart.reserved_carts()
# TODO: validate vouchers
items = rego.ProductItem.objects.filter(cart=self.cart)
for item in items:
# per-user limits are tested at add time, and are unliklely to change
prod = ProductController(item.product)
# If the cart is not reserved, we need to see if we can re-reserve
quantity = 0 if is_reserved else item.quantity
if not prod.can_add_with_enabling_conditions(self.cart.user, quantity):
raise ValidationError("Products are no longer available")
# Validate the discounts
discount_items = rego.DiscountItem.objects.filter(cart=self.cart)
seen_discounts = set()
for discount_item in discount_items:
discount = discount_item.discount
if discount in seen_discounts:
continue
seen_discounts.add(discount)
real_discount = rego.DiscountBase.objects.get_subclass(pk=discount.pk)
cond = ConditionController.for_condition(real_discount)
quantity = 0 if is_reserved else discount_item.quantity
if not cond.is_met(self.cart.user, quantity):
raise ValidationError("Discounts are no longer available")
def recalculate_discounts(self):
''' Calculates all of the discounts available for this product.
NB should be transactional, and it's terribly inefficient.
'''
# Delete the existing entries.
rego.DiscountItem.objects.filter(cart=self.cart).delete()
for item in self.cart.productitem_set.all():
self._add_discount(item.product, item.quantity)
def _add_discount(self, product, quantity):
''' Calculates the best available discounts for this product.
NB this will be super-inefficient in aggregate because discounts will be
re-tested for each product. We should work on that.'''
prod = ProductController(product)
discounts = prod.available_discounts(self.cart.user)
discounts.sort(key=lambda discount: discount.value)
for discount in reversed(discounts):
if quantity == 0:
break
# Get the count of past uses of this discount condition
# as this affects the total amount we're allowed to use now.
past_uses = rego.DiscountItem.objects.filter(
cart__active=False,
discount=discount.discount,
product=product,
)
agg = past_uses.aggregate(Sum("quantity"))
past_uses = agg["quantity__sum"]
if past_uses is None:
past_uses = 0
if past_uses == discount.condition.quantity:
continue
# Get a provisional instance for this DiscountItem
# with the quantity set to as much as we have in the cart
discount_item = rego.DiscountItem.objects.create(
product=product,
cart=self.cart,
discount=discount.discount,
quantity=quantity,
)
# Truncate the quantity for this DiscountItem if we exceed quantity
ours = discount_item.quantity
allowed = discount.condition.quantity - past_uses
if ours > allowed:
discount_item.quantity = allowed
# Update the remaining quantity.
quantity = ours - allowed
else:
quantity = 0
discount_item.save()

160
registrasion/conditions.py Normal file
View file

@ -0,0 +1,160 @@
from django.db.models import F, Q
from django.db.models import Sum
from django.utils import timezone
from registrasion import models as rego
class ConditionController(object):
''' Base class for testing conditions that activate EnablingCondition
or Discount objects. '''
def __init__(self):
pass
@staticmethod
def for_condition(condition):
CONTROLLERS = {
rego.CategoryEnablingCondition : CategoryConditionController,
rego.IncludedProductDiscount : ProductConditionController,
rego.ProductEnablingCondition : ProductConditionController,
rego.TimeOrStockLimitDiscount :
TimeOrStockLimitConditionController,
rego.TimeOrStockLimitEnablingCondition :
TimeOrStockLimitConditionController,
rego.VoucherDiscount : VoucherConditionController,
rego.VoucherEnablingCondition : VoucherConditionController,
}
try:
return CONTROLLERS[type(condition)](condition)
except KeyError:
return ConditionController()
def is_met(self, user, quantity):
return True
class CategoryConditionController(ConditionController):
def __init__(self, condition):
self.condition = condition
def is_met(self, user, quantity):
''' 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)
enabling_products = rego.Product.objects.filter(
category=self.condition.enabling_category)
products = rego.ProductItem.objects.filter(cart=carts,
product=enabling_products)
return len(products) > 0
class ProductConditionController(ConditionController):
''' Condition tests for ProductEnablingCondition and
IncludedProductDiscount. '''
def __init__(self, condition):
self.condition = condition
def is_met(self, user, quantity):
''' returns True if the user has a product that invokes this
condition in one of their carts '''
carts = rego.Cart.objects.filter(user=user)
products = rego.ProductItem.objects.filter(cart=carts,
product=self.condition.enabling_products.all())
return len(products) > 0
class TimeOrStockLimitConditionController(ConditionController):
''' Condition tests for TimeOrStockLimit EnablingCondition and
Discount.'''
def __init__(self, ceiling):
self.ceiling = ceiling
def is_met(self, user, quantity):
''' returns True if adding _quantity_ of _product_ will not vioilate
this ceiling. '''
# Test date range
if not self.test_date_range():
return False
# Test limits
if not self.test_limits(quantity):
return False
# All limits have been met
return True
def test_date_range(self):
now = timezone.now()
if self.ceiling.start_time is not None:
if now < self.ceiling.start_time:
return False
if self.ceiling.end_time is not None:
if now > self.ceiling.end_time:
return False
return True
def _products(self):
''' Abstracts away the product list, becuase enabling conditions
list products differently to discounts. '''
if isinstance(self.ceiling, rego.TimeOrStockLimitEnablingCondition):
category_products = rego.Product.objects.filter(
category=self.ceiling.categories.all()
)
return self.ceiling.products.all() | category_products
else:
categories = rego.Category.objects.filter(
discountforcategory__discount=self.ceiling
)
return rego.Product.objects.filter(
Q(discountforproduct__discount=self.ceiling) |
Q(category=categories.all())
)
def test_limits(self, quantity):
if self.ceiling.limit is None:
return True
reserved_carts = rego.Cart.reserved_carts()
product_items = rego.ProductItem.objects.filter(
product=self._products().all()
)
product_items = product_items.filter(cart=reserved_carts)
agg = product_items.aggregate(Sum("quantity"))
count = agg["quantity__sum"]
if count is None:
count = 0
if count + quantity > self.ceiling.limit:
return False
return True
class VoucherConditionController(ConditionController):
''' Condition test for VoucherEnablingCondition and VoucherDiscount.'''
def __init__(self, condition):
self.condition = condition
def is_met(self, user, quantity):
''' returns True if the user has the given voucher attached. '''
carts = rego.Cart.objects.filter(user=user,
vouchers=self.condition.voucher)
return len(carts) > 0

View file

@ -0,0 +1,99 @@
import itertools
from collections import namedtuple
from django.db.models import F, Q
from registrasion import models as rego
from conditions import ConditionController
DiscountEnabler = namedtuple("DiscountEnabler", ("discount", "condition", "value"))
class ProductController(object):
def __init__(self, product):
self.product = product
def user_can_add_within_limit(self, user, quantity):
''' Return true if the user is able to add _quantity_ to their count of
this Product without exceeding _limit_per_user_.'''
carts = rego.Cart.objects.filter(user=user)
items = rego.ProductItem.objects.filter(product=self.product, cart=carts)
count = 0
for item in items:
count += item.quantity
if quantity + count > self.product.limit_per_user:
return False
else:
return True
def can_add_with_enabling_conditions(self, user, quantity):
''' Returns true if the user is able to add _quantity_ to their count
of this Product without exceeding the ceilings the product is attached
to. '''
conditions = rego.EnablingConditionBase.objects.filter(
Q(products=self.product) | Q(categories=self.product.category)
).select_subclasses()
mandatory_violated = False
non_mandatory_met = False
for condition in conditions:
cond = ConditionController.for_condition(condition)
met = cond.is_met(user, quantity)
if condition.mandatory and not met:
mandatory_violated = True
break
if met:
non_mandatory_met = True
if mandatory_violated:
# All mandatory conditions must be met
return False
if len(conditions) > 0 and not non_mandatory_met:
# If there's any non-mandatory conditions, one must be met
return False
return True
def get_enabler(self, condition):
if condition.percentage is not None:
value = condition.percentage * self.product.price
else:
value = condition.price
return DiscountEnabler(
discount=condition.discount,
condition=condition,
value=value
)
def available_discounts(self, user):
''' Returns the set of available discounts for this user, for this
product. '''
product_discounts = rego.DiscountForProduct.objects.filter(
product=self.product)
category_discounts = rego.DiscountForCategory.objects.filter(
category=self.product.category
)
potential_discounts = set(itertools.chain(
(self.get_enabler(i) for i in product_discounts),
(self.get_enabler(i) for i in category_discounts),
))
discounts = []
for discount in potential_discounts:
real_discount = rego.DiscountBase.objects.get_subclass(pk=discount.discount.pk)
cond = ConditionController.for_condition(real_discount)
if cond.is_met(user, 0):
discounts.append(discount)
return discounts

137
registrasion/invoice.py Normal file
View file

@ -0,0 +1,137 @@
from decimal import Decimal
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Avg, Min, Max, Sum
from registrasion import models as rego
from cart import CartController
class InvoiceController(object):
def __init__(self, invoice):
self.invoice = invoice
@classmethod
def for_cart(cls, cart):
''' Returns an invoice object for a given cart at its current revision.
If such an invoice does not exist, the cart is validated, and if valid,
an invoice is generated.'''
try:
invoice = rego.Invoice.objects.get(
cart=cart, cart_revision=cart.revision)
except ObjectDoesNotExist:
cart_controller = CartController(cart)
cart_controller.validate_cart() # Raises ValidationError on fail.
invoice = cls._generate(cart)
return InvoiceController(invoice)
@classmethod
def resolve_discount_value(cls, item):
try:
condition = rego.DiscountForProduct.objects.get(
discount=item.discount,
product=item.product
)
except ObjectDoesNotExist:
condition = rego.DiscountForCategory.objects.get(
discount=item.discount,
category=item.product.category
)
if condition.percentage is not None:
value = item.product.price * (condition.percentage / 100)
else:
value = condition.price
return value
@classmethod
def _generate(cls, cart):
''' Generates an invoice for the given cart. '''
invoice = rego.Invoice.objects.create(
user=cart.user,
cart=cart,
cart_revision=cart.revision,
value=Decimal()
)
invoice.save()
# TODO: calculate line items.
product_items = rego.ProductItem.objects.filter(cart=cart)
discount_items = rego.DiscountItem.objects.filter(cart=cart)
invoice_value = Decimal()
for item in product_items:
line_item = rego.LineItem.objects.create(
invoice=invoice,
description=item.product.name,
quantity=item.quantity,
price=item.product.price,
)
line_item.save()
invoice_value += line_item.quantity * line_item.price
for item in discount_items:
line_item = rego.LineItem.objects.create(
invoice=invoice,
description=item.discount.description,
quantity=item.quantity,
price=cls.resolve_discount_value(item) * -1,
)
line_item.save()
invoice_value += line_item.quantity * line_item.price
# TODO: calculate line items from discounts
invoice.value = invoice_value
invoice.save()
return invoice
def is_valid(self):
''' Returns true if the attached invoice is not void and it represents
a valid cart. '''
if self.invoice.void:
return False
if self.invoice.cart is not None:
if self.invoice.cart.revision != self.invoice.cart_revision:
return False
return True
def void(self):
''' Voids the invoice. '''
self.invoice.void = True
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:
cart = CartController(self.invoice.cart)
cart.validate_cart() # Raises ValidationError if invalid
''' Adds a payment '''
payment = rego.Payment.objects.create(
invoice=self.invoice,
reference=reference,
amount=amount,
)
payment.save()
payments = rego.Payment.objects .filter(invoice=self.invoice)
agg = payments.aggregate(Sum("amount"))
total = agg["amount__sum"]
if total==self.invoice.value:
self.invoice.paid = True
cart = self.invoice.cart
cart.active = False
cart.save()
self.invoice.save()

View file

@ -0,0 +1,270 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import datetime
import django.utils.timezone
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Badge',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=256)),
('company', models.CharField(max_length=256)),
],
),
migrations.CreateModel(
name='Cart',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('time_last_updated', models.DateTimeField()),
('reservation_duration', models.DurationField()),
('revision', models.PositiveIntegerField(default=1)),
('active', models.BooleanField(default=True)),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Category',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=65, verbose_name='Name')),
('description', models.CharField(max_length=255, verbose_name='Description')),
('order', models.PositiveIntegerField(verbose_name='Display order')),
('render_type', models.IntegerField(verbose_name='Render type', choices=[(1, 'Radio button'), (2, 'Quantity boxes')])),
],
),
migrations.CreateModel(
name='DiscountBase',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('description', models.CharField(max_length=255, verbose_name='Description')),
],
),
migrations.CreateModel(
name='DiscountForCategory',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('percentage', models.DecimalField(max_digits=4, decimal_places=1, blank=True)),
('quantity', models.PositiveIntegerField()),
('category', models.ForeignKey(to='registrasion.Category')),
],
),
migrations.CreateModel(
name='DiscountForProduct',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('percentage', models.DecimalField(null=True, max_digits=4, decimal_places=1)),
('price', models.DecimalField(null=True, max_digits=8, decimal_places=2)),
('quantity', models.PositiveIntegerField()),
],
),
migrations.CreateModel(
name='DiscountItem',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('quantity', models.PositiveIntegerField()),
('cart', models.ForeignKey(to='registrasion.Cart')),
],
),
migrations.CreateModel(
name='EnablingConditionBase',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('description', models.CharField(max_length=255)),
('mandatory', models.BooleanField(default=False)),
],
),
migrations.CreateModel(
name='Invoice',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('cart_revision', models.IntegerField(null=True)),
('void', models.BooleanField(default=False)),
('paid', models.BooleanField(default=False)),
('value', models.DecimalField(max_digits=8, decimal_places=2)),
('cart', models.ForeignKey(to='registrasion.Cart', null=True)),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='LineItem',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('description', models.CharField(max_length=255)),
('quantity', models.PositiveIntegerField()),
('price', models.DecimalField(max_digits=8, decimal_places=2)),
('invoice', models.ForeignKey(to='registrasion.Invoice')),
],
),
migrations.CreateModel(
name='Payment',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('time', models.DateTimeField(default=django.utils.timezone.now)),
('reference', models.CharField(max_length=64)),
('amount', models.DecimalField(max_digits=8, decimal_places=2)),
('invoice', models.ForeignKey(to='registrasion.Invoice')),
],
),
migrations.CreateModel(
name='Product',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=65, verbose_name='Name')),
('description', models.CharField(max_length=255, verbose_name='Description')),
('price', models.DecimalField(verbose_name='Price', max_digits=8, decimal_places=2)),
('limit_per_user', models.PositiveIntegerField(verbose_name='Limit per user', blank=True)),
('reservation_duration', models.DurationField(default=datetime.timedelta(0, 3600), verbose_name='Reservation duration')),
('order', models.PositiveIntegerField(verbose_name='Display order')),
('category', models.ForeignKey(verbose_name='Product category', to='registrasion.Category')),
],
),
migrations.CreateModel(
name='ProductItem',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('quantity', models.PositiveIntegerField()),
('cart', models.ForeignKey(to='registrasion.Cart')),
('product', models.ForeignKey(to='registrasion.Product')),
],
),
migrations.CreateModel(
name='Profile',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('completed_registration', models.BooleanField(default=False)),
('highest_complete_category', models.IntegerField(default=0)),
('user', models.OneToOneField(to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Voucher',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('recipient', models.CharField(max_length=64, verbose_name='Recipient')),
('code', models.CharField(unique=True, max_length=16, verbose_name='Voucher code')),
('limit', models.PositiveIntegerField(verbose_name='Voucher use limit')),
],
),
migrations.CreateModel(
name='CategoryEnablingCondition',
fields=[
('enablingconditionbase_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='registrasion.EnablingConditionBase')),
('enabling_category', models.ForeignKey(to='registrasion.Category')),
],
bases=('registrasion.enablingconditionbase',),
),
migrations.CreateModel(
name='IncludedProductDiscount',
fields=[
('discountbase_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='registrasion.DiscountBase')),
('enabling_products', models.ManyToManyField(to='registrasion.Product', verbose_name='Including product')),
],
options={
'verbose_name': 'Product inclusion',
},
bases=('registrasion.discountbase',),
),
migrations.CreateModel(
name='ProductEnablingCondition',
fields=[
('enablingconditionbase_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='registrasion.EnablingConditionBase')),
('enabling_products', models.ManyToManyField(to='registrasion.Product')),
],
bases=('registrasion.enablingconditionbase',),
),
migrations.CreateModel(
name='TimeOrStockLimitDiscount',
fields=[
('discountbase_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='registrasion.DiscountBase')),
('start_time', models.DateTimeField(null=True, verbose_name='Start time')),
('end_time', models.DateTimeField(null=True, verbose_name='End time')),
('limit', models.PositiveIntegerField(null=True, verbose_name='Limit')),
],
options={
'verbose_name': 'Promotional discount',
},
bases=('registrasion.discountbase',),
),
migrations.CreateModel(
name='TimeOrStockLimitEnablingCondition',
fields=[
('enablingconditionbase_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='registrasion.EnablingConditionBase')),
('start_time', models.DateTimeField(null=True, verbose_name='Start time')),
('end_time', models.DateTimeField(null=True, verbose_name='End time')),
('limit', models.PositiveIntegerField(null=True, verbose_name='Limit')),
],
bases=('registrasion.enablingconditionbase',),
),
migrations.CreateModel(
name='VoucherDiscount',
fields=[
('discountbase_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='registrasion.DiscountBase')),
('voucher', models.OneToOneField(verbose_name='Voucher', to='registrasion.Voucher')),
],
bases=('registrasion.discountbase',),
),
migrations.CreateModel(
name='VoucherEnablingCondition',
fields=[
('enablingconditionbase_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='registrasion.EnablingConditionBase')),
('voucher', models.OneToOneField(to='registrasion.Voucher')),
],
bases=('registrasion.enablingconditionbase',),
),
migrations.AddField(
model_name='enablingconditionbase',
name='categories',
field=models.ManyToManyField(to='registrasion.Category'),
),
migrations.AddField(
model_name='enablingconditionbase',
name='products',
field=models.ManyToManyField(to='registrasion.Product'),
),
migrations.AddField(
model_name='discountitem',
name='discount',
field=models.ForeignKey(to='registrasion.DiscountBase'),
),
migrations.AddField(
model_name='discountitem',
name='product',
field=models.ForeignKey(to='registrasion.Product'),
),
migrations.AddField(
model_name='discountforproduct',
name='discount',
field=models.ForeignKey(to='registrasion.DiscountBase'),
),
migrations.AddField(
model_name='discountforproduct',
name='product',
field=models.ForeignKey(to='registrasion.Product'),
),
migrations.AddField(
model_name='discountforcategory',
name='discount',
field=models.ForeignKey(to='registrasion.DiscountBase'),
),
migrations.AddField(
model_name='cart',
name='vouchers',
field=models.ManyToManyField(to='registrasion.Voucher', blank=True),
),
migrations.AddField(
model_name='badge',
name='profile',
field=models.OneToOneField(to='registrasion.Profile'),
),
]

View file

375
registrasion/models.py Normal file
View file

@ -0,0 +1,375 @@
from __future__ import unicode_literals
import datetime
from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ValidationError
from django.contrib.auth.models import User
from django.db import models
from django.db.models import F, Q
from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from model_utils.managers import InheritanceManager
from symposion.markdown_parser import parse
from symposion.proposals.models import ProposalBase
# User models
@python_2_unicode_compatible
class Profile(models.Model):
''' Miscellaneous user-related data. '''
def __str__(self):
return "%s" % self.user
user = models.OneToOneField(User, on_delete=models.CASCADE)
# Badge is linked
completed_registration = models.BooleanField(default=False)
highest_complete_category = models.IntegerField(default=0)
@python_2_unicode_compatible
class Badge(models.Model):
''' Information for an attendee's badge. '''
def __str__(self):
return "Badge for: %s of %s" % (self.name, self.company)
profile = models.OneToOneField(Profile, on_delete=models.CASCADE)
name = models.CharField(max_length=256)
company = models.CharField(max_length=256)
# Inventory Models
@python_2_unicode_compatible
class Category(models.Model):
''' Registration product categories '''
def __str__(self):
return self.name
RENDER_TYPE_RADIO = 1
RENDER_TYPE_QUANTITY = 2
CATEGORY_RENDER_TYPES = [
(RENDER_TYPE_RADIO, _("Radio button")),
(RENDER_TYPE_QUANTITY, _("Quantity boxes")),
]
name = models.CharField(max_length=65, verbose_name=_("Name"))
description = models.CharField(max_length=255, verbose_name=_("Description"))
order = models.PositiveIntegerField(verbose_name=("Display order"))
render_type = models.IntegerField(choices=CATEGORY_RENDER_TYPES, verbose_name=_("Render type"))
@python_2_unicode_compatible
class Product(models.Model):
''' Registration products '''
def __str__(self):
return self.name
name = models.CharField(max_length=65, verbose_name=_("Name"))
description = models.CharField(max_length=255, verbose_name=_("Description"))
category = models.ForeignKey(Category, verbose_name=_("Product category"))
price = models.DecimalField(max_digits=8, decimal_places=2, verbose_name=_("Price"))
limit_per_user = models.PositiveIntegerField(blank=True, verbose_name=_("Limit per user"))
reservation_duration = models.DurationField(
default=datetime.timedelta(hours=1),
verbose_name=_("Reservation duration"))
order = models.PositiveIntegerField(verbose_name=("Display order"))
@python_2_unicode_compatible
class Voucher(models.Model):
''' Registration vouchers '''
# Vouchers reserve a cart for a fixed amount of time, so that
# items may be added without the voucher being swiped by someone else
RESERVATION_DURATION = datetime.timedelta(hours=1)
def __str__(self):
return "Voucher for %s" % self.recipient
recipient = models.CharField(max_length=64, verbose_name=_("Recipient"))
code = models.CharField(max_length=16, unique=True, verbose_name=_("Voucher code"))
limit = models.PositiveIntegerField(verbose_name=_("Voucher use limit"))
# Product Modifiers
@python_2_unicode_compatible
class DiscountBase(models.Model):
''' Base class for discounts. Each subclass has controller code that
determines whether or not the given discount is available to be added to the
current cart. '''
objects = InheritanceManager()
def __str__(self):
return "Discount: " + self.description
description = models.CharField(max_length=255,
verbose_name=_("Description"))
@python_2_unicode_compatible
class DiscountForProduct(models.Model):
''' Represents a discount on an individual product. Each Discount can
contain multiple products and categories. Discounts can either be a
percentage or a fixed amount, but not both. '''
def __str__(self):
if self.percentage:
return "%s%% off %s" % (self.percentage, self.product)
elif self.price:
return "$%s off %s" % (self.price, self.product)
def clean(self):
if self.percentage is None and self.price is None:
raise ValidationError(
_("Discount must have a percentage or a price."))
elif self.percentage is not None and self.price is not None:
raise ValidationError(
_("Discount may only have a percentage or only a price."))
discount = models.ForeignKey(DiscountBase, on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
percentage = models.DecimalField(max_digits=4, decimal_places=1, null=True)
price = models.DecimalField(max_digits=8, decimal_places=2, null=True)
quantity = models.PositiveIntegerField()
@python_2_unicode_compatible
class DiscountForCategory(models.Model):
''' Represents a discount for a category of products. Each discount can
contain multiple products. Category discounts can only be a percentage. '''
def __str__(self):
return "%s%% off %s" % (self.percentage, self.category)
discount = models.ForeignKey(DiscountBase, on_delete=models.CASCADE)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
percentage = models.DecimalField(max_digits=4, decimal_places=1, blank=True)
quantity = models.PositiveIntegerField()
class TimeOrStockLimitDiscount(DiscountBase):
''' Discounts that are generally available, but are limited by timespan or
usage count. This is for e.g. Early Bird discounts. '''
class Meta:
verbose_name = _("Promotional discount")
start_time = models.DateTimeField(null=True, verbose_name=_("Start time"))
end_time = models.DateTimeField(null=True, verbose_name=_("End time"))
limit = models.PositiveIntegerField(null=True, verbose_name=_("Limit"))
class VoucherDiscount(DiscountBase):
''' Discounts that are enabled when a voucher code is in the current
cart. '''
voucher = models.OneToOneField(Voucher, on_delete=models.CASCADE,
verbose_name=_("Voucher"))
class IncludedProductDiscount(DiscountBase):
''' Discounts that are enabled because another product has been purchased.
e.g. A conference ticket includes a free t-shirt. '''
class Meta:
verbose_name = _("Product inclusion")
enabling_products = models.ManyToManyField(Product,
verbose_name=_("Including product"))
class RoleDiscount(object):
''' Discounts that are enabled because the active user has a specific
role. This is for e.g. volunteers who can get a discount ticket. '''
## TODO: implement RoleDiscount
pass
@python_2_unicode_compatible
class EnablingConditionBase(models.Model):
''' This defines a condition which allows products or categories to
be made visible. If there is at least one mandatory enabling condition
defined on a Product or Category, it will only be enabled if *all*
mandatory conditions are met, otherwise, if there is at least one enabling
condition defined on a Product or Category, it will only be enabled if at
least one condition is met. '''
objects = InheritanceManager()
def __str__(self):
return self.name
description = models.CharField(max_length=255)
mandatory = models.BooleanField(default=False)
products = models.ManyToManyField(Product)
categories = models.ManyToManyField(Category)
class TimeOrStockLimitEnablingCondition(EnablingConditionBase):
''' Registration product ceilings '''
start_time = models.DateTimeField(null=True, verbose_name=_("Start time"))
end_time = models.DateTimeField(null=True, verbose_name=_("End time"))
limit = models.PositiveIntegerField(null=True, verbose_name=_("Limit"))
@python_2_unicode_compatible
class ProductEnablingCondition(EnablingConditionBase):
''' The condition is met because a specific product is purchased. '''
def __str__(self):
return "Enabled by product: "
enabling_products = models.ManyToManyField(Product)
@python_2_unicode_compatible
class CategoryEnablingCondition(EnablingConditionBase):
''' The condition is met because a product in a particular product is
purchased. '''
def __str__(self):
return "Enabled by product in category: "
enabling_category = models.ForeignKey(Category)
@python_2_unicode_compatible
class VoucherEnablingCondition(EnablingConditionBase):
''' The condition is met because a Voucher is present. This is for e.g.
enabling sponsor tickets. '''
def __str__(self):
return "Enabled by voucher: %s" % voucher
voucher = models.OneToOneField(Voucher)
#@python_2_unicode_compatible
class RoleEnablingCondition(object):
''' The condition is met because the active user has a particular Role.
This is for e.g. enabling Team tickets. '''
## TODO: implement RoleEnablingCondition
pass
# Commerce Models
@python_2_unicode_compatible
class Cart(models.Model):
''' Represents a set of product items that have been purchased, or are
pending purchase. '''
def __str__(self):
return "%d rev #%d" % (self.id, self.revision)
user = models.ForeignKey(User)
# ProductItems (foreign key)
vouchers = models.ManyToManyField(Voucher, blank=True)
time_last_updated = models.DateTimeField()
reservation_duration = models.DurationField()
revision = models.PositiveIntegerField(default=1)
active = models.BooleanField(default=True)
@classmethod
def reserved_carts(cls):
''' Gets all carts that are 'reserved' '''
return Cart.objects.filter(
(Q(active=True) &
Q(time_last_updated__gt=timezone.now()-F('reservation_duration')
)) |
Q(active=False)
)
@python_2_unicode_compatible
class ProductItem(models.Model):
''' Represents a product-quantity pair in a Cart. '''
def __str__(self):
return "product: %s * %d in Cart: %s" % (
self.product, self.quantity, self.cart)
cart = models.ForeignKey(Cart)
product = models.ForeignKey(Product)
quantity = models.PositiveIntegerField()
@python_2_unicode_compatible
class DiscountItem(models.Model):
''' Represents a discount-product-quantity relation in a Cart. '''
def __str__(self):
return "%s: %s * %d in Cart: %s" % (
self.discount, self.product, self.quantity, self.cart)
cart = models.ForeignKey(Cart)
product = models.ForeignKey(Product)
discount = models.ForeignKey(DiscountBase)
quantity = models.PositiveIntegerField()
@python_2_unicode_compatible
class Invoice(models.Model):
''' An invoice. Invoices can be automatically generated when checking out
a Cart, in which case, it is attached to a given revision of a Cart. '''
def __str__(self):
return "Invoice #%d" % self.id
def clean(self):
if self.cart is not None and self.cart_revision is None:
raise ValidationError(
"If this is a cart invoice, it must have a revision")
# Invoice Number
user = models.ForeignKey(User)
cart = models.ForeignKey(Cart, null=True)
cart_revision = models.IntegerField(null=True)
# Line Items (foreign key)
void = models.BooleanField(default=False)
paid = models.BooleanField(default=False)
value = models.DecimalField(max_digits=8, decimal_places=2)
@python_2_unicode_compatible
class LineItem(models.Model):
''' Line items for an invoice. These are denormalised from the ProductItems
and DiscountItems that belong to a cart (for consistency), but also allow
for arbitrary line items when required. '''
def __str__(self):
return "Line: %s * %d @ %s" % (
self.description, self.quantity, self.price)
invoice = models.ForeignKey(Invoice)
description = models.CharField(max_length=255)
quantity = models.PositiveIntegerField()
price = models.DecimalField(max_digits=8, decimal_places=2)
@python_2_unicode_compatible
class Payment(models.Model):
''' A payment for an invoice. Each invoice can have multiple payments
attached to it.'''
def __str__(self):
return "Payment: ref=%s amount=%s" % (self.reference, self.amount)
invoice = models.ForeignKey(Invoice)
time = models.DateTimeField(default=timezone.now)
reference = models.CharField(max_length=64)
amount = models.DecimalField(max_digits=8, decimal_places=2)

View file

@ -0,0 +1 @@
default_app_config = "registrasion.apps.RegistrationConfig"

View file

@ -0,0 +1,24 @@
from django.utils import timezone
class SetTimeMixin(object):
''' Patches timezone.now() for the duration of a test case. Allows us to
test time-based conditions (ceilings etc) relatively easily. '''
def setUp(self):
super(SetTimeMixin, self).setUp()
self._old_timezone_now = timezone.now
self.now = timezone.now()
timezone.now = self.new_timezone_now
def tearDown(self):
timezone.now = self._old_timezone_now
super(SetTimeMixin, self).tearDown()
def set_time(self, time):
self.now = time
def add_timedelta(self, delta):
self.now += delta
def new_timezone_now(self):
return self.now

View file

@ -0,0 +1,185 @@
import datetime
import pytz
from decimal import Decimal
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.test import TestCase
from django.utils import timezone
from registrasion import models as rego
from registrasion.cart import CartController
from patch_datetime import SetTimeMixin
UTC = pytz.timezone('UTC')
class RegistrationCartTestCase(SetTimeMixin, TestCase):
def setUp(self):
super(RegistrationCartTestCase, self).setUp()
@classmethod
def setUpTestData(cls):
cls.USER_1 = User.objects.create_user(username='testuser',
email='test@example.com', password='top_secret')
cls.USER_2 = User.objects.create_user(username='testuser2',
email='test2@example.com', password='top_secret')
cls.CAT_1 = rego.Category.objects.create(
name="Category 1",
description="This is a test category",
order=10,
render_type=rego.Category.RENDER_TYPE_RADIO,
)
cls.CAT_1.save()
cls.CAT_2 = rego.Category.objects.create(
name="Category 2",
description="This is a test category",
order=10,
render_type=rego.Category.RENDER_TYPE_RADIO,
)
cls.CAT_2.save()
cls.RESERVATION = datetime.timedelta(hours=1)
cls.PROD_1 = rego.Product.objects.create(
name="Product 1",
description= "This is a test product. It costs $10. " \
"A user may have 10 of them.",
category=cls.CAT_1,
price=Decimal("10.00"),
reservation_duration=cls.RESERVATION,
limit_per_user=10,
order=10,
)
cls.PROD_1.save()
cls.PROD_2 = rego.Product.objects.create(
name="Product 2",
description= "This is a test product. It costs $10. " \
"A user may have 10 of them.",
category=cls.CAT_1,
price=Decimal("10.00"),
limit_per_user=10,
order=10,
)
cls.PROD_2.save()
cls.PROD_3 = rego.Product.objects.create(
name="Product 3",
description= "This is a test product. It costs $10. " \
"A user may have 10 of them.",
category=cls.CAT_2,
price=Decimal("10.00"),
limit_per_user=10,
order=10,
)
cls.PROD_2.save()
@classmethod
def make_ceiling(cls, name, limit=None, start_time=None, end_time=None):
limit_ceiling = rego.TimeOrStockLimitEnablingCondition.objects.create(
description=name,
mandatory=True,
limit=limit,
start_time=start_time,
end_time=end_time
)
limit_ceiling.save()
limit_ceiling.products.add(cls.PROD_1, cls.PROD_2)
limit_ceiling.save()
@classmethod
def make_category_ceiling(cls, name, limit=None, start_time=None, end_time=None):
limit_ceiling = rego.TimeOrStockLimitEnablingCondition.objects.create(
description=name,
mandatory=True,
limit=limit,
start_time=start_time,
end_time=end_time
)
limit_ceiling.save()
limit_ceiling.categories.add(cls.CAT_1)
limit_ceiling.save()
@classmethod
def make_discount_ceiling(cls, name, limit=None, start_time=None, end_time=None):
limit_ceiling = rego.TimeOrStockLimitDiscount.objects.create(
description=name,
start_time=start_time,
end_time=end_time,
limit=limit,
)
limit_ceiling.save()
rego.DiscountForProduct.objects.create(
discount=limit_ceiling,
product=cls.PROD_1,
percentage=100,
quantity=10,
).save()
class BasicCartTests(RegistrationCartTestCase):
def test_get_cart(self):
current_cart = CartController.for_user(self.USER_1)
current_cart.cart.active = False
current_cart.cart.save()
old_cart = current_cart
current_cart = CartController.for_user(self.USER_1)
self.assertNotEqual(old_cart.cart, current_cart.cart)
current_cart2 = CartController.for_user(self.USER_1)
self.assertEqual(current_cart.cart, current_cart2.cart)
def test_add_to_cart_collapses_product_items(self):
current_cart = CartController.for_user(self.USER_1)
# Add a product twice
current_cart.add_to_cart(self.PROD_1, 1)
current_cart.add_to_cart(self.PROD_1, 1)
## Count of products for a given user should be collapsed.
items = rego.ProductItem.objects.filter(cart=current_cart.cart,
product=self.PROD_1)
self.assertEqual(1, len(items))
item = items[0]
self.assertEquals(2, item.quantity)
def test_add_to_cart_per_user_limit(self):
current_cart = CartController.for_user(self.USER_1)
# User should be able to add 1 of PROD_1 to the current cart.
current_cart.add_to_cart(self.PROD_1, 1)
# User should be able to add 1 of PROD_1 to the current cart.
current_cart.add_to_cart(self.PROD_1, 1)
# User should not be able to add 10 of PROD_1 to the current cart now,
# because they have a limit of 10.
with self.assertRaises(ValidationError):
current_cart.add_to_cart(self.PROD_1, 10)
current_cart.cart.active = False
current_cart.cart.save()
current_cart = CartController.for_user(self.USER_1)
# User should not be able to add 10 of PROD_1 to the current cart now,
# even though it's a new cart.
with self.assertRaises(ValidationError):
current_cart.add_to_cart(self.PROD_1, 10)
# Second user should not be affected by first user's limits
second_user_cart = CartController.for_user(self.USER_2)
second_user_cart.add_to_cart(self.PROD_1, 10)

View file

@ -0,0 +1,141 @@
import datetime
import pytz
from decimal import Decimal
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.test import TestCase
from django.utils import timezone
from registrasion import models as rego
from registrasion.cart import CartController
from test_cart import RegistrationCartTestCase
UTC = pytz.timezone('UTC')
class CeilingsTestCases(RegistrationCartTestCase):
def test_add_to_cart_ceiling_limit(self):
self.make_ceiling("Limit ceiling", limit=9)
self.__add_to_cart_test()
def test_add_to_cart_ceiling_category_limit(self):
self.make_category_ceiling("Limit ceiling", limit=9)
self.__add_to_cart_test()
def __add_to_cart_test(self):
current_cart = CartController.for_user(self.USER_1)
# User should not be able to add 10 of PROD_1 to the current cart
# because it is affected by limit_ceiling
with self.assertRaises(ValidationError):
current_cart.add_to_cart(self.PROD_2, 10)
# User should be able to add 5 of PROD_1 to the current cart
current_cart.add_to_cart(self.PROD_1, 5)
# User should not be able to add 6 of PROD_2 to the current cart
# because it is affected by CEIL_1
with self.assertRaises(ValidationError):
current_cart.add_to_cart(self.PROD_2, 6)
# User should be able to add 5 of PROD_2 to the current cart
current_cart.add_to_cart(self.PROD_2, 4)
def test_add_to_cart_ceiling_date_range(self):
self.make_ceiling("date range ceiling",
start_time=datetime.datetime(2015, 01, 01, tzinfo=UTC),
end_time=datetime.datetime(2015, 02, 01, tzinfo=UTC)
)
current_cart = CartController.for_user(self.USER_1)
# User should not be able to add whilst we're before start_time
self.set_time(datetime.datetime(2014, 01, 01, tzinfo=UTC))
with self.assertRaises(ValidationError):
current_cart.add_to_cart(self.PROD_1, 1)
# User should be able to add whilst we're during date range
# On edge of start
self.set_time(datetime.datetime(2015, 01, 01, tzinfo=UTC))
current_cart.add_to_cart(self.PROD_1, 1)
# In middle
self.set_time(datetime.datetime(2015, 01, 15, tzinfo=UTC))
current_cart.add_to_cart(self.PROD_1, 1)
# On edge of end
self.set_time(datetime.datetime(2015, 02, 01, tzinfo=UTC))
current_cart.add_to_cart(self.PROD_1, 1)
# User should not be able to add whilst we're after date range
self.set_time(datetime.datetime(2014, 01, 01, minute=01, tzinfo=UTC))
with self.assertRaises(ValidationError):
current_cart.add_to_cart(self.PROD_1, 1)
def test_add_to_cart_ceiling_limit_reserved_carts(self):
self.make_ceiling("Limit ceiling", limit=1)
self.set_time(datetime.datetime(2015, 01, 01, tzinfo=UTC))
first_cart = CartController.for_user(self.USER_1)
second_cart = CartController.for_user(self.USER_2)
first_cart.add_to_cart(self.PROD_1, 1)
# User 2 should not be able to add item to their cart
# because user 1 has item reserved, exhausting the ceiling
with self.assertRaises(ValidationError):
second_cart.add_to_cart(self.PROD_1, 1)
# User 2 should be able to add item to their cart once the
# reservation duration is elapsed
self.add_timedelta(self.RESERVATION + datetime.timedelta(seconds=1))
second_cart.add_to_cart(self.PROD_1, 1)
# User 2 pays for their cart
second_cart.cart.active = False
second_cart.cart.save()
# User 1 should not be able to add item to their cart
# because user 2 has paid for their reserved item, exhausting
# the ceiling, regardless of the reservation time.
self.add_timedelta(self.RESERVATION * 20)
with self.assertRaises(ValidationError):
first_cart.add_to_cart(self.PROD_1, 1)
def test_validate_cart_fails_product_ceilings(self):
self.make_ceiling("Limit ceiling", limit=1)
self.__validation_test()
def test_validate_cart_fails_product_discount_ceilings(self):
self.make_discount_ceiling("Limit ceiling", limit=1)
self.__validation_test()
def __validation_test(self):
self.set_time(datetime.datetime(2015, 01, 01, tzinfo=UTC))
first_cart = CartController.for_user(self.USER_1)
second_cart = CartController.for_user(self.USER_2)
# Adding a valid product should validate.
first_cart.add_to_cart(self.PROD_1, 1)
first_cart.validate_cart()
# Cart should become invalid if lapsed carts are claimed.
self.add_timedelta(self.RESERVATION + datetime.timedelta(seconds=1))
# Unpaid cart within reservation window
second_cart.add_to_cart(self.PROD_1, 1)
with self.assertRaises(ValidationError):
first_cart.validate_cart()
# Paid cart outside the reservation window
second_cart.cart.active = False
second_cart.cart.save()
self.add_timedelta(self.RESERVATION + datetime.timedelta(seconds=1))
with self.assertRaises(ValidationError):
first_cart.validate_cart()

View file

@ -0,0 +1,184 @@
import datetime
import pytz
from decimal import Decimal
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.test import TestCase
from django.utils import timezone
from registrasion import models as rego
from registrasion.cart import CartController
from test_cart import RegistrationCartTestCase
UTC = pytz.timezone('UTC')
class DiscountTestCase(RegistrationCartTestCase):
@classmethod
def add_discount_prod_1_includes_prod_2(cls, amount=Decimal(100)):
discount = rego.IncludedProductDiscount.objects.create(
description="PROD_1 includes PROD_2 " + str(amount) + "%",
)
discount.save()
discount.enabling_products.add(cls.PROD_1)
discount.save()
rego.DiscountForProduct.objects.create(
discount=discount,
product=cls.PROD_2,
percentage=amount,
quantity=2
).save()
return discount
@classmethod
def add_discount_prod_1_includes_cat_2(cls, amount=Decimal(100)):
discount = rego.IncludedProductDiscount.objects.create(
description="PROD_1 includes CAT_2 " + str(amount) + "%",
)
discount.save()
discount.enabling_products.add(cls.PROD_1)
discount.save()
rego.DiscountForCategory.objects.create(
discount=discount,
category=cls.CAT_2,
percentage=amount,
quantity=2
).save()
return discount
def test_discount_is_applied(self):
discount = self.add_discount_prod_1_includes_prod_2()
cart = CartController.for_user(self.USER_1)
cart.add_to_cart(self.PROD_1, 1)
cart.add_to_cart(self.PROD_2, 1)
# Discounts should be applied at this point...
self.assertEqual(1, len(cart.cart.discountitem_set.all()))
def test_discount_is_applied_for_category(self):
discount = self.add_discount_prod_1_includes_cat_2()
cart = CartController.for_user(self.USER_1)
cart.add_to_cart(self.PROD_1, 1)
cart.add_to_cart(self.PROD_3, 1)
# Discounts should be applied at this point...
self.assertEqual(1, len(cart.cart.discountitem_set.all()))
def test_discount_does_not_apply_if_not_met(self):
discount = self.add_discount_prod_1_includes_prod_2()
cart = CartController.for_user(self.USER_1)
cart.add_to_cart(self.PROD_2, 1)
# No discount should be applied as the condition is not met
self.assertEqual(0, len(cart.cart.discountitem_set.all()))
def test_discount_applied_out_of_order(self):
discount = self.add_discount_prod_1_includes_prod_2()
cart = CartController.for_user(self.USER_1)
cart.add_to_cart(self.PROD_2, 1)
cart.add_to_cart(self.PROD_1, 1)
# No discount should be applied as the condition is not met
self.assertEqual(1, len(cart.cart.discountitem_set.all()))
def test_discounts_collapse(self):
discount = self.add_discount_prod_1_includes_prod_2()
cart = CartController.for_user(self.USER_1)
cart.add_to_cart(self.PROD_1, 1)
cart.add_to_cart(self.PROD_2, 1)
cart.add_to_cart(self.PROD_2, 1)
# Discounts should be applied and collapsed at this point...
self.assertEqual(1, len(cart.cart.discountitem_set.all()))
def test_discounts_respect_quantity(self):
discount = self.add_discount_prod_1_includes_prod_2()
cart = CartController.for_user(self.USER_1)
cart.add_to_cart(self.PROD_1, 1)
cart.add_to_cart(self.PROD_2, 3)
# There should be three items in the cart, but only two should
# attract a discount.
discount_items = list(cart.cart.discountitem_set.all())
self.assertEqual(2, discount_items[0].quantity)
def test_multiple_discounts_apply_in_order(self):
discount_full = self.add_discount_prod_1_includes_prod_2()
discount_half = self.add_discount_prod_1_includes_prod_2(Decimal(50))
cart = CartController.for_user(self.USER_1)
cart.add_to_cart(self.PROD_1, 1)
cart.add_to_cart(self.PROD_2, 3)
# There should be two discounts
discount_items = list(cart.cart.discountitem_set.all())
discount_items.sort(key=lambda item: item.quantity)
self.assertEqual(2, len(discount_items))
# The half discount should be applied only once
self.assertEqual(1, discount_items[0].quantity)
self.assertEqual(discount_half.pk, discount_items[0].discount.pk)
# The full discount should be applied twice
self.assertEqual(2, discount_items[1].quantity)
self.assertEqual(discount_full.pk, discount_items[1].discount.pk)
def test_discount_applies_across_carts(self):
discount_full = self.add_discount_prod_1_includes_prod_2()
# Enable the discount during the first cart.
cart = CartController.for_user(self.USER_1)
cart.add_to_cart(self.PROD_1, 1)
cart.cart.active = False
cart.cart.save()
# Use the discount in the second cart
cart = CartController.for_user(self.USER_1)
cart.add_to_cart(self.PROD_2, 1)
# The discount should be applied.
self.assertEqual(1, len(cart.cart.discountitem_set.all()))
cart.cart.active = False
cart.cart.save()
# The discount should respect the total quantity across all
# of the user's carts.
cart = CartController.for_user(self.USER_1)
cart.add_to_cart(self.PROD_2, 2)
# Having one item in the second cart leaves one more item where
# the discount is applicable. The discount should apply, but only for
# quantity=1
discount_items = list(cart.cart.discountitem_set.all())
self.assertEqual(1, discount_items[0].quantity)
def test_discount_applies_only_once_enabled(self):
# Enable the discount during the first cart.
cart = CartController.for_user(self.USER_1)
cart.add_to_cart(self.PROD_1, 1)
cart.add_to_cart(self.PROD_2, 2) # This would exhaust discount if present
cart.cart.active = False
cart.cart.save()
discount_full = self.add_discount_prod_1_includes_prod_2()
cart = CartController.for_user(self.USER_1)
cart.add_to_cart(self.PROD_2, 2)
discount_items = list(cart.cart.discountitem_set.all())
self.assertEqual(2, discount_items[0].quantity)

View file

@ -0,0 +1,171 @@
import datetime
import pytz
from decimal import Decimal
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.test import TestCase
from django.utils import timezone
from registrasion import models as rego
from registrasion.cart import CartController
from test_cart import RegistrationCartTestCase
UTC = pytz.timezone('UTC')
class EnablingConditionTestCases(RegistrationCartTestCase):
@classmethod
def add_product_enabling_condition(cls, mandatory=False):
''' Adds a product enabling condition: adding PROD_1 to a cart is
predicated on adding PROD_2 beforehand. '''
enabling_condition = rego.ProductEnablingCondition.objects.create(
description="Product condition",
mandatory=mandatory,
)
enabling_condition.save()
enabling_condition.products.add(cls.PROD_1)
enabling_condition.enabling_products.add(cls.PROD_2)
enabling_condition.save()
@classmethod
def add_product_enabling_condition_on_category(cls, mandatory=False):
''' Adds a product enabling condition that operates on a category:
adding an item from CAT_1 is predicated on adding PROD_3 beforehand '''
enabling_condition = rego.ProductEnablingCondition.objects.create(
description="Product condition",
mandatory=mandatory,
)
enabling_condition.save()
enabling_condition.categories.add(cls.CAT_1)
enabling_condition.enabling_products.add(cls.PROD_3)
enabling_condition.save()
def add_category_enabling_condition(cls, mandatory=False):
''' Adds a category enabling condition: adding PROD_1 to a cart is
predicated on adding an item from CAT_2 beforehand.'''
enabling_condition = rego.CategoryEnablingCondition.objects.create(
description="Category condition",
mandatory=mandatory,
enabling_category=cls.CAT_2,
)
enabling_condition.save()
enabling_condition.products.add(cls.PROD_1)
enabling_condition.save()
def test_product_enabling_condition_enables_product(self):
self.add_product_enabling_condition()
# Cannot buy PROD_1 without buying PROD_2
current_cart = CartController.for_user(self.USER_1)
with self.assertRaises(ValidationError):
current_cart.add_to_cart(self.PROD_1, 1)
current_cart.add_to_cart(self.PROD_2, 1)
current_cart.add_to_cart(self.PROD_1, 1)
def test_product_enabled_by_product_in_previous_cart(self):
self.add_product_enabling_condition()
current_cart = CartController.for_user(self.USER_1)
current_cart.add_to_cart(self.PROD_2, 1)
current_cart.cart.active = False
current_cart.cart.save()
# Create new cart and try to add PROD_1
current_cart = CartController.for_user(self.USER_1)
current_cart.add_to_cart(self.PROD_1, 1)
def test_product_enabling_condition_enables_category(self):
self.add_product_enabling_condition_on_category()
# Cannot buy PROD_1 without buying item from CAT_2
current_cart = CartController.for_user(self.USER_1)
with self.assertRaises(ValidationError):
current_cart.add_to_cart(self.PROD_1, 1)
current_cart.add_to_cart(self.PROD_3, 1)
current_cart.add_to_cart(self.PROD_1, 1)
def test_category_enabling_condition_enables_product(self):
self.add_category_enabling_condition()
# Cannot buy PROD_1 without buying PROD_2
current_cart = CartController.for_user(self.USER_1)
with self.assertRaises(ValidationError):
current_cart.add_to_cart(self.PROD_1, 1)
# PROD_3 is in CAT_2
current_cart.add_to_cart(self.PROD_3, 1)
current_cart.add_to_cart(self.PROD_1, 1)
def test_product_enabled_by_category_in_previous_cart(self):
self.add_category_enabling_condition()
current_cart = CartController.for_user(self.USER_1)
current_cart.add_to_cart(self.PROD_3, 1)
current_cart.cart.active = False
current_cart.cart.save()
# Create new cart and try to add PROD_1
current_cart = CartController.for_user(self.USER_1)
current_cart.add_to_cart(self.PROD_1, 1)
def test_multiple_non_mandatory_conditions(self):
self.add_product_enabling_condition()
self.add_category_enabling_condition()
# User 1 is testing the product enabling condition
cart_1 = CartController.for_user(self.USER_1)
# Cannot add PROD_1 until a condition is met
with self.assertRaises(ValidationError):
cart_1.add_to_cart(self.PROD_1, 1)
cart_1.add_to_cart(self.PROD_2, 1)
cart_1.add_to_cart(self.PROD_1, 1)
# User 2 is testing the category enabling condition
cart_2 = CartController.for_user(self.USER_2)
# Cannot add PROD_1 until a condition is met
with self.assertRaises(ValidationError):
cart_2.add_to_cart(self.PROD_1, 1)
cart_2.add_to_cart(self.PROD_3, 1)
cart_2.add_to_cart(self.PROD_1, 1)
def test_multiple_mandatory_conditions(self):
self.add_product_enabling_condition(mandatory=True)
self.add_category_enabling_condition(mandatory=True)
cart_1 = CartController.for_user(self.USER_1)
# Cannot add PROD_1 until both conditions are met
with self.assertRaises(ValidationError):
cart_1.add_to_cart(self.PROD_1, 1)
cart_1.add_to_cart(self.PROD_2, 1) # Meets the product condition
with self.assertRaises(ValidationError):
cart_1.add_to_cart(self.PROD_1, 1)
cart_1.add_to_cart(self.PROD_3, 1) # Meets the category condition
cart_1.add_to_cart(self.PROD_1, 1)
def test_mandatory_conditions_are_mandatory(self):
self.add_product_enabling_condition(mandatory=False)
self.add_category_enabling_condition(mandatory=True)
cart_1 = CartController.for_user(self.USER_1)
# Cannot add PROD_1 until both conditions are met
with self.assertRaises(ValidationError):
cart_1.add_to_cart(self.PROD_1, 1)
cart_1.add_to_cart(self.PROD_2, 1) # Meets the product condition
with self.assertRaises(ValidationError):
cart_1.add_to_cart(self.PROD_1, 1)
cart_1.add_to_cart(self.PROD_3, 1) # Meets the category condition
cart_1.add_to_cart(self.PROD_1, 1)

View file

@ -0,0 +1,108 @@
import datetime
import pytz
from decimal import Decimal
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.test import TestCase
from django.utils import timezone
from registrasion import models as rego
from registrasion.cart import CartController
from registrasion.invoice import InvoiceController
from test_cart import RegistrationCartTestCase
UTC = pytz.timezone('UTC')
class InvoiceTestCase(RegistrationCartTestCase):
def test_create_invoice(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_1 = InvoiceController.for_cart(current_cart.cart)
# That invoice should have a single line item
line_items = rego.LineItem.objects.filter(invoice=invoice_1.invoice)
self.assertEqual(1, len(line_items))
# That invoice should have a value equal to cost of PROD_1
self.assertEqual(self.PROD_1.price, invoice_1.invoice.value)
# Adding item to cart should void all active invoices and produce
# a new invoice
current_cart.add_to_cart(self.PROD_2, 1)
invoice_2 = InvoiceController.for_cart(current_cart.cart)
self.assertNotEqual(invoice_1.invoice, invoice_2.invoice)
# Invoice should have two line items
line_items = rego.LineItem.objects.filter(invoice=invoice_2.invoice)
self.assertEqual(2, len(line_items))
# Invoice should have a value equal to cost of PROD_1 and PROD_2
self.assertEqual(
self.PROD_1.price + self.PROD_2.price,
invoice_2.invoice.value)
def test_create_invoice_fails_if_cart_invalid(self):
self.make_ceiling("Limit ceiling", limit=1)
self.set_time(datetime.datetime(2015, 01, 01, tzinfo=UTC))
current_cart = CartController.for_user(self.USER_1)
current_cart.add_to_cart(self.PROD_1, 1)
self.add_timedelta(self.RESERVATION * 2)
cart_2 = CartController.for_user(self.USER_2)
cart_2.add_to_cart(self.PROD_1, 1)
# Now try to invoice the first user
with self.assertRaises(ValidationError):
InvoiceController.for_cart(current_cart.cart)
def test_paying_invoice_makes_new_cart(self):
current_cart = CartController.for_user(self.USER_1)
current_cart.add_to_cart(self.PROD_1, 1)
invoice = InvoiceController.for_cart(current_cart.cart)
invoice.pay("A payment!", invoice.invoice.value)
# This payment is for the correct amount invoice should be paid.
self.assertTrue(invoice.invoice.paid)
# Cart should not be active
self.assertFalse(invoice.invoice.cart.active)
# Asking for a cart should generate a new one
new_cart = CartController.for_user(self.USER_1)
self.assertNotEqual(current_cart.cart, new_cart.cart)
def test_invoice_includes_discounts(self):
voucher = rego.Voucher.objects.create(
recipient="Voucher recipient",
code="VOUCHER",
limit=1
)
voucher.save()
discount = rego.VoucherDiscount.objects.create(
description="VOUCHER RECIPIENT",
voucher=voucher,
)
discount.save()
rego.DiscountForProduct.objects.create(
discount=discount,
product=self.PROD_1,
percentage=Decimal(50),
quantity=1
).save()
current_cart = CartController.for_user(self.USER_1)
current_cart.apply_voucher(voucher)
# Should be able to create an invoice after the product is added
current_cart.add_to_cart(self.PROD_1, 1)
invoice_1 = InvoiceController.for_cart(current_cart.cart)
# That invoice should have two line items
line_items = rego.LineItem.objects.filter(invoice=invoice_1.invoice)
self.assertEqual(2, len(line_items))
# That invoice should have a value equal to 50% of the cost of PROD_1
self.assertEqual(self.PROD_1.price * Decimal("0.5"), invoice_1.invoice.value)

View file

@ -0,0 +1,97 @@
import datetime
import pytz
from decimal import Decimal
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.test import TestCase
from django.utils import timezone
from registrasion import models as rego
from registrasion.cart import CartController
from test_cart import RegistrationCartTestCase
UTC = pytz.timezone('UTC')
class VoucherTestCases(RegistrationCartTestCase):
@classmethod
def new_voucher(self):
voucher = rego.Voucher.objects.create(
recipient="Voucher recipient",
code="VOUCHER",
limit=1
)
voucher.save()
return voucher
def test_apply_voucher(self):
voucher = self.new_voucher()
self.set_time(datetime.datetime(2015, 01, 01, tzinfo=UTC))
cart_1 = CartController.for_user(self.USER_1)
cart_1.apply_voucher(voucher)
self.assertIn(voucher, cart_1.cart.vouchers.all())
# Second user should not be able to apply this voucher (it's exhausted)
cart_2 = CartController.for_user(self.USER_2)
with self.assertRaises(ValidationError):
cart_2.apply_voucher(voucher)
# After the reservation duration, user 2 should be able to apply voucher
self.add_timedelta(rego.Voucher.RESERVATION_DURATION * 2)
cart_2.apply_voucher(voucher)
cart_2.cart.active = False
cart_2.cart.save()
# After the reservation duration, user 1 should not be able to apply
# voucher, as user 2 has paid for their cart.
self.add_timedelta(rego.Voucher.RESERVATION_DURATION * 2)
with self.assertRaises(ValidationError):
cart_1.apply_voucher(voucher)
def test_voucher_enables_item(self):
voucher = self.new_voucher()
enabling_condition = rego.VoucherEnablingCondition.objects.create(
description="Voucher condition",
voucher=voucher,
mandatory=False,
)
enabling_condition.save()
enabling_condition.products.add(self.PROD_1)
enabling_condition.save()
# Adding the product without a voucher will not work
current_cart = CartController.for_user(self.USER_1)
with self.assertRaises(ValidationError):
current_cart.add_to_cart(self.PROD_1, 1)
# Apply the voucher
current_cart.apply_voucher(voucher)
current_cart.add_to_cart(self.PROD_1, 1)
def test_voucher_enables_discount(self):
voucher = self.new_voucher()
discount = rego.VoucherDiscount.objects.create(
description="VOUCHER RECIPIENT",
voucher=voucher,
)
discount.save()
rego.DiscountForProduct.objects.create(
discount=discount,
product=self.PROD_1,
percentage=Decimal(100),
quantity=1
).save()
# Having PROD_1 in place should add a discount
current_cart = CartController.for_user(self.USER_1)
current_cart.apply_voucher(voucher)
current_cart.add_to_cart(self.PROD_1, 1)
self.assertEqual(1, len(current_cart.cart.discountitem_set.all()))