diff --git a/Directory.Packages.props b/Directory.Packages.props
index 03890ac..261ee50 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -24,6 +24,7 @@
+
diff --git a/src/Meezi.API/Controllers/AuthController.cs b/src/Meezi.API/Controllers/AuthController.cs
index d4f7fd4..529fb8f 100644
--- a/src/Meezi.API/Controllers/AuthController.cs
+++ b/src/Meezi.API/Controllers/AuthController.cs
@@ -6,7 +6,6 @@ using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Meezi.API.Models.Auth;
using Meezi.API.Services;
-using Meezi.API.Services;
using Meezi.Core.Constants;
using Meezi.Shared;
diff --git a/src/Meezi.Infrastructure/DependencyInjection.cs b/src/Meezi.Infrastructure/DependencyInjection.cs
index ea2b0e8..ea3554c 100644
--- a/src/Meezi.Infrastructure/DependencyInjection.cs
+++ b/src/Meezi.Infrastructure/DependencyInjection.cs
@@ -29,7 +29,7 @@ public static class DependencyInjection
services.AddScoped();
services.AddScoped();
- services.AddHttpClient();
+ services.AddScoped();
services.AddHttpClient();
services.AddHttpClient();
services.AddHttpClient();
diff --git a/src/Meezi.Infrastructure/ExternalServices/KavenegarSmsService.cs b/src/Meezi.Infrastructure/ExternalServices/KavenegarSmsService.cs
index cc16d29..d183a4e 100644
--- a/src/Meezi.Infrastructure/ExternalServices/KavenegarSmsService.cs
+++ b/src/Meezi.Infrastructure/ExternalServices/KavenegarSmsService.cs
@@ -1,5 +1,5 @@
-using System.Net.Http.Json;
-using System.Text.Json.Serialization;
+using Kavenegar;
+using Kavenegar.Exceptions;
using Meezi.Core.Interfaces;
using Meezi.Infrastructure.Services.Platform;
using Microsoft.Extensions.Configuration;
@@ -9,35 +9,31 @@ using Microsoft.Extensions.Logging;
namespace Meezi.Infrastructure.ExternalServices;
///
-/// Kavenegar SMS gateway implementation.
+/// Kavenegar SMS gateway implementation using the official Kavenegar .NET SDK.
/// Reads config from DB (via IPlatformRuntimeConfig) first, then falls back
/// to IConfiguration ("Kavenegar:ApiKey", "Kavenegar:SenderNumber", etc.).
///
public class KavenegarSmsService : ISmsService
{
// ── DB config keys ────────────────────────────────────────────────────────
- private const string DbKeyApiKey = "integrations.kavenegar.apiKey";
- private const string DbKeyEnabled = "integrations.kavenegar.enabled";
- private const string DbKeySender = "integrations.kavenegar.senderNumber";
- private const string DbKeyOtpTemplate = "integrations.kavenegar.otpTemplate";
+ private const string DbKeyApiKey = "integrations.kavenegar.apiKey";
+ private const string DbKeyEnabled = "integrations.kavenegar.enabled";
+ private const string DbKeySender = "integrations.kavenegar.senderNumber";
+ private const string DbKeyOtpTemplate = "integrations.kavenegar.otpTemplate";
- private const string BaseUrl = "https://api.kavenegar.com/v1";
- private const int MaxBatchSize = 200;
+ private const int MaxBatchSize = 200;
- private readonly HttpClient _httpClient;
private readonly IConfiguration _configuration;
private readonly IPlatformRuntimeConfig _platform;
private readonly IHostEnvironment _environment;
private readonly ILogger _logger;
public KavenegarSmsService(
- HttpClient httpClient,
IConfiguration configuration,
IPlatformRuntimeConfig platform,
IHostEnvironment environment,
ILogger logger)
{
- _httpClient = httpClient;
_configuration = configuration;
_platform = platform;
_environment = environment;
@@ -61,16 +57,11 @@ public class KavenegarSmsService : ISmsService
return;
}
- var url = $"{BaseUrl}/{apiKey}/verify/lookup.json";
- var content = new FormUrlEncodedContent(new Dictionary
+ var receptor = NormalizePhone(phone);
+ await RunSdkAsync(apiKey, api =>
{
- ["receptor"] = NormalizePhone(phone),
- ["token"] = otp,
- ["template"] = template,
- });
-
- var response = await _httpClient.PostAsync(url, content, cancellationToken);
- await EnsureKavenegarSuccessAsync(response, "OTP", cancellationToken);
+ api.VerifyLookup(receptor, otp, null, null, template);
+ }, "OTP");
}
public async Task SendMessageAsync(string phone, string message, CancellationToken cancellationToken = default)
@@ -82,11 +73,11 @@ public class KavenegarSmsService : ISmsService
return;
}
- var url = $"{BaseUrl}/{apiKey}/sms/send.json";
- var content = BuildSendForm(phone, message, sender);
-
- var response = await _httpClient.PostAsync(url, content, cancellationToken);
- await EnsureKavenegarSuccessAsync(response, "Send", cancellationToken);
+ var receptor = NormalizePhone(phone);
+ await RunSdkAsync(apiKey, api =>
+ {
+ api.Send(sender, receptor, message);
+ }, "Send");
}
public async Task SendBulkAsync(
@@ -103,17 +94,18 @@ public class KavenegarSmsService : ISmsService
return new BulkSendResult(0, phones.Count);
}
- var url = $"{BaseUrl}/{apiKey}/sms/send.json";
int sent = 0, failed = 0;
foreach (var batch in phones.Chunk(MaxBatchSize))
{
try
{
- // Kavenegar /sms/send.json accepts comma-separated receptors
- var content = BuildSendForm(string.Join(",", batch), message, sender);
- var response = await _httpClient.PostAsync(url, content, cancellationToken);
- await EnsureKavenegarSuccessAsync(response, "BulkSend", cancellationToken);
+ var receptors = batch.Select(NormalizePhone).ToList();
+ await RunSdkAsync(apiKey, api =>
+ {
+ api.Send(sender, receptors, message);
+ }, "BulkSend");
+
sent += batch.Length;
_logger.LogInformation("Kavenegar bulk batch: {Count} sent", batch.Length);
}
@@ -134,20 +126,12 @@ public class KavenegarSmsService : ISmsService
try
{
- var url = $"{BaseUrl}/{apiKey}/account/info.json";
- var response = await _httpClient.GetAsync(url, cancellationToken);
-
- if (!response.IsSuccessStatusCode)
+ return await Task.Run(() =>
{
- _logger.LogWarning("Kavenegar account info returned HTTP {Status}", response.StatusCode);
- return null;
- }
-
- var body = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken);
- if (body?.Return?.Status is not 200 || body.Entries is null)
- return null;
-
- return new KavenegarAccountInfo(body.Entries.RemainCredit, body.Entries.Type ?? "master");
+ var api = new KavenegarApi(apiKey);
+ var info = api.AccountInfo();
+ return new KavenegarAccountInfo(info.RemainCredit, info.Type ?? "master");
+ }, cancellationToken);
}
catch (Exception ex)
{
@@ -156,42 +140,42 @@ public class KavenegarSmsService : ISmsService
}
}
+ // ── SDK runner ────────────────────────────────────────────────────────────
+
+ ///
+ /// Runs a synchronous Kavenegar SDK call on the thread pool.
+ /// Translates SDK exceptions to logged InvalidOperationException.
+ ///
+ private async Task RunSdkAsync(string apiKey, Action action, string operation)
+ {
+ await Task.Run(() =>
+ {
+ try
+ {
+ var api = new KavenegarApi(apiKey);
+ action(api);
+ }
+ catch (ApiException ex)
+ {
+ _logger.LogWarning(
+ "Kavenegar {Op} API error {Code}: {Message}",
+ operation, ex.Code, ex.Message);
+ throw new InvalidOperationException(
+ $"Kavenegar {operation} failed (code {ex.Code}): {ex.Message}", ex);
+ }
+ catch (HttpException ex)
+ {
+ _logger.LogWarning(
+ "Kavenegar {Op} HTTP error {Code}: {Message}",
+ operation, ex.Code, ex.Message);
+ throw new InvalidOperationException(
+ $"Kavenegar {operation} HTTP error (code {ex.Code}): {ex.Message}", ex);
+ }
+ });
+ }
+
// ── Helpers ───────────────────────────────────────────────────────────────
- private static FormUrlEncodedContent BuildSendForm(string receptor, string message, string sender)
- {
- var dict = new Dictionary
- {
- ["receptor"] = receptor,
- ["message"] = message,
- };
- if (!string.IsNullOrWhiteSpace(sender))
- dict["sender"] = sender;
- return new FormUrlEncodedContent(dict);
- }
-
- private async Task EnsureKavenegarSuccessAsync(
- HttpResponseMessage response,
- string operation,
- CancellationToken cancellationToken)
- {
- if (!response.IsSuccessStatusCode)
- {
- var errorCode = (int)response.StatusCode;
- var detail = KavenegarHttpError(errorCode);
- _logger.LogWarning("Kavenegar {Op} HTTP {Code}: {Detail}", operation, errorCode, detail);
- throw new InvalidOperationException($"Kavenegar {operation} failed (HTTP {errorCode}): {detail}");
- }
-
- var body = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken);
- if (body?.Return?.Status is not 200)
- {
- var status = body?.Return?.Status ?? -1;
- _logger.LogWarning("Kavenegar {Op} returned status {Status}: {Message}", operation, status, body?.Return?.Message);
- throw new InvalidOperationException($"Kavenegar {operation} failed (status {status}): {body?.Return?.Message}");
- }
- }
-
// Strip leading 0 from Iranian mobile numbers (09xxxxxxxxx → 9xxxxxxxxx)
private static string NormalizePhone(string phone)
{
@@ -200,35 +184,6 @@ public class KavenegarSmsService : ISmsService
return p;
}
- private static string KavenegarHttpError(int code) => code switch
- {
- 400 => "Missing or invalid parameters",
- 401 => "Account is inactive",
- 403 => "Invalid API key",
- 404 => "Method not found",
- 405 => "Wrong HTTP method",
- 406 => "Recipient is on the blacklist or number is deactivated",
- 411 => "Invalid recipient number",
- 412 => "Invalid sender number",
- 413 => "Message empty or too long",
- 414 => "Too many recipients",
- 415 => "Server error on Kavenegar side",
- 416 => "Recipient is invalid, blacklisted, or deactivated",
- 417 => "Invalid scheduled date",
- 418 => "Insufficient credit",
- 419 => "OTP token already used or expired",
- 420 => "IP not allowed",
- 421 => "Message could not be sent",
- 422 => "Invalid characters in message",
- 423 => "Kavenegar server unreachable",
- 424 => "OTP template not found — check template name in Kavenegar panel",
- 426 => "IP is not whitelisted",
- 428 => "Voice call requires numeric token",
- 431 => "SMS sending is disabled on this account",
- 432 => "Code parameter missing in OTP template",
- _ => $"Undocumented Kavenegar error {code}"
- };
-
private async Task<(string? ApiKey, string Sender, string OtpTemplate)> GetConfigAsync(CancellationToken ct)
{
var enabled = await _platform.GetAsync(DbKeyEnabled, ct);
@@ -250,42 +205,4 @@ public class KavenegarSmsService : ISmsService
return (apiKey, sender, template);
}
-
- // ── Response models ───────────────────────────────────────────────────────
-
- private sealed class KavenegarReturnEnvelope
- {
- [JsonPropertyName("return")]
- public KavenegarReturn? Return { get; set; }
- }
-
- private sealed class KavenegarReturn
- {
- [JsonPropertyName("status")]
- public int Status { get; set; }
-
- [JsonPropertyName("message")]
- public string? Message { get; set; }
- }
-
- private sealed class KavenegarAccountInfoResponse
- {
- [JsonPropertyName("return")]
- public KavenegarReturn? Return { get; set; }
-
- [JsonPropertyName("entries")]
- public KavenegarAccountEntries? Entries { get; set; }
- }
-
- private sealed class KavenegarAccountEntries
- {
- [JsonPropertyName("remaincredit")]
- public long RemainCredit { get; set; }
-
- [JsonPropertyName("expiredate")]
- public long ExpireDate { get; set; }
-
- [JsonPropertyName("type")]
- public string? Type { get; set; }
- }
}
diff --git a/src/Meezi.Infrastructure/Meezi.Infrastructure.csproj b/src/Meezi.Infrastructure/Meezi.Infrastructure.csproj
index 98893f2..fab6c5c 100644
--- a/src/Meezi.Infrastructure/Meezi.Infrastructure.csproj
+++ b/src/Meezi.Infrastructure/Meezi.Infrastructure.csproj
@@ -14,6 +14,7 @@
all
+