Skip to content

Commit e0321fe

Browse files
committed
feat: Add get_minimum_due_date function
1 parent b7919ab commit e0321fe

4 files changed

Lines changed: 132 additions & 2 deletions

File tree

netsgiro/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
from netsgiro.constants import *
66
from netsgiro.enums import *
77
from netsgiro.objects import *
8+
from netsgiro.utils import *
89

9-
from netsgiro import constants, enums, objects # isort: skip
10+
from netsgiro import constants, enums, objects, utils # isort: skip
1011

1112
__version__ = '1.3.0'
1213

13-
__all__: List[str] = constants.__all__ + enums.__all__ + objects.__all__
14+
__all__: List[str] = constants.__all__ + enums.__all__ + objects.__all__ + utils.__all__

netsgiro/utils.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from contextlib import suppress
2+
from datetime import timedelta
3+
from typing import TYPE_CHECKING
4+
5+
if TYPE_CHECKING:
6+
from datetime import date, datetime
7+
8+
try:
9+
import zoneinfo
10+
except ImportError:
11+
from backports import zoneinfo # type: ignore[no-redef]
12+
13+
__all__ = ['get_minimum_due_date']
14+
15+
# Nets operates in the Norwegian timezone
16+
OSLO_TZ = zoneinfo.ZoneInfo('Europe/Oslo')
17+
18+
19+
def get_minimum_due_date(now: 'datetime') -> 'date':
20+
"""
21+
Return the minimum valid due date for an avtalegiro ocrgiro created right now.
22+
23+
The avtalegiro spec specifies that customers should have at least
24+
4 calendar days notice before a payment, so files containing due dates
25+
earlier than that will fail.
26+
27+
"Calendar days" include weekends, but not holidays, so the holidays
28+
library is used to offset by holidays if it's installed.
29+
30+
Logic is used for validation in netsgiro internally, but is also exported
31+
to be used downstream, when generating OCR files.
32+
"""
33+
today = now.date()
34+
35+
# 14:00 is the cut-off for sending in new transmissions;
36+
# files sent after 14:00 are processed the next day.
37+
delta = 4 if now.hour < 14 else 5
38+
39+
# Adjust for holidays, if the dependency is installed
40+
# - Users of the library that want this, should install
41+
# - netsgiro with `pip install netsgiro[holidays]
42+
with suppress(ImportError):
43+
from holidays import country_holidays
44+
45+
# Get holidays
46+
holidays_in_the_date_range = country_holidays('NO')[now : now + timedelta(days=delta)] # type: ignore[misc]
47+
48+
# Add days to `delta` for each holiday found in the date range
49+
delta += len(holidays_in_the_date_range)
50+
51+
# Calendar days don't count weekends, but file do have
52+
# to be received on weekdays to be processed the same day
53+
if today.weekday() in [5, 6]:
54+
delta += 7 - today.weekday()
55+
56+
return (now + timedelta(days=delta)).date()

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ ignore=
1414

1515
per-file-ignores=
1616
netsgiro/__init__.py:F403,F401,
17+
netsgiro/utils.py:E203,E501,
1718

1819
exclude =
1920
.git,

tests/test_utils.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from datetime import datetime, timedelta
2+
3+
import holidays
4+
5+
from netsgiro.utils import OSLO_TZ, get_minimum_due_date
6+
7+
monday = datetime(2022, 3, 28, 13, 59, tzinfo=OSLO_TZ)
8+
9+
10+
def test_minimum_due_date_before_cutoff():
11+
"""
12+
Files generated before 14:00 Norwegian time should be
13+
adjusted by 4 days, minimum.
14+
"""
15+
assert get_minimum_due_date(monday) == (monday + timedelta(days=4)).date()
16+
17+
18+
monday_after_cutoff = datetime(2022, 4, 1, 14, 1, tzinfo=OSLO_TZ)
19+
20+
21+
def test_minimum_due_date_after_cutoff():
22+
"""
23+
Because files sent in after 14:00 are processed the next day, files
24+
generated after 14:00 Norwegian time should be adjusted by 5 days.
25+
"""
26+
assert (
27+
get_minimum_due_date(monday_after_cutoff)
28+
== (monday_after_cutoff + timedelta(days=5)).date()
29+
)
30+
31+
32+
day_before_easter = datetime(2022, 4, 13, 13, 59, tzinfo=OSLO_TZ)
33+
34+
35+
def test_minimum_due_date_with_holidays_before_cutoff():
36+
"""
37+
There are 2 holidays in the span 13-17th of April 2022,
38+
so we expect the function to adjust the timedelta by 2 days.
39+
"""
40+
assert get_minimum_due_date(day_before_easter) == (day_before_easter + timedelta(days=6)).date()
41+
42+
43+
day_before_easter_after_cutoff = datetime(2022, 4, 13, 23, 59, tzinfo=OSLO_TZ)
44+
45+
46+
def test_minimum_due_date_with_holidays_after_cutoff():
47+
"""
48+
If we send in the file after the cut-off, number of
49+
holidays is upped to 3, so we expect 2 more days of offsetting.
50+
"""
51+
assert (
52+
get_minimum_due_date(day_before_easter_after_cutoff)
53+
== (day_before_easter_after_cutoff + timedelta(days=8)).date()
54+
)
55+
56+
57+
friday = datetime(2022, 4, 1, 23, 59, tzinfo=OSLO_TZ)
58+
59+
60+
def test_minimum_due_date_with_now_being_a_saturday():
61+
assert get_minimum_due_date(friday) == (friday + timedelta(days=5)).date()
62+
63+
64+
def test_minimum_due_date_without_holiday_dependency():
65+
"""
66+
Make sure an ImportError from a missing dependency isn't propagated.
67+
"""
68+
import sys
69+
70+
sys.modules['holidays'] = None
71+
assert get_minimum_due_date(monday) == (monday + timedelta(days=4)).date()
72+
sys.modules['holidays'] = holidays

0 commit comments

Comments
 (0)