From f6dd04eeee5525960c04b374cdf7582abc7e2172 Mon Sep 17 00:00:00 2001 From: WuYongJie Date: Tue, 15 Sep 2026 13:45:40 +0800 Subject: [PATCH] fix(finished-progress): backfill sample merchandiser owner --- ...-product-factory-intake.repository.test.ts | 27 +++++++++++++++++ ...ished-product-factory-intake.repository.ts | 22 ++++++++++++++ ...hed-product-factory-intake.service.test.ts | 29 ++++++++++++++++--- ...finished-product-factory-intake.service.ts | 6 +++- .../finished-product-factory-intake.types.ts | 1 + 5 files changed, 80 insertions(+), 5 deletions(-) diff --git a/furniture-3d-system/src/server/repositories/__tests__/finished-product-factory-intake.repository.test.ts b/furniture-3d-system/src/server/repositories/__tests__/finished-product-factory-intake.repository.test.ts index ebd1f931..4f005691 100644 --- a/furniture-3d-system/src/server/repositories/__tests__/finished-product-factory-intake.repository.test.ts +++ b/furniture-3d-system/src/server/repositories/__tests__/finished-product-factory-intake.repository.test.ts @@ -2,6 +2,7 @@ jest.mock('@/lib/prisma', () => ({ prisma: { $transaction: jest.fn(), sampleProgressItem: { findMany: jest.fn(), findUnique: jest.fn() }, intakeNonIntegratedProduct: { findFirst: jest.fn() }, + finishedProductProgressItem: { updateMany: jest.fn() }, } })) import { prisma } from '@/lib/prisma' @@ -13,6 +14,7 @@ const mockPrisma = prisma as unknown as { $transaction: jest.Mock sampleProgressItem: { findMany: jest.Mock; findUnique: jest.Mock } intakeNonIntegratedProduct: { findFirst: jest.Mock } + finishedProductProgressItem: { updateMany: jest.Mock } } function row(skuId: string, overrides: Partial = {}): FinishedProductFactoryAutoRowInput { @@ -72,6 +74,31 @@ describe('finished product factory intake persistence', () => { }) }) + it('backfills only empty owners on automatic rows from the exact sample source', async () => { + mockPrisma.finishedProductProgressItem.updateMany.mockResolvedValue({ count: 3 }) + + await expect(repository.backfillEmptyFollowUpOwner('XP1', 'prod-1', '跟单甲')).resolves.toBe(3) + + expect(mockPrisma.finishedProductProgressItem.updateMany).toHaveBeenCalledWith({ + where: { + followUpOwner: '', + OR: [ + { sourceOpeningCode: 'XP1', sourceProductId: 'prod-1' }, + { + sourceOpeningCode: null, + sourceProductId: null, + sourceSkuId: null, + remarks: { + startsWith: '定厂完成自动生成 ', + contains: '(打样单 XP1)', + }, + }, + ], + }, + data: { followUpOwner: '跟单甲' }, + }) + }) + it('allocates one factory contract and legacy order sequence per source group, even across later catches', async () => { const { records, tx } = mockTransaction() await expect(repository.createAutoRows([row('1'), row('2')], '20260914')).resolves.toEqual({ created: 2, skipped: 0 }) diff --git a/furniture-3d-system/src/server/repositories/finished-product-factory-intake.repository.ts b/furniture-3d-system/src/server/repositories/finished-product-factory-intake.repository.ts index 2d5851d0..dc7b1206 100644 --- a/furniture-3d-system/src/server/repositories/finished-product-factory-intake.repository.ts +++ b/furniture-3d-system/src/server/repositories/finished-product-factory-intake.repository.ts @@ -109,6 +109,28 @@ export const finishedProductFactoryIntakeRepository: FinishedProductFactoryIntak }) }, + async backfillEmptyFollowUpOwner(openingCode, productId, followUpOwner) { + const result = await prisma.finishedProductProgressItem.updateMany({ + where: { + followUpOwner: '', + OR: [ + { sourceOpeningCode: openingCode, sourceProductId: productId }, + { + sourceOpeningCode: null, + sourceProductId: null, + sourceSkuId: null, + remarks: { + startsWith: '定厂完成自动生成 ', + contains: `(打样单 ${openingCode})`, + }, + }, + ], + }, + data: { followUpOwner }, + }) + return result.count + }, + async listCompletedSamplesSince(cutoffDate) { return prisma.sampleProgressItem.findMany({ where: { diff --git a/furniture-3d-system/src/server/services/__tests__/finished-product-factory-intake.service.test.ts b/furniture-3d-system/src/server/services/__tests__/finished-product-factory-intake.service.test.ts index 1ec08e10..a3b87b95 100644 --- a/furniture-3d-system/src/server/services/__tests__/finished-product-factory-intake.service.test.ts +++ b/furniture-3d-system/src/server/services/__tests__/finished-product-factory-intake.service.test.ts @@ -37,10 +37,16 @@ function makeDeps() { const loadProduct = jest.fn().mockResolvedValue(product) const completedSample = { ...SAMPLE, status: 'COMPLETED', autoGenerated: true, actualSampleCompletedAt: new Date('2026-09-14T06:00:00Z'), suppliers: [] } const loadSample = jest.fn().mockResolvedValue(completedSample) + const backfillEmptyFollowUpOwner = jest.fn().mockResolvedValue(0) const createAutoRows = jest.fn().mockImplementation(async (rows) => ({ created: rows.length, skipped: 0 })) const listCompletedSamplesSince = jest.fn().mockResolvedValue([SAMPLE, { openingCode: 'XP2', productDevelopmentFlowUrl: null }]) - const deps: FinishedProductFactoryIntakeDeps = { loadSample, loadProduct, createAutoRows, listCompletedSamplesSince } - return { deps, product, completedSample, loadSample, loadProduct, createAutoRows, listCompletedSamplesSince } + const deps: FinishedProductFactoryIntakeDeps = { + loadSample, loadProduct, backfillEmptyFollowUpOwner, createAutoRows, listCompletedSamplesSince, + } + return { + deps, product, completedSample, loadSample, loadProduct, + backfillEmptyFollowUpOwner, createAutoRows, listCompletedSamplesSince, + } } describe('FinishedProductFactoryIntakeService', () => { @@ -70,8 +76,21 @@ describe('FinishedProductFactoryIntakeService', () => { expect(rows[0].sourceGroupKey).not.toBe(rows[2].sourceGroupKey) }) + it('backfills an empty existing owner from the sample merchandiser even when every SKU is already in Jushuitan', async () => { + const { deps, product, backfillEmptyFollowUpOwner, createAutoRows } = makeDeps() + product.pricingRows = product.skuItems + .filter((item) => item.skuItemId !== 'sku-2') + .map((item) => ({ skuItemId: item.skuItemId, systemEnteredAt: new Date('2026-09-14T00:00:00Z') })) + + await expect(new FinishedProductFactoryIntakeService(deps).syncCompletedSampleToFinishedProgress(SAMPLE)) + .resolves.toEqual({ created: 0, skipped: 4 }) + + expect(backfillEmptyFollowUpOwner).toHaveBeenCalledWith(SAMPLE.openingCode, 'prod-1', '打样跟单') + expect(createAutoRows).toHaveBeenCalledWith([], expect.any(String)) + }) + it('keeps the same source identity after factory and barcode changes, without using the actor as owner', async () => { - const { deps, product, createAutoRows, completedSample } = makeDeps() + const { deps, product, backfillEmptyFollowUpOwner, createAutoRows, completedSample } = makeDeps() completedSample.merchandiserOwner = '' const service = new FinishedProductFactoryIntakeService(deps) await service.syncCompletedSampleToFinishedProgress({ ...SAMPLE, merchandiserOwner: null }, { name: '执行人' }) @@ -87,6 +106,7 @@ describe('FinishedProductFactoryIntakeService', () => { sourceOpeningCode: initial.sourceOpeningCode, sourceProductId: initial.sourceProductId, sourceSkuId: initial.sourceSkuId, followUpOwner: '', barcode: 'EDITED', }) + expect(backfillEmptyFollowUpOwner).not.toHaveBeenCalled() }) it('skips SKUs already entered into Jushuitan while importing the remaining SKUs', async () => { @@ -230,7 +250,7 @@ describe('FinishedProductFactoryIntakeService', () => { }) it('does not write for a missing source link, opening code, or deleted product', async () => { - const { deps, loadProduct, createAutoRows } = makeDeps() + const { deps, loadProduct, backfillEmptyFollowUpOwner, createAutoRows } = makeDeps() const service = new FinishedProductFactoryIntakeService(deps) await expect(service.syncCompletedSampleToFinishedProgress({ ...SAMPLE, productDevelopmentFlowUrl: null })) .resolves.toEqual({ created: 0, skipped: 0 }) @@ -239,6 +259,7 @@ describe('FinishedProductFactoryIntakeService', () => { expect(loadProduct).not.toHaveBeenCalled() loadProduct.mockResolvedValueOnce(null) await expect(service.syncCompletedSampleToFinishedProgress(SAMPLE)).resolves.toEqual({ created: 0, skipped: 0 }) + expect(backfillEmptyFollowUpOwner).not.toHaveBeenCalled() expect(createAutoRows).not.toHaveBeenCalled() expect(linkedProductIdFromFlowUrl(FLOW_URL)).toBe('prod-1') expect(linkedProductIdFromFlowUrl('not-a-url')).toBe('') diff --git a/furniture-3d-system/src/server/services/finished-product-factory-intake.service.ts b/furniture-3d-system/src/server/services/finished-product-factory-intake.service.ts index 69a0252e..b000930d 100644 --- a/furniture-3d-system/src/server/services/finished-product-factory-intake.service.ts +++ b/furniture-3d-system/src/server/services/finished-product-factory-intake.service.ts @@ -63,6 +63,10 @@ export class FinishedProductFactoryIntakeService { } const product = await this.deps.loadProduct(productId) if (!product) return { created: 0, skipped: 0 } + const followUpOwner = readFactoryIntakeText(sample.merchandiserOwner) + if (followUpOwner) { + await this.deps.backfillEmptyFollowUpOwner(openingCode, productId, followUpOwner) + } const importedAt = this.now() const today = factoryIntakeDate(importedAt) @@ -95,7 +99,7 @@ export class FinishedProductFactoryIntakeService { supplier, supplierCode, imageUrl, - followUpOwner: readFactoryIntakeText(sample.merchandiserOwner), + followUpOwner, orderedQuantity: parseFactoryMinimumOrderQuantity(row.minimumOrderQuantity), firstDueDate: parseFactoryLeadTime(row.leadTime), remarks: `定厂完成自动生成 ${today}(打样单 ${openingCode})`, diff --git a/furniture-3d-system/src/server/services/finished-product-factory-intake.types.ts b/furniture-3d-system/src/server/services/finished-product-factory-intake.types.ts index 241ce1c8..9abc6fe2 100644 --- a/furniture-3d-system/src/server/services/finished-product-factory-intake.types.ts +++ b/furniture-3d-system/src/server/services/finished-product-factory-intake.types.ts @@ -57,6 +57,7 @@ export type FinishedProductFactoryAutoRowInput = { export type FinishedProductFactoryIntakeDeps = { loadSample(openingCode: string): Promise loadProduct(productId: string): Promise + backfillEmptyFollowUpOwner(openingCode: string, productId: string, followUpOwner: string): Promise createAutoRows(rows: FinishedProductFactoryAutoRowInput[], dateKey: string): Promise listCompletedSamplesSince(cutoffDate: string): Promise }