$data, 'meta' => ['version' => 'v1'], 'errors' => []], JSON_UNESCAPED_UNICODE); exit; } function hata(string $kod, string $detay, int $durum = 400): never { http_response_code($durum); header('Content-Type: application/json; charset=utf-8'); echo json_encode(['data' => null, 'meta' => ['version' => 'v1'], 'errors' => [['code' => $kod, 'detail' => $detay]]], JSON_UNESCAPED_UNICODE); exit; } // -------------------------------------------------------------- kimlik /** * Jetonu nasıl saklayacağınız size kalmış (veritabanı, Redis, JWT). * Burada dosyada tutuluyor — ÖRNEK olduğu için. */ function jetonDosyasi(): string { return sys_get_temp_dir().'/oa-saglayici-jetonlar.json'; } function jetonUret(): array { $jeton = 'oa_'.bin2hex(random_bytes(24)); $liste = json_decode(@file_get_contents(jetonDosyasi()) ?: '{}', true) ?: []; $liste[$jeton] = time() + JETON_OMRU; // Süresi geçenleri temizle $liste = array_filter($liste, fn ($bitis) => $bitis > time()); file_put_contents(jetonDosyasi(), json_encode($liste)); return ['token_type' => 'Bearer', 'access_token' => $jeton, 'expires_in' => JETON_OMRU]; } /** Bearer jeton doğrulaması; geçersizse INVALID_TOKEN (biz bir kez tazeleyip yineleriz). */ function jetonuDogrula(): void { $baslik = $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? ''; $jeton = str_starts_with($baslik, 'Bearer ') ? substr($baslik, 7) : ''; $liste = json_decode(@file_get_contents(jetonDosyasi()) ?: '{}', true) ?: []; if ($jeton === '' || ($liste[$jeton] ?? 0) < time()) { hata('INVALID_TOKEN', 'Jeton geçersiz ya da süresi dolmuş.', 401); } } // ------------------------------------------------------- ÖRNEK VERİ TABANI // // BURAYI KENDİ SİSTEMİNİZE BAĞLAYACAKSINIZ. Alan adları ve biçimler // sözleşmede sabittir; kendi sütun adlarınızı buraya EŞLEYİN. function musteriler(): array { return [ [ 'abone_no' => '1001234', 'isim' => 'AYHAN', // MASKELEMEYİN: asistan adıyla hitap eder 'soyisim' => 'SARI', 'firma_unvan' => null, 'durum' => 'Aktif', 'bitis_tarihi' => '2026-12-31', 'tarife' => 'FIBER 100', 'tarife_hiz' => '100 Mbps', 'tarife_fiyat' => '750,00', // biçimli metin 'toplam_borc' => 1500, // çıplak sayı (TL) — TUTARLI OLUN 'online_mi' => true, 'tesis_adres_satir' => 'ÖZGÜR MAH. KAKTÜS CAD. NO 5 D 13 YÜREĞİR/ADANA', 'telefon_1' => '5321112233', 'tc' => '12345671234', // yalnız son 4 hanesini karşılaştırırız 'faturalar' => [ ['fatura_no' => 'F-2026-001', 'fiyat' => 750, 'odenen_tutar' => 0, 'kalan' => 750, 'durum' => 'Ödenmedi', 'son_odeme_tarihi' => '2026-08-31'], ['fatura_no' => 'F-2026-002', 'fiyat' => 750, 'odenen_tutar' => 0, 'kalan' => 750, 'durum' => 'Ödenmedi', 'son_odeme_tarihi' => '2026-09-30'], ], ], ]; } /** Basit arama: telefon / müşteri no / TC / ad-soyad. */ function musteriAra(string $sorgu): array { $sorgu = mb_strtolower(trim($sorgu), 'UTF-8'); if ($sorgu === '') { return []; } return array_values(array_filter(musteriler(), function (array $m) use ($sorgu): bool { $adSoyad = mb_strtolower($m['isim'].' '.$m['soyisim'], 'UTF-8'); return str_contains(mb_strtolower($m['abone_no'], 'UTF-8'), $sorgu) || str_contains($m['telefon_1'], $sorgu) || str_contains((string) $m['tc'], $sorgu) || str_contains($adSoyad, $sorgu); })); } function musteriBul(string $aboneNo): ?array { foreach (musteriler() as $m) { if ($m['abone_no'] === trim($aboneNo)) { return $m; } } return null; } /** Açık talepler — mükerrer kayıt açmamak için (dosyada tutuluyor, örnek). */ function acikTalepler(): array { return json_decode(@file_get_contents(sys_get_temp_dir().'/oa-saglayici-talepler.json') ?: '{}', true) ?: []; } function talepYaz(array $liste): void { file_put_contents(sys_get_temp_dir().'/oa-saglayici-talepler.json', json_encode($liste)); } /** * Mahalle adını sadeleştir. * * MÜŞTERİ "Özgür Mahallesi" DER, KAYDINIZDA "ÖZGÜR" YAZAR. Birebir * eşleştirirseniz hizmet verdiğiniz adrese "bölgemizde değil" dersiniz — * bu gerçek bir müşteri kaybına yol açtı. */ function mahalleSadelestir(string $ad): string { $ekler = ['mahallesi', 'mahalle', 'mah.', 'mah', 'mh.', 'mh']; $ad = trim(preg_replace('/\s+/u', ' ', $ad) ?? $ad); $parcalar = explode(' ', $ad); if (count($parcalar) > 1 && in_array(mb_strtolower(end($parcalar), 'UTF-8'), $ekler, true)) { array_pop($parcalar); } return mb_strtolower(implode(' ', $parcalar), 'UTF-8'); } // ------------------------------------------------------------------- yönlendirme $yolParcasi = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?? '/'; $uc = str_starts_with($yolParcasi, YOL) ? substr($yolParcasi, strlen(YOL)) : $yolParcasi; $metot = $_SERVER['REQUEST_METHOD'] ?? 'GET'; $govde = json_decode(file_get_contents('php://input') ?: '{}', true) ?: []; // Jeton ucu kimlik doğrulamadan ÖNCE gelir. if ($uc === '/auth/token' && $metot === 'POST') { $kullanici = $_SERVER['PHP_AUTH_USER'] ?? ''; $sifre = $_SERVER['PHP_AUTH_PW'] ?? ''; if (! hash_equals(CLIENT_ID, $kullanici) || ! hash_equals(CLIENT_SECRET, $sifre)) { hata('AUTH_FAILED', 'client_id veya client_secret hatalı.', 401); } veri(jetonUret()); } jetonuDogrula(); switch (true) { // ---------------------------------------------------- müşteri arama case $uc === '/customers/find' && $metot === 'GET': $kayitlar = musteriAra((string) ($_GET['find'] ?? '')); $adet = max(1, (int) ($_GET['records'] ?? 5)); // TC'yi asistana GÖNDERMEYİN; yalnız doğrulamada kullanılır. $kayitlar = array_map(function (array $m): array { unset($m['tc']); return $m; }, array_slice($kayitlar, 0, $adet)); veri(['records' => $kayitlar]); // ------------------------------------------------ kimlik doğrulama case $uc === '/agent/verify' && $metot === 'POST': $aboneNo = trim((string) ($govde['abone_no'] ?? '')); $adSoyad = mb_strtolower(trim((string) ($govde['ad_soyad'] ?? '')), 'UTF-8'); $tcSon4 = preg_replace('/\D/', '', (string) ($govde['tc_son4'] ?? '')) ?? ''; $arayan = preg_replace('/\D/', '', (string) ($govde['arayan_no'] ?? '')) ?? ''; // Abone no verilmediyse arayan numarasından bul (telefonda böyle olur). $adaylar = $aboneNo !== '' ? array_filter([musteriBul($aboneNo)]) : ($arayan !== '' ? musteriAra(substr($arayan, -10)) : []); foreach ($adaylar as $m) { $gercekAd = mb_strtolower($m['isim'].' '.$m['soyisim'], 'UTF-8'); $adTutuyor = $adSoyad !== '' && similar_text($gercekAd, $adSoyad) >= mb_strlen($gercekAd) * 0.7; $tcTutuyor = strlen($tcSon4) === 4 && str_ends_with((string) $m['tc'], $tcSon4); // İkisinden biri yeterli değil: ad-soyad TEK BAŞINA kimlik değildir. if ($adTutuyor && $tcTutuyor) { // Kişinin birden çok kaydı varsa hepsini verin; asistan // numaralı liste sunup müşteriye seçtirir. $abonelikler = array_map(fn (array $x): array => [ 'abone_no' => $x['abone_no'], 'adres' => $x['tesis_adres_satir'], ], array_filter(musteriler(), fn ($x) => $x['tc'] === $m['tc'])); veri(['result' => [ 'dogrulandi' => true, 'abone_no' => $m['abone_no'], 'kalan_deneme' => 3, 'abonelikler' => array_values($abonelikler), ]]); } } veri(['result' => ['dogrulandi' => false, 'kalan_deneme' => 2]]); // ------------------------------------------------- hizmet durumu case $uc === '/customers/connection-check' && $metot === 'POST': $m = musteriBul((string) ($govde['abone_no'] ?? '')); if (! $m) { hata('CUSTOMER_NOT_FOUND', 'Kayıt bulunamadı.', 404); } veri(['result' => [ // Sözlükten seçin: online | offline | inactive | expired | // radacct_only | inconsistent | mikrotik_unreachable 'status' => $m['online_mi'] ? 'online' : 'offline', 'bitis_tarihi' => $m['bitis_tarihi'], ]]); // -------------------------------------------------------- talep case $uc === '/tickets' && $metot === 'POST': $aboneNo = trim((string) ($govde['abone_no'] ?? '')); $aciklama = mb_substr(trim((string) ($govde['description'] ?? '')), 0, 255); if (! musteriBul($aboneNo)) { hata('CUSTOMER_NOT_FOUND', 'Kayıt bulunamadı.', 404); } if (mb_strlen($aciklama) < 5) { hata('VALIDATION_ERROR', 'Açıklama çok kısa.', 422); } $talepler = acikTalepler(); // AÇIK KAYIT VARSA YENİSİ AÇILMAZ. Aynı sorun için kayıt yığmak, // teknik ekibin listesini kullanılmaz hâle getiriyor. if (isset($talepler[$aboneNo])) { veri([ 'ticket_id' => $talepler[$aboneNo], 'existing' => true, 'message' => 'Bu abone için zaten açık bir kayıt var.', ]); } $no = 'T-'.date('Y').'-'.random_int(1000, 9999); $talepler[$aboneNo] = $no; talepYaz($talepler); veri(['ticket_id' => $no, 'existing' => false, 'message' => '']); // ------------------------------------------------- mesaj gönder case $uc === '/agent/message' && $metot === 'POST': $m = musteriBul((string) ($govde['abone_no'] ?? '')); $tur = (string) ($govde['tur'] ?? ''); if (! $m) { hata('CUSTOMER_NOT_FOUND', 'Kayıt bulunamadı.', 404); } if (! in_array($tur, ['odeme_linki', 'oim_linki', 'pppoe_sifresi', 'oim_sifresi'], true)) { hata('VALIDATION_ERROR', 'Geçersiz mesaj türü.', 422); } // BURADA KENDİ SMS/WhatsApp ALTYAPINIZDAN GÖNDERİN. // Numara müşterinin SİZDE KAYITLI numarasıdır; asistan başka numara vermez. // ornekGonder($m['telefon_1'], $tur); veri(['result' => ['kanal' => 'sms', 'gonderildi' => true]]); // ---------------------------------------------------- kapsama case $uc === '/agent/altyapi' && $metot === 'POST': $mahalle = mahalleSadelestir((string) ($govde['mahalle'] ?? '')); $ilce = mb_strtolower(trim((string) ($govde['ilce'] ?? '')), 'UTF-8'); // Kendi hizmet bölgesi tablonuzla eşleştirin (sadeleştirilmiş adla!). $bolgeler = [ 'yüreğir' => ['özgür', 'sinanpaşa'], 'seyhan' => ['pınar'], ]; $bulundu = in_array($mahalle, $bolgeler[$ilce] ?? [], true); veri(['result' => [ 'bulundu' => $bulundu, // Sonuç ADRESE mi bölgeye mi ait? Emin değilseniz false bırakın: // asistan o zaman "bölgenizde şu seçenekler olabiliyor" der. 'adres_kesin' => false, 'altyapilar' => $bulundu ? ['gpon'] : [], 'tarifeler' => $bulundu ? [['ad' => 'FIBER 100', 'fiyat' => '750,00']] : [], 'vae_var' => false, 'vae_urunler' => [], 'vae_tarifeleri' => [], ]]); // ---------------------------------------------- genel duyuru case $uc === '/agent/genel-ariza' && $metot === 'GET': $aboneNo = (string) ($_GET['abone_no'] ?? ''); veri([ 'genel_ariza' => false, // abone_no verildiyse O MÜŞTERİNİN etkilenip etkilenmediğini dönün. // Etkilenmiyorsa asistan genel arızadan hiç söz etmez. 'abone_etkilenen' => false, 'arizalar' => [], ]); // ------------------------------------------- asistan yapılandırması case $uc === '/agent/config' && $metot === 'GET': veri([ 'aktif' => true, 'marka' => [ 'ad' => 'Örnek Firma', 'asistan_adi' => 'Kont', 'uslup' => 'resmi', 'isimle_hitap' => true, 'basvuru_link' => 'https://ornek.com/basvuru', ], 'karsilama_metni' => 'Örnek Firma\'ya hoş geldiniz.', 'ek_talimat' => '', // ASISTANIN DAVRANIŞ KURALLARI. Kendi panelinizden yönetin; // web, WhatsApp ve telefonda dakikalar içinde geçerli olur. 'ogretiler' => [ ['baslik' => 'İptal talepleri', 'icerik' => 'İptal taleplerini kendin işleme alma, temsilciye aktar.'], ], 'kimlik_dogrulama_zorunlu' => true, 'kimlik_dogrulamasiz_yasak_aksiyonlar' => ['borc_sorgula', 'ariza_kaydi_ac', 'mesaj_gonder'], 'aktarma' => [ 'temsilci_var' => true, 'temsilci_dahililer' => ['100', '101'], 'yedek_hedef' => '700', 'temsilci_mesai' => '09:00-18:00', ], 'tarifeler' => [['ad' => 'FIBER 100', 'fiyat' => '750,00']], 'sss' => [['soru' => 'Ödeme nasıl yapılır?', 'cevap' => 'Panelden ya da havale ile.']], 'mesaj' => ['kanal' => 'otomatik', 'whatsapp_hazir' => false], ]); // ----------------------------------------------- görüşme sonucu case $uc === '/agent/calls' && $metot === 'POST': // Görüşme özetini müşteri kartına yazın; ilgilenmiyorsanız bu ucu // hiç açmayın (404 dönün), asistan bunu kapalı sayar. // ornekKaydet($govde); veri(['kaydedildi' => true, 'uniqueid' => $govde['uniqueid'] ?? null]); default: hata('NOT_FOUND', 'Bu uç tanımlı değil: '.$metot.' '.$uc, 404); }