dodil/erp-brick-time-off

DODIL ERP brick: time off

tests/test_family_leave.py

"""Slice B item 6: family leave recorded by HR with its statutory facts (TOF-09) - payroll assesses statutory pay from them and computes the money."""
from conftest import EMPLOYEE, HR_ADMIN, MANAGER, OWNER, PAYROLL_CLIENT, api, as_role, decided, entries_of, must, on_day, refused

JUNE = "2026-06-01T09:00:00+00:00"
E4431 = "emp4431@uk.example"


def record(who=None, person="4431", leave_type_code="MATERNITY", from_date="2026-11-02", **kw):
    body = {"person_id": person, "leave_type_code": leave_type_code, "from_date": from_date, **kw}
    return api("POST", "/family-leave", who or HR_ADMIN, body)


def story(hr, at=JUNE, **kw):
    hr.person(4431, E4431, manager=hr.as_manager(2280), manager2=hr.as_manager(1904))
    with on_day(at):
        return must(record(ewc="2026-11-15", evidence_received_on="2026-06-01", **kw), 201)


def payroll(first="2026-11-01", last="2026-11-30"):
    return must(api("GET", "/absences", PAYROLL_CLIENT, {"company_code": "UK01", "from": first, "to": last}))["absences"]


def test_TOF_09_maternity_is_recorded_for_52_weeks_with_its_expected_week_and_evidence(hr):
    got = story(hr)
    assert (got["leave_type_code"], got["status"], got["from_date"], got["to_date"], got["weeks"]) == ("MATERNITY", "approved", "2026-11-02", "2027-10-31", 52)
    assert got["statutory"] == {"kind": "maternity", "ewc": "2026-11-15", "evidence_received_on": "2026-06-01", "kit_days": 0, "kit_dates": []}
    assert got["cost"] == "257.00"                                                          # 260 working days less the three bank holidays in it, hr's calendar
    rows = payroll()
    assert [(r["absence_ref"], r["leave_type_code"], r["pay_treatment"], r["status"]) for r in rows] == [(got["request_id"], "MATERNITY", "statutory_maternity", "approved")]
    assert rows[0]["statutory"] == {"kind": "maternity", "ewc": "2026-11-15", "evidence_received_on": "2026-06-01", "kit_days": 0}
    assert all("2026-11-01" <= d["date"] <= "2026-11-30" for d in rows[0]["days"]) and len(rows[0]["days"]) == 42        # the days inside the period asked for


def test_TOF_09_the_leave_holds_its_days_so_nothing_else_is_booked_over_it(hr):
    story(hr)
    hr.person(4431, E4431, manager=hr.as_manager(2280))
    hr.person(4432, "emp4432@uk.example", manager=hr.as_manager(2280))
    with on_day(JUNE):
        refused(record(person="4431", leave_type_code="PATERNITY", from_date="2026-12-14", weeks=2), 409, "day_free")
    assert must(api("GET", "/leave-requests/UK01-TOF-2026-00001", HR_ADMIN))["days"][0] == {"date": "2026-11-02", "part": "am", "quantity": "0.50"}


def test_TOF_09_a_start_before_the_11th_week_before_the_ewc_is_refused_with_the_date_named(hr):
    hr.person(4431, E4431, manager=hr.as_manager(2280))
    with on_day(JUNE):
        body = refused(record(from_date="2026-08-29", ewc="2026-11-15"), 422, "2026-08-30")
        assert body["gate"] == "earliest_start"
        assert must(record(from_date="2026-08-30", ewc="2026-11-15", weeks=2), 201)["to_date"] == "2026-09-12"      # exactly the 11th week before: allowed
        refused(record(from_date="2026-11-02"), 422, "expected week of childbirth")                              # maternity needs the ewc


