Files
cannaiq/backend/src/routes/changes.ts
Kelly b4a2fb7d03 feat: Add v2 architecture with multi-state support and orchestrator services
Major additions:
- Multi-state expansion: states table, StateSelector, NationalDashboard, StateHeatmap, CrossStateCompare
- Orchestrator services: trace service, error taxonomy, retry manager, proxy rotator
- Discovery system: dutchie discovery service, geo validation, city seeding scripts
- Analytics infrastructure: analytics v2 routes, brand/pricing/stores intelligence pages
- Local development: setup-local.sh starts all 5 services (postgres, backend, cannaiq, findadispo, findagram)
- Migrations 037-056: crawler profiles, states, analytics indexes, worker metadata

Frontend pages added:
- Discovery, ChainsDashboard, IntelligenceBrands, IntelligencePricing, IntelligenceStores
- StateHeatmap, CrossStateCompare, SyncInfoPanel

Components added:
- StateSelector, OrchestratorTraceModal, WorkflowStepper

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-07 11:30:57 -07:00

173 lines
4.5 KiB
TypeScript

import { Router } from 'express';
import { authMiddleware } from '../auth/middleware';
import { pool } from '../db/pool';
const router = Router();
router.use(authMiddleware);
// Get all changes with optional status filter
router.get('/', async (req, res) => {
try {
const { status } = req.query;
let query = `
SELECT
dc.id,
dc.dispensary_id,
dc.field_name,
dc.old_value,
dc.new_value,
dc.source,
dc.confidence_score,
dc.change_notes,
dc.status,
dc.requires_recrawl,
dc.created_at,
dc.reviewed_at,
dc.reviewed_by,
dc.rejection_reason,
d.name as dispensary_name,
d.slug as dispensary_slug,
d.city,
d.state
FROM dispensary_changes dc
JOIN dispensaries d ON dc.dispensary_id = d.id
`;
const params: any[] = [];
if (status) {
query += ` WHERE dc.status = $1`;
params.push(status);
}
query += ` ORDER BY dc.created_at DESC`;
const result = await pool.query(query, params);
res.json({ changes: result.rows });
} catch (error) {
console.error('Error fetching changes:', error);
res.status(500).json({ error: 'Failed to fetch changes' });
}
});
// Get changes statistics (for alert banner)
router.get('/stats', async (req, res) => {
try {
const result = await pool.query(`
SELECT
COUNT(*) FILTER (WHERE status = 'pending') as pending_count,
COUNT(*) FILTER (WHERE status = 'pending' AND requires_recrawl = TRUE) as pending_recrawl_count,
COUNT(*) FILTER (WHERE status = 'approved') as approved_count,
COUNT(*) FILTER (WHERE status = 'rejected') as rejected_count
FROM dispensary_changes
`);
res.json(result.rows[0]);
} catch (error) {
console.error('Error fetching change stats:', error);
res.status(500).json({ error: 'Failed to fetch change stats' });
}
});
// Approve a change and apply it to the dispensary
router.post('/:id/approve', async (req, res) => {
const client = await pool.connect();
try {
await client.query('BEGIN');
const { id } = req.params;
const userId = (req as any).user?.id; // From auth middleware
// Get the change record
const changeResult = await client.query(`
SELECT * FROM dispensary_changes WHERE id = $1 AND status = 'pending'
`, [id]);
if (changeResult.rows.length === 0) {
await client.query('ROLLBACK');
return res.status(404).json({ error: 'Pending change not found' });
}
const change = changeResult.rows[0];
// Apply the change to the dispensary table
const updateQuery = `
UPDATE dispensaries
SET ${change.field_name} = $1, updated_at = CURRENT_TIMESTAMP
WHERE id = $2
RETURNING *
`;
const dispensaryResult = await client.query(updateQuery, [
change.new_value,
change.dispensary_id
]);
if (dispensaryResult.rows.length === 0) {
await client.query('ROLLBACK');
return res.status(404).json({ error: 'Dispensary not found' });
}
// Mark the change as approved
await client.query(`
UPDATE dispensary_changes
SET
status = 'approved',
reviewed_at = CURRENT_TIMESTAMP,
reviewed_by = $1
WHERE id = $2
`, [userId, id]);
await client.query('COMMIT');
res.json({
message: 'Change approved and applied',
dispensary: dispensaryResult.rows[0],
requires_recrawl: change.requires_recrawl
});
} catch (error) {
await client.query('ROLLBACK');
console.error('Error approving change:', error);
res.status(500).json({ error: 'Failed to approve change' });
} finally {
client.release();
}
});
// Reject a change with optional reason
router.post('/:id/reject', async (req, res) => {
try {
const { id } = req.params;
const { reason } = req.body;
const userId = (req as any).user?.id; // From auth middleware
const result = await pool.query(`
UPDATE dispensary_changes
SET
status = 'rejected',
reviewed_at = CURRENT_TIMESTAMP,
reviewed_by = $1,
rejection_reason = $2
WHERE id = $3 AND status = 'pending'
RETURNING *
`, [userId, reason, id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Pending change not found' });
}
res.json({
message: 'Change rejected',
change: result.rows[0]
});
} catch (error) {
console.error('Error rejecting change:', error);
res.status(500).json({ error: 'Failed to reject change' });
}
});
export default router;