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)
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)
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', }, }
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."), }, }
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', }, }