ultimatepos/Modules/AssetExchange/Services/AppraisalService.php

311 lines
12 KiB
PHP

<?php
namespace Modules\AssetExchange\Services;
use App\Business;
use App\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Modules\AssetExchange\Models\Appraisal;
use Modules\AssetExchange\Models\AppraisalLine;
use Modules\AssetExchange\Models\Listing;
use Modules\AssetExchange\Utils\AssetExchangeUtil;
class AppraisalService
{
public function __construct(
protected AssetExchangeUtil $aexUtil,
protected ValuationEngine $valuationEngine,
protected AssetExchangeIeBridgeService $ieBridge,
protected AssetExchangeCmmsBridgeService $cmmsBridge,
protected PipelineService $pipelineService,
protected ListingService $listingService
) {
}
public function startAppraisal(Business $business, Listing $listing, ?User $user = null, array $options = []): Appraisal
{
if (! in_array($listing->status, ['under_review', 'submitted', 'appraised', 'in_deal'], true)) {
throw new \RuntimeException('وضعیت عرضه برای شروع کارشناسی مناسب نیست.');
}
return DB::transaction(function () use ($business, $listing, $user, $options) {
$settings = $this->aexUtil->getAexSettings($business->id);
$appraisal = Appraisal::create([
'business_id' => $business->id,
'listing_id' => $listing->id,
'appraisal_number' => $this->aexUtil->generateAppraisalNumber($business->id),
'status' => 'draft',
'methodology' => $options['methodology'] ?? ($settings['valuation_method'] ?? 'hybrid'),
'appraiser_user_id' => $user?->id,
'field_visit_date' => $options['field_visit_date'] ?? null,
'appraisal_fee' => (int) ($options['appraisal_fee'] ?? ($settings['appraisal_fee_default'] ?? 0)),
]);
$this->pipelineService->advanceStage($business, $listing, 'appraisal_in_progress', null, Appraisal::class, $appraisal->id, $user);
$this->pipelineService->logEvent(
$business,
Appraisal::class,
$appraisal->id,
'appraisal_started',
'کارشناسی آغاز شد',
$user
);
if (! empty($options['auto_import'])) {
$this->autoImportSources($appraisal, $listing);
}
return $appraisal->fresh(['lines']);
});
}
public function importFromIeBom(Appraisal $appraisal, ?int $bomId = null, bool $replaceExisting = false): int
{
$count = $this->ieBridge->importBomToAppraisalLines($appraisal, $bomId, $replaceExisting);
$this->recalculate($appraisal);
return $count;
}
public function importFromCmmsEquipmentParts(Appraisal $appraisal, ?int $equipmentId = null, bool $replaceExisting = false): int
{
$count = $this->cmmsBridge->importEquipmentPartsToAppraisal($appraisal, $equipmentId, $replaceExisting);
$this->recalculate($appraisal);
return $count;
}
public function importFromInstalledAsset(Appraisal $appraisal, ?int $installedAssetId = null, bool $replaceExisting = false): int
{
if (! class_exists(\Modules\IndustrialEngineering\Models\CustomerInstalledAsset::class)) {
throw new \RuntimeException('ماژول مهندسی صنعتی نصب نیست.');
}
if (! Schema::hasTable('ie_customer_installed_assets')) {
throw new \RuntimeException('جدول دارایی‌های نصب‌شده موجود نیست.');
}
$appraisal->loadMissing('listing');
$assetId = $installedAssetId ?: $appraisal->listing?->ie_installed_asset_id;
if (! $assetId) {
throw new \RuntimeException('شناسه دارایی نصب‌شده مشخص نشده است.');
}
$asset = \Modules\IndustrialEngineering\Models\CustomerInstalledAsset::where('business_id', $appraisal->business_id)
->with(['serializedUnit'])
->find($assetId);
if (! $asset) {
throw new \RuntimeException('دارایی نصب‌شده یافت نشد.');
}
if ($replaceExisting) {
AppraisalLine::where('appraisal_id', $appraisal->id)->delete();
}
$count = 0;
$sort = 0;
if (class_exists(\Modules\IndustrialEngineering\Models\SerializedUnitComponent::class)
&& Schema::hasTable('ie_serialized_unit_components')
&& $asset->serialized_unit_id) {
$components = \Modules\IndustrialEngineering\Models\SerializedUnitComponent::where('business_id', $appraisal->business_id)
->where('serialized_unit_id', $asset->serialized_unit_id)
->orderBy('id')
->get();
$componentMap = [];
foreach ($components as $component) {
$parentLineId = $component->parent_component_id && isset($componentMap[$component->parent_component_id])
? $componentMap[$component->parent_component_id]
: null;
$line = AppraisalLine::create([
'business_id' => $appraisal->business_id,
'appraisal_id' => $appraisal->id,
'parent_line_id' => $parentLineId,
'line_kind' => 'part',
'name' => $component->name ?? ('قطعه #'.$component->id),
'manufacturer' => $component->manufacturer,
'model' => $component->model,
'serial_number' => $component->serial_number,
'ie_item_master_id' => $component->item_master_id,
'ie_bom_line_id' => $component->bom_line_id,
'year_manufactured' => $component->manufactured_at?->year,
'health_percentage' => 100,
'renovation_need' => 'none',
'keep_in_line' => true,
'sort_order' => $sort++,
]);
$componentMap[$component->id] = $line->id;
$count++;
}
}
if ($count === 0) {
AppraisalLine::create([
'business_id' => $appraisal->business_id,
'appraisal_id' => $appraisal->id,
'line_kind' => 'machine',
'name' => $asset->name,
'serial_number' => $asset->serial_number,
'health_percentage' => 100,
'renovation_need' => 'none',
'keep_in_line' => true,
'sort_order' => 0,
]);
$count = 1;
}
if ($appraisal->listing && ! $appraisal->listing->ie_installed_asset_id) {
$appraisal->listing->update(['ie_installed_asset_id' => $asset->id]);
}
$this->recalculate($appraisal);
return $count;
}
public function recalculate(Appraisal $appraisal): Appraisal
{
$settings = $this->aexUtil->getAexSettings($appraisal->business_id);
$rollup = $this->valuationEngine->rollupAppraisal($appraisal, $settings);
$appraisal->update([
'as_is_value' => $rollup['as_is_value'],
'renovation_cost_estimate' => $rollup['renovation_cost_estimate'],
'post_renovation_value' => $rollup['post_renovation_value'],
'recommended_buy_price' => $rollup['recommended_buy_price'],
'recommended_sell_price' => $rollup['recommended_sell_price'],
'status' => $appraisal->status === 'draft' ? 'priced' : $appraisal->status,
]);
return $appraisal->fresh(['lines']);
}
public function approve(Business $business, Appraisal $appraisal, ?User $user = null, ?string $summary = null): Appraisal
{
if (! in_array($appraisal->status, ['priced', 'teardown', 'field_visit', 'draft'], true)) {
throw new \RuntimeException('کارشناسی در وضعیت فعلی قابل تأیید نیست.');
}
return DB::transaction(function () use ($business, $appraisal, $user, $summary) {
$settings = $this->aexUtil->getAexSettings($business->id);
$validDays = (int) ($settings['appraisal_valid_days'] ?? 90);
$appraisal->update([
'status' => 'approved',
'approved_by' => $user?->id,
'approved_at' => now(),
'valid_until' => Carbon::now()->addDays($validDays)->toDateString(),
'report_summary' => $summary ?: $appraisal->report_summary,
]);
$appraisal->loadMissing('listing');
if ($appraisal->listing) {
$this->listingService->markAppraised($business, $appraisal->listing, $user);
}
$this->pipelineService->logEvent(
$business,
Appraisal::class,
$appraisal->id,
'appraisal_approved',
'کارشناسی تأیید شد',
$user
);
$appraisal = $appraisal->fresh(['lines', 'listing']);
try {
$this->storeReportPdf($appraisal);
} catch (\Throwable) {
}
return $appraisal;
});
}
public function generateReportHtml(Appraisal $appraisal): string
{
$appraisal->loadMissing(['lines', 'listing.seller', 'appraiser', 'approvedByUser']);
$settings = $this->aexUtil->getAexSettings($appraisal->business_id);
$business = Business::find($appraisal->business_id);
$lines = $appraisal->lines->where('keep_in_line', true);
return view('assetexchange::appraisals.report', compact('appraisal', 'lines', 'settings', 'business'))
->with('aexUtil', $this->aexUtil)
->render();
}
public function storeReportPdf(Appraisal $appraisal): string
{
$businessId = (int) $appraisal->business_id;
$pdfBinary = $this->renderReportPdf($appraisal);
$fileName = 'appraisal-'.$appraisal->appraisal_number.'.pdf';
$relativePath = 'app/assetexchange/reports/'.$businessId.'/'.$fileName;
$storagePath = storage_path($relativePath);
if (! is_dir(dirname($storagePath))) {
mkdir(dirname($storagePath), 0755, true);
}
file_put_contents($storagePath, $pdfBinary);
$appraisal->update(['report_pdf_path' => $relativePath]);
return $relativePath;
}
public function renderReportPdf(Appraisal $appraisal): string
{
$html = $this->generateReportHtml($appraisal);
$tempDir = storage_path('app/mpdf-temp');
if (! is_dir($tempDir)) {
mkdir($tempDir, 0755, true);
}
$mpdf = new \Mpdf\Mpdf([
'tempDir' => $tempDir,
'mode' => 'utf-8',
'format' => 'A4',
]);
$mpdf->WriteHTML($html);
return $mpdf->Output('', 'S');
}
protected function autoImportSources(Appraisal $appraisal, Listing $listing): void
{
if ($listing->ie_bom_id || $this->ieBridge->resolveBomForListing($listing)) {
try {
$bom = $this->ieBridge->resolveBomForListing($listing);
$this->importFromIeBom($appraisal, $bom?->id, false);
return;
} catch (\Throwable) {
}
}
if ($listing->maintenance_equipment_id) {
try {
$this->importFromCmmsEquipmentParts($appraisal, $listing->maintenance_equipment_id, false);
return;
} catch (\Throwable) {
}
}
if ($listing->ie_installed_asset_id) {
try {
$this->importFromInstalledAsset($appraisal, $listing->ie_installed_asset_id, false);
} catch (\Throwable) {
}
}
}
}