fix(finished-progress): backfill sample merchandiser owner
This commit is contained in:
parent
499cbf66d1
commit
f6dd04eeee
@ -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 })
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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('')
|
||||
|
||||
@ -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})`,
|
||||
|
||||
@ -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[]>
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user