import { describe, it, expect } from "vitest"; import { PURCHASE_INTENT_RE, detectPurchaseIntent, extractPaymentPreference, extractPurchaseUsername, purchaseMenuText, marketplaceGuidanceText, adminContactText, buildAdminPurchaseLink, purchaseMenuKeyboard, marketplaceGuidanceKeyboard, adminContactKeyboard, } from "../src/channels/telegram/commands/purchase.js"; const PLATFORM_NOIS = ["Shopee", "Lazada", "Add-On", "Touch 'n Go", "TnG", "bank-in", "bank transfer 链接"]; describe("第2批 — 购买意图检测(确定性规则,不靠 LLM)", () => { it("matches zh purchase phrases", () => { expect(detectPurchaseIntent("我要买 CZH01")).toBe(true); expect(detectPurchaseIntent("我想买这个产品")).toBe(true); expect(detectPurchaseIntent("怎么买?")).toBe(true); expect(detectPurchaseIntent("如何购买")).toBe(true); expect(detectPurchaseIntent("我想下单")).toBe(true); expect(detectPurchaseIntent("怎么付款")).toBe(true); }); it("matches en purchase phrases, but not 'in order to'", () => { expect(detectPurchaseIntent("how to buy")).toBe(true); expect(detectPurchaseIntent("I want to buy a product")).toBe(true); expect(detectPurchaseIntent("buy")).toBe(true); expect(detectPurchaseIntent("buyer")).toBe(false); expect(detectPurchaseIntent("how do I pay?")).toBe(true); expect(detectPurchaseIntent("want to order CZH01")).toBe(true); expect(detectPurchaseIntent("In order to access the portal")).toBe(false); }); it("matches ms purchase phrases", () => { expect(detectPurchaseIntent("saya nak beli produk")).toBe(true); expect(detectPurchaseIntent("macam mana nak bayar")).toBe(true); expect(detectPurchaseIntent("saya ingin beli CZH01")).toBe(true); expect(detectPurchaseIntent("boleh tempah tak?")).toBe(true); }); it("does not match ordinary chat / price questions / free-trial talk", () => { expect(detectPurchaseIntent("这个产品多少钱")).toBe(false); expect(detectPurchaseIntent("CZH01 价格是多少")).toBe(false); expect(detectPurchaseIntent("how much is it")).toBe(false); expect(detectPurchaseIntent("你好")).toBe(false); expect(detectPurchaseIntent("谢谢")).toBe(false); expect(detectPurchaseIntent("免费版有吗")).toBe(false); expect(detectPurchaseIntent("")).toBe(false); }); }); describe("第2批 — 付款偏好提取(只认支付方式,不认平台名)", () => { it("extracts bank / ewallet / cod preferences in zh/en/ms", () => { expect(extractPaymentPreference("我想用银行转账")).toBe("bank"); expect(extractPaymentPreference("bank transfer ok?")).toBe("bank"); expect(extractPaymentPreference("boleh bank in?")).toBe("bank"); expect(extractPaymentPreference("可以用 touch n go 吗")).toBe("ewallet"); expect(extractPaymentPreference("tng boleh?")).toBe("ewallet"); expect(extractPaymentPreference("ewallet payment")).toBe("ewallet"); expect(extractPaymentPreference("cash on delivery")).toBe("cod"); expect(extractPaymentPreference("货到付款可以")).toBe("cod"); }); it("returns undefined when no payment preference is stated", () => { expect(extractPaymentPreference("我要买 CZH01")).toBeUndefined(); expect(extractPaymentPreference("介绍下这个产品")).toBeUndefined(); expect(extractPaymentPreference("")).toBeUndefined(); }); }); describe("第2批 — 账号名提取(只认 label,防裸 SKU 误当账号)", () => { it("extracts username only when labelled", () => { expect(extractPurchaseUsername("我要买 CZH01 账号 username:abc123")).toBe("abc123"); expect(extractPurchaseUsername("buy CZH01 username:abc123")).toBe("abc123"); expect(extractPurchaseUsername("我要买 CZH01")).toBeUndefined(); // 裸 SKU 不算 expect(extractPurchaseUsername("CZH01")).toBeUndefined(); expect(extractPurchaseUsername("")).toBeUndefined(); }); }); describe("第2批 — admin 深链构建(第1批防嵌套契约延续)", () => { it("SKU only, per language", () => { const zh = buildAdminPurchaseLink({ sku: "CZH01", lang: "zh" }); expect(zh.startsWith("https://t.me/MrFullStackDev?text=")).toBe(true); expect(decodeURIComponent(zh.split("text=")[1])).toBe("我要买 CZH01"); const en = buildAdminPurchaseLink({ sku: "CZH01", lang: "en" }); expect(decodeURIComponent(en.split("text=")[1])).toBe("I want to buy CZH01"); const ms = buildAdminPurchaseLink({ sku: "CZH01", lang: "ms" }); expect(ms).not.toContain("text= "); // 无空格前缀 expect(ms).toContain("text="); expect(decodeURIComponent(ms.split("text=")[1])).toBe("Saya nak beli CZH01"); }); it("no SKU → generic product message per language", () => { expect(decodeURIComponent(buildAdminPurchaseLink({ lang: "zh" }).split("text=")[1])).toBe( "我想购买产品", ); expect(decodeURIComponent(buildAdminPurchaseLink({ lang: "en" }).split("text=")[1])).toBe( "I want to buy a product", ); expect(decodeURIComponent(buildAdminPurchaseLink({ lang: "ms" }).split("text=")[1])).toBe( "Saya nak beli produk", ); }); it("SKU + payment preference + username all appended in one sentence", () => { const zh = buildAdminPurchaseLink({ sku: "CZH01", paymentPreference: "bank", username: "love1r", lang: "zh", }); const msg = decodeURIComponent(zh.split("text=")[1]); expect(msg).toBe("我要买 CZH01 (想用银行转账) 账号 username:love1r"); const en = buildAdminPurchaseLink({ sku: "CZH01", paymentPreference: "ewallet", username: "love1r", lang: "en", }); const enMsg = decodeURIComponent(en.split("text=")[1]); expect(enMsg).toBe("I want to buy CZH01 (prefers e-wallet) account username:love1r"); }); it("deep-link text never contains another URL (anti double-nesting)", () => { for (const link of [ buildAdminPurchaseLink({ sku: "CZH01", lang: "zh" }), buildAdminPurchaseLink({ sku: "CZH01", paymentPreference: "bank", lang: "en" }), buildAdminPurchaseLink({ lang: "ms" }), ]) { expect(link.split("text=")[1]).not.toContain("t.me/"); expect(link.split("text=")[1]).not.toContain("MrFullStackDev"); expect(link.split("text=")[1]).not.toContain("%2F"); // 无斜杠 → 无 URL } }); }); describe("第2批 — 文案中性词纪律(不出现未上线平台/支付名)", () => { it("menu, guidance and admin-contact texts never mention platforms by name", () => { for (const lang of ["zh", "en", "ms"] as const) { for (const text of [ purchaseMenuText(lang), marketplaceGuidanceText(lang), adminContactText("https://t.me/MrFullStackDev?text=x", lang), ]) { for (const noise of PLATFORM_NOIS) { expect(text).not.toContain(noise); } } } }); it("menu is single-language and mentions the online store first (priority order)", () => { const zh = purchaseMenuText("zh"); expect(zh.indexOf("网店下单")).toBeGreaterThan(-1); expect(zh.indexOf("网店下单")).toBeLessThan(zh.indexOf("直接付款")); expect(zh).not.toContain("our online store"); const en = purchaseMenuText("en"); expect(en.indexOf("online store")).toBeGreaterThan(-1); expect(en.indexOf("online store")).toBeLessThan(en.indexOf("buy here directly")); expect(en).not.toContain("网店"); const ms = purchaseMenuText("ms"); expect(ms.indexOf("kedai dalam talian")).toBeGreaterThan(-1); expect(ms.indexOf("kedai dalam talian")).toBeLessThan(ms.indexOf("beli terus")); }); it("marketplace guidance offers seller/come-back + admin handoff", () => { const zh = marketplaceGuidanceText("zh"); expect(zh).toContain("网店"); expect(zh).toContain("联系卖家"); expect(zh).toContain("admin"); }); it("admin contact text embeds the deep link standalone (URL gluing rule)", () => { const link = "https://t.me/MrFullStackDev?text=hello"; const zh = adminContactText(link, "zh"); const lines = zh.split("\n"); expect(lines).toContain(link); const idx = lines.indexOf(link); // URL 独占一行:前后都是空行 expect(lines[idx - 1]).toBe(""); expect(lines[idx + 1]).toBe(""); }); }); describe("第2批 — 按钮键盘(两行平铺,callback_data 契约)", () => { it("menu keyboard: marketplace on top row, admin below", () => { const kb = purchaseMenuKeyboard("zh"); const rows = kb.inline_keyboard as { text: string | undefined; callback_data: string; }[][]; expect(rows.length).toBe(2); expect(rows[0][0].callback_data).toBe("buy:marketplace"); expect(rows[1][0].callback_data).toBe("buy:admin"); }); it("guidance keyboard: contact admin + back", () => { const kb = marketplaceGuidanceKeyboard("zh"); const rows = kb.inline_keyboard as { text: string | undefined; callback_data: string; }[][]; expect(rows.length).toBe(2); expect(rows[0][0].callback_data).toBe("buy:contact"); expect(rows[1][0].callback_data).toBe("buy:back"); }); it("admin keyboard: back only", () => { const kb = adminContactKeyboard("zh"); const rows = kb.inline_keyboard as { text: string | undefined; callback_data: string; }[][]; expect(rows.length).toBe(1); expect(rows[0][0].callback_data).toBe("buy:back"); }); });