fix(report): load candidate ranges through a read-only RPC
Accepted profiles were selecting a revoked rectification table and failing the report before the engine ran. Degrade to a null window when that optional read fails. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -24,6 +24,7 @@ import {
|
||||
type PersonalReportService,
|
||||
} from "@/lib/personal-report-service";
|
||||
import { createSupabasePersonalReportJobService } from "@/lib/personal-report-job-service";
|
||||
import { loadReportCandidateRange } from "@/lib/report-candidate-range";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { authorizeUsage, completeUsage, releaseUsage } from "@/lib/consultation-billing";
|
||||
import { FeaturePricingError, resolveFeaturePricing } from "@/lib/feature-pricing";
|
||||
@@ -187,36 +188,10 @@ export async function POST(request: Request) {
|
||||
const caseId = typeof profileRow.rectification_case_id === "string"
|
||||
? profileRow.rectification_case_id
|
||||
: null;
|
||||
if (caseId) {
|
||||
const { data, error } = await admin
|
||||
.from("birth_time_rectification_cases")
|
||||
.select("candidate_start,candidate_end")
|
||||
.eq("id", caseId)
|
||||
.eq("user_id", userId as string)
|
||||
.in("status", ["confirmed", "completed"])
|
||||
.maybeSingle();
|
||||
if (error) throw error;
|
||||
const row = data && typeof data === "object" ? data as Record<string, unknown> : null;
|
||||
if (row && typeof row.candidate_start === "string" && typeof row.candidate_end === "string") {
|
||||
return { startTime: row.candidate_start, endTime: row.candidate_end };
|
||||
}
|
||||
}
|
||||
const { data, error } = await admin
|
||||
.from("agentic_rectification_cases")
|
||||
.select("candidate_range,updated_at")
|
||||
.eq("user_id", userId as string)
|
||||
.eq("status", "candidate_accepted")
|
||||
.order("updated_at", { ascending: false })
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
if (error) throw error;
|
||||
const row = data && typeof data === "object" ? data as Record<string, unknown> : null;
|
||||
const range = row?.candidate_range && typeof row.candidate_range === "object"
|
||||
? row.candidate_range as Record<string, unknown>
|
||||
: null;
|
||||
return range && typeof range.start_time === "string" && typeof range.end_time === "string"
|
||||
? { startTime: range.start_time, endTime: range.end_time }
|
||||
: null;
|
||||
return loadReportCandidateRange(admin, {
|
||||
userId: userId as string,
|
||||
rectificationCaseId: caseId,
|
||||
});
|
||||
},
|
||||
checkSessionOwned: async (sessionId) => {
|
||||
const { data, error } = await supabase
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
resolveReportBirthClock,
|
||||
resolveReportBirthTimeSensitivityInput,
|
||||
} from "@/lib/personal-report-route-core";
|
||||
import { loadReportCandidateRange } from "@/lib/report-candidate-range";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { completeUsage, releaseUsage } from "@/lib/consultation-billing";
|
||||
import type { ConsultationInput } from "@/mastra/consultation-workflow";
|
||||
@@ -256,39 +257,11 @@ function createProductionWorker(workerId: string) {
|
||||
if (resolveReportBirthClock(profile)?.status === "confirmed") {
|
||||
return generateProductionReport(context);
|
||||
}
|
||||
const caseId = text(profile.rectification_case_id);
|
||||
if (caseId) {
|
||||
const { data, error } = await admin
|
||||
.from("birth_time_rectification_cases")
|
||||
.select("candidate_start,candidate_end")
|
||||
.eq("id", caseId)
|
||||
.eq("user_id", context.report.userId)
|
||||
.in("status", ["confirmed", "completed"])
|
||||
.maybeSingle();
|
||||
if (error) throw new PersonalReportWorkerError("calculation_unavailable", true);
|
||||
const row = record(data);
|
||||
if (typeof row?.candidate_start === "string" && typeof row.candidate_end === "string") {
|
||||
return generateProductionReport(context, {
|
||||
startTime: row.candidate_start,
|
||||
endTime: row.candidate_end,
|
||||
});
|
||||
}
|
||||
}
|
||||
const { data, error } = await admin
|
||||
.from("agentic_rectification_cases")
|
||||
.select("candidate_range,updated_at")
|
||||
.eq("user_id", context.report.userId)
|
||||
.eq("status", "candidate_accepted")
|
||||
.order("updated_at", { ascending: false })
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
if (error) throw new PersonalReportWorkerError("calculation_unavailable", true);
|
||||
const range = record(record(data)?.candidate_range);
|
||||
return generateProductionReport(context, range
|
||||
&& typeof range.start_time === "string"
|
||||
&& typeof range.end_time === "string"
|
||||
? { startTime: range.start_time, endTime: range.end_time }
|
||||
: null);
|
||||
const range = await loadReportCandidateRange(admin, {
|
||||
userId: context.report.userId,
|
||||
rectificationCaseId: text(profile.rectification_case_id),
|
||||
});
|
||||
return generateProductionReport(context, range);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
export const READ_REPORT_CANDIDATE_RANGE_RPC = "read_report_candidate_range";
|
||||
|
||||
export type ReportCandidateClockRange = Readonly<{
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
}>;
|
||||
|
||||
export type ReportCandidateRangeRpcClient = Readonly<{
|
||||
rpc: (
|
||||
fn: string,
|
||||
args: Record<string, unknown>,
|
||||
) => PromiseLike<{ data: unknown; error: unknown }>;
|
||||
}>;
|
||||
|
||||
export type ReportCandidateRangeWarn = (payload: Readonly<{
|
||||
event: "report_candidate_range_unavailable";
|
||||
reason: string;
|
||||
}>) => void;
|
||||
|
||||
const candidateClockPattern = /^((?:[01]\d|2[0-3]):[0-5]\d)(?::00(?:\.0+)?)?$/;
|
||||
|
||||
export function reportCandidateRangeErrorCode(error: unknown): string {
|
||||
if (error && typeof error === "object" && "code" in error) {
|
||||
const code = error.code;
|
||||
if (typeof code === "string" && code.trim().length > 0) return code.trim();
|
||||
}
|
||||
if (error instanceof Error && error.name.trim().length > 0) return error.name;
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
export function parseReportCandidateRange(value: unknown): ReportCandidateClockRange | null {
|
||||
const row = value !== null && typeof value === "object" && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: null;
|
||||
if (!row) return null;
|
||||
const startTime = matchClock(row.start_time ?? row.startTime);
|
||||
const endTime = matchClock(row.end_time ?? row.endTime);
|
||||
if (!startTime || !endTime || startTime > endTime) return null;
|
||||
return { startTime, endTime };
|
||||
}
|
||||
|
||||
export async function loadReportCandidateRange(
|
||||
client: ReportCandidateRangeRpcClient,
|
||||
input: Readonly<{ userId: string; rectificationCaseId?: string | null }>,
|
||||
warn: ReportCandidateRangeWarn = defaultWarn,
|
||||
): Promise<ReportCandidateClockRange | null> {
|
||||
try {
|
||||
const result = await client.rpc(READ_REPORT_CANDIDATE_RANGE_RPC, {
|
||||
p_user_id: input.userId,
|
||||
p_rectification_case_id: input.rectificationCaseId ?? null,
|
||||
});
|
||||
if (result.error) {
|
||||
warn({
|
||||
event: "report_candidate_range_unavailable",
|
||||
reason: reportCandidateRangeErrorCode(result.error),
|
||||
});
|
||||
return null;
|
||||
}
|
||||
return parseReportCandidateRange(result.data);
|
||||
} catch (error) {
|
||||
warn({
|
||||
event: "report_candidate_range_unavailable",
|
||||
reason: reportCandidateRangeErrorCode(error),
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function matchClock(value: unknown): string | null {
|
||||
if (typeof value !== "string") return null;
|
||||
return value.trim().match(candidateClockPattern)?.[1] ?? null;
|
||||
}
|
||||
|
||||
function defaultWarn(payload: Readonly<{ event: string; reason: string }>): void {
|
||||
console.warn(JSON.stringify(payload));
|
||||
}
|
||||
Reference in New Issue
Block a user