Пример #1
0
    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
Пример #2
0
    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
Пример #3
0
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
Пример #4
0
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
Пример #5
0
    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
Пример #6
0
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
Пример #7
0
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
Пример #8
0
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
Пример #9
0
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
Пример #10
0
    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,
        )
Пример #11
0
    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)
Пример #12
0
    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)