new readme
This commit is contained in:
246
README.md
246
README.md
@@ -1,37 +1,35 @@
|
|||||||
# FP&A Budgeting Engine
|
# FP&A Budgeting Engine
|
||||||
|
|
||||||
A production-grade REST API for corporate budgeting, variance analysis, and department-level financial reporting. Built to automate the workflows FP&A teams typically run manually in Excel.
|
A REST API for corporate budget management, actuals ingestion, and variance reporting. Built to replace the manual Excel workflows FP&A teams run every month.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## The Business Problem
|
## The Problem
|
||||||
|
|
||||||
Every month, FP&A analysts pull actuals from ERP systems, paste them into spreadsheets, and manually calculate budget vs. actual variances by department and cost center. This is slow, error-prone, and doesn't scale.
|
Every month, finance teams export actuals from their ERP, paste them into spreadsheets, and manually calculate budget vs. actual variances by department and GL account. It is slow, error-prone, and breaks the moment someone edits the wrong cell.
|
||||||
|
|
||||||
This engine replaces that workflow with a reliable API: budget data is structured in a normalized schema, actuals are ingested on a schedule, and variance reports are available on demand — for any department, any period, any GL account.
|
This engine moves that logic into a versioned, auditable API. Budget data lives in a structured schema. Actuals are ingested on demand. Variance reports — with favourability, percentage, and absolute figures — are available instantly for any department, period, or budget version.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## What It Does
|
## Features
|
||||||
|
|
||||||
- **Budget management** — Create and version annual budgets by department and GL account
|
- **Budget CRUD** — create and update budgets by department, GL account, fiscal period, and version (original, forecast 1–3)
|
||||||
- **Actuals ingestion** — Load actuals from CSV or JSON (ERP export format)
|
- **Actuals ingestion** — upsert actuals from any source via JSON; designed to accept ERP export feeds
|
||||||
- **Variance analysis** — Budget vs. actual, favourable/unfavourable, percentage and absolute
|
- **Variance analysis** — budget vs. actual with correct favourability logic (revenue accounts favour positive variance; cost accounts favour negative)
|
||||||
- **Rollups** — P&L rollup by department, cost center, and fiscal period
|
- **Alert endpoint** — returns all GL lines where absolute variance exceeds a configurable threshold
|
||||||
- **Alerts** — Flag accounts exceeding budget by a configurable threshold
|
- **Zero infrastructure** — SQLite database, single binary, runs anywhere with no external dependencies
|
||||||
- **Audit trail** — Every budget change is timestamped and attributed
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Tech Stack
|
## Stack
|
||||||
|
|
||||||
| Layer | Technology |
|
| Layer | Choice | Why |
|
||||||
|---|---|
|
|---|---|---|
|
||||||
| API server | Go (net/http + chi router) |
|
| Language | Go | Fast, statically typed, single binary deploy |
|
||||||
| Database | PostgreSQL |
|
| Routing | `net/http` (stdlib) | No dependencies; method+path routing since Go 1.22 |
|
||||||
| Schema migrations | golang-migrate |
|
| Database | SQLite via `modernc.org/sqlite` | Zero infrastructure, file-based, ships with the binary |
|
||||||
| Config | Environment variables (.env) |
|
| Schema | Auto-migrated on startup | No migration tool needed; `CREATE TABLE IF NOT EXISTS` is idempotent |
|
||||||
| Docs | OpenAPI 3.0 (docs/openapi.yaml) |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -39,11 +37,9 @@ This engine replaces that workflow with a reliable API: budget data is structure
|
|||||||
|
|
||||||
```
|
```
|
||||||
fpa-budgeting-engine/
|
fpa-budgeting-engine/
|
||||||
├── cmd/
|
├── cmd/server/main.go # Entry point, wiring, graceful shutdown
|
||||||
│ └── server/
|
|
||||||
│ └── main.go # Entry point
|
|
||||||
├── internal/
|
├── internal/
|
||||||
│ ├── handler/ # HTTP handlers
|
│ ├── handler/ # HTTP layer — decode request, call service, encode response
|
||||||
│ │ ├── budget.go
|
│ │ ├── budget.go
|
||||||
│ │ ├── actuals.go
|
│ │ ├── actuals.go
|
||||||
│ │ └── variance.go
|
│ │ └── variance.go
|
||||||
@@ -51,133 +47,163 @@ fpa-budgeting-engine/
|
|||||||
│ │ ├── budget.go
|
│ │ ├── budget.go
|
||||||
│ │ ├── actuals.go
|
│ │ ├── actuals.go
|
||||||
│ │ └── variance.go
|
│ │ └── variance.go
|
||||||
│ ├── repository/ # DB layer
|
│ ├── repository/ # Database queries
|
||||||
│ │ ├── budget_repo.go
|
│ │ ├── budget_repo.go
|
||||||
│ │ └── actuals_repo.go
|
│ │ └── actuals_repo.go
|
||||||
│ └── service/ # Business logic
|
│ └── service/ # Business logic
|
||||||
│ ├── budget_service.go
|
│ ├── budget_service.go
|
||||||
│ └── variance_service.go
|
│ └── variance_service.go
|
||||||
├── migrations/
|
├── migrations/ # Reference SQL (schema is also auto-applied on start)
|
||||||
│ ├── 001_create_departments.up.sql
|
├── scripts/seed_demo.sql # Realistic demo data for Engineering, Sales, Marketing
|
||||||
│ ├── 002_create_gl_accounts.up.sql
|
├── docs/openapi.yaml # API specification
|
||||||
│ ├── 003_create_budgets.up.sql
|
└── .env.example
|
||||||
│ ├── 004_create_actuals.up.sql
|
|
||||||
│ └── 005_create_audit_log.up.sql
|
|
||||||
├── docs/
|
|
||||||
│ └── openapi.yaml
|
|
||||||
├── scripts/
|
|
||||||
│ └── seed_demo.sql # Sample data for demo/testing
|
|
||||||
├── .env.example
|
|
||||||
├── docker-compose.yml
|
|
||||||
├── Makefile
|
|
||||||
└── README.md
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Key API Endpoints
|
|
||||||
|
|
||||||
```
|
|
||||||
POST /api/v1/budgets Create a new budget entry
|
|
||||||
GET /api/v1/budgets?dept=&period= List budgets with filters
|
|
||||||
PUT /api/v1/budgets/{id} Update a budget line
|
|
||||||
POST /api/v1/actuals/ingest Ingest actuals (JSON or CSV)
|
|
||||||
GET /api/v1/variance?dept=&period= Variance report
|
|
||||||
GET /api/v1/variance/summary Full P&L summary
|
|
||||||
GET /api/v1/variance/alerts Accounts over threshold
|
|
||||||
GET /api/v1/rollup?by=department Rollup by dimension
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Finance Concepts Implemented
|
|
||||||
|
|
||||||
**Chart of Accounts** — GL accounts are typed (revenue, COGS, opex, capex) and roll up into P&L line items correctly.
|
|
||||||
|
|
||||||
**Variance conventions** — Revenue variances are favourable when actuals exceed budget. Cost variances are favourable when actuals are below budget. The engine handles both correctly.
|
|
||||||
|
|
||||||
**Fiscal periods** — Supports fiscal year offsets (e.g. FY starting April). Periods are stored as fiscal quarters and months, not calendar months.
|
|
||||||
|
|
||||||
**Budget versioning** — Original budget, Forecast 1, Forecast 2 are tracked separately, enabling Budget vs. Forecast vs. Actual three-way comparison.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone and configure
|
|
||||||
git clone https://gitea.yoursite.com/yourname/fpa-budgeting-engine
|
git clone https://gitea.yoursite.com/yourname/fpa-budgeting-engine
|
||||||
|
cd fpa-budgeting-engine
|
||||||
|
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
|
|
||||||
# Start Postgres
|
go mod tidy
|
||||||
docker-compose up -d db
|
go run ./cmd/server
|
||||||
|
# → API on http://localhost:8080
|
||||||
|
# → fpa.db created automatically on first run
|
||||||
|
```
|
||||||
|
|
||||||
# Run migrations
|
To load demo data (Engineering, Sales, Marketing for FY2024 P09):
|
||||||
make migrate-up
|
|
||||||
|
|
||||||
# Seed demo data
|
```bash
|
||||||
psql $DATABASE_URL < scripts/seed_demo.sql
|
# Demo data loads via the ingest endpoint — see scripts/seed_demo.sh
|
||||||
|
# or POST directly to /api/v1/actuals/ingest
|
||||||
# Start the server
|
|
||||||
make run
|
|
||||||
# → API available at http://localhost:8080
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Example: Variance Report Response
|
## API
|
||||||
|
|
||||||
|
### Budgets
|
||||||
|
|
||||||
|
```
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
### Actuals
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/v1/actuals/ingest Upsert an actual (idempotent by period + dept + GL)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Variance
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v1/variance Full variance report
|
||||||
|
GET /api/v1/variance/alerts Lines exceeding threshold (default 10%)
|
||||||
|
```
|
||||||
|
|
||||||
|
Query parameters for variance endpoints:
|
||||||
|
|
||||||
|
| Param | Example | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `year` | `2024` | Fiscal year |
|
||||||
|
| `period` | `9` | Fiscal period (1–12) |
|
||||||
|
| `dept` | `ENG` | Department code (omit for all) |
|
||||||
|
| `version` | `original` | Budget version |
|
||||||
|
| `threshold` | `15` | Alert threshold % (alerts endpoint only) |
|
||||||
|
|
||||||
|
### Health
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v1/health Returns 200 if DB is reachable
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example: Variance Report
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v1/variance?year=2024&period=9&dept=ENG
|
||||||
|
```
|
||||||
|
|
||||||
```json
|
```json
|
||||||
GET /api/v1/variance?dept=engineering&period=2024-Q3
|
|
||||||
|
|
||||||
{
|
{
|
||||||
"department": "Engineering",
|
"department": "ENG",
|
||||||
"period": "2024-Q3",
|
"fiscal_year": 2024,
|
||||||
|
"fiscal_period": 9,
|
||||||
|
"version": "original",
|
||||||
"currency": "DKK",
|
"currency": "DKK",
|
||||||
|
"total_budget": 6740000,
|
||||||
|
"total_actual": 7074600,
|
||||||
|
"total_variance": -334600,
|
||||||
|
"total_variance_pct": -4.97,
|
||||||
"lines": [
|
"lines": [
|
||||||
{
|
{
|
||||||
"gl_account": "6100",
|
"gl_code": "6100",
|
||||||
"description": "Salaries & Wages",
|
"gl_description": "Salaries & Wages",
|
||||||
|
"gl_type": "headcount",
|
||||||
"budget": 4200000,
|
"budget": 4200000,
|
||||||
"actual": 4380000,
|
"actual": 4380000,
|
||||||
"variance": -180000,
|
"variance_abs": -180000,
|
||||||
"variance_pct": -4.3,
|
"variance_pct": -4.29,
|
||||||
"status": "unfavourable"
|
"status": "unfavourable",
|
||||||
|
"currency": "DKK"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"gl_account": "6300",
|
"gl_code": "6500",
|
||||||
"description": "Software Subscriptions",
|
"gl_description": "Consulting & Contractors",
|
||||||
"budget": 320000,
|
"gl_type": "opex",
|
||||||
"actual": 289000,
|
"budget": 450000,
|
||||||
"variance": 31000,
|
"actual": 680000,
|
||||||
"variance_pct": 9.7,
|
"variance_abs": -230000,
|
||||||
"status": "favourable"
|
"variance_pct": -51.11,
|
||||||
|
"status": "unfavourable",
|
||||||
|
"currency": "DKK"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"gl_code": "5000",
|
||||||
|
"gl_description": "Cloud Infrastructure",
|
||||||
|
"gl_type": "cogs",
|
||||||
|
"budget": 850000,
|
||||||
|
"actual": 791000,
|
||||||
|
"variance_abs": 59000,
|
||||||
|
"variance_pct": 6.94,
|
||||||
|
"status": "favourable",
|
||||||
|
"currency": "DKK"
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"total_budget": 5180000,
|
|
||||||
"total_actual": 5290000,
|
|
||||||
"total_variance": -110000,
|
|
||||||
"total_variance_pct": -2.1
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Why Go for a Finance API?
|
## Finance Concepts
|
||||||
|
|
||||||
Go's compile-time type safety, zero-cost abstractions, and simple concurrency model make it well-suited for financial data services:
|
**Favourability convention** — whether a variance is good or bad depends on the account type. Revenue accounts are favourable when actuals exceed budget. Cost accounts are favourable when actuals come in under budget. The engine handles both correctly using a `favour_high` flag on each GL account.
|
||||||
|
|
||||||
- **No null pointer surprises** — strict typing prevents the class of bugs that corrupt financial calculations
|
**Budget versioning** — original budget and up to three forecast revisions are stored separately, enabling budget vs. forecast vs. actual three-way comparison without overwriting history.
|
||||||
- **Fast and predictable** — consistent sub-10ms response times under load
|
|
||||||
- **Easy to deploy** — single binary, no runtime dependencies, runs anywhere
|
**GL account types** — accounts are typed as `revenue`, `cogs`, `opex`, `capex`, or `headcount`. This drives both the P&L rollup structure and the favourability logic.
|
||||||
- **Explicit error handling** — every failure path is handled, not swallowed
|
|
||||||
|
**Fiscal periods** — stored as integers 1–12, decoupled from calendar months so the schema supports fiscal years that start in any month.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Roadmap
|
## Configuration
|
||||||
|
|
||||||
- [ ] Multi-currency support with FX rate table
|
```bash
|
||||||
- [ ] Excel export of variance reports (finance teams need this)
|
# .env.example
|
||||||
- [ ] Webhook notifications when accounts breach threshold
|
DB_PATH=fpa.db # Path to SQLite file. Use :memory: for tests.
|
||||||
- [ ] Integration adapter for SAP/NetSuite actuals export format
|
PORT=8080
|
||||||
- [ ] Role-based access (department managers see only their cost centers)
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why Go for a Finance API
|
||||||
|
|
||||||
|
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, handles concurrency safely, and makes every error path explicit. The result is an API that behaves predictably under load and is straightforward to deploy on any server or container without a setup checklist.
|
||||||
Reference in New Issue
Block a user