## 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>
138 lines
4.1 KiB
TypeScript
138 lines
4.1 KiB
TypeScript
#!/usr/bin/env npx tsx
|
|
/**
|
|
* Retry resolving platform IDs for Dutchie stores that have menu_url but no platform_dispensary_id
|
|
*
|
|
* Usage:
|
|
* npx tsx src/scripts/retry-platform-ids.ts
|
|
*/
|
|
|
|
import { Pool } from 'pg';
|
|
import dotenv from 'dotenv';
|
|
import { resolveDispensaryIdWithDetails } from '../platforms/dutchie/queries';
|
|
|
|
dotenv.config();
|
|
|
|
const pool = new Pool({
|
|
connectionString: process.env.DATABASE_URL ||
|
|
`postgresql://${process.env.CANNAIQ_DB_USER || 'dutchie'}:${process.env.CANNAIQ_DB_PASS || 'dutchie_local_pass'}@${process.env.CANNAIQ_DB_HOST || 'localhost'}:${process.env.CANNAIQ_DB_PORT || '54320'}/${process.env.CANNAIQ_DB_NAME || 'dutchie_menus'}`
|
|
});
|
|
|
|
interface DispensaryRow {
|
|
id: number;
|
|
name: string;
|
|
menu_url: string;
|
|
}
|
|
|
|
function extractSlugFromUrl(menuUrl: string): string | null {
|
|
// Extract slug from Dutchie URLs like:
|
|
// https://dutchie.com/stores/Nirvana-North-Phoenix
|
|
// https://dutchie.com/dispensary/curaleaf-dispensary-peoria
|
|
// https://dutchie.com/embedded-menu/some-slug
|
|
|
|
const patterns = [
|
|
/dutchie\.com\/stores\/([^/?]+)/i,
|
|
/dutchie\.com\/dispensary\/([^/?]+)/i,
|
|
/dutchie\.com\/embedded-menu\/([^/?]+)/i,
|
|
];
|
|
|
|
for (const pattern of patterns) {
|
|
const match = menuUrl.match(pattern);
|
|
if (match) {
|
|
return match[1];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
async function main() {
|
|
console.log('='.repeat(60));
|
|
console.log('Retry Platform ID Resolution');
|
|
console.log('='.repeat(60));
|
|
console.log('');
|
|
|
|
// Get Dutchie dispensaries with menu_url but no platform_dispensary_id
|
|
const result = await pool.query<DispensaryRow>(`
|
|
SELECT id, name, menu_url
|
|
FROM dispensaries
|
|
WHERE menu_type = 'dutchie'
|
|
AND menu_url IS NOT NULL AND menu_url != ''
|
|
AND (platform_dispensary_id IS NULL OR platform_dispensary_id = '')
|
|
ORDER BY name
|
|
`);
|
|
|
|
console.log(`Found ${result.rows.length} stores to retry\n`);
|
|
|
|
if (result.rows.length === 0) {
|
|
console.log('No stores need platform ID resolution.');
|
|
await pool.end();
|
|
return;
|
|
}
|
|
|
|
const successes: { id: number; name: string; platformId: string }[] = [];
|
|
const failures: { id: number; name: string; slug: string | null; error: string }[] = [];
|
|
|
|
for (const row of result.rows) {
|
|
console.log(`\n[${row.id}] ${row.name}`);
|
|
console.log(` URL: ${row.menu_url}`);
|
|
|
|
const slug = extractSlugFromUrl(row.menu_url);
|
|
if (!slug) {
|
|
console.log(` ❌ Could not extract slug from URL`);
|
|
failures.push({ id: row.id, name: row.name, slug: null, error: 'Could not extract slug' });
|
|
continue;
|
|
}
|
|
|
|
console.log(` Slug: ${slug}`);
|
|
|
|
try {
|
|
const resolveResult = await resolveDispensaryIdWithDetails(slug);
|
|
|
|
if (resolveResult.dispensaryId) {
|
|
console.log(` ✅ Resolved: ${resolveResult.dispensaryId}`);
|
|
|
|
// Update database
|
|
await pool.query(
|
|
'UPDATE dispensaries SET platform_dispensary_id = $1 WHERE id = $2',
|
|
[resolveResult.dispensaryId, row.id]
|
|
);
|
|
console.log(` 💾 Updated database`);
|
|
|
|
successes.push({ id: row.id, name: row.name, platformId: resolveResult.dispensaryId });
|
|
} else {
|
|
const errorMsg = resolveResult.error || 'Unknown error';
|
|
console.log(` ❌ Failed: ${errorMsg}`);
|
|
failures.push({ id: row.id, name: row.name, slug, error: errorMsg });
|
|
}
|
|
} catch (error: any) {
|
|
console.log(` ❌ Error: ${error.message}`);
|
|
failures.push({ id: row.id, name: row.name, slug, error: error.message });
|
|
}
|
|
|
|
// Small delay between requests
|
|
await new Promise(r => setTimeout(r, 500));
|
|
}
|
|
|
|
console.log('\n' + '='.repeat(60));
|
|
console.log('SUMMARY');
|
|
console.log('='.repeat(60));
|
|
|
|
console.log(`\n✅ Successes (${successes.length}):`);
|
|
for (const s of successes) {
|
|
console.log(` [${s.id}] ${s.name} -> ${s.platformId}`);
|
|
}
|
|
|
|
console.log(`\n❌ Failures (${failures.length}):`);
|
|
for (const f of failures) {
|
|
console.log(` [${f.id}] ${f.name} (slug: ${f.slug || 'N/A'})`);
|
|
console.log(` ${f.error}`);
|
|
}
|
|
|
|
await pool.end();
|
|
}
|
|
|
|
main().catch(e => {
|
|
console.error('Fatal error:', e);
|
|
process.exit(1);
|
|
});
|