adding revenue handlers and shell
This commit is contained in:
@@ -53,7 +53,28 @@ func InitDB(db *sql.DB) {
|
||||
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,
|
||||
company_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
FOREIGN KEY (company_id) REFERENCES companies(id),
|
||||
UNIQUE(company_id, name)
|
||||
);
|
||||
|
||||
-- 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 {
|
||||
log.Fatal("Failed to create tables:", err)
|
||||
|
||||
44
internal/handlers/currency.go
Normal file
44
internal/handlers/currency.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"Portifolio/internal/model"
|
||||
"Portifolio/internal/service"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func AddCurrencyHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var input model.CurrencyInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
http.Error(w, "invalid json", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := service.InsertCurrency(db, input)
|
||||
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", "id": id})
|
||||
}
|
||||
}
|
||||
|
||||
func GetCurrenciesHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
currencies, err := service.GetAllCurrencies(db)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(currencies)
|
||||
}
|
||||
}
|
||||
@@ -6,47 +6,104 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
var startTime = time.Now()
|
||||
|
||||
func HealthHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
dbStatus := "ok"
|
||||
if err := db.Ping(); err != nil {
|
||||
dbStatus = "error: " + err.Error()
|
||||
health := checkHealth(db)
|
||||
|
||||
if health["status"] != "ok" {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ok",
|
||||
"database": dbStatus,
|
||||
})
|
||||
json.NewEncoder(w).Encode(health)
|
||||
}
|
||||
}
|
||||
|
||||
func checkHealth(db *sql.DB) map[string]any {
|
||||
// database
|
||||
dbStatus := "ok"
|
||||
dbLatency := ""
|
||||
t := time.Now()
|
||||
if err := db.Ping(); err != nil {
|
||||
dbStatus = "error: " + err.Error()
|
||||
} else {
|
||||
dbLatency = time.Since(t).String()
|
||||
}
|
||||
|
||||
// db pool stats
|
||||
stats := db.Stats()
|
||||
|
||||
// overall status
|
||||
status := "ok"
|
||||
if dbStatus != "ok" {
|
||||
status = "degraded"
|
||||
}
|
||||
|
||||
var mem runtime.MemStats
|
||||
runtime.ReadMemStats(&mem)
|
||||
|
||||
return map[string]any{
|
||||
"status": status,
|
||||
"uptime": time.Since(startTime).String(),
|
||||
"database": map[string]any{
|
||||
"status": dbStatus,
|
||||
"latency": dbLatency,
|
||||
"open_connections": stats.OpenConnections,
|
||||
"in_use": stats.InUse,
|
||||
"idle": stats.Idle,
|
||||
},
|
||||
"memory": map[string]any{
|
||||
"alloc_mb": bToMb(mem.Alloc),
|
||||
"sys_mb": bToMb(mem.Sys),
|
||||
"num_gc": mem.NumGC,
|
||||
},
|
||||
"go_version": runtime.Version(),
|
||||
"goroutines": runtime.NumGoroutine(),
|
||||
}
|
||||
}
|
||||
|
||||
func bToMb(b uint64) float64 {
|
||||
return float64(b) / 1024 / 1024
|
||||
}
|
||||
|
||||
func AddCompanyHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var input model.CompanyInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
http.Error(w, "invalid json", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := service.AddCompany(input, db); err != nil {
|
||||
id, err := service.InsertCompany(db, input)
|
||||
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]string{"status": "created"})
|
||||
json.NewEncoder(w).Encode(map[string]any{"status": "created", "id": id})
|
||||
}
|
||||
}
|
||||
|
||||
func GetCompaniesHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
companies, err := service.GetAllCompanies(db)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(companies)
|
||||
}
|
||||
}
|
||||
|
||||
130
internal/handlers/revenue.go
Normal file
130
internal/handlers/revenue.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"Portifolio/internal/model"
|
||||
"Portifolio/internal/service"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
_ "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 {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var input struct {
|
||||
CompanyID int `json:"company_id"`
|
||||
CurrencyID int `json:"currency_id"`
|
||||
PeriodType string `json:"period_type"`
|
||||
Year int `json:"year"`
|
||||
Index int `json:"index"`
|
||||
Category string `json:"category"`
|
||||
Label string `json:"label"`
|
||||
Value float64 `json:"value"`
|
||||
}
|
||||
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", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := service.InsertRevenue(db, input.CompanyID, input.CurrencyID, input.Category, input.Label, input.Value, period); 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]string{"status": "created"})
|
||||
}
|
||||
}
|
||||
|
||||
func GetRevenueReportHandler(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"))
|
||||
idx, _ := strconv.Atoi(r.URL.Query().Get("index"))
|
||||
|
||||
entries, err := service.GetRevenue(db, companyID, model.PeriodType(periodType), year, idx)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -21,14 +21,23 @@ type Period struct {
|
||||
}
|
||||
|
||||
// RevenueCategory is what the revenue is broken down by
|
||||
type RevenueCategory string
|
||||
// RevenueCategory is now just a string — categories are dynamic per company
|
||||
type RevenueCategory = string
|
||||
|
||||
// Built-in reserved categories
|
||||
const (
|
||||
CategoryProduct RevenueCategory = "product"
|
||||
CategoryLocation RevenueCategory = "location"
|
||||
CategoryTotal RevenueCategory = "total"
|
||||
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
|
||||
@@ -59,11 +68,57 @@ func (r *RevenueReport) Total(category RevenueCategory) float64 {
|
||||
return sum
|
||||
}
|
||||
|
||||
// Validate checks that product + location sums match the total entry
|
||||
func (r *RevenueReport) Validate() (bool, float64, float64) {
|
||||
total := r.Total(CategoryTotal)
|
||||
products := r.Total(CategoryProduct)
|
||||
locations := r.Total(CategoryLocation)
|
||||
// both breakdowns should equal the total
|
||||
return products == total && locations == total, products, locations
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package service
|
||||
import (
|
||||
"Portifolio/internal/model"
|
||||
"database/sql"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func InsertCompany(db *sql.DB, input model.CompanyInput) (int, error) {
|
||||
|
||||
@@ -3,6 +3,8 @@ package service
|
||||
import (
|
||||
"Portifolio/internal/model"
|
||||
"database/sql"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func InsertCurrency(db *sql.DB, input model.CurrencyInput) (int, error) {
|
||||
|
||||
135
internal/service/revenue.go
Normal file
135
internal/service/revenue.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"Portifolio/internal/model"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func InsertRevenue(db *sql.DB, companyID, currencyID int, category, label string, value float64, period model.Period) error {
|
||||
periodID, err := InsertPeriod(db, period)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reportID, err := getOrCreateReport(db, companyID, periodID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.Exec(
|
||||
`INSERT INTO revenue_entries (report_id, currency_id, category, label, value) VALUES (?, ?, ?, ?, ?)`,
|
||||
reportID, currencyID, category, label, 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 {
|
||||
return 0, err
|
||||
}
|
||||
id, err := res.LastInsertId()
|
||||
return int(id), err
|
||||
}
|
||||
|
||||
func getOrCreateReport(db *sql.DB, companyID, periodID int) (int, error) {
|
||||
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) {
|
||||
rows, err := db.Query(`
|
||||
SELECT e.id, e.category, e.label, e.value,
|
||||
p.type, p.year, p.idx, p.start_date, p.end_date
|
||||
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 = ? AND p.idx = ?`,
|
||||
companyID, string(periodType), year, idx,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var entries []model.Revenue
|
||||
for rows.Next() {
|
||||
var e model.Revenue
|
||||
var p model.Period
|
||||
var start, end string
|
||||
if err := rows.Scan(&e.ID, &e.Category, &e.Label, &e.Value,
|
||||
&p.Type, &p.Year, &p.Index, &start, &end); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Start, _ = time.Parse("2006-01-02", start)
|
||||
p.End, _ = time.Parse("2006-01-02", end)
|
||||
e.Period = p
|
||||
entries = append(entries, e)
|
||||
}
|
||||
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()
|
||||
}
|
||||
@@ -52,3 +52,27 @@ func AddCompany(scanner *bufio.Scanner, db *sql.DB) {
|
||||
}
|
||||
fmt.Printf(" ✓ Company '%s' added.\n", input.Name)
|
||||
}
|
||||
|
||||
func ListCompanies(db *sql.DB) {
|
||||
companies, err := service.GetAllCompanies(db)
|
||||
if err != nil {
|
||||
fmt.Println(" ✗ Error:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(companies) == 0 {
|
||||
fmt.Println(" No companies found.")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("\n %-5s %-20s %-10s %-15s %s\n", "ID", "NAME", "CURRENCY", "PRICE", "SHARES")
|
||||
fmt.Println(" " + strings.Repeat("-", 60))
|
||||
for _, c := range companies {
|
||||
currency := "N/A"
|
||||
if c.Currency != nil {
|
||||
currency = c.Currency.Code
|
||||
}
|
||||
fmt.Printf(" %-5d %-20s %-10s %-15.2f %d\n",
|
||||
c.ID, c.Name, currency, c.Price, c.SharesOutstanding)
|
||||
}
|
||||
}
|
||||
|
||||
153
internal/shell/revenue.go
Normal file
153
internal/shell/revenue.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"Portifolio/internal/model"
|
||||
"Portifolio/internal/service"
|
||||
"bufio"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func AddRevenue(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(" 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 {
|
||||
case "Q":
|
||||
period = model.QuarterPeriod(year, idx)
|
||||
case "H":
|
||||
period = model.HalfYearPeriod(year, idx)
|
||||
case "Y":
|
||||
period = model.FullYearPeriod(year)
|
||||
default:
|
||||
fmt.Println(" ✗ Invalid period type")
|
||||
return
|
||||
}
|
||||
|
||||
if err := service.InsertRevenue(db, companyID, currencyID, category, label, value, period); err != nil {
|
||||
fmt.Println(" ✗ Error:", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" ✓ Revenue entry added: %s / %s = %.2f (%s)\n", category, label, value, period.String())
|
||||
}
|
||||
|
||||
func ListRevenue(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 (Q/H/Y): ")
|
||||
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 {
|
||||
fmt.Println(" ✗ Error:", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("\n %-12s %-20s %12s\n", "CATEGORY", "LABEL", "VALUE")
|
||||
fmt.Println(" " + strings.Repeat("-", 46))
|
||||
for _, e := range entries {
|
||||
fmt.Printf(" %-12s %-20s %12.2f\n", e.Category, e.Label, 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user