dodil/erp-brick-tax

DODIL ERP brick: tax

tests/test_v6.py

"""Spec v6: TAX-24 (issue date, address object), TAX-50 (less detailed invoice), TAX-51 (eat in / take away), TAX-52 (GET /rates),
TAX-53 (compute without a business partner)."""
from conftest import ADMIN, SYSTEM, api, compute, compute_purchase, gates_of, line, must, purchase_setup, setup

ADDRESS = {"lines": ["9 Dock Road"], "city": "Hull", "postcode": "HU1 1AA", "country": "GB"}
BILL = {"invoice_number": "AS-2210", "issue_date": "2026-11-19", "supplier_name": "Acme Supplies Ltd", "supplier_address": ADDRESS,
        "supplier_vat_number": "GB987654321"}


def check(ref, contents):
    return must(api("POST", f"/documents/{ref}/check-contents", SYSTEM, {"direction": "purchase", "contents": contents}))


def test_tax24_the_supplier_address_is_an_object_and_a_string_still_counts_as_one_line():
    purchase_setup()
    must(compute_purchase([line(1, "1000.00")]))
    out = check("PB-1", {**BILL, "customer_name": "Dodil Ltd"})
    assert out["ready"] is True and out["title"] == "Bill"
    assert check("PB-1", {**BILL, "customer_name": "Dodil Ltd", "supplier_address": "9 Dock Road, Hull"})["ready"] is True
    bad = {**BILL, "supplier_address": {"lines": ["x"], "city": "Hull", "country": "gb"}}
    assert api("POST", "/documents/PB-1/check-contents", SYSTEM, {"direction": "purchase", "contents": bad})[0] == 422


def test_tax50_a_receipt_up_to_250_gross_is_a_less_detailed_invoice_without_our_name_and_over_it_needs_a_full_invoice():
    purchase_setup()
    must(compute_purchase([line(1, "8.00")], ref="R-1"))                                # 9.60 gross
    small = check("R-1", BILL)
    assert small["ready"] is True and small["title"] == "less_detailed_invoice"
    assert "customer_name" not in {g["gate"] for g in small["gates"]}
    must(compute_purchase([line(1, "260.00")], ref="R-2"))                              # 312.00 gross
    big = check("R-2", BILL)
    unmet = {g["gate"]: g for g in big["gates"] if not g["met"]}
    assert big["ready"] is False and big["title"] == "Bill" and "customer_name" in unmet
    assert unmet["customer_name"]["reason"] == "a VAT invoice over 250.00 must show your company's name: ask the supplier for a full invoice"
    must(compute_purchase([line(1, "208.33")], ref="R-3"))                              # 249.996 -> 250.00 gross: at the limit
    assert check("R-3", BILL)["title"] == "less_detailed_invoice"


def food_rules():
    purchase_setup()
    must(api("POST", "/categories/food_hot", ADMIN, {"description": "hot food"}))
    must(api("POST", "/categories/coffee", ADMIN, {"description": "coffee"}))
    must(api("POST", "/rules", ADMIN, {"category_code": "food_hot", "treatment_code": "uk_business", "tax_code": "S", "consumption": "eat_in"}))
    must(api("POST", "/rules", ADMIN, {"category_code": "food_hot", "treatment_code": "uk_business", "tax_code": "R", "consumption": "takeaway"}))
    must(api("POST", "/rules", ADMIN, {"category_code": "coffee", "treatment_code": "uk_business", "tax_code": "S"}))


