From 7ac170471f0809bd87df0e58139c8f6c1c03da7f Mon Sep 17 00:00:00 2001 From: chrisjsimpson Date: Sat, 10 Feb 2024 20:50:17 +0000 Subject: [PATCH] Fix #1295 As a shop owner, I can see a list of recently cancelled subscriptions via the dashboard --- subscribie/blueprints/admin/__init__.py | 52 +++++++++++ subscribie/blueprints/admin/stats.py | 15 ++++ .../admin/templates/admin/dashboard.html | 1 + .../recent_subscription_cancellations.html | 87 +++++++++++++++++++ 4 files changed, 155 insertions(+) create mode 100644 subscribie/blueprints/admin/templates/admin/recent_subscription_cancellations.html diff --git a/subscribie/blueprints/admin/__init__.py b/subscribie/blueprints/admin/__init__.py index e01dac37..174fff42 100644 --- a/subscribie/blueprints/admin/__init__.py +++ b/subscribie/blueprints/admin/__init__.py @@ -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 @@ -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") @@ -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, ) @@ -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() diff --git a/subscribie/blueprints/admin/stats.py b/subscribie/blueprints/admin/stats.py index 82a7391e..a75be6b3 100644 --- a/subscribie/blueprints/admin/stats.py +++ b/subscribie/blueprints/admin/stats.py @@ -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 @@ -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) diff --git a/subscribie/blueprints/admin/templates/admin/dashboard.html b/subscribie/blueprints/admin/templates/admin/dashboard.html index d68dd8a0..c5f26a3b 100644 --- a/subscribie/blueprints/admin/templates/admin/dashboard.html +++ b/subscribie/blueprints/admin/templates/admin/dashboard.html @@ -35,6 +35,7 @@

Checklist

Stats

You have: {{ num_active_subscribers }} subscribers with active subscriptions.

+

You've had: {{ num_recent_subscription_cancellations }} subscription cancellations in the last 30 days.

You've had: {{ num_subscribers }} subscribers since starting your shop.

You've had: {{ num_one_off_purchases }} people buy a one-off item from your shop.

You've had: {{ num_signups }} people either buy one-off or start a subscription{% if settings.donations_enabled %} or donations {% endif %} since starting your shop.

diff --git a/subscribie/blueprints/admin/templates/admin/recent_subscription_cancellations.html b/subscribie/blueprints/admin/templates/admin/recent_subscription_cancellations.html new file mode 100644 index 00000000..2ad25e22 --- /dev/null +++ b/subscribie/blueprints/admin/templates/admin/recent_subscription_cancellations.html @@ -0,0 +1,87 @@ +{% extends "admin/layout.html" %} +{% block title %} Recent Subscription Cancellations{% endblock %} + +{% block body %} + +

Recent Subscription Cancellations

+ +
+ +
+
+
+
+ +

+ Below is the list of recent subscription cancellations (if any) within the last 30 days. +

+

+ Be sure to click the subscriber name to investigate further, since they may have since signed-up to a new, or different plan. +

+ +

Reason Code

+
    +
  • payment_failed - means all retry attempts have failed and the subscription is cancelled
  • +
  • payment_disputed - means the subscriber disputed the charge(s) at their bank or card issuer
  • +
  • cancellation_requested - 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
  • +
+ + + + + + + + + + + + + + {% for cancellation in cancellations %} + + + + + + + {% endfor %} + +
SubscriberSubscriptionCancellation DateReason
+ {{ cancellation['person'].given_name }} {{ cancellation['person'].family_name }} +
+
+ {{ cancellation['subscription'].plan.title }} + + {{ cancellation['cancellation_date'] | timestampToDate }} + + {{ cancellation['cancellation_reason'] }} +
+
+
+
+ + + +{% endblock body %}