Skip to content

Commit a166f1a

Browse files
authored
feat: add branch details endpoint (#30)
2 parents 0fdcc32 + 0342699 commit a166f1a

4 files changed

Lines changed: 275 additions & 1 deletion

File tree

AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Agent Instructions
2+
3+
- Before handing work over, run autofixable checks first. Use `uv run ruff format && uv run ruff check --fix`, then verify with `uv run ruff format --check && uv run ruff check`.
4+
- For Python test validation in this repo, use `PYTHONPATH=$PWD uv run --no-project --with requests python3 -m unittest discover -s tests`.

ergani/client.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from ergani.auth import ErganiAuthentication
88
from ergani.exceptions import APIError, AuthenticationError
99
from ergani.models import (
10+
BusinessBranch,
1011
CompanyDailySchedule,
1112
CompanyOvertime,
1213
CompanyWeeklySchedule,
@@ -283,6 +284,30 @@ def get_services_list(self) -> Optional[Response]:
283284

284285
return self._request("GET", "/WebServices/ServicesList", None)
285286

287+
def get_branch_details(self) -> List[BusinessBranch]:
288+
"""
289+
Fetches the authenticated employer's branch details from the Ergani API.
290+
291+
Returns:
292+
List[BusinessBranch]: The parsed branch detail entries.
293+
294+
Raises:
295+
APIError: An error occurred while communicating with the Ergani API
296+
AuthenticationError: Raised if there is an authentication error with the Ergani API
297+
ValueError: The response payload could not be parsed as a branch list
298+
"""
299+
300+
response = self._execute_service("EX_BASE_02")
301+
payload = None
302+
303+
if response:
304+
try:
305+
payload = response.json()
306+
except ValueError as error:
307+
raise ValueError("EX_BASE_02 returned a non-JSON response") from error
308+
309+
return BusinessBranch.parse_many(payload)
310+
286311
def get_employer_details(self) -> EmployerDetails:
287312
"""
288313
Fetches employer details from the Ergani API.

ergani/models.py

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,81 @@
2323
)
2424

2525

26+
@dataclass
27+
class BusinessBranch:
28+
"""
29+
Represents a business branch returned by the Ergani query services.
30+
31+
Attributes:
32+
branch_number (Optional[int]): The verified branch identifier used by later
33+
query endpoints when present in the payload.
34+
address (Optional[str]): The branch address when returned by EX_BASE_02.
35+
sepe_service_code (Optional[str]): The SEPE service code for the branch.
36+
oaed_service_code (Optional[str]): The OAED service code for the branch.
37+
business_branch_activity_code (Optional[str]): The branch activity code.
38+
kallikratis_municipal_code (Optional[str]): The Kallikratis municipal code.
39+
status_description (Optional[str]): The current branch status description.
40+
raw_payload (Dict[str, Any]): The raw branch payload returned by the API.
41+
"""
42+
43+
branch_number: Optional[int]
44+
address: Optional[str]
45+
sepe_service_code: Optional[str]
46+
oaed_service_code: Optional[str]
47+
business_branch_activity_code: Optional[str]
48+
kallikratis_municipal_code: Optional[str]
49+
status_description: Optional[str]
50+
raw_payload: Dict[str, Any]
51+
52+
@classmethod
53+
def parse(cls, payload: Dict[str, Any]) -> BusinessBranch:
54+
if not isinstance(payload, dict):
55+
raise ValueError("Expected EX_BASE_02 branch payload to be an object")
56+
57+
return cls(
58+
branch_number=_parse_int(payload.get("Aa")),
59+
address=payload.get("Address"),
60+
sepe_service_code=payload.get("YpiresiaSepe"),
61+
oaed_service_code=payload.get("YpiresiaOaed"),
62+
business_branch_activity_code=payload.get("Kad"),
63+
kallikratis_municipal_code=payload.get("Kallikratis"),
64+
status_description=payload.get("StatusDescription"),
65+
raw_payload=payload,
66+
)
67+
68+
@classmethod
69+
def parse_many(cls, payload: Any) -> List[BusinessBranch]:
70+
if payload is None:
71+
return []
72+
73+
branch_payload = cls._unwrap_payload(payload)
74+
75+
if isinstance(branch_payload, dict):
76+
return [cls.parse(branch_payload)]
77+
78+
if not isinstance(branch_payload, list):
79+
raise ValueError(
80+
"Expected EX_BASE_02 branch payload to be an object or list"
81+
)
82+
83+
return [cls.parse(item) for item in branch_payload]
84+
85+
@staticmethod
86+
def _unwrap_payload(payload: Any) -> Any:
87+
if not isinstance(payload, dict):
88+
raise ValueError("Expected EX_BASE_02 payload to be an object")
89+
90+
if "EX_BASE_02" in payload:
91+
payload = payload["EX_BASE_02"]
92+
93+
if not isinstance(payload, dict):
94+
raise ValueError("Expected EX_BASE_02 payload to contain an object")
95+
96+
branch_payload = payload.get("Pararthma", payload)
97+
98+
return branch_payload
99+
100+
26101
@dataclass
27102
class EmployerDetails:
28103
employer_id: int | None = None
@@ -68,7 +143,7 @@ def _parse_payload(cls, payload: Any) -> Dict[str, Any]:
68143
return employer_payload
69144

70145

71-
def _parse_int(value: Any) -> int | None:
146+
def _parse_int(value: Any) -> Optional[int]:
72147
if value is None or value == "":
73148
return None
74149

tests/test_branch_details.py

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
from unittest import TestCase
2+
from unittest.mock import Mock, patch
3+
4+
from requests.models import Response
5+
6+
from ergani.client import ErganiClient
7+
from ergani.models import BusinessBranch
8+
9+
SINGLE_BRANCH_PAYLOAD = {
10+
"Aa": "0",
11+
"Address": "ΠΛΑΤΕΙΑ ΑΓ. ΘΕΟΔΩΡΩΝ 6 10561 ΑΘΗΝΑ",
12+
"YpiresiaSepe": "11010",
13+
"YpiresiaOaed": "101201",
14+
"Kad": "6210",
15+
"Kallikratis": "91860101",
16+
"StatusDescription": "Έναρξη απασχόλησης",
17+
}
18+
19+
BRANCH_PAYLOAD = {
20+
"EX_BASE_02": {
21+
"Pararthma": [
22+
{
23+
"Aa": "0",
24+
"Address": "ΑΓ. ΜΑΡΙΝΑΣ 19400 ΚΟΡΩΠΙ",
25+
},
26+
{
27+
"Aa": "1",
28+
"Address": "ΛΕΒΙΔΟΥ 14562 ΚΗΦΙΣΙΑ",
29+
},
30+
]
31+
}
32+
}
33+
34+
SINGLE_BRANCH_RAW_PAYLOAD = {
35+
"Aa": "0",
36+
"Address": "ΠΛΑΤΕΙΑ ΑΓ. ΘΕΟΔΩΡΩΝ 6 10561 ΑΘΗΝΑ",
37+
"YpiresiaSepe": "11010",
38+
"YpiresiaOaed": "101201",
39+
"Kad": "6210",
40+
"Kallikratis": "91860101",
41+
"StatusDescription": "Έναρξη απασχόλησης",
42+
}
43+
44+
BRANCH_RAW_PAYLOADS = [
45+
{
46+
"Aa": "0",
47+
"Address": "ΑΓ. ΜΑΡΙΝΑΣ 19400 ΚΟΡΩΠΙ",
48+
},
49+
{
50+
"Aa": "1",
51+
"Address": "ΛΕΒΙΔΟΥ 14562 ΚΗΦΙΣΙΑ",
52+
},
53+
]
54+
55+
56+
class BusinessBranchTests(TestCase):
57+
def test_business_branch_parse_reads_single_branch_payload(self) -> None:
58+
self.assertEqual(
59+
BusinessBranch.parse(SINGLE_BRANCH_PAYLOAD),
60+
BusinessBranch(
61+
branch_number=0,
62+
address="ΠΛΑΤΕΙΑ ΑΓ. ΘΕΟΔΩΡΩΝ 6 10561 ΑΘΗΝΑ",
63+
sepe_service_code="11010",
64+
oaed_service_code="101201",
65+
business_branch_activity_code="6210",
66+
kallikratis_municipal_code="91860101",
67+
status_description="Έναρξη απασχόλησης",
68+
raw_payload=SINGLE_BRANCH_RAW_PAYLOAD,
69+
),
70+
)
71+
72+
def test_business_branch_parse_many_reads_wrapped_response(self) -> None:
73+
self.assertEqual(
74+
BusinessBranch.parse_many(BRANCH_PAYLOAD),
75+
[
76+
BusinessBranch(
77+
branch_number=0,
78+
address="ΑΓ. ΜΑΡΙΝΑΣ 19400 ΚΟΡΩΠΙ",
79+
sepe_service_code=None,
80+
oaed_service_code=None,
81+
business_branch_activity_code=None,
82+
kallikratis_municipal_code=None,
83+
status_description=None,
84+
raw_payload=BRANCH_RAW_PAYLOADS[0],
85+
),
86+
BusinessBranch(
87+
branch_number=1,
88+
address="ΛΕΒΙΔΟΥ 14562 ΚΗΦΙΣΙΑ",
89+
sepe_service_code=None,
90+
oaed_service_code=None,
91+
business_branch_activity_code=None,
92+
kallikratis_municipal_code=None,
93+
status_description=None,
94+
raw_payload=BRANCH_RAW_PAYLOADS[1],
95+
),
96+
],
97+
)
98+
99+
def test_business_branch_parse_requires_object_payload(self) -> None:
100+
with self.assertRaises(ValueError):
101+
BusinessBranch.parse("invalid")
102+
103+
def test_business_branch_parse_many_accepts_single_branch_payload(self) -> None:
104+
self.assertEqual(
105+
BusinessBranch.parse_many(
106+
{"EX_BASE_02": {"Pararthma": SINGLE_BRANCH_PAYLOAD}}
107+
),
108+
[
109+
BusinessBranch(
110+
branch_number=0,
111+
address="ΠΛΑΤΕΙΑ ΑΓ. ΘΕΟΔΩΡΩΝ 6 10561 ΑΘΗΝΑ",
112+
sepe_service_code="11010",
113+
oaed_service_code="101201",
114+
business_branch_activity_code="6210",
115+
kallikratis_municipal_code="91860101",
116+
status_description="Έναρξη απασχόλησης",
117+
raw_payload=SINGLE_BRANCH_RAW_PAYLOAD,
118+
)
119+
],
120+
)
121+
122+
def test_get_branch_details_uses_execute_service_and_parses_wrapped_response(
123+
self,
124+
) -> None:
125+
client = ErganiClient("username", "password", "https://example.test")
126+
response = Mock(spec=Response)
127+
response.json.return_value = BRANCH_PAYLOAD
128+
129+
with patch.object(
130+
client, "_execute_service", return_value=response
131+
) as execute_service_mock:
132+
result = client.get_branch_details()
133+
134+
self.assertEqual(
135+
result,
136+
[
137+
BusinessBranch(
138+
branch_number=0,
139+
address="ΑΓ. ΜΑΡΙΝΑΣ 19400 ΚΟΡΩΠΙ",
140+
sepe_service_code=None,
141+
oaed_service_code=None,
142+
business_branch_activity_code=None,
143+
kallikratis_municipal_code=None,
144+
status_description=None,
145+
raw_payload=BRANCH_RAW_PAYLOADS[0],
146+
),
147+
BusinessBranch(
148+
branch_number=1,
149+
address="ΛΕΒΙΔΟΥ 14562 ΚΗΦΙΣΙΑ",
150+
sepe_service_code=None,
151+
oaed_service_code=None,
152+
business_branch_activity_code=None,
153+
kallikratis_municipal_code=None,
154+
status_description=None,
155+
raw_payload=BRANCH_RAW_PAYLOADS[1],
156+
),
157+
],
158+
)
159+
execute_service_mock.assert_called_once_with("EX_BASE_02")
160+
161+
def test_get_branch_details_returns_empty_list_for_no_content(self) -> None:
162+
client = ErganiClient("username", "password", "https://example.test")
163+
164+
with patch.object(
165+
client, "_execute_service", return_value=None
166+
) as execute_service_mock:
167+
result = client.get_branch_details()
168+
169+
self.assertEqual(result, [])
170+
execute_service_mock.assert_called_once_with("EX_BASE_02")

0 commit comments

Comments
 (0)