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.

184 lines
5.8 KiB

/**
* Flock fill-only 结账:选档 → 入队 RPA
*/
import { prisma } from "@/lib/prisma";
import {
validateFlockCheckoutSelection,
type FlockCheckoutTier,
type FlockFlexibilityKey,
} from "@/lib/flock/flock-checkout-selection";
import {
saveFlockCheckoutStatus,
readFlockCheckoutStatus,
} from "@/lib/flock/flock-checkout-store";
import { enqueueFlockCheckoutFillJob } from "@/modules/flock/checkout-queue";
import { parseFlockStoredQuotes } from "@/modules/flock/quote-storage";
import type { FlockCheckoutDetails } from "@/modules/flock/checkout-validation";
import type { FlockQuoteInput } from "@/workers/rpa/flock/types";
import {
assertFlockQuoteHoldUsable,
clearFlockQuoteHold,
readFlockQuoteHoldByQuoteId,
updateFlockQuoteHoldStatus,
} from "@/lib/flock/flock-quote-hold-store";
import {
FLOCK_HOLD_MSG,
isFlockQuoteHoldExpired,
shouldAutoDeclineFlockHold,
} from "@/lib/constants/flock-quote-hold";
import { declineFlockQuoteHold } from "@/modules/flock/hold-service";
function tomorrowIso(): string {
const d = new Date();
d.setDate(d.getDate() + 1);
while (d.getDay() === 0 || d.getDay() === 6) {
d.setDate(d.getDate() + 1);
}
const yyyy = d.getFullYear();
const mm = String(d.getMonth() + 1).padStart(2, "0");
const dd = String(d.getDate()).padStart(2, "0");
return `${yyyy}-${mm}-${dd}`;
}
function reconstructFlockInput(record: {
pickupJson: unknown;
deliveryJson: unknown;
weightLb: unknown;
dimLIn: unknown;
dimWIn: unknown;
dimHIn: unknown;
palletCount: number;
}): FlockQuoteInput {
const pickup =
record.pickupJson && typeof record.pickupJson === "object"
? (record.pickupJson as Record<string, unknown>)
: {};
const delivery =
record.deliveryJson && typeof record.deliveryJson === "object"
? (record.deliveryJson as Record<string, unknown>)
: {};
const pickupDate =
typeof pickup.flock_pickup_date === "string" && pickup.flock_pickup_date
? pickup.flock_pickup_date
: tomorrowIso();
return {
pickupDate,
pickupZip: String(pickup.zip ?? ""),
deliveryZip: String(delivery.zip ?? ""),
palletCount: record.palletCount,
totalWeightLb: Number(record.weightLb) || 0,
lengthIn: Number(record.dimLIn) || 48,
widthIn: Number(record.dimWIn) || 40,
heightIn: Number(record.dimHIn) || 48,
formMode: "logged_in_quick",
};
}
export async function submitFlockCheckoutFill(input: {
customerId: string;
quoteId: string;
preferredTier: FlockCheckoutTier;
preferredFlexibility?: FlockFlexibilityKey | null;
preferredCarrier?: string | null;
flockDetails?: FlockCheckoutDetails;
}): Promise<{ quote_id: string; status: "processing" }> {
const err = validateFlockCheckoutSelection({
preferredTier: input.preferredTier,
preferredFlexibility: input.preferredFlexibility,
preferredCarrier: input.preferredCarrier,
});
if (err) throw new Error(err);
const record = await prisma.quoteRecord.findUnique({
where: { quoteId: input.quoteId },
});
if (!record || record.customerId !== input.customerId) {
throw new Error("报价单不存在");
}
if (record.status !== "done") {
throw new Error("请等待初步报价完成后再结账");
}
const existing = await readFlockCheckoutStatus(input.quoteId);
if (existing?.status === "processing") {
throw new Error("结账同步进行中,请稍候");
}
const stored = parseFlockStoredQuotes(record.quotesJson);
const reference = stored?.reference ?? null;
const flockInput = reconstructFlockInput(record);
if (!flockInput.pickupZip || !flockInput.deliveryZip) {
throw new Error("报价缺少邮编信息,无法同步结账");
}
let quoteSessionId: string | null = null;
const hold = await readFlockQuoteHoldByQuoteId(input.quoteId);
if (hold && hold.customer_id === input.customerId) {
if (
(hold.status === "awaiting_decision" &&
shouldAutoDeclineFlockHold(hold)) ||
isFlockQuoteHoldExpired(hold)
) {
await declineFlockQuoteHold({
customerId: input.customerId,
quoteId: input.quoteId,
sessionId: hold.quote_session_id,
}).catch(() => undefined);
throw new Error(
isFlockQuoteHoldExpired(hold)
? FLOCK_HOLD_MSG.totalTimeoutApi
: FLOCK_HOLD_MSG.decisionTimeoutApi,
);
}
try {
await assertFlockQuoteHoldUsable(hold.quote_session_id);
await updateFlockQuoteHoldStatus(hold.quote_session_id, "checking_out");
quoteSessionId = hold.quote_session_id;
} catch {
quoteSessionId = null;
}
}
await saveFlockCheckoutStatus({
quote_id: input.quoteId,
status: "processing",
preferred_tier: input.preferredTier,
message: "正在官网选择报价档并填齐详情…",
updated_at_ms: Date.now(),
});
try {
await enqueueFlockCheckoutFillJob({
quoteId: input.quoteId,
requestId: record.requestId,
customerId: input.customerId,
preferredTier: input.preferredTier,
preferredFlexibility: input.preferredFlexibility,
preferredCarrier: input.preferredCarrier,
reference,
input: flockInput,
flockDetails: input.flockDetails,
quoteSessionId,
});
} catch (enqueueErr) {
const msg =
enqueueErr instanceof Error ? enqueueErr.message : String(enqueueErr);
if (/already exists|JobId/i.test(msg)) {
throw new Error("结账同步进行中,请稍候");
}
await saveFlockCheckoutStatus({
quote_id: input.quoteId,
status: "failed",
preferred_tier: input.preferredTier,
message: "官网同步失败,请稍后重试",
updated_at_ms: Date.now(),
});
if (quoteSessionId) {
await clearFlockQuoteHold(quoteSessionId).catch(() => undefined);
}
throw enqueueErr;
}
return { quote_id: input.quoteId, status: "processing" };
}