Anna Sirota
2d66f8dfee
All other ticket orders should only be created on checkout_session.completed.
288 lines
10 KiB
Python
288 lines
10 KiB
Python
"""Tickets tasks, such as sending of emails and requesting of invoices."""
|
|
from typing import Dict, Any, List
|
|
import logging
|
|
|
|
from background_task import background
|
|
from django.contrib.auth import get_user_model
|
|
from django.utils import timezone
|
|
from django.conf import settings
|
|
|
|
from background_task.tasks import TaskSchedule
|
|
|
|
from emails.util import get_template_context, is_noreply, construct_and_send_email
|
|
from .saleor_client import get_order, request_invoice
|
|
import tickets.models
|
|
import tickets.stripe_utils
|
|
|
|
logger = logging.getLogger(__name__)
|
|
logger.setLevel(logging.DEBUG)
|
|
User = get_user_model()
|
|
|
|
|
|
def _send_mail_to(user: User, email_name: str, **email_context) -> int:
|
|
email = user.email
|
|
if is_noreply(email):
|
|
logger.warning('Not sending %s to a no-reply address %s', email_name, email)
|
|
return 0
|
|
|
|
logger.debug('Sending %s to %s', email_name, email)
|
|
|
|
context = {
|
|
'user': user,
|
|
**email_context,
|
|
**get_template_context(),
|
|
}
|
|
|
|
return construct_and_send_email(email_name, context, recipient_list=[email])
|
|
|
|
|
|
@background()
|
|
def send_mail_bank_transfer_required(
|
|
ticket_id: int, email_name: str = 'bank_transfer_required'
|
|
) -> int:
|
|
"""Send out an email notifying about the required bank transfer payment.
|
|
|
|
`email_name` can be used to override the email text for
|
|
sending other messages about unpaid ticket orders, e.g. via admin actions.
|
|
"""
|
|
ticket = tickets.models.Ticket.objects.get(pk=ticket_id)
|
|
|
|
if ticket.is_paid or ticket.is_free:
|
|
logger.error(
|
|
'Ticket pk=%s is either paid or free, not sending payment instructions',
|
|
ticket_id,
|
|
)
|
|
return False
|
|
|
|
user = ticket.user
|
|
res = _send_mail_to(user, email_name=email_name, ticket=ticket, order=ticket.order)
|
|
return res
|
|
|
|
|
|
@background()
|
|
def send_mail_confirm_tickets(
|
|
ticket_id: int, user_id: int, email_name: str = 'ticket_confirmed'
|
|
) -> int:
|
|
"""Send an email to a confirmed Conference attendee.
|
|
|
|
First and foremost, this task is used to automatically confirm paid tickets
|
|
via email, however `email_name` can be used to override the email text for
|
|
sending other messages to attendees, e.g. via admin actions.
|
|
"""
|
|
ticket = tickets.models.Ticket.objects.get(pk=ticket_id)
|
|
if not ticket.is_claimed_by(user_id=user_id):
|
|
logger.error('User pk=%s is not listed in ticket pk=%s claims', user_id, ticket_id)
|
|
return 0
|
|
|
|
claim = ticket.claims.get(user_id=user_id)
|
|
if email_name == 'ticket_confirmed' and claim.confirmation_sent_at:
|
|
logger.error('Ticket pk=%s confirmation was already sent to user pk=%s', ticket_id, user_id)
|
|
return 0
|
|
|
|
if not ticket.is_paid and not ticket.is_free:
|
|
logger.error(
|
|
'Ticket pk=%s is neither paid nor free, not sending %s email',
|
|
email_name,
|
|
ticket_id,
|
|
)
|
|
return 0
|
|
|
|
user = User.objects.get(pk=user_id)
|
|
res = _send_mail_to(user, email_name=email_name, ticket=ticket, order=ticket.order)
|
|
|
|
if email_name == 'ticket_confirmed':
|
|
claim.confirmation_sent_at = timezone.now()
|
|
claim.save(update_fields={'confirmation_sent_at'})
|
|
return res
|
|
|
|
|
|
@background()
|
|
def send_mail_tickets_paid(ticket_id: int, email_name: str = 'tickets_paid') -> int:
|
|
"""Send out an email with instruction about ticket claim link."""
|
|
ticket = tickets.models.Ticket.objects.get(pk=ticket_id)
|
|
|
|
if not ticket.is_paid and not ticket.is_free:
|
|
logger.error(
|
|
'Ticket pk=%s is neither paid nor free, not sending confirmation for paid tickets',
|
|
ticket_id,
|
|
)
|
|
return 0
|
|
|
|
if not ticket.unclaimed:
|
|
logger.error(
|
|
'All tickets pk=%s are claimed, not sending additional confirmation for paid tickets',
|
|
ticket_id,
|
|
)
|
|
return 0
|
|
|
|
user = ticket.user
|
|
res = _send_mail_to(user, email_name=email_name, ticket=ticket, order=ticket.order)
|
|
|
|
return res
|
|
|
|
|
|
@background()
|
|
def send_mail_general_info(ticket_id: int, user_id: int) -> int:
|
|
"""Send out an email with general info about Conference ticket to an attendee."""
|
|
ticket = tickets.models.Ticket.objects.get(pk=ticket_id)
|
|
if not ticket.is_claimed_by(user_id=user_id):
|
|
logger.error('User pk=%s is not listed in ticket pk=%s claims', user_id, ticket_id)
|
|
return 0
|
|
|
|
claim = ticket.claims.get(user_id=user_id)
|
|
if claim.general_info_sent_at:
|
|
logger.error('Ticket pk=%s general info was already sent to user pk=%s', ticket_id, user_id)
|
|
return 0
|
|
|
|
if not ticket.is_paid and not ticket.is_free:
|
|
logger.error(
|
|
'Ticket pk=%s is neither paid nor free, not sending general info email',
|
|
ticket_id,
|
|
)
|
|
return 0
|
|
|
|
user = User.objects.get(pk=user_id)
|
|
email_name = 'general_information'
|
|
res = _send_mail_to(user, email_name=email_name, ticket=ticket, order=ticket.order)
|
|
|
|
claim.general_info_sent_at = timezone.now()
|
|
claim.save(update_fields={'general_info_sent_at'})
|
|
return res
|
|
|
|
|
|
@background
|
|
def generate_invoice(ticket_id: int) -> Dict[Any, Any]:
|
|
if settings.ACTIVE_PAYMENT_BACKEND == 'stripe':
|
|
return {}
|
|
ticket = tickets.models.Ticket.objects.get(pk=ticket_id)
|
|
if not ticket.order_id:
|
|
logger.error(
|
|
'Unable to request an invoice, ticket pk=%s is missing an order ID',
|
|
ticket.pk,
|
|
)
|
|
return
|
|
|
|
invoice = request_invoice(order_id=ticket.order_id)
|
|
|
|
# Save invoice URL to display it in My Tickets
|
|
ticket.invoice_url = invoice['url']
|
|
ticket.save(update_fields={'invoice_url'})
|
|
|
|
logger.info('Generated an invoice for ticket pk=%s', ticket_id)
|
|
return invoice
|
|
|
|
|
|
def _set_profile_fields_from_saleor_order(ticket, profile):
|
|
order = get_order(ticket.order_id)
|
|
billing_address = order['billingAddress']
|
|
|
|
update_fields = []
|
|
if not profile.full_name:
|
|
profile.full_name = f"{billing_address['firstName']} {billing_address['lastName']}"
|
|
update_fields.append('full_name')
|
|
if not profile.country:
|
|
profile.country = billing_address['country']['code']
|
|
update_fields.append('country')
|
|
if not profile.company and billing_address['companyName']:
|
|
profile.company = billing_address['companyName']
|
|
update_fields.append('company')
|
|
return update_fields
|
|
|
|
|
|
def _set_profile_fields_from_stripe_order(ticket, profile):
|
|
update_fields = []
|
|
order = ticket.order
|
|
payment_intent = order.payment_intent
|
|
charge = payment_intent.latest_charge
|
|
|
|
update_fields = []
|
|
customer_details = order.customer_details
|
|
billing_details = charge.billing_details if charge else None
|
|
name = address = None
|
|
if billing_details:
|
|
name = billing_details.name
|
|
address = billing_details.address
|
|
if not profile.full_name and name:
|
|
profile.full_name = name
|
|
update_fields.append('full_name')
|
|
if not profile.country and address:
|
|
profile.country = address['country']
|
|
update_fields.append('country')
|
|
if not profile.company and customer_details.tax_ids:
|
|
profile.company = customer_details.name
|
|
update_fields.append('company')
|
|
return update_fields
|
|
|
|
|
|
@background()
|
|
def update_profile_from_ticket_order(ticket_id: int) -> List[str]:
|
|
"""Update profile using billing address from ticket order."""
|
|
ticket = tickets.models.Ticket.objects.get(pk=ticket_id)
|
|
user = ticket.user
|
|
profile = user.profile
|
|
|
|
update_fields = None
|
|
if ticket.is_saleor:
|
|
update_fields = _set_profile_fields_from_saleor_order(ticket, profile)
|
|
elif ticket.is_stripe:
|
|
update_fields = _set_profile_fields_from_stripe_order(ticket, profile)
|
|
|
|
if update_fields:
|
|
profile.save(update_fields=update_fields)
|
|
logger.info(
|
|
'Updated fields %s of profile user pk=%s based on billing address',
|
|
update_fields,
|
|
profile.user.pk,
|
|
)
|
|
return update_fields
|
|
|
|
|
|
@background()
|
|
def handle_order_fully_paid(payload: List[Dict[str, Any]]) -> bool:
|
|
"""Mark a matching ticket order as paid."""
|
|
order_token = payload[0]['token']
|
|
ticket = tickets.models.Ticket.objects.filter(order_token=order_token, is_paid=False).first()
|
|
if not ticket:
|
|
return False
|
|
ticket.is_paid = True
|
|
ticket.save(update_fields={'is_paid'})
|
|
logger.info('Ticket pk=%s is now fully paid', ticket.pk)
|
|
|
|
# Claim the ticket if only one was bought and user doesn't have other tickets.
|
|
ticket.claim_if_paid_for_one()
|
|
|
|
# Request invoice unless invoice URL is already set
|
|
if not ticket.invoice_url:
|
|
generate_invoice(ticket_id=ticket.pk)
|
|
return True
|
|
|
|
|
|
@background(schedule={'action': TaskSchedule.RESCHEDULE_EXISTING})
|
|
def handle_charge_refunded(payload: dict):
|
|
"""Handle payload of Stripe's charge.refunded event."""
|
|
payload = tickets.stripe_utils.construct_event(payload)
|
|
tickets.stripe_utils.process_charge_refunded(payload)
|
|
|
|
|
|
@background(schedule={'action': TaskSchedule.RESCHEDULE_EXISTING})
|
|
def handle_checkout_session_completed(payload: dict, **kwargs):
|
|
"""Handle payload of Stripe's checkout_session.completed event."""
|
|
payload = tickets.stripe_utils.construct_event(payload)
|
|
session = payload.get('data', {}).get('object', {})
|
|
assert session['object'] == 'checkout.session'
|
|
tickets.stripe_utils.process_checkout_session_completed(session['id'])
|
|
|
|
|
|
@background(schedule={'action': TaskSchedule.RESCHEDULE_EXISTING})
|
|
def handle_payment_intent_requires_action(payload: dict):
|
|
"""Create an unpaid Ticket based onpayment_intent.requires_action event."""
|
|
payload = tickets.stripe_utils.construct_event(payload)
|
|
payment_intent = payload.get('data', {}).get('object', {})
|
|
# We only expect to have to handle next actions for bank transfers
|
|
next_action_type = payment_intent['next_action']['type']
|
|
if next_action_type not in {'display_bank_transfer_instructions'}:
|
|
logger.warning('Ignoring payment intent with next action "%s"', next_action_type)
|
|
return
|
|
session = tickets.stripe_utils.retrieve_session_for_payment_intent(payment_intent['id'])
|
|
tickets.stripe_utils.process_checkout_session_completed(session['id'])
|