249 lines
8.2 KiB
PHP
249 lines
8.2 KiB
PHP
<?php
|
|
|
|
namespace Modules\AssetExchange\Services;
|
|
|
|
use App\Business;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Modules\AssetExchange\Models\Listing;
|
|
use Modules\AssetExchange\Models\MarketComp;
|
|
use Modules\AssetExchange\Utils\AssetExchangeUtil;
|
|
|
|
class MarketIntelService
|
|
{
|
|
public function __construct(
|
|
protected AssetExchangeUtil $aexUtil
|
|
) {
|
|
}
|
|
|
|
public function addComp(Business $business, array $data): MarketComp
|
|
{
|
|
if (! Schema::hasTable('aex_market_comps')) {
|
|
throw new \RuntimeException('جدول مقایسههای بازار موجود نیست.');
|
|
}
|
|
|
|
if (! empty($data['listing_id'])) {
|
|
Listing::where('business_id', $business->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);
|
|
}
|
|
}
|