diff --git a/locale/fi/LC_MESSAGES/django.po b/locale/fi/LC_MESSAGES/django.po index 5900f49c..c8d8e975 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: 2023-12-02 15:09+0200\n" +"POT-Creation-Date: 2024-01-31 23:25+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -60,14 +60,14 @@ msgstr "Pysäköintialuetta ei löytynyt tälle sijainnille" msgid "Multiple parking zones found for the location" msgstr "Useita pysäköintialueita löytyi tälle sijainnille" -msgid "Person not found" -msgstr "Henkilöä ei löytynyt" +msgid "Customer not found" +msgstr "Asiakasta ei löytynyt" msgid "Customer search error" msgstr "Asiakkaan hakuvirhe" -msgid "Owner/holder data of a vehicle could not be verified. Please check with Traficom for data transfer restrictions." -msgstr "Omistajuutta/haltijuutta ei pystytty todentamaan. Tarkistathan Traficomilta tiedonluovutuskiellot." +msgid "Owner/holder data of a vehicle could not be verified" +msgstr "Omistajuutta/haltijuutta ei pystytty todentamaan" msgid "Customer does not have a valid driving licence for this vehicle" msgstr "Puutteelliset ajokorttitiedot tälle ajoneuvolle" @@ -182,6 +182,13 @@ msgstr "" msgid "Permit for a given vehicle already exist." msgstr "Kyseisellä ajoneuvolla on jo pysäköintitunnus." +msgid "" +"Owner/holder data of a vehicle could not be verified. Please check with " +"Traficom for data transfer restrictions." +msgstr "" +"Omistajuutta/haltijuutta ei pystytty todentamaan. Tarkistathan Traficomilta " +"tiedonluovutuskiellot." + msgid "Customer does not have a valid driving licence" msgstr "Puutteelliset ajokorttitiedot" @@ -318,9 +325,6 @@ msgstr "Helsingin kaupunki" msgid "Personal data - Digital and population data services agency" msgstr "Henkilötiedot - Digi- ja väestötietovirasto" -msgid "Source: Transport register, Traficom" -msgstr "Lähde: Liikenneasioidenrekisteri, Traficom" - msgid "Permit ID" msgstr "Tunniste" @@ -772,15 +776,24 @@ msgstr "Väliaikaiset ajoneuvot" msgid "M1" msgstr "M1" +msgid "M1G" +msgstr "M1G" + msgid "M2" msgstr "M2" msgid "N1" msgstr "N1" +msgid "N1G" +msgstr "N1G" + msgid "N2" msgstr "N2" +msgid "N2G" +msgstr "N2G" + msgid "L3e-A1" msgstr "L3e-A1" @@ -910,6 +923,9 @@ msgstr "Asiakkaalla ei ole aktiivisia tunnuksia" msgid "Conflict parking zones for active permits" msgstr "Pysäköintialueiden ja aktiivisten tunnusten konfliktitilanne" +msgid "Person not found" +msgstr "Henkilöä ei löytynyt" + msgid "New parking permit has been created for you" msgstr "Sinulle on luotu pysäköintitunnus" @@ -954,6 +970,10 @@ msgstr "" msgid "Vehicle %(registration_number)s is decommissioned" msgstr "Ajoneuvo %(registration_number)s on liikennekäytöstäpoistettu" +#, python-format +msgid "Vehicle's %(registration_number)s weight exceeds maximum allowed limit" +msgstr "Ajoneuvon %(registration_number)s paino ylittää sallitun maksimirajan" + msgid "The person has no driving licence" msgstr "Henkilölle ei löydy ajokorttia" @@ -986,6 +1006,9 @@ msgstr "" "* Tunnus on voimassa valitsemastasi alkamispäivästä lähtien, kun " "maksusuoritus on hyväksytty" +msgid "Source: Transport register, Traficom" +msgstr "Lähde: Liikenneasioidenrekisteri, Traficom" + msgid "Parking permit expiration date" msgstr "Pysäköintitunnuksen päättymispäivä" diff --git a/locale/sv/LC_MESSAGES/django.po b/locale/sv/LC_MESSAGES/django.po index baf0affc..8eecebf4 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: 2023-12-02 15:09+0200\n" +"POT-Creation-Date: 2024-01-31 23:25+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -60,14 +60,14 @@ msgstr "Det finns inte parkering zon för det plats" msgid "Multiple parking zones found for the location" msgstr "Flera parkerings zon till denna plats" -msgid "Person not found" +msgid "Customer not found" msgstr "Ingen kund hittas" msgid "Customer search error" msgstr "Kundsökningsfel" -msgid "Owner/holder data of a vehicle could not be verified. Please check with Traficom for data transfer restrictions." -msgstr "Ägar/innehavaruppgifter av fordon kunde inte verifieras. Kontrollera med Traficom för dataöverföringsförbud." +msgid "Owner/holder data of a vehicle could not be verified" +msgstr "Ägar/innehavaruppgifter av fordon kunde inte verifieras" msgid "Customer does not have a valid driving licence for this vehicle" msgstr "Kunden har inte ett giltigt körkort för detta fordon" @@ -178,6 +178,13 @@ msgstr "" msgid "Permit for a given vehicle already exist." msgstr "Tillstånd för ett givet fordon finns redan." +msgid "" +"Owner/holder data of a vehicle could not be verified. Please check with " +"Traficom for data transfer restrictions." +msgstr "" +"Ägar/innehavaruppgifter av fordon kunde inte verifieras. Kontrollera med " +"Traficom för dataöverföringsförbud." + msgid "Customer does not have a valid driving licence" msgstr "Kunden har inte giltigt körkort" @@ -313,9 +320,6 @@ msgstr "Helsingfors stad" msgid "Personal data - Digital and population data services agency" msgstr "Personuppgifter - Digital- och befolkningsdatatjänstbyrå" -msgid "Source: Transport register, Traficom" -msgstr "Källä: Trafik- och transportregistret, Traficom" - msgid "Permit ID" msgstr "Permit ID" @@ -769,15 +773,24 @@ msgstr "Tillfälliga fordon" msgid "M1" msgstr "M1" +msgid "M1G" +msgstr "M1G" + msgid "M2" msgstr "M2" msgid "N1" msgstr "N1" +msgid "N1G" +msgstr "N1G" + msgid "N2" msgstr "N2" +msgid "N2G" +msgstr "N2G" + msgid "L3e-A1" msgstr "L3e-A1" @@ -907,6 +920,9 @@ msgstr "Inga aktiva tillstånd för kunden" msgid "Conflict parking zones for active permits" msgstr "Konflict för parkering zoner och aktiva tillståndet" +msgid "Person not found" +msgstr "Ingen kund hittas" + msgid "New parking permit has been created for you" msgstr "Nytt parkeringstillstånd har skapats åt dig" @@ -952,6 +968,10 @@ msgstr "" msgid "Vehicle %(registration_number)s is decommissioned" 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" + msgid "The person has no driving licence" msgstr "Personen har inget körkort" @@ -984,6 +1004,9 @@ msgstr "" "* Parkeringpermit är i kraft från den valda startdatum, när betalningen är " "godkännt" +msgid "Source: Transport register, Traficom" +msgstr "Källä: Trafik- och transportregistret, Traficom" + msgid "Parking permit expiration date" msgstr "Tillståndet upphör att gälla" diff --git a/parking_permits/admin_resolvers.py b/parking_permits/admin_resolvers.py index c6543945..826d4a77 100644 --- a/parking_permits/admin_resolvers.py +++ b/parking_permits/admin_resolvers.py @@ -571,6 +571,7 @@ def resolve_create_resident_permit(obj, info, permit, audit_msg: AuditMsg = None address_apartment=permit.get("address_apartment"), address_apartment_sv=permit.get("address_apartment"), primary_vehicle=primary_vehicle, + bypass_traficom_validation=permit.get("bypass_traficom_validation", False), ) audit_msg.target = parking_permit @@ -814,10 +815,13 @@ def resolve_update_resident_permit( ) send_refund_email(RefundEmailType.CREATED, customer, refund) + bypass_traficom_validation = permit_info.get("bypass_traficom_validation", False) + # Update permit address and zone for all active permits for permit in active_permits: permit.parking_zone = new_zone permit.address = permit_address if not security_ban else None + permit.bypass_traficom_validation = bypass_traficom_validation permit.save() # get updated permit info diff --git a/parking_permits/cron.py b/parking_permits/cron.py index 46c29135..2ef2a286 100644 --- a/parking_permits/cron.py +++ b/parking_permits/cron.py @@ -39,6 +39,7 @@ def automatic_expiration_of_permits(): active_permit = active_permits.first() active_permit.primary_vehicle = True active_permit.save() + logger.info(f"Permit {permit.pk} ended") logger.info("Automatically ending permits completed.") diff --git a/parking_permits/customer_permit.py b/parking_permits/customer_permit.py index 91553df1..e130387e 100644 --- a/parking_permits/customer_permit.py +++ b/parking_permits/customer_permit.py @@ -153,9 +153,13 @@ def get(self): ) vehicle = permit.vehicle # Update vehicle detail from traficom if it wasn't updated today - if permit.vehicle.updated_from_traficom_on < tz.localdate(tz.now()): + if ( + not permit.vehicle.updated_from_traficom_on + or permit.vehicle.updated_from_traficom_on < tz.localdate(tz.now()) + ): vehicle = self.customer.fetch_vehicle_detail( - vehicle.registration_number + vehicle.registration_number, + permit=permit, ) user_of_vehicle = self.customer.is_user_of_vehicle(vehicle) diff --git a/parking_permits/migrations/0046_add_bypass_traficom_validation.py b/parking_permits/migrations/0046_add_bypass_traficom_validation.py new file mode 100644 index 00000000..32f91ca8 --- /dev/null +++ b/parking_permits/migrations/0046_add_bypass_traficom_validation.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.1 on 2024-01-26 08:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("parking_permits", "0045_alter_product_low_emission_discount"), + ] + + operations = [ + migrations.AddField( + model_name="parkingpermit", + name="bypass_traficom_validation", + field=models.BooleanField( + default=False, verbose_name="Bypass Traficom validation" + ), + ), + ] diff --git a/parking_permits/migrations/0046_alter_vehicle_vehicle_class.py b/parking_permits/migrations/0046_alter_vehicle_vehicle_class.py new file mode 100644 index 00000000..0abfeeb8 --- /dev/null +++ b/parking_permits/migrations/0046_alter_vehicle_vehicle_class.py @@ -0,0 +1,46 @@ +# Generated by Django 4.2.1 on 2024-02-01 09:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("parking_permits", "0045_alter_product_low_emission_discount"), + ] + + operations = [ + migrations.AlterField( + model_name="vehicle", + name="vehicle_class", + field=models.CharField( + blank=True, + choices=[ + ("M1", "M1"), + ("M1G", "M1G"), + ("M2", "M2"), + ("N1", "N1"), + ("N1G", "N1G"), + ("N2", "N2"), + ("N2G", "N2G"), + ("L3e-A1", "L3e-A1"), + ("L3e-A2", "L3e-A2"), + ("L3e-A3", "L3e-A3"), + ("L3e-A1E", "L3e-A1E"), + ("L3e-A2E", "L3e-A2E"), + ("L3e-A3E", "L3e-A3E"), + ("L3e-A1T", "L3e-A1T"), + ("L3e-A2T", "L3e-A2T"), + ("L3e-A3T", "L3e-A3T"), + ("L4e", "L4e"), + ("L5e-A", "L5e-A"), + ("L5e-B", "L5e-B"), + ("L6e-A", "L6e-A"), + ("L6e-B", "L6e-B"), + ("L6e-BP", "L6e-BP"), + ("L6e-BU", "L6e-BU"), + ], + max_length=16, + verbose_name="VehicleClass", + ), + ), + ] diff --git a/parking_permits/migrations/0047_set_updated_from_traficom_null.py b/parking_permits/migrations/0047_set_updated_from_traficom_null.py new file mode 100644 index 00000000..c0e7de09 --- /dev/null +++ b/parking_permits/migrations/0047_set_updated_from_traficom_null.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.1 on 2024-01-29 11:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("parking_permits", "0046_add_bypass_traficom_validation"), + ] + + operations = [ + migrations.AlterField( + model_name="vehicle", + name="updated_from_traficom_on", + field=models.DateField( + blank=True, null=True, verbose_name="Update from traficom on" + ), + ), + ] diff --git a/parking_permits/migrations/0048_merge_20240202_0838.py b/parking_permits/migrations/0048_merge_20240202_0838.py new file mode 100644 index 00000000..606e3bf6 --- /dev/null +++ b/parking_permits/migrations/0048_merge_20240202_0838.py @@ -0,0 +1,12 @@ +# Generated by Django 4.2.1 on 2024-02-02 06:38 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("parking_permits", "0046_alter_vehicle_vehicle_class"), + ("parking_permits", "0047_set_updated_from_traficom_null"), + ] + + operations = [] diff --git a/parking_permits/models/customer.py b/parking_permits/models/customer.py index f0477364..ad6a57ae 100644 --- a/parking_permits/models/customer.py +++ b/parking_permits/models/customer.py @@ -135,8 +135,8 @@ def age(self): ) return relativedelta(datetime.today(), date_of_birth).years - def fetch_vehicle_detail(self, registration_number): - return Traficom().fetch_vehicle_details(registration_number) + def fetch_vehicle_detail(self, registration_number, permit=None): + return Traficom().fetch_vehicle_details(registration_number, permit=permit) def is_user_of_vehicle(self, vehicle): if not settings.TRAFICOM_CHECK: @@ -144,9 +144,10 @@ def is_user_of_vehicle(self, vehicle): users_nin = [user.national_id_number for user in vehicle.users.all()] return self.national_id_number in users_nin - def fetch_driving_licence_detail(self): + def fetch_driving_licence_detail(self, permit=None): licence_details = Traficom().fetch_driving_licence_details( - self.national_id_number + self.national_id_number, + permit=permit, ) driving_licence = DrivingLicence.objects.update_or_create( customer=self, diff --git a/parking_permits/models/driving_class.py b/parking_permits/models/driving_class.py index 7bc80ea5..ea8446dd 100644 --- a/parking_permits/models/driving_class.py +++ b/parking_permits/models/driving_class.py @@ -15,12 +15,16 @@ ), "B": ( VehicleClass.M1, - VehicleClass.M2, # M2 mass less than 3500KG + VehicleClass.M1G, VehicleClass.N1, + VehicleClass.N1G, VehicleClass.L6eB, ), - "C": (VehicleClass.N2,), - "D": (VehicleClass.M2,), # M2 mass not more than 3500 - 4000KG + "C": ( + VehicleClass.N2, + VehicleClass.N2G, + ), + "D": (VehicleClass.M2,), } diff --git a/parking_permits/models/parking_permit.py b/parking_permits/models/parking_permit.py index cc075f69..c2ce35f8 100644 --- a/parking_permits/models/parking_permit.py +++ b/parking_permits/models/parking_permit.py @@ -135,6 +135,10 @@ class ParkingPermit(SerializableMixin, TimestampedModelMixin): primary_vehicle = models.BooleanField(default=True) vehicle_changed = models.BooleanField(default=False) synced_with_parkkihubi = models.BooleanField(default=False) + bypass_traficom_validation = models.BooleanField( + verbose_name=_("Bypass Traficom validation"), + default=False, + ) vehicle_changed_date = models.DateField( _("Vehicle changed date"), null=True, blank=True ) diff --git a/parking_permits/models/vehicle.py b/parking_permits/models/vehicle.py index 26ec8b06..8299f45e 100644 --- a/parking_permits/models/vehicle.py +++ b/parking_permits/models/vehicle.py @@ -8,9 +8,12 @@ class VehicleClass(models.TextChoices): M1 = "M1", _("M1") + M1G = "M1G", _("M1G") M2 = "M2", _("M2") N1 = "N1", _("N1") + N1G = "N1G", _("N1G") N2 = "N2", _("N2") + N2G = "N2G", _("N2G") L3eA1 = "L3e-A1", _("L3e-A1") L3eA2 = "L3e-A2", _("L3e-A2") L3eA3 = "L3e-A3", _("L3e-A3") @@ -155,7 +158,7 @@ class Vehicle(TimestampedModelMixin): _("Last inspection date"), null=True, blank=True ) updated_from_traficom_on = models.DateField( - _("Update from traficom on"), default=tz.now + _("Update from traficom on"), null=True, blank=True ) users = models.ManyToManyField( VehicleUser, verbose_name=_("Vehicle users"), related_name="vehicles" diff --git a/parking_permits/resolvers.py b/parking_permits/resolvers.py index 31e804ff..1b2b5ea9 100644 --- a/parking_permits/resolvers.py +++ b/parking_permits/resolvers.py @@ -448,6 +448,7 @@ def resolve_update_permit_vehicle( permit.vehicle_changed = False permit.vehicle_changed_date = None + permit.bypass_traficom_validation = False permit.save() ParkingPermitEventFactory.make_update_permit_event( diff --git a/parking_permits/schema/parking_permit.graphql b/parking_permits/schema/parking_permit.graphql index 7c56b60c..9bf8a863 100644 --- a/parking_permits/schema/parking_permit.graphql +++ b/parking_permits/schema/parking_permit.graphql @@ -43,6 +43,7 @@ type VehicleNode { registrationNumber: String emission: Int isLowEmission: Boolean + updatedFromTraficomOn: String restrictions: [String] } diff --git a/parking_permits/schema/parking_permit_admin.graphql b/parking_permits/schema/parking_permit_admin.graphql index abb665c4..37c50886 100644 --- a/parking_permits/schema/parking_permit_admin.graphql +++ b/parking_permits/schema/parking_permit_admin.graphql @@ -120,6 +120,7 @@ type PermitNode { endTime: String description: String type: String + bypassTraficomValidation: Boolean primaryVehicle: Boolean! } @@ -289,6 +290,7 @@ type PermitDetailNode { endTime: String description: String consentLowEmissionAccepted: Boolean + bypassTraficomValidation: Boolean contractType: String monthCount: Int monthsLeft: Int @@ -494,6 +496,7 @@ input ResidentPermitInput { description: String address: AddressInput addressApartment: String + bypassTraficomValidation: Boolean zone: String } diff --git a/parking_permits/services/traficom.py b/parking_permits/services/traficom.py index d5e0b708..8decc1b4 100644 --- a/parking_permits/services/traficom.py +++ b/parking_permits/services/traficom.py @@ -17,6 +17,7 @@ VehiclePowerType, VehicleUser, ) +from parking_permits.utils import safe_cast ssl.match_hostname = lambda cert, hostname: True @@ -48,6 +49,7 @@ DRIVING_LICENSE_SEARCH = 890 NO_DRIVING_LICENSE_ERROR_CODE = "562" NO_VALID_DRIVING_LICENSE_ERROR_CODE = "578" +VEHICLE_MAX_WEIGHT_KG = 4000 POWER_TYPE_MAPPER = { "01": "Bensin", @@ -77,8 +79,8 @@ class Traficom: url = settings.TRAFICOM_ENDPOINT headers = {"Content-type": "application/xml"} - def fetch_vehicle_details(self, registration_number): - if settings.TRAFICOM_MOCK: + def fetch_vehicle_details(self, registration_number, permit=None): + if self._bypass_traficom(permit): return self._fetch_vehicle_from_db(registration_number) et = self._fetch_info(registration_number=registration_number) @@ -149,7 +151,15 @@ def fetch_vehicle_details(self, registration_number): mass = et.find(".//massa") module_weight = mass.find("modulinKokonaismassa") technical_weight = mass.find("teknSuurSallKokmassa") - weight = module_weight if module_weight is not None else technical_weight + weight_et = module_weight if module_weight is not None else technical_weight + weight = safe_cast(weight_et.text, int, 0) if weight_et.text else 0 + if weight and weight >= VEHICLE_MAX_WEIGHT_KG: + raise TraficomFetchVehicleError( + _( + "Vehicle's %(registration_number)s weight exceeds maximum allowed limit" + ) + % {"registration_number": registration_number} + ) vehicle_power_type = motor.find("kayttovoima") vehicle_manufacturer = vehicle_detail.find("merkkiSelvakielinen") @@ -171,7 +181,7 @@ def fetch_vehicle_details(self, registration_number): "vehicle_class": vehicle_class, "manufacturer": vehicle_manufacturer.text, "model": vehicle_model.text if vehicle_model is not None else "", - "weight": int(weight.text) if weight else 0, + "weight": weight, "registration_number": registration_number, "euro_class": 6, # It will always be 6 class atm. "emission": float(co2emission) if co2emission else 0, @@ -192,8 +202,8 @@ def fetch_vehicle_details(self, registration_number): vehicle.users.set(vehicle_users) return vehicle - def fetch_driving_licence_details(self, hetu): - if settings.TRAFICOM_MOCK: + def fetch_driving_licence_details(self, hetu, permit=None): + if self._bypass_traficom(permit): return self._fetch_driving_licence_details_from_db(hetu) error_code = None @@ -251,6 +261,13 @@ def _fetch_driving_licence_details_from_db(self, hetu): "driving_classes": licence.driving_classes.all(), } + def _bypass_traficom(self, permit=None): + if settings.TRAFICOM_MOCK: + return True + if permit and permit.bypass_traficom_validation: + return True + return False + def _fetch_info(self, registration_number=None, hetu=None): is_l_type_vehicle = ( len(registration_number) == 6 if registration_number else False diff --git a/parking_permits/talpa/order.py b/parking_permits/talpa/order.py index 646249bd..975de224 100644 --- a/parking_permits/talpa/order.py +++ b/parking_permits/talpa/order.py @@ -32,7 +32,7 @@ class TalpaOrderManager: } @classmethod - def _create_item_data(cls, order, order_item): + def create_item_data(cls, order, order_item): item = { "productId": str(order_item.product.talpa_product_id), "productName": order_item.product.name, @@ -67,7 +67,7 @@ def _create_item_data(cls, order, order_item): return item @classmethod - def _append_detail_meta(cls, item, permit): + def append_detail_meta(cls, item, permit, fixed_end_time=None): start_time = tz.localtime(permit.start_time).strftime(DATE_FORMAT) item["meta"] += [ {"key": "permitId", "value": str(permit.id), "visibleInCheckout": False}, @@ -105,8 +105,9 @@ def _append_detail_meta(cls, item, permit): "ordinal": 5, }, ] - if permit.end_time: - end_time = tz.localtime(permit.end_time).strftime(TIME_FORMAT) + permit_end_time = fixed_end_time or permit.end_time + if permit_end_time: + end_time = tz.localtime(permit_end_time).strftime(TIME_FORMAT) item["meta"].append( { "key": "endDate", @@ -121,7 +122,7 @@ def _append_detail_meta(cls, item, permit): return item @classmethod - def _create_customer_data(cls, customer): + def create_customer_data(cls, customer): return { "firstName": customer.first_name, "lastName": customer.last_name, @@ -129,7 +130,7 @@ def _create_customer_data(cls, customer): } @classmethod - def _create_order_data(cls, order): + def create_order_data(cls, order): items = [] order_items = ( order.order_items.all() @@ -147,7 +148,7 @@ def _create_order_data(cls, order): order_items_of_single_permit = [] for index, order_item in enumerate(order_items_by_permit[permit]): if order_item.quantity: - item = cls._create_item_data(order, order_item) + item = cls.create_item_data(order, order_item) if index == 0: item.update( { @@ -157,10 +158,11 @@ def _create_order_data(cls, order): order_items_of_single_permit.append(item) # Append details of permit only to the last order item of permit. - cls._append_detail_meta(order_items_of_single_permit[-1], permit) - items += order_items_of_single_permit + if len(order_items_of_single_permit) > 0: + cls.append_detail_meta(order_items_of_single_permit[-1], permit) + items += order_items_of_single_permit - customer = cls._create_customer_data(order.customer) + customer = cls.create_customer_data(order.customer) last_valid_purchase_date_time = ( format_local_time(order.talpa_last_valid_purchase_time) if order.talpa_last_valid_purchase_time @@ -186,7 +188,7 @@ def round_up(cls, v): return round_up(v) @classmethod - def _set_flow_steps(cls, order_id, user_id): + def set_flow_steps(cls, order_id, user_id): data = { "activeStep": 4, "totalSteps": 7, @@ -215,7 +217,7 @@ def _set_flow_steps(cls, order_id, user_id): ) @classmethod - def _set_order_details(cls, order): + def set_order_details(cls, order): payment_period = settings.TALPA_ORDER_PAYMENT_MAX_PERIOD_MINS order.talpa_last_valid_purchase_time = tz.localtime( tz.now() + tz.timedelta(minutes=payment_period) @@ -225,8 +227,8 @@ def _set_order_details(cls, order): @classmethod def send_to_talpa(cls, order): - cls._set_order_details(order) - order_data = cls._create_order_data(order) + cls.set_order_details(order) + order_data = cls.create_order_data(order) order_data_raw = json.dumps(order_data, default=str) logger.info(f"Order data sent to talpa: {order_data_raw}") response = requests.post(cls.url, data=order_data_raw, headers=cls.headers) @@ -260,6 +262,6 @@ def send_to_talpa(cls, order): ) order_item.save() - cls._set_flow_steps(order.talpa_order_id, str(order.customer.user.uuid)) + cls.set_flow_steps(order.talpa_order_id, str(order.customer.user.uuid)) return response_data.get("loggedInCheckoutUrl") diff --git a/parking_permits/tests/models/test_parking_permit.py b/parking_permits/tests/models/test_parking_permit.py index 723ea798..8d25b4fd 100644 --- a/parking_permits/tests/models/test_parking_permit.py +++ b/parking_permits/tests/models/test_parking_permit.py @@ -677,6 +677,7 @@ def test_can_be_refunded_fixed_inactive(self): self.assertFalse(permit.can_be_refunded) + @freeze_time(timezone.make_aware(datetime(2024, 1, 1))) def test_can_be_refunded_open_ended_already_started(self): permit = ParkingPermitFactory( contract_type=ContractType.OPEN_ENDED, @@ -687,6 +688,7 @@ def test_can_be_refunded_open_ended_already_started(self): self.assertFalse(permit.can_be_refunded) + @freeze_time(timezone.make_aware(datetime(2024, 1, 1))) def test_can_be_refunded_open_ended_ends_more_than_month(self): permit = ParkingPermitFactory( contract_type=ContractType.OPEN_ENDED, diff --git a/parking_permits/tests/services/mocks/traficom/vehicle_ok.xml b/parking_permits/tests/services/mocks/traficom/vehicle_ok.xml index c585b2eb..cb30e141 100644 --- a/parking_permits/tests/services/mocks/traficom/vehicle_ok.xml +++ b/parking_permits/tests/services/mocks/traficom/vehicle_ok.xml @@ -15,7 +15,7 @@ 0041237599 - M1 + M1G 135 Ford 4D FOCUS STW 1.6VCT-DA3/264 diff --git a/parking_permits/tests/services/mocks/traficom/vehicle_too_heavy.xml b/parking_permits/tests/services/mocks/traficom/vehicle_too_heavy.xml new file mode 100644 index 00000000..346f8590 --- /dev/null +++ b/parking_permits/tests/services/mocks/traficom/vehicle_too_heavy.xml @@ -0,0 +1,341 @@ + + + TPSUOTIEDOTLAAJAHAKUOUT + + + TPSUO + LONTOO,LONTOO + + + + + + ELU-660 + WBAFH01040LL68582 + 0098448962 + + + M1G + 206 + BMW + X6 xDRIVE40d Farmari (AC) 4ov 2993cm3 A + 3 + 01 + + 28 + 2024-01-26 + + + 18 + 2024-01-26 + + + + 2010-06-02 + 0988005-9 + OY BMW SUOMI AB + Äyritie 8b + 01510 + VANTAA + + + e1*2007/46*0412*00 + FH01 + 5C + X6 xDRIVE40d + 20100726 + 2023-06-14 + 2024-06-14 + 2023-06-14 + 2 + K1 Katsastajat Oy/Lahti Laune + false + 21557378 + BAYERISCHE MOTOREN WERKE + 1 + 233 + 445BCZ + 03 + false + 01 + + 5 + 4 + + X6 + 2012-04-12 + + + 282073 + 2023-06-14 + + + + + + 010190-905V + Testilä + Tenho + 0 + 01 + 2024-01-26 + false + Mäkelänkatu 10 C 1c + 00550 + HELSINKI + fi + + + + 36 + 01 + Pohjola + 010190-905V + Testilä, Tenho + 2024-01-26 + + + 01 + 2 + false + 2 + 8 + Oviaukko, O + false + + + 1 + true + false + false + false + true + 1 + false + 1300 + 1300 + false + false + + 1 + 1 + 275/40R20 + 10JX20 + 40.0 + 106W + + + 1 + 1 + 255/50R19 + 9JX19 + 48.0 + 107V + + + 1 + 1 + 255/50R19 + 9JX19 + 48.0 + 107V + + + 1 + 1 + 285/35R21 + 10JX21 + 40.0 + 105W + + + + 2 + false + false + false + false + true + 1 + false + 1500 + 1500 + false + false + + 2 + 1 + 325/30R21 + 11,5JX21 + 38.0 + 108W + + + 2 + 1 + 315/35R20 + 11JX20 + 37.0 + 110W + + + 2 + 1 + 255/50R19 + 9JX19 + 18.0 + 107V + + + 2 + 1 + 285/45R19 + 10JX19 + 21.0 + 111V + + + + + + 02 + 2993 + 225.0 + 236 + 6 + N57D30B + true + false + 162 + + 02 + 03 + 06 + + + + 02 + 79.0 + 72.0 + 3300 + + + 06 + 01 + 306.30 + + + 06 + 03 + 164.70 + + + 06 + 10 + 0.09 + + + 06 + 11 + 185.80 + + + + + 1 + 6.8 + + + 2 + 8.8 + + + 3 + 7.5 + + + 4 + 198.0 + + + + + false + + + 100 + 4185 + 4760 + 4760 + + + 4877 + 1983 + 1690 + + + 1 + 2933 + + + + + AC + 1 + 1 + 4 + + + 05 + + 1 + + + + 2880 + 5580 + 750 + 2700 + + + + et + 1 + 1 + 1 + + + st + 1 + 1 + 1 + + + vk + 1 + 1 + 1 + + + et + 1 + 3 + 1 + + + st + 1 + 3 + 1 + + + vk + 1 + 3 + 1 + + + + + + diff --git a/parking_permits/tests/services/test_talpa.py b/parking_permits/tests/services/test_talpa.py index bb7bf5d8..de03cb88 100644 --- a/parking_permits/tests/services/test_talpa.py +++ b/parking_permits/tests/services/test_talpa.py @@ -24,7 +24,7 @@ def test_create_order_data(self): vat=Decimal(0.24), ) - data = TalpaOrderManager._create_order_data(order_item.order) + data = TalpaOrderManager.create_order_data(order_item.order) self.assertEqual(data["priceNet"], "193.55") self.assertEqual(data["priceVat"], "46.45") self.assertEqual(data["priceTotal"], "240.00") @@ -37,7 +37,7 @@ def test_create_item_data(self): vat=Decimal(0.24), ) - data = TalpaOrderManager._create_item_data(order_item.order, order_item) + data = TalpaOrderManager.create_item_data(order_item.order, order_item) self.assertEqual(data["priceNet"], "24.19") self.assertEqual(data["priceVat"], "5.81") diff --git a/parking_permits/tests/services/test_traficom.py b/parking_permits/tests/services/test_traficom.py index 7c02ab59..cde56dbd 100644 --- a/parking_permits/tests/services/test_traficom.py +++ b/parking_permits/tests/services/test_traficom.py @@ -9,6 +9,7 @@ from parking_permits.models.vehicle import EmissionType from parking_permits.services.traficom import Traficom from parking_permits.tests.factories.customer import CustomerFactory +from parking_permits.tests.factories.parking_permit import ParkingPermitFactory from parking_permits.tests.factories.vehicle import VehicleFactory @@ -46,6 +47,19 @@ def test_fetch_vehicle(self): assert vehicle.emission_type == EmissionType.NEDC assert vehicle.emission == 155.00 + @override_settings(TRAFICOM_MOCK=False) + def test_fetch_vehicle_too_heavy(self): + with mock.patch( + "requests.post", + return_value=MockResponse(get_mock_xml("vehicle_too_heavy.xml")), + ): + self.assertRaises( + TraficomFetchVehicleError, + self.traficom.fetch_vehicle_details, + self.registration_number, + ) + + @override_settings(TRAFICOM_MOCK=False) def test_fetch_vehicle_wltp(self): with mock.patch( "requests.post", return_value=MockResponse(get_mock_xml("vehicle_wltp.xml")) @@ -134,6 +148,29 @@ def test_fetch_vehicle_from_db(self): self.assertEqual(vehicle.registration_number, self.registration_number) mock_traficom.assert_not_called() + @override_settings(TRAFICOM_MOCK=False) + def test_fetch_vehicle_from_db_if_permit_bypass_true(self): + VehicleFactory(registration_number=self.registration_number) + permit = ParkingPermitFactory(bypass_traficom_validation=True) + with mock.patch("requests.post") as mock_traficom: + vehicle = self.traficom.fetch_vehicle_details( + self.registration_number, permit + ) + self.assertEqual(vehicle.registration_number, self.registration_number) + mock_traficom.assert_not_called() + + @override_settings(TRAFICOM_MOCK=False) + def test_fetch_vehicle_from_db_if_permit_bypass_false(self): + permit = ParkingPermitFactory(bypass_traficom_validation=False) + with mock.patch( + "requests.post", return_value=MockResponse(get_mock_xml("vehicle_ok.xml")) + ) as mock_traficom: + vehicle = self.traficom.fetch_vehicle_details( + self.registration_number, permit + ) + self.assertEqual(vehicle.registration_number, self.registration_number) + mock_traficom.assert_called() + @override_settings(TRAFICOM_MOCK=True) def test_fetch_vehicle_from_db_not_found(self): with mock.patch("requests.post") as mock_traficom: @@ -184,6 +221,41 @@ def test_fetch_licence_from_db(self): self.assertEqual(result["driving_classes"].count(), 1) mock_traficom.assert_not_called() + @override_settings(TRAFICOM_MOCK=False) + def test_fetch_licence_from_db_if_permit_bypass(self): + customer = CustomerFactory(national_id_number=self.hetu) + permit = ParkingPermitFactory( + customer=customer, bypass_traficom_validation=True + ) + licence = DrivingLicence.objects.create( + customer=customer, + start_date=datetime.date(2023, 6, 3), + ) + assert licence.start_date == datetime.date(2023, 6, 3) + driving_class = DrivingClass.objects.create(identifier="A") + licence.driving_classes.add(driving_class) + + with mock.patch("requests.post") as mock_traficom: + result = self.traficom.fetch_driving_licence_details(self.hetu, permit) + self.assertEqual(result["issue_date"], licence.start_date) + self.assertEqual(result["driving_classes"].count(), 1) + mock_traficom.assert_not_called() + + @override_settings(TRAFICOM_MOCK=False) + def test_fetch_valid_licence_if_not_permit_bypass(self): + customer = CustomerFactory(national_id_number=self.hetu) + permit = ParkingPermitFactory( + customer=customer, bypass_traficom_validation=False + ) + with mock.patch( + "requests.post", + return_value=MockResponse(get_mock_xml("licence_ok.xml")), + ) as mock_traficom: + result = self.traficom.fetch_driving_licence_details(self.hetu, permit) + self.assertEqual(len(result["driving_classes"]), 7) + self.assertEqual(result["issue_date"], "2023-09-01") + mock_traficom.assert_called() + @override_settings(TRAFICOM_MOCK=True) def test_fetch_licence_from_db_not_found(self): with mock.patch("requests.post") as mock_traficom: diff --git a/parking_permits/tests/test_customer_permit.py b/parking_permits/tests/test_customer_permit.py index 138b2ef9..80dea41b 100644 --- a/parking_permits/tests/test_customer_permit.py +++ b/parking_permits/tests/test_customer_permit.py @@ -3,7 +3,7 @@ from dateutil.relativedelta import relativedelta from django.core.exceptions import ObjectDoesNotExist -from django.test import TestCase +from django.test import TestCase, override_settings from django.utils import timezone as tz from django.utils.translation import gettext as _ from freezegun import freeze_time @@ -104,10 +104,12 @@ def setUp(self): vehicle=self.vehicle_b, ) + @override_settings(TRAFICOM_MOCK=True) def test_customer_a_should_get_only_his_draft_permit(self): permits = CustomerPermit(self.customer_a.id).get() self.assertEqual(len(permits), 2) + @override_settings(TRAFICOM_MOCK=True) def test_customer_b_should_delete_draft_permit_that_is_created_before_today(self): query_set = ParkingPermit.objects.filter( customer=self.customer_b, status__in=[VALID, PAYMENT_IN_PROGRESS, DRAFT] diff --git a/parking_permits/tests/test_views.py b/parking_permits/tests/test_views.py index b1666e33..9f75470e 100644 --- a/parking_permits/tests/test_views.py +++ b/parking_permits/tests/test_views.py @@ -1,9 +1,11 @@ import datetime +import zoneinfo from datetime import timezone as dt_tz from decimal import Decimal from unittest.mock import patch import requests_mock +from dateutil.relativedelta import relativedelta from django.conf import settings from django.test import override_settings from django.urls import reverse @@ -47,12 +49,16 @@ VehiclePowerTypeFactory, ) from parking_permits.tests.factories.zone import ParkingZoneFactory +from parking_permits.utils import get_meta_value from users.tests.factories.user import UserFactory from ..models import Customer from ..models.common import SourceSystem from .keys import rsa_key +HELSINKI_TZ = zoneinfo.ZoneInfo("Europe/Helsinki") +TIME_FORMAT = "%d.%m.%Y %H:%M" + def get_validated_order_data(talpa_order_id, talpa_order_item_id): return { @@ -203,10 +209,10 @@ def prepare_test_data( euro_min_class_limit=6, ) permit_start_time = datetime.datetime( - now.year, 9, 12, 13, 46, 0, tzinfo=datetime.timezone.utc + now.year, 1, 20, 14, 26, 0, tzinfo=HELSINKI_TZ ) permit_end_time = datetime.datetime( - now.year, 10, 11, 23, 59, 0, tzinfo=datetime.timezone.utc + now.year, 2, 19, 23, 59, 0, tzinfo=HELSINKI_TZ ) zone_a = ParkingZoneFactory(name="A") product_detail_list = [[(start_date, end_date), unit_price]] @@ -223,6 +229,26 @@ def prepare_test_data( month_count=1, primary_vehicle=primary_permit, ) + user = UserFactory(uuid=self.user_id) + customer = CustomerFactory(user=user) + order = OrderFactory( + talpa_order_id=self.talpa_order_id, + customer=customer, + status=OrderStatus.CONFIRMED, + ) + order.permits.add(permit) + order.save() + subscription = SubscriptionFactory( + talpa_subscription_id=self.talpa_subscription_id, + status=SubscriptionStatus.CONFIRMED, + ) + OrderItemFactory( + talpa_order_item_id=self.talpa_order_item_id, + order=order, + product=product, + permit=permit, + subscription=subscription, + ) return permit, product def prepare_request_data( @@ -499,6 +525,12 @@ def test_resolve_product_view_for_permit(self): 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) + order_item_metas = response.data.get("orderItemMetas") + permit_end_time = get_meta_value(order_item_metas, "endDate") + fixed_end_time = tz.localtime( + permit.end_time + relativedelta(months=1) + ).strftime(TIME_FORMAT) + self.assertEqual(permit_end_time, fixed_end_time) def test_resolve_product_view_after_vehicle_change(self): unit_price = Decimal(60) @@ -539,6 +571,12 @@ def test_resolve_product_view_after_vehicle_change(self): 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) + order_item_metas = response.data.get("orderItemMetas") + permit_end_time = get_meta_value(order_item_metas, "endDate") + fixed_end_time = tz.localtime( + permit.end_time + relativedelta(months=1) + ).strftime(TIME_FORMAT) + self.assertEqual(permit_end_time, fixed_end_time) def test_resolve_product_view_after_address_change(self): unit_price = Decimal(60) @@ -588,6 +626,12 @@ def test_resolve_product_view_after_address_change(self): ) self.assertEqual(response.data.get("productName"), product_b.name) self.assertEqual(response.data.get("productLabel"), permit.vehicle.description) + order_item_metas = response.data.get("orderItemMetas") + permit_end_time = get_meta_value(order_item_metas, "endDate") + fixed_end_time = tz.localtime( + permit.end_time + relativedelta(months=1) + ).strftime(TIME_FORMAT) + self.assertEqual(permit_end_time, fixed_end_time) def test_resolve_product_view_after_primary_permit_ending(self): unit_price = Decimal(60) @@ -612,6 +656,12 @@ def test_resolve_product_view_after_primary_permit_ending(self): 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) + order_item_metas = response.data.get("orderItemMetas") + permit_end_time = get_meta_value(order_item_metas, "endDate") + fixed_end_time = tz.localtime( + permit.end_time + relativedelta(months=1) + ).strftime(TIME_FORMAT) + self.assertEqual(permit_end_time, fixed_end_time) permit.refresh_from_db() # Check that permit has been marked as primary diff --git a/parking_permits/utils.py b/parking_permits/utils.py index 600f33e5..1f7c93a4 100644 --- a/parking_permits/utils.py +++ b/parking_permits/utils.py @@ -65,6 +65,13 @@ def __repr__(self): ) +def safe_cast(val, to_type, default=None): + try: + return to_type(val) + except (ValueError, TypeError): + return default + + def diff_months_floor(start_date, end_date): if start_date > end_date: return 0 diff --git a/parking_permits/views.py b/parking_permits/views.py index 731ec981..311ff940 100644 --- a/parking_permits/views.py +++ b/parking_permits/views.py @@ -76,6 +76,7 @@ send_permit_email, send_vehicle_low_emission_discount_email, ) +from .talpa.order import TalpaOrderManager from .utils import ( get_end_time, get_meta_item, @@ -226,6 +227,24 @@ def post(self, request, format=None): product = product_with_quantity[0] if not product: return bad_request_response("Product not found") + + order_item_response_data = {"meta": []} + order_item = permit.order_items.first() + if order_item: + order_item_response_data.get("meta").append( + { + "key": "sourceOrderItemId", + "value": str(order_item.id), + "visibleInCheckout": False, + "ordinal": 0, + }, + ) + TalpaOrderManager.append_detail_meta( + order_item_response_data, + permit, + fixed_end_time=permit.end_time + relativedelta(months=1), + ) + response = snake_to_camel_dict( { "subscription_id": subscription_id, @@ -233,6 +252,7 @@ def post(self, request, format=None): "product_id": str(product.talpa_product_id), "product_name": product.name, "product_label": permit.vehicle.description, + "order_item_metas": order_item_response_data.get("meta"), } ) except Exception as e: @@ -359,11 +379,13 @@ def post(self, request): permit = ParkingPermit.objects.get(pk=permit_id) customer = permit.customer if settings.TRAFICOM_CHECK: - customer.fetch_driving_licence_detail() + customer.fetch_driving_licence_detail(permit) is_driving_licence_active = customer.driving_licence.active else: is_driving_licence_active = True - vehicle = customer.fetch_vehicle_detail(permit.vehicle.registration_number) + vehicle = customer.fetch_vehicle_detail( + permit.vehicle.registration_number, permit + ) is_user_of_vehicle = customer.is_user_of_vehicle(vehicle) has_valid_driving_licence = customer.has_valid_driving_licence_for_vehicle( vehicle