Files
meezi/src/Meezi.API/Services/SmsMarketingService.cs
T

132 lines
4.4 KiB
C#
Raw Normal View History

2026-05-27 21:33:48 +03:30
using Meezi.API.Models.Crm;
using Meezi.Core.Constants;
using Meezi.Core.Enums;
using Meezi.Core.Interfaces;
using Meezi.Core.Utilities;
using Meezi.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using StackExchange.Redis;
namespace Meezi.API.Services;
public interface ISmsMarketingService
{
Task<SmsUsageDto> GetUsageAsync(string cafeId, PlanTier planTier, CancellationToken cancellationToken = default);
Task<(bool Success, SmsCampaignResult? Data, string? ErrorCode, string? Message)> SendCampaignAsync(
string cafeId,
PlanTier planTier,
SendSmsCampaignRequest request,
CancellationToken cancellationToken = default);
}
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)
{
_db = db;
_smsService = smsService;
_redis = redis;
_logger = logger;
}
public async Task<SmsUsageDto> GetUsageAsync(
string cafeId,
PlanTier planTier,
CancellationToken cancellationToken = default)
{
var month = DateTime.UtcNow.ToString("yyyy-MM");
var used = await GetUsedCountAsync(cafeId, month);
var limit = PlanLimits.MaxSmsPerMonth(planTier);
return new SmsUsageDto(used, limit == int.MaxValue ? -1 : limit, month);
}
public async Task<(bool Success, SmsCampaignResult? Data, string? ErrorCode, string? Message)> SendCampaignAsync(
string cafeId,
PlanTier planTier,
SendSmsCampaignRequest request,
CancellationToken cancellationToken = default)
{
var maxSms = PlanLimits.MaxSmsPerMonth(planTier);
if (maxSms == 0)
return (false, null, "PLAN_LIMIT_REACHED", "SMS is not available on the Free plan.");
var phones = await ResolvePhonesAsync(cafeId, request, cancellationToken);
if (phones.Count == 0)
return (false, null, "NOT_FOUND", "No recipients found.");
var month = DateTime.UtcNow.ToString("yyyy-MM");
var used = await GetUsedCountAsync(cafeId, month);
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;
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 (sent > 0)
await IncrementUsageAsync(cafeId, month, sent);
return (true, new SmsCampaignResult(sent, failed), null, null);
}
private async Task<List<string>> ResolvePhonesAsync(
string cafeId,
SendSmsCampaignRequest request,
CancellationToken cancellationToken)
{
if (request.Phones is { Count: > 0 })
{
return request.Phones
.Select(PhoneNormalizer.Normalize)
.Where(PhoneNormalizer.IsValidIranMobile)
.Distinct()
.ToList();
}
var query = _db.Customers.Where(c => c.CafeId == cafeId);
if (request.TargetGroup.HasValue)
query = query.Where(c => c.Group == request.TargetGroup.Value);
return await query.Select(c => c.Phone).Distinct().ToListAsync(cancellationToken);
}
private async Task<int> GetUsedCountAsync(string cafeId, string month)
{
var redis = _redis.GetDatabase();
var value = await redis.StringGetAsync(UsageKey(cafeId, month));
return value.HasValue ? (int)value : 0;
}
private async Task IncrementUsageAsync(string cafeId, string month, int count)
{
var redis = _redis.GetDatabase();
var key = UsageKey(cafeId, month);
await redis.StringIncrementAsync(key, count);
await redis.KeyExpireAsync(key, TimeSpan.FromDays(40));
}
private static string UsageKey(string cafeId, string month) => $"sms:usage:{cafeId}:{month}";
}