def test_TOF_09_keeping_in_touch_days_are_recorded_up_to_ten_and_never_end_the_leave(hr):
    got = story(hr)
    rid = got["request_id"]
    with on_day("2027-03-31T09:00:00+00:00"):
        for n, d in enumerate(("2027-01-04", "2027-01-05", "2027-01-06", "2027-01-07", "2027-01-08", "2027-02-01", "2027-02-02", "2027-02-03", "2027-02-04", "2027-02-05"), 1):
            out = must(api("POST", f"/family-leave/{rid}/keeping-in-touch-day", HR_ADMIN, {"work_date": d}), 201)
            assert out["statutory"]["kit_days"] == n and out["status"] == "approved"
        refused(api("POST", f"/family-leave/{rid}/keeping-in-touch-day", HR_ADMIN, {"work_date": "2027-02-08"}), 409, "10 keeping-in-touch days")
        refused(api("POST", f"/family-leave/{rid}/keeping-in-touch-day", HR_ADMIN, {"work_date": "2027-01-04"}), 409, "already a keeping-in-touch day")
        refused(api("POST", f"/family-leave/{rid}/keeping-in-touch-day", HR_ADMIN, {"work_date": "2026-10-30"}), 422, "outside the leave")
        refused(api("POST", f"/family-leave/{rid}/keeping-in-touch-day", HR_ADMIN, {"work_date": "2027-04-05"}), 422, "has not happened")
        refused(api("POST", f"/family-leave/{rid}/keeping-in-touch-day", HR_ADMIN, {"work_date": "x"}), 422, "bounds")
        assert must(api("GET", f"/family-leave/{rid}", HR_ADMIN))["statutory"]["kit_days"] == 10
        row = [r for r in payroll("2027-01-01", "2027-03-31") if r["absence_ref"] == rid][0]
        assert row["statutory"]["kit_days"] == 10 and row["status"] == "approved"                              # the leave stands
    refused(api("POST", "/family-leave/UK01-TOF-2026-99999/keeping-in-touch-day", HR_ADMIN, {"work_date": "2027-01-04"}), 404)


def test_TOF_09_only_hr_records_it_never_for_themselves_and_only_a_family_leave_type(hr):
    hr.person(4431, E4431, manager=hr.as_manager(2280))
    for who in (EMPLOYEE, MANAGER, OWNER):
        refused(record(who=who, ewc="2026-11-15"), 403, "tof:absence:record")
    refused(record(person="3100", ewc="2026-11-15"), 409, "your own leave")
    refused(record(person="3100", ewc="2026-11-15", who=as_role("hr_admin", "hr3100.alt@uk.example", 3100)), 409, "second_person")           # the same sub under another email
    for typ in ("ANNUAL", "UNPAID", "SICK", "NO_SUCH"):
        refused(record(leave_type_code=typ, ewc="2026-11-15"), 409, "not a family leave type")
    refused(record(person="99999", ewc="2026-11-15"), 409, "not employed")
    assert entries_of(3100) == []


def test_TOF_09_a_day_in_other_leave_blocks_it_and_bounds_are_422(hr):
    hr.person(4431, E4431, manager=hr.as_manager(2280))
    decided("2026-12-21")
    refused(record(person="4410", leave_type_code="PATERNITY", from_date="2026-12-21", weeks=1), 409, "day_free")
    for kw in ({"weeks": 0}, {"weeks": 53}, {"from_date": "soon"}, {"ewc": "never"}, {"evidence_received_on": "2099-01-01"}, {"leave_type_code": ""}, {"person_id": "x"}):
        got = record(**{"ewc": "2026-11-15", **kw})
        assert got[0] == 422, (kw, got)


def test_TOF_09_a_cancelled_family_leave_gives_its_days_back_and_payroll_sees_it_cancelled(hr):
    got = story(hr)
    must(api("POST", f"/leave-requests/{got['request_id']}/cancel", HR_ADMIN, {"reason": "the baby came early and she asked to start at once"}))
    assert payroll() == []
    again = must(api("GET", "/absences", PAYROLL_CLIENT, {"company_code": "UK01", "from": "2026-11-01", "to": "2026-11-30", "changed_since": "2026-05-01T00:00:00Z"}))["absences"]
    assert [(r["absence_ref"], r["status"], r["days"]) for r in again] == [(got["request_id"], "cancelled", [])]
    assert again[0]["statutory"]["ewc"] == "2026-11-15"