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 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; public SmsMarketingService( AppDbContext db, ISmsService smsService, IConnectionMultiplexer redis) { _db = db; _smsService = smsService; _redis = redis; } public async Task 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 result = await _smsService.SendBulkAsync(phones, request.Message, cancellationToken); if (result.SentCount > 0) await IncrementUsageAsync(cafeId, month, result.SentCount); return (true, new SmsCampaignResult(result.SentCount, result.FailedCount), null, null); } private async Task> 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 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}"; }