372 lines
14 KiB
Python
372 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 common.utils import html_to_text
|
|
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 = (
|
|
f'{base_path}/{mail_name}_subject.txt',
|
|
f'{base_path}/{mail_name}.html',
|
|
)
|
|
|
|
subject: str = loader.render_to_string(subj_tmpl, context)
|
|
context['subject'] = subject.strip()
|
|
|
|
email_body_html = loader.render_to_string(html_tmpl, context)
|
|
# Generate plain text content from the HTML one
|
|
email_body_txt = html_to_text(email_body_html)
|
|
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)
|