11from __future__ import annotations
22
3+ import re
4+ from copy import deepcopy
5+ from datetime import date , datetime , timedelta , timezone
36from typing import Any
47
58from ..models import BinaryContent , InvoiceContent
69from .base import AsyncBaseApiClient , BaseApiClient
710
11+ _OFFSET_SUFFIX_RE = re .compile (r"(?:Z|[+-]\d{2}:?\d{2})$" )
12+
13+
14+ def _last_sunday_of_month (year : int , month : int ) -> int :
15+ cursor = date (year , 12 , 31 ) if month == 12 else date (year , month + 1 , 1 ) - timedelta (days = 1 )
16+ while cursor .weekday () != 6 : # Sunday
17+ cursor -= timedelta (days = 1 )
18+ return cursor .day
19+
20+
21+ def _warsaw_offset_for_local_datetime (local_dt : datetime ) -> timedelta :
22+ year = local_dt .year
23+ dst_start = datetime (year , 3 , _last_sunday_of_month (year , 3 ), 2 , 0 , 0 )
24+ dst_end = datetime (year , 10 , _last_sunday_of_month (year , 10 ), 3 , 0 , 0 )
25+ if dst_start <= local_dt < dst_end :
26+ return timedelta (hours = 2 )
27+ return timedelta (hours = 1 )
28+
29+
30+ def _normalize_datetime_without_offset (value : str ) -> str :
31+ if "T" not in value or _OFFSET_SUFFIX_RE .search (value ):
32+ return value
33+ try :
34+ parsed = datetime .fromisoformat (value )
35+ except ValueError :
36+ return value
37+ if parsed .tzinfo is not None :
38+ return value
39+ offset = _warsaw_offset_for_local_datetime (parsed )
40+ return parsed .replace (tzinfo = timezone (offset )).isoformat ()
41+
42+
43+ def _normalize_invoice_date_range_payload (request_payload : dict [str , Any ]) -> dict [str , Any ]:
44+ normalized = deepcopy (request_payload )
45+ date_range_candidates : list [dict [str , Any ]] = []
46+
47+ top_level = normalized .get ("dateRange" )
48+ if isinstance (top_level , dict ):
49+ date_range_candidates .append (top_level )
50+
51+ filters = normalized .get ("filters" )
52+ if isinstance (filters , dict ):
53+ nested = filters .get ("dateRange" )
54+ if isinstance (nested , dict ):
55+ date_range_candidates .append (nested )
56+
57+ for date_range in date_range_candidates :
58+ for field_name in ("from" , "to" ):
59+ value = date_range .get (field_name )
60+ if isinstance (value , str ):
61+ date_range [field_name ] = _normalize_datetime_without_offset (value )
62+ return normalized
63+
864
965class InvoicesClient (BaseApiClient ):
1066 def get_invoice (self , ksef_number : str , * , access_token : str ) -> InvoiceContent :
@@ -39,6 +95,7 @@ def query_invoice_metadata(
3995 page_size : int | None = None ,
4096 sort_order : str | None = None ,
4197 ) -> Any :
98+ normalized_payload = _normalize_invoice_date_range_payload (request_payload )
4299 params : dict [str , Any ] = {}
43100 if page_offset is not None :
44101 params ["pageOffset" ] = page_offset
@@ -50,15 +107,16 @@ def query_invoice_metadata(
50107 "POST" ,
51108 "/invoices/query/metadata" ,
52109 params = params or None ,
53- json = request_payload ,
110+ json = normalized_payload ,
54111 access_token = access_token ,
55112 )
56113
57114 def export_invoices (self , request_payload : dict [str , Any ], * , access_token : str ) -> Any :
115+ normalized_payload = _normalize_invoice_date_range_payload (request_payload )
58116 return self ._request_json (
59117 "POST" ,
60118 "/invoices/exports" ,
61- json = request_payload ,
119+ json = normalized_payload ,
62120 access_token = access_token ,
63121 expected_status = {201 , 202 },
64122 )
@@ -119,6 +177,7 @@ async def query_invoice_metadata(
119177 page_size : int | None = None ,
120178 sort_order : str | None = None ,
121179 ) -> Any :
180+ normalized_payload = _normalize_invoice_date_range_payload (request_payload )
122181 params : dict [str , Any ] = {}
123182 if page_offset is not None :
124183 params ["pageOffset" ] = page_offset
@@ -130,15 +189,16 @@ async def query_invoice_metadata(
130189 "POST" ,
131190 "/invoices/query/metadata" ,
132191 params = params or None ,
133- json = request_payload ,
192+ json = normalized_payload ,
134193 access_token = access_token ,
135194 )
136195
137196 async def export_invoices (self , request_payload : dict [str , Any ], * , access_token : str ) -> Any :
197+ normalized_payload = _normalize_invoice_date_range_payload (request_payload )
138198 return await self ._request_json (
139199 "POST" ,
140200 "/invoices/exports" ,
141- json = request_payload ,
201+ json = normalized_payload ,
142202 access_token = access_token ,
143203 expected_status = {201 , 202 },
144204 )
0 commit comments