new sub page paradigm

This commit is contained in:
samantha42
2026-08-28 01:36:11 +02:00
parent 5d3c428112
commit 3d3d87fad9
23 changed files with 896 additions and 252 deletions
+314
View File
@@ -0,0 +1,314 @@
<!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="/static/style/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="/">← 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:me@samantha42.xyz">me@samantha42.xyz</a>
<span>© 2026 — All rights reserved</span>
</div>
</footer>
</body>
</html>