ultimatepos/Modules/AssetExchange/Services/ValuationEngine.php

162 lines
5.6 KiB
PHP

<?php
namespace Modules\AssetExchange\Services;
use Carbon\Carbon;
use Modules\AssetExchange\Models\Appraisal;
use Modules\AssetExchange\Models\AppraisalLine;
class ValuationEngine
{
public function defaultSettings(): array
{
return [
'depreciation_rate_machine' => 0.08,
'depreciation_rate_line' => 0.10,
'min_age_factor' => 0.3,
'target_health_factor' => 0.9,
'renovation_multipliers' => [
'none' => 0.0,
'minor' => 0.05,
'major' => 0.25,
'replace' => 1.0,
],
'labor_rate_per_hour' => 0,
'buy_margin_percent' => 15,
'sell_margin_percent' => 20,
];
}
public function resolveSettings(array $businessSettings = []): array
{
return array_merge($this->defaultSettings(), $businessSettings);
}
/**
* @return array{health_factor: float, age_factor: float, line_as_is_value: int, renovation_cost: int, line_post_renovation_value: int, age_years: float|null}
*/
public function calculateLineValue(AppraisalLine $line, array $settings = []): array
{
$settings = $this->resolveSettings($settings);
$replacementCost = max(0, (int) $line->replacement_cost);
$healthPct = $line->health_percentage !== null ? (float) $line->health_percentage : 100.0;
$healthFactor = max(0, min(1, $healthPct / 100));
$ageYears = $this->resolveAgeYears($line);
$depRate = (float) ($line->depreciation_rate ?: $this->depreciationRateForLine($line, $settings));
$minAgeFactor = (float) $settings['min_age_factor'];
$ageFactor = $ageYears !== null
? max($minAgeFactor, 1 - ($ageYears * $depRate))
: 1.0;
$renovationCost = $this->estimateRenovationCost($line, $replacementCost, $settings);
$targetHealth = (float) $settings['target_health_factor'];
$postRenovationValue = (int) round($replacementCost * $targetHealth);
$asIsValue = (int) round($replacementCost * $healthFactor * $ageFactor);
if ($line->renovation_need === 'replace') {
$asIsValue = (int) round($asIsValue * 0.5);
}
return [
'health_factor' => round($healthFactor, 4),
'age_factor' => round($ageFactor, 4),
'age_years' => $ageYears,
'line_as_is_value' => $asIsValue,
'renovation_cost' => $renovationCost,
'line_post_renovation_value' => $postRenovationValue,
];
}
/**
* @return array{as_is_value: int, renovation_cost_estimate: int, post_renovation_value: int, recommended_buy_price: int, recommended_sell_price: int, line_count: int}
*/
public function rollupAppraisal(Appraisal $appraisal, array $settings = []): array
{
$settings = $this->resolveSettings($settings);
$appraisal->loadMissing('lines');
$asIsTotal = 0;
$renovationTotal = 0;
$postRenovationTotal = 0;
$counted = 0;
foreach ($appraisal->lines as $line) {
if (! $line->keep_in_line) {
continue;
}
$values = $this->calculateLineValue($line, $settings);
$line->fill([
'age_years' => $values['age_years'],
'line_as_is_value' => $values['line_as_is_value'],
'renovation_cost' => $values['renovation_cost'],
'line_post_renovation_value' => $values['line_post_renovation_value'],
]);
$line->save();
$asIsTotal += $values['line_as_is_value'];
$renovationTotal += $values['renovation_cost'];
$postRenovationTotal += $values['line_post_renovation_value'];
$counted++;
}
$buyMargin = (float) ($settings['buy_margin_percent'] ?? 15);
$sellMargin = (float) ($settings['sell_margin_percent'] ?? 20);
return [
'as_is_value' => $asIsTotal,
'renovation_cost_estimate' => $renovationTotal,
'post_renovation_value' => $postRenovationTotal,
'recommended_buy_price' => (int) round($asIsTotal * (1 - ($buyMargin / 100))),
'recommended_sell_price' => (int) round($postRenovationTotal * (1 + ($sellMargin / 100))),
'line_count' => $counted,
];
}
protected function resolveAgeYears(AppraisalLine $line): ?float
{
if ($line->age_years !== null) {
return (float) $line->age_years;
}
if ($line->year_manufactured) {
return max(0, (float) (Carbon::now()->year - (int) $line->year_manufactured));
}
return null;
}
protected function depreciationRateForLine(AppraisalLine $line, array $settings): float
{
return in_array($line->line_kind, ['machine', 'subsystem'], true)
? (float) $settings['depreciation_rate_machine']
: (float) $settings['depreciation_rate_line'];
}
protected function estimateRenovationCost(AppraisalLine $line, int $replacementCost, array $settings): int
{
$need = $line->renovation_need ?: 'none';
$multipliers = $settings['renovation_multipliers'] ?? [];
$multiplier = (float) ($multipliers[$need] ?? 0);
$base = (int) round($replacementCost * $multiplier);
if ($need === 'replace') {
return max($base, $replacementCost);
}
$laborHours = match ($need) {
'minor' => 4,
'major' => 24,
default => 0,
};
$laborCost = (int) round($laborHours * (float) ($settings['labor_rate_per_hour'] ?? 0));
return $base + $laborCost;
}
}