From 33b100f3251cea5efacd8a5826d64a207beb81e1 Mon Sep 17 00:00:00 2001 From: samantha42 Date: Tue, 24 Mar 2026 11:37:45 +0100 Subject: [PATCH] adding revenue --- internal/database/main.go | 56 ++++++++++++++++++++++++------- internal/model/company.go | 11 ------ internal/model/currency.go | 12 +++++++ internal/model/periode.go | 52 ++++++++++++++++++++++++++++ internal/model/revenue.go | 69 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 176 insertions(+), 24 deletions(-) create mode 100644 internal/model/currency.go create mode 100644 internal/model/periode.go create mode 100644 internal/model/revenue.go diff --git a/internal/database/main.go b/internal/database/main.go index 4a5faad..dce51b9 100644 --- a/internal/database/main.go +++ b/internal/database/main.go @@ -10,20 +10,50 @@ import ( func InitDB(db *sql.DB) { schema := ` - CREATE TABLE IF NOT EXISTS currencies ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - code TEXT NOT NULL UNIQUE, - name TEXT NOT NULL - ); + CREATE TABLE IF NOT EXISTS currencies ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + code TEXT NOT NULL UNIQUE, + name TEXT NOT NULL + ); - CREATE TABLE IF NOT EXISTS companies ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - shares_outstanding INTEGER NOT NULL, - price REAL NOT NULL, - currency_id INTEGER NOT NULL, - FOREIGN KEY (currency_id) REFERENCES currencies(id) - );` + CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS revenue_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + report_id INTEGER NOT NULL, + currency_id INTEGER NOT NULL, + category TEXT NOT NULL CHECK(category IN ('product', 'location', 'total')), + label TEXT NOT NULL, + value REAL NOT NULL, + FOREIGN KEY (report_id) REFERENCES revenue_reports(id), + FOREIGN KEY (currency_id) REFERENCES currencies(id) + );` if _, err := db.Exec(schema); err != nil { log.Fatal("Failed to create tables:", err) diff --git a/internal/model/company.go b/internal/model/company.go index 92c3260..74ee95c 100644 --- a/internal/model/company.go +++ b/internal/model/company.go @@ -1,11 +1,5 @@ package model -type Currency struct { - ID int - Code string - Name string -} - type Company struct { ID int Name string @@ -21,8 +15,3 @@ type CompanyInput struct { Price float64 `json:"price"` CurrencyID int `json:"currency_id"` } - -type CurrencyInput struct { - Code string `json:"code"` - Name string `json:"name"` -} diff --git a/internal/model/currency.go b/internal/model/currency.go new file mode 100644 index 0000000..2e488d2 --- /dev/null +++ b/internal/model/currency.go @@ -0,0 +1,12 @@ +package model + +type Currency struct { + ID int + Code string + Name string +} + +type CurrencyInput struct { + Code string `json:"code"` + Name string `json:"name"` +} diff --git a/internal/model/periode.go b/internal/model/periode.go new file mode 100644 index 0000000..c909907 --- /dev/null +++ b/internal/model/periode.go @@ -0,0 +1,52 @@ +// model/period.go +package model + +import ( + "fmt" + "time" +) + +func QuarterPeriod(year, q int) Period { + months := map[int][2]int{ + 1: {1, 3}, 2: {4, 6}, 3: {7, 9}, 4: {10, 12}, + } + m := months[q] + return Period{ + Type: PeriodQuarter, + Year: year, + Index: q, + Start: date(year, m[0], 1), + End: date(year, m[1]+1, 0), // last day of end month + } +} + +func HalfYearPeriod(year, h int) Period { + if h == 1 { + return Period{Type: PeriodHalfYear, Year: year, Index: 1, + Start: date(year, 1, 1), End: date(year, 7, 0)} + } + return Period{Type: PeriodHalfYear, Year: year, Index: 2, + Start: date(year, 7, 1), End: date(year, 12, 31)} +} + +func FullYearPeriod(year int) Period { + return Period{Type: PeriodYear, Year: year, Index: 1, + Start: date(year, 1, 1), End: date(year, 12, 31)} +} + +func (p Period) String() string { + switch p.Type { + case PeriodQuarter: + return fmt.Sprintf("Q%d %d", p.Index, p.Year) + case PeriodHalfYear: + return fmt.Sprintf("H%d %d", p.Index, p.Year) + case PeriodYear: + return fmt.Sprintf("FY%d", p.Year) + } + return "unknown" +} + +func date(year, month, day int) time.Time { + // day=0 means last day of previous month + return time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC) +} diff --git a/internal/model/revenue.go b/internal/model/revenue.go new file mode 100644 index 0000000..b48b25f --- /dev/null +++ b/internal/model/revenue.go @@ -0,0 +1,69 @@ +package model + +import "time" + +// PeriodType defines the granularity of the revenue entry +type PeriodType string + +const ( + PeriodQuarter PeriodType = "Q" + PeriodHalfYear PeriodType = "H" + PeriodYear PeriodType = "Y" +) + +// Period holds the actual time range for a revenue entry +type Period struct { + Type PeriodType + Year int + Index int // Q1=1 Q2=2 Q3=3 Q4=4 | H1=1 H2=2 | FY=1 + Start time.Time + End time.Time +} + +// RevenueCategory is what the revenue is broken down by +type RevenueCategory string + +const ( + CategoryProduct RevenueCategory = "product" + CategoryLocation RevenueCategory = "location" + CategoryTotal RevenueCategory = "total" +) + +// Revenue is a single line in a financial report +type Revenue struct { + ID int + Company *Company + Currency *Currency + Category RevenueCategory + Label string // e.g. "North America", "iPhone", "Total" + Value float64 + Period Period +} + +// RevenueReport groups revenue lines for a company/period +// and verifies that sub-categories sum to total +type RevenueReport struct { + Company *Company + Period Period + Entries []Revenue +} + +// Total returns the sum of all entries matching a category +func (r *RevenueReport) Total(category RevenueCategory) float64 { + var sum float64 + for _, e := range r.Entries { + if e.Category == category { + sum += e.Value + } + } + return sum +} + +// Validate checks that product + location sums match the total entry +func (r *RevenueReport) Validate() (bool, float64, float64) { + total := r.Total(CategoryTotal) + products := r.Total(CategoryProduct) + locations := r.Total(CategoryLocation) + // both breakdowns should equal the total + return products == total && locations == total, products, locations +}