ultimatepos/Modules/AssetExchange/Services/MarketIntelService.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);
}
}