import type { ReportCandidateClockRange } from "./report-candidate-range"; import { chinaLocations } from "../data/china-locations.ts"; import { isBirthClockTime, parseBirthDate } from "./birth-time-intake-model.ts"; import { resolveMissingBirthTimezoneOffset } from "./birth-profile-timezone.ts"; import { effectiveBirthDate } from "./effective-birth-date.ts"; import { declaredClockRangeFromProfile, declaredRangeWrapsMidnight, } from "./declared-birth-window.ts"; import { isNatalMinuteConsultationMode, type ConsultationBirthTimeMode, } from "./consultation-birth-time-mode.ts"; import type { GeneralDailyReference } from "./general-daily-panchanga.ts"; import { resolveAyanamsa, type AyanamsaName } from "./ayanamsa.ts"; import { parseProfileGender, type ProfileGender } from "./profile-gender.ts"; import { ConsultationSubjectError, resolveConsultationSubject, type ConsultationSubjectBinding, type OwnedChartProfile, } from "./consultation-subject-resolver.ts"; export type ConsultationProfileTruthErrorCode = | "profile_unavailable" | "profile_incomplete" | "profile_inconsistent" | "mode_changed"; export class ConsultationProfileTruthError extends Error { readonly code: ConsultationProfileTruthErrorCode; constructor(code: ConsultationProfileTruthErrorCode) { super(`Consultation profile truth rejected: ${code}`); this.name = "ConsultationProfileTruthError"; this.code = code; } } export type DeclaredBirthAccuracy = Readonly<{ declaredAccuracy: "rectified" | "minute" | "15min"; timeSource: string; }>; type ServerChartToolInput = Readonly<{ year: number; month: number; day: number; hour: number; minute: number; city: string; lat: number; lon: number; tz: number; ayanamsa: AyanamsaName; declared_accuracy: DeclaredBirthAccuracy["declaredAccuracy"]; time_source: string; birth_time_accuracy?: "provisional"; candidate_range?: Readonly<{ start_time: string; end_time: string }>; unreliable_vargas?: readonly ("D1" | "D9" | "D10")[]; }>; export type ServerChartConsultation = Readonly<{ name: string; toolInput: ServerChartToolInput; truth: Readonly<{ birthDate: string; reportedBirthTime: string | null; activeBirthTime: string | null; selectedTimeKind: "reported" | "active"; birthTimeSource: string; birthTimeStatus: string; placeLabel: string; placeCodes: Readonly<{ countryCode: string | null; provinceCode: string | null; cityCode: string | null; districtCode: string | null; }>; placeId: string | null; placeType: string | null; placeProvider: string | null; timezoneId: string | null; timezoneSource: string | null; latitude: number; longitude: number; timezoneOffset: number; }>; /** * The bound subject's optional gender (self: profiles, other: that person's * chart_profiles row). Not birth truth and never sent to the engine; only the * marriage evidence card and checklist read it. */ gender?: ProfileGender | null; }>; export type DeclaredBirthWindowConsultation = Readonly<{ name: string; toolInput: Readonly<{ year: number; month: number; day: number; city: string; lat: number; lon: number; tz: number; ayanamsa: AyanamsaName; rangeStart: string; rangeEnd: string; }>; truth: Readonly<{ birthDate: string; birthTimeSource: string; birthTimePeriod: string | null; birthTimeStatus: string; wrapsMidnight: boolean; placeLabel: string; placeCodes: Readonly<{ countryCode: string | null; provinceCode: string | null; cityCode: string | null; districtCode: string | null; }>; placeId: string | null; placeType: string | null; placeProvider: string | null; timezoneId: string | null; timezoneSource: string | null; latitude: number; longitude: number; timezoneOffset: number; }>; }>; type ConsultationPreReserveContext = Readonly<{ consultationMode: ConsultationBirthTimeMode; serverChart: ServerChartConsultation | null; declaredWindow: DeclaredBirthWindowConsultation | null; generalDailyReference: GeneralDailyReference | null; }>; type PrepareConsultationRouteInput = Readonly<{ userId: string; mode: ConsultationBirthTimeMode; loadProfile: (userId: string) => Promise; loadCandidateRange?: (userId: string) => Promise; resolveTimezoneOffset?: (profile: unknown, selectedTime?: string) => Promise; beforeReserve?: (context: ConsultationPreReserveContext) => unknown | Promise; reserve: () => Promise; subject?: Readonly<{ binding: ConsultationSubjectBinding; loadOwnedChartProfile: (chartProfileId: string) => Promise; clientBirth?: unknown; }>; }>; type PrepareConsultationRouteWithGuard = Omit< PrepareConsultationRouteInput, "beforeReserve" > & Readonly<{ beforeReserve: (context: ConsultationPreReserveContext) => GuardResult | Promise; }>; export type PreparedConsultationSubject = Readonly<{ role: "self" | "other"; name: string; chartProfileId: string | null; }>; export type PreparedConsultationRoute = Readonly<{ consultationMode: ConsultationBirthTimeMode; serverChart: ServerChartConsultation | null; declaredWindow: DeclaredBirthWindowConsultation | null; generalDailyReference: GeneralDailyReference | null; reservation: Reservation; preReserveResult: GuardResult; subject: PreparedConsultationSubject | null; }>; type RecordValue = Record; const allowedBirthTimeSources = new Set([ "hospital_record", "family_exact", "approximate", "period_only", "unknown", "legacy_import", ]); const allowedBirthTimeStatuses = new Set([ "reported", "assessing", "rectifying", "candidate", "accepted", "confirmed", ]); const concreteReportedSources = new Set([ "hospital_record", "family_exact", "approximate", ]); /** * Map persisted profile truth onto the Python rectification-gate vocabulary. * `rectified` is reserved for an accepted or confirmed active birth time. */ export function declaredBirthAccuracyFromProfile(input: { birthTimeStatus: string; birthTimeSource: string; hasActiveBirthTime: boolean; }): DeclaredBirthAccuracy { if ( (input.birthTimeStatus === "accepted" || input.birthTimeStatus === "confirmed") && input.hasActiveBirthTime ) { return { declaredAccuracy: "rectified", timeSource: "rectified" }; } if (input.birthTimeSource === "hospital_record") { return { declaredAccuracy: "minute", timeSource: "hospital" }; } if (input.birthTimeSource === "family_exact") { return { declaredAccuracy: "minute", timeSource: "family_clear" }; } return { declaredAccuracy: "15min", timeSource: "family_vague" }; } function record(value: unknown): RecordValue | null { return value !== null && typeof value === "object" && !Array.isArray(value) ? value as RecordValue : null; } function requiredText(profile: RecordValue, key: string): string { const value = profile[key]; if (typeof value !== "string" || !value.trim()) { throw new ConsultationProfileTruthError("profile_incomplete"); } return value.trim(); } function nullableClock(profile: RecordValue, key: string): string | null { const value = profile[key]; if (value === null || value === undefined) return null; if (typeof value !== "string") { throw new ConsultationProfileTruthError("profile_inconsistent"); } const clock = value.slice(0, 5); if (!isBirthClockTime(clock)) { throw new ConsultationProfileTruthError("profile_inconsistent"); } return clock; } function requiredFiniteNumber(profile: RecordValue, key: string, minimum: number, maximum: number) { const value = profile[key]; if (typeof value !== "number" || !Number.isFinite(value)) { throw new ConsultationProfileTruthError("profile_incomplete"); } if (value < minimum || value > maximum) { throw new ConsultationProfileTruthError("profile_inconsistent"); } return value; } function optionalText(profile: RecordValue, key: string): string | null { const value = profile[key]; return typeof value === "string" && value.trim() ? value.trim() : null; } function generalDailyReferenceFromProfile(value: unknown): GeneralDailyReference | null { const profile = record(value); if (!profile) return null; const latitude = profile.latitude; const longitude = profile.longitude; const timezoneOffset = profile.timezone_offset; if (typeof latitude !== "number" || !Number.isFinite(latitude) || latitude < -90 || latitude > 90 || typeof longitude !== "number" || !Number.isFinite(longitude) || longitude < -180 || longitude > 180 || typeof timezoneOffset !== "number" || !Number.isFinite(timezoneOffset) || timezoneOffset < -12 || timezoneOffset > 14) { return null; } return Object.freeze({ latitude, longitude, timezoneOffset, placeLabel: optionalText(profile, "birth_place_label") ?? "已保存地点", }); } function persistedConsultationMode(value: unknown): Exclude | null { const profile = record(value); if (!profile) return null; const status = optionalText(profile, "birth_time_status"); const source = optionalText(profile, "birth_time_source"); const activeTime = optionalText(profile, "active_birth_time")?.slice(0, 5) ?? ""; const reportedTime = optionalText(profile, "reported_birth_time")?.slice(0, 5) ?? ""; if ((status === "accepted" || status === "confirmed") && isBirthClockTime(activeTime)) return "verified_chart"; if (status && allowedBirthTimeStatuses.has(status) && status !== "accepted" && status !== "confirmed" && source && concreteReportedSources.has(source) && isBirthClockTime(reportedTime)) return "unverified_birth_time"; if (status && allowedBirthTimeStatuses.has(status) && status !== "accepted" && status !== "confirmed" && source && (source === "period_only" || source === "unknown" || source === "legacy_import") && !isBirthClockTime(reportedTime)) return "declared_birth_window"; return null; } function legacyChinaPlaceLabel(profile: RecordValue): string | null { const countryCode = optionalText(profile, "country_code"); const provinceCode = optionalText(profile, "province_code"); const cityCode = optionalText(profile, "city_code"); const districtCode = optionalText(profile, "district_code"); const country = chinaLocations.country; if (countryCode !== country.code || !provinceCode || !cityCode) return null; const province = country.provinces.find((candidate) => candidate.code === provinceCode); const city = province?.cities.find((candidate) => candidate.code === cityCode); const district = districtCode ? city?.districts.find((candidate) => candidate.code === districtCode) : undefined; if (!province || !city || (districtCode && !district)) return null; return [country.name, province.name, city.name, district?.name] .filter((label, index, labels) => Boolean(label) && labels.indexOf(label) === index) .join(" · "); } function serverChartFromProfile( value: unknown, mode: Extract, ): ServerChartConsultation { const profile = record(value); if (!profile) throw new ConsultationProfileTruthError("profile_incomplete"); const name = requiredText(profile, "name"); if (name.length > 80) throw new ConsultationProfileTruthError("profile_inconsistent"); const birthDate = effectiveBirthDate(profile, mode === "verified_chart") ?? requiredText(profile, "birth_date"); if (!parseBirthDate(birthDate)) { throw new ConsultationProfileTruthError("profile_inconsistent"); } const [year, month, day] = birthDate.split("-").map(Number); const reportedBirthTime = nullableClock(profile, "reported_birth_time"); const activeBirthTime = nullableClock(profile, "active_birth_time"); const birthTimeSource = requiredText(profile, "birth_time_source"); const birthTimeStatus = requiredText(profile, "birth_time_status"); if (!allowedBirthTimeSources.has(birthTimeSource) || !allowedBirthTimeStatuses.has(birthTimeStatus)) { throw new ConsultationProfileTruthError("profile_inconsistent"); } const countryCode = optionalText(profile, "country_code"); const provinceCode = optionalText(profile, "province_code"); const cityCode = optionalText(profile, "city_code"); const districtCode = optionalText(profile, "district_code"); const placeId = optionalText(profile, "birth_place_provider_id"); const placeType = optionalText(profile, "birth_place_type"); const placeProvider = optionalText(profile, "birth_place_provider"); const timezoneId = optionalText(profile, "timezone_id"); const timezoneSource = optionalText(profile, "timezone_source"); const latitude = requiredFiniteNumber(profile, "latitude", -90, 90); const longitude = requiredFiniteNumber(profile, "longitude", -180, 180); const timezoneOffset = requiredFiniteNumber(profile, "timezone_offset", -12, 14); const placeLabel = optionalText(profile, "birth_place_label") ?? legacyChinaPlaceLabel(profile) ?? placeId; if (!placeLabel) throw new ConsultationProfileTruthError("profile_incomplete"); let selectedTime: string; let selectedTimeKind: "reported" | "active"; if (mode === "verified_chart") { if (birthTimeStatus !== "accepted" && birthTimeStatus !== "confirmed") { throw new ConsultationProfileTruthError("mode_changed"); } if (!activeBirthTime) throw new ConsultationProfileTruthError("profile_incomplete"); selectedTime = activeBirthTime; selectedTimeKind = "active"; } else { if (birthTimeStatus === "accepted" || birthTimeStatus === "confirmed" || !concreteReportedSources.has(birthTimeSource)) { throw new ConsultationProfileTruthError("mode_changed"); } if (!reportedBirthTime) throw new ConsultationProfileTruthError("profile_incomplete"); selectedTime = reportedBirthTime; selectedTimeKind = "reported"; } const [hour, minute] = selectedTime.split(":").map(Number); const accuracy = declaredBirthAccuracyFromProfile({ birthTimeStatus, birthTimeSource, hasActiveBirthTime: Boolean(activeBirthTime), }); return Object.freeze({ name, gender: parseProfileGender(profile.gender), toolInput: Object.freeze({ year, month, day, hour, minute, city: placeLabel, lat: latitude, lon: longitude, tz: timezoneOffset, ayanamsa: resolveAyanamsa(profile), declared_accuracy: accuracy.declaredAccuracy, time_source: accuracy.timeSource, }), truth: Object.freeze({ birthDate, reportedBirthTime, activeBirthTime, selectedTimeKind, birthTimeSource, birthTimeStatus, placeLabel, placeCodes: Object.freeze({ countryCode, provinceCode, cityCode, districtCode, }), placeId, placeType, placeProvider, timezoneId, timezoneSource, latitude, longitude, timezoneOffset, }), }); } function declaredWindowFromProfile(value: unknown): DeclaredBirthWindowConsultation { const profile = record(value); if (!profile) throw new ConsultationProfileTruthError("profile_incomplete"); const name = requiredText(profile, "name"); if (name.length > 80) throw new ConsultationProfileTruthError("profile_inconsistent"); const birthDate = requiredText(profile, "birth_date"); if (!parseBirthDate(birthDate)) { throw new ConsultationProfileTruthError("profile_inconsistent"); } const [year, month, day] = birthDate.split("-").map(Number); const birthTimeSource = requiredText(profile, "birth_time_source"); const birthTimeStatus = requiredText(profile, "birth_time_status"); if (!allowedBirthTimeSources.has(birthTimeSource) || !allowedBirthTimeStatuses.has(birthTimeStatus)) { throw new ConsultationProfileTruthError("profile_inconsistent"); } if (birthTimeStatus === "accepted" || birthTimeStatus === "confirmed") { throw new ConsultationProfileTruthError("mode_changed"); } if (concreteReportedSources.has(birthTimeSource) && nullableClock(profile, "reported_birth_time")) { throw new ConsultationProfileTruthError("mode_changed"); } const birthTimePeriod = optionalText(profile, "birth_time_period"); const range = declaredClockRangeFromProfile(profile); if (!range) throw new ConsultationProfileTruthError("profile_incomplete"); const countryCode = optionalText(profile, "country_code"); const provinceCode = optionalText(profile, "province_code"); const cityCode = optionalText(profile, "city_code"); const districtCode = optionalText(profile, "district_code"); const placeId = optionalText(profile, "birth_place_provider_id"); const placeType = optionalText(profile, "birth_place_type"); const placeProvider = optionalText(profile, "birth_place_provider"); const timezoneId = optionalText(profile, "timezone_id"); const timezoneSource = optionalText(profile, "timezone_source"); const latitude = requiredFiniteNumber(profile, "latitude", -90, 90); const longitude = requiredFiniteNumber(profile, "longitude", -180, 180); const timezoneOffset = requiredFiniteNumber(profile, "timezone_offset", -12, 14); const placeLabel = optionalText(profile, "birth_place_label") ?? legacyChinaPlaceLabel(profile) ?? placeId; if (!placeLabel) throw new ConsultationProfileTruthError("profile_incomplete"); return Object.freeze({ name, toolInput: Object.freeze({ year, month, day, city: placeLabel, lat: latitude, lon: longitude, tz: timezoneOffset, ayanamsa: resolveAyanamsa(profile), rangeStart: range.startTime, rangeEnd: range.endTime, }), truth: Object.freeze({ birthDate, birthTimeSource, birthTimePeriod, birthTimeStatus, wrapsMidnight: declaredRangeWrapsMidnight(range), placeLabel, placeCodes: Object.freeze({ countryCode, provinceCode, cityCode, districtCode, }), placeId, placeType, placeProvider, timezoneId, timezoneSource, latitude, longitude, timezoneOffset, }), }); } function reconcileConsultationMode( requested: ConsultationBirthTimeMode, profile: unknown, ): ConsultationBirthTimeMode { const persisted = persistedConsultationMode(profile); if (requested === "general_no_birth_time") return persisted ?? requested; if (requested === "declared_birth_window") { if (persisted === "verified_chart" || persisted === "unverified_birth_time") return persisted; return "declared_birth_window"; } return requested; } /** * The route's pre-billing service boundary. Chart modes must load and resolve * account truth successfully before the reservation callback can run. */ export function prepareConsultationRoute( input: PrepareConsultationRouteWithGuard, ): Promise>>; export function prepareConsultationRoute( input: PrepareConsultationRouteInput, ): Promise>; export async function prepareConsultationRoute( input: PrepareConsultationRouteInput | PrepareConsultationRouteWithGuard, ): Promise | undefined>> { let profile: unknown; let subjectRole: "self" | "other" = "self"; let subject: PreparedConsultationSubject | null = null; try { if (input.subject) { const resolved = await resolveConsultationSubject({ userId: input.userId, binding: input.subject.binding, loadSelfProfile: input.loadProfile, loadOwnedChartProfile: input.subject.loadOwnedChartProfile, clientBirth: input.subject.clientBirth, }); subjectRole = resolved.role; subject = { role: resolved.role, name: resolved.name, chartProfileId: resolved.chartProfileId, }; if (resolved.profile == null) throw new ConsultationProfileTruthError("profile_unavailable"); profile = resolved.profile; } else { profile = await input.loadProfile(input.userId); } } catch (error) { if (error instanceof ConsultationSubjectError) throw error; if (input.mode === "general_no_birth_time" && subjectRole !== "other") profile = null; else if (error instanceof ConsultationProfileTruthError) throw error; else throw new ConsultationProfileTruthError("profile_unavailable"); } const consultationMode = reconcileConsultationMode(input.mode, profile); let serverChart: ServerChartConsultation | null = null; let declaredWindow: DeclaredBirthWindowConsultation | null = null; const generalDailyReference = consultationMode === "general_no_birth_time" || consultationMode === "declared_birth_window" ? generalDailyReferenceFromProfile(profile) : null; if (isNatalMinuteConsultationMode(consultationMode)) { const profileValue = record(profile); const selectedTime = consultationMode === "verified_chart" ? nullableClock(profileValue ?? {}, "active_birth_time") : nullableClock(profileValue ?? {}, "reported_birth_time"); try { profile = await (input.resolveTimezoneOffset ?? ((value, time) => ( resolveMissingBirthTimezoneOffset(value, { preferredTime: time, useActiveDate: consultationMode === "verified_chart" }) )))(profile, selectedTime ?? undefined); } catch { throw new ConsultationProfileTruthError("profile_unavailable"); } serverChart = serverChartFromProfile(profile, consultationMode); if ( consultationMode === "verified_chart" && serverChart.truth.birthTimeStatus === "accepted" && subjectRole === "self" && input.loadCandidateRange ) { try { const range = await input.loadCandidateRange(input.userId); if (range) { serverChart = Object.freeze({ ...serverChart, toolInput: Object.freeze({ ...serverChart.toolInput, birth_time_accuracy: "provisional" as const, candidate_range: Object.freeze({ start_time: range.startTime, end_time: range.endTime, ...(range.candidate_intervals ? { candidate_intervals: range.candidate_intervals } : {}), }), ...(range.unreliableVargas?.length ? { unreliable_vargas: [...range.unreliableVargas] } : {}), }), }); } } catch { // Fail closed: accepted charts still consult the representative minute. } } } else if (consultationMode === "declared_birth_window") { const range = declaredClockRangeFromProfile(record(profile) ?? {}); try { profile = await (input.resolveTimezoneOffset ?? ((value, time) => ( resolveMissingBirthTimezoneOffset(value, { preferredTime: time }) )))(profile, range?.startTime); } catch { throw new ConsultationProfileTruthError("profile_unavailable"); } declaredWindow = declaredWindowFromProfile(profile); } const preReserveResult = input.beforeReserve ? await input.beforeReserve({ consultationMode, serverChart, declaredWindow, generalDailyReference, }) as Awaited : undefined; const reservation = await input.reserve(); return Object.freeze({ consultationMode, serverChart, declaredWindow, generalDailyReference, reservation, preReserveResult, subject, }); }