using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using LittleShop.Services; using LittleShop.DTOs; namespace LittleShop.Areas.Admin.Controllers; [Area("Admin")] [Authorize(AuthenticationSchemes = "Cookies", Roles = "Admin")] public class CustomersController : Controller { private readonly ICustomerService _customerService; private readonly IOrderService _orderService; private readonly ILogger _logger; public CustomersController( ICustomerService customerService, IOrderService orderService, ILogger logger) { _customerService = customerService; _orderService = orderService; _logger = logger; } public async Task Index(string searchTerm = "") { try { IEnumerable customers; if (!string.IsNullOrWhiteSpace(searchTerm)) { customers = await _customerService.SearchCustomersAsync(searchTerm); ViewData["SearchTerm"] = searchTerm; } else { customers = await _customerService.GetAllCustomersAsync(); } return View(customers.OrderByDescending(c => c.CreatedAt)); } catch (Exception ex) { _logger.LogError(ex, "Error loading customers index"); TempData["ErrorMessage"] = "Error loading customers"; return View(new List()); } } public async Task Details(Guid id) { try { var customer = await _customerService.GetCustomerByIdAsync(id); if (customer == null) { TempData["ErrorMessage"] = "Customer not found"; return RedirectToAction(nameof(Index)); } // Get customer's order history var allOrders = await _orderService.GetAllOrdersAsync(); var customerOrders = allOrders .Where(o => o.CustomerId == id) .OrderByDescending(o => o.CreatedAt) .ToList(); ViewData["CustomerOrders"] = customerOrders; return View(customer); } catch (Exception ex) { _logger.LogError(ex, "Error loading customer {CustomerId}", id); TempData["ErrorMessage"] = "Error loading customer details"; return RedirectToAction(nameof(Index)); } } [HttpPost] [ValidateAntiForgeryToken] public async Task Block(Guid id, string reason) { try { if (string.IsNullOrWhiteSpace(reason)) { TempData["ErrorMessage"] = "Block reason is required"; return RedirectToAction(nameof(Details), new { id }); } var success = await _customerService.BlockCustomerAsync(id, reason); if (success) { _logger.LogWarning("Customer {CustomerId} blocked by admin. Reason: {Reason}", id, reason); TempData["SuccessMessage"] = "Customer blocked successfully"; } else { TempData["ErrorMessage"] = "Failed to block customer"; } } catch (Exception ex) { _logger.LogError(ex, "Error blocking customer {CustomerId}", id); TempData["ErrorMessage"] = "Error blocking customer"; } return RedirectToAction(nameof(Details), new { id }); } [HttpPost] [ValidateAntiForgeryToken] public async Task Unblock(Guid id) { try { var success = await _customerService.UnblockCustomerAsync(id); if (success) { _logger.LogInformation("Customer {CustomerId} unblocked by admin", id); TempData["SuccessMessage"] = "Customer unblocked successfully"; } else { TempData["ErrorMessage"] = "Failed to unblock customer"; } } catch (Exception ex) { _logger.LogError(ex, "Error unblocking customer {CustomerId}", id); TempData["ErrorMessage"] = "Error unblocking customer"; } return RedirectToAction(nameof(Details), new { id }); } [HttpPost] [ValidateAntiForgeryToken] public async Task RefreshRiskScore(Guid id) { try { await _customerService.UpdateCustomerMetricsAsync(id); TempData["SuccessMessage"] = "Risk score recalculated successfully"; } catch (Exception ex) { _logger.LogError(ex, "Error refreshing risk score for customer {CustomerId}", id); TempData["ErrorMessage"] = "Error refreshing risk score"; } return RedirectToAction(nameof(Details), new { id }); } [HttpPost] [ValidateAntiForgeryToken] public async Task Delete(Guid id) { try { var success = await _customerService.DeleteCustomerAsync(id); if (success) { _logger.LogWarning("Customer {CustomerId} deleted by admin", id); TempData["SuccessMessage"] = "Customer deleted successfully (soft delete - data retained)"; return RedirectToAction(nameof(Index)); } else { TempData["ErrorMessage"] = "Failed to delete customer"; } } catch (Exception ex) { _logger.LogError(ex, "Error deleting customer {CustomerId}", id); TempData["ErrorMessage"] = "Error deleting customer"; } return RedirectToAction(nameof(Details), new { id }); } /// /// Export customer data as JSON (GDPR "Right to Data Portability") /// [HttpGet] public async Task ExportJson(Guid id) { try { var exportData = await _customerService.GetCustomerDataForExportAsync(id); if (exportData == null) { return NotFound(); } var fileName = $"customer-data-{exportData.TelegramUsername ?? exportData.TelegramUserId.ToString()}-{DateTime.UtcNow:yyyyMMdd-HHmmss}.json"; var jsonOptions = new System.Text.Json.JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase }; var jsonData = System.Text.Json.JsonSerializer.Serialize(exportData, jsonOptions); _logger.LogInformation("Customer {CustomerId} data exported as JSON by admin", id); return File(System.Text.Encoding.UTF8.GetBytes(jsonData), "application/json", fileName); } catch (Exception ex) { _logger.LogError(ex, "Error exporting customer {CustomerId} data as JSON", id); TempData["ErrorMessage"] = "Error generating JSON export"; return RedirectToAction(nameof(Details), new { id }); } } /// /// Export customer data as CSV (GDPR "Right to Data Portability") /// [HttpGet] public async Task ExportCsv(Guid id) { try { var exportData = await _customerService.GetCustomerDataForExportAsync(id); if (exportData == null) { return NotFound(); } var fileName = $"customer-data-{exportData.TelegramUsername ?? exportData.TelegramUserId.ToString()}-{DateTime.UtcNow:yyyyMMdd-HHmmss}.csv"; var csvData = GenerateCsvExport(exportData); _logger.LogInformation("Customer {CustomerId} data exported as CSV by admin", id); return File(System.Text.Encoding.UTF8.GetBytes(csvData), "text/csv", fileName); } catch (Exception ex) { _logger.LogError(ex, "Error exporting customer {CustomerId} data as CSV", id); TempData["ErrorMessage"] = "Error generating CSV export"; return RedirectToAction(nameof(Details), new { id }); } } /// /// Helper method to generate CSV export from customer data /// private string GenerateCsvExport(LittleShop.DTOs.CustomerDataExportDto exportData) { var csv = new System.Text.StringBuilder(); // Customer Profile Section csv.AppendLine("CUSTOMER PROFILE"); csv.AppendLine("Field,Value"); csv.AppendLine($"Customer ID,{exportData.CustomerId}"); csv.AppendLine($"Telegram User ID,{exportData.TelegramUserId}"); csv.AppendLine($"Telegram Username,\"{EscapeCsv(exportData.TelegramUsername)}\""); csv.AppendLine($"Telegram Display Name,\"{EscapeCsv(exportData.TelegramDisplayName)}\""); csv.AppendLine($"First Name,\"{EscapeCsv(exportData.TelegramFirstName)}\""); csv.AppendLine($"Last Name,\"{EscapeCsv(exportData.TelegramLastName)}\""); csv.AppendLine($"Email,\"{EscapeCsv(exportData.Email)}\""); csv.AppendLine($"Phone,\"{EscapeCsv(exportData.PhoneNumber)}\""); csv.AppendLine($"Allow Marketing,{exportData.AllowMarketing}"); csv.AppendLine($"Allow Order Updates,{exportData.AllowOrderUpdates}"); csv.AppendLine($"Language,{exportData.Language}"); csv.AppendLine($"Timezone,{exportData.Timezone}"); csv.AppendLine($"Total Orders,{exportData.TotalOrders}"); csv.AppendLine($"Total Spent,{exportData.TotalSpent:F2}"); csv.AppendLine($"Average Order Value,{exportData.AverageOrderValue:F2}"); csv.AppendLine($"Is Blocked,{exportData.IsBlocked}"); csv.AppendLine($"Block Reason,\"{EscapeCsv(exportData.BlockReason)}\""); csv.AppendLine($"Risk Score,{exportData.RiskScore}"); csv.AppendLine($"Created At,{exportData.CreatedAt:yyyy-MM-dd HH:mm:ss}"); csv.AppendLine($"Updated At,{exportData.UpdatedAt:yyyy-MM-dd HH:mm:ss}"); csv.AppendLine(); // Orders Section csv.AppendLine("ORDERS"); csv.AppendLine("Order ID,Status,Total Amount,Currency,Order Date,Shipping Name,Shipping Address,City,Post Code,Country,Tracking Number"); foreach (var order in exportData.Orders) { csv.AppendLine($"{order.OrderId},{order.Status},{order.TotalAmount:F2},{order.Currency},{order.OrderDate:yyyy-MM-dd HH:mm:ss}," + $"\"{EscapeCsv(order.ShippingName)}\",\"{EscapeCsv(order.ShippingAddress)}\",\"{EscapeCsv(order.ShippingCity)}\"," + $"\"{order.ShippingPostCode}\",\"{order.ShippingCountry}\",\"{EscapeCsv(order.TrackingNumber)}\""); // Order Items sub-section if (order.Items.Any()) { csv.AppendLine(" Order Items:"); csv.AppendLine(" Product Name,Variant,Quantity,Unit Price,Total Price"); foreach (var item in order.Items) { csv.AppendLine($" \"{EscapeCsv(item.ProductName)}\",\"{EscapeCsv(item.VariantName)}\"," + $"{item.Quantity},{item.UnitPrice:F2},{item.TotalPrice:F2}"); } } } csv.AppendLine(); // Messages Section if (exportData.Messages.Any()) { csv.AppendLine("MESSAGES"); csv.AppendLine("Sent At,Type,Content,Was Read,Read At"); foreach (var message in exportData.Messages) { csv.AppendLine($"{message.SentAt:yyyy-MM-dd HH:mm:ss},{message.MessageType}," + $"\"{EscapeCsv(message.Content)}\",{message.WasRead}," + $"{(message.ReadAt.HasValue ? message.ReadAt.Value.ToString("yyyy-MM-dd HH:mm:ss") : "")}"); } csv.AppendLine(); } // Reviews Section if (exportData.Reviews.Any()) { csv.AppendLine("REVIEWS"); csv.AppendLine("Product Name,Rating,Comment,Created At,Is Approved,Is Verified Purchase"); foreach (var review in exportData.Reviews) { csv.AppendLine($"\"{EscapeCsv(review.ProductName)}\",{review.Rating}," + $"\"{EscapeCsv(review.Comment)}\",{review.CreatedAt:yyyy-MM-dd HH:mm:ss}," + $"{review.IsApproved},{review.IsVerifiedPurchase}"); } } return csv.ToString(); } /// /// Escape CSV values containing quotes, commas, or newlines /// private string EscapeCsv(string? value) { if (string.IsNullOrEmpty(value)) return string.Empty; return value.Replace("\"", "\"\""); // Escape quotes by doubling them } }