def test_tax51_consumption_selects_among_rules_that_distinguish_it_and_a_line_that_says_nothing_is_refused():
    food_rules()
    out = must(compute([line(1, "10.00", category="food_hot", consumption="eat_in"), line(2, "10.00", category="food_hot", consumption="takeaway"),
                        line(3, "10.00", category="coffee")]))
    assert [(x["tax_code"], x["rate_pct"], x["consumption"]) for x in out["lines"]] == [("S", "20", "eat_in"), ("R", "5", "takeaway"), ("S", "20", None)]
    assert out["lines"][1]["rule"].startswith("(food_hot, uk_business, any, takeaway)")
    s, body = api("POST", "/documents/compute", SYSTEM, {"lines": [line(1, "10.00", category="food_hot")], "tax_point": "2026-11-20", "bp_id": 7311})
    assert s == 409 and gates_of(body)[0]["gate"] == "consumption_given"
    again = must(compute([line(1, "10.00", category="coffee", consumption="takeaway")]))              # an `any` rule covers both
    assert again["lines"][0]["tax_code"] == "S"


def test_tax51_the_consumption_is_part_of_the_rules_key_and_listed():
    food_rules()
    rows = must(api("GET", "/rules", ADMIN, {"category_code": "food_hot"}))["rules"]
    assert sorted((r["consumption"], r["tax_code"]) for r in rows) == [("eat_in", "S"), ("takeaway", "R")]
    assert all(r["in_force_today"] for r in rows)
    s, _ = api("POST", "/rules", ADMIN, {"category_code": "food_hot", "treatment_code": "uk_business", "tax_code": "S", "consumption": "eat_in",
                                          "valid_from": "2026-11-21"})
    assert s == 200                                                                                  # a later row of the same key


def test_tax52_the_rate_table_for_a_register_has_one_row_per_category_and_consumption_and_the_day_it_goes_stale():
    food_rules()
    must(api("POST", "/treatments/uk_consumer", ADMIN, {}))
    for cat, code, cons in (("food_hot", "S", "eat_in"), ("food_hot", "R", "takeaway"), ("coffee", "S", "any")):  # zero is covered by the pack's (zero, any) rule
        must(api("POST", "/rules", ADMIN, {"category_code": cat, "treatment_code": "uk_consumer", "tax_code": code, "consumption": cons}))
    must(api("POST", "/codes/R/schedule-rate", ADMIN, {"rate_pct": "7", "valid_from": "2027-04-01"}))
    out = must(api("GET", "/rates", SYSTEM, {"country": "GB", "categories": "food_hot,coffee,zero,mystery", "on": "2026-11-20"}))
    assert out["treatment_code"] == "uk_consumer" and out["missing"] == ["mystery"] and out["valid_until"] == "2027-03-31"
    got = {(r["category_code"], r["consumption"]): (r["tax_code"], r["rate_pct"]) for r in out["rates"]}
    assert got == {("food_hot", "eat_in"): ("S", "20"), ("food_hot", "takeaway"): ("R", "5"), ("coffee", "any"): ("S", "20"), ("zero", "any"): ("Z", "0")}
    only = must(api("GET", "/rates", SYSTEM, {"country": "GB", "categories": "food_hot", "on": "2026-11-20", "consumption": "takeaway"}))
    assert [r["consumption"] for r in only["rates"]] == ["takeaway"]


def test_tax53_a_document_computes_without_a_partner_when_a_treatment_is_named():
    setup()
    s, out = api("POST", "/documents/compute", SYSTEM, {"lines": [line(1, "10.00")], "tax_point": "2026-11-20", "treatment_code": "uk_business"})
    assert s == 200 and out["lines"][0]["tax_code"] == "S" and out["bp_id"] is None and out["treatment_code"] == "uk_business"
    s, body = api("POST", "/documents/compute", SYSTEM, {"lines": [line(1, "10.00")], "tax_point": "2026-11-20"})
    assert s == 422 and body["gate"] == "party_or_treatment"
    s, body = api("POST", "/documents/compute", SYSTEM, {"lines": [line(1, "10.00")], "tax_point": "2026-11-20", "bp_id": 7311, "treatment_code": "uk_consumer"})
    assert s == 422 and body["gate"] == "treatment_matches_party"
    assert api("POST", "/documents/compute", SYSTEM, {"lines": [line(1, "10.00")], "tax_point": "2026-11-20", "bp_id": 7311, "treatment_code": "uk_business"})[0] == 200