feat: Implement supplier export functionality with workbook generation

- Add `writeSupplierWorkbook` function to create Excel workbooks for supplier analysis results.
- Introduce `SupplierExportSummary` type for summarizing export data.
- Create tests for `writeSupplierWorkbook` to ensure correct sheet creation and data population.
- Implement supplier scoring logic in `supplier-scoring.ts` to evaluate product profitability and demand.
- Add tests for supplier scoring to validate scoring logic and verdict determination.
- Enhance UPC file analysis to integrate supplier scoring and export results to Excel.
- Update database writing logic to accommodate new supplier analysis results.
- Refactor types to include supplier-specific data structures and scoring metrics.
- Ensure proper cleanup of temporary files after tests.
This commit is contained in:
Victor Noguera
2026-05-19 01:19:48 -04:00
parent 41ef57a7bc
commit f3e4d3ac52
25 changed files with 1320 additions and 155 deletions

View File

@@ -1,5 +1,5 @@
import { getDb } from "./database.ts";
import type { AnalysisResult } from "./types.ts";
import type { AnalysisResult, SupplierAnalysisResult } from "./types.ts";
export type RunCounts = {
totalProducts: number;
@@ -222,6 +222,83 @@ export function appendResultsToRun(
})();
}
export function appendSupplierResultsToRun(
dbPath: string,
runId: number,
results: SupplierAnalysisResult[],
): 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,
avg_price_90d, sales_rank, rank_avg_90d, sellers,
amazon_is_seller, amazon_buybox_share_pct_90d, monthly_sold,
rank_drops_30d, rank_drops_90d, upc, fba_fee, fbm_fee,
referral_percent, supplier_score, supplier_profit, supplier_margin,
supplier_roi, supplier_reason, upc_lookup_status, upc_lookup_reason,
candidate_asins, can_sell, sellability_status, sellability_reason,
verdict, confidence, reasoning, fetched_at
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)`,
);
database.transaction(() => {
for (const result of results) {
const keepa = result.keepa;
const spApi = result.spApi;
const asin = result.lookup.asin ?? result.record.asin ?? result.upc;
const category =
result.record.category ?? keepa?.categoryTree?.join(" > ") ?? null;
const canSell =
spApi?.canSell == null ? null : spApi.canSell ? "yes" : "no";
insertResult.run(
runId,
asin,
result.record.name,
result.record.brand ?? null,
category,
result.record.unitCost || null,
result.score.salePrice,
keepa?.avgPrice90 ?? null,
keepa?.salesRank ?? null,
keepa?.salesRankAvg90 ?? null,
keepa?.sellerCount ?? null,
keepa?.amazonIsSeller == null ? null : keepa.amazonIsSeller ? 1 : 0,
keepa?.amazonBuyboxSharePct90d ?? null,
keepa?.monthlySold ?? null,
keepa?.salesRankDrops30 ?? null,
keepa?.salesRankDrops90 ?? null,
result.upc,
result.score.fbaFee,
spApi?.fbmFee ?? null,
spApi?.referralFeePercent ?? null,
result.score.score,
result.score.profit,
result.score.margin,
result.score.roi,
result.score.reason,
result.lookup.status,
result.lookup.reason ?? null,
result.lookup.candidateAsins.join(","),
canSell,
spApi?.sellabilityStatus ?? null,
spApi?.sellabilityReason ?? null,
result.score.verdict,
Math.round(result.score.score),
result.score.reason,
result.fetchedAt,
);
}
})();
}
export function refreshRunCountsInDb(dbPath: string, runId: number): RunCounts {
const database = getDb(dbPath);
const stats = database