Files
api-extranetwork/app/Http/Controllers/bulutController.php
ExtraNetwork e5c4b6aa13 first commit
2026-05-12 17:04:54 +03:00

1080 lines
51 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Http\Controllers;
use App\Models\PropertyPhoto;
use App\Models\PropertyFact;
use Exception;
use GuzzleHttp\Client;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Intervention\Image\Facades\Image;
class bulutController extends Controller
{
const API_HOST = 'booking-com15.p.rapidapi.com';
const API_BASE = 'https://booking-com15.p.rapidapi.com';
private function client()
{
return new Client(array(
'timeout' => 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 = <<<PROMPT
You are a hotel facility semantic matching assistant.
I have two lists of hotel facility names.
LIST A - From Booking.com API (could not be matched by string comparison, need semantic analysis):
{$unmatchedList}
LIST B - From my hotel database (property_fact table, 708 entries):
{$dbList}
TASK: For each item in LIST A:
1. Check if there is a semantically equivalent entry in LIST B (same concept, just different wording). Examples:
- "Family rooms" = "Family Room" → MATCH
- "Wine/champagne" = "Wine Cellar" → MATCH (if exists in LIST B)
- "City view" = "Sea View" → NO MATCH (different concepts)
- "City view" = "City Centre" → NO MATCH (location ≠ view)
2. If there is a good semantic match, report it.
3. If there is absolutely no equivalent concept in LIST B, mark it as missing.
IMPORTANT: Only match when you are confident (≥80%) they refer to the same hotel facility/service. Do NOT stretch.
Return ONLY valid JSON (no markdown, no explanation):
{
"ai_matches": [
{"api_name": "...", "db_name": "...", "reason": "..."}
],
"truly_missing": ["...", "..."]
}
PROMPT;
// ── ADIM 5: Gemini API çağrısı ────────────────────────────────────────
$geminiClient = new Client(array('timeout' => 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);
}
}
}