311 lines
12 KiB
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) {
|
|
}
|
|
}
|
|
}
|
|
}
|