diff --git a/locale/fi/LC_MESSAGES/django.po b/locale/fi/LC_MESSAGES/django.po index c8d8e975..6f5e4e63 100644 --- a/locale/fi/LC_MESSAGES/django.po +++ b/locale/fi/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-01-31 23:25+0200\n" +"POT-Creation-Date: 2024-02-11 09:55+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -619,18 +619,6 @@ msgstr "Heti" msgid "From" msgstr "Alkaen" -msgid "Arrived" -msgstr "Saapunut" - -msgid "Processing" -msgstr "Käsittelyssä" - -msgid "Accepted" -msgstr "Hyväksytty" - -msgid "Rejected" -msgstr "Hylätty" - msgid "Payment in progress" msgstr "Maksussa" @@ -646,6 +634,9 @@ msgstr "Tuleva ajoneuvo" msgid "Next parking zone" msgstr "Tuleva pysäköintialue" +msgid "Bypass Traficom validation" +msgstr "Ohita Traficom-tarkistus" + msgid "Vehicle changed date" msgstr "Ajoneuvon tietojen muuttamispäivä" @@ -758,6 +749,12 @@ msgstr "Avoin" msgid "Request for approval" msgstr "Hyväksyttävänä" +msgid "Accepted" +msgstr "Hyväksytty" + +msgid "Rejected" +msgstr "Hylätty" + msgid "Permits" msgstr "Pysäköintitunnukset" @@ -1091,22 +1088,27 @@ msgstr "Pysäköintitunnukseen on päivitetty tilapäisen ajoneuvon tiedot." msgid "Original vehicle has been restored to your permit." msgstr "Pysäköintitunnukseen on päivitetty alkuperäisen ajoneuvon tiedot." -#, python-format -msgid "" -"The attached vehicle is entitled to a 50%% discount in street parking when " -"paying with parking applications." -msgstr "" -"Oheinen ajoneuvo on oikeutettu 50%% alennukseen kadunvarsipysäköinnissä " -"pysäköintisovelluksilla maksettaessa." +msgid "THIS MESSAGE HAS BEEN SENT AUTOMATICALLY, DO NOT REPLY" +msgstr "TÄMÄ ON AUTOMAATTISESTI LÄHETETTY VIESTI, ÄLÄ VASTAA TÄHÄN" + +msgid "Dear partner!" +msgstr "Hyvä yhteistyökumppani!" + +msgid "A low-emission vehicle parking benefit has been added to the vehicle." +msgstr "Ohessa ajoneuvo, jolle on myönnetty vähäpäästöisen auton pysäköintietuus." -msgid "Discount: Low-emission vehicle" -msgstr "Alennus: Vähäpäästöinen ajoneuvo" +msgid "With kind regards" +msgstr "Ystävällisin terveisin" -msgid "Discount valid from" -msgstr "Alennusoikeus voimassa alkaen" +msgid "Urban environmental service" +msgstr "Kaupunkiympäristön asiakaspalvelu" -msgid "The right to a discount has been removed from the vehicle." -msgstr "Ajoneuvolta on poistettu oikeus alennukseen." +msgid "Tel. 09-310 22111" +msgstr "Puh. 09-310 22111" -msgid "Discount right ends" -msgstr "Alennusoikeus päättyy" +msgid "This vehicle is no longer subject to a discount" +msgstr "Ajoneuvolta on poistettu oikeus alennukseen" + +msgid "" +"A low-emission vehicle parking benefit has been removed from the vehicle." +msgstr "Ohessa ajoneuvo, jolta on poistettu vähäpäästöisen auton pysäköintietuus." diff --git a/locale/sv/LC_MESSAGES/django.po b/locale/sv/LC_MESSAGES/django.po index 8eecebf4..4499563b 100644 --- a/locale/sv/LC_MESSAGES/django.po +++ b/locale/sv/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-01-31 23:25+0200\n" +"POT-Creation-Date: 2024-02-11 09:55+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -614,18 +614,6 @@ msgstr "Genast" msgid "From" msgstr "Från" -msgid "Arrived" -msgstr "Anlände" - -msgid "Processing" -msgstr "Under behandling" - -msgid "Accepted" -msgstr "Accepterad" - -msgid "Rejected" -msgstr "Avslå" - msgid "Payment in progress" msgstr "Betalning pågår" @@ -641,6 +629,9 @@ msgstr "Nästa fordon" msgid "Next parking zone" msgstr "Nästa parkering zone" +msgid "Bypass Traficom validation" +msgstr "Bypass Traficom-validering" + msgid "Vehicle changed date" msgstr "Fordonet ändringdatum" @@ -755,6 +746,12 @@ msgstr "Öppna" msgid "Request for approval" msgstr "Acceptabel" +msgid "Accepted" +msgstr "Accepterad" + +msgid "Rejected" +msgstr "Avslå" + msgid "Permits" msgstr "Parkeringstillstånder" @@ -970,7 +967,8 @@ msgstr "Fordonet %(registration_number)s är avställd" #, python-format msgid "Vehicle's %(registration_number)s weight exceeds maximum allowed limit" -msgstr "Fordonets %(registration_number)s vikt överstiger den högsta tillåtna gränsen" +msgstr "" +"Fordonets %(registration_number)s vikt överstiger den högsta tillåtna gränsen" msgid "The person has no driving licence" msgstr "Personen har inget körkort" @@ -1093,22 +1091,27 @@ msgstr "Tillfällig fordonsinformation har uppdaterats till ditt tillstånd." msgid "Original vehicle has been restored to your permit." msgstr "Originalfordonet har återställts till ditt tillstånd." -#, python-format -msgid "" -"The attached vehicle is entitled to a 50%% discount in street parking when " -"paying with parking applications." -msgstr "" -"Det bifogade fordonet har rätt till 50%% rabatt på gatuparkering när betala " -"med parkeringsansökningar." +msgid "THIS MESSAGE HAS BEEN SENT AUTOMATICALLY, DO NOT REPLY" +msgstr "DETTA ÄR AUTOMATISKT SKICKA MEDDELANDE, SVARA INTE PÅ DETTA" + +msgid "Dear partner!" +msgstr "Bra partner!" + +msgid "A low-emission vehicle parking benefit has been added to the vehicle." +msgstr "En förmån för fordonsparkering med låga utsläpp har lagts till fordonet." -msgid "Discount: Low-emission vehicle" -msgstr "Rabatt: Fordon med låga utsläpp" +msgid "With kind regards" +msgstr "Vänliga Hälsningar" -msgid "Discount valid from" -msgstr "Rabatten giltig från" +msgid "Urban environmental service" +msgstr "Stadsmiljö kundservice" -msgid "The right to a discount has been removed from the vehicle." +msgid "Tel. 09-310 22111" +msgstr "Tel. 09-310 22111" + +msgid "This vehicle is no longer subject to a discount" msgstr "Rätten till rabatt har tagits bort från fordonet." -msgid "Discount right ends" -msgstr "Rabatten till höger upphör" +msgid "" +"A low-emission vehicle parking benefit has been removed from the vehicle." +msgstr "En förmån för fordonsparkering med låga utsläpp har tagits bort från fordonet." diff --git a/parking_permits/admin_resolvers.py b/parking_permits/admin_resolvers.py index 826d4a77..2ca5601a 100644 --- a/parking_permits/admin_resolvers.py +++ b/parking_permits/admin_resolvers.py @@ -1316,7 +1316,10 @@ def resolve_announcement(obj, info, announcement_id): def post_create_announcement(announcement: Announcement): customer_ids = ( - ParkingPermit.objects.filter(parking_zone__in=announcement.parking_zones) + ParkingPermit.objects.filter( + parking_zone__in=announcement.parking_zones, + status=ParkingPermitStatus.VALID, + ) .values_list("customer_id", flat=True) .order_by("customer") .distinct() diff --git a/parking_permits/cron.py b/parking_permits/cron.py index 2ef2a286..bff00b93 100644 --- a/parking_permits/cron.py +++ b/parking_permits/cron.py @@ -52,8 +52,9 @@ def automatic_expiration_remind_notification_of_permits(): end_time__lt=now + relativedelta(weeks=1), status=ParkingPermitStatus.VALID ) for permit in expiring_permits: - send_permit_email(PermitEmailType.EXPIRATION_REMIND, permit) - count += 1 + success = send_permit_email(PermitEmailType.EXPIRATION_REMIND, permit) + if success: + count += 1 logger.info( "Automatically sending remind notifications for permits completed. " f"{count} notifications sent." @@ -79,8 +80,6 @@ def automatic_syncing_of_permits_to_parkkihubi(): statuses_to_sync = [ ParkingPermitStatus.CLOSED, ParkingPermitStatus.VALID, - ParkingPermitStatus.ACCEPTED, - ParkingPermitStatus.REJECTED, ] permits = ParkingPermit.objects.filter( synced_with_parkkihubi=False, status__in=statuses_to_sync diff --git a/parking_permits/customer_permit.py b/parking_permits/customer_permit.py index e130387e..4fab3086 100644 --- a/parking_permits/customer_permit.py +++ b/parking_permits/customer_permit.py @@ -57,8 +57,8 @@ OPEN_ENDED = ContractType.OPEN_ENDED DRAFT = ParkingPermitStatus.DRAFT VALID = ParkingPermitStatus.VALID -PROCESSING = ParkingPermitStatus.PROCESSING PAYMENT_IN_PROGRESS = ParkingPermitStatus.PAYMENT_IN_PROGRESS +CANCELLED = ParkingPermitStatus.CANCELLED FROM = ParkingPermitStartType.FROM FIXED_PERIOD = ContractType.FIXED_PERIOD @@ -174,8 +174,8 @@ def get(self): products.append(product) permit.products = products - # automatically change permit status to draft if payment is not completed in configured time - # (default 20 minutes) + # automatically cancel permit and it's latest order if payment is not completed in configured time + # (default 15 minutes) payment_wait_time_buffer = ( settings.TALPA_ORDER_PAYMENT_WEBHOOK_WAIT_BUFFER_MINS ) @@ -188,7 +188,10 @@ def get(self): ) < tz.localtime(tz.now()) ): - permit.status = DRAFT + permit.status = CANCELLED + latest_order = permit.latest_order + latest_order.status = OrderStatus.CANCELLED + latest_order.save() permit.save() permits.append(permit) return permits diff --git a/parking_permits/management/commands/remind_permit_expiration.py b/parking_permits/management/commands/remind_permit_expiration.py index f1e48714..e6311a21 100644 --- a/parking_permits/management/commands/remind_permit_expiration.py +++ b/parking_permits/management/commands/remind_permit_expiration.py @@ -1,6 +1,6 @@ from django.core.management.base import BaseCommand -from parking_permits.cron import automatic_expiration_remind_notification_of_permits +# from parking_permits.cron import automatic_expiration_remind_notification_of_permits class Command(BaseCommand): @@ -10,5 +10,5 @@ def handle(self, *args, **options): self.stdout.write( self.style.SUCCESS("Reminding of permit expiration started...") ) - automatic_expiration_remind_notification_of_permits() + # automatic_expiration_remind_notification_of_permits() self.stdout.write(self.style.SUCCESS("Reminding of permit expiration done.")) diff --git a/parking_permits/migrations/0049_alter_parkingpermit_status.py b/parking_permits/migrations/0049_alter_parkingpermit_status.py new file mode 100644 index 00000000..b9f74b01 --- /dev/null +++ b/parking_permits/migrations/0049_alter_parkingpermit_status.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.1 on 2024-02-06 11:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("parking_permits", "0048_merge_20240202_0838"), + ] + + operations = [ + migrations.AlterField( + model_name="parkingpermit", + name="status", + field=models.CharField( + choices=[ + ("DRAFT", "Draft"), + ("PAYMENT_IN_PROGRESS", "Payment in progress"), + ("VALID", "Valid"), + ("CANCELLED", "Cancelled"), + ("CLOSED", "Closed"), + ], + default="DRAFT", + max_length=32, + verbose_name="Status", + ), + ), + ] diff --git a/parking_permits/models/parking_permit.py b/parking_permits/models/parking_permit.py index c2ce35f8..21c62d21 100644 --- a/parking_permits/models/parking_permit.py +++ b/parking_permits/models/parking_permit.py @@ -51,12 +51,9 @@ class ParkingPermitStartType(models.TextChoices): class ParkingPermitStatus(models.TextChoices): DRAFT = "DRAFT", _("Draft") - ARRIVED = "ARRIVED", _("Arrived") - PROCESSING = "PROCESSING", _("Processing") - ACCEPTED = "ACCEPTED", _("Accepted") - REJECTED = "REJECTED", _("Rejected") PAYMENT_IN_PROGRESS = "PAYMENT_IN_PROGRESS", _("Payment in progress") VALID = "VALID", _("Valid") + CANCELLED = "CANCELLED", _("Cancelled") CLOSED = "CLOSED", _("Closed") @@ -344,8 +341,14 @@ def can_end_immediately(self): @property def can_end_after_current_period(self): - return self.is_valid and ( - self.end_time is None or self.current_period_end_time < self.end_time + if not self.is_valid: + return False + + if self.end_time is None: + return False + + return timezone.localdate(self.current_period_end_time) <= timezone.localdate( + self.end_time ) @property diff --git a/parking_permits/schema/parking_permit_admin.graphql b/parking_permits/schema/parking_permit_admin.graphql index 37c50886..adaf2dd1 100644 --- a/parking_permits/schema/parking_permit_admin.graphql +++ b/parking_permits/schema/parking_permit_admin.graphql @@ -99,12 +99,9 @@ type AnnouncementNode { enum ParkingPermitStatus { DRAFT - ARRIVED - PROCESSING - ACCEPTED - REJECTED PAYMENT_IN_PROGRESS VALID + CANCELLED CLOSED } diff --git a/parking_permits/services/mail.py b/parking_permits/services/mail.py index d1c258b7..351719d0 100644 --- a/parking_permits/services/mail.py +++ b/parking_permits/services/mail.py @@ -1,3 +1,5 @@ +import logging + from django.conf import settings from django.core import mail from django.template.loader import render_to_string @@ -5,6 +7,8 @@ from django.utils.html import strip_tags from django.utils.translation import gettext_lazy as _ +logger = logging.getLogger("db") + class PermitEmailType: CREATED = "created" @@ -53,35 +57,49 @@ class PermitEmailType: def send_permit_email(action, permit): with translation.override(permit.customer.language): + logger.info(f"Sending permit {permit.pk} {action} email") subject = permit_email_subjects[action] template = permit_email_templates[action] html_message = render_to_string(template, context={"permit": permit}) plain_message = strip_tags(html_message) recipient_list = [permit.customer.email] - mail.send_mail( - subject, - plain_message, - None, - recipient_list, - html_message=html_message, - ) + try: + mail.send_mail( + subject, + plain_message, + None, + recipient_list, + html_message=html_message, + ) + return True + except Exception as e: + logger.error("Could not send permit email", exc_info=e) + return False def send_vehicle_low_emission_discount_email(action, permit): with translation.override("fi"): + logger.info( + f"Sending vehicle low emission discount email for permit {permit.pk} {action} email" + ) subject = permit_email_subjects[action] template = permit_email_templates[action] html_message = render_to_string(template, context={"permit": permit}) plain_message = strip_tags(html_message) recipient_list = settings.THIRD_PARTY_PARKING_PROVIDER_EMAILS for recipient in recipient_list: - mail.send_mail( - subject, - plain_message, - None, - [recipient], - html_message=html_message, - ) + try: + mail.send_mail( + subject, + plain_message, + None, + [recipient], + html_message=html_message, + ) + except Exception as e: + logger.error( + "Could not send vehicle low emission discount email", exc_info=e + ) class RefundEmailType: @@ -105,18 +123,22 @@ class RefundEmailType: def send_refund_email(action, customer, refund): with translation.override(customer.language): + logger.info(f"Sending refund {refund.pk} {action} email") subject = refund_email_subjects[action] template = refund_email_templates[action] html_message = render_to_string(template, context={"refund": refund}) plain_message = strip_tags(html_message) recipient_list = [customer.email] - mail.send_mail( - subject, - plain_message, - None, - recipient_list, - html_message=html_message, - ) + try: + mail.send_mail( + subject, + plain_message, + None, + recipient_list, + html_message=html_message, + ) + except Exception as e: + logger.error("Could not send refund email", exc_info=e) def send_announcement_email(customers, announcement): diff --git a/parking_permits/templates/emails/vehicle_low_emission_discount_activated.html b/parking_permits/templates/emails/vehicle_low_emission_discount_activated.html index 4e43e907..d84bf69f 100644 --- a/parking_permits/templates/emails/vehicle_low_emission_discount_activated.html +++ b/parking_permits/templates/emails/vehicle_low_emission_discount_activated.html @@ -1,15 +1,31 @@ -{% extends "emails/base.html" %} {% load i18n %} + + + + + + {% translate "The vehicle is entitled to a discount" %} + -{% block title %}{% translate "The vehicle is entitled to a discount" %}{% endblock %} - -{% block content %} -

