fix: preserve component detail spu on stock conflicts

This commit is contained in:
WuYongJie 2026-06-04 15:33:10 +08:00
parent e4809ef843
commit 7f41d0bca5
2 changed files with 114 additions and 6 deletions

View File

@ -49,6 +49,7 @@ jest.mock('../../repositories', () => ({
},
intakeNonIntegratedProductRepository: {
listActive: jest.fn(),
findBySpu: jest.fn(),
updatePayloadByUser: jest.fn(),
updateAttachmentPayloadByUser: jest.fn(),
},
@ -173,6 +174,7 @@ describe('assetCenterService', () => {
mockedAssetVariantRepository.findActiveBySeriesCode.mockResolvedValue([] as never)
mockedAssetVariantRepository.releaseDeletedUniqueKeys.mockResolvedValue(undefined as never)
mockedIntakeNonIntegratedProductRepository.listActive.mockResolvedValue([] as never)
mockedIntakeNonIntegratedProductRepository.findBySpu.mockResolvedValue(null as never)
mockedIntakeNonIntegratedProductRepository.updatePayloadByUser.mockResolvedValue({} as never)
mockedIntakeNonIntegratedProductRepository.updateAttachmentPayloadByUser.mockResolvedValue({} as never)
mockedSubstrateRepository.findById.mockResolvedValue({
@ -365,6 +367,90 @@ describe('assetCenterService', () => {
}
})
it('preserves the persisted component detail SPU when the configured SPU is occupied by stock data', async () => {
const syncSpy = jest
.spyOn(intakeNonIntegratedProductService, 'syncSeriesComponentDetailProductForSystem')
.mockResolvedValue(undefined)
const configuredSeries = {
...defaultAssetSeries,
metadata: {
componentSpu: 'DNZ0384',
finishedProductSpu: 'DNZ0385',
},
}
mockedAssetSeriesRepository.findById.mockResolvedValueOnce(configuredSeries as never)
mockedIntakeNonIntegratedProductRepository.listActive.mockResolvedValueOnce([
{
id: 'DBZ-SPJ-1780539433902',
spu: 'PJ0039',
brand: '其他品牌-普通大货',
updatedBy: 'user-1',
payload: {
id: 'DBZ-SPJ-1780539433902',
spu: 'PJ0039',
productCode: 'PJ0039',
productDevelopmentKind: 'series',
metadata: {
componentDetailType: 'asset-series-components',
sourceSeriesCode: 'DBZ',
},
openingReference: {
productName: '单板桌 部件开发明细',
temporaryCode: 'DBZ-SPJ',
},
skuItems: [],
},
},
] as never)
mockedIntakeNonIntegratedProductRepository.findBySpu.mockResolvedValueOnce({
id: 'jst-stock-1c617912f7581c50',
spu: 'DNZ0384',
brand: '其他品牌-普通大货',
} as never)
mockedAssetVariantRepository.findActiveBySeriesCode.mockResolvedValueOnce([] as never)
try {
const result = await assetCenterService.ensureSeriesComponentDetailProduct(
configuredSeries.id,
{ id: 'user-2' } as never,
)
expect(result).toMatchObject({
productId: 'DBZ-SPJ-1780539433902',
spu: 'DNZ0384',
componentSpu: 'DNZ0384',
finishedProductSpu: 'DNZ0385',
created: false,
})
expect(mockedIntakeNonIntegratedProductRepository.findBySpu).toHaveBeenCalledWith(
'DNZ0384',
'DBZ-SPJ-1780539433902',
'其他品牌-普通大货',
)
expect(syncSpy).toHaveBeenCalledWith(
expect.objectContaining({
productId: 'DBZ-SPJ-1780539433902',
updatedBy: 'user-2',
productInput: expect.objectContaining({
id: 'DBZ-SPJ-1780539433902',
spu: 'PJ0039',
productCode: 'DNZ0384',
metadata: expect.objectContaining({
componentSpu: 'DNZ0384',
finishedProductSpu: 'DNZ0385',
sourceSeriesCode: 'DBZ',
}),
openingReference: expect.objectContaining({
temporaryCode: '',
}),
}),
}),
)
} finally {
syncSpy.mockRestore()
}
})
it('配置部件SPU后允许按该SPU保存部件型号和规格款', async () => {
const customSeries = {
...defaultAssetSeries,

View File

@ -1080,8 +1080,9 @@ function buildSeriesComponentDetailProductPayload(input: {
}
function buildSeriesComponentDetailProductPayloadFromExistingRecord(input: {
record: { id: string; payload: Prisma.JsonValue }
record: { id: string; payload: Prisma.JsonValue; spu?: string | null }
series: AssetSeriesDTO
preservePersistedSpu?: boolean
}): Record<string, unknown> {
const payload = isRecord(input.record.payload) ? input.record.payload : {}
const metadata = isRecord(payload.metadata) ? payload.metadata : {}
@ -1093,6 +1094,11 @@ function buildSeriesComponentDetailProductPayloadFromExistingRecord(input: {
const productName = buildSeriesComponentProductName(input.series)
const existingPayloadSpu =
typeof payload.spu === 'string' ? payload.spu.trim().toUpperCase() : ''
const existingRecordSpu =
typeof input.record.spu === 'string' ? input.record.spu.trim().toUpperCase() : ''
const persistedSpu = input.preservePersistedSpu
? existingPayloadSpu || existingRecordSpu || spu
: spu
const existingComponentSpu =
typeof metadata.componentSpu === 'string'
? metadata.componentSpu.trim().toUpperCase()
@ -1115,7 +1121,7 @@ function buildSeriesComponentDetailProductPayloadFromExistingRecord(input: {
componentSpu: spu,
finishedProductSpu,
},
spu,
spu: persistedSpu,
productCode: spu,
openingReference: {
...openingReference,
@ -2474,6 +2480,13 @@ class AssetCenterService {
const existingSummary = readSeriesComponentDetailSummaryFromRecord(existingRecord, false)
const existingRecordSpu = existingRecord.spu.trim().toUpperCase()
const existingSummarySpu = existingSummary?.spu.trim().toUpperCase() ?? existingRecordSpu
const conflictingSpuRecord = await intakeNonIntegratedProductRepository.findBySpu(
spu,
existingRecord.id,
typeof existingRecord.brand === 'string' ? existingRecord.brand : undefined,
)
const preservePersistedSpu = Boolean(conflictingSpuRecord)
const expectedPersistedSpu = preservePersistedSpu ? existingRecordSpu : spu
const existingComponentSpu = existingSummary?.componentSpu ?? null
const existingFinishedProductSpu = existingSummary?.finishedProductSpu ?? null
const payload = isRecord(existingRecord.payload) ? existingRecord.payload : null
@ -2498,8 +2511,8 @@ class AssetCenterService {
)
const alreadySynced =
existingRecordSpu === spu &&
existingSummarySpu === spu &&
existingRecordSpu === expectedPersistedSpu &&
existingSummarySpu === expectedPersistedSpu &&
existingComponentSpu === spu &&
existingFinishedProductSpu === finishedProductSpu &&
!existingTemporaryCodeIsSpu
@ -2510,6 +2523,7 @@ class AssetCenterService {
productInput: buildSeriesComponentDetailProductPayloadFromExistingRecord({
record: existingRecord,
series: input.series,
preservePersistedSpu,
}),
updatedBy: updatedById,
})
@ -2678,6 +2692,13 @@ class AssetCenterService {
const existingSummary = readSeriesComponentDetailSummaryFromRecord(existingRecord, false)
const existingRecordSpu = existingRecord.spu.trim().toUpperCase()
const existingSummarySpu = existingSummary?.spu.trim().toUpperCase() ?? existingRecordSpu
const conflictingSpuRecord = await intakeNonIntegratedProductRepository.findBySpu(
spu,
existingRecord.id,
typeof existingRecord.brand === 'string' ? existingRecord.brand : undefined,
)
const preservePersistedSpu = Boolean(conflictingSpuRecord)
const expectedPersistedSpu = preservePersistedSpu ? existingRecordSpu : spu
const existingComponentSpu = existingSummary?.componentSpu ?? null
const existingFinishedProductSpu = existingSummary?.finishedProductSpu ?? null
const payload = isRecord(existingRecord.payload) ? existingRecord.payload : null
@ -2701,8 +2722,8 @@ class AssetCenterService {
.some((item) => item.trim().toUpperCase() === existingTemporaryCode),
)
if (
existingRecordSpu !== spu ||
existingSummarySpu !== spu ||
existingRecordSpu !== expectedPersistedSpu ||
existingSummarySpu !== expectedPersistedSpu ||
existingComponentSpu !== spu ||
existingFinishedProductSpu !== finishedProductSpu ||
existingTemporaryCodeIsSpu
@ -2712,6 +2733,7 @@ class AssetCenterService {
productInput: buildSeriesComponentDetailProductPayloadFromExistingRecord({
record: existingRecord,
series,
preservePersistedSpu,
}),
updatedBy: user.id,
})