fix(web): persist uncertain birth time as a start/end clock window

Users pick a clock range instead of a coarse period plus notes, so
rectification and window consult scan that range instead of a leftover afternoon bucket.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-22 20:48:01 +08:00
co-authored by Cursor
parent f3197ce3f6
commit 8ed6118b9a
31 changed files with 760 additions and 144 deletions
+93 -29
View File
@@ -49,26 +49,43 @@ export async function GET() {
const admin = createAdminSupabaseClient();
let { data: profile, error: profileError } = await supabase
.from("profiles")
.select("credits,active_birth_time,birth_time_status,birth_date,reported_birth_time,birth_time_source,birth_time_period,birth_time_clue,uncertainty_before_minutes,uncertainty_after_minutes,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,birth_place_label,birth_place_type,birth_place_provider,birth_place_provider_id,timezone_id,timezone_source,name,birth_time,rectification_case_id")
.select("credits,active_birth_time,birth_time_status,birth_date,reported_birth_time,birth_time_source,birth_time_period,declared_window_start,declared_window_end,birth_time_clue,uncertainty_before_minutes,uncertainty_after_minutes,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,birth_place_label,birth_place_type,birth_place_provider,birth_place_provider_id,timezone_id,timezone_source,name,birth_time,rectification_case_id")
.eq("id", userId)
.single();
if (profileError && isMissingProfileColumn(profileError)) {
const fallback = await supabase
const withoutDeclaredWindow = await supabase
.from("profiles")
.select("credits,active_birth_time,birth_time_status,birth_date,reported_birth_time,birth_time_source,birth_time_period,birth_time_clue,uncertainty_before_minutes,uncertainty_after_minutes,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,name,birth_time,rectification_case_id")
.select("credits,active_birth_time,birth_time_status,birth_date,reported_birth_time,birth_time_source,birth_time_period,birth_time_clue,uncertainty_before_minutes,uncertainty_after_minutes,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,birth_place_label,birth_place_type,birth_place_provider,birth_place_provider_id,timezone_id,timezone_source,name,birth_time,rectification_case_id")
.eq("id", userId)
.single();
profile = fallback.data ? {
...fallback.data,
birth_place_label: undefined,
birth_place_type: undefined,
birth_place_provider: undefined,
birth_place_provider_id: undefined,
timezone_id: undefined,
timezone_source: undefined,
} : null;
profileError = fallback.error;
if (!withoutDeclaredWindow.error && withoutDeclaredWindow.data) {
profile = {
...withoutDeclaredWindow.data,
declared_window_start: undefined,
declared_window_end: undefined,
};
profileError = withoutDeclaredWindow.error;
} else if (withoutDeclaredWindow.error && isMissingProfileColumn(withoutDeclaredWindow.error)) {
const fallback = await supabase
.from("profiles")
.select("credits,active_birth_time,birth_time_status,birth_date,reported_birth_time,birth_time_source,birth_time_period,birth_time_clue,uncertainty_before_minutes,uncertainty_after_minutes,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,name,birth_time,rectification_case_id")
.eq("id", userId)
.single();
profile = fallback.data ? {
...fallback.data,
birth_place_label: undefined,
birth_place_type: undefined,
birth_place_provider: undefined,
birth_place_provider_id: undefined,
timezone_id: undefined,
timezone_source: undefined,
} : null;
profileError = fallback.error;
} else {
profile = withoutDeclaredWindow.data;
profileError = withoutDeclaredWindow.error;
}
}
if (profileError || !profile) {
@@ -170,28 +187,45 @@ export async function PATCH(request: Request) {
const admin = createAdminSupabaseClient();
let { data: currentProfile, error: currentProfileError } = await admin
.from("profiles")
.select("birth_date,birth_time,reported_birth_time,active_birth_time,birth_time_source,birth_time_period,birth_time_clue,uncertainty_before_minutes,uncertainty_after_minutes,birth_time_status,rectification_case_id,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,birth_place_label,birth_place_type,birth_place_provider,birth_place_provider_id,timezone_id,timezone_source")
.select("birth_date,birth_time,reported_birth_time,active_birth_time,birth_time_source,birth_time_period,declared_window_start,declared_window_end,birth_time_clue,uncertainty_before_minutes,uncertainty_after_minutes,birth_time_status,rectification_case_id,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,birth_place_label,birth_place_type,birth_place_provider,birth_place_provider_id,timezone_id,timezone_source")
.eq("id", userId)
.maybeSingle();
if (currentProfileError && isMissingProfileColumn(currentProfileError)) {
const fallback = await admin
const withoutDeclaredWindow = await admin
.from("profiles")
.select("birth_date,birth_time,reported_birth_time,active_birth_time,birth_time_source,birth_time_period,birth_time_clue,uncertainty_before_minutes,uncertainty_after_minutes,birth_time_status,rectification_case_id,country_code,province_code,city_code,district_code")
.select("birth_date,birth_time,reported_birth_time,active_birth_time,birth_time_source,birth_time_period,birth_time_clue,uncertainty_before_minutes,uncertainty_after_minutes,birth_time_status,rectification_case_id,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,birth_place_label,birth_place_type,birth_place_provider,birth_place_provider_id,timezone_id,timezone_source")
.eq("id", userId)
.maybeSingle();
currentProfile = fallback.data ? {
...fallback.data,
latitude: undefined,
longitude: undefined,
timezone_offset: undefined,
birth_place_label: undefined,
birth_place_type: undefined,
birth_place_provider: undefined,
birth_place_provider_id: undefined,
timezone_id: undefined,
timezone_source: undefined,
} : null;
currentProfileError = fallback.error;
if (!withoutDeclaredWindow.error && withoutDeclaredWindow.data) {
currentProfile = {
...withoutDeclaredWindow.data,
declared_window_start: undefined,
declared_window_end: undefined,
};
currentProfileError = withoutDeclaredWindow.error;
} else if (withoutDeclaredWindow.error && isMissingProfileColumn(withoutDeclaredWindow.error)) {
const fallback = await admin
.from("profiles")
.select("birth_date,birth_time,reported_birth_time,active_birth_time,birth_time_source,birth_time_period,birth_time_clue,uncertainty_before_minutes,uncertainty_after_minutes,birth_time_status,rectification_case_id,country_code,province_code,city_code,district_code")
.eq("id", userId)
.maybeSingle();
currentProfile = fallback.data ? {
...fallback.data,
latitude: undefined,
longitude: undefined,
timezone_offset: undefined,
birth_place_label: undefined,
birth_place_type: undefined,
birth_place_provider: undefined,
birth_place_provider_id: undefined,
timezone_id: undefined,
timezone_source: undefined,
} : null;
currentProfileError = fallback.error;
} else {
currentProfile = withoutDeclaredWindow.data;
currentProfileError = withoutDeclaredWindow.error;
}
}
if (currentProfileError) {
return NextResponse.json({ error: "暂时无法核对现有出生资料" }, { status: 500 });
@@ -210,6 +244,12 @@ export async function PATCH(request: Request) {
...(payload.birth_time_period !== undefined
? { birth_time_period: payload.birth_time_period }
: {}),
...(payload.declared_window_start !== undefined
? { declared_window_start: payload.declared_window_start }
: {}),
...(payload.declared_window_end !== undefined
? { declared_window_end: payload.declared_window_end }
: {}),
...(payload.birth_time_clue !== undefined
? { birth_time_clue: payload.birth_time_clue }
: {}),
@@ -261,6 +301,30 @@ export async function PATCH(request: Request) {
data = fallback.data;
error = fallback.error;
}
if (error && isMissingProfileColumn(error)) {
const {
declared_window_start: _start,
declared_window_end: _end,
...withoutDeclaredWindow
} = withCoordinates;
void _start;
void _end;
const fallback = await writeProfile(withoutDeclaredWindow);
data = fallback.data;
error = fallback.error;
}
if (error && isMissingProfileColumn(error)) {
const {
declared_window_start: _start,
declared_window_end: _end,
...withoutCoordinatesOrWindow
} = withoutCoordinates;
void _start;
void _end;
const fallback = await writeProfile(withoutCoordinatesOrWindow);
data = fallback.data;
error = fallback.error;
}
if (!error && !data && invalidatesUnconfirmedApplication) {
return NextResponse.json({
+1 -1
View File
@@ -341,7 +341,7 @@ export async function POST(request: Request) {
async loadProfile(profileUserId) {
const { data, error } = await supabase
.from("profiles")
.select("name,birth_date,reported_birth_time,active_birth_time,birth_time_source,birth_time_period,birth_time_status,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,birth_place_label,birth_place_type,birth_place_provider,birth_place_provider_id,timezone_id,timezone_source")
.select("name,birth_date,reported_birth_time,active_birth_time,birth_time_source,birth_time_period,declared_window_start,declared_window_end,birth_time_status,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,birth_place_label,birth_place_type,birth_place_provider,birth_place_provider_id,timezone_id,timezone_source")
.eq("id", profileUserId)
.single();
if (error || !data) throw new ConsultationProfileTruthError("profile_unavailable");
+1 -1
View File
@@ -20,7 +20,7 @@ function createProfileRepository(
async loadProfile(userId) {
return admin
.from("profiles")
.select("id,name,birth_date,birth_time,reported_birth_time,active_birth_time,birth_time_source,birth_time_period,birth_time_clue,uncertainty_before_minutes,uncertainty_after_minutes,birth_time_status,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,birth_place_label,birth_place_type,birth_place_provider,birth_place_provider_id,timezone_id,timezone_source,onboarding_payload,onboarding_version,onboarding_generated_at")
.select("id,name,birth_date,birth_time,reported_birth_time,active_birth_time,birth_time_source,birth_time_period,declared_window_start,declared_window_end,birth_time_clue,uncertainty_before_minutes,uncertainty_after_minutes,birth_time_status,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset,birth_place_label,birth_place_type,birth_place_provider,birth_place_provider_id,timezone_id,timezone_source,onboarding_payload,onboarding_version,onboarding_generated_at")
.eq("id", userId)
.maybeSingle();
},
+12 -5
View File
@@ -2050,8 +2050,17 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
.birth-time-source-option b { font-size: var(--type-body-sm); line-height: 1.35; }
.birth-time-source-option small { font-size: var(--type-caption); line-height: 1.5; }
.birth-time-detail-grid { grid-template-columns: minmax(0, .9fr) minmax(0, 1.1fr); gap: var(--space-3); padding: var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-canvas-soft); }
.birth-time-period-details { align-items: start; }
.birth-time-period-details > label { align-self: start; }
.birth-time-window-details { align-items: start; }
.birth-time-window-details > label { align-self: start; }
.birth-time-window-intro {
grid-column: 1 / -1;
margin: 0;
color: var(--color-ink-secondary);
font-size: var(--type-caption);
line-height: 1.5;
}
.birth-time-window-details .birth-time-skip-button { grid-column: 1 / -1; justify-self: start; min-height: 40px; padding-inline: 13px; border-color: transparent; background: transparent; color: var(--color-action); font-size: var(--type-caption); }
.birth-time-window-details .birth-time-skip-button:hover { background: color-mix(in srgb, var(--color-action-soft) 72%, transparent); }
.birth-time-detail-grid > label { gap: 7px; }
.birth-time-detail-grid > label > span { color: var(--color-ink-secondary); font-size: var(--type-caption); font-weight: 700; }
.birth-time-detail-grid textarea {
@@ -2081,8 +2090,6 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
.birth-time-detail-grid textarea::-webkit-resizer {
background: color-mix(in srgb, var(--color-border-strong) 70%, transparent);
}
.birth-time-period-details .birth-time-skip-button { grid-column: 1 / -1; justify-self: start; min-height: 40px; padding-inline: 13px; border-color: transparent; background: transparent; color: var(--color-action); font-size: var(--type-caption); }
.birth-time-period-details .birth-time-skip-button:hover { background: color-mix(in srgb, var(--color-action-soft) 72%, transparent); }
.birth-time-detail-note[role="status"] { display: grid; gap: var(--space-3); padding: var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas-soft); }
.birth-time-detail-note[role="status"] p { margin: 0; }
.onboarding-card-actions { align-items: center; justify-content: space-between; gap: var(--space-4); padding-top: var(--space-2); }
@@ -2091,7 +2098,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
@media (max-width: 620px) {
.onboarding-card-heading { align-items: flex-start; flex-direction: column; gap: var(--space-2); }
.birth-time-source-list, .birth-time-detail-grid { grid-template-columns: 1fr; }
.birth-time-period-details .birth-time-skip-button { grid-column: auto; justify-self: stretch; }
.birth-time-window-details .birth-time-skip-button { grid-column: auto; justify-self: stretch; }
.onboarding-card-actions { align-items: stretch; flex-direction: column; }
.onboarding-card-actions .button-primary { width: 100%; }
}
+19 -3
View File
@@ -61,6 +61,7 @@ import {
birthTimePersistenceValues,
declaredBirthInputChanged,
describeBirthTimeDraft,
hydrateDeclaredWindowDraft,
isDeclaredBirthProfileComplete,
isBirthTimeDraftReady,
birthTimeDraftReadyHint,
@@ -358,6 +359,8 @@ const emptyProfile: Profile = {
reportedTime: "",
birthTimeSource: "",
birthTimePeriod: "",
declaredWindowStart: "",
declaredWindowEnd: "",
birthTimeClue: "",
uncertaintyBeforeMinutes: null,
uncertaintyAfterMinutes: null,
@@ -659,6 +662,8 @@ function readProfile(value: unknown): Profile {
active_birth_time?: unknown;
birth_time_source?: unknown;
birth_time_period?: unknown;
declared_window_start?: unknown;
declared_window_end?: unknown;
birth_time_clue?: unknown;
uncertainty_before_minutes?: unknown;
uncertainty_after_minutes?: unknown;
@@ -696,6 +701,13 @@ function readProfile(value: unknown): Profile {
const reportedTime = persistedReportedTime || (source === "legacy_import" ? time : "");
const knownPeriods = ["early_morning", "morning", "afternoon", "evening", "late_night"] as const;
const period = knownPeriods.find((item) => item === profile.birth_time_period) ?? "";
const windowStart = typeof profile.declared_window_start === "string" ? profile.declared_window_start.slice(0, 5) : "";
const windowEnd = typeof profile.declared_window_end === "string" ? profile.declared_window_end.slice(0, 5) : "";
const declaredWindow = hydrateDeclaredWindowDraft({
period,
start: windowStart,
end: windowEnd,
});
const knownStatuses = ["reported", "assessing", "rectifying", "candidate", "accepted", "confirmed"] as const;
const status = knownStatuses.find((item) => item === profile.birth_time_status)
?? (time ? "confirmed" : "");
@@ -723,8 +735,10 @@ function readProfile(value: unknown): Profile {
time: typeof time === "string" ? time : "",
reportedTime: typeof reportedTime === "string" ? reportedTime : "",
birthTimeSource: source,
birthTimePeriod: period,
birthTimeClue: typeof profile.birth_time_clue === "string" ? profile.birth_time_clue.slice(0, 240) : "",
birthTimePeriod: declaredWindow.birthTimePeriod,
declaredWindowStart: declaredWindow.declaredWindowStart,
declaredWindowEnd: declaredWindow.declaredWindowEnd,
birthTimeClue: "",
uncertaintyBeforeMinutes: typeof profile.uncertainty_before_minutes === "number" ? profile.uncertainty_before_minutes : null,
uncertaintyAfterMinutes: typeof profile.uncertainty_after_minutes === "number" ? profile.uncertainty_after_minutes : null,
birthTimeStatus: status,
@@ -1475,7 +1489,9 @@ export default function Home() {
time: isCompletedCandidatePreview ? "04:53" : isRectificationPreview || isAssessmentLoadingPreview ? "" : "12:30",
reportedTime: isRectificationPreview ? "14:30" : isAssessmentLoadingPreview || isCompletedCandidatePreview ? "" : "12:30",
birthTimeSource: isRectificationPreview ? "approximate" : isAssessmentLoadingPreview || isCompletedCandidatePreview ? "period_only" : "legacy_import",
birthTimePeriod: isAssessmentLoadingPreview || isCompletedCandidatePreview ? "early_morning" : "",
...hydrateDeclaredWindowDraft({
period: isAssessmentLoadingPreview || isCompletedCandidatePreview ? "early_morning" : "",
}),
birthTimeClue: "",
uncertaintyBeforeMinutes: isRectificationPreview ? 30 : null,
uncertaintyAfterMinutes: isRectificationPreview ? 30 : null,
+35 -33
View File
@@ -6,7 +6,6 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { birthTimeConsultationOptionsCopy } from "@/lib/birth-time-consultation-consent";
import {
birthTimeDisplayState,
birthTimePeriodOptions,
birthTimeSourceDefaults,
birthTimeSourceOptions,
type BirthTimeDraft,
@@ -30,9 +29,18 @@ function splitBirthTime(value: string) {
type BirthClockSelectProps = {
readonly value: string;
readonly onChange: (value: string) => void;
readonly ariaLabel?: string;
readonly hourLabel?: string;
readonly minuteLabel?: string;
};
function BirthClockSelect({ value, onChange }: BirthClockSelectProps) {
function BirthClockSelect({
value,
onChange,
ariaLabel = "选择出生时间",
hourLabel = "出生时间小时",
minuteLabel = "出生时间分钟",
}: BirthClockSelectProps) {
const currentTime = value || "";
const [timeParts, setTimeParts] = useState(() => splitBirthTime(currentTime));
@@ -44,9 +52,9 @@ function BirthClockSelect({ value, onChange }: BirthClockSelectProps) {
};
return (
<div className="birth-time-clock-selects" aria-label="选择出生时间">
<div className="birth-time-clock-selects" aria-label={ariaLabel}>
<Select required value={timeParts.hour || null} onValueChange={(nextValue) => patchTime("hour", nextValue)}>
<SelectTrigger aria-label="出生时间小时">
<SelectTrigger aria-label={hourLabel}>
<SelectValue placeholder="时" />
</SelectTrigger>
<SelectContent className="birth-time-clock-menu" style={{ width: 108, minWidth: 108 }}>
@@ -55,7 +63,7 @@ function BirthClockSelect({ value, onChange }: BirthClockSelectProps) {
</Select>
<span className="birth-time-clock-separator" aria-hidden="true">:</span>
<Select required value={timeParts.minute || null} onValueChange={(nextValue) => patchTime("minute", nextValue)}>
<SelectTrigger aria-label="出生时间分钟">
<SelectTrigger aria-label={minuteLabel}>
<SelectValue placeholder="分" />
</SelectTrigger>
<SelectContent className="birth-time-clock-menu" style={{ width: 108, minWidth: 108 }}>
@@ -168,36 +176,28 @@ export function BirthTimeIntakeFields({ value, onPatch }: BirthTimeIntakeProps)
)}
{source === "period_only" && (
<div className="birth-time-detail-grid birth-time-period-details onboarding-card-reveal">
<div className="birth-time-detail-grid birth-time-window-details onboarding-card-reveal">
<p className="birth-time-window-intro">选择你记得的开始和结束时间。可以跨午夜,例如 23:00 到 03:59。</p>
<label>
<span>请选择最接近的时间范围</span>
<Select
required
value={value.birthTimePeriod || null}
onValueChange={(nextValue) => {
if (typeof nextValue === "string") onPatch({ birthTimePeriod: nextValue });
}}
>
<SelectTrigger aria-label="最接近的时间范围">
<SelectValue placeholder="请选择大致时段">
{(selectedValue) => birthTimePeriodOptions.find((option) => option.value === selectedValue)?.label ?? "请选择大致时段"}
</SelectValue>
</SelectTrigger>
<SelectContent>
{birthTimePeriodOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>
))}
</SelectContent>
</Select>
<span>开始</span>
<BirthClockSelect
key={`start-${value.declaredWindowStart}`}
value={value.declaredWindowStart}
ariaLabel="选择开始时间"
hourLabel="开始小时"
minuteLabel="开始分钟"
onChange={(declaredWindowStart) => onPatch({ declaredWindowStart })}
/>
</label>
<label>
<span>补充描述(可选)</span>
<textarea
maxLength={240}
placeholder="例如:天刚亮、午饭前后、家人记得大约 6—8 点"
rows={2}
value={value.birthTimeClue}
onChange={(event) => onPatch({ birthTimeClue: event.target.value })}
<span>结束</span>
<BirthClockSelect
key={`end-${value.declaredWindowEnd}`}
value={value.declaredWindowEnd}
ariaLabel="选择结束时间"
hourLabel="结束小时"
minuteLabel="结束分钟"
onChange={(declaredWindowEnd) => onPatch({ declaredWindowEnd })}
/>
</label>
<button
@@ -207,6 +207,8 @@ export function BirthTimeIntakeFields({ value, onPatch }: BirthTimeIntakeProps)
birthTimeSource: "unknown",
reportedTime: "",
birthTimePeriod: "",
declaredWindowStart: "",
declaredWindowEnd: "",
birthTimeClue: "",
uncertaintyBeforeMinutes: null,
uncertaintyAfterMinutes: null,
@@ -224,7 +226,7 @@ export function BirthTimeIntakeFields({ value, onPatch }: BirthTimeIntakeProps)
className="button-secondary"
type="button"
onClick={() => onPatch({ birthTimeSource: "period_only", birthTimeStatus: "reported" })}
>我可以描述一个时间范围</button>
>我可以选一段时间范围</button>
</div>
)}
</div>
+31 -2
View File
@@ -34,6 +34,8 @@ export const accountProfilePatchSchema = z.object({
reported_birth_time: nullableBirthClock.optional(),
birth_time_source: birthTimeSourceSchema.nullable().optional(),
birth_time_period: birthTimePeriodSchema.nullable().optional(),
declared_window_start: nullableBirthClock.optional(),
declared_window_end: nullableBirthClock.optional(),
birth_time_clue: nullableTrimmedString(240).optional(),
uncertainty_before_minutes: z.number().int().min(0).max(720).nullable().optional(),
uncertainty_after_minutes: z.number().int().min(0).max(720).nullable().optional(),
@@ -66,6 +68,8 @@ export const accountProfilePatchSchema = z.object({
"reported_birth_time",
"birth_time_source",
"birth_time_period",
"declared_window_start",
"declared_window_end",
"birth_time_clue",
"uncertainty_before_minutes",
"uncertainty_after_minutes",
@@ -95,7 +99,8 @@ export const accountProfilePatchSchema = z.object({
if (value.birth_date !== undefined && value.birth_date !== null) {
addIssue("birth_time_source", "填写出生日期后必须说明时间来源");
}
if (time || value.birth_time || value.birth_time_period || value.birth_time_clue
if (time || value.birth_time || value.birth_time_period || value.declared_window_start
|| value.declared_window_end || value.birth_time_clue
|| before != null || after != null) {
addIssue("birth_time_source", "未选择时间来源时不得提交时间或误差范围");
}
@@ -109,6 +114,11 @@ export const accountProfilePatchSchema = z.object({
const ensureNoPeriod = () => {
if (value.birth_time_period) addIssue("birth_time_period", "具体时间来源不得同时提交时段");
};
const ensureNoDeclaredWindow = () => {
if (value.declared_window_start || value.declared_window_end) {
addIssue("declared_window_start", "具体时间来源不得同时提交开始结束范围");
}
};
const ensureNoUncertainty = () => {
if (before != null || after != null) {
addIssue("uncertainty_before_minutes", "该时间来源不得提交误差范围");
@@ -119,29 +129,44 @@ export const accountProfilePatchSchema = z.object({
if (!time) addIssue("reported_birth_time", "医院记录需要具体时间");
if (before !== 2 || after !== 2) addIssue("uncertainty_before_minutes", "医院记录固定检查前后 2 分钟");
ensureNoPeriod();
ensureNoDeclaredWindow();
} else if (source === "family_exact") {
if (!time) addIssue("reported_birth_time", "家人记忆需要具体时间");
if (![0, 5, 10, 15].includes(before ?? -1) || before !== after) {
addIssue("uncertainty_before_minutes", "准确时间填报必须前后一致");
}
ensureNoPeriod();
ensureNoDeclaredWindow();
} else if (source === "approximate") {
if (!time) addIssue("reported_birth_time", "大概时间需要具体 HH:mm");
if (![15, 30, 60].includes(before ?? -1) || before !== after) {
addIssue("uncertainty_before_minutes", "大概时间误差必须为前后 15、30 或 60 分钟");
}
ensureNoPeriod();
ensureNoDeclaredWindow();
} else if (source === "legacy_import") {
if (!time && !value.birth_time) addIssue("reported_birth_time", "既有资料需要具体时间");
ensureNoPeriod();
ensureNoDeclaredWindow();
ensureNoUncertainty();
} else if (source === "period_only") {
if (!value.birth_time_period) addIssue("birth_time_period", "只知道时段时必须选择时段");
const start = value.declared_window_start;
const end = value.declared_window_end;
const hasWindow = Boolean(start && end && start !== end);
if (!hasWindow && !value.birth_time_period) {
addIssue("declared_window_start", "不确定准确时间时必须选择开始和结束时间");
}
if ((start && !end) || (!start && end) || (start && end && start === end)) {
addIssue("declared_window_end", "开始和结束必须是不同的钟点");
}
if (time) addIssue("reported_birth_time", "只知道时段时不得同时提交具体分钟");
ensureNoUncertainty();
} else if (source === "unknown") {
if (time) addIssue("reported_birth_time", "时间未知时不得提交具体分钟");
if (value.birth_time_period) addIssue("birth_time_period", "时间未知时不得提交确定时段");
if (value.declared_window_start || value.declared_window_end) {
addIssue("declared_window_start", "时间未知时不得提交开始结束范围");
}
ensureNoUncertainty();
}
@@ -154,6 +179,8 @@ type AccountBirthTimeState = Readonly<{
reported_birth_time: string | null;
birth_time_source: string | null;
birth_time_period: string | null;
declared_window_start?: string | null;
declared_window_end?: string | null;
birth_time_clue: string | null;
uncertainty_before_minutes: number | null;
uncertainty_after_minutes: number | null;
@@ -181,6 +208,8 @@ const declarationFields = [
"reported_birth_time",
"birth_time_source",
"birth_time_period",
"declared_window_start",
"declared_window_end",
"birth_time_clue",
"uncertainty_before_minutes",
"uncertainty_after_minutes",
@@ -46,6 +46,8 @@ export function birthProfileReferenceTime(value: unknown, preferredTime?: string
const direct = text(profile ? valueFor(profile, "birth_time", "time") : null)?.slice(0, 5);
if (direct && /^([01]\d|2[0-3]):[0-5]\d$/.test(direct)) return direct;
const period = text(profile ? valueFor(profile, "birth_time_period", "birthTimePeriod") : null);
const windowStart = text(profile ? valueFor(profile, "declared_window_start", "declaredWindowStart") : null)?.slice(0, 5);
if (windowStart && /^([01]\d|2[0-3]):[0-5]\d$/.test(windowStart)) return windowStart;
return {
early_morning: "06:00",
morning: "10:00",
+76 -18
View File
@@ -1,5 +1,9 @@
import { format, isValid, parse } from "date-fns";
import type { JourneySnapshot } from "./birth-time-journey.ts";
import {
declaredClockRange,
matchingDeclaredPeriod,
} from "./declared-birth-window.ts";
const birthDatePattern = "yyyy-MM-dd";
const birthClockPattern = /^(?:[01]\d|2[0-3]):[0-5]\d$/;
@@ -38,6 +42,8 @@ export type BirthTimeDraft = {
readonly reportedTime: string;
readonly birthTimeSource: BirthTimeSource;
readonly birthTimePeriod: BirthTimePeriod;
readonly declaredWindowStart: string;
readonly declaredWindowEnd: string;
readonly birthTimeClue: string;
readonly uncertaintyBeforeMinutes: number | null;
readonly uncertaintyAfterMinutes: number | null;
@@ -84,30 +90,39 @@ export function formatBirthDate(value: Date): string {
export const birthTimeSourceOptions = [
{ value: "family_exact", label: "我知道准确出生时间", hint: "按医院记录或家人记得的时间填写,精确到分钟" },
{ value: "period_only", label: "我不确定准确时间", hint: "只知道大概几点、上午下午,或完全不清楚" },
{ value: "period_only", label: "我不确定准确时间", hint: "选择一段开始到结束的时间,或完全不清楚" },
] as const;
export const birthTimeSourceDefaults = {
hospital_record: {
birthTimePeriod: "",
declaredWindowStart: "",
declaredWindowEnd: "",
birthTimeClue: "",
uncertaintyBeforeMinutes: 2,
uncertaintyAfterMinutes: 2,
},
family_exact: {
birthTimePeriod: "",
declaredWindowStart: "",
declaredWindowEnd: "",
birthTimeClue: "",
uncertaintyBeforeMinutes: 0,
uncertaintyAfterMinutes: 0,
},
approximate: {
birthTimePeriod: "",
declaredWindowStart: "",
declaredWindowEnd: "",
birthTimeClue: "",
uncertaintyBeforeMinutes: 30,
uncertaintyAfterMinutes: 30,
},
period_only: {
reportedTime: "",
birthTimePeriod: "",
declaredWindowStart: "",
declaredWindowEnd: "",
birthTimeClue: "",
uncertaintyBeforeMinutes: null,
uncertaintyAfterMinutes: null,
@@ -115,6 +130,8 @@ export const birthTimeSourceDefaults = {
unknown: {
reportedTime: "",
birthTimePeriod: "",
declaredWindowStart: "",
declaredWindowEnd: "",
uncertaintyBeforeMinutes: null,
uncertaintyAfterMinutes: null,
},
@@ -136,8 +153,8 @@ export type BirthTimeDisplayState = {
function reportedBirthTimeLabel(draft: BirthTimeDraft): string {
if (draft.birthTimeSource === "period_only") {
return birthTimePeriodOptions.find((option) => option.value === draft.birthTimePeriod)?.label
?? "未选择时段";
const range = declaredWindowRangeForDraft(draft);
return range ? range.label : "未选择时间范围";
}
if (draft.birthTimeSource === "unknown") return "具体时间未知";
return draft.reportedTime || draft.time || "尚未填报";
@@ -155,15 +172,6 @@ export function birthTimeDisplayState(draft: BirthTimeDraft): BirthTimeDisplaySt
};
}
const periodLabels = {
"": "未选择时段",
early_morning: "凌晨或清晨",
morning: "上午",
afternoon: "下午",
evening: "晚上",
late_night: "深夜",
} as const satisfies Record<BirthTimePeriod, string>;
const intentCopy = {
confirm_stable_record: "医院记录前后两分钟内结构稳定,可以直接作为当前排盘时间。",
explain_sensitive_boundary: "这个时间靠近敏感边界,需要先做轻量校正,暂不直接排盘。",
@@ -185,6 +193,38 @@ export function assistantIntentCopy(intent: JourneySnapshot["assistantIntent"])
return intentCopy[intent];
}
export function declaredWindowRangeForDraft(draft: Pick<BirthTimeDraft, "birthTimeSource" | "birthTimePeriod" | "declaredWindowStart" | "declaredWindowEnd">) {
return declaredClockRange({
source: draft.birthTimeSource || "period_only",
period: draft.birthTimePeriod || null,
startTime: draft.declaredWindowStart || null,
endTime: draft.declaredWindowEnd || null,
});
}
export function hydrateDeclaredWindowDraft(input: {
readonly period?: string | null;
readonly start?: string | null;
readonly end?: string | null;
}): Pick<BirthTimeDraft, "birthTimePeriod" | "declaredWindowStart" | "declaredWindowEnd"> {
const period = birthTimePeriodOptions.some((option) => option.value === input.period)
? input.period as BirthTimePeriod
: "";
const range = declaredClockRange({
source: "period_only",
period: period || null,
startTime: input.start,
endTime: input.end,
});
return {
birthTimePeriod: range
? matchingDeclaredPeriod(range.startTime, range.endTime) ?? ""
: period,
declaredWindowStart: range?.startTime ?? "",
declaredWindowEnd: range?.endTime ?? "",
};
}
export function isBirthTimeDraftReady(draft: BirthTimeDraft) {
if (!parseBirthDate(draft.date) || draft.birthTimeClue.length > 240) return false;
switch (draft.birthTimeSource) {
@@ -205,7 +245,7 @@ export function isBirthTimeDraftReady(draft: BirthTimeDraft) {
&& (draft.uncertaintyAfterMinutes === null
|| draft.uncertaintyBeforeMinutes === draft.uncertaintyAfterMinutes);
case "period_only":
return birthTimePeriodOptions.some((option) => option.value === draft.birthTimePeriod)
return Boolean(declaredWindowRangeForDraft(draft))
&& !draft.reportedTime
&& draft.uncertaintyBeforeMinutes === null
&& draft.uncertaintyAfterMinutes === null;
@@ -227,7 +267,13 @@ export function birthTimeDraftReadyHint(draft: BirthTimeDraft) {
if (!parseBirthDate(draft.date)) return "请先选择出生日期";
if (!draft.birthTimeSource) return "请选择你对出生时间了解多少";
if (isBirthTimeDraftReady(draft)) return "";
if (draft.birthTimeSource === "period_only") return "请选择一个大致时段";
if (draft.birthTimeSource === "period_only") {
if (draft.declaredWindowStart && draft.declaredWindowEnd
&& draft.declaredWindowStart === draft.declaredWindowEnd) {
return "开始和结束不能是同一分钟";
}
return "请选择开始和结束时间";
}
if (draft.birthTimeSource === "unknown") return "";
return "请填写时和分";
}
@@ -266,6 +312,8 @@ const declaredBirthInputKeys = [
"reportedTime",
"birthTimeSource",
"birthTimePeriod",
"declaredWindowStart",
"declaredWindowEnd",
"birthTimeClue",
"uncertaintyBeforeMinutes",
"uncertaintyAfterMinutes",
@@ -316,11 +364,19 @@ export function birthTimePersistenceValues(draft: BirthTimeDraft) {
: draft.birthTimeSource === "approximate"
? draft.uncertaintyBeforeMinutes ?? birthTimeSourceDefaults.approximate.uncertaintyBeforeMinutes
: null;
const declaredRange = draft.birthTimeSource === "period_only"
? declaredWindowRangeForDraft(draft)
: null;
const matchedPeriod = declaredRange
? matchingDeclaredPeriod(declaredRange.startTime, declaredRange.endTime)
: null;
return {
reported_birth_time: reportedTime,
birth_time_source: draft.birthTimeSource || null,
birth_time_period: draft.birthTimePeriod || null,
birth_time_clue: draft.birthTimeClue.trim() || null,
birth_time_period: matchedPeriod,
declared_window_start: declaredRange?.startTime ?? null,
declared_window_end: declaredRange?.endTime ?? null,
birth_time_clue: null,
uncertainty_before_minutes: uncertainty,
uncertainty_after_minutes: uncertainty,
};
@@ -361,8 +417,10 @@ export function describeBirthTimeDraft(draft: BirthTimeDraft) {
return `${date} ${draft.reportedTime}(填报准确时间)`;
case "approximate":
return `${date},约 ${draft.reportedTime}`;
case "period_only":
return `${date},${periodLabels[draft.birthTimePeriod]}${draft.birthTimeClue.trim() ? `(${draft.birthTimeClue.trim()})` : ""}`;
case "period_only": {
const range = declaredWindowRangeForDraft(draft);
return `${date},${range ? range.label : "未选择时间范围"}`;
}
case "unknown":
return `${date},具体时间未知`;
case "legacy_import":
@@ -2,7 +2,7 @@ import { chinaLocations } from "../data/china-locations.ts";
import { isBirthClockTime, parseBirthDate } from "./birth-time-intake-model.ts";
import { resolveMissingBirthTimezoneOffset } from "./birth-profile-timezone.ts";
import {
declaredClockRange,
declaredClockRangeFromProfile,
declaredRangeWrapsMidnight,
} from "./declared-birth-window.ts";
import {
@@ -367,7 +367,7 @@ function declaredWindowFromProfile(value: unknown): DeclaredBirthWindowConsultat
throw new ConsultationProfileTruthError("mode_changed");
}
const birthTimePeriod = optionalText(profile, "birth_time_period");
const range = declaredClockRange({ source: birthTimeSource, period: birthTimePeriod });
const range = declaredClockRangeFromProfile(profile);
if (!range) throw new ConsultationProfileTruthError("profile_incomplete");
const countryCode = optionalText(profile, "country_code");
@@ -481,10 +481,7 @@ export async function prepareConsultationRoute<Reservation, GuardResult>(
}
serverChart = serverChartFromProfile(profile, consultationMode);
} else if (consultationMode === "declared_birth_window") {
const range = declaredClockRange({
source: optionalText(record(profile) ?? {}, "birth_time_source") ?? "",
period: optionalText(record(profile) ?? {}, "birth_time_period"),
});
const range = declaredClockRangeFromProfile(record(profile) ?? {});
try {
profile = await (input.resolveTimezoneOffset ?? ((value, time) => (
resolveMissingBirthTimezoneOffset(value, { preferredTime: time })
+46 -3
View File
@@ -111,18 +111,61 @@ export function declaredWindowProbeRole(
return "interior";
}
function customDeclaredRange(startTime: string | null | undefined, endTime: string | null | undefined): DeclaredClockRange | null {
if (!isBirthClockText(startTime) || !isBirthClockText(endTime) || startTime === endTime) return null;
return {
label: `${startTime}—${endTime}`,
startTime,
endTime,
};
}
export function matchingDeclaredPeriod(startTime: string, endTime: string): DeclaredBirthPeriod | null {
for (const period of DECLARED_PERIOD_IDS) {
const range = DECLARED_PERIOD_RANGES[period];
if (range.startTime === startTime && range.endTime === endTime) return period;
}
return null;
}
export function declaredClockRange(input: Readonly<{
source: string;
period?: string | null;
startTime?: string | null;
endTime?: string | null;
}>): DeclaredClockRange | null {
const custom = customDeclaredRange(input.startTime, input.endTime);
if (input.source === "period_only") {
return isDeclaredBirthPeriod(input.period) ? DECLARED_PERIOD_RANGES[input.period] : null;
return custom ?? (isDeclaredBirthPeriod(input.period) ? DECLARED_PERIOD_RANGES[input.period] : null);
}
if (input.source === "legacy_import" && isDeclaredBirthPeriod(input.period)) {
return DECLARED_PERIOD_RANGES[input.period];
if (input.source === "legacy_import" && (custom || isDeclaredBirthPeriod(input.period))) {
return custom ?? DECLARED_PERIOD_RANGES[input.period as DeclaredBirthPeriod];
}
if (input.source === "unknown" || input.source === "legacy_import") {
return UNKNOWN_DECLARED_CLOCK_RANGE;
}
return null;
}
function textClock(value: unknown): string | null {
return typeof value === "string" && isBirthClockText(value.slice(0, 5)) ? value.slice(0, 5) : null;
}
export function declaredClockRangeFromProfile(profile: Readonly<Record<string, unknown>>): DeclaredClockRange | null {
const source = typeof profile.birth_time_source === "string"
? profile.birth_time_source
: typeof profile.birthTimeSource === "string"
? profile.birthTimeSource
: "";
const period = typeof profile.birth_time_period === "string"
? profile.birth_time_period
: typeof profile.birthTimePeriod === "string"
? profile.birthTimePeriod
: null;
return declaredClockRange({
source,
period,
startTime: textClock(profile.declared_window_start ?? profile.declaredWindowStart),
endTime: textClock(profile.declared_window_end ?? profile.declaredWindowEnd),
});
}
@@ -13,6 +13,8 @@ type OnboardingProfileInput = {
readonly activeBirthTime: string | null;
readonly birthTimeSource?: string | null;
readonly birthTimePeriod?: string | null;
readonly declaredWindowStart?: string | null;
readonly declaredWindowEnd?: string | null;
readonly birthTimeClue?: string | null;
readonly uncertaintyBeforeMinutes?: number | null;
readonly uncertaintyAfterMinutes?: number | null;
@@ -56,6 +58,8 @@ export function createOnboardingCacheIdentity(
profile.activeBirthTime,
profile.birthTimeSource,
profile.birthTimePeriod,
profile.declaredWindowStart,
profile.declaredWindowEnd,
profile.birthTimeClue,
profile.uncertaintyBeforeMinutes,
profile.uncertaintyAfterMinutes,
+4
View File
@@ -76,6 +76,8 @@ type OnboardingProfileFingerprintInput = {
readonly reportedTime: string;
readonly birthTimeSource: string;
readonly birthTimePeriod: string;
readonly declaredWindowStart: string;
readonly declaredWindowEnd: string;
readonly birthTimeClue: string;
readonly uncertaintyBeforeMinutes: number | null;
readonly uncertaintyAfterMinutes: number | null;
@@ -142,6 +144,8 @@ export function onboardingProfileFingerprint(profile: OnboardingProfileFingerpri
profile.reportedTime,
profile.birthTimeSource,
profile.birthTimePeriod,
profile.declaredWindowStart,
profile.declaredWindowEnd,
profile.birthTimeClue,
profile.uncertaintyBeforeMinutes,
profile.uncertaintyAfterMinutes,
+13 -9
View File
@@ -10,6 +10,7 @@ import {
} from "./onboarding-payload.ts";
import {
formatBirthDate,
hydrateDeclaredWindowDraft,
isDeclaredBirthProfileComplete,
type BirthTimeSource,
type BirthTimeStatus,
@@ -24,6 +25,8 @@ export type OnboardingProfileRow = {
readonly active_birth_time: string | null;
readonly birth_time_source: string | null;
readonly birth_time_period: string | null;
readonly declared_window_start?: string | null;
readonly declared_window_end?: string | null;
readonly birth_time_clue: string | null;
readonly uncertainty_before_minutes: number | null;
readonly uncertainty_after_minutes: number | null;
@@ -103,21 +106,20 @@ function hasCompleteBirthProfile(profile: OnboardingProfileRow): boolean {
];
const status = knownStatuses.find((item) => item === profile.birth_time_status)
?? (persistedTime ? "confirmed" : "");
const clock = (value: string | null) => value ? value.slice(0, 5) : "";
const clock = (value: string | null | undefined) => value ? value.slice(0, 5) : "";
const declaredWindow = hydrateDeclaredWindowDraft({
period: profile.birth_time_period,
start: profile.declared_window_start,
end: profile.declared_window_end,
});
const birthDraft = {
date: normalizeBirthDate(profile.birth_date),
time: clock(persistedTime),
reportedTime: clock(profile.reported_birth_time)
|| (source === "legacy_import" ? clock(persistedTime) : ""),
birthTimeSource: source,
birthTimePeriod: profile.birth_time_period === "early_morning"
|| profile.birth_time_period === "morning"
|| profile.birth_time_period === "afternoon"
|| profile.birth_time_period === "evening"
|| profile.birth_time_period === "late_night"
? profile.birth_time_period
: "",
birthTimeClue: profile.birth_time_clue ?? "",
...declaredWindow,
birthTimeClue: "",
uncertaintyBeforeMinutes: profile.uncertainty_before_minutes,
uncertaintyAfterMinutes: profile.uncertainty_after_minutes,
birthTimeStatus: status,
@@ -181,6 +183,8 @@ export function createOnboardingPost(dependencies: OnboardingPostDependencies):
activeBirthTime: profile.active_birth_time,
birthTimeSource: profile.birth_time_source,
birthTimePeriod: profile.birth_time_period,
declaredWindowStart: profile.declared_window_start,
declaredWindowEnd: profile.declared_window_end,
birthTimeClue: profile.birth_time_clue,
uncertaintyBeforeMinutes: profile.uncertainty_before_minutes,
uncertaintyAfterMinutes: profile.uncertainty_after_minutes,
@@ -5,6 +5,7 @@ import type {
AgenticRectificationContext,
} from "@/mastra/rectification-tools";
import { normalizePersistedBirthDate } from "../birth-time-intake-model.ts";
import { declaredClockRange } from "../declared-birth-window.ts";
type AccountingClient = SupabaseClient;
@@ -52,14 +53,6 @@ const timeValue = (value: unknown): string | null => {
return /^([01]\d|2[0-3]):[0-5]\d$/.test(time) ? time : null;
};
const periodRanges = {
early_morning: { start_time: "04:00", end_time: "07:59" },
morning: { start_time: "08:00", end_time: "11:59" },
afternoon: { start_time: "12:00", end_time: "17:59" },
evening: { start_time: "18:00", end_time: "22:59" },
late_night: { start_time: "23:00", end_time: "03:59" },
} as const;
function shiftedTime(time: string, offsetMinutes: number): string {
const [hour = 0, minute = 0] = time.split(":").map(Number);
const normalized = ((hour * 60 + minute + offsetMinutes) % 1_440 + 1_440) % 1_440;
@@ -71,6 +64,8 @@ function candidateRangeFrom(input: {
reportedTime: string | null;
source: string;
period: unknown;
windowStart: string | null;
windowEnd: string | null;
uncertaintyBefore: number | null;
uncertaintyAfter: number | null;
}): AgenticRectificationContext["candidateRange"] {
@@ -88,16 +83,14 @@ function candidateRangeFrom(input: {
end_time: shiftedTime(referenceTime, input.uncertaintyAfter ?? fallback),
};
}
if (input.source === "period_only" || input.source === "legacy_import") {
const period = typeof input.period === "string"
? periodRanges[input.period as keyof typeof periodRanges]
: null;
if (period) return period;
if (input.source === "period_only") throw new AgenticRectificationProfileError("missing_birth_time_period");
}
if (input.source === "unknown" || input.source === "legacy_import") {
return { start_time: "00:00", end_time: "23:59" };
}
const range = declaredClockRange({
source: input.source,
period: typeof input.period === "string" ? input.period : null,
startTime: input.windowStart,
endTime: input.windowEnd,
});
if (range) return { start_time: range.startTime, end_time: range.endTime };
if (input.source === "period_only") throw new AgenticRectificationProfileError("missing_birth_time_period");
throw new AgenticRectificationProfileError("missing_birth_time");
}
@@ -173,7 +166,7 @@ export async function loadAgenticRectificationProfile(
): Promise<AgenticRectificationProfile> {
const { data, error } = await accounting
.from("profiles")
.select("birth_date,reported_birth_time,active_birth_time,birth_time_source,birth_time_period,uncertainty_before_minutes,uncertainty_after_minutes,latitude,longitude,timezone_offset")
.select("birth_date,reported_birth_time,active_birth_time,birth_time_source,birth_time_period,declared_window_start,declared_window_end,uncertainty_before_minutes,uncertainty_after_minutes,latitude,longitude,timezone_offset")
.eq("id", userId)
.single();
if (error || !data) throw new AgenticRectificationProfileError("profile_unavailable");
@@ -198,6 +191,8 @@ export async function loadAgenticRectificationProfile(
reportedTime,
source: rawSource,
period: data.birth_time_period,
windowStart: timeValue(data.declared_window_start),
windowEnd: timeValue(data.declared_window_end),
uncertaintyBefore,
uncertaintyAfter,
}),
@@ -39,6 +39,8 @@ export type V9BaselineSnapshot = Readonly<{
timezone_offset: number;
birth_time_source: string;
birth_time_period: string | null;
declared_window_start: string | null;
declared_window_end: string | null;
reported_birth_time: string | null;
active_birth_time: string | null;
uncertainty_before_minutes: number | null;
@@ -119,6 +121,8 @@ function deriveCandidateRange(input: {
reportedTime: string | null;
source: string;
period: string | null;
windowStart: string | null;
windowEnd: string | null;
}): { start_time: string; end_time: string } {
if (input.reportedTime) {
return {
@@ -127,7 +131,12 @@ function deriveCandidateRange(input: {
};
}
const range = declaredClockRange({ source: input.source, period: input.period });
const range = declaredClockRange({
source: input.source,
period: input.period,
startTime: input.windowStart,
endTime: input.windowEnd,
});
if (!range) throw new RectificationCaseServiceError("profile_incomplete");
return { start_time: range.startTime, end_time: range.endTime };
}
@@ -142,6 +151,8 @@ function baselineFingerprint(baseline: V9BaselineSnapshot): string {
String(baseline.timezone_offset),
baseline.birth_time_source,
baseline.birth_time_period ?? "",
baseline.declared_window_start ?? "",
baseline.declared_window_end ?? "",
baseline.reported_birth_time ?? "",
baseline.active_birth_time ?? "",
String(baseline.uncertainty_before_minutes ?? ""),
@@ -157,7 +168,7 @@ export async function loadV9RectificationProfile(
const { data, error } = await accounting
.from("profiles")
.select(
"birth_date,birth_place_label,reported_birth_time,birth_time_source,birth_time_period,uncertainty_before_minutes,uncertainty_after_minutes,latitude,longitude,timezone_id,timezone_offset",
"birth_date,birth_place_label,reported_birth_time,birth_time_source,birth_time_period,declared_window_start,declared_window_end,uncertainty_before_minutes,uncertainty_after_minutes,latitude,longitude,timezone_id,timezone_offset",
)
.eq("id", userId)
.single();
@@ -179,6 +190,8 @@ export async function loadV9RectificationProfile(
const source = typeof row.birth_time_source === "string" ? row.birth_time_source.trim() : "";
const reportedTime = timeValue(row.reported_birth_time);
const period = typeof row.birth_time_period === "string" ? row.birth_time_period : null;
const windowStart = timeValue(row.declared_window_start);
const windowEnd = timeValue(row.declared_window_end);
const uncertaintyBefore = numberOrNull(row.uncertainty_before_minutes);
const uncertaintyAfter = numberOrNull(row.uncertainty_after_minutes);
if (!birthDate || !birthPlaceLabel || latitude === null || longitude === null || !timezoneId || timezoneOffset === null || !source) {
@@ -194,6 +207,8 @@ export async function loadV9RectificationProfile(
timezone_offset: timezoneOffset,
birth_time_source: source,
birth_time_period: period,
declared_window_start: windowStart,
declared_window_end: windowEnd,
reported_birth_time: reportedTime,
// A fresh Case must never inherit an accepted/active minute as its new baseline.
active_birth_time: null,
@@ -205,7 +220,13 @@ export async function loadV9RectificationProfile(
userId,
baseline,
baselineFingerprint: baselineFingerprint(baseline),
candidateRange: deriveCandidateRange({ reportedTime, source, period }),
candidateRange: deriveCandidateRange({
reportedTime,
source,
period,
windowStart,
windowEnd,
}),
};
}