Skip to content

Commit

Permalink
fix open ended permit end time calculation.
Browse files Browse the repository at this point in the history
ParkingPermit end_time was wrong after renewal in some cases when
incrementing end time by a month.

refs: PV-852
  • Loading branch information
AnttiRae authored and mhieta committed Oct 2, 2024
1 parent 2f83785 commit 3450296
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 10 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ jobs:

- name: Install required Ubuntu packages
run: |
sudo apt-get update
sudo apt-get install gdal-bin
- name: Install PyPI dependencies
Expand Down
2 changes: 1 addition & 1 deletion parking_permits/models/parking_permit.py
Original file line number Diff line number Diff line change
Expand Up @@ -807,7 +807,7 @@ def renew_open_ended_permit(self):
if self.contract_type != ContractType.OPEN_ENDED:
raise ValueError("This permit is not open-ended so cannot be renewed")
self.end_time = increment_end_time(
self.end_time or self.current_period_end_time(), months=1
self.start_time, self.end_time or self.current_period_end_time(), months=1
)
self.save()

Expand Down
104 changes: 97 additions & 7 deletions parking_permits/tests/models/test_parking_permit.py
Original file line number Diff line number Diff line change
Expand Up @@ -1113,6 +1113,96 @@ def test_extend_permit(self):
self.assertEqual(permit.month_count, 4)
self.assertEqual(permit.end_time.date(), date(2024, 7, 3))

def test_renew_parking_permit_multiple_renews_middle_of_month(self):
permit = ParkingPermitFactory(
status=ParkingPermitStatus.VALID,
contract_type=ContractType.OPEN_ENDED,
start_time=timezone.make_aware(datetime(2024, 1, 10, 21, 0), pytz.UTC),
end_time=timezone.make_aware(
datetime(2024, 2, 9, 20, 59, 59, 999999), pytz.UTC
),
month_count=1,
)

with freeze_time("2024-1-15"):
permit.renew_open_ended_permit()
permit.refresh_from_db()

self.assertEqual(
permit.end_time.isoformat(),
"2024-03-09T21:59:59.999999+00:00",
)

self.assertEqual(
timezone.localtime(permit.end_time).isoformat(),
"2024-03-09T23:59:59.999999+02:00",
)

with freeze_time("2024-2-15"):
permit.renew_open_ended_permit()
permit.refresh_from_db()

self.assertEqual(
permit.end_time.isoformat(),
"2024-04-09T20:59:59.999999+00:00",
)

self.assertEqual(
timezone.localtime(permit.end_time).isoformat(),
"2024-04-09T23:59:59.999999+03:00",
)

def test_renew_parking_permit_multiple_renews_end_of_month(self):
permit = ParkingPermitFactory(
status=ParkingPermitStatus.VALID,
contract_type=ContractType.OPEN_ENDED,
start_time=timezone.make_aware(datetime(2024, 1, 1, 21, 0), pytz.UTC),
end_time=timezone.make_aware(
datetime(2024, 1, 31, 20, 59, 59, 999999), pytz.UTC
),
month_count=1,
)

with freeze_time("2024-1-24"):
permit.renew_open_ended_permit()
permit.refresh_from_db()

self.assertEqual(
permit.end_time.isoformat(),
"2024-02-29T21:59:59.999999+00:00",
)

self.assertEqual(
timezone.localtime(permit.end_time).isoformat(),
"2024-02-29T23:59:59.999999+02:00",
)

with freeze_time("2024-2-24"):
permit.renew_open_ended_permit()
permit.refresh_from_db()
self.assertEqual(
permit.end_time.isoformat(),
"2024-03-31T20:59:59.999999+00:00",
)

self.assertEqual(
timezone.localtime(permit.end_time).isoformat(),
"2024-03-31T23:59:59.999999+03:00",
)
with freeze_time("2024-3-24"):
permit.renew_open_ended_permit()
permit.refresh_from_db()

self.assertEqual(
permit.end_time.isoformat(),
"2024-04-30T20:59:59.999999+00:00",
)

self.assertEqual(
timezone.localtime(permit.end_time).isoformat(),
"2024-04-30T23:59:59.999999+03:00",
)

@freeze_time("2024-1-28")
def test_renew_parking_permit(self):
permit = ParkingPermitFactory(
Expand All @@ -1137,13 +1227,13 @@ def test_renew_parking_permit(self):
"2024-02-29T23:59:59.999999+02:00",
)

@freeze_time("2024-3-7")
@freeze_time("2024-3-31")
def test_renew_parking_permit_dst_to_summer(self):
permit = ParkingPermitFactory(
status=ParkingPermitStatus.VALID,
contract_type=ContractType.OPEN_ENDED,
start_time=timezone.make_aware(datetime(2024, 1, 1, 12, 0), pytz.UTC),
end_time=timezone.make_aware(datetime(2024, 3, 10, 21, 59), pytz.UTC),
end_time=timezone.make_aware(datetime(2024, 3, 31, 20, 59), pytz.UTC),
month_count=1,
)

