new rev sys

This commit is contained in:
zipfriis
2026-03-24 19:42:49 +01:00
parent a4c5c4cb1d
commit 7e2c332e60
9 changed files with 263 additions and 395 deletions

Binary file not shown.

BIN
app.db

Binary file not shown.

View File

@@ -35,46 +35,28 @@ func InitDB(db *sql.DB) {
UNIQUE(type, year, idx) UNIQUE(type, year, idx)
); );
CREATE TABLE IF NOT EXISTS revenue_reports ( CREATE TABLE IF NOT EXISTS category (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
company_id INTEGER NOT NULL, company_id INTEGER NOT NULL,
period_id INTEGER NOT NULL, parent_id INTEGER,
name TEXT NOT NULL,
FOREIGN KEY (company_id) REFERENCES companies(id), FOREIGN KEY (company_id) REFERENCES companies(id),
FOREIGN KEY (period_id) REFERENCES periods(id), FOREIGN KEY (parent_id) REFERENCES category(id),
UNIQUE(company_id, period_id) UNIQUE(company_id, name)
); );
CREATE TABLE IF NOT EXISTS revenue_entries ( 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)
);
-- one row per category type per company
-- e.g. Apple has "product" and "location"
CREATE TABLE IF NOT EXISTS category_defs (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
company_id INTEGER NOT NULL, company_id INTEGER NOT NULL,
name TEXT NOT NULL, currency_id INTEGER NOT NULL,
category_id INTEGER NOT NULL,
period_id INTEGER NOT NULL,
value REAL NOT NULL,
FOREIGN KEY (company_id) REFERENCES companies(id), FOREIGN KEY (company_id) REFERENCES companies(id),
UNIQUE(company_id, name) FOREIGN KEY (currency_id) REFERENCES currencies(id),
); FOREIGN KEY (category_id) REFERENCES category(id),
FOREIGN KEY (period_id) REFERENCES periods(id)
-- the actual labels belonging to each category );`
-- e.g. category_def "product" → "iPhone", "Mac", "Services"
CREATE TABLE IF NOT EXISTS category_labels (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category_def_id INTEGER NOT NULL,
label TEXT NOT NULL,
FOREIGN KEY (category_def_id) REFERENCES category_defs(id),
UNIQUE(category_def_id, label)
);
`
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)

View File

@@ -0,0 +1,43 @@
package database
import (
"Portifolio/internal/model"
"database/sql"
"fmt"
"time"
_ "github.com/mattn/go-sqlite3"
)
func GetCategoryByName(db *sql.DB, companyID int, name string) (model.RevenueCategory, error) {
var rc model.RevenueCategory
err := db.QueryRow(
`SELECT id, company_id, parent_id, name FROM category WHERE company_id = ? AND name = ?`,
companyID, name,
).Scan(&rc.ID, &rc.CompanyID, &rc.ParentID, &rc.Name)
if err == sql.ErrNoRows {
return rc, fmt.Errorf("category %q not found for company %d", name, companyID)
}
if err != nil {
return rc, fmt.Errorf("get category by name: %w", err)
}
return rc, nil
}
func GetPeriodByID(db *sql.DB, periodID int) (model.Period, error) {
var p model.Period
var start, end string
err := db.QueryRow(
`SELECT type, year, idx, start_date, end_date FROM periods WHERE id = ?`,
periodID,
).Scan(&p.Type, &p.Year, &p.Index, &start, &end)
if err == sql.ErrNoRows {
return p, fmt.Errorf("period %d not found", periodID)
}
if err != nil {
return p, fmt.Errorf("get period by id: %w", err)
}
p.Start, _ = time.Parse("2006-01-02", start)
p.End, _ = time.Parse("2006-01-02", end)
return p, nil
}

View File

