Implement payment flow using Stripe Checkout (#15753)

This commit is contained in:
William Durand 2020-10-21 20:19:07 +02:00 коммит произвёл GitHub
Родитель 6500418a09
Коммит 9e20c0e9b4
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
12 изменённых файлов: 770 добавлений и 7 удалений

Просмотреть файл

@ -508,3 +508,6 @@ cached-property==1.5.2 \
django-jsonfield-backport==1.0.2 \ django-jsonfield-backport==1.0.2 \
--hash=sha256:5574505967f6d7ada8c9269a5f873cfdca9812dc9502eee2b7a86be5c3798c76 \ --hash=sha256:5574505967f6d7ada8c9269a5f873cfdca9812dc9502eee2b7a86be5c3798c76 \
--hash=sha256:0286dcc1c112389d52096f269eed83a77364ea2b349fe1777f5e4464c3c36fa9 --hash=sha256:0286dcc1c112389d52096f269eed83a77364ea2b349fe1777f5e4464c3c36fa9
stripe==2.54.0 \
--hash=sha256:6daf0dcb7f9cb8d1af93e94eb7d91df4c0b112db517c8c446017ea0b7f037c83 \
--hash=sha256:a622bce0d6d0ac99a53b6e6a6475c39250207d644f28ad0ed276af7b96d12617

Просмотреть файл

@ -0,0 +1,79 @@
{% extends "devhub/base_impala.html" %}
{% set title = _('Set Up Payment') %}
{% block js %}
{{ super() }}
<script src="https://js.stripe.com/v3/"></script>
<script src="{{ static('js/devhub/stripe.js') }}"></script>
{% endblock %}
{% block title %}
{{ dev_page_title(title, addon) }}
{% endblock %}
{% block content %}
<section class="primary full">
<div id="onboard-addon" class="devhub-form">
<h2>{{ _('Set Up Payment') }}</h2>
{% if stripe_checkout_completed %}
<p>
{% trans %}
You're almost done!
{% endtrans %}
</p>
<p>
{% if promoted_group == amo.promoted.SPONSORED %}
{% trans addon_name=addon.name %}
Please upload a new version of your add-on <em>{{ addon_name }}</em>.
Once that version passes review, your add-on will be published as
Sponsored and should become available shortly. If you have any
questions, don't hesitate to contact us.
{% endtrans %}
{% else %}
{% trans addon_name=addon.name %}
Please upload a new version of your add-on <em>{{ addon_name }}</em>.
Once that version passes review, your add-on will be published as
Verified and should become available shortly. If you have any
questions, don't hesitate to contact us.
{% endtrans %}
{% endif %}
</p>
{% else %}
{% if stripe_checkout_cancelled %}
<div class="notification-box error">
<p>
{% trans %}
There was an error while setting up payment for your add-on. If you're
experiencing problems with this process, please contact us at
<a href="mailto:amo-admins@mozilla.com">amo-admins@mozilla.com</a>.
{% endtrans %}
</p>
</div>
{% endif %}
<p>
{% trans %}
Thank you for joining the Promoted Add-ons Program!
{% endtrans %}
</p>
<p>
{% trans addon_name=addon.name %}
Your add-on <em>{{ addon_name }}</em> will be included in the program
once payment is set up through the Stripe system. Click on the button
below to continue:
{% endtrans %}
</p>
<p>
<button
id="checkout-button"
data-sessionid="{{ stripe_session_id }}"
data-publickey="{{ stripe_api_public_key }}"
>
{{ _('Continue to Stripe Checkout') }}
</button>
</p>
{% endif %}
</div>
</section>
{% endblock %}

Просмотреть файл

@ -0,0 +1,309 @@
import datetime
from unittest import mock
from waffle.testutils import override_switch
from olympia.amo.tests import TestCase, addon_factory, user_factory
from olympia.amo.urlresolvers import reverse
from olympia.constants.promoted import VERIFIED
from olympia.promoted.models import PromotedAddon, PromotedSubscription
@override_switch("enable-subscriptions-for-promoted-addons", active=True)
class OnboardingSubscriptionTestCase(TestCase):
def setUp(self):
super().setUp()
self.user = user_factory()
self.addon = addon_factory(users=[self.user])
self.promoted_addon = PromotedAddon.objects.create(
addon=self.addon, group_id=VERIFIED.id
)
self.subscription = PromotedSubscription.objects.filter(
promoted_addon=self.promoted_addon
).get()
self.url = reverse(self.url_name, args=[self.addon.slug])
self.client.login(email=self.user.email)
class TestOnboardingSubscription(OnboardingSubscriptionTestCase):
url_name = "devhub.addons.onboarding_subscription"
@override_switch("enable-subscriptions-for-promoted-addons", active=False)
def test_returns_404_when_switch_is_disabled(self):
assert self.client.get(self.url).status_code == 404
def test_returns_404_when_subscription_is_not_found(self):
# Create an add-on without a subscription.
addon = addon_factory(users=[self.user])
url = reverse(self.url_name, args=[addon.slug])
assert self.client.get(url).status_code == 404
@mock.patch("olympia.devhub.views.create_stripe_checkout_session")
def test_get_for_the_first_time(self, create_mock):
create_mock.return_value = mock.MagicMock(id="session-id")
assert not self.subscription.link_visited_at
assert not self.subscription.stripe_session_id
response = self.client.get(self.url)
self.subscription.refresh_from_db()
assert response.status_code == 200
create_mock.assert_called_with(
self.subscription, customer_email=self.user.email
)
assert self.subscription.link_visited_at is not None
assert self.subscription.stripe_session_id == "session-id"
assert (
response.context["stripe_session_id"] ==
self.subscription.stripe_session_id
)
assert response.context["addon"] == self.addon
assert not response.context["stripe_checkout_completed"]
assert not response.context["stripe_checkout_cancelled"]
assert response.context["promoted_group"] == self.promoted_addon.group
assert (
b"Thank you for joining the Promoted Add-ons Program!"
in response.content
)
@mock.patch("olympia.devhub.views.create_stripe_checkout_session")
def test_get(self, create_mock):
create_mock.side_effect = [
mock.MagicMock(id="session-id-1"),
mock.MagicMock(id="session-id-2"),
]
# Get the page.
queries = 31
with self.assertNumQueries(queries):
# 31 queries:
# - 3 users + groups
# - 2 savepoints (test)
# - 3 addon and its translations
# - 2 addon categories
# - 4 versions and translations
# - 2 application versions
# - 2 files
# - 5 addon users
# - 2 previews
# - 1 waffle switch
# - 1 promoted subscription
# - 1 promoted add-on
# - 1 UPDATE promoted subscription
# - 1 addons_collections
# - 1 config (site notice)
response = self.client.get(self.url)
self.subscription.refresh_from_db()
link_visited_at = self.subscription.link_visited_at
assert link_visited_at
assert self.subscription.stripe_session_id == "session-id-1"
# Get the page, again.
with self.assertNumQueries(queries - 1):
# - waffle switch is cached
response = self.client.get(self.url)
self.subscription.refresh_from_db()
assert response.status_code == 200
assert self.subscription.link_visited_at == link_visited_at
assert self.subscription.stripe_session_id == "session-id-2"
@mock.patch("olympia.devhub.views.create_stripe_checkout_session")
def test_shows_page_with_admin(self, create_mock):
admin = user_factory()
self.grant_permission(admin, "*:*")
self.client.logout()
self.client.login(email=admin.email)
create_mock.return_value = mock.MagicMock(id="session-id")
assert not self.subscription.link_visited_at
response = self.client.get(self.url)
self.subscription.refresh_from_db()
assert response.status_code == 200
# We don't set this date when the user is not the owner (in this case,
# the user is an admin).
assert not self.subscription.link_visited_at
@mock.patch("olympia.devhub.views.create_stripe_checkout_session")
def test_shows_error_message_when_payment_was_previously_cancelled(
self, create_mock
):
create_mock.return_value = mock.MagicMock(id="session-id")
self.subscription.update(payment_cancelled_at=datetime.datetime.now())
response = self.client.get(self.url)
assert (
b"There was an error while setting up payment for your add-on."
in response.content
)
create_mock.assert_called_with(
self.subscription, customer_email=self.user.email
)
@mock.patch("olympia.devhub.views.retrieve_stripe_checkout_session")
def test_shows_confirmation_after_payment(self, retrieve_mock):
stripe_session_id = "some session id"
retrieve_mock.return_value = mock.MagicMock(id=stripe_session_id)
self.subscription.update(
stripe_session_id=stripe_session_id,
paid_at=datetime.datetime.now(),
)
response = self.client.get(self.url)
assert b"You're almost done!" in response.content
retrieve_mock.assert_called_with(self.subscription)
@mock.patch("olympia.devhub.views.retrieve_stripe_checkout_session")
def test_get_returns_500_when_retrieve_has_failed(self, retrieve_mock):
stripe_session_id = "some session id"
self.subscription.update(
stripe_session_id=stripe_session_id,
paid_at=datetime.datetime.now(),
)
retrieve_mock.side_effect = Exception("stripe error")
response = self.client.get(self.url)
assert response.status_code == 500
@mock.patch("olympia.devhub.views.create_stripe_checkout_session")
def test_get_returns_500_when_create_has_failed(self, create_mock):
create_mock.side_effect = Exception("stripe error")
response = self.client.get(self.url)
assert response.status_code == 500
class TestOnboardingSubscriptionSuccess(OnboardingSubscriptionTestCase):
url_name = "devhub.addons.onboarding_subscription_success"
@override_switch("enable-subscriptions-for-promoted-addons", active=False)
def test_returns_404_when_switch_is_disabled(self):
assert self.client.get(self.url).status_code == 404
def test_returns_404_when_subscription_is_not_found(self):
# Create an add-on without a subscription.
addon = addon_factory(users=[self.user])
url = reverse(self.url_name, args=[addon.slug])
assert self.client.get(url).status_code == 404
@mock.patch("olympia.devhub.views.retrieve_stripe_checkout_session")
def test_get_returns_404_when_session_not_found(self, retrieve_mock):
retrieve_mock.side_effect = Exception("stripe error")
response = self.client.get(self.url)
assert response.status_code == 404
@mock.patch("olympia.devhub.views.retrieve_stripe_checkout_session")
def test_get_redirects_to_main_page(self, retrieve_mock):
retrieve_mock.return_value = mock.MagicMock(
id="session-id", payment_status="unpaid"
)
response = self.client.get(self.url)
assert response.status_code == 302
assert response["Location"].endswith("/onboarding-subscription")
@mock.patch("olympia.devhub.views.retrieve_stripe_checkout_session")
def test_get_records_payment_once(self, retrieve_mock):
retrieve_mock.return_value = mock.MagicMock(
id="session-id", payment_status="paid"
)
assert not self.subscription.paid_at
self.client.get(self.url)
self.subscription.refresh_from_db()
paid_at = self.subscription.paid_at
assert paid_at is not None
self.client.get(self.url)
self.subscription.refresh_from_db()
# Make sure we don't update this date again.
assert self.subscription.paid_at == paid_at
assert not self.subscription.payment_cancelled_at
@mock.patch("olympia.devhub.views.retrieve_stripe_checkout_session")
def test_get_resets_payment_cancelled_date_after_success(
self, retrieve_mock
):
retrieve_mock.return_value = mock.MagicMock(
id="session-id", payment_status="paid"
)
self.subscription.update(payment_cancelled_at=datetime.datetime.now())
self.client.get(self.url)
self.subscription.refresh_from_db()
assert not self.subscription.payment_cancelled_at
class TestOnboardingSubscriptionCancel(OnboardingSubscriptionTestCase):
url_name = "devhub.addons.onboarding_subscription_cancel"
@override_switch("enable-subscriptions-for-promoted-addons", active=False)
def test_returns_404_when_switch_is_disabled(self):
assert self.client.get(self.url).status_code == 404
def test_returns_404_when_subscription_is_not_found(self):
# Create an add-on without a subscription.
addon = addon_factory(users=[self.user])
url = reverse(self.url_name, args=[addon.slug])
assert self.client.get(url).status_code == 404
@mock.patch("olympia.devhub.views.retrieve_stripe_checkout_session")
def test_get_returns_404_when_session_not_found(self, retrieve_mock):
retrieve_mock.side_effect = Exception("stripe error")
response = self.client.get(self.url)
assert response.status_code == 404
@mock.patch("olympia.devhub.views.retrieve_stripe_checkout_session")
def test_get_redirects_to_main_page(self, retrieve_mock):
retrieve_mock.return_value = mock.MagicMock(id="session-id")
response = self.client.get(self.url)
assert response.status_code == 302
assert response["Location"].endswith("/onboarding-subscription")
@mock.patch("olympia.devhub.views.retrieve_stripe_checkout_session")
def test_get_sets_payment_cancelled_date(self, retrieve_mock):
stripe_session_id = "some session id"
self.subscription.update(stripe_session_id=stripe_session_id)
retrieve_mock.return_value = mock.MagicMock(id=stripe_session_id)
assert not self.subscription.payment_cancelled_at
self.client.get(self.url)
self.subscription.refresh_from_db()
assert self.subscription.payment_cancelled_at
@mock.patch("olympia.devhub.views.retrieve_stripe_checkout_session")
def test_get_does_not_set_payment_cancelled_date_when_already_paid(
self, retrieve_mock
):
retrieve_mock.return_value = mock.MagicMock(id="session-id")
self.subscription.update(paid_at=datetime.datetime.now())
assert not self.subscription.payment_cancelled_at
self.client.get(self.url)
self.subscription.refresh_from_db()
assert not self.subscription.payment_cancelled_at

Просмотреть файл

@ -24,6 +24,14 @@ detail_patterns = [
name='devhub.addons.invitation'), name='devhub.addons.invitation'),
re_path(r'^edit_(?P<section>[^/]+)(?:/(?P<editable>[^/]+))?$', re_path(r'^edit_(?P<section>[^/]+)(?:/(?P<editable>[^/]+))?$',
views.addons_section, name='devhub.addons.section'), views.addons_section, name='devhub.addons.section'),
re_path(r'^onboarding-subscription$', views.onboarding_subscription,
name='devhub.addons.onboarding_subscription'),
re_path(r'^onboarding-subscription/success$',
views.onboarding_subscription_success,
name='devhub.addons.onboarding_subscription_success'),
re_path(r'^onboarding-subscription/cancel$',
views.onboarding_subscription_cancel,
name='devhub.addons.onboarding_subscription_cancel'),
re_path(r'^upload_preview$', views.upload_image, re_path(r'^upload_preview$', views.upload_image,
{'upload_type': 'preview'}, {'upload_type': 'preview'},

Просмотреть файл

@ -45,6 +45,11 @@ from olympia.devhub.utils import (
UploadRestrictionChecker, wizard_unsupported_properties) UploadRestrictionChecker, wizard_unsupported_properties)
from olympia.files.models import File, FileUpload from olympia.files.models import File, FileUpload
from olympia.files.utils import parse_addon from olympia.files.utils import parse_addon
from olympia.promoted.models import PromotedSubscription
from olympia.promoted.utils import (
create_stripe_checkout_session,
retrieve_stripe_checkout_session,
)
from olympia.reviewers.forms import PublicWhiteboardForm from olympia.reviewers.forms import PublicWhiteboardForm
from olympia.reviewers.models import Whiteboard from olympia.reviewers.models import Whiteboard
from olympia.reviewers.templatetags.code_manager import code_manager_url from olympia.reviewers.templatetags.code_manager import code_manager_url
@ -1837,3 +1842,130 @@ def logout(request):
logout_user(request, response) logout_user(request, response)
return response return response
def get_promoted_subscription_or_404(addon):
if not waffle.switch_is_active('enable-subscriptions-for-promoted-addons'):
log.info(
'cannot retrieve a promoted subscription because waffle switch '
'is disabled.'
)
raise http.Http404()
qs = PromotedSubscription.objects.select_related('promoted_addon')
return get_object_or_404(qs, promoted_addon__addon=addon)
@dev_required
@csp_update(
SCRIPT_SRC="https://js.stripe.com",
CONNECT_SRC="https://api.stripe.com",
FRAME_SRC=["https://js.stripe.com", "https://hooks.stripe.com"],
)
def onboarding_subscription(request, addon_id, addon):
sub = get_promoted_subscription_or_404(addon=addon)
fields_to_update = {}
if addon.has_author(request.user) and not sub.link_visited_at:
fields_to_update['link_visited_at'] = datetime.datetime.now()
try:
if sub.stripe_checkout_completed:
session = retrieve_stripe_checkout_session(sub)
log.debug(
'retrieved a stripe checkout session for PromotedSubscription'
' %s.',
sub.pk
)
else:
session = create_stripe_checkout_session(
sub, customer_email=request.user.email
)
log.debug(
'created a stripe checkout session for PromotedSubscription'
' %s.',
sub.pk
)
except Exception:
log.exception(
'could not retrieve or create a Stripe Checkout session for '
'PromotedSubscription {}.'.format(sub.pk)
)
return http.HttpResponseServerError()
if sub.stripe_session_id != session.id:
fields_to_update['stripe_session_id'] = session.id
if len(fields_to_update) > 0:
sub.update(**fields_to_update)
data = {
"addon": addon,
"stripe_session_id": sub.stripe_session_id,
"stripe_api_public_key": settings.STRIPE_API_PUBLIC_KEY,
"stripe_checkout_completed": sub.stripe_checkout_completed,
"stripe_checkout_cancelled": sub.stripe_checkout_cancelled,
"promoted_group": sub.promoted_addon.group,
}
return render(request, "devhub/addons/onboarding_subscription.html", data)
@dev_required
def onboarding_subscription_success(request, addon_id, addon):
sub = get_promoted_subscription_or_404(addon=addon)
try:
session = retrieve_stripe_checkout_session(sub)
except Exception:
log.exception(
"error while trying to retrieve a stripe checkout session for "
"PromotedSubscription %s (success).",
sub.pk
)
raise http.Http404()
if session.payment_status == "paid" and not sub.paid_at:
# When the user has completed the Stripe Checkout process, we record
# this event.
#
# We reset `payment_cancelled_at` because it does not matter if the
# user has cancelled or not in the past (this simply means the user
# opened the Checkout page and didn't subscribe), mainly because as of
# now, the user has finally subscribed.
#
# Note: "cancellation" of an active subscription is not supported yet.
sub.update(payment_cancelled_at=None, paid_at=datetime.datetime.now())
log.info('PromotedSubscription %s has been paid.', sub.pk)
return redirect(
reverse("devhub.addons.onboarding_subscription", args=[addon.id])
)
@dev_required
def onboarding_subscription_cancel(request, addon_id, addon):
sub = get_promoted_subscription_or_404(addon=addon)
try:
retrieve_stripe_checkout_session(sub)
except Exception:
log.exception(
"error while trying to retrieve a stripe checkout session for "
"PromotedSubscription %s (cancel).",
sub.pk
)
raise http.Http404()
if not sub.stripe_checkout_completed:
# We record this date only when the user has cancelled the Stripe
# Checkout process on the Checkout page (i.e. the user has not
# subscribed yet).
#
# If the user has completed the checkout process, then we prevent this
# date to be changed.
sub.update(payment_cancelled_at=datetime.datetime.now())
log.info('PromotedSubscription %s has been cancelled.', sub.pk)
return redirect(
reverse("devhub.addons.onboarding_subscription", args=[addon.id])
)

Просмотреть файл

@ -1933,3 +1933,11 @@ ADZERK_URL = f'https://e-{ADZERK_NETWORK_ID}.adzerk.net/api/v2'
ADZERK_IMPRESSION_TIMEOUT = 60 # seconds ADZERK_IMPRESSION_TIMEOUT = 60 # seconds
ADZERK_EVENT_URL = f'https://e-{ADZERK_NETWORK_ID}.adzerk.net/' ADZERK_EVENT_URL = f'https://e-{ADZERK_NETWORK_ID}.adzerk.net/'
ADZERK_EVENT_TIMEOUT = 60 * 60 * 24 # seconds ADZERK_EVENT_TIMEOUT = 60 * 60 * 24 # seconds
# Subscription
STRIPE_API_SECRET_KEY = env('STRIPE_API_SECRET_KEY', default=None)
STRIPE_API_PUBLIC_KEY = env('STRIPE_API_PUBLIC_KEY', default=None)
STRIPE_API_VERIFIED_PRICE_ID = env('STRIPE_API_VERIFIED_PRICE_ID',
default=None)
STRIPE_API_SPONSORED_PRICE_ID = env('STRIPE_API_SPONSORED_PRICE_ID',
default=None)

Просмотреть файл

@ -0,0 +1,22 @@
# Generated by Django 2.2.16 on 2020-10-16 14:48
from django.db import migrations
def create_waffle_switch(apps, schema_editor):
Switch = apps.get_model("waffle", "Switch")
Switch.objects.create(
name="enable-subscriptions-for-promoted-addons",
active=False,
note="Enable the subscription feature for promoted add-ons.",
)
class Migration(migrations.Migration):
dependencies = [
("promoted", "0010_promotedsubscription"),
]
operations = [migrations.RunPython(create_waffle_switch)]

Просмотреть файл

@ -5,6 +5,7 @@ from urllib.parse import urljoin
from olympia.addons.models import Addon from olympia.addons.models import Addon
from olympia.amo.models import ModelBase from olympia.amo.models import ModelBase
from olympia.amo.urlresolvers import reverse
from olympia.constants.applications import APP_IDS, APPS_CHOICES, APP_USAGE from olympia.constants.applications import APP_IDS, APPS_CHOICES, APP_USAGE
from olympia.constants.promoted import ( from olympia.constants.promoted import (
NOT_PROMOTED, PRE_REVIEW_GROUPS, PROMOTED_GROUPS, PROMOTED_GROUPS_BY_ID, NOT_PROMOTED, PRE_REVIEW_GROUPS, PROMOTED_GROUPS, PROMOTED_GROUPS_BY_ID,
@ -190,11 +191,20 @@ class PromotedSubscription(ModelBase):
def __str__(self): def __str__(self):
return f'Subscription for {self.promoted_addon}' return f'Subscription for {self.promoted_addon}'
def get_onboarding_url(self): def get_onboarding_url(self, absolute=True):
if not self.id: if not self.id:
return None return None
return urljoin(
settings.EXTERNAL_SITE_URL, url = reverse('devhub.addons.onboarding_subscription',
# TODO: replace with `reverse()` once we have a route/view. args=[self.promoted_addon.addon.slug])
f'/{self.promoted_addon.addon.slug}/onboarding' if absolute:
) url = urljoin(settings.EXTERNAL_SITE_URL, url)
return url
@property
def stripe_checkout_completed(self):
return bool(self.paid_at)
@property
def stripe_checkout_cancelled(self):
return bool(self.payment_cancelled_at)

Просмотреть файл

@ -1,4 +1,9 @@
import datetime
from django.test.utils import override_settings
from olympia.amo.tests import addon_factory, TestCase from olympia.amo.tests import addon_factory, TestCase
from olympia.amo.urlresolvers import reverse
from olympia.constants import applications, promoted from olympia.constants import applications, promoted
from olympia.promoted.models import ( from olympia.promoted.models import (
PromotedAddon, PromotedApproval, PromotedSubscription) PromotedAddon, PromotedApproval, PromotedSubscription)
@ -80,6 +85,19 @@ class TestPromotedSubscription(TestCase):
assert sub.get_onboarding_url() is None assert sub.get_onboarding_url() is None
def test_get_relative_onboarding_url(self):
promoted_addon = PromotedAddon.objects.create(
addon=addon_factory(), group_id=promoted.SPONSORED.id
)
sub = PromotedSubscription.objects.filter(
promoted_addon=promoted_addon
).get()
assert sub.get_onboarding_url(absolute=False) == reverse(
"devhub.addons.onboarding_subscription",
args=[sub.promoted_addon.addon.slug],
)
def test_get_onboarding_url(self): def test_get_onboarding_url(self):
promoted_addon = PromotedAddon.objects.create( promoted_addon = PromotedAddon.objects.create(
addon=addon_factory(), group_id=promoted.SPONSORED.id addon=addon_factory(), group_id=promoted.SPONSORED.id
@ -88,4 +106,30 @@ class TestPromotedSubscription(TestCase):
promoted_addon=promoted_addon promoted_addon=promoted_addon
).get() ).get()
assert 'onboarding' in sub.get_onboarding_url() external_site_url = "http://example.org"
with override_settings(EXTERNAL_SITE_URL=external_site_url):
assert sub.get_onboarding_url() == "{}{}".format(
external_site_url,
reverse(
"devhub.addons.onboarding_subscription",
args=[sub.promoted_addon.addon.slug],
),
)
def test_stripe_checkout_completed(self):
sub = PromotedSubscription()
assert not sub.stripe_checkout_completed
sub.update(paid_at=datetime.datetime.now())
assert sub.stripe_checkout_completed
def test_stripe_checkout_cancelled(self):
sub = PromotedSubscription()
assert not sub.stripe_checkout_cancelled
sub.update(payment_cancelled_at=datetime.datetime.now())
assert sub.stripe_checkout_cancelled

Просмотреть файл

@ -0,0 +1,83 @@
import pytest
from unittest import mock
from django.test.utils import override_settings
from olympia.amo.templatetags.jinja_helpers import absolutify
from olympia.amo.tests import addon_factory
from olympia.amo.urlresolvers import reverse
from olympia.constants.promoted import SPONSORED, RECOMMENDED
from olympia.promoted.models import PromotedSubscription, PromotedAddon
from olympia.promoted.utils import (
create_stripe_checkout_session,
retrieve_stripe_checkout_session,
)
def test_retrieve_stripe_checkout_session():
stripe_session_id = "some stripe session id"
sub = PromotedSubscription(stripe_session_id=stripe_session_id)
with mock.patch(
"olympia.promoted.utils.stripe.checkout.Session.retrieve"
) as stripe_retrieve:
retrieve_stripe_checkout_session(subscription=sub)
stripe_retrieve.assert_called_once_with(stripe_session_id)
@override_settings(STRIPE_API_SPONSORED_PRICE_ID="sponsored-price-id")
def test_create_stripe_checkout_session():
addon = addon_factory()
promoted_addon = PromotedAddon.objects.create(
addon=addon, group_id=SPONSORED.id
)
sub = PromotedSubscription.objects.filter(
promoted_addon=promoted_addon
).get()
customer_email = "some-email@example.org"
fake_session = "fake session"
with mock.patch(
"olympia.promoted.utils.stripe.checkout.Session.create"
) as stripe_create:
stripe_create.return_value = fake_session
session = create_stripe_checkout_session(
subscription=sub, customer_email=customer_email
)
assert session == fake_session
stripe_create.assert_called_once_with(
payment_method_types=["card"],
mode="subscription",
cancel_url=absolutify(
reverse(
"devhub.addons.onboarding_subscription_cancel",
args=[addon.id],
)
),
success_url=absolutify(
reverse(
"devhub.addons.onboarding_subscription_success",
args=[addon.id],
)
),
line_items=[{"price": "sponsored-price-id", "quantity": 1}],
customer_email=customer_email,
)
def test_create_stripe_checkout_session_with_invalid_group_id():
promoted_addon = PromotedAddon.objects.create(
addon=addon_factory(), group_id=RECOMMENDED.id
)
# We create the promoted subscription because the promoted add-on above
# (recommended) does not create it automatically. This is because
# recommended add-ons should not have a subscription.
sub = PromotedSubscription.objects.create(promoted_addon=promoted_addon)
with pytest.raises(ValueError):
create_stripe_checkout_session(
subscription=sub, customer_email="doesnotmatter@example.org"
)

Просмотреть файл

@ -0,0 +1,54 @@
import stripe
from django.conf import settings
from olympia.amo.templatetags.jinja_helpers import absolutify
from olympia.amo.urlresolvers import reverse
from olympia.constants.promoted import SPONSORED, VERIFIED
def create_stripe_checkout_session(subscription, customer_email):
"""This function creates a Stripe Checkout Session object for a given
subscription. The `customer_email` is passed to Stripe to autofill the
input field on the Checkout page.
This function might raise if the promoted group isn't supported or the API
call has failed."""
price_id = {
SPONSORED.id: settings.STRIPE_API_SPONSORED_PRICE_ID,
VERIFIED.id: settings.STRIPE_API_VERIFIED_PRICE_ID,
}.get(subscription.promoted_addon.group_id)
if not price_id:
raise ValueError(
"No price ID for promoted group ID: {}.".format(
subscription.promoted_addon.group_id
)
)
stripe.api_key = settings.STRIPE_API_SECRET_KEY
return stripe.checkout.Session.create(
payment_method_types=["card"],
mode="subscription",
cancel_url=absolutify(
reverse(
"devhub.addons.onboarding_subscription_cancel",
args=[subscription.promoted_addon.addon_id],
)
),
success_url=absolutify(
reverse(
"devhub.addons.onboarding_subscription_success",
args=[subscription.promoted_addon.addon_id],
)
),
line_items=[{"price": price_id, "quantity": 1}],
customer_email=customer_email,
)
def retrieve_stripe_checkout_session(subscription):
"""This function returns a Stripe Checkout Session object or raises an
error when the session does not exist or the API call has failed."""
stripe.api_key = settings.STRIPE_API_SECRET_KEY
return stripe.checkout.Session.retrieve(subscription.stripe_session_id)

Просмотреть файл

@ -0,0 +1,11 @@
const checkoutButton = document.getElementById('checkout-button');
if (checkoutButton && checkoutButton.dataset) {
const stripe = Stripe(checkoutButton.dataset['publickey']);
checkoutButton.addEventListener('click', function () {
stripe.redirectToCheckout({
sessionId: checkoutButton.dataset['sessionid'],
});
});
}