fix: scope non-integrated product factory saves

This commit is contained in:
WuYongJie 2026-07-15 15:05:16 +08:00
parent 1b4ea170aa
commit f3e7d93aa3
2 changed files with 322 additions and 0 deletions

View File

@ -3446,6 +3446,204 @@ describe('intakeNonIntegratedProductService', () => {
)
})
it('scopes purchase owner product factory saves when a full payload carries other node edits', async () => {
const existingConfirmations = {
...createDefaultDocumentWorkspaceConfirmations(),
基础信息: false,
开品参考: false,
产品定厂: false,
}
const existingSku = createSkuItem({
id: 'sku-1',
skuCode: 'DNZ040901',
barcode: 'DNZ040901N1280V01',
manufacturer: '供应商A',
manufacturerCode: 'N1280',
minimumOrderQuantity: '',
specName: '款式一',
})
const existingFactoryRows = [
{
skuId: 'sku-1',
confirmed: false,
model: 'K01',
skuCode: 'DNZ040901',
setCount: '1',
barcode: 'DNZ040901N1280V01',
specName: '款式一',
size: '',
color: '',
material: '',
installMethod: '',
supplierName: '供应商A',
supplierCode: 'N1280',
warehouse: '',
minimumOrderQuantity: '',
leadTime: '',
assemblyMode: '',
productionType: '',
installationService: '',
supplierCategory: '',
purchaseOrderMethod: '',
executeStandard: '',
},
]
const existingPayload = createPayload({
designer: '设计A',
purchaser: '采购A',
purchaseOwner: '采购A',
openingReference: {
productName: '旧品名',
productImage: 'https://s3.example.com/opening-old.jpg',
structureDesigner: '设计A',
},
skuItems: [existingSku],
factoryRows: existingFactoryRows,
uploadFiles: [
{
id: 'opening-old',
name: 'old.jpg',
category: '开品参考',
dataUrl: 'https://s3.example.com/opening-old.jpg',
mimeType: 'image/jpeg',
},
],
documentWorkspaceConfirmations: existingConfirmations,
})
const incomingPayload = createPayload({
...existingPayload,
designer: '误带设计B',
openingReference: {
productName: '误带新品名',
productImage: 'https://s3.example.com/opening-new.jpg',
structureDesigner: '误带设计B',
},
skuItems: [
{
...existingSku,
barcode: 'DNZ040901N1281V01',
manufacturer: '供应商B',
manufacturerCode: 'N1281',
minimumOrderQuantity: '480',
specName: '误带款式二',
},
],
factoryRows: existingFactoryRows.map((row) => ({
...row,
barcode: 'DNZ040901N1281V01',
supplierName: '供应商B',
supplierCode: 'N1281',
minimumOrderQuantity: '480',
confirmed: true,
})),
uploadFiles: [
...(existingPayload.uploadFiles as Array<Record<string, unknown>>),
{
id: 'opening-new',
name: 'new.jpg',
category: '开品参考',
dataUrl: 'https://s3.example.com/opening-new.jpg',
mimeType: 'image/jpeg',
},
],
documentWorkspaceConfirmations: {
...existingConfirmations,
基础信息: true,
开品参考: true,
产品定厂: true,
},
documentWorkspaceApprovedAt: {
基础信息: '2026-05-21T01:00:00.000Z',
开品参考: '2026-05-21T01:30:00.000Z',
产品定厂: '2026-05-21T02:00:00.000Z',
},
})
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 expect(
intakeNonIntegratedProductService.save(
incomingPayload,
{
...baseUser,
id: 'purchase-1',
email: 'purchase@example.com',
name: '采购A',
businessRoles: ['PURCHASING'] as const,
departmentPaths: [
'江苏简尚品牌管理有限公司 > 供应链中心 > 基本盘 采购部 > 采购一部',
],
},
{ workspaceCategory: '产品定厂' },
),
).resolves.toEqual(expect.objectContaining({ id: 'non-integrated-1' }))
const updatePayload = mockedRepository.update.mock.calls[0]?.[1]?.payload as
| Record<string, unknown>
| undefined
const updateConfirmations =
updatePayload?.documentWorkspaceConfirmations as Record<string, boolean>
const updateApprovedAt =
updatePayload?.documentWorkspaceApprovedAt as Record<string, string>
const updateOpeningReference =
updatePayload?.openingReference as Record<string, unknown>
const updateUploadFiles =
updatePayload?.uploadFiles as Array<Record<string, unknown>>
expect(updatePayload).toEqual(
expect.objectContaining({
designer: '设计A',
skuItems: [
expect.objectContaining({
barcode: 'DNZ040901N1281V01',
manufacturer: '供应商B',
manufacturerCode: 'N1281',
minimumOrderQuantity: '480',
specName: '款式一',
}),
],
factoryRows: [
expect.objectContaining({
barcode: 'DNZ040901N1281V01',
supplierName: '供应商B',
supplierCode: 'N1281',
minimumOrderQuantity: '480',
confirmed: true,
}),
],
}),
)
expect(updateOpeningReference).toEqual(
expect.objectContaining({
productName: '旧品名',
productImage: 'https://s3.example.com/opening-old.jpg',
structureDesigner: '设计A',
}),
)
expect(updateUploadFiles.map((file) => file.id)).toEqual(['opening-old'])
expect(updateConfirmations.基础信息).toBe(false)
expect(updateConfirmations.开品参考).toBe(false)
expect(updateConfirmations.产品定厂).toBe(true)
expect(updateApprovedAt?.基础信息).toBeUndefined()
expect(updateApprovedAt?.开品参考).toBeUndefined()
expect(updateApprovedAt?.产品定厂).toBe('2026-05-21T02:00:00.000Z')
})
it('keeps downstream factory rows when a designer batch save carries derived factory changes', async () => {
const existingFactoryRows = [
{

View File

@ -1729,6 +1729,36 @@ function getWritableChangedCategories(
)
}
function logNonIntegratedScopedWriteDenied({
changedCategories,
deniedChangedCategories,
productFactoryTargetSkuIds,
record,
requestedCategory,
user,
writableCategories,
}: {
changedCategories: Iterable<string>
deniedChangedCategories: Iterable<string>
productFactoryTargetSkuIds: ReadonlySet<string>
record: IntakeNonIntegratedRecord
requestedCategory?: string | null
user: AuthUser
writableCategories: Iterable<string>
}) {
console.warn('[IntakeNonIntegratedProduct] scoped save denied', {
changedCategories: Array.from(changedCategories),
deniedChangedCategories: Array.from(deniedChangedCategories),
productId: record.id,
requestedCategory: requestedCategory ?? null,
spu: record.spu,
targetSkuCount: productFactoryTargetSkuIds.size,
userId: user.id,
userName: user.name,
writableCategories: Array.from(writableCategories),
})
}
function canTreatRequestedWorkspaceSaveAsNoop(
requestedCategory: string | null | undefined,
writableCategories: Set<NonIntegratedWorkspaceCategoryKey>,
@ -1930,6 +1960,91 @@ async function prepareProductForPermittedWrite(
? Array.from(productFactoryTargetSkuIds)
: undefined
const isStockProduct = isStockNonIntegratedProduct(existingProduct)
if (requestedCategory) {
const canWriteRequestedCategory =
!isStockProduct || requestedCategory === PLANNING_LAYOUT_WORKSPACE_CATEGORY
? await intakeNonIntegratedScopeService.canWriteWorkspaceRecordWithSettings(
record,
user,
requestedCategory,
targetSkuItemIdsForCategory(requestedCategory),
)
: false
if (canWriteRequestedCategory) {
const preserveOptions = {
allowProductFactorySkuSync:
!isStockProduct && requestedCategory === PRODUCT_FACTORY_WORKSPACE_CATEGORY,
productFactoryTargetSkuIds,
}
const categoriesToPreserve = TRACKED_WORKSPACE_CATEGORIES.filter(
(category) => category !== requestedCategory,
)
const scopedNextProduct = categoriesToPreserve.reduce(
(currentProduct, category) =>
preserveWorkspaceCategoryFromProduct(
currentProduct,
category,
existingProduct,
preserveOptions,
),
nextProductForWrite,
)
const scopedChangedCategories = getChangedWorkspaceCategories(
existingProduct,
scopedNextProduct,
preserveOptions,
)
const deniedScopedCategories = Array.from(scopedChangedCategories).filter(
(category) => category !== requestedCategory,
)
if (deniedScopedCategories.length > 0) {
logNonIntegratedScopedWriteDenied({
changedCategories: scopedChangedCategories,
deniedChangedCategories: deniedScopedCategories,
productFactoryTargetSkuIds,
record,
requestedCategory,
user,
writableCategories: [requestedCategory],
})
throwNonIntegratedWriteDenied()
}
if (scopedChangedCategories.size === 0) {
return {
product: existingProduct,
preserveRecordColumns: true,
skipUpdate: true,
}
}
await intakeNonIntegratedScopeService.assertCanWriteRecord(
record,
user,
requestedCategory,
targetSkuItemIdsForCategory(requestedCategory),
)
return {
product: categoriesToPreserve.reduce(
(currentProduct, category) =>
preserveWorkspaceCategoryFromPayload(
currentProduct,
category,
record.payload,
existingProduct,
preserveOptions,
),
scopedNextProduct,
),
preserveRecordColumns: true,
}
}
}
const changedCategories = getChangedWorkspaceCategories(existingProduct, nextProductForWrite)
if (changedCategories.size === 0) {
@ -2046,6 +2161,15 @@ async function prepareProductForPermittedWrite(
throwNonIntegratedWriteDenied()
}
logNonIntegratedScopedWriteDenied({
changedCategories,
deniedChangedCategories,
productFactoryTargetSkuIds,
record,
requestedCategory,
user,
writableCategories,
})
await intakeNonIntegratedScopeService.assertCanWriteRecord(
record,
user,