Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-bom-cost-dynamic-20260807
This commit is contained in:
commit
c353757d6b
@ -0,0 +1,31 @@
|
||||
CREATE TABLE "IntakeJushuitanFixedVirtualInventoryTask" (
|
||||
"id" TEXT NOT NULL,
|
||||
"productId" TEXT NOT NULL,
|
||||
"productSpu" TEXT NOT NULL DEFAULT '',
|
||||
"skuItemId" TEXT NOT NULL,
|
||||
"skuCode" TEXT NOT NULL DEFAULT '',
|
||||
"barcode" TEXT NOT NULL DEFAULT '',
|
||||
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||
"scheduledAt" TIMESTAMP(3) NOT NULL,
|
||||
"retryCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"lastAttemptAt" TIMESTAMP(3),
|
||||
"pushedAt" TIMESTAMP(3),
|
||||
"error" TEXT NOT NULL DEFAULT '',
|
||||
"requestId" TEXT NOT NULL DEFAULT '',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "IntakeJushuitanFixedVirtualInventoryTask_pkey" PRIMARY KEY ("id"),
|
||||
CONSTRAINT "IntakeJushuitanFixedVirtualInventoryTask_productId_fkey"
|
||||
FOREIGN KEY ("productId") REFERENCES "IntakeNonIntegratedProduct"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "IntakeJushuitanFixedVirtualInventoryTask_productId_skuItemId_key"
|
||||
ON "IntakeJushuitanFixedVirtualInventoryTask"("productId", "skuItemId");
|
||||
|
||||
CREATE INDEX "IntakeJushuitanFixedVirtualInventoryTask_status_scheduledAt_idx"
|
||||
ON "IntakeJushuitanFixedVirtualInventoryTask"("status", "scheduledAt");
|
||||
|
||||
CREATE INDEX "IntakeJushuitanFixedVirtualInventoryTask_productId_idx"
|
||||
ON "IntakeJushuitanFixedVirtualInventoryTask"("productId");
|
||||
@ -256,6 +256,7 @@ model IntakeNonIntegratedProduct {
|
||||
skuItems IntakeNonIntegratedSkuItem[]
|
||||
pricingRows IntakeNonIntegratedPricingRow[]
|
||||
productLimitRows IntakeNonIntegratedProductLimitRow[]
|
||||
fixedVirtualInventoryTasks IntakeJushuitanFixedVirtualInventoryTask[]
|
||||
profile IntakeNonIntegratedProductProfile?
|
||||
modelAssets IntakeProductModelAsset[]
|
||||
|
||||
@ -671,6 +672,29 @@ model IntakeNonIntegratedPricingRow {
|
||||
@@index([systemEnteredAt, productId])
|
||||
}
|
||||
|
||||
model IntakeJushuitanFixedVirtualInventoryTask {
|
||||
id String @id @default(cuid())
|
||||
productId String
|
||||
product IntakeNonIntegratedProduct @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||
productSpu String @default("")
|
||||
skuItemId String
|
||||
skuCode String @default("")
|
||||
barcode String @default("")
|
||||
status String @default("pending")
|
||||
scheduledAt DateTime
|
||||
retryCount Int @default(0)
|
||||
lastAttemptAt DateTime?
|
||||
pushedAt DateTime?
|
||||
error String @default("")
|
||||
requestId String @default("")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([productId, skuItemId])
|
||||
@@index([status, scheduledAt])
|
||||
@@index([productId])
|
||||
}
|
||||
|
||||
model IntakeNonIntegratedProductLimitRow {
|
||||
id String @id
|
||||
productId String
|
||||
|
||||
@ -3,6 +3,7 @@ import { AppError, ErrorCode } from '@/server/core/errors'
|
||||
import { handleRequest, jsonError, jsonSuccess } from '@/server/core/response'
|
||||
import { intakeNonIntegratedProductService } from '@/server/services/intake-non-integrated-product.service'
|
||||
import { jushuitanProductPricingPushService } from '@/server/services/jushuitan-product-pricing-push.service'
|
||||
import { jushuitanFixedVirtualInventoryService } from '@/server/services/jushuitan-fixed-virtual-inventory.service'
|
||||
import {
|
||||
OperationLogAction,
|
||||
OperationLogStatus,
|
||||
@ -69,6 +70,11 @@ export async function POST(request: NextRequest, context: RouteContext) {
|
||||
product,
|
||||
targetSkuIds,
|
||||
})
|
||||
const fixedVirtualInventoryQueuedCount =
|
||||
await jushuitanFixedVirtualInventoryService.enqueueForProduct(
|
||||
product,
|
||||
result.pushedSkuIds,
|
||||
)
|
||||
|
||||
await operationLogService.record({
|
||||
action: OperationLogAction.JUSHUITAN_PRODUCT_PUSH,
|
||||
@ -84,10 +90,15 @@ export async function POST(request: NextRequest, context: RouteContext) {
|
||||
normalItemCount: result.normalItemCount,
|
||||
combineItemCount: result.combineItemCount,
|
||||
virtualInventoryPendingCount: result.virtualInventoryPendingCount,
|
||||
fixedVirtualInventoryQueuedCount,
|
||||
},
|
||||
})
|
||||
|
||||
return jsonSuccess(result, 200, ctx.requestId)
|
||||
return jsonSuccess(
|
||||
{ ...result, fixedVirtualInventoryQueuedCount },
|
||||
200,
|
||||
ctx.requestId,
|
||||
)
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
return jsonError(error.code, error.message, error.statusCode, ctx.requestId)
|
||||
|
||||
@ -14,6 +14,7 @@ import {
|
||||
applyVirtualInventoryRunFailureToRows,
|
||||
isDueVirtualInventoryRow,
|
||||
} from '@/server/services/jushuitan-virtual-inventory-queue.service'
|
||||
import { jushuitanFixedVirtualInventoryService } from '@/server/services/jushuitan-fixed-virtual-inventory.service'
|
||||
|
||||
function getCronUser(request: NextRequest): AuthUser | null {
|
||||
const cronSecret = process.env.JST_VIRTUAL_INVENTORY_CRON_SECRET?.trim()
|
||||
@ -225,6 +226,15 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
const fixedVirtualInventory =
|
||||
await jushuitanFixedVirtualInventoryService.processDue({
|
||||
actor: user,
|
||||
source: logSource,
|
||||
actorId,
|
||||
requestId: ctx.requestId,
|
||||
now,
|
||||
})
|
||||
|
||||
return jsonSuccess(
|
||||
{
|
||||
processedProductCount: processedProducts.length,
|
||||
@ -232,6 +242,7 @@ export async function POST(request: NextRequest) {
|
||||
skippedCount: processedProducts.reduce((sum, item) => sum + item.skipped, 0),
|
||||
failedCount: processedProducts.reduce((sum, item) => sum + item.failed, 0),
|
||||
processedProducts,
|
||||
fixedVirtualInventory,
|
||||
},
|
||||
200,
|
||||
ctx.requestId,
|
||||
|
||||
@ -0,0 +1,93 @@
|
||||
import type { IntakeJushuitanFixedVirtualInventoryTask } from "@prisma/client";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export type CreateFixedVirtualInventoryTaskInput = {
|
||||
productId: string;
|
||||
productSpu: string;
|
||||
skuItemId: string;
|
||||
skuCode: string;
|
||||
barcode: string;
|
||||
scheduledAt: Date;
|
||||
};
|
||||
|
||||
export type FixedVirtualInventoryRetryInput = {
|
||||
id: string;
|
||||
attemptedAt: Date;
|
||||
scheduledAt: Date;
|
||||
error: string;
|
||||
requestId: string;
|
||||
};
|
||||
|
||||
export type FixedVirtualInventorySuccessInput = {
|
||||
id: string;
|
||||
attemptedAt: Date;
|
||||
requestId: string;
|
||||
};
|
||||
|
||||
class JushuitanFixedVirtualInventoryTaskRepository {
|
||||
async createMissing(tasks: CreateFixedVirtualInventoryTaskInput[]) {
|
||||
if (tasks.length === 0) return 0;
|
||||
|
||||
const result =
|
||||
await prisma.intakeJushuitanFixedVirtualInventoryTask.createMany({
|
||||
data: tasks.map((task) => ({
|
||||
...task,
|
||||
status: "pending",
|
||||
retryCount: 0,
|
||||
error: "",
|
||||
requestId: "",
|
||||
})),
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
return result.count;
|
||||
}
|
||||
|
||||
async findDue(now: Date, take = 100) {
|
||||
return prisma.intakeJushuitanFixedVirtualInventoryTask.findMany({
|
||||
where: {
|
||||
status: "pending",
|
||||
scheduledAt: { lte: now },
|
||||
},
|
||||
orderBy: [{ scheduledAt: "asc" }, { createdAt: "asc" }],
|
||||
take,
|
||||
});
|
||||
}
|
||||
|
||||
async markSucceeded(input: FixedVirtualInventorySuccessInput) {
|
||||
return prisma.intakeJushuitanFixedVirtualInventoryTask.update({
|
||||
where: { id: input.id },
|
||||
data: {
|
||||
status: "pushed",
|
||||
pushedAt: input.attemptedAt,
|
||||
lastAttemptAt: input.attemptedAt,
|
||||
error: "",
|
||||
requestId: input.requestId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async scheduleRetry(input: FixedVirtualInventoryRetryInput) {
|
||||
return prisma.intakeJushuitanFixedVirtualInventoryTask.update({
|
||||
where: { id: input.id },
|
||||
data: {
|
||||
status: "pending",
|
||||
scheduledAt: input.scheduledAt,
|
||||
retryCount: { increment: 1 },
|
||||
lastAttemptAt: input.attemptedAt,
|
||||
error: input.error,
|
||||
requestId: input.requestId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type JushuitanFixedVirtualInventoryTaskRecord =
|
||||
IntakeJushuitanFixedVirtualInventoryTask;
|
||||
export type JushuitanFixedVirtualInventoryTaskRepositoryContract = Pick<
|
||||
JushuitanFixedVirtualInventoryTaskRepository,
|
||||
"createMissing" | "findDue" | "markSucceeded" | "scheduleRetry"
|
||||
>;
|
||||
|
||||
export const jushuitanFixedVirtualInventoryTaskRepository =
|
||||
new JushuitanFixedVirtualInventoryTaskRepository();
|
||||
@ -0,0 +1,166 @@
|
||||
import type { AuthUser } from "@/server/core/types";
|
||||
import type {
|
||||
JushuitanFixedVirtualInventoryTaskRepositoryContract,
|
||||
JushuitanFixedVirtualInventoryTaskRecord,
|
||||
} from "@/server/repositories/jushuitan-fixed-virtual-inventory-task.repository";
|
||||
import {
|
||||
OperationLogStatus,
|
||||
operationLogService,
|
||||
} from "./operation-log.service";
|
||||
import {
|
||||
buildFixedVirtualInventoryPayload,
|
||||
FIXED_VIRTUAL_INVENTORY_RETRY_DELAY_MS,
|
||||
JUSHUITAN_FIXED_VIRTUAL_INVENTORY_PATH,
|
||||
JUSHUITAN_FIXED_VIRTUAL_INVENTORY_QTY,
|
||||
JUSHUITAN_FIXED_VIRTUAL_INVENTORY_WMS_CO_ID,
|
||||
JushuitanFixedVirtualInventoryService,
|
||||
} from "./jushuitan-fixed-virtual-inventory.service";
|
||||
|
||||
function createTask(
|
||||
overrides: Partial<JushuitanFixedVirtualInventoryTaskRecord> = {},
|
||||
): JushuitanFixedVirtualInventoryTaskRecord {
|
||||
return {
|
||||
id: "task-1",
|
||||
productId: "product-1",
|
||||
productSpu: "SPU-1",
|
||||
skuItemId: "sku-1",
|
||||
skuCode: "SKU-1",
|
||||
barcode: "BARCODE-1",
|
||||
status: "pending",
|
||||
scheduledAt: new Date("2026-08-10T00:00:00.000Z"),
|
||||
retryCount: 0,
|
||||
lastAttemptAt: null,
|
||||
pushedAt: null,
|
||||
error: "",
|
||||
requestId: "",
|
||||
createdAt: new Date("2026-08-09T00:00:00.000Z"),
|
||||
updatedAt: new Date("2026-08-09T00:00:00.000Z"),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createRepository(tasks: JushuitanFixedVirtualInventoryTaskRecord[]) {
|
||||
return {
|
||||
createMissing: jest.fn(async () => 1),
|
||||
findDue: jest.fn(async () => tasks),
|
||||
markSucceeded: jest.fn(async () => undefined),
|
||||
scheduleRetry: jest.fn(async () => undefined),
|
||||
} satisfies JushuitanFixedVirtualInventoryTaskRepositoryContract;
|
||||
}
|
||||
|
||||
const actor = {
|
||||
id: "user-1",
|
||||
email: "user@example.test",
|
||||
name: "Test User",
|
||||
role: "SUPER_ADMIN",
|
||||
status: "ACTIVE",
|
||||
businessRoles: [],
|
||||
} as AuthUser;
|
||||
|
||||
describe("Jushuitan fixed virtual inventory service", () => {
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("builds the fixed warehouse and quantity payload from a barcode", () => {
|
||||
expect(buildFixedVirtualInventoryPayload("BARCODE-1")).toEqual({
|
||||
wms_co_id: JUSHUITAN_FIXED_VIRTUAL_INVENTORY_WMS_CO_ID,
|
||||
items: [
|
||||
{ sku_id: "BARCODE-1", qty: JUSHUITAN_FIXED_VIRTUAL_INVENTORY_QTY },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("enqueues only the selected sku items", async () => {
|
||||
const repository = createRepository([]);
|
||||
const service = new JushuitanFixedVirtualInventoryService(repository);
|
||||
|
||||
await service.enqueueForProduct(
|
||||
{
|
||||
id: "product-1",
|
||||
spu: "SPU-1",
|
||||
skuItems: [
|
||||
{ id: "sku-1", skuCode: "SKU-1", barcode: "BARCODE-1" } as never,
|
||||
{ id: "sku-2", skuCode: "SKU-2", barcode: "BARCODE-2" } as never,
|
||||
],
|
||||
},
|
||||
["sku-2"],
|
||||
);
|
||||
|
||||
expect(repository.createMissing).toHaveBeenCalledWith([
|
||||
expect.objectContaining({
|
||||
productId: "product-1",
|
||||
skuItemId: "sku-2",
|
||||
barcode: "BARCODE-2",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("pushes a due task and records the external request id", async () => {
|
||||
const repository = createRepository([createTask()]);
|
||||
const post = jest.fn(async () => ({
|
||||
path: JUSHUITAN_FIXED_VIRTUAL_INVENTORY_PATH,
|
||||
code: 0,
|
||||
message: "ok",
|
||||
raw: { code: 0, data: { request_id: "jst-request-1" } },
|
||||
}));
|
||||
const record = jest
|
||||
.spyOn(operationLogService, "record")
|
||||
.mockResolvedValue(undefined);
|
||||
const service = new JushuitanFixedVirtualInventoryService(repository, post);
|
||||
|
||||
const result = await service.processDue({
|
||||
actor,
|
||||
source: "SYSTEM",
|
||||
requestId: "local-request-1",
|
||||
now: new Date("2026-08-10T00:00:00.000Z"),
|
||||
});
|
||||
|
||||
expect(result).toEqual({ processed: 1, pushed: 1, failed: 0 });
|
||||
expect(post).toHaveBeenCalledWith(
|
||||
JUSHUITAN_FIXED_VIRTUAL_INVENTORY_PATH,
|
||||
buildFixedVirtualInventoryPayload("BARCODE-1"),
|
||||
);
|
||||
expect(repository.markSucceeded).toHaveBeenCalledWith({
|
||||
id: "task-1",
|
||||
attemptedAt: new Date("2026-08-10T00:00:00.000Z"),
|
||||
requestId: "jst-request-1",
|
||||
});
|
||||
expect(record).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ status: OperationLogStatus.SUCCESS }),
|
||||
);
|
||||
});
|
||||
|
||||
it("schedules every failed task exactly two hours later", async () => {
|
||||
const repository = createRepository([createTask()]);
|
||||
const post = jest.fn(async () => {
|
||||
throw new Error("Jushuitan rejected the request");
|
||||
});
|
||||
const record = jest
|
||||
.spyOn(operationLogService, "record")
|
||||
.mockResolvedValue(undefined);
|
||||
const service = new JushuitanFixedVirtualInventoryService(repository, post);
|
||||
const now = new Date("2026-08-10T00:00:00.000Z");
|
||||
|
||||
const result = await service.processDue({
|
||||
actor,
|
||||
source: "SYSTEM",
|
||||
requestId: "local-request-1",
|
||||
now,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ processed: 1, pushed: 0, failed: 1 });
|
||||
expect(repository.scheduleRetry).toHaveBeenCalledWith({
|
||||
id: "task-1",
|
||||
attemptedAt: now,
|
||||
scheduledAt: new Date(
|
||||
now.getTime() + FIXED_VIRTUAL_INVENTORY_RETRY_DELAY_MS,
|
||||
),
|
||||
error: "Jushuitan rejected the request",
|
||||
requestId: "local-request-1",
|
||||
});
|
||||
expect(record).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ status: OperationLogStatus.FAILED }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,230 @@
|
||||
import type { AuthUser } from "@/server/core/types";
|
||||
import type { NonIntegratedSkuItem } from "@/app/intake/non-integrated/nonIntegratedStorage";
|
||||
import {
|
||||
postJushuitanWithDefaultCredentials,
|
||||
type JushuitanApiResult,
|
||||
} from "./jushuitan-product-pricing-push.service";
|
||||
import {
|
||||
OperationLogAction,
|
||||
OperationLogStatus,
|
||||
operationLogService,
|
||||
} from "./operation-log.service";
|
||||
import {
|
||||
jushuitanFixedVirtualInventoryTaskRepository,
|
||||
type JushuitanFixedVirtualInventoryTaskRepositoryContract,
|
||||
type JushuitanFixedVirtualInventoryTaskRecord,
|
||||
} from "@/server/repositories/jushuitan-fixed-virtual-inventory-task.repository";
|
||||
|
||||
export const JUSHUITAN_FIXED_VIRTUAL_INVENTORY_PATH =
|
||||
"/open/webapi/itemapi/iteminventory/batchupdatewmsvirtualqtys";
|
||||
export const JUSHUITAN_FIXED_VIRTUAL_INVENTORY_WMS_CO_ID = 15994660;
|
||||
export const JUSHUITAN_FIXED_VIRTUAL_INVENTORY_QTY = 100000;
|
||||
export const FIXED_VIRTUAL_INVENTORY_RETRY_DELAY_MS = 2 * 60 * 60 * 1000;
|
||||
|
||||
type FixedVirtualInventoryProduct = {
|
||||
id: string;
|
||||
spu: string;
|
||||
skuItems: NonIntegratedSkuItem[];
|
||||
};
|
||||
|
||||
type FixedVirtualInventoryRunInput = {
|
||||
actor: AuthUser;
|
||||
source: "USER" | "SYSTEM";
|
||||
actorId?: string;
|
||||
requestId: string;
|
||||
now?: Date;
|
||||
};
|
||||
|
||||
type FixedVirtualInventoryApi = (
|
||||
path: string,
|
||||
biz: Record<string, unknown>,
|
||||
) => Promise<JushuitanApiResult>;
|
||||
|
||||
function readRequestId(raw: unknown) {
|
||||
if (!raw || typeof raw !== "object") return "";
|
||||
const record = raw as Record<string, unknown>;
|
||||
const data = record.data;
|
||||
const dataRecord =
|
||||
data && typeof data === "object" ? (data as Record<string, unknown>) : null;
|
||||
|
||||
for (const value of [
|
||||
record.request_id,
|
||||
record.requestId,
|
||||
dataRecord?.request_id,
|
||||
dataRecord?.requestId,
|
||||
]) {
|
||||
if (typeof value === "string" && value.trim()) return value.trim();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function normalizeError(error: unknown) {
|
||||
return error instanceof Error
|
||||
? error.message
|
||||
: "Jushuitan fixed virtual inventory push failed";
|
||||
}
|
||||
|
||||
function buildFixedVirtualInventoryPayload(barcode: string) {
|
||||
return {
|
||||
wms_co_id: JUSHUITAN_FIXED_VIRTUAL_INVENTORY_WMS_CO_ID,
|
||||
items: [
|
||||
{
|
||||
sku_id: barcode,
|
||||
qty: JUSHUITAN_FIXED_VIRTUAL_INVENTORY_QTY,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
class JushuitanFixedVirtualInventoryService {
|
||||
constructor(
|
||||
private readonly repository: JushuitanFixedVirtualInventoryTaskRepositoryContract = jushuitanFixedVirtualInventoryTaskRepository,
|
||||
private readonly post: FixedVirtualInventoryApi = postJushuitanWithDefaultCredentials,
|
||||
) {}
|
||||
|
||||
async enqueueForProduct(
|
||||
product: FixedVirtualInventoryProduct,
|
||||
targetSkuIds: string[],
|
||||
now = new Date(),
|
||||
) {
|
||||
const targetSkuIdSet = new Set(targetSkuIds);
|
||||
const tasks = product.skuItems
|
||||
.filter((sku) => targetSkuIdSet.has(sku.id))
|
||||
.map((sku) => ({
|
||||
productId: product.id,
|
||||
productSpu: product.spu,
|
||||
skuItemId: sku.id,
|
||||
skuCode: sku.skuCode,
|
||||
barcode: sku.barcode,
|
||||
scheduledAt: now,
|
||||
}));
|
||||
|
||||
return this.repository.createMissing(tasks);
|
||||
}
|
||||
|
||||
async processDue(input: FixedVirtualInventoryRunInput) {
|
||||
const now = input.now ?? new Date();
|
||||
const tasks = await this.repository.findDue(now);
|
||||
const result = { processed: 0, pushed: 0, failed: 0 };
|
||||
|
||||
for (const task of tasks) {
|
||||
result.processed += 1;
|
||||
if (await this.processOne(task, input)) {
|
||||
result.pushed += 1;
|
||||
} else {
|
||||
result.failed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async processOne(
|
||||
task: JushuitanFixedVirtualInventoryTaskRecord,
|
||||
input: FixedVirtualInventoryRunInput,
|
||||
) {
|
||||
const attemptedAt = input.now ?? new Date();
|
||||
const barcode = task.barcode.trim();
|
||||
if (!barcode) {
|
||||
return this.scheduleRetry(
|
||||
task,
|
||||
input,
|
||||
attemptedAt,
|
||||
"SKU barcode is empty",
|
||||
);
|
||||
}
|
||||
const payload = buildFixedVirtualInventoryPayload(barcode);
|
||||
|
||||
try {
|
||||
const response = await this.post(
|
||||
JUSHUITAN_FIXED_VIRTUAL_INVENTORY_PATH,
|
||||
payload,
|
||||
);
|
||||
const requestId = readRequestId(response.raw) || input.requestId;
|
||||
await this.repository.markSucceeded({
|
||||
id: task.id,
|
||||
attemptedAt,
|
||||
requestId,
|
||||
});
|
||||
await operationLogService.record({
|
||||
action: OperationLogAction.JUSHUITAN_VIRTUAL_INVENTORY_PUSH,
|
||||
status: OperationLogStatus.SUCCESS,
|
||||
source: input.source,
|
||||
actorId: input.actorId,
|
||||
actorName: input.actor.name,
|
||||
productId: task.productId,
|
||||
productSpu: task.productSpu,
|
||||
skuId: task.skuItemId,
|
||||
skuCode: task.skuCode,
|
||||
message: "Jushuitan fixed virtual inventory push succeeded",
|
||||
requestId,
|
||||
details: {
|
||||
flow: "fixed-wms-virtual-inventory",
|
||||
path: JUSHUITAN_FIXED_VIRTUAL_INVENTORY_PATH,
|
||||
wmsCoId: JUSHUITAN_FIXED_VIRTUAL_INVENTORY_WMS_CO_ID,
|
||||
qty: JUSHUITAN_FIXED_VIRTUAL_INVENTORY_QTY,
|
||||
barcode: task.barcode,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
return this.scheduleRetry(
|
||||
task,
|
||||
input,
|
||||
attemptedAt,
|
||||
normalizeError(error),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async scheduleRetry(
|
||||
task: JushuitanFixedVirtualInventoryTaskRecord,
|
||||
input: FixedVirtualInventoryRunInput,
|
||||
attemptedAt: Date,
|
||||
reason: string,
|
||||
) {
|
||||
const scheduledAt = new Date(
|
||||
attemptedAt.getTime() + FIXED_VIRTUAL_INVENTORY_RETRY_DELAY_MS,
|
||||
);
|
||||
await this.repository.scheduleRetry({
|
||||
id: task.id,
|
||||
attemptedAt,
|
||||
scheduledAt,
|
||||
error: reason,
|
||||
requestId: input.requestId,
|
||||
});
|
||||
await operationLogService.record({
|
||||
action: OperationLogAction.JUSHUITAN_VIRTUAL_INVENTORY_PUSH,
|
||||
status: OperationLogStatus.FAILED,
|
||||
source: input.source,
|
||||
actorId: input.actorId,
|
||||
actorName: input.actor.name,
|
||||
productId: task.productId,
|
||||
productSpu: task.productSpu,
|
||||
skuId: task.skuItemId,
|
||||
skuCode: task.skuCode,
|
||||
errorCode: "JUSHUITAN_FIXED_VIRTUAL_INVENTORY_FAILED",
|
||||
message: `Jushuitan fixed virtual inventory push failed; retry at ${scheduledAt.toISOString()}: ${reason}`,
|
||||
requestId: input.requestId,
|
||||
details: {
|
||||
flow: "fixed-wms-virtual-inventory",
|
||||
path: JUSHUITAN_FIXED_VIRTUAL_INVENTORY_PATH,
|
||||
wmsCoId: JUSHUITAN_FIXED_VIRTUAL_INVENTORY_WMS_CO_ID,
|
||||
qty: JUSHUITAN_FIXED_VIRTUAL_INVENTORY_QTY,
|
||||
barcode: task.barcode,
|
||||
scheduledAt: scheduledAt.toISOString(),
|
||||
reason,
|
||||
},
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const jushuitanFixedVirtualInventoryService =
|
||||
new JushuitanFixedVirtualInventoryService();
|
||||
|
||||
export {
|
||||
JushuitanFixedVirtualInventoryService,
|
||||
buildFixedVirtualInventoryPayload,
|
||||
};
|
||||
@ -50,7 +50,7 @@ export type JushuitanVirtualInventoryPushInput = {
|
||||
targetSkuIds: string[]
|
||||
}
|
||||
|
||||
type JushuitanApiResult = {
|
||||
export type JushuitanApiResult = {
|
||||
path: string
|
||||
code: string | number | null
|
||||
message: string
|
||||
@ -625,6 +625,13 @@ async function postJushuitan(
|
||||
}
|
||||
}
|
||||
|
||||
export async function postJushuitanWithDefaultCredentials(
|
||||
path: string,
|
||||
biz: Record<string, unknown>,
|
||||
) {
|
||||
return postJushuitan(readJushuitanCredentials(), path, biz)
|
||||
}
|
||||
|
||||
function requireText(value: string, label: string) {
|
||||
const normalizedValue = value.trim()
|
||||
if (!normalizedValue) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user