162 lines
5.6 KiB
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;
|
|
}
|
|
}
|