This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -102,6 +103,9 @@ CREATE INDEX IF NOT EXISTS idx_order_events_order ON order_events(order_id);
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.createTradingSchema(); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := s.db.Exec(`INSERT OR IGNORE INTO storage_locations(name, normalized_name, created_by_id, created_by_name, created_at, updated_at)
|
||||
SELECT DISTINCT TRIM(location), lower(TRIM(location)), 'migration', 'migration', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
||||
FROM inventory
|
||||
@@ -608,3 +612,204 @@ func formatAmount(v float64) string {
|
||||
}
|
||||
return fmt.Sprintf("%.2f", v)
|
||||
}
|
||||
|
||||
func (s *Store) createTradingSchema() error {
|
||||
_, err := s.db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS trading_runs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL,
|
||||
user_name TEXT NOT NULL,
|
||||
purpose TEXT NOT NULL,
|
||||
ship TEXT NOT NULL,
|
||||
buy_value INTEGER NOT NULL DEFAULT 0,
|
||||
sell_value INTEGER NOT NULL DEFAULT 0,
|
||||
costs INTEGER NOT NULL DEFAULT 0,
|
||||
repetitions INTEGER NOT NULL DEFAULT 1,
|
||||
scu REAL NOT NULL DEFAULT 0,
|
||||
distance REAL NOT NULL DEFAULT 0,
|
||||
scu_grade TEXT NOT NULL DEFAULT '',
|
||||
commodity TEXT NOT NULL,
|
||||
post_a TEXT NOT NULL DEFAULT '',
|
||||
post_b TEXT NOT NULL DEFAULT '',
|
||||
note TEXT NOT NULL DEFAULT '',
|
||||
month INTEGER NOT NULL CHECK(month >= 1 AND month <= 12),
|
||||
year INTEGER NOT NULL CHECK(year >= 2000 AND year <= 2200),
|
||||
org_percent REAL NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
deleted_at DATETIME,
|
||||
deleted_by_id TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_trading_month_year ON trading_runs(year, month);
|
||||
CREATE INDEX IF NOT EXISTS idx_trading_user ON trading_runs(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_trading_deleted ON trading_runs(deleted_at);
|
||||
`)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) CreateTradingRun(t *TradingRun) (int64, error) {
|
||||
now := time.Now().UTC()
|
||||
if t.Month < 1 || t.Month > 12 {
|
||||
return 0, errors.New("monat muss zwischen 1 und 12 liegen")
|
||||
}
|
||||
if t.Year < 2000 || t.Year > 2200 {
|
||||
return 0, errors.New("jahr muss zwischen 2000 und 2200 liegen")
|
||||
}
|
||||
if t.Repetitions <= 0 {
|
||||
t.Repetitions = 1
|
||||
}
|
||||
res, err := s.db.Exec(`INSERT INTO trading_runs
|
||||
(user_id, user_name, purpose, ship, buy_value, sell_value, costs, repetitions, scu, distance, scu_grade, commodity, post_a, post_b, note, month, year, org_percent, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
t.UserID, t.UserName, t.Purpose, t.Ship, t.BuyValue, t.SellValue, t.Costs, t.Repetitions, t.SCU, t.Distance, t.SCUGrade, t.Commodity, t.PostA, t.PostB, t.Note, t.Month, t.Year, t.OrgPercent, now, now)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.LastInsertId()
|
||||
}
|
||||
|
||||
func scanTradingRun(scanner interface{ Scan(dest ...any) error }) (*TradingRun, error) {
|
||||
t := &TradingRun{}
|
||||
if err := scanner.Scan(&t.ID, &t.UserID, &t.UserName, &t.Purpose, &t.Ship, &t.BuyValue, &t.SellValue, &t.Costs, &t.Repetitions, &t.SCU, &t.Distance, &t.SCUGrade, &t.Commodity, &t.PostA, &t.PostB, &t.Note, &t.Month, &t.Year, &t.OrgPercent, &t.CreatedAt, &t.UpdatedAt, &t.DeletedAt, &t.DeletedByID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetTradingRun(id int64) (*TradingRun, error) {
|
||||
row := s.db.QueryRow(`SELECT id, user_id, user_name, purpose, ship, buy_value, sell_value, costs, repetitions, scu, distance, scu_grade, commodity, post_a, post_b, note, month, year, org_percent, created_at, updated_at, deleted_at, deleted_by_id FROM trading_runs WHERE id=?`, id)
|
||||
return scanTradingRun(row)
|
||||
}
|
||||
|
||||
func (s *Store) DeleteTradingRun(id int64, actorID string) (*TradingRun, error) {
|
||||
t, err := s.GetTradingRun(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if t.DeletedAt.Valid {
|
||||
return nil, errors.New("trading-run ist bereits gelöscht")
|
||||
}
|
||||
_, err = s.db.Exec(`UPDATE trading_runs SET deleted_at=?, deleted_by_id=?, updated_at=? WHERE id=? AND deleted_at IS NULL`, time.Now().UTC(), actorID, time.Now().UTC(), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.GetTradingRun(id)
|
||||
}
|
||||
|
||||
func (s *Store) ListTradingRuns(f TradingListFilter) ([]TradingRun, error) {
|
||||
if f.Limit <= 0 {
|
||||
f.Limit = 20
|
||||
}
|
||||
if f.Limit > 5000 {
|
||||
f.Limit = 5000
|
||||
}
|
||||
q := `SELECT id, user_id, user_name, purpose, ship, buy_value, sell_value, costs, repetitions, scu, distance, scu_grade, commodity, post_a, post_b, note, month, year, org_percent, created_at, updated_at, deleted_at, deleted_by_id FROM trading_runs WHERE deleted_at IS NULL`
|
||||
args := []any{}
|
||||
if f.Month > 0 {
|
||||
q += ` AND month=?`
|
||||
args = append(args, f.Month)
|
||||
}
|
||||
if f.Year > 0 {
|
||||
q += ` AND year=?`
|
||||
args = append(args, f.Year)
|
||||
}
|
||||
if strings.TrimSpace(f.UserID) != "" {
|
||||
q += ` AND user_id=?`
|
||||
args = append(args, f.UserID)
|
||||
}
|
||||
q += ` ORDER BY year DESC, month DESC, created_at DESC LIMIT ?`
|
||||
args = append(args, f.Limit)
|
||||
rows, err := s.db.Query(q, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []TradingRun
|
||||
for rows.Next() {
|
||||
t, err := scanTradingRun(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, *t)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) SummarizeTradingRuns(month, year int) (*TradingSummary, error) {
|
||||
runs, err := s.ListTradingRuns(TradingListFilter{Month: month, Year: year, Limit: 100000})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sum := &TradingSummary{Month: month, Year: year, Count: len(runs)}
|
||||
traderMap := map[string]*TradingTraderSummary{}
|
||||
purposeMap := map[string]*TradingPurposeSummary{}
|
||||
for _, r := range runs {
|
||||
profit := tradingProfit(r)
|
||||
share := tradingOrgShare(r)
|
||||
scu := r.SCU * float64(r.Repetitions)
|
||||
costs := r.Costs * r.Repetitions
|
||||
distance := r.Distance * float64(r.Repetitions)
|
||||
sum.GrossProfit += profit
|
||||
sum.OrgShare += share
|
||||
sum.TotalSCU += scu
|
||||
sum.TotalCosts += costs
|
||||
sum.TotalDistance += distance
|
||||
|
||||
key := r.UserID
|
||||
if key == "" {
|
||||
key = r.UserName
|
||||
}
|
||||
tr := traderMap[key]
|
||||
if tr == nil {
|
||||
tr = &TradingTraderSummary{UserID: r.UserID, UserName: r.UserName}
|
||||
traderMap[key] = tr
|
||||
}
|
||||
tr.Profit += profit
|
||||
tr.OrgShare += share
|
||||
tr.SCU += scu
|
||||
tr.Runs++
|
||||
|
||||
ps := purposeMap[r.Purpose]
|
||||
if ps == nil {
|
||||
ps = &TradingPurposeSummary{Purpose: r.Purpose}
|
||||
purposeMap[r.Purpose] = ps
|
||||
}
|
||||
ps.Profit += profit
|
||||
ps.OrgShare += share
|
||||
ps.SCU += scu
|
||||
ps.Runs++
|
||||
}
|
||||
for _, tr := range traderMap {
|
||||
sum.TopTraders = append(sum.TopTraders, *tr)
|
||||
}
|
||||
for _, ps := range purposeMap {
|
||||
sum.PurposeSummary = append(sum.PurposeSummary, *ps)
|
||||
}
|
||||
sortTradingSummary(sum)
|
||||
return sum, nil
|
||||
}
|
||||
|
||||
func tradingProfit(r TradingRun) int64 {
|
||||
return (r.SellValue - r.BuyValue - r.Costs) * r.Repetitions
|
||||
}
|
||||
|
||||
func tradingOrgShare(r TradingRun) int64 {
|
||||
profit := tradingProfit(r)
|
||||
if profit <= 0 || r.OrgPercent <= 0 {
|
||||
return 0
|
||||
}
|
||||
return int64(math.Round(float64(profit) * r.OrgPercent))
|
||||
}
|
||||
|
||||
func sortTradingSummary(sum *TradingSummary) {
|
||||
sort.Slice(sum.TopTraders, func(i, j int) bool {
|
||||
if sum.TopTraders[i].Profit == sum.TopTraders[j].Profit {
|
||||
return sum.TopTraders[i].UserName < sum.TopTraders[j].UserName
|
||||
}
|
||||
return sum.TopTraders[i].Profit > sum.TopTraders[j].Profit
|
||||
})
|
||||
if len(sum.TopTraders) > 3 {
|
||||
sum.TopTraders = sum.TopTraders[:3]
|
||||
}
|
||||
sort.Slice(sum.PurposeSummary, func(i, j int) bool { return sum.PurposeSummary[i].Purpose < sum.PurposeSummary[j].Purpose })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user