, ?string): array{0: int, 1: string} */ private $tasiyici; /** * @param callable|null $tasiyici fn(yöntem, adres, başlıklar, gövde): [HTTP durumu, yanıt gövdesi] — testler için */ public function __construct( private readonly string $adres, private readonly string $apiAnahtari, private readonly ?string $webhookSirri = null, private readonly int $zamanAsimiSn = 15, ?callable $tasiyici = null, ) { $this->tasiyici = $tasiyici ?? $this->curl(...); } // --- Yeti ile giriş (PKCE) ------------------------------------------------------------------------------- /** Oturumda saklanacak rastgele "durum" (dönüşte karşılaştırılır). */ public static function yeniDurum(): string { return bin2hex(random_bytes(20)); } /** Oturumda saklanacak PKCE doğrulayıcısı (64 karakter). */ public static function yeniDogrulayici(): string { return rtrim(strtr(base64_encode(random_bytes(48)), '+/', '-_'), '='); } /** base64url(sha256(doğrulayıcı)) — RFC 7636 S256. */ public static function kodOzeti(string $dogrulayici): string { return rtrim(strtr(base64_encode(hash('sha256', $dogrulayici, true)), '+/', '-_'), '='); } /** * Üyenin yönlendirileceği adres. $donus uygulamanızın alan adında https (ya da yönetimde kayıtlı mobil dönüş * adresi; o durumda doğrulayıcı zorunlu). */ public function girisAdresi(string $uygulamaKodu, string $donus, string $durum, ?string $dogrulayici = null): string { $sorgu = ['donus' => $donus, 'durum' => $durum]; if ($dogrulayici !== null) { $sorgu['kod_ozeti'] = self::kodOzeti($dogrulayici); } return rtrim($this->adres, '/').'/baglan/'.rawurlencode($uygulamaKodu).'?'.http_build_query($sorgu); } /** Dönüşteki tek kullanımlık kodu üyenin Yeti koduna çevirir: ['yeti_kodu', 'ad', 'izin' => [...]]. */ public function baglantiCoz(string $kod, ?string $dogrulayici = null): array { return $this->istek('POST', '/baglanti', array_filter(['kod' => $kod, 'kod_dogrulayici' => $dogrulayici])); } // --- Yeti ile öde ------------------------------------------------------------------------------------------ /** * Ödeme talebi açar; üyeyi dönen 'odeme_url'e yönlendirin. Siparişi yalnız webhook ya da odemeTalebiGetir() * ile 'odendi' gördükten sonra onaylayın. * * @param array{siparis_no?: string, donus_url?: string, iptal_url?: string, yeti_kodu?: string} $secenek */ public function odemeTalebi(int $tutar, string $aciklama, string $tekrarAnahtari, array $secenek = []): array { return $this->istek('POST', '/odeme-talepleri', ['tutar' => (string) $tutar, 'aciklama' => $aciklama] + $secenek, $tekrarAnahtari); } public function odemeTalebiGetir(string $id): array { return $this->istek('GET', '/odeme-talepleri/'.rawurlencode($id)); } /** $tutar null ise kalanın tamamı iade edilir. */ public function iadeEt(string $id, ?int $tutar, string $tekrarAnahtari): array { return $this->istek('POST', '/odeme-talepleri/'.rawurlencode($id).'/iade', $tutar === null ? [] : ['tutar' => (string) $tutar], $tekrarAnahtari); } // --- Otomatik harcama izni --------------------------------------------------------------------------------- /** ['izin_var', 'gunluk_limit', 'bugun_harcanan', 'kalan', 'izin_url'] */ public function izin(string $yetiKodu): array { return $this->istek('GET', '/izinler/'.rawurlencode($yetiKodu)); } public function harca(string $yetiKodu, int $tutar, string $aciklama, string $tekrarAnahtari): array { return $this->istek('POST', '/harcamalar', ['yeti_kodu' => $yetiKodu, 'tutar' => (string) $tutar, 'aciklama' => $aciklama], $tekrarAnahtari); } // --- Ödül ve bildirim -------------------------------------------------------------------------------------- /** Uygulamanın ödül bütçesinden üyeye Yeti verir: ['islem_id', 'tutar', 'kalan_butce']. */ public function odulVer(string $yetiKodu, int $tutar, string $aciklama, string $tekrarAnahtari, string $kova = 'kazanilan'): array { return $this->istek('POST', '/oduller', ['yeti_kodu' => $yetiKodu, 'tutar' => (string) $tutar, 'aciklama' => $aciklama, 'kova' => $kova], $tekrarAnahtari); } /** * Bağlı üyeye hizmet bildirimi (reklam yasak; metinde e-posta/telefon olmaz; üye başına günde 10). * Dönen 'iletildi' false ise üye uygulama bildirimlerini kapatmış. */ public function bildirimGonder(string $yetiKodu, string $baslik, string $govde, ?string $baglanti, string $tekrarAnahtari): array { return $this->istek('POST', '/bildirimler', array_filter(['yeti_kodu' => $yetiKodu, 'baslik' => $baslik, 'govde' => $govde, 'baglanti' => $baglanti]), $tekrarAnahtari); } // --- Çevirici ---------------------------------------------------------------------------------------------- /** @return list durum: odendi | teslim | iade */ public function cevirmeler(?string $durum = null): array { return $this->istek('GET', '/cevirmeler'.($durum ? '?durum='.rawurlencode($durum) : ''))['veri'] ?? []; } public function cevirme(string $id): array { return $this->istek('GET', '/cevirmeler/'.rawurlencode($id)); } /** Uygulama bakiyesini yükledikten sonra çağrılır; yapılmazsa süre sonunda Yeti üyeye iade edilir. */ public function cevirmeTeslim(string $id): array { return $this->istek('POST', '/cevirmeler/'.rawurlencode($id).'/teslim', []); } public function cevirmeIade(string $id, string $tekrarAnahtari, ?string $neden = null): array { return $this->istek('POST', '/cevirmeler/'.rawurlencode($id).'/iade', array_filter(['neden' => $neden]), $tekrarAnahtari); } /** ['hizmet_acik', 'proje' => [kod, ad, butce, gelir], 'jeton' => [...]] */ public function durum(): array { return $this->istek('GET', '/durum'); } // --- Webhook ----------------------------------------------------------------------------------------------- /** * "Yeti-Imza" başlığını doğrular (en fazla 5 dakika eski) ve olayı döndürür: ['id', 'olay', 'olusturma', 'veri']. * İmza geçersizse null — isteği 401 ile reddedin. */ public function webhookDogrula(string $govde, ?string $imzaBasligi, ?int $simdi = null): ?array { if (! $this->webhookSirri || ! $imzaBasligi || ! preg_match('/^t=(\d+),v1=([a-f0-9]{64})$/', trim($imzaBasligi), $m)) { return null; } if (abs(($simdi ?? time()) - (int) $m[1]) > 300 || ! hash_equals(hash_hmac('sha256', $m[1].'.'.$govde, $this->webhookSirri), $m[2])) { return null; } $olay = json_decode($govde, true); return is_array($olay) ? $olay : null; } /** "t=,v1=" — cüzdan webhook'larının ve Yeduty görev bildirimlerinin biçimi. */ public static function imzala(string $govde, string $sir, ?int $zaman = null): string { $zaman ??= time(); return 't='.$zaman.',v1='.hash_hmac('sha256', $zaman.'.'.$govde, $sir); } // --- İç ---------------------------------------------------------------------------------------------------- private function istek(string $yontem, string $yol, ?array $govde = null, ?string $tekrarAnahtari = null): array { $basliklar = [ 'Authorization' => 'Bearer '.$this->apiAnahtari, 'Accept' => 'application/json', 'User-Agent' => 'YetiCuzdanPHP/'.self::SURUM, ]; if ($govde !== null) { $basliklar['Content-Type'] = 'application/json'; } if ($tekrarAnahtari !== null) { $basliklar['Idempotency-Key'] = $tekrarAnahtari; } $json = $govde === null ? null : json_encode($govde, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); [$durum, $yanit] = ($this->tasiyici)($yontem, rtrim($this->adres, '/').'/api/v1'.$yol, $basliklar, $json); $veri = json_decode($yanit, true); if ($durum >= 200 && $durum < 300 && is_array($veri)) { return $veri; } throw new YetiCuzdanHatasi( is_array($veri) && isset($veri['mesaj']) ? (string) $veri['mesaj'] : 'Yeti Cüzdan yanıt vermedi (HTTP '.$durum.').', is_array($veri) && isset($veri['hata']) ? (string) $veri['hata'] : 'http_'.$durum, $durum, is_array($veri) ? $veri : null, ); } /** @return array{0: int, 1: string} */ private function curl(string $yontem, string $url, array $basliklar, ?string $govde): array { $c = curl_init($url); curl_setopt_array($c, [ CURLOPT_CUSTOMREQUEST => $yontem, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => $this->zamanAsimiSn, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_HTTPHEADER => array_map(fn ($k, $v) => $k.': '.$v, array_keys($basliklar), $basliklar), ]); if ($govde !== null) { curl_setopt($c, CURLOPT_POSTFIELDS, $govde); } $yanit = curl_exec($c); $durum = (int) curl_getinfo($c, CURLINFO_RESPONSE_CODE); $hata = curl_error($c); curl_close($c); if ($yanit === false) { throw new YetiCuzdanHatasi('Yeti Cüzdan\'a ulaşılamadı: '.$hata, 'baglanti_yok', 0); } return [$durum, (string) $yanit]; } } /** Cüzdanın döndürdüğü hata: $kod makine-okur ('baglanti_yok', 'yetersiz_bakiye', 'hizmet_kapali', ...). */ final class YetiCuzdanHatasi extends RuntimeException { public function __construct(string $mesaj, public readonly string $kod, public readonly int $durum, public readonly ?array $yanit = null) { parent::__construct($mesaj, $durum); } } /** * Yeduty'de "uygulama doğrular" görevleri: üye görevi sizin uygulamanızda tamamlayınca Yeduty'ye bildirin; ödül * kendiliğinden ödenir. Görev sırrı Yeduty yönetiminden alınır (Yönetim → Ayarlar → Otomatik doğrulama sırları). */ final class YedutyGorevleri { /** @var callable(string, string, array, ?string): array{0: int, 1: string}|null */ private $tasiyici; public function __construct(private readonly string $yedutyAdresi, private readonly string $gorevSirri, ?callable $tasiyici = null) { $this->tasiyici = $tasiyici; } /** ['durum' => 'onaylandi', 'odul_durumu' => 'odendi'|'bekliyor']; aynı referansla tekrar göndermek güvenli. */ public function tamamlandi(string $gorevKimligi, string $yetiKodu, string $referans): array { $govde = json_encode(['gorev' => $gorevKimligi, 'yeti_kodu' => $yetiKodu, 'referans' => $referans], JSON_UNESCAPED_UNICODE); $basliklar = ['Content-Type' => 'application/json', 'Accept' => 'application/json', 'Yeduty-Imza' => YetiCuzdan::imzala($govde, $this->gorevSirri)]; $url = rtrim($this->yedutyAdresi, '/').'/api/gorev-tamamlandi'; if ($this->tasiyici) { [$durum, $yanit] = ($this->tasiyici)('POST', $url, $basliklar, $govde); } else { $c = curl_init($url); curl_setopt_array($c, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $govde, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_HTTPHEADER => array_map(fn ($k, $v) => $k.': '.$v, array_keys($basliklar), $basliklar)]); $yanit = (string) curl_exec($c); $durum = (int) curl_getinfo($c, CURLINFO_RESPONSE_CODE); curl_close($c); } $veri = json_decode($yanit, true); if ($durum === 200 && is_array($veri)) { return $veri; } throw new YetiCuzdanHatasi((string) ($veri['mesaj'] ?? 'Yeduty yanıt vermedi (HTTP '.$durum.').'), (string) ($veri['hata'] ?? 'http_'.$durum), $durum, is_array($veri) ? $veri : null); } }