运营部门负责人/运营负责人接入聚水潭销售宽表cater/ider
- dws repository 新增 listLatestOpsOwners:rpt.dws_sn_sales_full_info_di 按 skuoid 取 stat_date 最新一条的 cater/ider,全条码范围 - enrichment 链路新增 ops owners patch:来源码 DWS_SALES_OPS 可追溯;字段级有值才覆盖(dws 无值保留档案原值) - opsDirector(运营总负责人)不接 - 本地全量验证:抽样 10 条与上游 100% 一致;3001 API 验收通过
This commit is contained in:
parent
1b3f210e0f
commit
904b80d4c9
@ -10,6 +10,7 @@ export const JST_STOCK_MONITOR_ARCHIVE_SOURCE_CODE = 'JST_STOCK_MONITOR'
|
||||
export const JST_CLEARANCE_STOCK_ARCHIVE_SOURCE_CODE = 'JST_CLEARANCE_STOCK'
|
||||
export const JST_SKUMAP_PIC_ARCHIVE_SOURCE_CODE = 'JST_SKUMAP_PIC'
|
||||
export const DWS_SALES_GOODS_COST_ARCHIVE_SOURCE_CODE = 'DWS_SALES_GOODS_COST'
|
||||
export const DWS_SALES_OPS_ARCHIVE_SOURCE_CODE = 'DWS_SALES_OPS'
|
||||
export const JST_PUBLIC_AVAILABLE_STOCK_ARCHIVE_SOURCE_CODE = 'JST_PUBLIC_AVAILABLE_STOCK'
|
||||
export const JST_WAREHOUSE_ARCHIVE_SOURCE_CODE = 'JST_WAREHOUSE_INFO'
|
||||
export const JST_SKU_PRICE_ARCHIVE_SOURCE_CODE = 'JST_SKU_PRICE'
|
||||
|
||||
@ -5,8 +5,15 @@ export type DwsSalesGoodsCostProfile = {
|
||||
goodsCost: string
|
||||
}
|
||||
|
||||
export type DwsSalesOpsOwnerProfile = {
|
||||
skuoid: string
|
||||
cater: string
|
||||
ider: string
|
||||
}
|
||||
|
||||
export interface DwsSalesGoodsCostRepository {
|
||||
listDfLatestGoodsCost(): Promise<DwsSalesGoodsCostProfile[]>
|
||||
listLatestOpsOwners(): Promise<DwsSalesOpsOwnerProfile[]>
|
||||
close(): Promise<void>
|
||||
}
|
||||
|
||||
@ -87,6 +94,36 @@ class DwsSalesGoodsCostRepositoryClass implements DwsSalesGoodsCostRepository {
|
||||
return result.recordset
|
||||
}
|
||||
|
||||
// Latest non-empty ops owners per skuoid (all barcodes, not only DF).
|
||||
// One row per skuoid from the newest stat_date; cater/ider are returned
|
||||
// as-is and the mapper decides per field whether to overwrite.
|
||||
async listLatestOpsOwners() {
|
||||
const schema = safeIdentifier(
|
||||
process.env.JST_REPORT_DB_SCHEMA?.trim() || 'rpt',
|
||||
'JST_REPORT_DB_SCHEMA',
|
||||
)
|
||||
const pool = await this.getPool()
|
||||
const result = await pool.request().query<DwsSalesOpsOwnerProfile>(`
|
||||
WITH ranked AS (
|
||||
SELECT
|
||||
UPPER(LTRIM(RTRIM(CAST(skuoid AS varchar(200))))) AS skuoid,
|
||||
LTRIM(RTRIM(CAST(cater AS varchar(100)))) AS cater,
|
||||
LTRIM(RTRIM(CAST(ider AS varchar(100)))) AS ider,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY UPPER(LTRIM(RTRIM(CAST(skuoid AS varchar(200)))))
|
||||
ORDER BY stat_date DESC
|
||||
) AS rn
|
||||
FROM [${schema}].[dws_sn_sales_full_info_di]
|
||||
WHERE (cater IS NOT NULL AND LTRIM(RTRIM(CAST(cater AS varchar(100)))) <> '')
|
||||
OR (ider IS NOT NULL AND LTRIM(RTRIM(CAST(ider AS varchar(100)))) <> '')
|
||||
)
|
||||
SELECT skuoid, cater, ider
|
||||
FROM ranked
|
||||
WHERE rn = 1
|
||||
`)
|
||||
return result.recordset
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (!this.pool) return
|
||||
await this.pool.close()
|
||||
|
||||
@ -57,6 +57,7 @@ describe('supply-chain product archive enrichment sync', () => {
|
||||
prices: [],
|
||||
dwsGoodsCosts: [],
|
||||
skumapPics: [],
|
||||
dwsOpsOwners: [],
|
||||
catalogCosts: [],
|
||||
warehouseConfigs: [],
|
||||
},
|
||||
@ -78,6 +79,7 @@ describe('supply-chain product archive enrichment sync', () => {
|
||||
],
|
||||
dwsGoodsCosts: [{ skuoid: 'DF-A3BJ006601', goodsCost: '70.18' }],
|
||||
skumapPics: [{ skuId: 'DF-A3BJ006601', pic: 'https://img.pddpic.com/mms-material-img/df.jpg' }],
|
||||
dwsOpsOwners: [],
|
||||
catalogCosts: [],
|
||||
warehouseConfigs: [],
|
||||
}
|
||||
@ -135,6 +137,79 @@ describe('supply-chain product archive enrichment sync', () => {
|
||||
expect(barUpdates.some((update) => 'image' in update.patch)).toBe(false)
|
||||
})
|
||||
|
||||
it('takes ops owners from the dws sales table only when dws has a value', () => {
|
||||
const lookups = {
|
||||
clearanceStocks: [],
|
||||
publicAvailableStocks: [],
|
||||
suppliers: [],
|
||||
stocks: [],
|
||||
warehouses: [],
|
||||
prices: [],
|
||||
dwsGoodsCosts: [],
|
||||
skumapPics: [],
|
||||
dwsOpsOwners: [
|
||||
{ skuoid: 'BAR-001', cater: '比干', ider: '精卫' },
|
||||
{ skuoid: 'BAR-002', cater: '香茗', ider: '' },
|
||||
],
|
||||
catalogCosts: [],
|
||||
warehouseConfigs: [],
|
||||
}
|
||||
|
||||
const bothUpdates = buildProductArchiveEnrichmentUpdates(
|
||||
{
|
||||
id: 'archive-1',
|
||||
barcode: 'BAR-001',
|
||||
spu: 'SPU-001',
|
||||
sku: 'SKU-001',
|
||||
supplierName: '',
|
||||
inOutWarehouse: '',
|
||||
},
|
||||
lookups,
|
||||
)
|
||||
const bothPatch = bothUpdates.find(
|
||||
(update) => 'opsDeptHead' in update.patch || 'opsHead' in update.patch,
|
||||
)
|
||||
expect(bothPatch?.patch).toEqual({ opsDeptHead: '比干', opsHead: '精卫' })
|
||||
expect(bothPatch?.source).toEqual(
|
||||
expect.objectContaining({
|
||||
sourceCode: 'DWS_SALES_OPS',
|
||||
sourceObject: 'dws_sn_sales_full_info_di',
|
||||
sourceColumns: { opsDeptHead: 'cater', opsHead: 'ider' },
|
||||
}),
|
||||
)
|
||||
|
||||
// Only cater present: patch covers opsDeptHead only (field-level overwrite)
|
||||
const partialUpdates = buildProductArchiveEnrichmentUpdates(
|
||||
{
|
||||
id: 'archive-2',
|
||||
barcode: 'BAR-002',
|
||||
spu: 'SPU-002',
|
||||
sku: 'SKU-002',
|
||||
supplierName: '',
|
||||
inOutWarehouse: '',
|
||||
},
|
||||
lookups,
|
||||
)
|
||||
const partialPatch = partialUpdates.find((update) => 'opsDeptHead' in update.patch)
|
||||
expect(partialPatch?.patch).toEqual({ opsDeptHead: '香茗' })
|
||||
expect(partialUpdates.some((update) => 'opsHead' in update.patch)).toBe(false)
|
||||
|
||||
// Barcode without dws ops data: no ops patch at all (keeps archive value)
|
||||
const untouchedUpdates = buildProductArchiveEnrichmentUpdates(
|
||||
{
|
||||
id: 'archive-3',
|
||||
barcode: 'BAR-009',
|
||||
spu: 'SPU-009',
|
||||
sku: 'SKU-009',
|
||||
supplierName: '',
|
||||
inOutWarehouse: '',
|
||||
},
|
||||
lookups,
|
||||
)
|
||||
expect(untouchedUpdates.some((update) => 'opsDeptHead' in update.patch)).toBe(false)
|
||||
expect(untouchedUpdates.some((update) => 'opsHead' in update.patch)).toBe(false)
|
||||
})
|
||||
|
||||
it('takes dropship (DF) cost from the sales goods_cost source and keeps jst_sku cost for others', () => {
|
||||
const lookups = {
|
||||
clearanceStocks: [],
|
||||
@ -148,6 +223,7 @@ describe('supply-chain product archive enrichment sync', () => {
|
||||
],
|
||||
dwsGoodsCosts: [{ skuoid: 'DF-A3BJ006601', goodsCost: '70.18' }],
|
||||
skumapPics: [],
|
||||
dwsOpsOwners: [],
|
||||
catalogCosts: [],
|
||||
warehouseConfigs: [],
|
||||
}
|
||||
@ -304,6 +380,7 @@ describe('supply-chain product archive enrichment sync', () => {
|
||||
clearanceStockRepository,
|
||||
dwsGoodsCostRepository: {
|
||||
listDfLatestGoodsCost: jest.fn().mockResolvedValue([]),
|
||||
listLatestOpsOwners: jest.fn().mockResolvedValue([]),
|
||||
close: jest.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
skumapPicRepository: {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import {
|
||||
DWS_SALES_GOODS_COST_ARCHIVE_SOURCE_CODE,
|
||||
DWS_SALES_OPS_ARCHIVE_SOURCE_CODE,
|
||||
JST_CLEARANCE_STOCK_ARCHIVE_SOURCE_CODE,
|
||||
JST_PUBLIC_AVAILABLE_STOCK_ARCHIVE_SOURCE_CODE,
|
||||
JST_SKUMAP_PIC_ARCHIVE_SOURCE_CODE,
|
||||
@ -11,7 +12,10 @@ import {
|
||||
WAREHOUSE_CONFIG_ARCHIVE_SOURCE_CODE,
|
||||
} from '@/lib/supply-chain-product-archive-field-source'
|
||||
import type { ProductArchiveClearanceStockProfile } from '@/server/repositories/supply-chain-product-archive-clearance-stock.repository'
|
||||
import type { DwsSalesGoodsCostProfile } from '@/server/repositories/dws-sales-goods-cost.repository'
|
||||
import type {
|
||||
DwsSalesGoodsCostProfile,
|
||||
DwsSalesOpsOwnerProfile,
|
||||
} from '@/server/repositories/dws-sales-goods-cost.repository'
|
||||
import type { JstSkumapPicProfile } from '@/server/repositories/jst-skumap-pic.repository'
|
||||
import type { JstInventoryPublicAvailableProfile } from '@/server/repositories/jst-inventory-public-available.repository'
|
||||
import type { JstSkuPriceProfile } from '@/server/repositories/jst-sku-price.repository'
|
||||
@ -48,6 +52,7 @@ export type ProductArchiveEnrichmentLookups = {
|
||||
prices: readonly JstSkuPriceProfile[]
|
||||
dwsGoodsCosts: readonly DwsSalesGoodsCostProfile[]
|
||||
skumapPics: readonly JstSkumapPicProfile[]
|
||||
dwsOpsOwners: readonly DwsSalesOpsOwnerProfile[]
|
||||
catalogCosts: readonly ProductCatalogCostPriceProfile[]
|
||||
warehouseConfigs: readonly ProductArchiveWarehouseConfigRecord[]
|
||||
}
|
||||
@ -142,6 +147,32 @@ export function buildProductArchiveEnrichmentUpdates(
|
||||
(profile) => normalizeIdentity(profile.skuId) === normalizeIdentity(target.barcode),
|
||||
)
|
||||
: undefined
|
||||
// Ops owners come from the sales detail table for every barcode with sales
|
||||
// history; per agreed contract, empty dws values keep the current archive
|
||||
// value untouched (field-level overwrite only when dws has a name).
|
||||
const dwsOpsOwner = lookups.dwsOpsOwners.find(
|
||||
(profile) => normalizeIdentity(profile.skuoid) === normalizeIdentity(target.barcode),
|
||||
)
|
||||
const opsOwnerPatch = {
|
||||
...(dwsOpsOwner?.cater ? { opsDeptHead: dwsOpsOwner.cater } : {}),
|
||||
...(dwsOpsOwner?.ider ? { opsHead: dwsOpsOwner.ider } : {}),
|
||||
}
|
||||
const opsOwnerUpdates: ProductArchiveEnrichmentUpdate[] =
|
||||
Object.keys(opsOwnerPatch).length > 0
|
||||
? [
|
||||
{
|
||||
patch: opsOwnerPatch,
|
||||
source: source(
|
||||
DWS_SALES_OPS_ARCHIVE_SOURCE_CODE,
|
||||
JST_SOURCE_SYSTEM,
|
||||
JST_SOURCE_SCHEMA,
|
||||
'dws_sn_sales_full_info_di',
|
||||
dwsOpsOwner?.skuoid || target.barcode,
|
||||
{ opsDeptHead: 'cater', opsHead: 'ider' },
|
||||
),
|
||||
},
|
||||
]
|
||||
: []
|
||||
return [
|
||||
...(isDropshipSku
|
||||
? [
|
||||
@ -160,6 +191,7 @@ export function buildProductArchiveEnrichmentUpdates(
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...opsOwnerUpdates,
|
||||
{
|
||||
patch: {
|
||||
stockClearance: clearanceStock?.remainingStock ?? '',
|
||||
|
||||
@ -56,7 +56,10 @@ type EnrichmentDependencies = {
|
||||
ProductArchiveClearanceStockRepository,
|
||||
'listByBarcodes' | 'close'
|
||||
>
|
||||
dwsGoodsCostRepository: Pick<DwsSalesGoodsCostRepository, 'listDfLatestGoodsCost' | 'close'>
|
||||
dwsGoodsCostRepository: Pick<
|
||||
DwsSalesGoodsCostRepository,
|
||||
'listDfLatestGoodsCost' | 'listLatestOpsOwners' | 'close'
|
||||
>
|
||||
skumapPicRepository: Pick<JstSkumapPicRepository, 'listDfLatestSkuPics' | 'close'>
|
||||
publicAvailableStockRepository: Pick<
|
||||
JstInventoryPublicAvailableRepository,
|
||||
@ -110,6 +113,8 @@ export class SupplyChainProductArchiveEnrichmentSyncService {
|
||||
const dwsGoodsCosts = await this.dependencies.dwsGoodsCostRepository.listDfLatestGoodsCost()
|
||||
// One shot: latest platform picture for every DF skuoid.
|
||||
const skumapPics = await this.dependencies.skumapPicRepository.listDfLatestSkuPics()
|
||||
// One shot: latest ops owners (cater/ider) for every barcode with sales.
|
||||
const dwsOpsOwners = await this.dependencies.dwsGoodsCostRepository.listLatestOpsOwners()
|
||||
try {
|
||||
while (true) {
|
||||
const targets = await this.dependencies.repository.listTargets({
|
||||
@ -159,6 +164,7 @@ export class SupplyChainProductArchiveEnrichmentSyncService {
|
||||
prices,
|
||||
dwsGoodsCosts,
|
||||
skumapPics,
|
||||
dwsOpsOwners,
|
||||
catalogCosts,
|
||||
warehouseConfigs,
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user