fix: show fixed virtual inventory task status
This commit is contained in:
parent
50317a3c0f
commit
6858161f3f
@ -586,6 +586,13 @@ export type NonIntegratedPricingRow = {
|
||||
jstVirtualInventoryLastAttemptAt?: string;
|
||||
jstVirtualInventoryError?: string;
|
||||
jstVirtualInventorySoId?: string;
|
||||
/** Read-only status from the separate fixed virtual-inventory task queue. */
|
||||
fixedVirtualInventoryStatus?: "pending" | "pushed";
|
||||
fixedVirtualInventoryScheduledAt?: string;
|
||||
fixedVirtualInventoryPushedAt?: string;
|
||||
fixedVirtualInventoryRetryCount?: string;
|
||||
fixedVirtualInventoryLastAttemptAt?: string;
|
||||
fixedVirtualInventoryError?: string;
|
||||
};
|
||||
|
||||
export type NonIntegratedProductLimitRow = {
|
||||
|
||||
@ -0,0 +1,57 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { ProductPricingWorkspace } from "./ProductPricingWorkspace";
|
||||
import type { ProductPricingDisplayRow } from "./product-workflow/productWorkflowRowHelpers";
|
||||
|
||||
function createPricingRow(): ProductPricingDisplayRow {
|
||||
return {
|
||||
skuId: "sku-1",
|
||||
model: "MODEL-001",
|
||||
skuCode: "SKU-001",
|
||||
barcode: "BAR-001",
|
||||
setCount: "1",
|
||||
specName: "规格一",
|
||||
supplierName: "供应商",
|
||||
costPrice: "88",
|
||||
productTag: "",
|
||||
systemEnteredAt: "2026-08-10T08:00:00.000Z",
|
||||
jstVirtualInventoryStatus: "failed",
|
||||
jstVirtualInventoryScheduledAt: "",
|
||||
jstVirtualInventoryPushedAt: "",
|
||||
jstVirtualInventoryRetryCount: "0",
|
||||
jstVirtualInventoryLastAttemptAt: "",
|
||||
jstVirtualInventoryError: "聚水潭未找到对应仓库",
|
||||
jstVirtualInventorySoId: "",
|
||||
fixedVirtualInventoryStatus: "pushed",
|
||||
fixedVirtualInventoryScheduledAt: "2026-08-10T09:00:00.000Z",
|
||||
fixedVirtualInventoryPushedAt: "2026-08-10T09:00:01.000Z",
|
||||
fixedVirtualInventoryRetryCount: "0",
|
||||
fixedVirtualInventoryLastAttemptAt: "2026-08-10T09:00:01.000Z",
|
||||
fixedVirtualInventoryError: "",
|
||||
};
|
||||
}
|
||||
|
||||
describe("ProductPricingWorkspace", () => {
|
||||
it("shows the fixed virtual inventory result before a failed legacy result", () => {
|
||||
const row = createPricingRow();
|
||||
|
||||
render(
|
||||
<ProductPricingWorkspace
|
||||
allRows={[row]}
|
||||
displayedRows={[row]}
|
||||
inheritedSkuBatchTabs={null}
|
||||
isReadOnly={false}
|
||||
isNodeLocked={false}
|
||||
canUsePushControls={false}
|
||||
isRowReadyForPush={() => true}
|
||||
onUpdateRow={jest.fn()}
|
||||
onConfirmPushRows={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("库存已推")).toHaveAttribute(
|
||||
"title",
|
||||
"固定虚拟库存已于 2026-08-10T09:00:01.000Z 推送",
|
||||
);
|
||||
expect(screen.queryByText("库存失败")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -38,6 +38,29 @@ function ReadOnlyCell({ value }: { value: string }) {
|
||||
}
|
||||
|
||||
function getVirtualInventoryStatusMeta(row: ProductPricingDisplayRow) {
|
||||
if (row.fixedVirtualInventoryStatus === "pending") {
|
||||
const retryCount = Number(row.fixedVirtualInventoryRetryCount || "0");
|
||||
return {
|
||||
label: retryCount > 0 ? "库存重试中" : "库存待推",
|
||||
tone: "bg-[#e7f1ff] text-[#1764ad]",
|
||||
title: row.fixedVirtualInventoryError
|
||||
? `固定虚拟库存推送失败,将于 ${row.fixedVirtualInventoryScheduledAt || "稍后"} 重试:${row.fixedVirtualInventoryError}`
|
||||
: row.fixedVirtualInventoryScheduledAt
|
||||
? `固定虚拟库存已入队,预计 ${row.fixedVirtualInventoryScheduledAt} 发起`
|
||||
: "固定虚拟库存已入推送队列",
|
||||
};
|
||||
}
|
||||
|
||||
if (row.fixedVirtualInventoryStatus === "pushed") {
|
||||
return {
|
||||
label: "库存已推",
|
||||
tone: "bg-[#dff7e8] text-[#137a38]",
|
||||
title: row.fixedVirtualInventoryPushedAt
|
||||
? `固定虚拟库存已于 ${row.fixedVirtualInventoryPushedAt} 推送`
|
||||
: "固定虚拟库存已推送",
|
||||
};
|
||||
}
|
||||
|
||||
const virtualInventorySkipTitle =
|
||||
row.jstVirtualInventoryError || "无需推虚拟库存";
|
||||
const normalizedVirtualInventoryError =
|
||||
|
||||
@ -6,6 +6,7 @@ import type {
|
||||
import {
|
||||
applyProductLimitWorkbookRows,
|
||||
buildProductLimitRows,
|
||||
normalizePricingRowsForPayload,
|
||||
} from "./productWorkflowRowHelpers"
|
||||
|
||||
function createSkuItem(
|
||||
@ -111,4 +112,26 @@ describe("productWorkflowRowHelpers", () => {
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("does not include fixed virtual inventory task fields in pricing saves", () => {
|
||||
expect(
|
||||
normalizePricingRowsForPayload([
|
||||
{
|
||||
skuId: "sku-test-1",
|
||||
costPrice: "88",
|
||||
productTag: "",
|
||||
fixedVirtualInventoryStatus: "pushed",
|
||||
fixedVirtualInventoryPushedAt: "2026-08-10T09:00:01.000Z",
|
||||
fixedVirtualInventoryRetryCount: "2",
|
||||
fixedVirtualInventoryError: "previous failure",
|
||||
},
|
||||
]),
|
||||
).toEqual([
|
||||
{
|
||||
skuId: "sku-test-1",
|
||||
costPrice: "88",
|
||||
productTag: "",
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@ -130,6 +130,17 @@ export function createProductPricingRowFromSku(
|
||||
jstVirtualInventoryError:
|
||||
existingRow?.jstVirtualInventoryError?.trim() ?? "",
|
||||
jstVirtualInventorySoId: existingRow?.jstVirtualInventorySoId?.trim() ?? "",
|
||||
fixedVirtualInventoryStatus: existingRow?.fixedVirtualInventoryStatus,
|
||||
fixedVirtualInventoryScheduledAt:
|
||||
existingRow?.fixedVirtualInventoryScheduledAt?.trim() ?? "",
|
||||
fixedVirtualInventoryPushedAt:
|
||||
existingRow?.fixedVirtualInventoryPushedAt?.trim() ?? "",
|
||||
fixedVirtualInventoryRetryCount:
|
||||
existingRow?.fixedVirtualInventoryRetryCount?.trim() ?? "",
|
||||
fixedVirtualInventoryLastAttemptAt:
|
||||
existingRow?.fixedVirtualInventoryLastAttemptAt?.trim() ?? "",
|
||||
fixedVirtualInventoryError:
|
||||
existingRow?.fixedVirtualInventoryError?.trim() ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -54,6 +54,22 @@ class JushuitanFixedVirtualInventoryTaskRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async findByProductId(productId: string) {
|
||||
return prisma.intakeJushuitanFixedVirtualInventoryTask.findMany({
|
||||
where: { productId },
|
||||
orderBy: [{ skuItemId: "asc" }, { createdAt: "asc" }],
|
||||
select: {
|
||||
skuItemId: true,
|
||||
status: true,
|
||||
scheduledAt: true,
|
||||
retryCount: true,
|
||||
lastAttemptAt: true,
|
||||
pushedAt: true,
|
||||
error: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async markSucceeded(input: FixedVirtualInventorySuccessInput) {
|
||||
return prisma.intakeJushuitanFixedVirtualInventoryTask.update({
|
||||
where: { id: input.id },
|
||||
@ -86,7 +102,11 @@ export type JushuitanFixedVirtualInventoryTaskRecord =
|
||||
IntakeJushuitanFixedVirtualInventoryTask;
|
||||
export type JushuitanFixedVirtualInventoryTaskRepositoryContract = Pick<
|
||||
JushuitanFixedVirtualInventoryTaskRepository,
|
||||
"createMissing" | "findDue" | "markSucceeded" | "scheduleRetry"
|
||||
| "createMissing"
|
||||
| "findDue"
|
||||
| "findByProductId"
|
||||
| "markSucceeded"
|
||||
| "scheduleRetry"
|
||||
>;
|
||||
|
||||
export const jushuitanFixedVirtualInventoryTaskRepository =
|
||||
|
||||
@ -94,6 +94,12 @@ jest.mock('../../repositories/draft-group.repository', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
jest.mock('../../repositories/jushuitan-fixed-virtual-inventory-task.repository', () => ({
|
||||
jushuitanFixedVirtualInventoryTaskRepository: {
|
||||
findByProductId: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
jest.mock('../spu-number-reservation.service', () => ({
|
||||
spuNumberReservationService: {
|
||||
reserveInTransaction: jest.fn(),
|
||||
@ -136,6 +142,7 @@ import { intakeNonIntegratedProductRepository } from '../../repositories/intake-
|
||||
import { userRepository } from '../../repositories/user.repository'
|
||||
import { assetSkuGroupRepository, assetVariantRepository } from '../../repositories/asset-center.repository'
|
||||
import { draftGroupRepository, preSkuRepository } from '../../repositories/draft-group.repository'
|
||||
import { jushuitanFixedVirtualInventoryTaskRepository } from '../../repositories/jushuitan-fixed-virtual-inventory-task.repository'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { ErrorCode } from '@/server/core/errors'
|
||||
import {
|
||||
@ -157,6 +164,9 @@ const mockedAssetSkuGroupRepository = jest.mocked(assetSkuGroupRepository)
|
||||
const mockedAssetVariantRepository = jest.mocked(assetVariantRepository)
|
||||
const mockedDraftGroupRepository = jest.mocked(draftGroupRepository)
|
||||
const mockedPreSkuRepository = jest.mocked(preSkuRepository)
|
||||
const mockedFixedVirtualInventoryTaskRepository = jest.mocked(
|
||||
jushuitanFixedVirtualInventoryTaskRepository,
|
||||
)
|
||||
const mockedSpuNumberReservationService = jest.mocked(spuNumberReservationService)
|
||||
const mockedPrisma = jest.mocked(prisma)
|
||||
const mockedNodePermissionSettingsService = jest.mocked(intakeNodePermissionSettingsService)
|
||||
@ -447,6 +457,7 @@ describe('intakeNonIntegratedProductService', () => {
|
||||
mockedRepository.findActiveRecordsBySpuInTransaction.mockResolvedValue([] as never)
|
||||
mockedNodePermissionSettingsService.getSettings.mockResolvedValue({ overrides: {} })
|
||||
mockedUserRepository.findIdsByBusinessRole.mockResolvedValue([])
|
||||
mockedFixedVirtualInventoryTaskRepository.findByProductId.mockResolvedValue([])
|
||||
})
|
||||
|
||||
it('lists all shared records when permissions are relaxed', async () => {
|
||||
@ -1237,6 +1248,53 @@ describe('intakeNonIntegratedProductService', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('returns fixed virtual inventory task status alongside the legacy status', async () => {
|
||||
mockedRepository.findActiveById.mockResolvedValue(
|
||||
createRecord({
|
||||
payload: createPayload({
|
||||
skuItems: [createSkuItem({ id: 'sku-1' })],
|
||||
pricingRows: [
|
||||
{
|
||||
skuId: 'sku-1',
|
||||
costPrice: '199.00',
|
||||
productTag: '',
|
||||
jstVirtualInventoryStatus: 'failed',
|
||||
jstVirtualInventoryError: '聚水潭未找到对应仓库',
|
||||
},
|
||||
],
|
||||
}),
|
||||
}) as never,
|
||||
)
|
||||
mockedFixedVirtualInventoryTaskRepository.findByProductId.mockResolvedValue([
|
||||
{
|
||||
skuItemId: 'sku-1',
|
||||
status: 'pushed',
|
||||
scheduledAt: new Date('2026-08-10T09:00:00.000Z'),
|
||||
retryCount: 0,
|
||||
lastAttemptAt: new Date('2026-08-10T09:00:00.000Z'),
|
||||
pushedAt: new Date('2026-08-10T09:00:01.000Z'),
|
||||
error: '',
|
||||
},
|
||||
] as never)
|
||||
|
||||
const product = await intakeNonIntegratedProductService.getById(
|
||||
'non-integrated-1',
|
||||
baseUser,
|
||||
)
|
||||
|
||||
expect(mockedFixedVirtualInventoryTaskRepository.findByProductId).toHaveBeenCalledWith(
|
||||
'non-integrated-1',
|
||||
)
|
||||
expect(product?.pricingRows).toEqual([
|
||||
expect.objectContaining({
|
||||
skuId: 'sku-1',
|
||||
jstVirtualInventoryStatus: 'failed',
|
||||
fixedVirtualInventoryStatus: 'pushed',
|
||||
fixedVirtualInventoryPushedAt: '2026-08-10T09:00:01.000Z',
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it('preserves the legacy modular tag after all pricing rows are already entered', async () => {
|
||||
mockedRepository.findActiveById.mockResolvedValue(
|
||||
createRecord({
|
||||
|
||||
@ -70,6 +70,7 @@ import {
|
||||
draftGroupRepository,
|
||||
preSkuRepository,
|
||||
} from '../repositories/draft-group.repository'
|
||||
import { jushuitanFixedVirtualInventoryTaskRepository } from '../repositories/jushuitan-fixed-virtual-inventory-task.repository'
|
||||
import {
|
||||
assertNonIntegratedProductMasterDataReadyForFormalUse,
|
||||
resolveNonIntegratedSkuQuality,
|
||||
@ -1979,6 +1980,51 @@ function buildStructuredProductRecordFromRecord(record: IntakeNonIntegratedRecor
|
||||
return applyPersistedRecordIdentity(applySharedProductTag(hydratedProduct), record)
|
||||
}
|
||||
|
||||
async function hydrateFixedVirtualInventoryStatuses(
|
||||
product: SharedNonIntegratedProductRecord,
|
||||
): Promise<SharedNonIntegratedProductRecord> {
|
||||
const tasks = await jushuitanFixedVirtualInventoryTaskRepository.findByProductId(
|
||||
product.id,
|
||||
)
|
||||
if (tasks.length === 0 || product.pricingRows.length === 0) return product
|
||||
|
||||
const taskBySkuItemId = new Map(tasks.map((task) => [task.skuItemId, task]))
|
||||
let changed = false
|
||||
const pricingRows = product.pricingRows.map((row) => {
|
||||
const task = taskBySkuItemId.get(row.skuId)
|
||||
const fixedVirtualInventoryStatus: NonIntegratedPricingRow['fixedVirtualInventoryStatus'] =
|
||||
task?.status === 'pending'
|
||||
? 'pending'
|
||||
: task?.status === 'pushed'
|
||||
? 'pushed'
|
||||
: undefined
|
||||
if (!task || !fixedVirtualInventoryStatus) {
|
||||
return row
|
||||
}
|
||||
|
||||
changed = true
|
||||
return {
|
||||
...row,
|
||||
fixedVirtualInventoryStatus,
|
||||
...(task.scheduledAt
|
||||
? { fixedVirtualInventoryScheduledAt: task.scheduledAt.toISOString() }
|
||||
: {}),
|
||||
...(task.pushedAt
|
||||
? { fixedVirtualInventoryPushedAt: task.pushedAt.toISOString() }
|
||||
: {}),
|
||||
...(task.retryCount > 0
|
||||
? { fixedVirtualInventoryRetryCount: String(task.retryCount) }
|
||||
: {}),
|
||||
...(task.lastAttemptAt
|
||||
? { fixedVirtualInventoryLastAttemptAt: task.lastAttemptAt.toISOString() }
|
||||
: {}),
|
||||
...(task.error ? { fixedVirtualInventoryError: task.error } : {}),
|
||||
}
|
||||
})
|
||||
|
||||
return changed ? { ...product, pricingRows } : product
|
||||
}
|
||||
|
||||
function buildBarcodePrintProductFromPricingRecord(record: BarcodePrintPricingRecord) {
|
||||
return buildStructuredProductRecordFromRecord({
|
||||
...record.product,
|
||||
@ -6265,7 +6311,9 @@ class IntakeNonIntegratedProductService {
|
||||
if (!scopedProduct) return null
|
||||
|
||||
const hydratedProduct = await hydrateSeriesProductDisplayName(scopedProduct)
|
||||
return hydrateProductSkuModelSnapshots(hydratedProduct)
|
||||
const productWithInventoryStatus =
|
||||
await hydrateFixedVirtualInventoryStatuses(hydratedProduct)
|
||||
return hydrateProductSkuModelSnapshots(productWithInventoryStatus)
|
||||
}
|
||||
|
||||
async getDownloadableById(
|
||||
|
||||
@ -43,6 +43,7 @@ function createRepository(tasks: JushuitanFixedVirtualInventoryTaskRecord[]) {
|
||||
return {
|
||||
createMissing: jest.fn(async () => 1),
|
||||
findDue: jest.fn(async () => tasks),
|
||||
findByProductId: jest.fn(async () => []),
|
||||
markSucceeded: jest.fn(async () => undefined),
|
||||
scheduleRetry: jest.fn(async () => undefined),
|
||||
} satisfies JushuitanFixedVirtualInventoryTaskRepositoryContract;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user