import ExcelJS from "exceljs"; import { dirname } from "node:path"; import { mkdirSync } from "node:fs"; import type { KeepaUpcLookupStatus, SupplierAnalysisResult, SupplierVerdict, } from "../types.ts"; export type SupplierExportSummary = { processedRows: number; resolvedRows: number; eligibleRows: number; verdictCounts: Record; unresolvedByStatus: Record; }; function pct(value: number | null): number | "" { return value == null ? "" : Math.round(value * 10_000) / 100; } function rowForResult(result: SupplierAnalysisResult) { const category = result.record.category ?? result.keepa?.categoryTree?.join(" > ") ?? ""; const canSell = result.spApi?.canSell == null ? "" : result.spApi.canSell ? "yes" : "no"; return { UPC: result.upc, ASIN: result.lookup.asin ?? "", Name: result.record.name, Brand: result.record.brand ?? "", Category: category, "Unit Cost": result.record.unitCost || "", "Sale Price": result.score.salePrice ?? "", "FBA Fee": result.score.fbaFee ?? "", Profit: result.score.profit ?? "", "Margin %": pct(result.score.margin), "ROI %": pct(result.score.roi), "BSR Current": result.keepa?.salesRank ?? "", "BSR 90d": result.keepa?.salesRankAvg90 ?? "", "Rank Drops 30d": result.keepa?.salesRankDrops30 ?? "", "Rank Drops 90d": result.keepa?.salesRankDrops90 ?? "", "Monthly Sold": result.keepa?.monthlySold ?? "", "Seller Count": result.keepa?.sellerCount ?? "", "Amazon Share 90d %": result.keepa?.amazonBuyboxSharePct90d ?? "", "Can Sell": canSell, Sellability: result.spApi?.sellabilityStatus ?? "", Score: result.score.score, Verdict: result.score.verdict, Reason: result.score.reason, "Lookup Status": result.lookup.status, "Candidate ASINs": result.lookup.candidateAsins.join(","), "Lookup Reason": result.lookup.reason ?? "", }; } function addRowsSheet( workbook: ExcelJS.Workbook, name: string, rows: ReturnType[], ): void { const sheet = workbook.addWorksheet(name); const headers = rows[0] ? Object.keys(rows[0]) : Object.keys(rowForResult({ upc: "", record: { name: "", unitCost: 0 }, product: null, lookup: { requestedUpc: "", normalizedUpc: "", status: "not_found", asin: null, candidateAsins: [], keepaData: null, }, keepa: null, spApi: null, score: { salePrice: null, fbaFee: null, profit: null, margin: null, roi: null, demandScore: 0, competitionPenalty: 1, score: 0, verdict: "SKIP", reason: "", }, fetchedAt: "", })); sheet.columns = headers.map((header) => ({ header, key: header, width: Math.min(Math.max(header.length + 4, 12), 28), })); sheet.addRows(rows); sheet.views = [{ state: "frozen", ySplit: 1 }]; sheet.autoFilter = { from: { row: 1, column: 1 }, to: { row: 1, column: headers.length }, }; sheet.getRow(1).font = { bold: true }; } function addSummarySheet( workbook: ExcelJS.Workbook, summary: SupplierExportSummary, ): void { const sheet = workbook.addWorksheet("Summary"); sheet.columns = [ { header: "Metric", key: "Metric", width: 28 }, { header: "Value", key: "Value", width: 18 }, ]; sheet.addRows([ { Metric: "Processed Rows", Value: summary.processedRows }, { Metric: "Resolved Rows", Value: summary.resolvedRows }, { Metric: "Eligible Rows", Value: summary.eligibleRows }, { Metric: "BUY", Value: summary.verdictCounts.BUY }, { Metric: "WATCH", Value: summary.verdictCounts.WATCH }, { Metric: "SKIP", Value: summary.verdictCounts.SKIP }, { Metric: "Unresolved invalid_upc", Value: summary.unresolvedByStatus.invalid_upc }, { Metric: "Unresolved not_found", Value: summary.unresolvedByStatus.not_found }, { Metric: "Unresolved multiple_asins", Value: summary.unresolvedByStatus.multiple_asins }, { Metric: "Unresolved request_failed", Value: summary.unresolvedByStatus.request_failed }, ]); sheet.getRow(1).font = { bold: true }; } export async function writeSupplierWorkbook( outputFile: string, results: SupplierAnalysisResult[], summary: SupplierExportSummary, ): Promise { const outputDir = dirname(outputFile); if (outputDir && outputDir !== ".") { mkdirSync(outputDir, { recursive: true }); } const workbook = new ExcelJS.Workbook(); workbook.creator = "asin-check"; workbook.created = new Date(); const ranked = results .filter((result) => result.score.verdict !== "SKIP") .sort((a, b) => b.score.score - a.score.score) .map(rowForResult); const skipped = results .filter((result) => result.score.verdict === "SKIP") .map(rowForResult); addRowsSheet(workbook, "Ranked Leads", ranked); addRowsSheet(workbook, "Skipped", skipped); addSummarySheet(workbook, summary); await workbook.xlsx.writeFile(outputFile); }