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