# Tax (UK VAT first): the module source.
#
# PROVENANCE. Written 2026-10-18 for milestone M1 (Lead to Cash), conveyor steps 1-2, from:
# - HMRC VAT Notice 700 (The VAT guide), read 2026-10-18 at gov.uk/guidance/vat-guide-notice-700:
# para 16.3.1 what a VAT invoice must show (sequential number, tax point, issue date, our name, address
# and VAT number, the customer's name and address, per line the description, quantity, rate
# and net amount, the total net, the rate of any cash discount, the total VAT in sterling,
# the unit price)
# para 16.4 a foreign-currency invoice still shows its VAT total in sterling
# para 16.5 zero-rated or exempt items show no VAT and a separate total
# para 16.6.1 a simplified invoice is allowed at 250 GBP or less, with the customer's agreement
# para 17.5 rounding: the invoice's total VAT may be rounded DOWN to a whole penny; per line (17.5.1)
# round down to 0.1p, or to the nearest 1p or 0.5p, consistently; per unit (17.5.2) to 3
# decimals, or to the nearest 1p / 0.5p but never to nil
# para 17.6 retailers calculating at line or invoice level must not round down
# - HMRC, VAT domestic reverse charge for building and construction services (from 2021-03-01) and its
# technical guide: VAT is not charged and the invoice shows that the reverse charge applies; VAT Notice
# 735 for the specified goods
# - APQC PCF v8.0: 10929 (9.9.1.3 Maintain tax master data), 10766 (9.9.2 Process taxes), 10934 (9.9.2.5
# Account for taxes);
# - the end-to-end process and customization level of a mainstream SME suite's taxes and fiscal positions,
# as a parity reference only.
# SIGNED OFF BY THE CUSTOMER: Seemo, DODIL (reference customer), 2026-10-18, after the M1 review page. Each
# process is signed on its own (signoff/tax/<process>.json, erp-compose signoff verify); the decisions settled
# on the review are under `decisions:` below.
#
# VERSION 3 (2026-10-22, wave 2, DRAFT for sign-off; v2 as signed is unchanged otherwise): a document is computed
# and checked in a DIRECTION, sale (every v2 document) or purchase (purchasing's bills and credits). A purchase uses
# the SUPPLIER's treatment and the input-VAT rules, answers reclaimable and non-reclaimable input VAT per line, and
# its contents check names the SUPPLIER's name and VAT number (VAT Notice 700 para 16.3.1, read from the bill). The
# Odoo slice also carries type_tax_use = 'purchase' taxes. The map route is shared.yaml's one form.
#
# The decisions an accountant checks first:
# - a product carries a tax CATEGORY and a customer a tax TREATMENT; a dated rule turns the pair into a
# tax CODE, and the code's rate on the TAX POINT date is what is charged (the equivalent of a fiscal
# position, without a second copy of the products)
# - tax is computed by THIS module and frozen on the document at issue; a later rate change never alters an
# issued document
# - the rounding METHOD is a setting chosen from the methods HMRC permits; each method is code, and
# net + tax = total holds under every one of them
# - "not registered" is a real state: such a business charges no VAT and its document may not call
# itself a VAT invoice
# - every rate, rule, treatment, registration and setting is a DATED row; a change starts after today (in
# business_timezone) and a correction is a new row, never an edit; only a key's first row, which nothing
# in this system has used yet, may carry an earlier date (a new installation, the Odoo migration)
#
# VERSION 4 (2026-10-22, the SAP full-scope pass, DRAFT for the one re-sign; v3 is unchanged otherwise): every capability an
# SME-to-mid-market UK business files or accounts for is designed, extending erp-brick-tax rather than replacing it:
# - the Making Tax Digital VAT return (boxes 1-9 from the frozen document tax, obligations, the legal declaration, submission
# to HMRC's VAT (MTD) API with the fraud-prevention headers, the receipt, liabilities and payments, the settle journal),
# error corrections within HMRC's threshold, and the "is this tax point in a filed return?" answer ar's void and issue
# gates ask (L2C leftover: it had no callee) (TAX-26..TAX-33)
# - the HMRC VAT-number check (TAX-13, un-deferred: the 2026-10-18 deferral to M2 is put to the re-sign, see decisions)
# - bad debt relief and its repayment on a later receipt; cash accounting and the flat rate scheme computed, not only
# recorded; partial exemption (TAX-34..TAX-37, TAX-48)
# - CIS both sides: subcontractor verification, the deduction purchasing applies, the monthly CIS300 and statements, the
# deduction a contractor customer suffers on us (TAX-38..TAX-41)
# - Northern Ireland goods: the EC sales list and Intrastat; import VAT by postponed accounting and the monthly statement
# reconciliation; the construction reverse charge's end-user declaration and purchase side (TAX-42..TAX-46)
# - a line PINNED to an earlier document's frozen code and rate (a deposit deduction, a credit-note correction, a settlement
# discount) so tax accepts it without re-determination (L2C leftover "deposits") (TAX-47)
# - the rename the leftovers ask for: accept_suggestion -> accept_tax_suggestion (unique across the method)
# Sources added for v4: HMRC VAT (MTD) API v1.0 (obligations, returns, liabilities, payments), HMRC Fraud Prevention
# specification (connection method WEB_APP_VIA_SERVER), HMRC Check a UK VAT number API v2.0, VAT Notice 700/45 (correcting
# errors), 700/18 (bad debt relief), 731 (cash accounting), 733 (flat rate scheme), 706 (partial exemption), 702 (imports,
# postponed accounting), 725 (EC sales list, NI), CIS 340 (Construction Industry Scheme), HMRC Intrastat (NI) guidance.
#
# REVISED 2026-10-22 for sign-off: brought to the conventions of the product and projects specs (dated rows,
# owner-held controls, exact machine grants, readiness gates, AI acceptance) and given its Odoo migration slice.
# Version 7 (2026-10-26, DRAFT for sign-off, spec gaps wave 4; version 6 stays the signed one until it is signed):
# - seams only, no process change: the four v6 routes for pos and expenses (TAX-52, TAX-53 twice, TAX-50) moved
# from external_services: to seams:, where erp_platform.contract reads them; their shapes are unchanged
#
# Version 6 (2026-10-25, DRAFT for sign-off; the fixes the bricks and the end-to-end wiring found, and wave 3's routes):
# - TAX-24's seam takes what purchasing now sends: the supplier's invoice date and the supplier's address in the
# one address shape (shared.yaml conventions.address); a string address is still accepted until the next major
# - PROCESS (determine): POST /documents/compute works without bp_id when treatment_code is given (a merchant on
# an expense receipt, an anonymous till sale): the treatment named stands in for the party's
# - PROCESS (check_contents): the less detailed invoice rule for a purchase (UK VAT Regulations 1995 reg. 16, VAT
# Notice 700 para 16.6.1: up to 250.00 GBP including VAT, no customer name or address needed)
# - a line-level consumption (eat_in | takeaway) on compute and on tax rules, which decides food rates in DE, FR
# and AT (POS-08)
# - GET /rates?country=&categories=&on=&consumption=: the dated rate table an offline register sells with (POS-30)
# - use cases (MCP tools) of the shared families name the module noun (tool_names)
#
id: tax
name: Tax (VAT)
short: Tax
version: 7
maturity: tested
summary: >-
The right VAT on every quote, order and invoice without anyone choosing a rate: standard, reduced, zero,
exempt and reverse charge worked out from what you sell and who you sell it to, rounded the way HMRC
allows, and frozen on the invoice so a rate change never rewrites what a customer received.
decisions: # settled by the reference customer on the M1 review, 2026-10-18
- decided: "this module owns tax_code (sole writer, shared.yaml keys.tax_code); the ledger only maps each code to an account"
by: "Seemo, DODIL (reference customer)"
date: "2026-10-18"
- decided: "the DEFAULT rounding is per line, to the nearest penny (VAT Notice 700 para 17.5.1); the per-invoice round-down (17.5) stays a setting"
by: "Seemo, DODIL (reference customer)"
date: "2026-10-18"
- decided: "the VAT-number check against HMRC (TAX-13) is deferred to M2: no check step in the M1 process; the AI name comparison stays listed in ai_steps as deferred"
by: "Seemo, DODIL (reference customer)"
date: "2026-10-18"
- decided: "the UK seed pack is S 20%, R 5%, Z, E, OS, RC, NV (TAX-02)"
by: "Seemo, DODIL (reference customer)"
date: "2026-10-18"
- proposed: "v4: the VAT-number check (TAX-13) is designed now and runs before a reverse-charge or overseas treatment is used; this supersedes the 2026-10-18 deferral to M2 once re-signed"
status: "for the SAP full-scope re-sign (2026-10-22)"
- proposed: "v4: the VAT return is prepared by the tax admin and submitted by a person who makes HMRC's declaration; a second person submits by default where the business has more than one (vat_return_needs_second_person, owner's control)"
status: "for the SAP full-scope re-sign (2026-10-22)"
- proposed: "v4: statutory thresholds and rates (cash accounting and flat-rate limits, the error-correction threshold, CIS rates, partial-exemption de minimis, Intrastat thresholds) are dated rows of the UK pack (tax_statutory), never constants in code"
status: "for the SAP full-scope re-sign (2026-10-22)"
facets:
domain: finance
industries: [any]
customer_words:
- "VAT"
- "the right tax on invoices"
- "reverse charge"
- "zero-rated exports"
- "we're not VAT registered yet"
- "the VAT rate changed"
- "file our VAT return"
- "Making Tax Digital"
- "check a customer's VAT number"
- "CIS deductions"
- "bad debt relief"
- "flat rate scheme"
- "cash accounting"
- "postponed import VAT"
- "EC sales list"
replaces: [a VAT column typed by hand, the tax settings of the accounting package, Odoo taxes and fiscal positions, bridging software for MTD, a CIS spreadsheet]
pillars: [sql]
first_module_fit: foundation
depends_on:
- module: business-partner
why: a customer's tax treatment and VAT number belong to the party, by bp_id
required: recommended
- module: product
why: a line's tax category comes from its product
required: recommended
- module: gl
why: the account each tax code posts to, and the sterling rate for a foreign-currency VAT total
required: optional
- module: payments
why: the payment dates cash accounting taxes on, receipts after a bad debt claim, the CIS a contractor customer deducted (v4)
required: optional
- module: purchasing
why: the CIS deduction on a subcontractor's payment, import entries, supplier payments under cash accounting (v4)
required: optional
- module: ar
why: invoices written off for bad debt relief; issue and void ask whether a tax point is in a filed return (v4)
required: optional
feeds: [crm, sales-order, ar, purchasing, gl, payments]
owns_master_data: [tax_code, tax_category]
references_master_data: [business_partner, product, fx_rate]
actors:
- id: tax_admin
name: Whoever is responsible for VAT (the owner, the bookkeeper, the accountant)
does: records the VAT registration, keeps tax codes, rates, categories, treatments and the rules that join them
sees: everything
permissions: [tax:code:read, tax:code:write, tax:code:approve, tax:category:write, tax:registration:write, tax:party_tax:write, tax:document_tax:approve_override, tax:settings:write, tax:vat_return:read, tax:vat_return:prepare, tax:vat_return:submit, tax:vat_adjustment:write, tax:bad_debt:claim, tax:cis:write, tax:cis_return:submit, tax:trade_declaration:write, tax:import_vat:write, tax:partial_exemption:write]
- id: owner
name: The owner or a finance controller
does: sets the controls on whoever keeps VAT (whether a rate, rule or registration change waits for a second person, who approves an override that lowers tax, the business time zone)
sees: everything
permissions: [tax:code:read, tax:code:approve, tax:settings:controls, tax:hmrc:connect, tax:vat_return:read, tax:vat_return:submit, tax:cis_return:submit]
note: "v4: the owner also connects the business to HMRC (TAX-26) and may submit a VAT or CIS return; submitting is a legal declaration a person makes, never a service client"
- id: biller
name: Whoever raises orders and invoices
does: sees the tax worked out on a document, records a customer's VAT number, overrides a line's code with a reason
sees: tax codes, a document's tax
permissions: [tax:code:read, tax:party_tax:write, tax:document_tax:override]
- id: system
name: The system
does: loads the country's codes, determines and computes tax for a document in its direction (sale or purchase), suggests a code where no rule covers a line, checks its contents, freezes it at issue, runs the one-off Odoo migration; v4 - reads HMRC obligations, liabilities and payments, checks VAT numbers, answers whether a tax point is filed, computes a CIS deduction and records one suffered, records import entries, finds bad debts eligible for relief
sees: everything, through its own service account (one service client per calling brick, each with only its exact grants)
permissions: [tax:code:read, tax:code:seed, tax:document_tax:compute, tax:document_tax:suggest, tax:history:import, tax:migration:read, tax:obligation:sync, tax:vat_number:check, tax:tax_point:read, tax:cis:deduct, tax:cis:record_suffered, tax:import_vat:record, tax:bad_debt:suggest, tax:cis:read]
stories:
# ── person ────────────────────────────────────────────────────────────────────────────────
- id: TAX-01
kind: person
actor: tax_admin
story: As the person responsible for VAT, I want to record whether and from when we are VAT registered, so that we charge VAT exactly when we must and never before.
given: a business not registered for VAT
when: I record registration number GB123456789 from 2027-01-01
then: 'documents with a tax point before 2027-01-01 charge no VAT and are not titled "VAT invoice"; from that date the rules apply and the number prints on every VAT invoice; registration with no number is refused, and so is one from today or earlier (documents already used "not registered" for those days): the readiness answers {gate: starts_after_today, met: false, reason: "a registration starts tomorrow at the earliest", fix: "Tax settings > Registration"}'
screen: Tax settings
path: { process: tax.registration, choices: {} }
reference: not built
- id: TAX-02
kind: person
actor: tax_admin
story: As the tax admin, I want the UK codes ready on day one, so that I am correcting a list rather than building one.
given: a new installation for a UK business
when: I open the tax codes
then: "the UK pack is there: S standard 20%, R reduced 5%, Z zero 0%, E exempt, OS outside the scope, RC domestic reverse charge (VAT due 20%, charged 0%), NV no VAT (not registered); each with its treatment and its wording on the document; I can add, rename or retire a code, never delete one a document used"
screen: Tax codes
path: { process: tax.tax_code, choices: {} }
reference: not built
- id: TAX-03
kind: person
actor: tax_admin
story: As the tax admin, I want to schedule a rate change from a date, so that the change happens at midnight without anyone editing invoices.
given: code S at 20% from 2011-01-04
when: I add 21% from 2027-04-01
then: a tax point on 2027-03-31 charges 20%, on 2027-04-01 21%; documents already issued are unchanged; a second rate for the same code and date is refused, and so is a rate from today or earlier; the 21% row may be withdrawn until 2027-04-01, after which a correction is a new row from tomorrow, never an edit; where the owner's control asks for it, the change waits for a second person's approval
screen: Tax codes
reference: not built
- id: TAX-04
kind: person
actor: tax_admin
story: As the tax admin, I want rules that pick the code from what is sold and who buys it, so that nobody chooses a rate on an invoice.
given: "rules: (standard, uk_business) -> S; (standard, overseas_business, services) -> OS; (construction_service, uk_cis_contractor) -> RC; (reduced, any) -> R"
when: a line of category standard is added for a UK business, and the same line for an overseas business buying services
then: 'S at 20% for the first, OS with no VAT for the second, each line naming the rule that chose it; a pair no rule covers is refused with "no tax rule for (category, treatment)" rather than defaulting to 0%; a rule is a dated row: a new rule for a pair no rule covers yet may start today, a change to a covered pair starts after today'
screen: Tax rules
reference: not built
customer_varies: which categories and treatments exist; most UK service businesses need four rules
- id: TAX-05
kind: person
actor: biller
story: As the biller, I want to record a customer's VAT treatment and number once, so that every document for them is taxed the same way.
given: a customer with bp_id 7311 and no treatment
when: I set treatment uk_business with VAT number GB987654321
then: their documents are determined from the treatment valid on each tax point; the first treatment of a customer holds from the start (no document could issue without one); a change is a new row from tomorrow and applies to documents not yet issued with a tax point from then; treatments that need a VAT number (reverse charge, overseas business) refuse to save without one
screen: Customer tax
reference: not built
- id: TAX-06
kind: person
actor: biller
story: As the biller, I want to override a line's tax code with a reason, so that an unusual supply is handled without a new rule, and the override is visible.
given: a draft invoice line determined as S
when: I change it to Z with the reason "export of goods, proof of export held"
then: the line shows Z, the reason and who changed it; where the override lowers the tax, it waits for approval by a holder of tax:document_tax:approve_override who is not me (unless the owner's control allows self-approval with a reason) before the document can issue
screen: Document tax
reference: not built
- id: TAX-07
kind: person
actor: tax_admin
story: As the tax admin, I want to choose how VAT is rounded, from the methods HMRC allows, so that our invoices match what we file.
given: "three lines of 10.03 at 20% (VAT 2.006 each, 6.018 in all)"
when: the invoice is computed under each method
then: "per_invoice_round_down: VAT 6.01 (para 17.5); per_line_nearest: 2.01 x 3 = 6.03 (17.5.1); per_line_down_0_1p: 2.006 x 3 = 6.018, total rounded down to 6.01 (17.5.1); a retail setting refuses both round-down methods (17.6); net + VAT = total every time (30.09 + 6.01 = 36.10, or 30.09 + 6.03 = 36.12)"
screen: Tax settings
reference: not built
- id: TAX-08
kind: person
actor: tax_admin
story: As the tax admin, I want prices that include VAT to be handled, so that a consumer price of 12.00 is 10.00 plus 2.00 on the invoice.
given: a price list with prices including VAT and a line at 12.00 under S
when: the document's tax is computed
then: net 10.00, VAT 2.00, total 12.00; the gross stays the price the customer was shown
screen: Document tax
reference: not built
- id: TAX-09
kind: person
actor: biller
story: As the biller, I want to accept or reject the code the AI suggests for a line no rule covers, so that an unusual line is taxed by a person's decision with the reason kept.
given: "a draft invoice line \"on-site training, Dublin office\" with no product, and ai_suggest_code in suggest mode"
when: the AI suggests OS with "services to an overseas business are outside the scope (VAT Notice 741A)" and I accept it
then: the line takes OS, recorded as an override with the reason, the suggestion, its confidence, the model and me as the person who accepted; rejecting leaves the line refused until I choose a code; an accepted code that lowers tax below the category's standard code waits for approval as in TAX-06; nothing is applied without a person
screen: Document tax
reference: not built
- id: TAX-14
kind: person
actor: owner
story: As the owner, I want to set the controls on whoever keeps VAT, so that the person who changes a rate or lowers a line's tax cannot also waive the check on it.
given: a tax admin who is also the biller
when: I set tax_changes_need_second_person true and the business time zone Europe/London
then: a rate, rule or registration change waits for a second holder of tax:code:approve; "today" is the date in Europe/London; a tax admin holding tax:settings:write is refused on these controls
screen: Tax settings
reference: not built
# ── system ────────────────────────────────────────────────────────────────────────────────
- id: TAX-10
kind: system
actor: system
trigger: when a document's lines, customer or tax point change (in the same request)
story: The system determines each line's code and computes the document's tax, so that the tax is never typed.
given: an order with lines in S and Z for a UK business, tax point 2026-11-20
when: it is computed
then: per line code, rate and VAT; a per-rate summary (one row per code, net and VAT, zero and exempt as separate totals, para 16.5); the total; computing twice gives the same answer
reference: not built
- id: TAX-11
kind: system
actor: system
trigger: when the document issues
story: The system freezes the tax on the document at issue, so that it is the tax the customer received forever.
given: an invoice computed at 20%, and a rate change to 21% scheduled for the day after its tax point
when: it issues, and is read after the rate change
then: 'it still shows 20%; recomputing an issued document is refused ("issued: credit it instead")'
reference: not built
- id: TAX-12
kind: system
actor: system
trigger: before a document issues
story: The system checks the document carries everything a VAT invoice must show, so that an incomplete invoice never leaves.
given: a VAT-registered business and a draft with no customer address
when: issue is asked
then: 'refused with the unmet readiness gates, each {gate, met, reason, fix}, one per absent item from para 16.3.1 (here {gate: customer_address, met: false, reason: "a VAT invoice shows the customer''s address (VAT Notice 700 para 16.3.1)", fix: "the customer''s address on the business partner"}); a business that is not registered is checked for the opposite (no VAT number, not titled "VAT invoice")'
reference: not built
- id: TAX-13
kind: system
actor: system
trigger: when a reverse-charge or overseas-business treatment is saved, and before such a document issues (v4; was deferred to M2)
story: The system checks the customer's VAT number with HMRC, so that a reverse charge is not applied to someone who is not registered.
given: treatment uk_cis_contractor with VAT number GB987654321, our own VRN 123456789
when: the number is checked (HMRC Check a UK VAT number API v2.0, GET /organisations/vat/check-vat-number/lookup/987654321/123456789)
then: 'the result (valid, invalid, could_not_check), the name and address HMRC holds, HMRC''s consultation number and the date are stored on the treatment row (checked_on, check_result, check_ref); "invalid" refuses the reverse charge with {gate: vat_number_valid, met: false, reason: "HMRC has no registration for GB987654321", fix: "Customer tax: correct the number, or a consumer treatment"}; "could not check" (HMRC down, a non-GB number) answers {gate: vat_number_valid, met: false} with fix "a person confirms the number" and a tax admin may confirm it with a reason; a check older than vat_number_check_days (default 90) is re-run before the next reverse-charge document issues; the AI name comparison (check_vat_number) flags a name that differs from the partner''s, as a suggestion only'
reference: not built
- id: TAX-15
kind: system
actor: system
trigger: once, at cutover from Odoo, after the customer has answered migration.odoo.door
story: The system loads the customer's Odoo taxes, fiscal positions and customer VAT data as this module's first rows, so that the first invoice after cutover is taxed exactly as Odoo would have taxed it.
given: an Odoo company with sale taxes 20% S, 0% Z, Exempt, a reverse-charge tax and the fiscal positions "UK", "EU B2B" and "Rest of world"
when: the migration runs, and runs again
then: tax codes, rates, treatments, rules and customer treatments exist as first rows with the dates the customer gave at the door, the purchase taxes (type_tax_use = purchase) as purchase codes and rules (v3); every row in migration.odoo.reconcile is equal or explained; the second run changes nothing; no issued document's tax is recomputed
reference: not built
# ── integration ───────────────────────────────────────────────────────────────────────────
- id: TAX-23
kind: integration
actor: system
trigger: when purchasing records or changes a supplier bill or credit (v3)
story: Purchasing asks this module for the input VAT on a bill, with the supplier's treatment and the purchase rules, so that VAT reclaimed is never typed.
given: "a UK VAT-registered supplier (treatment uk_business, its tax_party row); a bill dated 2026-11-20 with a standard-rated goods line of 1,000.00 and a business-entertainment line of 100.00"
when: "purchasing calls POST /documents/compute with direction: purchase"
then: "the rules are read for (category, the SUPPLIER's treatment, supply, direction purchase): the goods line takes the input code at 20% with VAT 200.00 reclaimable; the entertainment line takes the blocked input code with VAT 20.00 non-reclaimable (it is a cost, not a reclaim); totals carry vat, vat_reclaimable 200.00 and vat_non_reclaimable 20.00; a sale-direction rule is never used for a purchase, and a pair with no purchase rule is refused (never 0%)"
owner: tax
reference: not built
- id: TAX-24
kind: integration
actor: system
trigger: when a supplier bill is about to be approved (v3)
story: Purchasing asks this module whether a bill is a valid VAT invoice from the supplier, so that input VAT is reclaimed only on one.
given: "a bill from Acme Supplies Ltd with no VAT number on it, and our own name as the customer"
when: "purchasing calls POST /documents/{document_ref}/check-contents with direction: purchase"
then: "the content gates check the SUPPLIER's name, address and VAT number (and the supplier's invoice number and date, contents.invoice_number and contents.issue_date) and our name as the customer (contents.customer_name; v6: the request carries all of them, purchasing PUR-28): supplier_vat_number is unmet (\"the bill shows no VAT number for Acme Supplies Ltd: input VAT cannot be reclaimed on it\"); with direction sale the gates still check OUR name and VAT number as v2"
owner: tax
reference: not built
- id: TAX-25
kind: integration
actor: system
trigger: when a purchase from an overseas supplier of services is computed (v3)
story: A reverse-charged purchase is self-accounted both sides, so that the return shows the output and the input VAT and the business pays nothing net where it may reclaim.
given: "an overseas supplier (treatment overseas_business) billing consultancy services of 500.00"
when: "it is computed with direction: purchase"
then: "the line takes the purchase reverse-charge code: charged 0.00, VAT due 100.00 accounted as output (its output reporting box) and 100.00 as input VAT reclaimable (its input box), so the net cost of VAT is 0.00; non-reclaimable where the code's reclaimable_pct is 0"
owner: tax
reference: not built
- id: TAX-20
kind: integration
actor: system
trigger: when a quote, order, invoice or credit note is priced
story: Every selling document asks this module for its tax rather than computing its own.
given: a sales order with three lines
when: sales-order calls compute(lines, bp_id, tax_point, currency)
then: it receives per line code, rate and VAT, the per-rate summary and the total, with the method that rounded them; it stores the answer as its own copy
owner: tax
reference: not built
- id: TAX-21
kind: integration
actor: system
trigger: when an invoice or credit note posts to the ledger
story: The ledger finds the VAT account for each code from its account map, so that the VAT liability per code is in the books.
given: an invoice with S 200.00 and RC 0.00 (VAT due 100.00 by the customer)
when: invoicing posts
then: output VAT 200.00 credits the S account; RC posts no VAT and is recorded for the return with its net; journal ids derived as the ledger requires
owner: gl
reference: not built
- id: TAX-22
kind: integration
actor: system
trigger: when an invoice is in a foreign currency
story: The VAT total is also shown in sterling at the rate the business uses, as the VAT guide requires.
given: an invoice in EUR with VAT 200.00 EUR and the ledger's rate for the tax point
when: tax is computed
then: the VAT total in GBP is computed at that rate and frozen with the document (para 16.4); without the ledger, the rate is entered on the document
owner: gl
reference: not built
# ── v4: Making Tax Digital VAT return ─────────────────────────────────────────────────────
- id: TAX-26
kind: person
actor: owner
story: As the owner, I want to connect our VAT registration to HMRC once, so that obligations are read and returns are filed from here without bridging software.
given: registration GB123456789 recorded and no HMRC connection
when: I choose Connect to HMRC and sign in to HMRC's Government Gateway as our business, granting read:vat and write:vat
then: 'the connection is recorded (vrn, scopes, authorised_by, authorised_at, expires_at: HMRC''s grant lasts 18 months); the tokens are kept in the platform''s secret store, never in a row or a log; until it exists every return transition answers {gate: hmrc_connected, met: false, reason: "this business is not connected to HMRC", fix: "Tax settings > HMRC connection (the owner)"}; 30 days before expiry the owner is told to reconnect; hmrc_environment (sandbox | production, an owner''s control) decides which HMRC base URL is called'
screen: Tax settings
path: { process: tax.vat_return, choices: {} }
reference: not built
- id: TAX-27
kind: system
actor: system
trigger: daily, and when the VAT returns screen opens
story: The system reads our VAT obligations from HMRC, so that the periods to file and their deadlines are HMRC's, never typed.
given: a connection for VRN 123456789, quarterly stagger ending March, June, September, December
when: GET /organisations/vat/123456789/obligations?from=2026-01-01&to=2027-12-31 runs, twice
then: 'one vat_obligation row per periodKey (e.g. "26AD": 2026-10-01 to 2026-12-31, due 2027-02-07, status O); a period HMRC reports as F (filed, even if filed elsewhere) is marked filed with its received date; the second run changes nothing; dates are HMRC''s, never computed here; an HMRC error is recorded with its code and retried next day'
reference: not built
- id: TAX-28
kind: person
actor: tax_admin
story: As the tax admin, I want the VAT return for an open period prepared from what we invoiced and were billed, so that every box is a sum of frozen document tax I can drill into.
given: 'obligation 26AD (2026-10-01 to 2026-12-31); frozen sale documents with output VAT 41,200.00 on net 206,000.00 (of which RC construction sales net 12,000.00 with no VAT); purchase documents with reclaimable input VAT 9,850.00 on net 49,250.00, non-reclaimable 140.00; one supplier bill dated 2026-09-28 received late and not in any return; NI dispatches to the EU net 3,000.00'
when: I prepare the return
then: 'box 1 = 41,200.00 output VAT plus reverse-charge and postponed-import output VAT; box 2 = VAT on NI acquisitions of goods from the EU; box 3 = 1 + 2; box 4 = reclaimable input VAT (incl. reverse-charge and postponed-import input, bad debt relief claimed); box 5 = |3 - 4|; boxes 6-9 the net values in whole pounds (pence dropped, HMRC''s rule): 6 sales incl. RC net and zero-rated, 7 purchases, 8 NI goods to the EU, 9 NI goods from the EU; each document''s tax enters ONE return only (a vat_return_entry row keyed [document_ref, tax_code], claimed with a plain INSERT, so two concurrent preparations never count it twice); the late bill dated 2026-09-28 enters this return (input VAT is claimed in the period the VAT invoice is held); every box drills to its entries; preparing again replaces only this draft''s entries; the return is draft until locked'
screen: VAT returns
reference: not built
- id: TAX-29
kind: person
actor: tax_admin
story: As the tax admin, I want to correct an error from an earlier period on this return when HMRC allows it, and be stopped when it must be notified instead, so that corrections follow VAT Notice 700/45.
given: 'a draft return with box 6 of 400,000; an under-declared output VAT of 3,200.00 found for period 26AB'
when: I record the adjustment with the reason and the period it corrects, and then try one of 12,000.00
then: 'the 3,200.00 adds to box 1 with its reason and period (net errors within 10,000.00, or within 50,000.00 and 1% of box 6, are corrected on the next return; the thresholds are dated tax_statutory rows); the 12,000.00 exceeds both (1% of 400,000 is 4,000) and is refused with {gate: within_error_threshold, met: false, reason: "net errors of 12,000.00 exceed what may be corrected on a return (Notice 700/45)", fix: "notify HMRC on form VAT652 and record the notice"}; a notice records the reference, the date sent and the periods; an error older than four years is refused (the four-year cap)'
screen: VAT returns
reference: not built
- id: TAX-30
kind: person
actor: owner
story: As the person who signs off VAT, I want to submit the locked return to HMRC with the legal declaration, so that it is filed digitally and the receipt is kept.
given: 'return 26AD locked by the tax admin; vat_return_needs_second_person true; the HMRC connection valid'
when: 'I read the boxes, tick "When you submit this VAT information you are making a legal declaration that the information is true and complete. A false declaration can result in prosecution." and submit'
then: 'POST /organisations/vat/123456789/returns with {periodKey, vatDueSales, vatDueAcquisitions, totalVatDue, vatReclaimedCurrPeriod, netVatDue, totalValueSalesExVAT, totalValuePurchasesExVAT, totalValueGoodsSuppliedExVAT, totalAcquisitionsExVAT, finalised: true} and the fraud-prevention headers (TAX-31); HMRC''s receipt (processingDate, formBundleNumber, paymentIndicator, chargeRefNumber) is stored and the return is submitted and immutable; the person who locked it may not submit it while the control is on (the same identity by email OR sub is refused); submitting a return whose obligation HMRC already shows F is refused (HMRC''s DUPLICATE_SUBMISSION is recorded, never retried); a timeout leaves it submitting, and a re-read of GET /returns/{periodKey} settles whether HMRC has it before any retry'
screen: VAT returns
reference: not built
- id: TAX-31
kind: system
actor: system
trigger: on every call to an HMRC API made for a person (submit, view a return, liabilities, payments)
story: The system sends HMRC's fraud-prevention headers from the person's own session, so that the submission meets HMRC's terms of use.
given: a submission from a browser session of the owner
when: the call is built
then: 'connection method WEB_APP_VIA_SERVER with Gov-Client-Connection-Method, Gov-Client-Browser-JS-User-Agent, Gov-Client-Device-ID, Gov-Client-Multi-Factor, Gov-Client-Public-IP, Gov-Client-Public-IP-Timestamp, Gov-Client-Public-Port, Gov-Client-Screens, Gov-Client-Timezone, Gov-Client-User-IDs, Gov-Client-Window-Size, Gov-Vendor-Forwarded, Gov-Vendor-License-IDs, Gov-Vendor-Product-Name, Gov-Vendor-Public-IP, Gov-Vendor-Version; a header the session cannot supply refuses the submission with {gate: fraud_headers_complete, met: false, reason: "HMRC requires <header>", fix: "submit from the web screen, not a script"}; the headers sent are kept with the submission for audit; the set is validated against HMRC''s Test Fraud Prevention Headers API in sandbox before production is switched on'
reference: not built
- id: TAX-32
kind: person
actor: tax_admin
story: As the tax admin, I want to see what we owe HMRC per period and what HMRC says we have paid, and the books to agree, so that a VAT payment is never missed or posted twice.
given: return 26AD submitted with box 5 = 31,350.00 to pay, due 2027-02-07
when: I open the liability view after HMRC has taken the direct debit
then: 'per period: the return''s net VAT, HMRC''s liabilities (GET /liabilities) and payments (GET /payments) as HMRC holds them, the outstanding amount and the due date (one month and seven days after the period end); on submission the settle journal posts to the ledger (each tax code''s VAT against vat_control, journal id derived from the return), once; the HMRC payment itself is the bank line payments reconciles to vat_control (PAY-21); a difference between our box 5 and HMRC''s liability is shown, never hidden'
screen: VAT returns
reference: not built
- id: TAX-33
kind: integration
actor: system
trigger: when ar issues or voids a document, or approves a credit note (the gates tax_point_open and void_policy)
story: Invoicing asks this module whether a tax point falls in a VAT return already locked or filed, so that an issued invoice in a filed period is never voided or back-dated into it.
given: return 26AC (2026-07-01 to 2026-09-30) submitted, 26AD draft
when: ar calls GET /vat-returns/tax-point-status?date=2026-09-15, then ?date=2026-11-20
then: '{date: 2026-09-15, period_key: 26AC, status: submitted, filed: true, gate: {gate: tax_point_open, met: false, reason: "2026-09-15 is in VAT return 26AC, submitted 2026-11-04", fix: "credit it instead, or issue with today''s tax point"}}; for 2026-11-20 {period_key: 26AD, status: draft, filed: false, gate: {met: true}}; a locked (not yet submitted) return answers filed false with met false (reason "return 26AD is locked for submission"); a date in no obligation answers period_key null, met true (the answer is the L2C leftover''s missing callee)'
owner: tax
reference: not built
# ── v4: reliefs and schemes ───────────────────────────────────────────────────────────────
- id: TAX-34
kind: person
actor: tax_admin
story: As the tax admin, I want to claim back the VAT on invoices a customer never paid, once the law allows, so that we do not pay HMRC VAT we never received.
given: 'invoice INV-2026-0107 (net 1,000.00, VAT 200.00, total 1,200.00), supplied 2026-03-10, due 2026-04-09, VAT accounted in submitted return 26AA; 480.00 received; written off by invoicing on 2026-10-30 (AR-09); today 2026-11-02'
when: the system lists it as eligible and I claim it on return 26AD
then: 'relief = the VAT fraction of the unpaid consideration: (1,200.00 - 480.00) x 200/1,200 = 120.00, added to box 4 with the invoice named (bad_debt_claim keyed invoice_id, one claim per invoice); eligibility is each gate {gate, met, reason, fix}: six_months_passed (six months from the later of the due date and the supply date: 2026-10-09 here), written_off (an approved ar write-off or the receivable written off in the books), vat_accounted (the invoice''s tax is in a submitted return), within_time_limit (four years and six months from that date), not_claimed; a claim for an invoice still inside six months is refused with its date'
screen: VAT returns
reference: not built
- id: TAX-35
kind: system
actor: system
trigger: when a return is prepared
story: The system repays bad debt relief when the customer pays after all, so that the relief and the receipt never both stand.
given: the claim of 120.00 on INV-2026-0107 in return 26AD; a receipt of 360.00 allocated to it on 2027-02-20 (payments)
when: return 27AA is prepared
then: 'the repayment 360.00 x 200/1,200 = 60.00 adds to box 1, naming the claim and the allocation; it is derived from payments'' allocations dated after the claim (GET /allocations?invoice_ids=&from=), never typed; the same allocation is repaid once (an entry keyed [claim, allocation]); an undo of that allocation (PAY-06) reverses the repayment on the next return'
reference: not built
- id: TAX-36
kind: person
actor: tax_admin
story: As the tax admin of a small business on cash accounting, I want the return to tax what we were paid and what we paid, so that we never pay VAT before the customer pays us.
given: 'registration with accounting_basis cash from 2027-01-01 (expected taxable turnover 900,000, within the 1,350,000 joining limit); invoice INV-2027-0012 (VAT 200.00) issued 2027-01-15 and paid 2027-04-03; a supplier bill (input VAT 50.00) paid 2027-03-20'
when: return 27AA (Jan-Mar) and 27AB (Apr-Jun) are prepared
then: 'the invoice''s VAT is in 27AB (the receipt date is the tax point for the return), the bill''s input VAT in 27AA (the payment date); a part payment taxes the VAT fraction of what was received; read from payments'' cash-basis route (receipts allocated in the period, by invoice) and purchasing''s supplier payments; documents excluded from the scheme (an invoice payable more than six months ahead, an import) stay on the invoice basis; leaving the scheme (turnover above 1,600,000) brings every outstanding VAT into the return for the leaving period; the basis changes only from the start of a VAT period after today'
screen: VAT returns
reference: not built
- id: TAX-37
kind: person
actor: tax_admin
story: As the tax admin of a business on the flat rate scheme, I want the VAT due computed as our flat rate of gross turnover, so that I file the right figure without a side spreadsheet.
given: 'flat_rate from 2027-01-01 at 14.5% (sector "computer and IT consultancy"), first year of VAT registration (1% discount until 2027-12-31); quarter turnover including VAT 60,000.00; goods costs 300.00 (below 2% of turnover, so the limited cost trader test is applied); a laptop bought for 2,400.00 including VAT'
when: return 27AA is prepared
then: 'the limited cost trader test is a gate answered first: goods 300.00 are below 2% of 60,000.00 (1,200.00), so the limited cost trader rate 16.5% replaces the sector''s 14.5%, less the first-year 1%: box 1 = 60,000.00 x 15.5% = 9,300.00; box 6 = the flat-rate turnover including VAT; box 4 holds only capital goods costing 2,000.00 or more including VAT (the laptop''s 400.00); the sector, its rate and the thresholds are dated rows (tax_statutory, registration); joining needs expected taxable turnover at most 150,000 excluding VAT; turnover above 230,000 including VAT in a year answers {gate: flat_rate_eligible, met: false} on the next return'
screen: VAT returns
reference: not built
- id: TAX-48
kind: person
actor: tax_admin
story: As the tax admin of a partly exempt business, I want residual input VAT recovered at our provisional percentage each return and adjusted at the year end, so that we reclaim exactly what VAT Notice 706 allows.
given: 'purchase input VAT attributed to taxable supplies 6,000.00, to exempt supplies 900.00, residual 3,000.00; last year''s recovery 71%; partial exemption year ending 31 March'
when: each return is prepared, and the annual adjustment is run after the year ends
then: 'each return reclaims 6,000.00 + 71% of 3,000.00 = 8,130.00; the 900.00 exempt input VAT is reclaimed in full while the de minimis test holds (exempt input tax at most 625.00 a month on average and at most half of all input tax: test 1/2/3 of Notice 706 answered as gates); the annual adjustment recomputes the year''s percentage (taxable supplies / all supplies, rounded UP to a whole percent while residual input tax averages at most 400,000 a month) and puts the difference on the first return after the year end; attribution (taxable | exempt | residual) is a property of the purchase tax code, so purchasing''s lines carry it without a new field'
screen: VAT returns
reference: not built
- id: TAX-49
kind: person
actor: tax_admin
story: As the tax admin of a partly exempt business, I want input VAT on a building or a large computer system adjusted each year as our taxable use changes, so that the Capital Goods Scheme is applied without a spreadsheet.
given: 'a building bought 2026-06-01 with input VAT 100,000.00 (an item of 250,000.00 or more: ten intervals), first-year recovery 80%; year-two recovery 70%'
when: the annual adjustment for year two is run
then: 'the item is recorded once (capital_goods_item keyed item_ref) with its VAT, intervals (10 for land and buildings, 5 for computers of 50,000.00 or more, both tax_statutory rows) and first-interval percentage; year two adjusts (100,000.00 / 10) x (70% - 80%) = -1,000.00 onto the first return after the year end, as an adjustment entry naming the item and interval; each interval is adjusted once; a sale of the item in its adjustment period answers a gate asking for the disposal adjustment'
screen: VAT returns
reference: not built
# ── v4: Construction Industry Scheme ──────────────────────────────────────────────────────
- id: TAX-38
kind: person
actor: tax_admin
story: As the contractor's tax admin, I want each subcontractor verified with HMRC before we pay them, so that the deduction rate is HMRC's, not a guess.
given: 'a new subcontractor bp_id 8801, Bright Sparks Ltd, UTR 1234567890, company number 01234567, not paid in this or the last two tax years'
when: I verify them
then: 'the verification (HMRC CIS online verification through the HMRC connection, or the result a person read from HMRC''s CIS service, entered with the verification number) stores status gross | net | unmatched, the rate 0 | 20 | 30 (dated tax_statutory rows), the verification number (V1234567890 and a suffix for unmatched) and the date; a payment to a subcontractor with no verification answers {gate: cis_verified, met: false, reason: "Bright Sparks Ltd has not been verified with HMRC", fix: "CIS > Subcontractors > Verify"}; one already paid in this or the previous two tax years needs none; a re-verification is a new dated row, never an edit'
screen: CIS
path: { process: tax.cis_return, choices: {} }
reference: not built
- id: TAX-39
kind: integration
actor: system
trigger: when purchasing approves a payment to a subcontractor (a run or a manual payment)
story: Purchasing asks this module for the CIS deduction on a subcontractor's payment, so that the deduction is HMRC's rate on the labour only.
given: 'subcontractor 8801 verified net (20%); a bill of labour 4,000.00, materials 1,000.00, VAT reverse-charged (0.00 charged)'
when: purchasing calls POST /cis/deductions with the payment
then: 'deduction = 20% x (4,000.00 + 1,000.00 - 1,000.00 materials) = 800.00; purchasing pays 4,200.00 and books the 800.00 to the CIS liability; the deduction row is keyed [payment_ref, bp_id] in the tax month (6th to 5th) of the payment date, so a retry adds nothing; materials above the bill''s own figure are refused; an unmatched subcontractor is deducted at 30%; a gross one at 0% with the row still recorded for the return'
owner: tax
reference: not built
- id: TAX-40
kind: person
actor: tax_admin
story: As the contractor's tax admin, I want the monthly CIS return prepared from the deductions and the statements sent to each subcontractor, so that the CIS300 is filed by the 19th.
given: 'tax month 6 October to 5 November 2026 with three payments to two subcontractors'
when: I prepare the return, a person confirms the declarations (employment status, verification) and submits it
then: 'a CIS300 per tax month with each subcontractor''s name, UTR, verification number, gross paid (excl. VAT), materials and deduction; a nil month is a nil return unless a person records inactivity; submission goes through HMRC''s CIS online filing over the HMRC connection, the submission receipt kept, by the 19th (overdue answers a gate with the penalty warning); a payment and deduction statement per subcontractor is produced and sent (due by the 19th); the deductions total is what the business pays HMRC by the 22nd with its PAYE, shown in the liability view'
screen: CIS
reference: not built
- id: TAX-41
kind: integration
actor: system
trigger: when payments records a receipt from a contractor customer that deducted CIS (PAY-45)
story: Payments tells this module what a contractor deducted from our invoice under CIS, so that the deduction suffered is claimed back and never lost.
given: 'our invoice of labour 2,000.00 to contractor bp_id 7311 (reverse charge, no VAT charged); they pay 1,600.00 and send a payment and deduction statement showing 400.00 deducted for tax month ending 2026-12-05'
when: payments calls POST /cis/suffered
then: 'a cis_suffered row keyed [contractor bp_id, statement_ref] with the tax month, gross, materials and deduction; the CIS view sums what was suffered per tax month and year, for the PAYE offset (a limited company''s employer payment summary) or self assessment; a deduction above 30% of the labour named is refused; the same statement recorded twice adds nothing'
owner: tax
reference: not built
# ── v4: Northern Ireland goods, imports, construction reverse charge ─────────────────────
- id: TAX-42
kind: person
actor: tax_admin
story: As the tax admin of a business moving goods from Northern Ireland to EU customers, I want the EC sales list prepared from our invoices, so that it matches box 8.
given: 'registration with ni_goods true (XI prefix); in November, invoices of goods to two EU VAT-registered customers: IE1234567T net 12,000.00 and FR12345678901 net 3,000.00, one credit note of 500.00'
when: I prepare the November EC sales list and record it submitted
then: 'one line per customer VAT number with the net value in whole pounds (IE 12,000; FR 2,500 after the credit) and indicator 0 (goods) or 2 (triangulation); the total ties to box 8 of the return for the months it covers (a difference is a gate, never silent); monthly, or quarterly while goods stay below 35,000 a quarter for the current and four previous quarters (dated tax_statutory row); the file is HMRC''s bulk-upload CSV for the person to upload, and the person records the submission reference and date (HMRC offers no API for it); an amendment is a new version of the declaration'
screen: Trade declarations
reference: not built
- id: TAX-43
kind: person
actor: tax_admin
story: As the tax admin of a Northern Ireland business above the Intrastat threshold, I want the monthly supplementary declaration built from our goods movements with the EU, so that it is filed without retyping.
given: 'NI dispatches to the EU above 250,000 a year (dispatches threshold) and arrivals below 500,000 (arrivals threshold), both dated tax_statutory rows'
when: I prepare November's dispatches declaration
then: 'one line per (commodity code, partner country, nature of transaction, country of origin) with value in whole pounds, net mass in kg and supplementary units where the commodity needs them, partner VAT number; read from ar''s issued invoice lines to EU customers with goods (net value, unit_code) and product''s commodity code and net weight; a line whose product has no commodity code is a gate {gate: commodity_code, met: false, fix: "Product > the product > Trade"}; arrivals are only declared once the arrivals threshold is passed; the file is HMRC''s upload format and the person records the submission'
screen: Trade declarations
reference: not built
- id: TAX-44
kind: integration
actor: system
trigger: when purchasing records an import (a customs entry on a supplier bill or a freight agent's invoice)
story: Purchasing records each import entry here with how its import VAT was accounted, so that postponed import VAT is self-accounted on the return and import VAT paid is reclaimed only with evidence.
given: 'an import from China, customs value 10,000.00, MRN 26GB12345678901234, postponed VAT accounting chosen; a second import where the freight agent paid 2,000.00 import VAT'
when: purchasing calls POST /imports
then: 'the first is an import_entry with code PVA (rate 20%, charged 0): output 2,000.00 in box 1, input 2,000.00 in box 4 (by the code''s reclaimable_pct), value 10,000 in box 7; the second carries code IMPORT_PAID with its 2,000.00 reclaimable only once the monthly import VAT certificate (C79, or its CDS successor) lists the entry; an entry keyed by MRN is recorded once'
owner: tax
reference: not built
- id: TAX-45
kind: person
actor: tax_admin
story: As the tax admin, I want each month's postponed import VAT statement reconciled to our import entries, so that box 1 and box 4 carry exactly HMRC's figures.
given: 'the November statement downloaded from the Customs Declaration Service with three entries (MRN, import VAT); our import entries hold two of them, one with a different amount'
when: I upload the statement
then: 'each statement line is matched by MRN: one matches, one differs (the difference shown with both amounts), one has no import entry; the statement is reconciled only when every line is matched or explained by a person with a reason; the return uses HMRC''s figure for a line explained as HMRC''s (an adjustment row naming the MRN); the statement file is kept as an object, read-only'
screen: Import VAT
reference: not built
- id: TAX-46
kind: person
actor: tax_admin
story: As the tax admin of a construction business, I want the reverse charge applied only where the customer is VAT- and CIS-registered and not an end user, and self-accounted when we buy such services, so that we never charge or omit VAT wrongly.
given: 'customer 7400 (treatment uk_cis_contractor) gives written notice it is an end user; supplier 8801 (VAT- and CIS-registered) bills us specified construction services of 4,000.00'
when: I record the end-user notice on 7400 from tomorrow, and the supplier bill is computed with direction purchase
then: 'from that date 7400''s construction lines take S at 20% (an end user or intermediary is outside the reverse charge), the notice kept with its date and reference; the purchase is self-accounted with code RC_PURCHASE: VAT 800.00 output (box 1) and input (box 4), net in box 7 and never in box 6; a supply whose reverse-charge services are 5% or less of its value may be taxed normally (the 5% disregard, a gate on the rule the tax admin confirms)'
screen: Customer tax
reference: not built
- id: TAX-47
kind: integration
actor: system
trigger: when ar or sales-order computes a line that reverses or deducts an earlier document (a deposit deduction, a credit-note correction, a settlement discount)
story: A line pinned to an earlier frozen document takes that document's code and rate, so that a deduction or a correction carries the tax it reverses and tax accepts it.
given: 'deposit invoice DEP-2026-0003 frozen with S 20% on 5,000.00; a final invoice deducting 2,000.00 of it on 2027-04-15, after S became 21%'
when: ar calls POST /documents/compute with a line {line_no 4, amount "-2000.00", pinned_from "<deposit invoice_id>", tax_code "S"}
then: 'the line takes S at the deposit''s frozen 20% (VAT -400.00), named "pinned to <invoice_id>"; it is never re-determined from category and treatment, so a deduction carries the deposit''s category without the source sending one; a pinned code the earlier document does not carry is refused (gate pinned_code); the pinned amount may not exceed the earlier document''s net for that code less what earlier pinned lines on issued documents already took (gate pinned_within_original, read from tax''s own frozen rows)'
owner: tax
reference: not built
# ── v6 (2026-10-25, wave 3): tills, receipts and expense claims ─────────────────────────
- id: TAX-50
kind: integration
actor: system
trigger: when expenses checks a till receipt (EXP-18) or purchasing a small supplier invoice, before VAT is reclaimed on it
story: A less detailed VAT invoice is accepted for what it is, so that VAT on a 9.60 GBP train-station coffee receipt is reclaimed without our company's name on it, and a receipt over the limit still needs a full invoice.
given: 'a till receipt from a UK retailer for 9.60 GBP including VAT 1.60 at 20%, showing the retailer''s name, address and VAT number, its receipt number, the date and a description; the setting simplified_invoice_threshold 250'
when: 'expenses calls POST /documents/exp:UK01-EXP-2026-00042:3/check-contents with direction purchase and no customer_name, then one with a 312.00 GBP receipt in the same form'
then: >-
the first is a less detailed invoice (VAT Notice 700 para 16.6.1; VAT Regulations 1995 reg. 16): the gates for
the supplier's name, address and VAT number, the invoice number, the tax point, a description, the total
including VAT and the rate per line are checked, customer_name and customer_address are not required, and
ready is true with title less_detailed_invoice; the second (gross above 250.00) needs our name as the customer
(gate customer_name unmet: "a VAT invoice over 250.00 must show your company's name: ask the supplier for a full
invoice"); a sale-direction document is unchanged (its own simplified invoice rule); the threshold is in GBP and
compared with the gross including VAT, in the document's currency converted at gl's rate on the tax point
owner: tax
reference: not built
- id: TAX-51
kind: integration
actor: system
trigger: when a till or an order computes food and drink that is taxed by where it is consumed
story: A line says whether it is eaten in or taken away, so that the rate is the one the law sets for that consumption and not a guess per product.
given: 'rules for DE: category food_hot with consumption eat_in 19% (DE-S) and takeaway 7% (DE-R) from 2026-01-01; category coffee with consumption any 19%; UK: hot food standard either way'
when: 'pos computes receipt pos:R-7001 in Berlin with a currywurst eat_in, a currywurst takeaway and a coffee, sending each line''s consumption'
then: >-
the eat-in line takes DE-S at 19%, the takeaway line DE-R at 7% and the coffee DE-S (a rule of consumption any
covers both); a line of a category whose rules distinguish consumption and that sends none is refused (gate
consumption_given: "say whether it is eaten in or taken away"), never defaulted; a line of a category with only
any rules may omit it; consumption is part of the rule's key and of the frozen line
owner: tax
reference: not built
- id: TAX-52
kind: integration
actor: system
trigger: when a register takes its offline snapshot (pos POS-30)
story: A register reads the rates in force for the categories it sells, so that it can sell with the right VAT while offline and know when its snapshot goes stale.
given: 'UK categories standard, zero, food_hot, food_cold; a rate change for food_hot scheduled for 2027-04-01'
when: 'pos calls GET /rates?country=GB&categories=standard,zero,food_hot,food_cold&on=2026-11-20'
then: >-
one row per (category, consumption) with the tax_code, rate_pct and the rule's and the rate's validity in force
on 2026-11-20 for the treatment of an anonymous buyer (walk_in_treatment_code, default uk_consumer); valid_until
2027-03-31 (the day before the next change of any answered row, or null), so the register refreshes before it; a
category with no rule for the treatment is answered in missing and the register must not sell it offline; the
receipt is still recomputed by POST /documents/compute at sync and the computed answer is the one kept
owner: tax
reference: not built
- id: TAX-53
kind: integration
actor: system
trigger: when the party on a document is not a business partner (a merchant on an expense receipt, an anonymous till sale)
story: A document can be computed without a business partner when its treatment is named, so that a receipt from a merchant we will never buy from again is taxed by the same rules without creating a supplier.
given: 'an expense line: a hotel receipt from a UK VAT-registered merchant, treatment_code uk_business, no bp_id; a till sale with treatment_code uk_consumer, no bp_id'
when: 'expenses and pos call POST /documents/compute without bp_id'
then: >-
the rules are read for the named treatment exactly as for a party's (determine); the document records party
null and the treatment used; without bp_id AND without treatment_code the call is refused (gate party_or_treatment,
422); with both, bp_id's treatment row wins and a different treatment_code is refused (gate treatment_matches_party)
owner: tax
reference: not built
processes:
- key: tax_code
pcf_id: 10929
pcf_name: Maintain tax master data
reference_names: ["Tax master data", "Taxes and fiscal positions"]
brick: tax
- key: document_tax
pcf_id: 10766
pcf_name: Process taxes
reference_names: ["Tax determination"]
brick: tax
- key: registration
pcf_id: 10929
pcf_name: Maintain tax master data
reference_names: ["VAT registration"]
brick: tax
- key: vat_return
pcf_id: 10766
pcf_name: Process taxes
reference_names: ["Advance return for tax on sales/purchases", "Making Tax Digital VAT return"]
brick: tax
- key: cis_return
pcf_id: 10766
pcf_name: Process taxes
reference_names: ["Construction Industry Scheme", "Withholding tax reporting"]
brick: tax
- key: trade_declaration
pcf_id: 10766
pcf_name: Process taxes
reference_names: ["EC sales list", "Intrastat"]
brick: tax
- key: import_vat
pcf_id: 10934
pcf_name: Account for taxes
reference_names: ["Postponed import VAT", "Import VAT"]
brick: tax
workflows:
- entity: registration
states: [not_registered, registered, deregistered]
terminal: [deregistered]
transitions:
- { key: record_not_registered, name: Record that we are not registered, from: none, to: not_registered, by: tax_admin, gate: "the first registration row (it may carry any date: nothing has used it)", effect: "documents charge no VAT and are not titled VAT invoice" }
- { key: register, name: Record our VAT registration, from: not_registered, to: registered, by: tax_admin, requires: vat_number, gate: "a VAT number in the GB format; effective_from after today; a second person's approval where tax_changes_need_second_person", effect: "tax points from that date charge VAT" }
- { key: record_registered, name: Record an existing registration, from: none, to: registered, by: tax_admin, requires: vat_number, gate: "the first registration row (a new installation or the migration): it carries the date HMRC registered us" }
- { key: change_vat_scheme, name: "Change the accounting basis or the flat rate scheme (v4)", from: registered, to: registered, by: tax_admin, requires: scheme, gate: "from the first day of a VAT period after today; eligibility from the dated tax_statutory limits (cash accounting: expected taxable turnover at most the joining limit; flat rate: at most its joining limit, a sector and its rate); a second person's approval where tax_changes_need_second_person", effect: "a new registration row: accounting_basis invoice | cash, flat_rate with sector, rate and first_year_until; returns of later periods compute under it (TAX-36, TAX-37)" }
- { key: deregister, name: Record deregistration, from: registered, to: deregistered, by: tax_admin, requires: effective_date, approval: true, gate: "effective_date after today; the approver is not the author", effect: "tax points from that date charge no VAT" }
- entity: tax_code
states: [draft, active, retired]
terminal: [retired]
transitions:
- { key: load_pack, name: Load the country's codes, from: none, to: draft, by: system, gate: "a new installation with no codes, and no migration planned", effect: "the UK pack: S, R, Z, E, OS, RC, NV, with their rates from their real dates" }
- { key: add_code, name: Add a tax code, from: none, to: draft, by: tax_admin, gate: "the code is not in use" }
- { key: activate_code, name: Activate the code, from: draft, to: active, by: tax_admin, gate: "a treatment, a first rate (any date: no document has used the code), and the document wording where the treatment needs one (reverse charge)" }
- { key: schedule_rate, name: Schedule a rate change, from: active, to: active, by: tax_admin, gate: "valid_from after today; no other rate for the code on that date; a second person's approval where tax_changes_need_second_person", effect: "a dated rate row; issued documents unchanged" }
- { key: withdraw_rate, name: Withdraw a rate not yet in effect, from: active, to: active, by: tax_admin, gate: "its valid_from is after today", effect: "the row is marked withdrawn, with who and when; it never takes effect" }
- { key: retire_code, name: Retire the code, from: active, to: retired, by: tax_admin, gate: "no rule in effect today or later points at it", effect: "no new determinations; documents keep it" }
note: "a code's treatment and reporting_box are fixed once a document has used it (a different meaning is a new code); its description and wording may change, because each issued document keeps the wording it froze"
- entity: document_tax
states: [computed, frozen]
terminal: [frozen]
transitions:
- { key: determine, name: Determine each line's code, from: none, to: computed, by: system, gate: "a rule of the document's direction (sale | purchase) valid on the tax point covers every (category, treatment) pair, except a line pinned to an earlier frozen document (v4, TAX-47), which takes that document's code and rate; the party has a treatment: the customer's for a sale, the SUPPLIER's for a purchase (v6, TAX-53: or, with no bp_id, the treatment_code the document names; neither refuses with gate party_or_treatment); v6, TAX-51: a line's consumption (eat_in | takeaway) selects among rules that distinguish it", effect: "code, rate, VAT per line; the per-rate summary; the total; for a purchase, VAT split into reclaimable and non-reclaimable by each code's reclaimable_pct" }
- { key: ai_suggest_code, name: "AI suggests a code for a line no rule covers (suggest)", from: computed, to: computed, by: system, gate: "a line has no rule, or no product, and the step is on", effect: "a proposed code with its reason; nothing is applied" }
- { key: accept_tax_suggestion, name: Accept or reject the suggestion, from: computed, to: computed, by: biller, gate: "a suggestion is open on the line", effect: "accepted: recorded as an override with the suggestion, the model and who accepted; rejected: the line stays refused until a person chooses" }
- { key: override_line, name: Override a line's code, from: computed, to: computed, by: biller, requires: reason, gate: "the biller disagrees with the rule's code" }
- { key: approve_override, name: Approve an override that lowers tax, from: computed, to: computed, by: tax_admin, approval: true, gate: "an override lowers the tax; the approver is not the person who overrode, unless override_approval is self_with_reason" }
- { key: check_contents, name: Check the invoice contents, from: computed, to: computed, by: system, gate: "the document is about to issue (a sale) or be approved (a purchase)", effect: "every para 16.3.1 gate as {gate, met, reason, fix}; for a purchase the supplier is the SUPPLIER (its name, address and VAT number on the bill) and we are the customer; v6, TAX-50: a purchase at or under simplified_invoice_threshold (gross including VAT) is a less detailed invoice (VAT Notice 700 para 16.6.1, VAT Regulations 1995 reg. 16): our name and address are not required and the title says so" }
- { key: freeze, name: Freeze the tax at issue, from: computed, to: frozen, by: system, gate: "every readiness gate met: contents complete, no open suggestion, no override waiting for approval", effect: "never recomputed" }
note: "the document (an order, an invoice, a credit note) owns its own copy of the result; this lifecycle is the tax on it"
- entity: party_tax
states: [recorded]
terminal: []
transitions:
- { key: set_party_tax, name: "Record a customer's or supplier's treatment and VAT number", from: none, to: recorded, by: biller, gate: "a treatment that needs a VAT number has one; the first row of a party holds from the start", effect: "the party's first tax row (TAX-05)" }
- { key: change_party_tax, name: "Change a party's treatment, number or end-user notice (v4)", from: recorded, to: recorded, by: biller, gate: "valid_from after today; a construction end-user or intermediary notice carries its reference and date", effect: "a new dated row; documents not yet issued with a tax point from then use it (TAX-05, TAX-46)" }
- { key: check_party_vat_number, name: "Check the VAT number with HMRC (v4)", from: recorded, to: recorded, by: system, gate: "a treatment that needs a VAT number was saved, or its last check is older than vat_number_check_days before a reverse-charge document issues", effect: "check_result valid | invalid | could_not_check, HMRC's name and address and consultation number stored on the row (TAX-13)" }
- { key: confirm_party_vat_number, name: "Confirm a number HMRC could not check (v4)", from: recorded, to: recorded, by: tax_admin, requires: reason, gate: "the last check is could_not_check (a non-GB number, or HMRC unavailable)", effect: "recorded as confirmed by a person, with the reason" }
note: "v4: the party's tax rows were a table only; drawing them makes the VAT-number check and the end-user notice visible in the signed process"
- entity: vat_return
states: [open, draft, locked, submitted]
terminal: [submitted]
transitions:
- { key: sync_vat_obligations, name: Read our obligations from HMRC, from: none, to: open, by: system, gate: "an HMRC connection for the VRN", effect: "one vat_obligation per periodKey with HMRC's dates; a period HMRC shows filed is marked so (TAX-27)", timer: "daily" }
- { key: prepare_vat_return, name: Prepare the return, from: open, to: draft, by: tax_admin, gate: "the obligation is open at HMRC", effect: "boxes 1-9 from frozen document tax not yet in a return (claimed per [document_ref, tax_code]), under the period's scheme: invoice or cash basis, flat rate, partial exemption, bad debt claims and repayments, PVA entries, adjustments (TAX-28, TAX-35..TAX-37, TAX-48)" }
- { key: refresh_vat_return, name: Prepare the draft again, from: draft, to: draft, by: tax_admin, effect: "this draft's entries replaced; entries of other returns untouched" }
- { key: record_vat_adjustment, name: Correct an earlier period's error on this return, from: draft, to: draft, by: tax_admin, requires: reason, gate: "net errors within the error-correction threshold and the error within four years", effect: "an adjustment row naming the period it corrects, in box 1 or 4 (TAX-29)" }
- { key: record_vat_error_notice, name: Record an error notified to HMRC (VAT652), from: draft, to: draft, by: tax_admin, requires: reference, gate: "an error above the threshold", effect: "the notice kept with its date, periods and amount; it is not on the return" }
- { key: ai_review_vat_return, name: "AI reviews the draft return (suggest)", from: draft, to: draft, by: system, gate: "the step is on", effect: "findings (a box moving far from the last four periods, a document with an unusual code) listed for the tax admin; nothing changes" }
- { key: lock_vat_return, name: Lock the return for submission, from: draft, to: locked, by: tax_admin, gate: "the period has ended (its end before today in business_timezone); the period's postponed-import statements reconciled; flat-rate eligibility answered; no adjustment above the threshold", effect: "boxes and entries frozen; a new document with a tax point in the period is refused (tax_point_open)" }
- { key: reopen_vat_return, name: Reopen a locked return, from: locked, to: draft, by: tax_admin, requires: reason, gate: "not submitted" }
- { key: submit_vat_return, name: Submit to HMRC with the declaration, from: locked, to: submitted, by: owner, approval: true, requires: declaration, gate: "the HMRC connection is valid; the person ticked the legal declaration; a second person where vat_return_needs_second_person (not the identity that locked it, by email or sub); every fraud-prevention header present; HMRC still shows the obligation open", effect: "HMRC's receipt stored; the return immutable; its tax points filed (TAX-30, TAX-31)" }
- { key: settle_vat_return, name: Post the return's settlement to the ledger, from: submitted, to: submitted, by: system, gate: "the ledger is installed", effect: "each tax code's VAT against vat_control, journal id derived from the return; once (TAX-32)" }
- { key: refresh_vat_liability, name: "Read HMRC's liabilities and payments", from: submitted, to: submitted, by: system, gate: "an HMRC connection", effect: "liability, paid and outstanding per period as HMRC holds them (TAX-32)", timer: "daily" }
note: "a submission that times out is settled by GET /organisations/vat/{vrn}/returns/{periodKey} before any retry, so a return is never filed twice; a correction after submission is an adjustment on a later return or a VAT652 notice, never an edit"
- entity: bad_debt_claim
states: [eligible, claimed, dismissed]
terminal: [claimed, dismissed]
transitions:
- { key: suggest_bad_debt_relief, name: List invoices eligible for relief, from: none, to: eligible, by: system, gate: "an invoice six months past the later of its due and supply dates, written off, its VAT in a submitted return", effect: "one eligible row per invoice", timer: "when a return is prepared" }
- { key: claim_bad_debt_relief, name: Claim the relief on this return, from: eligible, to: claimed, by: tax_admin, gate: "every eligibility gate met; within four years and six months", effect: "the VAT fraction of the unpaid amount in box 4 (TAX-34)" }
- { key: dismiss_bad_debt_relief, name: Do not claim, from: eligible, to: dismissed, by: tax_admin, requires: reason }
- { key: repay_bad_debt_relief, name: Repay relief on a later receipt, from: claimed, to: claimed, by: system, gate: "a receipt allocated to the invoice after the claim", effect: "its VAT fraction in box 1 of the next return, once per allocation (TAX-35)" }
- entity: cis_subcontractor
states: [unverified, verified]
terminal: []
transitions:
- { key: add_cis_subcontractor, name: Add a subcontractor, from: none, to: unverified, by: tax_admin, requires: utr, gate: "a business partner with a UTR (and a company number or NI number)" }
- { key: verify_cis_subcontractor, name: Verify with HMRC, from: unverified, to: verified, by: tax_admin, gate: "an HMRC connection, or a verification number a person read from HMRC", effect: "status gross | net | unmatched, rate and verification number from today (TAX-38)" }
- { key: reverify_cis_subcontractor, name: Verify again, from: verified, to: verified, by: tax_admin, effect: "a new dated row; deductions from then use it" }
- entity: cis_return
states: [open, prepared, submitted]
terminal: [submitted]
transitions:
- { key: compute_cis_deduction, name: "Compute the deduction on a subcontractor's payment", from: open, to: open, by: system, gate: "the subcontractor is verified, or was paid in this or the last two tax years", effect: "a deduction row keyed [payment_ref, bp_id] in the payment's tax month (TAX-39)" }
- { key: reverse_cis_deduction, name: Reverse a deduction when the payment is rejected (v4), from: open, to: open, by: system, gate: "the deduction's tax month is not submitted (cis_return_open); a repeat answers the same", effect: "a reversing deduction row keyed [payment_ref, bp_id]; the month's return and the subcontractor's statement no longer count it" }
- { key: prepare_cis_return, name: Prepare the monthly CIS return, from: open, to: prepared, by: tax_admin, gate: "the tax month (6th to 5th) has ended", effect: "the CIS300 lines per subcontractor, or a nil return" }
- { key: submit_cis_return, name: Submit the CIS return, from: prepared, to: submitted, by: tax_admin, approval: true, requires: declaration, gate: "the employment-status and verification declarations confirmed by a person; an HMRC connection", effect: "HMRC's receipt kept (TAX-40)" }
- { key: send_cis_statements, name: Send each subcontractor its statement, from: submitted, to: submitted, by: tax_admin, effect: "a payment and deduction statement per subcontractor, the send recorded" }
- entity: trade_declaration
states: [draft, filed]
terminal: [filed]
transitions:
- { key: prepare_trade_declaration, name: Prepare the EC sales list or Intrastat declaration, from: none, to: draft, by: tax_admin, gate: "ni_goods registered; for Intrastat, the flow's threshold passed", effect: "lines from ar's (and purchasing's) goods documents with EU partners; the upload file (TAX-42, TAX-43)" }
- { key: record_trade_declaration_filed, name: Record that it was filed with HMRC, from: draft, to: filed, by: tax_admin, requires: submission_ref, gate: "the EC sales list ties to box 8 for its months; every Intrastat line has a commodity code", effect: "filed with the reference and date; an amendment is a new version" }
- entity: pva_statement
states: [imported, reconciled]
terminal: [reconciled]
transitions:
- { key: import_pva_statement, name: "Upload the month's postponed import VAT statement", from: none, to: imported, by: tax_admin, gate: "one statement per month", effect: "statement lines kept, the file kept as an object" }
- { key: match_pva_statement, name: Match statement lines to import entries, from: imported, to: imported, by: system, effect: "each line matched by MRN, with any difference" }
- { key: explain_pva_difference, name: Explain a difference, from: imported, to: imported, by: tax_admin, requires: reason }
- { key: reconcile_pva_statement, name: Reconcile the statement, from: imported, to: reconciled, by: tax_admin, gate: "every line matched or explained", effect: "the return uses the reconciled figures (TAX-45)" }
readiness:
shape: "{gate, met, reason, fix}"
rule: >-
For every transition above with a gate, the brick answers each gate as {gate, met, reason, fix}: gate a stable
id, met true or false, reason one sentence in the customer's words (citing the VAT guide where it applies), fix
the screen or the record that satisfies it. A refused transition returns its unmet gates; a screen, the MCP
tools and the selling bricks show them and never re-derive a rule. Issue in sales-order and ar asks
document_tax for these gates before it issues.
gates:
- { transition: register, gate: vat_number, reason: "a VAT registration needs its number (GB and 9 or 12 digits)", fix: "Tax settings > Registration" }
- { transition: [register, deregister, schedule_rate], gate: starts_after_today, reason: "a change starts tomorrow at the earliest, in business_timezone", fix: "pick a later date" }
- { transition: [register, schedule_rate], gate: second_person, reason: "the owner asks a second person to approve tax changes", fix: "ask another holder of tax:code:approve" }
- { transition: activate_code, gate: first_rate, reason: "a code needs a rate before it can be used", fix: "Tax codes > the code > Rates" }
- { transition: activate_code, gate: wording, reason: "a reverse-charge code prints its legend on the invoice", fix: "Tax codes > the code > Wording" }
- { transition: retire_code, gate: no_rule_points_here, reason: "a rule in effect still chooses this code", fix: "Tax rules: end that rule first" }
- { transition: determine, gate: rule_covers_pair, reason: "no tax rule for (category, treatment) on the tax point", fix: "Tax rules, or override the line" }
- { transition: determine, gate: customer_treatment, reason: "the customer has no tax treatment", fix: "Customer tax" }
- { transition: determine, gate: supplier_treatment, reason: "the supplier has no tax treatment (a purchase)", fix: "Customer tax: the supplier's row (the same party tax, keyed bp_id)" }
- { transition: determine, gate: purchase_rule_covers_pair, reason: "no purchase rule for (category, the supplier's treatment) on the tax point", fix: "Tax rules (direction purchase), or override the line" }
- { transition: freeze, gate: supplier_vat_number, reason: "a purchase bill without the supplier's VAT number is not a VAT invoice: its input VAT cannot be reclaimed", fix: "ask the supplier for a valid VAT invoice" }
- { transition: freeze, gate: contents_complete, reason: "one gate per absent item of VAT Notice 700 para 16.3.1", fix: "the record that holds the item" }
- { transition: freeze, gate: no_open_suggestion, reason: "a person has not yet accepted or rejected the AI's code", fix: "Document tax > the line" }
- { transition: freeze, gate: override_approved, reason: "an override that lowers tax waits for approval", fix: "ask a holder of tax:document_tax:approve_override" }
# v4
- { transition: freeze, gate: vat_number_valid, reason: "HMRC has no registration for the customer's VAT number, or it could not be checked and no person confirmed it", fix: "Customer tax: correct or confirm the number" }
- { transition: determine, gate: pinned_code, reason: "the pinned line names a code the earlier document does not carry", fix: "pin to the code the earlier document froze" }
- { transition: determine, gate: pinned_within_original, reason: "the pinned lines take more than the earlier document's net for that code", fix: "lower the amount to what remains" }
- { transition: change_vat_scheme, gate: period_start, reason: "a scheme or basis changes from the first day of a VAT period after today", fix: "pick the next period's start" }
- { transition: change_vat_scheme, gate: scheme_eligible, reason: "expected taxable turnover is above the scheme's joining limit (tax_statutory)", fix: "stay on the invoice basis" }
- { transition: [sync_vat_obligations, submit_vat_return, refresh_vat_liability, submit_cis_return], gate: hmrc_connected, reason: "this business is not connected to HMRC, or the grant expired", fix: "Tax settings > HMRC connection (the owner)" }
- { transition: prepare_vat_return, gate: obligation_open, reason: "HMRC shows no open obligation for this period", fix: "VAT returns: read obligations again" }
- { transition: record_vat_adjustment, gate: within_error_threshold, reason: "net errors above 10,000.00, or above 50,000.00 or 1% of box 6, are notified to HMRC (Notice 700/45)", fix: "record a VAT652 notice instead" }
- { transition: record_vat_adjustment, gate: within_four_years, reason: "an error older than four years cannot be corrected", fix: "none: the four-year cap" }
- { transition: lock_vat_return, gate: period_ended, reason: "the period ends on its end date; a return is locked after it", fix: "wait until the day after" }
- { transition: lock_vat_return, gate: pva_reconciled, reason: "a postponed import VAT statement of the period is not reconciled", fix: "Import VAT > the statement" }
- { transition: lock_vat_return, gate: flat_rate_eligible, reason: "flat-rate turnover passed the leaving limit; the scheme ends", fix: "record the change of scheme" }
- { transition: submit_vat_return, gate: declaration_confirmed, reason: "submitting is a legal declaration a person makes", fix: "tick the declaration" }
- { transition: submit_vat_return, gate: second_person, reason: "the owner asks someone other than the person who locked it to submit (same email or sub refused)", fix: "ask another holder of tax:vat_return:submit" }
- { transition: submit_vat_return, gate: fraud_headers_complete, reason: "HMRC requires every fraud-prevention header for WEB_APP_VIA_SERVER", fix: "submit from the web screen" }
- { transition: submit_vat_return, gate: obligation_still_open, reason: "HMRC already shows this period filed", fix: "none: an error is corrected on a later return" }
- { transition: claim_bad_debt_relief, gate: six_months_passed, reason: "relief starts six months after the later of the due date and the supply date", fix: "claim on a later return" }
- { transition: claim_bad_debt_relief, gate: written_off, reason: "the debt is not written off in the accounts", fix: "Invoicing: write it off (AR-09)" }
- { transition: claim_bad_debt_relief, gate: vat_accounted, reason: "the invoice's VAT is not in a submitted return", fix: "none: no relief on VAT never paid" }
- { transition: claim_bad_debt_relief, gate: within_time_limit, reason: "more than four years and six months have passed", fix: "none: the time limit" }
- { transition: claim_bad_debt_relief, gate: not_claimed, reason: "relief on this invoice is already claimed", fix: "none" }
- { transition: compute_cis_deduction, gate: cis_verified, reason: "the subcontractor has not been verified with HMRC", fix: "CIS > Subcontractors > Verify" }
- { transition: compute_cis_deduction, gate: materials_within_bill, reason: "materials exceed the bill's own materials figure", fix: "use the bill's figure" }
- { transition: prepare_cis_return, gate: tax_month_ended, reason: "the tax month runs 6th to 5th", fix: "prepare after the 5th" }
- { transition: submit_cis_return, gate: declarations_confirmed, reason: "the employment-status and verification declarations are a person's", fix: "confirm both" }
- { transition: record_trade_declaration_filed, gate: ties_to_box_8, reason: "the EC sales list total differs from box 8 for its months", fix: "find the document in one and not the other" }
- { transition: record_trade_declaration_filed, gate: commodity_code, reason: "a goods line's product has no commodity code", fix: "Product > the product > Trade" }
- { transition: reconcile_pva_statement, gate: lines_matched, reason: "a statement line is neither matched to an import entry nor explained", fix: "Import VAT > the line" }
- { transition: confirm_party_vat_number, gate: could_not_check, reason: "only a number HMRC could not check is confirmed by a person", fix: "correct an invalid number instead" }
entities:
- id: registration
meaning: whether, and from when, this business is registered for VAT
natural_key: [jurisdiction, effective_from]
key_attributes: [vat_number, status, scheme (standard | flat_rate | cash_accounting), "accounting_basis (invoice | cash, v4)", "flat_rate (bool), flat_rate_sector, flat_rate_pct (percent), first_year_until (v4)", "ni_goods (bool: an XI registration trading goods with the EU, v4)", "partial_exemption_year_end (MM-DD, v4)", stagger, effective_to]
master_data: module-owned (configuration)
reference_table: tax_registration
note: "v4: cash accounting and the flat rate scheme are COMPUTED by the return (TAX-36, TAX-37); a scheme change starts at a VAT period's first day after today. Dated rows: a change is a new row from after today; only the first row may carry an earlier date"
- id: tax_code
meaning: a jurisdiction's code for one tax treatment (S standard, Z zero, RC reverse charge)
natural_key: "tax_code: VARCHAR, the code the customer recognises (shared.yaml keys.tax_code)"
key_attributes: [jurisdiction (ISO 3166-1 alpha-2), treatment (standard | reduced | zero | exempt | outside_scope | reverse_charge | not_registered | postponed_import, v4), "direction (sale | purchase; every v2 code is sale)", description, document_wording, reporting_box, "input_reporting_box (purchase reverse charge: the input side)", "value_box (6 | 7 | 8 | 9: where the net value is reported, v4)", "reclaimable_pct (percent, purchase codes only: 100 reclaimable input VAT, 0 non-reclaimable, e.g. business entertainment)", "attribution (taxable | exempt | residual: partial exemption, purchase codes only, v4)", "eu_goods (bool: NI goods to or from the EU, feeds boxes 2, 8, 9 and the trade declarations, v4)", lifecycle]
relationships: ["1..n tax_rate"]
master_data: "SHARED: sole writer tax (moves from gl / invoicing, see shared.yaml)"
reference_table: tax_code
- id: tax_rate
meaning: a code's percentage from a date
natural_key: [tax_code, valid_from]
key_attributes: [rate_pct (percent), charged_pct (percent: 0 for reverse charge), withdrawn_by, withdrawn_at]
master_data: module-owned (configuration)
reference_table: tax_rate
note: "rate_pct is what is due, charged_pct what the supplier charges; they differ only under reverse charge. A row in effect is never edited or deleted; one not yet in effect may be withdrawn"
- id: tax_category
meaning: how a product is taxed, in the abstract (standard, reduced, zero, exempt, construction_service)
natural_key: "category_code (shared.yaml keys.tax_category)"
key_attributes: [description]
master_data: "SHARED: sole writer tax; product stores it on each product"
reference_table: tax_category
- id: treatment
meaning: how a customer, or a supplier on a purchase (v3), is taxed (UK business, UK consumer, overseas business, overseas consumer, CIS contractor)
natural_key: treatment_code
key_attributes: [description, needs_vat_number]
master_data: module-owned (configuration)
reference_table: tax_treatment
- id: party_tax
meaning: one customer's treatment and VAT number
natural_key: [bp_id, valid_from]
key_attributes: [treatment_code, vat_number, checked_on, "check_result (valid | invalid | could_not_check | confirmed, v4)", "check_ref (HMRC consultation number, v4)", "hmrc_name, hmrc_address (as HMRC holds them, v4)", "end_user_notice_ref, end_user_notice_on (construction: an end user or intermediary is outside the reverse charge, v4)", "cis_registered (bool, v4)", recorded_by]
relationships: ["-> business_partner"]
master_data: module-owned
reference_table: tax_party
note: "the business-partner master holds identity, not tax: the treatment lives here, keyed on bp_id; a supplier's row is the same table (v3: a purchase reads the SUPPLIER's). A customer's first row holds from the start (valid_from null); a change is a new row from after today"
- id: tax_rule
meaning: which code a (category, treatment, supply) combination gets, from a date
natural_key: [category_code, treatment_code, supply, direction, consumption, valid_from]
key_attributes: [tax_code, priority, valid_to, withdrawn_by, withdrawn_at]
master_data: module-owned (configuration)
reference_table: tax_rule
note: "supply is goods | services | any; the place-of-supply distinction a fiscal position would make. direction is sale | purchase (v3; every v2 row is sale): a purchase is determined only by purchase rules. A rule for a pair no rule covers may start today; a change to a covered pair starts after today. v6: consumption is eat_in | takeaway | any (every earlier row is any); a category's rules either all say any or distinguish eat_in and takeaway (TAX-51)"
- id: statutory_value
meaning: "a figure the law sets and changes (v4): the cash-accounting and flat-rate limits, the flat rate per sector, the error-correction threshold, CIS rates, partial-exemption de minimis, Intrastat and EC-sales-list thresholds"
natural_key: [key, valid_from]
key_attributes: [value, unit, source (the HMRC notice and paragraph), set_by]
master_data: module-owned (configuration; seeded by the UK pack)
reference_table: tax_statutory
note: "dated rows like rates: a change from after today; the pack loads each with its real date; code reads the row in force on the date it applies to, never a constant"
- id: hmrc_connection
meaning: "this business's grant to call HMRC's APIs for its VRN (v4)"
natural_key: vrn
key_attributes: [scopes (read:vat, write:vat), environment (sandbox | production), authorised_by, authorised_at, expires_at, secret_ref (the platform secret holding the tokens), status (connected | expired | revoked)]
master_data: module-owned (state)
reference_table: tax_hmrc_connection
note: "tokens are never stored in a row or logged: only the secret's reference"
- id: vat_obligation
meaning: "a VAT period HMRC expects a return for (v4)"
natural_key: [vrn, period_key]
key_attributes: [period_start, period_end, due_date, status (O | F), received_on, synced_at]
master_data: module-owned (a copy of HMRC's, refreshed)
reference_table: tax_vat_obligation
- id: vat_return
meaning: "one VAT return: its nine boxes, how they were prepared and HMRC's receipt (v4)"
natural_key: [vrn, period_key]
key_attributes: [lifecycle, accounting_basis, flat_rate_pct, box1_vat_due_sales, box2_vat_due_acquisitions, box3_total_vat_due, box4_vat_reclaimed, box5_net_vat_due, box6_total_sales_ex_vat, box7_total_purchases_ex_vat, box8_goods_supplied_ex_vat, box9_acquisitions_ex_vat, prepared_by, locked_by, locked_at, submitted_by, submitted_at, declaration_text, processing_date, form_bundle_number, payment_indicator, charge_ref_number, fraud_headers_json, settle_journal_id]
relationships: ["1..n vat_return_entry", "0..n vat_adjustment"]
master_data: module-owned
reference_table: tax_vat_return
note: "boxes 1-5 NUMERIC(18,2); boxes 6-9 whole pounds held as NUMERIC(18,2) with .00 (HMRC's rule); every box is SUM of its entries, re-derived, never typed; immutable once submitted"
- id: vat_return_entry
meaning: "one document's tax by code in one return: the claim that it is counted once (v4)"
natural_key: [document_ref, tax_code, entry_kind]
key_attributes: [vrn, period_key, direction, tax_point, net_amount, vat_amount, vat_reclaimable, box_vat, box_value, source (document | cash_basis | flat_rate | pva | bad_debt_claim | bad_debt_repayment | adjustment | partial_exemption)]
master_data: module-owned
reference_table: tax_vat_return_entry
note: "entry_kind separates a document's invoice-basis entry from a cash-basis part payment ('cash:<payment_id>:<seq>'), a bad-debt repayment ('bdr:<allocation>') and so on; the primary key is the claim, taken with a plain INSERT so two preparations never count one document twice"
- id: vat_adjustment
meaning: "an earlier period's error corrected on this return (v4)"
natural_key: [vrn, period_key, adjustment_no]
key_attributes: [corrects_period_key, box (1 | 4), amount, reason, recorded_by, recorded_at]
master_data: module-owned
reference_table: tax_vat_adjustment
- id: vat_error_notice
meaning: "an error above the threshold notified to HMRC on VAT652 (v4)"
natural_key: [vrn, notice_ref]
key_attributes: [periods, amount, sent_on, recorded_by]
master_data: module-owned
reference_table: tax_vat_error_notice
- id: vat_liability
meaning: "what HMRC says we owe and have paid per period (v4), a refreshed copy"
natural_key: [vrn, period_key, charge_type]
key_attributes: [original_amount, outstanding_amount, due_date, paid_amount, paid_on, synced_at]
master_data: module-owned (a copy of HMRC's)
reference_table: tax_vat_liability
- id: bad_debt_claim
meaning: "relief claimed on one unpaid invoice, and repayments when it is paid after all (v4)"
natural_key: invoice_id
key_attributes: [lifecycle, bp_id, supply_date, due_date, eligible_from, gross, unpaid_at_claim, vat_fraction, relief_amount, claimed_in_period_key, claimed_by, dismissed_reason]
master_data: module-owned
reference_table: tax_bad_debt_claim
- id: cis_subcontractor
meaning: "a subcontractor's CIS verification, from a date (v4)"
natural_key: [bp_id, valid_from]
key_attributes: [utr, company_number, ni_number_masked, status (gross | net | unmatched), rate_pct (percent), verification_number, verified_by, verified_on, last_paid_tax_year]
master_data: module-owned
reference_table: tax_cis_subcontractor
- id: cis_deduction
meaning: "the CIS deducted from one payment to a subcontractor (v4)"
natural_key: [payment_ref, bp_id]
key_attributes: [tax_month (YYYY-MM, the month ending on the 5th), paid_on, gross_ex_vat, materials, labour, rate_pct (percent), deduction, cis_return_id]
master_data: module-owned
reference_table: tax_cis_deduction
- id: cis_return
meaning: "the monthly CIS300 (v4)"
natural_key: 'cis_return_id = "<contractor_utr>:<tax_month>"'
key_attributes: [lifecycle, nil_return, total_gross, total_materials, total_deduction, submitted_by, submitted_at, hmrc_ref]
master_data: module-owned
reference_table: tax_cis_return
- id: cis_suffered
meaning: "CIS a contractor customer deducted from what it paid us (v4)"
natural_key: [contractor_bp_id, statement_ref]
key_attributes: [tax_month, invoice_id, payment_id, gross, materials, deduction, recorded_at]
master_data: module-owned
reference_table: tax_cis_suffered
- id: trade_declaration
meaning: "an EC sales list or an Intrastat declaration for a period (NI goods, v4)"
natural_key: [kind (esl | intrastat_dispatches | intrastat_arrivals), period, version]
key_attributes: [lifecycle, line_count, total_value, file_key, submission_ref, filed_on, filed_by]
relationships: ["1..n trade_declaration_line"]
master_data: module-owned
reference_table: tax_trade_declaration
- id: trade_declaration_line
meaning: "one line of a trade declaration (v4)"
natural_key: [kind, period, version, line_no]
key_attributes: [partner_vat_number, partner_country, commodity_code, nature_of_transaction, country_of_origin, value, net_mass_kg, supplementary_units, indicator, source_refs]
master_data: module-owned
reference_table: tax_trade_declaration_line
- id: import_entry
meaning: "one customs import entry and how its import VAT is accounted (v4)"
natural_key: mrn
key_attributes: [entry_date, bill_ref, bp_id, customs_value, import_vat, tax_code (PVA | IMPORT_PAID), evidence (pva_statement | import_vat_certificate), period_key]
master_data: module-owned
reference_table: tax_import_entry
- id: pva_statement
meaning: "the monthly postponed import VAT statement from CDS, and its lines (v4)"
natural_key: [vrn, statement_month]
key_attributes: [lifecycle, file_key, lines_json (mrn, import_vat, value), matched_count, explained_json]
master_data: module-owned
reference_table: tax_pva_statement
- id: capital_goods_item
meaning: "an asset under the Capital Goods Scheme and its yearly adjustments (v4)"
natural_key: item_ref
key_attributes: [kind (land_building | computer | ship_aircraft), bought_on, input_vat, intervals, first_pct (percent), adjustments_json (interval, pct, amount, period_key), disposed_on]
master_data: module-owned
reference_table: tax_capital_goods
- id: partial_exemption_year
meaning: "one partial-exemption year: its provisional and final recovery and the de minimis tests (v4)"
natural_key: [vrn, year_end]
key_attributes: [provisional_pct (percent), final_pct (percent), taxable_supplies, exempt_supplies, exempt_input_vat, residual_input_vat, de_minimis_met, adjustment_amount, adjusted_in_period_key]
master_data: module-owned
reference_table: tax_partial_exemption
- id: settings
meaning: how this business computes VAT, and the owner's controls
natural_key: [key, valid_from]
key_attributes: [value, set_by]
master_data: module-owned (configuration)
reference_table: tax_settings
master_data:
owns: [tax_code, tax_rate, tax_category, tax_treatment, tax_party, tax_rule, tax_registration, tax_settings, tax_migration_map, tax_statutory, tax_hmrc_connection, tax_vat_obligation, tax_vat_return, tax_vat_return_entry, tax_vat_adjustment, tax_vat_error_notice, tax_vat_liability, tax_bad_debt_claim, tax_cis_subcontractor, tax_cis_deduction, tax_cis_return, tax_cis_suffered, tax_trade_declaration, tax_trade_declaration_line, tax_import_ent