Fully implement Kavenegar SMS support
Core changes: - ISmsService: add SendBulkAsync (batches of 200) + GetAccountInfoAsync - KavenegarSmsService: POST requests, sender number config, bulk send via comma-separated receptors, account balance, full error code mapping (HTTP 400-432), enabled-flag check before any send - SmsMarketingService: replaced per-recipient loop with SendBulkAsync - SmsController: new GET /sms/balance endpoint returns Kavenegar credit - SmsDtos: SmsBalanceDto - IntegrationDtos + PlatformIntegrationService: SenderNumber field - appsettings.json + docker-compose: Kavenegar__SenderNumber = 90005671 Dashboard: - sms-screen: char counter, SMS parts indicator (Persian 70/67 chars, Latin 160/153), account balance card, sender line display, result banner Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,16 +11,32 @@ namespace Meezi.API.Controllers;
|
||||
public class SmsController : CafeApiControllerBase
|
||||
{
|
||||
private readonly ISmsMarketingService _smsMarketingService;
|
||||
private readonly ISmsService _smsService;
|
||||
private readonly IValidator<SendSmsCampaignRequest> _campaignValidator;
|
||||
|
||||
public SmsController(
|
||||
ISmsMarketingService smsMarketingService,
|
||||
ISmsService smsService,
|
||||
IValidator<SendSmsCampaignRequest> campaignValidator)
|
||||
{
|
||||
_smsMarketingService = smsMarketingService;
|
||||
_smsService = smsService;
|
||||
_campaignValidator = campaignValidator;
|
||||
}
|
||||
|
||||
[HttpGet("balance")]
|
||||
public async Task<IActionResult> GetBalance(string cafeId, ITenantContext tenant, CancellationToken cancellationToken)
|
||||
{
|
||||
if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied;
|
||||
|
||||
var info = await _smsService.GetAccountInfoAsync(cancellationToken);
|
||||
var dto = info is not null
|
||||
? new SmsBalanceDto(info.RemainCredit, info.AccountType, true)
|
||||
: new SmsBalanceDto(0, "master", false);
|
||||
|
||||
return Ok(new ApiResponse<SmsBalanceDto>(true, dto));
|
||||
}
|
||||
|
||||
[HttpGet("usage")]
|
||||
public async Task<IActionResult> GetUsage(string cafeId, ITenantContext tenant, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
@@ -10,3 +10,6 @@ public record SendSmsCampaignRequest(
|
||||
public record SmsCampaignResult(int SentCount, int FailedCount);
|
||||
|
||||
public record SmsUsageDto(int UsedThisMonth, int MonthlyLimit, string Month);
|
||||
|
||||
/// <summary>Kavenegar account credit balance returned to the dashboard.</summary>
|
||||
public record SmsBalanceDto(long RemainCredit, string AccountType, bool IsConfigured);
|
||||
|
||||
@@ -24,18 +24,15 @@ public class SmsMarketingService : ISmsMarketingService
|
||||
private readonly AppDbContext _db;
|
||||
private readonly ISmsService _smsService;
|
||||
private readonly IConnectionMultiplexer _redis;
|
||||
private readonly ILogger<SmsMarketingService> _logger;
|
||||
|
||||
public SmsMarketingService(
|
||||
AppDbContext db,
|
||||
ISmsService smsService,
|
||||
IConnectionMultiplexer redis,
|
||||
ILogger<SmsMarketingService> logger)
|
||||
IConnectionMultiplexer redis)
|
||||
{
|
||||
_db = db;
|
||||
_smsService = smsService;
|
||||
_redis = redis;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<SmsUsageDto> GetUsageAsync(
|
||||
@@ -68,27 +65,12 @@ public class SmsMarketingService : ISmsMarketingService
|
||||
if (maxSms != int.MaxValue && used + phones.Count > maxSms)
|
||||
return (false, null, "PLAN_LIMIT_REACHED", "Monthly SMS limit would be exceeded.");
|
||||
|
||||
var sent = 0;
|
||||
var failed = 0;
|
||||
var result = await _smsService.SendBulkAsync(phones, request.Message, cancellationToken);
|
||||
|
||||
foreach (var phone in phones)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _smsService.SendMessageAsync(phone, request.Message, cancellationToken);
|
||||
sent++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to send SMS to recipient");
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
if (result.SentCount > 0)
|
||||
await IncrementUsageAsync(cafeId, month, result.SentCount);
|
||||
|
||||
if (sent > 0)
|
||||
await IncrementUsageAsync(cafeId, month, sent);
|
||||
|
||||
return (true, new SmsCampaignResult(sent, failed), null, null);
|
||||
return (true, new SmsCampaignResult(result.SentCount, result.FailedCount), null, null);
|
||||
}
|
||||
|
||||
private async Task<List<string>> ResolvePhonesAsync(
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
},
|
||||
"Kavenegar": {
|
||||
"ApiKey": "",
|
||||
"SenderNumber": "90005671",
|
||||
"OtpTemplate": "verify"
|
||||
},
|
||||
"ZarinPal": {
|
||||
|
||||
Reference in New Issue
Block a user