Skip to content

Commit

Permalink
script: update stripe subscriptions
Browse files Browse the repository at this point in the history
  • Loading branch information
vas3k committed Jan 5, 2025
1 parent 8d252d8 commit bd9ac4b
Show file tree
Hide file tree
Showing 3 changed files with 245 additions and 2 deletions.
82 changes: 82 additions & 0 deletions frontend/html/emails/price_increase.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{% extends "emails/layout.html" %}
{% load static %}
{% load text_filters %}

{% block css %}
<style>
h1, h2, h3, header {
text-align: left;
}

.button {
display: inline-block;
padding: 17px 30px;
box-sizing: border-box;
text-decoration: none;
border-radius: 20px;
background-color: #000;
border: solid 2px #000;
color: #FFF;
text-align: center;
cursor: pointer;
line-height: 1em;
font-size: 25px;
margin: 20px 0 30px;
}

.button:hover {
color: #000 !important;
background-color: #FFF !important;
border-color: #000 !important;
}

p {
font-size: 19px;
}

</style>
{% endblock %}

{% block title %}
€{{ old_price }} -> €{{ new_price }} в год
{% endblock %}

{% block body %}
<p>
Привет, это Вастрик! 👋
</p>

<p>
Во-первых, спасибо, что ты с нами давно! Благодаря таким людям, которые много лет верят в Клуб, он и существует уже 5 лет.
И даже когда мы в 2022-м уже поднимали цены, мы решили оставить старые цены для тех, кто не отменял подписку.
</p>

<p>
Во-вторых, поднимать цены все-таки приходится. Расходы на содержание Клуба растут, налоги растут, но и ценность Клуба как коммьюнити тоже растёт, так что, надеюсь, это компенсирует.
</p>

<p>
Для новых членов Клуба год стоит уже <strong>€25 + VAT</strong>, но для старых подписчиков, как ты, мы решили снова сделать подарок и <strong>в этом году поднять ее лишь до €{{ new_price }} + VAT</strong> (сам VAT теперь зависит от вашей страны).
</p>

<p>
Новая цена начнет действовать с момента следующего автопродления подписки, то есть с <strong>{{ current_period_end | date:"j E Y" }}</strong>. Посмотреть сколько еще осталось и управлять активными подписками можно вот по этой кнопке:
</p>

<p>
<a href="https://vas3k.club/user/me/edit/monies/" class="button">Мои подписки</a>
</p>

<p>
Клуб существует за счет взносов его членов, что позволяет нам окупаться без повесточки от инвесторов или агрессивной рекламы, как у других.
</p>

<p>
Спасибо за понимание 🙏
</p>

<br><br><br>
{% endblock %}

{% block unsubscribe %}
{% endblock %}
4 changes: 2 additions & 2 deletions frontend/html/emails/subscription_expired.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
</p>

<p>
<a href="{% url "edit_payments" user.slug %}" class="button">Продлить подписку</a>
<a href="{{ settings.APP_HOST }}{% url "edit_payments" user.slug %}" class="button">Продлить подписку</a>
</p>

<p>
Expand All @@ -66,7 +66,7 @@
</p>

<p>
Ну а если вам у нас не понравилось, вы можете <a href="{% url "edit_account" user.slug %}">удалить аккаунт</a> и мы сотрем ваш профиль насовсем.
Ну а если вам у нас не понравилось, вы можете <a href="{{ settings.APP_HOST }}{% url "edit_account" user.slug %}">удалить аккаунт</a> и мы сотрем ваш профиль насовсем.
</p>

<p>
Expand Down
161 changes: 161 additions & 0 deletions payments/management/commands/update_subscription_price.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
from datetime import datetime

import stripe
import logging
from typing import List, Optional

from django.core.management import BaseCommand
from django.conf import settings
from django.template.loader import render_to_string

from notifications.email.sender import send_transactional_email

stripe.api_key = settings.STRIPE_API_KEY


