Files
cannaiq/backend/dist/routes/dispensaries.js
Kelly 66e07b2009 fix(monitor): remove non-existent worker columns from job_run_logs query
The job_run_logs table tracks scheduled job orchestration, not individual
worker jobs. Worker info (worker_id, worker_hostname) belongs on
dispensary_crawl_jobs, not job_run_logs.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 18:45:05 -07:00

438 lines
14 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = require("express");
const middleware_1 = require("../auth/middleware");
const migrate_1 = require("../db/migrate");
const router = (0, express_1.Router)();
router.use(middleware_1.authMiddleware);
// Valid menu_type values
const VALID_MENU_TYPES = ['dutchie', 'treez', 'jane', 'weedmaps', 'leafly', 'meadow', 'blaze', 'flowhub', 'dispense', 'cova', 'other', 'unknown'];
// Get all dispensaries
router.get('/', async (req, res) => {
try {
const { menu_type } = req.query;
let query = `
SELECT
id,
azdhs_id,
name,
company_name,
slug,
address,
city,
state,
zip,
phone,
email,
website,
dba_name,
google_rating,
google_review_count,
status_line,
azdhs_url,
latitude,
longitude,
menu_url,
menu_type,
menu_provider,
menu_provider_confidence,
scraper_template,
last_menu_scrape,
menu_scrape_status,
platform_dispensary_id,
created_at,
updated_at
FROM dispensaries
`;
const params = [];
// Filter by menu_type if provided
if (menu_type) {
query += ` WHERE menu_type = $1`;
params.push(menu_type);
}
query += ` ORDER BY name`;
const result = await migrate_1.pool.query(query, params);
res.json({ dispensaries: result.rows });
}
catch (error) {
console.error('Error fetching dispensaries:', error);
res.status(500).json({ error: 'Failed to fetch dispensaries' });
}
});
// Get menu type stats
router.get('/stats/menu-types', async (req, res) => {
try {
const result = await migrate_1.pool.query(`
SELECT menu_type, COUNT(*) as count
FROM dispensaries
GROUP BY menu_type
ORDER BY count DESC
`);
res.json({ menu_types: result.rows, valid_types: VALID_MENU_TYPES });
}
catch (error) {
console.error('Error fetching menu type stats:', error);
res.status(500).json({ error: 'Failed to fetch menu type stats' });
}
});
// Get single dispensary by slug
router.get('/:slug', async (req, res) => {
try {
const { slug } = req.params;
const result = await migrate_1.pool.query(`
SELECT
id,
azdhs_id,
name,
company_name,
slug,
address,
city,
state,
zip,
phone,
email,
website,
dba_name,
google_rating,
google_review_count,
status_line,
azdhs_url,
latitude,
longitude,
menu_url,
menu_type,
menu_provider,
menu_provider_confidence,
scraper_template,
scraper_config,
last_menu_scrape,
menu_scrape_status,
platform_dispensary_id,
created_at,
updated_at
FROM dispensaries
WHERE slug = $1
`, [slug]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Dispensary not found' });
}
res.json(result.rows[0]);
}
catch (error) {
console.error('Error fetching dispensary:', error);
res.status(500).json({ error: 'Failed to fetch dispensary' });
}
});
// Update dispensary
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const { dba_name, website, phone, email, google_rating, google_review_count, menu_url, menu_type, scraper_template, scraper_config, menu_scrape_status } = req.body;
// Validate menu_type if provided
if (menu_type !== undefined && menu_type !== null && menu_type !== '' && !VALID_MENU_TYPES.includes(menu_type)) {
return res.status(400).json({
error: `Invalid menu_type. Must be one of: ${VALID_MENU_TYPES.join(', ')}`
});
}
const result = await migrate_1.pool.query(`
UPDATE dispensaries
SET
dba_name = COALESCE($1, dba_name),
website = COALESCE($2, website),
phone = COALESCE($3, phone),
email = COALESCE($4, email),
google_rating = COALESCE($5, google_rating),
google_review_count = COALESCE($6, google_review_count),
menu_url = COALESCE($7, menu_url),
menu_type = COALESCE($8, menu_type),
scraper_template = COALESCE($9, scraper_template),
scraper_config = COALESCE($10, scraper_config),
menu_scrape_status = COALESCE($11, menu_scrape_status),
updated_at = CURRENT_TIMESTAMP
WHERE id = $12
RETURNING *
`, [
dba_name,
website,
phone,
email,
google_rating,
google_review_count,
menu_url,
menu_type,
scraper_template,
scraper_config,
menu_scrape_status,
id
]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Dispensary not found' });
}
res.json(result.rows[0]);
}
catch (error) {
console.error('Error updating dispensary:', error);
res.status(500).json({ error: 'Failed to update dispensary' });
}
});
// Get products for a dispensary by slug
router.get('/:slug/products', async (req, res) => {
try {
const { slug } = req.params;
const { category } = req.query;
// First get the dispensary ID from slug
const dispensaryResult = await migrate_1.pool.query(`
SELECT id FROM dispensaries WHERE slug = $1
`, [slug]);
if (dispensaryResult.rows.length === 0) {
return res.status(404).json({ error: 'Dispensary not found' });
}
const dispensaryId = dispensaryResult.rows[0].id;
// Build query for products
let query = `
SELECT
p.id,
p.name,
p.brand,
p.variant,
p.slug,
p.description,
p.regular_price,
p.sale_price,
p.thc_percentage,
p.cbd_percentage,
p.strain_type,
p.terpenes,
p.effects,
p.flavors,
p.image_url,
p.dutchie_url,
p.in_stock,
p.created_at,
p.updated_at
FROM products p
WHERE p.dispensary_id = $1
`;
const params = [dispensaryId];
if (category) {
query += ` AND p.category = $2`;
params.push(category);
}
query += ` ORDER BY p.created_at DESC`;
const result = await migrate_1.pool.query(query, params);
res.json({ products: result.rows });
}
catch (error) {
console.error('Error fetching dispensary products:', error);
res.status(500).json({ error: 'Failed to fetch products' });
}
});
// Get unique brands for a dispensary by slug
router.get('/:slug/brands', async (req, res) => {
try {
const { slug } = req.params;
const { search } = req.query;
// First get the dispensary ID from slug
const dispensaryResult = await migrate_1.pool.query(`
SELECT id FROM dispensaries WHERE slug = $1
`, [slug]);
if (dispensaryResult.rows.length === 0) {
return res.status(404).json({ error: 'Dispensary not found' });
}
const dispensaryId = dispensaryResult.rows[0].id;
// Build query with optional search filter
let query = `
SELECT DISTINCT
brand,
COUNT(*) as product_count
FROM products
WHERE dispensary_id = $1 AND brand IS NOT NULL
`;
const params = [dispensaryId];
// Add search filter if provided
if (search) {
query += ` AND brand ILIKE $2`;
params.push(`%${search}%`);
}
query += ` GROUP BY brand ORDER BY product_count DESC, brand ASC`;
const result = await migrate_1.pool.query(query, params);
res.json({ brands: result.rows });
}
catch (error) {
console.error('Error fetching dispensary brands:', error);
res.status(500).json({ error: 'Failed to fetch brands' });
}
});
// Get products with discounts/specials for a dispensary by slug
router.get('/:slug/specials', async (req, res) => {
try {
const { slug } = req.params;
const { search } = req.query;
// First get the dispensary ID from slug
const dispensaryResult = await migrate_1.pool.query(`
SELECT id FROM dispensaries WHERE slug = $1
`, [slug]);
if (dispensaryResult.rows.length === 0) {
return res.status(404).json({ error: 'Dispensary not found' });
}
const dispensaryId = dispensaryResult.rows[0].id;
// Build query to get products with discounts
let query = `
SELECT
p.id,
p.name,
p.brand,
p.variant,
p.slug,
p.description,
p.regular_price,
p.sale_price,
p.discount_type,
p.discount_value,
p.thc_percentage,
p.cbd_percentage,
p.strain_type,
p.terpenes,
p.effects,
p.flavors,
p.image_url,
p.dutchie_url,
p.in_stock,
p.created_at,
p.updated_at
FROM products p
WHERE p.dispensary_id = $1
AND p.discount_type IS NOT NULL
AND p.discount_value IS NOT NULL
`;
const params = [dispensaryId];
// Add search filter if provided
if (search) {
query += ` AND (p.name ILIKE $2 OR p.brand ILIKE $2 OR p.description ILIKE $2)`;
params.push(`%${search}%`);
}
query += ` ORDER BY p.created_at DESC`;
const result = await migrate_1.pool.query(query, params);
res.json({ specials: result.rows });
}
catch (error) {
console.error('Error fetching dispensary specials:', error);
res.status(500).json({ error: 'Failed to fetch specials' });
}
});
// Trigger scraping for a dispensary
router.post('/:slug/scrape', async (req, res) => {
try {
const { slug } = req.params;
const { type } = req.body; // 'products' | 'brands' | 'specials' | 'all'
if (!['products', 'brands', 'specials', 'all'].includes(type)) {
return res.status(400).json({ error: 'Invalid type. Must be: products, brands, specials, or all' });
}
// Get the dispensary
const dispensaryResult = await migrate_1.pool.query(`
SELECT id, name, slug, website, menu_url, scraper_template, scraper_config
FROM dispensaries
WHERE slug = $1
`, [slug]);
if (dispensaryResult.rows.length === 0) {
return res.status(404).json({ error: 'Dispensary not found' });
}
const dispensary = dispensaryResult.rows[0];
if (!dispensary.menu_url && !dispensary.website) {
return res.status(400).json({ error: 'Dispensary has no menu URL or website configured' });
}
// Update last_menu_scrape time and status
await migrate_1.pool.query(`
UPDATE dispensaries
SET
last_menu_scrape = CURRENT_TIMESTAMP,
menu_scrape_status = 'pending',
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`, [dispensary.id]);
// Log the scrape request
console.log(`[SCRAPE REQUEST] Dispensary: ${dispensary.name} (${slug}), Type: ${type}`);
console.log(` Menu URL: ${dispensary.menu_url || dispensary.website}`);
console.log(` Template: ${dispensary.scraper_template || 'N/A'}`);
// TODO: Actually trigger the scraper here
// For now, this is a placeholder that updates the status
// You can integrate with your existing scraper infrastructure
res.json({
success: true,
message: `Scraping queued for ${dispensary.name}`,
type,
dispensary: {
id: dispensary.id,
name: dispensary.name,
slug: dispensary.slug
}
});
}
catch (error) {
console.error('Error triggering scrape:', error);
res.status(500).json({ error: 'Failed to trigger scraping' });
}
});
// Update menu_type for a dispensary (dedicated endpoint)
router.patch('/:id/menu-type', async (req, res) => {
try {
const { id } = req.params;
const { menu_type } = req.body;
// Validate menu_type
if (menu_type !== null && menu_type !== '' && !VALID_MENU_TYPES.includes(menu_type)) {
return res.status(400).json({
error: `Invalid menu_type. Must be one of: ${VALID_MENU_TYPES.join(', ')} (or null to clear)`
});
}
const result = await migrate_1.pool.query(`
UPDATE dispensaries
SET menu_type = $1, updated_at = CURRENT_TIMESTAMP
WHERE id = $2
RETURNING id, name, slug, menu_type, menu_provider, menu_url
`, [menu_type || null, id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Dispensary not found' });
}
res.json({
success: true,
dispensary: result.rows[0]
});
}
catch (error) {
console.error('Error updating menu_type:', error);
res.status(500).json({ error: 'Failed to update menu_type' });
}
});
// Bulk update menu_type for multiple dispensaries
router.post('/bulk/menu-type', async (req, res) => {
try {
const { dispensary_ids, menu_type } = req.body;
if (!Array.isArray(dispensary_ids) || dispensary_ids.length === 0) {
return res.status(400).json({ error: 'dispensary_ids must be a non-empty array' });
}
// Validate menu_type
if (menu_type !== null && menu_type !== '' && !VALID_MENU_TYPES.includes(menu_type)) {
return res.status(400).json({
error: `Invalid menu_type. Must be one of: ${VALID_MENU_TYPES.join(', ')} (or null to clear)`
});
}
const result = await migrate_1.pool.query(`
UPDATE dispensaries
SET menu_type = $1, updated_at = CURRENT_TIMESTAMP
WHERE id = ANY($2::int[])
RETURNING id, name, slug, menu_type
`, [menu_type || null, dispensary_ids]);
res.json({
success: true,
updated_count: result.rowCount,
dispensaries: result.rows
});
}
catch (error) {
console.error('Error bulk updating menu_type:', error);
res.status(500).json({ error: 'Failed to bulk update menu_type' });
}
});
exports.default = router;