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[]) => {

View File

@@ -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");
}

View File

@@ -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
`;

View File

@@ -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: () =
<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 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>
</thead>
<tbody>
{loading ? (
<tr><td colSpan={7}>Loading...</td></tr>
<tr><td colSpan={12}>Loading...</td></tr>
) : items?.items.length ? (
items.items.map((item) => (
<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.sales_rank)}</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><td colSpan={7}>No products found</td></tr>
<tr><td colSpan={12}>No products found</td></tr>
)}
</tbody>
</table>