Skip to content

Commit

Permalink
Fix #1295 As a shop owner, I can see a list of recently cancelled sub…
Browse files Browse the repository at this point in the history
…scriptions via the dashboard
  • Loading branch information
chrisjsimpson committed Feb 10, 2024
1 parent 7495868 commit 7ac1704
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 0 deletions.
52 changes: 52 additions & 0 deletions subscribie/blueprints/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
get_number_of_signups,
get_number_of_one_off_purchases,
get_number_of_transactions_with_donations,
get_number_of_recent_subscription_cancellations,
)

import stripe
Expand Down Expand Up @@ -487,6 +488,9 @@ def dashboard():
num_signups = get_number_of_signups()
num_one_off_purchases = get_number_of_one_off_purchases()
num_donations = get_number_of_transactions_with_donations()
num_recent_subscription_cancellations = (
get_number_of_recent_subscription_cancellations()
)
shop_default_country_code = get_shop_default_country_code()
saas_url = current_app.config.get("SAAS_URL")

Expand All @@ -500,6 +504,7 @@ def dashboard():
num_signups=num_signups,
num_donations=num_donations,
num_one_off_purchases=num_one_off_purchases,
num_recent_subscription_cancellations=num_recent_subscription_cancellations,
shop_default_country_code=shop_default_country_code,
saas_url=saas_url,
)
Expand Down Expand Up @@ -1292,6 +1297,53 @@ def subscribers():
)


@admin.route("/recent-subscription-cancellations")
@login_required
def show_recent_subscription_cancellations():
"""Get the last 30 days subscription cancellations (if any)
Note: Stripe api only guarentees the last 30 days of events.
At time of writing this method performs no caching of events,
see StripeInvoice for possible improvements
"""
stripe.api_key = get_stripe_secret_key()
connect_account = get_stripe_connect_account()

subscription_cancellations = stripe.Event.list(
stripe_account=connect_account.id,
limit=100,
types=["customer.subscription.deleted"],
)
cancellations = []
# subscription id
for index, value in enumerate(subscription_cancellations):
# Get Person
person = (
Person.query.execution_options(include_archived=True)
.filter_by(uuid=value.data.object.metadata.person_uuid)
.one()
)
# Get Subscription
subscription = (
Subscription.query.execution_options(include_archived=True)
.filter_by(stripe_subscription_id=value.data.object.id)
.one()
)
cancellation_date = value.data.object.canceled_at
cancellation_reason = value.data.object.cancellation_details.reason
cancellations.append(
{
"subscription": subscription,
"person": person,
"cancellation_date": cancellation_date,
"cancellation_reason": cancellation_reason,
}
)
return render_template(
"admin/recent_subscription_cancellations.html",
cancellations=cancellations,
)


@admin.route("/refresh-subscription-statuses")
def refresh_subscriptions():
update_stripe_subscription_statuses()
Expand Down
15 changes: 15 additions & 0 deletions subscribie/blueprints/admin/stats.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from subscribie.database import database
from subscribie.models import Person, Subscription, Plan, PlanRequirements, Transaction
from subscribie.utils import get_stripe_secret_key, get_stripe_connect_account_id
from sqlalchemy.sql import func
import stripe
import logging


Expand Down Expand Up @@ -95,3 +97,16 @@ def get_number_of_transactions_with_donations():
.count()
)
return count


def get_number_of_recent_subscription_cancellations():
stripe.api_key = get_stripe_secret_key()
connect_account_id = get_stripe_connect_account_id()

subscription_cancellations = stripe.Event.list(
stripe_account=connect_account_id,
limit=100,
types=["customer.subscription.deleted"],
)