BATCH_SIZE = 100
MAX_LIMIT = 1000


class Command(BaseCommand):
help = "Update Stripe subscription prices"

def add_arguments(self, parser):
parser.add_argument("--old-price-id", type=str, required=True)
parser.add_argument("--new-price-id", type=str, required=True)
parser.add_argument("--limit", type=int, required=False)

def handle(self, *args, **options):
# Configuration
old_price_id = options.get("old_price_id")
new_price_id = options.get("new_price_id")
global_limit = options.get("limit") or MAX_LIMIT

# Get prices
old_stripe_price = stripe.Price.retrieve(old_price_id)
new_stripe_price = stripe.Price.retrieve(new_price_id)

self.stdout.write(f"Price update from {old_stripe_price.unit_amount // 100} "
f"to {new_stripe_price.unit_amount // 100}")

# Fetch existing subscriptions
subscriptions = fetch_subscriptions(old_price_id, limit=global_limit)
self.stdout.write(f"Found {len(subscriptions)} subscriptions to update")

# For stats
success_count = 0
failure_count = 0

for subscription in subscriptions:
customer_email = subscription.customer.email
current_period_end = datetime.fromtimestamp(subscription.current_period_end)

self.stdout.write(f"Customer: {customer_email}, ID {subscription.id} "
f"Subscription: {subscription['items']['data']}")

result = update_subscription_price(
subscription,
old_price_id,
new_price_id,
)

if result:
self.stdout.write(f"Sending email to {customer_email}, period ends {current_period_end}...")

email = render_to_string("emails/price_increase.html", {
"old_price": old_stripe_price.unit_amount // 100,
"new_price": new_stripe_price.unit_amount // 100,
"current_period_end": current_period_end,
})

send_transactional_email(
recipient=customer_email,
subject=f"🥲 Долор уже не тот, что раньше",
html=email,
)

success_count += 1
else:
failure_count += 1

self.stdout.write(f"Next one...")

self.stdout.write(f"""
Price update completed:
- Total subscriptions processed: {len(subscriptions)}
- Successful updates: {success_count}
- Failed updates: {failure_count}
""")


def fetch_subscriptions(old_price_id: str, limit: int = MAX_LIMIT) -> List[stripe.Subscription]:
subscriptions = []
has_more = True
starting_after = None

while has_more and len(subscriptions) < limit:
try:
result = stripe.Subscription.list(
limit=BATCH_SIZE,
price=old_price_id,
status="active",
starting_after=starting_after,
expand=['data.customer']
)

subscriptions.extend(result.data)

has_more = result.has_more

if has_more and result.data:
starting_after = result.data[-1].id

except stripe.error.StripeError as e:
logging.error(f"Error fetching subscriptions: {str(e)}")
raise

return subscriptions[:limit]


def update_subscription_price(
subscription: stripe.Subscription,
old_price_id: str,
new_price_id: str,
) -> Optional[stripe.Subscription]:
try:
# Find the subscription item with the old price
sub_item = next(
(item for item in subscription["items"]["data"] if item["price"].id == old_price_id),
None
)

if not sub_item:
logging.warning(f"Subscription {subscription.id} doesn't have the old price ID")
return None

# Update the subscription
updated_subscription = stripe.Subscription.modify(
subscription.id,
automatic_tax={"enabled": True},
proration_behavior="none", # apply only from the next billing cycle
items=[{
'id': sub_item.id,
'price': new_price_id,
}]
)

logging.info(
f"Successfully updated subscription {subscription.id} "
f"for customer {subscription.customer.email}"
)

logging.info(f"Subscription {subscription.id} with sub_item {sub_item.id} "
f"is updated to new price {new_price_id}")
return updated_subscription

except stripe.error.StripeError as e:
logging.error(
f"Error updating subscription {subscription.id} "
f"for customer {subscription.customer.email}: {str(e)}"
)
return None

0 comments on commit bd9ac4b

Please sign in to comment.