dodil/erp-brick-tax
DODIL ERP brick: tax
tests/test_audit_musts.py
"""Audit of tax v4 (b69b7c9..eb22e6b): each test FAILS on the audited code for the reason in its docstring."""
from decimal import Decimal
import conftest
from conftest import (
ADMIN,
AFTER_26AD,
BROWSER,
OWNER,
SYSTEM,
api,
filing_ready,
frozen_purchase,
frozen_sale,
hmrc_ob,
line,
must,
on_day,
prepare,
sync,
)
from sqlalchemy import text as sql
D = Decimal
LATE = "2027-04-10T09:00:00+00:00"
AFTER_27AB = "2027-07-10T09:00:00+00:00"
def _entries(engine, ref):
with engine.connect() as c:
return [dict(r._mapping) for r in c.execute(sql("select * from tax_vat_return_entry where document_ref = :r"), {"r": ref})]
def _box(out, n):
return D(str(out["boxes"][str(n)]))
def _2027(h):
h.obs += [hmrc_ob("27AA", "2027-01-01", "2027-03-31", "2027-05-07"), hmrc_ob("27AB", "2027-04-01", "2027-06-30", "2027-08-07")]
sync(to="2029-12-31")
def _scheme(body):
return must(api("POST", "/registration/change-scheme", ADMIN, body))
def _submit(who):
return api("POST", "/vat-returns/26AD/submit", who, {"declaration": True, "client": BROWSER})
# ── M1 second person ──────────────────────────────────────────────────────────────────────────
def test_m1_after_a_timeout_the_person_who_locked_the_return_may_not_file_it(hmrc_world):
"""returns.py:532 skips the second-person check when the return is SUBMITTING: the locker files it after the owner's timeout."""
h = hmrc_world[0]
filing_ready()
frozen_sale("INV-1", [line(1, "1000.00")])
must(prepare())
with on_day(AFTER_26AD):
must(api("POST", "/vat-returns/26AD/lock", ADMIN, {}))
h.timeout_next = True
assert _submit(OWNER)[0] == 504 # left submitting, HMRC has nothing
status, body = _submit(ADMIN) # the locker
assert status == 409 and "second_person" in str(body), body
# ── M2 a document's tax enters ONE return (not zero) ──────────────────────────────────────────
def test_m2_a_sale_frozen_between_prepare_and_lock_is_in_no_return(engine):
"""lock_vat_return (returns.py:465) freezes the boxes of the last prepare without re-claiming, and plan (schemes.py:85) skips a sale
whose tax point is before a later period: the sale's VAT is never reported."""
filing_ready()
frozen_sale("INV-1", [line(1, "1000.00")])
must(prepare())
with on_day("2026-12-20T09:00:00+00:00"):
frozen_sale("INV-LATE", [line(1, "500.00")], tax_point="2026-12-15")
with on_day(AFTER_26AD):
out = api("POST", "/vat-returns/26AD/lock", ADMIN, {})
if out[0] == 200: # locked without it: it must then land somewhere
assert _entries(engine, "INV-LATE"), "INV-LATE's VAT 100.00 is in no VAT return"
def test_m3_de_minimis_counts_only_this_periods_input_vat(hmrc_world):
"""schemes.py:254 buckets every purchase row the plan produced, including documents already claimed by an earlier return, so the
de minimis test of 27AB fails on 27AA's exempt VAT (1,000 + 1,000 > 1,875) though 27AB alone holds."""
from test_schemes import pe_bills, pe_world
pe_world(hmrc_world)
pe_bills(exempt="5000.00") # exempt VAT 1,000 in 27AA
with on_day(LATE):
assert must(prepare("27AA"))["partial_exemption"]["de_minimis_holds"] is True
must(api("POST", "/partial-exemption/years", ADMIN, {"year_end": "2028-03-31", "provisional_pct": "71"}))
frozen_purchase("PB-T2", [line(1, "30000.00", category="pe_taxable")], tax_point="2027-04-20")
frozen_purchase("PB-X2", [line(1, "5000.00", category="pe_exempt")], tax_point="2027-05-01")
frozen_purchase("PB-R2", [line(1, "15000.00", category="pe_residual")], tax_point="2027-05-15")
with on_day(AFTER_27AB):
out = must(prepare("27AB"))
pe = out["partial_exemption"]
assert pe["de_minimis_holds"] is True and D(str(pe["exempt_reclaimed"])) == D("1000.00"), pe
def test_m4_joining_cash_accounting_never_taxes_an_invoice_twice(hmrc_world, ar_and_payments):
"""schemes.py:146/151 look only at cash* entries: an invoice already in 27AA on the invoice basis is taxed again in 27AB when paid."""
pays = ar_and_payments[1]
filing_ready()
_2027(hmrc_world[0])
_scheme({"scheme": "cash_accounting", "effective_from": "2027-04-01", "expected_turnover": "900000.00"})
frozen_sale("INV-A", [line(1, "1000.00")], tax_point="2027-02-01")
with on_day(LATE):
assert _box(must(prepare("27AA")), 1) == D("200.00")
pays.cash.append({"payment_id": "PAY-A", "invoice_id": "INV-A", "seq": 1, "amount": "1200.00", "currency": "GBP",
"received_on": "2027-04-20", "allocated_on": "2027-04-20", "source_kind": "receipt"})
with on_day(AFTER_27AB):
assert _box(must(prepare("27AB")), 1) == 0 # already reported in 27AA
def test_m5_leaving_cash_accounting_voluntarily_keeps_the_outstanding_vat(hmrc_world):
"""schemes.py:85 skips a sale with a tax point before the period on the invoice basis; only the turnover-limit exit brings the
outstanding VAT in, so an unpaid cash-basis invoice is never taxed after a voluntary move back to standard."""
filing_ready()
_2027(hmrc_world[0])
_scheme({"scheme": "cash_accounting", "effective_from": "2027-01-01", "expected_turnover": "900000.00"})
_scheme({"scheme": "standard", "effective_from": "2027-04-01"})
frozen_sale("INV-B", [line(1, "1000.00")], tax_point="2027-02-01") # unpaid
with on_day(AFTER_27AB):
total = _box(must(prepare("27AA")), 1) + _box(must(prepare("27AB")), 1)
assert total == D("200.00")
def _registered_with(monkeypatch, scheme):
# a flat rate registration now carries its percentage (422 without it: test_m7_flat_rate_registration_without_a_percentage_is_422)
extra = {"flat_rate_pct": "14.5"} if scheme == "flat_rate" else {}
monkeypatch.setattr(conftest, "registered", lambda vat="GB123456789", since="2011-01-04": must(api(
"POST", "/registration/record-registered", ADMIN, {"vat_number": vat, "effective_from": since, "scheme": scheme, **extra})))
def test_m7_flat_rate_registration_without_a_percentage_is_422():
status, _ = api("POST", "/registration/record-registered", ADMIN,
{"vat_number": "GB123456789", "effective_from": "2011-01-04", "scheme": "flat_rate"})
assert status == 422
def test_m2_lock_refuses_with_return_stale_when_a_sale_was_frozen_after_prepare():
filing_ready()
frozen_sale("INV-1", [line(1, "1000.00")])
must(prepare())
with on_day("2026-12-20T09:00:00+00:00"):
frozen_sale("INV-LATE", [line(1, "500.00")], tax_point="2026-12-15")
with on_day(AFTER_26AD):
status, body = api("POST", "/vat-returns/26AD/lock", ADMIN, {})
assert status == 409 and "return_stale" in str(body), (status, body)
must(prepare())
must(api("POST", "/vat-returns/26AD/lock", ADMIN, {}))
def test_naive_utc_accepts_the_aware_datetimes_the_pg_wire_returns():
from datetime import datetime, timedelta, timezone
from brick_tax.application import returns
aware = datetime(2030, 1, 1, 12, 0, tzinfo=timezone(timedelta(hours=1)))
assert returns._naive_utc(aware) == datetime(2030, 1, 1, 11, 0)
assert returns._naive_utc(aware) > returns._utcnow()
assert returns._naive_utc(datetime(2030, 1, 1)) == datetime(2030, 1, 1) and returns._naive_utc(None) is None
view = returns._conn_view({"vrn": "1", "scopes": "", "environment": "sandbox", "authorised_by": "x", "authorised_at": aware,
"expires_at": aware, "status": "connected"})
assert view["connected"] is True
def test_m6_an_existing_cash_accounting_registration_prepares_on_the_cash_basis(monkeypatch):
"""registration.record_first (registration.py:86) leaves accounting_basis 'invoice' for scheme cash_accounting, so plan uses the
invoice basis for a business recorded as on cash accounting."""
_registered_with(monkeypatch, "cash_accounting")
filing_ready()
assert must(prepare())["accounting_basis"] == "cash"
def test_m7_an_existing_flat_rate_registration_answers_a_gate_not_a_500(monkeypatch):
"""record_registered(scheme=flat_rate) stores no flat_rate_pct; when the limited cost trader test is not met schemes.py:216
multiplies by None -> 500."""
_registered_with(monkeypatch, "flat_rate")
filing_ready()
frozen_sale("INV-F", [line(1, "50000.00")])
frozen_purchase("PB-S", [line(1, "1000.00", supply="goods"), line(2, "1000.00", supply="goods")])
with on_day(AFTER_26AD):
status, _ = prepare()
assert status in (200, 409, 422), status
def test_m8_import_vat_paid_listed_after_its_period_is_locked_is_still_reclaimed(hmrc_world, engine):
"""_imports (schemes.py:280) takes an entry only in the period of its entry date; the certificate arriving after that return is
locked means the import VAT is never reclaimed ('reclaimable on the next return prepared')."""
filing_ready()
_2027(hmrc_world[0])
must(api("POST", "/imports", SYSTEM, {"mrn": "26GB99900000000001", "entry_date": "2026-11-05", "bill_ref": "PB-IMP", "bp_id": 8800,
"customs_value": "10000.00", "import_vat": "2000.00", "method": "paid"}))
must(prepare())
with on_day(AFTER_26AD):
must(api("POST", "/vat-returns/26AD/lock", ADMIN, {}))
must(api("POST", "/imports/26GB99900000000001/certificate-listed", ADMIN, {}))
with on_day(LATE):
must(prepare("27AA"))
assert _entries(engine, "26GB99900000000001"), "the 2,000.00 import VAT paid is reclaimed in no return"
# ── CIS ───────────────────────────────────────────────────────────────────────────────────────
def test_m9_a_deduction_in_a_submitted_cis_month_is_refused_not_silently_left_off_every_cis300(hmrc_world):
"""compute_cis_deduction (cis.py:180) books a deduction into a tax month whose CIS300 is submitted; prepare refuses that month and
nothing else carries it, so the deduction withheld is never filed (reverse_cis_deduction has the cis_return_open gate, compute none)."""
from test_slice_b import cis_world, deduct
cis_world()
must(api("POST", "/cis/subcontractors/8801/verify", ADMIN, {}))
must(deduct("P1"))
with on_day("2026-12-10T09:00:00+00:00"):
must(api("POST", "/cis/returns/2026-12-05/prepare", ADMIN, {}))
must(api("POST", "/cis/returns/2026-12-05/submit", OWNER, {"employment_status_declared": True, "verification_declared": True}))
status, body = deduct("P-LATE", paid="2026-11-28")
if status == 200:
filed = must(api("GET", "/cis/returns/2026-12-05", ADMIN, {}))
assert D(str(filed["total_deduction"])) == D("1600.00"), "the 800.00 deducted on P-LATE is on no CIS300"
else:
assert status == 409, (status, body)
def test_m10_two_same_day_verifications_collide_instead_of_one_overwriting_the_other(monkeypatch):
"""_verify (cis.py:131) checks, then UPSERTS the dated row: a racer that passed the same check silently overwrites the first
verification ('a re-verification is a new dated row, never an edit')."""
from test_slice_b import cis_world
from brick_tax.infrastructure import tables
cis_world()
must(api("POST", "/cis/subcontractors/8801/verify", ADMIN, {"status": "net", "verification_number": "V1"}))
real = tables._Rows.get
def stale(self, **key): # the racer's snapshot: taken before the first committed
if self.table.name == "tax_cis_subcontractor" and key.get("valid_from") is not None and str(key["valid_from"]) == "2026-11-20":
return None
return real(self, **key)
monkeypatch.setattr(tables._Rows, "get", stale)
status, _ = api("POST", "/cis/subcontractors/8801/verify", ADMIN, {"status": "unmatched", "verification_number": "V2"})
monkeypatch.setattr(tables._Rows, "get", real)
assert status == 409, "the second verification overwrote the first"