Expand All @@ -1153,12 +1243,12 @@ def test_renew_parking_permit_dst_to_summer(self):
# should be 23:59 Helsinki time i.e. UTC + 2 hours
self.assertEqual(
permit.end_time.isoformat(),
"2024-04-10T20:59:59.999999+00:00",
"2024-04-30T20:59:59.999999+00:00",
)

self.assertEqual(
timezone.localtime(permit.end_time).isoformat(),
"2024-04-10T23:59:59.999999+03:00",
"2024-04-30T23:59:59.999999+03:00",
)

@freeze_time("2024-10-15")
Expand All @@ -1167,7 +1257,7 @@ def test_renew_parking_permit_dst_to_winter(self):
status=ParkingPermitStatus.VALID,
contract_type=ContractType.OPEN_ENDED,
start_time=timezone.make_aware(datetime(2024, 1, 1, 12, 0), pytz.UTC),
end_time=timezone.make_aware(datetime(2024, 10, 12, 20, 59), pytz.UTC),
end_time=timezone.make_aware(datetime(2024, 10, 31, 20, 59), pytz.UTC),
month_count=1,
)

Expand All @@ -1177,12 +1267,12 @@ def test_renew_parking_permit_dst_to_winter(self):
# should be 23:59 Helsinki time i.e. UTC + 2 hours
self.assertEqual(
permit.end_time.isoformat(),
"2024-11-12T21:59:59.999999+00:00",
"2024-11-30T21:59:59.999999+00:00",
)

self.assertEqual(
timezone.localtime(permit.end_time).isoformat(),
"2024-11-12T23:59:59.999999+02:00",
"2024-11-30T23:59:59.999999+02:00",
)

@freeze_time("2024-3-15 9:00+02:00")
Expand Down
22 changes: 22 additions & 0 deletions parking_permits/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,32 @@
diff_months_floor,
find_next_date,
flatten_dict,
get_last_day_of_month,
get_model_diff,
)


def test_get_last_day_of_month():
assert get_last_day_of_month(datetime(2024, 1, 2, 12, 12, 00)) == 31
assert get_last_day_of_month(datetime(2023, 2, 2, 12, 12, 00)) == 28
assert get_last_day_of_month(datetime(2024, 3, 2, 12, 12, 00)) == 31
assert get_last_day_of_month(datetime(2024, 4, 2, 12, 12, 00)) == 30
assert get_last_day_of_month(datetime(2024, 5, 2, 12, 12, 00)) == 31
assert get_last_day_of_month(datetime(2024, 6, 2, 12, 12, 00)) == 30
assert get_last_day_of_month(datetime(2024, 7, 2, 12, 12, 00)) == 31
assert get_last_day_of_month(datetime(2024, 8, 2, 12, 12, 00)) == 31
assert get_last_day_of_month(datetime(2024, 9, 2, 12, 12, 00)) == 30
assert get_last_day_of_month(datetime(2024, 10, 2, 12, 12, 00)) == 31
assert get_last_day_of_month(datetime(2024, 11, 2, 12, 12, 00)) == 30
assert get_last_day_of_month(datetime(2024, 12, 2, 12, 12, 00)) == 31

# leap year
assert get_last_day_of_month(datetime(2024, 2, 2, 12, 12, 00)) == 29

for i in range(1, 31):
assert get_last_day_of_month(datetime(2024, 5, i, 12, 12, 00)) == 31


@pytest.mark.parametrize(
"gross_price,vat,net_price,vat_price",
[
Expand Down
19 changes: 17 additions & 2 deletions parking_permits/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import zoneinfo
from collections import OrderedDict
from collections.abc import Callable
from datetime import datetime
from datetime import datetime, timedelta
from decimal import ROUND_UP, Decimal
from itertools import chain
from typing import Any, Iterable, Iterator, Optional, Union
Expand Down Expand Up @@ -129,15 +129,30 @@ def get_end_time(start_time, diff_months):
return normalize_end_time(end_time)


def increment_end_time(end_time, months=1):
def get_last_day_of_month(date: datetime):
next_month = date.replace(day=28) + timedelta(days=4)
return (next_month - timedelta(days=next_month.day)).day


def increment_end_time(start_time, end_time, months=1):
"""Increment the end time based on the current value (rather than start time).
start_time is used to calculate the original end day, which is used when setting
permit end_time's day after the month has been incremented.
Example: 1st Jan 23:59 -> 1st Feb 23:59.
Should account for DST changes.
"""
original_end_day = (start_time.date() + relativedelta(days=30)).day

end_time = end_time.astimezone(tz.get_default_timezone())

end_time += relativedelta(months=months)
try:
# try to set end day to be same as originally
end_time = end_time.replace(day=original_end_day)
except ValueError:
# end day not in month -> set to last day of the month
end_time = end_time.replace(day=get_last_day_of_month(end_time))
return normalize_end_time(end_time)


Expand Down

0 comments on commit 3450296

Please sign in to comment.