From 937fe5da4001e135d76f5e8131365be38e5b56f4 Mon Sep 17 00:00:00 2001 From: Victor Noguera Date: Mon, 13 Apr 2026 02:53:49 -0400 Subject: [PATCH] feat: expand results schema and refine LLM analysis policy - Adds new columns to the `results` table for tracking spreadsheet-sourced financial data, supplier details, and lead metadata. - Implements `ensureResultsTableColumns` to automatically migrate existing databases with the new schema. - Simplifies LLM evaluation logic by removing mandatory skip triggers for Amazon-exclusive and single-seller products. - Standardizes formatting for database index and table creation statements. --- src/database.ts | 99 +++++++++++++++++++++++++++++++++++++++++++------ src/llm.ts | 8 +--- 2 files changed, 88 insertions(+), 19 deletions(-) diff --git a/src/database.ts b/src/database.ts index 32fa026..594a182 100644 --- a/src/database.ts +++ b/src/database.ts @@ -71,7 +71,9 @@ function ensureProductAnalysisResultsTable(database: Database): void { ); if (!hasIdColumn || hasAsinPrimaryKey) { - database.run("ALTER TABLE product_analysis_results RENAME TO product_analysis_results_legacy"); + database.run( + "ALTER TABLE product_analysis_results RENAME TO product_analysis_results_legacy", + ); createProductAnalysisResultsTable(database); database.run(` INSERT INTO product_analysis_results ( @@ -97,6 +99,42 @@ function ensureProductAnalysisResultsTable(database: Database): void { } } +function ensureResultsTableColumns(database: Database): void { + const tableInfo = database + .query("PRAGMA table_info(results)") + .all() as Array<{ name: string }>; + + if (tableInfo.length === 0) { + return; + } + + const existingColumns = new Set(tableInfo.map((col) => col.name)); + const requiredColumns: Array<{ name: string; type: string }> = [ + { name: "fba_net_sheet", type: "REAL" }, + { name: "gross_profit_dollar", type: "REAL" }, + { name: "gross_profit_pct", type: "REAL" }, + { name: "net_profit_sheet", type: "REAL" }, + { name: "roi_sheet", type: "REAL" }, + { name: "moq", type: "INTEGER" }, + { name: "moq_cost", type: "REAL" }, + { name: "qty_available", type: "INTEGER" }, + { name: "supplier", type: "TEXT" }, + { name: "source_url", type: "TEXT" }, + { name: "asin_link", type: "TEXT" }, + { name: "promo_coupon_code", type: "TEXT" }, + { name: "notes", type: "TEXT" }, + { name: "lead_date", type: "TEXT" }, + ]; + + for (const column of requiredColumns) { + if (!existingColumns.has(column.name)) { + database.run( + `ALTER TABLE results ADD COLUMN ${column.name} ${column.type}`, + ); + } + } +} + export function initDb(dbPath: string): void { const database = getDb(dbPath); database.run(` @@ -130,6 +168,20 @@ export function initDb(dbPath: string): void { monthly_sold INTEGER, rank_drops_30d INTEGER, rank_drops_90d INTEGER, + fba_net_sheet REAL, + gross_profit_dollar REAL, + gross_profit_pct REAL, + net_profit_sheet REAL, + roi_sheet REAL, + moq INTEGER, + moq_cost REAL, + qty_available INTEGER, + supplier TEXT, + source_url TEXT, + asin_link TEXT, + promo_coupon_code TEXT, + notes TEXT, + lead_date TEXT, fba_fee REAL, fbm_fee REAL, referral_percent REAL, @@ -143,6 +195,7 @@ export function initDb(dbPath: string): void { FOREIGN KEY (run_id) REFERENCES runs(id) ); `); + ensureResultsTableColumns(database); database.run(` CREATE TABLE IF NOT EXISTS category_analysis_runs ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -160,16 +213,38 @@ export function initDb(dbPath: string): void { `); ensureProductAnalysisResultsTable(database); - database.run(`CREATE INDEX IF NOT EXISTS idx_runs_timestamp ON runs(timestamp DESC);`); - database.run(`CREATE INDEX IF NOT EXISTS idx_results_run_id ON results(run_id);`); - database.run(`CREATE INDEX IF NOT EXISTS idx_results_verdict ON results(verdict);`); - database.run(`CREATE INDEX IF NOT EXISTS idx_results_sellability_status ON results(sellability_status);`); - database.run(`CREATE INDEX IF NOT EXISTS idx_results_fetched_at ON results(fetched_at DESC);`); + database.run( + `CREATE INDEX IF NOT EXISTS idx_runs_timestamp ON runs(timestamp DESC);`, + ); + database.run( + `CREATE INDEX IF NOT EXISTS idx_results_run_id ON results(run_id);`, + ); + database.run( + `CREATE INDEX IF NOT EXISTS idx_results_verdict ON results(verdict);`, + ); + database.run( + `CREATE INDEX IF NOT EXISTS idx_results_sellability_status ON results(sellability_status);`, + ); + database.run( + `CREATE INDEX IF NOT EXISTS idx_results_fetched_at ON results(fetched_at DESC);`, + ); database.run(`CREATE INDEX IF NOT EXISTS idx_results_asin ON results(asin);`); - database.run(`CREATE INDEX IF NOT EXISTS idx_category_runs_timestamp ON category_analysis_runs(run_timestamp DESC);`); - database.run(`CREATE INDEX IF NOT EXISTS idx_category_runs_status ON category_analysis_runs(status);`); - database.run(`CREATE INDEX IF NOT EXISTS idx_product_results_run_id ON product_analysis_results(run_id);`); - database.run(`CREATE INDEX IF NOT EXISTS idx_product_results_verdict ON product_analysis_results(verdict);`); - database.run(`CREATE INDEX IF NOT EXISTS idx_product_results_sellability_status ON product_analysis_results(sellability_status);`); - database.run(`CREATE INDEX IF NOT EXISTS idx_product_results_fetched_at ON product_analysis_results(fetched_at DESC);`); + database.run( + `CREATE INDEX IF NOT EXISTS idx_category_runs_timestamp ON category_analysis_runs(run_timestamp DESC);`, + ); + database.run( + `CREATE INDEX IF NOT EXISTS idx_category_runs_status ON category_analysis_runs(status);`, + ); + database.run( + `CREATE INDEX IF NOT EXISTS idx_product_results_run_id ON product_analysis_results(run_id);`, + ); + database.run( + `CREATE INDEX IF NOT EXISTS idx_product_results_verdict ON product_analysis_results(verdict);`, + ); + database.run( + `CREATE INDEX IF NOT EXISTS idx_product_results_sellability_status ON product_analysis_results(sellability_status);`, + ); + database.run( + `CREATE INDEX IF NOT EXISTS idx_product_results_fetched_at ON product_analysis_results(fetched_at DESC);`, + ); } diff --git a/src/llm.ts b/src/llm.ts index 0b0eaf3..44f168b 100644 --- a/src/llm.ts +++ b/src/llm.ts @@ -7,14 +7,9 @@ Given product data, evaluate each product's viability for selling on Amazon. Con 1. **Sales Velocity**: monthlySold and salesRankDrops30 are the most important signals. A product that doesn't sell is worthless regardless of margin. salesRankDrops30 = approximate units sold in 30 days. monthlySold is Keepa's estimate. 2. **Margin Analysis**: Sale price minus unit cost minus fees (FBA or FBM). Aim for >30% ROI minimum. The spreadsheet may include FBA NET and gross profit estimates — cross-check against Keepa pricing data. - - If unitCost is 0, it means NO COST DATA is available — this is NOT a free product. Do not assume infinite or zero-cost margins. When estimatedROI is null and unitCost is 0, evaluate purely on demand and velocity signals; never extrapolate profitability. - - If estimatedProfit is null, price data was unavailable at analysis time — treat as elevated uncertainty and be conservative. 3. **Sales Rank (BSR)**: Lower rank = higher demand. Rank <50,000 is good, <1,000 is excellent. 4. **Sales Rank Trend**: Compare current rank vs 90d average. Lower current = improving demand. -5. **Competition & Buy Box**: - - Fewer sellers = easier entry, but verify who holds the Buy Box. - - If buyBoxSeller is "ATVPDKIKX0DER" (Amazon retail), the product is Amazon-exclusive — return "SKIP". Amazon-sold items cannot be competed with by third-party sellers. - - If sellerCount is 1 and buyBoxSeller is not null, assume the market is closed — return "SKIP". +5. **Competition**: Number of sellers and Buy Box dynamics. Fewer sellers = easier entry. 6. **Price Stability**: Large price swings (high max vs low min over 90d) = volatile/risky. 7. **FBA vs FBM**: FBA preferred for fast-selling, small/light items. FBM for oversized, slow-moving, or high-margin items where fee savings matter. 8. **MOQ & Capital**: High MOQ with thin margins is risky. @@ -26,7 +21,6 @@ Given product data, evaluate each product's viability for selling on Amazon. Con Decision policy: - Do not recommend products that cannot be listed by this seller account. -- Do not recommend Amazon-exclusive products (buyBoxSeller = "ATVPDKIKX0DER"). - Prioritize profitable + high-velocity + listable products. - Use "SKIP" when data quality is poor or risk is high.