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:
Victor Noguera
2026-05-25 00:53:47 -04:00
parent b982edd160
commit c006d87c54
36 changed files with 1905 additions and 113 deletions

View 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");
});