@@ -11,48 +11,6 @@ import (
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
) )
func AddRevenueReportHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var input struct {
CompanyID int `json:"company_id"`
PeriodType string `json:"period_type"`
Year int `json:"year"`
Index int `json:"index"`
}
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
var period model.Period
switch input.PeriodType {
case "Q":
period = model.QuarterPeriod(input.Year, input.Index)
case "H":
period = model.HalfYearPeriod(input.Year, input.Index)
case "Y":
period = model.FullYearPeriod(input.Year)
default:
http.Error(w, "invalid period_type (Q/H/Y)", http.StatusBadRequest)
return
}
id, err := service.InsertPeriod(db, period)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]any{
"status": "created",
"period_id": id,
"period": period.String(),
})
}
}
func AddRevenueEntryHandler(db *sql.DB) http.HandlerFunc { func AddRevenueEntryHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
var input struct { var input struct {
@@ -62,7 +20,6 @@ func AddRevenueEntryHandler(db *sql.DB) http.HandlerFunc {
Year int `json:"year"` Year int `json:"year"`
Index int `json:"index"` Index int `json:"index"`
Category string `json:"category"` Category string `json:"category"`
Label string `json:"label"`
Value float64 `json:"value"` Value float64 `json:"value"`
} }
if err := json.NewDecoder(r.Body).Decode(&input); err != nil { if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
@@ -83,7 +40,7 @@ func AddRevenueEntryHandler(db *sql.DB) http.HandlerFunc {
return return
} }
if err := service.InsertRevenue(db, input.CompanyID, input.CurrencyID, input.Category, input.Label, input.Value, period); err != nil { if err := service.InsertRevenue(db, input.CompanyID, input.CurrencyID, input.Category, nil, input.Value, period); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
@@ -111,20 +68,3 @@ func GetRevenueReportHandler(db *sql.DB) http.HandlerFunc {
json.NewEncoder(w).Encode(entries) json.NewEncoder(w).Encode(entries)
} }
} }
func GetRevenueSumHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
companyID, _ := strconv.Atoi(r.URL.Query().Get("company_id"))
periodType := r.URL.Query().Get("period_type")
year, _ := strconv.Atoi(r.URL.Query().Get("year"))
rs, err := service.SumRevenue(db, companyID, model.PeriodType(periodType), year)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(rs)
}
}

View File

