Anna Sirota
ec2ce855b9
This also updates plan variations fixture (and historical migrations) to match what is currently in production.
371 lines
14 KiB
Python
371 lines
14 KiB
Python
"""Subscriptions tasks, such as sending of the emails."""
|
|
from typing import Tuple, Dict, Any
|
|
import logging
|
|
|
|
from background_task import background
|
|
from background_task.tasks import TaskSchedule
|
|
from django.conf import settings
|
|
from django.template import loader
|
|
import django.core.mail
|
|
|
|
from looper.stripe_utils import (
|
|
create_active_subscription_from_payment_intent,
|
|
upsert_subscription_payment_method_from_setup_intent,
|
|
process_payment_intent_for_subscription_order,
|
|
)
|
|
import looper.models
|
|
import looper.signals
|
|
|
|
from blog.models import Post
|
|
from common import mailgun
|
|
from common.queries import get_latest_trainings_and_production_lessons
|
|
from emails.util import get_template_context, absolute_url, is_noreply
|
|
import subscriptions.queries as queries
|
|
|
|
logger = logging.getLogger(__name__)
|
|
logger.setLevel(logging.DEBUG)
|
|
|
|
|
|
def _construct_subscription_mail(mail_name: str, context: Dict[str, Any]) -> Tuple[str, str, str]:
|
|
"""Construct a mail about a subscription.
|
|
|
|
:return: tuple (html, text, subject)
|
|
"""
|
|
base_path = 'subscriptions/emails'
|
|
subj_tmpl, html_tmpl, txt_tmpl = (
|
|
f'{base_path}/{mail_name}_subject.txt',
|
|
f'{base_path}/{mail_name}.html',
|
|
f'{base_path}/{mail_name}.txt',
|
|
)
|
|
|
|
subject: str = loader.render_to_string(subj_tmpl, context)
|
|
context['subject'] = subject.strip()
|
|
|
|
email_body_html = loader.render_to_string(html_tmpl, context)
|
|
email_body_txt = loader.render_to_string(txt_tmpl, context)
|
|
return email_body_html, email_body_txt, context['subject']
|
|
|
|
|
|
@background()
|
|
def send_mail_bank_transfer_required(subscription_id: int):
|
|
"""Send out an email notifying about the required bank transfer payment."""
|
|
subscription = looper.models.Subscription.objects.get(pk=subscription_id)
|
|
customer = subscription.customer
|
|
user = customer.user
|
|
email = customer.billing_address.email or user.email
|
|
assert (
|
|
email
|
|
), f'Cannot send notification about bank payment for subscription {subscription.pk}: no email'
|
|
if is_noreply(email):
|
|
logger.debug(
|
|
'Not sending subscription-bank-info notification to no-reply address %s', email
|
|
)
|
|
return
|
|
|
|
# An Unsubscribe record will prevent this message from being delivered by Mailgun.
|
|
# This records might have been previously created for an existing account.
|
|
mailgun.delete_unsubscribe_record(email)
|
|
|
|
logger.debug('Sending subscription-bank-info notification to %s', email)
|
|
|
|
order = subscription.latest_order()
|
|
assert order, "Can't send a notificaton about bank transfer without an existing order"
|
|
|
|
context = {
|
|
'user': user,
|
|
'subscription': subscription,
|
|
'order': order,
|
|
**get_template_context(),
|
|
}
|
|
|
|
mail_name = 'bank_transfer_required'
|
|
email_body_html, email_body_txt, subject = _construct_subscription_mail(mail_name, context)
|
|
|
|
django.core.mail.send_mail(
|
|
subject,
|
|
message=email_body_txt,
|
|
html_message=email_body_html,
|
|
from_email=None, # just use the configured default From-address.
|
|
recipient_list=[email],
|
|
fail_silently=False,
|
|
)
|
|
logger.info('Sent notification about bank transfer to %s', email)
|
|
|
|
|
|
@background()
|
|
def send_mail_subscription_status_changed(subscription_id: int):
|
|
"""Send out an email notifying about the activated subscription."""
|
|
subscription = looper.models.Subscription.objects.get(pk=subscription_id)
|
|
customer = subscription.customer
|
|
user = customer.user
|
|
email = customer.billing_address.email or user.email
|
|
assert email, f'Cannot send notification about subscription {subscription.pk} status: no email'
|
|
if is_noreply(email):
|
|
raise
|
|
logger.debug('Not sending subscription-changed notification to no-reply address %s', email)
|
|
return
|
|
|
|
# An Unsubscribe record will prevent this message from being delivered by Mailgun.
|
|
# This records might have been previously created for an existing account.
|
|
mailgun.delete_unsubscribe_record(email)
|
|
|
|
logger.debug('Sending subscription-changed notification to %s', email)
|
|
|
|
if subscription.status == 'active':
|
|
verb = 'activated'
|
|
else:
|
|
verb = 'deactivated'
|
|
|
|
context = {
|
|
'user': user,
|
|
'subscription': subscription,
|
|
'verb': verb,
|
|
**get_template_context(),
|
|
}
|
|
mail_name = f'subscription_{verb}'
|
|
email_body_html, email_body_txt, subject = _construct_subscription_mail(mail_name, context)
|
|
|
|
django.core.mail.send_mail(
|
|
subject,
|
|
message=email_body_txt,
|
|
html_message=email_body_html,
|
|
from_email=None, # just use the configured default From-address.
|
|
recipient_list=[email],
|
|
fail_silently=False,
|
|
)
|
|
logger.info('Sent subscription-changed notification to %s', email)
|
|
|
|
|
|
@background()
|
|
def send_mail_automatic_payment_performed(order_id: int, transaction_id: int):
|
|
"""Send out an email notifying about the soft-failed payment."""
|
|
order = looper.models.Order.objects.get(pk=order_id)
|
|
transaction = looper.models.Transaction.objects.get(pk=transaction_id)
|
|
customer = order.customer
|
|
user = customer.user
|
|
email = customer.billing_address.email or user.email
|
|
logger.debug('Sending %r notification to %s', order.status, email)
|
|
|
|
# An Unsubscribe record will prevent this message from being delivered by Mailgun.
|
|
# This records might have been previously created for an existing account.
|
|
mailgun.delete_unsubscribe_record(email)
|
|
|
|
subscription = order.subscription
|
|
|
|
pay_url = absolute_url('subscriptions:pay-existing-order', kwargs={'order_id': order.pk})
|
|
receipt_url = absolute_url('subscriptions:receipt', kwargs={'order_id': order.pk})
|
|
|
|
context = {
|
|
'user': user,
|
|
'email': email,
|
|
'order': order,
|
|
'subscription': subscription,
|
|
'pay_url': pay_url,
|
|
'receipt_url': receipt_url,
|
|
'failure_message': transaction.failure_message,
|
|
'payment_method': transaction.payment_method.recognisable_name,
|
|
'maximum_collection_attemps': settings.LOOPER_CLOCK_MAX_AUTO_ATTEMPTS,
|
|
**get_template_context(),
|
|
}
|
|
|
|
mail_name = f'payment_{order.status}'
|
|
email_body_html, email_body_txt, subject = _construct_subscription_mail(mail_name, context)
|
|
|
|
django.core.mail.send_mail(
|
|
subject,
|
|
message=email_body_txt,
|
|
html_message=email_body_html,
|
|
from_email=None, # just use the configured default From-address.
|
|
recipient_list=[email],
|
|
fail_silently=False,
|
|
)
|
|
logger.info('Sent %r notification to %s', order.status, email)
|
|
|
|
|
|
@background()
|
|
def send_mail_managed_subscription_notification(subscription_id: int):
|
|
"""Send out an email notifying a manager about an expiring managed subscription."""
|
|
subscription = looper.models.Subscription.objects.get(pk=subscription_id)
|
|
email = settings.LOOPER_MANAGER_MAIL
|
|
logger.debug(
|
|
'Notifying %s about managed subscription %r passing its next_payment date',
|
|
email,
|
|
subscription.pk,
|
|
)
|
|
|
|
customer = subscription.customer
|
|
user = customer.user
|
|
admin_url = absolute_url(
|
|
'admin:looper_subscription_change',
|
|
kwargs={'object_id': subscription.id},
|
|
)
|
|
|
|
context = {
|
|
'user': user,
|
|
'subscription': subscription,
|
|
'admin_url': admin_url,
|
|
**get_template_context(),
|
|
}
|
|
|
|
mail_name = 'managed_notification'
|
|
email_body_html, email_body_txt, subject = _construct_subscription_mail(mail_name, context)
|
|
|
|
django.core.mail.send_mail(
|
|
subject,
|
|
message=email_body_txt,
|
|
html_message=email_body_html,
|
|
from_email=None, # just use the configured default From-address.
|
|
recipient_list=[email],
|
|
fail_silently=False,
|
|
)
|
|
logger.info(
|
|
'Notified %s about managed subscription %r passing its next_payment date',
|
|
email,
|
|
subscription.pk,
|
|
)
|
|
|
|
|
|
@background()
|
|
def send_mail_subscription_expired(subscription_id: int):
|
|
"""Send out an email notifying about an expired subscription."""
|
|
subscription = looper.models.Subscription.objects.get(pk=subscription_id)
|
|
customer = subscription.customer
|
|
user = customer.user
|
|
|
|
assert (
|
|
subscription.status == 'expired'
|
|
), f'Expected expired, got "{subscription.status} (pk={subscription_id})"'
|
|
# Only send a "subscription expired" email when there are no other active subscriptions
|
|
if queries.has_active_subscription(user):
|
|
logger.error(
|
|
'Not sending subscription-expired notification: pk=%s has other active subscriptions',
|
|
user.pk,
|
|
)
|
|
return
|
|
|
|
# We use account's email here, that email is most likely the same as Blender ID's email
|
|
# they'll use to login into the platform
|
|
email = user.email
|
|
assert email, f'Cannot send notification about subscription {subscription.pk} status: no email'
|
|
if is_noreply(email):
|
|
raise
|
|
logger.debug('Not sending subscription-expired notification to no-reply address %s', email)
|
|
return
|
|
|
|
# An Unsubscribe record will prevent this message from being delivered by Mailgun.
|
|
# This records might have been previously created for an existing account.
|
|
mailgun.delete_unsubscribe_record(email)
|
|
|
|
logger.debug('Sending subscription-expired notification to %s', email)
|
|
context = {
|
|
'user': user,
|
|
'subscription': subscription,
|
|
'latest_trainings': get_latest_trainings_and_production_lessons(),
|
|
'latest_posts': Post.objects.filter(is_published=True)[:5],
|
|
**get_template_context(),
|
|
}
|
|
mail_name = 'subscription_expired'
|
|
email_body_html, email_body_txt, subject = _construct_subscription_mail(mail_name, context)
|
|
|
|
django.core.mail.send_mail(
|
|
subject,
|
|
message=email_body_txt,
|
|
html_message=email_body_html,
|
|
from_email=None, # just use the configured default From-address.
|
|
recipient_list=[email],
|
|
fail_silently=False,
|
|
)
|
|
logger.info('Sent subscription-expired notification to %s', email)
|
|
|
|
|
|
@background()
|
|
def send_mail_no_payment_method(order_id: int):
|
|
"""Notify about [soft-]failed automatic collection of a subscription w/o a payment method."""
|
|
order = looper.models.Order.objects.get(pk=order_id)
|
|
assert (
|
|
order.collection_method == 'automatic'
|
|
), 'send_mail_no_payment_method expects automatic order'
|
|
assert (
|
|
order.subscription.collection_method == 'automatic'
|
|
), 'send_mail_no_payment_method expects automatic subscription'
|
|
assert 'fail' in order.status, f'Unexpected order pk={order_id} status: {order.status}'
|
|
|
|
customer = order.customer
|
|
user = customer.user
|
|
email = customer.billing_address.email or user.email
|
|
logger.debug('Sending %r notification to %s', order.status, email)
|
|
|
|
# An Unsubscribe record will prevent this message from being delivered by Mailgun.
|
|
# This records might have been previously created for an existing account.
|
|
mailgun.delete_unsubscribe_record(email)
|
|
|
|
subscription = order.subscription
|
|
|
|
pay_url = absolute_url('subscriptions:pay-existing-order', kwargs={'order_id': order.pk})
|
|
receipt_url = absolute_url('subscriptions:receipt', kwargs={'order_id': order.pk})
|
|
|
|
context = {
|
|
'user': user,
|
|
'email': email,
|
|
'order': order,
|
|
'subscription': subscription,
|
|
'pay_url': pay_url,
|
|
'receipt_url': receipt_url,
|
|
'failure_message': 'Unsupported payment method, please update subscription',
|
|
'payment_method': 'Unknown',
|
|
'maximum_collection_attemps': settings.LOOPER_CLOCK_MAX_AUTO_ATTEMPTS,
|
|
**get_template_context(),
|
|
}
|
|
|
|
mail_name = f'payment_{order.status}'
|
|
email_body_html, email_body_txt, subject = _construct_subscription_mail(mail_name, context)
|
|
|
|
django.core.mail.send_mail(
|
|
subject,
|
|
message=email_body_txt,
|
|
html_message=email_body_html,
|
|
from_email=None, # just use the configured default From-address.
|
|
recipient_list=[email],
|
|
fail_silently=False,
|
|
)
|
|
logger.info('Sent %r notification to %s', order.status, email)
|
|
|
|
|
|
# Delay handling of this event to avoid racing with the synchronous success page
|
|
@background(schedule={'action': TaskSchedule.RESCHEDULE_EXISTING, 'run_at': 30})
|
|
def handle_payment_intent_succeeded(payload: str):
|
|
"""Handle payload of payment_intent.succeeded event."""
|
|
gateway = looper.models.Gateway.objects.get(name='stripe')
|
|
event = gateway.provider.construct_event(payload)
|
|
payment_intent_id = event.data.object.id
|
|
payment_intent = gateway.provider.retrieve_payment_intent(payment_intent_id)
|
|
metadata = payment_intent.get('metadata', {})
|
|
plan_variation_id = metadata.get('plan_variation_id', None)
|
|
order_id = metadata.get('order_id', None)
|
|
|
|
if plan_variation_id:
|
|
create_active_subscription_from_payment_intent(payload, plan_variation_id)
|
|
|
|
elif order_id:
|
|
order = looper.models.Order.objects.get(pk=order_id)
|
|
trans = process_payment_intent_for_subscription_order(payment_intent, order)
|
|
logger.info(
|
|
'Created transaction pk=%d for order pk=%d from payment intent %s',
|
|
trans.pk,
|
|
order.pk,
|
|
payment_intent_id,
|
|
)
|
|
|
|
else:
|
|
logger.error(
|
|
'Payment intent %s has neither plan_variation_id nor order_id in metadata',
|
|
payment_intent_id,
|
|
)
|
|
|
|
|
|
# Delay handling of this event to avoid racing with the synchronous success page
|
|
@background(schedule={'action': TaskSchedule.RESCHEDULE_EXISTING, 'run_at': 30})
|
|
def handle_setup_intent_succeeded(payload: str):
|
|
"""Handle payload of setup_intent.succeeded event."""
|
|
return upsert_subscription_payment_method_from_setup_intent(payload)
|