30, 'headers' => array( 'x-rapidapi-host' => env('BULUT_RAPIDAPI_HOST', self::API_HOST), 'x-rapidapi-key' => env('BULUT_RAPIDAPI_KEY'), 'Content-Type' => 'application/json', ), )); } // ───────────────────────────────────────────────────────────────────────────── // ADIM 1: Otel adına göre ara → hotel_id bul // GET /bulut/search?name=Grand+Yavuz+Hotel // ───────────────────────────────────────────────────────────────────────────── public function search(Request $request) { $name = $request->query('name', ''); if (!$name) { return response()->json(array('status' => false, 'message' => 'name parametresi gerekli'), 422); } try { $response = $this->client()->get( self::API_BASE . '/api/v1/hotels/searchDestination', array('query' => array('query' => $name)) ); $data = json_decode($response->getBody()->getContents(), true); return response()->json(array('status' => true, 'data' => $data)); } catch (Exception $e) { Log::error('Bulut.search error: ' . $e->getMessage()); return response()->json(array('status' => false, 'message' => $e->getMessage()), 500); } } // ───────────────────────────────────────────────────────────────────────────── // ADIM 2: Otel detayları // GET /bulut/hotel-data?hotel_id=89675&checkin=2026-06-01&checkout=2026-06-02 // ───────────────────────────────────────────────────────────────────────────── public function hotelData(Request $request) { $hotelId = $request->query('hotel_id'); $checkin = $request->query('checkin', date('Y-m-d', strtotime('+30 days'))); $checkout = $request->query('checkout', date('Y-m-d', strtotime('+31 days'))); if (!$hotelId) { return response()->json(array('status' => false, 'message' => 'hotel_id gerekli'), 422); } try { $response = $this->client()->get( self::API_BASE . '/api/v1/hotels/getHotelDetails', array('query' => array( 'hotel_id' => $hotelId, 'arrival_date' => $checkin, 'departure_date' => $checkout, 'adults' => 1, 'room_qty' => 1, 'languagecode' => 'en-us', 'currency_code' => 'USD', 'units' => 'metric', 'temperature_unit' => 'c', )) ); $data = json_decode($response->getBody()->getContents(), true); return response()->json(array('status' => true, 'data' => $data)); } catch (Exception $e) { Log::error('Bulut.hotelData error: ' . $e->getMessage()); return response()->json(array('status' => false, 'message' => $e->getMessage()), 500); } } // ───────────────────────────────────────────────────────────────────────────── // ADIM 3: Otel fotoğraflarını indir → property_photo tablosuna kaydet // GET /bulut/hotel-photos?hotel_id=89675&property_id=1 // ───────────────────────────────────────────────────────────────────────────── public function hotelPhotos(Request $request) { $hotelId = $request->query('hotel_id'); $propertyId = $request->query('property_id', 1); $limit = (int) $request->query('limit', 20); if (!$hotelId) { return response()->json(array('status' => false, 'message' => 'hotel_id gerekli'), 422); } try { $response = $this->client()->get( self::API_BASE . '/api/v1/hotels/getHotelPhotos', array('query' => array( 'hotel_id' => $hotelId, 'page_number' => 1, )) ); $data = json_decode($response->getBody()->getContents(), true); $savedPhotos = $this->processAndSavePhotos($data, $hotelId, $propertyId, $limit); return response()->json(array( 'status' => true, 'hotel_id' => $hotelId, 'property_id' => $propertyId, 'photos_saved' => $savedPhotos, )); } catch (Exception $e) { Log::error('Bulut.hotelPhotos error: ' . $e->getMessage()); return response()->json(array('status' => false, 'message' => $e->getMessage()), 500); } } // ───────────────────────────────────────────────────────────────────────────── // TAM PIPELINE: otel adı gönder → ara → detay + fotoğrafları kaydet // GET /bulut/test-api?name=Grand+Yavuz+Hotel&property_id=1&limit=5 // ───────────────────────────────────────────────────────────────────────────── public function testApi(Request $request) { $name = $request->query('name', ''); $propertyId = $request->query('property_id', 1); $checkin = $request->query('checkin', date('Y-m-d', strtotime('+30 days'))); $checkout = $request->query('checkout', date('Y-m-d', strtotime('+31 days'))); $limit = (int) $request->query('limit', 5); if (!$name) { return response()->json(array('status' => false, 'message' => 'name parametresi gerekli'), 422); } try { // ── 1. Ara ─────────────────────────────────────────────────────────── $searchResponse = $this->client()->get( self::API_BASE . '/api/v1/hotels/searchDestination', array('query' => array('query' => $name)) ); $searchData = json_decode($searchResponse->getBody()->getContents(), true); if (empty($searchData['data'])) { return response()->json(array( 'status' => false, 'message' => 'Otel bulunamadı', 'raw' => $searchData ), 404); } // Önce dest_type=hotel olanı bul, yoksa ilk sonucu al $hotelResult = null; foreach ($searchData['data'] as $item) { if (isset($item['dest_type']) && $item['dest_type'] === 'hotel') { $hotelResult = $item; break; } } if (!$hotelResult) { $hotelResult = $searchData['data'][0]; } $hotelId = isset($hotelResult['dest_id']) ? $hotelResult['dest_id'] : null; $hotelName = isset($hotelResult['label']) ? $hotelResult['label'] : $name; if (!$hotelId) { return response()->json(array( 'status' => false, 'message' => 'hotel_id bulunamadı', 'search_result' => $hotelResult, ), 404); } // ── 2. Otel detayı ─────────────────────────────────────────────────── $detailResponse = $this->client()->get( self::API_BASE . '/api/v1/hotels/getHotelDetails', array('query' => array( 'hotel_id' => $hotelId, 'arrival_date' => $checkin, 'departure_date' => $checkout, 'adults' => 1, 'room_qty' => 1, 'languagecode' => 'en-us', 'currency_code' => 'USD', 'units' => 'metric', 'temperature_unit' => 'c', )) ); $detailData = json_decode($detailResponse->getBody()->getContents(), true); // ── 3. Fotoğrafları çek ve kaydet ──────────────────────────────────── $photoResponse = $this->client()->get( self::API_BASE . '/api/v1/hotels/getHotelPhotos', array('query' => array( 'hotel_id' => $hotelId, 'page_number' => 1, )) ); $photoData = json_decode($photoResponse->getBody()->getContents(), true); $savedPhotos = $this->processAndSavePhotos($photoData, $hotelId, $propertyId, $limit); // ── 4. Facility matching ────────────────────────────────────────────── $detail = isset($detailData['data']) ? $detailData['data'] : $detailData; $facilityResult = $this->matchAndLogFacilities($detail, $hotelId, $propertyId); return response()->json(array( 'status' => true, 'hotel_id' => $hotelId, 'hotel_name' => $hotelName, 'hotel_details' => $detail, 'photos_saved' => $savedPhotos, 'facility_matching' => $facilityResult, )); } catch (Exception $e) { Log::error('Bulut.testApi error: ' . $e->getMessage()); return response()->json(array('status' => false, 'message' => $e->getMessage()), 500); } } // ───────────────────────────────────────────────────────────────────────────── // Private: API'den gelen foto listesinden URL çıkar, indir, boyutlandır, DB'ye kaydet // ───────────────────────────────────────────────────────────────────────────── private function processAndSavePhotos(array $responseData, $hotelId, $propertyId, $limit = 20) { $photoUrls = $this->extractPhotoUrls($responseData); if (empty($photoUrls)) { return array( 'count' => 0, 'message' => 'API yanıtında fotoğraf URL bulunamadı', 'raw_keys' => array_keys($responseData), ); } $uploadRoot = rtrim(Config::get('app.fileSystemDriver'), '/'); $urlPath = '/property-photos/' . $propertyId . '/'; $filePath = $uploadRoot . $urlPath; if (!File::exists($filePath)) { File::makeDirectory($filePath, 0777, true); } // Bu property için zaten default foto var mı? $hasDefault = PropertyPhoto::where('property_id', $propertyId) ->where('status', 1) ->where('is_default', 1) ->exists(); $saved = array(); $isFirstPhoto = true; $total = min(count($photoUrls), $limit); for ($i = 0; $i < $total; $i++) { $photoUrl = $photoUrls[$i]; try { $fileName = time() . '_bk' . $hotelId . '_' . $i; $fileExt = 'jpg'; $tempPath = sys_get_temp_dir() . '/' . $fileName . '_orig.jpg'; // ── İndir ───────────────────────────────────────────────────────── $this->downloadRawImage($photoUrl, $tempPath); // ── Intervention Image ile işle ─────────────────────────────────── $image = Image::make($tempPath); $imageWidth = $image->width(); $imageHeight = $image->height(); $fileSize = (int) ceil(filesize($tempPath) / 1024); $quality = 80; // Orijinal (2000px üzeriyse küçült) if ($imageHeight > 2000 || $imageWidth > 2000) { if ($imageHeight > $imageWidth) { $r = $imageHeight / 2000; $image->fit((int)($imageWidth / $r), 2000); } else { $r = $imageWidth / 2000; $image->fit(2000, (int)($imageHeight / $r)); } } $image->encode($fileExt, $quality)->orientate()->save($filePath . $fileName . '.' . $fileExt); // _medium (max 1600x768) $mediumImage = Image::make($tempPath); $ratio = $imageHeight > 0 ? ($imageHeight / 768) : 1; $resizeWidth = (int)($imageWidth / max($ratio, 1)); if ($resizeWidth > 1599) { $mediumImage->fit(1600, 768); } else { $mediumImage->fit(max($resizeWidth, 1), 768); } $mediumImage->encode($fileExt, $quality)->orientate()->save($filePath . $fileName . '_medium.' . $fileExt); // _thumbnail (300x300) Image::make($tempPath) ->fit(300, 300) ->encode($fileExt, $quality) ->orientate() ->save($filePath . $fileName . '_thumbnail.' . $fileExt); // Temp dosyayı sil if (File::exists($tempPath)) { File::delete($tempPath); } $isCompatible = ($imageWidth >= 1150 && $imageHeight >= 600) ? 1 : 0; $isDefault = (!$hasDefault && $isFirstPhoto) ? 1 : 0; $isFirstPhoto = false; // ── DB'ye kaydet ────────────────────────────────────────────────── $photoRecord = PropertyPhoto::create(array( 'property_id' => $propertyId, 'property_photo_category_id' => 1, 'photo_path' => $urlPath, 'photo_name' => $fileName, 'file_size' => $fileSize, 'file_ext' => $fileExt, 'photo_resolution' => $imageWidth . 'x' . $imageHeight, 'is_default' => $isDefault, 'is_compatible_with_myweb_slider' => $isCompatible, 'photo_order' => 0, 'photo_rank' => 0, 'is_temp' => 1, 'status' => 1, 'created_by' => 1, 'updated_by' => 1, 'created_at' => time(), 'updated_at' => time(), )); $saved[] = array( 'db_id' => $photoRecord->id, 'file_name' => $fileName . '.' . $fileExt, 'resolution' => $imageWidth . 'x' . $imageHeight, 'source_url' => $photoUrl, ); } catch (Exception $e) { Log::error('Bulut.photo.save [' . $i . ']: ' . $e->getMessage() . ' | ' . $photoUrl); $saved[] = array('error' => $e->getMessage(), 'source_url' => $photoUrl); } } return array( 'saved_count' => count(array_filter($saved, function ($s) { return !isset($s['error']); })), 'total_in_api' => count($photoUrls), 'limit_applied' => $total, 'photos' => $saved, ); } // ───────────────────────────────────────────────────────────────────────────── // booking-com15 getHotelPhotos yanıtından URL listesi çıkar // ───────────────────────────────────────────────────────────────────────────── private function extractPhotoUrls(array $data) { $urls = array(); // Format 1: data[] dizisi – her elemanın url_original / url_max / url alanı if (isset($data['data']) && is_array($data['data']) && !empty($data['data'])) { foreach ($data['data'] as $item) { if (!is_array($item)) { continue; } if (!empty($item['url_original'])) { $urls[] = $item['url_original']; } elseif (!empty($item['url_max'])) { $urls[] = $item['url_max']; } elseif (!empty($item['url'])) { $urls[] = $item['url']; } elseif (!empty($item['photo']['url_original'])) { $urls[] = $item['photo']['url_original']; } } } // Format 2: data.photos[] if (empty($urls) && isset($data['data']['photos']) && is_array($data['data']['photos'])) { foreach ($data['data']['photos'] as $item) { if (!empty($item['url_original'])) { $urls[] = $item['url_original']; } elseif (!empty($item['url'])) { $urls[] = $item['url']; } } } // Format 3: photos[] direkt root'ta if (empty($urls) && isset($data['photos']) && is_array($data['photos'])) { foreach ($data['photos'] as $item) { if (!empty($item['url_original'])) { $urls[] = $item['url_original']; } elseif (!empty($item['url'])) { $urls[] = $item['url']; } } } return $urls; } // ───────────────────────────────────────────────────────────────────────────── // Görseli disk'e indir (GuzzleHttp sink kullan) // ───────────────────────────────────────────────────────────────────────────── private function downloadRawImage($url, $savePath) { $client = new Client(array( 'timeout' => 30, 'verify' => false, 'headers' => array('User-Agent' => 'Mozilla/5.0'), )); $response = $client->get($url, array('sink' => $savePath)); if ($response->getStatusCode() !== 200) { throw new Exception('Image download failed HTTP ' . $response->getStatusCode() . ' | ' . $url); } } // ───────────────────────────────────────────────────────────────────────────── // Public: GET /bulut/match-facilities?hotel_id=89675&property_id=1 // ───────────────────────────────────────────────────────────────────────────── public function matchFacilities(Request $request) { $hotelId = $request->query('hotel_id', ''); $propertyId = $request->query('property_id', 1); if (!$hotelId) { return response()->json(array('status' => false, 'message' => 'hotel_id gerekli'), 422); } try { $response = $this->client()->get( self::API_BASE . '/api/v1/hotels/getHotelFacilities', array('query' => array('hotel_id' => $hotelId)) ); $data = json_decode($response->getBody()->getContents(), true); $result = $this->matchAndLogFacilities( isset($data['data']) ? $data['data'] : array(), $hotelId, $propertyId ); return response()->json(array( 'status' => true, 'hotel_id' => $hotelId, 'result' => $result, )); } catch (Exception $e) { Log::error('Bulut.matchFacilities error: ' . $e->getMessage()); return response()->json(array('status' => false, 'message' => $e->getMessage()), 500); } } // ───────────────────────────────────────────────────────────────────────────── // Private: API yanıtındaki facilitylerle property_fact tablosunu eşleştir + logla // ───────────────────────────────────────────────────────────────────────────── private function matchAndLogFacilities(array $detail, $hotelId, $propertyId) { // ── 1. API'den facility isimlerini topla ────────────────────────────────── // getHotelFacilities yapısı: facilities[], accommodationHighlights[], highlights[] // getHotelDetails yapısı (eski): facilities_block.facilities[], top_ufi_benefits[], property_highlight_strip[] $apiNames = array(); $addUniq = function ($name) use (&$apiNames) { $name = trim($name); if ($name !== '' && !in_array($name, $apiNames)) { $apiNames[] = $name; } }; // getHotelFacilities → facilities[].instances[].title (96 adet zengin veri) if (isset($detail['facilities']) && is_array($detail['facilities'])) { foreach ($detail['facilities'] as $fac) { if (!empty($fac['instances']) && is_array($fac['instances'])) { foreach ($fac['instances'] as $inst) { if (!empty($inst['title'])) { $addUniq($inst['title']); } } } } } // getHotelFacilities → accommodationHighlights[].title if (isset($detail['accommodationHighlights']) && is_array($detail['accommodationHighlights'])) { foreach ($detail['accommodationHighlights'] as $h) { if (!empty($h['title'])) { $addUniq($h['title']); } } } // getHotelFacilities → highlights[].instances[].title if (isset($detail['highlights']) && is_array($detail['highlights'])) { foreach ($detail['highlights'] as $h) { if (!empty($h['instances']) && is_array($h['instances'])) { foreach ($h['instances'] as $inst) { if (!empty($inst['title'])) { $addUniq($inst['title']); } } } } } // getHotelDetails uyumluluğu — eski endpoint hâlâ çalışsın if (isset($detail['facilities_block']['facilities']) && is_array($detail['facilities_block']['facilities'])) { foreach ($detail['facilities_block']['facilities'] as $fac) { if (!empty($fac['name'])) { $addUniq($fac['name']); } } } if (isset($detail['top_ufi_benefits']) && is_array($detail['top_ufi_benefits'])) { foreach ($detail['top_ufi_benefits'] as $ben) { if (!empty($ben['translated_name'])) { $addUniq($ben['translated_name']); } } } if (isset($detail['property_highlight_strip']) && is_array($detail['property_highlight_strip'])) { foreach ($detail['property_highlight_strip'] as $strip) { if (!empty($strip['name'])) { $addUniq($strip['name']); } } } $apiNames = array_values($apiNames); if (empty($apiNames)) { return array( 'matched' => array(), 'unmatched' => array(), 'message' => 'API yanıtında facility bulunamadı', ); } // ── 2. property_fact tablosundaki TÜM kayıtları çek ────────────────────── $allFacts = PropertyFact::select(array('id', 'name', 'icon', 'parent_id', 'type'))->get(); $exactIndex = array(); foreach ($allFacts as $fact) { $key = mb_strtolower(trim($fact->name)); if (!isset($exactIndex[$key]) || $fact->type == 1) { $exactIndex[$key] = $fact; } } // ── 3. Alias map — Booking.com adı ≠ DB adı olan terimler ─────────────── $aliasMap = array( // Internet 'internet services' => 'wi-fi', 'free wifi' => 'wi-fi', 'wifi' => 'wi-fi', 'wi-fi' => 'wi-fi', 'wifi in all areas' => 'wi-fi', 'free wi-fi' => 'wi-fi', 'internet' => 'wi-fi', 'internet access' => 'wi-fi', // Ulaşım 'airport shuttle' => 'airport transfer', 'airport pick up' => 'airport to hotel transfer', 'airport drop off' => 'hotel to airport transfer', 'car hire' => 'rent a car', 'car rental' => 'rent a car', 'shuttle service' => 'airport transfer', // Servis '24-hour front desk' => 'reception', 'concierge service' => 'concierge', 'luggage storage' => 'luggage storage', 'laundry' => 'cleaning service', 'daily housekeeping' => 'housekeeping', 'tour desk' => 'tour desk', 'private check-in/check-out' => 'check-in service', 'express check-in/check-out' => 'check-in service', 'wake-up service' => 'wake up service', 'dry cleaning' => 'dry cleaning', 'currency exchange' => 'currency exchange', // Engelli 'facilities for disabled guests' => 'disabled', 'wheelchair accessible' => 'wheelchair', 'upper floors accessible by elevator' => 'elevator', 'lower bathroom sink' => 'lower bathroom sink', 'toilet with grab rails' => 'toilet with grab rails', // Yiyecek 'breakfast' => 'bed and breakfast', 'snack bar' => 'snack bar', 'meeting/banquet facilities' => 'meeting room', 'business centre' => 'working area', 'business center' => 'working area', 'fax/photocopying' => 'photocopy', // Oda 'air conditioning' => 'air conditioner', 'flat-screen tv' => 'tv', 'flat screen tv' => 'tv', 'bath or shower' => 'shower', 'free toiletries' => 'shower', 'single-room air conditioning for guest accommodation' => 'air conditioner', 'minibar' => 'mini bar', 'mini bar' => 'mini bar', 'hairdryer' => 'hair dryer', 'hair dryer' => 'hair dryer', 'wardrobe or closet' => 'clothes cabinet', 'socket near the bed' => 'planning electrical sockets', 'family rooms' => 'family room', 'clothes rack' => 'clotheshorse', // Güvenlik 'cctv in common areas' => 'security camera', 'cctv outside property' => 'security camera', 'smoke alarms' => 'smoke alarm', 'safety deposit box' => 'safety deposit box', '24-hour security' => 'security service', 'key card access' => 'security service', // Hijyen 'hand sanitizer in guest accommodation and key areas' => 'hand sanitiser', 'hand sanitiser' => 'hand sanitiser', 'face masks for guests available' => 'protective masks for guests', 'physical distancing rules followed' => 'social distancing regulations', 'screens or physical barriers placed between staff and guests in appropriate areas' => 'protective hygiene screens', 'guest accommodation is disinfected between stays' => 'disinfection in all rooms', // Aktivite 'tennis court' => 'tennis', 'golf course' => 'golf', 'table tennis' => 'ping pong', 'jogging track' => 'jogging', // Çevre 'lift' => 'elevator', 'elevator' => 'elevator', ); // ── 4. Eşleştir ─────────────────────────────────────────────────────────── $matched = array(); $unmatched = array(); foreach ($apiNames as $apiName) { $lower = mb_strtolower(trim($apiName)); $searchKey = isset($aliasMap[$lower]) ? $aliasMap[$lower] : $lower; // ADIM 1: Alias → exact if (isset($exactIndex[$searchKey])) { $fact = $exactIndex[$searchKey]; $tag = ($searchKey !== $lower) ? 'alias' : 'exact'; $matched[] = array( 'api_name' => $apiName, 'fact_id' => $fact->id, 'fact_name' => $fact->name, 'match_type' => $tag, ); continue; } // ADIM 2: DB LIKE — fact adı içinde api ismi geçiyor mu? $likeResult = PropertyFact::where('type', 1) ->whereRaw('LOWER(name) LIKE ?', array('%' . $lower . '%')) ->orderByRaw('LENGTH(name) ASC') ->first(); if ($likeResult) { $matched[] = array( 'api_name' => $apiName, 'fact_id' => $likeResult->id, 'fact_name' => $likeResult->name, 'match_type' => 'like', ); continue; } // ADIM 3: Ters LIKE — fact adı api isminin içinde mi? $reverseResult = PropertyFact::where('type', 1) ->whereRaw('LOWER(?) LIKE CONCAT(\'%\', LOWER(name), \'%\')', array($lower)) ->whereRaw('LENGTH(name) >= 5') ->orderByRaw('LENGTH(name) DESC') ->first(); if ($reverseResult) { $matched[] = array( 'api_name' => $apiName, 'fact_id' => $reverseResult->id, 'fact_name' => $reverseResult->name, 'match_type' => 'reverse_like', ); continue; } $unmatched[] = $apiName; } // ── 5. storage/fact.log — okunabilir format ─────────────────────────────── $logPath = storage_path('fact.log'); $ts = date('Y-m-d H:i:s'); $divider = str_repeat('═', 80); $thin = str_repeat('─', 80); $lines = array(); $lines[] = $divider; $lines[] = sprintf(' [%s] hotel_id=%-12s property_id=%s', $ts, $hotelId, $propertyId); $lines[] = sprintf( ' Toplam API facility: %d | Eşleşen: %d | Eşleşmeyen: %d', count($apiNames), count($matched), count($unmatched) ); $lines[] = $divider; $lines[] = ''; $lines[] = sprintf(' EŞLEŞENLER (%d)', count($matched)); $lines[] = ' ' . $thin; if (count($matched)) { foreach ($matched as $m) { $lines[] = sprintf( ' [%-12s] %-45s → #%-5d %s', $m['match_type'], '"' . $m['api_name'] . '"', $m['fact_id'], $m['fact_name'] ); } } else { $lines[] = ' (eşleşen yok)'; } $lines[] = ''; $lines[] = sprintf(' EŞLEŞMEYENLEr (%d)', count($unmatched)); $lines[] = ' ' . $thin; if (count($unmatched)) { foreach ($unmatched as $u) { $lines[] = ' [?] ' . $u; } } else { $lines[] = ' (tümü eşleşti — property_fact\'e eklenebilir)'; } $lines[] = ''; $lines[] = ''; file_put_contents($logPath, implode(PHP_EOL, $lines), FILE_APPEND | LOCK_EX); return array( 'api_facilities_count' => count($apiNames), 'matched_count' => count($matched), 'unmatched_count' => count($unmatched), 'matched' => $matched, 'unmatched' => $unmatched, 'log_file' => $logPath, ); } // ───────────────────────────────────────────────────────────────────────────── // AI Semantic Match: getHotelFacilities → string match → kalan unmatched'ları // Gemini'ye gönder → "aynı kavram farklı isim mi?" veya "gerçekten eksik mi?" // GET /bulut/ai-match?hotel_id=...&property_id=... // ───────────────────────────────────────────────────────────────────────────── public function aiMatch(Request $request) { $hotelId = $request->query('hotel_id', ''); $propertyId = $request->query('property_id', 1); if (!$hotelId) { return response()->json(array('status' => false, 'message' => 'hotel_id gerekli'), 422); } $geminiKey = env('GEMINI_API_KEY', ''); if (!$geminiKey) { return response()->json(array('status' => false, 'message' => 'GEMINI_API_KEY .env\'de tanımlı değil'), 500); } try { // ── ADIM 1: Booking.com'dan tüm faciliteleri çek ───────────────────── $response = $this->client()->get( self::API_BASE . '/api/v1/hotels/getHotelFacilities', array('query' => array('hotel_id' => $hotelId)) ); $data = json_decode($response->getBody()->getContents(), true); $facilData = isset($data['data']) ? $data['data'] : array(); // Tüm API isimlerini topla $apiNames = array(); $addUniq = function ($name) use (&$apiNames) { $name = trim($name); if ($name !== '' && !in_array($name, $apiNames)) { $apiNames[] = $name; } }; if (isset($facilData['facilities']) && is_array($facilData['facilities'])) { foreach ($facilData['facilities'] as $fac) { if (!empty($fac['instances']) && is_array($fac['instances'])) { foreach ($fac['instances'] as $inst) { if (!empty($inst['title'])) { $addUniq($inst['title']); } } } } } if (isset($facilData['accommodationHighlights']) && is_array($facilData['accommodationHighlights'])) { foreach ($facilData['accommodationHighlights'] as $h) { if (!empty($h['title'])) { $addUniq($h['title']); } } } if (isset($facilData['highlights']) && is_array($facilData['highlights'])) { foreach ($facilData['highlights'] as $h) { if (!empty($h['instances']) && is_array($h['instances'])) { foreach ($h['instances'] as $inst) { if (!empty($inst['title'])) { $addUniq($inst['title']); } } } } } if (empty($apiNames)) { return response()->json(array('status' => false, 'message' => 'API\'den facility alınamadı'), 500); } // ── ADIM 2: String tabanlı match (mevcut mantık) ───────────────────── $stringResult = $this->matchAndLogFacilities($facilData, $hotelId, $propertyId); $stringMatched = $stringResult['matched']; $unmatched = $stringResult['unmatched']; if (empty($unmatched)) { return response()->json(array( 'status' => true, 'hotel_id' => $hotelId, 'string_matched' => $stringMatched, 'ai_matched' => array(), 'truly_missing' => array(), 'message' => 'Tüm facilityler string eşleştirme ile bulundu, AI çağrısı gerekmedi', )); } // ── ADIM 3: DB'deki tüm fact isimlerini çek ────────────────────────── $allFactNames = PropertyFact::where('type', 1) ->orderBy('name') ->pluck('name', 'id') ->toArray(); // Gemini'ye göndermek için isim → id lookup $nameToId = array(); foreach ($allFactNames as $id => $name) { $nameToId[mb_strtolower(trim($name))] = array('id' => $id, 'name' => $name); } // ── ADIM 4: Gemini prompt ───────────────────────────────────────────── $unmatchedList = implode("\n", array_map(function ($n) { return '- ' . $n; }, $unmatched)); $dbList = implode("\n", array_map(function ($n) { return '- ' . $n; }, $allFactNames)); $prompt = << 60, 'http_errors' => false)); // Sırayla farklı modeller dene (free tier kota sorunu için) $geminiModels = array( 'gemini-2.0-flash-lite', 'gemini-2.0-flash', 'gemini-flash-lite-latest', 'gemini-flash-latest', ); $rawText = null; $lastStatus = 0; foreach ($geminiModels as $model) { $geminiResponse = $geminiClient->post( 'https://generativelanguage.googleapis.com/v1beta/models/' . $model . ':generateContent?key=' . $geminiKey, array( 'json' => array( 'contents' => array( array('parts' => array(array('text' => $prompt))) ), 'generationConfig' => array( 'responseMimeType' => 'application/json', 'temperature' => 0.1, ), ), ) ); $lastStatus = $geminiResponse->getStatusCode(); if ($lastStatus === 200) { $geminiBody = json_decode($geminiResponse->getBody()->getContents(), true); $rawText = isset($geminiBody['candidates'][0]['content']['parts'][0]['text']) ? $geminiBody['candidates'][0]['content']['parts'][0]['text'] : null; break; } // 429 = quota exceeded → bir sonraki modeli dene if ($lastStatus !== 429) { $errBody = $geminiResponse->getBody()->getContents(); throw new Exception('Gemini API hatası (' . $model . '): HTTP ' . $lastStatus . ' → ' . substr($errBody, 0, 200)); } } if ($rawText === null) { return response()->json(array( 'status' => true, 'hotel_id' => $hotelId, 'api_total' => count($apiNames), 'string_matched' => array('count' => count($stringMatched), 'matches' => $stringMatched), 'ai_matched' => array('count' => 0, 'matches' => array()), 'truly_missing' => array('count' => count($unmatched), 'items' => $unmatched), 'total_matched' => count($stringMatched), 'warning' => 'Tüm Gemini modelleri 429 quota hatası verdi. String match sonuçları döndürüldü. Birkaç dakika bekleyip tekrar deneyin.', )); } $aiResult = json_decode($rawText, true); if (!$aiResult) { $aiResult = array('ai_matches' => array(), 'truly_missing' => $unmatched); } $aiMatches = isset($aiResult['ai_matches']) ? $aiResult['ai_matches'] : array(); $trulyMissing = isset($aiResult['truly_missing']) ? $aiResult['truly_missing'] : array(); // AI eşleşmelerine DB id'si ekle foreach ($aiMatches as $idx => $m) { $key = mb_strtolower(trim($m['db_name'])); if (isset($nameToId[$key])) { $aiMatches[$idx]['fact_id'] = $nameToId[$key]['id']; $aiMatches[$idx]['fact_name'] = $nameToId[$key]['name']; $aiMatches[$idx]['match_type'] = 'ai_semantic'; } } // ── ADIM 6: off_fact.log — TÜM liste: var olanlar + olmayanlar ────────── $logPath = storage_path('off_fact.log'); $ts = date('Y-m-d H:i:s'); $eq = str_repeat('═', 90); $dash = str_repeat('─', 90); // Tüm eşleşmeleri birleştir (string + AI) $allMatched = $stringMatched; foreach ($aiMatches as $am) { $allMatched[] = array( 'api_name' => $am['api_name'], 'fact_id' => isset($am['fact_id']) ? $am['fact_id'] : '???', 'fact_name' => isset($am['fact_name']) ? $am['fact_name'] : $am['db_name'], 'match_type' => 'ai_semantic', 'reason' => isset($am['reason']) ? $am['reason'] : '', ); } $logLines = array(); $logLines[] = $eq; $logLines[] = sprintf( ' [%s] hotel_id: %-12s property_id: %s', $ts, $hotelId, $propertyId ); $logLines[] = sprintf( ' Booking.com\'dan gelen: %d | DB\'de VAR: %d | DB\'de YOK: %d', count($apiNames), count($allMatched), count($trulyMissing) ); $logLines[] = $eq; $logLines[] = ''; // ── VAR OLANLAR ─────────────────────────────────────────────────────── $logLines[] = sprintf(' ✔ DB\'DE VAR (%d / %d)', count($allMatched), count($apiNames)); $logLines[] = ' ' . $dash; foreach ($allMatched as $m) { $type = isset($m['match_type']) ? $m['match_type'] : 'exact'; $line = sprintf( ' [%-12s] %-50s → #%-5s %s', $type, '"' . $m['api_name'] . '"', $m['fact_id'], $m['fact_name'] ); $logLines[] = $line; if (!empty($m['reason'])) { $logLines[] = sprintf(' AI notu: %s', $m['reason']); } } $logLines[] = ''; // ── YOK OLANLAR ─────────────────────────────────────────────────────── $logLines[] = sprintf(' ✘ DB\'DE YOK (%d / %d) — property_fact\'e eklenmesi değerlendirilebilir', count($trulyMissing), count($apiNames)); $logLines[] = ' ' . $dash; if (count($trulyMissing)) { foreach ($trulyMissing as $missing) { $logLines[] = ' [MISSING] "' . $missing . '"'; } } else { $logLines[] = ' (tüm facilityler DB\'de mevcut)'; } $logLines[] = ''; $logLines[] = ''; file_put_contents($logPath, implode(PHP_EOL, $logLines), FILE_APPEND | LOCK_EX); return response()->json(array( 'status' => true, 'hotel_id' => $hotelId, 'api_total' => count($apiNames), 'string_matched' => array( 'count' => count($stringMatched), 'matches' => $stringMatched, ), 'ai_matched' => array( 'count' => count($aiMatches), 'matches' => $aiMatches, ), 'truly_missing' => array( 'count' => count($trulyMissing), 'items' => $trulyMissing, ), 'total_matched' => count($stringMatched) + count($aiMatches), )); } catch (Exception $e) { Log::error('Bulut.aiMatch error: ' . $e->getMessage()); return response()->json(array('status' => false, 'message' => $e->getMessage()), 500); } } }