626 lines
23 KiB
TypeScript
626 lines
23 KiB
TypeScript
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 {
|
|
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 }>;
|
|
}>;
|
|
|
|
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;
|
|
}>;
|
|
}>;
|
|
|
|
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<Reservation> = Readonly<{
|
|
userId: string;
|
|
mode: ConsultationBirthTimeMode;
|
|
loadProfile: (userId: string) => Promise<unknown>;
|
|
loadCandidateRange?: (userId: string) => Promise<ReportCandidateClockRange | null>;
|
|
resolveTimezoneOffset?: (profile: unknown, selectedTime?: string) => Promise<unknown>;
|
|
beforeReserve?: (context: ConsultationPreReserveContext) => unknown | Promise<unknown>;
|
|
reserve: () => Promise<Reservation>;
|
|
subject?: Readonly<{
|
|
binding: ConsultationSubjectBinding;
|
|
loadOwnedChartProfile: (chartProfileId: string) => Promise<OwnedChartProfile | null>;
|
|
clientBirth?: unknown;
|
|
}>;
|
|
}>;
|
|
|
|
type PrepareConsultationRouteWithGuard<Reservation, GuardResult> = Omit<
|
|
PrepareConsultationRouteInput<Reservation>,
|
|
"beforeReserve"
|
|
> & Readonly<{
|
|
beforeReserve: (context: ConsultationPreReserveContext) => GuardResult | Promise<GuardResult>;
|
|
}>;
|
|
|
|
export type PreparedConsultationSubject = Readonly<{
|
|
role: "self" | "other";
|
|
name: string;
|
|
chartProfileId: string | null;
|
|
}>;
|
|
|
|
export type PreparedConsultationRoute<Reservation, GuardResult = undefined> = Readonly<{
|
|
consultationMode: ConsultationBirthTimeMode;
|
|
serverChart: ServerChartConsultation | null;
|
|
declaredWindow: DeclaredBirthWindowConsultation | null;
|
|
generalDailyReference: GeneralDailyReference | null;
|
|
reservation: Reservation;
|
|
preReserveResult: GuardResult;
|
|
subject: PreparedConsultationSubject | null;
|
|
}>;
|
|
|
|
type RecordValue = Record<string, unknown>;
|
|
|
|
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<ConsultationBirthTimeMode, "general_no_birth_time"> | 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<ConsultationBirthTimeMode, "verified_chart" | "unverified_birth_time">,
|
|
): 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,
|
|
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<Reservation, GuardResult>(
|
|
input: PrepareConsultationRouteWithGuard<Reservation, GuardResult>,
|
|
): Promise<PreparedConsultationRoute<Reservation, Awaited<GuardResult>>>;
|
|
export function prepareConsultationRoute<Reservation>(
|
|
input: PrepareConsultationRouteInput<Reservation>,
|
|
): Promise<PreparedConsultationRoute<Reservation>>;
|
|
export async function prepareConsultationRoute<Reservation, GuardResult>(
|
|
input: PrepareConsultationRouteInput<Reservation> | PrepareConsultationRouteWithGuard<Reservation, GuardResult>,
|
|
): Promise<PreparedConsultationRoute<Reservation, Awaited<GuardResult> | 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 } : {}),
|
|
}),
|
|
}),
|
|
});
|
|
}
|
|
} 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<GuardResult>
|
|
: undefined;
|
|
const reservation = await input.reserve();
|
|
return Object.freeze({
|
|
consultationMode,
|
|
serverChart,
|
|
declaredWindow,
|
|
generalDailyReference,
|
|
reservation,
|
|
preReserveResult,
|
|
subject,
|
|
});
|
|
}
|