Skip to content

Repository files navigation

TieOut

CI Security License: MIT

TieOut: the month-end close, tied out. A dark control-tower dashboard for CFOs and accounting teams showing the close pulse, days-to-close trend, late tasks, and bottlenecks, beside live captures of the close board

Every finance team runs its month-end close over email and a shared spreadsheet, and every month the CFO asks the same two questions nobody can answer from either: where is the close stuck right now? and why did it take six working days instead of five? TieOut is the control tower for that close: the checklist with its dependencies, sign-offs with separation of duties enforced by the system, a working-day clock (BD+n) that skips weekends and holidays, bottleneck analytics that name the slowest category, and an append-only audit trail that turns "what happened in March?" into a query instead of an archaeology dig.

It is a Blazor web app over PostgreSQL, with EF Core on the command side and Dapper serving the analytics SQL, and every rule it enforces is tested against a real database and a real browser.

Reading this page. Finance leader? The screenshots and the bold lines below are the whole story. Engineer? TECHNICAL.md holds the architecture, the EF Core + Dapper decision record, and the test ladder, including the parity tests that pin the SQL analytics to the same answers the C# domain computes.

The problem, precisely

The close is a relay race: AP, AR, payroll, prepaids, bank recs, intercompany, tax, then the GL review and the controller's certification. It fails as a spreadsheet because the spreadsheet cannot know three things TieOut knows structurally:

The spreadsheet cannot TieOut does
Tell you a task is blocked, or by what Every dependency is declared; a blocked card names exactly the tasks it waits on, and the server refuses completion until each is signed off
Enforce that the person who did the work is not the person who approved it Four-eyes is a rule: whoever completes a sign-off-required task cannot sign it off, in the UI or the API, no exceptions
Tell you the true cost of a reopen Every reopen demands a reason, kept verbatim, timestamped on the working-day clock, and feeds the bottleneck analytics

And the metric the CFO actually owns, days-to-close, is computed on working days from month end (BD+n, holidays excluded) rather than wall-clock days, so a close that "took nine days" across Labor Day is honest about what nine days means.

See it

The control tower: KPI strip, close pulse ring, days-to-close trend against target, late list, bottlenecks The close board: four lanes, dependencies visible, late cards flagged, actions per state

The demo company is Cedar Ridge Foods, Inc.: seven finance staff, five locked months of real-looking history (including a painful June), and one live August close mid-pain, with an intercompany task reopened, four sign-offs waiting, and thirteen tasks late. Reset it any time; the seed is deterministic, so every screenshot and test tells the same story.

The three rules that make it a close, not a checklist

  1. Dependencies gate completion. The controller certification cannot complete while the subledger tie-outs are unsigned, and a task waiting for sign-off does not satisfy its dependents. Refusals are typed, carry the reason in plain English, and write nothing.
  2. Four-eyes sign-off. The completer is recorded and can never be the signer. The board greys its own case out for you; the API answers 409 with FourEyesViolation.
  3. A period locks only when everything is completed, and only by the Controller or CFO. Locking writes the actual close day in working days next to the target, and a locked period is frozen: reopens and completions are refused with the period named.

The life of one task in the close: not started, in progress, completion gates, waiting sign-off, completed; typed refusals; reopens with reasons; the lock gate; everything lands in the audit trail

How it is built

Architecture: Blazor browser and typed API over one CloseStore command side on EF Core; Dapper serves analytics SQL over the same PostgreSQL schema; pure CloseRules gate every write; parity tests pin the two together

One command side, one read side, one rule core:

  • EF Core writes. Every state change (complete, sign off, reopen, comment, lock) loads what it needs, asks the pure CloseRules, applies the change, appends its audit event, and saves both in one transaction. A refused rule writes nothing.
  • Dapper reads. The dashboard's trend, bottleneck, and late-list panels are served by reviewed SQL, with working days computed in the database (generate_series minus weekends minus the holiday table). Integration tests pin that SQL to the same answers the C# WorkingDayCalendar produces, so the two data paths cannot drift.
  • Blazor renders. Static SSR for the dashboard and audit trail, one Interactive Server island for the board where the actions live, and a hand-built dark design system (no CSS framework, no chart library: the trend chart is SVG the app draws itself).

The proof ladder: 30 domain-rule tests (working-day math, four-eyes, gates), 14 integration tests on real PostgreSQL (including the Dapper/EF parity contracts and a full close walked to lock in dependency order), 9 Playwright tests in a real browser, and an 11-request newman regression against the booted app. scripts/run-e2e.sh boots the whole thing against a scratch database and runs the last two in one command.

Run it

Requires .NET 10 SDK and PostgreSQL 17 (any local instance; the app creates and migrates its database on first boot):

git clone https://github.com/Alex5350/tieout.git
cd tieout
export TIEOUT_DB="Host=localhost;Port=5432;Database=tieout_dev;Username=<your-user>"
dotnet run --project src/TieOut.App

Open http://localhost:5008 (or the port it prints), act as Maria Vance (Controller) from the header switcher, and drive the August close. To prove the whole stack end to end, including the browser tests:

./scripts/run-e2e.sh     # boots a scratch app + database, replays newman, runs Playwright

Verified toolchain

Component Version
Runtime and language .NET 10, C# 14
UI Blazor Web App (Static SSR + Interactive Server)
ORM, command side EF Core 10 (Npgsql provider 10.0.3)
Micro-ORM, read side Dapper 2.1.79
Database PostgreSQL 17
Tests xUnit, Microsoft.Playwright 1.62, newman 6

Repository map

  • src/TieOut.Domain - the close's entities, working-day calendar, and rules; zero dependencies
  • src/TieOut.Data - EF Core context and migrations, CloseStore (commands), AnalyticsQueries (Dapper reads), deterministic seeder
  • src/TieOut.App - Blazor UI, typed API, cookie-based demo identity
  • tests/ - domain and integration suites (the latter includes the parity contracts)
  • e2e/ - Playwright suite against the booted app
  • postman/ - the newman regression collection
  • docs/errors.md - every typed refusal the API can return, and when

Honest limits

Demo identity is a cookie switcher (a real deployment wires SSO and keeps everything else identical). Reopens do not cascade to dependents that already completed; the audit trail surfaces the risk instead. Multi-entity consolidation, journal entry creation, and ERP integrations are deliberately out of scope: this is the close's control plane, not the ledger (for the ledger, see LedgerLite in the same portfolio).

License

MIT. Cedar Ridge Foods, Inc. is fictional; every number in the demo is generated by the committed seeder.

About

The month-end close, tied out: a Blazor control tower for CFOs and accounting teams, with four-eyes sign-offs, a working-day clock, Dapper analytics, and an audit trail on EF Core + PostgreSQL

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages