calculator2026/src/lib/pricingScaleDetail.ts
2026-03-24 16:43:15 +08:00

165 lines
5.9 KiB
TypeScript

import { addNumbers, roundTo } from '@/lib/decimal'
import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit, type ScaleFeeSplitResult } from '@/lib/pricingScaleFee'
export type ScaleMode = 'cost' | 'area'
export interface ScaleBudgetCheckRow {
benchmarkBudgetBasicChecked?: boolean
benchmarkBudgetOptionalChecked?: boolean
}
export interface ScaleBudgetSourceRow extends ScaleBudgetCheckRow {
amount?: number | null
landArea?: number | null
majorFactor?: number | null
consultCategoryFactor?: number | null
workStageFactor?: number | null
workRatio?: number | null
}
export interface ScaleDetailComputedRow extends ScaleBudgetSourceRow {
benchmarkBudget?: number | null
benchmarkBudgetBasic?: number | null
benchmarkBudgetOptional?: number | null
basicFormula?: string | null
optionalFormula?: string | null
budgetFee?: number | null
budgetFeeBasic?: number | null
budgetFeeOptional?: number | null
}
export type CheckedScaleFeeSplitResult = Omit<ScaleFeeSplitResult, 'total'> & {
total: number | null
}
const getScaleValueByMode = (
row: Pick<ScaleBudgetSourceRow, 'amount' | 'landArea'> | undefined,
mode: ScaleMode
) => (mode === 'cost' ? row?.amount : row?.landArea)
export const isSameNullableNumber = (left: number | null | undefined, right: number | null | undefined) => {
if (left == null && right == null) return true
if (left == null || right == null) return false
return roundTo(left, 6) === roundTo(right, 6)
}
export const isSameNullableText = (left: string | null | undefined, right: string | null | undefined) =>
String(left ?? '') === String(right ?? '')
export const isBenchmarkBudgetFullyUnchecked = (
row?: Pick<ScaleBudgetCheckRow, 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked'>
) => row?.benchmarkBudgetBasicChecked === false && row?.benchmarkBudgetOptionalChecked === false
export const getBenchmarkBudgetRawSplitByRow = (
row: Pick<ScaleBudgetSourceRow, 'amount' | 'landArea'> | undefined,
mode: ScaleMode
) => getBenchmarkBudgetSplitByScale(getScaleValueByMode(row, mode), mode)
export const getCheckedBenchmarkBudgetSplitByRow = (
row?: Pick<ScaleBudgetSourceRow, 'amount' | 'landArea' | 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked'>,
mode: ScaleMode = 'cost'
): CheckedScaleFeeSplitResult | null => {
const split = getBenchmarkBudgetRawSplitByRow(row, mode)
if (!split) return null
const basic = row?.benchmarkBudgetBasicChecked === false ? 0 : split.basic
const optional = row?.benchmarkBudgetOptionalChecked === false ? 0 : split.optional
return {
...split,
basic,
optional,
total: isBenchmarkBudgetFullyUnchecked(row) ? null : roundTo(addNumbers(basic, optional), 2)
}
}
export const getScaleBudgetFeeSplitByRow = (
row?: Pick<
ScaleBudgetSourceRow,
| 'amount'
| 'landArea'
| 'benchmarkBudgetBasicChecked'
| 'benchmarkBudgetOptionalChecked'
| 'majorFactor'
| 'consultCategoryFactor'
| 'workStageFactor'
| 'workRatio'
>,
mode: ScaleMode = 'cost'
) => {
if (isBenchmarkBudgetFullyUnchecked(row)) return null
const benchmarkBudgetSplit = getCheckedBenchmarkBudgetSplitByRow(row, mode)
if (!benchmarkBudgetSplit) return null
return getScaleBudgetFeeSplit({
benchmarkBudgetBasic: benchmarkBudgetSplit.basic,
benchmarkBudgetOptional: benchmarkBudgetSplit.optional,
majorFactor: row?.majorFactor,
consultCategoryFactor: row?.consultCategoryFactor,
workStageFactor: row?.workStageFactor,
workRatio: row?.workRatio
})
}
export const getScaleBudgetFeeByRow = (
row?: Pick<
ScaleBudgetSourceRow,
| 'amount'
| 'landArea'
| 'benchmarkBudgetBasicChecked'
| 'benchmarkBudgetOptionalChecked'
| 'majorFactor'
| 'consultCategoryFactor'
| 'workStageFactor'
| 'workRatio'
>,
mode: ScaleMode = 'cost'
) => getScaleBudgetFeeSplitByRow(row, mode)?.total ?? null
export const recomputeScaleDetailRow = <TRow extends ScaleDetailComputedRow>(
row: TRow,
mode: ScaleMode
): TRow => {
const benchmarkBudgetRawSplit = getBenchmarkBudgetRawSplitByRow(row, mode)
const benchmarkBudgetSplit = getCheckedBenchmarkBudgetSplitByRow(row, mode)
const budgetFeeSplit = getScaleBudgetFeeSplitByRow(row, mode)
return {
...row,
benchmarkBudget: benchmarkBudgetSplit?.total ?? null,
benchmarkBudgetBasic: benchmarkBudgetSplit?.basic ?? null,
benchmarkBudgetOptional: benchmarkBudgetSplit?.optional ?? null,
basicFormula: row.benchmarkBudgetBasicChecked === false ? null : (benchmarkBudgetRawSplit?.basicFormula ?? ''),
optionalFormula: row.benchmarkBudgetOptionalChecked === false ? null : (benchmarkBudgetRawSplit?.optionalFormula ?? ''),
budgetFee: budgetFeeSplit?.total ?? null,
budgetFeeBasic: budgetFeeSplit?.basic ?? null,
budgetFeeOptional: budgetFeeSplit?.optional ?? null
}
}
export const recomputeScaleDetailRowsInPlace = <TRow extends ScaleDetailComputedRow>(
rows: TRow[],
mode: ScaleMode
) => {
for (const row of rows) {
Object.assign(row, recomputeScaleDetailRow(row, mode))
}
}
export const isSameScaleDetailRow = (
left: ScaleDetailComputedRow,
right: ScaleDetailComputedRow,
mode: ScaleMode
) => {
const isSameScaleValue = mode === 'cost'
? isSameNullableNumber(left.amount, right.amount)
: isSameNullableNumber(left.landArea, right.landArea)
return isSameScaleValue
&& isSameNullableNumber(left.benchmarkBudget, right.benchmarkBudget)
&& isSameNullableNumber(left.benchmarkBudgetBasic, right.benchmarkBudgetBasic)
&& isSameNullableNumber(left.benchmarkBudgetOptional, right.benchmarkBudgetOptional)
&& isSameNullableText(left.basicFormula, right.basicFormula)
&& isSameNullableText(left.optionalFormula, right.optionalFormula)
&& isSameNullableNumber(left.budgetFee, right.budgetFee)
&& isSameNullableNumber(left.budgetFeeBasic, right.budgetFeeBasic)
&& isSameNullableNumber(left.budgetFeeOptional, right.budgetFeeOptional)
}