From 83b1d832ae620c9028eec5016af33d3ae955addf Mon Sep 17 00:00:00 2001 From: RomainFayolle Date: Mon, 17 Jun 2024 11:52:42 -0400 Subject: [PATCH] update coupon usages display --- log_management/models.py | 2 +- store/exports.py | 78 ++++++++++------ store/serializers.py | 93 +++++++++++-------- store/tests/tests_viewset_Coupon.py | 138 +++++++++++++++++++++++++++- 4 files changed, 244 insertions(+), 67 deletions(-) diff --git a/log_management/models.py b/log_management/models.py index 242f7bea..c6842b1b 100644 --- a/log_management/models.py +++ b/log_management/models.py @@ -169,7 +169,7 @@ def anonymize_data(cls, start_date=None, end_date=None, targetIds=None): session_uuid_matching = {} queryset = cls.objects.filter(id__in=targetIds) if targetIds else cls.objects.all() if start_date and end_date: - queryset = queryset.objects.filter( + queryset = queryset.filter( created__gte=start_date, created__lte=end_date, ) diff --git a/store/exports.py b/store/exports.py index 60dcdf5f..c69fdbe4 100644 --- a/store/exports.py +++ b/store/exports.py @@ -18,6 +18,35 @@ LOCAL_TIMEZONE = pytz.timezone(settings.TIME_ZONE) +def _get_coupon_export_usage(coupon): + usages_per_order = {} + for line in OrderLine.objects.filter(coupon=coupon): + is_refunded = Refund.objects.filter(orderline=line).exists() + if is_refunded: continue + if line.order.id not in usages_per_order: + usages_per_order[line.order.id] = { + 'date': line.order.transaction_date. + astimezone(LOCAL_TIMEZONE). + strftime("%Y-%m-%d %H:%M:%S"), + 'user': line.order.user, + 'amount_used': line.coupon_real_value, + 'product_name': set(), + } + usages_per_order[line.order.id]['product_name'].add( + line.content_object.name) + else: + usages_per_order[line.order.id]['amount_used'] += \ + line.coupon_real_value + usages_per_order[line.order.id]['product_name'].add( + line.content_object.name + ) + display_usages = [] + for key, value in usages_per_order.items(): + value['product_name'] = ', '.join(list(value['product_name'])) + display_usages.append(value) + return display_usages + + @shared_task() def generate_coupon_usage(admin_id, coupon_id): """ @@ -40,38 +69,35 @@ def generate_coupon_usage(admin_id, coupon_id): 'Numéro étudiant', 'Code programme académique', 'Valeur utilisée', - 'Élément associé', + 'Éléments associés', 'Date d\'utilisation', ] writer.writerow(header) coupon = Coupon.objects.get(pk=coupon_id) + usages = _get_coupon_export_usage(coupon) - for line in OrderLine.objects.filter(coupon=coupon): - is_refunded = Refund.objects.filter(orderline=line).exists() - if not is_refunded: - line_array = [None] * len(header) - user = line.order.user - line_array[0] = user.id - line_array[1] = user.email - university = user.university - line_array[2] = university.name if university else '' - academic_field = user.academic_field - line_array[3] = academic_field.name if academic_field else '' - academic_level = user.academic_level - line_array[4] = academic_level.name if academic_level else '' - line_array[5] = user.first_name - line_array[6] = user.last_name - line_array[7] = user.gender - line_array[8] = user.city - line_array[9] = user.student_number - line_array[10] = user.academic_program_code - line_array[11] = line.coupon_real_value - line_array[12] = line.content_object.name - line_array[13] = (line.order.transaction_date. - astimezone(LOCAL_TIMEZONE). - strftime("%Y-%m-%d %H:%M:%S")) - writer.writerow(line_array) + for usage in usages: + line_array = [None] * len(header) + user = usage['user'] + line_array[0] = user.id + line_array[1] = user.email + university = user.university + line_array[2] = university.name if university else '' + academic_field = user.academic_field + line_array[3] = academic_field.name if academic_field else '' + academic_level = user.academic_level + line_array[4] = academic_level.name if academic_level else '' + line_array[5] = user.first_name + line_array[6] = user.last_name + line_array[7] = user.gender + line_array[8] = user.city + line_array[9] = user.student_number + line_array[10] = user.academic_program_code + line_array[11] = usage['amount_used'] + line_array[12] = usage['product_name'] + line_array[13] = usage['date'] + writer.writerow(line_array) date_file = LOCAL_TIMEZONE.localize(datetime.now()) \ .strftime("%Y%m%d") diff --git a/store/serializers.py b/store/serializers.py index d5b607c6..429f22cc 100644 --- a/store/serializers.py +++ b/store/serializers.py @@ -47,7 +47,7 @@ create_external_payment_profile, create_external_card, get_external_cards, - PAYSAFE_CARD_TYPE,) + PAYSAFE_CARD_TYPE, ) User = get_user_model() @@ -79,7 +79,6 @@ def to_representation(self, instance): class OrderLineBaseProductSerializer(serializers.ModelSerializer): - id = serializers.IntegerField() quantity = serializers.IntegerField() metadata = serializers.JSONField(required=False) @@ -117,7 +116,7 @@ class SimpleBaseProductSerializer(serializers.HyperlinkedModelSerializer): product_type = serializers.SerializerMethodField() def get_product_type(self, obj): - return BaseProduct.objects.get_subclass(id=obj.id).\ + return BaseProduct.objects.get_subclass(id=obj.id). \ __class__.__name__.lower() class Meta: @@ -486,7 +485,7 @@ def to_representation(self, instance: OrderLine): option: BaseProduct for option in options: option_id = option.id - option_data = option.orderlinebaseproduct_set.\ + option_data = option.orderlinebaseproduct_set. \ get(order_line_id=instance.id) option_data = { @@ -570,8 +569,8 @@ def create(self, validated_data): else: raise serializers.ValidationError({ 'non_field_errors': [_( - "You don't have the permission to create " - "an order for another user." + "You don't have the permission to create " + "an order for another user." )] }) if 'bypass_payment' in validated_data.keys(): @@ -760,7 +759,7 @@ def create(self, validated_data): 'non_field_errors': [_( "You already are registered " "to this timeslot: ") + str(timeslot) + "." - ] + ] }) if (timeslot.period.workplace and timeslot.period.workplace.seats - reserved > 0): @@ -1047,7 +1046,7 @@ def to_representation(self, instance): data = super(OrderSerializer, self).to_representation(instance) # TTC is in cents after serialization data['total_cost_with_taxes'] = round( - data['total_cost_with_taxes']/100, 2) + data['total_cost_with_taxes'] / 100, 2) data['taxes'] = data['total_cost_with_taxes'] - data['total_cost'] return data @@ -1162,47 +1161,63 @@ def update(self, instance, validated_data): return super(CouponSerializer, self).update(instance, validated_data) - def to_representation(self, instance): - data = super(CouponSerializer, self).to_representation(instance) - from workplace.serializers import TimeSlotSerializer + def get_coupon_usage(self, instance): + """ + Get coupon usage per order, listing elements for frontend + """ from blitz_api.serializers import ( OrganizationSerializer, ReservationUserSerializer, ) + usages_per_order = {} + for line in OrderLine.objects.filter(coupon=instance): + is_refunded = Refund.objects.filter(orderline=line).exists() + if is_refunded: continue + if line.order.id not in usages_per_order: + usages_per_order[line.order.id] = { + 'date': line.order.transaction_date, + 'user': ReservationUserSerializer( + line.order.user, + context={ + 'request': self.context['request'], + 'view': self.context['view'], + }, + ).data, + 'amount_used': line.coupon_real_value, + 'user_university': OrganizationSerializer( + line.order.user.university, + context={ + 'request': self.context['request'], + 'view': self.context['view'], + }, + ).data, + 'product_name': set(), + } + usages_per_order[line.order.id]['product_name'].add( + line.content_object.name) + else: + usages_per_order[line.order.id]['amount_used'] += \ + line.coupon_real_value + usages_per_order[line.order.id]['product_name'].add( + line.content_object.name + ) + display_usages = [] + for key, value in usages_per_order.items(): + value['product_name'] = ', '.join(list(sorted(value['product_name']))) + display_usages.append(value) + return display_usages + + def to_representation(self, instance): + data = super(CouponSerializer, self).to_representation(instance) + from workplace.serializers import TimeSlotSerializer + from blitz_api.serializers import OrganizationSerializer from retirement.serializers import ( RetreatSerializer, RetreatTypeSerializer, ) action = self.context['view'].action if action == 'retrieve' or action == 'list': - - usages = list() - for line in OrderLine.objects.filter(coupon=instance): - is_refunded = Refund.objects.filter(orderline=line).exists() - usages.append( - { - 'date': line.order.transaction_date, - 'user': ReservationUserSerializer( - line.order.user, - context={ - 'request': self.context['request'], - 'view': self.context['view'], - }, - ).data, - 'amount_used': line.coupon_real_value, - 'user_university': OrganizationSerializer( - line.order.user.university, - context={ - 'request': self.context['request'], - 'view': self.context['view'], - }, - ).data, - 'product_name': line.content_object.name, - 'orderline_refunded': is_refunded, - } - ) - - data['usages'] = usages + data['usages'] = self.get_coupon_usage(instance) data['applicable_retreats'] = RetreatSerializer( instance.applicable_retreats, many=True, diff --git a/store/tests/tests_viewset_Coupon.py b/store/tests/tests_viewset_Coupon.py index 89d6d082..ffbf41fa 100644 --- a/store/tests/tests_viewset_Coupon.py +++ b/store/tests/tests_viewset_Coupon.py @@ -7,7 +7,6 @@ from rest_framework import status from rest_framework.test import ( APIClient, - APITestCase, ) from unittest import mock from django.conf import settings @@ -20,6 +19,8 @@ UserFactory, AdminFactory, CouponFactory, + OrderFactory, + OrderLineFactory, ) from blitz_api.testing_tools import CustomAPITestCase from workplace.models import ( @@ -37,6 +38,7 @@ Membership, Coupon, CouponUser, + Refund, ) User = get_user_model() @@ -77,6 +79,7 @@ def setUpClass(cls): cls.client = APIClient() cls.user = UserFactory() cls.admin = AdminFactory() + cls.retreat_type = ContentType.objects.get_for_model(Retreat) cls.package_type = ContentType.objects.get_for_model(Package) cls.package = Package.objects.create( name="extreme_package", @@ -1388,6 +1391,139 @@ def test_read_admin(self): content ) + def test_usages(self): + """ + Ensure we have the correct info for usages. + """ + self.client.force_authenticate(user=self.admin) + + coupon = Coupon.objects.create( + value=13, + code="ABCDEFGH", + start_time="2019-01-06T15:11:05-05:00", + end_time="2100-01-06T15:11:06-05:00", + max_use=100, + max_use_per_user=2, + details="Coupon to test usages", + owner=self.user, + ) + coupon.applicable_product_types.add(self.package_type) + coupon.applicable_retreats.add(self.retreat) + + order1 = OrderFactory(user=self.user) + ol11 = OrderLineFactory( + order=order1, + content_type=self.package_type, + object_id=self.package.id, + coupon=coupon, + coupon_real_value=5 + ) + + ol12 = OrderLineFactory( + order=order1, + content_type=self.retreat_type, + object_id=self.retreat.id, + coupon=coupon, + coupon_real_value=6 + ) + ol13 = OrderLineFactory( + order=order1, + content_type=self.retreat_type, + object_id=self.retreat.id, + coupon=coupon, + coupon_real_value=7 + ) + + order2 = OrderFactory(user=self.user) + ol21 = OrderLineFactory( + order=order2, + content_type=self.package_type, + object_id=self.package.id, + coupon=coupon, + coupon_real_value=8 + ) + + order3 = OrderFactory(user=self.user) + ol31 = OrderLineFactory( + order=order3, + content_type=self.retreat_type, + object_id=self.retreat.id, + coupon=coupon, + coupon_real_value=9 + ) + + Refund.objects.create( + orderline=ol13, + amount=self.retreat.price, + refund_date="2019-01-06T15:11:05-05:00" + ) + Refund.objects.create( + orderline=ol31, + amount=self.retreat.price, + refund_date="2019-01-06T15:11:05-05:00" + ) + + response = self.client.get( + reverse( + 'coupon-detail', + kwargs={'pk': coupon.id}, + ), + ) + + self.assertEqual( + response.status_code, + status.HTTP_200_OK, + response.content + ) + + data = json.loads(response.content) + + expected_usages = [ + { + 'date': order1.transaction_date.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + 'user': { + 'id': self.user.id, + 'first_name': self.user.first_name, + 'last_name': self.user.last_name, + 'email': self.user.email, + 'url': f'http://testserver/users/{self.user.id}', + 'phone': None, + 'personnal_restrictions': None + }, + 'amount_used': 11.0, + 'user_university': { + 'name': '', + 'name_fr': '', + 'name_en': '' + }, + 'product_name': 'extreme_package, mega_retreat' + }, + { + 'date': order2.transaction_date.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + 'user': { + 'id': self.user.id, + 'first_name': self.user.first_name, + 'last_name': self.user.last_name, + 'email': self.user.email, + 'url': f'http://testserver/users/{self.user.id}', + 'phone': None, + 'personnal_restrictions': None + }, + 'amount_used': 8.0, + 'user_university': { + 'name': '', + 'name_fr': '', + 'name_en': '' + }, + 'product_name': 'extreme_package' + } + ] + + self.assertEqual( + data['usages'], + expected_usages, + ) + def test_read_non_existent(self): """ Ensure we get not found when asking for a coupon that doesn't