- 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>
139 lines
3.9 KiB
TypeScript
139 lines
3.9 KiB
TypeScript
/**
|
|
* Run Jane product discovery for stores in database
|
|
* Usage: npx ts-node scripts/run-jane-product-discovery.ts [DISPENSARY_ID]
|
|
* Example: npx ts-node scripts/run-jane-product-discovery.ts 4220
|
|
* Or run for all Jane stores: npx ts-node scripts/run-jane-product-discovery.ts all
|
|
*/
|
|
|
|
import { Pool } from 'pg';
|
|
import { fetchProductsByStoreIdDirect } from '../src/platforms/jane';
|
|
import { saveRawPayload } from '../src/utils/payload-storage';
|
|
|
|
async function main() {
|
|
const arg = process.argv[2];
|
|
|
|
console.log('='.repeat(60));
|
|
console.log('Jane Product Discovery');
|
|
console.log('='.repeat(60));
|
|
|
|
const pool = new Pool({
|
|
connectionString: process.env.DATABASE_URL,
|
|
});
|
|
|
|
try {
|
|
// Get dispensaries to process
|
|
let dispensaries: any[];
|
|
|
|
if (arg === 'all') {
|
|
const result = await pool.query(
|
|
`SELECT id, name, menu_url, platform_dispensary_id
|
|
FROM dispensaries
|
|
WHERE platform = 'jane' AND menu_url IS NOT NULL
|
|
ORDER BY id`
|
|
);
|
|
dispensaries = result.rows;
|
|
} else if (arg) {
|
|
const result = await pool.query(
|
|
`SELECT id, name, menu_url, platform_dispensary_id
|
|
FROM dispensaries
|
|
WHERE id = $1`,
|
|
[parseInt(arg)]
|
|
);
|
|
dispensaries = result.rows;
|
|
} else {
|
|
// Default: get first Jane store
|
|
const result = await pool.query(
|
|
`SELECT id, name, menu_url, platform_dispensary_id
|
|
FROM dispensaries
|
|
WHERE platform = 'jane' AND menu_url IS NOT NULL
|
|
ORDER BY id LIMIT 1`
|
|
);
|
|
dispensaries = result.rows;
|
|
}
|
|
|
|
if (dispensaries.length === 0) {
|
|
console.log('No Jane dispensaries found');
|
|
return;
|
|
}
|
|
|
|
console.log(`Processing ${dispensaries.length} dispensary(ies)...\n`);
|
|
|
|
let successCount = 0;
|
|
let failCount = 0;
|
|
|
|
for (const disp of dispensaries) {
|
|
console.log(`\n${'─'.repeat(60)}`);
|
|
console.log(`${disp.name} (ID: ${disp.id}, Jane ID: ${disp.platform_dispensary_id})`);
|
|
console.log('─'.repeat(60));
|
|
|
|
try {
|
|
const result = await fetchProductsByStoreIdDirect(disp.platform_dispensary_id);
|
|
|
|
if (result.products.length === 0) {
|
|
console.log(' ✗ No products captured');
|
|
failCount++;
|
|
continue;
|
|
}
|
|
|
|
console.log(` ✓ Captured ${result.products.length} products`);
|
|
|
|
// Build payload
|
|
const rawPayload = {
|
|
hits: result.products.map(p => p.raw),
|
|
store: result.store?.raw || null,
|
|
capturedAt: new Date().toISOString(),
|
|
platform: 'jane',
|
|
dispensaryId: disp.id,
|
|
storeId: disp.platform_dispensary_id,
|
|
};
|
|
|
|
// Save payload
|
|
const { id: payloadId, sizeBytes } = await saveRawPayload(
|
|
pool,
|
|
disp.id,
|
|
rawPayload,
|
|
null,
|
|
result.products.length,
|
|
'jane'
|
|
);
|
|
|
|
console.log(` ✓ Saved payload ${payloadId} (${Math.round(sizeBytes / 1024)}KB)`);
|
|
|
|
// Update dispensary
|
|
await pool.query(
|
|
`UPDATE dispensaries
|
|
SET stage = 'hydrating',
|
|
last_fetch_at = NOW(),
|
|
product_count = $2,
|
|
consecutive_successes = consecutive_successes + 1,
|
|
consecutive_failures = 0,
|
|
updated_at = NOW()
|
|
WHERE id = $1`,
|
|
[disp.id, result.products.length]
|
|
);
|
|
|
|
console.log(` ✓ Updated dispensary (product_count: ${result.products.length})`);
|
|
successCount++;
|
|
|
|
} catch (error: any) {
|
|
console.log(` ✗ Error: ${error.message}`);
|
|
failCount++;
|
|
}
|
|
}
|
|
|
|
console.log('\n' + '='.repeat(60));
|
|
console.log('RESULTS');
|
|
console.log('='.repeat(60));
|
|
console.log(`Success: ${successCount}`);
|
|
console.log(`Failed: ${failCount}`);
|
|
|
|
} catch (error: any) {
|
|
console.error('Error:', error.message);
|
|
process.exit(1);
|
|
} finally {
|
|
await pool.end();
|
|
}
|
|
}
|
|
|
|
main();
|