rm stripe/payment stuff (#16454)
* rm stripe/payment stuff * rm some more unused constants * revert the less auto-reformatting
This commit is contained in:
Родитель
bd327d16cc
Коммит
3b5810722f
|
@ -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, don’t 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'],
|
||||
});
|
||||
});
|
||||
}
|
Загрузка…
Ссылка в новой задаче