def create_fake_tickets(self, user): if random.random() < 0.3: return if random.random() < 0.2: currency = "EUR" else: currency = "GBP" b = Basket(user, currency) pt = PriceTier.query.filter_by(name="full-std").one() b[pt] = int(round(random.uniform(1, 4))) if random.random() < 0.5: pt = PriceTier.query.filter_by(name="parking").one() b[pt] = 1 b.create_purchases() b.ensure_purchase_capacity() payment_type = { "gc": GoCardlessPayment, "bank": BankPayment, "stripe": StripePayment, }.get(random_state({"gc": 0.3, "bank": 0.2, "stripe": 0.5})) payment = b.create_payment(payment_type) if random.random() < 0.8: payment.paid() else: payment.cancel()
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 create_purchases(self, tier, count): basket = Basket(self.user, 'GBP') basket[tier] = count basket.create_purchases() basket.ensure_purchase_capacity() payment = basket.create_payment(BankPayment) assert len(payment.purchases) == count self.db.session.commit() return payment.purchases
def create_purchases(tier, count, user): basket = Basket(user, "GBP") basket[tier] = count basket.create_purchases() basket.ensure_purchase_capacity() payment = basket.create_payment(BankPayment) db.session.add(payment) db.session.commit() assert len(payment.purchases) == count return payment.purchases
def create_purchases(tier, count, user): basket = Basket(user, 'GBP') basket[tier] = count basket.create_purchases() basket.ensure_purchase_capacity() payment = basket.create_payment(BankPayment) db.session.add(payment) db.session.commit() assert len(payment.purchases) == count return payment.purchases
def create_fake_tickets(self, user): if random.random() < 0.3: return if random.random() < 0.2: currency = 'EUR' else: currency = 'GBP' b = Basket(user, currency) pt = PriceTier.query.filter_by(name='full-std').one() b[pt] = int(round(random.uniform(1, 4))) if random.random() < 0.5: pt = PriceTier.query.filter_by(name='parking').one() b[pt] = 1 b.create_purchases() b.ensure_purchase_capacity() payment_type = { 'gc': GoCardlessPayment, 'bank': BankPayment, 'stripe': StripePayment }.get(random_state({ 'gc': 0.3, 'bank': 0.2, 'stripe': 0.5 })) payment = b.create_payment(payment_type) if random.random() < 0.8: payment.paid() else: payment.cancel()
def handle_ticket_selection(form, view: ProductView, flow: str, basket: Basket): """ For consistency and to avoid surprises, we try to ensure a few things here: - if the user successfully submits a form with no errors, their basket is updated - if they don't, the basket is left untouched - the basket is updated to match what was submitted, even if they added tickets in another tab - if they already have tickets in their basket, only reserve the extra tickets as necessary - don't unreserve surplus tickets until the payment is created - if the user hasn't submitted anything, we use their current reserved ticket counts - if the user has reserved tickets from exhausted tiers on this view, we still show them - if the user has reserved tickets from other views, don't show and don't mess with them - this means the user can combine tickets from multiple views into a single basket - show the sold-out/unavailable states only when the user doesn't have reserved tickets We currently don't deal with multiple price tiers being available around the same time. Reserved tickets from a previous tier should be cancelled before activating a new one. """ if form.currency_code.data != get_user_currency(): set_user_currency(form.currency_code.data) # Commit so we don't lose the currency change if an error occurs db.session.commit() # Reload the basket because set_user_currency has changed it under us basket = Basket.from_session(current_user, get_user_currency()) form.add_to_basket(basket) if not any(basket.values()): empty_baskets.inc() if view.type == "tickets": flash("Please select at least one ticket to buy.") elif view.type == "hire": flash("Please select at least one item to hire.") else: flash("Please select at least one item to buy.") basket.save_to_session() return redirect(url_for("tickets.main", flow=flow)) # Ensure this purchase is valid for this voucher. voucher = Voucher.get_by_code(basket.voucher) if voucher and not voucher.check_capacity(basket): basket.save_to_session() if voucher.is_used: flash("Your voucher has been used by someone else.") else: flash(f"You can only purchase {voucher.tickets_remaining} adult " "tickets with this voucher. Please select fewer tickets.") return redirect(url_for("tickets.main", flow=flow)) app.logger.info("Saving basket %s", basket) try: # Convert the user's basket into a purchase. basket.create_purchases() basket.ensure_purchase_capacity() db.session.commit() except CapacityException as e: # Damn, capacity's gone since we created the purchases # Redirect back to show what's currently in the basket db.session.rollback() no_capacity.inc() app.logger.warn("Limit exceeded creating tickets: %s", e) flash( "We're very sorry, but there is not enough capacity available to " "allocate these tickets. You may be able to try again with a smaller amount." ) return redirect(url_for("tickets.main", flow=flow)) basket.save_to_session() if basket.total != 0: # Send the user off to pay return redirect(url_for("tickets.pay", flow=flow)) else: return handle_free_tickets(flow, view, basket)
def tickets_choose_free(user_id=None): free_pts = PriceTier.query.join(Product).filter( ~PriceTier.prices.any(Price.price_int > 0), ).order_by( Product.name).all() if user_id is None: form = TicketsNewUserForm() user = None new_user = True else: form = TicketsForm() user = User.query.get_or_404(user_id) new_user = False if request.method != 'POST': for pt in free_pts: form.price_tiers.append_entry() form.price_tiers[-1].tier_id.data = pt.id pts = {pt.id: pt for pt in free_pts} for f in form.price_tiers: f._tier = pts[f.tier_id.data] values = range(f._tier.personal_limit + 1) f.amount.values = values f._any = any(values) if form.validate_on_submit(): if not any(f.amount.data for f in form.price_tiers): flash('Please choose some tickets to allocate') return redirect(url_for('.tickets_choose_free', user_id=user_id)) if new_user: app.logger.info('Creating new user with email %s and name %s', form.email.data, form.name.data) user = User(form.email.data, form.name.data) db.session.add(user) flash('Created account for %s' % form.email.data) basket = Basket(user, 'GBP') for f in form.price_tiers: if f.amount.data: basket[f._tier] = f.amount.data app.logger.info('Admin basket for %s %s', user.email, basket) try: basket.create_purchases() basket.ensure_purchase_capacity() assert basket.total == 0 except CapacityException as e: db.session.rollback() app.logger.warn('Limit exceeded creating admin tickets: %s', e) return redirect(url_for('.tickets_choose_free', user_id=user_id)) for p in basket.purchases: p.set_state('paid') app.logger.info('Allocated %s tickets to user', len(basket.purchases)) db.session.commit() code = user.login_code(app.config['SECRET_KEY']) msg = Message('Your complimentary tickets to EMF', sender=app.config['TICKETS_EMAIL'], recipients=[user.email]) msg.body = render_template('emails/tickets-free.txt', user=user, code=code, tickets=basket.purchases, new_user=new_user) if feature_enabled('ISSUE_TICKETS'): attach_tickets(msg, user) mail.send(msg) db.session.commit() flash('Allocated %s ticket(s)' % len(basket.purchases)) return redirect(url_for('.tickets_choose_free')) if new_user: users = User.query.order_by(User.id).all() else: users = None return render_template('admin/tickets/tickets-choose-free.html', form=form, user=user, users=users)
def tickets_reserve(user_id=None): pts = PriceTier.query.join(Product, ProductGroup) \ .order_by(ProductGroup.name, Product.display_name, Product.id).all() if user_id is None: form = TicketsNewUserForm() user = None new_user = True else: form = TicketsForm() user = User.query.get_or_404(user_id) new_user = False if request.method != 'POST': for pt in pts: form.price_tiers.append_entry() form.price_tiers[-1].tier_id.data = pt.id pts = {pt.id: pt for pt in pts} for f in form.price_tiers: f._tier = pts[f.tier_id.data] # TODO: apply per-user limits values = range(f._tier.personal_limit + 1) f.amount.values = values f._any = any(values) if form.validate_on_submit(): if new_user: email, name = form.email.data, form.name.data if not name: name = email app.logger.info('Creating new user with email %s and name %s', email, name) user = User(email, name) flash('Created account for %s' % name) db.session.add(user) currency = form.currency.data if currency: basket = Basket(user, currency) else: basket = Basket(user, 'GBP') for f in form.price_tiers: if f.amount.data: basket[f._tier] = f.amount.data app.logger.info('Admin basket for %s %s', user.email, basket) try: basket.create_purchases() basket.ensure_purchase_capacity() db.session.commit() except CapacityException as e: db.session.rollback() app.logger.warn('Limit exceeded creating admin tickets: %s', e) return redirect(url_for('.tickets_reserve', user_id=user_id)) code = user.login_code(app.config['SECRET_KEY']) msg = Message('Your reserved tickets to EMF', sender=app.config['TICKETS_EMAIL'], recipients=[user.email]) msg.body = render_template('emails/tickets-reserved.txt', user=user, code=code, tickets=basket.purchases, new_user=new_user, currency=currency) mail.send(msg) db.session.commit() flash('Reserved tickets and emailed {}'.format(user.email)) return redirect(url_for('.tickets_reserve')) if new_user: users = User.query.order_by(User.id).all() else: users = None return render_template('admin/tickets/tickets-reserve.html', form=form, pts=pts, user=user, users=users)
def test_create_stripe_purchase(user, app, monkeypatch): # Add some tickets to a basket (/tickets/choose) basket = Basket(user, "GBP") tier = PriceTier.query.filter_by(name="full-std").one_or_none() basket[tier] = 2 basket.create_purchases() basket.ensure_purchase_capacity() db.session.commit() # This matches the intent ID in stored fixtures intent_id = "pi_1GUslpIcI91cWsdeheAuRsyg" with app.test_request_context("/tickets/pay"): login_user(user) payment = basket.create_payment(StripePayment) stripe_start(payment) assert payment.state == "new" with app.test_request_context(f"/pay/stripe/{payment.id}/capture"): login_user(user) # Start capture process - this creates a payment intent from fake-stripe stripe_capture(payment.id) # A payment_intent.created webhook should be generated here, but it # doesn't cause any action on our end so we don't simulate this. assert payment.intent_id == intent_id assert payment.state == "new" # User is now on the Stripe form, which captures the card details. # Once this is complete, payment details are sent to Stripe and the form # submission triggers stripe_capture_post stripe_capture_post(payment.id) assert payment.state == "charging" with app.test_request_context("/stripe-webhook"): # Stripe will now send a webhook to notify us of the payment success. stripe_payment_intent_updated( "payment_intent.succeeded", load_webhook_fixture("payment_intent.succeeded")) # A charge.succeeded webhook is also sent but we ignore it. assert payment.state == "paid" assert all( purchase.state == "paid" for purchase in payment.purchases), "Purchases should be marked as paid after payment" # Payment is all paid. Now we test refunding it. # Create a refund request for the entire payment, with £20 donation. refund_request = RefundRequest(payment=payment, donation=20, currency=payment.currency) payment.state = "refund-requested" db.session.add(refund_request) db.session.commit() handle_refund_request(refund_request) with app.test_request_context("/stripe-webhook"): # charge.refunded webhook. We do process this but currently we don't use it for anything. stripe_charge_refunded("charge.refunded", load_webhook_fixture("charge.refunded")) # Payment should be marked as fully refunded. assert payment.state == "refunded" assert all(purchase.state == "refunded" for purchase in payment.purchases ), "Purchases should be marked as refunded after refund"