From 85587d563d421f2d5328e0800fcce64dd8865b42 Mon Sep 17 00:00:00 2001 From: db123 Date: Thu, 25 Jun 2026 01:58:27 +0330 Subject: [PATCH] fix: computeTrend division by zero, add tests for trend and currency parsing, add username to settings --- internal/bot/burnout.go | 30 ++++++++++++++++++++-- internal/bot/rpg_test.go | 54 ++++++++++++++++++++++++++++++++++++++++ internal/bot/salary.go | 30 +++++++++++++--------- internal/bot/settings.go | 8 +++--- 4 files changed, 104 insertions(+), 18 deletions(-) diff --git a/internal/bot/burnout.go b/internal/bot/burnout.go index da1226c..d099c49 100644 --- a/internal/bot/burnout.go +++ b/internal/bot/burnout.go @@ -212,11 +212,37 @@ func computeTrend(store *db.Store, userID int64, today string, loc *time.Locatio recentAvg /= 3.0 olderAvg /= 3.0 + if olderAvg < 1 { + if recentAvg > 0 { + return 15, "new" + } + return 5, "stable" + } + + changePct := math.Round((recentAvg - olderAvg) / olderAvg * 100) if recentAvg > olderAvg*1.2 { - return 15, fmt.Sprintf("+%.0f%%", math.Round((recentAvg-olderAvg)/math.Max(olderAvg, 1)*100)) + return 15, fmt.Sprintf("+%.0f%%", changePct) } if olderAvg > recentAvg*1.2 { - return 0, fmt.Sprintf("-%.0f%%", math.Round((olderAvg-recentAvg)/math.Max(olderAvg, 1)*100)) + return 0, fmt.Sprintf("%.0f%%", changePct) + } + return 5, "stable" +} + +// computeTrendFromAvgs is a pure version of the trend computation for testing. +func computeTrendFromAvgs(recentAvg, olderAvg float64) (pts int, direction string) { + if olderAvg < 1 { + if recentAvg > 0 { + return 15, "new" + } + return 5, "stable" + } + changePct := math.Round((recentAvg - olderAvg) / olderAvg * 100) + if recentAvg > olderAvg*1.2 { + return 15, fmt.Sprintf("+%.0f%%", changePct) + } + if olderAvg > recentAvg*1.2 { + return 0, fmt.Sprintf("%.0f%%", changePct) } return 5, "stable" } diff --git a/internal/bot/rpg_test.go b/internal/bot/rpg_test.go index 2b9ae03..ad21178 100644 --- a/internal/bot/rpg_test.go +++ b/internal/bot/rpg_test.go @@ -1,6 +1,7 @@ package bot import ( + "strings" "testing" ) @@ -170,4 +171,57 @@ func TestSanitizeDisplayName(t *testing.T) { if got := SanitizeDisplayName("Ali\x00ce"); got != "Alice" { t.Errorf("SanitizeDisplayName control: got %q", got) } + if got := SanitizeDisplayName(""); got != "" { + t.Errorf("SanitizeDisplayName empty: got %q", got) + } +} + +func TestParseCurrencyInput(t *testing.T) { + cases := []struct { + input string + want string + ok bool + }{ + {"$ USD", "$ USD", true}, + {"T Toman", "T Toman", true}, + {"IRR Rial", "IRR Rial", true}, + {"€ EUR", "€ EUR", true}, + {"", "", false}, + {"$", "", false}, + {" ", "", false}, + {"$$$ TooLongCode", "", false}, + } + for _, c := range cases { + got, errMsg := parseCurrencyInput(c.input) + if c.ok && (got != c.want || errMsg != "") { + t.Errorf("parseCurrencyInput(%q) = (%q, %q), want (%q, \"\")", c.input, got, errMsg, c.want) + } + if !c.ok && errMsg == "" { + t.Errorf("parseCurrencyInput(%q) = (%q, \"\"), want error", c.input, got) + } + } +} + +func TestComputeTrendFromAvgs(t *testing.T) { + cases := []struct { + recent, older float64 + wantPts int + wantDirSub string + }{ + {0, 0, 5, "stable"}, + {100, 0, 15, "new"}, + {0, 100, 0, "-100"}, + {121, 100, 15, "+21"}, + {100, 100, 5, "stable"}, + {100, 130, 0, "-23"}, + } + for _, c := range cases { + pts, dir := computeTrendFromAvgs(c.recent, c.older) + if pts != c.wantPts { + t.Errorf("computeTrendFromAvgs(%.0f, %.0f) pts = %d, want %d", c.recent, c.older, pts, c.wantPts) + } + if !strings.Contains(dir, c.wantDirSub) { + t.Errorf("computeTrendFromAvgs(%.0f, %.0f) dir = %q, want containing %q", c.recent, c.older, dir, c.wantDirSub) + } + } } diff --git a/internal/bot/salary.go b/internal/bot/salary.go index bf326ac..a3af332 100644 --- a/internal/bot/salary.go +++ b/internal/bot/salary.go @@ -6,6 +6,7 @@ import ( "math" "strings" "time" + "unicode" "github.com/go-telegram/bot/models" @@ -179,6 +180,20 @@ func (h *Handler) handleSetRateMsg(ctx context.Context, msg *models.Message) { h.sendText(ctx, msg.Chat.ID, fmt.Sprintf("Hourly rate set to %s%.2f", currencySymbol(st.Currency), st.HourlyRate)) } +// parseCurrencyInput validates and normalizes currency input (symbol + code). +func parseCurrencyInput(s string) (string, string) { + parts := strings.Fields(s) + if len(parts) < 2 { + return "", "Please provide both symbol and code.\nUsage: /setcurrency \nExamples:\n/setcurrency $ USD\n/setcurrency T Toman\n/setcurrency IRR Rial" + } + for _, p := range parts[:2] { + if len(p) > 10 || strings.ContainsFunc(p, unicode.IsControl) { + return "", "Invalid currency. Use short printable strings like $ USD, T Toman, IRR Rial." + } + } + return parts[0] + " " + parts[1], "" +} + // handleSetCurrencyMsg handles /setcurrency . func (h *Handler) handleSetCurrencyMsg(ctx context.Context, msg *models.Message) { args := strings.TrimSpace(strings.TrimPrefix(msg.Text, "/setcurrency")) @@ -192,20 +207,11 @@ func (h *Handler) handleSetCurrencyMsg(ctx context.Context, msg *models.Message) h.sendText(ctx, msg.Chat.ID, fmt.Sprintf("Current currency: %s\nUsage: /setcurrency \nExamples:\n/setcurrency $ USD\n/setcurrency T Toman\n/setcurrency IRR Rial", st.Currency)) return } - parts := strings.Fields(args) - if len(parts) < 2 { - h.sendText(ctx, msg.Chat.ID, "Please provide both symbol and code.\nUsage: /setcurrency \nExamples:\n/setcurrency $ USD\n/setcurrency T Toman\n/setcurrency IRR Rial") + currency, errMsg := parseCurrencyInput(args) + if errMsg != "" { + h.sendText(ctx, msg.Chat.ID, errMsg) return } - if len(parts[0]) > 10 || strings.ContainsFunc(parts[0], func(r rune) bool { return r < 32 || r > 126 }) { - h.sendText(ctx, msg.Chat.ID, "Invalid currency symbol. Use a short printable string like $, €, £.") - return - } - if len(parts[1]) > 10 || strings.ContainsFunc(parts[1], func(r rune) bool { return r < 32 || r > 126 }) { - h.sendText(ctx, msg.Chat.ID, "Invalid currency code.") - return - } - currency := parts[0] + " " + parts[1] user, err := h.getOrCreateUser(msg.Chat.ID) if err != nil { h.sendText(ctx, msg.Chat.ID, "Error loading profile") diff --git a/internal/bot/settings.go b/internal/bot/settings.go index 2a103a4..858af35 100644 --- a/internal/bot/settings.go +++ b/internal/bot/settings.go @@ -192,6 +192,9 @@ func (h *Handler) buildSettingsKeyboard(user *db.User, st *db.UserSettings, brea leagueStatus = "on" } rows := [][]models.InlineKeyboardButton{ + { + {Text: fmt.Sprintf("Username: %s", user.Username), CallbackData: "username"}, + }, { {Text: fmt.Sprintf("Timezone: %s", user.Timezone), CallbackData: "timezone"}, {Text: fmt.Sprintf("Calendar: %s", user.Calendar), CallbackData: "caltype"}, @@ -208,12 +211,9 @@ func (h *Handler) buildSettingsKeyboard(user *db.User, st *db.UserSettings, brea {Text: fmt.Sprintf("RPG: %s", rpgStatus), CallbackData: "rpgtoggle"}, {Text: fmt.Sprintf("League: %s", leagueStatus), CallbackData: "leaguetoggle"}, }, - { - {Text: fmt.Sprintf("Username: %s", user.Username), CallbackData: "username"}, - {Text: fmt.Sprintf("Currency: %s", st.Currency), CallbackData: "currency"}, - }, { {Text: fmt.Sprintf("Rate: %.2f", st.HourlyRate), CallbackData: "setrate"}, + {Text: fmt.Sprintf("Currency: %s", st.Currency), CallbackData: "currency"}, }, {{Text: "Back to Menu", CallbackData: "back_menu"}}, }