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 }