fix(coin): accept symbol-first trades

This commit is contained in:
2026-06-12 11:07:27 +07:00
parent b234b54b2e
commit 49a6e7091a
3 changed files with 93 additions and 8 deletions
+10 -8
View File
@@ -70,16 +70,17 @@ func (s *state) handleBuy(ctx context.Context, b *bot.Bot, update *models.Update
}
args := argsAfterCommand(update.Message.Text)
if len(args) != 2 {
return chathelper.Reply(ctx, b, update.Message, "Usage: /coin_buy <usd_amount> <COIN>\nExample: /coin_buy 100 BTC")
return chathelper.Reply(ctx, b, update.Message, "Usage: /coin_buy <COIN> <usd_amount>\nExample: /coin_buy BTC 10")
}
amount, ok := parsePositiveFinite(args[0])
if !ok || !isSafeUSD(amount) {
parsed, err := parseCoinValueArgs(args, isSafeUSD, errInvalidUSDAmount)
if errors.Is(err, errInvalidUSDAmount) {
return chathelper.Reply(ctx, b, update.Message, "USD amount must be a positive finite number within the supported range.")
}
coin, err := ResolveCoinSymbol(args[1])
if err != nil {
return s.replyPriceError(ctx, b, update, err)
}
coin := parsed.coin
amount := parsed.value
price, err := s.prices.FetchUSD(ctx, coin)
if err != nil {
return s.replyPriceError(ctx, b, update, err)
@@ -120,16 +121,17 @@ func (s *state) handleSell(ctx context.Context, b *bot.Bot, update *models.Updat
}
args := argsAfterCommand(update.Message.Text)
if len(args) != 2 {
return chathelper.Reply(ctx, b, update.Message, "Usage: /coin_sell <qty> <COIN>\nExample: /coin_sell 0.01 BTC")
return chathelper.Reply(ctx, b, update.Message, "Usage: /coin_sell <COIN> <qty>\nExample: /coin_sell BTC 0.01")
}
qty, ok := parsePositiveFinite(args[0])
if !ok {
parsed, err := parseCoinValueArgs(args, isPositiveFinite, errInvalidQuantity)
if errors.Is(err, errInvalidQuantity) {
return chathelper.Reply(ctx, b, update.Message, "Quantity must be a positive finite number.")
}
coin, err := ResolveCoinSymbol(args[1])
if err != nil {
return s.replyPriceError(ctx, b, update, err)
}
coin := parsed.coin
qty := parsed.value
price, err := s.prices.FetchUSD(ctx, coin)
if err != nil {
return s.replyPriceError(ctx, b, update, err)
@@ -0,0 +1,45 @@
package coin
import (
"context"
"testing"
"github.com/tiennm99/miti99bot/internal/testutil"
)
func TestHandleBuyAcceptsCoinFirstOrder(t *testing.T) {
ctx := context.Background()
s := newTestState(map[string]CoinPrice{"BTC": {USD: 50000, Source: "Binance"}}, nil)
rb := testutil.NewRecordingBot(t)
_ = s.handleTopup(ctx, rb.Bot, testutil.NewPrivateMessage(7, "/coin_topup 1000"))
rb.Reset()
if err := s.handleBuy(ctx, rb.Bot, testutil.NewPrivateMessage(7, "/coin_buy BTC 10")); err != nil {
t.Fatalf("handleBuy: %v", err)
}
rb.AssertSentText(t, "Bought 0.0002 BTC")
p, _ := LoadPortfolio(ctx, s.kv, 7, 999)
if p.USD != 990 || p.Assets["BTC"] != 0.0002 {
t.Fatalf("after coin-first buy = %+v", p)
}
}
func TestHandleSellAcceptsCoinFirstOrder(t *testing.T) {
ctx := context.Background()
s := newTestState(map[string]CoinPrice{"BTC": {USD: 50000, Source: "Binance"}}, nil)
rb := testutil.NewRecordingBot(t)
_ = s.handleTopup(ctx, rb.Bot, testutil.NewPrivateMessage(7, "/coin_topup 1000"))
_ = s.handleBuy(ctx, rb.Bot, testutil.NewPrivateMessage(7, "/coin_buy 500 BTC"))
rb.Reset()
if err := s.handleSell(ctx, rb.Bot, testutil.NewPrivateMessage(7, "/coin_sell BTC 0.01")); err != nil {
t.Fatalf("handleSell: %v", err)
}
rb.AssertSentText(t, "Sold 0.01 BTC")
p, _ := LoadPortfolio(ctx, s.kv, 7, 999)
if p.USD != 1000 || len(p.Assets) != 0 {
t.Fatalf("after coin-first sell = %+v", p)
}
}
+38
View File
@@ -0,0 +1,38 @@
package coin
import "errors"
var (
errInvalidUSDAmount = errors.New("coin: invalid USD amount")
errInvalidQuantity = errors.New("coin: invalid quantity")
)
type coinValueArgs struct {
coin CoinSymbol
value float64
}
func parseCoinValueArgs(args []string, validValue func(float64) bool, invalidValueErr error) (coinValueArgs, error) {
if len(args) != 2 {
return coinValueArgs{}, invalidValueErr
}
if coin, err := ResolveCoinSymbol(args[0]); err == nil {
value, ok := parsePositiveFinite(args[1])
if !ok || !validValue(value) {
return coinValueArgs{}, invalidValueErr
}
return coinValueArgs{coin: coin, value: value}, nil
}
value, ok := parsePositiveFinite(args[0])
if !ok || !validValue(value) {
if _, secondOK := parsePositiveFinite(args[1]); secondOK {
return coinValueArgs{}, ErrUnsupportedCoin
}
return coinValueArgs{}, invalidValueErr
}
coin, err := ResolveCoinSymbol(args[1])
if err != nil {
return coinValueArgs{}, err
}
return coinValueArgs{coin: coin, value: value}, nil
}