diff --git a/furniture-3d-system/src/server/services/__tests__/asset-center.service.test.ts b/furniture-3d-system/src/server/services/__tests__/asset-center.service.test.ts index 7a6cfcf8..961d633e 100644 --- a/furniture-3d-system/src/server/services/__tests__/asset-center.service.test.ts +++ b/furniture-3d-system/src/server/services/__tests__/asset-center.service.test.ts @@ -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, diff --git a/furniture-3d-system/src/server/services/asset-center.service.ts b/furniture-3d-system/src/server/services/asset-center.service.ts index 7047bb27..ec2f248e 100644 --- a/furniture-3d-system/src/server/services/asset-center.service.ts +++ b/furniture-3d-system/src/server/services/asset-center.service.ts @@ -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 { 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, })