Skip to content
This repository was archived by the owner on Apr 29, 2022. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 42 additions & 31 deletions conference/cart.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
is_business_order,
is_non_conference_ticket_order,
)
from .payments import PaymentError, charge_for_payment
from .payments import PaymentError, prepare_for_payment, verify_payment


GLOBAL_MAX_PER_FARE_TYPE = 6
Expand Down Expand Up @@ -165,55 +165,61 @@ def cart_step4_payment(request, order_uuid):
"cart:step5_congrats_order_complete", order_uuid=order.uuid
)

stripe_payment = StripePayment.objects.create(
order=order,
user=request.user,
uuid=str(uuid.uuid4()),
amount=order.total(),
token=request.POST.get("stripeToken"),
token_type=request.POST.get("stripeTokenType"),
email=request.POST.get("stripeEmail"),
description=f"payment for order {order.pk} by {request.user.email}",
)
try:
# Save the payment information as soon as it goes through to
# avoid data loss.
charge_for_payment(stripe_payment)
order.payment_date = timezone.now()
order.save()

with transaction.atomic():
create_invoices_for_order(order)
current_site = get_current_site(request)
send_order_confirmation_email(order, current_site)

return redirect(
"cart:step5_congrats_order_complete", order_uuid=order.uuid
)
except PaymentError:
# Redirect to the same page, show information about failed
# payment(s) and reshow the same Pay with Card button
return redirect(".")

# sanity/security check to make sure we don't publish the the wrong key
stripe_key = settings.STRIPE_PUBLISHABLE_KEY
assert stripe_key.startswith("pk_")
stripe_session_id = None
if total_for_stripe > 0:
stripe_session = prepare_for_payment(
request,
order = order,
description = f"payment for order {order.pk} by {request.user.email}"
)
stripe_session_id = stripe_session.id

return TemplateResponse(
request,
"ep19/bs/cart/step_4_payment.html",
{
"order": order,
"payments": payments,
"payment": payments,
"stripe_key": stripe_key,
"total_for_stripe": total_for_stripe,
"stripe_session_id": stripe_session_id,
},
)


@login_required
def cart_step4b_verify_payment(request, payment_uuid, session_id):
payment = get_object_or_404(StripePayment, uuid=payment_uuid)
order = payment.order
if payment.status != 'SUCCESSFUL':
try:
verify_payment(payment, session_id)
except PaymentError:
return redirect("cart:step4_payment", order_uuid=order.uuid)

if order.payment_date != None:
# XXX This order was already paid for(!)
payment.message = 'Duplicate payment?!?'
payment.save()
else:
order.payment_date = timezone.now()
order.save()

with transaction.atomic():
create_invoices_for_order(order)
current_site = get_current_site(request)
send_order_confirmation_email(order, current_site)

return redirect("cart:step5_congrats_order_complete", order.uuid)

@login_required
def cart_step5_congrats_order_complete(request, order_uuid):
order = get_object_or_404(Order, uuid=order_uuid)

return TemplateResponse(
request,
"ep19/bs/cart/step_5_congrats_order_complete.html",
Expand Down Expand Up @@ -351,6 +357,11 @@ class Meta:
cart_step4_payment,
name="step4_payment",
),
url(
r"^verify/(?P<payment_uuid>[\w-]+)/(?P<session_id>[-_\w{}]+)/$",
cart_step4b_verify_payment,
name="step4b_verify_payment",
),
url(
r"^thanks/(?P<order_uuid>[\w-]+)/$",
cart_step5_congrats_order_complete,
Expand Down
20 changes: 20 additions & 0 deletions conference/migrations/0019_add_stripe_session_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import conference.models
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('conference', '0018_remove_unused_models'),
]

operations = [
migrations.AddField(
model_name='stripepayment',
name='session_id',
field=models.CharField(max_length=100, null=True),
),
]
1 change: 1 addition & 0 deletions conference/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1351,6 +1351,7 @@ class StripePayment(models.Model):
token = models.CharField(max_length=100)
token_type = models.CharField(max_length=20)
charge_id = models.CharField(max_length=100, null=True)
session_id = models.CharField(max_length=100, null=True)
email = models.CharField(max_length=255)

user = models.ForeignKey(get_user_model())
Expand Down
100 changes: 100 additions & 0 deletions conference/payments.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@

