From 5bc0a33b502e0bf03b3ef2daab42c5f658e6a8ca Mon Sep 17 00:00:00 2001 From: lizerk Date: Tue, 21 Apr 2026 13:56:20 +0300 Subject: [PATCH] Add LogStorm attendance payload builder --- backend/common/logstorm_attendance.py | 109 +++++++++++++++++ .../integration/test_logstorm_attendance.py | 112 ++++++++++++++++++ 2 files changed, 221 insertions(+) create mode 100644 backend/common/logstorm_attendance.py create mode 100644 backend/tests/integration/test_logstorm_attendance.py diff --git a/backend/common/logstorm_attendance.py b/backend/common/logstorm_attendance.py new file mode 100644 index 00000000..da8167a8 --- /dev/null +++ b/backend/common/logstorm_attendance.py @@ -0,0 +1,109 @@ +"""Helpers for building EUSRR -> LogStorm attendance requests.""" + +from datetime import date +from typing import Any, Optional + +from employees.models import Employee + +from common.logstorm_client import LogStormClient + + +DEFAULT_WORKDAYS = [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", +] + + +def build_logstorm_attendance_payload( + *, + employee: Employee, + period_start: date, + period_end: date, + schedule: Optional[dict[str, Any]] = None, + date_overrides: Optional[list[dict[str, Any]]] = None, +) -> dict[str, Any]: + """ + Build a LogStorm attendance analysis payload. + + If schedule is omitted, the payload intentionally omits it so LogStorm can + apply its configured default schedule or reject the request by policy. + """ + payload = { + "employee_id": str(employee.id), + "display_name": _employee_display_name(employee), + "period_start": period_start.isoformat(), + "period_end": period_end.isoformat(), + } + + if schedule is not None: + payload["schedule"] = normalize_logstorm_schedule( + schedule, + date_overrides=date_overrides, + ) + + return payload + + +def normalize_logstorm_schedule( + schedule: dict[str, Any], + *, + date_overrides: Optional[list[dict[str, Any]]] = None, +) -> dict[str, Any]: + return { + "start_time": schedule["start_time"], + "end_time": schedule["end_time"], + "expected_hours": schedule["expected_hours"], + "workdays": schedule.get("workdays", DEFAULT_WORKDAYS), + "date_overrides": [ + _normalize_date_override(override) + for override in ( + date_overrides + if date_overrides is not None + else schedule.get("date_overrides", []) + ) + ], + } + + +def analyze_employee_attendance( + *, + employee: Employee, + period_start: date, + period_end: date, + schedule: Optional[dict[str, Any]] = None, + date_overrides: Optional[list[dict[str, Any]]] = None, + client: Optional[LogStormClient] = None, +) -> dict[str, Any]: + payload = build_logstorm_attendance_payload( + employee=employee, + period_start=period_start, + period_end=period_end, + schedule=schedule, + date_overrides=date_overrides, + ) + return (client or LogStormClient()).analyze_attendance(payload) + + +def _normalize_date_override(override: dict[str, Any]) -> dict[str, Any]: + result = { + "date": _format_date(override["date"]), + "is_workday": bool(override["is_workday"]), + } + for key in ("reason", "start_time", "end_time", "expected_hours"): + if key in override and override[key] is not None: + result[key] = override[key] + return result + + +def _format_date(value: Any) -> str: + if isinstance(value, date): + return value.isoformat() + return str(value) + + +def _employee_display_name(employee: Employee) -> str: + full_name = employee.get_full_name().strip() + return full_name or str(employee) diff --git a/backend/tests/integration/test_logstorm_attendance.py b/backend/tests/integration/test_logstorm_attendance.py new file mode 100644 index 00000000..c063e6bf --- /dev/null +++ b/backend/tests/integration/test_logstorm_attendance.py @@ -0,0 +1,112 @@ +from datetime import date + +import pytest + +from common.logstorm_attendance import ( + analyze_employee_attendance, + build_logstorm_attendance_payload, + normalize_logstorm_schedule, +) + +pytestmark = pytest.mark.django_db + + +class FakeLogStormClient: + def __init__(self): + self.payloads = [] + + def analyze_attendance(self, payload): + self.payloads.append(payload) + return {"records": []} + + +def test_payload_omits_schedule_when_eusrr_has_no_schedule(user_factory): + employee = user_factory(first_name="Ivan", last_name="Petrov") + + payload = build_logstorm_attendance_payload( + employee=employee, + period_start=date(2026, 4, 20), + period_end=date(2026, 4, 21), + ) + + assert payload["employee_id"] == str(employee.id) + assert payload["display_name"] == "Ivan Petrov" + assert payload["period_start"] == "2026-04-20" + assert payload["period_end"] == "2026-04-21" + assert "schedule" not in payload + + +def test_payload_includes_schedule_and_date_overrides(user_factory): + employee = user_factory(first_name="Ivan", last_name="Petrov") + + payload = build_logstorm_attendance_payload( + employee=employee, + period_start=date(2026, 4, 20), + period_end=date(2026, 4, 26), + schedule={ + "start_time": "09:00", + "end_time": "18:00", + "expected_hours": 9, + "workdays": ["Monday"], + }, + date_overrides=[ + { + "date": date(2026, 4, 25), + "is_workday": True, + "reason": "transferred_workday", + "start_time": "10:00", + "end_time": "16:00", + "expected_hours": 6, + } + ], + ) + + assert payload["schedule"] == { + "start_time": "09:00", + "end_time": "18:00", + "expected_hours": 9, + "workdays": ["Monday"], + "date_overrides": [ + { + "date": "2026-04-25", + "is_workday": True, + "reason": "transferred_workday", + "start_time": "10:00", + "end_time": "16:00", + "expected_hours": 6, + } + ], + } + + +def test_schedule_defaults_to_regular_workweek(): + schedule = normalize_logstorm_schedule({ + "start_time": "09:00", + "end_time": "18:00", + "expected_hours": 9, + }) + + assert schedule["workdays"] == [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + ] + assert schedule["date_overrides"] == [] + + +def test_analyze_employee_attendance_calls_logstorm_client(user_factory): + employee = user_factory(first_name="Ivan", last_name="Petrov") + client = FakeLogStormClient() + + result = analyze_employee_attendance( + employee=employee, + period_start=date(2026, 4, 20), + period_end=date(2026, 4, 20), + client=client, + ) + + assert result == {"records": []} + assert client.payloads[0]["employee_id"] == str(employee.id) + assert "schedule" not in client.payloads[0]