125 lines
3.2 KiB
Go
125 lines
3.2 KiB
Go
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
|
|
// RevenueCategory is now just a string — categories are dynamic per company
|
|
type RevenueCategory = string
|
|
|
|
// Built-in reserved categories
|
|
const (
|
|
CategoryTotal RevenueCategory = "total" // always required, never custom
|
|
)
|
|
|
|
// CategoryDef defines a custom category for a specific company
|
|
// e.g. Company: Apple, Name: "product", Labels: ["iPhone", "Mac", "Services"]
|
|
type CategoryDef struct {
|
|
ID int
|
|
Company *Company
|
|
Name string // e.g. "product", "location", "segment"
|
|
Labels []string // populated from category_labels table
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// SumByCategory returns a map of category → sum for all entries
|
|
func (r *RevenueReport) SumByCategory() map[RevenueCategory]float64 {
|
|
sums := make(map[RevenueCategory]float64)
|
|
for _, e := range r.Entries {
|
|
sums[e.Category] += e.Value
|
|
}
|
|
return sums
|
|
}
|
|
|
|
// Validate checks every category independently sums to total
|
|
func (r *RevenueReport) Validate() (bool, map[RevenueCategory]float64) {
|
|
total := r.Total(CategoryTotal)
|
|
sums := r.SumByCategory()
|
|
delete(sums, CategoryTotal) // don't compare total to itself
|
|
|
|
allMatch := true
|
|
for cat, sum := range sums {
|
|
if sum != total {
|
|
allMatch = false
|
|
_ = cat // caller can inspect the map to see which failed
|
|
}
|
|
}
|
|
return allMatch, sums
|
|
}
|
|
|
|
// RevSum aggregates revenue across multiple reports or periods
|
|
// Use this when you want e.g. FY = Q1+Q2+Q3+Q4
|
|
type RevSum struct {
|
|
Company *Company
|
|
Categories map[RevenueCategory]float64 // category → total across all periods
|
|
Labels map[string]float64 // label → total (e.g. "iPhone" across quarters)
|
|
Total float64
|
|
Periods []Period // which periods were summed
|
|
}
|
|
|
|
func NewRevSum(company *Company, reports []RevenueReport) *RevSum {
|
|
rs := &RevSum{
|
|
Company: company,
|
|
Categories: make(map[RevenueCategory]float64),
|
|
Labels: make(map[string]float64),
|
|
}
|
|
for _, report := range reports {
|
|
rs.Periods = append(rs.Periods, report.Period)
|
|
for _, e := range report.Entries {
|
|
if e.Category == CategoryTotal {
|
|
rs.Total += e.Value
|
|
continue
|
|
}
|
|
rs.Categories[e.Category] += e.Value
|
|
rs.Labels[e.Label] += e.Value
|
|
}
|
|
}
|
|
return rs
|
|
}
|