feat: enhance product analysis results with additional fields and update handling logic

This commit is contained in:
Victor Noguera
2026-04-13 03:32:46 -04:00
parent 299ad7a1a6
commit 811fe9b10a
4 changed files with 83 additions and 7 deletions

View File

@@ -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[]) => { db.transaction((resultsBatch: AnalysisResult[]) => {

View File

@@ -49,7 +49,7 @@ function createProductAnalysisResultsTable(database: Database): void {
confidence REAL NOT NULL, confidence REAL NOT NULL,
reasoning TEXT, reasoning TEXT,
fetched_at TEXT NOT NULL, fetched_at TEXT NOT NULL,
UNIQUE(run_id, asin), UNIQUE(asin),
FOREIGN KEY (run_id) REFERENCES category_analysis_runs(id) 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, (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( database.run(
"ALTER TABLE product_analysis_results RENAME TO product_analysis_results_legacy", "ALTER TABLE product_analysis_results RENAME TO product_analysis_results_legacy",
); );
createProductAnalysisResultsTable(database); createProductAnalysisResultsTable(database);
database.run(` 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 ( INSERT INTO product_analysis_results (
asin, run_id, name, brand, category, unit_cost, asin, run_id, name, brand, category, unit_cost,
current_price, avg_price_90d, avg_price_90d_sheet, 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, fba_fee, fbm_fee, referral_percent, can_sell,
sellability_status, sellability_reason, sellability_status, sellability_reason,
verdict, confidence, reasoning, fetched_at verdict, confidence, reasoning, fetched_at
FROM product_analysis_results_legacy FROM ranked
WHERE row_num = 1
`); `);
database.run("DROP TABLE product_analysis_results_legacy"); database.run("DROP TABLE product_analysis_results_legacy");
} }

View File

@@ -31,6 +31,8 @@ type ProductListRecord = {
seller_count: number | null; seller_count: number | null;
sales_rank: number | null; sales_rank: number | null;
current_price: number | null; current_price: number | null;
avg_price_90d: number | null;
reasoning: string | null;
fetched_at: string; fetched_at: string;
}; };
@@ -286,6 +288,11 @@ function getProductList(filters: URLSearchParams) {
"sales_rank", "sales_rank",
"current_price", "current_price",
"product_name", "product_name",
"brand",
"category",
"avg_price_90d",
"confidence",
"reasoning",
"fetched_at", "fetched_at",
]); ]);
const orderBy = parseResultSort( const orderBy = parseResultSort(
@@ -309,6 +316,8 @@ function getProductList(filters: URLSearchParams) {
sellers AS seller_count, sellers AS seller_count,
sales_rank, sales_rank,
current_price, current_price,
avg_price_90d,
reasoning,
fetched_at fetched_at
FROM results FROM results
UNION ALL UNION ALL
@@ -326,6 +335,8 @@ function getProductList(filters: URLSearchParams) {
seller_count, seller_count,
sales_rank, sales_rank,
current_price, current_price,
avg_price_90d,
reasoning,
fetched_at fetched_at
FROM product_analysis_results FROM product_analysis_results
`; `;

View File

@@ -91,6 +91,8 @@ type ProductListItem = {
seller_count: number | null; seller_count: number | null;
sales_rank: number | null; sales_rank: number | null;
current_price: number | null; current_price: number | null;
avg_price_90d: number | null;
reasoning: string | null;
fetched_at: string; fetched_at: string;
}; };
@@ -680,11 +682,16 @@ function ProductList({ verdict, onBack }: { verdict: VerdictFilter; onBack: () =
<th><button onClick={() => setSort(nextSort(sort, "sales_rank"))}>Sales Rank</button></th> <th><button onClick={() => setSort(nextSort(sort, "sales_rank"))}>Sales Rank</button></th>
<th><button onClick={() => setSort(nextSort(sort, "current_price"))}>Current Price</button></th> <th><button onClick={() => setSort(nextSort(sort, "current_price"))}>Current Price</button></th>
<th className="product-col"><button onClick={() => setSort(nextSort(sort, "product_name"))}>Product</button></th> <th className="product-col"><button onClick={() => setSort(nextSort(sort, "product_name"))}>Product</button></th>
<th><button onClick={() => setSort(nextSort(sort, "brand"))}>Brand</button></th>
<th><button onClick={() => setSort(nextSort(sort, "category"))}>Category</button></th>
<th><button onClick={() => setSort(nextSort(sort, "avg_price_90d"))}>Avg 90d</button></th>
<th><button onClick={() => setSort(nextSort(sort, "confidence"))}>Confidence</button></th>
<th className="reason-col"><button onClick={() => setSort(nextSort(sort, "reasoning"))}>Reasoning</button></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{loading ? ( {loading ? (
<tr><td colSpan={7}>Loading...</td></tr> <tr><td colSpan={12}>Loading...</td></tr>
) : items?.items.length ? ( ) : items?.items.length ? (
items.items.map((item) => ( items.items.map((item) => (
<tr key={`${item.processType}-${item.runId}-${item.asin}-${item.fetched_at}`}> <tr key={`${item.processType}-${item.runId}-${item.asin}-${item.fetched_at}`}>
@@ -694,11 +701,16 @@ function ProductList({ verdict, onBack }: { verdict: VerdictFilter; onBack: () =
<td>{formatNumber(item.seller_count)}</td> <td>{formatNumber(item.seller_count)}</td>
<td>{formatNumber(item.sales_rank)}</td> <td>{formatNumber(item.sales_rank)}</td>
<td>{formatCurrency(item.current_price)}</td> <td>{formatCurrency(item.current_price)}</td>
<td className="product-col" title={item.product_name || undefined}>{item.product_name || "-"}</td> <td className="product-col" title={item.reasoning || undefined}>{item.product_name || "-"}</td>
<td>{item.brand || "-"}</td>
<td>{item.category || "-"}</td>
<td>{formatCurrency(item.avg_price_90d)}</td>
<td>{formatNumber(item.confidence)}</td>
<td className="reason-col" title={item.reasoning || undefined}>{item.reasoning || "-"}</td>
</tr> </tr>
)) ))
) : ( ) : (
<tr><td colSpan={7}>No products found</td></tr> <tr><td colSpan={12}>No products found</td></tr>
)} )}
</tbody> </tbody>
</table> </table>