fix: Correct Analytics V2 SQL queries for schema alignment

- Fix JOIN path: store_products -> dispensaries -> states (was incorrectly joining sp.state_id which doesn't exist)
- Fix column names to use *_raw suffixes (category_raw, brand_name_raw, name_raw)
- Fix row mappings to read correct column names from query results
- Add ::timestamp casts for interval arithmetic in StoreAnalyticsService

All Analytics V2 endpoints now work correctly:
- /state/legal-breakdown
- /state/recreational
- /category/all
- /category/rec-vs-med
- /state/:code/summary
- /store/:id/summary

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Kelly
2025-12-10 18:52:57 -07:00
parent 1fa9ea496c
commit 9aefb554bc
4 changed files with 142 additions and 127 deletions

View File

@@ -108,14 +108,14 @@ export class StateAnalyticsService {
SELECT
COUNT(DISTINCT d.id) AS dispensary_count,
COUNT(DISTINCT sp.id) AS product_count,
COUNT(DISTINCT sp.brand_name) FILTER (WHERE sp.brand_name IS NOT NULL) AS brand_count,
COUNT(DISTINCT sp.category) FILTER (WHERE sp.category IS NOT NULL) AS category_count,
COUNT(DISTINCT sp.brand_name_raw) FILTER (WHERE sp.brand_name_raw IS NOT NULL) AS brand_count,
COUNT(DISTINCT sp.category_raw) FILTER (WHERE sp.category_raw IS NOT NULL) AS category_count,
COUNT(sps.id) AS snapshot_count,
MAX(sps.captured_at) AS last_crawl_at
FROM states s
LEFT JOIN dispensaries d ON d.state_id = s.id
LEFT JOIN store_products sp ON sp.state_id = s.id AND sp.is_in_stock = TRUE
LEFT JOIN store_product_snapshots sps ON sps.state_id = s.id
LEFT JOIN store_products sp ON sp.dispensary_id = d.id AND sp.is_in_stock = TRUE
LEFT JOIN store_product_snapshots sps ON sps.dispensary_id = d.id
WHERE s.code = $1
`, [stateCode]);
@@ -129,7 +129,8 @@ export class StateAnalyticsService {
MIN(price_rec) AS min_price,
MAX(price_rec) AS max_price
FROM store_products sp
JOIN states s ON s.id = sp.state_id
JOIN dispensaries d ON d.id = sp.dispensary_id
JOIN states s ON s.id = d.state_id
WHERE s.code = $1
AND sp.price_rec IS NOT NULL
AND sp.is_in_stock = TRUE
@@ -140,14 +141,15 @@ export class StateAnalyticsService {
// Get top categories
const topCategoriesResult = await this.pool.query(`
SELECT
sp.category,
sp.category_raw,
COUNT(*) AS count
FROM store_products sp
JOIN states s ON s.id = sp.state_id
JOIN dispensaries d ON d.id = sp.dispensary_id
JOIN states s ON s.id = d.state_id
WHERE s.code = $1
AND sp.category IS NOT NULL
AND sp.category_raw IS NOT NULL
AND sp.is_in_stock = TRUE
GROUP BY sp.category
GROUP BY sp.category_raw
ORDER BY count DESC
LIMIT 10
`, [stateCode]);
@@ -155,14 +157,15 @@ export class StateAnalyticsService {
// Get top brands
const topBrandsResult = await this.pool.query(`
SELECT
sp.brand_name AS brand,
sp.brand_name_raw AS brand,
COUNT(*) AS count
FROM store_products sp
JOIN states s ON s.id = sp.state_id
JOIN dispensaries d ON d.id = sp.dispensary_id
JOIN states s ON s.id = d.state_id
WHERE s.code = $1
AND sp.brand_name IS NOT NULL
AND sp.brand_name_raw IS NOT NULL
AND sp.is_in_stock = TRUE
GROUP BY sp.brand_name
GROUP BY sp.brand_name_raw
ORDER BY count DESC
LIMIT 10
`, [stateCode]);
@@ -191,7 +194,7 @@ export class StateAnalyticsService {
max_price: pricing.max_price ? parseFloat(pricing.max_price) : null,
},
top_categories: topCategoriesResult.rows.map((row: any) => ({
category: row.category,
category: row.category_raw,
count: parseInt(row.count),
})),
top_brands: topBrandsResult.rows.map((row: any) => ({
@@ -215,8 +218,8 @@ export class StateAnalyticsService {
COUNT(sps.id) AS snapshot_count
FROM states s
LEFT JOIN dispensaries d ON d.state_id = s.id
LEFT JOIN store_products sp ON sp.state_id = s.id AND sp.is_in_stock = TRUE
LEFT JOIN store_product_snapshots sps ON sps.state_id = s.id
LEFT JOIN store_products sp ON sp.dispensary_id = d.id AND sp.is_in_stock = TRUE
LEFT JOIN store_product_snapshots sps ON sps.dispensary_id = d.id
WHERE s.recreational_legal = TRUE
GROUP BY s.code, s.name
ORDER BY dispensary_count DESC
@@ -232,8 +235,8 @@ export class StateAnalyticsService {
COUNT(sps.id) AS snapshot_count
FROM states s
LEFT JOIN dispensaries d ON d.state_id = s.id
LEFT JOIN store_products sp ON sp.state_id = s.id AND sp.is_in_stock = TRUE
LEFT JOIN store_product_snapshots sps ON sps.state_id = s.id
LEFT JOIN store_products sp ON sp.dispensary_id = d.id AND sp.is_in_stock = TRUE
LEFT JOIN store_product_snapshots sps ON sps.dispensary_id = d.id
WHERE s.medical_legal = TRUE
AND (s.recreational_legal = FALSE OR s.recreational_legal IS NULL)
GROUP BY s.code, s.name
@@ -295,46 +298,48 @@ export class StateAnalyticsService {
let groupBy = 'NULL';
if (category) {
categoryFilter = 'AND sp.category = $1';
categoryFilter = 'AND sp.category_raw = $1';
params.push(category);
groupBy = 'sp.category';
groupBy = 'sp.category_raw';
} else {
groupBy = 'sp.category';
groupBy = 'sp.category_raw';
}
const result = await this.pool.query(`
WITH rec_prices AS (
SELECT
${category ? 'sp.category' : 'sp.category'},
${category ? 'sp.category_raw' : 'sp.category_raw'},
COUNT(DISTINCT s.code) AS state_count,
COUNT(*) AS product_count,
AVG(sp.price_rec) AS avg_price,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY sp.price_rec) AS median_price
FROM store_products sp
JOIN states s ON s.id = sp.state_id
JOIN dispensaries d ON d.id = sp.dispensary_id
JOIN states s ON s.id = d.state_id
WHERE s.recreational_legal = TRUE
AND sp.price_rec IS NOT NULL
AND sp.is_in_stock = TRUE
AND sp.category IS NOT NULL
AND sp.category_raw IS NOT NULL
${categoryFilter}
GROUP BY sp.category
GROUP BY sp.category_raw
),
med_prices AS (
SELECT
${category ? 'sp.category' : 'sp.category'},
${category ? 'sp.category_raw' : 'sp.category_raw'},
COUNT(DISTINCT s.code) AS state_count,
COUNT(*) AS product_count,
AVG(sp.price_rec) AS avg_price,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY sp.price_rec) AS median_price
FROM store_products sp
JOIN states s ON s.id = sp.state_id
JOIN dispensaries d ON d.id = sp.dispensary_id
JOIN states s ON s.id = d.state_id
WHERE s.medical_legal = TRUE
AND (s.recreational_legal = FALSE OR s.recreational_legal IS NULL)
AND sp.price_rec IS NOT NULL
AND sp.is_in_stock = TRUE
AND sp.category IS NOT NULL
AND sp.category_raw IS NOT NULL
${categoryFilter}
GROUP BY sp.category
GROUP BY sp.category_raw
)
SELECT
COALESCE(r.category, m.category) AS category,
@@ -357,7 +362,7 @@ export class StateAnalyticsService {
`, params);
return result.rows.map((row: any) => ({
category: row.category,
category: row.category_raw,
recreational: {
state_count: parseInt(row.rec_state_count) || 0,
product_count: parseInt(row.rec_product_count) || 0,
@@ -395,12 +400,12 @@ export class StateAnalyticsService {
COALESCE(s.medical_legal, FALSE) AS medical_legal,
COUNT(DISTINCT d.id) AS dispensary_count,
COUNT(DISTINCT sp.id) AS product_count,
COUNT(DISTINCT sp.brand_name) FILTER (WHERE sp.brand_name IS NOT NULL) AS brand_count,
COUNT(DISTINCT sp.brand_name_raw) FILTER (WHERE sp.brand_name_raw IS NOT NULL) AS brand_count,
MAX(sps.captured_at) AS last_crawl_at
FROM states s
LEFT JOIN dispensaries d ON d.state_id = s.id
LEFT JOIN store_products sp ON sp.state_id = s.id AND sp.is_in_stock = TRUE
LEFT JOIN store_product_snapshots sps ON sps.state_id = s.id
LEFT JOIN store_products sp ON sp.dispensary_id = d.id AND sp.is_in_stock = TRUE
LEFT JOIN store_product_snapshots sps ON sps.dispensary_id = d.id
GROUP BY s.code, s.name, s.recreational_legal, s.medical_legal
ORDER BY dispensary_count DESC, s.name
`);
@@ -451,8 +456,8 @@ export class StateAnalyticsService {
END AS gap_reason
FROM states s
LEFT JOIN dispensaries d ON d.state_id = s.id
LEFT JOIN store_products sp ON sp.state_id = s.id AND sp.is_in_stock = TRUE
LEFT JOIN store_product_snapshots sps ON sps.state_id = s.id
LEFT JOIN store_products sp ON sp.dispensary_id = d.id AND sp.is_in_stock = TRUE
LEFT JOIN store_product_snapshots sps ON sps.dispensary_id = d.id
WHERE s.recreational_legal = TRUE OR s.medical_legal = TRUE
GROUP BY s.code, s.name, s.recreational_legal, s.medical_legal
HAVING COUNT(DISTINCT d.id) = 0
@@ -499,7 +504,8 @@ export class StateAnalyticsService {
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY sp.price_rec) AS median_price,
COUNT(*) AS product_count
FROM states s
JOIN store_products sp ON sp.state_id = s.id
JOIN dispensaries d ON d.state_id = s.id
JOIN store_products sp ON sp.dispensary_id = d.id
WHERE sp.price_rec IS NOT NULL
AND sp.is_in_stock = TRUE
AND (s.recreational_legal = TRUE OR s.medical_legal = TRUE)