import { test, expect, beforeAll, afterAll, beforeEach, mock } from "bun:test"; import { Database } from "bun:sqlite"; import { getDb, initDb, closeDb } from "./database.ts"; import path from "node:path"; import { rmSync, mkdirSync } from "node:fs"; const fetchSellabilityBatchMock = mock(async (asins: string[]) => { return new Map( asins.map((asin) => { if (asin === "B000000003") { return [ asin, { canSell: false, sellabilityStatus: "restricted" as const, sellabilityReason: "restricted", }, ]; } return [ asin, { canSell: true, sellabilityStatus: "available" as const, sellabilityReason: "ok", }, ]; }), ); }); const fetchSpApiPricingAndFeesMock = mock(async () => ({ fbaFee: 4, fbmFee: 2, referralFeePercent: 15, estimatedSalePrice: 25, canSell: true, sellabilityStatus: "available" as const, sellabilityReason: "ok", })); const analyzeProductsMock = mock(async (products: any[]) => { return products.map((p) => ({ asin: p.record.asin, verdict: "FBA", confidence: 90, reasoning: "mocked", })); }); mock.module("./sp-api.ts", () => ({ fetchSellabilityBatch: fetchSellabilityBatchMock, fetchSpApiPricingAndFees: fetchSpApiPricingAndFeesMock, })); mock.module("./llm.ts", () => ({ analyzeProducts: analyzeProductsMock, })); const modulePromise = import("./top-monthly-sold-by-category.ts"); const DB_TEST_PATH = path.join( process.cwd(), "test_output", "test_monthly_sold_analysis.sqlite", ); let db: Database; let processCategory: ( db: Database, runId: number, category: any, perCategoryTop: number, categoryCandidatePool: number, minMonthlySold: number, ) => Promise; let insertCategoryRunSummary: ( db: Database, summary: any, runTimestamp: string, ) => Promise; let originalFetch: typeof globalThis.fetch; beforeAll(async () => { const mod = await modulePromise; processCategory = mod.processCategory; insertCategoryRunSummary = mod.insertCategoryRunSummary; rmSync(path.dirname(DB_TEST_PATH), { recursive: true, force: true }); mkdirSync(path.dirname(DB_TEST_PATH), { recursive: true }); initDb(DB_TEST_PATH); db = getDb(DB_TEST_PATH); originalFetch = globalThis.fetch; }); afterAll(() => { globalThis.fetch = originalFetch; closeDb(); rmSync(path.dirname(DB_TEST_PATH), { recursive: true, force: true }); }); beforeEach(() => { db.run("DELETE FROM product_analysis_results"); db.run("DELETE FROM category_analysis_runs"); 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 === "/bestsellers") { return new Response( JSON.stringify({ bestSellersList: [ "B000000001", "B000000002", "B000000003", "B000000004", ], tokensLeft: 10, refillRate: 1, }), { status: 200 }, ); } if (url.pathname === "/product") { return new Response( JSON.stringify({ products: [ { asin: "B000000001", title: "Product One", monthlySold: 600, stats: { current: [ null, null, null, 1000, null, null, null, null, null, null, null, 2, null, null, null, null, null, null, 2599, ], avg: [2400, null, null, 1200], }, csv: [[1, 2599]], categoryTree: [{ name: "Category 1" }], }, { asin: "B000000002", title: "Product Two", monthlySold: 250, stats: { current: [ null, null, null, 2000, null, null, null, null, null, null, null, 3, null, null, null, null, null, null, 1999, ], avg: [1800, null, null, 2200], }, csv: [[1, 1999]], categoryTree: [{ name: "Category 1" }], }, { asin: "B000000003", title: "Product Three", monthlySold: 800, stats: { current: [ null, null, null, 1500, null, null, null, null, null, null, null, 1, null, null, null, null, null, null, 2099, ], avg: [2000, null, null, 1800], }, csv: [[1, 2099]], categoryTree: [{ name: "Category 1" }], }, { asin: "B000000004", title: "Product Four", monthlySold: 400, stats: { current: [ null, null, null, 3000, null, null, null, null, null, null, null, 4, null, null, null, null, null, null, 2899, ], avg: [2600, null, null, 2800], }, csv: [[1, 2899]], categoryTree: [{ name: "Category 1" }], }, ], tokensLeft: 10, refillRate: 1, }), { status: 200 }, ); } return new Response("not found", { status: 404 }); }) as unknown as typeof globalThis.fetch; }); test("processCategory filters to sellable ASINs with monthly sold >= threshold and keeps top-N", async () => { const mockCategory = { id: 1, label: "Category 1", parentId: 0, childCount: 0, }; const runId = await insertCategoryRunSummary( db, { categoryId: mockCategory.id, categoryLabel: mockCategory.label, topAsinsChecked: 0, availableAsins: 0, fba: 0, fbm: 0, skip: 0, status: "running", error: "", results: [], }, new Date().toISOString(), ); const summary = await processCategory(db, runId, mockCategory, 2, 4, 300); expect(summary.status).toBe("ok"); expect(summary.topAsinsChecked).toBe(4); expect(summary.availableAsins).toBe(2); expect(summary.results?.length).toBe(2); const productResults = db .query( "SELECT asin, monthly_sold FROM product_analysis_results ORDER BY monthly_sold DESC", ) .all() as Array<{ asin: string; monthly_sold: number }>; expect(productResults.length).toBe(2); expect(productResults[0]?.asin).toBe("B000000001"); expect(productResults[0]?.monthly_sold).toBe(600); expect(productResults[1]?.asin).toBe("B000000004"); expect(productResults[1]?.monthly_sold).toBe(400); });