feat: SEO template library, discovery pipeline, and orchestrator enhancements

## SEO Template Library
- Add complete template library with 7 page types (state, city, category, brand, product, search, regeneration)
- Add Template Library tab in SEO Orchestrator with accordion-based editors
- Add template preview, validation, and variable injection engine
- Add API endpoints: /api/seo/templates, preview, validate, generate, regenerate

## Discovery Pipeline
- Add promotion.ts for discovery location validation and promotion
- Add discover-all-states.ts script for multi-state discovery
- Add promotion log migration (067)
- Enhance discovery routes and types

## Orchestrator & Admin
- Add crawl_enabled filter to stores page
- Add API permissions page
- Add job queue management
- Add price analytics routes
- Add markets and intelligence routes
- Enhance dashboard and worker monitoring

## Infrastructure
- Add migrations for worker definitions, SEO settings, field alignment
- Add canonical pipeline for scraper v2
- Update hydration and sync orchestrator
- Enhance multi-state query service

🤖 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-09 00:05:34 -07:00
parent 9711d594db
commit 2f483b3084
83 changed files with 16700 additions and 1277 deletions

View File

@@ -234,99 +234,94 @@ export async function syncProductsToCanonical(
const result = await pool.query(
`INSERT INTO store_products (
dispensary_id, state_id, provider, provider_product_id,
provider_brand_id, provider_dispensary_id, enterprise_product_id,
legacy_dutchie_product_id,
name, brand_name, category, subcategory, product_type, strain_type,
description, effects, cannabinoids,
thc_percent, cbd_percent, thc_content_text, cbd_content_text,
is_in_stock, stock_status, stock_quantity,
total_quantity_available, total_kiosk_quantity_available,
image_url, local_image_url, local_image_thumb_url, local_image_medium_url,
original_image_url, additional_images,
is_on_special, is_featured, medical_only, rec_only,
dispensary_id, provider, provider_product_id, provider_brand_id,
platform_dispensary_id, external_product_id,
name_raw, brand_name_raw, category_raw, subcategory_raw, strain_type,
description, effects, cannabinoids_v2,
thc_percent, cbd_percent, thc_content, cbd_content,
is_in_stock, stock_status, stock_quantity, total_quantity_available,
image_url, primary_image_url, images,
is_on_special, featured, medical_only, rec_only,
is_below_threshold, is_below_kiosk_threshold,
platform_status, c_name, weight, options, measurements,
first_seen_at, last_seen_at, updated_at
status, c_name, weight, measurements,
first_seen_at, last_seen_at, created_at, updated_at
) VALUES (
$1, $2, 'dutchie', $3,
$4, $5, $6,
$7,
$8, $9, $10, $11, $12, $13,
$14, $15, $16,
$17, $18, $19, $20,
$21, $22, $23,
$24, $25,
$26, $27, $28, $29,
$30, $31,
$32, $33, $34, $35,
$36, $37,
$38, $39, $40, $41, $42,
$43, $44, NOW()
$1, 'dutchie', $2, $3,
$4, $5,
$6, $7, $8, $9, $10,
$11, $12, $13,
$14, $15, $16, $17,
$18, $19, $20, $21,
$22, $23, $24,
$25, $26, $27, $28,
$29, $30,
$31, $32, $33, $34,
$35, $36, NOW(), NOW()
)
ON CONFLICT (dispensary_id, provider, provider_product_id)
DO UPDATE SET
legacy_dutchie_product_id = EXCLUDED.legacy_dutchie_product_id,
name = EXCLUDED.name,
brand_name = EXCLUDED.brand_name,
category = EXCLUDED.category,
subcategory = EXCLUDED.subcategory,
name_raw = EXCLUDED.name_raw,
brand_name_raw = EXCLUDED.brand_name_raw,
category_raw = EXCLUDED.category_raw,
subcategory_raw = EXCLUDED.subcategory_raw,
strain_type = EXCLUDED.strain_type,
is_in_stock = EXCLUDED.is_in_stock,
stock_status = EXCLUDED.stock_status,
stock_quantity = EXCLUDED.stock_quantity,
total_quantity_available = EXCLUDED.total_quantity_available,
thc_percent = EXCLUDED.thc_percent,
cbd_percent = EXCLUDED.cbd_percent,
thc_content = EXCLUDED.thc_content,
cbd_content = EXCLUDED.cbd_content,
image_url = EXCLUDED.image_url,
local_image_url = EXCLUDED.local_image_url,
primary_image_url = EXCLUDED.primary_image_url,
is_on_special = EXCLUDED.is_on_special,
platform_status = EXCLUDED.platform_status,
status = EXCLUDED.status,
description = COALESCE(EXCLUDED.description, store_products.description),
effects = COALESCE(EXCLUDED.effects, store_products.effects),
cannabinoids_v2 = COALESCE(EXCLUDED.cannabinoids_v2, store_products.cannabinoids_v2),
weight = EXCLUDED.weight,
measurements = EXCLUDED.measurements,
last_seen_at = NOW(),
updated_at = NOW()
RETURNING (xmax = 0) as is_new`,
[
dispensaryId,
stateId,
p.external_product_id,
p.brand_id,
p.platform_dispensary_id,
p.enterprise_product_id,
p.id,
p.name,
p.brand_name,
p.category || p.type,
p.subcategory,
p.type,
p.strain_type,
p.description,
p.effects,
p.cannabinoids_v2,
thcPercent,
cbdPercent,
p.thc_content,
p.cbd_content,
isInStock,
stockStatus,
p.total_quantity_available,
p.total_quantity_available,
p.total_kiosk_quantity_available,
p.primary_image_url,
p.local_image_url,
p.local_image_thumb_url,
p.local_image_medium_url,
p.original_image_url,
p.additional_images,
p.special || false,
p.featured || false,
p.medical_only || false,
p.rec_only || false,
p.is_below_threshold || false,
p.is_below_kiosk_threshold || false,
p.status,
p.c_name,
p.weight,
p.options,
p.measurements,
p.first_seen_at || p.updated_at,
p.last_seen_at || p.updated_at,
dispensaryId, // $1
p.external_product_id, // $2
p.brand_id, // $3
p.platform_dispensary_id, // $4
p.external_product_id, // $5 external_product_id
p.name, // $6
p.brand_name, // $7
p.type || p.category, // $8 category_raw
p.subcategory, // $9
p.strain_type, // $10
p.description, // $11
p.effects, // $12
p.cannabinoids_v2, // $13
thcPercent, // $14
cbdPercent, // $15
p.thc_content, // $16
p.cbd_content, // $17
isInStock, // $18
stockStatus, // $19
p.total_quantity_available || 0, // $20 stock_quantity
p.total_quantity_available || 0, // $21
p.primary_image_url, // $22 image_url
p.primary_image_url, // $23
p.additional_images, // $24 images
p.special || false, // $25
p.featured || false, // $26
p.medical_only || false, // $27
p.rec_only || false, // $28
p.is_below_threshold || false, // $29
p.is_below_kiosk_threshold || false, // $30
p.status, // $31
p.c_name, // $32
p.weight, // $33
p.measurements, // $34
p.first_seen_at || p.updated_at, // $35
p.last_seen_at || p.updated_at, // $36
]
);