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
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" };
|
|
}
|