adding revenue
This commit is contained in:
@@ -10,20 +10,50 @@ import (
|
|||||||
|
|
||||||
func InitDB(db *sql.DB) {
|
func InitDB(db *sql.DB) {
|
||||||
schema := `
|
schema := `
|
||||||
CREATE TABLE IF NOT EXISTS currencies (
|
CREATE TABLE IF NOT EXISTS currencies (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
code TEXT NOT NULL UNIQUE,
|
code TEXT NOT NULL UNIQUE,
|
||||||
name TEXT NOT NULL
|
name TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS companies (
|
CREATE TABLE IF NOT EXISTS companies (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL UNIQUE,
|
||||||
shares_outstanding INTEGER NOT NULL,
|
shares_outstanding INTEGER NOT NULL,
|
||||||
price REAL NOT NULL,
|
price REAL NOT NULL,
|
||||||
currency_id INTEGER NOT NULL,
|
currency_id INTEGER NOT NULL,
|
||||||
FOREIGN KEY (currency_id) REFERENCES currencies(id)
|
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 {
|
if _, err := db.Exec(schema); err != nil {
|
||||||
log.Fatal("Failed to create tables:", err)
|
log.Fatal("Failed to create tables:", err)
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
type Currency struct {
|
|
||||||
ID int
|
|
||||||
Code string
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Company struct {
|
type Company struct {
|
||||||
ID int
|
ID int
|
||||||
Name string
|
Name string
|
||||||
@@ -21,8 +15,3 @@ type CompanyInput struct {
|
|||||||
Price float64 `json:"price"`
|
Price float64 `json:"price"`
|
||||||
CurrencyID int `json:"currency_id"`
|
CurrencyID int `json:"currency_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CurrencyInput struct {
|
|
||||||
Code string `json:"code"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|||||||
12
internal/model/currency.go
Normal file
12
internal/model/currency.go
Normal file
@@ -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"`
|
||||||
|
}
|
||||||
52
internal/model/periode.go
Normal file
52
internal/model/periode.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
69
internal/model/revenue.go
Normal file
69
internal/model/revenue.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user