feat: AZ dispensary harmonization with Dutchie source of truth
Major changes: - Add harmonize-az-dispensaries.ts script to sync dispensaries with Dutchie API - Add migration 057 for crawl_enabled and dutchie_verified fields - Remove legacy dutchie-az module (replaced by platforms/dutchie) - Clean up deprecated crawlers, scrapers, and orchestrator code - Update location-discovery to not fallback to slug when ID is missing - Add crawl-rotator service for proxy rotation - Add types/index.ts for shared type definitions - Add woodpecker-agent k8s manifest Harmonization script: - Queries ConsumerDispensaries API for all 32 AZ cities - Matches dispensaries by platform_dispensary_id (not slug) - Updates existing records with full Dutchie data - Creates new records for unmatched Dutchie dispensaries - Disables dispensaries not found in Dutchie 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,6 @@
|
||||
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { pool } from '../db/pool';
|
||||
import { query as dutchieAzQuery } from '../dutchie-az/db/connection';
|
||||
import ipaddr from 'ipaddr.js';
|
||||
import {
|
||||
ApiScope,
|
||||
@@ -140,7 +139,7 @@ async function validatePublicApiKey(
|
||||
|
||||
try {
|
||||
// Query WordPress permissions table with store info
|
||||
const result = await pool.query<ApiKeyPermission>(`
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
p.id,
|
||||
p.user_name,
|
||||
@@ -198,7 +197,7 @@ async function validatePublicApiKey(
|
||||
|
||||
// Resolve the dutchie_az store for wordpress keys
|
||||
if (permission.key_type === 'wordpress' && permission.store_name) {
|
||||
const storeResult = await dutchieAzQuery<{ id: number }>(`
|
||||
const storeResult = await pool.query(`
|
||||
SELECT id FROM dispensaries
|
||||
WHERE LOWER(TRIM(name)) = LOWER(TRIM($1))
|
||||
OR LOWER(TRIM(name)) LIKE LOWER(TRIM($1)) || '%'
|
||||
@@ -439,7 +438,7 @@ router.get('/products', async (req: PublicApiRequest, res: Response) => {
|
||||
|
||||
// Query products with latest snapshot data
|
||||
// Note: Price filters use HAVING clause since they reference the snapshot subquery
|
||||
const { rows: products } = await dutchieAzQuery(`
|
||||
const { rows: products } = await pool.query(`
|
||||
SELECT
|
||||
p.id,
|
||||
p.dispensary_id,
|
||||
@@ -482,7 +481,7 @@ router.get('/products', async (req: PublicApiRequest, res: Response) => {
|
||||
`, params);
|
||||
|
||||
// Get total count for pagination (include price filters if specified)
|
||||
const { rows: countRows } = await dutchieAzQuery(`
|
||||
const { rows: countRows } = await pool.query(`
|
||||
SELECT COUNT(*) as total FROM dutchie_products p
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT rec_min_price_cents, special FROM dutchie_product_snapshots
|
||||
@@ -567,7 +566,7 @@ router.get('/products/:id', async (req: PublicApiRequest, res: Response) => {
|
||||
const { id } = req.params;
|
||||
|
||||
// Get product (without dispensary filter to check access afterward)
|
||||
const { rows: products } = await dutchieAzQuery(`
|
||||
const { rows: products } = await pool.query(`
|
||||
SELECT
|
||||
p.*,
|
||||
s.rec_min_price_cents,
|
||||
@@ -677,7 +676,7 @@ router.get('/categories', async (req: PublicApiRequest, res: Response) => {
|
||||
});
|
||||
}
|
||||
|
||||
const { rows: categories } = await dutchieAzQuery(`
|
||||
const { rows: categories } = await pool.query(`
|
||||
SELECT
|
||||
type as category,
|
||||
subcategory,
|
||||
@@ -733,7 +732,7 @@ router.get('/brands', async (req: PublicApiRequest, res: Response) => {
|
||||
});
|
||||
}
|
||||
|
||||
const { rows: brands } = await dutchieAzQuery(`
|
||||
const { rows: brands } = await pool.query(`
|
||||
SELECT
|
||||
brand_name as brand,
|
||||
COUNT(*) as product_count,
|
||||
@@ -796,7 +795,7 @@ router.get('/specials', async (req: PublicApiRequest, res: Response) => {
|
||||
|
||||
params.push(limitNum, offsetNum);
|
||||
|
||||
const { rows: products } = await dutchieAzQuery(`
|
||||
const { rows: products } = await pool.query(`
|
||||
SELECT
|
||||
p.id,
|
||||
p.dispensary_id,
|
||||
@@ -828,7 +827,7 @@ router.get('/specials', async (req: PublicApiRequest, res: Response) => {
|
||||
|
||||
// Get total count
|
||||
const countParams = params.slice(0, -2);
|
||||
const { rows: countRows } = await dutchieAzQuery(`
|
||||
const { rows: countRows } = await pool.query(`
|
||||
SELECT COUNT(*) as total
|
||||
FROM dutchie_products p
|
||||
INNER JOIN LATERAL (
|
||||
@@ -906,7 +905,7 @@ router.get('/dispensaries', async (req: PublicApiRequest, res: Response) => {
|
||||
}
|
||||
|
||||
// Get single dispensary for wordpress key
|
||||
const { rows: dispensaries } = await dutchieAzQuery(`
|
||||
const { rows: dispensaries } = await pool.query(`
|
||||
SELECT
|
||||
d.id,
|
||||
d.name,
|
||||
@@ -1013,7 +1012,7 @@ router.get('/dispensaries', async (req: PublicApiRequest, res: Response) => {
|
||||
const limitNum = Math.min(parseInt(limit as string, 10) || 100, 500);
|
||||
const offsetNum = parseInt(offset as string, 10) || 0;
|
||||
|
||||
const { rows: dispensaries } = await dutchieAzQuery(`
|
||||
const { rows: dispensaries } = await pool.query(`
|
||||
SELECT
|
||||
d.id,
|
||||
d.name,
|
||||
@@ -1051,7 +1050,7 @@ router.get('/dispensaries', async (req: PublicApiRequest, res: Response) => {
|
||||
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
||||
`, [...params, limitNum, offsetNum]);
|
||||
|
||||
const { rows: countRows } = await dutchieAzQuery(`
|
||||
const { rows: countRows } = await pool.query(`
|
||||
SELECT COUNT(*) as total
|
||||
FROM dispensaries d
|
||||
LEFT JOIN LATERAL (
|
||||
@@ -1178,7 +1177,7 @@ router.get('/search', async (req: PublicApiRequest, res: Response) => {
|
||||
|
||||
params.push(limitNum, offsetNum);
|
||||
|
||||
const { rows: products } = await dutchieAzQuery(`
|
||||
const { rows: products } = await pool.query(`
|
||||
SELECT
|
||||
p.id,
|
||||
p.dispensary_id,
|
||||
@@ -1221,7 +1220,7 @@ router.get('/search', async (req: PublicApiRequest, res: Response) => {
|
||||
|
||||
// Count query (without relevance param)
|
||||
const countParams = params.slice(0, paramIndex - 3); // Remove relevance, limit, offset
|
||||
const { rows: countRows } = await dutchieAzQuery(`
|
||||
const { rows: countRows } = await pool.query(`
|
||||
SELECT COUNT(*) as total
|
||||
FROM dutchie_products p
|
||||
${whereClause}
|
||||
@@ -1302,7 +1301,7 @@ router.get('/menu', async (req: PublicApiRequest, res: Response) => {
|
||||
}
|
||||
|
||||
// Get counts by category
|
||||
const { rows: categoryCounts } = await dutchieAzQuery(`
|
||||
const { rows: categoryCounts } = await pool.query(`
|
||||
SELECT
|
||||
type as category,
|
||||
COUNT(*) as total,
|
||||
@@ -1314,7 +1313,7 @@ router.get('/menu', async (req: PublicApiRequest, res: Response) => {
|
||||
`, params);
|
||||
|
||||
// Get overall stats
|
||||
const { rows: stats } = await dutchieAzQuery(`
|
||||
const { rows: stats } = await pool.query(`
|
||||
SELECT
|
||||
COUNT(*) as total_products,
|
||||
COUNT(*) FILTER (WHERE stock_status = 'in_stock') as in_stock_count,
|
||||
@@ -1326,7 +1325,7 @@ router.get('/menu', async (req: PublicApiRequest, res: Response) => {
|
||||
`, params);
|
||||
|
||||
// Get specials count
|
||||
const { rows: specialsCount } = await dutchieAzQuery(`
|
||||
const { rows: specialsCount } = await pool.query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM dutchie_products p
|
||||
INNER JOIN LATERAL (
|
||||
|
||||
Reference in New Issue
Block a user