- Implemented supplier scoring logic in `supplier-scoring.ts` with functions to compute demand score, competition penalty, and overall supplier product score. - Created unit tests for supplier scoring in `supplier-scoring.test.ts` to validate scoring logic against various scenarios. - Developed UPC file analysis tool in `upc-file-analysis.ts` to process UPCs in batches, fetch product data from Keepa and SP-API, and generate supplier results. - Added UPC input reading functionality in `upc-file-reader.ts` to handle XLSX and XLS files, including validation for UPC formats. - Introduced a command-line tool in `upc-lookup.ts` for looking up UPCs and displaying detailed results or mappings to ASINs. - Enhanced error handling and logging throughout the new modules for better traceability and user feedback.
98 lines
2.3 KiB
TypeScript
98 lines
2.3 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { scoreSupplierProduct } from "./supplier-scoring.ts";
|
|
import type { KeepaData, ProductRecord, SpApiData } from "../types.ts";
|
|
|
|
function record(overrides: Partial<ProductRecord> = {}): ProductRecord {
|
|
return {
|
|
asin: "B000000001",
|
|
name: "Test Product",
|
|
unitCost: 10,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function keepa(overrides: Partial<KeepaData> = {}): KeepaData {
|
|
return {
|
|
currentPrice: 30,
|
|
avgPrice90: 29,
|
|
minPrice90: 25,
|
|
maxPrice90: 35,
|
|
salesRank: 8_000,
|
|
salesRankAvg90: 10_000,
|
|
salesRankDrops30: 80,
|
|
salesRankDrops90: 220,
|
|
sellerCount: 4,
|
|
amazonIsSeller: false,
|
|
amazonBuyboxSharePct90d: 0,
|
|
buyBoxSeller: "SELLER",
|
|
buyBoxPrice: 30,
|
|
buyBoxAvg90: 29,
|
|
monthlySold: 350,
|
|
categoryTree: ["Grocery"],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function spApi(overrides: Partial<SpApiData> = {}): SpApiData {
|
|
return {
|
|
fbaFee: 5,
|
|
fbmFee: 3,
|
|
referralFeePercent: 15,
|
|
estimatedSalePrice: 30,
|
|
canSell: true,
|
|
sellabilityStatus: "available",
|
|
sellabilityReason: "ok",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test("profitable high-demand product ranks above competitive product", () => {
|
|
const strong = scoreSupplierProduct(record(), keepa(), spApi());
|
|
const competitive = scoreSupplierProduct(
|
|
record(),
|
|
keepa({
|
|
sellerCount: 35,
|
|
amazonIsSeller: true,
|
|
amazonBuyboxSharePct90d: 90,
|
|
}),
|
|
spApi(),
|
|
);
|
|
|
|
expect(strong.verdict).toBe("BUY");
|
|
expect(strong.score).toBeGreaterThan(competitive.score);
|
|
});
|
|
|
|
test("missing cost skips", () => {
|
|
const score = scoreSupplierProduct(record({ unitCost: 0 }), keepa(), spApi());
|
|
|
|
expect(score.verdict).toBe("SKIP");
|
|
expect(score.reason).toContain("unit cost");
|
|
});
|
|
|
|
test("restricted ASIN skips", () => {
|
|
const score = scoreSupplierProduct(
|
|
record(),
|
|
keepa(),
|
|
spApi({ canSell: false, sellabilityStatus: "restricted" }),
|
|
);
|
|
|
|
expect(score.verdict).toBe("SKIP");
|
|
expect(score.reason).toContain("restricted");
|
|
});
|
|
|
|
test("missing price skips", () => {
|
|
const score = scoreSupplierProduct(
|
|
record(),
|
|
keepa({
|
|
currentPrice: null,
|
|
avgPrice90: null,
|
|
buyBoxPrice: null,
|
|
buyBoxAvg90: null,
|
|
}),
|
|
spApi({ estimatedSalePrice: 0 }),
|
|
);
|
|
|
|
expect(score.verdict).toBe("SKIP");
|
|
expect(score.reason).toContain("price");
|
|
});
|