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 committed Oct 1, 2024
1 parent 7801d6d commit 2fa4396
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 3 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: |
sudod apt-get update
sudo apt-get install gdal-bin
- name: Install PyPI dependencies
Expand Down
4 changes: 3 additions & 1 deletion parking_permits/models/parking_permit.py
Original file line number Diff line number Diff line change
Expand Up @@ -822,7 +822,9 @@ 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
86 changes: 86 additions & 0 deletions parking_permits/tests/models/test_parking_permit.py
Original file line number Diff line number Diff line change
Expand Up @@ -1119,6 +1119,92 @@ 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-09T21:59:59.999999+00:00",
)

self.assertEqual(
timezone.localtime(permit.end_time).isoformat(),
"2024-04-09T23:59:59.999999+02: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 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 2fa4396

Please sign in to comment.