537 lines
20 KiB
C#
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; }
|
|
}
|
|
}
|