Skip to content

Commit

Permalink
Merge branch '773-outstanding-payments' into 1276-upgrade-to-python-3…
Browse files Browse the repository at this point in the history
…12-use-rye
  • Loading branch information
chrisjsimpson committed Jan 31, 2024
2 parents aed69c1 + 51fd805 commit 074a9d4
Show file tree
Hide file tree
Showing 12 changed files with 640 additions and 121 deletions.
41 changes: 35 additions & 6 deletions subscribie/blueprints/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,41 @@ def dec2pence(amount):
return int(math.ceil(float(amount) * 100))


def ordinal(n):
"""
Convert 1 -> 1st, 2 -> 2nd etc...
Credit Dr. Drang 2020 https://leancrew.com/all-this/2020/06/ordinals-in-python/
"""
return str(n) + (
"th" if 4 <= n % 100 <= 20 else {1: "st", 2: "nd", 3: "rd"}.get(n % 10, "th")
) # noqa: E501


@admin.app_template_filter()
def timestampToDate(timestamp: str):
if timestamp is None:
return None
return datetime.fromtimestamp(int(timestamp)).strftime("%d-%m-%Y")


def dtStylish(dt, f):
"""
Add "nd", "th" and "rd" to date formatting for
human readable dates.
Credit https://stackoverflow.com/a/16671271
"""
return dt.strftime(f).replace("{th}", ordinal(dt.day))


@admin.app_template_filter()
def timestampToHumanReadableDate(timestamp: str):
if timestamp is None:
return None
dt = datetime.fromtimestamp(int(timestamp))

return dtStylish(dt, "{th} %B %Y")


def store_stripe_transaction(stripe_external_id):
"""Store Stripe invoice payment in transactions table"""
stripe.api_key = get_stripe_secret_key()
Expand Down Expand Up @@ -768,7 +796,7 @@ def list_documents():
documents = (
Document.query.where(Document.type == "terms-and-conditions-agreed")
.execution_options(include_archived=True)
.where(Document.read_only == True)
.where(Document.read_only == True) # noqa: E712
.all()
)
else:
Expand Down Expand Up @@ -1249,11 +1277,11 @@ def subscribers():
)
elif action == "show_donors":
query = query.filter(Person.transactions)
query = query.where(Transaction.is_donation == True)
query = query.where(Transaction.is_donation == True) # noqa: E712

elif action == "show_one_off_payments":
query = query.filter(Person.subscriptions)
query = query.where(Subscription.stripe_subscription_id == None)
query = query.where(Subscription.stripe_subscription_id == None) # noqa: E711

people = query.order_by(desc(Person.created_at))

Expand Down Expand Up @@ -1356,6 +1384,7 @@ def invoices():
@login_required
def transactions():
action = request.args.get("action", None)

page = request.args.get("page", 1, type=int)
plan_title = request.args.get("plan_title", None)
subscriber_name = request.args.get("subscriber_name", None)
Expand Down Expand Up @@ -1390,10 +1419,10 @@ def transactions():
query = query.filter(False)

if action == "show_refunded":
query = query.filter(Transaction.external_refund_id != None)
query = query.filter(Transaction.external_refund_id != None) # noqa: E711

if action == "show_donations":
query = query.filter(Transaction.is_donation == True)
query = query.filter(Transaction.is_donation == True) # noqa: E712

