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"