"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const express_1 = require("express"); const middleware_1 = require("../auth/middleware"); const crawl_scheduler_1 = require("../services/crawl-scheduler"); const store_crawl_orchestrator_1 = require("../services/store-crawl-orchestrator"); const dispensary_orchestrator_1 = require("../services/dispensary-orchestrator"); const migrate_1 = require("../db/migrate"); const graphql_client_1 = require("../dutchie-az/services/graphql-client"); const router = (0, express_1.Router)(); router.use(middleware_1.authMiddleware); // ============================================ // Global Schedule Endpoints // ============================================ /** * GET /api/schedule/global * Get global schedule settings */ router.get('/global', async (req, res) => { try { const schedules = await (0, crawl_scheduler_1.getGlobalSchedule)(); res.json({ schedules }); } catch (error) { console.error('Error fetching global schedule:', error); res.status(500).json({ error: 'Failed to fetch global schedule' }); } }); /** * PUT /api/schedule/global/:type * Update global schedule setting */ router.put('/global/:type', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { try { const { type } = req.params; const { enabled, interval_hours, run_time } = req.body; if (type !== 'global_interval' && type !== 'daily_special') { return res.status(400).json({ error: 'Invalid schedule type' }); } const schedule = await (0, crawl_scheduler_1.updateGlobalSchedule)(type, { enabled, interval_hours, run_time }); // Restart scheduler to apply changes await (0, crawl_scheduler_1.restartCrawlScheduler)(); res.json({ schedule, message: 'Schedule updated and scheduler restarted' }); } catch (error) { console.error('Error updating global schedule:', error); res.status(500).json({ error: 'Failed to update global schedule' }); } }); // ============================================ // Store Schedule Endpoints // ============================================ /** * GET /api/schedule/stores * Get all store schedule statuses */ router.get('/stores', async (req, res) => { try { const stores = await (0, crawl_scheduler_1.getStoreScheduleStatuses)(); res.json({ stores }); } catch (error) { console.error('Error fetching store schedules:', error); res.status(500).json({ error: 'Failed to fetch store schedules' }); } }); /** * GET /api/schedule/stores/:storeId * Get schedule for a specific store */ router.get('/stores/:storeId', async (req, res) => { try { const storeId = parseInt(req.params.storeId); if (isNaN(storeId)) { return res.status(400).json({ error: 'Invalid store ID' }); } const schedule = await (0, crawl_scheduler_1.getStoreSchedule)(storeId); res.json({ schedule }); } catch (error) { console.error('Error fetching store schedule:', error); res.status(500).json({ error: 'Failed to fetch store schedule' }); } }); /** * PUT /api/schedule/stores/:storeId * Update schedule for a specific store */ router.put('/stores/:storeId', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { try { const storeId = parseInt(req.params.storeId); if (isNaN(storeId)) { return res.status(400).json({ error: 'Invalid store ID' }); } const { enabled, interval_hours, daily_special_enabled, daily_special_time, priority } = req.body; const schedule = await (0, crawl_scheduler_1.updateStoreSchedule)(storeId, { enabled, interval_hours, daily_special_enabled, daily_special_time, priority }); res.json({ schedule }); } catch (error) { console.error('Error updating store schedule:', error); res.status(500).json({ error: 'Failed to update store schedule' }); } }); // ============================================ // Job Queue Endpoints // ============================================ /** * GET /api/schedule/jobs * Get recent jobs */ router.get('/jobs', async (req, res) => { try { const limit = parseInt(req.query.limit) || 50; const jobs = await (0, crawl_scheduler_1.getAllRecentJobs)(Math.min(limit, 200)); res.json({ jobs }); } catch (error) { console.error('Error fetching jobs:', error); res.status(500).json({ error: 'Failed to fetch jobs' }); } }); /** * GET /api/schedule/jobs/store/:storeId * Get recent jobs for a specific store */ router.get('/jobs/store/:storeId', async (req, res) => { try { const storeId = parseInt(req.params.storeId); if (isNaN(storeId)) { return res.status(400).json({ error: 'Invalid store ID' }); } const limit = parseInt(req.query.limit) || 10; const jobs = await (0, crawl_scheduler_1.getRecentJobs)(storeId, Math.min(limit, 100)); res.json({ jobs }); } catch (error) { console.error('Error fetching store jobs:', error); res.status(500).json({ error: 'Failed to fetch store jobs' }); } }); /** * POST /api/schedule/jobs/:jobId/cancel * Cancel a pending job */ router.post('/jobs/:jobId/cancel', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { try { const jobId = parseInt(req.params.jobId); if (isNaN(jobId)) { return res.status(400).json({ error: 'Invalid job ID' }); } const cancelled = await (0, crawl_scheduler_1.cancelJob)(jobId); if (cancelled) { res.json({ success: true, message: 'Job cancelled' }); } else { res.status(400).json({ error: 'Job could not be cancelled (may not be pending)' }); } } catch (error) { console.error('Error cancelling job:', error); res.status(500).json({ error: 'Failed to cancel job' }); } }); // ============================================ // Manual Trigger Endpoints // ============================================ /** * POST /api/schedule/trigger/store/:storeId * Manually trigger orchestrated crawl for a specific store * Uses the intelligent orchestrator which: * - Checks provider detection status * - Runs detection if needed * - Queues appropriate crawl type (production/sandbox) */ router.post('/trigger/store/:storeId', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { try { const storeId = parseInt(req.params.storeId); if (isNaN(storeId)) { return res.status(400).json({ error: 'Invalid store ID' }); } // Use the orchestrator instead of simple triggerManualCrawl const result = await (0, store_crawl_orchestrator_1.runStoreCrawlOrchestrator)(storeId); res.json({ result, message: result.summary, success: result.status === 'success' || result.status === 'sandbox_only', }); } catch (error) { console.error('Error triggering orchestrated crawl:', error); res.status(500).json({ error: 'Failed to trigger crawl' }); } }); /** * POST /api/schedule/trigger/store/:storeId/legacy * Legacy: Simple job queue trigger (no orchestration) */ router.post('/trigger/store/:storeId/legacy', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { try { const storeId = parseInt(req.params.storeId); if (isNaN(storeId)) { return res.status(400).json({ error: 'Invalid store ID' }); } const job = await (0, crawl_scheduler_1.triggerManualCrawl)(storeId); res.json({ job, message: 'Crawl job created' }); } catch (error) { console.error('Error triggering manual crawl:', error); res.status(500).json({ error: 'Failed to trigger crawl' }); } }); /** * POST /api/schedule/trigger/all * Manually trigger crawls for all stores */ router.post('/trigger/all', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { try { const jobsCreated = await (0, crawl_scheduler_1.triggerAllStoresCrawl)(); res.json({ jobs_created: jobsCreated, message: `Created ${jobsCreated} crawl jobs` }); } catch (error) { console.error('Error triggering all crawls:', error); res.status(500).json({ error: 'Failed to trigger crawls' }); } }); /** * POST /api/schedule/restart * Restart the scheduler */ router.post('/restart', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { try { await (0, crawl_scheduler_1.restartCrawlScheduler)(); res.json({ message: 'Scheduler restarted', mode: (0, crawl_scheduler_1.getSchedulerMode)() }); } catch (error) { console.error('Error restarting scheduler:', error); res.status(500).json({ error: 'Failed to restart scheduler' }); } }); // ============================================ // Scheduler Mode Endpoints // ============================================ /** * GET /api/schedule/mode * Get current scheduler mode */ router.get('/mode', async (req, res) => { try { const mode = (0, crawl_scheduler_1.getSchedulerMode)(); res.json({ mode }); } catch (error) { console.error('Error getting scheduler mode:', error); res.status(500).json({ error: 'Failed to get scheduler mode' }); } }); /** * PUT /api/schedule/mode * Set scheduler mode (legacy or orchestrator) */ router.put('/mode', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { try { const { mode } = req.body; if (mode !== 'legacy' && mode !== 'orchestrator') { return res.status(400).json({ error: 'Invalid mode. Must be "legacy" or "orchestrator"' }); } (0, crawl_scheduler_1.setSchedulerMode)(mode); // Restart scheduler with new mode await (0, crawl_scheduler_1.restartCrawlScheduler)(); res.json({ mode, message: `Scheduler mode set to ${mode} and restarted` }); } catch (error) { console.error('Error setting scheduler mode:', error); res.status(500).json({ error: 'Failed to set scheduler mode' }); } }); /** * GET /api/schedule/due * Get stores that are due for orchestration */ router.get('/due', async (req, res) => { try { const limit = parseInt(req.query.limit) || 10; const storeIds = await (0, store_crawl_orchestrator_1.getStoresDueForOrchestration)(Math.min(limit, 50)); res.json({ stores_due: storeIds, count: storeIds.length }); } catch (error) { console.error('Error getting stores due for orchestration:', error); res.status(500).json({ error: 'Failed to get stores due' }); } }); // ============================================ // Dispensary Schedule Endpoints (NEW - dispensary-centric) // ============================================ /** * GET /api/schedule/dispensaries * Get all dispensary schedule statuses with optional filters * Query params: * - state: filter by state (e.g., 'AZ') * - search: search by name or slug */ router.get('/dispensaries', async (req, res) => { try { const { state, search } = req.query; // Build dynamic query with optional filters const conditions = []; const params = []; let paramIndex = 1; if (state) { conditions.push(`d.state = $${paramIndex}`); params.push(state); paramIndex++; } if (search) { conditions.push(`(d.name ILIKE $${paramIndex} OR d.slug ILIKE $${paramIndex} OR d.dba_name ILIKE $${paramIndex})`); params.push(`%${search}%`); paramIndex++; } const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; const query = ` SELECT d.id AS dispensary_id, COALESCE(d.dba_name, d.name) AS dispensary_name, d.slug AS dispensary_slug, d.city, d.state, d.menu_url, d.menu_type, d.platform_dispensary_id, d.scrape_enabled, d.last_crawl_at, d.crawl_status, d.product_crawler_mode, d.product_provider, cs.interval_minutes, cs.is_active, cs.priority, cs.last_run_at, cs.next_run_at, cs.last_status AS schedule_last_status, cs.last_error AS schedule_last_error, cs.consecutive_failures, j.id AS latest_job_id, j.status AS latest_job_status, j.job_type AS latest_job_type, j.started_at AS latest_job_started, j.completed_at AS latest_job_completed, j.products_found AS latest_products_found, j.products_new AS latest_products_created, j.products_updated AS latest_products_updated, j.error_message AS latest_job_error, CASE WHEN d.menu_type = 'dutchie' AND d.platform_dispensary_id IS NOT NULL THEN true ELSE false END AS can_crawl, CASE WHEN d.menu_type IS NULL OR d.menu_type = 'unknown' THEN 'menu_type not detected' WHEN d.menu_type != 'dutchie' THEN 'not dutchie platform' WHEN d.platform_dispensary_id IS NULL THEN 'platform ID not resolved' WHEN d.scrape_enabled = false THEN 'scraping disabled' ELSE 'ready' END AS schedule_status_reason FROM public.dispensaries d LEFT JOIN public.dispensary_crawl_schedule cs ON cs.dispensary_id = d.id LEFT JOIN LATERAL ( SELECT * FROM public.dispensary_crawl_jobs dj WHERE dj.dispensary_id = d.id ORDER BY dj.created_at DESC LIMIT 1 ) j ON true ${whereClause} ORDER BY cs.priority DESC NULLS LAST, COALESCE(d.dba_name, d.name) `; const result = await migrate_1.pool.query(query, params); res.json({ dispensaries: result.rows }); } catch (error) { console.error('Error fetching dispensary schedules:', error); res.status(500).json({ error: 'Failed to fetch dispensary schedules' }); } }); /** * GET /api/schedule/dispensaries/:id * Get schedule for a specific dispensary */ router.get('/dispensaries/:id', async (req, res) => { try { const dispensaryId = parseInt(req.params.id); if (isNaN(dispensaryId)) { return res.status(400).json({ error: 'Invalid dispensary ID' }); } const result = await migrate_1.pool.query(` SELECT * FROM dispensary_crawl_status WHERE dispensary_id = $1 `, [dispensaryId]); if (result.rows.length === 0) { return res.status(404).json({ error: 'Dispensary not found' }); } res.json({ schedule: result.rows[0] }); } catch (error) { console.error('Error fetching dispensary schedule:', error); res.status(500).json({ error: 'Failed to fetch dispensary schedule' }); } }); /** * PUT /api/schedule/dispensaries/:id * Update schedule for a specific dispensary */ router.put('/dispensaries/:id', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { try { const dispensaryId = parseInt(req.params.id); if (isNaN(dispensaryId)) { return res.status(400).json({ error: 'Invalid dispensary ID' }); } const { is_active, interval_minutes, priority } = req.body; // Upsert schedule const result = await migrate_1.pool.query(` INSERT INTO dispensary_crawl_schedule (dispensary_id, is_active, interval_minutes, priority) VALUES ($1, COALESCE($2, TRUE), COALESCE($3, 240), COALESCE($4, 0)) ON CONFLICT (dispensary_id) DO UPDATE SET is_active = COALESCE($2, dispensary_crawl_schedule.is_active), interval_minutes = COALESCE($3, dispensary_crawl_schedule.interval_minutes), priority = COALESCE($4, dispensary_crawl_schedule.priority), updated_at = NOW() RETURNING * `, [dispensaryId, is_active, interval_minutes, priority]); res.json({ schedule: result.rows[0] }); } catch (error) { console.error('Error updating dispensary schedule:', error); res.status(500).json({ error: 'Failed to update dispensary schedule' }); } }); /** * GET /api/schedule/dispensary-jobs * Get recent dispensary crawl jobs */ router.get('/dispensary-jobs', async (req, res) => { try { const limit = parseInt(req.query.limit) || 50; const result = await migrate_1.pool.query(` SELECT dcj.*, d.name as dispensary_name FROM dispensary_crawl_jobs dcj JOIN dispensaries d ON d.id = dcj.dispensary_id ORDER BY dcj.created_at DESC LIMIT $1 `, [Math.min(limit, 200)]); res.json({ jobs: result.rows }); } catch (error) { console.error('Error fetching dispensary jobs:', error); res.status(500).json({ error: 'Failed to fetch dispensary jobs' }); } }); /** * GET /api/schedule/dispensary-jobs/:dispensaryId * Get recent jobs for a specific dispensary */ router.get('/dispensary-jobs/:dispensaryId', async (req, res) => { try { const dispensaryId = parseInt(req.params.dispensaryId); if (isNaN(dispensaryId)) { return res.status(400).json({ error: 'Invalid dispensary ID' }); } const limit = parseInt(req.query.limit) || 10; const result = await migrate_1.pool.query(` SELECT dcj.*, d.name as dispensary_name FROM dispensary_crawl_jobs dcj JOIN dispensaries d ON d.id = dcj.dispensary_id WHERE dcj.dispensary_id = $1 ORDER BY dcj.created_at DESC LIMIT $2 `, [dispensaryId, Math.min(limit, 100)]); res.json({ jobs: result.rows }); } catch (error) { console.error('Error fetching dispensary jobs:', error); res.status(500).json({ error: 'Failed to fetch dispensary jobs' }); } }); /** * POST /api/schedule/trigger/dispensary/:id * Trigger orchestrator for a specific dispensary (Run Now button) */ router.post('/trigger/dispensary/:id', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { try { const dispensaryId = parseInt(req.params.id); if (isNaN(dispensaryId)) { return res.status(400).json({ error: 'Invalid dispensary ID' }); } // Run the dispensary orchestrator const result = await (0, dispensary_orchestrator_1.runDispensaryOrchestrator)(dispensaryId); res.json({ result, message: result.summary, success: result.status === 'success' || result.status === 'sandbox_only' || result.status === 'detection_only', }); } catch (error) { console.error('Error triggering dispensary orchestrator:', error); res.status(500).json({ error: 'Failed to trigger orchestrator' }); } }); /** * POST /api/schedule/trigger/dispensaries/batch * Trigger orchestrator for multiple dispensaries */ router.post('/trigger/dispensaries/batch', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { try { const { dispensary_ids, concurrency } = 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' }); } const results = await (0, dispensary_orchestrator_1.runBatchDispensaryOrchestrator)(dispensary_ids, concurrency || 3); const summary = { total: results.length, success: results.filter(r => r.status === 'success').length, sandbox_only: results.filter(r => r.status === 'sandbox_only').length, detection_only: results.filter(r => r.status === 'detection_only').length, error: results.filter(r => r.status === 'error').length, }; res.json({ results, summary }); } catch (error) { console.error('Error triggering batch orchestrator:', error); res.status(500).json({ error: 'Failed to trigger batch orchestrator' }); } }); /** * GET /api/schedule/dispensary-due * Get dispensaries that are due for orchestration */ router.get('/dispensary-due', async (req, res) => { try { const limit = parseInt(req.query.limit) || 10; const dispensaryIds = await (0, dispensary_orchestrator_1.getDispensariesDueForOrchestration)(Math.min(limit, 50)); // Get details for the due dispensaries if (dispensaryIds.length > 0) { const details = await migrate_1.pool.query(` SELECT d.id, d.name, d.product_provider, d.product_crawler_mode, dcs.next_run_at, dcs.last_status, dcs.priority FROM dispensaries d LEFT JOIN dispensary_crawl_schedule dcs ON dcs.dispensary_id = d.id WHERE d.id = ANY($1) ORDER BY COALESCE(dcs.priority, 0) DESC, dcs.last_run_at ASC NULLS FIRST `, [dispensaryIds]); res.json({ dispensaries_due: details.rows, count: dispensaryIds.length }); } else { res.json({ dispensaries_due: [], count: 0 }); } } catch (error) { console.error('Error getting dispensaries due for orchestration:', error); res.status(500).json({ error: 'Failed to get dispensaries due' }); } }); /** * POST /api/schedule/dispensaries/bootstrap * Ensure all dispensaries have schedule entries */ router.post('/dispensaries/bootstrap', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { try { const { interval_minutes } = req.body; const result = await (0, dispensary_orchestrator_1.ensureAllDispensariesHaveSchedules)(interval_minutes || 240); res.json({ message: `Created ${result.created} new schedules, ${result.existing} already existed`, created: result.created, existing: result.existing, }); } catch (error) { console.error('Error bootstrapping dispensary schedules:', error); res.status(500).json({ error: 'Failed to bootstrap schedules' }); } }); // ============================================ // Platform ID & Menu Type Detection Endpoints // ============================================ /** * POST /api/schedule/dispensaries/:id/resolve-platform-id * Resolve the Dutchie platform_dispensary_id from menu_url slug */ router.post('/dispensaries/:id/resolve-platform-id', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { try { const dispensaryId = parseInt(req.params.id); if (isNaN(dispensaryId)) { return res.status(400).json({ error: 'Invalid dispensary ID' }); } // Get dispensary info const dispensaryResult = await migrate_1.pool.query(` SELECT id, name, slug, menu_url, menu_type, platform_dispensary_id FROM dispensaries WHERE id = $1 `, [dispensaryId]); if (dispensaryResult.rows.length === 0) { return res.status(404).json({ error: 'Dispensary not found' }); } const dispensary = dispensaryResult.rows[0]; // Check if already resolved if (dispensary.platform_dispensary_id) { return res.json({ success: true, message: 'Platform ID already resolved', platform_dispensary_id: dispensary.platform_dispensary_id, already_resolved: true }); } // Extract slug from menu_url for Dutchie URLs let slugToResolve = dispensary.slug; if (dispensary.menu_url) { // Match embedded-menu or dispensary URLs const match = dispensary.menu_url.match(/(?:embedded-menu|dispensar(?:y|ies))\/([^\/\?#]+)/i); if (match) { slugToResolve = match[1]; } } if (!slugToResolve) { return res.status(400).json({ error: 'No slug available to resolve platform ID', menu_url: dispensary.menu_url }); } console.log(`[Schedule] Resolving platform ID for ${dispensary.name} using slug: ${slugToResolve}`); // Resolve platform ID using GraphQL client const platformId = await (0, graphql_client_1.resolveDispensaryId)(slugToResolve); if (!platformId) { return res.status(404).json({ error: 'Could not resolve platform ID', slug_tried: slugToResolve, message: 'The dispensary might not be on Dutchie or the slug is incorrect' }); } // Update the dispensary with resolved platform ID await migrate_1.pool.query(` UPDATE dispensaries SET platform_dispensary_id = $1, menu_type = COALESCE(menu_type, 'dutchie'), updated_at = NOW() WHERE id = $2 `, [platformId, dispensaryId]); res.json({ success: true, platform_dispensary_id: platformId, slug_resolved: slugToResolve, message: `Platform ID resolved: ${platformId}` }); } catch (error) { console.error('Error resolving platform ID:', error); res.status(500).json({ error: 'Failed to resolve platform ID', details: error.message }); } }); /** * POST /api/schedule/dispensaries/:id/detect-menu-type * Detect menu type from menu_url */ router.post('/dispensaries/:id/detect-menu-type', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { try { const dispensaryId = parseInt(req.params.id); if (isNaN(dispensaryId)) { return res.status(400).json({ error: 'Invalid dispensary ID' }); } // Get dispensary info const dispensaryResult = await migrate_1.pool.query(` SELECT id, name, menu_url, website FROM dispensaries WHERE id = $1 `, [dispensaryId]); if (dispensaryResult.rows.length === 0) { return res.status(404).json({ error: 'Dispensary not found' }); } const dispensary = dispensaryResult.rows[0]; const urlToCheck = dispensary.menu_url || dispensary.website; if (!urlToCheck) { return res.status(400).json({ error: 'No menu_url or website to detect from' }); } // Detect menu type from URL patterns let detectedType = 'unknown'; if (urlToCheck.includes('dutchie.com') || urlToCheck.includes('embedded-menu')) { detectedType = 'dutchie'; } else if (urlToCheck.includes('iheartjane.com') || urlToCheck.includes('jane.co')) { detectedType = 'jane'; } else if (urlToCheck.includes('weedmaps.com')) { detectedType = 'weedmaps'; } else if (urlToCheck.includes('leafly.com')) { detectedType = 'leafly'; } else if (urlToCheck.includes('treez.io') || urlToCheck.includes('treez.co')) { detectedType = 'treez'; } else if (urlToCheck.includes('meadow.com')) { detectedType = 'meadow'; } else if (urlToCheck.includes('blaze.me') || urlToCheck.includes('blazepay')) { detectedType = 'blaze'; } else if (urlToCheck.includes('flowhub.com')) { detectedType = 'flowhub'; } else if (urlToCheck.includes('dispense.app')) { detectedType = 'dispense'; } else if (urlToCheck.includes('covasoft.com')) { detectedType = 'cova'; } // Update menu_type await migrate_1.pool.query(` UPDATE dispensaries SET menu_type = $1, updated_at = NOW() WHERE id = $2 `, [detectedType, dispensaryId]); res.json({ success: true, menu_type: detectedType, url_checked: urlToCheck, message: `Menu type detected: ${detectedType}` }); } catch (error) { console.error('Error detecting menu type:', error); res.status(500).json({ error: 'Failed to detect menu type' }); } }); /** * POST /api/schedule/dispensaries/:id/refresh-detection * Combined: detect menu_type AND resolve platform_dispensary_id if dutchie */ router.post('/dispensaries/:id/refresh-detection', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { try { const dispensaryId = parseInt(req.params.id); if (isNaN(dispensaryId)) { return res.status(400).json({ error: 'Invalid dispensary ID' }); } // Get dispensary info const dispensaryResult = await migrate_1.pool.query(` SELECT id, name, slug, menu_url, website FROM dispensaries WHERE id = $1 `, [dispensaryId]); if (dispensaryResult.rows.length === 0) { return res.status(404).json({ error: 'Dispensary not found' }); } const dispensary = dispensaryResult.rows[0]; const urlToCheck = dispensary.menu_url || dispensary.website; if (!urlToCheck) { return res.status(400).json({ error: 'No menu_url or website to detect from' }); } // Detect menu type from URL patterns let detectedType = 'unknown'; if (urlToCheck.includes('dutchie.com') || urlToCheck.includes('embedded-menu')) { detectedType = 'dutchie'; } else if (urlToCheck.includes('iheartjane.com') || urlToCheck.includes('jane.co')) { detectedType = 'jane'; } else if (urlToCheck.includes('weedmaps.com')) { detectedType = 'weedmaps'; } else if (urlToCheck.includes('leafly.com')) { detectedType = 'leafly'; } else if (urlToCheck.includes('treez.io') || urlToCheck.includes('treez.co')) { detectedType = 'treez'; } else if (urlToCheck.includes('meadow.com')) { detectedType = 'meadow'; } else if (urlToCheck.includes('blaze.me') || urlToCheck.includes('blazepay')) { detectedType = 'blaze'; } else if (urlToCheck.includes('flowhub.com')) { detectedType = 'flowhub'; } else if (urlToCheck.includes('dispense.app')) { detectedType = 'dispense'; } else if (urlToCheck.includes('covasoft.com')) { detectedType = 'cova'; } // Update menu_type first await migrate_1.pool.query(` UPDATE dispensaries SET menu_type = $1, updated_at = NOW() WHERE id = $2 `, [detectedType, dispensaryId]); let platformId = null; // If dutchie, also try to resolve platform ID if (detectedType === 'dutchie') { let slugToResolve = dispensary.slug; const match = urlToCheck.match(/(?:embedded-menu|dispensar(?:y|ies))\/([^\/\?#]+)/i); if (match) { slugToResolve = match[1]; } if (slugToResolve) { try { console.log(`[Schedule] Resolving platform ID for ${dispensary.name} using slug: ${slugToResolve}`); platformId = await (0, graphql_client_1.resolveDispensaryId)(slugToResolve); if (platformId) { await migrate_1.pool.query(` UPDATE dispensaries SET platform_dispensary_id = $1, updated_at = NOW() WHERE id = $2 `, [platformId, dispensaryId]); } } catch (err) { console.warn(`[Schedule] Failed to resolve platform ID: ${err.message}`); } } } res.json({ success: true, menu_type: detectedType, platform_dispensary_id: platformId, url_checked: urlToCheck, can_crawl: detectedType === 'dutchie' && !!platformId }); } catch (error) { console.error('Error refreshing detection:', error); res.status(500).json({ error: 'Failed to refresh detection' }); } }); /** * PUT /api/schedule/dispensaries/:id/toggle-active * Enable or disable schedule for a dispensary */ router.put('/dispensaries/:id/toggle-active', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { try { const dispensaryId = parseInt(req.params.id); if (isNaN(dispensaryId)) { return res.status(400).json({ error: 'Invalid dispensary ID' }); } const { is_active } = req.body; // Upsert schedule with new is_active value const result = await migrate_1.pool.query(` INSERT INTO dispensary_crawl_schedule (dispensary_id, is_active, interval_minutes, priority) VALUES ($1, $2, 240, 0) ON CONFLICT (dispensary_id) DO UPDATE SET is_active = $2, updated_at = NOW() RETURNING * `, [dispensaryId, is_active]); res.json({ success: true, schedule: result.rows[0], message: is_active ? 'Schedule enabled' : 'Schedule disabled' }); } catch (error) { console.error('Error toggling schedule active status:', error); res.status(500).json({ error: 'Failed to toggle schedule' }); } }); /** * DELETE /api/schedule/dispensaries/:id/schedule * Delete schedule for a dispensary */ router.delete('/dispensaries/:id/schedule', (0, middleware_1.requireRole)('superadmin', 'admin'), async (req, res) => { try { const dispensaryId = parseInt(req.params.id); if (isNaN(dispensaryId)) { return res.status(400).json({ error: 'Invalid dispensary ID' }); } const result = await migrate_1.pool.query(` DELETE FROM dispensary_crawl_schedule WHERE dispensary_id = $1 RETURNING id `, [dispensaryId]); const deleted = (result.rowCount ?? 0) > 0; res.json({ success: true, deleted, message: deleted ? 'Schedule deleted' : 'No schedule to delete' }); } catch (error) { console.error('Error deleting schedule:', error); res.status(500).json({ error: 'Failed to delete schedule' }); } }); exports.default = router;