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