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