> ## Documentation Index
> Fetch the complete documentation index at: https://docs.diyanet.pro/llms.txt
> Use this file to discover all available pages before exploring further.

# SMS Servisi

> NetGSM REST v2 OTP entegrasyonu — Basic Auth, ASCII-only mesaj, ortam bazlı telefon çözümleme ve Polly retry.

SMS servisi `src/DiyanetCleanArchitecture.Infrastructure.Services.Sms` projesindedir ve yalnızca OTP (tek kullanımlık kod) SMS'i gönderir. Sağlayıcı **NetGSM REST v2 OTP** API'sidir (`/sms/rest/v2/otp`).

## Arayüz

```csharp theme={null}
public interface IOtpSmsService
{
    Task SendAsync(string message, Phone to);
    Task SendAsync(OtpCode otpCode, Phone to);
    Task SendAsync(string message, params Phone[] to);
}
```

`OtpCode` overload'u mesajı sabit bir kalıpla kendisi oluşturur (ASCII-only):

```csharp theme={null}
public async Task SendAsync(OtpCode otpCode, Phone to)
{
    string message = $"DiyanetCleanArchitecture OTP kodunuz: {otpCode.Value} Kimseyle paylasmayin.";
    await SendAsync(message, new[] { to });
}
```

## ASCII zorunluluğu

NetGSM OTP API'si Türkçe karakter kabul etmez. Gönderimden önce mesaj ASCII kontrolünden geçer; değilse `SmsServiceException` fırlatılır:

```csharp theme={null}
if (!message.IsAscii())
    throw new SmsServiceException("OTP SMS ASCII karakterler içermelidir (Türkçe karakter kullanılamaz).");
```

<Warning>
  OTP mesaj metinlerinde "ı, ş, ğ, ü, ö, ç" gibi karakterler kullanılamaz. Yukarıdaki sabit mesajda da bilinçli olarak "paylasmayin" yazılmıştır.
</Warning>

## İstek/yanıt modeli

İstek gövdesi NetGSM'in beklediği alan adlarıyla (küçük harf) serialize edilir. Telefon numarası `Phone.ToNetGsmFormat()` ile NetGSM formatına çevrilir:

```csharp theme={null}
var request = new GsmOtpRequest
{
    msgheader = _options.MessageHeader,
    msg = message,                 // ASCII only
    no = phone.ToNetGsmFormat(),
    appname = null
};

var response = await _client.PostAsync(_options.OtpPath, content);
var body = await response.Content.ReadAsStringAsync();
GsmOtpResponse? result = TryDeserialize(body);
```

Esas başarı kontrolü HTTP status değil, **gövdedeki `code` alanıdır**. `code != "00"` ise iş hatası olarak değerlendirilir:

```csharp theme={null}
if (result is not null && result.code != "00")
{
    throw new SmsServiceException(
        $"OTP SMS başarısız. Code: {result.code}, Desc: {result.description}. " +
        GsmOtpErrors.ToMessage(result.code));
}

if (!response.IsSuccessStatusCode)
    throw new SmsServiceException($"OTP SMS HTTP hatası. Status: {response.StatusCode}, Body: {body}");
```

## NetGSM hata kodları

`GsmOtpErrors.ToMessage(code)` kod → mesaj eşlemesini yapar:

| Code        | Anlam                                         |
| ----------- | --------------------------------------------- |
| `00`        | Başarılı                                      |
| `20`        | Mesaj içeriği veya uzunluğu hatalı            |
| `30`        | API kullanıcı bilgileri hatalı veya IP kısıtı |
| `40` / `41` | Gönderici adı (`msgheader`) hatalı            |
| `50` / `52` | Telefon numarası hatalı                       |
| `60`        | OTP SMS paketi tanımlı değil                  |
| `70`        | Parametreler hatalı                           |
| `100`       | NetGSM sistem hatası                          |

## Ortam bazlı telefon çözümleme — `SmsPhoneResolver`

