feat: rebuild birth time rectification workflow
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { parseBirthTimeProfile } from "@/lib/birth-time-journey-adapters";
|
||||
import { assessBirthTime } from "@/lib/birth-time-journey";
|
||||
import { resolveMissingBirthTimezoneOffset } from "@/lib/birth-profile-timezone";
|
||||
import type { CalculationSpec } from "@/lib/rectification-v4/contracts";
|
||||
import { createRectificationV4CaseService } from "@/lib/rectification-v4/case-service";
|
||||
import { RectificationV4StoreError } from "@/lib/rectification-v4/store";
|
||||
import { createRectificationV4SupabaseStore } from "@/lib/rectification-v4/supabase-store";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { isSupabaseConfigurationError } from "@/lib/supabase/config";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
|
||||
const idSchema = z.string().uuid();
|
||||
|
||||
export async function rectificationV4Context() {
|
||||
const auth = await createServerSupabaseClient();
|
||||
const { data: { user }, error } = await auth.auth.getUser();
|
||||
if (error || !user) throw new RectificationV4HttpError(401, "请先登录后再继续生时校正。");
|
||||
const admin = createAdminSupabaseClient();
|
||||
return {
|
||||
userId: user.id,
|
||||
auth,
|
||||
service: createRectificationV4CaseService(createRectificationV4SupabaseStore(admin)),
|
||||
};
|
||||
}
|
||||
|
||||
export async function requestBody<T>(request: Request, schema: z.ZodType<T>): Promise<T> {
|
||||
const body = await request.json().catch(() => null);
|
||||
const parsed = schema.safeParse(body);
|
||||
if (!parsed.success) throw new RectificationV4HttpError(400, "提交内容不完整,请检查后重试。");
|
||||
return parsed.data;
|
||||
}
|
||||
|
||||
export function routeId(value: string): string {
|
||||
const parsed = idSchema.safeParse(value);
|
||||
if (!parsed.success) throw new RectificationV4HttpError(404, "没有找到这次生时校正记录。");
|
||||
return parsed.data;
|
||||
}
|
||||
|
||||
export async function calculationSpecForUser(
|
||||
auth: Awaited<ReturnType<typeof createServerSupabaseClient>>,
|
||||
userId: string,
|
||||
): Promise<CalculationSpec> {
|
||||
const { data, error } = await auth.from("profiles")
|
||||
.select("birth_date,reported_birth_time,birth_time_source,birth_time_period,birth_time_clue,uncertainty_before_minutes,uncertainty_after_minutes,latitude,longitude,timezone_id,timezone_offset")
|
||||
.eq("id", userId).maybeSingle();
|
||||
if (error) throw error;
|
||||
if (!data) throw new RectificationV4HttpError(409, "请先补全出生日期、时间线索和出生地点。");
|
||||
const profile = await resolveMissingBirthTimezoneOffset(data);
|
||||
const assessment = parseBirthTimeProfile(profile);
|
||||
const range = assessBirthTime(assessment, { kind: "unavailable" }).reportedRange;
|
||||
return {
|
||||
version: "rectification-calculation-spec-v4",
|
||||
birthDate: assessment.date,
|
||||
candidateRange: {
|
||||
start: range.startTime ?? "00:00",
|
||||
end: range.endTime ?? "23:59",
|
||||
},
|
||||
latitude: assessment.location.lat,
|
||||
longitude: assessment.location.lon,
|
||||
timezoneOffsetHours: assessment.location.tz,
|
||||
ayanamsa: "lahiri",
|
||||
nodeMode: "mean",
|
||||
minuteStep: 1,
|
||||
};
|
||||
}
|
||||
|
||||
export class RectificationV4HttpError extends Error {
|
||||
constructor(readonly status: number, message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export function rectificationV4Error(error: unknown): NextResponse {
|
||||
if (error instanceof RectificationV4HttpError) {
|
||||
return NextResponse.json({ error: error.message }, { status: error.status });
|
||||
}
|
||||
if (error instanceof RectificationV4StoreError) {
|
||||
const responses: Record<RectificationV4StoreError["code"], readonly [number, string]> = {
|
||||
not_found: [404, "没有找到这次生时校正记录。"],
|
||||
stale_version: [409, "记录已在其他位置更新,正在重新载入。"],
|
||||
invalid_state: [409, "当前状态无法执行这个操作,请刷新后重试。"],
|
||||
stale_job: [409, "这次计算已过期,请以最新结果为准。"],
|
||||
lease_lost: [409, "计算任务已由其他进程接管,请稍后刷新。"],
|
||||
};
|
||||
const response = responses[error.code];
|
||||
return NextResponse.json({ error: response[1] }, { status: response[0] });
|
||||
}
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json({ error: "出生资料或提交内容格式不正确。" }, { status: 400 });
|
||||
}
|
||||
if (isSupabaseConfigurationError(error)) {
|
||||
return NextResponse.json({ error: "生时校正服务尚未配置。" }, { status: 503 });
|
||||
}
|
||||
console.error("rectification_v4_route_failed", error);
|
||||
return NextResponse.json({ error: "暂时无法处理,请稍后再试。" }, { status: 500 });
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { rectificationV4Error } from "../../../_server";
|
||||
import { transitionCase } from "../../_action";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function POST(request: Request, { params }: { params: Promise<{ caseId: string }> }) {
|
||||
try {
|
||||
return await transitionCase(request, params, "abandon");
|
||||
} catch (error) {
|
||||
return rectificationV4Error(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { acceptRangeRequestSchema } from "@/lib/rectification-v4/contracts";
|
||||
import { rectificationV4Context, rectificationV4Error, requestBody, routeId } from "../../../_server";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function POST(request: Request, { params }: { params: Promise<{ caseId: string }> }) {
|
||||
try {
|
||||
const body = await requestBody(request, acceptRangeRequestSchema);
|
||||
const context = await rectificationV4Context();
|
||||
const result = await context.service.acceptRange({
|
||||
...body,
|
||||
userId: context.userId,
|
||||
caseId: routeId((await params).caseId),
|
||||
});
|
||||
return result
|
||||
? NextResponse.json(result)
|
||||
: NextResponse.json({ error: "当前结果还不足以保存这个范围。" }, { status: 409 });
|
||||
} catch (error) {
|
||||
return rectificationV4Error(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { answerRequestSchema } from "@/lib/rectification-v4/contracts";
|
||||
import { rectificationV4Context, rectificationV4Error, requestBody, routeId } from "../../../_server";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function POST(request: Request, { params }: { params: Promise<{ caseId: string }> }) {
|
||||
try {
|
||||
const body = await requestBody(request, answerRequestSchema);
|
||||
const context = await rectificationV4Context();
|
||||
const result = await context.service.answer({ ...body, userId: context.userId, caseId: routeId((await params).caseId) });
|
||||
return result
|
||||
? NextResponse.json(result, { status: 202 })
|
||||
: NextResponse.json({ error: "当前没有待回答的问题,请刷新后重试。" }, { status: 409 });
|
||||
} catch (error) {
|
||||
return rectificationV4Error(error);
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { reviseEventRequestSchema } from "@/lib/rectification-v4/contracts";
|
||||
import { appendEventRevision } from "@/lib/rectification-v4/evidence-ledger";
|
||||
import { rectificationV4Context, rectificationV4Error, requestBody, routeId } from "../../../../../_server";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function POST(request: Request, { params }: { params: Promise<{ caseId: string; eventId: string }> }) {
|
||||
try {
|
||||
const body = await requestBody(request, reviseEventRequestSchema);
|
||||
const context = await rectificationV4Context();
|
||||
const values = await params;
|
||||
const caseId = routeId(values.caseId);
|
||||
const eventId = routeId(values.eventId);
|
||||
const current = await context.service.loadCase(context.userId, caseId);
|
||||
if (!current) return NextResponse.json({ error: "没有找到这次生时校正记录。" }, { status: 404 });
|
||||
const revision = appendEventRevision(current.events, {
|
||||
eventId,
|
||||
domain: body.domain,
|
||||
eventKind: body.eventKind,
|
||||
summary: body.summary,
|
||||
rawText: body.rawText,
|
||||
dateRange: body.dateRange,
|
||||
scoreability: body.scoreability,
|
||||
});
|
||||
return NextResponse.json(await context.service.reviseEvent({
|
||||
userId: context.userId,
|
||||
caseId,
|
||||
actionId: body.actionId,
|
||||
expectedCaseVersion: body.expectedCaseVersion,
|
||||
revision,
|
||||
}), { status: 202 });
|
||||
} catch (error) {
|
||||
return rectificationV4Error(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { rectificationV4Error } from "../../../_server";
|
||||
import { transitionCase } from "../../_action";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function POST(request: Request, { params }: { params: Promise<{ caseId: string }> }) {
|
||||
try {
|
||||
return await transitionCase(request, params, "pause");
|
||||
} catch (error) {
|
||||
return rectificationV4Error(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { rectificationV4Error } from "../../../_server";
|
||||
import { transitionCase } from "../../_action";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function POST(request: Request, { params }: { params: Promise<{ caseId: string }> }) {
|
||||
try {
|
||||
return await transitionCase(request, params, "resume");
|
||||
} catch (error) {
|
||||
return rectificationV4Error(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { rectificationV4Context, rectificationV4Error, routeId } from "../../_server";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function GET(_request: Request, { params }: { params: Promise<{ caseId: string }> }) {
|
||||
try {
|
||||
const context = await rectificationV4Context();
|
||||
const result = await context.service.loadCase(context.userId, routeId((await params).caseId));
|
||||
return result ? NextResponse.json(result) : NextResponse.json({ error: "没有找到这次生时校正记录。" }, { status: 404 });
|
||||
} catch (error) {
|
||||
return rectificationV4Error(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { caseActionRequestSchema } from "@/lib/rectification-v4/contracts";
|
||||
import { rectificationV4Context, requestBody, routeId } from "../_server";
|
||||
|
||||
export async function transitionCase(
|
||||
request: Request,
|
||||
params: Promise<{ caseId: string }>,
|
||||
kind: "pause" | "resume" | "abandon",
|
||||
) {
|
||||
const body = await requestBody(request, caseActionRequestSchema);
|
||||
const context = await rectificationV4Context();
|
||||
return NextResponse.json(await context.service.transition({
|
||||
...body,
|
||||
userId: context.userId,
|
||||
caseId: routeId((await params).caseId),
|
||||
kind,
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { rectificationV4Context, rectificationV4Error } from "../../_server";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const context = await rectificationV4Context();
|
||||
const result = await context.service.loadActive(context.userId);
|
||||
return result ? NextResponse.json(result) : new NextResponse(null, { status: 204 });
|
||||
} catch (error) {
|
||||
return rectificationV4Error(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { createCaseRequestSchema } from "@/lib/rectification-v4/contracts";
|
||||
import { calculationSpecForUser, rectificationV4Context, rectificationV4Error, requestBody } from "../_server";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await requestBody(request, createCaseRequestSchema);
|
||||
const context = await rectificationV4Context();
|
||||
return NextResponse.json(await context.service.createCase({
|
||||
userId: context.userId,
|
||||
actionId: body.actionId,
|
||||
calculationSpec: await calculationSpecForUser(context.auth, context.userId),
|
||||
}));
|
||||
} catch (error) {
|
||||
return rectificationV4Error(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
import { createRectificationV4HandoffService } from "@/lib/rectification-handoff-service";
|
||||
import {
|
||||
createRectificationV4HandoffHandlers,
|
||||
type RectificationV4HandoffRouteDependencies,
|
||||
} from "@/lib/rectification-v4/handoff-route";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
const dependencies: RectificationV4HandoffRouteDependencies = {
|
||||
async authenticate() {
|
||||
const supabase = await createServerSupabaseClient();
|
||||
const { data: { user }, error } = await supabase.auth.getUser();
|
||||
return error || !user ? null : { userId: user.id };
|
||||
},
|
||||
service() {
|
||||
return createRectificationV4HandoffService(createAdminSupabaseClient());
|
||||
},
|
||||
};
|
||||
|
||||
const handlers = createRectificationV4HandoffHandlers(dependencies);
|
||||
|
||||
export async function GET(request: Request) {
|
||||
return handlers.get(request);
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
return handlers.post(request);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { rectificationV4Context, rectificationV4Error, routeId } from "../../_server";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function GET(_request: Request, { params }: { params: Promise<{ jobId: string }> }) {
|
||||
try {
|
||||
const context = await rectificationV4Context();
|
||||
const job = await context.service.loadJob(context.userId, routeId((await params).jobId));
|
||||
return job ? NextResponse.json({ job }) : NextResponse.json({ error: "没有找到这次处理任务。" }, { status: 404 });
|
||||
} catch (error) {
|
||||
return rectificationV4Error(error);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user