using MongoDB.Driver; using MongoDB.Bson; using System.Text.Json; using System.Net.Http; using OnlyOneAccessTemplate.Models; namespace OnlyOneAccessTemplate.Services { public class ConversionService : IConversionService { private readonly IMongoDatabase _database; private readonly IMongoCollection _conversions; private readonly IMongoCollection _events; private readonly ISiteConfigurationService _siteConfig; private readonly HttpClient _httpClient; private readonly ILogger _logger; public ConversionService( IMongoDatabase database, ISiteConfigurationService siteConfig, HttpClient httpClient, ILogger logger) { _database = database; _siteConfig = siteConfig; _httpClient = httpClient; _logger = logger; _conversions = _database.GetCollection("conversions"); _events = _database.GetCollection("conversion_events"); } public async Task ProcessConversionAsync(ConversionData data) { try { var configuration = await _siteConfig.GetConfigurationAsync(data.Language); // Criar registro de conversão var conversionRecord = new ConversionRecord { Id = ObjectId.GenerateNewId().ToString(), Language = data.Language, PageName = data.PageName, FormData = data.FormData, UserAgent = data.UserAgent, IpAddress = data.IpAddress, Referrer = data.Referrer, ConvertedAt = DateTime.UtcNow, SessionId = GenerateSessionId(), Source = ExtractSource(data.Referrer), Medium = ExtractMedium(data.Referrer), Campaign = ExtractCampaign(data.Referrer) }; await _conversions.InsertOneAsync(conversionRecord); // Log evento de conversão await LogConversionEventAsync("conversion_completed", new Dictionary { { "conversion_id", conversionRecord.Id }, { "page_name", data.PageName }, { "language", data.Language }, { "source", conversionRecord.Source }, { "medium", conversionRecord.Medium } }); // Enviar para webhook se configurado //if (!string.IsNullOrEmpty(configuration..Conversion.WebhookUrl)) //{ // _ = Task.Run(() => SendWebhookAsync(configuration.Conversion.WebhookUrl, conversionRecord)); //} //// Disparar pixel de conversão se configurado //if (!string.IsNullOrEmpty(configuration.Conversion.ConversionPixel)) //{ // _ = Task.Run(() => FireConversionPixelAsync(configuration.Conversion.ConversionPixel, conversionRecord)); //} _logger.LogInformation("Conversão processada com sucesso: {ConversionId}", conversionRecord.Id); return true; } catch (Exception ex) { _logger.LogError(ex, "Erro ao processar conversão para {Language}/{PageName}", data.Language, data.PageName); return false; } } public async Task GetConversionStatsAsync(string language, DateTime from, DateTime to) { try { var filter = Builders.Filter.And( Builders.Filter.Eq(x => x.Language, language), Builders.Filter.Gte(x => x.ConvertedAt, from), Builders.Filter.Lte(x => x.ConvertedAt, to) ); // Total de conversões var totalConversions = await _conversions.CountDocumentsAsync(filter); // Conversões por fonte var conversionsBySource = await _conversions .Aggregate() .Match(filter) .Group(x => x.Source, g => new { Source = g.Key, Count = g.Count() }) .ToListAsync(); var sourceDict = conversionsBySource.ToDictionary(x => x.Source ?? "direct", x => x.Count); // Para calcular taxa de conversão, precisaríamos de dados de visitantes // Por enquanto, vamos usar uma estimativa baseada em eventos var visitorFilter = Builders.Filter.And( Builders.Filter.Eq("Properties.language", language), Builders.Filter.Eq(x => x.EventName, "page_view"), Builders.Filter.Gte(x => x.Timestamp, from), Builders.Filter.Lte(x => x.Timestamp, to) ); var totalVisits = await _events.CountDocumentsAsync(visitorFilter); var conversionRate = totalVisits > 0 ? (double)totalConversions / totalVisits * 100 : 0; return new ConversionStats( TotalVisits: (int)totalVisits, TotalConversions: (int)totalConversions, ConversionRate: Math.Round(conversionRate, 2), ConversionsBySource: sourceDict ); } catch (Exception ex) { _logger.LogError(ex, "Erro ao buscar estatísticas de conversão para {Language}", language); return new ConversionStats(0, 0, 0, new Dictionary()); } } public async Task LogConversionEventAsync(string eventName, Dictionary properties) { try { var conversionEvent = new ConversionEvent { Id = ObjectId.GenerateNewId().ToString(), EventName = eventName, Properties = properties, Timestamp = DateTime.UtcNow }; await _events.InsertOneAsync(conversionEvent); } catch (Exception ex) { _logger.LogError(ex, "Erro ao registrar evento {EventName}", eventName); } } public async Task GetConversionByIdAsync(string conversionId) { try { var filter = Builders.Filter.Eq(x => x.Id, conversionId); return await _conversions.Find(filter).FirstOrDefaultAsync(); } catch (Exception ex) { _logger.LogError(ex, "Erro ao buscar conversão {ConversionId}", conversionId); return null; } } public async Task> GetConversionsAsync(string language, int skip = 0, int take = 50) { try { var filter = Builders.Filter.Eq(x => x.Language, language); return await _conversions .Find(filter) .SortByDescending(x => x.ConvertedAt) .Skip(skip) .Limit(take) .ToListAsync(); } catch (Exception ex) { _logger.LogError(ex, "Erro ao buscar conversões para idioma {Language}", language); return new List(); } } public async Task ValidateFormDataAsync(Dictionary formData, List formFields) { try { foreach (var field in formFields.Where(f => f.Required)) { if (!formData.ContainsKey(field.Name) || string.IsNullOrWhiteSpace(formData[field.Name])) { return false; } // Validação de email if (field.Type == "email" && !IsValidEmail(formData[field.Name])) { return false; } // Validação de regex personalizada if (!string.IsNullOrEmpty(field.ValidationRegex) && !System.Text.RegularExpressions.Regex.IsMatch(formData[field.Name], field.ValidationRegex)) { return false; } } return true; } catch (Exception ex) { _logger.LogError(ex, "Erro ao validar dados do formulário"); return false; } } public async Task GetRealTimeStatsAsync(string language) { var now = DateTime.UtcNow; var last24Hours = now.AddHours(-24); return await GetConversionStatsAsync(language, last24Hours, now); } public async Task> GetConversionMetricsAsync(string language, string period) { try { var (from, to) = GetDateRangeFromPeriod(period); var stats = await GetConversionStatsAsync(language, from, to); var metrics = new Dictionary { { "total_conversions", stats.TotalConversions }, { "total_visits", stats.TotalVisits }, { "conversion_rate", stats.ConversionRate }, { "period", period }, { "from_date", from }, { "to_date", to }, { "conversions_by_source", stats.ConversionsBySource } }; return metrics; } catch (Exception ex) { _logger.LogError(ex, "Erro ao buscar métricas para {Language}/{Period}", language, period); return new Dictionary(); } } public async Task SendConversionNotificationAsync(ConversionRecord conversion) { try { // Este método pode ser expandido para enviar notificações por email, Slack, etc. await LogConversionEventAsync("conversion_notification_sent", new Dictionary { { "conversion_id", conversion.Id }, { "language", conversion.Language }, { "page_name", conversion.PageName } }); return true; } catch (Exception ex) { _logger.LogError(ex, "Erro ao enviar notificação de conversão {ConversionId}", conversion.Id); return false; } } public async Task> GetConversionTrendsAsync(string language, DateTime from, DateTime to, string groupBy = "day") { try { var filter = Builders.Filter.And( Builders.Filter.Eq(x => x.Language, language), Builders.Filter.Gte(x => x.ConvertedAt, from), Builders.Filter.Lte(x => x.ConvertedAt, to) ); var conversions = await _conversions.Find(filter).ToListAsync(); var trends = new List(); var currentDate = from.Date; while (currentDate <= to.Date) { var dayConversions = conversions.Where(c => c.ConvertedAt.Date == currentDate).Count(); // Para visitas, você pode implementar tracking separado ou usar uma estimativa var dayVisits = dayConversions > 0 ? dayConversions * 10 : 0; // Estimativa var conversionRate = dayVisits > 0 ? (double)dayConversions / dayVisits * 100 : 0; trends.Add(new ConversionTrend( Date: currentDate, Conversions: dayConversions, Visits: dayVisits, ConversionRate: Math.Round(conversionRate, 2) )); currentDate = groupBy switch { "hour" => currentDate.AddHours(1), "week" => currentDate.AddDays(7), "month" => currentDate.AddMonths(1), _ => currentDate.AddDays(1) }; } return trends; } catch (Exception ex) { _logger.LogError(ex, "Erro ao buscar tendências de conversão para {Language}", language); return new List(); } } private async Task SendWebhookAsync(string webhookUrl, ConversionRecord conversion) { try { var payload = new { conversion_id = conversion.Id, language = conversion.Language, page_name = conversion.PageName, form_data = conversion.FormData, converted_at = conversion.ConvertedAt, source = conversion.Source, medium = conversion.Medium, campaign = conversion.Campaign }; var jsonContent = JsonSerializer.Serialize(payload); var content = new StringContent(jsonContent, System.Text.Encoding.UTF8, "application/json"); var response = await _httpClient.PostAsync(webhookUrl, content); if (response.IsSuccessStatusCode) { _logger.LogInformation("Webhook enviado com sucesso para {WebhookUrl}", webhookUrl); } else { _logger.LogWarning("Falha ao enviar webhook para {WebhookUrl}. Status: {StatusCode}", webhookUrl, response.StatusCode); } } catch (Exception ex) { _logger.LogError(ex, "Erro ao enviar webhook para {WebhookUrl}", webhookUrl); } } private async Task FireConversionPixelAsync(string pixelUrl, ConversionRecord conversion) { try { // Substituir placeholders no pixel URL var processedUrl = pixelUrl .Replace("{conversion_id}", conversion.Id) .Replace("{value}", "1") .Replace("{currency}", "BRL"); var response = await _httpClient.GetAsync(processedUrl); if (response.IsSuccessStatusCode) { _logger.LogInformation("Pixel de conversão disparado com sucesso"); } else { _logger.LogWarning("Falha ao disparar pixel de conversão. Status: {StatusCode}", response.StatusCode); } } catch (Exception ex) { _logger.LogError(ex, "Erro ao disparar pixel de conversão"); } } private string GenerateSessionId() { return Guid.NewGuid().ToString("N")[..16]; } private string ExtractSource(string? referrer) { if (string.IsNullOrEmpty(referrer)) return "direct"; try { var uri = new Uri(referrer); var host = uri.Host.ToLowerInvariant(); return host switch { var h when h.Contains("google") => "google", var h when h.Contains("facebook") => "facebook", var h when h.Contains("instagram") => "instagram", var h when h.Contains("youtube") => "youtube", var h when h.Contains("linkedin") => "linkedin", var h when h.Contains("twitter") => "twitter", var h when h.Contains("tiktok") => "tiktok", _ => host }; } catch { return "unknown"; } } private string ExtractMedium(string? referrer) { if (string.IsNullOrEmpty(referrer)) return "direct"; try { var uri = new Uri(referrer); var query = uri.Query; if (query.Contains("utm_medium=")) { var match = System.Text.RegularExpressions.Regex.Match(query, @"utm_medium=([^&]+)"); if (match.Success) return Uri.UnescapeDataString(match.Groups[1].Value); } var host = uri.Host.ToLowerInvariant(); return host switch { var h when h.Contains("google") => "search", var h when h.Contains("facebook") || h.Contains("instagram") => "social", var h when h.Contains("youtube") || h.Contains("tiktok") => "video", var h when h.Contains("linkedin") || h.Contains("twitter") => "social", _ => "referral" }; } catch { return "unknown"; } } private string ExtractCampaign(string? referrer) { if (string.IsNullOrEmpty(referrer)) return "direct"; try { var uri = new Uri(referrer); var query = uri.Query; if (query.Contains("utm_campaign=")) { var match = System.Text.RegularExpressions.Regex.Match(query, @"utm_campaign=([^&]+)"); if (match.Success) return Uri.UnescapeDataString(match.Groups[1].Value); } return "organic"; } catch { return "unknown"; } } private bool IsValidEmail(string email) { try { var addr = new System.Net.Mail.MailAddress(email); return addr.Address == email; } catch { return false; } } private (DateTime from, DateTime to) GetDateRangeFromPeriod(string period) { var now = DateTime.UtcNow; return period.ToLowerInvariant() switch { "today" => (now.Date, now), "yesterday" => (now.Date.AddDays(-1), now.Date.AddSeconds(-1)), "last7days" => (now.AddDays(-7), now), "last30days" => (now.AddDays(-30), now), "thismonth" => (new DateTime(now.Year, now.Month, 1), now), "lastmonth" => (new DateTime(now.Year, now.Month, 1).AddMonths(-1), new DateTime(now.Year, now.Month, 1).AddSeconds(-1)), _ => (now.AddDays(-7), now) // default to last 7 days }; } } // Modelos para MongoDB public class ConversionRecord { public string Id { get; set; } = string.Empty; public string Language { get; set; } = string.Empty; public string PageName { get; set; } = string.Empty; public Dictionary FormData { get; set; } = new(); public string UserAgent { get; set; } = string.Empty; public string IpAddress { get; set; } = string.Empty; public string? Referrer { get; set; } public DateTime ConvertedAt { get; set; } public string SessionId { get; set; } = string.Empty; public string Source { get; set; } = "direct"; public string Medium { get; set; } = "direct"; public string Campaign { get; set; } = "direct"; } public class ConversionEvent { public string Id { get; set; } = string.Empty; public string EventName { get; set; } = string.Empty; public Dictionary Properties { get; set; } = new(); public DateTime Timestamp { get; set; } } }