← back to Ventura Corridor
src/jobs/seed_pitches.ts
94 lines
// One-shot — seed a pitch row for every corridor business that doesn't have one yet.
// Pitch type is derived deterministically from NAICS description so /pitches.html
// has a real pipeline to work. No LLM, no outbound — pure DB write.
//
// Run once: npx tsx src/jobs/seed_pitches.ts
import { pool, query } from '../../db/pool.ts';
// NAICS-description → pitch_type. Order matters: first match wins.
// Covers ~85% of the corridus by descending volume; everything unmatched falls
// to 'office-tenant' (the most generic outreach archetype).
const RULES: [RegExp, string][] = [
[/restaurant|food service|caterer|bar|tavern|coffee|bakery|patisserie|donut/i, 'restaurant'],
[/legal services|attorney|law office|notary/i, 'law-office'],
[/dental|dentist|orthodont/i, 'dental-office'],
[/physician|medical|chiropract|podiatr|optometr|home health|health practitioner|nursing/i, 'medical-clinic'],
[/mental health|psycholog|therap|counsel/i, 'medical-clinic'],
[/beauty|nail salon|hair|cosmetolog|spa|esthetic|skincare|massage/i, 'beauty-spa'],
[/barber/i, 'salon-spa'],
[/fitness|gym|yoga|pilates|martial|sports/i, 'fitness-studio'],
[/real estate|property manag|lessor|leasing|realtor/i, 'real-estate'],
[/hotel|motel|inn|lodging|hospitality/i, 'hospitality'],
[/clothing|jewelry|shoes|retail|general merchand|grocery|specialty food|gift|book|wine|liquor/i, 'retail-storefront'],
[/auto|motor vehicle|repair|car wash|towing|tire/i, 'office-tenant'],
[/dry clean|laundry|tailor|alteration|shoe repair/i, 'retail-storefront'],
[/insurance|financial|bank|credit|invest|account|tax/i, 'office-tenant'],
[/management|consulting|professional|scientific|technical/i, 'office-tenant'],
[/personal services|other personal|all other personal/i, 'office-tenant'],
[/educational|school|college|tutor|preschool/i, 'office-tenant'],
[/construction|contract|builder/i, 'office-tenant'],
[/artist|writer|perform|production|motion picture|music|publishing|broadcast/i, 'office-tenant'],
];
function pitchTypeFor(naics: string | null): string {
if (!naics) return 'office-tenant';
for (const [re, t] of RULES) if (re.test(naics)) return t;
return 'office-tenant';
}
// dw_proximity tag — encodes how the business overlaps with DW's catalog/services.
// Used by /pitches.html to colour-code priority. Heuristic on naics + name.
function dwProximityFor(naics: string | null, name: string): string {
const n = (naics || '').toLowerCase();
const nm = name.toLowerCase();
if (/restaurant|coffee|bar|food|bakery/.test(n)) return 'wallcovering-target';
if (/hotel|motel|spa|salon|beauty/.test(n)) return 'wallcovering-target';
if (/legal|medical|dental|chiropract|therap/.test(n)) return 'office-tenant';
if (/real estate|property|lessor/.test(n)) return 'building-owner';
if (/parking|valet|abm|laz|propark/i.test(nm)) return 'low-priority';
return 'general';
}
async function main() {
// Skip merged dupes, LLC/INC/CORP shells (per generate_features convention),
// anyone already pitched.
const rows = (await query<{ id: number; name: string; naics: string | null }>(`
SELECT b.id, b.name, b.raw->>'primary_naics_description' AS naics
FROM businesses b
LEFT JOIN pitches p ON p.business_id = b.id
WHERE b.on_corridor
AND b.merged_into IS NULL
AND b.address IS NOT NULL
AND b.name NOT ILIKE '%LLC' AND b.name NOT ILIKE '%INC' AND b.name NOT ILIKE '%CORP'
AND length(b.name) BETWEEN 4 AND 60
AND p.id IS NULL
`)).rows;
console.log(`[seed_pitches] candidates without a pitch: ${rows.length.toLocaleString()}`);
const counts: Record<string, number> = {};
let n = 0;
for (const b of rows) {
const pitch_type = pitchTypeFor(b.naics);
const dw_proximity = dwProximityFor(b.naics, b.name);
counts[pitch_type] = (counts[pitch_type] || 0) + 1;
await query(
`INSERT INTO pitches (business_id, pitch_type, priority, status, dw_proximity)
VALUES ($1, $2, 5, 'draft', $3)`,
[b.id, pitch_type, dw_proximity]
);
n++;
if (n % 1000 === 0) console.log(`[seed_pitches] ${n.toLocaleString()} written…`);
}
console.log(`[seed_pitches] done · ${n.toLocaleString()} pitches created`);
console.log(`[seed_pitches] by type:`);
for (const [t, c] of Object.entries(counts).sort((a, b) => b[1] - a[1])) {
console.log(` ${c.toString().padStart(6)} ${t}`);
}
await pool.end();
}
main().catch(e => { console.error('[seed_pitches]', e); process.exit(1); });