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; } }