feat(supply-chain): map warehouse attributes from JST
This commit is contained in:
parent
80c4dcc91b
commit
c1ba901f7a
@ -28,6 +28,7 @@ export type SupplyChainProductArchiveRow = {
|
||||
brand: string
|
||||
orderType: string
|
||||
inOutWarehouse: string
|
||||
warehouseType: string
|
||||
warehouseHead: string
|
||||
storageType: string
|
||||
isSeasonal: string
|
||||
|
||||
@ -0,0 +1,96 @@
|
||||
import sql from 'mssql'
|
||||
|
||||
export type JstWarehouseProfile = {
|
||||
warehouseName: string
|
||||
warehouseType: string
|
||||
}
|
||||
|
||||
export interface JstWarehouseProfileRepository {
|
||||
listByNames(names: readonly string[]): Promise<JstWarehouseProfile[]>
|
||||
close(): Promise<void>
|
||||
}
|
||||
|
||||
function requiredEnv(name: string) {
|
||||
const value = process.env[name]?.trim()
|
||||
if (!value) throw new Error(`Missing required environment variable: ${name}`)
|
||||
return value
|
||||
}
|
||||
|
||||
function booleanEnv(name: string, fallback: boolean) {
|
||||
const value = process.env[name]?.trim().toLowerCase()
|
||||
if (!value) return fallback
|
||||
return value === 'true' || value === '1'
|
||||
}
|
||||
|
||||
function safeIdentifier(value: string, label: string) {
|
||||
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
|
||||
throw new Error(`${label} is not a valid SQL Server identifier`)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function normalizedNames(names: readonly string[]) {
|
||||
return Array.from(new Set(names.map((name) => name.trim()).filter(Boolean))).slice(0, 200)
|
||||
}
|
||||
|
||||
class JstWarehouseProfileRepositoryClass implements JstWarehouseProfileRepository {
|
||||
private pool: sql.ConnectionPool | null = null
|
||||
|
||||
private async getPool() {
|
||||
if (this.pool?.connected) return this.pool
|
||||
|
||||
this.pool = await new sql.ConnectionPool({
|
||||
server: requiredEnv('JST_REPORT_DB_HOST'),
|
||||
port: Number(process.env.JST_REPORT_DB_PORT?.trim() || '1433'),
|
||||
database: requiredEnv('JST_REPORT_DB_NAME'),
|
||||
user: requiredEnv('JST_REPORT_DB_USER'),
|
||||
password: requiredEnv('JST_REPORT_DB_PASSWORD'),
|
||||
options: {
|
||||
encrypt: booleanEnv('JST_REPORT_DB_ENCRYPT', true),
|
||||
trustServerCertificate: booleanEnv('JST_REPORT_DB_TRUST_SERVER_CERTIFICATE', true),
|
||||
},
|
||||
pool: {
|
||||
min: 0,
|
||||
max: 4,
|
||||
idleTimeoutMillis: 30_000,
|
||||
},
|
||||
requestTimeout: 30_000,
|
||||
connectionTimeout: 15_000,
|
||||
}).connect()
|
||||
return this.pool
|
||||
}
|
||||
|
||||
async listByNames(names: readonly string[]) {
|
||||
const warehouseNames = normalizedNames(names)
|
||||
if (warehouseNames.length === 0) return []
|
||||
|
||||
const schema = safeIdentifier(
|
||||
process.env.JST_REPORT_DB_SCHEMA?.trim() || 'rpt',
|
||||
'JST_REPORT_DB_SCHEMA',
|
||||
)
|
||||
const pool = await this.getPool()
|
||||
const request = pool.request()
|
||||
const parameters = warehouseNames.map((name, index) => {
|
||||
const parameter = `warehouseName${index}`
|
||||
request.input(parameter, sql.NVarChar(255), name)
|
||||
return `@${parameter}`
|
||||
})
|
||||
|
||||
const result = await request.query<JstWarehouseProfile>(`
|
||||
SELECT
|
||||
LTRIM(RTRIM(CAST(name AS nvarchar(255)))) AS warehouseName,
|
||||
LTRIM(RTRIM(COALESCE(CAST(remark2 AS nvarchar(1000)), N''))) AS warehouseType
|
||||
FROM [${schema}].[jst_warehouseInfo]
|
||||
WHERE LTRIM(RTRIM(CAST(name AS nvarchar(255)))) IN (${parameters.join(', ')})
|
||||
`)
|
||||
return result.recordset
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (!this.pool) return
|
||||
await this.pool.close()
|
||||
this.pool = null
|
||||
}
|
||||
}
|
||||
|
||||
export const jstWarehouseProfileRepository = new JstWarehouseProfileRepositoryClass()
|
||||
@ -8,6 +8,11 @@ jest.mock('@/server/repositories/jst-stock-monitor.repository', () => ({
|
||||
listByKeys: jest.fn().mockResolvedValue([]),
|
||||
},
|
||||
}))
|
||||
jest.mock('@/server/repositories/jst-warehouse-profile.repository', () => ({
|
||||
jstWarehouseProfileRepository: {
|
||||
listByNames: jest.fn().mockResolvedValue([]),
|
||||
},
|
||||
}))
|
||||
|
||||
import {
|
||||
SupplyChainProductArchiveService,
|
||||
@ -109,6 +114,14 @@ describe('supply-chain product archive field mapping', () => {
|
||||
},
|
||||
]),
|
||||
}
|
||||
const warehouseProfileRepository = {
|
||||
listByNames: jest.fn().mockResolvedValue([
|
||||
{
|
||||
warehouseName: 'WH-1',
|
||||
warehouseType: '成品发货仓-真实库存',
|
||||
},
|
||||
]),
|
||||
}
|
||||
const service = new SupplyChainProductArchiveService(
|
||||
repository as never,
|
||||
supplierProfileRepository,
|
||||
@ -118,6 +131,7 @@ describe('supply-chain product archive field mapping', () => {
|
||||
{ warehouseName: 'WH-1', warehouseHead: '负责人', storageType: 'INBOUND' },
|
||||
]),
|
||||
} as never,
|
||||
warehouseProfileRepository,
|
||||
)
|
||||
|
||||
const page = await service.listPage({ page: 1, pageSize: 50 })
|
||||
@ -139,6 +153,7 @@ describe('supply-chain product archive field mapping', () => {
|
||||
volume: '139264',
|
||||
packType: '双包',
|
||||
orderType: '订单式',
|
||||
warehouseType: '成品发货仓-真实库存',
|
||||
warehouseHead: '负责人',
|
||||
storageType: '入库',
|
||||
supplier: '源沃家具',
|
||||
@ -163,5 +178,8 @@ describe('supply-chain product archive field mapping', () => {
|
||||
expect(stockMonitorRepository.listByKeys).toHaveBeenCalledWith([
|
||||
{ sku: 'SKU-001', barcode: 'BAR-001' },
|
||||
])
|
||||
expect(warehouseProfileRepository.listByNames).toHaveBeenCalledWith([
|
||||
'WH-1',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@ -20,6 +20,10 @@ import {
|
||||
type JstStockMonitorProfile,
|
||||
type JstStockMonitorRepository,
|
||||
} from '@/server/repositories/jst-stock-monitor.repository'
|
||||
import {
|
||||
jstWarehouseProfileRepository,
|
||||
type JstWarehouseProfileRepository,
|
||||
} from '@/server/repositories/jst-warehouse-profile.repository'
|
||||
import { storageTypeLabel } from '@/lib/supply-chain-product-archive-values'
|
||||
import {
|
||||
supplyChainProductArchiveWarehouseConfigRepository,
|
||||
@ -40,6 +44,10 @@ function normalizeSupplierName(value: string) {
|
||||
return value.trim().toLocaleLowerCase('zh-CN')
|
||||
}
|
||||
|
||||
function normalizeWarehouseName(value: string) {
|
||||
return value.trim().toLocaleLowerCase('zh-CN')
|
||||
}
|
||||
|
||||
function normalizeStockKey(value: string) {
|
||||
return value.trim().toLocaleUpperCase('zh-CN')
|
||||
}
|
||||
@ -55,6 +63,7 @@ function mapProductArchiveRow(
|
||||
now: Date,
|
||||
supplierProfile?: JstSupplierProfile,
|
||||
stockProfile?: JstStockMonitorProfile,
|
||||
warehouseTypes?: Map<string, string>,
|
||||
warehouseStorageTypes?: Map<string, string>,
|
||||
warehouseHeads?: Map<string, string>,
|
||||
) {
|
||||
@ -94,9 +103,10 @@ function mapProductArchiveRow(
|
||||
brand: record.brand,
|
||||
orderType: record.orderType,
|
||||
inOutWarehouse: record.inOutWarehouse,
|
||||
warehouseHead: warehouseHeads?.get(record.inOutWarehouse.trim().toLocaleLowerCase('zh-CN')) ?? '',
|
||||
warehouseType: warehouseTypes?.get(normalizeWarehouseName(record.inOutWarehouse)) ?? '',
|
||||
warehouseHead: warehouseHeads?.get(normalizeWarehouseName(record.inOutWarehouse)) ?? '',
|
||||
storageType: storageTypeLabel(
|
||||
warehouseStorageTypes?.get(record.inOutWarehouse.trim().toLocaleLowerCase('zh-CN')),
|
||||
warehouseStorageTypes?.get(normalizeWarehouseName(record.inOutWarehouse)),
|
||||
),
|
||||
isSeasonal: record.isSeasonal,
|
||||
replaceCount: record.replaceCount,
|
||||
@ -133,6 +143,10 @@ export class SupplyChainProductArchiveService {
|
||||
SupplyChainProductArchiveWarehouseConfigRepository,
|
||||
'listActive'
|
||||
> = supplyChainProductArchiveWarehouseConfigRepository,
|
||||
private readonly warehouseProfileRepository: Pick<
|
||||
JstWarehouseProfileRepository,
|
||||
'listByNames'
|
||||
> = jstWarehouseProfileRepository,
|
||||
) {}
|
||||
|
||||
async listPage(params: {
|
||||
@ -151,13 +165,13 @@ export class SupplyChainProductArchiveService {
|
||||
const warehouseConfigs = await this.warehouseConfigRepository.listActive()
|
||||
const warehouseStorageTypes = new Map(
|
||||
warehouseConfigs.map((item) => [
|
||||
item.warehouseName.trim().toLocaleLowerCase('zh-CN'),
|
||||
normalizeWarehouseName(item.warehouseName),
|
||||
item.storageType,
|
||||
]),
|
||||
)
|
||||
const warehouseHeads = new Map(
|
||||
warehouseConfigs.map((item) => [
|
||||
item.warehouseName.trim().toLocaleLowerCase('zh-CN'),
|
||||
normalizeWarehouseName(item.warehouseName),
|
||||
item.warehouseHead.trim(),
|
||||
]),
|
||||
)
|
||||
@ -182,6 +196,20 @@ export class SupplyChainProductArchiveService {
|
||||
} catch {
|
||||
// Keep the archive usable when the external stock monitor source is temporarily unavailable.
|
||||
}
|
||||
let warehouseTypes = new Map<string, string>()
|
||||
try {
|
||||
const profiles = await this.warehouseProfileRepository.listByNames(
|
||||
records.map((record) => record.inOutWarehouse),
|
||||
)
|
||||
warehouseTypes = new Map(
|
||||
profiles.map((profile) => [
|
||||
normalizeWarehouseName(profile.warehouseName),
|
||||
profile.warehouseType,
|
||||
]),
|
||||
)
|
||||
} catch {
|
||||
// Keep the archive usable when the external warehouse source is temporarily unavailable.
|
||||
}
|
||||
const stockByBarcode = new Map(
|
||||
stockProfiles
|
||||
.filter((profile) => profile.barcode.trim())
|
||||
@ -206,6 +234,7 @@ export class SupplyChainProductArchiveService {
|
||||
(stockBySku.get(normalizeStockKey(record.sku))?.length === 1
|
||||
? stockBySku.get(normalizeStockKey(record.sku))?.[0]
|
||||
: undefined),
|
||||
warehouseTypes,
|
||||
warehouseStorageTypes,
|
||||
warehouseHeads,
|
||||
),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user