1080 lines
51 KiB
PHP
1080 lines
51 KiB
PHP
<?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);
|
||
}
|
||
}
|
||
}
|