Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/xuxiake-mainline

This commit is contained in:
WuYongJie 2026-06-05 12:07:29 +08:00
commit 6cc339cdb5
2 changed files with 231 additions and 10 deletions

View File

@ -922,12 +922,12 @@ describe('intakeNonIntegratedProductService', () => {
)
})
it('分配正式 SPU 前必须补齐 SKU 主数据', async () => {
it('分配正式 SPU 时允许 SKU 主数据在后续节点补齐', async () => {
const payload = createPayload({
id: 'draft-incomplete-before-formal-spu',
spu: '',
productCategory: '餐桌',
productDevelopmentKind: '',
productDevelopmentKind: 'product',
openingReference: {
productCategory: '餐桌',
},
@ -949,6 +949,60 @@ describe('intakeNonIntegratedProductService', () => {
})
mockedRepository.findDeletedBySpuInTransaction.mockResolvedValue(null as never)
mockedRepository.findBySpuInTransaction.mockResolvedValue(null as never)
mockedRepository.createInTransaction.mockResolvedValue(
createRecord({ id: 'CZ0011-1770000000000' }) as never,
)
mockedRepository.findActiveById.mockResolvedValue(
createRecord({
id: 'CZ0011-1770000000000',
spu: 'CZ0011',
spuBase: 'CZ0011',
payload: createPayload({ id: 'CZ0011-1770000000000', spu: 'CZ0011' }),
}) as never,
)
await intakeNonIntegratedProductService.save(payload, baseUser, {
formalSpuAssignmentProductCategory: '餐桌',
})
const createArg = mockedRepository.createInTransaction.mock.calls[0]?.[1] as {
skuItems?: { createMany?: { data?: Array<Record<string, unknown>> } }
}
expect(createArg.skuItems?.createMany?.data?.[0]).toEqual(
expect.objectContaining({
skuCode: 'CZ001101',
barcode: 'CZ001101N01V01',
model: '',
specName: '',
}),
)
})
it('分配正式 SPU 前仍必须补齐产品主数据', async () => {
const payload = createPayload({
id: 'draft-incomplete-product-before-formal-spu',
spu: '',
productCategory: '',
productDevelopmentKind: 'product',
openingReference: {
productCategory: '',
},
skuItems: [
createSkuItem({
skuSequence: 1,
skuCode: '',
barcode: '',
}),
],
})
mockedSpuNumberReservationService.reserveInTransaction.mockResolvedValue({
productCategory: '餐桌',
prefix: 'CZ',
index: 12,
spu: 'CZ0012',
})
mockedRepository.findDeletedBySpuInTransaction.mockResolvedValue(null as never)
mockedRepository.findBySpuInTransaction.mockResolvedValue(null as never)
const promise = intakeNonIntegratedProductService.save(payload, baseUser, {
formalSpuAssignmentProductCategory: '餐桌',
@ -957,7 +1011,7 @@ describe('intakeNonIntegratedProductService', () => {
await expect(promise).rejects.toMatchObject({
code: ErrorCode.VALIDATION_REQUIRED_FIELD,
})
await expect(promise).rejects.toThrow('正式 SPU 分配存在未补齐的 SKU 主数据')
await expect(promise).rejects.toThrow('正式 SPU 分配存在未补齐的产品主数据')
expect(mockedRepository.createInTransaction).not.toHaveBeenCalled()
expect(mockedRepository.updateInTransaction).not.toHaveBeenCalled()
@ -1872,6 +1926,148 @@ describe('intakeNonIntegratedProductService', () => {
expect(mockedRepository.update).toHaveBeenCalled()
})
it('keeps downstream factory rows when a designer batch save carries derived factory changes', async () => {
const existingFactoryRows = [
{
skuId: 'sku-1',
confirmed: false,
model: 'K01',
skuCode: 'DNZ000101',
setCount: '1',
barcode: 'DNZ000101N1280V01',
specName: '款式一',
size: '',
color: '',
material: '',
installMethod: '',
supplierName: '供应商A',
supplierCode: 'N1280',
warehouse: '',
minimumOrderQuantity: '',
leadTime: '',
assemblyMode: '',
productionType: '',
installationService: '',
supplierCategory: '',
purchaseOrderMethod: '',
executeStandard: '',
},
]
const existingPayload = createPayload({
designer: 'Creator',
openingReference: {
designer: 'Creator',
structureDesigner: 'Creator',
},
skuItems: [createSkuItem({ id: 'sku-1', skuCode: 'DNZ000101' })],
skuBatchCount: 1,
skuBatchFlowNos: {},
skuBatchChangeTypes: {},
factoryRows: existingFactoryRows,
})
const incomingPayload = createPayload({
...existingPayload,
skuBatchCount: 2,
skuBatchFlowNos: { '2': 'FLOW-002' },
skuBatchChangeTypes: { '2': '新增' },
factoryRows: existingFactoryRows.map((row) => ({
...row,
supplierName: '供应商B',
})),
})
mockedRepository.findBySpu.mockResolvedValue(null as never)
mockedRepository.findById.mockResolvedValue(
createRecord({ payload: existingPayload }) as never,
)
mockedRepository.update.mockResolvedValue(
createRecord({ payload: incomingPayload }) as never,
)
mockedRepository.findActiveById.mockResolvedValue(
createRecord({ payload: incomingPayload }) as never,
)
await intakeNonIntegratedProductService.save(
incomingPayload,
baseUser,
{ workspaceCategory: '产品定厂' },
)
const updatePayload = mockedRepository.update.mock.calls[0]?.[1]?.payload as
| Record<string, unknown>
| undefined
expect(updatePayload).toEqual(
expect.objectContaining({
skuBatchCount: 2,
skuBatchFlowNos: { '2': 'FLOW-002' },
skuBatchChangeTypes: { '2': '新增' },
factoryRows: existingFactoryRows,
}),
)
})
it('treats writable workspace autosaves with only downstream derived changes as no-ops', async () => {
const existingFactoryRows = [
{
skuId: 'sku-1',
confirmed: false,
model: 'K01',
skuCode: 'DNZ000101',
setCount: '1',
barcode: 'DNZ000101N1280V01',
specName: '款式一',
size: '',
color: '',
material: '',
installMethod: '',
supplierName: '供应商A',
supplierCode: 'N1280',
warehouse: '',
minimumOrderQuantity: '',
leadTime: '',
assemblyMode: '',
productionType: '',
installationService: '',
supplierCategory: '',
purchaseOrderMethod: '',
executeStandard: '',
},
]
const existingPayload = createPayload({
designer: 'Creator',
openingReference: {
designer: 'Creator',
structureDesigner: 'Creator',
},
factoryRows: existingFactoryRows,
})
const incomingPayload = createPayload({
...existingPayload,
factoryRows: existingFactoryRows.map((row) => ({
...row,
supplierName: '供应商B',
})),
})
mockedRepository.findBySpu.mockResolvedValue(null as never)
mockedRepository.findById.mockResolvedValue(
createRecord({ payload: existingPayload }) as never,
)
mockedRepository.findActiveById.mockResolvedValue(
createRecord({ payload: existingPayload }) as never,
)
await expect(
intakeNonIntegratedProductService.save(
incomingPayload,
baseUser,
{ workspaceCategory: '基础信息' },
),
).resolves.toEqual(expect.objectContaining({ id: 'non-integrated-1' }))
expect(mockedRepository.update).not.toHaveBeenCalled()
})
it('allows space visual department users to save planning layout through the shared save API', async () => {
const skuItems = [
{

View File

@ -40,7 +40,7 @@ import {
preSkuRepository,
} from '../repositories/draft-group.repository'
import {
assertNonIntegratedProductMasterDataReadyForFormalUse,
assertNonIntegratedProductProfileReadyForFormalUse,
resolveNonIntegratedSkuQuality,
} from './non-integrated-sku-quality'
import {
@ -1455,6 +1455,25 @@ function getRequestedWritableCategories(
return category ? new Set<NonIntegratedWorkspaceCategoryKey>([category]) : fallbackCategories
}
function getWritableChangedCategories(
changedCategories: Set<NonIntegratedWorkspaceCategoryKey>,
writableCategories: Set<NonIntegratedWorkspaceCategoryKey>,
) {
return new Set(
Array.from(changedCategories).filter((category) =>
writableCategories.has(category),
),
)
}
function canTreatRequestedWorkspaceSaveAsNoop(
requestedCategory: string | null | undefined,
writableCategories: Set<NonIntegratedWorkspaceCategoryKey>,
) {
const category = toDocumentWorkspaceCategory(requestedCategory ?? undefined)
return Boolean(category && writableCategories.has(category))
}
function isExplicitPlanningLayoutWriteRequest(
requestedCategory: string | null | undefined,
) {
@ -1618,6 +1637,10 @@ async function prepareProductForPermittedWrite(
deniedChangedCategories.push(category)
}
const effectivePermittedCategories =
permittedCategories.size > 0
? permittedCategories
: getWritableChangedCategories(changedCategories, writableCategories)
const preserveOptions = {
allowProductFactorySkuSync:
@ -1629,7 +1652,7 @@ async function prepareProductForPermittedWrite(
existingProduct,
nextProduct,
changedCategories,
permittedCategories,
effectivePermittedCategories,
preserveOptions,
)
) {
@ -1638,6 +1661,11 @@ async function prepareProductForPermittedWrite(
options.workspaceCategory,
writableCategories,
) ||
(effectivePermittedCategories.size === 0 &&
canTreatRequestedWorkspaceSaveAsNoop(
options.workspaceCategory,
writableCategories,
)) ||
canTreatStockReadonlyCompatibilitySaveAsNoop(
isStockProduct,
changedCategories,
@ -1664,10 +1692,7 @@ async function prepareProductForPermittedWrite(
}
const categoriesToPreserve = TRACKED_WORKSPACE_CATEGORIES.filter(
(category) =>
!writableCategories.has(category) ||
!requestedWritableCategories.has(category) ||
!changedCategories.has(category),
(category) => !effectivePermittedCategories.has(category),
)
const scopedNextProduct = categoriesToPreserve.reduce(
(currentProduct, category) =>
@ -4149,7 +4174,7 @@ class IntakeNonIntegratedProductService {
if (!normalizedProduct) {
throw new AppError(ErrorCode.VALIDATION_FAILED, '非集成产品数据不完整,无法保存')
}
assertNonIntegratedProductMasterDataReadyForFormalUse(normalizedProduct, {
assertNonIntegratedProductProfileReadyForFormalUse(normalizedProduct, {
usageLabel: '正式 SPU 分配',
})