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) { } } } }