* rm stripe/payment stuff

* rm some more unused constants

* revert the less auto-reformatting
This commit is contained in:
Andrew Williamson 2021-02-03 14:03:33 +00:00 коммит произвёл GitHub
Родитель bd327d16cc
Коммит 3b5810722f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
35 изменённых файлов: 4 добавлений и 2836 удалений

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

@ -41,7 +41,6 @@ using the API.
categories
collections
discovery
promoted
ratings
reviewers
scanners

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

@ -409,6 +409,7 @@ These are `v5` specific changes - `v4` changes apply also.
* 2021-01-28: dropped the pagination fields from the shelves api (it's still an object with a ``results`` property though). https://github.com/mozilla/addons-server/issues/16342
* 2021-01-28: made ``description_text`` in discovery endpoint a translated field in the response. (It was always localized, we just didn't return it as such). https://github.com/mozilla/addons-server/issues/8712
* 2021-02-04: dropped /shelves/sponsored endpoint https://github.com/mozilla/addons-server/issues/16390
* 2021-02-11: removed Stripe webhook endpoint https://github.com/mozilla/addons-server/issues/16391
.. _`#11380`: https://github.com/mozilla/addons-server/issues/11380/
.. _`#11379`: https://github.com/mozilla/addons-server/issues/11379/

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

@ -1,21 +0,0 @@
================
Promoted add-ons
================
.. note::
These APIs are subject to change at any time and are for internal use only.
--------------
Stripe Webhook
--------------
.. _stripe-webhook:
This endpoint receives `event notifications
<https://stripe.com/docs/webhooks>`_ from Stripe.
.. note::
Requests are signed by Stripe and verified by the server.
.. http:post:: /api/v5/promoted/stripe-webhook

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

@ -47,7 +47,6 @@ using the API.
categories
collections
discovery
promoted
ratings
reviewers
scanners

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

@ -1,21 +0,0 @@
================
Promoted add-ons
================
.. note::
These APIs are subject to change at any time and are for internal use only.
--------------
Stripe Webhook
--------------
.. _v4-stripe-webhook:
This endpoint receives `event notifications
<https://stripe.com/docs/webhooks>`_ from Stripe.
.. note::
Requests are signed by Stripe and verified by the server.
.. http:post:: /api/v4/promoted/stripe-webhook

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

@ -503,6 +503,3 @@ cached-property==1.5.2 \
django-jsonfield-backport==1.0.2 \
--hash=sha256:5574505967f6d7ada8c9269a5f873cfdca9812dc9502eee2b7a86be5c3798c76 \
--hash=sha256:0286dcc1c112389d52096f269eed83a77364ea2b349fe1777f5e4464c3c36fa9
stripe==2.55.1 \
--hash=sha256:fd98ae43b105e75cb4f1d23ba3d0c16b45e3957d432002398a2f75d083d606ce \
--hash=sha256:6b70e2cf87cfbe0cb891b725b690495bc3d34ab0d82545a5989ecd3b5fa83e2a

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

@ -1434,17 +1434,6 @@ class Addon(OnChangeMixin, ModelBase):
return PromotedTheme(addon=self, group_id=RECOMMENDED.id)
return None
@property
def promoted_subscription(self):
"""Returns a PromotedSubscription if it exists, None otherwise."""
from olympia.promoted.models import PromotedAddon, PromotedSubscription
try:
return self.promotedaddon.promotedsubscription
except (PromotedAddon.DoesNotExit, PromotedSubscription.DoesNotExist):
pass
return None
@cached_property
def tags_partitioned_by_developer(self):
"""Returns a tuple of developer tags and user tags for this addon."""

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

@ -30,7 +30,6 @@ v4_api_urls = [
re_path(r'^reviewers/', include('olympia.reviewers.api_urls')),
re_path(r'^', include('olympia.signing.urls')),
re_path(r'^', include(amo_api_patterns)),
re_path(r'^promoted/', include('olympia.promoted.api_urls')),
re_path(r'^scanner/', include('olympia.scanners.api_urls')),
]

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

@ -175,7 +175,6 @@ DJANGO_PERMISSIONS_MAPPING.update(
'hero.delete_secondaryheromodule': DISCOVERY_EDIT,
'promoted.view_promotedapproval': DISCOVERY_EDIT,
'promoted.delete_promotedapproval': DISCOVERY_EDIT,
'promoted.change_promotedsubscription': DISCOVERY_EDIT,
'reviewers.delete_reviewerscore': ADMIN_ADVANCED,
'scanners.add_scannerrule': ADMIN_SCANNERS_RULES_EDIT,
'scanners.change_scannerrule': ADMIN_SCANNERS_RULES_EDIT,

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

@ -20,7 +20,6 @@ _PromotedSuperClass = namedtuple(
'autograph_signing_states',
'can_primary_hero',
'can_be_selected_by_adzerk',
'require_subscription',
'immediate_approval',
],
defaults=(
@ -34,7 +33,6 @@ _PromotedSuperClass = namedtuple(
{}, # autograph_signing_states - should be a dict of App.short: state
False, # can_primary_hero - can be added to a primary hero shelf
False, # can_be_selected_by_adzerk
False, # require_subscription
False, # immediate_approval - will addon be auto-approved once added
),
)
@ -81,7 +79,6 @@ SPONSORED = PromotedClass(
},
can_primary_hero=True,
can_be_selected_by_adzerk=True,
require_subscription=True,
)
VERIFIED = PromotedClass(
@ -96,7 +93,6 @@ VERIFIED = PromotedClass(
applications.ANDROID.short: 'verified',
},
can_be_selected_by_adzerk=True,
require_subscription=True,
)
LINE = PromotedClass(
@ -154,10 +150,3 @@ PROMOTED_API_NAME_TO_IDS = {
**{p.api_name: [p.id] for p in PROMOTED_GROUPS if p},
**{BADGED_API_NAME: list({p.id for p in BADGED_GROUPS})},
}
BILLING_PERIOD_MONTHLY = 'monthly'
BILLING_PERIOD_YEARLY = 'yearly'
BILLING_PERIODS = (
(BILLING_PERIOD_MONTHLY, 'Monthly'),
(BILLING_PERIOD_YEARLY, 'Yearly'),
)

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

@ -1,90 +0,0 @@
{% 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 %}
{% if already_promoted %}
<p>
{% trans %}
You're done!
{% endtrans %}
</p>
{% else %}
<p>
{% if new_version_number %}
{% trans addon_name=addon.name, group_name=promoted_group.name %}
You're done! A new version <strong>{{ new_version_number }}</strong> of your add-on
<strong>{{ addon_name }}</strong> will be published as {{ group_name }}
and should become available shortly.
{% endtrans %}
{% if existing_version_pending %}
{{ _('A newer version of your add-on is currently pending review.') }}
{% endif %}
{{ _('If you have any questions, dont hesitate to contact us.') }}
{% else %}
{% trans %}
Your payment has been recorded, but there are currently no published versions of your add-on.
Please upload a new public version in order to complete this process.
{% endtrans %}
{% endif %}
</p>
{% endif %}
<p>
<a class="button" href="{{ addon.get_dev_url() }}">
{{ _('Manage add-on') }}
</a>
</p>
{% else %}
{% if stripe_checkout_cancelled %}
<div class="notification-box error">
<p>
{% trans email=settings.VERIFIED_ADDONS_EMAIL %}
There was an error while setting up payment for your add-on. If you're
experiencing problems with this process or you've changed your mind,
please contact us at
<a href="mailto:{{ email }}">{{ email }}</a>.
{% endtrans %}
</p>
</div>
{% else %}
<p>
{% trans %}
Thank you for joining the Promoted Add-ons Program!
{% endtrans %}
</p>
{% endif %}
<p>
{% trans addon_name=addon.name %}
Your add-on <strong>{{ addon_name }}</strong> 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 %}

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

@ -29,15 +29,6 @@
<li {% if url in request.path|urlencode %}class="selected"{% endif %}>
<a href="{{ url }}">{{ title }}</a></li>
{% endfor %}
{% if check_addon_ownership(request, addon) and addon.promoted_subscription and addon.promoted_subscription.is_active %}
<li class="stripe-customer-portal">
<form method="POST" action="{{ url('devhub.addons.subscription_customer_portal', addon.slug) }}">
{% csrf_token %}
<button class="link" type="submit">{{ _('Manage Billing on Stripe') }}</button>
</form>
</li>
{% endif %}
</ul>
<ul class="refinements">
{% if show_listed_fields %}

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

@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
import datetime
import json
import os
from unittest import mock
@ -25,9 +24,7 @@ from olympia.amo.tests import (
from olympia.amo.tests.test_helpers import get_image_path
from olympia.amo.urlresolvers import reverse
from olympia.amo.utils import image_size
from olympia.constants.promoted import VERIFIED
from olympia.devhub.forms import DescribeForm
from olympia.promoted.models import PromotedAddon
from olympia.tags.models import AddonTag, Tag
from olympia.users.models import UserProfile
from olympia.versions.models import VersionPreview
@ -745,63 +742,6 @@ class TestEditDescribeListed(BaseTestEditDescribe, L10nTestsMixin):
assert addon.description_id
assert addon.description == 'Sométhing descriptive.'
def test_no_manage_billing_when_no_subscription(self):
response = self.client.get(self.url)
assert pq(response.content)('.stripe-customer-portal').length == 0
def test_no_manage_billing_when_subscription_process_not_completed(self):
PromotedAddon.objects.create(addon=self.get_addon(), group_id=VERIFIED.id)
response = self.client.get(self.url)
assert pq(response.content)('.stripe-customer-portal').length == 0
def test_show_manage_billing_when_subscription_process_completed(self):
promoted = PromotedAddon.objects.create(
addon=self.get_addon(), group_id=VERIFIED.id
)
promoted.promotedsubscription.update(
checkout_completed_at=datetime.datetime.now()
)
assert self.get_addon().promoted_subscription
assert self.get_addon().promoted_subscription.stripe_checkout_completed
response = self.client.get(self.url)
assert pq(response.content)('.stripe-customer-portal').length == 1
def test_no_manage_billing_when_user_is_not_owner(self):
promoted = PromotedAddon.objects.create(
addon=self.get_addon(), group_id=VERIFIED.id
)
promoted.promotedsubscription.update(
checkout_completed_at=datetime.datetime.now()
)
assert self.get_addon().promoted_subscription
assert self.get_addon().promoted_subscription.stripe_checkout_completed
user_dev = UserProfile.objects.get(pk=999)
self.get_addon().addonuser_set.create(user=user_dev, role=amo.AUTHOR_ROLE_DEV)
self.client.logout()
self.client.login(email=user_dev.email)
response = self.client.get(self.url)
assert pq(response.content)('.stripe-customer-portal').length == 0
def test_no_manage_billing_when_subscription_has_been_cancelled(self):
promoted = PromotedAddon.objects.create(
addon=self.get_addon(), group_id=VERIFIED.id
)
promoted.promotedsubscription.update(
checkout_completed_at=datetime.datetime.now(),
cancelled_at=datetime.datetime.now(),
)
response = self.client.get(self.url)
assert pq(response.content)('.stripe-customer-portal').length == 0
class TestEditDescribeUnlisted(BaseTestEditDescribe, L10nTestsMixin):
listed = False

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

@ -1,568 +0,0 @@
import datetime
from unittest import mock
from waffle.testutils import override_switch
from olympia import amo
from olympia.amo.tests import (
TestCase,
addon_factory,
user_factory,
version_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 SubscriptionTestCase(TestCase):
def setUp(self):
super().setUp()
self.user = user_factory()
self.dev_user = user_factory()
self.addon = addon_factory(users=[self.user])
self.addon.addonuser_set.create(user=self.dev_user, role=amo.AUTHOR_ROLE_DEV)
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(SubscriptionTestCase):
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
def test_returns_403_for_non_owners(self):
self.client.logout()
self.client.login(email=self.dev_user.email)
response = self.client.get(self.url)
assert response.status_code == 403
def test_returns_404_when_subscription_has_been_cancelled(self):
self.subscription.update(
checkout_completed_at=datetime.datetime.now(),
cancelled_at=datetime.datetime.now(),
)
response = self.client.get(self.url)
assert response.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 = dict(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 = [
dict(id='session-id-1'),
dict(id='session-id-2'),
]
# Get the page.
queries = 36
with self.assertNumQueries(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 = dict(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 = dict(id='session-id')
self.subscription.update(checkout_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
)
assert b'Continue to Stripe Checkout' in response.content
assert b'Manage add-on' not 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):
# With an approved version but nothing in the session this is testing
# a subscription where the addon was already approved for promotion.
self.promoted_addon.approve_for_version(self.addon.current_version)
stripe_session_id = 'some session id'
retrieve_mock.return_value = dict(id=stripe_session_id)
self.subscription.update(
stripe_session_id=stripe_session_id,
checkout_completed_at=datetime.datetime.now(),
)
response = self.client.get(self.url)
assert b"You're done!" in response.content
assert b'Continue to Stripe Checkout' not in response.content
assert b'Manage add-on' in response.content
retrieve_mock.assert_called_with(self.subscription)
@mock.patch('olympia.devhub.views.retrieve_stripe_checkout_session')
def test_shows_request_to_upload_after_payment(self, retrieve_mock):
# If there isn't an approved version the developer needs to upload one.
stripe_session_id = 'some session id'
retrieve_mock.return_value = dict(id=stripe_session_id)
self.subscription.update(
stripe_session_id=stripe_session_id,
checkout_completed_at=datetime.datetime.now(),
)
response = self.client.get(self.url)
assert b"You're done!" not in response.content
assert b'Please upload a new public version' in response.content
assert b'Continue to Stripe Checkout' not in response.content
assert b'Manage add-on' 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,
checkout_completed_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
@mock.patch('olympia.devhub.views.retrieve_stripe_checkout_session')
def test_shows_confirmation_after_payment_already_approved(self, retrieve_mock):
stripe_session_id = 'session id'
retrieve_mock.return_value = dict(id=stripe_session_id)
self.subscription.update(
stripe_session_id=stripe_session_id,
checkout_completed_at=datetime.datetime.now(),
)
self.promoted_addon.approve_for_version(self.addon.current_version)
response = self.client.get(self.url)
assert b"You're done!" in response.content
retrieve_mock.assert_called_with(self.subscription)
class TestOnboardingSubscriptionSuccess(SubscriptionTestCase):
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
def test_returns_403_for_non_owners(self):
self.client.logout()
self.client.login(email=self.dev_user.email)
response = self.client.get(self.url)
assert response.status_code == 403
def test_returns_404_when_subscription_has_been_cancelled(self):
self.subscription.update(
checkout_completed_at=datetime.datetime.now(),
cancelled_at=datetime.datetime.now(),
)
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 = {
'id': 'session-id',
'payment_status': 'unpaid',
'subscription': None,
}
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):
self.subscription.promoted_addon.approve_for_version(
self.subscription.promoted_addon.addon.current_version
)
stripe_subscription_id = 'some-subscription-id'
retrieve_mock.return_value = {
'id': 'session-id',
'payment_status': 'paid',
'subscription': stripe_subscription_id,
}
assert not self.subscription.checkout_completed_at
self.client.get(self.url)
self.subscription.refresh_from_db()
checkout_completed_at = self.subscription.checkout_completed_at
assert checkout_completed_at is not None
assert self.subscription.stripe_subscription_id == stripe_subscription_id
self.client.get(self.url)
self.subscription.refresh_from_db()
# Make sure we don't update this date again.
assert self.subscription.checkout_completed_at == checkout_completed_at
assert not self.subscription.checkout_cancelled_at
assert self.subscription.stripe_subscription_id == stripe_subscription_id
@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 = {
'id': 'session-id',
'payment_status': 'paid',
'subscription': 'some-subscription-id',
}
self.subscription.promoted_addon.approve_for_version(
self.subscription.promoted_addon.addon.current_version
)
self.subscription.update(checkout_cancelled_at=datetime.datetime.now())
self.client.get(self.url)
self.subscription.refresh_from_db()
assert not self.subscription.checkout_cancelled_at
@mock.patch('olympia.devhub.views.retrieve_stripe_checkout_session')
def test_current_version_is_approved_after_success(self, retrieve_mock):
retrieve_mock.return_value = {
'id': 'session-id',
'payment_status': 'paid',
'subscription': 'some-subscription-id',
}
self.subscription.promoted_addon.addon.current_version.update(version='123')
assert not self.subscription.promoted_addon.addon.promoted_group()
with mock.patch('olympia.lib.crypto.tasks.sign_addons') as sign_mock:
response = self.client.get(self.url, follow=True)
sign_mock.assert_called()
self.subscription.refresh_from_db()
assert self.subscription.promoted_addon.addon.promoted_group() == VERIFIED
assert b'123.1-signed' in response.content
assert b'currently pending review' not in response.content
# the message is gone the second time
response = self.client.get(
reverse(TestOnboardingSubscription.url_name, args=[self.addon.slug]),
follow=True,
)
assert b"You're done" in response.content
assert b'123.1-signed' not in response.content
@mock.patch('olympia.devhub.views.retrieve_stripe_checkout_session')
def test_current_version_is_approved_pending_version(self, retrieve_mock):
"""Same as test_current_version_is_approved_after_success but when
there is a pending version too."""
retrieve_mock.return_value = {
'id': 'session-id',
'payment_status': 'paid',
'subscription': 'some-subscription-id',
}
self.subscription.promoted_addon.addon.current_version.update(version='123')
# add a pending version we'll highlight too
version_factory(
addon=self.addon, file_kw={'status': amo.STATUS_AWAITING_REVIEW}
)
assert not self.subscription.promoted_addon.addon.promoted_group()
with mock.patch('olympia.lib.crypto.tasks.sign_addons') as sign_mock:
response = self.client.get(self.url, follow=True)
sign_mock.assert_called()
self.subscription.refresh_from_db()
assert self.subscription.promoted_addon.addon.promoted_group() == VERIFIED
assert b'123.1-signed' in response.content
assert b'currently pending review' in response.content
# the message is gone the second time
response = self.client.get(
reverse(TestOnboardingSubscription.url_name, args=[self.addon.slug]),
follow=True,
)
assert b"You're done" in response.content
assert b'123.1-signed' not in response.content
@mock.patch('olympia.devhub.views.retrieve_stripe_checkout_session')
def test_version_isnt_resigned_if_already_approved(self, retrieve_mock):
retrieve_mock.return_value = {
'id': 'session-id',
'payment_status': 'paid',
'subscription': 'some-subscription-id',
}
promo = self.subscription.promoted_addon
promo.approve_for_version(promo.addon.current_version)
assert promo.addon.promoted_group() == VERIFIED # approved already
with mock.patch('olympia.lib.crypto.tasks.sign_addons') as sign_mock:
self.client.get(self.url)
sign_mock.assert_not_called() # no resigning needed
self.subscription.refresh_from_db()
assert self.subscription.checkout_completed_at
assert promo.addon.promoted_group() == VERIFIED # still approved
class TestOnboardingSubscriptionCancel(SubscriptionTestCase):
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
def test_returns_403_for_non_owners(self):
self.client.logout()
self.client.login(email=self.dev_user.email)
response = self.client.get(self.url)
assert response.status_code == 403
def test_returns_404_when_subscription_has_been_cancelled(self):
self.subscription.update(
checkout_completed_at=datetime.datetime.now(),
cancelled_at=datetime.datetime.now(),
)
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 = dict(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 = dict(id=stripe_session_id)
assert not self.subscription.checkout_cancelled_at
self.client.get(self.url)
self.subscription.refresh_from_db()
assert self.subscription.checkout_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 = dict(id='session-id')
self.subscription.update(checkout_completed_at=datetime.datetime.now())
assert not self.subscription.checkout_cancelled_at
self.client.get(self.url)
self.subscription.refresh_from_db()
assert not self.subscription.checkout_cancelled_at
@override_switch('enable-subscriptions-for-promoted-addons', active=True)
class TestSubscriptionCustomerPortal(SubscriptionTestCase):
url_name = 'devhub.addons.subscription_customer_portal'
@override_switch('enable-subscriptions-for-promoted-addons', active=False)
def test_returns_404_when_switch_is_disabled(self):
assert self.client.post(self.url).status_code == 404
def test_rejects_get_requests(self):
assert self.client.get(self.url).status_code == 405
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.post(url).status_code == 404
@mock.patch('olympia.devhub.views.retrieve_stripe_subscription')
def test_get_returns_404_when_subscription_not_found(self, retrieve_mock):
retrieve_mock.side_effect = Exception('stripe error')
response = self.client.post(self.url)
assert response.status_code == 404
def test_returns_403_for_non_owners(self):
self.client.logout()
self.client.login(email=self.dev_user.email)
response = self.client.post(self.url)
assert response.status_code == 403
def test_returns_404_when_subscription_has_been_cancelled(self):
self.subscription.update(
checkout_completed_at=datetime.datetime.now(),
cancelled_at=datetime.datetime.now(),
)
response = self.client.post(self.url)
assert response.status_code == 404
@mock.patch('olympia.devhub.views.retrieve_stripe_subscription')
@mock.patch('olympia.devhub.views.create_stripe_customer_portal')
def test_redirects_to_stripe_customer_portal(self, create_mock, retrieve_mock):
customer_id = 'some-customer-id'
portal_url = 'https://stripe-portal.example.org'
retrieve_mock.return_value = {'customer': customer_id}
create_mock.return_value = {'url': portal_url}
response = self.client.post(self.url)
assert response.status_code == 302
assert response['Location'] == portal_url
retrieve_mock.assert_called_once_with(self.subscription)
create_mock.assert_called_once_with(customer_id=customer_id, addon=self.addon)
@mock.patch('olympia.devhub.views.retrieve_stripe_subscription')
@mock.patch('olympia.devhub.views.create_stripe_customer_portal')
def test_get_returns_500_when_create_has_failed(self, create_mock, retrieve_mock):
retrieve_mock.return_value = {'customer': 'customer-id'}
create_mock.side_effect = Exception('stripe error')
response = self.client.post(self.url)
assert response.status_code == 500

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

@ -28,26 +28,6 @@ detail_patterns = [
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'^subscription/customer-portal$',
views.subscription_customer_portal,
name='devhub.addons.subscription_customer_portal',
),
re_path(
r'^upload_preview$',
views.upload_image,

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

@ -53,13 +53,6 @@ from olympia.devhub.utils import (
)
from olympia.files.models import File, FileUpload
from olympia.files.utils import parse_addon
from olympia.promoted.models import PromotedSubscription
from olympia.promoted.utils import (
create_stripe_checkout_session,
create_stripe_customer_portal,
retrieve_stripe_checkout_session,
retrieve_stripe_subscription,
)
from olympia.reviewers.forms import PublicWhiteboardForm
from olympia.reviewers.models import Whiteboard
from olympia.reviewers.templatetags.code_manager import code_manager_url
@ -1982,182 +1975,3 @@ def logout(request):
logout_user(request, 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__addon')
subscription = get_object_or_404(qs, promoted_addon__addon=addon)
if subscription.is_active is False:
raise http.Http404()
return subscription
@dev_required(owner_for_get=True)
@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)
new_version_number = request.session.pop('resigned_version_string', None)
already_promoted = not new_version_number and sub.promoted_addon.has_approvals
existing_version_pending = (
new_version_number
and addon.versions.filter(
channel=amo.RELEASE_CHANNEL_LISTED, files__status=amo.STATUS_AWAITING_REVIEW
).exists()
)
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,
'already_promoted': already_promoted,
'new_version_number': new_version_number,
'existing_version_pending': existing_version_pending,
}
return render(request, 'devhub/addons/onboarding_subscription.html', data)
@dev_required(owner_for_get=True)
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.checkout_completed_at:
# When the user has completed the Stripe Checkout process, we record
# this event.
#
# We reset `checkout_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(
checkout_cancelled_at=None,
checkout_completed_at=datetime.datetime.now(),
stripe_subscription_id=session['subscription'],
)
log.info('PromotedSubscription %s has been completed.', sub.pk)
if not sub.promoted_addon.has_approvals:
# we get what the new version string would be after resigning
request.session[
'resigned_version_string'
] = sub.promoted_addon.get_resigned_version_number()
sub.promoted_addon.approve_for_addon()
return redirect(reverse('devhub.addons.onboarding_subscription', args=[addon.slug]))
@dev_required(owner_for_get=True)
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(checkout_cancelled_at=datetime.datetime.now())
log.info('PromotedSubscription %s has been cancelled.', sub.pk)
return redirect(reverse('devhub.addons.onboarding_subscription', args=[addon.id]))
@dev_required(owner_for_post=True)
@post_required
def subscription_customer_portal(request, addon_id, addon):
sub = get_promoted_subscription_or_404(addon=addon)
try:
stripe_sub = retrieve_stripe_subscription(sub)
except Exception:
log.exception(
'error while trying to retrieve a stripe subscription for '
'PromotedSubscription %s (customer_portal).',
sub.pk,
)
raise http.Http404()
try:
portal = create_stripe_customer_portal(
customer_id=stripe_sub['customer'], addon=addon
)
except Exception:
log.exception(
'error while creating a stripe customer portal for '
'PromotedSubscription %s.',
sub.pk,
)
return http.HttpResponseServerError()
return redirect(portal['url'])

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

@ -1072,11 +1072,6 @@ CELERY_TASK_ROUTES = {
'olympia.devhub.tasks.validate_file': {'queue': 'devhub'},
'olympia.devhub.tasks.validate_upload': {'queue': 'devhub'},
'olympia.files.tasks.repack_fileupload': {'queue': 'devhub'},
'olympia.promoted.tasks.on_stripe_charge_failed': {'queue': 'devhub'},
'olympia.promoted.tasks.on_stripe_charge_succeeded': {'queue': 'devhub'},
'olympia.promoted.tasks.on_stripe_customer_subscription_deleted': {
'queue': 'devhub'
},
'olympia.scanners.tasks.run_customs': {'queue': 'devhub'},
'olympia.scanners.tasks.run_wat': {'queue': 'devhub'},
'olympia.scanners.tasks.run_yara': {'queue': 'devhub'},
@ -1800,21 +1795,3 @@ ADZERK_URL = f'https://e-{ADZERK_NETWORK_ID}.adzerk.net/api/v2'
ADZERK_IMPRESSION_TIMEOUT = 60 # seconds
ADZERK_EVENT_URL = f'https://e-{ADZERK_NETWORK_ID}.adzerk.net/'
ADZERK_EVENT_TIMEOUT = 60 * 60 * 24 # seconds
# Subscription
STRIPE_DASHBOARD_URL = 'https://dashboard.stripe.com'
STRIPE_API_SECRET_KEY = env('STRIPE_API_SECRET_KEY', default=None)
STRIPE_API_PUBLIC_KEY = env('STRIPE_API_PUBLIC_KEY', default=None)
STRIPE_API_WEBHOOK_SECRET = env('STRIPE_API_WEBHOOK_SECRET', default=None)
STRIPE_API_VERIFIED_MONTHLY_PRICE_ID = env(
'STRIPE_API_VERIFIED_MONTHLY_PRICE_ID', default=None
)
STRIPE_API_VERIFIED_YEARLY_PRICE_ID = env(
'STRIPE_API_VERIFIED_YEARLY_PRICE_ID', default=None
)
STRIPE_API_SPONSORED_MONTHLY_PRICE_ID = env(
'STRIPE_API_SPONSORED_MONTHLY_PRICE_ID', default=None
)
STRIPE_API_SPONSORED_YEARLY_PRICE_ID = env(
'STRIPE_API_SPONSORED_YEARLY_PRICE_ID', default=None
)

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

@ -1,15 +1,13 @@
from django.conf import settings
from django.contrib import admin
from django.db.models import Prefetch
from django.forms.models import modelformset_factory
from django.utils.html import format_html
from olympia.addons.models import Addon
from olympia.hero.admin import PrimaryHeroInline
from olympia.versions.models import Version
from .forms import AdminBasePromotedApprovalFormSet
from .models import PromotedApproval, PromotedSubscription
from .models import PromotedApproval
class PromotedApprovalInlineChecks(admin.checks.InlineModelAdminChecks):
@ -20,68 +18,6 @@ class PromotedApprovalInlineChecks(admin.checks.InlineModelAdminChecks):
return []
class PromotedSubscriptionInline(admin.StackedInline):
model = PromotedSubscription
view_on_site = False
extra = 0 # No extra form should be added...
max_num = 1 # ...and we expect up to one form.
fields = (
'onboarding_rate',
'onboarding_period',
'onboarding_url',
'link_visited_at',
'checkout_cancelled_at',
'checkout_completed_at',
'cancelled_at',
'stripe_information',
)
readonly_fields = (
'onboarding_url',
'link_visited_at',
'checkout_cancelled_at',
'checkout_completed_at',
'cancelled_at',
'stripe_information',
)
def get_readonly_fields(self, request, obj=None):
readonly_fields = self.readonly_fields
onboarding_fields = ('onboarding_rate', 'onboarding_period')
if (
obj
and hasattr(obj, 'promotedsubscription')
and obj.promotedsubscription.stripe_checkout_completed
):
readonly_fields = onboarding_fields + readonly_fields
return readonly_fields
def has_add_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return False
def onboarding_url(self, obj):
return format_html('<pre>{}</pre>', obj.get_onboarding_url())
onboarding_url.short_description = 'Onboarding URL'
def stripe_information(self, obj):
if not obj or not obj.stripe_subscription_id:
return '-'
stripe_sub_url = '/'.join(
[settings.STRIPE_DASHBOARD_URL, 'subscriptions', obj.stripe_subscription_id]
)
return format_html(
'<a href="{}">View subscription on Stripe</a>', stripe_sub_url
)
stripe_information.short_description = 'Stripe information'
class PromotedApprovalInline(admin.TabularInline):
model = PromotedApproval
extra = 0
@ -133,10 +69,7 @@ class PromotedAddonAdmin(admin.ModelAdmin):
raw_id_fields = ('addon',)
fields = ('addon', 'group_id', 'application_id')
list_filter = ('group_id', 'application_id')
inlines = (PromotedApprovalInline, PrimaryHeroInline, PromotedSubscriptionInline)
class Media:
js = ('js/admin/promotedaddon.js',)
inlines = (PromotedApprovalInline, PrimaryHeroInline)
@classmethod
def _transformer(self, objs):

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

@ -1,12 +0,0 @@
from django.urls import re_path
from . import views
urlpatterns = [
re_path(
r'^stripe-webhook$',
views.stripe_webhook,
name='promoted.stripe_webhook',
),
]

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

@ -1,18 +1,14 @@
from django.conf import settings
from django.db import models
from django.dispatch import receiver
from urllib.parse import urljoin
from olympia.addons.models import Addon
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.promoted import (
NOT_PROMOTED,
PRE_REVIEW_GROUPS,
PROMOTED_GROUPS,
PROMOTED_GROUPS_BY_ID,
BILLING_PERIODS,
)
from olympia.versions.models import Version
@ -97,18 +93,6 @@ class PromotedAddon(ModelBase):
except AttributeError:
pass
@property
def has_pending_subscription(self):
"""Checks if there is a subscription needed for this promotion, and if
so, if it has been completed. Returns True if there is outstanding
payment needed."""
return (
self.group.require_subscription
and (subscr := getattr(self, 'promotedsubscription', None))
and not subscr.is_active
and not self.has_approvals
)
def approve_for_addon(self):
"""This sets up the addon as approved for the current promoted group.
@ -136,10 +120,7 @@ class PromotedAddon(ModelBase):
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if self.group.require_subscription:
if not hasattr(self, 'promotedsubscription'):
PromotedSubscription.objects.create(promoted_addon=self)
elif (
if (
self.group.immediate_approval
and self.approved_applications != self.all_applications
):
@ -214,93 +195,3 @@ def update_es_for_promoted(sender, instance, **kw):
)
def update_es_for_promoted_approval(sender, instance, **kw):
update_es_for_promoted(sender=sender, instance=instance.version, **kw)
class PromotedSubscription(ModelBase):
promoted_addon = models.OneToOneField(
PromotedAddon,
on_delete=models.CASCADE,
null=False,
)
link_visited_at = models.DateTimeField(
null=True,
help_text=(
'This date is set when the developer has visited the onboarding page.'
),
)
# This field should only be used for the Stripe Checkout process, use
# `stripe_subscription_id` when interacting with the API.
stripe_session_id = models.CharField(default=None, null=True, max_length=100)
stripe_subscription_id = models.CharField(default=None, null=True, max_length=100)
checkout_cancelled_at = models.DateTimeField(
null=True,
help_text=(
'This date is set when the developer has cancelled the initial '
'payment process.'
),
)
checkout_completed_at = models.DateTimeField(
null=True,
help_text=(
'This date is set when the developer has successfully completed '
'the initial payment process.'
),
)
cancelled_at = models.DateTimeField(
null=True,
help_text='This date is set when the subscription has been cancelled.',
)
onboarding_rate = models.PositiveIntegerField(
default=None,
blank=True,
null=True,
help_text=(
'If set, this rate will be used to charge the developer for this'
' subscription. The value should be a non-negative integer in'
' cents. The default rate configured in Stripe for the promoted'
' group will be used otherwise.'
),
)
onboarding_period = models.CharField(
choices=BILLING_PERIODS,
max_length=10,
blank=True,
null=True,
help_text=(
'If set, this billing period will be used for this subscription.'
' The default period configured in Stripe for the promoted group'
'will be used otherwise.'
),
)
def __str__(self):
return f'Subscription for {self.promoted_addon}'
def get_onboarding_url(self, absolute=True):
if not self.id:
return None
url = reverse(
'devhub.addons.onboarding_subscription',
args=[self.promoted_addon.addon.slug],
add_prefix=False,
)
if absolute:
url = urljoin(settings.EXTERNAL_SITE_URL, url)
return url
@property
def stripe_checkout_completed(self):
return bool(self.checkout_completed_at)
@property
def stripe_checkout_cancelled(self):
return bool(self.checkout_cancelled_at)
@property
def is_active(self):
"""A subscription can only be active when it has started so we return a
boolean value only in this case. None is returned otheriwse."""
if self.stripe_checkout_completed:
return not self.cancelled_at
return None

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

@ -1,205 +0,0 @@
import datetime
import olympia.core.logger
from django.conf import settings
from django.template import loader
from olympia.amo.celery import task
from olympia.amo.decorators import use_primary_db
from olympia.amo.templatetags.jinja_helpers import absolutify
from olympia.amo.urlresolvers import reverse
from olympia.amo.utils import send_mail
from olympia.constants.promoted import NOT_PROMOTED
from .models import PromotedSubscription
from .utils import retrieve_stripe_subscription_for_invoice
log = olympia.core.logger.getLogger('z.promoted.task')
@task
def on_stripe_charge_failed(event):
event_id = event.get('id')
event_type = event.get('type')
if event_type != 'charge.failed':
log.error('invalid event "%s" received (event_id=%s).', event_type, event_id)
return
# This event should contain a `charge` object.
charge = event.get('data', {}).get('object')
if not charge:
log.error('no charge object in event (event_id=%s).', event_id)
return
charge_id = charge['id']
log.info('received "%s" event with charge_id=%s.', event_type, charge_id)
# It is possible that a `charge` object isn't bound to an invoice, e.g.,
# when a Stripe admin manually attempts to charge a customer.
if not charge['invoice']:
log.error('"charge.failed" events without an invoice are not supported')
return
try:
# Retrieve the `PromotedSubscription` for the current `charge` object.
# We need to retrieve the Stripe Subscription first (via the Stripe
# Invoice because a Stripe Charge isn't directly linked to a
# Subscription).
stripe_sub = retrieve_stripe_subscription_for_invoice(
invoice_id=charge['invoice']
)
subscription_id = stripe_sub['id']
log.debug(
'retrieved stripe subscription with subscription_id=%s.',
subscription_id,
)
sub = PromotedSubscription.objects.get(stripe_subscription_id=subscription_id)
except PromotedSubscription.DoesNotExist:
log.error(
'received a "%s" event (event_id=%s) for a non-existent'
' promoted subscription (subscription_id=%s).',
event_type,
event_id,
subscription_id,
)
return
except Exception:
log.exception(
'error while trying to retrieve a subscription for "%s" '
'event with event_id=%s and charge_id=%s.',
event_type,
event_id,
charge_id,
)
return
addon = sub.promoted_addon.addon
# Create the Stripe URL pointing to the Stripe subscription.
stripe_sub_url = settings.STRIPE_DASHBOARD_URL
if not stripe_sub['livemode']:
stripe_sub_url = stripe_sub_url + '/test'
stripe_sub_url = f'{stripe_sub_url}/subscriptions/{subscription_id}'
subject = f'Stripe payment failure detected for add-on: {addon.name}'
template = loader.get_template('promoted/emails/stripe_charge_failed.txt')
context = {
'addon_url': absolutify(addon.get_detail_url()),
'admin_url': absolutify(
reverse(
'admin:discovery_promotedaddon_change',
args=[sub.promoted_addon_id],
)
),
'stripe_sub_url': stripe_sub_url,
}
send_mail(
subject,
template.render(context),
from_email=settings.ADDONS_EMAIL,
recipient_list=[settings.VERIFIED_ADDONS_EMAIL],
)
@task
@use_primary_db
def on_stripe_customer_subscription_deleted(event):
event_id = event.get('id')
event_type = event.get('type')
if event_type != 'customer.subscription.deleted':
log.error('invalid event "%s" received (event_id=%s).', event_type, event_id)
return
# This event should contain a `subscription` object.
subscription = event.get('data', {}).get('object')
if not subscription:
log.error('no subscription object in event (event_id=%s).', event_id)
return
subscription_id = subscription['id']
log.info(
'received "%s" event with subscription_id=%s.',
event_type,
subscription_id,
)
try:
sub = PromotedSubscription.objects.get(stripe_subscription_id=subscription_id)
except PromotedSubscription.DoesNotExist:
log.exception(
'received a "%s" event (event_id=%s) for a non-existent '
'promoted subscription (subscription_id=%s).',
event_type,
event_id,
subscription_id,
)
return
if sub.cancelled_at:
log.info(
'PromotedSubscription %s already cancelled, ignoring event '
'(event_id=%s).',
sub.id,
event_id,
)
return
sub.update(
cancelled_at=datetime.datetime.fromtimestamp(subscription['canceled_at'])
)
log.info('PromotedSubscription %s has been marked as cancelled.', sub.id)
# TODO: this should trigger resigning somehow, see:
# https://github.com/mozilla/addons-server/issues/15921
sub.promoted_addon.update(group_id=NOT_PROMOTED.id)
log.info(
'Addon %s is not promoted anymore (PromotedAddon=%s and '
'PromotedSubscription=%s).',
sub.promoted_addon.addon_id,
sub.promoted_addon.id,
sub.id,
)
@task
def on_stripe_charge_succeeded(event):
event_id = event.get('id')
event_type = event.get('type')
if event_type != 'charge.succeeded':
log.error('invalid event "%s" received (event_id=%s).', event_type, event_id)
return
# This event should contain a `charge` object.
charge = event.get('data', {}).get('object')
if not charge:
log.error('no charge object in event (event_id=%s).', event_id)
return
charge_id = charge['id']
log.info('received "%s" event with charge_id=%s.', event_type, charge_id)
# Create the Stripe URL pointing to the Stripe payment.
stripe_payment_url = settings.STRIPE_DASHBOARD_URL
if not charge.get('livemode'):
stripe_payment_url = stripe_payment_url + '/test'
stripe_payment_url = f"{stripe_payment_url}/payments/{charge.get('payment_intent')}"
subject = 'Stripe payment succeeded'
template = loader.get_template('promoted/emails/stripe_charge_succeeded.txt')
context = {
'stripe_payment_url': stripe_payment_url,
}
send_mail(
subject,
template.render(context),
from_email=settings.ADDONS_EMAIL,
recipient_list=[settings.VERIFIED_ADDONS_EMAIL],
)

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

@ -1,11 +0,0 @@
{% block content %}
Hello,
We received a notification from Stripe about a payment failure for the following add-on: {{ addon_url }}
The promoted subscription/add-on is available at: {{ admin_url }}
The Stripe subscription is available at: {{ stripe_sub_url }}
Yours.
{% endblock %}

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

@ -1,7 +0,0 @@
{% block content %}
Hello,
We received a notification from Stripe about a successful payment, see: {{ stripe_payment_url }}
Yours.
{% endblock %}

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

@ -1,7 +1,3 @@
import datetime
from pyquery import PyQuery as pq
from olympia import amo
from olympia.amo.tests import addon_factory, TestCase, user_factory, version_factory
from olympia.amo.tests.test_helpers import get_uploaded_file
@ -10,7 +6,6 @@ from olympia.constants.promoted import (
LINE,
RECOMMENDED,
VERIFIED,
SPONSORED,
NOT_PROMOTED,
)
from olympia.hero.models import PrimaryHero, PrimaryHeroImage
@ -57,18 +52,6 @@ class TestPromotedAddonAdmin(TestCase):
'primaryhero-__prefix__-promoted_addon': item_id,
}
def _get_promotedsubscriptionform(self, item_id):
return {
'promotedsubscription-TOTAL_FORMS': '1',
'promotedsubscription-INITIAL_FORMS': '0',
'promotedsubscription-MIN_NUM_FORMS': '0',
'promotedsubscription-MAX_NUM_FORMS': '1',
'promotedsubscription-0-onboarding_rate': '',
'promotedsubscription-0-onboarding_period': '',
'promotedsubscription-0-id': '',
'promotedsubscription-0-promoted_addon': item_id,
}
def test_can_see_in_admin_with_discovery_edit(self):
user = user_factory(email='someone@mozilla.com')
self.grant_permission(user, 'Discovery:Edit')
@ -158,7 +141,6 @@ class TestPromotedAddonAdmin(TestCase):
detail_url,
dict(
self._get_approval_form(item, approvals),
**self._get_promotedsubscriptionform(''),
**self._get_heroform(''),
**{
'group_id': LINE.id, # change the group
@ -181,7 +163,6 @@ class TestPromotedAddonAdmin(TestCase):
detail_url,
dict(
self._get_approval_form(item, approvals),
**self._get_promotedsubscriptionform(''),
**self._get_heroform(''),
**{
'form-0-DELETE': 'on', # delete the latest approval
@ -212,7 +193,6 @@ class TestPromotedAddonAdmin(TestCase):
detail_url,
dict(
self._get_approval_form(item, [approval]),
**self._get_promotedsubscriptionform(''),
**self._get_heroform(''),
**{
'form-0-group_id': str(LINE.id),
@ -230,7 +210,6 @@ class TestPromotedAddonAdmin(TestCase):
detail_url,
dict(
self._get_approval_form(item, [approval]),
**self._get_promotedsubscriptionform(''),
**self._get_heroform(''),
**{
'form-1-id': '',
@ -348,7 +327,6 @@ class TestPromotedAddonAdmin(TestCase):
add_url,
dict(
self._get_approval_form(None, []),
**self._get_promotedsubscriptionform(''),
**self._get_heroform(''),
**{
'addon': str(addon.id),
@ -390,7 +368,6 @@ class TestPromotedAddonAdmin(TestCase):
add_url,
dict(
self._get_approval_form(None, []),
**self._get_promotedsubscriptionform(''),
**self._get_heroform(''),
**{
'addon': str(addon.id),
@ -444,7 +421,6 @@ class TestPromotedAddonAdmin(TestCase):
self.detail_url,
dict(
self._get_heroform(item.pk),
**self._get_promotedsubscriptionform(item.pk),
**self._get_approval_form(item, []),
**{
'primaryhero-INITIAL_FORMS': '1',
@ -484,7 +460,6 @@ class TestPromotedAddonAdmin(TestCase):
self.detail_url,
dict(
self._get_heroform(item.pk),
**self._get_promotedsubscriptionform(item.pk),
**self._get_approval_form(item, []),
**{
'primaryhero-0-gradient_color': '#054096',
@ -546,65 +521,6 @@ class TestPromotedAddonAdmin(TestCase):
# The approval *won't* have been deleted though
assert PromotedApproval.objects.filter().exists()
def test_hides_subscription_when_group_is_not_applicable(self):
item = PromotedAddon.objects.create(
addon=addon_factory(), group_id=RECOMMENDED.id
)
user = user_factory(email='someone@mozilla.com')
self.grant_permission(user, 'Discovery:Edit')
self.client.login(email=user.email)
response = self.client.get(reverse(self.detail_url_name, args=(item.pk,)))
assert response.status_code == 200
assert b'Onboarding URL' not in response.content
assert b'Stripe information' not in response.content
def test_shows_subscription_when_group_is_verified(self):
item = PromotedAddon.objects.create(addon=addon_factory(), group_id=VERIFIED.id)
user = user_factory(email='someone@mozilla.com')
self.grant_permission(user, 'Discovery:Edit')
self.client.login(email=user.email)
response = self.client.get(reverse(self.detail_url_name, args=(item.pk,)))
assert response.status_code == 200
assert b'Onboarding URL' in response.content
def test_shows_subscription_when_group_is_sponsored(self):
item = PromotedAddon.objects.create(
addon=addon_factory(), group_id=SPONSORED.id
)
user = user_factory(email='someone@mozilla.com')
self.grant_permission(user, 'Discovery:Edit')
self.client.login(email=user.email)
response = self.client.get(reverse(self.detail_url_name, args=(item.pk,)))
assert response.status_code == 200
assert b'Onboarding URL' in response.content
assert b'Stripe information' in response.content
assert b'View subscription on Stripe' not in response.content
def test_shows_link_to_stripe_subscription(self):
item = PromotedAddon.objects.create(
addon=addon_factory(), group_id=SPONSORED.id
)
stripe_subscription_id = 'some-id'
item.promotedsubscription.update(stripe_subscription_id=stripe_subscription_id)
user = user_factory(email='someone@mozilla.com')
self.grant_permission(user, 'Discovery:Edit')
self.client.login(email=user.email)
response = self.client.get(reverse(self.detail_url_name, args=(item.pk,)))
assert response.status_code == 200
content = response.content.decode('utf-8')
assert 'Onboarding URL' in content
assert 'Stripe information' in content
assert 'View subscription on Stripe' in content
assert f'/subscriptions/{stripe_subscription_id}' in content
def test_updates_not_promoted_to_verified(self):
item = PromotedAddon.objects.create(
addon=addon_factory(), group_id=NOT_PROMOTED.id
@ -618,7 +534,6 @@ class TestPromotedAddonAdmin(TestCase):
detail_url,
dict(
self._get_approval_form(item, []),
**self._get_promotedsubscriptionform(item.id),
**self._get_heroform(item.id),
**{'group_id': VERIFIED.id}, # change group
),
@ -628,41 +543,3 @@ class TestPromotedAddonAdmin(TestCase):
assert response.status_code == 200
assert item.group_id == VERIFIED.id
def test_cannot_update_onboarding_rate_when_payment_completed(self):
item = PromotedAddon.objects.create(
addon=addon_factory(),
group_id=SPONSORED.id,
)
# Pretend the subscription is active.
item.promotedsubscription.update(checkout_completed_at=datetime.datetime.now())
assert item.promotedsubscription.stripe_checkout_completed
user = user_factory(email='someone@mozilla.com')
self.grant_permission(user, 'Discovery:Edit')
self.client.login(email=user.email)
response = self.client.get(reverse(self.detail_url_name, args=(item.pk,)))
assert response.status_code == 200
doc = pq(response.content)
assert doc('.field-onboarding_rate').length == 1
assert doc('.field-onboarding_rate .readonly').length == 1
def test_cannot_update_onboarding_period_when_payment_completed(self):
item = PromotedAddon.objects.create(
addon=addon_factory(),
group_id=SPONSORED.id,
)
# Pretend the subscription is active.
item.promotedsubscription.update(checkout_completed_at=datetime.datetime.now())
assert item.promotedsubscription.stripe_checkout_completed
user = user_factory(email='someone@mozilla.com')
self.grant_permission(user, 'Discovery:Edit')
self.client.login(email=user.email)
response = self.client.get(reverse(self.detail_url_name, args=(item.pk,)))
assert response.status_code == 200
doc = pq(response.content)
assert doc('.field-onboarding_period').length == 1
assert doc('.field-onboarding_period .readonly').length == 1

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

@ -1,18 +1,14 @@
import datetime
from unittest import mock
from django.conf import settings
from django.test.utils import override_settings
from olympia import amo, core
from olympia.activity.models import ActivityLog
from olympia.amo.tests import addon_factory, TestCase, user_factory
from olympia.amo.urlresolvers import reverse
from olympia.constants import applications, promoted
from olympia.promoted.models import (
PromotedAddon,
PromotedApproval,
PromotedSubscription,
)
@ -69,27 +65,6 @@ class TestPromotedAddon(TestCase):
applications.ANDROID,
]
def test_creates_a_subscription_when_group_should_have_one(self):
assert PromotedSubscription.objects.count() == 0
promoted_addon = PromotedAddon.objects.create(
addon=addon_factory(), group_id=promoted.SPONSORED.id
)
assert PromotedSubscription.objects.count() == 1
assert PromotedSubscription.objects.all()[0].promoted_addon == promoted_addon
# Do not create a subscription twice.
promoted_addon.save()
assert PromotedSubscription.objects.count() == 1
def test_no_subscription_created_when_group_should_not_have_one(self):
assert PromotedSubscription.objects.count() == 0
PromotedAddon.objects.create(addon=addon_factory(), group_id=promoted.LINE.id)
assert PromotedSubscription.objects.count() == 0
def test_auto_approves_addon_when_saved_for_immediate_approval(self):
# empty case with no group set
promo = PromotedAddon.objects.create(
@ -185,66 +160,6 @@ class TestPromotedAddon(TestCase):
assert addon.current_version is None
assert promo.get_resigned_version_number() is None
def test_has_pending_subscription(self):
promo = PromotedAddon.objects.create(
addon=addon_factory(), group_id=promoted.RECOMMENDED.id
)
PromotedSubscription.objects.create(promoted_addon=promo)
# checking the group doesn't require subscription
assert not promo.group.require_subscription
assert hasattr(promo, 'promotedsubscription')
assert not promo.promotedsubscription.is_active
assert not promo.has_approvals
assert not promo.has_pending_subscription
# and when it does
promo.update(group_id=promoted.VERIFIED.id)
assert promo.group.require_subscription
assert hasattr(promo, 'promotedsubscription')
assert not promo.promotedsubscription.is_active
assert not promo.has_approvals
assert promo.has_pending_subscription
# when there isn't a subscription (existing promo before subscriptions)
promo.promotedsubscription.delete()
promo = PromotedAddon.objects.get(id=promo.id)
assert promo.group.require_subscription
assert not hasattr(promo, 'promotedsubscription')
assert not promo.has_pending_subscription
# and when there is
PromotedSubscription.objects.create(promoted_addon=promo)
assert promo.group.require_subscription
assert hasattr(promo, 'promotedsubscription')
assert not promo.promotedsubscription.is_active
assert not promo.has_approvals
assert promo.has_pending_subscription
# when there's a subscription that's been paid
promo.promotedsubscription.update(checkout_completed_at=datetime.datetime.now())
assert promo.group.require_subscription
assert hasattr(promo, 'promotedsubscription')
assert promo.promotedsubscription.is_active
assert not promo.has_approvals
assert not promo.has_pending_subscription
# and when it's not been paid
promo.promotedsubscription.update(checkout_completed_at=None)
assert promo.group.require_subscription
assert hasattr(promo, 'promotedsubscription')
assert not promo.promotedsubscription.is_active
assert not promo.has_approvals
assert promo.has_pending_subscription
# when there's an existing version approved (existing promo)
promo.approve_for_version(promo.addon.current_version)
assert promo.group.require_subscription
assert hasattr(promo, 'promotedsubscription')
assert not promo.promotedsubscription.is_active
assert promo.has_approvals
assert not promo.has_pending_subscription
def test_has_approvals(self):
addon = addon_factory()
promoted_addon = PromotedAddon.objects.create(
@ -257,72 +172,3 @@ class TestPromotedAddon(TestCase):
promoted_addon.reload()
assert promoted_addon.has_approvals
class TestPromotedSubscription(TestCase):
def test_get_onboarding_url_with_new_object(self):
sub = PromotedSubscription()
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],
add_prefix=False,
)
def test_get_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()
external_site_url = 'http://example.org'
with override_settings(EXTERNAL_SITE_URL=external_site_url):
url = sub.get_onboarding_url()
assert url == '{}{}'.format(
external_site_url,
reverse(
'devhub.addons.onboarding_subscription',
args=[sub.promoted_addon.addon.slug],
add_prefix=False,
),
)
assert 'en-US' not in url
def test_stripe_checkout_completed(self):
sub = PromotedSubscription()
assert not sub.stripe_checkout_completed
sub.update(checkout_completed_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(checkout_cancelled_at=datetime.datetime.now())
assert sub.stripe_checkout_cancelled
def test_is_active(self):
sub = PromotedSubscription()
assert sub.is_active is None
sub.update(checkout_completed_at=datetime.datetime.now())
assert sub.is_active
sub.update(cancelled_at=datetime.datetime.now())
assert sub.is_active is False

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

@ -1,262 +0,0 @@
import datetime
from unittest import mock
from django.core import mail
from django.test.utils import override_settings
from olympia.amo.templatetags.jinja_helpers import absolutify
from olympia.amo.tests import (
addon_factory,
TestCase,
)
from olympia.amo.urlresolvers import reverse
from olympia.constants.promoted import VERIFIED, NOT_PROMOTED
from olympia.promoted.models import PromotedAddon
from olympia.promoted.tasks import (
on_stripe_charge_failed,
on_stripe_charge_succeeded,
on_stripe_customer_subscription_deleted,
)
class PromotedAddonTestCase(TestCase):
EVENT_TYPE = 'invalid.event.type'
def setUp(self):
super().setUp()
self.addon = addon_factory()
self.promoted_addon = PromotedAddon.objects.create(
addon=self.addon, group_id=VERIFIED.id
)
def create_stripe_event(self, event_id='some-id', **kwargs):
return {
'id': event_id,
'type': self.EVENT_TYPE,
**kwargs,
}
class TestOnStripeChargeFailed(PromotedAddonTestCase):
EVENT_TYPE = 'charge.failed'
@mock.patch('olympia.promoted.tasks.retrieve_stripe_subscription_for_invoice')
def test_ignores_invalid_event_type(self, retrieve_sub_mock):
event = self.create_stripe_event(event_type='not-charge-failed')
on_stripe_charge_failed(event=event)
retrieve_sub_mock.assert_not_called()
@mock.patch('olympia.promoted.tasks.retrieve_stripe_subscription_for_invoice')
def test_ignores_events_without_data(self, retrieve_sub_mock):
event = self.create_stripe_event(data={})
on_stripe_charge_failed(event=event)
retrieve_sub_mock.assert_not_called()
@mock.patch('olympia.promoted.tasks.retrieve_stripe_subscription_for_invoice')
def test_ignores_events_without_data_object(self, retrieve_sub_mock):
event = self.create_stripe_event(data={'object': None})
on_stripe_charge_failed(event=event)
retrieve_sub_mock.assert_not_called()
@mock.patch('olympia.promoted.tasks.retrieve_stripe_subscription_for_invoice')
def test_ignores_unknown_promoted_subscriptions(self, retrieve_sub_mock):
subscription_id = 'subscription-id'
retrieve_sub_mock.return_value = {
'id': subscription_id,
'livemode': False,
}
invoice_id = 'invoice-id'
fake_charge = {'id': 'charge-id', 'invoice': invoice_id}
event = self.create_stripe_event(data={'object': fake_charge})
on_stripe_charge_failed(event=event)
retrieve_sub_mock.assert_called_once_with(invoice_id=invoice_id)
assert len(mail.outbox) == 0
@mock.patch('olympia.promoted.tasks.retrieve_stripe_subscription_for_invoice')
def test_ignores_stripe_errors(self, retrieve_sub_mock):
retrieve_sub_mock.side_effect = ValueError('stripe error')
invoice_id = 'invoice-id'
fake_charge = {'id': 'charge-id', 'invoice': invoice_id}
event = self.create_stripe_event(data={'object': fake_charge})
on_stripe_charge_failed(event=event)
retrieve_sub_mock.assert_called_once_with(invoice_id=invoice_id)
assert len(mail.outbox) == 0
@override_settings(VERIFIED_ADDONS_EMAIL='verified@example.com')
@mock.patch('olympia.promoted.tasks.retrieve_stripe_subscription_for_invoice')
def test_sends_email_to_amo_admins(self, retrieve_sub_mock):
subscription_id = 'subscription-id'
self.promoted_addon.promotedsubscription.update(
stripe_subscription_id=subscription_id
)
retrieve_sub_mock.return_value = {
'id': subscription_id,
'livemode': False,
}
invoice_id = 'invoice-id'
fake_charge = {'id': 'charge-id', 'invoice': invoice_id}
event = self.create_stripe_event(data={'object': fake_charge})
on_stripe_charge_failed(event=event)
retrieve_sub_mock.assert_called_once_with(invoice_id=invoice_id)
assert len(mail.outbox) == 1
email = mail.outbox[0]
assert (
email.subject
== f'Stripe payment failure detected for add-on: {self.addon.name}'
)
assert email.to == ['verified@example.com']
assert (
f'following add-on: {absolutify(self.addon.get_detail_url())}' in email.body
)
assert (
absolutify(
reverse(
'admin:discovery_promotedaddon_change',
args=[self.promoted_addon.id],
)
)
in email.body
)
assert (
f'//dashboard.stripe.com/test/subscriptions/{subscription_id}' in email.body
)
@mock.patch('olympia.promoted.tasks.retrieve_stripe_subscription_for_invoice')
def test_sends_email_to_amo_admins_in_livemode(self, retrieve_sub_mock):
subscription_id = 'subscription-id'
self.promoted_addon.promotedsubscription.update(
stripe_subscription_id=subscription_id
)
retrieve_sub_mock.return_value = {
'id': subscription_id,
'livemode': True,
}
fake_charge = {'id': 'charge-id', 'invoice': 'invoice-id'}
event = self.create_stripe_event(data={'object': fake_charge})
on_stripe_charge_failed(event=event)
assert (
f'//dashboard.stripe.com/subscriptions/{subscription_id}'
in mail.outbox[0].body
)
class TestOnStripeCustomerSubscriptionDeleted(PromotedAddonTestCase):
EVENT_TYPE = 'customer.subscription.deleted'
def test_ignores_invalid_event_type(self):
event = self.create_stripe_event(event_type='not-subscription-deleted')
on_stripe_customer_subscription_deleted(event=event)
assert not self.promoted_addon.promotedsubscription.cancelled_at
def test_ignores_events_without_data(self):
event = self.create_stripe_event(data={})
on_stripe_customer_subscription_deleted(event=event)
assert not self.promoted_addon.promotedsubscription.cancelled_at
def test_ignores_events_without_data_object(self):
event = self.create_stripe_event(data={'object': None})
on_stripe_customer_subscription_deleted(event=event)
assert not self.promoted_addon.promotedsubscription.cancelled_at
def test_ignores_unknown_promoted_subscriptions(self):
fake_subscription = {'id': 'unknown-sub-id'}
event = self.create_stripe_event(data={'object': fake_subscription})
on_stripe_customer_subscription_deleted(event=event)
assert not self.promoted_addon.promotedsubscription.cancelled_at
def test_ignores_already_cancelled_subscriptions(self):
subscription_id = 'stripe-sub-id'
cancelled_at = datetime.datetime(2020, 11, 1)
self.promoted_addon.promotedsubscription.update(
stripe_subscription_id=subscription_id, cancelled_at=cancelled_at
)
fake_subscription = {'id': subscription_id}
event = self.create_stripe_event(data={'object': fake_subscription})
on_stripe_customer_subscription_deleted(event=event)
assert self.promoted_addon.promotedsubscription.cancelled_at == cancelled_at
def test_cancels_subscription(self):
subscription_id = 'stripe-sub-id'
self.promoted_addon.promotedsubscription.update(
stripe_subscription_id=subscription_id
)
cancelled_at = datetime.datetime.now()
fake_subscription = {
'id': subscription_id,
'canceled_at': cancelled_at.timestamp(),
}
event = self.create_stripe_event(data={'object': fake_subscription})
on_stripe_customer_subscription_deleted(event=event)
self.promoted_addon.refresh_from_db()
assert self.promoted_addon.promotedsubscription.cancelled_at == cancelled_at
assert self.promoted_addon.group_id == NOT_PROMOTED.id
class TestOnStripeChargeSucceeded(PromotedAddonTestCase):
EVENT_TYPE = 'charge.succeeded'
def test_ignores_invalid_event_type(self):
event = self.create_stripe_event(event_type='not-charge-succeeded')
on_stripe_charge_succeeded(event=event)
assert len(mail.outbox) == 0
def test_ignores_events_without_data(self):
event = self.create_stripe_event(data={})
on_stripe_charge_succeeded(event=event)
assert len(mail.outbox) == 0
def test_ignores_events_without_data_object(self):
event = self.create_stripe_event(data={'object': None})
on_stripe_charge_succeeded(event=event)
assert len(mail.outbox) == 0
@override_settings(VERIFIED_ADDONS_EMAIL='verified@example.com')
def test_sends_email(self):
payment_intent = 'payment-intent'
fake_charge = {
'id': 'charge-id',
'payment_intent': payment_intent,
}
event = self.create_stripe_event(data={'object': fake_charge})
on_stripe_charge_succeeded(event=event)
assert len(mail.outbox) == 1
email = mail.outbox[0]
assert email.subject == 'Stripe payment succeeded'
assert email.to == ['verified@example.com']
assert f'//dashboard.stripe.com/test/payments/{payment_intent}' in email.body

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

@ -1,416 +0,0 @@
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 (
BILLING_PERIOD_MONTHLY,
BILLING_PERIOD_YEARLY,
RECOMMENDED,
SPONSORED,
VERIFIED,
)
from olympia.promoted.models import PromotedSubscription, PromotedAddon
from olympia.promoted.utils import (
create_stripe_checkout_session,
create_stripe_customer_portal,
create_stripe_webhook_event,
retrieve_stripe_checkout_session,
retrieve_stripe_subscription,
retrieve_stripe_subscription_for_invoice,
)
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_YEARLY_PRICE_ID='yearly-sponsored-price-id')
def test_create_stripe_checkout_session_for_sponsored():
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': 'yearly-sponsored-price-id', 'quantity': 1}],
customer_email=customer_email,
)
@override_settings(STRIPE_API_VERIFIED_MONTHLY_PRICE_ID='monthly-verified-price-id')
def test_create_stripe_checkout_session_for_verified():
addon = addon_factory()
promoted_addon = PromotedAddon.objects.create(addon=addon, group_id=VERIFIED.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': 'monthly-verified-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'
)
@override_settings(STRIPE_API_SPONSORED_YEARLY_PRICE_ID='yearly-sponsored-price-id')
def test_create_stripe_checkout_session_with_custom_rate():
addon = addon_factory()
promoted_addon = PromotedAddon.objects.create(addon=addon, group_id=SPONSORED.id)
sub = PromotedSubscription.objects.filter(promoted_addon=promoted_addon).get()
# Set a custom onboarding rate, in cents.
onboarding_rate = 1234
sub.update(onboarding_rate=onboarding_rate)
customer_email = 'some-email@example.org'
fake_session = 'fake session'
fake_product = {
'product': 'some-product-id',
'currency': 'some-currency',
'recurring': {'interval': 'year', 'interval_count': 1},
}
with mock.patch(
'olympia.promoted.utils.stripe.checkout.Session.create'
) as stripe_create, mock.patch(
'olympia.promoted.utils.stripe.Price.retrieve'
) as stripe_retrieve:
stripe_create.return_value = fake_session
stripe_retrieve.return_value = fake_product
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_data': {
'product': fake_product['product'],
'currency': fake_product['currency'],
'recurring': {
'interval': fake_product['recurring']['interval'],
'interval_count': fake_product['recurring'][
'interval_count'
],
},
'unit_amount': onboarding_rate,
},
'quantity': 1,
}
],
customer_email=customer_email,
)
@override_settings(STRIPE_API_SPONSORED_YEARLY_PRICE_ID='yearly-sponsored-price-id')
@override_settings(STRIPE_API_SPONSORED_MONTHLY_PRICE_ID='monthly-sponsored-price-id')
def test_create_stripe_checkout_session_for_sponsored_with_custom_period():
addon = addon_factory()
promoted_addon = PromotedAddon.objects.create(addon=addon, group_id=SPONSORED.id)
sub = PromotedSubscription.objects.filter(promoted_addon=promoted_addon).get()
# Set a custom billing period.
sub.update(onboarding_period=BILLING_PERIOD_MONTHLY)
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': 'monthly-sponsored-price-id', 'quantity': 1}],
customer_email=customer_email,
)
@override_settings(STRIPE_API_VERIFIED_YEARLY_PRICE_ID='yearly-verified-price-id')
@override_settings(STRIPE_API_VERIFIED_MONTHLY_PRICE_ID='monthly-verified-price-id')
def test_create_stripe_checkout_session_for_verified_with_custom_period():
addon = addon_factory()
promoted_addon = PromotedAddon.objects.create(addon=addon, group_id=VERIFIED.id)
sub = PromotedSubscription.objects.filter(promoted_addon=promoted_addon).get()
# Set a custom billing period.
sub.update(onboarding_period=BILLING_PERIOD_YEARLY)
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': 'yearly-verified-price-id', 'quantity': 1}],
customer_email=customer_email,
)
@override_settings(STRIPE_API_SPONSORED_MONTHLY_PRICE_ID='monthlty-sponsored-price-id')
def test_create_stripe_checkout_session_with_custom_rate_and_period():
addon = addon_factory()
promoted_addon = PromotedAddon.objects.create(addon=addon, group_id=SPONSORED.id)
sub = PromotedSubscription.objects.filter(promoted_addon=promoted_addon).get()
# Set a custom onboarding rate (in cents) and a custom onboarding period.
onboarding_rate = 1234
sub.update(
onboarding_rate=onboarding_rate,
onboarding_period=BILLING_PERIOD_MONTHLY,
)
customer_email = 'some-email@example.org'
fake_session = 'fake session'
fake_product = {
'product': 'some-product-id',
'currency': 'some-currency',
'recurring': {'interval': 'month', 'interval_count': 1},
}
with mock.patch(
'olympia.promoted.utils.stripe.checkout.Session.create'
) as stripe_create, mock.patch(
'olympia.promoted.utils.stripe.Price.retrieve'
) as stripe_retrieve:
stripe_create.return_value = fake_session
stripe_retrieve.return_value = fake_product
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_data': {
'product': fake_product['product'],
'currency': fake_product['currency'],
'recurring': {
'interval': fake_product['recurring']['interval'],
'interval_count': fake_product['recurring'][
'interval_count'
],
},
'unit_amount': onboarding_rate,
},
'quantity': 1,
}
],
customer_email=customer_email,
)
def test_create_stripe_customer_portal():
addon = addon_factory()
customer_id = 'some-customer-id'
fake_portal = 'fake-return-value'
with mock.patch(
'olympia.promoted.utils.stripe.billing_portal.Session.create'
) as create_portal_mock:
create_portal_mock.return_value = fake_portal
portal = create_stripe_customer_portal(customer_id=customer_id, addon=addon)
assert portal == fake_portal
create_portal_mock.assert_called_once_with(
customer=customer_id,
return_url=absolutify(reverse('devhub.addons.edit', args=[addon.slug])),
)
@override_settings(STRIPE_API_WEBHOOK_SECRET='webhook-secret')
def test_create_stripe_webhook_event():
fake_event = 'fake-event'
payload = 'some payload'
sig_header = 'some sig_header'
with mock.patch(
'olympia.promoted.utils.stripe.Webhook.construct_event'
) as stripe_construct_event:
stripe_construct_event.return_value = fake_event
event = create_stripe_webhook_event(payload=payload, sig_header=sig_header)
assert event == fake_event
stripe_construct_event.assert_called_once_with(
payload,
sig_header,
'webhook-secret',
)
def test_retrieve_stripe_subscription():
stripe_subscription_id = 'some stripe subscription id'
sub = PromotedSubscription(stripe_subscription_id=stripe_subscription_id)
fake_subscription = 'fake-stripe-subscription'
with mock.patch(
'olympia.promoted.utils.stripe.Subscription.retrieve'
) as stripe_retrieve:
stripe_retrieve.return_value = fake_subscription
stripe_sub = retrieve_stripe_subscription(subscription=sub)
assert stripe_sub == fake_subscription
stripe_retrieve.assert_called_once_with(stripe_subscription_id)
def test_retrieve_stripe_subscription_for_invoice():
invoice_id = 'invoice-id'
subscription_id = 'subscription-id'
fake_invoice = {'subscription': subscription_id}
fake_subscription = 'fake-stripe-subscription'
with mock.patch(
'olympia.promoted.utils.stripe.Invoice.retrieve'
) as invoice_mock, mock.patch(
'olympia.promoted.utils.stripe.Subscription.retrieve'
) as subscription_mock:
invoice_mock.return_value = fake_invoice
subscription_mock.return_value = fake_subscription
sub = retrieve_stripe_subscription_for_invoice(invoice_id=invoice_id)
assert sub == fake_subscription
invoice_mock.assert_called_once_with(invoice_id)
subscription_mock.assert_called_once_with(subscription_id)

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

@ -1,90 +0,0 @@
import stripe
from unittest import mock
from olympia.amo.tests import (
APITestClient,
TestCase,
reverse_ns,
)
class TestStripeWebhook(TestCase):
client_class = APITestClient
def setUp(self):
super().setUp()
self.url = reverse_ns('promoted.stripe_webhook', api_version='v5')
def test_invalid_http_method(self):
response = self.client.get(self.url)
assert response.status_code == 405
@mock.patch('olympia.promoted.views.create_stripe_webhook_event')
def test_invalid_signature(self, create_mock):
create_mock.side_effect = stripe.error.SignatureVerificationError(
message='error', sig_header=''
)
response = self.client.post(self.url)
assert response.status_code == 400
@mock.patch('olympia.promoted.views.create_stripe_webhook_event')
def test_invalid_payload(self, create_mock):
create_mock.side_effect = ValueError()
response = self.client.post(self.url)
assert response.status_code == 400
@mock.patch('olympia.promoted.views.create_stripe_webhook_event')
def test_event_received(self, create_mock):
create_mock.return_value = mock.MagicMock(type='some-stripe-type')
payload = b'some payload'
sig_header = 'some signature'
response = self.client.post(
self.url,
data=payload,
content_type='text/plain',
HTTP_STRIPE_SIGNATURE=sig_header,
)
assert response.status_code == 202
create_mock.assert_called_once_with(payload=payload, sig_header=sig_header)
@mock.patch('olympia.promoted.views.on_stripe_charge_failed.delay')
@mock.patch('olympia.promoted.views.create_stripe_webhook_event')
def test_charge_failed(self, create_mock, task_mock):
fake_event = mock.MagicMock(type='charge.failed')
create_mock.return_value = fake_event
response = self.client.post(self.url)
assert response.status_code == 202
task_mock.assert_called_once_with(event=fake_event)
@mock.patch('olympia.promoted.views.on_stripe_customer_subscription_deleted.delay')
@mock.patch('olympia.promoted.views.create_stripe_webhook_event')
def test_customer_subscription_deleted(self, create_mock, task_mock):
fake_event = mock.MagicMock(type='customer.subscription.deleted')
create_mock.return_value = fake_event
response = self.client.post(self.url)
assert response.status_code == 202
task_mock.assert_called_once_with(event=fake_event)
@mock.patch('olympia.promoted.views.on_stripe_charge_succeeded.delay')
@mock.patch('olympia.promoted.views.create_stripe_webhook_event')
def test_charge_succeeded(self, create_mock, task_mock):
fake_event = mock.MagicMock(type='charge.succeeded')
create_mock.return_value = fake_event
response = self.client.post(self.url)
assert response.status_code == 202
task_mock.assert_called_once_with(event=fake_event)

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

@ -1,131 +0,0 @@
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 (
BILLING_PERIOD_MONTHLY,
BILLING_PERIOD_YEARLY,
SPONSORED,
VERIFIED,
)
def get_stripe_price_id_for_subscription(subscription):
if subscription.onboarding_period == BILLING_PERIOD_YEARLY:
price_id_by_group_id = {
SPONSORED.id: settings.STRIPE_API_SPONSORED_YEARLY_PRICE_ID,
VERIFIED.id: settings.STRIPE_API_VERIFIED_YEARLY_PRICE_ID,
}
elif subscription.onboarding_period == BILLING_PERIOD_MONTHLY:
price_id_by_group_id = {
SPONSORED.id: settings.STRIPE_API_SPONSORED_MONTHLY_PRICE_ID,
VERIFIED.id: settings.STRIPE_API_VERIFIED_MONTHLY_PRICE_ID,
}
else:
# Default billing rate/period configuration for each promoted group.
price_id_by_group_id = {
SPONSORED.id: settings.STRIPE_API_SPONSORED_YEARLY_PRICE_ID,
VERIFIED.id: settings.STRIPE_API_VERIFIED_MONTHLY_PRICE_ID,
}
return price_id_by_group_id.get(subscription.promoted_addon.group_id)
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."""
stripe.api_key = settings.STRIPE_API_SECRET_KEY
price_id = get_stripe_price_id_for_subscription(subscription)
if not price_id:
raise ValueError(
'No price ID for promoted group ID: {}.'.format(
subscription.promoted_addon.group_id
)
)
if subscription.onboarding_rate:
# When we have a custom onboarding rate, we have to retrieve the Stripe
# Product associated with the default Stripe Price first, so that we
# can pass the Product ID to Stripe with a custom amount.
price = stripe.Price.retrieve(price_id)
line_item = {
'price_data': {
'product': price.get('product'),
'currency': price.get('currency'),
'recurring': {
'interval': price.get('recurring', {}).get('interval'),
'interval_count': price.get('recurring', {}).get('interval_count'),
},
'unit_amount': subscription.onboarding_rate,
},
'quantity': 1,
}
else:
# The default price will be used for this subscription.
line_item = {'price': price_id, 'quantity': 1}
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=[line_item],
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.
This function should only be used during the Checkout process."""
stripe.api_key = settings.STRIPE_API_SECRET_KEY
return stripe.checkout.Session.retrieve(subscription.stripe_session_id)
def retrieve_stripe_subscription(subscription):
"""This function returns a Stripe Subscription object or raises errors."""
stripe.api_key = settings.STRIPE_API_SECRET_KEY
return stripe.Subscription.retrieve(subscription.stripe_subscription_id)
def create_stripe_customer_portal(customer_id, addon):
"""This function creates a Stripe Customer Portal 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.billing_portal.Session.create(
customer=customer_id,
return_url=absolutify(reverse('devhub.addons.edit', args=[addon.slug])),
)
def create_stripe_webhook_event(payload, sig_header):
"""This function returns a Stripe Webhook Event object or raises errors."""
stripe.api_key = settings.STRIPE_API_SECRET_KEY
webhook_secret = settings.STRIPE_API_WEBHOOK_SECRET
return stripe.Webhook.construct_event(payload, sig_header, webhook_secret)
def retrieve_stripe_subscription_for_invoice(invoice_id):
"""This function returns a Stripe Subscription object or raises errors."""
stripe.api_key = settings.STRIPE_API_SECRET_KEY
invoice = stripe.Invoice.retrieve(invoice_id)
return stripe.Subscription.retrieve(invoice['subscription'])

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

@ -1,55 +0,0 @@
import stripe
from django.views.decorators.csrf import csrf_exempt
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.status import HTTP_202_ACCEPTED, HTTP_400_BAD_REQUEST
import olympia.core.logger
from .tasks import (
on_stripe_charge_failed,
on_stripe_charge_succeeded,
on_stripe_customer_subscription_deleted,
)
from .utils import create_stripe_webhook_event
log = olympia.core.logger.getLogger('z.promoted')
# This dict maps a Stripe event type with a Celery task that handles events of
# that type.
ON_STRIPE_EVENT_TASKS = {
'charge.failed': on_stripe_charge_failed,
'charge.succeeded': on_stripe_charge_succeeded,
'customer.subscription.deleted': on_stripe_customer_subscription_deleted,
}
@api_view(['POST'])
@csrf_exempt
def stripe_webhook(request):
payload = request.body
sig_header = request.META.get('HTTP_STRIPE_SIGNATURE')
try:
event = create_stripe_webhook_event(payload=payload, sig_header=sig_header)
except stripe.error.SignatureVerificationError:
log.exception('received stripe event with invalid signature')
return Response(status=HTTP_400_BAD_REQUEST)
except ValueError:
log.exception('received stripe event with invalid payload')
return Response(status=HTTP_400_BAD_REQUEST)
task = ON_STRIPE_EVENT_TASKS.get(event.type)
if task:
task.delay(event=event)
else:
# This would only happen if a Stripe admin updates the configured
# webhook to send new events (because we have to select the events we
# want in the Stripe settings).
log.info('received unhandled stripe event type: "%s".', event.type)
return Response(status=HTTP_202_ACCEPTED)

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

@ -30,7 +30,6 @@ from olympia.constants.promoted import (
SPOTLIGHT,
STRATEGIC,
SPONSORED,
VERIFIED,
)
from olympia.files.models import File
from olympia.lib.crypto.tests.test_signing import (
@ -686,54 +685,6 @@ class TestReviewHelper(TestReviewHelperBase):
== expected
)
def test_actions_promoted_no_subscription_cant_be_approved(self):
# the public action shouldn't be available if a subscription is needed
# for the promoted group
self.make_addon_promoted(self.addon, SPONSORED)
self.grant_permission(self.request.user, 'Addons:Review')
expected = ['reject', 'reject_multiple_versions', 'reply', 'super', 'comment']
assert (
list(
self.get_review_actions(
addon_status=amo.STATUS_APPROVED,
file_status=amo.STATUS_AWAITING_REVIEW,
).keys()
)
== expected
)
# finish the checkout
self.addon.promotedaddon.promotedsubscription.update(
checkout_completed_at=datetime.now()
)
expected = ['public'] + expected
assert (
list(
self.get_review_actions(
addon_status=amo.STATUS_APPROVED,
file_status=amo.STATUS_AWAITING_REVIEW,
).keys()
)
== expected
)
# and check the case where the addon is already fully promoted but
# hasn't paid yet (trial participant)
self.addon.promotedaddon.promotedsubscription.update(checkout_completed_at=None)
assert (
not self.addon.promotedaddon.promotedsubscription.stripe_checkout_completed
)
self.make_addon_promoted(self.addon, VERIFIED, approve_version=True)
assert (
list(
self.get_review_actions(
addon_status=amo.STATUS_APPROVED,
file_status=amo.STATUS_AWAITING_REVIEW,
).keys()
)
== expected
)
def test_actions_unlisted(self):
# Just regular review permissions don't let you do much on an unlisted
# review page.
@ -2352,32 +2303,6 @@ class TestReviewHelper(TestReviewHelperBase):
).exists()
assert self.addon.promoted_group() == LINE
def test_nominated_to_approved_promoted_unpaid_subscription_fails(self):
self.make_addon_promoted(self.addon, VERIFIED)
subscription = self.addon.promotedaddon.promotedsubscription
assert not self.addon.promoted_group()
assert not subscription.stripe_checkout_completed
with self.assertRaises(AssertionError):
self.test_nomination_to_public()
assert not self.addon.current_version.promoted_approvals.filter(
group_id=VERIFIED.id
).exists()
assert not self.addon.promoted_group()
def test_nominated_to_approved_promoted_has_subscription(self):
self.make_addon_promoted(self.addon, VERIFIED)
subscription = self.addon.promotedaddon.promotedsubscription
subscription.update(checkout_completed_at=datetime.now())
assert not self.addon.promoted_group()
assert subscription.stripe_checkout_completed
self.test_nomination_to_public()
assert self.addon.current_version.promoted_approvals.filter(
group_id=VERIFIED.id
).exists()
assert self.addon.promoted_group() == VERIFIED
def test_approved_update_recommended(self):
self.make_addon_promoted(self.addon, RECOMMENDED)
assert not self.addon.promoted_group()
@ -2396,32 +2321,6 @@ class TestReviewHelper(TestReviewHelperBase):
).exists()
assert self.addon.promoted_group() == LINE
def test_approved_update_promoted_unpaid_subscription_fails(self):
self.make_addon_promoted(self.addon, VERIFIED)
subscription = self.addon.promotedaddon.promotedsubscription
assert not self.addon.promoted_group()
assert not subscription.stripe_checkout_completed
with self.assertRaises(AssertionError):
self.test_public_addon_with_version_awaiting_review_to_public()
assert not self.addon.current_version.promoted_approvals.filter(
group_id=VERIFIED.id
).exists()
assert not self.addon.promoted_group()
def test_approved_update_promoted_has_subscription(self):
self.make_addon_promoted(self.addon, VERIFIED)
subscription = self.addon.promotedaddon.promotedsubscription
subscription.update(checkout_completed_at=datetime.now())
assert not self.addon.promoted_group()
assert subscription.stripe_checkout_completed
self.test_public_addon_with_version_awaiting_review_to_public()
assert self.addon.current_version.promoted_approvals.filter(
group_id=VERIFIED.id
).exists()
assert self.addon.promoted_group() == VERIFIED
def test_autoapprove_fails_for_promoted(self):
self.make_addon_promoted(self.addon, RECOMMENDED)
assert not self.addon.promoted_group()

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

@ -526,10 +526,6 @@ class ReviewHelper(object):
)
version_is_blocked = self.version and self.version.is_blocked
promoted_subscription_okay = (
not promoted_group or not self.addon.promotedaddon.has_pending_subscription
)
# Special logic for availability of reject multiple action:
if version_is_unlisted:
can_reject_multiple = is_appropriate_reviewer
@ -566,7 +562,6 @@ class ReviewHelper(object):
and version_is_unreviewed
and is_appropriate_reviewer
and not version_is_blocked
and promoted_subscription_okay
),
}
actions['reject'] = {
@ -758,7 +753,6 @@ class ReviewBase(object):
def set_promoted(self):
group = self.addon.promoted_group(currently_approved=False)
if group and group.pre_review:
assert not self.addon.promotedaddon.has_pending_subscription
# These addons shouldn't be be attempted for auto approval anyway,
# but double check that the cron job isn't trying to approve it.
assert not self.user.id == settings.TASK_USER_ID

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

@ -268,26 +268,7 @@
font-weight: inherit;
padding: 0 0 0 10px;
}
li.stripe-customer-portal {
form {
margin-bottom: 0;
}
button {
color: #3d6db5;
font-family: inherit;
font-size: .923em;
font-weight: inherit;
padding: 0 0 0 10px;
&:active,
&:focus,
&:hover {
box-shadow: none;
top: initial;
}
}
}
li.addon-manage a {
display: inline;
&:last-child {

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

@ -1,27 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
'use strict';
const promotedAddonForm = document.getElementById('promotedaddon_form');
if (!promotedAddonForm) {
return;
}
const groupSelect = promotedAddonForm.querySelector('#id_group_id');
let initialIndex = groupSelect.selectedIndex;
const initialGroup = groupSelect.options[initialIndex].label || '';
// Enable the listener only when loading a promoted add-on belonging to a
// group that requires a subscription.
if (['sponsored', 'verified'].includes(initialGroup.toLowerCase())) {
groupSelect.addEventListener('change', () => {
if (
!confirm(
"This promoted add-on belongs to a group that requires a Stripe subscription, which should be cancelled in Stripe first. If you confirm this action, you will be able to update the group but Stripe won't be notified and the Stripe subscription will remain active. Do you really want to continue?",
)
) {
groupSelect.selectedIndex = initialIndex;
}
});
}
});

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

@ -1,11 +0,0 @@
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'],
});
});
}