Files

225 lines
9.3 KiB
TypeScript

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");
});
});