feat: initialize asin-check project with Bun

- Add README.md with installation and usage instructions.
- Create bun.lock for dependency management.
- Add package.json to define project metadata and dependencies.
- Implement caching with Redis in cache.ts for ASIN data.
- Configure environment variables in config.ts for API keys and Redis URL.
- Develop main application logic in index.ts to read products, fetch data, and analyze results.
- Integrate Keepa API for product data retrieval in keepa.ts.
- Create LLM analysis functionality in llm.ts for product viability assessment.
- Implement product reading from Excel files in reader.ts.
- Stub SP-API integration in sp-api.ts for future implementation.
- Define TypeScript types in types.ts for product and analysis data structures.
- Write results to console and CSV in writer.ts.
- Configure TypeScript settings in tsconfig.json for project compilation.
This commit is contained in:
Victor Noguera
2026-04-04 21:33:27 -04:00
commit 061f771279
17 changed files with 1005 additions and 0 deletions

81
src/reader.ts Normal file
View File

@@ -0,0 +1,81 @@
import * as XLSX from "xlsx";
import type { ProductRecord } from "./types.ts";
export function readProducts(filePath: string): ProductRecord[] {
const workbook = XLSX.readFile(filePath);
const sheetName = workbook.SheetNames[0];
if (!sheetName) throw new Error("No sheets found in file");
const sheet = workbook.Sheets[sheetName]!;
const rows = XLSX.utils.sheet_to_json<Record<string, unknown>>(sheet);
if (rows.length === 0) throw new Error("File contains no data rows");
const headers = Object.keys(rows[0]!);
const asinCol = findColumn(headers, ["asin"]);
const nameCol = findColumn(headers, ["name", "product name", "title", "product title"]);
const costCol = findColumn(headers, ["unit cost", "cost", "unitcost", "unit_cost", "price", "buy cost"]);
const brandCol = findColumn(headers, ["brand"]);
const categoryCol = findColumn(headers, ["category"]);
const amazonRankCol = findColumn(headers, ["amazon rank", "amazonrank", "sales rank", "bsr"]);
const fbaNetCol = findColumn(headers, ["fba net", "fbanet", "fba_net"]);
const grossProfitCol = findColumn(headers, ["gross profit $", "gross profit", "grossprofit"]);
const grossProfitPctCol = findColumn(headers, ["gross profit %", "gross profit pct", "grossprofitpct"]);
const moqCol = findColumn(headers, ["moq", "min order qty", "minimum order quantity"]);
const moqCostCol = findColumn(headers, ["moq cost", "moqcost"]);
const totalQtyCol = findColumn(headers, ["total qty avail", "totalqtyavail", "qty available", "quantity"]);
const linkCol = findColumn(headers, ["link", "url", "source"]);
if (!asinCol) throw new Error(`No ASIN column found. Available columns: ${headers.join(", ")}`);
const knownCols = new Set([asinCol, nameCol, costCol, brandCol, categoryCol, amazonRankCol, fbaNetCol, grossProfitCol, grossProfitPctCol, moqCol, moqCostCol, totalQtyCol, linkCol].filter(Boolean));
const products: ProductRecord[] = [];
for (const row of rows) {
const asin = String(row[asinCol] ?? "").trim().toUpperCase();
if (!asin || !/^B[0-9A-Z]{9}$/.test(asin)) {
console.warn(`Skipping invalid ASIN: "${asin}"`);
continue;
}
const name = nameCol ? String(row[nameCol] ?? "") : "";
const unitCost = costCol ? parseFloat(String(row[costCol] ?? "0")) : 0;
const extra: Record<string, unknown> = {};
for (const h of headers) {
if (!knownCols.has(h)) extra[h] = row[h];
}
products.push({
asin,
name,
unitCost,
brand: brandCol ? String(row[brandCol] ?? "") : undefined,
category: categoryCol ? String(row[categoryCol] ?? "") : undefined,
amazonRank: amazonRankCol ? Number(row[amazonRankCol]) || undefined : undefined,
fbaNet: fbaNetCol ? Number(row[fbaNetCol]) || undefined : undefined,
grossProfit: grossProfitCol ? Number(row[grossProfitCol]) || undefined : undefined,
grossProfitPct: grossProfitPctCol ? Number(row[grossProfitPctCol]) || undefined : undefined,
moq: moqCol ? Number(row[moqCol]) || undefined : undefined,
moqCost: moqCostCol ? Number(row[moqCostCol]) || undefined : undefined,
totalQtyAvail: totalQtyCol ? Number(row[totalQtyCol]) || undefined : undefined,
link: linkCol ? String(row[linkCol] ?? "") : undefined,
...extra,
});
}
console.log(`Read ${products.length} valid products from ${filePath}`);
return products;
}
function findColumn(headers: string[], candidates: string[]): string | undefined {
for (const candidate of candidates) {
const match = headers.find((h) => h.toLowerCase().trim() === candidate);
if (match) return match;
}
return undefined;
}