fix(finished-progress): backfill sample merchandiser owner

This commit is contained in:
WuYongJie 2026-09-15 13:45:40 +08:00
parent 499cbf66d1
commit f6dd04eeee
5 changed files with 80 additions and 5 deletions

View File

@ -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> = {}): 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 })

View File

@ -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: {

View File

@ -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('')

View File

@ -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})`,

View File

@ -57,6 +57,7 @@ export type FinishedProductFactoryAutoRowInput = {
export type FinishedProductFactoryIntakeDeps = {
loadSample(openingCode: string): Promise<FinishedProductFactorySourceSample | null>
loadProduct(productId: string): Promise<FinishedProductFactorySourceProduct | null>
backfillEmptyFollowUpOwner(openingCode: string, productId: string, followUpOwner: string): Promise<number>
createAutoRows(rows: FinishedProductFactoryAutoRowInput[], dateKey: string): Promise<FinishedProductFactoryIntakeResult>
listCompletedSamplesSince(cutoffDate: string): Promise<FinishedProductCompletedSampleInput[]>
}