Exemplo n.º 1
0
 def to_representation(self, instance):
     is_staff = self.context['request'].user.is_staff
     if self.context['view'].action == 'retrieve' and is_staff:
         self.fields['users'] = UserSerializer(many=True)
     data = super(RetirementSerializer, self).to_representation(instance)
     if is_staff:
         return data
     return remove_translation_fields(data)
Exemplo n.º 2
0
    def to_representation(self, instance):
        is_staff = self.context['request'].user.is_staff
        if self.context['view'].action == 'retrieve' and is_staff:
            from blitz_api.serializers import UserSerializer
            self.fields['users'] = UserSerializer(many=True)
        data = super(RetreatSerializer, self).to_representation(instance)

        # We don't need orderlines for retreat in this serializer
        if data.get("order_lines") is not None:
            data.pop("order_lines")

        # TODO put back available after migration from is_active
        data.pop("available")

        if is_staff:
            return data
        return remove_translation_fields(data)
Exemplo n.º 3
0
class ReservationSerializer(serializers.HyperlinkedModelSerializer):
    id = serializers.ReadOnlyField()
    # Custom names are needed to overcome an issue with DRF:
    # https://github.com/encode/django-rest-framework/issues/2719
    # I
    retirement_details = RetirementSerializer(
        read_only=True,
        source='retirement',
    )
    user_details = UserSerializer(
        read_only=True,
        source='user',
    )
    payment_token = serializers.CharField(
        write_only=True,
        required=False,
        allow_blank=True,
        allow_null=True,
    )
    single_use_token = serializers.CharField(
        write_only=True,
        required=False,
        allow_blank=True,
        allow_null=True,
    )

    def validate(self, attrs):
        """Prevents overlapping reservations."""
        validated_data = super(ReservationSerializer, self).validate(attrs)

        action = self.context['view'].action

        # This validation is here instead of being in the 'update()' method
        # because we need to stop validation if improper fields are passed in
        # a partial update.
        if action == 'partial_update':
            # Only allow modification of is_present & retirement fields.
            is_invalid = validated_data.copy()
            is_invalid.pop('is_present', None)
            is_invalid.pop('retirement', None)
            is_invalid.pop('payment_token', None)
            is_invalid.pop('single_use_token', None)
            if is_invalid:
                raise serializers.ValidationError({
                    'non_field_errors': [
                        _("Only is_present and retirement can be updated. To "
                          "change other fields, delete this reservation and "
                          "create a new one.")
                    ]
                })
            return attrs

        # Generate a list of tuples containing start/end time of
        # existing reservations.
        start = validated_data['retirement'].start_time
        end = validated_data['retirement'].end_time
        active_reservations = Reservation.objects.filter(
            user=validated_data['user'],
            is_active=True,
        ).exclude(**validated_data).values_list(
            'retirement__start_time',
            'retirement__end_time',
        )

        for retirements in active_reservations:
            if max(retirements[0], start) < min(retirements[1], end):
                raise serializers.ValidationError({
                    'non_field_errors': [
                        _("This reservation overlaps with another active "
                          "reservations for this user.")
                    ]
                })
        return attrs

    def create(self, validated_data):
        """
        Allows an admin to create retirements reservations for another user.
        """
        validated_data['refundable'] = False
        validated_data['exchangeable'] = False
        validated_data['is_active'] = True

        return super().create(validated_data)

    def update(self, instance, validated_data):

        if not instance.exchangeable and validated_data.get('retirement'):
            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_retirement = instance.retirement
        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 retirement 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 retirement seats
            free_seats = (current_retirement.seats -
                          current_retirement.total_reservations)
            if (current_retirement.reserved_seats or free_seats == 1):
                current_retirement.reserved_seats += 1
                current_retirement.save()

            if validated_data.get('retirement'):
                # Validate if user has the right to reserve a seat in the new
                # retirement
                new_retirement = instance.retirement
                old_retirement = current_retirement

                user_waiting = new_retirement.wait_queue.filter(user=user)
                free_seats = (new_retirement.seats -
                              new_retirement.total_reservations -
                              new_retirement.reserved_seats + 1)
                reserved_for_user = (new_retirement.reserved_seats
                                     and WaitQueueNotification.objects.filter(
                                         user=user, retirement=new_retirement))
                if not (free_seats > 0 or reserved_for_user):
                    raise serializers.ValidationError({
                        'non_field_errors': [
                            _("There are no places left in the requested "
                              "retirement.")
                        ]
                    })
                if user_waiting:
                    user_waiting.delete()

            if (self.context['view'].action == 'partial_update'
                    and validated_data.get('retirement')):
                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_retirement.start_time - timezone.now()
                days_exchange = timedelta(
                    days=current_retirement.min_day_exchange)
                respects_minimum_days = (days_remaining >= days_exchange)
                new_retirement_price = validated_data['retirement'].price
                if current_retirement.price < new_retirement_price:
                    # If the new retirement 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['retirement'].price -
                              order_line.coupon_real_value)
                    if not (payment_token or single_use_token):
                        raise serializers.ValidationError({
                            'non_field_errors': [
                                _("The new retirement is more expensive than "
                                  "the current one. Provide a payment_token or "
                                  "single_use_token to charge the balance.")
                            ]
                        })
                if current_retirement.price > new_retirement_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 retirements.
                    need_refund = True
                    price_diff = (current_retirement.price -
                                  validated_data['retirement'].price)
                    real_cost = order_line.cost
                    amount = min(price_diff, real_cost)
                if current_retirement == validated_data['retirement']:
                    raise serializers.ValidationError({
                        'retirement': [
                            _("That retirement 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})
                    # 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['retirement'].start_time
                end = validated_data['retirement'].end_time
                active_reservations = Reservation.objects.filter(
                    user=user,
                    is_active=True,
                ).exclude(pk=instance.pk).values_list(
                    'retirement__start_time',
                    'retirement__end_time',
                )

                for retirements in active_reservations:
                    if max(retirements[0], start) < min(retirements[1], 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(
                            Retirement),
                        object_id=validated_data['retirement'].id,
                        cost=amount,
                        coupon=coupon,
                        coupon_real_value=coupon_value,
                    )
                    tax = round(amount * Decimal(TAX_RATE), 2)
                    amount *= Decimal(TAX_RATE + 1)
                    amount = round(amount * 100, 2)
                    retirement = validated_data['retirement']

                    # Do a complete refund of the previous retirement
                    try:
                        refund_instance = refund_retirement(
                            canceled_reservation, 100,
                            "Exchange retirement {0} for retirement "
                            "{1}".format(str(current_retirement),
                                         str(validated_data['retirement'])))
                    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.")
                            })
                        raise serializers.ValidationError(
                            {'message': str(err)})

                    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})

                    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})
                    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)
                    retirement = validated_data['retirement']

                    refund_instance = Refund.objects.create(
                        orderline=order_line,
                        refund_date=timezone.now(),
                        amount=amount / 100,
                        details="Exchange retirement {0} for "
                        "retirement {1}".format(
                            str(current_retirement),
                            str(validated_data['retirement'])),
                    )

                    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.")
                            })
                        raise serializers.ValidationError(
                            {'message': str(err)})

                    new_retirement = retirement
                    old_retirement = current_retirement

            # Ask the external scheduler to start calling /notify if the
            # reserved_seats count == 1. Otherwise, the scheduler should
            # already be calling /notify at specified intervals.
            #
            # Since we are in the context of a cancelation, if reserved_seats
            # equals 1, that means that this is the first cancelation.
            if current_retirement.reserved_seats == 1:
                scheduler_url = '{0}'.format(
                    settings.EXTERNAL_SCHEDULER['URL'], )

                data = {
                    "hour":
                    timezone.now().hour,
                    "minute": (timezone.now().minute + 5) % 60,
                    "url":
                    '{0}{1}'.format(
                        request.build_absolute_uri(
                            reverse('retirement:waitqueuenotification-list')),
                        "/notify"),
                    "description":
                    "Retirement wait queue notification"
                }

                try:
                    auth_data = {
                        "username": settings.EXTERNAL_SCHEDULER['USER'],
                        "password": settings.EXTERNAL_SCHEDULER['PASSWORD']
                    }
                    auth = requests.post(
                        scheduler_url + "/authentication",
                        json=auth_data,
                    )
                    auth.raise_for_status()

                    r = requests.post(
                        scheduler_url + '/tasks',
                        json=data,
                        headers={
                            'Authorization':
                            'Token ' + json.loads(auth.content)['token']
                        },
                        timeout=(10, 10),
                    )
                    r.raise_for_status()
                except (requests.exceptions.HTTPError,
                        requests.exceptions.ConnectionError) as err:
                    mail_admins("Thèsez-vous: external scheduler error",
                                traceback.format_exc())

        # 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_retirement.price) *
                    Decimal(TAX_RATE),
                    2,
                ),
                'DISCOUNT':
                current_retirement.price,
                'COUPON': {
                    'code': _("Échange")
                },
                'SUBTOTAL':
                round(new_order_line.cost - current_retirement.price, 2),
                'COST':
                round((new_order_line.cost - current_retirement.price) *
                      Decimal(TAX_RATE + 1), 2),
            }

            plain_msg = render_to_string("invoice.txt", merge_data)
            msg_html = render_to_string("invoice.html", merge_data)

            send_mail(
                "Confirmation d'achat",
                plain_msg,
                settings.DEFAULT_FROM_EMAIL,
                [order.user.email],
                html_message=msg_html,
            )

        # 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_RETIREMENT': new_retirement,
                'OLD_RETIREMENT': old_retirement,
                'SUBTOTAL': old_retirement.price - new_retirement.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)

            send_mail(
                "Confirmation de remboursement",
                plain_msg,
                settings.DEFAULT_FROM_EMAIL,
                [user.email],
                html_message=msg_html,
            )

        # Send exchange confirmation email
        if validated_data.get('retirement'):
            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_RETIREMENT': new_retirement,
                'OLD_RETIREMENT': old_retirement,
            }

            plain_msg = render_to_string("exchange.txt", merge_data)
            msg_html = render_to_string("exchange.html", merge_data)

            send_mail(
                "Confirmation d'échange",
                plain_msg,
                settings.DEFAULT_FROM_EMAIL,
                [user.email],
                html_message=msg_html,
            )

            merge_data = {
                'RETIREMENT': new_retirement,
                'USER': instance.user,
            }

            plain_msg = render_to_string("retirement_info.txt", merge_data)
            msg_html = render_to_string("retirement_info.html", merge_data)

            send_mail(
                "Confirmation d'inscription à la retraite",
                plain_msg,
                settings.DEFAULT_FROM_EMAIL,
                [instance.user.email],
                html_message=msg_html,
            )

        return Reservation.objects.get(id=instance_pk)

    class Meta:
        model = Reservation
        exclude = ('deleted', )
        extra_kwargs = {
            'retirement': {
                'help_text': _("Retirement represented by the picture."),
                'view_name': 'retirement:retirement-detail',
            },
            'is_active': {
                'required': False,
                'help_text': _("Whether the reservation is active or not."),
            },
            'url': {
                'view_name': 'retirement:reservation-detail',
            },
        }