transactions = query.paginate(page=page, per_page=10)
if transactions.total == 0:
Expand Down Expand Up @@ -1946,7 +1975,7 @@ def change_thank_you_url():
if request.form.get("default"):
settings.custom_thank_you_url = None
database.session.commit()
flash("Custom thank you url changed to default")
flash("Thank page has been set back to the to the default thank you page.")
return render_template(
"admin/settings/custom_thank_you_page.html",
form=form,
Expand Down
83 changes: 79 additions & 4 deletions subscribie/blueprints/admin/invoice.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
from . import admin
import json
import logging
from subscribie.auth import login_required, stripe_connect_id_required
from subscribie.database import database
from subscribie.models import UpcomingInvoice, Subscription
from subscribie.models import UpcomingInvoice, Subscription, StripeInvoice, Person
from subscribie.utils import (
get_stripe_secret_key,
get_stripe_connect_account,
get_stripe_connect_account_id,
)
from subscribie.utils import (
getBadInvoices,
get_stripe_invoices,
)
from flask import render_template, flash, request, redirect, url_for
Expand All @@ -21,13 +22,87 @@
@login_required
@stripe_connect_id_required
def failed_invoices():
stripe.api_key = get_stripe_secret_key()
stripe_connect_account_id = get_stripe_connect_account_id()
if "refreshFailedInvoices" in request.args:
flash("Invoice statuses are being refreshed")
get_stripe_invoices()

badInvoices = getBadInvoices()
# Get failed invoices, grouped by person and their invoices
failedInvoices = (
database.session.query(StripeInvoice)
.join(Subscription, StripeInvoice.subscribie_subscription)
.join(Person, Subscription.person)
.group_by(Person.id, StripeInvoice.id)
.where(StripeInvoice.status == "open")
.where(StripeInvoice.next_payment_attempt == None) # noqa: E711
.execution_options(include_archived=True)
.order_by(Person.given_name)
.all()
)
# Build dictionary of person uuid -> (bad) invoices so
# that it's easier for template to display bad invoices broken down
# per person
subscribersWithFailedInvoicesMap = {}

for failedInvoice in failedInvoices:
# Populate map with each Person.uuid
if (
failedInvoice.subscribie_subscription.person.uuid
not in subscribersWithFailedInvoicesMap
):
# Create person uuid key in map
subscribersWithFailedInvoicesMap[
failedInvoice.subscribie_subscription.person.uuid
] = {}
# Create empty list to store persons bad invoices
subscribersWithFailedInvoicesMap[
failedInvoice.subscribie_subscription.person.uuid
]["failedInvoices"] = []

# Create reference to person object via invoice reference
subscribersWithFailedInvoicesMap[
failedInvoice.subscribie_subscription.person.uuid
]["person"] = failedInvoice.subscribie_subscription.person

# Add hosted_invoice_url attribute to invoice
try:
stripe_invoice = stripe.Invoice.retrieve(
id=failedInvoice.id, stripe_account=stripe_connect_account_id
)
setattr(
failedInvoice,
"hosted_invoice_url",
stripe_invoice.hosted_invoice_url,
)
except Exception as e:
log.error(
f"Unable to get/set hosted_invoice_url for invoice: {failedInvoice.id}. {e}" # noqa: E501
)

# Get stripe_decline_code if possible
try:
stripeRawInvoice = json.loads(failedInvoice.stripe_invoice_raw_json)

payment_intent_id = stripeRawInvoice["payment_intent"]
stripe_decline_code = stripe.PaymentIntent.retrieve(
payment_intent_id,
stripe_account=stripe_connect_account_id,
).last_payment_error.decline_code
setattr(failedInvoice, "stripe_decline_code", stripe_decline_code)
except Exception as e:
log.debug(
f"Failed to get stripe_decline_code for invoice {failedInvoice.id}. Exeption: {e}" # noqa: E501
)

# Insert invoices per person
subscribersWithFailedInvoicesMap[
failedInvoice.subscribie_subscription.person.uuid
]["failedInvoices"].append(failedInvoice)

return render_template(
"admin/invoice/failed_invoices.html", badInvoices=badInvoices
"admin/invoice/failed_invoices.html",
debtors=subscribersWithFailedInvoicesMap,
)


Expand Down
34 changes: 33 additions & 1 deletion subscribie/blueprints/admin/subscriber.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from subscribie.auth import login_required
from subscribie.models import Person
from flask import render_template
from subscribie.utils import get_stripe_secret_key, get_stripe_connect_account, get_stripe_connect_account_id
import stripe
import logging

log = logging.getLogger(__name__)
Expand All @@ -10,8 +12,38 @@
@admin.route("/show-subscriber/<subscriber_id>", methods=["GET", "POST"])
@login_required
def show_subscriber(subscriber_id):
stripe.api_key = get_stripe_secret_key()
stripe_connect_account_id = get_stripe_connect_account_id()

person = Person.query.execution_options(include_archived=True).get(subscriber_id)
return render_template("admin/subscriber/show_subscriber.html", person=person)
customer_balance_list = person.balance() # See models.py 'class Person'
invoices = person.invoices()
open_invoices = person.failed_invoices()
# Add hosted_invoice_url attribute to all open invoices
try:
for index, open_invoice in enumerate(open_invoices):
stripe_invoice = stripe.Invoice.retrieve(id=open_invoice.id, stripe_account=stripe_connect_account_id)
setattr(open_invoices[index], 'hosted_invoice_url', stripe_invoice.hosted_invoice_url)
except Exception as e:
log.error(f"Unable to get/set hosted_invoice_url for invoice: {open_invoice.id}. {e}")

# Try to be helpful to the shop owner by highlighting recent payment
# payment stripe_decline_code errors (if any).
collection_decline_codes = []
for invoice in invoices:
try:
collection_decline_codes.append(invoice.stripe_decline_code)
except AttributeError:
pass

return render_template(
"admin/subscriber/show_subscriber.html",
person=person,
invoices=invoices,
open_invoices=open_invoices,
customer_balance_list=customer_balance_list,
collection_decline_codes=set(collection_decline_codes),
)


@admin.route("/charge/subscriber/<person_id>", methods=["GET"])
Expand Down
Loading

0 comments on commit 074a9d4

Please sign in to comment.