Files
calculator2026/src/lib/pricingMethodTotals.ts
T
2026-03-12 12:02:05 +08:00

939 lines
33 KiB
TypeScript

import {
expertList,
getMajorDictEntries,
getMajorIdAliasMap,
getServiceDictById,
taskList
} from '@/sql'
import { roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
import { toFiniteNumberOrNull } from '@/lib/number'
import { getBenchmarkBudgetByScale, getScaleBudgetFee } from '@/lib/pricingScaleFee'
import { useZxFwPricingStore, type ServicePricingMethod } from '@/pinia/zxFwPricing'
import { useKvStore } from '@/pinia/kv'
interface StoredDetailRowsState<T = any> {
detailRows?: T[]
}
interface StoredFactorState {
detailRows?: Array<{
id: string
standardFactor?: number | null
budgetValue?: number | null
}>
}
type MaybeNumber = number | null | undefined
const sumByNumberNullable = <T>(list: T[], pick: (item: T) => MaybeNumber): number | null => {
let hasValid = false
const total = sumByNumber(list, item => {
const value = toFiniteNumberOrNull(pick(item))
if (value == null) return null
hasValid = true
return value
})
return hasValid ? total : null
}
interface ScaleRow {
id: string
amount: number | null
landArea: number | null
consultCategoryFactor: number | null
majorFactor: number | null
workStageFactor: number | null
workRatio: number | null
}
interface WorkloadRow {
id: string
conversion: number | null
workload: number | null
basicFee: number | null
budgetAdoptedUnitPrice: number | null
consultCategoryFactor: number | null
}
interface HourlyRow {
id: string
adoptedBudgetUnitPrice: number | null
personnelCount: number | null
workdayCount: number | null
}
interface MajorLite {
code: string
defCoe: number | null
hasCost?: boolean
hasArea?: boolean
industryId?: string | number | null
}
interface ServiceLite {
defCoe: number | null
onlyCostScale?: boolean | null
}
interface TaskLite {
serviceID: number
conversion: number | null
defPrice: number | null
}
interface ExpertLite {
defPrice: number | null
manageCoe: number | null
}
interface XmBaseInfoState {
projectIndustry?: string
}
export interface PricingMethodTotals {
investScale: number | null
landScale: number | null
workload: number | null
hourly: number | null
}
interface PricingMethodTotalsOptions {
excludeInvestmentCostAndAreaRows?: boolean
}
interface PricingMethodDetailDbKeys {
investScale: string
landScale: string
workload: string
hourly: string
}
interface PricingMethodDefaultDetailRows {
investScale: unknown[]
landScale: unknown[]
workload: unknown[]
hourly: unknown[]
}
interface PricingMethodDefaultBuildContext {
htData: StoredDetailRowsState | null
consultCategoryFactorMap: Map<string, number | null>
majorFactorMap: Map<string, number | null>
industryId: string
excludeInvestmentCostAndAreaRows: boolean
}
const ONLY_COST_SCALE_ROW_ID = '__only-cost-scale-total__'
const SERVICE_PRICING_METHODS: ServicePricingMethod[] = ['investScale', 'landScale', 'workload', 'hourly']
const getZxFwStoreSafely = () => {
try {
return useZxFwPricingStore()
} catch {
return null
}
}
const getKvStoreSafely = () => {
try {
return useKvStore()
} catch {
return null
}
}
const kvGetItem = async <T = unknown>(key: string): Promise<T | null> => {
const store = getKvStoreSafely()
if (!store) return null
return store.getItem<T>(key)
}
const kvSetItem = async <T = unknown>(key: string, value: T): Promise<void> => {
const store = getKvStoreSafely()
if (!store) return
await store.setItem(key, value)
}
const toStoredDetailRowsState = <TRow = unknown>(state: { detailRows?: TRow[] } | null | undefined): StoredDetailRowsState<TRow> | null => {
if (!state || !Array.isArray(state.detailRows)) return null
return {
detailRows: JSON.parse(JSON.stringify(state.detailRows))
}
}
const hasOwn = (obj: unknown, key: string) =>
Object.prototype.hasOwnProperty.call(obj || {}, key)
const isGroupScaleRow = (row: unknown) =>
Boolean(row && typeof row === 'object' && (row as Record<string, unknown>).isGroupRow === true)
const stripGroupScaleRows = <TRow>(rows: TRow[] | undefined): TRow[] =>
(rows || []).filter(row => !isGroupScaleRow(row))
const getRowNumberOrFallback = (
row: Record<string, unknown> | undefined,
key: string,
fallback: number | null
) => {
if (!row) return fallback
const value = toFiniteNumberOrNull(row[key])
if (value != null) return value
return hasOwn(row, key) ? null : fallback
}
const toRowMap = <TRow extends { id: string }>(rows?: TRow[]) => {
const map = new Map<string, TRow>()
for (const row of rows || []) {
map.set(String(row.id), row)
}
return map
}
const getDefaultConsultCategoryFactor = (serviceId: string | number) => {
const service = (getServiceDictById() as Record<string, ServiceLite | undefined>)[String(serviceId)]
return toFiniteNumberOrNull(service?.defCoe)
}
const isOnlyCostScaleService = (serviceId: string | number) => {
const service = (getServiceDictById() as Record<string, ServiceLite | undefined>)[String(serviceId)]
return service?.onlyCostScale === true
}
const majorById = new Map(getMajorDictEntries().map(({ id, item }) => [id, item as MajorLite]))
const majorIdAliasMap = getMajorIdAliasMap()
const getDefaultMajorFactorById = (id: string) => {
const resolvedId = majorById.has(id) ? id : majorIdAliasMap.get(id) || id
const major = majorById.get(resolvedId)
return toFiniteNumberOrNull(major?.defCoe)
}
const isCostMajorById = (id: string) => {
const resolvedId = majorById.has(id) ? id : majorIdAliasMap.get(id) || id
const major = majorById.get(resolvedId)
if (!major) return false
return major.hasCost !== false
}
const isAreaMajorById = (id: string) => {
const resolvedId = majorById.has(id) ? id : majorIdAliasMap.get(id) || id
const major = majorById.get(resolvedId)
if (!major) return false
return major.hasArea !== false
}
const isDualScaleMajorById = (id: string) => {
const resolvedId = majorById.has(id) ? id : majorIdAliasMap.get(id) || id
const major = majorById.get(resolvedId)
if (!major) return false
const hasCost = major.hasCost !== false
const hasArea = major.hasArea !== false
return hasCost && hasArea
}
const getIndustryMajorEntryByIndustryId = (industryId: string | null | undefined) => {
const key = String(industryId || '').trim()
if (!key) return null
for (const [id, item] of majorById.entries()) {
const majorIndustryId = String(item?.industryId ?? '').trim()
if (majorIndustryId === key && !String(item?.code || '').includes('-')) {
return { id, item }
}
}
return null
}
const resolveFactorValue = (
row: { budgetValue?: number | null; standardFactor?: number | null } | undefined,
fallback: number | null
) => {
if (!row) return fallback
const budgetValue = toFiniteNumberOrNull(row.budgetValue)
if (budgetValue != null) return budgetValue
const standardFactor = toFiniteNumberOrNull(row.standardFactor)
if (standardFactor != null) return standardFactor
return fallback
}
const buildConsultCategoryFactorMap = (state: StoredFactorState | null) => {
const map = new Map<string, number | null>()
const serviceDict = getServiceDictById() as Record<string, ServiceLite | undefined>
for (const [id, item] of Object.entries(serviceDict)) {
map.set(String(id), toFiniteNumberOrNull(item?.defCoe))
}
for (const row of state?.detailRows || []) {
if (!row?.id) continue
const id = String(row.id)
map.set(id, resolveFactorValue(row, map.get(id) ?? null))
}
return map
}
const buildMajorFactorMap = (state: StoredFactorState | null) => {
const map = new Map<string, number | null>()
for (const [id, item] of majorById.entries()) {
map.set(String(id), toFiniteNumberOrNull(item?.defCoe))
}
for (const row of state?.detailRows || []) {
if (!row?.id) continue
const rowId = String(row.id)
const id = map.has(rowId) ? rowId : majorIdAliasMap.get(rowId) || rowId
map.set(id, resolveFactorValue(row, map.get(id) ?? null))
}
return map
}
const getMajorLeafIds = () =>
getMajorDictEntries()
.filter(({ item }) => Boolean(item?.code && String(item.code).includes('-')))
.map(({ id }) => id)
const buildDefaultScaleRows = (
serviceId: string | number,
consultCategoryFactorMap?: Map<string, number | null>,
majorFactorMap?: Map<string, number | null>
): ScaleRow[] => {
const defaultConsultCategoryFactor =
consultCategoryFactorMap?.get(String(serviceId)) ?? getDefaultConsultCategoryFactor(serviceId)
return getMajorLeafIds().map(id => ({
id,
amount: null,
landArea: null,
consultCategoryFactor: defaultConsultCategoryFactor,
majorFactor: majorFactorMap?.get(id) ?? getDefaultMajorFactorById(id),
workStageFactor: 1,
workRatio: 100
}))
}
const mergeScaleRows = (
serviceId: string | number,
rowsFromDb: Array<Partial<ScaleRow> & Pick<ScaleRow, 'id'>> | undefined,
consultCategoryFactorMap?: Map<string, number | null>,
majorFactorMap?: Map<string, number | null>
): ScaleRow[] => {
const sourceRows = stripGroupScaleRows(rowsFromDb)
const dbValueMap = toRowMap(sourceRows)
for (const row of sourceRows) {
const rowId = String(row.id)
const nextId = majorIdAliasMap.get(rowId)
if (nextId && !dbValueMap.has(nextId)) {
dbValueMap.set(nextId, row as ScaleRow)
}
}
const defaultConsultCategoryFactor =
consultCategoryFactorMap?.get(String(serviceId)) ?? getDefaultConsultCategoryFactor(serviceId)
return buildDefaultScaleRows(serviceId, consultCategoryFactorMap, majorFactorMap).map(row => {
const fromDb = dbValueMap.get(row.id)
if (!fromDb) return row
const hasConsultCategoryFactor = hasOwn(fromDb, 'consultCategoryFactor')
const hasMajorFactor = hasOwn(fromDb, 'majorFactor')
const hasWorkStageFactor = hasOwn(fromDb, 'workStageFactor')
const hasWorkRatio = hasOwn(fromDb, 'workRatio')
return {
...row,
amount: toFiniteNumberOrNull(fromDb.amount),
landArea: toFiniteNumberOrNull(fromDb.landArea),
consultCategoryFactor:
toFiniteNumberOrNull(fromDb.consultCategoryFactor) ??
(hasConsultCategoryFactor ? null : defaultConsultCategoryFactor),
majorFactor:
toFiniteNumberOrNull(fromDb.majorFactor) ??
(hasMajorFactor ? null : (majorFactorMap?.get(row.id) ?? getDefaultMajorFactorById(row.id))),
workStageFactor:
toFiniteNumberOrNull((fromDb as Partial<ScaleRow>).workStageFactor) ??
(hasWorkStageFactor ? null : row.workStageFactor),
workRatio:
toFiniteNumberOrNull((fromDb as Partial<ScaleRow>).workRatio) ??
(hasWorkRatio ? null : row.workRatio)
}
})
}
const getBenchmarkBudgetByAmount = (amount: MaybeNumber) =>
getBenchmarkBudgetByScale(amount, 'cost')
const getBenchmarkBudgetByLandArea = (landArea: MaybeNumber) =>
getBenchmarkBudgetByScale(landArea, 'area')
const getInvestmentBudgetFee = (row: ScaleRow) => {
return getScaleBudgetFee({
benchmarkBudget: getBenchmarkBudgetByAmount(row.amount),
majorFactor: row.majorFactor,
consultCategoryFactor: row.consultCategoryFactor,
workStageFactor: row.workStageFactor,
workRatio: row.workRatio
})
}
const getOnlyCostScaleBudgetFee = (
serviceId: string,
rowsFromDb: Array<Record<string, unknown>> | undefined,
consultCategoryFactorMap?: Map<string, number | null>,
majorFactorMap?: Map<string, number | null>,
industryId?: string | null
) => {
const industryMajorEntry = getIndustryMajorEntryByIndustryId(industryId)
const sourceRows = stripGroupScaleRows(rowsFromDb)
const defaultConsultCategoryFactor =
consultCategoryFactorMap?.get(String(serviceId)) ?? getDefaultConsultCategoryFactor(serviceId)
const defaultMajorFactor =
(industryMajorEntry ? majorFactorMap?.get(industryMajorEntry.id) ?? null : null) ??
toFiniteNumberOrNull(industryMajorEntry?.item?.defCoe) ??
1
// 新版 onlyCostScale 支持“按项目行”存储(如 1::majorId、2::majorId),每行需独立计费后求和。
const usePerRowCalculation = sourceRows.some(row => {
if (typeof row?.projectIndex === 'number' && Number.isFinite(row.projectIndex)) return true
const id = String(row?.id || '')
return /^\d+::/.test(id)
})
if (usePerRowCalculation) {
return sumByNumberNullable(sourceRows, row => {
const amount = toFiniteNumberOrNull(row?.amount)
if (amount == null) return null
return getScaleBudgetFee({
benchmarkBudget: getBenchmarkBudgetByAmount(amount),
majorFactor: getRowNumberOrFallback(row, 'majorFactor', defaultMajorFactor),
consultCategoryFactor: getRowNumberOrFallback(row, 'consultCategoryFactor', defaultConsultCategoryFactor),
workStageFactor: getRowNumberOrFallback(row, 'workStageFactor', 1),
workRatio: getRowNumberOrFallback(row, 'workRatio', 100)
})
})
}
const totalAmount = sumByNumberNullable(sourceRows, row =>
typeof row?.amount === 'number' && Number.isFinite(row.amount) ? row.amount : null
)
if (totalAmount == null) return null
const onlyRow =
sourceRows.find(row => String(row?.id || '') === ONLY_COST_SCALE_ROW_ID) ||
sourceRows.find(row => hasOwn(row, 'consultCategoryFactor') || hasOwn(row, 'majorFactor')) ||
sourceRows[0]
const consultCategoryFactor = getRowNumberOrFallback(onlyRow, 'consultCategoryFactor', defaultConsultCategoryFactor)
const majorFactor = getRowNumberOrFallback(onlyRow, 'majorFactor', defaultMajorFactor)
const workStageFactor = getRowNumberOrFallback(onlyRow, 'workStageFactor', 1)
const workRatio = getRowNumberOrFallback(onlyRow, 'workRatio', 100)
return getScaleBudgetFee({
benchmarkBudget: getBenchmarkBudgetByAmount(totalAmount),
majorFactor,
consultCategoryFactor,
workStageFactor,
workRatio
})
}
const buildOnlyCostScaleDetailRows = (
serviceId: string,
rowsFromDb: Array<Record<string, unknown>> | undefined,
consultCategoryFactorMap?: Map<string, number | null>,
majorFactorMap?: Map<string, number | null>,
industryId?: string | null
) => {
const sourceRows = stripGroupScaleRows(rowsFromDb)
const totalAmount = sumByNumberNullable(sourceRows, row =>
typeof row?.amount === 'number' && Number.isFinite(row.amount) ? row.amount : null
)
const industryMajorEntry = getIndustryMajorEntryByIndustryId(industryId)
const onlyCostRowId = industryMajorEntry?.id || ONLY_COST_SCALE_ROW_ID
const onlyRow =
sourceRows.find(row => String(row?.id || '') === ONLY_COST_SCALE_ROW_ID) ||
sourceRows.find(row => String(row?.id || '') === onlyCostRowId)
const consultCategoryFactor = getRowNumberOrFallback(
onlyRow,
'consultCategoryFactor',
consultCategoryFactorMap?.get(String(serviceId)) ?? getDefaultConsultCategoryFactor(serviceId)
)
const majorFactor = getRowNumberOrFallback(
onlyRow,
'majorFactor',
(industryMajorEntry ? majorFactorMap?.get(industryMajorEntry.id) ?? null : null) ??
toFiniteNumberOrNull(industryMajorEntry?.item?.defCoe) ??
1
)
const workStageFactor = getRowNumberOrFallback(onlyRow, 'workStageFactor', 1)
const workRatio = getRowNumberOrFallback(onlyRow, 'workRatio', 100)
return [
{
id: onlyCostRowId,
amount: totalAmount,
consultCategoryFactor,
majorFactor,
workStageFactor,
workRatio,
benchmarkBudgetBasicChecked: typeof onlyRow?.benchmarkBudgetBasicChecked === 'boolean' ? onlyRow.benchmarkBudgetBasicChecked : true,
benchmarkBudgetOptionalChecked: typeof onlyRow?.benchmarkBudgetOptionalChecked === 'boolean' ? onlyRow.benchmarkBudgetOptionalChecked : true
}
]
}
const getLandBudgetFee = (row: ScaleRow) => {
return getScaleBudgetFee({
benchmarkBudget: getBenchmarkBudgetByLandArea(row.landArea),
majorFactor: row.majorFactor,
consultCategoryFactor: row.consultCategoryFactor,
workStageFactor: row.workStageFactor,
workRatio: row.workRatio
})
}
const getTaskEntriesByServiceId = (serviceId: string | number) =>
Object.entries(taskList as Record<string, TaskLite>)
.sort((a, b) => Number(a[0]) - Number(b[0]))
.filter(([, task]) => Number(task.serviceID) === Number(serviceId))
const buildDefaultWorkloadRows = (
serviceId: string | number,
consultCategoryFactorMap?: Map<string, number | null>
): WorkloadRow[] => {
const defaultConsultCategoryFactor =
consultCategoryFactorMap?.get(String(serviceId)) ?? getDefaultConsultCategoryFactor(serviceId)
return getTaskEntriesByServiceId(serviceId).map(([taskId, task], order) => ({
id: `task-${taskId}-${order}`,
conversion: toFiniteNumberOrNull(task.conversion),
workload: null,
basicFee: null,
budgetAdoptedUnitPrice: toFiniteNumberOrNull(task.defPrice),
consultCategoryFactor: defaultConsultCategoryFactor
}))
}
const mergeWorkloadRows = (
serviceId: string | number,
rowsFromDb: Array<Partial<WorkloadRow> & Pick<WorkloadRow, 'id'>> | undefined,
consultCategoryFactorMap?: Map<string, number | null>
): WorkloadRow[] => {
const dbValueMap = toRowMap(rowsFromDb)
return buildDefaultWorkloadRows(serviceId, consultCategoryFactorMap).map(row => {
const fromDb = dbValueMap.get(row.id)
if (!fromDb) return row
return {
...row,
workload: toFiniteNumberOrNull(fromDb.workload),
basicFee: toFiniteNumberOrNull(fromDb.basicFee),
budgetAdoptedUnitPrice: toFiniteNumberOrNull(fromDb.budgetAdoptedUnitPrice),
consultCategoryFactor: toFiniteNumberOrNull(fromDb.consultCategoryFactor)
}
})
}
const calcWorkloadBasicFee = (row: WorkloadRow) => {
if (
row.budgetAdoptedUnitPrice == null ||
row.conversion == null ||
row.workload == null
) {
return null
}
return roundTo(
toDecimal(row.budgetAdoptedUnitPrice).mul(row.conversion).mul(row.workload),
2
)
}
const calcWorkloadServiceFee = (row: WorkloadRow) => {
if (row.consultCategoryFactor == null) {
return null
}
const basicFee = row.basicFee ?? calcWorkloadBasicFee(row)
if (basicFee == null) return null
return roundTo(
toDecimal(basicFee).mul(row.consultCategoryFactor),
2
)
}
const getExpertEntries = () =>
Object.entries(expertList as Record<string, ExpertLite>).sort((a, b) => Number(a[0]) - Number(b[0]))
const getDefaultHourlyAdoptedPrice = (expert: ExpertLite) => {
if (expert.defPrice == null || expert.manageCoe == null) return null
return roundTo(toDecimal(expert.defPrice).mul(expert.manageCoe), 2)
}
const buildDefaultHourlyRows = (): HourlyRow[] =>
getExpertEntries().map(([expertId, expert]) => ({
id: `expert-${expertId}`,
adoptedBudgetUnitPrice: getDefaultHourlyAdoptedPrice(expert),
personnelCount: null,
workdayCount: null
}))
const mergeHourlyRows = (
rowsFromDb: Array<Partial<HourlyRow> & Pick<HourlyRow, 'id'>> | undefined
): HourlyRow[] => {
const dbValueMap = toRowMap(rowsFromDb)
return buildDefaultHourlyRows().map(row => {
const fromDb = dbValueMap.get(row.id)
if (!fromDb) return row
return {
...row,
adoptedBudgetUnitPrice: toFiniteNumberOrNull(fromDb.adoptedBudgetUnitPrice),
personnelCount: toFiniteNumberOrNull(fromDb.personnelCount),
workdayCount: toFiniteNumberOrNull(fromDb.workdayCount)
}
})
}
const calcHourlyServiceBudget = (row: HourlyRow) => {
if (row.adoptedBudgetUnitPrice == null || row.personnelCount == null || row.workdayCount == null) return null
return roundTo(toDecimal(row.adoptedBudgetUnitPrice).mul(row.personnelCount).mul(row.workdayCount), 2)
}
const resolveScaleRows = (
serviceId: string,
pricingData: StoredDetailRowsState | null,
htData: StoredDetailRowsState | null,
consultCategoryFactorMap?: Map<string, number | null>,
majorFactorMap?: Map<string, number | null>
) => {
if (pricingData?.detailRows != null) {
return mergeScaleRows(
serviceId,
pricingData.detailRows as any,
consultCategoryFactorMap,
majorFactorMap
)
}
if (htData?.detailRows != null) {
return mergeScaleRows(
serviceId,
stripGroupScaleRows(htData.detailRows as any),
consultCategoryFactorMap,
majorFactorMap
)
}
return buildDefaultScaleRows(serviceId, consultCategoryFactorMap, majorFactorMap)
}
export const getPricingMethodDetailDbKeys = (
contractId: string,
serviceId: string | number
): PricingMethodDetailDbKeys => {
const normalizedServiceId = String(serviceId)
const store = getZxFwStoreSafely()
if (store) {
return {
investScale: store.getServicePricingStorageKey(contractId, normalizedServiceId, 'investScale'),
landScale: store.getServicePricingStorageKey(contractId, normalizedServiceId, 'landScale'),
workload: store.getServicePricingStorageKey(contractId, normalizedServiceId, 'workload'),
hourly: store.getServicePricingStorageKey(contractId, normalizedServiceId, 'hourly')
}
}
return {
investScale: `tzGMF-${contractId}-${normalizedServiceId}`,
landScale: `ydGMF-${contractId}-${normalizedServiceId}`,
workload: `gzlF-${contractId}-${normalizedServiceId}`,
hourly: `hourlyPricing-${contractId}-${normalizedServiceId}`
}
}
const loadPricingMethodDefaultBuildContext = async (
contractId: string,
options?: PricingMethodTotalsOptions
): Promise<PricingMethodDefaultBuildContext> => {
const htDbKey = `ht-info-v3-${contractId}`
const consultFactorDbKey = `ht-consult-category-factor-v1-${contractId}`
const majorFactorDbKey = `ht-major-factor-v1-${contractId}`
const baseInfoDbKey = 'xm-base-info-v1'
const [htData, consultFactorData, majorFactorData, baseInfo] = await Promise.all([
kvGetItem<StoredDetailRowsState>(htDbKey),
kvGetItem<StoredFactorState>(consultFactorDbKey),
kvGetItem<StoredFactorState>(majorFactorDbKey),
kvGetItem<XmBaseInfoState>(baseInfoDbKey)
])
return {
htData,
consultCategoryFactorMap: buildConsultCategoryFactorMap(consultFactorData),
majorFactorMap: buildMajorFactorMap(majorFactorData),
industryId: typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : '',
excludeInvestmentCostAndAreaRows: options?.excludeInvestmentCostAndAreaRows === true
}
}
const buildDefaultPricingMethodDetailRows = (
serviceId: string,
context: PricingMethodDefaultBuildContext
): PricingMethodDefaultDetailRows => {
const onlyCostScale = isOnlyCostScaleService(serviceId)
const scaleRows = resolveScaleRows(
serviceId,
null,
context.htData,
context.consultCategoryFactorMap,
context.majorFactorMap
)
const investScale = onlyCostScale
? buildOnlyCostScaleDetailRows(
serviceId,
context.htData?.detailRows as Array<Record<string, unknown>> | undefined,
context.consultCategoryFactorMap,
context.majorFactorMap,
context.industryId
)
: scaleRows.filter(row => {
if (!isCostMajorById(row.id)) return false
if (context.excludeInvestmentCostAndAreaRows && isDualScaleMajorById(row.id)) return false
return true
})
const landScale = scaleRows.filter(row => isAreaMajorById(row.id))
return {
investScale,
landScale,
workload: buildDefaultWorkloadRows(serviceId, context.consultCategoryFactorMap),
hourly: buildDefaultHourlyRows()
}
}
export const persistDefaultPricingMethodDetailRowsForServices = async (params: {
contractId: string
serviceIds: Array<string | number>
options?: PricingMethodTotalsOptions
}) => {
const uniqueServiceIds = Array.from(new Set(params.serviceIds.map(serviceId => String(serviceId))))
if (uniqueServiceIds.length === 0) return
const context = await loadPricingMethodDefaultBuildContext(params.contractId, params.options)
const store = getZxFwStoreSafely()
await Promise.all(
uniqueServiceIds.map(async serviceId => {
const dbKeys = getPricingMethodDetailDbKeys(params.contractId, serviceId)
const defaultRows = buildDefaultPricingMethodDetailRows(serviceId, context)
if (store) {
for (const method of SERVICE_PRICING_METHODS) {
store.setServicePricingMethodState(params.contractId, serviceId, method, {
detailRows: defaultRows[method]
}, { force: true })
}
}
await Promise.all([
kvSetItem(dbKeys.investScale, { detailRows: defaultRows.investScale }),
kvSetItem(dbKeys.landScale, { detailRows: defaultRows.landScale }),
kvSetItem(dbKeys.workload, { detailRows: defaultRows.workload }),
kvSetItem(dbKeys.hourly, { detailRows: defaultRows.hourly })
])
})
)
}
export const getPricingMethodTotalsForService = async (params: {
contractId: string
serviceId: string | number
options?: PricingMethodTotalsOptions
}): Promise<PricingMethodTotals> => {
const serviceId = String(params.serviceId)
const htDbKey = `ht-info-v3-${params.contractId}`
const consultFactorDbKey = `ht-consult-category-factor-v1-${params.contractId}`
const majorFactorDbKey = `ht-major-factor-v1-${params.contractId}`
const baseInfoDbKey = 'xm-base-info-v1'
const dbKeys = getPricingMethodDetailDbKeys(params.contractId, serviceId)
const store = getZxFwStoreSafely()
const [storeInvestData, storeLandData, storeWorkloadData, storeHourlyData, htData, consultFactorData, majorFactorData, baseInfo] = await Promise.all([
store?.loadServicePricingMethodState<Record<string, unknown>>(params.contractId, serviceId, 'investScale') || Promise.resolve(null),
store?.loadServicePricingMethodState<Record<string, unknown>>(params.contractId, serviceId, 'landScale') || Promise.resolve(null),
store?.loadServicePricingMethodState<Record<string, unknown>>(params.contractId, serviceId, 'workload') || Promise.resolve(null),
store?.loadServicePricingMethodState<Record<string, unknown>>(params.contractId, serviceId, 'hourly') || Promise.resolve(null),
kvGetItem<StoredDetailRowsState>(htDbKey),
kvGetItem<StoredFactorState>(consultFactorDbKey),
kvGetItem<StoredFactorState>(majorFactorDbKey),
kvGetItem<XmBaseInfoState>(baseInfoDbKey)
])
const [investDataFallback, landDataFallback, workloadDataFallback, hourlyDataFallback] = await Promise.all([
storeInvestData ? Promise.resolve(null) : kvGetItem<StoredDetailRowsState>(dbKeys.investScale),
storeLandData ? Promise.resolve(null) : kvGetItem<StoredDetailRowsState>(dbKeys.landScale),
storeWorkloadData ? Promise.resolve(null) : kvGetItem<StoredDetailRowsState>(dbKeys.workload),
storeHourlyData ? Promise.resolve(null) : kvGetItem<StoredDetailRowsState>(dbKeys.hourly)
])
const investData = toStoredDetailRowsState(storeInvestData) || investDataFallback
const landData = toStoredDetailRowsState(storeLandData) || landDataFallback
const workloadData = toStoredDetailRowsState(storeWorkloadData) || workloadDataFallback
const hourlyData = toStoredDetailRowsState(storeHourlyData) || hourlyDataFallback
const consultCategoryFactorMap = buildConsultCategoryFactorMap(consultFactorData)
const majorFactorMap = buildMajorFactorMap(majorFactorData)
const onlyCostScale = isOnlyCostScaleService(serviceId)
const industryId = typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
// 优先使用对应计费页的数据;不存在时回退合同段规模信息,再回退默认字典行。
const excludeInvestmentCostAndAreaRows = params.options?.excludeInvestmentCostAndAreaRows === true
const investScale = onlyCostScale
? getOnlyCostScaleBudgetFee(
serviceId,
(investData?.detailRows as Array<Record<string, unknown>> | undefined) ||
(htData?.detailRows as Array<Record<string, unknown>> | undefined),
consultCategoryFactorMap,
majorFactorMap,
industryId
)
: (() => {
const investRows = resolveScaleRows(
serviceId,
investData,
htData,
consultCategoryFactorMap,
majorFactorMap
)
return sumByNumberNullable(investRows, row => {
if (!isCostMajorById(row.id)) return null
if (excludeInvestmentCostAndAreaRows && isDualScaleMajorById(row.id)) return null
return getInvestmentBudgetFee(row)
})
})()
const landRows = resolveScaleRows(
serviceId,
landData,
htData,
consultCategoryFactorMap,
majorFactorMap
)
const landScale = sumByNumberNullable(landRows, row => (isAreaMajorById(row.id) ? getLandBudgetFee(row) : null))
const defaultWorkloadRows = buildDefaultWorkloadRows(serviceId, consultCategoryFactorMap)
const workload =
defaultWorkloadRows.length === 0
? null
: sumByNumberNullable(
workloadData?.detailRows != null
? mergeWorkloadRows(serviceId, workloadData.detailRows as any, consultCategoryFactorMap)
: defaultWorkloadRows,
row => calcWorkloadServiceFee(row)
)
const hourlyRows =
hourlyData?.detailRows != null
? mergeHourlyRows(hourlyData.detailRows as any)
: buildDefaultHourlyRows()
const hourly = sumByNumberNullable(hourlyRows, row => calcHourlyServiceBudget(row))
return {
investScale,
landScale,
workload,
hourly
}
}
export const ensurePricingMethodDetailRowsForServices = async (params: {
contractId: string
serviceIds: Array<string | number>
options?: PricingMethodTotalsOptions
}) => {
const uniqueServiceIds = Array.from(new Set(params.serviceIds.map(serviceId => String(serviceId))))
if (uniqueServiceIds.length === 0) return
const context = await loadPricingMethodDefaultBuildContext(params.contractId, params.options)
const store = getZxFwStoreSafely()
await Promise.all(
uniqueServiceIds.map(async serviceId => {
const dbKeys = getPricingMethodDetailDbKeys(params.contractId, serviceId)
const [storeInvestData, storeLandData, storeWorkloadData, storeHourlyData] = await Promise.all([
store?.loadServicePricingMethodState<Record<string, unknown>>(params.contractId, serviceId, 'investScale') || Promise.resolve(null),
store?.loadServicePricingMethodState<Record<string, unknown>>(params.contractId, serviceId, 'landScale') || Promise.resolve(null),
store?.loadServicePricingMethodState<Record<string, unknown>>(params.contractId, serviceId, 'workload') || Promise.resolve(null),
store?.loadServicePricingMethodState<Record<string, unknown>>(params.contractId, serviceId, 'hourly') || Promise.resolve(null)
])
const [investDataFallback, landDataFallback, workloadDataFallback, hourlyDataFallback] = await Promise.all([
storeInvestData ? Promise.resolve(null) : kvGetItem<StoredDetailRowsState>(dbKeys.investScale),
storeLandData ? Promise.resolve(null) : kvGetItem<StoredDetailRowsState>(dbKeys.landScale),
storeWorkloadData ? Promise.resolve(null) : kvGetItem<StoredDetailRowsState>(dbKeys.workload),
storeHourlyData ? Promise.resolve(null) : kvGetItem<StoredDetailRowsState>(dbKeys.hourly)
])
const investData = toStoredDetailRowsState(storeInvestData) || investDataFallback
const landData = toStoredDetailRowsState(storeLandData) || landDataFallback
const workloadData = toStoredDetailRowsState(storeWorkloadData) || workloadDataFallback
const hourlyData = toStoredDetailRowsState(storeHourlyData) || hourlyDataFallback
const shouldInitInvest = !Array.isArray(investData?.detailRows) || investData!.detailRows!.length === 0
const shouldInitLand = !Array.isArray(landData?.detailRows) || landData!.detailRows!.length === 0
const shouldInitWorkload = !Array.isArray(workloadData?.detailRows) || workloadData!.detailRows!.length === 0
const shouldInitHourly = !Array.isArray(hourlyData?.detailRows) || hourlyData!.detailRows!.length === 0
const writeTasks: Promise<unknown>[] = []
let defaultRows: PricingMethodDefaultDetailRows | null = null
const getDefaultRows = () => {
if (!defaultRows) {
defaultRows = buildDefaultPricingMethodDetailRows(serviceId, context)
}
return defaultRows
}
if (shouldInitInvest) {
if (store) {
store.setServicePricingMethodState(params.contractId, serviceId, 'investScale', {
detailRows: getDefaultRows().investScale
}, { force: true })
}
writeTasks.push(kvSetItem(dbKeys.investScale, { detailRows: getDefaultRows().investScale }))
}
if (shouldInitLand) {
if (store) {
store.setServicePricingMethodState(params.contractId, serviceId, 'landScale', {
detailRows: getDefaultRows().landScale
}, { force: true })
}
writeTasks.push(kvSetItem(dbKeys.landScale, { detailRows: getDefaultRows().landScale }))
}
if (shouldInitWorkload) {
if (store) {
store.setServicePricingMethodState(params.contractId, serviceId, 'workload', {
detailRows: getDefaultRows().workload
}, { force: true })
}
writeTasks.push(kvSetItem(dbKeys.workload, { detailRows: getDefaultRows().workload }))
}
if (shouldInitHourly) {
if (store) {
store.setServicePricingMethodState(params.contractId, serviceId, 'hourly', {
detailRows: getDefaultRows().hourly
}, { force: true })
}
writeTasks.push(kvSetItem(dbKeys.hourly, { detailRows: getDefaultRows().hourly }))
}
if (writeTasks.length > 0) {
await Promise.all(writeTasks)
}
})
)
}
export const getPricingMethodTotalsForServices = async (params: {
contractId: string
serviceIds: Array<string | number>
options?: PricingMethodTotalsOptions
}) => {
const result = new Map<string, PricingMethodTotals>()
await Promise.all(
params.serviceIds.map(async serviceId => {
const totals = await getPricingMethodTotalsForService({
contractId: params.contractId,
serviceId,
options: params.options
})
result.set(String(serviceId), totals)
})
)
return result
}