316 lines
11 KiB
PHP
316 lines
11 KiB
PHP
<?php
|
|
|
|
namespace Modules\AssetExchange\Services;
|
|
|
|
use App\Business;
|
|
use App\User;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Modules\AssetExchange\Models\CatalogMatch;
|
|
use Modules\AssetExchange\Models\Listing;
|
|
|
|
class CatalogMatchService
|
|
{
|
|
public const CATALOG_TYPES = [
|
|
'ie_line_template',
|
|
'ie_item_master',
|
|
'ie_installed_asset',
|
|
'maintenance_equipment',
|
|
'ie_product_platform',
|
|
];
|
|
|
|
public function discoverMatches(Business $business, Listing $listing, bool $replaceSuggested = false): Collection
|
|
{
|
|
if (! Schema::hasTable('aex_catalog_matches')) {
|
|
return collect();
|
|
}
|
|
|
|
$candidates = collect();
|
|
|
|
$candidates = $candidates->merge($this->searchIeLineTemplates($business, $listing));
|
|
$candidates = $candidates->merge($this->searchIeItemMasters($business, $listing));
|
|
$candidates = $candidates->merge($this->searchInstalledAssets($business, $listing));
|
|
$candidates = $candidates->merge($this->searchCmmsEquipment($business, $listing));
|
|
|
|
return DB::transaction(function () use ($business, $listing, $replaceSuggested, $candidates) {
|
|
if ($replaceSuggested) {
|
|
CatalogMatch::where('listing_id', $listing->id)
|
|
->where('status', 'suggested')
|
|
->delete();
|
|
}
|
|
|
|
$saved = collect();
|
|
foreach ($candidates->sortByDesc('score')->take(15) as $row) {
|
|
$match = CatalogMatch::updateOrCreate(
|
|
[
|
|
'listing_id' => $listing->id,
|
|
'catalog_type' => $row['catalog_type'],
|
|
'catalog_id' => $row['catalog_id'],
|
|
],
|
|
[
|
|
'business_id' => $business->id,
|
|
'catalog_label' => $row['label'],
|
|
'match_score' => $row['score'],
|
|
'match_reason' => $row['reason'],
|
|
'status' => 'suggested',
|
|
]
|
|
);
|
|
$saved->push($match);
|
|
}
|
|
|
|
return $saved;
|
|
});
|
|
}
|
|
|
|
public function confirmMatch(Business $business, CatalogMatch $match, ?User $user = null): Listing
|
|
{
|
|
$match->loadMissing('listing');
|
|
$listing = $match->listing;
|
|
|
|
if (! $listing || $listing->business_id !== $business->id) {
|
|
throw new \RuntimeException('عرضه نامعتبر است.');
|
|
}
|
|
|
|
return DB::transaction(function () use ($business, $match, $listing, $user) {
|
|
$match->update([
|
|
'status' => 'confirmed',
|
|
'confirmed_by' => $user?->id,
|
|
'confirmed_at' => now(),
|
|
]);
|
|
|
|
$updates = match ($match->catalog_type) {
|
|
'ie_line_template' => ['ie_line_template_id' => $match->catalog_id],
|
|
'ie_item_master' => [],
|
|
'ie_installed_asset' => ['ie_installed_asset_id' => $match->catalog_id],
|
|
'maintenance_equipment' => ['maintenance_equipment_id' => $match->catalog_id],
|
|
default => [],
|
|
};
|
|
|
|
if ($match->catalog_type === 'ie_line_template' && Schema::hasTable('ie_line_revisions')) {
|
|
$revision = DB::table('ie_line_revisions')
|
|
->where('business_id', $business->id)
|
|
->where('line_template_id', $match->catalog_id)
|
|
->orderByDesc('id')
|
|
->first();
|
|
if ($revision) {
|
|
$updates['ie_line_revision_id'] = $revision->id;
|
|
if (Schema::hasTable('ie_boms')) {
|
|
$bom = DB::table('ie_boms')
|
|
->where('business_id', $business->id)
|
|
->where('line_revision_id', $revision->id)
|
|
->orderByDesc('id')
|
|
->first();
|
|
if ($bom) {
|
|
$updates['ie_bom_id'] = $bom->id;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (! empty($updates)) {
|
|
$listing->update($updates);
|
|
}
|
|
|
|
CatalogMatch::where('listing_id', $listing->id)
|
|
->where('id', '!=', $match->id)
|
|
->where('catalog_type', $match->catalog_type)
|
|
->where('status', 'suggested')
|
|
->update(['status' => 'rejected']);
|
|
|
|
return $listing->fresh(['catalogMatches']);
|
|
});
|
|
}
|
|
|
|
public function searchCatalog(Business $business, string $term, ?string $type = null): array
|
|
{
|
|
$listing = new Listing([
|
|
'business_id' => $business->id,
|
|
'title' => $term,
|
|
'manufacturer' => $term,
|
|
'model' => $term,
|
|
]);
|
|
|
|
$all = collect();
|
|
if (! $type || $type === 'ie_line_template') {
|
|
$all = $all->merge($this->searchIeLineTemplates($business, $listing, 30));
|
|
}
|
|
if (! $type || $type === 'ie_item_master') {
|
|
$all = $all->merge($this->searchIeItemMasters($business, $listing, 30));
|
|
}
|
|
if (! $type || $type === 'maintenance_equipment') {
|
|
$all = $all->merge($this->searchCmmsEquipment($business, $listing, 30));
|
|
}
|
|
if (! $type || $type === 'ie_installed_asset') {
|
|
$all = $all->merge($this->searchInstalledAssets($business, $listing, 30));
|
|
}
|
|
|
|
return $all->sortByDesc('score')->take(25)->values()->all();
|
|
}
|
|
|
|
protected function searchIeLineTemplates(Business $business, Listing $listing, int $limit = 10): Collection
|
|
{
|
|
if (! Schema::hasTable('ie_line_templates')) {
|
|
return collect();
|
|
}
|
|
|
|
$terms = $this->searchTerms($listing);
|
|
$query = DB::table('ie_line_templates')->where('business_id', $business->id);
|
|
|
|
$query->where(function ($q) use ($terms) {
|
|
foreach ($terms as $term) {
|
|
$q->orWhere('name', 'like', '%'.$term.'%')
|
|
->orWhere('code', 'like', '%'.$term.'%');
|
|
}
|
|
});
|
|
|
|
return $query->limit($limit)->get()->map(function ($row) use ($terms) {
|
|
return [
|
|
'catalog_type' => 'ie_line_template',
|
|
'catalog_id' => $row->id,
|
|
'label' => $row->name.' ('.$row->code.')',
|
|
'score' => $this->scoreMatch($row->name.' '.$row->code, $terms),
|
|
'reason' => 'تطبیق نام/کد خط تولید در IE',
|
|
];
|
|
});
|
|
}
|
|
|
|
protected function searchIeItemMasters(Business $business, Listing $listing, int $limit = 10): Collection
|
|
{
|
|
if (! Schema::hasTable('ie_item_masters')) {
|
|
return collect();
|
|
}
|
|
|
|
$terms = $this->searchTerms($listing);
|
|
$query = DB::table('ie_item_masters')->where('business_id', $business->id);
|
|
|
|
$query->where(function ($q) use ($terms, $listing) {
|
|
foreach ($terms as $term) {
|
|
$q->orWhere('name', 'like', '%'.$term.'%')
|
|
->orWhere('item_code', 'like', '%'.$term.'%');
|
|
}
|
|
if ($listing->manufacturer) {
|
|
$q->orWhere('manufacturer', 'like', '%'.$listing->manufacturer.'%');
|
|
}
|
|
});
|
|
|
|
return $query->limit($limit)->get()->map(function ($row) use ($terms) {
|
|
$label = $row->name;
|
|
if (! empty($row->item_code)) {
|
|
$label .= ' ['.$row->item_code.']';
|
|
}
|
|
|
|
return [
|
|
'catalog_type' => 'ie_item_master',
|
|
'catalog_id' => $row->id,
|
|
'label' => $label,
|
|
'score' => $this->scoreMatch($row->name.' '.($row->item_code ?? ''), $terms),
|
|
'reason' => 'تطبیق آیتم/قطعه/ماشین در بانک IE',
|
|
];
|
|
});
|
|
}
|
|
|
|
protected function searchInstalledAssets(Business $business, Listing $listing, int $limit = 10): Collection
|
|
{
|
|
if (! Schema::hasTable('ie_customer_installed_assets')) {
|
|
return collect();
|
|
}
|
|
|
|
$terms = $this->searchTerms($listing);
|
|
$query = DB::table('ie_customer_installed_assets')->where('business_id', $business->id);
|
|
|
|
$query->where(function ($q) use ($terms) {
|
|
foreach ($terms as $term) {
|
|
$q->orWhere('asset_tag', 'like', '%'.$term.'%')
|
|
->orWhere('site_name', 'like', '%'.$term.'%');
|
|
}
|
|
});
|
|
|
|
return $query->limit($limit)->get()->map(function ($row) use ($terms) {
|
|
return [
|
|
'catalog_type' => 'ie_installed_asset',
|
|
'catalog_id' => $row->id,
|
|
'label' => ($row->asset_tag ?? 'Asset').' — '.($row->site_name ?? ''),
|
|
'score' => $this->scoreMatch(($row->asset_tag ?? '').' '.($row->site_name ?? ''), $terms),
|
|
'reason' => 'تطبیق دارایی نصبشده مشتری در IE',
|
|
];
|
|
});
|
|
}
|
|
|
|
protected function searchCmmsEquipment(Business $business, Listing $listing, int $limit = 10): Collection
|
|
{
|
|
if (! Schema::hasTable('maintenance_equipment')) {
|
|
return collect();
|
|
}
|
|
|
|
$terms = $this->searchTerms($listing);
|
|
$query = DB::table('maintenance_equipment')->where('business_id', $business->id);
|
|
|
|
$query->where(function ($q) use ($terms, $listing) {
|
|
foreach ($terms as $term) {
|
|
$q->orWhere('name', 'like', '%'.$term.'%')
|
|
->orWhere('code', 'like', '%'.$term.'%')
|
|
->orWhere('model', 'like', '%'.$term.'%')
|
|
->orWhere('serial_number', 'like', '%'.$term.'%');
|
|
}
|
|
if ($listing->serial_number) {
|
|
$q->orWhere('serial_number', $listing->serial_number);
|
|
}
|
|
});
|
|
|
|
return $query->limit($limit)->get()->map(function ($row) use ($terms, $listing) {
|
|
$score = $this->scoreMatch($row->name.' '.($row->model ?? '').' '.($row->serial_number ?? ''), $terms);
|
|
if ($listing->serial_number && $row->serial_number === $listing->serial_number) {
|
|
$score = 100;
|
|
}
|
|
|
|
return [
|
|
'catalog_type' => 'maintenance_equipment',
|
|
'catalog_id' => $row->id,
|
|
'label' => $row->name.' / '.($row->model ?? '—'),
|
|
'score' => $score,
|
|
'reason' => 'تطبیق تجهیز CMMS',
|
|
];
|
|
});
|
|
}
|
|
|
|
protected function searchTerms(Listing $listing): array
|
|
{
|
|
$parts = array_filter([
|
|
$listing->title,
|
|
$listing->manufacturer,
|
|
$listing->model,
|
|
$listing->serial_number,
|
|
]);
|
|
|
|
$terms = [];
|
|
foreach ($parts as $part) {
|
|
foreach (preg_split('/\s+/', (string) $part) as $word) {
|
|
$word = trim($word);
|
|
if (strlen($word) >= 2) {
|
|
$terms[] = $word;
|
|
}
|
|
}
|
|
}
|
|
|
|
return array_values(array_unique($terms));
|
|
}
|
|
|
|
protected function scoreMatch(string $haystack, array $terms): float
|
|
{
|
|
if (empty($terms)) {
|
|
return 0;
|
|
}
|
|
|
|
$haystack = mb_strtolower($haystack);
|
|
$hits = 0;
|
|
foreach ($terms as $term) {
|
|
if (str_contains($haystack, mb_strtolower($term))) {
|
|
$hits++;
|
|
}
|
|
}
|
|
|
|
return round(min(100, ($hits / count($terms)) * 100), 2);
|
|
}
|
|
}
|