mercury-web/src/features/accounting/suggestCategory.test.ts
Munkherdene 79dd6ed591 fix(web): parse real merchant out of card-string :MCI: segment
Card charges were showing as raw masked-card strings with no suggestion.
prettyMerchant/suggestCategory now extract the merchant after the last
:MCI: segment (e.g. "...:MCI:ANTHROPIC 1" -> "ANTHROPIC"), and the
keyword map is grounded in real uncategorized merchants (card-processed
software/cloud, cross-border marketplaces, local lenders, grocers,
carriers, electronics, gaming). Also fixes a Cyrillic \b word-boundary
bug (JS's \b never matches non-ASCII text) and moves CAFE/КАФЕ to the
Coffee rule instead of Food & Drink.
2026-08-23 00:41:58 +08:00

159 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, it, expect } from "vitest";
import { suggestCategory, extractMerchant, looksLikeCardNumber } from "./suggestCategory";
import type { Category } from "@/api/schemas";
function cat(name: string, kind: "income" | "expense" = "expense", depth = 1): Category {
return { name, kind, depth };
}
const CATEGORIES: Category[] = [
cat("Food & Drink"),
cat("Coffee"),
cat("Transport"),
cat("Shopping"),
cat("Bills & Services"),
cat("Entertainment"),
cat("Groceries"),
cat("Insurance"),
cat("Electronics"),
];
describe("extractMerchant", () => {
it("pulls the real merchant out of a card string's :MCI: segment", () => {
expect(extractMerchant("554835******6886:30-07-2026 11:02:10:MCI:ANTHROPIC 1")).toBe("ANTHROPIC");
expect(extractMerchant("554835******6886:13-08-2026 12:13:52:MCI:WWW HOSTI 5")).toBe("WWW HOSTI");
expect(extractMerchant("554835******6886:11-08-2026 10:07:35:MCI:TAOBAO CO 4")).toBe("TAOBAO CO");
});
it("falls back to the head before the first colon when there's no :MCI: segment", () => {
expect(extractMerchant("ЛИЗИНГ ХХК")).toBe("ЛИЗИНГ ХХК");
expect(extractMerchant("1234******5678")).toBe("1234******5678");
});
});
describe("suggestCategory — real card charges (:MCI: merchant extraction)", () => {
it("suggests Bills & Services for ANTHROPIC (no Software/Subscriptions category present)", () => {
expect(suggestCategory("554835******6886:30-07-2026 11:02:10:MCI:ANTHROPIC 1", CATEGORIES)).toBe(
"Bills & Services",
);
});
it("prefers Software over Bills & Services when the category exists", () => {
const withSoftware = [...CATEGORIES, cat("Software")];
expect(suggestCategory("554835******6886:30-07-2026 11:02:10:MCI:ANTHROPIC 1", withSoftware)).toBe("Software");
});
it("suggests Bills & Services for a hosting merchant (WWW HOSTI)", () => {
expect(suggestCategory("554835******6886:13-08-2026 12:13:52:MCI:WWW HOSTI 5", CATEGORIES)).toBe(
"Bills & Services",
);
});
it("suggests Shopping for TAOBAO", () => {
expect(suggestCategory("554835******6886:11-08-2026 10:07:35:MCI:TAOBAO CO 4", CATEGORIES)).toBe("Shopping");
});
});
describe("suggestCategory — local merchants", () => {
it("matches ТОКИ (non-bank lender) to Loan", () => {
const withLoan = [...CATEGORIES, cat("Loan")];
expect(suggestCategory("ТОКИ ББСБ ХХК", withLoan)).toBe("Loan");
});
it("falls back to Bills & Services when Loan doesn't exist", () => {
expect(suggestCategory("ЛИЗИНГ ХХК", CATEGORIES)).toBe("Bills & Services");
});
it("matches the Cyrillic ЗЭЭЛ keyword (word-boundary-free, Cyrillic-safe)", () => {
const withLoan = [...CATEGORIES, cat("Loan")];
expect(suggestCategory("ХААН ЗЭЭЛ ТӨЛБӨР", withLoan)).toBe("Loan");
});
it("matches TUSHIG to Groceries", () => {
expect(suggestCategory("TUSHIG SUPERMARKET", CATEGORIES)).toBe("Groceries");
});
it("matches other grocery keywords", () => {
expect(suggestCategory("NOMIN SUPERMARKET", CATEGORIES)).toBe("Groceries");
expect(suggestCategory("CU-24 CONVENIENCE", CATEGORIES)).toBe("Groceries");
expect(suggestCategory("ХҮНСНИЙ ДЭЛГҮҮР", CATEGORIES)).toBe("Groceries");
});
it("matches insurance keywords", () => {
expect(suggestCategory("MONGOL ДААТГАЛ LLC", CATEGORIES)).toBe("Insurance");
expect(suggestCategory("SOME INSURANCE CO", CATEGORIES)).toBe("Insurance");
});
it("matches electronics retailers", () => {
expect(suggestCategory("MAGIC TECH STORE", CATEGORIES)).toBe("Electronics");
expect(suggestCategory("ITOPIA MALL", CATEGORIES)).toBe("Electronics");
});
it("matches mobile carriers to Bills & Services", () => {
expect(suggestCategory("MOBICOM PAYMENT", CATEGORIES)).toBe("Bills & Services");
expect(suggestCategory("UNITEL TOP UP", CATEGORIES)).toBe("Bills & Services");
});
it("matches apparel/shopping brands", () => {
expect(suggestCategory("CONVERSE STORE", CATEGORIES)).toBe("Shopping");
expect(suggestCategory("SANT ASAR TRADE", CATEGORIES)).toBe("Shopping");
});
it("matches gaming merchants to Entertainment", () => {
expect(suggestCategory("STEAM GAMES", CATEGORIES)).toBe("Entertainment");
expect(suggestCategory("PGAMING WALLET", CATEGORIES)).toBe("Entertainment");
});
it("matches CAFE/КАФЕ to Coffee, not Food & Drink", () => {
expect(suggestCategory("КАФЕ МОДЕРН", CATEGORIES)).toBe("Coffee");
expect(suggestCategory("TOM CAFE LLC", CATEGORIES)).toBe("Coffee");
expect(suggestCategory("КОФЕ ЦЭГ", CATEGORIES)).toBe("Coffee");
expect(suggestCategory("STARBUCKS COFFEE", CATEGORIES)).toBe("Coffee");
});
it("matches restaurant/food keywords (no CAFE overlap) to Food & Drink", () => {
expect(suggestCategory("KFC ULAANBAATAR", CATEGORIES)).toBe("Food & Drink");
expect(suggestCategory("ХООЛНЫ ГАЗАР", CATEGORIES)).toBe("Food & Drink");
expect(suggestCategory("BURGER KING", CATEGORIES)).toBe("Food & Drink");
});
it("matches transport/fuel keywords", () => {
expect(suggestCategory("UBCAB TRIP", CATEGORIES)).toBe("Transport");
expect(suggestCategory("ШАТАХУУНЫ СТАНЦ", CATEGORIES)).toBe("Transport");
});
});
describe("suggestCategory — no signal", () => {
it("returns null for a masked card number with no :MCI: merchant segment", () => {
expect(suggestCategory("554835******6886:13-08-2026 09:41:22", CATEGORIES)).toBeNull();
expect(suggestCategory("1234******5678", CATEGORIES)).toBeNull();
});
it("returns null when nothing matches", () => {
expect(suggestCategory("TLJ CENTR", CATEGORIES)).toBeNull();
expect(suggestCategory("TSENGELDE", CATEGORIES)).toBeNull();
});
it("returns null when every matching rule's candidates are all absent", () => {
// The phone-carrier rule has a single candidate ("Bills & Services") with
// no fallback, unlike Loan/Insurance/Groceries/Electronics which fall
// back to a broader category — removing it leaves nothing to suggest.
const noBills = CATEGORIES.filter((c) => c.name !== "Bills & Services");
expect(suggestCategory("MOBICOM PAYMENT", noBills)).toBeNull();
});
it("returns null for empty input", () => {
expect(suggestCategory("", CATEGORIES)).toBeNull();
});
});
describe("looksLikeCardNumber", () => {
it("recognizes a masked card head as a card number", () => {
expect(looksLikeCardNumber("554835******6886")).toBe(true);
});
it("does not flag a normal merchant name", () => {
expect(looksLikeCardNumber("ANTHROPIC")).toBe(false);
expect(looksLikeCardNumber("ТОКИ ББСБ ХХК")).toBe(false);
});
});