mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-04-28 02:21:16 +00:00
7f47799733
The TCBS `apipubaws.tcbs.com.vn` host returns HTTP 404/500 for every
request, so every ticker resolved as "Unknown stock ticker" and /trade_buy
was unusable. Switch price + symbol resolution to the KBS public endpoint
that vnstock currently defaults to (`kbbuddywts.kbsec.com.vn/iis-server/
investment/stocks/{TICKER}/data_day`). KBS needs no auth, returns JSON,
and is Worker-compatible.
- `prices.fetchStockPrice` now queries KBS with a 14-day lookback window
(covers weekends/holidays) and drops the TCBS-specific ×1000 scaling;
KBS returns real VND.
- `symbols.resolveSymbol` delegates to `fetchStockPrice` for existence
checks — empty `data_day` means unknown ticker.
- Update test fetch stubs to match the `kbsec` host and KBS response
shape (`{ symbol, data_day: [{ c }] }`).
189 lines
5.5 KiB
JavaScript
189 lines
5.5 KiB
JavaScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { createStore } from "../../../src/db/create-store.js";
|
|
import {
|
|
handleBuy,
|
|
handleConvert,
|
|
handleSell,
|
|
handleTopup,
|
|
} from "../../../src/modules/trading/handlers.js";
|
|
import { savePortfolio } from "../../../src/modules/trading/portfolio.js";
|
|
import { handleStats } from "../../../src/modules/trading/stats-handler.js";
|
|
import { makeFakeKv } from "../../fakes/fake-kv-namespace.js";
|
|
|
|
function makeCtx(match = "", userId = 42) {
|
|
const replies = [];
|
|
return {
|
|
match,
|
|
from: { id: userId },
|
|
reply: vi.fn((text) => replies.push(text)),
|
|
replies,
|
|
};
|
|
}
|
|
|
|
/** Stub fetch — KBS returns stock data, BIDV returns forex */
|
|
function stubFetch() {
|
|
global.fetch = vi.fn((url) => {
|
|
if (url.includes("kbsec")) {
|
|
return Promise.resolve({
|
|
ok: true,
|
|
json: () =>
|
|
Promise.resolve({
|
|
symbol: "TCB",
|
|
data_day: [{ t: "2026-04-21 07:00", c: 25000 }],
|
|
}),
|
|
});
|
|
}
|
|
if (url.includes("bidv")) {
|
|
return Promise.resolve({
|
|
ok: true,
|
|
json: () =>
|
|
Promise.resolve({
|
|
data: [{ currency: "USD", muaCk: "25,200", ban: "25,600" }],
|
|
}),
|
|
});
|
|
}
|
|
return Promise.reject(new Error(`unexpected fetch: ${url}`));
|
|
});
|
|
}
|
|
|
|
describe("trading/handlers", () => {
|
|
let db;
|
|
|
|
beforeEach(() => {
|
|
db = createStore("trading", { KV: makeFakeKv() });
|
|
stubFetch();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("handleTopup", () => {
|
|
it("tops up VND", async () => {
|
|
const ctx = makeCtx("5000000");
|
|
await handleTopup(ctx, db);
|
|
expect(ctx.reply).toHaveBeenCalledOnce();
|
|
expect(ctx.replies[0]).toContain("5.000.000 VND");
|
|
});
|
|
|
|
it("tracks meta.invested", async () => {
|
|
const ctx = makeCtx("1000000");
|
|
await handleTopup(ctx, db);
|
|
const { getPortfolio } = await import("../../../src/modules/trading/portfolio.js");
|
|
const p = await getPortfolio(db, 42);
|
|
expect(p.meta.invested).toBe(1000000);
|
|
expect(p.currency.VND).toBe(1000000);
|
|
});
|
|
|
|
it("rejects missing args", async () => {
|
|
const ctx = makeCtx("");
|
|
await handleTopup(ctx, db);
|
|
expect(ctx.replies[0]).toContain("Usage:");
|
|
});
|
|
|
|
it("rejects negative amount", async () => {
|
|
const ctx = makeCtx("-100");
|
|
await handleTopup(ctx, db);
|
|
expect(ctx.replies[0]).toContain("positive");
|
|
});
|
|
});
|
|
|
|
describe("handleBuy", () => {
|
|
it("buys stock with sufficient VND", async () => {
|
|
const { emptyPortfolio } = await import("../../../src/modules/trading/portfolio.js");
|
|
const p = emptyPortfolio();
|
|
p.currency.VND = 5000000;
|
|
p.meta.invested = 5000000;
|
|
await savePortfolio(db, 42, p);
|
|
|
|
const ctx = makeCtx("10 TCB");
|
|
await handleBuy(ctx, db);
|
|
expect(ctx.replies[0]).toContain("Bought");
|
|
expect(ctx.replies[0]).toContain("TCB");
|
|
});
|
|
|
|
it("rejects buy with insufficient VND", async () => {
|
|
const ctx = makeCtx("10 TCB");
|
|
await handleBuy(ctx, db);
|
|
expect(ctx.replies[0]).toContain("Insufficient VND");
|
|
});
|
|
|
|
it("rejects unknown ticker", async () => {
|
|
// stub KBS to return empty data_day for unknown ticker
|
|
global.fetch = vi.fn(() =>
|
|
Promise.resolve({
|
|
ok: true,
|
|
json: () => Promise.resolve({ symbol: "NOPE", data_day: [] }),
|
|
}),
|
|
);
|
|
const ctx = makeCtx("10 NOPE");
|
|
await handleBuy(ctx, db);
|
|
expect(ctx.replies[0]).toContain("Unknown stock ticker");
|
|
expect(ctx.replies[0]).toContain("coming soon");
|
|
});
|
|
|
|
it("rejects fractional stock quantity", async () => {
|
|
const ctx = makeCtx("1.5 TCB");
|
|
await handleBuy(ctx, db);
|
|
expect(ctx.replies[0]).toContain("whole numbers");
|
|
});
|
|
|
|
it("rejects missing args", async () => {
|
|
const ctx = makeCtx("");
|
|
await handleBuy(ctx, db);
|
|
expect(ctx.replies[0]).toContain("Usage:");
|
|
});
|
|
});
|
|
|
|
describe("handleSell", () => {
|
|
it("sells stock holding", async () => {
|
|
const { emptyPortfolio } = await import("../../../src/modules/trading/portfolio.js");
|
|
const p = emptyPortfolio();
|
|
p.assets.TCB = 50;
|
|
await savePortfolio(db, 42, p);
|
|
|
|
const ctx = makeCtx("10 TCB");
|
|
await handleSell(ctx, db);
|
|
expect(ctx.replies[0]).toContain("Sold");
|
|
expect(ctx.replies[0]).toContain("TCB");
|
|
});
|
|
|
|
it("rejects sell with insufficient holdings", async () => {
|
|
const ctx = makeCtx("10 TCB");
|
|
await handleSell(ctx, db);
|
|
expect(ctx.replies[0]).toContain("Insufficient TCB");
|
|
});
|
|
});
|
|
|
|
describe("handleConvert", () => {
|
|
it("shows coming soon message", async () => {
|
|
const ctx = makeCtx("100 USD VND");
|
|
await handleConvert(ctx);
|
|
expect(ctx.replies[0]).toContain("coming soon");
|
|
});
|
|
});
|
|
|
|
describe("handleStats", () => {
|
|
it("shows empty portfolio for new user", async () => {
|
|
const ctx = makeCtx("");
|
|
await handleStats(ctx, db);
|
|
expect(ctx.replies[0]).toContain("Portfolio Summary");
|
|
expect(ctx.replies[0]).toContain("Total value:");
|
|
});
|
|
|
|
it("shows portfolio with stock assets", async () => {
|
|
const { emptyPortfolio } = await import("../../../src/modules/trading/portfolio.js");
|
|
const p = emptyPortfolio();
|
|
p.currency.VND = 5000000;
|
|
p.assets.TCB = 10;
|
|
p.meta.invested = 10000000;
|
|
await savePortfolio(db, 42, p);
|
|
|
|
const ctx = makeCtx("");
|
|
await handleStats(ctx, db);
|
|
expect(ctx.replies[0]).toContain("TCB");
|
|
expect(ctx.replies[0]).toContain("P&L:");
|
|
});
|
|
});
|
|
});
|