diff --git a/README.md b/README.md index 6148785..121fc01 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,45 @@ # Portfolio Engine -A lightweight portfolio tracking backend written in Go. Replaces spreadsheets with a proper database, REST API, and an interactive shell — all in a single binary. +A lightweight portfolio and financial data backend written in Go. Tracks companies, currencies, and revenue reports across configurable time periods — replacing spreadsheets with a proper database, REST API, and interactive shell. ## Features - SQLite database with foreign key enforcement - REST API on port `8080` - Interactive shell for managing data without an HTTP client -- Tracks companies with share count, price, and currency +- Revenue tracking by period (quarterly, half-year, full year) +- Revenue broken down by custom categories (product, location, segment...) +- Health endpoint with DB latency, memory, and uptime stats ## Project Structure ``` . -├── main.go # Entry point, HTTP routes, shell loop -├── app.db # SQLite database (auto-created on first run) -├── build.sh # Build script +├── main.go # Entry point — HTTP routes + shell loop +├── app.db # SQLite database (auto-created on first run) +├── build.sh # Build script ├── go.mod / go.sum └── internal/ ├── database/ - │ └── main.go # Schema init (InitDB) + │ └── main.go # Schema init (InitDB) — all CREATE TABLE statements ├── handlers/ - │ └── main.go # HTTP handlers (HealthHandler, AddCompanyHandler) + │ ├── main.go # HealthHandler + │ ├── currency.go # AddCurrencyHandler, GetCurrenciesHandler + │ └── revenue.go # AddRevenueEntryHandler, GetRevenueReportHandler, GetRevenueSumHandler ├── model/ - │ └── company.go # Structs + SQL queries (Company, Currency, CompanyInput) + │ ├── company.go # Company struct + SQL (InsertCompany, GetAllCompanies) + │ ├── currency.go # Currency struct + SQL (InsertCurrency, GetAllCurrencies) + │ ├── periode.go # Period struct + helpers (QuarterPeriod, HalfYearPeriod, FullYearPeriod) + │ └── revenue.go # Revenue, RevenueReport, RevSum structs + SQL ├── service/ - │ ├── company.go # Company business logic - │ ├── currency.go # Currency business logic - │ └── main.go # Service setup + │ ├── main.go # Service wiring + │ ├── company.go # Company business logic + │ ├── currency.go # Currency business logic + │ └── revenue.go # Revenue aggregation logic └── shell/ - ├── company.go # Shell commands for companies - └── currency.go # Shell commands for currencies + ├── company.go # add-company, list-companies + ├── currency.go # add-currency, list-currency + └── revenue.go # add-revenue, list-revenue, sum-revenue ``` ## Getting Started @@ -51,7 +60,7 @@ go mod download go run main.go ``` -Or use the build script: +Or build first: ```bash chmod +x build.sh @@ -59,23 +68,26 @@ chmod +x build.sh ./Portifolio ``` -On startup you'll see: +On startup: ``` Connected to SQLite database +Tables ready Server running on :8080 -Shell ready. Commands: add-company, help, exit +Shell ready. Type 'help' for commands. > ``` -The HTTP server and the shell run concurrently — the API is live while you type shell commands. +The HTTP server and shell run concurrently — the API is live while you type shell commands. + +--- ## REST API ### `GET /health` -Returns the status of the server and database connection. +Returns server status, DB connection info, memory usage, and uptime. ```bash curl http://localhost:8080/health @@ -84,44 +96,126 @@ curl http://localhost:8080/health ```json { "status": "ok", - "database": "ok" + "uptime": "4m32s", + "database": { + "status": "ok", + "latency": "121µs", + "open_connections": 1, + "in_use": 0, + "idle": 1 + }, + "memory": { + "alloc_mb": 2.31, + "sys_mb": 9.44, + "num_gc": 3 + }, + "go_version": "go1.22.0", + "goroutines": 4 } ``` -Returns `503` if the database is unreachable. +Returns `503` with `"status": "degraded"` if the database is unreachable. --- ### `POST /add/company` -Add a new company. The `currency_id` must reference an existing currency in the database. - ```bash curl -X POST http://localhost:8080/add/company \ + -H "Content-Type: application/json" \ + -d '{"name":"Novo Nordisk","shares_outstanding":4442064180,"price":251.00,"currency_id":1}' +``` + +--- + +### `GET /companies` + +```bash +curl http://localhost:8080/companies +``` + +--- + +### `POST /add/currency` + +```bash +curl -X POST http://localhost:8080/add/currency \ + -H "Content-Type: application/json" \ + -d '{"code":"DKK","name":"Danish Krone"}' +``` + +--- + +### `GET /currencies` + +```bash +curl http://localhost:8080/currencies +``` + +--- + +### `POST /add/revenue/entry` + +Add a single revenue line to a report. The period is created automatically if it doesn't exist. + +```bash +curl -X POST http://localhost:8080/add/revenue/entry \ -H "Content-Type: application/json" \ -d '{ - "name": "Novo Nordisk", - "shares_outstanding": 4442064180, - "price": 251.00, - "currency_id": 1 + "company_id": 1, + "currency_id": 1, + "period_type": "Q", + "year": 2025, + "index": 1, + "category": "product", + "label": "iPhone", + "value": 69143 }' ``` -```json -{ "status": "created" } +`period_type` is one of: + +| Value | Meaning | `index` range | +|-------|-----------|---------------| +| `Q` | Quarter | 1–4 | +| `H` | Half-year | 1–2 | +| `Y` | Full year | 1 | + +--- + +### `GET /revenue/report` + +Get all revenue entries for a company and period. + +```bash +curl "http://localhost:8080/revenue/report?company_id=1&period_type=Q&year=2025&index=1" ``` +--- + +### `GET /revenue/sum` + +Sum all revenue entries for a company across all periods of a given type in a year. + +```bash +curl "http://localhost:8080/revenue/sum?company_id=1&period_type=Q&year=2025" +``` + +--- + ## Shell Commands -The shell starts automatically after the server and accepts commands interactively. - -| Command | Description | -|------------------|--------------------------------------| -| `add-currency` | Add a new currency interactively | -| `list-currency` | List all currencies with their IDs | -| `add-company` | Add a new company interactively | -| `help` | Show available commands | -| `exit` | Quit the application | +| Command | Description | +|------------------|-----------------------------------------------| +| `add-company` | Add a new company interactively | +| `list-companies` | List all companies with price and share count | +| `add-currency` | Add a new currency interactively | +| `list-currency` | List all currencies with their IDs | +| `add-revenue` | Add a revenue entry interactively | +| `list-revenue` | List revenue entries for a company/period | +| `sum-revenue` | Sum revenue across all periods in a year | +| `help` | Show all available commands | +| `exit` | Quit | ### Example session @@ -131,19 +225,46 @@ The shell starts automatically after the server and accepts commands interactive Name (e.g. Danish Krone): Danish Krone ✓ Currency 'Danish Krone' (DKK) added with ID 1 -> list-currency - ID CODE NAME - ------------------------------ - 1 DKK Danish Krone - > add-company - Name: Maersk - Shares outstanding: 15224309 - Price: 15270.00 + Name: Novo Nordisk + Shares outstanding: 4442064180 + Price: 251.00 Currency ID: 1 - ✓ Company 'Maersk' added. + ✓ Company 'Novo Nordisk' added. + +> add-revenue + Company ID: 1 + Currency ID: 1 + Period type (Q/H/Y): Q + Year: 2025 + Index (Q: 1-4 | H: 1-2 | Y: 1): 1 + Category (product/location/total): product + Label (e.g. iPhone, Americas): Diabetes Care + Value: 54200 + ✓ Revenue entry added: product / Diabetes Care = 54200.00 (Q1 2025) + +> sum-revenue + Company ID: 1 + Period type to sum (Q/H/Y): Q + Year: 2025 + + Revenue Sum — FY2025 + Total: 54200.00 + + By Category: + product 54200.00 + + By Label: + Diabetes Care 54200.00 + +> list-companies + ID NAME CURRENCY PRICE SHARES + ------------------------------------------------------------ + 1 Novo Nordisk DKK 251.00 4442064180 ``` +--- + ## Database Schema ```sql @@ -161,13 +282,60 @@ CREATE TABLE companies ( currency_id INTEGER NOT NULL, FOREIGN KEY (currency_id) REFERENCES currencies(id) ); + +CREATE TABLE periods ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT NOT NULL CHECK(type IN ('Q', 'H', 'Y')), + year INTEGER NOT NULL, + idx INTEGER NOT NULL, + start_date TEXT NOT NULL, + end_date TEXT NOT NULL, + UNIQUE(type, year, idx) +); + +CREATE TABLE revenue_reports ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + company_id INTEGER NOT NULL, + period_id INTEGER NOT NULL, + FOREIGN KEY (company_id) REFERENCES companies(id), + FOREIGN KEY (period_id) REFERENCES periods(id), + UNIQUE(company_id, period_id) +); + +CREATE TABLE revenue_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + report_id INTEGER NOT NULL, + currency_id INTEGER NOT NULL, + category TEXT NOT NULL, + label TEXT NOT NULL, + value REAL NOT NULL, + FOREIGN KEY (report_id) REFERENCES revenue_reports(id), + FOREIGN KEY (currency_id) REFERENCES currencies(id) +); + +CREATE TABLE category_defs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + company_id INTEGER NOT NULL, + name TEXT NOT NULL, + FOREIGN KEY (company_id) REFERENCES companies(id), + UNIQUE(company_id, name) +); + +CREATE TABLE category_labels ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + category_def_id INTEGER NOT NULL, + label TEXT NOT NULL, + FOREIGN KEY (category_def_id) REFERENCES category_defs(id), + UNIQUE(category_def_id, label) +); ``` +--- + ## Roadmap -- `GET /companies` — list all companies -- `GET /currencies` — list all currencies +- `GET /company/{id}/revenue` — full revenue history for a company - Price update endpoint -- Market cap calculation (price × shares) +- Market cap calculation (price × shares outstanding) - Multi-currency conversion - Frontend dashboard \ No newline at end of file