341 lines
8.8 KiB
Markdown
341 lines
8.8 KiB
Markdown
# Portfolio Engine
|
||
|
||
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
|
||
- 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
|
||
├── go.mod / go.sum
|
||
└── internal/
|
||
├── database/
|
||
│ └── main.go # Schema init (InitDB) — all CREATE TABLE statements
|
||
├── handlers/
|
||
│ ├── main.go # HealthHandler
|
||
│ ├── currency.go # AddCurrencyHandler, GetCurrenciesHandler
|
||
│ └── revenue.go # AddRevenueEntryHandler, GetRevenueReportHandler, GetRevenueSumHandler
|
||
├── model/
|
||
│ ├── 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/
|
||
│ ├── main.go # Service wiring
|
||
│ ├── company.go # Company business logic
|
||
│ ├── currency.go # Currency business logic
|
||
│ └── revenue.go # Revenue aggregation logic
|
||
└── shell/
|
||
├── company.go # add-company, list-companies
|
||
├── currency.go # add-currency, list-currency
|
||
└── revenue.go # add-revenue, list-revenue, sum-revenue
|
||
```
|
||
|
||
## Getting Started
|
||
|
||
### Prerequisites
|
||
|
||
- Go 1.21+
|
||
- GCC (required for SQLite CGO bindings)
|
||
- Ubuntu/Debian: `sudo apt install gcc`
|
||
- macOS: comes with Xcode Command Line Tools
|
||
|
||
### Install & Run
|
||
|
||
```bash
|
||
git clone git@git.samantha42.xyz:samantha/Portifolio-Engine.git
|
||
cd Portifolio-Engine
|
||
go mod download
|
||
go run main.go
|
||
```
|
||
|
||
Or build first:
|
||
|
||
```bash
|
||
chmod +x build.sh
|
||
./build.sh
|
||
./Portifolio
|
||
```
|
||
|
||
On startup:
|
||
|
||
```
|
||
Connected to SQLite database
|
||
Tables ready
|
||
Server running on :8080
|
||
|
||
Shell ready. Type 'help' for commands.
|
||
>
|
||
```
|
||
|
||
The HTTP server and shell run concurrently — the API is live while you type shell commands.
|
||
|
||
---
|
||
|
||
## REST API
|
||
|
||
### `GET /health`
|
||
|
||
Returns server status, DB connection info, memory usage, and uptime.
|
||
|
||
```bash
|
||
curl http://localhost:8080/health
|
||
```
|
||
|
||
```json
|
||
{
|
||
"status": "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` with `"status": "degraded"` if the database is unreachable.
|
||
|
||
---
|
||
|
||
### `POST /add/company`
|
||
|
||
```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 '{
|
||
"company_id": 1,
|
||
"currency_id": 1,
|
||
"period_type": "Q",
|
||
"year": 2025,
|
||
"index": 1,
|
||
"category": "product",
|
||
"label": "iPhone",
|
||
"value": 69143
|
||
}'
|
||
```
|
||
|
||
`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
|
||
|
||
| 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
|
||
|
||
```
|
||
> add-currency
|
||
Code (e.g. DKK): DKK
|
||
Name (e.g. Danish Krone): Danish Krone
|
||
✓ Currency 'Danish Krone' (DKK) added with ID 1
|
||
|
||
> add-company
|
||
Name: Novo Nordisk
|
||
Shares outstanding: 4442064180
|
||
Price: 251.00
|
||
Currency ID: 1
|
||
✓ 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
|
||
CREATE TABLE currencies (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
code TEXT NOT NULL UNIQUE,
|
||
name TEXT NOT NULL
|
||
);
|
||
|
||
CREATE TABLE companies (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name TEXT NOT NULL UNIQUE,
|
||
shares_outstanding INTEGER NOT NULL,
|
||
price REAL NOT NULL,
|
||
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 /company/{id}/revenue` — full revenue history for a company
|
||
- Price update endpoint
|
||
- Market cap calculation (price × shares outstanding)
|
||
- Multi-currency conversion
|
||
- Frontend dashboard |