import assert from "node:assert/strict"; import { mkdir, rm } from "node:fs/promises"; import { test } from "node:test"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { buildDashboard, buildLivePitch, buildStyleGroups, calculateProfit, detectPlatform, extractPrice, inferJewelryTrack, scoreCapture, toCsv, } from "../src/core.js"; import { JsonStore } from "../src/store.js"; test("detectPlatform recognizes target marketplaces", () => { assert.equal(detectPlatform("https://www.taobao.com/list?q=耳环"), "taobao"); assert.equal(detectPlatform("https://detail.tmall.com/item.htm?id=1"), "tmall"); assert.equal(detectPlatform("https://www.xiaohongshu.com/search_result?keyword=藏式手链"), "xiaohongshu"); assert.equal(detectPlatform("https://detail.1688.com/offer/123.html"), "1688"); assert.equal(detectPlatform("https://example.com/item"), "other"); }); test("extractPrice parses Chinese ecommerce price strings", () => { assert.equal(extractPrice("¥39.90 起"), 39.9); assert.equal(extractPrice("¥ 12 - 18"), 12); assert.equal(extractPrice("批发价 3.50 元"), 3.5); assert.equal(extractPrice("暂无报价"), null); }); test("inferJewelryTrack classifies jewelry categories and Tibetan style", () => { assert.equal(inferJewelryTrack("藏式编绳手链 民族风 绿松石色"), "藏式/民族风"); assert.equal(inferJewelryTrack("法式珍珠耳环耳夹"), "耳饰"); assert.equal(inferJewelryTrack("复古发簪抓夹"), "发饰"); assert.equal(inferJewelryTrack("手机链包挂钥匙扣"), "包挂/手机链"); assert.equal(inferJewelryTrack("小众戒指开口戒"), "戒指"); }); test("scoreCapture gives actionable sourcing decisions", () => { const strong = scoreCapture({ platform: "1688", title: "藏式编绳手链 民族风 绿松石色 直播爆款", price: 8, image: "https://img.example/a.jpg", supplier: "义乌源头饰品厂", metrics: { sales: "1000+人付款", comments: "求链接 问材质" }, }); assert.equal(strong.track, "藏式/民族风"); assert.equal(strong.decision, "拿样"); assert.ok(strong.score >= 75); assert.ok(strong.reasons.some((reason) => reason.includes("直播"))); const risky = scoreCapture({ platform: "taobao", title: "天然绿松石纯银开光转运手串", price: 188, image: "", metrics: {}, }); assert.equal(risky.decision, "暂不做"); assert.ok(risky.risks.length >= 2); }); test("calculateProfit estimates live selling margin for low-ticket jewelry", () => { const profit = calculateProfit({ costPrice: 8, targetPrice: 39, shippingFee: 4, packagingFee: 1, platformFeeRate: 0.05, promoFee: 3, }); assert.equal(profit.grossProfit, 21.05); assert.equal(profit.grossMarginRate, 0.54); assert.equal(profit.recommendation, "可跑量"); }); test("buildLivePitch creates practical livestream talking points and warnings", () => { const item = scoreCapture({ platform: "1688", title: "藏式绿松石色编绳手链 民族风 直播叠戴", price: 7.8, supplier: "义乌源头饰品厂", metrics: { visibleText: "现货 混批" }, }); const pitch = buildLivePitch(item); assert.equal(pitch.audience, "喜欢民族风、旅行感、叠戴感的用户"); assert.ok(pitch.talkingPoints.some((point) => point.includes("藏式"))); assert.ok(pitch.supplierQuestions.some((question) => question.includes("材质"))); }); test("buildStyleGroups groups similar captured products across platforms", () => { const items = [ scoreCapture({ platform: "taobao", title: "藏式绿松石编绳手链 民族风", url: "https://item.taobao.com/item.htm?id=style-a", price: 39, }), scoreCapture({ platform: "1688", title: "民族风绿松石色编织手链 藏式", url: "https://detail.1688.com/offer/style-b.html", price: 6.8, supplier: "义乌源头厂", }), scoreCapture({ platform: "1688", title: "珍珠耳钉小众耳饰", url: "https://detail.1688.com/offer/style-c.html", price: 3.5, }), ]; const groups = buildStyleGroups(items); assert.equal(groups.length, 2); assert.equal(groups[0].items.length, 2); assert.equal(groups[0].bestSupply.title, "民族风绿松石色编织手链 藏式"); assert.ok(groups[0].suggestedRetailPrice >= 29); }); test("buildDashboard summarizes sourcing work and next actions", () => { const items = [ scoreCapture({ platform: "1688", title: "藏式手机链", price: 4.2, status: "ordered_sample" }), scoreCapture({ platform: "1688", title: "民族风手链", price: 8.8, status: "asking_supplier" }), scoreCapture({ platform: "taobao", title: "珍珠耳环", price: 59, status: "new" }), ]; const dashboard = buildDashboard(items); assert.equal(dashboard.totals.items, 3); assert.equal(dashboard.totals.samplePipeline, 2); assert.equal(dashboard.priceBands.under10, 2); assert.ok(dashboard.nextActions.length > 0); }); test("JsonStore inserts, updates, lists, deletes, and exports captures", async () => { const dir = join(tmpdir(), `sourcing-store-${Date.now()}`); await mkdir(dir, { recursive: true }); const store = new JsonStore(join(dir, "items.json")); const inserted = await store.insertMany([ { platform: "1688", title: "民族风手机链", url: "https://detail.1688.com/offer/1.html", image: "https://img.example/1.jpg", price: 4.2, }, ]); assert.equal(inserted.length, 1); assert.equal(inserted[0].decision, "拿样"); const updated = await store.update(inserted[0].id, { status: "ordered_sample" }); assert.equal(updated.status, "ordered_sample"); const listed = await store.list({ platform: "1688" }); assert.equal(listed.length, 1); const csv = toCsv(listed); assert.match(csv, /民族风手机链/); assert.match(csv, /ordered_sample/); const [header, firstRow] = csv.trim().split("\n"); const targetPriceIndex = header.split(",").indexOf("targetPrice"); assert.notEqual(firstRow.split(",")[targetPriceIndex], ""); await store.delete(inserted[0].id); assert.equal((await store.list()).length, 0); await rm(dir, { recursive: true, force: true }); }); test("JsonStore deletes multiple selected captures at once", async () => { const dir = join(tmpdir(), `sourcing-store-bulk-${Date.now()}`); await mkdir(dir, { recursive: true }); const store = new JsonStore(join(dir, "items.json")); const inserted = await store.insertMany([ { platform: "1688", title: "误采集款 A", url: "https://detail.1688.com/offer/bulk-a.html", price: 5, }, { platform: "1688", title: "误采集款 B", url: "https://detail.1688.com/offer/bulk-b.html", price: 6, }, { platform: "1688", title: "保留款 C", url: "https://detail.1688.com/offer/bulk-c.html", price: 7, }, ]); const result = await store.deleteMany([inserted[0].id, inserted[1].id, "not-found"]); assert.equal(result.deleted, 2); assert.deepEqual(result.missing, ["not-found"]); assert.deepEqual( (await store.list()).map((item) => item.title), ["保留款 C"], ); await rm(dir, { recursive: true, force: true }); }); test("JsonStore bulk-updates selected captures with status and tags", async () => { const dir = join(tmpdir(), `sourcing-store-bulk-update-${Date.now()}`); await mkdir(dir, { recursive: true }); const store = new JsonStore(join(dir, "items.json")); const inserted = await store.insertMany([ { platform: "1688", title: "待拿样藏式手链", url: "https://detail.1688.com/offer/bulk-update-a.html", price: 7, }, { platform: "1688", title: "待拿样手机链", url: "https://detail.1688.com/offer/bulk-update-b.html", price: 4, }, ]); const result = await store.bulkUpdate({ ids: inserted.map((item) => item.id), patch: { status: "asking_supplier" }, addTags: ["低价跑量", "直播待测"], }); assert.equal(result.updated, 2); const listed = await store.list({ status: "asking_supplier" }); assert.equal(listed.length, 2); assert.deepEqual(listed[0].tags, ["低价跑量", "直播待测"]); await rm(dir, { recursive: true, force: true }); }); test("JsonStore filters by live sourcing decision dimensions", async () => { const dir = join(tmpdir(), `sourcing-store-live-filters-${Date.now()}`); await mkdir(dir, { recursive: true }); const store = new JsonStore(join(dir, "items.json")); await store.insertMany([ { platform: "1688", title: "藏式编绳手机链 低价跑量", url: "https://detail.1688.com/offer/filter-a.html", price: 4.8, image: "https://img.example/a.jpg", supplier: "义乌源头饰品厂", }, { platform: "taobao", title: "天然绿松石纯银开光转运手串", url: "https://item.taobao.com/item.htm?id=filter-b", price: 188, }, { platform: "taobao", title: "珍珠耳环小众耳饰", url: "https://item.taobao.com/item.htm?id=filter-c", price: 18, tags: ["直播待测"], }, ]); assert.deepEqual( (await store.list({ priceBand: "under10", source: "only1688" })).map((item) => item.title), ["藏式编绳手机链 低价跑量"], ); assert.deepEqual( (await store.list({ risk: "high" })).map((item) => item.title), ["天然绿松石纯银开光转运手串"], ); assert.deepEqual( (await store.list({ tag: "低价" })).map((item) => item.title), ["藏式编绳手机链 低价跑量"], ); assert.deepEqual( (await store.list({ source: "non1688", priceBand: "10to30" })).map((item) => item.title), ["珍珠耳环小众耳饰"], ); await rm(dir, { recursive: true, force: true }); });