dodil/erp-brick-payroll

DODIL ERP brick: payroll

tests/test_weekly.py

"""Weekly (and, in the tables, fortnightly and four-weekly) pay: the tax period of a pay date, HMRC's cumulative PAYE and Class 1 by period
table, and a weekly run end to end (PRL-05's worked example to the penny, from the rate rows)."""
from datetime import date
from decimal import Decimal

import pytest
from conftest import ADMIN, ADMIN2, MANAGER, api, approve, must, on_day, peer_approves_own, refused, setup_company, slip

from brick_payroll.domain import pay_run as pr
from brick_payroll.domain import paye
from brick_payroll.domain.errors import RuleBroken
from brick_payroll.domain.money import money
from brick_payroll.domain.values import DEFAULT_RATES, PERIOD_RATES


def seeded_rates():
    return paye.Rates([{"rate_key": k, "region": rg, "frequency": f, "valid_from": date(2026, 4, 6), "value": Decimal(v)}
                       for k, rg, f, v, _, _ in DEFAULT_RATES] + [{"rate_key": k, "region": rg, "frequency": f, "valid_from": vf, "value": Decimal(v)}
                                                                 for k, rg, f, vf, v, _, _ in PERIOD_RATES])


def test_the_tax_period_of_a_pay_date_by_frequency():
    assert paye.tax_period_of("monthly", date(2026, 11, 30)) == 8
    assert paye.tax_period_of("weekly", date(2026, 11, 20)) == 33                       # PRL-05: UK01-W-2026-27-W33
    assert paye.tax_period_of("weekly", date(2026, 4, 6)) == 1 and paye.tax_period_of("weekly", date(2026, 4, 12)) == 1
    assert paye.tax_period_of("weekly", date(2026, 4, 13)) == 2
    assert paye.tax_period_of("weekly", date(2027, 4, 5)) == 53                         # the 53rd week: one or two days at the end of the year
    assert paye.tax_period_of("fortnightly", date(2026, 11, 20)) == 17 and paye.tax_period_of("four_weekly", date(2026, 11, 20)) == 9


def test_a_weekly_run_is_the_iso_week_worked_paid_the_friday_after():
    assert pr.period_dates("weekly", "2026-W46") == (date(2026, 11, 9), date(2026, 11, 15), date(2026, 11, 20))
    assert pr.period_dates("monthly", "2026-11")[2] == date(2026, 11, 30)
    assert pr.pay_run_id("UK01", "W", date(2026, 11, 20), 1, "weekly") == "UK01-W-2026-27-W33"
    assert pr.pay_run_id("UK01", "W", date(2026, 11, 20), 2, "weekly") == "UK01-W-2026-27-W33-R2"
    with pytest.raises(RuleBroken, match="ISO week"):
        pr.week_bounds("2026-W54")
    with pytest.raises(RuleBroken, match="monthly and weekly only"):
        pr.period_dates("fortnightly", "2026-W46")


def test_class_1_uses_the_period_table_of_the_schedule_and_never_prorates():
    r = seeded_rates()
    ee, er = paye.class1_ni("A", "weekly", Decimal("416.00"), r).employee, paye.class1_ni("A", "weekly", Decimal("416.00"), r).employer
    assert (ee, er) == (Decimal("13.92"), Decimal("48.00"))                              # (416.00 - 242.00) x 8%, (416.00 - 96.00) x 15%
    assert paye.class1_ni("A", "monthly", Decimal("416.00"), r).employee == Decimal("0.00")      # under the monthly threshold
    assert paye.class1_ni("A", "fortnightly", Decimal("1000.00"), r).employee == money(Decimal("1000.00") - Decimal("484")) * Decimal("0.08")
    assert paye.class1_ni("A", "four_weekly", Decimal("2000.00"), r).employer == money((Decimal("2000.00") - Decimal("384")) * Decimal("0.15"))


def test_cumulative_paye_scales_the_free_pay_and_the_bands_by_the_number_of_periods():
    r = seeded_rates()
    code = paye.parse_tax_code("1257L")
    week = paye.paye(code, "cumulative", "weekly", 33, Decimal("20000.00"), Decimal("0.00"), Decimal("0.00"), r)
    # free pay to date 12,579 x 33 / 52 = 7,982.83 (up to the penny); taxable 12,017.17 -> 12,017 (down); basic band 37,700 x 33 / 52 = 23,925 (up)
    assert week.detail["free_pay_to_date"] == "7982.83" and week.detail["taxable_to_date"] == "12017" and week.detail["basic_band_to_date"] == "23925"
    assert week.tax == money(Decimal("12017") * Decimal("0.2"))
    month = paye.paye(code, "cumulative", "monthly", 8, Decimal("23008.00"), Decimal("0.00"), Decimal("0.00"), r)
    assert month.detail["free_pay_to_date"] == "8386.00"                                    # the PRL-04 figure is unchanged
    week53 = paye.paye(code, "cumulative", "weekly", 53, Decimal("20000.00"), Decimal("0.00"), Decimal("0.00"), r)
    assert week53.detail["free_pay_to_date"] == "12820.91" and week53.detail["basic_band_to_date"] == "38425"        # 53 / 52 of the year: the tables run on


