feat: make birth-time rectification a soft homepage flow

This commit is contained in:
Jesse_Chen
2026-07-21 06:28:47 +08:00
parent 5c8d965f16
commit fc1e9f1085
11 changed files with 727 additions and 115 deletions
+49 -1
View File
@@ -1,4 +1,8 @@
import { NextResponse } from "next/server";
import {
parseRectificationPriceCredits,
type AccountRectificationCaseState,
} from "@/lib/birth-time-consultation-consent";
import { createAdminSupabaseClient, isAdminEmail } from "@/lib/supabase/admin";
import {
isSupabaseConfigurationError,
@@ -28,6 +32,7 @@ type ProfilePatchPayload = {
const birthTimeSources = ["hospital_record", "family_exact", "approximate", "period_only", "unknown", "legacy_import"] as const;
const birthTimePeriods = ["early_morning", "morning", "afternoon", "evening", "late_night"] as const;
const rectificationStatuses = ["starting", "active", "paused", "confirming", "completed", "abandoned"] as const;
function nullableString(value: unknown) {
return typeof value === "string" && value.trim() ? value.trim() : null;
@@ -53,6 +58,28 @@ function isMissingProfileColumn(error: { code?: string; message?: string } | nul
|| message.includes("column");
}
function projectRectificationCase(value: unknown): AccountRectificationCaseState | null {
if (value === null || typeof value !== "object") return null;
const row = value as Record<string, unknown>;
const status = rectificationStatuses.find((candidate) => candidate === row.status);
if (typeof row.id !== "string"
|| row.journey_protocol !== "conversational-evidence-v3"
|| !status
|| typeof row.turn_version !== "number"
|| !Number.isSafeInteger(row.turn_version)
|| row.turn_version < 0) {
throw new Error("invalid rectification case projection");
}
return Object.freeze({
caseId: row.id,
journeyProtocol: "conversational-evidence-v3",
status,
turnVersion: row.turn_version,
isRevision: typeof row.revision_of_case_id === "string",
preservesActiveTime: typeof row.baseline_active_time === "string",
});
}
export async function GET() {
try {
const supabase = await createServerSupabaseClient();
@@ -62,9 +89,12 @@ export async function GET() {
return NextResponse.json({ error: "请先登录" }, { status: 401 });
}
const rectificationPriceCredits = parseRectificationPriceCredits(
process.env.RECTIFICATION_PRICE_CREDITS,
);
const { data: profile, error } = await supabase
.from("profiles")
.select("credits")
.select("credits,active_birth_time,birth_time_status")
.eq("id", user.id)
.single();
@@ -72,10 +102,28 @@ export async function GET() {
return NextResponse.json({ error: "暂时无法读取账户余额" }, { status: 500 });
}
const admin = createAdminSupabaseClient();
const { data: rectificationCaseRow, error: rectificationCaseError } = await admin
.from("birth_time_rectification_cases")
.select("id,journey_protocol,status,turn_version,revision_of_case_id,baseline_active_time,updated_at")
.eq("user_id", user.id)
.eq("journey_protocol", "conversational-evidence-v3")
.order("updated_at", { ascending: false })
.limit(1)
.maybeSingle();
if (rectificationCaseError) {
return NextResponse.json({ error: "暂时无法读取生时校正状态" }, { status: 500 });
}
const rectificationCase = projectRectificationCase(rectificationCaseRow);
return NextResponse.json({
user: { id: user.id, email: user.email ?? null },
credits: profile.credits,
isAdmin: isAdminEmail(user.email),
rectificationPriceCredits,
hasConfirmedBirthTime: profile.birth_time_status === "confirmed"
&& typeof profile.active_birth_time === "string",
rectificationCase,
});
} catch (error) {
if (isSupabaseConfigurationError(error)) {