feat: transition bestseller analysis storage to SQLite and add category blacklist

- Replaces Excel output with structured database tables for tracking category analysis runs and product results.
- Implements a blacklist to exclude specific category IDs from the bestseller pipeline.
- Adds unit tests for category processing and enhances logging with levels and timestamps.
- Introduces foreign key enforcement and updated schema definitions in the database module.
This commit is contained in:
Victor Noguera
2026-04-13 00:28:23 -04:00
parent 7ba6397578
commit a906f5ede3
7 changed files with 434 additions and 242 deletions

View File

@@ -0,0 +1,99 @@
import { test, expect, beforeAll, afterAll, beforeEach, mock } from "bun:test";
import { Database } from "bun:sqlite";
import { getDb, initDb, closeDb } from "./database";
import path from "node:path";
import { rmSync, mkdirSync } from "node:fs";
import {
main,
processCategory,
insertCategoryRunSummary,
insertProductAnalysisResults,
} from "./bestsellers-by-category";
import * as keepaModule from "./keepa";
import * as spApiModule from "./sp-api";
import * as llmModule from "./llm";
const DB_TEST_PATH = path.join(
process.cwd(),
"test_output",
"test_analysis.sqlite",
);
let db: Database;
beforeAll(() => {
// Ensure the test output directory exists and is clean
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);
});
afterAll(() => {
closeDb();
rmSync(path.dirname(DB_TEST_PATH), { recursive: true, force: true });
});
beforeEach(() => {
// Clear tables before each test if necessary, or use a fresh DB for each test
// For simplicity, we'll assume tables are clean after initDb in beforeAll
// and not clear for each test if data is not interdependent.
});
test("processCategory function test", async () => {
const mockCategory = {
id: 1,
label: "Category 1",
parentId: 0,
childCount: 0,
};
const summary = await processCategory(db, mockCategory, 2);
expect(summary.status).toBe("ok");
expect(summary.categoryId).toBe(mockCategory.id);
expect(summary.categoryLabel).toBe(mockCategory.label);
expect(summary.topAsinsChecked).toBe(2);
expect(summary.availableAsins).toBe(2);
expect(summary.fba).toBe(1);
expect(summary.fbm).toBe(1);
expect(summary.skip).toBe(0);
expect(summary.results?.length).toBe(2);
const runId = await insertCategoryRunSummary(
db,
summary,
new Date().toISOString(),
);
if (summary.results) {
await insertProductAnalysisResults(db, runId, summary.results);
}
// Verify category run summary insertion
const categoryRun = db
.query("SELECT * FROM category_analysis_runs")
.all() as any[];
expect(categoryRun.length).toBe(1);
expect(categoryRun[0].category_label).toBe("Category 1");
expect(categoryRun[0].top_asins_checked).toBe(2);
expect(categoryRun[0].available_asins).toBe(2);
expect(categoryRun[0].fba_count).toBe(1);
expect(categoryRun[0].fbm_count).toBe(1);
expect(categoryRun[0].status).toBe("ok");
// Verify product analysis results insertion
const productResults = db
.query("SELECT * FROM product_analysis_results ORDER BY asin")
.all() as any[];
expect(productResults.length).toBe(2);
expect(productResults[0].asin).toBe("B000000001");
expect(productResults[0].name).toBe("Product One");
expect(productResults[0].verdict).toBe("FBA");
expect(productResults[0].run_id).toBe(categoryRun[0].id);
expect(productResults[1].asin).toBe("B000000002");
expect(productResults[1].name).toBe("Product Two");
expect(productResults[1].verdict).toBe("FBM");
expect(productResults[1].run_id).toBe(categoryRun[0].id);
});