def test_prl_05_the_weekly_worked_example_to_the_penny():
    """32.00 hours x 13.0000 = 416.00; qualifying earnings 416.00 - 120.00 = 296.00: pension 14.80 / 8.88; BR on 401.20 = 80.24; NI 13.92 / 48.00; net 307.04."""
    run = pr.PayRun("UK01-W-2026-27-W33", "UK01", "W", "weekly", "regular", "2026-27", 33, "2026-W46", date(2026, 11, 9), date(2026, 11, 15),
                    date(2026, 11, 20), "123/AB45678")
    inp = pr.PersonInput("4470-1", 4470, [pr.Earning("SALARY", Decimal("32.0000"), Decimal("13.0000"), Decimal("416.00"), "hours", "2026-W46")],
                         paye.parse_tax_code("BR"), "cumulative", "A", paye.Scheme("UK01-QS", "qualifying_earnings", Decimal("5"), Decimal("3"), "net_pay"), {})
    s = pr.gross_to_net(run, inp, seeded_rates())
    assert (s.gross, s.ee_pension, s.er_pension, s.taxable, s.tax, s.ee_ni, s.er_ni, s.net) == tuple(
        Decimal(x) for x in ("416.00", "14.80", "8.88", "401.20", "80.24", "13.92", "48.00", "307.04"))
    assert any("week 33" in ln.source_ref for ln in s.lines)


def _weekly_schedule():
    must(api("POST", "/pay-schedules", MANAGER, {"company_code": "UK01", "schedule_code": "W", "paye_ref": "123/AB45678", "frequency": "weekly"}))


def _weekly_company():
    setup_company()
    _weekly_schedule()


def test_a_weekly_schedule_is_paid_friday_after_and_a_week_is_not_a_month(nb):
    setup_company()
    refused(api("POST", "/pay-schedules", MANAGER, {"company_code": "UK01", "schedule_code": "W", "paye_ref": "123/AB45678", "frequency": "weekly",
                                                    "pay_day_rule": "last_working_day"}), 422, "friday_after")
    _weekly_schedule()
    run = must(api("POST", "/pay-runs", ADMIN, {"company_code": "UK01", "schedule_code": "W", "period": "2026-W46"}), 201)
    assert (run["pay_run_id"], run["period_from"], run["period_to"], run["pay_date"], run["tax_period"]) == (
        "UK01-W-2026-27-W33", "2026-11-09", "2026-11-15", "2026-11-20", 33)
    refused(api("POST", "/pay-runs", ADMIN, {"company_code": "UK01", "schedule_code": "W", "period": "2026-W46"}), 409, "period")        # one regular run per period
    refused(api("POST", "/pay-runs", ADMIN, {"company_code": "UK01", "schedule_code": "W", "period": "2026-11"}), 422, "ISO week")
    refused(api("POST", "/pay-runs", ADMIN, {"company_code": "UK01", "schedule_code": "M", "period": "2026-W46"}), 422, "calendar month")


def _onboard_br(run_id):
    got = api("POST", f"/pay-runs/{run_id}/calculate", ADMIN)
    assert got[0] == 409
    for e in must(api("GET", "/payroll-employees", ADMIN, {"status": "pending_details"}))["employees"]:
        must(api("POST", f"/payroll-employees/{e['payroll_id']}/details", ADMIN2 if e["payroll_id"] == "2001-1" else ADMIN,
                 {"tax_code": "BR", "basis": "cumulative", "ni_category": "A", "source": "manual", "valid_from": "2026-04-06"}))
        if e["payroll_id"] == "4410-1":
            must(api("POST", f"/payroll-employees/{e['payroll_id']}/pension", ADMIN, {"scheme_code": "UK01-QS", "enrolled_on": "2026-04-06"}), 201)
    return must(api("POST", f"/pay-runs/{run_id}/calculate", ADMIN))


def test_a_salaried_weekly_run_calculates_with_the_weekly_tables_and_approves(nb):
    """4410: 36,000.00 / 52 = 692.31 a week, BR: pension 28.62 / 17.17 on 572.31, taxable 663.69, tax 132.73, NI 36.02 / 89.45, net 494.94."""
    _weekly_company()
    with on_day("2026-11-16T10:00:00+00:00"):
        run = must(api("POST", "/pay-runs", ADMIN, {"company_code": "UK01", "schedule_code": "W", "period": "2026-W46"}), 201)["pay_run_id"]
        s = slip(_onboard_br(run), "4410-1")
        assert (s["gross"], s["employee_pension"], s["employer_pension"], s["taxable"], s["tax"], s["employee_ni"], s["employer_ni"], s["net"]) == (
            "692.31", "28.62", "17.17", "663.69", "132.73", "36.02", "89.45", "494.94")
        tax = next(ln for ln in s["lines"] if ln["element_code"] == "TAX")
        assert tax["source_ref"] == "BR cumulative week 33" and "ni_primary_threshold/weekly@2026-04-06" in next(
            ln for ln in s["lines"] if ln["element_code"] == "EE_NI")["rate_rows_used"]
        peer_approves_own(run)
        done = must(approve(run))
        assert done["status"] == "approved" and done["journal_status"] == "posted"
    assert nb.journals and next(iter(nb.journals.values()))["journal_date"] == "2026-11-20"


def test_the_next_week_is_opened_only_after_the_previous_is_approved(nb):
    _weekly_company()
    with on_day("2026-11-16T10:00:00+00:00"):
        must(api("POST", "/pay-runs", ADMIN, {"company_code": "UK01", "schedule_code": "W", "period": "2026-W46"}), 201)
        refused(api("POST", "/pay-runs", ADMIN, {"company_code": "UK01", "schedule_code": "W", "period": "2026-W47"}), 409, "approve it first")