feat: enhance stalker functionality with inventory sellability checks and update frontend display
This commit is contained in:
163
src/stalker-sellability.test.ts
Normal file
163
src/stalker-sellability.test.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { afterAll, beforeEach, expect, mock, test } from "bun:test";
|
||||
import { mkdirSync, rmSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import * as XLSX from "xlsx";
|
||||
import { closeDb, getDb } from "./database.ts";
|
||||
|
||||
const TEST_DIR = path.join(process.cwd(), "test_output", "stalker-sellability");
|
||||
const originalFetch = globalThis.fetch;
|
||||
const originalKeepaKey = Bun.env.KEEPA_API_KEY;
|
||||
|
||||
const fetchSellabilityBatchMock = mock(async (asins: string[]) => {
|
||||
return new Map(
|
||||
asins.map((asin) => [
|
||||
asin,
|
||||
asin === "B111111111"
|
||||
? {
|
||||
canSell: true,
|
||||
sellabilityStatus: "available" as const,
|
||||
sellabilityReason: "No listing restrictions reported",
|
||||
}
|
||||
: {
|
||||
canSell: false,
|
||||
sellabilityStatus: "restricted" as const,
|
||||
sellabilityReason: "approval required",
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
mock.module("./sp-api.ts", () => ({
|
||||
fetchSellabilityBatch: fetchSellabilityBatchMock,
|
||||
}));
|
||||
|
||||
const modulePromise = import("./stalker.ts");
|
||||
|
||||
beforeEach(() => {
|
||||
closeDb();
|
||||
rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
mkdirSync(TEST_DIR, { recursive: true });
|
||||
globalThis.fetch = originalFetch;
|
||||
Bun.env.KEEPA_API_KEY = "test-keepa-key";
|
||||
fetchSellabilityBatchMock.mockClear();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
if (originalKeepaKey == null) {
|
||||
delete Bun.env.KEEPA_API_KEY;
|
||||
} else {
|
||||
Bun.env.KEEPA_API_KEY = originalKeepaKey;
|
||||
}
|
||||
closeDb();
|
||||
rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("sellability checks matched seller inventory, not the source ASIN", async () => {
|
||||
const { runStalker } = await modulePromise;
|
||||
const inputPath = path.join(TEST_DIR, "input.xlsx");
|
||||
const dbPath = path.join(TEST_DIR, "stalker.sqlite");
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(
|
||||
workbook,
|
||||
XLSX.utils.json_to_sheet([{ asin: "B000000001" }]),
|
||||
"Input",
|
||||
);
|
||||
XLSX.writeFile(workbook, inputPath);
|
||||
|
||||
globalThis.fetch = mock(async (input: string | URL | Request) => {
|
||||
const rawUrl =
|
||||
typeof input === "string"
|
||||
? input
|
||||
: input instanceof URL
|
||||
? input.toString()
|
||||
: input.url;
|
||||
const url = new URL(rawUrl);
|
||||
|
||||
if (url.pathname === "/product") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
products: [
|
||||
{
|
||||
asin: "B000000001",
|
||||
title: "Source Product",
|
||||
offers: [{ sellerId: "AQUALIFIED", price: 1999 }],
|
||||
},
|
||||
],
|
||||
tokensLeft: 10,
|
||||
refillRate: 10,
|
||||
}),
|
||||
{ status: 200 },
|
||||
);
|
||||
}
|
||||
|
||||
if (url.pathname === "/seller") {
|
||||
const wantsStorefront = url.searchParams.get("storefront") === "1";
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
sellers: {
|
||||
AQUALIFIED: {
|
||||
sellerName: "New Seller",
|
||||
currentRatingCount: 12,
|
||||
asinList: wantsStorefront ? ["B111111111", "B222222222"] : [],
|
||||
},
|
||||
},
|
||||
tokensLeft: 10,
|
||||
refillRate: 10,
|
||||
}),
|
||||
{ status: 200 },
|
||||
);
|
||||
}
|
||||
|
||||
return new Response("not found", { status: 404 });
|
||||
}) as unknown as typeof globalThis.fetch;
|
||||
|
||||
const stats = await runStalker({
|
||||
input: inputPath,
|
||||
dbPath,
|
||||
maxAsins: null,
|
||||
storefrontUpdateHours: 168,
|
||||
offerLimit: 20,
|
||||
sellerLimit: 30,
|
||||
inventoryLimit: 200,
|
||||
sellerCacheHours: 168,
|
||||
includeStock: false,
|
||||
dryRun: false,
|
||||
resume: true,
|
||||
maxSellerRequests: null,
|
||||
sellability: true,
|
||||
});
|
||||
|
||||
expect(fetchSellabilityBatchMock.mock.calls.length).toBe(1);
|
||||
expect(fetchSellabilityBatchMock.mock.calls[0]?.[0]).toEqual([
|
||||
"B111111111",
|
||||
"B222222222",
|
||||
]);
|
||||
expect(stats.inventorySellabilityCheckedAsins).toBe(2);
|
||||
expect(stats.inventorySellabilityAvailableAsins).toBe(1);
|
||||
expect(stats.inventorySellabilityExcludedAsins).toBe(1);
|
||||
expect(stats.persistedInventoryAsins).toBe(1);
|
||||
|
||||
const db = getDb(dbPath);
|
||||
const scan = db.query("SELECT source_asin FROM stalker_asin_scans").get() as {
|
||||
source_asin: string;
|
||||
};
|
||||
expect(scan.source_asin).toBe("B000000001");
|
||||
|
||||
const inventory = db
|
||||
.query(
|
||||
"SELECT asin, can_sell, sellability_status FROM stalker_seller_inventory ORDER BY asin",
|
||||
)
|
||||
.all() as Array<{
|
||||
asin: string;
|
||||
can_sell: number | null;
|
||||
sellability_status: string | null;
|
||||
}>;
|
||||
expect(inventory).toEqual([
|
||||
{
|
||||
asin: "B111111111",
|
||||
can_sell: 1,
|
||||
sellability_status: "available",
|
||||
},
|
||||
]);
|
||||
});
|
||||
Reference in New Issue
Block a user