-
Notifications
You must be signed in to change notification settings - Fork 244
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
245 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
161 changes: 161 additions & 0 deletions
161
payments/management/commands/update_subscription_price.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |