Files
personal-site/static/engine.html
T
2026-03-23 16:06:36 +01:00

314 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>FP&amp;A Budgeting Engine — Samantha Vero Friis</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&family=Libre+Caslon+Text:wght@400;700;800&family=Syne:wght@400;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="styles.css" />
<style>
/* ── Page-specific layout ── */
header { padding: 2.4rem 0 2rem; }
main { padding: 3rem 0 5rem; }
.prose-block { max-width: 660px; }
pre {
font-family: "DM Mono", monospace;
font-size: 0.78rem;
line-height: 1.8;
background: var(--surface);
border: 1px solid var(--border);
padding: 1.4rem 1.6rem;
overflow-x: auto;
margin: 1.6rem 0;
color: #c8c8c0;
}
/* stat cards at top */
.cards { margin: 2rem 0 2.5rem; }
footer {
border-top: 1px solid var(--border);
padding: 1.6rem 0;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.7rem;
color: var(--muted);
}
footer a { color: var(--muted); text-decoration: none; }
footer a:hover { color: var(--accent); }
@media (max-width: 560px) {
.cards { grid-template-columns: 1fr 1fr; }
}
</style>
</head>
<body>
<!-- ── HEADER ── -->
<header>
<div class="container">
<a class="back" href="https://samantha42.xyz/">← back</a>
<div class="tag">project</div>
<h1>FP&amp;A <span>Budgeting Engine</span></h1>
<div class="byline">
<span>Samantha Vero Friis</span>
<span style="color:var(--muted)">Go · REST API · Finance</span>
<span style="color:var(--muted)">March 2026</span>
</div>
</div>
</header>
<!-- ── STAT CARDS ── -->
<div class="container">
<div class="cards">
<div class="card">
<div class="card-label">Language</div>
<div class="card-value" style="color:var(--accent)">Go</div>
</div>
<div class="card">
<div class="card-label">Database</div>
<div class="card-value" style="color:var(--accent)">SQLite</div>
</div>
<div class="card">
<div class="card-label">Architecture</div>
<div class="card-value" style="font-size:1.1rem;color:var(--accent)">REST API</div>
</div>
<div class="card">
<div class="card-label">Infrastructure</div>
<div class="card-value" style="font-size:1.1rem;color:var(--green)">Zero deps</div>
</div>
</div>
</div>
<!-- ── MAIN ── -->
<main>
<div class="container">
<div class="prose-block">
<p class="lead">
A REST API for corporate budget management, actuals ingestion, and variance
reporting — built to replace the manual Excel workflow that smaller finance
teams still run every month. Single binary, no infrastructure, deploys anywhere.
</p>
<h2>The problem it solves</h2>
<p>
At companies below a certain size — typically below 500 employees, or without
the budget for enterprise planning platforms like Anaplan or Adaptive Planning —
the monthly FP&amp;A close still runs on spreadsheets. Actuals are exported from
the ERP, pasted into a workbook, and variances are computed by hand across
departments and GL accounts. The process is slow, error-prone, and breaks the
moment someone edits the wrong cell.
</p>
<p>
Large enterprises have solved this with dedicated tooling. The gap is the
mid-market: companies that have outgrown Excel but cannot justify a six-figure
implementation project. This engine sits in that space — structured schema,
versioned budgets, on-demand actuals ingestion, and instant variance reports
with no pivot tables and no shared file conflicts.
</p>
<blockquote>
The goal was not to build something clever.<br>
It was to build something that solves a real problem<br>
<span>finance teams in the mid-market actually have.</span>
</blockquote>
<h2>Stack</h2>
<p>
Most finance tooling is Python notebooks or Excel macros — useful for analysis,
fragile in production. Go compiles to a single binary with no runtime
dependencies, makes every error path explicit, and handles concurrency safely.
SQLite was chosen over Postgres deliberately: for a tool running inside a
finance team's environment, zero infrastructure matters. The database is a
single file. Backups are a file copy.
</p>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Layer</th>
<th style="text-align:left">Choice</th>
<th style="text-align:left">Reason</th>
</tr>
</thead>
<tbody>
<tr>
<td class="ticker">Language</td>
<td style="text-align:left">Go</td>
<td style="text-align:left;color:var(--muted)">Single binary, no runtime, explicit errors</td>
</tr>
<tr>
<td class="ticker">Routing</td>
<td style="text-align:left">net/http (stdlib)</td>
<td style="text-align:left;color:var(--muted)">No deps; method+path routing since Go 1.22</td>
</tr>
<tr>
<td class="ticker">Database</td>
<td style="text-align:left">SQLite</td>
<td style="text-align:left;color:var(--muted)">Zero infrastructure, file-based, ships with binary</td>
</tr>
<tr>
<td class="ticker">Schema</td>
<td style="text-align:left">Auto-migrated</td>
<td style="text-align:left;color:var(--muted)"><code>CREATE TABLE IF NOT EXISTS</code> on startup</td>
</tr>
</tbody>
</table>
</div>
<h2>API surface</h2>
<p>
Three resource groups. Budgets support full CRUD by department, GL account,
fiscal period, and version. Actuals are ingested via a single upsert endpoint
— idempotent by period, department, and GL code, designed to accept ERP
JSON exports directly. Variance endpoints return computed reports immediately.
</p>
<pre>POST /api/v1/budgets Create a budget line
PUT /api/v1/budgets/{id} Update amount or notes
DELETE /api/v1/budgets/{id} Remove a budget line
POST /api/v1/actuals/ingest Upsert an actual (idempotent)
GET /api/v1/variance Full variance report
GET /api/v1/variance/alerts Lines exceeding threshold
GET /api/v1/health Returns 200 if DB is reachable</pre>
<p>
Variance endpoints accept <code>year</code>, <code>period</code>,
<code>dept</code>, <code>version</code>, and <code>threshold</code>
as query parameters.
</p>
<h2>Finance concepts implemented correctly</h2>
<p>
<strong style="color:var(--text)">Favourability logic.</strong> Whether a
variance is good or bad depends on account type. Revenue accounts are
favourable when actuals exceed budget; cost accounts when actuals come in
under. The engine handles both via a <code>favour_high</code> flag per GL
account — a distinction generic reporting tools consistently leave to manual
configuration.
</p>
<p>
<strong style="color:var(--text)">Budget versioning.</strong> Original budget
and up to three forecast revisions are stored separately, enabling a full
budget vs. forecast vs. actual three-way comparison without overwriting
history — the standard FP&amp;A reforecast workflow.
</p>
<p>
<strong style="color:var(--text)">GL account typing.</strong> Accounts are
typed as <code>revenue</code>, <code>cogs</code>, <code>opex</code>,
<code>capex</code>, or <code>headcount</code>. This drives the P&amp;L
rollup structure and the favourability logic together, reflecting how an
actual chart of accounts is structured.
</p>
<p>
<strong style="color:var(--text)">Fiscal period decoupling.</strong> Periods
are stored as integers 112, decoupled from calendar months — required for
any company not on a January fiscal year.
</p>
<h2>Example output</h2>
<p>
Engineering department, FY2024 P09. Cloud infrastructure came in under budget.
Consulting exceeded by 51%.
</p>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>GL</th>
<th>Description</th>
<th>Budget</th>
<th>Actual</th>
<th>Var %</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td>6100</td>
<td style="text-align:left;color:var(--text)">Salaries &amp; Wages</td>
<td>4,200k</td>
<td>4,380k</td>
<td class="down">4.29%</td>
<td class="down">unfavourable</td>
</tr>
<tr>
<td>6500</td>
<td style="text-align:left;color:var(--text)">Consulting &amp; Contractors</td>
<td>450k</td>
<td>680k</td>
<td class="down">51.11%</td>
<td class="down">unfavourable</td>
</tr>
<tr>
<td>5000</td>
<td style="text-align:left;color:var(--text)">Cloud Infrastructure</td>
<td>850k</td>
<td>791k</td>
<td class="up">+6.94%</td>
<td class="up">favourable</td>
</tr>
</tbody>
</table>
</div>
<h2>Running it</h2>
<pre>git clone https://git.samantha42.xyz/samantha/FPandA-Engine
cd FPandA-Engine
cp .env.example .env
go mod tidy
go run ./cmd/server
# → http://localhost:8080
# → fpa.db created on first run</pre>
<p>
Two environment variables: <code>DB_PATH</code> (use <code>:memory:</code>
for tests) and <code>PORT</code>. Demo seed data for Engineering, Sales,
and Marketing is included in <code>scripts/seed_demo.sql</code>.
</p>
<hr />
<p>
Source at
<a href="https://git.samantha42.xyz/samantha/FPandA-Engine"
style="color:var(--accent);text-decoration:none"
target="_blank">git.samantha42.xyz/samantha/FPandA-Engine</a>
</p>
</div>
</div>
</main>
<footer>
<div class="container" style="display:flex;justify-content:space-between;width:100%">
<a href="mailto:contact@samantha42.xyz">contact@samantha42.xyz</a>
<span>© 2026 — All rights reserved</span>
</div>
</footer>
</body>
</html>