feat(jane): Direct Algolia product fetch and multi-platform product-refresh

- Add fetchProductsByStoreIdDirect() for reliable Algolia product fetching
- Update product-discovery-jane to use direct Algolia instead of network interception
- Fix product-refresh handler to support both Dutchie and Jane payloads
  - Handle both `products` (Dutchie) and `hits` (Jane) formats
  - Use platform-appropriate raw_json structure for normalizers
  - Fix consecutive_misses tracking to use correct provider
  - Extract product IDs correctly (Dutchie _id vs Jane product_id)
- Add store discovery deduplication (prefer REC over MED at same location)
- Add storeTypes field to DiscoveredStore interface
- Add scripts: run-jane-store-discovery.ts, run-jane-product-discovery.ts

🤖 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-13 16:05:50 -07:00
parent 88e590d026
commit d7da0b938d
7 changed files with 570 additions and 80 deletions

View File

@@ -0,0 +1,137 @@
/**
* Run Jane store discovery and insert into database
* Usage: npx ts-node scripts/run-jane-store-discovery.ts [STATE_CODE]
* Example: npx ts-node scripts/run-jane-store-discovery.ts AZ
*/
import { Pool } from 'pg';
import { discoverStoresByState } from '../src/platforms/jane';
/**
* Generate slug from store name
* e.g., "Hana Meds - Phoenix (REC)" -> "hana-meds-phoenix-rec"
*/
function generateSlug(name: string): string {
return name
.toLowerCase()
.replace(/[()]/g, '') // Remove parentheses
.replace(/[^a-z0-9\s-]/g, '') // Remove special chars
.replace(/\s+/g, '-') // Spaces to hyphens
.replace(/-+/g, '-') // Collapse multiple hyphens
.replace(/^-|-$/g, ''); // Trim hyphens
}
async function main() {
const stateCode = process.argv[2] || 'AZ';
console.log('='.repeat(60));
console.log(`Jane Store Discovery - ${stateCode}`);
console.log('='.repeat(60));
// Connect to database
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
try {
// Test connection
const testResult = await pool.query('SELECT COUNT(*) FROM dispensaries WHERE platform = $1', ['jane']);
console.log(`Current Jane stores in DB: ${testResult.rows[0].count}`);
// Discover stores
console.log(`\nDiscovering Jane stores in ${stateCode}...`);
const stores = await discoverStoresByState(stateCode);
if (stores.length === 0) {
console.log(`No stores found in ${stateCode}`);
return;
}
console.log(`\nFound ${stores.length} stores. Inserting into database...`);
// Insert stores
let inserted = 0;
let updated = 0;
const newIds: number[] = [];
for (const store of stores) {
const menuUrl = `https://www.iheartjane.com/stores/${store.storeId}/${store.urlSlug || 'menu'}`;
const slug = generateSlug(store.name);
try {
const result = await pool.query(
`INSERT INTO dispensaries (
name, slug, address1, city, state, zipcode,
latitude, longitude, menu_url, menu_type, platform,
platform_dispensary_id, is_medical, is_recreational,
stage, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, NOW(), NOW())
ON CONFLICT (platform_dispensary_id) WHERE platform_dispensary_id IS NOT NULL
DO UPDATE SET
name = EXCLUDED.name,
slug = EXCLUDED.slug,
address1 = EXCLUDED.address1,
city = EXCLUDED.city,
latitude = EXCLUDED.latitude,
longitude = EXCLUDED.longitude,
menu_url = EXCLUDED.menu_url,
is_medical = EXCLUDED.is_medical,
is_recreational = EXCLUDED.is_recreational,
updated_at = NOW()
RETURNING id, (xmax = 0) AS is_new`,
[
store.name,
slug,
store.address,
store.city,
stateCode,
store.zip,
store.lat,
store.long,
menuUrl,
'embedded', // menu_type: how it's displayed
'jane', // platform: who provides the menu
store.storeId,
store.medical,
store.recreational,
'discovered',
]
);
if (result.rows.length > 0) {
const { id, is_new } = result.rows[0];
if (is_new) {
inserted++;
newIds.push(id);
console.log(` + Inserted: ${store.name} (DB ID: ${id}, Jane ID: ${store.storeId})`);
} else {
updated++;
console.log(` ~ Updated: ${store.name} (DB ID: ${id})`);
}
}
} catch (error: any) {
console.error(` ! Error inserting ${store.name}: ${error.message}`);
}
}
console.log('\n' + '='.repeat(60));
console.log('RESULTS');
console.log('='.repeat(60));
console.log(`Stores discovered: ${stores.length}`);
console.log(`New stores inserted: ${inserted}`);
console.log(`Existing stores updated: ${updated}`);
console.log(`New dispensary IDs: ${newIds.join(', ') || '(none)'}`);
// Show final count
const finalResult = await pool.query('SELECT COUNT(*) FROM dispensaries WHERE platform = $1', ['jane']);
console.log(`\nTotal Jane stores in DB: ${finalResult.rows[0].count}`);
} catch (error: any) {
console.error('Error:', error.message);
process.exit(1);
} finally {
await pool.end();
}
}
main();