Production'da gerçek numaralara gönderim yapılır; Development/Staging'de ise yalnızca config'teki test numaralarına gönderilir (gerçek kullanıcılara SMS gitmesini engellemek için):

```csharp theme={null}
public Phone[] Resolve(params Phone[] to)
{
    if (_environment.IsProduction())
        return to;

    if (_environment.IsDevelopment() || _environment.IsStaging())
    {
        if (!_options.Phones.Any())
            throw new SmsServiceException("Tanımlı telefon numarası yok. Services:Sms:Phones");

        return _options.Phones.Select(p => new Phone(p)).Distinct().ToArray();
    }

    throw new SmsServiceException("Bilinmeyen environment");
}
```

## Config — `Services:Sms`

```json theme={null}
{
  "Services": {
    "Sms": {
      "BaseUrl": "https://api.gsm.com.tr",
      "OtpPath": "/sms/rest/v2/otp",
      "Username": "myusername",
      "Password": "mypassword",
      "MessageHeader": "myheader",
      "Encoding": "TR",
      "TimeoutSeconds": 10,
      "RetryCount": 1,
      "Phones": [ "5443590815" ]
    }
  }
}
```

<ParamField path="BaseUrl" type="string">NetGSM API kök adresi. HttpClient `BaseAddress` olarak ayarlanır.</ParamField>
<ParamField path="OtpPath" type="string" default="/sms/rest/v2/otp">OTP endpoint yolu.</ParamField>
<ParamField path="Username / Password" type="string">Basic Auth kimlik bilgileri (Base64'lenip `Authorization: Basic` header'a yazılır).</ParamField>
<ParamField path="MessageHeader" type="string">Onaylı gönderici adı (`msgheader`).</ParamField>
<ParamField path="Phones" type="string[]">Production **dışı** ortamlarda gönderim yalnızca bu numaralara yapılır.</ParamField>
<ParamField path="RetryCount" type="int" default="3">Polly HTTP retry sayısı (300ms × deneme lineer backoff).</ParamField>

## DI kaydı — HttpClient + Polly + Basic Auth

```csharp theme={null}
public static IServiceCollection AddSmsService(this IServiceCollection services, IConfiguration configuration)
{
    services.Configure<GsmOptions>(o => configuration.GetSection("Services:Sms").Bind(o));
    services.AddTransient<SmsPhoneResolver>();

    services.AddHttpClient<IOtpSmsService, OtpSmsService>((sp, client) =>
    {
        var options = sp.GetRequiredService<IOptions<GsmOptions>>().Value;
        var credentials = Convert.ToBase64String(
            Encoding.UTF8.GetBytes($"{options.Username}:{options.Password}"));

        client.BaseAddress = new Uri(options.BaseUrl);
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials);
        client.Timeout = TimeSpan.FromSeconds(options.TimeoutSeconds);
    })
    .AddPolicyHandler((sp, _) =>
    {
        var retry = sp.GetRequiredService<IOptions<GsmOptions>>().Value.RetryCount;
        return HttpPolicyExtensions
            .HandleTransientHttpError()
            .WaitAndRetryAsync(retry, r => TimeSpan.FromMilliseconds(300 * r));
    });

    // sms-provider health check — [External]: SMS down → /health/ready bozulmaz
    services.AddHealthChecks().AddUrlGroup(uri: smsUri, name: "sms-provider",
        failureStatus: HealthStatus.Degraded,
        tags: new[] { HealthCheckTags.External, HealthCheckTags.Sms },
        timeout: TimeSpan.FromSeconds(5));

    return services;
}
```

<CardGroup cols={2}>
  <Card title="Email Servisi" icon="envelope" href="/services/email">
    OTP'nin e-posta kanalı.
  </Card>

  <Card title="Value Objects" icon="cube" href="/domain/value-objects">
    `Phone.ToNetGsmFormat()` ve `OtpCode.Generate()` value object'leri.
  </Card>
</CardGroup>
