|
| 1 | +import datetime |
| 2 | +from decimal import Decimal |
| 3 | +from unittest.mock import patch |
| 4 | + |
| 5 | +import pytest |
| 6 | +from dateutil.relativedelta import relativedelta |
| 7 | +from django.test import TestCase |
| 8 | +from django.urls import reverse, reverse_lazy |
| 9 | +from freezegun import freeze_time |
| 10 | + |
| 11 | +from parking_permits.models.order import ( |
| 12 | + Order, |
| 13 | + SubscriptionStatus, |
| 14 | +) |
| 15 | +from parking_permits.models.parking_permit import ( |
| 16 | + ParkingPermitStatus, |
| 17 | +) |
| 18 | +from parking_permits.models.refund import Refund |
| 19 | +from parking_permits.tests.factories.customer import CustomerFactory |
| 20 | +from parking_permits.tests.factories.order import ( |
| 21 | + OrderItemFactory, |
| 22 | + SubscriptionFactory, |
| 23 | +) |
| 24 | +from parking_permits.tests.factories.parking_permit import ParkingPermitFactory |
| 25 | +from parking_permits.tests.factories.zone import ParkingZoneFactory |
| 26 | +from parking_permits.tests.test_resolver_utils import _create_zone_products |
| 27 | + |
| 28 | +MOCK_SYNC_WITH_PARKKIHUBI = "parking_permits.resolver_utils.sync_with_parkkihubi" |
| 29 | + |
| 30 | +MOCK_VALIDATE_ORDER = "parking_permits.models.order.OrderValidator.validate_order" |
| 31 | + |
| 32 | +MOCK_SEND_PERMIT_EMAIL = "parking_permits.resolver_utils.send_permit_email" |
| 33 | + |
| 34 | +MOCK_SEND_VEHICLE_DISCOUNT_EMAIL = ( |
| 35 | + "parking_permits.resolver_utils.send_vehicle_low_emission_discount_email" |
| 36 | +) |
| 37 | + |
| 38 | + |
| 39 | +def get_validated_order_data(talpa_order_id, talpa_order_item_id): |
| 40 | + return { |
| 41 | + "orderId": talpa_order_id, |
| 42 | + "checkoutUrl": "https://test.com", |
| 43 | + "loggedInCheckoutUrl": "https://test.com", |
| 44 | + "receiptUrl": "https://test.com", |
| 45 | + "items": [ |
| 46 | + { |
| 47 | + "orderItemId": talpa_order_item_id, |
| 48 | + "startDate": "2023-04-01T15:46:05.619", |
| 49 | + "priceGross": "45.00", |
| 50 | + "rowPriceTotal": "45.00", |
| 51 | + "vatPercentage": "25.50", |
| 52 | + "quantity": 1, |
| 53 | + } |
| 54 | + ], |
| 55 | + } |
| 56 | + |
| 57 | + |
| 58 | +class TestSubscriptionTestCase(TestCase): |
| 59 | + @pytest.mark.django_db() |
| 60 | + @patch(MOCK_SYNC_WITH_PARKKIHUBI) |
| 61 | + @patch(MOCK_SEND_PERMIT_EMAIL) |
| 62 | + @patch(MOCK_SEND_VEHICLE_DISCOUNT_EMAIL) |
| 63 | + @patch(MOCK_VALIDATE_ORDER) |
| 64 | + def test_subscription_cancel_does_not_generate_additional_zero_refund( |
| 65 | + self, |
| 66 | + mock_validate_order, |
| 67 | + mock_send_vehicle_discount_email, |
| 68 | + mock_send_permit_email, |
| 69 | + mock_sync_with_parkkihubi, |
| 70 | + ): |
| 71 | + talpa_order_id = "d86ca61d-97e9-410a-a1e3-4894873b1b35" |
| 72 | + talpa_order_item_id = "2f20c06d-2a9a-4a60-be4b-504d8a2f8c02" |
| 73 | + talpa_subscription_id = "f769b803-0bd0-489d-aa81-b35af391f391" |
| 74 | + |
| 75 | + zone = ParkingZoneFactory(name="A") |
| 76 | + |
| 77 | + customer = CustomerFactory() |
| 78 | + permit_start_time = datetime.datetime( |
| 79 | + 2023, 4, 30, 10, 00, 0, tzinfo=datetime.timezone.utc |
| 80 | + ) |
| 81 | + permit_end_time = permit_start_time + relativedelta(months=1) |
| 82 | + permit = ParkingPermitFactory( |
| 83 | + status=ParkingPermitStatus.VALID, |
| 84 | + customer=customer, |
| 85 | + parking_zone=zone, |
| 86 | + start_time=permit_start_time, |
| 87 | + end_time=permit_end_time, |
| 88 | + month_count=1, |
| 89 | + ) |
| 90 | + |
| 91 | + unit_price = Decimal(45) |
| 92 | + products_start_date = permit_start_time.date() - relativedelta(years=1) |
| 93 | + products_end_date = permit_start_time.date() + relativedelta(years=5) |
| 94 | + products = _create_zone_products( |
| 95 | + zone, |
| 96 | + [ |
| 97 | + [ |
| 98 | + (products_start_date, products_end_date), |
| 99 | + unit_price, |
| 100 | + ], |
| 101 | + ], |
| 102 | + ) |
| 103 | + |
| 104 | + subscription = SubscriptionFactory( |
| 105 | + talpa_subscription_id=talpa_subscription_id, |
| 106 | + status=SubscriptionStatus.CONFIRMED, |
| 107 | + ) |
| 108 | + |
| 109 | + initial_order = Order.objects.create_for_permits([permit]) |
| 110 | + initial_order.save() |
| 111 | + |
| 112 | + initial_order_item = OrderItemFactory( |
| 113 | + talpa_order_item_id=talpa_order_id, |
| 114 | + order=initial_order, |
| 115 | + product=products[0], |
| 116 | + permit=permit, |
| 117 | + subscription=subscription, |
| 118 | + start_time=permit_start_time, |
| 119 | + end_time=permit_end_time, |
| 120 | + ) |
| 121 | + |
| 122 | + mock_validate_order.return_value = get_validated_order_data( |
| 123 | + talpa_order_id, talpa_order_item_id |
| 124 | + ) |
| 125 | + |
| 126 | + with freeze_time(permit_start_time + relativedelta(days=20)): |
| 127 | + # Renew subscription |
| 128 | + order_view_url = reverse("parking_permits:order-notify") |
| 129 | + subscription_renewal_data = { |
| 130 | + "eventType": "SUBSCRIPTION_RENEWAL_ORDER_CREATED", |
| 131 | + "subscriptionId": talpa_subscription_id, |
| 132 | + "orderId": talpa_order_id, |
| 133 | + } |
| 134 | + response = self.client.post(order_view_url, subscription_renewal_data) |
| 135 | + self.assertEqual(response.status_code, 200) |
| 136 | + |
| 137 | + with freeze_time(permit_start_time + relativedelta(days=20, minutes=5)): |
| 138 | + # Subscription renewal payment |
| 139 | + payment_view_url = reverse_lazy("parking_permits:payment-notify") |
| 140 | + renewal_payment_data = { |
| 141 | + "eventType": "PAYMENT_PAID", |
| 142 | + "orderId": talpa_order_id, |
| 143 | + } |
| 144 | + response = self.client.post(payment_view_url, renewal_payment_data) |
| 145 | + self.assertEqual(response.status_code, 200) |
| 146 | + |
| 147 | + # There should be no refunds before ending the permit. |
| 148 | + refund_count_before_end = Refund.objects.count() |
| 149 | + self.assertEqual(refund_count_before_end, 0) |
| 150 | + |
| 151 | + # Cancel sub/end the permit, do this early enough to trigger a refund. |
| 152 | + with freeze_time(permit_start_time + relativedelta(days=20, minutes=6)): |
| 153 | + subscription_cancel_view_url = reverse( |
| 154 | + "parking_permits:subscription-notify" |
| 155 | + ) |
| 156 | + subscription_cancel_data = { |
| 157 | + "eventType": "SUBSCRIPTION_CANCELLED", |
| 158 | + "subscriptionId": talpa_subscription_id, |
| 159 | + "orderId": talpa_order_id, |
| 160 | + "orderItemId": talpa_order_item_id, |
| 161 | + } |
| 162 | + |
| 163 | + response = self.client.post( |
| 164 | + subscription_cancel_view_url, subscription_cancel_data |
| 165 | + ) |
| 166 | + self.assertEqual(response.status_code, 200) |
| 167 | + |
| 168 | + # Initial order item should not have refunds due to the logic |
| 169 | + # in subscription cancellation being run immediately after the |
| 170 | + # usual refund-logic when ending a permit. |
| 171 | + initial_order_item.refresh_from_db() |
| 172 | + order_item_refunds = initial_order_item.order.refunds.all() |
| 173 | + self.assertEqual(order_item_refunds.count(), 0) |
| 174 | + |
| 175 | + # The usual refunds are created before the sub is cancelled, |
| 176 | + # these should have positive amount. |
| 177 | + refunds_after_end = permit.refunds.all() |
| 178 | + assert len(refunds_after_end) > 0 |
| 179 | + for refund in refunds_after_end: |
| 180 | + assert refund.amount > 0 |
| 181 | + |
| 182 | + subscription.refresh_from_db() |
| 183 | + assert subscription.status == SubscriptionStatus.CANCELLED |
| 184 | + |
| 185 | + # VALID due to subscription cancellation using |
| 186 | + # AFTER_CURRENT_PERIOD as the end type. |
| 187 | + permit.refresh_from_db() |
| 188 | + assert permit.status == ParkingPermitStatus.VALID |
| 189 | + |
| 190 | + mock_sync_with_parkkihubi.assert_called() |
| 191 | + mock_validate_order.assert_called() |
| 192 | + mock_send_permit_email.assert_called() |
| 193 | + mock_send_vehicle_discount_email.assert_not_called() |
0 commit comments