/// // // Adds the fields needed to scrape the TENANT's OWN trip page, mirroring // how competitors are scraped, so build_prompt()'s comparison and the // `comments` field are grounded in a real extracted trip instead of only // the PM's typed destination/duration/style strings. // // tenants.website url the tenant's own storefront root — distinct // from `domain` (bare text, used only for // email-domain seat matching, never a URL) // client_trips.url text confirmed/matched own-trip page for a run // client_trips.extracted json full structured extraction blob (mirrors // products' shape — tripName/price/duration/ // activities/serviceLevel/etc.) // client_trips.last_scraped date drives the same 24hr freshness reuse as // competitors (core/scraper.py's // CACHE_STALENESS_HOURS) migrate((app) => { const tenants = app.findCollectionByNameOrId('tenants'); tenants.fields.add(new Field({ type: 'url', name: 'website' })); app.save(tenants); const clientTrips = app.findCollectionByNameOrId('client_trips'); clientTrips.fields.add(new Field({ type: 'text', name: 'url' })); clientTrips.fields.add(new Field({ type: 'json', name: 'extracted' })); clientTrips.fields.add(new Field({ type: 'date', name: 'last_scraped' })); return app.save(clientTrips); }, (app) => { const tenants = app.findCollectionByNameOrId('tenants'); tenants.fields.removeByName('website'); app.save(tenants); const clientTrips = app.findCollectionByNameOrId('client_trips'); clientTrips.fields.removeByName('url'); clientTrips.fields.removeByName('extracted'); clientTrips.fields.removeByName('last_scraped'); return app.save(clientTrips); });