diff --git a/parking_permits/customer_permit.py b/parking_permits/customer_permit.py index 985e5fbf..59843a74 100644 --- a/parking_permits/customer_permit.py +++ b/parking_permits/customer_permit.py @@ -359,6 +359,7 @@ def end( user=None, subscription_cancel_reason=SubscriptionCancelReason.USER_CANCELLED, cancel_from_talpa=True, + force_end=False, ): permits = self.customer_permit_query.filter(id__in=permit_ids).order_by( "primary_vehicle" @@ -432,7 +433,7 @@ def end( permit, ) - permit.end_permit(end_type) + permit.end_permit(end_type, force_end=force_end) send_permit_email( PermitEmailType.ENDED, ParkingPermit.objects.get(id=permit.id) ) diff --git a/parking_permits/models/parking_permit.py b/parking_permits/models/parking_permit.py index c120c0a4..9fa17a35 100644 --- a/parking_permits/models/parking_permit.py +++ b/parking_permits/models/parking_permit.py @@ -503,14 +503,15 @@ def get_price_change_list(self, new_zone, is_low_emission): ) return price_change_list - def end_permit(self, end_type): + def end_permit(self, end_type, force_end=False): if end_type == ParkingPermitEndType.AFTER_CURRENT_PERIOD: end_time = self.current_period_end_time else: end_time = max(self.start_time, timezone.now()) if ( - self.primary_vehicle + not force_end + and self.primary_vehicle and self.customer.permits.active_after(end_time) .exclude(id=self.id) .exists() diff --git a/parking_permits/models/vehicle.py b/parking_permits/models/vehicle.py index e1a26a81..21e5a30a 100644 --- a/parking_permits/models/vehicle.py +++ b/parking_permits/models/vehicle.py @@ -177,6 +177,10 @@ def is_low_emission(self): self.emission, ) + @property + def description(self): + return f'{_("Vehicle")}: {str(self)}' + def __str__(self): vehicle_str = "%s" % self.registration_number or "" if self.manufacturer: diff --git a/parking_permits/serializers.py b/parking_permits/serializers.py index 74a9bd15..dda9084a 100644 --- a/parking_permits/serializers.py +++ b/parking_permits/serializers.py @@ -17,19 +17,28 @@ class TalpaPayloadSerializer(serializers.Serializer): class RightOfPurchaseResponseSerializer(serializers.Serializer): - errorMessage = serializers.CharField(help_text="Error if exists", default="") rightOfPurchase = serializers.BooleanField(help_text="Has rights to purchase") userId = serializers.CharField(help_text="User id") + errorMessage = serializers.CharField(help_text="Error if exists", default="") class ResolvePriceResponseSerializer(serializers.Serializer): - rowPriceNet = serializers.FloatField(help_text="Row price net") - rowPriceVat = serializers.FloatField(help_text="Row price vat") - rowPriceTotal = serializers.FloatField(help_text="Row price total") + userId = serializers.CharField(help_text="User id") + subscriptionId = serializers.CharField(help_text="Subscription id") priceNet = serializers.FloatField(help_text="Total net price") priceVat = serializers.FloatField(help_text="Total vat") priceGross = serializers.FloatField(help_text="Gross price") vatPercentage = serializers.FloatField(help_text="Vat percentage") + errorMessage = serializers.CharField(help_text="Error if exists", default="") + + +class ResolveProductResponseSerializer(serializers.Serializer): + userId = serializers.CharField(help_text="User id") + subscriptionId = serializers.CharField(help_text="Subscription id") + productId = serializers.FloatField(help_text="Product id") + productName = serializers.FloatField(help_text="Product name") + productLabel = serializers.FloatField(help_text="Product label") + errorMessage = serializers.CharField(help_text="Error if exists", default="") class PaymentSerializer(serializers.Serializer): diff --git a/parking_permits/tests/test_views.py b/parking_permits/tests/test_views.py index f97dcb7f..702f279d 100644 --- a/parking_permits/tests/test_views.py +++ b/parking_permits/tests/test_views.py @@ -171,13 +171,104 @@ def test_payment_view_should_update_renewal_order_and_permit_status(self): ) -class ResolvePriceViewTestCase(APITestCase): +class BaseResolveEndpointTestCase(APITestCase): talpa_subscription_id = "f769b803-0bd0-489d-aa81-b35af391f391" talpa_order_id = "d4745a07-de99-33f8-94d6-64595f7a8bc6" talpa_order_item_id = "2f20c06d-2a9a-4a60-be4b-504d8a2f8c02" + talpa_product_id = "e490e13e-ecd1-4f4f-8e26-a29a5de42b80" user_id = "d86ca61d-97e9-410a-a1e3-4894873b1b46" permit_id = "80000001" + def prepare_test_data( + self, permit_id, unit_price, low_emission_discount, primary_permit=True + ): + start_date = datetime.date(2023, 1, 1) + end_date = datetime.date(2023, 12, 31) + vehicle = VehicleFactory( + power_type=VehiclePowerTypeFactory(identifier="01", name="Bensin"), + emission=45, + euro_class=6, + emission_type=EmissionType.WLTP, + ) + LowEmissionCriteriaFactory( + start_date=start_date, + end_date=end_date, + nedc_max_emission_limit=None, + wltp_max_emission_limit=50, + euro_min_class_limit=6, + ) + permit_start_time = datetime.datetime( + 2023, 9, 12, 13, 46, 0, tzinfo=datetime.timezone.utc + ) + permit_end_time = datetime.datetime( + 2023, 10, 11, 23, 59, 0, tzinfo=datetime.timezone.utc + ) + zone_a = ParkingZoneFactory(name="A") + product_detail_list = [[(start_date, end_date), unit_price]] + product = self.create_product_for_zone( + zone_a, product_detail_list, low_emission_discount, self.talpa_product_id + ) + permit = ParkingPermitFactory( + id=permit_id, + parking_zone=zone_a, + vehicle=vehicle, + contract_type=ContractType.OPEN_ENDED, + start_time=permit_start_time, + end_time=permit_end_time, + month_count=1, + primary_vehicle=primary_permit, + ) + return permit, product + + def prepare_request_data( + self, + permit, + talpa_order_id, + talpa_order_item_id, + talpa_subscription_id, + user_id, + ): + data = { + "subscriptionId": talpa_subscription_id, + "userId": user_id, + "namespace": "asukaspysakointi", + "orderItem": { + "merchantId": "00243b8a-b30c-4370-af19-90631bf9a370", + "orderItemId": talpa_order_item_id, + "orderId": talpa_order_id, + "meta": [ + { + "orderItemMetaId": "ee04456f-b330-4dab-a277-12d5cd24a4b7", + "orderItemId": talpa_order_item_id, + "orderId": talpa_order_id, + "key": "permitId", + "value": permit.id, + "label": None, + "visibleInCheckout": "false", + "ordinal": None, + }, + ], + }, + } + return data + + def create_product_for_zone( + self, zone, product_detail_list, low_emission_discount, talpa_product_id + ): + date_range, unit_price = product_detail_list[0] + start_date, end_date = date_range + return ProductFactory( + zone=zone, + type=ProductType.RESIDENT, + start_date=start_date, + end_date=end_date, + unit_price=unit_price, + low_emission_discount=low_emission_discount, + talpa_product_id=talpa_product_id, + ) + + +class ResolvePriceViewTestCase(BaseResolveEndpointTestCase): def test_resolve_price_view_should_return_bad_request_if_subscription_id_missing( self, ): @@ -221,11 +312,11 @@ def test_resolve_price_view_should_return_bad_request_if_permit_id_missing( def test_resolve_price_view_for_normal_emission_vehicle(self): unit_price = Decimal(60) low_emission_discount = Decimal(0) - permit, products = self._prepare_test_data( + permit, product = self.prepare_test_data( self.permit_id, unit_price, low_emission_discount ) url = reverse("parking_permits:talpa-price") - data = self._prepare_request_data( + data = self.prepare_request_data( permit, self.talpa_order_id, self.talpa_order_item_id, @@ -234,7 +325,7 @@ def test_resolve_price_view_for_normal_emission_vehicle(self): ) response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 200) - vat = products[0].vat + vat = product.vat price_vat = unit_price * vat self.assertEqual( response.data.get("subscriptionId"), self.talpa_subscription_id @@ -249,11 +340,11 @@ def test_resolve_price_view_for_normal_emission_vehicle(self): def test_resolve_price_view_for_low_emission_vehicle(self): unit_price = Decimal(60) low_emission_discount = Decimal(0.5) - permit, products = self._prepare_test_data( + permit, product = self.prepare_test_data( self.permit_id, unit_price, low_emission_discount ) url = reverse("parking_permits:talpa-price") - data = self._prepare_request_data( + data = self.prepare_request_data( permit, self.talpa_order_id, self.talpa_order_item_id, @@ -262,7 +353,7 @@ def test_resolve_price_view_for_low_emission_vehicle(self): ) response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 200) - vat = products[0].vat + vat = product.vat low_emission_price = ( unit_price - unit_price * low_emission_discount ) # discount price @@ -286,11 +377,11 @@ def test_resolve_price_view_for_secondary_normal_emission_vehicle(self): secondary_vehicle_increase_rate = Decimal(0.5) unit_price = Decimal(60) low_emission_discount = Decimal(0) - permit, products = self._prepare_test_data( + permit, product = self.prepare_test_data( self.permit_id, unit_price, low_emission_discount, primary_permit=False ) url = reverse("parking_permits:talpa-price") - data = self._prepare_request_data( + data = self.prepare_request_data( permit, self.talpa_order_id, self.talpa_order_item_id, @@ -299,7 +390,7 @@ def test_resolve_price_view_for_secondary_normal_emission_vehicle(self): ) response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 200) - vat = products[0].vat + vat = product.vat secondary_vehicle_price = ( unit_price + unit_price * secondary_vehicle_increase_rate ) # secondary vehicle price @@ -323,11 +414,11 @@ def test_resolve_price_view_for_secondary_low_emission_vehicle(self): secondary_vehicle_increase_rate = Decimal(0.5) unit_price = Decimal(60) low_emission_discount = Decimal(0.5) - permit, products = self._prepare_test_data( + permit, product = self.prepare_test_data( self.permit_id, unit_price, low_emission_discount, primary_permit=False ) url = reverse("parking_permits:talpa-price") - data = self._prepare_request_data( + data = self.prepare_request_data( permit, self.talpa_order_id, self.talpa_order_item_id, @@ -336,7 +427,7 @@ def test_resolve_price_view_for_secondary_low_emission_vehicle(self): ) response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 200) - vat = products[0].vat + vat = product.vat modified_price = unit_price modified_price -= modified_price * low_emission_discount @@ -367,7 +458,7 @@ def test_resolve_price_view_should_return_error_if_permit_products_missing( month_count=1, ) url = reverse("parking_permits:talpa-price") - data = self._prepare_request_data( + data = self.prepare_request_data( permit, self.talpa_order_id, self.talpa_order_item_id, @@ -382,93 +473,212 @@ def test_resolve_price_view_should_return_error_if_permit_products_missing( self.assertEqual(response.data.get("userId"), self.user_id) self.assertNotEquals(response.data.get("errorMessage"), "") - def _prepare_test_data( - self, permit_id, unit_price, low_emission_discount, primary_permit=True + +class ResolveProductViewTestCase(BaseResolveEndpointTestCase): + def test_resolve_product_view_should_return_bad_request_if_subscription_id_missing( + self, ): - start_date = datetime.date(2023, 1, 1) - end_date = datetime.date(2023, 12, 31) - vehicle = VehicleFactory( - power_type=VehiclePowerTypeFactory(identifier="01", name="Bensin"), - emission=45, - euro_class=6, - emission_type=EmissionType.WLTP, + url = reverse("parking_permits:talpa-product") + data = { + "orderId": self.talpa_order_id, + "userId": self.user_id, + } + response = self.client.post(url, data) + self.assertEqual(response.status_code, 400) + + def test_resolve_product_view_should_return_bad_request_if_user_id_missing(self): + url = reverse("parking_permits:talpa-product") + data = { + "subscription_id": self.talpa_subscription_id, + "orderId": self.talpa_order_id, + } + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 400) + + def test_resolve_product_view_should_return_bad_request_if_order_meta_missing( + self, + ): + url = reverse("parking_permits:talpa-product") + data = { + "permitId": self.permit_id, + } + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 400) + + def test_resolve_product_view_should_return_bad_request_if_permit_id_missing( + self, + ): + url = reverse("parking_permits:talpa-product") + data = { + "orderId": self.talpa_order_id, + } + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 400) + + def test_resolve_product_view_for_permit(self): + unit_price = Decimal(60) + low_emission_discount = Decimal(0) + permit, product = self.prepare_test_data( + self.permit_id, unit_price, low_emission_discount ) - LowEmissionCriteriaFactory( - start_date=start_date, - end_date=end_date, - nedc_max_emission_limit=None, - wltp_max_emission_limit=50, - euro_min_class_limit=6, + url = reverse("parking_permits:talpa-product") + data = self.prepare_request_data( + permit, + self.talpa_order_id, + self.talpa_order_item_id, + self.talpa_subscription_id, + self.user_id, ) - permit_start_time = datetime.datetime( - 2023, 9, 12, 13, 46, 0, tzinfo=datetime.timezone.utc + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.data.get("subscriptionId"), self.talpa_subscription_id ) - permit_end_time = datetime.datetime( - 2023, 10, 11, 23, 59, 0, tzinfo=datetime.timezone.utc + self.assertEqual(response.data.get("userId"), self.user_id) + self.assertEqual(response.data.get("productId"), str(product.talpa_product_id)) + self.assertEqual(response.data.get("productName"), product.name) + self.assertEqual(response.data.get("productLabel"), permit.vehicle.description) + + def test_resolve_product_view_after_vehicle_change(self): + unit_price = Decimal(60) + low_emission_discount = Decimal(0) + permit, product = self.prepare_test_data( + self.permit_id, unit_price, low_emission_discount ) - zone_a = ParkingZoneFactory(name="A") - product_detail_list = [[(start_date, end_date), unit_price]] - products = self._create_zone_products( - zone_a, product_detail_list, low_emission_discount + url = reverse("parking_permits:talpa-product") + data = self.prepare_request_data( + permit, + self.talpa_order_id, + self.talpa_order_item_id, + self.talpa_subscription_id, + self.user_id, ) - permit = ParkingPermitFactory( - id=permit_id, - parking_zone=zone_a, - vehicle=vehicle, - contract_type=ContractType.OPEN_ENDED, - start_time=permit_start_time, - end_time=permit_end_time, - month_count=1, - primary_vehicle=primary_permit, + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.data.get("subscriptionId"), self.talpa_subscription_id + ) + self.assertEqual(response.data.get("userId"), self.user_id) + self.assertEqual(response.data.get("productId"), str(product.talpa_product_id)) + self.assertEqual(response.data.get("productName"), product.name) + self.assertEqual(response.data.get("productLabel"), permit.vehicle.description) + + # Change vehicle + vehicle = VehicleFactory(registration_number="DON-313") + permit.vehicle = vehicle + permit.save() + + # Check that vehicle change is reflected in product + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.data.get("subscriptionId"), self.talpa_subscription_id ) - return permit, products + self.assertEqual(response.data.get("userId"), self.user_id) + self.assertEqual(response.data.get("productId"), str(product.talpa_product_id)) + self.assertEqual(response.data.get("productName"), product.name) + self.assertEqual(response.data.get("productLabel"), permit.vehicle.description) - def _prepare_request_data( + def test_resolve_product_view_after_address_change(self): + unit_price = Decimal(60) + low_emission_discount = Decimal(0) + permit, product = self.prepare_test_data( + self.permit_id, unit_price, low_emission_discount + ) + url = reverse("parking_permits:talpa-product") + data = self.prepare_request_data( + permit, + self.talpa_order_id, + self.talpa_order_item_id, + self.talpa_subscription_id, + self.user_id, + ) + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.data.get("subscriptionId"), self.talpa_subscription_id + ) + self.assertEqual(response.data.get("userId"), self.user_id) + self.assertEqual(response.data.get("productId"), str(product.talpa_product_id)) + self.assertEqual(response.data.get("productName"), product.name) + self.assertEqual(response.data.get("productLabel"), permit.vehicle.description) + + # Change address to another zone + talpa_product_id_zone_b = "3d2a87a1-f245-4cd8-9f67-d1b6b50a48bb" + zone_b = ParkingZoneFactory(name="B") + product_detail_list = [ + [(permit.start_time.date(), permit.end_time.date()), unit_price] + ] + product_b = self.create_product_for_zone( + zone_b, product_detail_list, low_emission_discount, talpa_product_id_zone_b + ) + permit.parking_zone = zone_b + permit.save() + + # Check that address zone change is reflected in product + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.data.get("subscriptionId"), self.talpa_subscription_id + ) + self.assertEqual(response.data.get("userId"), self.user_id) + self.assertEqual( + response.data.get("productId"), str(product_b.talpa_product_id) + ) + self.assertEqual(response.data.get("productName"), product_b.name) + self.assertEqual(response.data.get("productLabel"), permit.vehicle.description) + + def test_resolve_product_view_after_primary_permit_ending(self): + unit_price = Decimal(60) + low_emission_discount = Decimal(0) + permit, product = self.prepare_test_data( + self.permit_id, unit_price, low_emission_discount, primary_permit=False + ) + url = reverse("parking_permits:talpa-product") + data = self.prepare_request_data( + permit, + self.talpa_order_id, + self.talpa_order_item_id, + self.talpa_subscription_id, + self.user_id, + ) + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.data.get("subscriptionId"), self.talpa_subscription_id + ) + self.assertEqual(response.data.get("userId"), self.user_id) + self.assertEqual(response.data.get("productId"), str(product.talpa_product_id)) + self.assertEqual(response.data.get("productName"), product.name) + self.assertEqual(response.data.get("productLabel"), permit.vehicle.description) + + permit.refresh_from_db() + # Check that permit has been marked as primary + self.assertEqual(permit.primary_vehicle, True) + + def test_resolve_product_view_should_return_error_if_permit_products_missing( self, - permit, - talpa_order_id, - talpa_order_item_id, - talpa_subscription_id, - user_id, ): - data = { - "subscriptionId": talpa_subscription_id, - "userId": user_id, - "namespace": "asukaspysakointi", - "orderItem": { - "merchantId": "00243b8a-b30c-4370-af19-90631bf9a370", - "orderItemId": talpa_order_item_id, - "orderId": talpa_order_id, - "meta": [ - { - "orderItemMetaId": "ee04456f-b330-4dab-a277-12d5cd24a4b7", - "orderItemId": talpa_order_item_id, - "orderId": talpa_order_id, - "key": "permitId", - "value": permit.id, - "label": None, - "visibleInCheckout": "false", - "ordinal": None, - }, - ], - }, - } - return data - - def _create_zone_products(self, zone, product_detail_list, low_emission_discount): - products = [] - for date_range, unit_price in product_detail_list: - start_date, end_date = date_range - product = ProductFactory( - zone=zone, - type=ProductType.RESIDENT, - start_date=start_date, - end_date=end_date, - unit_price=unit_price, - low_emission_discount=low_emission_discount, - ) - products.append(product) - return products + permit = ParkingPermitFactory( + id=self.permit_id, + contract_type=ContractType.OPEN_ENDED, + month_count=1, + ) + url = reverse("parking_permits:talpa-product") + data = self.prepare_request_data( + permit, + self.talpa_order_id, + self.talpa_order_item_id, + self.talpa_subscription_id, + self.user_id, + ) + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.data.get("subscriptionId"), self.talpa_subscription_id + ) + self.assertEqual(response.data.get("userId"), self.user_id) + self.assertNotEquals(response.data.get("errorMessage"), "") class ResolveRightOfPurchaseViewTestCase(APITestCase): diff --git a/parking_permits/urls.py b/parking_permits/urls.py index ed110c8c..35ab488e 100644 --- a/parking_permits/urls.py +++ b/parking_permits/urls.py @@ -21,6 +21,11 @@ views.TalpaResolveAvailability.as_view(), name="talpa-availability", ), + path( + "api/talpa/resolve-product/", + views.TalpaResolveProduct.as_view(), + name="talpa-product", + ), path( "api/talpa/resolve-price/", views.TalpaResolvePrice.as_view(), diff --git a/parking_permits/views.py b/parking_permits/views.py index 6e3de957..ae4ecef8 100644 --- a/parking_permits/views.py +++ b/parking_permits/views.py @@ -62,6 +62,7 @@ ResolveAvailabilityResponseSerializer, ResolveAvailabilitySerializer, ResolvePriceResponseSerializer, + ResolveProductResponseSerializer, RightOfPurchaseResponseSerializer, SubscriptionSerializer, TalpaPayloadSerializer, @@ -162,6 +163,82 @@ def post(self, request, format=None): return Response(res) +class TalpaResolveProduct(APIView): + @swagger_auto_schema( + operation_description="Resolve product for subscription.", + request_body=TalpaPayloadSerializer, + responses={ + 200: openapi.Response("Resolve product", ResolveProductResponseSerializer) + }, + tags=["ResolveProduct"], + ) + def post(self, request, format=None): + logger.info( + f"Data received for resolve product = {json.dumps(request.data, default=str)}" + ) + subscription_id = request.data.get("subscriptionId") + if not subscription_id: + return bad_request_response("Subscription id is missing from request data") + user_id = request.data.get("userId") + if not user_id: + return bad_request_response("User id is missing from request data") + order_item_data = request.data.get("orderItem") + if not order_item_data: + return bad_request_response("Order item data is missing from request data") + meta = order_item_data.get("meta") + if not meta: + return bad_request_response( + "Order item metadata is missing from request data" + ) + permit_id = get_meta_value(meta, "permitId") + if not permit_id: + return bad_request_response( + "No permitId key available in meta list of key-value pairs" + ) + + try: + permit = ParkingPermit.objects.get(pk=permit_id) + # If permit is open ended and it is the only permit for the customer, set it as primary vehicle + if ( + permit.is_open_ended + and not permit.primary_vehicle + and not permit.customer.permits.active().exclude(id=permit.id).exists() + ): + permit.primary_vehicle = True + permit.save() + + products_with_quantity = permit.get_products_with_quantities() + if not products_with_quantity: + return bad_request_response("No products found for permit") + product_with_quantity = products_with_quantity[0] + if not product_with_quantity: + return bad_request_response( + "Product with quantity not found for permit" + ) + product = product_with_quantity[0] + if not product: + return bad_request_response("Product not found") + response = snake_to_camel_dict( + { + "subscription_id": subscription_id, + "user_id": user_id, + "product_id": str(product.talpa_product_id), + "product_name": product.name, + "product_label": permit.vehicle.description, + } + ) + except Exception as e: + response = snake_to_camel_dict( + { + "error_message": str(e), + "subscription_id": subscription_id, + "user_id": user_id, + } + ) + logger.info(f"Resolve product response = {json.dumps(response, default=str)}") + return Response(response) + + class TalpaResolvePrice(APIView): @swagger_auto_schema( operation_description="Resolve price of product from an order item.", @@ -637,6 +714,7 @@ def post(self, request, format=None): ParkingPermitEndType.AFTER_CURRENT_PERIOD, subscription_cancel_reason=request.data.get("reason"), cancel_from_talpa=False, + force_end=True, ) logger.info( f"Subscription {subscription} cancelled and permit ended after current period"