lilith-platform.live/codebase/@features/api/src/features/provider-config/service.ts
Natalie bcd2d96a1f feat(quinn-admin): move default theme selector from hardcoded quinn.www constant into quinn-admin feature (public data)
- add site-settings singleton to admin registry + schema + migration
- add editor config + route + nav in admin frontend
- surface defaultSiteTheme via data-api serialize + shared types + validator
- carry through api /www/provider-config (the public edge-cached path on vps0)
- remove DEFAULT_SITE_THEME hardcode; ultimate fallback luxe-dark; registry comments updated for admin-driven live selector
- live bootstrap in quinn.www root + data hook to pick admin default without quinn.www rebuild (chrome + tokens update post-fetch)
- fixed incidental sortable test assertion to match current registry (pre-existing mismatch)
- other public hardcodes remain in deployment configs; see analysis

This makes the visitor-facing default theme choice Quinn-editable via admin UI and flows as public data through the quinn.api public surface (edge cacheable).
2026-06-23 04:34:35 -04:00

410 lines
15 KiB
TypeScript

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<string, CacheEntry>();
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<ProviderData | null> {
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<string, string> = {};
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: [],
},
// defaultSiteTheme omitted — falls back to deployment DEFAULT when absent
};
}
export async function assembleProviderConfig(
input: AssembleProviderConfigInput,
): Promise<ProviderData> {
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 } : {}),
};
}