return len(subscription_cancellations)
1 change: 1 addition & 0 deletions subscribie/blueprints/admin/templates/admin/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ <h3 class="card-title">Checklist</h3>
<div class="card px-3 py-3 my-3">
<h3 class="card-title justify-content-between d-flex">Stats</h3>
<p>You have: {{ num_active_subscribers }} <a href="{{ url_for('admin.subscribers', action='show_active')}}">subscribers</a> with <em>active</em> subscriptions.</p>
<p>You've had: {{ num_recent_subscription_cancellations }} <a href="{{ url_for('admin.show_recent_subscription_cancellations')}}">subscription cancellations</a> in the last 30 days.</p>
<p>You've had: {{ num_subscribers }} <a href="{{ url_for('admin.subscribers') }}">subscribers</a> since starting your shop.</p>
<p>You've had: {{ num_one_off_purchases }} <a href="{{ url_for('admin.subscribers', action='show_one_off_payments')}}">people</a> buy a one-off item from your shop.</p>
<p>You've had: {{ num_signups }} <a href="{{ url_for('admin.subscribers') }}">people</a> either buy one-off or start a subscription{% if settings.donations_enabled %} or donations {% endif %} since starting your shop.</p>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{% extends "admin/layout.html" %}
{% block title %} Recent Subscription Cancellations{% endblock %}

{% block body %}

<h2 class="text-center text-dark mb-3">Recent Subscription Cancellations</h2>

<div class="container">
<ul class="breadcrumb">
<li class="breadcrumb-item"><a href="/">Shop</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('admin.dashboard') }}">Manage My Shop</a></li>
<li class="breadcrumb-item active" aria-current="page">Recent Subscription Cancellations</li>
</ul>
</div>
<main>
<div class="section">
<div class="container">

<p>
Below is the list of recent subscription cancellations (if any) within the last 30 days.
</p>
<p>
Be sure to click the subscriber name to investigate further, since they may have since signed-up to a new, or different plan.
</p>

<h3>Reason Code</h3>
<ul style="list-style: disc">
<li><em>payment_failed</em> - means all retry attempts have failed and the subscription is cancelled</li>
<li><em>payment_disputed</em> - means the subscriber disputed the charge(s) at their bank or card issuer</li>
<li><em>cancellation_requested</em> - means a cancellation was requested which caused the subscription to be cancelled. If plans have a "Cancel at" date set, they will natually cancel at the "Cancel at" date set on the plan</li>
</ul>

<p class="alert alert-warning" role="alert">Please note this list only goes back 30 days</p>

<table class="table mobile-optimised">
<thead>
<tr>
<th>Subscriber</th>
<th>Subscription</th>
<th>Cancellation Date</th>
<th>Reason</th>
</tr>
</thead>
<tbody>
{% for cancellation in cancellations %}
<tr>
<td data-th="Name">
<a href="{{ url_for('admin.show_subscriber', subscriber_id=cancellation['person'].id) }}">{{ cancellation['person'].given_name }} {{ cancellation['person'].family_name }}</a>
<br />
</td>
<td data-th="Plan">
{{ cancellation['subscription'].plan.title }}
</td>
<td>
{{ cancellation['cancellation_date'] | timestampToDate }}
</td>
<td>
{{ cancellation['cancellation_reason'] }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> <!-- end .container -->
</div><!-- end .section -->
</main>

<script>
{# give UI feedback whilst waiting for active subscribers to load #}
document.getElementById('show-active-subscribers').addEventListener('click', function(e) {
e.target.textContent = "Please wait...";
});

{# Refresh subscription statuses when button clicked #}
btnRefreshSubscriptions = document.getElementById('refresh_subscriptions');

btnRefreshSubscriptions.addEventListener('click', refreshSubscriptionStatuses);

function refreshSubscriptionStatuses() {
fetch("{{ url_for('admin.refresh_subscriptions') }}")
.then(response => { document.location = "{{ url_for('admin.refresh_subscriptions') }}" });
}
{# End Refresh subscription statuses when button clicked #}

</script>

{% endblock body %}

0 comments on commit 7ac1704

Please sign in to comment.