- {% translate "The attached vehicle is entitled to a 50% discount in street parking when paying with parking applications." %} -

-

- {% translate "Vehicle" %}: {{ permit.vehicle.registration_number }}
- {% translate "Discount: Low-emission vehicle" %}
- {% translate "Discount valid from" %}: {% now "j.n.Y, H:i" %} -

-{% endblock %} + +
+
{% translate "The vehicle is entitled to a discount" %}
+
+

+ {% translate "THIS MESSAGE HAS BEEN SENT AUTOMATICALLY, DO NOT REPLY" %}
+
+ {% translate "Dear partner!" %}
+
+ {% translate "A low-emission vehicle parking benefit has been added to the vehicle." %}
+
+ {{ permit.vehicle.registration_number }}
+
+ {% translate "With kind regards" %}
+ {% translate "Urban environmental service" %}
+ {% translate "Tel. 09-310 22111" %}
+ kaupunkiymparisto@hel.fi +

+
+
+ + diff --git a/parking_permits/templates/emails/vehicle_low_emission_discount_deactivated.html b/parking_permits/templates/emails/vehicle_low_emission_discount_deactivated.html index 651cc146..cbf3f140 100644 --- a/parking_permits/templates/emails/vehicle_low_emission_discount_deactivated.html +++ b/parking_permits/templates/emails/vehicle_low_emission_discount_deactivated.html @@ -1,14 +1,31 @@ -{% extends "emails/base.html" %} {% load i18n %} + + + + + + {% translate "This vehicle is no longer subject to a discount" %} + -{% block title %}{% translate "Vehicle discount right expired" %}{% endblock %} - -{% block content %} -