@@ -1,6 +1,10 @@
package model package model
import "time" import (
"database/sql"
"fmt"
"time"
)
// PeriodType defines the granularity of the revenue entry // PeriodType defines the granularity of the revenue entry
type PeriodType string type PeriodType string
@@ -13,6 +17,7 @@ const (
// Period holds the actual time range for a revenue entry // Period holds the actual time range for a revenue entry
type Period struct { type Period struct {
ID int
Type PeriodType Type PeriodType
Year int Year int
Index int // Q1=1 Q2=2 Q3=3 Q4=4 | H1=1 H2=2 | FY=1 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 End time.Time
} }
// RevenueCategory is what the revenue is broken down by func (p *Period) Insert(db *sql.DB) error {
// RevenueCategory is now just a string — categories are dynamic per company _, err := db.Exec(
type RevenueCategory = string `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 var id int
const ( err = db.QueryRow(
CategoryTotal RevenueCategory = "total" // always required, never custom `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 type RevenueCategory struct {
// e.g. Company: Apple, Name: "product", Labels: ["iPhone", "Mac", "Services"]
type CategoryDef struct {
ID int ID int
Company *Company CompanyID int
ParentID *int
Name string // e.g. "product", "location", "segment" Name string // e.g. "product", "location", "segment"
Labels []string // populated from category_labels table }
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 // Revenue is a single line in a financial report
@@ -43,82 +95,7 @@ type Revenue struct {
ID int ID int
Company *Company Company *Company
Currency *Currency Currency *Currency
Category RevenueCategory Category *RevenueCategory
Label string // e.g. "North America", "iPhone", "Total" Period *Period
Value float64 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
} }

View File

@@ -1,6 +1,7 @@
package service package service
import ( import (
"Portifolio/internal/database"
"Portifolio/internal/model" "Portifolio/internal/model"
"database/sql" "database/sql"
"fmt" "fmt"
@@ -9,69 +10,64 @@ import (
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
) )
func InsertRevenue(db *sql.DB, companyID, currencyID int, category, label string, value float64, period model.Period) error { func InsertRevenue(db *sql.DB, companyID, currencyID int, categoryName string, parentID *int, value float64, period model.Period) error {
periodID, err := InsertPeriod(db, period)
period, err := database.GetPeriodByID(db, period.ID)
if err != nil {
err = period.Insert(db)
if err != nil { if err != nil {
return err return err
} }
}
reportID, err := getOrCreateReport(db, companyID, periodID) // Getting Category, if error, trying to insert the category with the company.
category, err := database.GetCategoryByName(db, companyID, categoryName)
if err != nil {
category = model.RevenueCategory{
CompanyID: companyID,
ParentID: parentID,
Name: categoryName,
}
err := category.Insert(db)
if err != nil { if err != nil {
return err return err
} }
}
_, err = db.Exec( _, err = db.Exec(
`INSERT INTO revenue_entries (report_id, currency_id, category, label, value) VALUES (?, ?, ?, ?, ?)`, `INSERT INTO revenue_entries (company_id, currency_id, category_id, period_id, value)
reportID, currencyID, category, label, value, VALUES (?, ?, ?, ?, ?)`,
) companyID, currencyID, category.ID, period.ID, value,
return err
}
func InsertPeriod(db *sql.DB, p model.Period) (int, error) {
res, 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`,
string(p.Type), p.Year, p.Index, p.Start.Format("2006-01-02"), p.End.Format("2006-01-02"),
) )
if err != nil { if err != nil {
return 0, err return fmt.Errorf("insert revenue_entries: %w", err)
} }
id, err := res.LastInsertId()
return int(id), err
}
func getOrCreateReport(db *sql.DB, companyID, periodID int) (int, error) { return nil
var id int
err := db.QueryRow(
`SELECT id FROM revenue_reports WHERE company_id = ? AND period_id = ?`,
companyID, periodID,
).Scan(&id)
if err == sql.ErrNoRows {
res, err := db.Exec(
`INSERT INTO revenue_reports (company_id, period_id) VALUES (?, ?)`,
companyID, periodID,
)
if err != nil {
return 0, err
}
lid, _ := res.LastInsertId()
return int(lid), nil
}
return id, err
} }
func GetRevenue(db *sql.DB, companyID int, periodType model.PeriodType, year, idx int) ([]model.Revenue, error) { func GetRevenue(db *sql.DB, companyID int, periodType model.PeriodType, year, idx int) ([]model.Revenue, error) {
rows, err := db.Query(` rows, err := db.Query(`
SELECT e.id, e.category, e.label, e.value, WITH RECURSIVE tree AS (
SELECT id, name, parent_id
FROM category
WHERE company_id = ? AND parent_id IS NULL
UNION ALL
SELECT c.id, c.name, c.parent_id
FROM category c
JOIN tree t ON c.parent_id = t.id
)
SELECT e.id, t.name, e.value,
p.type, p.year, p.idx, p.start_date, p.end_date p.type, p.year, p.idx, p.start_date, p.end_date
FROM revenue_entries e FROM revenue_entries e
JOIN revenue_reports r ON e.report_id = r.id JOIN tree t ON e.category_id = t.id
JOIN periods p ON r.period_id = p.id JOIN periods p ON e.period_id = p.id
WHERE r.company_id = ? AND p.type = ? AND p.year = ? AND p.idx = ?`, WHERE e.company_id = ? AND p.type = ? AND p.year = ? AND p.idx = ?`,
companyID, string(periodType), year, idx, companyID, companyID, string(periodType), year, idx,
) )
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("query revenue: %w", err)
} }
defer rows.Close() defer rows.Close()
@@ -80,56 +76,14 @@ func GetRevenue(db *sql.DB, companyID int, periodType model.PeriodType, year, id
var e model.Revenue var e model.Revenue
var p model.Period var p model.Period
var start, end string var start, end string
if err := rows.Scan(&e.ID, &e.Category, &e.Label, &e.Value, if err := rows.Scan(&e.ID, &e.Category, &e.Value,
&p.Type, &p.Year, &p.Index, &start, &end); err != nil { &p.Type, &p.Year, &p.Index, &start, &end); err != nil {
return nil, err return nil, fmt.Errorf("scan revenue row: %w", err)
} }
p.Start, _ = time.Parse("2006-01-02", start) p.Start, _ = time.Parse("2006-01-02", start)
p.End, _ = time.Parse("2006-01-02", end) p.End, _ = time.Parse("2006-01-02", end)
e.Period = p e.Period = &p
entries = append(entries, e) entries = append(entries, e)
} }
return entries, rows.Err() return entries, rows.Err()
} }
func SumRevenue(db *sql.DB, companyID int, periodType model.PeriodType, year int) (*model.RevSum, error) {
rows, err := db.Query(`
SELECT e.category, e.label, e.value, p.type, p.year, p.idx
FROM revenue_entries e
JOIN revenue_reports r ON e.report_id = r.id
JOIN periods p ON r.period_id = p.id
WHERE r.company_id = ? AND p.type = ? AND p.year = ?`,
companyID, string(periodType), year,
)
if err != nil {
return nil, err
}
defer rows.Close()
rs := &model.RevSum{
Categories: make(map[model.RevenueCategory]float64),
Labels: make(map[string]float64),
}
seen := map[string]bool{}
for rows.Next() {
var category, label string
var value float64
var pType model.PeriodType
var pYear, pIdx int
if err := rows.Scan(&category, &label, &value, &pType, &pYear, &pIdx); err != nil {
return nil, err
}
key := fmt.Sprintf("%s-%d-%d", pType, pYear, pIdx)
if !seen[key] {
rs.Periods = append(rs.Periods, model.Period{Type: pType, Year: pYear, Index: pIdx})
seen[key] = true
}
if category == model.CategoryTotal {
rs.Total += value
} else {
rs.Categories[category] += value
rs.Labels[label] += value
}
}
return rs, rows.Err()
}

View File

@@ -10,144 +10,120 @@ import (
"strings" "strings"
) )
func AddRevenue(scanner *bufio.Scanner, db *sql.DB) { func promptInt(scanner *bufio.Scanner, label string) (int, error) {
fmt.Print(" Company ID: ") fmt.Print(label)
scanner.Scan() scanner.Scan()
companyID, err := strconv.Atoi(strings.TrimSpace(scanner.Text())) v, err := strconv.Atoi(strings.TrimSpace(scanner.Text()))
if err != nil { if err != nil {
fmt.Println(" ✗ Invalid company ID") return 0, fmt.Errorf("invalid %s", label)
return }
return v, nil
}
func promptFloat(scanner *bufio.Scanner, label string) (float64, error) {
fmt.Print(label)
scanner.Scan()
v, err := strconv.ParseFloat(strings.TrimSpace(scanner.Text()), 64)
if err != nil {
return 0, fmt.Errorf("invalid %s", label)
}
return v, nil
}
func promptString(scanner *bufio.Scanner, label string) string {
fmt.Print(label)
scanner.Scan()
return strings.TrimSpace(scanner.Text())
}
func promptPeriod(scanner *bufio.Scanner) (model.Period, error) {
periodType := strings.ToUpper(promptString(scanner, " Period type (Q/H/Y): "))
year, err := promptInt(scanner, " Year: ")
if err != nil {
return model.Period{}, err
} }
fmt.Print(" Currency ID: ")
scanner.Scan()
currencyID, err := strconv.Atoi(strings.TrimSpace(scanner.Text()))
if err != nil {
fmt.Println(" ✗ Invalid currency ID")
return
}
fmt.Print(" Period type (Q/H/Y): ")
scanner.Scan()
periodType := strings.ToUpper(strings.TrimSpace(scanner.Text()))
fmt.Print(" Year: ")
scanner.Scan()
year, err := strconv.Atoi(strings.TrimSpace(scanner.Text()))
if err != nil {
fmt.Println(" ✗ Invalid year")
return
}
fmt.Print(" Index (Q: 1-4 | H: 1-2 | Y: 1): ")
scanner.Scan()
idx, err := strconv.Atoi(strings.TrimSpace(scanner.Text()))
if err != nil {
fmt.Println(" ✗ Invalid index")
return
}
fmt.Print(" Category (product/location/total): ")
scanner.Scan()
category := strings.TrimSpace(scanner.Text())
fmt.Print(" Label (e.g. iPhone, Americas): ")
scanner.Scan()
label := strings.TrimSpace(scanner.Text())
fmt.Print(" Value: ")
scanner.Scan()
value, err := strconv.ParseFloat(strings.TrimSpace(scanner.Text()), 64)
if err != nil {
fmt.Println(" ✗ Invalid value")
return
}
var period model.Period
switch periodType { switch periodType {
case "Q":
period = model.QuarterPeriod(year, idx)
case "H":
period = model.HalfYearPeriod(year, idx)
case "Y": case "Y":
period = model.FullYearPeriod(year) return model.FullYearPeriod(year), nil
case "Q", "H":
label := map[string]string{"Q": " Index (1-4): ", "H": " Index (1-2): "}[periodType]
idx, err := promptInt(scanner, label)
if err != nil {
return model.Period{}, err
}
if periodType == "Q" {
return model.QuarterPeriod(year, idx), nil
}
return model.HalfYearPeriod(year, idx), nil
default: default:
fmt.Println(" ✗ Invalid period type") return model.Period{}, fmt.Errorf("invalid period type: %s", periodType)
}
}
func AddRevenue(scanner *bufio.Scanner, db *sql.DB) {
companyID, err := promptInt(scanner, " Company ID: ")
if err != nil {
fmt.Println(" ✗", err)
return
}
currencyID, err := promptInt(scanner, " Currency ID: ")
if err != nil {
fmt.Println(" ✗", err)
return
}
period, err := promptPeriod(scanner)
if err != nil {
fmt.Println(" ✗", err)
return
}
category := promptString(scanner, " Category name: ")
var parentID *int
parentStr := promptString(scanner, " Parent category ID (leave blank for root): ")
if parentStr != "" {
pid, err := strconv.Atoi(parentStr)
if err != nil {
fmt.Println(" ✗ Invalid parent ID")
return
}
parentID = &pid
}
value, err := promptFloat(scanner, " Value: ")
if err != nil {
fmt.Println(" ✗", err)
return return
} }
if err := service.InsertRevenue(db, companyID, currencyID, category, label, value, period); err != nil { if err := service.InsertRevenue(db, companyID, currencyID, category, parentID, value, period); err != nil {
fmt.Println(" ✗ Error:", err) fmt.Println(" ✗ Error:", err)
return return
} }
fmt.Printf(" ✓ Revenue entry added: %s / %s = %.2f (%s)\n", category, label, value, period.String()) fmt.Printf(" ✓ Revenue added: %s = %.2f (%s)\n", category, value, period.String())
} }
func ListRevenue(scanner *bufio.Scanner, db *sql.DB) { func ListRevenue(scanner *bufio.Scanner, db *sql.DB) {
fmt.Print(" Company ID: ") companyID, err := promptInt(scanner, " Company ID: ")
scanner.Scan()
companyID, err := strconv.Atoi(strings.TrimSpace(scanner.Text()))
if err != nil { if err != nil {
fmt.Println(" ✗ Invalid company ID") fmt.Println(" ✗", err)
return
}
period, err := promptPeriod(scanner)
if err != nil {
fmt.Println(" ✗", err)
return return
} }
fmt.Print(" Period type (Q/H/Y): ") entries, err := service.GetRevenue(db, companyID, period.Type, period.Year, period.Index)
scanner.Scan()
periodType := strings.ToUpper(strings.TrimSpace(scanner.Text()))
fmt.Print(" Year: ")
scanner.Scan()
year, _ := strconv.Atoi(strings.TrimSpace(scanner.Text()))
fmt.Print(" Index: ")
scanner.Scan()
idx, _ := strconv.Atoi(strings.TrimSpace(scanner.Text()))
entries, err := service.GetRevenue(db, companyID, model.PeriodType(periodType), year, idx)
if err != nil { if err != nil {
fmt.Println(" ✗ Error:", err) fmt.Println(" ✗ Error:", err)
return return
} }
fmt.Printf("\n %-12s %-20s %12s\n", "CATEGORY", "LABEL", "VALUE") fmt.Printf("\n %-20s %12s\n", "CATEGORY", "VALUE")
fmt.Println(" " + strings.Repeat("-", 46)) fmt.Println(" " + strings.Repeat("-", 34))
for _, e := range entries { for _, e := range entries {
fmt.Printf(" %-12s %-20s %12.2f\n", e.Category, e.Label, e.Value) fmt.Printf(" %-20s %12.2f\n", e.Category, e.Value)
}
}
func SumRevenue(scanner *bufio.Scanner, db *sql.DB) {
fmt.Print(" Company ID: ")
scanner.Scan()
companyID, err := strconv.Atoi(strings.TrimSpace(scanner.Text()))
if err != nil {
fmt.Println(" ✗ Invalid company ID")
return
}
fmt.Print(" Period type to sum (Q/H/Y): ")
scanner.Scan()
periodType := strings.ToUpper(strings.TrimSpace(scanner.Text()))
fmt.Print(" Year: ")
scanner.Scan()
year, _ := strconv.Atoi(strings.TrimSpace(scanner.Text()))
rs, err := service.SumRevenue(db, companyID, model.PeriodType(periodType), year)
if err != nil {
fmt.Println(" ✗ Error:", err)
return
}
fmt.Printf("\n Revenue Sum — FY%d\n", year)
fmt.Printf(" Total: %.2f\n\n", rs.Total)
fmt.Println(" By Category:")
for cat, sum := range rs.Categories {
fmt.Printf(" %-15s %.2f\n", cat, sum)
}
fmt.Println("\n By Label:")
for label, sum := range rs.Labels {
fmt.Printf(" %-20s %.2f\n", label, sum)
} }
} }

View File

@@ -42,10 +42,8 @@ func main() {
http.HandleFunc("GET /currencies", handlers.GetCurrenciesHandler(db)) http.HandleFunc("GET /currencies", handlers.GetCurrenciesHandler(db))
// Revenue // Revenue
http.HandleFunc("POST /add/revenue/report", handlers.AddRevenueReportHandler(db))
http.HandleFunc("POST /add/revenue/entry", handlers.AddRevenueEntryHandler(db)) http.HandleFunc("POST /add/revenue/entry", handlers.AddRevenueEntryHandler(db))
http.HandleFunc("GET /revenue/report", handlers.GetRevenueReportHandler(db)) http.HandleFunc("GET /revenue/report", handlers.GetRevenueReportHandler(db))
http.HandleFunc("GET /revenue/sum", handlers.GetRevenueSumHandler(db))
fmt.Println("Server running on :8080") fmt.Println("Server running on :8080")
go func() { go func() {
@@ -88,8 +86,6 @@ func runShell(db *sql.DB) {
shell.AddRevenue(scanner, db) shell.AddRevenue(scanner, db)
case "list-revenue": case "list-revenue":
shell.ListRevenue(scanner, db) shell.ListRevenue(scanner, db)
case "sum-revenue":
shell.SumRevenue(scanner, db)
case "help": case "help":
fmt.Println("\nCommands:") fmt.Println("\nCommands:")