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.
This commit is contained in:
@@ -71,7 +71,9 @@ function ensureProductAnalysisResultsTable(database: Database): void {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!hasIdColumn || hasAsinPrimaryKey) {
|
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);
|
createProductAnalysisResultsTable(database);
|
||||||
database.run(`
|
database.run(`
|
||||||
INSERT INTO product_analysis_results (
|
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 {
|
export function initDb(dbPath: string): void {
|
||||||
const database = getDb(dbPath);
|
const database = getDb(dbPath);
|
||||||
database.run(`
|
database.run(`
|
||||||
@@ -130,6 +168,20 @@ export function initDb(dbPath: string): void {
|
|||||||
monthly_sold INTEGER,
|
monthly_sold INTEGER,
|
||||||
rank_drops_30d INTEGER,
|
rank_drops_30d INTEGER,
|
||||||
rank_drops_90d 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,
|
fba_fee REAL,
|
||||||
fbm_fee REAL,
|
fbm_fee REAL,
|
||||||
referral_percent REAL,
|
referral_percent REAL,
|
||||||
@@ -143,6 +195,7 @@ export function initDb(dbPath: string): void {
|
|||||||
FOREIGN KEY (run_id) REFERENCES runs(id)
|
FOREIGN KEY (run_id) REFERENCES runs(id)
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
ensureResultsTableColumns(database);
|
||||||
database.run(`
|
database.run(`
|
||||||
CREATE TABLE IF NOT EXISTS category_analysis_runs (
|
CREATE TABLE IF NOT EXISTS category_analysis_runs (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -160,16 +213,38 @@ export function initDb(dbPath: string): void {
|
|||||||
`);
|
`);
|
||||||
ensureProductAnalysisResultsTable(database);
|
ensureProductAnalysisResultsTable(database);
|
||||||
|
|
||||||
database.run(`CREATE INDEX IF NOT EXISTS idx_runs_timestamp ON runs(timestamp DESC);`);
|
database.run(
|
||||||
database.run(`CREATE INDEX IF NOT EXISTS idx_results_run_id ON results(run_id);`);
|
`CREATE INDEX IF NOT EXISTS idx_runs_timestamp ON runs(timestamp DESC);`,
|
||||||
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(
|
||||||
database.run(`CREATE INDEX IF NOT EXISTS idx_results_fetched_at ON results(fetched_at DESC);`);
|
`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_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(
|
||||||
database.run(`CREATE INDEX IF NOT EXISTS idx_category_runs_status ON category_analysis_runs(status);`);
|
`CREATE INDEX IF NOT EXISTS idx_category_runs_timestamp ON category_analysis_runs(run_timestamp DESC);`,
|
||||||
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(
|
||||||
database.run(`CREATE INDEX IF NOT EXISTS idx_product_results_sellability_status ON product_analysis_results(sellability_status);`);
|
`CREATE INDEX IF NOT EXISTS idx_category_runs_status ON category_analysis_runs(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_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);`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
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.
|
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.
|
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.
|
4. **Sales Rank Trend**: Compare current rank vs 90d average. Lower current = improving demand.
|
||||||
5. **Competition & Buy Box**:
|
5. **Competition**: Number of sellers and Buy Box dynamics. Fewer sellers = easier entry.
|
||||||
- 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".
|
|
||||||
6. **Price Stability**: Large price swings (high max vs low min over 90d) = volatile/risky.
|
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.
|
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.
|
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:
|
Decision policy:
|
||||||
- Do not recommend products that cannot be listed by this seller account.
|
- 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.
|
- Prioritize profitable + high-velocity + listable products.
|
||||||
- Use "SKIP" when data quality is poor or risk is high.
|
- Use "SKIP" when data quality is poor or risk is high.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user