Esempio n. 1
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)