id)->findOrFail($data['listing_id']); } return MarketComp::create([ 'business_id' => $business->id, 'listing_id' => $data['listing_id'] ?? null, 'source' => $data['source'] ?? 'manual', 'ie_competitive_benchmark_id' => $data['ie_competitive_benchmark_id'] ?? null, 'comp_title' => $data['comp_title'], 'manufacturer' => $data['manufacturer'] ?? null, 'model' => $data['model'] ?? null, 'year_manufactured' => $data['year_manufactured'] ?? null, 'observed_price' => (int) ($data['observed_price'] ?? 0), 'currency' => $data['currency'] ?? 'IRR', 'observed_date' => ! empty($data['observed_date']) ? $this->aexUtil->parseInputDate($data['observed_date']) : now()->toDateString(), 'notes' => $data['notes'] ?? null, ]); } public function suggestPriceFromComps(Business $business, ?Listing $listing = null, array $filters = []): array { if (! Schema::hasTable('aex_market_comps')) { return [ 'suggested_price' => 0, 'comp_count' => 0, 'method' => 'none', 'comps' => [], ]; } $query = MarketComp::where('business_id', $business->id); if ($listing) { $query->where(function ($q) use ($listing) { $q->where('listing_id', $listing->id) ->orWhereNull('listing_id'); }); if ($listing->year_manufactured) { $query->where(function ($q) use ($listing) { $q->whereNull('year_manufactured') ->orWhereBetween('year_manufactured', [ $listing->year_manufactured - 3, $listing->year_manufactured + 3, ]); }); } } if (! empty($filters['manufacturer'])) { $query->where('manufacturer', 'like', '%'.$filters['manufacturer'].'%'); } if (! empty($filters['model'])) { $query->where('model', 'like', '%'.$filters['model'].'%'); } $comps = $query->orderByDesc('observed_date')->limit(50)->get(); if ($comps->isEmpty()) { return [ 'suggested_price' => $listing?->asking_price ?? 0, 'comp_count' => 0, 'method' => 'listing_asking_fallback', 'comps' => [], ]; } $prices = $comps->pluck('observed_price')->filter(fn ($p) => $p > 0)->values(); $median = $this->median($prices); $average = (int) round($prices->avg() ?: 0); $settings = $this->aexUtil->getAexSettings($business->id); $method = $settings['comp_pricing_method'] ?? 'median'; $suggested = $method === 'average' ? $average : $median; return [ 'suggested_price' => $suggested, 'median_price' => $median, 'average_price' => $average, 'comp_count' => $comps->count(), 'method' => $method, 'comps' => $comps, ]; } public function importFromIeBenchmarks(Business $business, ?int $listingId = null): int { if (! Schema::hasTable('aex_market_comps') || ! Schema::hasTable('ie_competitive_benchmarks')) { throw new \RuntimeException('جداول مقایسه بازار یا IE موجود نیست.'); } if (! class_exists(\Modules\IndustrialEngineering\Models\CompetitiveBenchmark::class)) { throw new \RuntimeException('ماژول مهندسی صنعتی نصب نیست.'); } $listing = null; if ($listingId) { $listing = Listing::where('business_id', $business->id)->findOrFail($listingId); } $query = \Modules\IndustrialEngineering\Models\CompetitiveBenchmark::where('business_id', $business->id) ->with(['competitiveProduct', 'lineRevision.lineTemplate']); if ($listing?->ie_line_revision_id) { $query->where('line_revision_id', $listing->ie_line_revision_id); } $benchmarks = $query->orderByDesc('id')->limit(200)->get(); $imported = 0; foreach ($benchmarks as $benchmark) { $product = $benchmark->competitiveProduct; $title = $product?->name ?? $benchmark->lineRevision?->lineTemplate?->name ?? ('Benchmark #'.$benchmark->id); $price = (int) round((float) ($benchmark->competitor_value ?: $benchmark->our_value ?: 0)); if ($price <= 0) { continue; } $exists = MarketComp::where('business_id', $business->id) ->where('ie_competitive_benchmark_id', $benchmark->id) ->exists(); if ($exists) { continue; } $this->addComp($business, [ 'listing_id' => $listing?->id, 'source' => 'ie_benchmark', 'ie_competitive_benchmark_id' => $benchmark->id, 'comp_title' => $title, 'manufacturer' => $product?->manufacturer ?? null, 'model' => $product?->model ?? null, 'observed_price' => $price, 'notes' => $benchmark->metric_name ?? null, ]); $imported++; } return $imported; } public function exportCsv(Business $business, array $filters = []): string { if (! Schema::hasTable('aex_market_comps')) { throw new \RuntimeException('جدول مقایسه‌های بازار موجود نیست.'); } $query = MarketComp::where('business_id', $business->id)->with('listing'); if (! empty($filters['listing_id'])) { $query->where('listing_id', (int) $filters['listing_id']); } if (! empty($filters['source'])) { $query->where('source', $filters['source']); } $comps = $query->orderByDesc('observed_date')->get(); $handle = fopen('php://temp', 'r+'); fputcsv($handle, [ 'id', 'listing_code', 'comp_title', 'manufacturer', 'model', 'year_manufactured', 'observed_price', 'currency', 'observed_date', 'source', 'notes', ]); foreach ($comps as $comp) { fputcsv($handle, [ $comp->id, $comp->listing?->listing_code, $comp->comp_title, $comp->manufacturer, $comp->model, $comp->year_manufactured, $comp->observed_price, $comp->currency, $comp->observed_date?->format('Y-m-d'), $comp->source, $comp->notes, ]); } rewind($handle); $csv = stream_get_contents($handle); fclose($handle); return $csv ?: ''; } protected function median(Collection $values): int { $sorted = $values->sort()->values(); $count = $sorted->count(); if ($count === 0) { return 0; } $middle = (int) floor($count / 2); if ($count % 2) { return (int) $sorted[$middle]; } return (int) round(($sorted[$middle - 1] + $sorted[$middle]) / 2); } }