def create_fixtures(self): self.user_email = self.user_email_template.format(random_string(8)) self.user = User(self.user_email, 'test_user') self.db.session.add(self.user) self.group_name = self.group_template.format(random_string(8)) self.group = ProductGroup(type='test', name=self.group_name, capacity_max=10) self.product1 = Product(name=self.product1_name, parent=self.group, capacity_max=3) self.tier1_1 = PriceTier(name=self.tier1_1_name, parent=self.product1) self.price1_1 = Price(price_tier=self.tier1_1, currency='GBP', price_int=10) self.db.session.add(self.tier1_1) self.tier1_2 = PriceTier(name=self.tier1_2_name, parent=self.product1) self.price1_2 = Price(price_tier=self.tier1_2, currency='GBP', price_int=20) self.db.session.add(self.tier1_2) self.product2 = Product(name=self.product2_name, parent=self.group) self.tier3 = PriceTier(name=self.tier3_name, parent=self.product2) self.price3 = Price(price_tier=self.tier3, currency='GBP', price_int=30) self.db.session.add(self.tier3) self.db.session.commit()
def price_tier_edit(tier_id): tier = PriceTier.query.get_or_404(tier_id) form = EditPriceTierForm(obj=tier) if form.validate_on_submit(): tier.name = form.name.data tier.personal_limit = form.personal_limit.data # Update prices but only if price tier has no purchases. price_gbp = tier.get_price("GBP") price_eur = tier.get_price("EUR") if tier.purchase_count == 0: if form.price_gbp.data != price_gbp.value: db.session.delete(price_gbp) tier.prices.append(Price("GBP", form.price_gbp.data)) if form.price_eur.data != price_eur.value: db.session.delete(price_eur) tier.prices.append(Price("EUR", form.price_eur.data)) db.session.commit() return redirect(url_for(".price_tier_details", tier_id=tier.id)) form.price_gbp.data = tier.get_price("GBP").value form.price_eur.data = tier.get_price("EUR").value return render_template("admin/products/price-tier-edit.html", tier=tier, form=form)
def test_capacity_propagation(db, parent_group, user): product1 = Product(name="product", parent=parent_group, capacity_max=3) tier1_1 = PriceTier(name="tier1", parent=product1) Price(price_tier=tier1_1, currency="GBP", price_int=10) db.session.add(tier1_1) tier1_2 = PriceTier(name="tier2", parent=product1) Price(price_tier=tier1_2, currency="GBP", price_int=20) db.session.add(tier1_2) product2 = Product(name="product2", parent=parent_group) tier3 = PriceTier(name="tier3", parent=product2) Price(price_tier=tier3, currency="GBP", price_int=30) db.session.commit() # Check all our items have the correct initial capacity assert parent_group.get_total_remaining_capacity() == 10 assert product1.get_total_remaining_capacity() == 3 assert tier1_1.get_total_remaining_capacity() == 3 assert tier1_2.get_total_remaining_capacity() == 3 assert product2.get_total_remaining_capacity() == 10 assert tier3.get_total_remaining_capacity() == 10 # Issue three instances to exhaust product1 create_purchases(tier1_1, 3, user) db.session.commit() # Now we shouldn't be able to issue any more tickets from this product with pytest.raises(CapacityException): create_purchases(tier1_1, 1, user) with pytest.raises(CapacityException): create_purchases(tier1_2, 1, user) db.session.commit() # All the capacity went from product1 assert tier1_1.get_total_remaining_capacity() == 0 assert tier1_2.get_total_remaining_capacity() == 0 assert product1.get_total_remaining_capacity() == 0 # produtill has capacity but is limited by the parent assert parent_group.get_total_remaining_capacity() == 7 assert product2.get_total_remaining_capacity() == 7 assert tier3.get_total_remaining_capacity() == 7 price1 = Price(price_tier=tier1_1, currency="GBP", price_int=5) price2 = Price(price_tier=tier1_2, currency="GBP", price_int=500) db.session.add(price1) db.session.add(price2) db.session.commit() assert price1 == product1.get_cheapest_price("GBP")
def new_price_tier(product_id): form = PriceTierForm() product = Product.query.get_or_404(product_id) if form.validate_on_submit(): pt = PriceTier(form.name.data) pt.prices = [Price('GBP', form.price_gbp.data), Price('EUR', form.price_eur.data)] # Only activate this price tier if it's the first one added. pt.active = (len(product.price_tiers) == 0) product.price_tiers.append(pt) db.session.commit() return redirect(url_for('.price_tier_details', tier_id=pt.id)) return render_template('admin/products/price-tier-new.html', product=product, form=form)
def test_create_purchases(db, parent_group, user): product = Product(name="product", capacity_max=3, parent=parent_group) tier = PriceTier(name="tier", parent=product) price = Price(price_tier=tier, currency="GBP", price_int=666) db.session.add(price) db.session.commit() assert tier.capacity_used == 0 purchases = create_purchases(tier, 1, user) purchase = purchases[0] assert tier.capacity_used == 1 assert product.capacity_used == 1 # NB: Decimal('6.66') != Decimal(6.66) == Decimal(float(6.66)) ~= 6.6600000000000001 assert purchase.price.value == Decimal("6.66") # Test issuing multiple instances works new_purchases = create_purchases(tier, 2, user) assert len(new_purchases) == 2 assert product.capacity_used == 3 # Test issuing beyond capacity errors with pytest.raises(CapacityException): create_purchases(tier, 1, user)
def test_check_in(db, parent_group, user): product = Product(name="product", capacity_max=3, parent=parent_group) tier = PriceTier(name="tier", parent=product) price = Price(price_tier=tier, currency="GBP", price_int=666) db.session.add(price) db.session.commit() purchases = create_purchases(tier, 1, user) purchase = purchases[0] with pytest.raises(PurchaseStateException): # Issuing tickets should fail if the purchase hasn't been paid for purchase.ticket_issued = True with pytest.raises(CheckinStateException): # Likewise, checking in should fail. purchase.check_in() purchase.state = "paid" db.session.commit() purchase.ticket_issued = True assert purchase.checked_in is False purchase.check_in() assert purchase.checked_in is True
def create_fixtures(self): self.user1_email = self.user1_email_template.format(random_string(8)) self.user2_email = self.user2_email_template.format(random_string(8)) self.user1 = User(self.user1_email, 'test_user1') self.user2 = User(self.user2_email, 'test_user2') self.db.session.add(self.user1) self.db.session.add(self.user2) self.group_name = self.group_template.format(random_string(8)) self.group = ProductGroup(type='admissions', name=self.group_name) self.product = Product(name=self.product_name, parent=self.group) self.tier = PriceTier(name=self.tier_name, parent=self.product) self.price = Price(price_tier=self.tier, currency='GBP', price_int=666) self.db.session.add(self.price) self.db.session.commit() # PriceTier needs to have been committed before this basket = Basket(self.user1, 'GBP') basket[self.tier] = 1 basket.create_purchases() basket.ensure_purchase_capacity() payment = basket.create_payment(BankPayment) assert len(payment.purchases) == 1 self.db.session.commit()
def test_get_cheapest(self): with self.app.app_context(): self.create_fixtures() price1 = Price(price_tier=self.tier1_1, currency='GBP', price_int=5) price2 = Price(price_tier=self.tier1_2, currency='GBP', price_int=500) self.db.session.add(price1) self.db.session.add(price2) self.db.session.commit() assert price1 == self.product1.get_cheapest_price('GBP')
def test_product_group_get_counts_by_state(db, parent_group, user): product = Product(name='product', capacity_max=3, parent=parent_group) tier = PriceTier(name='tier', parent=product) price = Price(price_tier=tier, currency='GBP', price_int=666) db.session.add(price) db.session.commit() # Test it works at the PriceTier level purchases = create_purchases(tier, 1, user) purchase1 = purchases[0] expected = { 'reserved': 1, } assert tier.purchase_count_by_state == expected assert product.purchase_count_by_state == expected assert parent_group.purchase_count_by_state == expected # Test that other states show up purchase1.set_state('payment-pending') db.session.commit() expected = { 'payment-pending': 1, } assert tier.purchase_count_by_state == expected assert product.purchase_count_by_state == expected assert parent_group.purchase_count_by_state == expected # Add another purchase in another tier tier2 = PriceTier(name='2', parent=product) price = Price(price_tier=tier2, currency='GBP', price_int=666) db.session.commit() create_purchases(tier2, 1, user) assert tier.purchase_count_by_state == expected expected = { 'payment-pending': 1, 'reserved': 1, } assert product.purchase_count_by_state == expected assert parent_group.purchase_count_by_state == expected
def product_group_copy(group_id): group = ProductGroup.query.get_or_404(group_id) form = CopyProductGroupForm() if group.children: # No recursive copying abort(404) if request.method != "POST": form.name.data = group.name + " (copy)" if group.capacity_max is None: del form.capacity_max_required else: del form.capacity_max if form.validate_on_submit(): capacity_max = None if group.capacity_max is not None: capacity_max = form.capacity_max_required.data group.capacity_max -= capacity_max new_group = ProductGroup( type=group.type, name=form.name.data, capacity_max=capacity_max, expires=form.expires.data, ) for product in group.products: new_product = Product( name=product.name, display_name=product.display_name, description=product.description, ) new_group.products.append(new_product) for pt in product.price_tiers: if not pt.active and not form.include_inactive.data: continue new_pt = PriceTier(name=pt.name, personal_limit=pt.personal_limit, active=pt.active) new_product.price_tiers.append(new_pt) for price in pt.prices: new_price = Price(price.currency, price.value) new_pt.prices.append(new_price) new_group.parent = group.parent db.session.add(new_group) db.session.commit() flash("ProductGroup copied") return redirect( url_for(".product_group_details", group_id=new_group.id)) return render_template("admin/products/product-group-copy.html", group=group, form=form)
def test_transfer(db, user, parent_group): user1 = user user2 = User("test_user_{}@test.invalid".format(random_string(8)), "test_user2") db.session.add(user2) product = Product(name="product", parent=parent_group) tier = PriceTier(name="tier", parent=product) price = Price(price_tier=tier, currency="GBP", price_int=666) db.session.add(price) db.session.commit() create_purchases(tier, 1, user1) item = user1.purchases[0] item.price_tier.allow_check_in = True item.price_tier.is_transferable = False with pytest.raises(PurchaseTransferException) as e: item.transfer(user1, user2) assert "Only paid items may be transferred." in e.args[0] item.state = "paid" db.session.commit() with pytest.raises(PurchaseTransferException) as e: item.transfer(user1, user2) assert "not transferable" in e.args[0] with pytest.raises(PurchaseTransferException) as e: item.transfer(user2, user1) assert "does not own this item" in e.args[0] db.session.commit() item.price_tier.parent.set_attribute("is_transferable", True) db.session.commit() with pytest.raises(PurchaseTransferException) as e: item.transfer(user1, user1) assert "users must be different" in e.args[0] item.transfer(user1, user2) db.session.commit() assert item.owner_id == user2.id assert item.purchaser_id == user1.id assert item == user2.owned_purchases[0] assert item not in user1.owned_purchases xfer = item.transfers[0] assert xfer.to_user.id == user2.id assert xfer.from_user.id == user1.id
def new_price_tier(product_id): form = NewPriceTierForm() product = Product.query.get_or_404(product_id) if form.validate_on_submit(): pt = PriceTier(form.name.data, personal_limit=form.personal_limit.data) pt.prices = [ Price("GBP", form.price_gbp.data), Price("EUR", form.price_eur.data), ] # Only activate this price tier if it's the first one added. pt.active = len(product.price_tiers) == 0 product.price_tiers.append(pt) db.session.commit() return redirect(url_for(".price_tier_details", tier_id=pt.id)) return render_template("admin/products/price-tier-new.html", product=product, form=form)
def test_check_in(db, parent_group, user): product = Product(name='product', capacity_max=3, parent=parent_group) tier = PriceTier(name='tier', parent=product) price = Price(price_tier=tier, currency='GBP', price_int=666) db.session.add(price) db.session.commit() purchases = create_purchases(tier, 1, user) purchase = purchases[0] purchase.state = 'receipt-emailed' assert purchase.checked_in is False purchase.check_in() assert purchase.checked_in is True
def create_fixtures(self): self.user_email = self.user_email_template.format(random_string(8)) self.user = User(self.user_email, 'test_user') self.db.session.add(self.user) self.group_name = self.group_template.format(random_string(8)) self.group = ProductGroup(type='admissions', name=self.group_name) self.product = Product(name=self.product_name, capacity_max=3, parent=self.group) self.tier = PriceTier(name=self.tier_name, parent=self.product) self.price = Price(price_tier=self.tier, currency='GBP', price_int=666) # These have `cascade=all` so just add the bottom of the hierarchy self.db.session.add(self.price) self.db.session.commit()
def test_set_state(db, parent_group, user): product = Product(name="product", capacity_max=3, parent=parent_group) tier = PriceTier(name="tier", parent=product) price = Price(price_tier=tier, currency="GBP", price_int=666) db.session.add(price) db.session.commit() purchases = create_purchases(tier, 1, user) purchase = purchases[0] with pytest.raises(PurchaseStateException): purchase.set_state("disallowed-state") purchase.set_state("payment-pending") assert purchase.state == "payment-pending", purchase.state
def test_product_group_get_counts_by_state(self): with self.app.app_context(): self.create_fixtures() # Test it works at the PriceTier level purchases = self.create_purchases(self.tier, 1) purchase1 = purchases[0] expected = { 'reserved': 1, } assert self.tier.purchase_count_by_state == expected assert self.product.purchase_count_by_state == expected assert self.group.purchase_count_by_state == expected # Test that other states show up purchase1.set_state('payment-pending') self.db.session.commit() expected = { 'payment-pending': 1, } assert self.tier.purchase_count_by_state == expected assert self.product.purchase_count_by_state == expected assert self.group.purchase_count_by_state == expected # Add another purchase in another tier self.tier2 = PriceTier(name='2', parent=self.product) self.price = Price(price_tier=self.tier2, currency='GBP', price_int=666) self.db.session.commit() self.create_purchases(self.tier2, 1) assert self.tier.purchase_count_by_state == expected expected = { 'payment-pending': 1, 'reserved': 1, } assert self.product.purchase_count_by_state == expected assert self.group.purchase_count_by_state == expected
def test_set_state(db, parent_group, user): product = Product(name='product', capacity_max=3, parent=parent_group) tier = PriceTier(name='tier', parent=product) price = Price(price_tier=tier, currency='GBP', price_int=666) db.session.add(price) db.session.commit() purchases = create_purchases(tier, 1, user) purchase = purchases[0] with pytest.raises(PurchaseStateException): purchase.set_state('disallowed-state') with pytest.raises(PurchaseStateException): purchase.set_state('receipt-emailed') purchase.set_state('payment-pending') assert purchase.state == 'payment-pending', purchase.state
def create_product_groups(): top_level_groups = [ # name, capacity, expires ('admissions', datetime(2018, 9, 3), app.config.get('MAXIMUM_ADMISSIONS')), ('parking', datetime(2018, 9, 3), None), ('campervan', datetime(2018, 9, 3), None), ('merchandise', datetime(2018, 8, 12), None), ] for name, expires, capacity in top_level_groups: if ProductGroup.get_by_name(name): continue pg = ProductGroup(name=name, type=name, capacity_max=capacity, expires=expires) db.session.add(pg) db.session.flush() allocations = [ # name, capacity ('vendors', 100), ('sponsors', 200), ('speakers', 100), ('general', 800), ] admissions = ProductGroup.get_by_name('admissions') for name, capacity in allocations: if ProductGroup.get_by_name(name): continue ProductGroup(name=name, capacity_max=capacity, parent=admissions) view = ProductView.get_by_name('main') if not view: view = ProductView(name='main', type='tickets') db.session.add(view) db.session.flush() general = ProductGroup.get_by_name('general') products = [ # name, display name, transferable, badge, capacity, description, (std cap, gbp eur), (early cap, gbp, eur), (late cap, gbp, eur) ('full', 'Full Camp Ticket', True, True, None, 'Full ticket', ((1500, 115, 135), (250, 105, 125), (None, 125, 145))), ('full-s', 'Full Camp Ticket (Supporter)', True, True, None, 'Support this non-profit event by paying a bit more. All money will go towards making EMF more awesome.', ((None, 150, 180), )), ('full-sg', 'Full Camp Ticket (Gold Supporter)', True, True, None, 'Support this non-profit event by paying a bit more. All money will go towards making EMF more awesome.', ((None, 200, 240), )), ('u18', 'Under-18', True, False, 150, 'For visitors born after August 30th, 2000. All under-18s must be accompanied by an adult.', ((None, 55, 63), )), ('u12', 'Under-12', True, False, 50, 'For children born after August 30th, 2006. All children must be accompanied by an adult.', ((None, 0, 0), )), ] order = 0 for name, display_name, has_xfer, has_badge, capacity, description, prices in products: if Product.get_by_name('general', name): continue product = Product(name=name, display_name=display_name, capacity_max=capacity, description=description, parent=general, attributes={ 'is_transferable': has_xfer, 'has_badge': has_badge }) for index, (price_cap, gbp, eur) in enumerate(prices): if len(prices) == 1 or index == 0: tier_name = name + '-std' active = True elif index == 1: tier_name = name + '-early-bird' active = False elif index == 2: tier_name = name + '-late' active = False if PriceTier.get_by_name('general', 'name', tier_name): continue pt = PriceTier(name=tier_name, capacity_max=price_cap, personal_limit=10, parent=product, active=active) Price(currency='GBP', price_int=gbp * 100, price_tier=pt) Price(currency='EUR', price_int=eur * 100, price_tier=pt) ProductViewProduct(view, product, order) order += 1 db.session.flush() misc = [ # name, display_name, cap, personal_limit, gbp, eur, description ('parking', 'Parking Ticket', 1700, 4, 15, 21, "We're trying to keep cars to a minimum. Please take public transport or car-share if you can." ), ('campervan', 'Caravan/\u200cCampervan Ticket', 60, 2, 30, 42, "If you bring a caravan, you won't need a separate parking ticket for the towing car." ), ] for name, display_name, cap, personal_limit, gbp, eur, description in misc: if Product.get_by_name(name, name): continue group = ProductGroup.get_by_name(name) product = Product(name=name, display_name=display_name, description=description, parent=group) pt = PriceTier(name=name, personal_limit=personal_limit, parent=product) db.session.add(pt) db.session.add( Price(currency='GBP', price_int=gbp * 100, price_tier=pt)) db.session.add( Price(currency='EUR', price_int=eur * 100, price_tier=pt)) ProductViewProduct(view, product, order) order += 1 db.session.commit()
def create_product_groups(): top_level_groups = [ # name, capacity, expires ("admissions", None, 2500), ("parking", None, None), ("campervan", None, None), ("merchandise", None, None), ] for name, expires, capacity in top_level_groups: if ProductGroup.get_by_name(name): continue pg = ProductGroup(name=name, type=name, capacity_max=capacity, expires=expires) db.session.add(pg) db.session.flush() allocations = [ # name, capacity ("vendors", 100), ("sponsors", 200), ("speakers", 100), ("general", 1800), ] admissions = ProductGroup.get_by_name("admissions") for name, capacity in allocations: if ProductGroup.get_by_name(name): continue ProductGroup(name=name, capacity_max=capacity, parent=admissions) view = ProductView.get_by_name("main") if not view: view = ProductView(name="main", type="tickets") db.session.add(view) db.session.flush() general = ProductGroup.get_by_name("general") products = [ # name, display name, transferable, badge, capacity, description, (std cap, gbp eur), (early cap, gbp, eur), (late cap, gbp, eur) ( "full", "Full Camp Ticket", True, True, None, "Full ticket", ((1500, 115, 135), (250, 105, 125), (None, 125, 145)), ), ( "full-s", "Full Camp Ticket (Supporter)", True, True, None, "Support this non-profit event by paying a bit more. All money will go towards making EMF more awesome.", ((None, 150, 180), ), ), ( "full-sg", "Full Camp Ticket (Gold Supporter)", True, True, None, "Support this non-profit event by paying a bit more. All money will go towards making EMF more awesome.", ((None, 200, 240), ), ), ( "u18", "Under-18", True, False, 150, "For visitors born after August 30th, 2000. All under-18s must be accompanied by an adult.", ((None, 55, 63), ), ), ( "u12", "Under-12", True, False, 50, "For children born after August 30th, 2006. All children must be accompanied by an adult.", ((None, 0, 0), ), ), ] order = 0 for ( name, display_name, has_xfer, has_badge, capacity, description, prices, ) in products: if Product.get_by_name("general", name): continue product = Product( name=name, display_name=display_name, capacity_max=capacity, description=description, parent=general, attributes={ "is_transferable": has_xfer, "has_badge": has_badge }, ) for index, (price_cap, gbp, eur) in enumerate(prices): if len(prices) == 1 or index == 0: tier_name = name + "-std" active = True elif index == 1: tier_name = name + "-early-bird" active = False elif index == 2: tier_name = name + "-late" active = False if PriceTier.get_by_name("general", "name", tier_name): continue pt = PriceTier( name=tier_name, capacity_max=price_cap, personal_limit=10, parent=product, active=active, ) Price(currency="GBP", price_int=gbp * 100, price_tier=pt) Price(currency="EUR", price_int=eur * 100, price_tier=pt) ProductViewProduct(view, product, order) order += 1 db.session.flush() misc = [ # name, display_name, cap, personal_limit, gbp, eur, description ( "parking", "Parking Ticket", 1700, 4, 15, 21, "We're trying to keep cars to a minimum. Please take public transport or car-share if you can.", ), ( "campervan", "Caravan/\u200cCampervan Ticket", 60, 2, 30, 42, "If you bring a caravan, you won't need a separate parking ticket for the towing car.", ), ] for name, display_name, cap, personal_limit, gbp, eur, description in misc: if Product.get_by_name(name, name): continue group = ProductGroup.get_by_name(name) product = Product(name=name, display_name=display_name, description=description, parent=group) pt = PriceTier(name=name, personal_limit=personal_limit, parent=product) db.session.add(pt) db.session.add( Price(currency="GBP", price_int=gbp * 100, price_tier=pt)) db.session.add( Price(currency="EUR", price_int=eur * 100, price_tier=pt)) ProductViewProduct(view, product, order) order += 1 db.session.commit()