Skip to content

Commit c781ebf

Browse files
committed
WIP Fix #1431 As shop owner I can pause all payment collections
1 parent 6c1d968 commit c781ebf

13 files changed

+268
-75
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ tests/browser-automated-tests-playwright/index.spec.js-snapshots/*
4646
tests/browser-automated-tests-playwright/worker*
4747
tests/browser-automated-tests-playwright/e2e/*-snapshots
4848
tests/browser-automated-tests-playwright/test-videos/*
49+
tests/browser-automated-tests-playwright/graphviz_output*
4950
subscribie/static/*
5051
subscribie/custom_pages/*
5152
playwright-report
@@ -54,3 +55,4 @@ playwright-report
5455
emails
5556
*.bk
5657
email-queue
58+
uploads/*

subscribie/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"""
33
subscribie.app
44
~~~~~~~~~
5-
A microframework for buiding subsciption websites.
5+
A microframework for building Subscription websites.
66
This module implements the central subscribie application.
77
88
:copyright: (c) 2018 by Karma Computing Ltd
@@ -216,9 +216,9 @@ def start_session():
216216
# Note that PriceLists must also be assigned to plan(s) to be in effect.
217217
price_lists = PriceList.query.all()
218218
# If there are zero PriceLists this may mean shop is outdated and
219-
# therefore needs its inital PriceLists created
219+
# therefore needs its initial PriceLists created
220220
if len(price_lists) == 0:
221-
# Create defaul PriceList with zero rules for each suported currency
221+
# Create default PriceList with zero rules for each supported currency
222222
for currency in settings.get("SUPPORTED_CURRENCIES"):
223223
log.debug(
224224
f"Creating PriceList with zero rules for currency {currency}" # noqa: E501
@@ -386,7 +386,7 @@ def alert_subscriber_update_choices(subscriber: Person):
386386
)
387387
alert_subscriber_update_choices(person)
388388

389-
@app.route("/test-lanuage")
389+
@app.route("/test-language")
390390
def test_language():
391391
return _("Hello")
392392

subscribie/blueprints/admin/__init__.py

Lines changed: 109 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import json
33
from subscribie import settings
44
from subscribie.database import database # noqa
5+
from subscribie.tasks import background_task
56
from flask import (
67
Blueprint,
78
render_template,
@@ -249,7 +250,7 @@ def update_payment_fulfillment(stripe_external_id):
249250
@admin.route("/stripe/charge", methods=["POST", "GET"])
250251
@login_required
251252
def stripe_create_charge():
252-
"""Charge an existing subscriber x ammount immediately
253+
"""Charge an existing subscriber x amount immediately
253254
254255
:param stripe_customer_id: Stripe customer id
255256
:param amount: Positive integer amount to charge in smallest currency unit
@@ -267,7 +268,7 @@ def stripe_create_charge():
267268
currency = data["currency"]
268269
statement_descriptor_suffix = data["statement_descriptor_suffix"]
269270
except Exception:
270-
# Assumme form submission
271+
# Assume form submission
271272
# Get stripe customer_id from subscribers subscription -> customer reference
272273
person = Person.query.get(request.form.get("person_id"))
273274
stripe_subscription_id = person.subscriptions[0].stripe_subscription_id
@@ -334,12 +335,84 @@ def stripe_create_charge():
334335
return jsonify(paymentIntent.status)
335336

336337

338+
@background_task
339+
def do_pause_stripe_subscription_payment_collection(
340+
subscription_id, pause_collection_behavior="keep_as_draft", app=None
341+
):
342+
"""Pause pause_stripe_subscription payment collections via its
343+
Stripe subscription_id
344+
345+
Choices of `pause_collection_behavior` include:
346+
347+
- keep_as_draft (Subscribie default- Temporarily offer services for free, and
348+
collect payment later)
349+
- void (Temporarily offer services for free and never collect payment)
350+
- mark_uncollectible (Temporarily offer services for free and
351+
mark invoice as uncollectible)
352+
See also: https://docs.stripe.com/billing/subscriptions/pause-payment
353+
"""
354+
with app.app_context():
355+
stripe.api_key = get_stripe_secret_key()
356+
connect_account_id = get_stripe_connect_account_id()
357+
358+
if subscription_id is None:
359+
log.error("subscription_id cannot be None")
360+
return False
361+
try:
362+
stripe_subscription = stripe.Subscription.retrieve(
363+
subscription_id, stripe_account=connect_account_id
364+
)
365+
if stripe_subscription.status != "canceled":
366+
stripe_pause = stripe.Subscription.modify(
367+
subscription_id,
368+
stripe_account=connect_account_id,
369+
pause_collection={"behavior": pause_collection_behavior},
370+
)
371+
# filtering for the pause_collection value
372+
stripe_pause_filter = stripe_pause["pause_collection"]["behavior"]
373+
374+
# adding the pause_collection status to the
375+
# stripe_pause_collection column
376+
pause_collection = Subscription.query.filter_by(
377+
stripe_subscription_id=subscription_id
378+
).first()
379+
380+
pause_collection.stripe_pause_collection = stripe_pause_filter
381+
database.session.commit()
382+
log.debug(f"Subscription paused ({subscription_id})")
383+
else:
384+
log.debug(
385+
f"Skipping. Subscription {subscription_id} because it's canceled."
386+
)
387+
except Exception as e:
388+
msg = f"Error pausing subscription ({subscription_id})"
389+
log.error(f"{msg}. {e}")
390+
raise
391+
392+
393+
@background_task
394+
def do_pause_all_stripe_subscriptions(app=None):
395+
# For each Subscription object, get it's Stripe subscription
396+
# object and pause it using pause_stripe_subscription.
397+
with app.app_context():
398+
subscriptions = Subscription.query.all()
399+
for subscription in subscriptions:
400+
log.debug(f"Attempting to pause subscription {subscription.uuid}")
401+
stripe_subscription_id = subscription.stripe_subscription_id
402+
try:
403+
do_pause_stripe_subscription_payment_collection(
404+
stripe_subscription_id, app=current_app
405+
)
406+
except Exception as e:
407+
log.error(
408+
f"Error trying to pause subscription {subscription.uuid}. Error: {e}" # noqa: E501
409+
)
410+
411+
337412
@admin.route("/stripe/subscriptions/<subscription_id>/actions/pause")
338413
@login_required
339414
def pause_stripe_subscription(subscription_id: str):
340415
"""Pause a Stripe subscription"""
341-
stripe.api_key = get_stripe_secret_key()
342-
connect_account_id = get_stripe_connect_account_id()
343416

344417
if "confirm" in request.args and request.args["confirm"] != "1":
345418
return render_template(
@@ -349,31 +422,24 @@ def pause_stripe_subscription(subscription_id: str):
349422
)
350423
if "confirm" in request.args and request.args["confirm"] == "1":
351424
try:
352-
stripe_pause = stripe.Subscription.modify(
353-
subscription_id,
354-
stripe_account=connect_account_id,
355-
pause_collection={"behavior": "void"},
425+
do_pause_stripe_subscription_payment_collection(
426+
subscription_id, pause_collection_behavior="void", app=current_app
356427
)
357-
# filtering for the pause_collection value
358-
stripe_pause_filter = stripe_pause["pause_collection"]["behavior"]
359-
360-
# adding the pause_collection status to the stripe_pause_collection column
361-
pause_collection = Subscription.query.filter_by(
362-
stripe_subscription_id=subscription_id
363-
).first()
364-
365-
pause_collection.stripe_pause_collection = stripe_pause_filter
366-
database.session.commit()
367-
368428
flash("Subscription paused")
369-
except Exception as e:
370-
msg = "Error pausing subscription"
429+
except Exception:
430+
msg = f"Error pausing subscription ({subscription_id})"
371431
flash(msg)
372-
log.error(f"{msg}. {e}")
373-
374432
return redirect(url_for("admin.subscribers"))
375433

376434

435+
@admin.route("/stripe/subscriptions/all/actions/pause")
436+
@login_required
437+
def pause_all_subscribers_subscriptions():
438+
"""Bulk action to pause all subscriptions in the shop"""
439+
do_pause_all_stripe_subscriptions() # Background task
440+
return """All payment collections are being paused in the background. You can move away from this page.""" # noqa: E501
441+
442+
377443
@admin.route("/stripe/subscriptions/<subscription_id>/actions/resume")
378444
@login_required
379445
def resume_stripe_subscription(subscription_id):
@@ -525,7 +591,7 @@ def edit():
525591
526592
Note plans are immutable, when a change is made to plan, its old
527593
plan is archived and a new plan is created with a new uuid. This is to
528-
protect data integriry and make sure plan history is retained, via its uuid.
594+
protect data integrity and make sure plan history is retained, via its uuid.
529595
If a user needs to change a subscription, they should change to a different
530596
plan with a different uuid.
531597
@@ -1030,7 +1096,7 @@ def stripe_connect():
10301096
log.error(e)
10311097
account = None
10321098

1033-
# Setup Stripe webhook endpoint if it dosent already exist
1099+
# Setup Stripe webhook endpoint if it doesn't already exist
10341100
if account:
10351101
# Attempt to Updates an existing Account Capability to accept card payments
10361102
try:
@@ -1411,11 +1477,23 @@ def subscribers():
14111477
)
14121478

14131479

1480+
@admin.route("/subscribers/bulk-operations")
1481+
@login_required
1482+
def subscribers_bulk_operations_index():
1483+
1484+
num_active_subscribers = get_number_of_active_subscribers()
1485+
return render_template(
1486+
"admin/subscribers_bulk_operations_index.html",
1487+
num_active_subscribers=num_active_subscribers,
1488+
confirm=request.args.get("confirm"),
1489+
)
1490+
1491+
14141492
@admin.route("/recent-subscription-cancellations")
14151493
@login_required
14161494
def show_recent_subscription_cancellations():
14171495
"""Get the last 30 days subscription cancellations (if any)
1418-
Note: Stripe api only guarentees the last 30 days of events.
1496+
Note: Stripe api only guarantees the last 30 days of events.
14191497
At time of writing this method performs no caching of events,
14201498
see StripeInvoice for possible improvements
14211499
"""
@@ -1439,7 +1517,7 @@ def show_recent_subscription_cancellations():
14391517
)
14401518
if person is None:
14411519
log.info(
1442-
f"""Person query retruned None- probably archived.\n
1520+
f"""Person query returned None- probably archived.\n
14431521
Skipping Person with uuid {value.data.object.metadata.person_uuid}"""
14441522
)
14451523
continue
@@ -1700,7 +1778,7 @@ def add_shop_admin():
17001778
form = AddShopAdminForm()
17011779
if request.method == "POST":
17021780
if form.validate_on_submit():
1703-
# Check user dosent already exist
1781+
# Check user doesn't already exist
17041782
email = escape(request.form["email"].lower())
17051783
if User.query.filter_by(email=email).first() is not None:
17061784
return f"Error, admin with email ({email}) already exists."
@@ -1747,7 +1825,7 @@ def delete_admin_confirmation(id: int):
17471825
else:
17481826
User.query.filter_by(id=id).delete()
17491827
database.session.commit()
1750-
flash("Account was deleted succesfully")
1828+
flash("Account was deleted successfully")
17511829

17521830
except Exception as e:
17531831
msg = "Error deleting the admin account"
@@ -1926,7 +2004,7 @@ def rename_shop_post():
19262004

19272005
@admin.route("/announce-stripe-connect", methods=["GET"])
19282006
def announce_shop_stripe_connect_ids():
1929-
"""Accounce this shop's stripe connect account id(s)
2007+
"""Announce this shop's stripe connect account id(s)
19302008
to the STRIPE_CONNECT_ACCOUNT_ANNOUNCER_HOST
19312009
- stripe_live_connect_account_id
19322010
- stripe_test_connect_account_id
@@ -2162,7 +2240,7 @@ def enable_geo_currency():
21622240
@admin.route("/spamcheck/<string:account_name>")
21632241
@login_required
21642242
def check_spam(account_name) -> int:
2165-
"""Check if shop name is likley to be spam or not"""
2243+
"""Check if shop name is likely to be spam or not"""
21662244
from subscribie.anti_spam_subscribie_shop_names.run import detect_spam_shop_name
21672245

21682246
return str(detect_spam_shop_name(account_name))

subscribie/blueprints/admin/templates/admin/dashboard.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,16 @@ <h3 class="card-title justify-content-between d-flex">Stats</h3>
113113
Export Subscribers
114114
</a>
115115
</div>
116+
<div class="px-3 py-3 my-3">
117+
<p class="card-subtitle mb-3 text-muted">
118+
Perform bulk actions across all subscribers such as
119+
pause all Subscribers payment collections.
120+
</p>
121+
<a class="btn btn-success btn-block"
122+
href="{{ url_for('admin.subscribers_bulk_operations_index') }}">
123+
Pause all Subscribers payment collections
124+
</a>
125+
</div>
116126
</div>
117127
</div>
118128
<div class="card">

subscribie/blueprints/admin/templates/admin/subscriber/show_subscriber.html

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ <h4 id="whyNegativeBalance">Why might this Subscriber owe money?</h4>
136136
<p>The card had insufficient funds to complete the purchase at the time it was charged</p>
137137
{% elif decline_code == "generic_decline" %}
138138
<details><summary>What does "<em>{{ decline_code }}</em>" mean?</summary>
139-
<p>The card was declined for an unknown reason or incorrectly flaged as a risky payment.</p>
139+
<p>The card was declined for an unknown reason or incorrectly flagged as a risky payment.</p>
140140
<p>The customer needs to contact their card issuer for more information.</p>
141141
{% endif %}
142142
</li>
@@ -208,7 +208,19 @@ <h3>Subscriptions</h3>
208208
{% for subscription in person.subscriptions %}
209209
<li class="list-group-item">
210210
<strong>{{ subscription.plan.title }}</strong><br />
211-
<strong>Status:</strong> {{ subscription.stripe_status or 'Unknown' }} {% if subscription.transactions|length > 0 %}<a href="{{ url_for('admin.refresh_subscription', subscription_uuid=subscription.uuid, person_id=person.id) }}">(Refresh)</a>{% endif %}
211+
<strong>Subscription Status:</strong> {{ subscription.stripe_status or 'Unknown' }} {% if subscription.transactions|length > 0 %}<a href="{{ url_for('admin.refresh_subscription', subscription_uuid=subscription.uuid, person_id=person.id) }}">(Refresh)</a>{% endif %}
212+
<br />
213+
<strong>Payment Collection Status:</strong>
214+
{% if subscription.plan.requirements and subscription.plan.requirements.subscription %}
215+
{% if subscription.stripe_pause_collection == "keep_as_draft" %}
216+
Paused - (keep_as_draft)
217+
<details style="display: inline">
218+
<summary><em><small>explain</small></em></summary>
219+
<p class="alert alert-info">Temporarily offer services for free and have the possibility of collecting payment later.</p>
220+
<p>Invoices are still generated, however no payment collection attempts are made against them.
221+
</details>
222+
{% endif %}
223+
{% endif %}
212224
<br />
213225
<strong>Started: </strong> {{ subscription.created_at.strftime('%d-%m-%Y') }}<br />
214226
<strong>Interval Unit:</strong>
@@ -258,7 +270,7 @@ <h3>Subscriptions</h3>
258270
{% else %}
259271
<ul>
260272
{% for document in subscription.documents %}
261-
{# Show documents assocated with subscription (if any) #}
273+
{# Show documents associated with subscription (if any) #}
262274
<li><a href="{{ url_for('document.show_document', document_uuid=document.uuid) }}">
263275
{{ document.name }}</a> |
264276
{{ document.created_at.strftime('%Y-%m-%d') }}</li>
@@ -300,7 +312,7 @@ <h3>Invoices</h3>
300312
padding: 8px;
301313
}
302314
#subscriber-invoices tr {
303-
border-bottom: 1px solid lightgray;
315+
border-bottom: 1px solid lightgrey;
304316
}
305317
#subscriber-invoices td {
306318
text-align: center;

subscribie/blueprints/admin/templates/admin/subscribers-archived.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ <h2 class="text-center text-dark mb-3">Archived Subscribers</h2>
9090
(No up-front fee)
9191
{% endif %}
9292
</span>
93-
<li><strong>Status: </strong>
93+
<li><strong>Subscription Status: </strong>
9494
{% if subscription.plan.requirements and subscription.plan.requirements.subscription %}
9595
{{ subscription.stripe_status }}
9696
{% else %}

0 commit comments

Comments
 (0)