Exemplo n.º 4
0
class ReservationSerializer(serializers.HyperlinkedModelSerializer):
    id = serializers.ReadOnlyField()
    # Custom names are needed to overcome an issue with DRF:
    # https://github.com/encode/django-rest-framework/issues/2719
    # I
    timeslot_details = TimeSlotSerializer(
        read_only=True,
        source='timeslot',
    )
    user_details = UserSerializer(
        read_only=True,
        source='user',
    )

    def validate(self, attrs):
        """Prevents overlapping and no-workplace reservations."""
        validated_data = super(ReservationSerializer, self).validate(attrs)

        action = self.context['view'].action

        if action == 'partial_update':
            # Only allow modification of is_present field.
            is_present = validated_data.get('is_present')
            if is_present is None or len(validated_data) > 1:
                raise serializers.ValidationError({
                    'is_present': _(
                        "Only is_present can be updated. To change other "
                        "fields, delete this reservation and create a new one."
                    ),
                })
            return attrs

        if 'timeslot' in attrs:
            if not attrs['timeslot'].period.workplace:
                raise serializers.ValidationError(
                    'No reservation are allowed for time slots without '
                    'workplace.'
                )

        if 'user' in validated_data or 'timeslot' in validated_data:
            # Generate a list of tuples containing start/end time of
            # existing reservations.
            start = validated_data['timeslot'].start_time
            end = validated_data['timeslot'].end_time
            active_reservations = Reservation.objects.filter(
                user=validated_data['user'],
                is_active=True,
            ).exclude(**validated_data).values_list(
                'timeslot__start_time',
                'timeslot__end_time'
            )

            for timeslots in active_reservations:
                if max(timeslots[0], start) < min(timeslots[1], end):
                    raise serializers.ValidationError(
                        'This reservation overlaps with another active '
                        'reservations for this user.'
                    )
        return attrs

    class Meta:
        model = Reservation
        exclude = ('deleted',)
        extra_kwargs = {
            'is_active': {
                'required': True,
                'help_text': _("Whether the reservation is active or not."),
            },
        }
