Initial release with production deploy config.
Add GitAtlas deploy assets, DATA_FILE support for isolated server data, and port documentation for sourcing.simosen.cn. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
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 });
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user