// Nitelora API .NET örnek istemcisi (tek dosya, bağımlılıksız; .NET 6+)
// Kullanım:
// var eo = new NiteloraIstemci("https://nitelora.com", "sk_test_...");
// var onay = await eo.OnayOlusturAsync(new { sablon = "dis-tedavi-plani", imzaci = new { ad = "Ayşe Yılmaz", telefon = "05321234567" },
// veriler = new { tedavi = "2 adet implant", tutar = "45.000 TL", odeme = "6 taksit", sure = "3 ay" }, refNo = "HASTA-1029" });
// Console.WriteLine(onay.GetProperty("onayLinki").GetString());
//
// Webhook doğrulama (ASP.NET Core):
// app.MapPost("/onay-sonuc", async (HttpRequest r) => {
// var govde = await new StreamReader(r.Body).ReadToEndAsync();
// if (!NiteloraIstemci.WebhookDogrula(govde, r.Headers["X-Nitelora-Imza"], webhookSecret)) return Results.Unauthorized();
// var olay = JsonDocument.Parse(govde).RootElement; ... return Results.Ok(); });
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
public sealed class NiteloraHatasi(int durum, string kod, string mesaj, string? alan) : Exception(mesaj)
{
public int Durum { get; } = durum;
public string Kod { get; } = kod;
public string? Alan { get; } = alan;
}
public sealed class NiteloraIstemci
{
private readonly HttpClient _http;
/// Yalnızca platform anahtarı (plt_) ile, işlem yapılacak klinik
public NiteloraIstemci(string tabanUrl, string anahtar, Guid? klinikId = null, HttpClient? http = null)
{
_http = http ?? new HttpClient();
_http.BaseAddress = new Uri(tabanUrl.TrimEnd('/') + "/");
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", anahtar);
if (klinikId is not null) _http.DefaultRequestHeaders.Add("X-Klinik-Id", klinikId.ToString());
_http.Timeout = TimeSpan.FromSeconds(60);
}
// ---------------- Onay linki (şablonlu, tek imzacı) ----------------
public Task OnayOlusturAsync(object istek, string? idempotencyKey = null) => GonderAsync(HttpMethod.Post, "v1/onaylar", istek, idempotencyKey);
public Task OnayAsync(Guid id) => GonderAsync(HttpMethod.Get, $"v1/onaylar/{id}");
public Task OnayPdfAsync(Guid id) => DosyaAsync($"v1/onaylar/{id}/pdf");
public Task OnayKanitAsync(Guid id) => GonderAsync(HttpMethod.Get, $"v1/onaylar/{id}/kanit");
public Task OnayIptalAsync(Guid id) => GonderAsync(HttpMethod.Post, $"v1/onaylar/{id}/iptal", new { });
// Tıbbi rıza (SBYS): hasta kodu hekime söyler, hekim girer
public Task KodGonderAsync(Guid id, string amac) => GonderAsync(HttpMethod.Post, $"v1/onaylar/{id}/kod", new { amac });
public Task TelefonDogrulaAsync(Guid id, string kod, object hekim) => GonderAsync(HttpMethod.Post, $"v1/onaylar/{id}/telefon-dogrula", new { kod, hekim });
public Task RizaKaydetAsync(Guid id, string kod, object hekim) => GonderAsync(HttpMethod.Post, $"v1/onaylar/{id}/riza", new { kod, hekim });
// ---------------- PDF imzalatma (zarf) ----------------
/// pdf: dosya içeriği; base64'e bu yöntem çevirir.
public Task ZarfOlusturAsync(string baslik, byte[] pdf, string belgeAdi, object[] imzacilar, object[] alanlar,
bool sirali = false, string? refNo = null, string? callbackUrl = null, string? idempotencyKey = null) =>
GonderAsync(HttpMethod.Post, "v1/zarflar", new
{
baslik, belgeAdi, belge = Convert.ToBase64String(pdf), imzacilar, alanlar, sirali, refNo, callbackUrl
}, idempotencyKey);
public Task ZarfAsync(Guid id) => GonderAsync(HttpMethod.Get, $"v1/zarflar/{id}");
public Task ImzaliPdfAsync(Guid id) => DosyaAsync($"v1/zarflar/{id}/pdf");
public Task SablondanGonderAsync(Guid sablonId, object[] imzacilar, string? refNo = null, string? idempotencyKey = null) =>
GonderAsync(HttpMethod.Post, $"v1/zarf-sablonlari/{sablonId}/gonder", new { imzacilar, refNo }, idempotencyKey);
public Task TopluGonderAsync(Guid sablonId, object[] satirlar, string? idempotencyKey = null) =>
GonderAsync(HttpMethod.Post, $"v1/zarf-sablonlari/{sablonId}/toplu", new { satirlar }, idempotencyKey);
// ---------------- Ortak ----------------
private async Task GonderAsync(HttpMethod yontem, string yol, object? govde = null, string? idempotencyKey = null)
{
using var istek = new HttpRequestMessage(yontem, yol);
if (govde is not null) istek.Content = JsonContent.Create(govde);
if (idempotencyKey is not null) istek.Headers.Add("Idempotency-Key", idempotencyKey);
using var yanit = await _http.SendAsync(istek);
var metin = await yanit.Content.ReadAsStringAsync();
if (!yanit.IsSuccessStatusCode) throw Hata((int)yanit.StatusCode, metin);
return metin.Length == 0 ? default : JsonDocument.Parse(metin).RootElement.Clone();
}
private async Task DosyaAsync(string yol)
{
using var yanit = await _http.GetAsync(yol);
if (!yanit.IsSuccessStatusCode) throw Hata((int)yanit.StatusCode, await yanit.Content.ReadAsStringAsync());
return await yanit.Content.ReadAsByteArrayAsync();
}
private static NiteloraHatasi Hata(int durum, string metin)
{
try
{
var h = JsonDocument.Parse(metin).RootElement.GetProperty("hata");
return new NiteloraHatasi(durum, h.GetProperty("kod").GetString()!, h.GetProperty("mesaj").GetString()!,
h.TryGetProperty("alan", out var a) ? a.GetString() : null);
}
catch { return new NiteloraHatasi(durum, "bilinmeyen", metin, null); }
}
/// X-Nitelora-Imza: t={unix},v1={hex HMAC-SHA256(secret, t + "." + ham gövde)}. 5 dakikadan eski imzalar reddedilir.
public static bool WebhookDogrula(string hamGovde, string? imzaBasligi, string webhookSecret, int toleransSaniye = 300)
{
if (string.IsNullOrEmpty(imzaBasligi)) return false;
var parcalar = imzaBasligi.Split(',').Select(p => p.Split('=', 2)).Where(p => p.Length == 2).ToDictionary(p => p[0].Trim(), p => p[1].Trim());
if (!parcalar.TryGetValue("t", out var ts) || !long.TryParse(ts, out var t) || !parcalar.TryGetValue("v1", out var v1)) return false;
if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - t) > toleransSaniye) return false;
var beklenen = HMACSHA256.HashData(Encoding.UTF8.GetBytes(webhookSecret), Encoding.UTF8.GetBytes($"{t}.{hamGovde}"));
byte[] gelen;
try { gelen = Convert.FromHexString(v1); } catch (FormatException) { return false; }
return CryptographicOperations.FixedTimeEquals(beklenen, gelen);
}
}