diff --git a/src/bestsellers-by-category.ts b/src/bestsellers-by-category.ts index 13231e9..fc406d9 100644 --- a/src/bestsellers-by-category.ts +++ b/src/bestsellers-by-category.ts @@ -212,7 +212,33 @@ export async function insertProductAnalysisResults( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? - ); + ) + ON CONFLICT(asin) DO UPDATE SET + run_id = excluded.run_id, + name = excluded.name, + brand = excluded.brand, + category = excluded.category, + unit_cost = excluded.unit_cost, + current_price = excluded.current_price, + avg_price_90d = excluded.avg_price_90d, + avg_price_90d_sheet = excluded.avg_price_90d_sheet, + selling_price_sheet = excluded.selling_price_sheet, + sales_rank = excluded.sales_rank, + sales_rank_avg_90d = excluded.sales_rank_avg_90d, + seller_count = excluded.seller_count, + monthly_sold = excluded.monthly_sold, + rank_drops_30d = excluded.rank_drops_30d, + rank_drops_90d = excluded.rank_drops_90d, + fba_fee = excluded.fba_fee, + fbm_fee = excluded.fbm_fee, + referral_percent = excluded.referral_percent, + can_sell = excluded.can_sell, + sellability_status = excluded.sellability_status, + sellability_reason = excluded.sellability_reason, + verdict = excluded.verdict, + confidence = excluded.confidence, + reasoning = excluded.reasoning, + fetched_at = excluded.fetched_at; `); db.transaction((resultsBatch: AnalysisResult[]) => { diff --git a/src/database.ts b/src/database.ts index 594a182..7e499a7 100644 --- a/src/database.ts +++ b/src/database.ts @@ -49,7 +49,7 @@ function createProductAnalysisResultsTable(database: Database): void { confidence REAL NOT NULL, reasoning TEXT, fetched_at TEXT NOT NULL, - UNIQUE(run_id, asin), + UNIQUE(asin), FOREIGN KEY (run_id) REFERENCES category_analysis_runs(id) ); `); @@ -70,12 +70,38 @@ function ensureProductAnalysisResultsTable(database: Database): void { (col) => col.name === "asin" && col.pk === 1, ); - if (!hasIdColumn || hasAsinPrimaryKey) { + const indexList = database + .query("PRAGMA index_list(product_analysis_results)") + .all() as Array<{ name: string; unique: number }>; + const hasUniqueAsinConstraint = indexList.some((idx) => { + if (idx.unique !== 1) return false; + const columns = database + .query(`PRAGMA index_info(${JSON.stringify(idx.name)})`) + .all() as Array<{ name: string }>; + return columns.length === 1 && columns[0]?.name === "asin"; + }); + + if (!hasIdColumn || hasAsinPrimaryKey || !hasUniqueAsinConstraint) { database.run( "ALTER TABLE product_analysis_results RENAME TO product_analysis_results_legacy", ); createProductAnalysisResultsTable(database); database.run(` + WITH ranked AS ( + SELECT + asin, run_id, name, brand, category, unit_cost, + current_price, avg_price_90d, avg_price_90d_sheet, + selling_price_sheet, sales_rank, sales_rank_avg_90d, + seller_count, monthly_sold, rank_drops_30d, rank_drops_90d, + fba_fee, fbm_fee, referral_percent, can_sell, + sellability_status, sellability_reason, + verdict, confidence, reasoning, fetched_at, + ROW_NUMBER() OVER ( + PARTITION BY asin + ORDER BY datetime(fetched_at) DESC, run_id DESC, id DESC + ) AS row_num + FROM product_analysis_results_legacy + ) INSERT INTO product_analysis_results ( asin, run_id, name, brand, category, unit_cost, current_price, avg_price_90d, avg_price_90d_sheet, @@ -93,7 +119,8 @@ function ensureProductAnalysisResultsTable(database: Database): void { fba_fee, fbm_fee, referral_percent, can_sell, sellability_status, sellability_reason, verdict, confidence, reasoning, fetched_at - FROM product_analysis_results_legacy + FROM ranked + WHERE row_num = 1 `); database.run("DROP TABLE product_analysis_results_legacy"); } diff --git a/src/server.ts b/src/server.ts index 314676a..d2418e9 100644 --- a/src/server.ts +++ b/src/server.ts @@ -31,6 +31,8 @@ type ProductListRecord = { seller_count: number | null; sales_rank: number | null; current_price: number | null; + avg_price_90d: number | null; + reasoning: string | null; fetched_at: string; }; @@ -286,6 +288,11 @@ function getProductList(filters: URLSearchParams) { "sales_rank", "current_price", "product_name", + "brand", + "category", + "avg_price_90d", + "confidence", + "reasoning", "fetched_at", ]); const orderBy = parseResultSort( @@ -309,6 +316,8 @@ function getProductList(filters: URLSearchParams) { sellers AS seller_count, sales_rank, current_price, + avg_price_90d, + reasoning, fetched_at FROM results UNION ALL @@ -326,6 +335,8 @@ function getProductList(filters: URLSearchParams) { seller_count, sales_rank, current_price, + avg_price_90d, + reasoning, fetched_at FROM product_analysis_results `; diff --git a/src/web/frontend.tsx b/src/web/frontend.tsx index 454cba5..f2e22ec 100644 --- a/src/web/frontend.tsx +++ b/src/web/frontend.tsx @@ -91,6 +91,8 @@ type ProductListItem = { seller_count: number | null; sales_rank: number | null; current_price: number | null; + avg_price_90d: number | null; + reasoning: string | null; fetched_at: string; }; @@ -680,11 +682,16 @@ function ProductList({ verdict, onBack }: { verdict: VerdictFilter; onBack: () =