import stripe
import uuid

from django.conf import settings
from django.core.urlresolvers import reverse

from conference.models import StripePayment

Expand All @@ -13,6 +15,104 @@ class PaymentError(Exception):
pass


def prepare_for_payment(request, order, description):
stripe.api_key = settings.STRIPE_SECRET_KEY

try:
stripe_payment = StripePayment.objects.create(
order=order,
user=request.user,
uuid=str(uuid.uuid4()),
amount=order.total(),
email=request.user.email,
description=f"payment for order {order.code} ({order.pk}) by {request.user.email}",
)

success_url = request.build_absolute_uri(
reverse('cart:step4b_verify_payment',
kwargs={'payment_uuid': stripe_payment.uuid,
'session_id': 'CHECKOUT_SESSION_ID'
}
))
# XXX {} must not be url-encoded
success_url = success_url.replace('CHECKOUT_SESSION_ID', '{CHECKOUT_SESSION_ID}')

cancel_url = request.build_absolute_uri(
reverse('cart:step4_payment',
kwargs={'order_uuid': order.uuid},
))
line_items = []
line_items.append({
'name': 'Invoice',
'description': stripe_payment.description,
'amount': stripe_payment.amount_for_stripe(),
'currency': STRIPE_PAYMENTS_CURRENCY,
'quantity': 1,
})

session = stripe.checkout.Session.create(
payment_intent_data=dict(
metadata={
'order': request.build_absolute_uri(reverse('admin:assopy_order_change', args=(order.id,))),
'order_uuid': order.uuid,
'order_code': order.code,
'order_pk': order.pk,
}),
# XXX Setting email disables editing it. Not sure we want that.
# customer_email=stripe_payment.email,
success_url=success_url,
cancel_url=cancel_url,
payment_method_types=['card'],
line_items=line_items,
)

stripe_payment.session_id = session.id
stripe_payment.save()

return session

except stripe.error.StripeError as e:
stripe_error = e.json_body['error']
stripe_payment.status = stripe_payment.STATUS_CHOICES.FAILED
stripe_payment.message = (
f"{stripe_error['type']} -- {stripe_error['message']}"
)
stripe_payment.save()

raise PaymentError(stripe_error)


def verify_payment(stripe_payment, session_id):
stripe.api_key = settings.STRIPE_SECRET_KEY

try:
session = stripe.checkout.Session.retrieve(session_id)
payment_intent = stripe.PaymentIntent.retrieve(session.payment_intent)
if payment_intent.status == 'succeeded' and payment_intent.amount_received == stripe_payment.amount_for_stripe():
charge = payment_intent.charges.data[0]
stripe_payment.status = stripe_payment.STATUS_CHOICES.SUCCESSFUL
stripe_payment.charge_id = charge.id
stripe_payment.save()
else:
stripe_payment.status = stripe_payment.STATUS_CHOICES.FAILED
stripe_payment.message = (
"There was an error processing your payment."
)
stripe_payment.save()
raise PaymentError({'type': 'unknown',
'message': 'error processing payment'})


except stripe.error.StripeError as e:
stripe_error = e.json_body['error']
stripe_payment.status = stripe_payment.STATUS_CHOICES.FAILED
stripe_payment.message = (
f"{stripe_error['type']} -- {stripe_error['message']}"
)
stripe_payment.save()

raise PaymentError(stripe_error)

def charge_for_payment(stripe_payment):
assert isinstance(stripe_payment, StripePayment)

Expand Down
2 changes: 2 additions & 0 deletions templates/ep19/bs/cart/_order_summary.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ <h4>Summary</h4>
<td>{{ item.code }}</td>
<td>{% if item.ticket %}{{ item.ticket }}{% else %}{{ item.description }}{% endif %}</td>
<td>{{ item.price }}</td>
{% if item.ticket %}
<td class='text-right'>
<a class='btn btn-outline-primary' href='{% url "user_panel:manage_ticket" item.ticket.id %}'>Configure ticket</a>
<a class='btn btn-outline-primary' href='{% url "user_panel:assign_ticket" item.ticket.id %}'>Assign ticket</a>
</td>
{% endif %}

</tr>
{% endfor %}
Expand Down
44 changes: 22 additions & 22 deletions templates/ep19/bs/cart/step_4_payment.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,31 @@ <h1>Step 4: Payment</h1>

