import { getCurrentVisit } from '@/entities/city-visit'; import { getProviderProfile } from '@/entities/provider-profile'; import { listDestinations } from '@/entities/destination'; import { listGalleryItems } from '@/entities/gallery-item'; import { listHeroStripItems } from '@/entities/hero-strip'; import { listLoreSections } from '@/entities/lore-section'; import { listPaymentMethods } from '@/entities/payment-method'; import { assembleProviderRates } from '@/entities/rate-card'; import { listRosterContent } from '@/entities/roster-content'; import { listShopListings } from '@/entities/shop-listing'; import { assembleSiteContent } from '@/entities/site-text'; import { listSpecialtyCategories_withItems } from '@/entities/specialty'; import { listTourStops } from '@/entities/tour-stop'; import { listVerifiedProfiles } from '@/entities/verified-profile'; import type { Sql } from '@/shared/db'; import { logger } from '@/shared/logger'; import type { ProviderData, ProviderTourStop } from './types'; interface CacheEntry { readonly data: ProviderData; readonly expires: number; } const providerConfigCache = new Map(); export function resetProviderConfigCache(): void { providerConfigCache.clear(); } export interface AssembleProviderConfigInput { readonly db: Sql; readonly dataApiUrl: string; readonly dataApiToken?: string | undefined; readonly providerSlug: string; readonly today: string; } async function fetchFromDataApi( dataApiUrl: string, providerSlug: string, dataApiToken?: string | undefined, ): Promise { const cacheKey = providerSlug; const now = Date.now(); const cached = providerConfigCache.get(cacheKey); if (cached && now < cached.expires) return cached.data; const url = `${dataApiUrl}/api/data`; const headers: Record = {}; if (dataApiToken) { headers['Authorization'] = `Bearer ${dataApiToken}`; } try { const res = await fetch(url, { method: 'GET', headers, signal: AbortSignal.timeout(5_000), }); if (!res.ok) { logger.warn('data-api provider-config fetch failed', { status: res.status, providerSlug }); return null; } const data = (await res.json()) as ProviderData; providerConfigCache.set(cacheKey, { data, expires: now + 60_000 }); return data; } catch (err) { logger.warn('data-api provider-config fetch error', { error: String(err), providerSlug }); return null; } } function buildDegradedShape(providerSlug: string): ProviderData { return { identity: { name: providerSlug, pronouns: '', gender: '', location: '', secondaryLocations: [], languages: [], tagline: '', }, physical: { age: '', height: '', bodyType: '', ethnicity: '', hairColor: '', hairLength: '', eyeColor: '', cupSize: '', additional: {}, }, rates: [], addOns: { entries: [] }, travelFees: { entries: [] }, touringPackages: { entries: [] }, onlineServices: { entries: [] }, tour: [], currentLocation: null, gallery: [], contact: { phone: '', communicationNote: '', responseTime: '', paymentMethods: [], }, policies: [], about: { bio: '', personality: [], availableFor: [], availableTo: [], }, }; } export async function assembleProviderConfig( input: AssembleProviderConfigInput, ): Promise { const base = await fetchFromDataApi(input.dataApiUrl, input.providerSlug, input.dataApiToken); const shape = base ?? buildDegradedShape(input.providerSlug); const nativeProfile = await getProviderProfile(input.db, input.providerSlug); const identityOverride = nativeProfile ? nativeProfile.identity : shape.identity; const physicalOverride = nativeProfile ? nativeProfile.physical : shape.physical; const nativeTourStops = await listTourStops(input.db, { providerSlug: input.providerSlug, publicOnly: true, }); const today = input.today; const tour: ProviderTourStop[] = nativeTourStops.map((s) => { // null endDate = open-ended (ongoing): never "completed", active once started. const timeStatus: 'upcoming' | 'active' | 'completed' = today < s.startDate ? 'upcoming' : s.endDate != null && today > s.endDate ? 'completed' : 'active'; return { city: s.city, state: s.state, startDate: s.startDate, endDate: s.endDate, status: timeStatus, bookingStatus: s.status, lat: s.lat, lng: s.lng, ...(s.destinationSlug && { destinationSlug: s.destinationSlug }), ...(s.pricingTiers && { pricingTiers: { ...(s.pricingTiers.incall !== undefined && { incall: s.pricingTiers.incall }), ...(s.pricingTiers.outcall !== undefined && { outcall: s.pricingTiers.outcall }), ...(s.pricingTiers.overnight !== undefined && { overnight: s.pricingTiers.overnight }), }, }), ...(s.notes !== '' && { notes: s.notes }), }; }); const cityVisit = await getCurrentVisit(input.db, input.providerSlug); const currentLocation = cityVisit ? { city: cityVisit.city, state: cityVisit.state, country: cityVisit.country, incallAvailable: cityVisit.incallAvailable, } : null; const nativePaymentMethods = await listPaymentMethods(input.db, { providerSlug: input.providerSlug, visibility: 'public', }); const paymentMethodsOverride: import('../../../../provider-website/shared/src/types').PaymentMethod[] | null = nativePaymentMethods.length > 0 ? nativePaymentMethods.map((pm) => { const isUrlKind = pm.kind === 'gift_card' || pm.kind === 'wishlist'; const base: import('../../../../provider-website/shared/src/types').PaymentMethod = { method: pm.label, }; if (isUrlKind) { return { ...base, url: pm.value }; } return { ...base, handle: pm.value }; }) : null; const nativeRates = await assembleProviderRates(input.db, input.providerSlug); // Per-city rate selection. When the data-api base shape exposes city-tagged // bundles, serve the one for the active city — Quinn's real whereabouts // (currentLocation from city_visits) when touring, else her home incallCity, // else the null "default" card. The chosen bundle collapses into the flat // rate fields the frontend already renders, so switching is invisible to it. const activeRateCity = currentLocation?.city ?? identityOverride.incallCity ?? null; const cityBundles = shape.rateCardsByCity; const selectedCityBundle = cityBundles && cityBundles.length > 0 ? (cityBundles.find((b) => b.city === activeRateCity) ?? cityBundles.find((b) => b.city === identityOverride.incallCity) ?? cityBundles.find((b) => b.city === null) ?? cityBundles[0]) : undefined; const ratesOverride = nativeRates ?? (selectedCityBundle ? { rates: selectedCityBundle.rates, addOns: selectedCityBundle.addOns, travelFees: selectedCityBundle.travelFees, touringPackages: selectedCityBundle.touringPackages, onlineServices: selectedCityBundle.onlineServices, } : { rates: shape.rates, addOns: shape.addOns, travelFees: shape.travelFees, touringPackages: shape.touringPackages, onlineServices: shape.onlineServices, }); const nativeSiteContent = await assembleSiteContent(input.db, input.providerSlug); const aboutOverride = nativeSiteContent?.about ?? shape.about; const etiquetteOverride = nativeSiteContent ? nativeSiteContent.etiquette : shape.etiquette; const policiesOverride = nativeSiteContent ? nativeSiteContent.policies : shape.policies; const siteTextOverride = nativeSiteContent ? nativeSiteContent.siteText : shape.siteText; const nativeDestinations = await listDestinations(input.db, { providerSlug: input.providerSlug, visibility: 'public', }); const destinationsOverride = nativeDestinations.length > 0 ? nativeDestinations.map((d) => { const mapped: import('../../../../provider-website/shared/src/types').Destination = { slug: d.slug, city: d.city, country: d.country, ...(d.region !== null ? { region: d.region } : {}), fmtyTier: d.fmtyTier, metaTitle: d.metaTitle, metaDescription: d.metaDescription, headline: d.headline, intro: d.intro, ...(d.linkedTourStop ? { linkedTourStop: d.linkedTourStop } : {}), ...(d.experiences.length > 0 ? { experiences: [...d.experiences] } : {}), ...(d.note !== null ? { note: d.note } : {}), ...(d.illustrationImage !== null ? { illustrationImage: d.illustrationImage } : {}), ...(d.illustrationSide !== null ? { illustrationSide: d.illustrationSide as 'left' | 'right' } : {}), ...(d.illustrationHeight !== null ? { illustrationHeight: d.illustrationHeight } : {}), ...(d.illustrationOpacity !== null ? { illustrationOpacity: d.illustrationOpacity } : {}), ...(d.lat !== null ? { lat: d.lat } : {}), ...(d.lng !== null ? { lng: d.lng } : {}), }; return mapped; }) : shape.destinations; const nativeSpecialtiesRaw = await listSpecialtyCategories_withItems(input.db, input.providerSlug); const specialtiesOverride = nativeSpecialtiesRaw ? [...nativeSpecialtiesRaw] : shape.specialties; const nativeShopListings = await listShopListings(input.db, { providerSlug: input.providerSlug, availableOnly: false, }); const shopOverride = nativeShopListings.length > 0 ? nativeShopListings.map((s) => { const mapped: import('../../../../provider-website/shared/src/types').ShopListing = { id: s.id, slug: s.slug, title: s.title, description: s.description, price: s.price, currency: s.currency, condition: s.condition, category: s.category, ...(s.size !== null ? { size: s.size } : {}), status: s.status, photos: s.photos as import('../../../../provider-website/shared/src/types').ShopListingPhoto[], }; return mapped; }) : shape.shop; const nativeRosterContent = await listRosterContent(input.db, { providerSlug: input.providerSlug }); const rosterContentOverride = nativeRosterContent.length > 0 ? nativeRosterContent.map((r) => { const mapped: import('../../../../provider-website/shared/src/types').RosterTrackContent = { slug: r.trackSlug, name: r.name, metaTitle: r.metaTitle, metaDescription: r.metaDescription, heroLine: r.heroLine, description: [...r.description], whatToExpect: [...r.whatToExpect], interestsConfig: r.interestsConfig.map((i) => ({ value: i.value, label: i.label })), }; return mapped; }) : shape.rosterContent; const nativeLoreSections = await listLoreSections(input.db, { providerSlug: input.providerSlug }); const cultOfLilithOverride = nativeLoreSections.length > 0 ? nativeLoreSections.map((l) => { const mapped: import('../../../../provider-website/shared/src/types').CultOfLilithSection = { sectionKey: l.sectionKey, title: l.title, body: l.body, }; return mapped; }) : shape.cultOfLilith; const nativeVerifiedProfiles = await listVerifiedProfiles(input.db, { providerSlug: input.providerSlug }); const verifiedProfilesOverride = nativeVerifiedProfiles.length > 0 ? nativeVerifiedProfiles.map((v) => { const mapped: import('../../../../provider-website/shared/src/types').VerifiedProfile = { platform: v.platform, href: v.href, imgSrc: v.imgSrc, imgAlt: v.imgAlt, description: v.description, }; return mapped; }) : shape.verifiedProfiles; const nativeGalleryItems = await listGalleryItems(input.db, { providerSlug: input.providerSlug }); const galleryOverride = nativeGalleryItems.length > 0 ? nativeGalleryItems.map((g) => { const mapped: import('../../../../provider-website/shared/src/types').GalleryItem = { src: g.src, alt: g.alt, reactionKey: g.src, ...(g.category !== null ? { category: g.category } : {}), ...(g.featured ? { featured: true } : {}), ...(g.webpSrc !== null ? { webpSrc: g.webpSrc } : {}), ...(g.intrinsicWidth !== null ? { intrinsicWidth: g.intrinsicWidth } : {}), ...(g.intrinsicHeight !== null ? { intrinsicHeight: g.intrinsicHeight } : {}), ...(g.tags.length > 0 ? { tags: [...g.tags] } : {}), }; return mapped; }) : shape.gallery; const nativeHeroStripItems = await listHeroStripItems(input.db, { providerSlug: input.providerSlug }); const heroStripOverride: import('../../../../provider-website/shared/src/types').HeroStripItem[] | undefined = nativeHeroStripItems.length > 0 ? nativeHeroStripItems.map((h) => { if (h.type === 'tour_stop') { return { id: h.id, type: 'tour_stop' as const, sortOrder: h.sortOrder, city: h.city, state: h.state, startDate: h.startDate, endDate: h.endDate, bookingStatus: h.bookingStatus, ...(h.availabilityNote !== undefined ? { availabilityNote: h.availabilityNote } : {}), }; } return { id: h.id, type: 'cta' as const, sortOrder: h.sortOrder, label: h.label, ...(h.subtitle !== undefined ? { subtitle: h.subtitle } : {}), href: h.href, }; }) : shape.heroStrip; const contactOverride: import('../../../../provider-website/shared/src/types').ContactInfo = paymentMethodsOverride !== null ? { ...shape.contact, paymentMethods: paymentMethodsOverride } : shape.contact; return { ...shape, contact: contactOverride, identity: identityOverride, physical: physicalOverride, tour, currentLocation, gallery: galleryOverride ?? shape.gallery, rates: ratesOverride.rates, addOns: ratesOverride.addOns, travelFees: ratesOverride.travelFees, touringPackages: ratesOverride.touringPackages, onlineServices: ratesOverride.onlineServices, about: aboutOverride, ...(etiquetteOverride !== undefined ? { etiquette: etiquetteOverride } : {}), policies: policiesOverride, ...(siteTextOverride !== undefined ? { siteText: siteTextOverride } : {}), ...(destinationsOverride !== undefined ? { destinations: destinationsOverride } : {}), ...(specialtiesOverride !== undefined ? { specialties: specialtiesOverride } : {}), ...(shopOverride !== undefined ? { shop: shopOverride } : {}), ...(rosterContentOverride !== undefined ? { rosterContent: rosterContentOverride } : {}), ...(cultOfLilithOverride !== undefined ? { cultOfLilith: cultOfLilithOverride } : {}), ...(verifiedProfilesOverride !== undefined ? { verifiedProfiles: verifiedProfilesOverride } : {}), ...(heroStripOverride !== undefined ? { heroStrip: heroStripOverride } : {}), }; }