# Tax & compliance

> ControlTestResult sits beside them and speaks their idiom. Every component in this family takes an explicit asOf. "Today" is never the client clock, so a server render, a printed copy and a screenshot agree on what is late. Shared types live in src/ledger/lib/compliance.types.ts; the tax-ID checks l

Source: https://lopifi.com/docs/catalog/14-tax-compliance
Part of: Lopifi — US GAAP books on autopilot, checked by a CPA (https://lopifi.com)

---

# Tax & compliance

Domain in [`domain/06-procure-to-pay.md`](https://lopifi.com/md/docs/domain/06-procure-to-pay) (1099 tracking, sanctions screening), [`domain/08-controls-and-audit.md`](https://lopifi.com/md/docs/domain/08-controls-and-audit) (control grades, approvals bound to a version) and [`domain/13-us-gaap.md`](https://lopifi.com/md/docs/domain/13-us-gaap) (sales tax as a liability).

**See also:** `#141 AuditTrail`, `#142 EvidencePanel` and `#144 MaterialityPolicy` in [close, controls & consolidation](https://lopifi.com/md/docs/catalog/12-close-controls-consolidation). `ControlTestResult` sits beside them and speaks their idiom.

> **The rule is data.** A filing deadline, a form name, a reporting threshold, a sanctions list, a tax rate, the wording of a control conclusion: each one varies by jurisdiction and by year, so each one arrives as a prop and none is a constant in a component. A component that knew "the" threshold would be wrong the year it changed, and wrong silently.

Every component in this family takes an explicit `asOf`. "Today" is never the client clock, so a server render, a printed copy and a screenshot agree on what is late. Shared types live in `src/ledger/lib/compliance.types.ts`; the tax-ID checks live in `src/ledger/lib/tax-id.ts`, pure, so a server can run the same check the field ran.

---

## 188. `ComplianceCalendar`
Every filing the entity owes: obligation, form, jurisdiction and agency, the period covered, the due date, the owner, the status, and **the date penalties start**. Two views: `upcoming` puts open filings soonest first, so past-due ones sit at the top and filed ones go last, and `month` groups every filing under the month its governing date falls in.

```
Filing calendar                         Northwind Ltd · as of 18 Oct 2026
1 past due · 1 due soon · 1 without an owner
─ Open ───────────────────────────────────────────────────── 5 filings
15 Oct   Payroll tax deposit                                  [Late]
3 days   Federal · IRS · September 2026 · A. Whitfield   Penalties from 16 Oct 2026
past due
```

**Status is supplied, not derived.** `upcoming | due-soon | filed | late | extended`. Whether a return is "due soon" depends on business days, holidays and the team's own lead time, and the consumer knows those; the component does not. It does compute the day count from `asOf`, by calendar date, so a clock time never makes a return late.

**An extension moves the filing date, and only that.** The row then shows the extended date, the original date beneath it, and, when the consumer supplies a separate `paymentDueDate`, the payment date too, because an extension to file isn't always an extension to pay.

**Refuses to render:** a penalty date nobody stated as "no penalty". A `null` penalty date prints "Penalty date not stated". A `null` owner prints "Unassigned" in the warning tone and is counted in the header. It's an action, not a blank.

**Edge cases:** no obligations at all says so and asks whether the registrations behind it are complete, because an empty calendar is a claim. A filing with no form name shows the obligation alone. A filed row shows the filing date and the agency's confirmation number.

**Accessibility:** each group is a labeled `section` holding an ordered list. Status is an icon plus a word. The past-due day count carries weight as well as color, so it reads in grayscale.

## 189. `SalesTaxSummary`
By jurisdiction: gross sales, exempt, taxable, rate, tax collected, tax due, and the return's state (`open | filed | paid`). Totals sit under single and double rules over the figure columns only.

**The tie-out is the point.** The unpaid liability on the returns (every row not yet paid) is compared to the sales-tax-payable balance in the GL, and the difference is on screen. Three renders: **ties**, **differs** (an alert, with the signed difference), and **not tied out**. A `null` GL balance never compares against zero; the component states that no tie-out was performed.

**Each row proves itself.** Gross less exempt must equal taxable. A row that doesn't gets a glyph in the taxable cell and is named in an alert above the table. A gap between collected and due is printed under the jurisdiction as "Collected $31.80 less than due". Over- and under-collection are both findings, in opposite directions.

**Refuses to render:** a computed average rate. A row blending several rates passes `rate: null` and prints "Blended". Mixed currencies are never summed either: one `currency` prop governs every row.

**Edge cases:** a 0-decimal currency (the amounts follow the currency's exponent through `Money`). Every row paid, so the unpaid liability is zero and ties to a zero GL balance. A jurisdiction exempt in full.

**Accessibility:** a real `<table>` with a caption naming the period and currency, row headers per jurisdiction, and the frozen header and first column from `.ledger-frozen` (CSS only, so the component stays server-safe).

## 190. `TaxFormTracker`
Vendors paid over a reporting threshold, and what stands between each of them and a filed information return: total paid, reportable amount, W-9 state, TIN-match result, form type, delivery and filing.

**The threshold is a prop and is printed in the header.** "Vendors paid at least $2,000 in reportable payments · tax year 2026". The figure changes by law and by year ([procure to pay](https://lopifi.com/md/docs/domain/06-procure-to-pay) keeps it as a dated table), and so does its wording: `comparison: "at-least" | "more-than"` has **no default**, because whether a vendor paid exactly the threshold is in or out depends on the rule's own text. Vendors under the threshold are counted ("1 under it, not listed"), not silently dropped.

**Reportable is not total paid.** Payments by card or through a third-party network are typically reported by the processor, not the payer, so the threshold is tested against `reportableAmount`, and both columns are shown.

**Rows that need work sort first:** a W-9 that's missing or requested, a TIN mismatch, an undetermined form, an undeliverable copy, a rejected filing. The header alert counts missing W-9s and mismatches, because before year-end they're still fixable.

**Refuses to render:** a full TIN. The list shows the last four at most, masked, with spoken text "TIN ending 4471".

**Accessibility:** every state is a glyph plus a word (Matched, Mismatch, Not run). The table has a caption naming the tax year.

## 191. `TaxIdInput`
One field for `ein | ssn | itin | abn | gst | vat`, each in its issuer's printed form, checked by the issuer's published algorithm where one exists and by format where none does. The result says which kind of check passed.

| Scheme | Printed | Check |
|---|---|---|
| `ein` | `12-3456789` | Format only: nine digits. There's no check digit, and the IRS's valid-prefix list changes, so a stale copy here would reject a real number. |
| `ssn` | `123-45-6789` | Format: the area is never 000, 666 or 900–999, the group never 00, the serial never 0000. |
| `itin` | `9XX-XX-XXXX` | Format: a leading 9, with the fourth and fifth digits in 50–65, 70–88, 90–92 or 94–99. |
| `abn` | `12 345 678 901` | Checksum: subtract 1 from the first digit, apply the weights 10, 1, 3, 5 … 19, and the sum must be divisible by 89. |
| `gst` | `123456789 RT0001` | Canadian GST/HST: the nine-digit business number must pass Luhn, followed by the RT program account. |
| `vat` | `DE123456789` | Format by country prefix only (EU member states, `XI`, `GB`). An unknown prefix is "not checked", not invalid. |

**A pass is not a verification.** A well-formed number may never have been issued. Registry lookups (IRS TIN matching, VIES, HMRC, ABN Lookup) belong to the consumer's backend and come back through `error`.

**Masked whenever it isn't focused.** Every character but the last four is a bullet *in the DOM*, so a screenshot, a screen share or a copied selection carries the mask, not the number. A VAT number keeps its country prefix, since the prefix isn't the secret. Focus shows the whole number, and formatting applies on blur only, because inserting a hyphen mid-typing moves the caret. With `name`, a hidden input posts the real value. The visible field never does.

**Never logged:** no console output, no data attribute, `autoComplete="off"`, no spellcheck. `type="text"` with the scheme's `inputMode`, and paste is never blocked.

**Validation timing:** "incomplete" waits until the field is left. A wrong number at full length says so at once. The message line is reserved, so a result arriving doesn't move the form. It crossfades in and never shakes.

**Accessibility:** a real `<label>`. `aria-invalid` and `aria-describedby` point at the message. At rest the masked field is described as "SSN hidden, ending 6789. Focus the field to show it."

## 192. `VerificationStatus`
KYC for a person, KYB for a business: the subject, every requirement (ID document, address, beneficial owners, sanctions screening …) with its state (`verified | pending | needs-action | failed`), and what's still due, **by when, and what happens if it isn't provided**. Requirements, deadlines and consequences come from the provider or the platform. The component names none of its own.

**The overall state is derived, worst first.** One failed requirement makes the subject failed, never an average. An empty requirement list is "No requirements on record", **never "Verified"**.

**Next due** is stated in the summary line ("Next due: Beneficial owners by 24 Oct 2026"), and an overdue requirement counts its days in the negative tone. `onResolve` adds an action to requirements that need action or failed. Without it, no action renders.

**Accessibility:** the action's accessible name includes the requirement ("Provide beneficial owners"). Pending is a clock, never a spinner: it's a state, not a load.

## 193. `SanctionsScreening`
One screening hit, laid out so a reviewer can decide it: the screened name beside the listed name, a field-by-field comparison (exact, partial, no match), the list source with its entry id and refresh date, the provider's match strength and score, and the disposition (`true-match | false-positive | escalated`) with reviewer, timestamp and rationale.

**Dispositions are append-only.** There's no clear, no dismiss and no edit. A changed mind is a new record with its own reason, and the earlier one stays in the history. Paying a sanctioned party is a legal event, not an accounting one ([procure to pay](https://lopifi.com/md/docs/domain/06-procure-to-pay)), and a hit that vanished without a reviewer's name is exactly what an examiner asks about.

**Refuses to render:** an anonymous disposition. The form names the reviewer and requires a rationale, and without `reviewer` and `onRecord` the hit renders read-only. An escalated hit is still open.

**Data, not constants:** the list's name, version and refresh date, and the strength thresholds, are the provider's. Demos use an illustrative list and fictional parties.

**Accessibility:** native radio inputs, so the browser owns the arrow keys. A real `<form>` and `<fieldset>`. The current disposition is a `status` region.

## 194. `ControlTestResult`
One control, one test, one conclusion: control id and description, owner, frequency, test type (`design` or `operating-effectiveness`), sample size against population, every exception (the sample item, what was found, the amount, the remediation), and the conclusion.

**The grade uses the auditor's vocabulary** from [controls & audit](https://lopifi.com/md/docs/domain/08-controls-and-audit): effective, control deficiency, significant deficiency, material weakness. Never a generic "issue". The wording is data. `CONCLUSION_COPY` is the default, and a firm with its own methodology passes `conclusionCopy` rather than forking the component.

**A material weakness owns its surface**, with a 2px negative border and an alert, as a failing `IntegrityCheckPanel` does.

**The component asserts:**
- A sample larger than the population is an error. One of the two figures is wrong.
- "Effective" with exceptions and no rationale is flagged. That conclusion can be right, but only with a reason.
- A `null` population prints "population not stated", never "of 0". An exception with a `null` amount prints "Not quantified", never $0.00.

**Accessibility:** the conclusion is an icon plus a label, and its consequence is spelled out in a sentence.

## 195. `PolicyAttestation`
A policy's name, version and effective date, who must attest, who has, who hasn't, and the overdue count.

**Acknowledgments bind to a version**, as approvals do ([controls & audit §3](https://lopifi.com/md/docs/domain/08-controls-and-audit#3-approvals)). Someone who acknowledged v3.0 hasn't acknowledged v3.1. They're counted as outstanding, and the row says "Acknowledged 3.0, not this version" so an old signature never stands in for a new policy.

**Overdue** is every outstanding attestor once `asOf` passes `dueBy`. Before the deadline the overdue count is zero, and the outstanding list says why each person is on it. `onRemind` adds a single "Remind N" action for the outstanding attestors.

**Refuses to animate** the progress bar. It's a count, and a bar that grows is a number being animated.

**Edge cases:** nobody assigned to attest says so, rather than showing 0 of 0 as complete.

**Accessibility:** the bar is a `progressbar` with its value and an accessible label ("3 of 5 attested"). The counts are a description list.