Exemplo n.º 5
0
class ReservationSerializer(serializers.HyperlinkedModelSerializer):
    from blitz_api.serializers import UserSerializer
    id = serializers.ReadOnlyField()
    # Custom names are needed to overcome an issue with DRF:
    # https://github.com/encode/django-rest-framework/issues/2719
    # I
    retreat_details = RetreatSerializer(
        read_only=True,
        source='retreat',
    )
    user_details = UserSerializer(
        read_only=True,
        source='user',
    )
    payment_token = serializers.CharField(
        write_only=True,
        required=False,
        allow_blank=True,
        allow_null=True,
    )
    single_use_token = serializers.CharField(
        write_only=True,
        required=False,
        allow_blank=True,
        allow_null=True,
    )

    def validate(self, attrs):
        """Prevents overlapping reservations."""
        validated_data = super(ReservationSerializer, self).validate(attrs)

        action = self.context['view'].action

        # This validation is here instead of being in the 'update()' method
        # because we need to stop validation if improper fields are passed in
        # a partial update.
        if action == 'partial_update':
            # Only allow modification of is_present & retreat fields.
            is_invalid = validated_data.copy()
            is_invalid.pop('is_present', None)
            is_invalid.pop('retreat', None)
            is_invalid.pop('payment_token', None)
            is_invalid.pop('single_use_token', None)
            if is_invalid:
                raise serializers.ValidationError({
                    'non_field_errors': [
                        _("Only is_present and retreat can be updated. To "
                          "change other fields, delete this reservation and "
                          "create a new one.")
                    ]
                })
            return attrs

        # 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=validated_data['user'],
            is_active=True,
        )

        active_reservations = active_reservations.all()

        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.")
                        ]
                    })
        return attrs

    def create(self, validated_data):
        """
        Allows an admin to create retreats reservations for another user.
        """
        validated_data['refundable'] = False
        validated_data['exchangeable'] = False
        validated_data['is_active'] = True

        if validated_data['retreat'].places_remaining <= 0:
            raise serializers.ValidationError({
                'non_field_errors': [
                    _("This retreat doesn't have available places. Please "
                      "check number of seats available and reserved seats.")
                ]
            })

        return super().create(validated_data)

    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)

    class Meta:
        model = Reservation
        exclude = ('deleted', )
        extra_kwargs = {
            'retreat': {
                'help_text': _("Retreat represented by the picture."),
                'view_name': 'retreat:retreat-detail',
            },
            'invitation': {
                'help_text': _("Retreat represented by the picture."),
                'view_name': 'retreat:retreatinvitation-detail',
            },
            'is_active': {
                'required': False,
                'help_text': _("Whether the reservation is active or not."),
            },
            'url': {
                'view_name': 'retreat:reservation-detail',
            },
        }