dodil/erp-brick-timesheets

DODIL ERP brick: timesheets

tests/test_seams.py

"""The neighbours - hr, projects, time-off, the ledger, sales-order - each behind one adapter: every answer is a reply, never an exception. Not
installed (409), failed (502), silent (503). And every call this brick makes is checked against the CALLEE's signed spec (conftest.checked_seams)."""
import pytest
from conftest import EMPLOYEE, P1, SHEET, SYSTEM, api, approved_week, hours, line, must, open_week, refused, sheet, story_week, submit
from erp_platform import seam
from erp_platform.contract import ContractViolation

from brick_timesheets.infrastructure import seams


def test_a_call_the_callees_spec_would_refuse_fails_the_test_at_the_call(nb, checked_seams):
    base = {"journal_id": "1", "source_module": "timesheets", "source_document": "ts:S:r1", "event": "approve", "company_code": "UK01",
            "journal_date": "2026-11-23", "currency": "GBP", "lines": []}
    with pytest.raises(ContractViolation, match="event"):
        seams.LEDGER.post("/journal-entry/journals", {**base, "event": "settle"}, actor="x")
    with pytest.raises(ContractViolation):
        seams.ORDERS.post("/sales-orders/SO-1/deliveries", {"line_no": 2, "quantity": "1", "source": "payroll", "delivery_ref": "r", "delivered_on": "2026-11-23"}, actor="x")
    with pytest.raises(ContractViolation):
        seams.HR.get("/persons/1/salary", params={}, actor="x")                              # a route the spec does not list
    with pytest.raises(ContractViolation):
        seams.TIME_OFF.get("/absences", params={"from": "2026-11-16", "to": "2026-11-22"}, actor="x")      # company_code is required
    seams.HR.get("/persons/4410/employment", params={"on": "2026-11-16"}, actor="x")          # the real ones pass
    seams.HR.get("/persons/4410/work-pattern", params={"from": "2026-11-16", "to": "2026-11-22"}, actor="x")
    seams.PROJECTS.get("/projects/bookable", params={"person_id": 4410, "on": "2026-11-16"}, actor="x")
    assert len(checked_seams.violations) == 4                                                    # recorded too, so a swallowed exception still fails a test
    checked_seams.violations.clear()


def test_hr_down_or_silent_is_a_reply_and_nothing_is_half_made(nb):
    nb.hr_status = 500
    refused(api("POST", "/sheets", EMPLOYEE, {"week": "2026-W47"}), 502, "hr failed")
    nb.hr_status = None
    assert open_week()["status"] == "open"
    open_week()
    story_week()
    nb.hr_status = 500
    refused(api("POST", f"/sheets/{SHEET}/submit", EMPLOYEE), 502, "hr failed")
    assert sheet()["status"] == "open"                                                             # nothing half-submitted
    nb.hr_status = None
    assert submit()["status"] == "submitted"


def test_projects_down_leaves_the_week_open_and_a_cost_centre_line_still_books(nb):
    open_week()
    nb.projects_status = 503
    refused(api("POST", f"/sheets/{SHEET}/lines", EMPLOYEE, {"activity_code": "DEV", "project_id": P1}), 502, "projects")
    assert must(api("POST", f"/sheets/{SHEET}/lines", EMPLOYEE, {"activity_code": "ADMIN", "cost_centre": "CONS-LON"}), 201)
    nb.projects_status = None
    n = line(SHEET)["lines"][-1]["line_no"]
    hours(SHEET, n, {"2026-11-16": "7.50"})
    nb.projects_status = 500
    refused(api("POST", f"/sheets/{SHEET}/submit", EMPLOYEE), 502, "projects")
    assert sheet()["status"] == "open"


def test_time_off_down_is_a_reply_and_without_time_off_a_week_has_no_absence(nb):
    nb.toff_status = 500
    refused(api("POST", "/sheets", EMPLOYEE, {"week": "2026-W47"}), 502, "time-off failed")
    nb.toff_status = None
    seam.unregister_local("time-off")
    s = open_week()
    assert [ln for ln in s["lines"] if ln["kind"] == "absence"] == [] and s["booked_hours"] == "0.00"


def test_the_ledger_down_leaves_the_week_approved_and_the_journal_pending_until_it_is_back(nb):
    nb.gl_status = 500
    s = approved_week()
    assert s["status"] == "approved"
    (gl,) = [o for o in s["outbox"] if o["target"] == "gl"]
    assert gl["status"] == "pending" and "failed (500" in gl["last_error"]
    assert [o["status"] for o in s["outbox"] if o["target"] == "sales-order"] == ["sent"]
    nb.gl_status = None
    assert [x["status"] for x in must(api("POST", "/outbox/drain", SYSTEM))["sent"]] == ["sent"] and len(nb.journals) == 1


def test_without_the_ledger_or_the_order_the_week_waits_approved_with_its_reason(nb):
    seam.unregister_local("gl")
    seam.unregister_local("sales-order")
    s = approved_week()
    reasons = {o["target"]: o["last_error"] for o in s["outbox"]}
    assert s["status"] == "approved" and "no ledger is installed" in reasons["gl"] and "sales-order is not part of this installation" in reasons["sales-order"]


def test_the_order_down_leaves_the_delivery_pending_and_it_is_sent_when_it_is_back(nb):
    nb.orders_status = 500
    s = approved_week()
    (d,) = [o for o in s["outbox"] if o["target"] == "sales-order"]
    assert d["status"] == "pending" and "failed (500" in d["last_error"] and len(nb.journals) == 1
    nb.orders_status = None
    assert must(api("POST", "/deliveries/report", SYSTEM))["sent"][0]["status"] == "sent" and len(nb.deliveries) == 1