def notify_reserved_seat(self, user): """ This function sends an email to notify a user that he has a reserved seat to a retreat for 24h hours. """ merge_data = {'RETREAT_NAME': self.name} plain_msg = render_to_string("reserved_place.txt", merge_data) msg_html = render_to_string("reserved_place.html", merge_data) try: response_send_mail = send_mail( f"Une place est disponible pour la retraite: {self.name}", plain_msg, settings.DEFAULT_FROM_EMAIL, [user.email], html_message=msg_html, ) EmailLog.add(user.email, 'reserved_place', response_send_mail) return response_send_mail except Exception as err: additional_data = { 'title': "Place exclusive pour 24h", 'default_from': settings.DEFAULT_FROM_EMAIL, 'user_email': user.email, 'merge_data': merge_data, 'template': 'reserved_place' } Log.error(source='SENDING_BLUE_TEMPLATE', message=err, additional_data=json.dumps(additional_data)) raise
def send_invoice(to, merge_data): if 'POLICY_URL' not in merge_data.keys(): merge_data['POLICY_URL'] = settings.\ LOCAL_SETTINGS['FRONTEND_INTEGRATION']['POLICY_URL'] plain_msg = render_to_string("invoice.txt", merge_data) msg_html = render_to_string("invoice.html", merge_data) try: response_send_mail = send_mail( "Confirmation d'achat", plain_msg, settings.DEFAULT_FROM_EMAIL, to, html_message=msg_html, ) EmailLog.add(to, 'INVOICE', response_send_mail) except Exception as err: additional_data = { 'title': "Confirmation d'achat", 'default_from': settings.DEFAULT_FROM_EMAIL, 'user_email': to, 'merge_data': merge_data, 'template': 'invoice' } Log.error(source='SENDING_BLUE_TEMPLATE', message=err, additional_data=json.dumps(additional_data)) raise
def notify_for_coupon(email, coupon): """ This function sends an email to notify a user that he has access to a coupon code for his next purchase. """ merge_data = {'COUPON': coupon} plain_msg = render_to_string("coupon_code.txt", merge_data) msg_html = render_to_string("coupon_code.html", merge_data) try: response_send_mail = send_mail( "Coupon rabais", plain_msg, settings.DEFAULT_FROM_EMAIL, [email], html_message=msg_html, ) EmailLog.add(email, 'coupon_code', response_send_mail) return response_send_mail except Exception as err: additional_data = { 'title': "Coupon rabais", 'default_from': settings.DEFAULT_FROM_EMAIL, 'user_email': email, 'merge_data': merge_data, 'template': 'coupon_code' } Log.error(source='SENDING_BLUE_TEMPLATE', message=err, additional_data=json.dumps(additional_data)) raise
def notify_reserved_retreat_seat(user, retreat): """ This function sends an email to notify a user that he has a reserved seat to a retreat for 24h hours. """ wait_queue: WaitQueue = WaitQueue.objects.get(user=user, retreat=retreat) # Setup the url for the activation button in the email wait_queue_url = settings.LOCAL_SETTINGS[ 'FRONTEND_INTEGRATION'][ 'RETREAT_UNSUBSCRIBE_URL'] \ .replace( "{{wait_queue_id}}", str(wait_queue.id) ) merge_data = {'RETREAT_NAME': retreat.name, 'WAIT_QUEUE_URL': wait_queue_url} plain_msg = render_to_string("reserved_place.txt", merge_data) msg_html = render_to_string("reserved_place.html", merge_data) try: response_send_mail = send_mail( "Place exclusive pour 24h", plain_msg, settings.DEFAULT_FROM_EMAIL, [user.email], html_message=msg_html, ) EmailLog.add(user.email, 'reserved_place', response_send_mail) return response_send_mail except Exception as err: additional_data = { 'title': "Place exclusive pour 24h", 'default_from': settings.DEFAULT_FROM_EMAIL, 'user_email': user.email, 'merge_data': merge_data, 'template': 'reserved_place' } Log.error( source='SENDING_BLUE_TEMPLATE', message=err, additional_data=json.dumps(additional_data) ) raise
def send_refund_confirmation_email(self, amount, retreat, order, user, total_amount, amount_tax): # Here the price takes the applied coupon into account, if # applicable. old_retreat = { 'price': (amount * retreat.refund_rate) / 100, 'name': "{0}: {1}".format(_("Retreat"), retreat.name) } # Send order confirmation email merge_data = { 'DATETIME': timezone.localtime().strftime("%x %X"), 'ORDER_ID': order.id, 'CUSTOMER_NAME': user.first_name + " " + user.last_name, 'CUSTOMER_EMAIL': user.email, 'CUSTOMER_NUMBER': user.id, 'TYPE': "Remboursement", 'OLD_RETREAT': old_retreat, 'COST': total_amount, 'TAX': amount_tax, } plain_msg = render_to_string("refund.txt", merge_data) msg_html = render_to_string("refund.html", merge_data) try: response_send_mail = django_send_mail( "Confirmation de remboursement", plain_msg, settings.DEFAULT_FROM_EMAIL, [user.email], html_message=msg_html, ) EmailLog.add(user.email, 'refund', response_send_mail) except Exception as err: additional_data = { 'title': "Confirmation de votre nouvelle adresse courriel", 'default_from': settings.DEFAULT_FROM_EMAIL, 'user_email': user.email, 'merge_data': merge_data, 'template': 'notify_user_of_change_email' } Log.error(source='SENDING_BLUE_TEMPLATE', message=err, additional_data=json.dumps(additional_data)) raise
def notify_user_of_change_email(email, activation_url, first_name): if settings.LOCAL_SETTINGS['EMAIL_SERVICE'] is False: raise MailServiceError(_("Email service is disabled.")) else: merge_data = { "ACTIVATION_URL": activation_url, "FIRST_NAME": first_name, } plain_msg = render_to_string( "notify_user_of_change_email.txt", merge_data ) msg_html = render_to_string( "notify_user_of_change_email.html", merge_data ) try: response_send_mail = django_send_mail( "Confirmation de votre nouvelle adresse courriel", plain_msg, settings.DEFAULT_FROM_EMAIL, [email], html_message=msg_html, ) EmailLog.add( email, 'notify_user_of_change_email', response_send_mail) return response_send_mail except Exception as err: additional_data = { 'title': "Confirmation de votre nouvelle adresse courriel", 'default_from': settings.DEFAULT_FROM_EMAIL, 'user_email': email, 'merge_data': merge_data, 'template': 'notify_user_of_change_email' } Log.error( source='SENDING_BLUE_TEMPLATE', message=err, additional_data=json.dumps(additional_data) ) raise
def notify_user_of_new_account(email, password): if settings.LOCAL_SETTINGS['EMAIL_SERVICE'] is False: raise MailServiceError(_("Email service is disabled.")) else: merge_data = { 'EMAIL': email, 'PASSWORD': password, } plain_msg = render_to_string( "notify_user_of_new_account.txt", merge_data ) msg_html = render_to_string( "notify_user_of_new_account.html", merge_data ) try: response_send_mail = django_send_mail( "Bienvenue à Thèsez-vous?", plain_msg, settings.DEFAULT_FROM_EMAIL, [email], html_message=msg_html, ) EmailLog.add( email, 'notify_user_of_new_account', response_send_mail) return response_send_mail except Exception as err: additional_data = { 'title': "Bienvenue à Thèsez-vous?", 'default_from': settings.DEFAULT_FROM_EMAIL, 'user_email': email, 'merge_data': merge_data, 'template': 'notify_user_of_new_account' } Log.error( source='SENDING_BLUE_TEMPLATE', message=err, additional_data=json.dumps(additional_data) ) raise
def send_mail(users, context, template): """ Uses Anymail to send templated emails. Returns a list of email addresses to which emails failed to be delivered. """ if settings.LOCAL_SETTINGS['EMAIL_SERVICE'] is False: raise MailServiceError(_( "Email service is disabled." )) MAIL_SERVICE = settings.ANYMAIL failed_emails = list() for user in users: message = EmailMessage( subject=None, # required for SendinBlue templates body='', # required for SendinBlue templates to=[user.email] ) message.from_email = None # required for SendinBlue templates # use this SendinBlue template message.template_id = MAIL_SERVICE["TEMPLATES"].get(template) message.merge_global_data = context try: # return number of successfully sent emails response = message.send() EmailLog.add(user.email, template, response) except Exception as err: additional_data = { 'email': user.email, 'context': context, 'template': template } Log.error( source='SENDING_BLUE_TEMPLATE', message=err, additional_data=json.dumps(additional_data) ) raise if not response: failed_emails.append(user.email) return failed_emails
def send_email_from_template_id(users, context, template): """ Uses Anymail to send templated emails. Returns a list of email addresses to which emails failed to be delivered. :param users: The list of users to notify :param context: The context variables of the template :param template: The template of the ESP :return: A list of email addresses to which emails failed to be delivered """ if settings.LOCAL_SETTINGS['EMAIL_SERVICE'] is False: raise MailServiceError(_("Email service is disabled.")) failed_emails = list() for user in users: message = EmailMessage( subject=None, # required for SendinBlue templates body='', # required for SendinBlue templates to=[user.email]) message.from_email = None # required for SendinBlue templates # use this SendinBlue template message.template_id = int(template) message.merge_global_data = context try: # return number of successfully sent emails response = message.send() EmailLog.add(user.email, "Template #" + str(template), response) except Exception as err: additional_data = { 'email': user.email, 'context': context, 'template': "Template #" + str(template) } Log.error(source='SENDING_BLUE_TEMPLATE', message=err, additional_data=json.dumps(additional_data)) raise if not response: failed_emails.append(user.email) return failed_emails
def update(self, instance, validated_data): """ If it is an update operation, we check if users will be affected by the update. If yes, we make sure that the field "force_update" is provided in the request. If provided, cancel reservations and refund affected users tickets. """ if instance.reservations.filter(is_active=True).exists(): if (validated_data.get('start_time') or validated_data.get('end_time')): custom_message = validated_data.get('custom_message') reservation_cancel = instance.reservations.filter( is_active=True) affected_users = User.objects.filter( reservations__in=reservation_cancel) reservations_cancel_copy = copy(reservation_cancel) affected_users.update(tickets=F('tickets') + 1) reservation_cancel.update( is_active=False, cancelation_reason='TM', # TimeSlot modified cancelation_date=timezone.now(), ) for reservation in reservations_cancel_copy: merge_data = { 'TIMESLOT_LIST': [instance], 'SUPPORT_EMAIL': settings.SUPPORT_EMAIL, 'CUSTOM_MESSAGE': custom_message, } plain_msg = render_to_string("cancelation.txt", merge_data) msg_html = render_to_string("cancelation.html", merge_data) try: response_send_mail = send_mail( "Annulation d'un bloc de rédaction", plain_msg, settings.DEFAULT_FROM_EMAIL, [reservation.user.email], html_message=msg_html, ) EmailLog.add(reservation.user.email, 'cancelation', response_send_mail) except Exception as err: additional_data = { 'title': "Annulation d'un bloc de rédaction", 'default_from': settings.DEFAULT_FROM_EMAIL, 'user_email': reservation.user.email, 'merge_data': merge_data, 'template': 'cancelation' } Log.error(source='SENDING_BLUE_TEMPLATE', message=err, additional_data=json.dumps(additional_data)) raise return super(TimeSlotSerializer, self).update( instance, validated_data, )
def destroy(self, request, *args, **kwargs): """ An admin can soft-delete a TimeSlot instance. From an API user perspective, this is no different from a normal delete. The deletion will automatically cancel associated reservations and refund used tickets to the registered users. """ instance = self.get_object() data = request.data serializer = serializers.TimeSlotSerializer(data=data) serializer.is_valid() if 'force_delete' in serializer.errors: raise rest_framework.serializers.ValidationError( {'force_delete': serializer.errors['force_delete']}) if 'custom_message' in serializer.errors: raise rest_framework.serializers.ValidationError( {'custom_message': serializer.errors['custom_message']}) if instance.reservations.filter(is_active=True).exists(): if not data.get('force_delete'): raise rest_framework.serializers.ValidationError({ "non_field_errors": [ _("Trying to do a TimeSlot deletion that affects " "users without providing `force_delete` field set " "to True.") ] }) custom_message = data.get('custom_message') reservation_cancel = instance.reservations.filter(is_active=True) affected_users = User.objects.filter( reservations__in=reservation_cancel) with transaction.atomic(): reservations_cancel_copy = copy(reservation_cancel) # The sequence is important here because the Queryset are # dynamically changing when doing update(). If the # `reservation_cancel` queryset objects are updated first, the # queryset will become empty since it was filtered using # "is_active=True". That would lead to an empty `affected_users` # queryset. # # For-loop required to handle duplicates (if user has multiple # reservations that must be canceled). # user.update(tickets=F('tickets') + 1) for user in affected_users: User.objects.filter(email=user.email).update( tickets=F('tickets') + 1) # Increment tickets reservation_cancel.update( is_active=False, cancelation_reason='TD', # TimeSlot deleted cancelation_date=timezone.now(), ) instance.delete() for reservation in reservations_cancel_copy: merge_data = { 'TIMESLOT_LIST': [instance], 'SUPPORT_EMAIL': settings.SUPPORT_EMAIL, 'CUSTOM_MESSAGE': custom_message, } plain_msg = render_to_string("cancelation.txt", merge_data) msg_html = render_to_string("cancelation.html", merge_data) try: response_send_mail = django_send_mail( "Annulation d'un bloc de rédaction", plain_msg, settings.DEFAULT_FROM_EMAIL, [reservation.user.email], html_message=msg_html, ) EmailLog.add(reservation.user.email, 'cancelation', response_send_mail) except Exception as err: additional_data = { 'title': "Annulation d'un bloc de rédaction", 'default_from': settings.DEFAULT_FROM_EMAIL, 'user_email': reservation.user.email, 'merge_data': merge_data, 'template': 'cancelation' } Log.error(source='SENDING_BLUE_TEMPLATE', message=err, additional_data=json.dumps(additional_data)) raise return Response(status=status.HTTP_204_NO_CONTENT)
def update(self, instance, validated_data): if not instance.exchangeable and validated_data.get('retreat'): raise serializers.ValidationError({ 'non_field_errors': [ _("This reservation is not exchangeable. Please contact us " "to make any changes to this reservation.") ] }) user = instance.user payment_token = validated_data.pop('payment_token', None) single_use_token = validated_data.pop('single_use_token', None) need_transaction = False need_refund = False amount = 0 profile = PaymentProfile.objects.filter(owner=user).first() instance_pk = instance.pk current_retreat: Retreat = instance.retreat coupon = instance.order_line.coupon coupon_value = instance.order_line.coupon_real_value order_line = instance.order_line request = self.context['request'] if not self.context['request'].user.is_staff: validated_data.pop('is_present', None) if not instance.is_active: raise serializers.ValidationError({ 'non_field_errors': [_("This reservation has already been canceled.")] }) with transaction.atomic(): # NOTE: This copy logic should probably be inside the "if" below # that checks if a retreat exchange is done. # Create a copy of the reservation. This copy keeps track of # the exchange. canceled_reservation = instance canceled_reservation.pk = None canceled_reservation.save() instance = Reservation.objects.get(id=instance_pk) canceled_reservation.is_active = False canceled_reservation.cancelation_reason = 'U' canceled_reservation.cancelation_action = 'E' canceled_reservation.cancelation_date = timezone.now() canceled_reservation.save() # Update the reservation instance = super(ReservationSerializer, self).update( instance, validated_data, ) # Update retreat seats free_seats = current_retreat.places_remaining if current_retreat.reserved_seats or free_seats == 1: current_retreat.add_wait_queue_place(user) if validated_data.get('retreat'): # Validate if user has the right to reserve a seat in the new # retreat new_retreat = instance.retreat old_retreat = current_retreat user_waiting = new_retreat.wait_queue.filter(user=user) if not new_retreat.can_order_the_retreat(user): raise serializers.ValidationError({ 'non_field_errors': [ _("There are no places left in the requested " "retreat.") ] }) if user_waiting: user_waiting.delete() if (self.context['view'].action == 'partial_update' and validated_data.get('retreat')): if order_line.quantity > 1: raise serializers.ValidationError({ 'non_field_errors': [ _("The order containing this reservation has a " "quantity bigger than 1. Please contact the " "support team.") ] }) days_remaining = current_retreat.start_time - timezone.now() days_exchange = timedelta( days=current_retreat.min_day_exchange) respects_minimum_days = (days_remaining >= days_exchange) new_retreat_price = validated_data['retreat'].price if current_retreat.price < new_retreat_price: # If the new retreat is more expensive, reapply the # coupon on the new orderline created. In other words, any # coupon used for the initial purchase is applied again # here. need_transaction = True amount = (validated_data['retreat'].price - order_line.coupon_real_value) if not (payment_token or single_use_token): raise serializers.ValidationError({ 'non_field_errors': [ _("The new retreat is more expensive than " "the current one. Provide a payment_token or " "single_use_token to charge the balance.") ] }) if current_retreat.price > new_retreat_price: # If a coupon was applied for the purchase, check if the # real cost of the purchase was lower than the price # difference. # If so, refund the real cost of the purchase. # Else refund the difference between the 2 retreats. need_refund = True price_diff = (current_retreat.price - validated_data['retreat'].price) real_cost = order_line.cost amount = min(price_diff, real_cost) if current_retreat == validated_data['retreat']: raise serializers.ValidationError({ 'retreat': [ _("That retreat is already assigned to this " "object.") ] }) if not respects_minimum_days: raise serializers.ValidationError({ 'non_field_errors': [_("Maximum exchange date exceeded.")] }) if need_transaction and (single_use_token and not profile): # Create external profile try: create_profile_res = create_external_payment_profile( user) except PaymentAPIError as err: raise serializers.ValidationError({ 'message': err, 'detail': err.detail }) # Create local profile profile = PaymentProfile.objects.create( name="Paysafe", owner=user, external_api_id=create_profile_res.json()['id'], external_api_url='{0}{1}'.format( create_profile_res.url, create_profile_res.json()['id'])) # Generate a list of tuples containing start/end time of # existing reservations. start = validated_data['retreat'].start_time end = validated_data['retreat'].end_time active_reservations = Reservation.objects.filter( user=user, is_active=True, ).exclude(pk=instance.pk) for reservation in active_reservations: for date in reservation.retreat.retreat_dates.all(): latest_start = max( date.start_time, start, ) shortest_end = min( date.end_time, end, ) if latest_start < shortest_end: raise serializers.ValidationError({ 'non_field_errors': [ _("This reservation overlaps with another " "active reservations for this user.") ] }) if need_transaction: order = Order.objects.create( user=user, transaction_date=timezone.now(), authorization_id=1, settlement_id=1, ) new_order_line = OrderLine.objects.create( order=order, quantity=1, content_type=ContentType.objects.get_for_model( Retreat), object_id=validated_data['retreat'].id, coupon=coupon, coupon_real_value=coupon_value, ) tax = round(amount * Decimal(TAX_RATE), 2) amount *= Decimal(TAX_RATE + 1) amount = round(amount * 100, 2) retreat = validated_data['retreat'] # Do a complete refund of the previous retreat try: refund_instance = refund_retreat( canceled_reservation, 100, "Exchange retreat {0} for retreat " "{1}".format(str(current_retreat), str(validated_data['retreat']))) except PaymentAPIError as err: if str(err) == PAYSAFE_EXCEPTION['3406']: raise serializers.ValidationError({ 'non_field_errors': [ _("The order has not been charged yet. " "Try again later.") ], 'detail': err.detail }) raise serializers.ValidationError({ 'message': str(err), 'detail': err.detail }) if payment_token and int(amount): # Charge the order with the external payment API try: charge_response = charge_payment( int(round(amount)), payment_token, str(order.id)) except PaymentAPIError as err: raise serializers.ValidationError({ 'message': err, 'detail': err.detail }) elif single_use_token and int(amount): # Add card to the external profile & charge user try: card_create_response = create_external_card( profile.external_api_id, single_use_token) charge_response = charge_payment( int(round(amount)), card_create_response.json()['paymentToken'], str(order.id)) except PaymentAPIError as err: raise serializers.ValidationError({ 'message': err, 'detail': err.detail }) charge_res_content = charge_response.json() order.authorization_id = charge_res_content['id'] order.settlement_id = charge_res_content['settlements'][0][ 'id'] order.reference_number = charge_res_content[ 'merchantRefNum'] order.save() instance.order_line = new_order_line instance.save() if need_refund: tax = round(amount * Decimal(TAX_RATE), 2) amount *= Decimal(TAX_RATE + 1) amount = round(amount * 100, 2) retreat = validated_data['retreat'] refund_instance = Refund.objects.create( orderline=order_line, refund_date=timezone.now(), amount=amount / 100, details="Exchange retreat {0} for " "retreat {1}".format(str(current_retreat), str(validated_data['retreat'])), ) try: refund_response = refund_amount( order_line.order.settlement_id, int(round(amount))) refund_res_content = refund_response.json() refund_instance.refund_id = refund_res_content['id'] refund_instance.save() except PaymentAPIError as err: if str(err) == PAYSAFE_EXCEPTION['3406']: raise serializers.ValidationError({ 'non_field_errors': [ _("The order has not been charged yet. " "Try again later.") ], 'detail': err.detail }) raise serializers.ValidationError({ 'message': str(err), 'detail': err.detail }) new_retreat = retreat old_retreat = current_retreat # Send appropriate emails # Send order confirmation email if need_transaction: items = [{ 'price': new_order_line.content_object.price, 'name': "{0}: {1}".format(str(new_order_line.content_type), new_order_line.content_object.name), }] merge_data = { 'STATUS': "APPROUVÉE", 'CARD_NUMBER': charge_res_content['card']['lastDigits'], 'CARD_TYPE': PAYSAFE_CARD_TYPE[charge_res_content['card']['type']], 'DATETIME': timezone.localtime().strftime("%x %X"), 'ORDER_ID': order.id, 'CUSTOMER_NAME': user.first_name + " " + user.last_name, 'CUSTOMER_EMAIL': user.email, 'CUSTOMER_NUMBER': user.id, 'AUTHORIZATION': order.authorization_id, 'TYPE': "Achat", 'ITEM_LIST': items, 'TAX': round( (new_order_line.cost - current_retreat.price) * Decimal(TAX_RATE), 2, ), 'DISCOUNT': current_retreat.price, 'COUPON': { 'code': _("Échange") }, 'SUBTOTAL': round(new_order_line.cost - current_retreat.price, 2), 'COST': round((new_order_line.cost - current_retreat.price) * Decimal(TAX_RATE + 1), 2), } Order.send_invoice([order.user.email], merge_data) # Send refund confirmation email if need_refund: merge_data = { 'DATETIME': timezone.localtime().strftime("%x %X"), 'ORDER_ID': order_line.order.id, 'CUSTOMER_NAME': user.first_name + " " + user.last_name, 'CUSTOMER_EMAIL': user.email, 'CUSTOMER_NUMBER': user.id, 'TYPE': "Remboursement", 'NEW_RETREAT': new_retreat, 'OLD_RETREAT': old_retreat, 'SUBTOTAL': old_retreat.price - new_retreat.price, 'COST': round(amount / 100, 2), 'TAX': round(Decimal(tax), 2), } plain_msg = render_to_string("refund.txt", merge_data) msg_html = render_to_string("refund.html", merge_data) try: response_send_mail = send_mail( "Confirmation de remboursement", plain_msg, settings.DEFAULT_FROM_EMAIL, [user.email], html_message=msg_html, ) EmailLog.add(user.email, 'refund', response_send_mail) except Exception as err: additional_data = { 'title': "Confirmation de remboursement", 'default_from': settings.DEFAULT_FROM_EMAIL, 'user_email': user.email, 'merge_data': merge_data, 'template': 'refund' } Log.error(source='SENDING_BLUE_TEMPLATE', message=err, additional_data=json.dumps(additional_data)) raise # Send exchange confirmation email if validated_data.get('retreat'): merge_data = { 'DATETIME': timezone.localtime().strftime("%x %X"), 'CUSTOMER_NAME': user.first_name + " " + user.last_name, 'CUSTOMER_EMAIL': user.email, 'CUSTOMER_NUMBER': user.id, 'TYPE': "Échange", 'NEW_RETREAT': new_retreat, 'OLD_RETREAT': old_retreat, } if len(new_retreat.pictures.all()): merge_data['RETREAT_PICTURE'] = "{0}{1}".format( settings.MEDIA_URL, new_retreat.pictures.first().picture.url) plain_msg = render_to_string("exchange.txt", merge_data) msg_html = render_to_string("exchange.html", merge_data) try: response_send_mail = send_mail( "Confirmation d'échange", plain_msg, settings.DEFAULT_FROM_EMAIL, [user.email], html_message=msg_html, ) EmailLog.add(user.email, 'exchange', response_send_mail) except Exception as err: additional_data = { 'title': "Confirmation d'échange", 'default_from': settings.DEFAULT_FROM_EMAIL, 'user_email': user.email, 'merge_data': merge_data, 'template': 'exchange' } Log.error(source='SENDING_BLUE_TEMPLATE', message=err, additional_data=json.dumps(additional_data)) raise send_retreat_confirmation_email(instance.user, new_retreat) return Reservation.objects.get(id=instance_pk)