feat: add UPC to ASIN mapping and large file UPC analysis

Introduces the capability to resolve UPCs to ASINs using the Keepa API. This includes a new `upc-file` command for processing large Excel files of UPCs, a `upc` CLI tool for quick lookups, and API endpoints for web-based integration. The analysis pipeline was refactored into a reusable module to support both standard ASIN leads and new UPC-driven workflows.
This commit is contained in:
Victor Noguera
2026-04-16 23:06:55 -04:00
parent d25cf5d5ec
commit 32e7b0c485
14 changed files with 2278 additions and 250 deletions

View File

@@ -1,6 +1,25 @@
import { getDb } from "./database.ts";
import type { AnalysisResult } from "./types.ts";
export type RunCounts = {
totalProducts: number;
fbaCount: number;
fbmCount: number;
skipCount: number;
};
function computeRunCountsFromResults(results: AnalysisResult[]): RunCounts {
const fbaCount = results.filter((r) => r.verdict.verdict === "FBA").length;
const fbmCount = results.filter((r) => r.verdict.verdict === "FBM").length;
const skipCount = results.filter((r) => r.verdict.verdict === "SKIP").length;
return {
totalProducts: results.length,
fbaCount,
fbmCount,
skipCount,
};
}
function buildRow(r: AnalysisResult) {
const price =
r.product.keepa?.currentPrice ??
@@ -68,12 +87,25 @@ export function writeResultsToDb(
inputFile: string,
outputFile: string | undefined,
): void {
const database = getDb(dbPath);
const runCounts = computeRunCountsFromResults(results);
const runId = startRunInDb(dbPath, inputFile, outputFile, runCounts);
appendResultsToRun(dbPath, runId, results);
console.log(`Results written to SQLite database for run_id: ${runId}`);
}
export function startRunInDb(
dbPath: string,
inputFile: string,
outputFile: string | undefined,
counts: RunCounts = {
totalProducts: 0,
fbaCount: 0,
fbmCount: 0,
skipCount: 0,
},
): number {
const database = getDb(dbPath);
const timestamp = new Date().toISOString();
const fbaCount = results.filter((r) => r.verdict.verdict === "FBA").length;
const fbmCount = results.filter((r) => r.verdict.verdict === "FBM").length;
const skipCount = results.filter((r) => r.verdict.verdict === "SKIP").length;
const insertRun = database.prepare(
`INSERT INTO runs (
@@ -86,25 +118,39 @@ export function writeResultsToDb(
skip_count
) VALUES (?, ?, ?, ?, ?, ?, ?)`,
);
const runInfo = insertRun.run(
timestamp,
inputFile,
outputFile ?? null,
results.length,
fbaCount,
fbmCount,
skipCount,
counts.totalProducts,
counts.fbaCount,
counts.fbmCount,
counts.skipCount,
);
const runId =
(runInfo.changes as number) > 0
? (runInfo.lastInsertRowid as number)
: null;
if (runId === null) {
console.error("Failed to insert run record into SQLite.");
throw new Error("Failed to insert run record into SQLite.");
}
return runId;
}
export function appendResultsToRun(
dbPath: string,
runId: number,
results: AnalysisResult[],
): void {
if (results.length === 0) {
return;
}
const database = getDb(dbPath);
const insertResult = database.prepare(
`INSERT INTO results (
run_id, asin, product_name, brand, category, unit_cost, current_price,
@@ -174,7 +220,49 @@ export function writeResultsToDb(
);
}
})();
console.log(`Results written to SQLite database for run_id: ${runId}`);
}
export function refreshRunCountsInDb(dbPath: string, runId: number): RunCounts {
const database = getDb(dbPath);
const stats = database
.query(
`SELECT
COUNT(*) AS total,
SUM(CASE WHEN verdict = 'FBA' THEN 1 ELSE 0 END) AS fba,
SUM(CASE WHEN verdict = 'FBM' THEN 1 ELSE 0 END) AS fbm,
SUM(CASE WHEN verdict = 'SKIP' THEN 1 ELSE 0 END) AS skip
FROM results
WHERE run_id = ?`,
)
.get(runId) as {
total: number;
fba: number | null;
fbm: number | null;
skip: number | null;
};
const counts: RunCounts = {
totalProducts: stats.total ?? 0,
fbaCount: stats.fba ?? 0,
fbmCount: stats.fbm ?? 0,
skipCount: stats.skip ?? 0,
};
database
.query(
`UPDATE runs
SET total_products = ?, fba_count = ?, fbm_count = ?, skip_count = ?
WHERE id = ?`,
)
.run(
counts.totalProducts,
counts.fbaCount,
counts.fbmCount,
counts.skipCount,
runId,
);
return counts;
}
export function printResults(results: AnalysisResult[]): void {
const rows = results