Files
Portfolio-Engine/internal/model/revenue.go
2026-03-24 12:09:19 +01:00

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
}