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:
81
src/reader.ts
Normal file
81
src/reader.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user