- {% translate "The right to a discount has been removed from the vehicle." %} -

-

- {% translate "Vehicle" %}: {{ permit.vehicle.registration_number }}
- {% translate "Discount right ends" %}: {% now "j.n.Y, H:i" %} -

-{% endblock %} + +
+
{% translate "This vehicle is no longer subject to a discount" %}
+
+

+ {% translate "THIS MESSAGE HAS BEEN SENT AUTOMATICALLY, DO NOT REPLY" %}
+
+ {% translate "Dear partner!" %}
+
+ {% translate "A low-emission vehicle parking benefit has been removed from the vehicle." %}
+
+ {{ permit.vehicle.registration_number }}
+
+ {% translate "With kind regards" %}
+ {% translate "Urban environmental service" %}
+ {% translate "Tel. 09-310 22111" %}
+ kaupunkiymparisto@hel.fi +

+
+
+ + diff --git a/parking_permits/tests/models/test_parking_permit.py b/parking_permits/tests/models/test_parking_permit.py index 8d25b4fd..268d19e6 100644 --- a/parking_permits/tests/models/test_parking_permit.py +++ b/parking_permits/tests/models/test_parking_permit.py @@ -58,6 +58,17 @@ def _create_zone_products(self, zone, product_detail_list): @freeze_time(timezone.make_aware(datetime(2021, 11, 15))) def test_should_return_correct_months_used(self): + start_time = timezone.now() + end_time = get_end_time(start_time, 1) + + open_ended_started_immediately = ParkingPermitFactory( + contract_type=ContractType.OPEN_ENDED, + start_time=start_time, + end_time=end_time, + ) + + self.assertEqual(open_ended_started_immediately.months_used, 1) + start_time = timezone.make_aware(datetime(2021, 9, 15)) end_time = get_end_time(start_time, 6) fixed_period_permit_started_2_months_ago = ParkingPermitFactory( @@ -95,6 +106,47 @@ def test_should_return_correct_months_used(self): ) self.assertEqual(open_ended_permit_started_two_years_ago.months_used, 25) + @freeze_time(timezone.make_aware(datetime(2024, 1, 1))) + def test_can_end_after_current_period_end_time_fixed_period(self): + start_time = timezone.make_aware(datetime(2024, 1, 1)) + end_time = get_end_time(start_time, 6) + permit = ParkingPermitFactory( + status=ParkingPermitStatus.VALID, + contract_type=ContractType.FIXED_PERIOD, + start_time=start_time, + end_time=end_time, + month_count=6, + ) + self.assertTrue(permit.can_end_after_current_period) + + @freeze_time(timezone.make_aware(datetime(2024, 1, 1))) + def test_can_end_after_current_period_end_time_fixed_period_current_period_end_time_gt_end_time( + self, + ): + start_time = timezone.make_aware(datetime(2023, 11, 1)) + end_time = get_end_time(start_time, 1) + permit = ParkingPermitFactory( + status=ParkingPermitStatus.VALID, + contract_type=ContractType.FIXED_PERIOD, + start_time=start_time, + end_time=end_time, + month_count=3, + ) + self.assertFalse(permit.can_end_after_current_period) + + @freeze_time(timezone.make_aware(datetime(2024, 1, 1))) + def test_can_end_after_current_period_end_time_open_ended(self): + start_time = timezone.make_aware(datetime(2024, 1, 1)) + end_time = get_end_time(start_time, 1) + permit = ParkingPermitFactory( + status=ParkingPermitStatus.VALID, + contract_type=ContractType.OPEN_ENDED, + start_time=start_time, + end_time=end_time, + month_count=1, + ) + self.assertTrue(permit.can_end_after_current_period) + @freeze_time(timezone.make_aware(datetime(2021, 11, 15))) def test_should_return_correct_months_left(self): start_time = timezone.make_aware(datetime(2021, 9, 15)) @@ -136,6 +188,25 @@ def test_should_return_correct_months_left(self): @freeze_time(timezone.make_aware(datetime(2022, 1, 20))) def test_should_return_correct_end_time_of_current_time(self): + start_time = timezone.make_aware(datetime(2022, 1, 20)) + end_time = get_end_time(start_time, 1) + permit = ParkingPermitFactory( + contract_type=ContractType.OPEN_ENDED, + start_time=start_time, + end_time=end_time, + month_count=1, + ) + + self.assertEqual( + permit.end_time, + permit.current_period_end_time, + ) + + self.assertEqual( + permit.current_period_end_time, + timezone.make_aware(datetime(2022, 2, 19, 23, 59, 59, 999999)), + ) + start_time = timezone.make_aware(datetime(2021, 11, 15)) end_time = get_end_time(start_time, 6) permit = ParkingPermitFactory( diff --git a/parking_permits/tests/test_announcement.py b/parking_permits/tests/test_announcement.py index 59bf06e0..933500d2 100644 --- a/parking_permits/tests/test_announcement.py +++ b/parking_permits/tests/test_announcement.py @@ -6,6 +6,7 @@ from parking_permits import admin_resolvers from parking_permits.models import Announcement +from parking_permits.models.parking_permit import ParkingPermitStatus from parking_permits.services.mail import send_announcement_email from parking_permits.tests.factories import ParkingZoneFactory from parking_permits.tests.factories.announcement import AnnouncementFactory @@ -99,7 +100,11 @@ def test_should_get_correct_customers_from_single_parking_zone( zone_a_customer = CustomerFactory(zone=zone_a) CustomerFactory(zone=zone_b) - ParkingPermitFactory(customer=zone_a_customer, parking_zone=zone_a) + ParkingPermitFactory( + customer=zone_a_customer, + parking_zone=zone_a, + status=ParkingPermitStatus.VALID, + ) # Set the announcement for zone A. self.announcement._parking_zones.set([zone_a]) @@ -127,8 +132,16 @@ def test_should_get_correct_customers_from_multiple_parking_zones( CustomerFactory(zone=zone_c) expected_customers = [zone_a_customer, zone_b_customer] - ParkingPermitFactory(customer=zone_a_customer, parking_zone=zone_a) - ParkingPermitFactory(customer=zone_b_customer, parking_zone=zone_b) + ParkingPermitFactory( + customer=zone_a_customer, + parking_zone=zone_a, + status=ParkingPermitStatus.VALID, + ) + ParkingPermitFactory( + customer=zone_b_customer, + parking_zone=zone_b, + status=ParkingPermitStatus.VALID, + ) # Set the announcement for zone A & B. self.announcement._parking_zones.set([zone_a, zone_b]) @@ -141,6 +154,48 @@ def test_should_get_correct_customers_from_multiple_parking_zones( for idx, customer in enumerate(customers_arg.order_by("zone__name")): self.assertEqual(customer, expected_customers[idx]) + def test_should_get_correct_customers_only_with_valid_status( + self, mock_send_announcement_email: MagicMock + ): + # Create zones A, B and C. + zone_a = ParkingZoneFactory(name="A") + zone_b = ParkingZoneFactory(name="B") + zone_c = ParkingZoneFactory(name="C") + + # Create customers for the zones. + zone_a_customer = CustomerFactory(zone=zone_a) + zone_b_customer = CustomerFactory(zone=zone_b) + zone_c_customer = CustomerFactory(zone=zone_c) + expected_customers = [zone_a_customer, zone_b_customer] + + # Create permits for the zones, but only A and B will be valid. + ParkingPermitFactory( + customer=zone_a_customer, + parking_zone=zone_a, + status=ParkingPermitStatus.VALID, + ) + ParkingPermitFactory( + customer=zone_b_customer, + parking_zone=zone_b, + status=ParkingPermitStatus.VALID, + ) + ParkingPermitFactory( + customer=zone_c_customer, + parking_zone=zone_c, + status=ParkingPermitStatus.DRAFT, + ) + + # Set the announcement for zone A, B & C. + self.announcement._parking_zones.set([zone_a, zone_b, zone_c]) + admin_resolvers.post_create_announcement(self.announcement) + + # Should have two customers (from zone A & B). + mock_send_announcement_email.assert_called_once() + customers_arg = mock_send_announcement_email.call_args.args[0] + self.assertEqual(len(customers_arg), 2) + for idx, customer in enumerate(customers_arg.order_by("zone__name")): + self.assertEqual(customer, expected_customers[idx]) + class SendAnnouncementMailTest(TestCase): def setUp(self): diff --git a/parking_permits/tests/test_views.py b/parking_permits/tests/test_views.py index 9f75470e..9751b497 100644 --- a/parking_permits/tests/test_views.py +++ b/parking_permits/tests/test_views.py @@ -1028,14 +1028,86 @@ def test_order_view_should_return_not_found_if_talpa_subscription_does_not_exist self.assertEqual(response.status_code, 404) def test_order_cancellation(self): + talpa_existing_order_id = "d86ca61d-97e9-410a-a1e3-4894873b1b35" + customer = CustomerFactory() + permit_start_time = datetime.datetime( + 2024, 2, 8, 10, 00, 0, tzinfo=datetime.timezone.utc + ) + permit_end_time = datetime.datetime( + 2024, 3, 7, 23, 59, 0, tzinfo=datetime.timezone.utc + ) + permit = ParkingPermitFactory( + status=ParkingPermitStatus.VALID, + customer=customer, + start_time=permit_start_time, + end_time=permit_end_time, + ) + order = OrderFactory( + talpa_order_id=talpa_existing_order_id, + customer=customer, + status=OrderStatus.CONFIRMED, + paid_time=tz.make_aware( + datetime.datetime.strptime( + "2024-02-08T10:00:00.000Z", "%Y-%m-%dT%H:%M:%S.%fZ" + ) + ), + ) + order.permits.add(permit) + order.save() + url = reverse("parking_permits:order-notify") data = { "eventType": "ORDER_CANCELLED", - "orderId": "d86ca61d-97e9-410a-a1e3-4894873b1b35", - "subscriptionId": "f769b803-0bd0-489d-aa81-b35af391f391", + "orderId": talpa_existing_order_id, + } + response = self.client.post(url, data) + self.assertEqual(response.status_code, 200) + order.refresh_from_db() + permit.refresh_from_db() + # Order and permit statuses should change to cancelled + self.assertEqual(order.status, OrderStatus.CANCELLED) + self.assertEqual(permit.status, ParkingPermitStatus.CANCELLED) + + def test_order_cancellation_for_closed_permit(self): + talpa_existing_order_id = "d86ca61d-97e9-410a-a1e3-4894873b1b35" + customer = CustomerFactory() + permit_start_time = datetime.datetime( + 2024, 2, 8, 10, 00, 0, tzinfo=datetime.timezone.utc + ) + permit_end_time = datetime.datetime( + 2024, 3, 7, 23, 59, 0, tzinfo=datetime.timezone.utc + ) + permit = ParkingPermitFactory( + status=ParkingPermitStatus.CLOSED, + customer=customer, + start_time=permit_start_time, + end_time=permit_end_time, + ) + order = OrderFactory( + talpa_order_id=talpa_existing_order_id, + customer=customer, + status=OrderStatus.CONFIRMED, + paid_time=tz.make_aware( + datetime.datetime.strptime( + "2024-02-08T10:00:00.000Z", "%Y-%m-%dT%H:%M:%S.%fZ" + ) + ), + ) + order.permits.add(permit) + order.save() + + url = reverse("parking_permits:order-notify") + data = { + "eventType": "ORDER_CANCELLED", + "orderId": talpa_existing_order_id, } response = self.client.post(url, data) self.assertEqual(response.status_code, 200) + order.refresh_from_db() + permit.refresh_from_db() + # Order and permit statuses should not change + self.assertEqual(order.status, OrderStatus.CONFIRMED) + self.assertEqual(permit.status, ParkingPermitStatus.CLOSED) @override_settings(DEBUG=True) @patch.object(OrderValidator, "validate_order") diff --git a/parking_permits/views.py b/parking_permits/views.py index 8e1ccf87..c842b9c2 100644 --- a/parking_permits/views.py +++ b/parking_permits/views.py @@ -244,7 +244,8 @@ def post(self, request, format=None): TalpaOrderManager.append_detail_meta( order_item_response_data, permit, - fixed_end_time=permit.end_time + relativedelta(months=1), + fixed_end_time=tz.localtime(permit.end_time) + + relativedelta(months=1), ) response = snake_to_camel_dict( @@ -535,12 +536,33 @@ def post(self, request, format=None): talpa_subscription_id = request.data.get("subscriptionId") event_type = request.data.get("eventType") - # Always bypass Order cancelled event - if event_type == "ORDER_CANCELLED": - return ok_response(f"Order {talpa_order_id} cancel bypassed") - if not talpa_order_id: return bad_request_response("Talpa order id is missing from request data") + + if event_type == "ORDER_CANCELLED": + try: + order = Order.objects.get(talpa_order_id=talpa_order_id) + order_permits = order.permits.filter( + status__in=[ + ParkingPermitStatus.DRAFT, + ParkingPermitStatus.PAYMENT_IN_PROGRESS, + ParkingPermitStatus.VALID, + ] + ) + if order_permits: + logger.info(f"Cancelling order: {talpa_order_id}") + order.status = OrderStatus.CANCELLED + order.save() + order_permits.update( + status=ParkingPermitStatus.CANCELLED, modified_at=tz.now() + ) + logger.info( + f"{order} is cancelled and order permits are set to CANCELLED-status" + ) + except Order.DoesNotExist: + return not_found_response(f"Order {talpa_order_id} does not exist") + return Response({"message": "Order cancel event processed"}, status=200) + if not talpa_subscription_id: return bad_request_response( "Talpa subscription id is missing from request data" diff --git a/project/settings.py b/project/settings.py index 8a820f8f..482b1e62 100644 --- a/project/settings.py +++ b/project/settings.py @@ -27,7 +27,7 @@ TALPA_PRODUCT_EXPERIENCE_API=(str, ""), TALPA_ORDER_EXPERIENCE_API=(str, ""), TALPA_ORDER_PAYMENT_MAX_PERIOD_MINS=(int, 15), - TALPA_ORDER_PAYMENT_WEBHOOK_WAIT_BUFFER_MINS=(int, 5), + TALPA_ORDER_PAYMENT_WEBHOOK_WAIT_BUFFER_MINS=(int, 0), TALPA_WEBHOOK_WAIT_BUFFER_SECONDS=(int, 5), TALPA_SUBSCRIPTION_PERIOD_UNIT=(str, "monthly"), KAMI_URL=(str, "https://kartta.hel.fi/ws/geoserver/avoindata/wfs"),