<div class='col-md-12'>
{% include "ep19/bs/cart/_order_summary.html" %}

{% if not order.is_complete %}
<form action="." method="POST" class='form' style='margin-top: 5em'>
{% if total_for_stripe == 0 %}
<form action="." method="POST" class='form' style='margin-top: 5em'>
{% csrf_token %}
{% if total_for_stripe == 0 %}
<button class="btn btn-primary btn-lg">Confirm Order</button>
{% else %}
<script
src="https://checkout.stripe.com/checkout.js"
class="stripe-button"
data-key="{{ stripe_key }}"
data-amount="{{ total_for_stripe }}"
data-currency='eur'
data-label="Finalize order and pay now"
data-name="Stripe.com"
data-description="EuroPython Order {{ order.code }}"
data-image="https://stripe.com/img/documentation/checkout/marketplace.png"
data-locale="auto"
data-zip-code="false">
</script>
{% endif %}
</form>
<button class="btn btn-primary btn-lg">Confirm Order</button>
</form>
{% else %}
<script src="https://js.stripe.com/v3/"></script>
<script type="text/javascript">
var stripe = Stripe("{{ stripe_key }}");
function do_stripe() {
stripe.redirectToCheckout({
sessionId: "{{ stripe_session_id }}"
}).then(function (result) {
// XXX make this pretty, and give retry button
alert(result.error.message);
});
}
</script>
<button class="btn btn-primary btn-lg"
onclick="do_stripe()">Pay now!</button>
{% endif %}
{% else %}
This order was paid in full on {{ order.payment_date|date:"Y-m-d" }}
<a href='{% url "cart:step5_congrats_order_complete" order.uuid %}'>See the confirmation page</a>
<div style="font-size: 1.5em">This order was paid in full on {{ order.payment_date|date:"Y-m-d" }}
<a href='{% url "cart:step5_congrats_order_complete" order.uuid%}'>See the confirmation page</a></div>
{% endif %}{# order.is_complete #}

{% if payments %}
Expand Down
27 changes: 18 additions & 9 deletions tests/test_cart.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from datetime import timedelta
from decimal import Decimal
from unittest import mock
import uuid

from pytest import mark, raises, approx

Expand Down Expand Up @@ -654,19 +655,26 @@ def test_cart_payment_with_zero_total(db, user_client):
assert email_sent_with_subject(ORDER_CONFIRMATION_EMAIL_SUBJECT)


@mock.patch('conference.cart.charge_for_payment')
def test_cart_payment_with_non_zero_total(mock_charge_for_payment, db, user_client):
@mock.patch('conference.cart.prepare_for_payment')
@mock.patch('conference.cart.verify_payment')
def test_cart_payment_with_non_zero_total(mock_prepare_for_payment, mock_verify_payment, db, user_client):
_, fares = setup_conference_with_typical_fares()
order = OrderFactory(items=[(fares[0], {"qty": 1})])

payload = dict(
stripeToken='fakeToken',
stripeTokenType='fakeTokenType',
stripeEmail='fakeStripeEmail',
payment = StripePayment.objects.create(
amount=fares[0].price,
order=order,
user=user_client.user,
uuid=str(uuid.uuid4()),
)

mock_prepare_for_payment.return_value = payment

payment_step_url = reverse("cart:step4_payment", args=[order.uuid])
response = user_client.post(payment_step_url, data=payload)
response = user_client.get(payment_step_url)

verify_payment_url = reverse("cart:step4b_verify_payment", args=(order.stripepayment_set.all()[0].uuid,
'SESSION_ID'))
response = user_client.get(verify_payment_url)
order_complete_url = reverse("cart:step5_congrats_order_complete", args=[order.uuid])
assert redirects_to(response, order_complete_url)

Expand All @@ -676,7 +684,8 @@ def test_cart_payment_with_non_zero_total(mock_charge_for_payment, db, user_clie
# assert order.payment_date.date() == timezone.now().date()
assert order.invoices.count() == 1
assert StripePayment.objects.count() == 1
assert mock_charge_for_payment.call_count == 1
assert mock_prepare_for_payment.call_count == 1
assert mock_verify_payment.call_count == 1
assert email_sent_with_subject(ORDER_CONFIRMATION_EMAIL_SUBJECT)


Expand Down