new rev sys
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PeriodType defines the granularity of the revenue entry
|
||||
type PeriodType string
|
||||
@@ -13,6 +17,7 @@ const (
|
||||
|
||||
// Period holds the actual time range for a revenue entry
|
||||
type Period struct {
|
||||
ID int
|
||||
Type PeriodType
|
||||
Year int
|
||||
Index int // Q1=1 Q2=2 Q3=3 Q4=4 | H1=1 H2=2 | FY=1
|
||||
@@ -20,22 +25,69 @@ type Period struct {
|
||||
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
|
||||
func (p *Period) Insert(db *sql.DB) error {
|
||||
_, err := db.Exec(
|
||||
`INSERT INTO periods (type, year, idx, start_date, end_date) VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(type, year, idx) DO UPDATE SET start_date=excluded.start_date, end_date=excluded.end_date`,
|
||||
string(p.Type), p.Year, p.Index, p.Start.Format("2006-01-02"), p.End.Format("2006-01-02"),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("upsert period: %w", err)
|
||||
}
|
||||
|
||||
// Built-in reserved categories
|
||||
const (
|
||||
CategoryTotal RevenueCategory = "total" // always required, never custom
|
||||
)
|
||||
var id int
|
||||
err = db.QueryRow(
|
||||
`SELECT id FROM periods WHERE type = ? AND year = ? AND idx = ?`,
|
||||
string(p.Type), p.Year, p.Index,
|
||||
).Scan(&id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("select period: %w", err)
|
||||
}
|
||||
p.ID = id
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
type RevenueCategory struct {
|
||||
ID int
|
||||
CompanyID int
|
||||
ParentID *int
|
||||
Name string // e.g. "product", "location", "segment"
|
||||
}
|
||||
|
||||
func (rc *RevenueCategory) Validate() error {
|
||||
if rc.ID == 0 {
|
||||
return fmt.Errorf("No ID Set")
|
||||
} else if rc.Name == "" {
|
||||
return fmt.Errorf("No Name found")
|
||||
} else if rc.CompanyID == 0 {
|
||||
return fmt.Errorf("No Company Set")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rc *RevenueCategory) Insert(db *sql.DB) error {
|
||||
if err := rc.Validate(); err != nil {
|
||||
return fmt.Errorf("failed to insert: %w", err)
|
||||
}
|
||||
|
||||
_, err := db.Exec(
|
||||
`INSERT INTO category (company_id, parent_id, name) VALUES (?, ?, ?)
|
||||
ON CONFLICT(company_id, name) DO UPDATE SET parent_id=excluded.parent_id`,
|
||||
rc.CompanyID, rc.ParentID, rc.Name,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("upsert category: %w", err)
|
||||
}
|
||||
|
||||
err = db.QueryRow(
|
||||
`SELECT id FROM category WHERE company_id = ? AND name = ?`,
|
||||
rc.CompanyID, rc.Name,
|
||||
).Scan(&rc.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("select category id: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Revenue is a single line in a financial report
|
||||
@@ -43,82 +95,7 @@ type Revenue struct {
|
||||
ID int
|
||||
Company *Company
|
||||
Currency *Currency
|
||||
Category RevenueCategory
|
||||
Label string // e.g. "North America", "iPhone", "Total"
|
||||
Category *RevenueCategory
|
||||
Period *Period
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user