Skip to content

Commit 0fdcc32

Browse files
authored
feat: add employer details endpoint (#29)
2 parents ec25b64 + 1267a50 commit 0fdcc32

3 files changed

Lines changed: 217 additions & 2 deletions

File tree

ergani/client.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
CompanyOvertime,
1212
CompanyWeeklySchedule,
1313
CompanyWorkCard,
14+
EmployerDetails,
1415
SubmissionResponse,
1516
)
1617
from ergani.utils import extract_error_message, normalize_base_url
@@ -54,7 +55,8 @@ def _request(
5455
Requests exceptions may be raised for network-related errors
5556
"""
5657

57-
url = f"{self.base_url}/{endpoint.lstrip('/')}"
58+
normalized_endpoint = endpoint.lstrip("/")
59+
url = f"{self.base_url}/{normalized_endpoint}"
5860
auth = ErganiAuthentication(self.username, self.password, self.base_url)
5961

6062
response = requests.request(
@@ -280,3 +282,28 @@ def get_services_list(self) -> Optional[Response]:
280282
"""
281283

282284
return self._request("GET", "/WebServices/ServicesList", None)
285+
286+
def get_employer_details(self) -> EmployerDetails:
287+
"""
288+
Fetches employer details from the Ergani API.
289+
290+
Returns:
291+
EmployerDetails: Employer details parsed from the EX_BASE_01 response.
292+
293+
Raises:
294+
APIError: An error occurred while communicating with the Ergani API
295+
AuthenticationError: Raised if there is an authentication error with the Ergani API
296+
ValueError: Raised if the query payload cannot be parsed into an employer details object
297+
"""
298+
299+
response = self._execute_service("EX_BASE_01")
300+
301+
if response is None:
302+
return EmployerDetails.parse({})
303+
304+
try:
305+
payload = response.json()
306+
except ValueError as error:
307+
raise ValueError("EX_BASE_01 returned a non-JSON response") from error
308+
309+
return EmployerDetails.parse(payload)

ergani/models.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
from __future__ import annotations
2+
13
from dataclasses import dataclass, field
24
from datetime import date, datetime, time
3-
from typing import List, Literal, Optional, TypedDict
5+
from typing import Any, Dict, List, Literal, Optional, TypedDict
46

57
from ergani.typings import (
68
LateDeclarationJustificationType,
@@ -21,6 +23,76 @@
2123
)
2224

2325

26+
@dataclass
27+
class EmployerDetails:
28+
employer_id: int | None = None
29+
employer_tax_identification_number: str | None = None
30+
name: str | None = None
31+
distinctive_title: str | None = None
32+
employer_registry_number: str | None = None
33+
is_in_card_sector: bool | None = None
34+
raw_payload: Dict[str, Any] = field(default_factory=dict)
35+
36+
@classmethod
37+
def parse(cls, payload: Any) -> EmployerDetails:
38+
employer_payload = cls._parse_payload(payload)
39+
40+
return cls(
41+
employer_id=_parse_int(employer_payload.get("Id")),
42+
employer_tax_identification_number=employer_payload.get("Afm"),
43+
name=employer_payload.get("Eponimia"),
44+
distinctive_title=employer_payload.get("DiakritikosTitlos"),
45+
employer_registry_number=employer_payload.get("Ame"),
46+
is_in_card_sector=_parse_card_sector_flag(
47+
employer_payload.get("IsInCardSector")
48+
),
49+
raw_payload=dict(employer_payload),
50+
)
51+
52+
@classmethod
53+
def _parse_payload(cls, payload: Any) -> Dict[str, Any]:
54+
if not isinstance(payload, dict):
55+
raise ValueError("Expected EX_BASE_01 payload to be an object")
56+
57+
if "EX_BASE_01" in payload:
58+
payload = payload["EX_BASE_01"]
59+
60+
if not isinstance(payload, dict):
61+
raise ValueError("Expected EX_BASE_01 payload to contain an object")
62+
63+
employer_payload = payload.get("Ergodotis", payload)
64+
65+
if not isinstance(employer_payload, dict):
66+
raise ValueError("Expected EX_BASE_01 employer payload to be an object")
67+
68+
return employer_payload
69+
70+
71+
def _parse_int(value: Any) -> int | None:
72+
if value is None or value == "":
73+
return None
74+
75+
try:
76+
return int(value)
77+
except (TypeError, ValueError):
78+
return None
79+
80+
81+
def _parse_card_sector_flag(value: Any) -> bool | None:
82+
if isinstance(value, str):
83+
normalized_value = value.strip()
84+
if normalized_value == "1":
85+
return True
86+
if normalized_value == "0":
87+
return False
88+
89+
return None
90+
91+
92+
def parse_employer_details(payload: Any) -> EmployerDetails:
93+
return EmployerDetails.parse(payload)
94+
95+
2496
@dataclass
2597
class WorkCard:
2698
"""

tests/test_employer_details.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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 EmployerDetails, parse_employer_details
8+
9+
10+
class EmployerDetailsTests(TestCase):
11+
def test_get_employer_details_uses_execute_service_and_parses_verified_fields(self):
12+
client = ErganiClient("username", "password", "https://example.test")
13+
response = Mock(spec=Response)
14+
response.json.return_value = {
15+
"EX_BASE_01": {
16+
"Ergodotis": {
17+
"Id": "33263",
18+
"Afm": "801405734",
19+
"Eponimia": "LOGIC ΜΟΝΟΠΡΟΣΩΠΗ ΙΚΕ",
20+
"DiakritikosTitlos": "LOGIC",
21+
"Ame": "9316208810",
22+
"IsInCardSector": "1",
23+
}
24+
}
25+
}
26+
27+
with patch.object(
28+
client, "_execute_service", return_value=response
29+
) as execute_service_mock:
30+
result = client.get_employer_details()
31+
32+
self.assertEqual(
33+
result,
34+
EmployerDetails(
35+
employer_id=33263,
36+
employer_tax_identification_number="801405734",
37+
name="LOGIC ΜΟΝΟΠΡΟΣΩΠΗ ΙΚΕ",
38+
distinctive_title="LOGIC",
39+
employer_registry_number="9316208810",
40+
is_in_card_sector=True,
41+
raw_payload={
42+
"Id": "33263",
43+
"Afm": "801405734",
44+
"Eponimia": "LOGIC ΜΟΝΟΠΡΟΣΩΠΗ ΙΚΕ",
45+
"DiakritikosTitlos": "LOGIC",
46+
"Ame": "9316208810",
47+
"IsInCardSector": "1",
48+
},
49+
),
50+
)
51+
execute_service_mock.assert_called_once_with("EX_BASE_01")
52+
53+
def test_employer_details_parse_reads_verified_fields(self):
54+
self.assertEqual(
55+
EmployerDetails.parse(
56+
{
57+
"EX_BASE_01": {
58+
"Ergodotis": {
59+
"Id": "33263",
60+
"Afm": "801405734",
61+
"Eponimia": "LOGIC ΜΟΝΟΠΡΟΣΩΠΗ ΙΚΕ",
62+
"DiakritikosTitlos": "LOGIC",
63+
"Ame": "9316208810",
64+
"IsInCardSector": "1",
65+
}
66+
}
67+
}
68+
),
69+
EmployerDetails(
70+
employer_id=33263,
71+
employer_tax_identification_number="801405734",
72+
name="LOGIC ΜΟΝΟΠΡΟΣΩΠΗ ΙΚΕ",
73+
distinctive_title="LOGIC",
74+
employer_registry_number="9316208810",
75+
is_in_card_sector=True,
76+
raw_payload={
77+
"Id": "33263",
78+
"Afm": "801405734",
79+
"Eponimia": "LOGIC ΜΟΝΟΠΡΟΣΩΠΗ ΙΚΕ",
80+
"DiakritikosTitlos": "LOGIC",
81+
"Ame": "9316208810",
82+
"IsInCardSector": "1",
83+
},
84+
),
85+
)
86+
87+
def test_parse_employer_details_keeps_verified_fields_optional(self):
88+
self.assertEqual(
89+
parse_employer_details({}),
90+
EmployerDetails(raw_payload={}),
91+
)
92+
93+
def test_parse_employer_details_handles_false_card_sector_values(self):
94+
self.assertEqual(
95+
parse_employer_details(
96+
{"EX_BASE_01": {"Ergodotis": {"IsInCardSector": "0"}}}
97+
),
98+
EmployerDetails(
99+
is_in_card_sector=False,
100+
raw_payload={"IsInCardSector": "0"},
101+
),
102+
)
103+
104+
def test_parse_employer_details_keeps_unknown_card_sector_values_optional(self):
105+
self.assertEqual(
106+
parse_employer_details(
107+
{"EX_BASE_01": {"Ergodotis": {"IsInCardSector": "yes"}}}
108+
),
109+
EmployerDetails(
110+
raw_payload={"IsInCardSector": "yes"},
111+
),
112+
)
113+
114+
def test_parse_employer_details_requires_object_payload(self):
115+
with self.assertRaises(ValueError):
116+
parse_employer_details([])

0 commit comments

Comments
 (0)