예제 #1
0
    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()
예제 #2
0
    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()
예제 #3
0
    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
예제 #4
0
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
예제 #5
0
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
예제 #6
0
파일: dev.py 프로젝트: emfcamp/Website
    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()
예제 #7
0
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)
예제 #8
0
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)
예제 #9
0
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)
예제 #10
0
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"