You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

177 lines
4.7 KiB

import { Prisma } from "@prisma/client";
import { parseAdminAuth } from "@/lib/api/admin-auth-context";
import { QUOTE_VALIDITY_MS } from "@/lib/constants/quote";
import { prisma } from "@/lib/prisma";
import { fail, ok } from "@/lib/response";
import { writeAlert } from "@/modules/alert/service";
import { writeAudit } from "@/modules/audit/service";
import { AuthError } from "@/modules/auth/errors";
import { requirePermission } from "@/modules/auth/rbac";
import {
applyMarkupToQuotes,
getMarkupRule,
type RawQuoteTier,
} from "@/modules/pricing/engine";
import { getConfidenceScore } from "@/modules/quote/confidence";
import { assertFourTiers } from "@/modules/quote/quote-completeness";
import { overwriteL1, setL3 } from "@/modules/cache/redis-cache";
type TierInput = {
service_level?: unknown;
rate_option?: unknown;
carrier?: unknown;
transit_days?: unknown;
transit_description?: unknown;
raw_freight?: unknown;
surcharges?: unknown;
raw_total?: unknown;
};
type ManualFallbackBody = {
quote_id?: unknown;
quotes?: unknown;
};
function parseTier(raw: TierInput): RawQuoteTier | null {
if (
typeof raw.service_level !== "string" ||
typeof raw.rate_option !== "string" ||
typeof raw.carrier !== "string" ||
typeof raw.transit_days !== "string" ||
typeof raw.transit_description !== "string" ||
typeof raw.raw_freight !== "number" ||
typeof raw.surcharges !== "number" ||
typeof raw.raw_total !== "number"
) {
return null;
}
if (raw.raw_freight <= 0 || raw.raw_total <= 0) {
return null;
}
return {
service_level: raw.service_level,
rate_option: raw.rate_option,
carrier: raw.carrier,
transit_days: raw.transit_days,
transit_description: raw.transit_description,
raw_freight: raw.raw_freight,
surcharges: raw.surcharges,
raw_total: raw.raw_total,
};
}
export async function POST(request: Request) {
let auth;
try {
auth = parseAdminAuth(request);
requirePermission(auth, "rpa:operate");
} catch (error) {
if (error instanceof AuthError) {
return fail(error.code, error.message, error.httpStatus);
}
throw error;
}
let body: ManualFallbackBody;
try {
body = (await request.json()) as ManualFallbackBody;
} catch {
return fail("VALIDATION_FAILED", "请求体格式无效", 400);
}
const quoteId =
typeof body.quote_id === "string" ? body.quote_id.trim() : "";
if (!quoteId) {
return fail("VALIDATION_FAILED", "请填写 quote_id", 400);
}
if (!Array.isArray(body.quotes) || body.quotes.length !== 4) {
return fail("VALIDATION_FAILED", "请提供完整四档报价", 400);
}
const rawQuotes: RawQuoteTier[] = [];
for (const item of body.quotes) {
const tier = parseTier(item as TierInput);
if (!tier) {
return fail("VALIDATION_FAILED", "报价格式无效或金额须大于 0", 400);
}
rawQuotes.push(tier);
}
try {
assertFourTiers(rawQuotes);
} catch (error) {
const message =
error instanceof Error ? error.message : "四档报价不完整";
return fail("VALIDATION_FAILED", message, 400);
}
const record = await prisma.quoteRecord.findUnique({
where: { quoteId },
select: {
quoteId: true,
requestId: true,
customerId: true,
cargoHash: true,
status: true,
},
});
if (!record) {
return fail("QUOTE_NOT_FOUND", "询价记录不存在", 404);
}
const markupRule = await getMarkupRule(record.customerId);
const markedQuotes = applyMarkupToQuotes(rawQuotes, markupRule);
const validUntil = new Date(Date.now() + QUOTE_VALIDITY_MS);
const cachePayload = { quotes: rawQuotes };
await setL3(record.cargoHash, cachePayload);
await prisma.quoteRecord.update({
where: { quoteId },
data: {
status: "done",
sourceType: "stale",
isRealtime: false,
confidenceScore: getConfidenceScore("stale"),
markupPercent:
markupRule.type === "percent" ? markupRule.percent : 0,
quotesJson: markedQuotes as unknown as Prisma.InputJsonValue,
validUntil,
errorCode: null,
},
});
await overwriteL1(record.requestId, {
quoteId: record.quoteId,
response: {
quote_id: record.quoteId,
status: "done",
source_type: "stale",
is_realtime: false,
},
});
await writeAudit("quotes:manual_fallback", auth.userId, quoteId, {
cargo_hash: record.cargoHash,
customer_id: record.customerId,
});
await writeAlert("MANUAL_FALLBACK", {
quoteId,
cargoHash: record.cargoHash,
detail: {
operator_id: auth.userId,
previous_status: record.status,
},
});
return ok({
quote_id: quoteId,
status: "done",
source_type: "stale",
is_realtime: false,
});
}