diff --git a/internal/modules/coin/handlers.go b/internal/modules/coin/handlers.go index 0f42e77..080463d 100644 --- a/internal/modules/coin/handlers.go +++ b/internal/modules/coin/handlers.go @@ -147,19 +147,20 @@ func (s *state) handleSell(ctx context.Context, b *bot.Bot, update *models.Updat return chathelper.Reply(ctx, b, update.Message, "USD amount is too small to convert to a tradeable coin quantity at the current price.") } defer s.locks.Acquire(strconv.FormatInt(userID, 10))() - var insufficientHeld *float64 + var insufficientHeldQty float64 + var insufficientHeld bool p, err := UpdatePortfolio(ctx, s.kv, userID, s.now().UnixMilli(), func(p *Portfolio) error { ok, held := p.DeductAsset(coin.Symbol, qty) if !ok { - insufficientHeld = &held + insufficientHeldQty = held + insufficientHeld = true return errInsufficientCoin } p.AddUSD(amount) return nil }) - if errors.Is(err, errInsufficientCoin) && insufficientHeld != nil { - return chathelper.Reply(ctx, b, update.Message, - "Insufficient "+coin.Symbol+". You have: "+FormatCoinQty(*insufficientHeld)) + if errors.Is(err, errInsufficientCoin) && insufficientHeld { + return chathelper.Reply(ctx, b, update.Message, formatInsufficientSellMessage(coin, amount, insufficientHeldQty, price.USD)) } if err != nil { log.Error("coin_save_portfolio", "user", userID, "err", err) @@ -169,3 +170,14 @@ func (s *state) handleSell(ctx context.Context, b *bot.Bot, update *models.Updat "Sold "+FormatCoinQty(qty)+" "+coin.Symbol+" @ "+FormatUSD(price.USD)+" ("+price.Source+")"+ "\nProceeds: "+FormatUSD(amount)+"\nRemaining: "+FormatUSD(p.USD)) } + +func formatInsufficientSellMessage(coin CoinSymbol, requestedUSD, heldQty, priceUSD float64) string { + heldQty = normalizeAmount(heldQty) + if heldQty == 0 { + return "No " + coin.Symbol + " available to sell.\nTry /coin_buy " + coin.Symbol + " first." + } + availableUSD := heldQty * priceUSD + return "Not enough " + coin.Symbol + " to sell " + FormatUSD(requestedUSD) + ".\n" + + "Available to sell: " + FormatUSD(availableUSD) + " (" + FormatCoinQty(heldQty) + " " + coin.Symbol + " @ " + FormatUSD(priceUSD) + ").\n" + + "Try " + FormatUSD(availableUSD) + " or less." +} diff --git a/internal/modules/coin/handlers_test.go b/internal/modules/coin/handlers_test.go index 8470d48..fbd0ae4 100644 --- a/internal/modules/coin/handlers_test.go +++ b/internal/modules/coin/handlers_test.go @@ -149,7 +149,17 @@ func TestHandleSellInsufficientCoin(t *testing.T) { if err := s.handleSell(context.Background(), rb.Bot, testutil.NewPrivateMessage(7, "/coin_sell 10 ETH")); err != nil { t.Fatalf("handleSell: %v", err) } - rb.AssertSentText(t, "Insufficient ETH") + text := rb.LastSent().Text() + for _, want := range []string{"No ETH available to sell.", "Try /coin_buy ETH first."} { + if !strings.Contains(text, want) { + t.Fatalf("zero-holdings sell message missing %q in %q", want, text) + } + } + for _, unwanted := range []string{"$0.00", "@"} { + if strings.Contains(text, unwanted) { + t.Fatalf("zero-holdings sell message included %q in %q", unwanted, text) + } + } } func TestHandleSellRejectsInvalidUSDAmount(t *testing.T) { @@ -181,7 +191,19 @@ func TestHandleSellInsufficientCoinWithHoldings(t *testing.T) { if err := s.handleSell(ctx, rb.Bot, testutil.NewPrivateMessage(7, "/coin_sell 600 BTC")); err != nil { t.Fatalf("handleSell: %v", err) } - rb.AssertSentText(t, "Insufficient BTC") + text := rb.LastSent().Text() + for _, want := range []string{ + "Not enough BTC to sell $600.00.", + "Available to sell: $500.00 (0.01 BTC @ $50,000.00).", + "Try $500.00 or less.", + } { + if !strings.Contains(text, want) { + t.Fatalf("insufficient sell message missing %q in %q", want, text) + } + } + if strings.Contains(text, "have") { + t.Fatalf("insufficient sell message used ambiguous have wording: %q", text) + } p, _ := LoadPortfolio(ctx, s.kv, 7, 999) if p.USD != 500 || p.Assets["BTC"] != 0.01 {