OneConversorTemplate/OnlyOneAccessTemplate/Services/ConversionService.cs
Ricardo Carneiro b1d75213ab first commit
2025-05-30 23:48:28 -03:00

537 lines
20 KiB
C#

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<ConversionRecord> _conversions;
private readonly IMongoCollection<ConversionEvent> _events;
private readonly ISiteConfigurationService _siteConfig;
private readonly HttpClient _httpClient;
private readonly ILogger<ConversionService> _logger;
public ConversionService(
IMongoDatabase database,
ISiteConfigurationService siteConfig,
HttpClient httpClient,
ILogger<ConversionService> logger)
{
_database = database;
_siteConfig = siteConfig;
_httpClient = httpClient;
_logger = logger;
_conversions = _database.GetCollection<ConversionRecord>("conversions");
_events = _database.GetCollection<ConversionEvent>("conversion_events");
}
public async Task<bool> 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<string, object>
{
{ "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<ConversionStats> GetConversionStatsAsync(string language, DateTime from, DateTime to)
{
try
{
var filter = Builders<ConversionRecord>.Filter.And(
Builders<ConversionRecord>.Filter.Eq(x => x.Language, language),
Builders<ConversionRecord>.Filter.Gte(x => x.ConvertedAt, from),
Builders<ConversionRecord>.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<ConversionEvent>.Filter.And(
Builders<ConversionEvent>.Filter.Eq("Properties.language", language),
Builders<ConversionEvent>.Filter.Eq(x => x.EventName, "page_view"),
Builders<ConversionEvent>.Filter.Gte(x => x.Timestamp, from),
Builders<ConversionEvent>.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<string, int>());
}
}
public async Task LogConversionEventAsync(string eventName, Dictionary<string, object> 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<ConversionRecord?> GetConversionByIdAsync(string conversionId)
{
try
{
var filter = Builders<ConversionRecord>.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<List<ConversionRecord>> GetConversionsAsync(string language, int skip = 0, int take = 50)
{
try
{
var filter = Builders<ConversionRecord>.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<ConversionRecord>();
}
}
public async Task<bool> ValidateFormDataAsync(Dictionary<string, string> formData, List<FormField> 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<ConversionStats> GetRealTimeStatsAsync(string language)
{
var now = DateTime.UtcNow;
var last24Hours = now.AddHours(-24);
return await GetConversionStatsAsync(language, last24Hours, now);
}
public async Task<Dictionary<string, object>> GetConversionMetricsAsync(string language, string period)
{
try
{
var (from, to) = GetDateRangeFromPeriod(period);
var stats = await GetConversionStatsAsync(language, from, to);
var metrics = new Dictionary<string, object>
{
{ "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<string, object>();
}
}
public async Task<bool> 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<string, object>
{
{ "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<List<ConversionTrend>> GetConversionTrendsAsync(string language, DateTime from, DateTime to, string groupBy = "day")
{
try
{
var filter = Builders<ConversionRecord>.Filter.And(
Builders<ConversionRecord>.Filter.Eq(x => x.Language, language),
Builders<ConversionRecord>.Filter.Gte(x => x.ConvertedAt, from),
Builders<ConversionRecord>.Filter.Lte(x => x.ConvertedAt, to)
);
var conversions = await _conversions.Find(filter).ToListAsync();
var trends = new List<ConversionTrend>();
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<ConversionTrend>();
}
}
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<string, string> 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<string, object> Properties { get; set; } = new();
public DateTime Timestamp { get; set; }
}
}