feat: add supplier scoring and UPC file analysis functionality
- 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.
This commit is contained in:
97
src/supplier/supplier-scoring.test.ts
Normal file
97
src/supplier/supplier-scoring.test.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
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");
|
||||
});
|
||||
Reference in New Issue
Block a user