fix: close birth-time consultation boundaries

This commit is contained in:
Jesse_Chen
2026-07-21 07:54:04 +08:00
parent 8be39d5ad6
commit bccfb60e41
17 changed files with 827 additions and 121 deletions
+18 -8
View File
@@ -5,6 +5,7 @@ import {
} from "@/lib/birth-time-consultation-consent";
import {
accountProfilePatchSchema,
applyAccountProfileConcurrencyGuards,
resolveAccountBirthTimeApplicationPatch,
} from "@/lib/account-profile-patch";
import { createAdminSupabaseClient, isAdminEmail } from "@/lib/supabase/admin";
@@ -121,11 +122,25 @@ export async function PATCH(request: Request) {
const payload = parsedPayload.data;
const admin = createAdminSupabaseClient();
const { data: currentProfile, error: currentProfileError } = await admin
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")
.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")
.eq("id", userId)
.maybeSingle();
if (currentProfileError && isMissingProfileColumn(currentProfileError)) {
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,
} : null;
currentProfileError = fallback.error;
}
if (currentProfileError) {
return NextResponse.json({ error: "暂时无法核对现有出生资料" }, { status: 500 });
}
@@ -179,12 +194,7 @@ export async function PATCH(request: Request) {
}
let query = admin.from("profiles").update(values).eq("id", userId);
if (invalidatesUnconfirmedApplication) {
query = currentProfile.birth_time_status === null
? query.is("birth_time_status", null)
: query.eq("birth_time_status", currentProfile.birth_time_status);
query = currentProfile.active_birth_time === null
? query.is("active_birth_time", null)
: query.eq("active_birth_time", currentProfile.active_birth_time);
query = applyAccountProfileConcurrencyGuards(query, currentProfile);
}
return query.select("id").maybeSingle();
}
+45 -45
View File
@@ -24,10 +24,13 @@ import {
applyBirthTimeModeToWorkflowContext,
consultationBirthTimeModeSchema,
createBirthTimeModeOutputGuard,
serverProfileAllowsBirthTimeMode,
shouldRunBirthChartWorkflow,
type ConsultationBirthTimeMode,
} from "@/lib/consultation-birth-time-mode";
import {
ConsultationProfileTruthError,
prepareConsultationRoute,
} from "@/lib/consultation-route-service";
import { z } from "zod";
export const runtime = "nodejs";
@@ -171,41 +174,6 @@ export async function POST(request: Request) {
);
}
const requestedConsultationMode = parsed.data.consultationMode;
if (shouldRunBirthChartWorkflow(requestedConsultationMode)) {
if (!("hour" in parsed.data) || !("minute" in parsed.data)) {
return NextResponse.json(
{ error: "出生时间请求格式不正确" },
{ status: 400 },
);
}
const requestedTime = `${String(parsed.data.hour).padStart(2, "0")}:${String(parsed.data.minute).padStart(2, "0")}`;
const { data: birthTimeProfile, error: birthTimeProfileError } = await supabase
.from("profiles")
.select("active_birth_time,reported_birth_time,birth_time_source,birth_time_status")
.eq("id", user.id)
.single();
if (birthTimeProfileError || !birthTimeProfile) {
return NextResponse.json(
{ error: "暂时无法核对出生时间状态", message: "请稍后重试,本次不会扣点。" },
{ status: 503 },
);
}
if (!serverProfileAllowsBirthTimeMode(
birthTimeProfile,
requestedConsultationMode,
requestedTime,
)) {
return NextResponse.json(
{
error: "出生时间状态已经变化",
message: "请刷新后重新选择使用填报时间、一般咨询或先完成校正,本次不会扣点。",
},
{ status: 409 },
);
}
}
const requestTime = new Date();
const resolvedQuestion = resolveConsultationQuestion({
visibleQuestion: parsed.data.question,
@@ -215,20 +183,47 @@ export async function POST(request: Request) {
const userId = user.id;
const requestId = parsed.data.requestId;
let modelSelection;
let prepared;
try {
modelSelection = await reserveConsultationModel(
parsed.data.modelId,
resolveLanguageModel,
() =>
runCreditRpc(
prepared = await prepareConsultationRoute({
userId,
mode: parsed.data.consultationMode,
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_status,country_code,province_code,city_code,district_code,latitude,longitude,timezone_offset")
.eq("id", profileUserId)
.single();
if (error || !data) throw new ConsultationProfileTruthError("profile_unavailable");
return data;
},
reserve: () => reserveConsultationModel(
parsed.data.modelId,
resolveLanguageModel,
() => runCreditRpc(
accounting,
"begin_consultation_credit",
userId,
requestId,
),
);
),
});
} catch (error) {
if (error instanceof ConsultationProfileTruthError) {
const modeChanged = error.code === "mode_changed";
return NextResponse.json(
modeChanged
? {
error: "出生时间状态已经变化",
message: "请刷新后重新选择使用填报时间、一般咨询或先完成校正,本次不会扣点。",
}
: {
error: "暂时无法核对完整出生资料",
message: "出生日期、时间来源或出生地点资料不完整或不一致,请重新保存后再试,本次不会扣点。",
},
{ status: modeChanged ? 409 : 503 },
);
}
const reason = error instanceof Error ? error.name : "UnknownError";
console.error(
`[billing] reservation failed request=${requestId} reason=${reason}`,
@@ -239,6 +234,8 @@ export async function POST(request: Request) {
);
}
const modelSelection = prepared.reservation;
if (modelSelection.status === "unavailable") {
return NextResponse.json(
{
@@ -299,7 +296,8 @@ export async function POST(request: Request) {
}
try {
const { history, name } = parsed.data;
const { history } = parsed.data;
const name = prepared.serverChart?.name ?? parsed.data.name;
const consultationMode: ConsultationBirthTimeMode = parsed.data.consultationMode;
if (!shouldRunBirthChartWorkflow(consultationMode)) {
const result = await getGeneralJyotishAgent(selectedModel).stream([
@@ -343,12 +341,14 @@ export async function POST(request: Request) {
});
}
if (!prepared.serverChart) throw new Error("server_chart_truth_missing");
const toolInput = consultationInputSchema.parse({
...parsed.data,
...prepared.serverChart.toolInput,
// Unverified use is still a normal chart calculation with a hard answer
// boundary. It must never reactivate the retired rectification questionnaire.
entryMode: "direct_chart",
question: resolvedQuestion.modelQuestion,
theme: parsed.data.theme,
});
const workflowContext = applyBirthTimeModeToWorkflowContext(
await runConsultationWorkflow(toolInput),