using Microsoft.EntityFrameworkCore; using System.Globalization; using System.Text; using LittleShop.Data; using LittleShop.Models; using LittleShop.DTOs; using LittleShop.Enums; namespace LittleShop.Services; public interface IProductImportService { Task ImportFromCsvAsync(Stream csvStream, bool replaceAll = false); Task ImportFromTextAsync(string csvText, bool replaceAll = false); Task ImportFromHumanTextAsync(string textContent, bool replaceAll = false); Task DeleteAllProductsAndCategoriesAsync(); Task DeleteAllOrdersAndSalesDataAsync(); string GenerateTemplateAsCsv(); string GenerateTemplateAsHumanText(); Task ExportProductsAsCsvAsync(); } public class ProductImportService : IProductImportService { private readonly LittleShopContext _context; private readonly IProductService _productService; private readonly ICategoryService _categoryService; private readonly ILogger _logger; public ProductImportService( LittleShopContext context, IProductService productService, ICategoryService categoryService, ILogger logger) { _context = context; _productService = productService; _categoryService = categoryService; _logger = logger; } public async Task ImportFromCsvAsync(Stream csvStream, bool replaceAll = false) { using var reader = new StreamReader(csvStream); var csvText = await reader.ReadToEndAsync(); return await ImportFromTextAsync(csvText, replaceAll); } public async Task ImportFromTextAsync(string csvText, bool replaceAll = false) { var result = new ProductImportResultDto(); // Replace all existing data if requested if (replaceAll) { _logger.LogWarning("REPLACE ALL: Deleting all existing products, variants, and categories"); var deletedCount = await DeleteAllProductsAndCategoriesAsync(); _logger.LogInformation("Deleted {Count} existing records", deletedCount); } var lines = csvText.Split('\n', StringSplitOptions.RemoveEmptyEntries); if (lines.Length == 0) { result.Errors.Add(new ProductImportErrorDto { RowNumber = 0, ProductName = "File", ErrorMessages = { "CSV file is empty" } }); return result; } // Parse header var headers = ParseCsvLine(lines[0]); var expectedHeaders = new[] { "Name", "Description", "Price", "Weight", "WeightUnit", "StockQuantity", "CategoryName", "IsActive", "Variations", "PhotoUrls" }; // Validate headers foreach (var expectedHeader in expectedHeaders.Take(7)) // First 7 are required { if (!headers.Contains(expectedHeader, StringComparer.OrdinalIgnoreCase)) { result.Errors.Add(new ProductImportErrorDto { RowNumber = 0, ProductName = "Header", ErrorMessages = { $"Missing required column: {expectedHeader}" } }); } } if (result.Errors.Any()) return result; result.TotalRows = lines.Length - 1; // Exclude header // Get categories for lookup (will be populated as we create new ones) var categories = await _categoryService.GetAllCategoriesAsync(); var categoryLookup = categories.ToDictionary(c => c.Name, c => c.Id, StringComparer.OrdinalIgnoreCase); // Auto-create categories from CSV if they don't exist var uniqueCategoryNames = new HashSet(StringComparer.OrdinalIgnoreCase); for (int i = 1; i < lines.Length; i++) { var values = ParseCsvLine(lines[i]); if (values.Length >= 7) { var categoryName = GetValue(values, headers, "CategoryName", ""); if (!string.IsNullOrEmpty(categoryName)) { uniqueCategoryNames.Add(categoryName); } } } // Create missing categories foreach (var categoryName in uniqueCategoryNames) { if (!categoryLookup.ContainsKey(categoryName)) { var createCategoryDto = new CreateCategoryDto { Name = categoryName, Description = $"Auto-created category for {categoryName} products" }; var newCategory = await _categoryService.CreateCategoryAsync(createCategoryDto); categoryLookup[categoryName] = newCategory.Id; _logger.LogInformation("Auto-created category: {CategoryName}", categoryName); } } // Process data rows for (int i = 1; i < lines.Length; i++) { try { var values = ParseCsvLine(lines[i]); if (values.Length < 7) // Minimum required columns { result.Errors.Add(new ProductImportErrorDto { RowNumber = i + 1, ProductName = values.Length > 0 ? values[0] : "Unknown", ErrorMessages = { "Insufficient columns in row" } }); result.FailedImports++; continue; } var importDto = new ProductImportDto { Name = GetValue(values, headers, "Name", ""), Description = GetValue(values, headers, "Description", ""), Price = decimal.Parse(GetValue(values, headers, "Price", "0"), CultureInfo.InvariantCulture), Weight = decimal.Parse(GetValue(values, headers, "Weight", "0"), CultureInfo.InvariantCulture), WeightUnit = GetValue(values, headers, "WeightUnit", "Grams"), StockQuantity = int.Parse(GetValue(values, headers, "StockQuantity", "0")), CategoryName = GetValue(values, headers, "CategoryName", ""), IsActive = bool.Parse(GetValue(values, headers, "IsActive", "true")), Variations = GetValue(values, headers, "Variations", null), PhotoUrls = GetValue(values, headers, "PhotoUrls", null) }; await ImportSingleProductAsync(importDto, categoryLookup, result, i + 1); } catch (Exception ex) { result.Errors.Add(new ProductImportErrorDto { RowNumber = i + 1, ProductName = "Parse Error", ErrorMessages = { ex.Message } }); result.FailedImports++; } } _logger.LogInformation("Product import completed: {Success} successful, {Failed} failed", result.SuccessfulImports, result.FailedImports); return result; } private async Task ImportSingleProductAsync(ProductImportDto importDto, Dictionary categoryLookup, ProductImportResultDto result, int rowNumber) { var errors = new List(); // Validate category if (!categoryLookup.TryGetValue(importDto.CategoryName, out var categoryId)) { errors.Add($"Category '{importDto.CategoryName}' not found"); } // Validate weight unit if (!Enum.TryParse(importDto.WeightUnit, true, out var weightUnit)) { errors.Add($"Invalid weight unit: {importDto.WeightUnit}"); } if (errors.Any()) { result.Errors.Add(new ProductImportErrorDto { RowNumber = rowNumber, ProductName = importDto.Name, ErrorMessages = errors }); result.FailedImports++; return; } try { // Create product var createProductDto = new CreateProductDto { Name = importDto.Name, Description = importDto.Description, Price = importDto.Price, Weight = importDto.Weight, WeightUnit = weightUnit, StockQuantity = importDto.StockQuantity, CategoryId = categoryId }; var product = await _productService.CreateProductAsync(createProductDto); result.ImportedProducts.Add(product); // Import variations if provided if (!string.IsNullOrEmpty(importDto.Variations)) { await ImportProductMultiBuysAsync(product.Id, importDto.Variations); } // Import photos if provided if (!string.IsNullOrEmpty(importDto.PhotoUrls)) { await ImportProductPhotosAsync(product.Id, importDto.PhotoUrls); } result.SuccessfulImports++; } catch (Exception ex) { result.Errors.Add(new ProductImportErrorDto { RowNumber = rowNumber, ProductName = importDto.Name, ErrorMessages = { ex.Message } }); result.FailedImports++; } } private async Task ImportProductMultiBuysAsync(Guid productId, string variationsText) { // Format: "Single Item:1:10.00;Twin Pack:2:19.00;Triple Pack:3:25.00" var variations = variationsText.Split(';', StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < variations.Length; i++) { var parts = variations[i].Split(':', StringSplitOptions.RemoveEmptyEntries); if (parts.Length >= 3) { var multiBuyDto = new CreateProductMultiBuyDto { ProductId = productId, Name = parts[0].Trim(), Description = parts.Length > 3 ? parts[3].Trim() : "", Quantity = int.Parse(parts[1].Trim()), Price = decimal.Parse(parts[2].Trim(), CultureInfo.InvariantCulture), SortOrder = i }; await _productService.CreateProductMultiBuyAsync(multiBuyDto); } } } private async Task ImportProductPhotosAsync(Guid productId, string photoUrlsText) { // Format: "url1;url2;url3" var urls = photoUrlsText.Split(';', StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < urls.Length; i++) { var photoDto = new CreateProductPhotoDto { ProductId = productId, PhotoUrl = urls[i].Trim(), DisplayOrder = i + 1 }; await _productService.AddProductPhotoAsync(photoDto); } } public string GenerateTemplateAsCsv() { var sb = new StringBuilder(); // Header sb.AppendLine("Name,Description,Price,Weight,WeightUnit,StockQuantity,CategoryName,IsActive,Variations,PhotoUrls"); // Example rows sb.AppendLine("\"Example Product 1\",\"High-quality example product with great features\",29.99,150,Grams,50,Electronics,true,\"Single Item:1:29.99;Twin Pack:2:55.00;Triple Pack:3:79.99\",\"https://example.com/photo1.jpg;https://example.com/photo2.jpg\""); sb.AppendLine("\"Example Product 2\",\"Another sample product for import testing\",19.99,200,Grams,25,Clothing,true,\"Individual:1:19.99;Pair:2:35.00\",\"\""); sb.AppendLine("\"Simple Product\",\"Basic product without variations\",9.99,100,Grams,100,Books,true,\"\",\"\""); return sb.ToString(); } public async Task ExportProductsAsCsvAsync() { var products = await _productService.GetAllProductsAsync(); var sb = new StringBuilder(); // Header sb.AppendLine("Name,Description,Price,Weight,WeightUnit,StockQuantity,CategoryName,IsActive,Variations,PhotoUrls"); foreach (var product in products) { // Build variations string var variationsText = string.Join(";", product.MultiBuys.OrderBy(v => v.SortOrder) .Select(v => $"{v.Name}:{v.Quantity}:{v.Price:F2}")); // Build photo URLs string var photoUrlsText = string.Join(";", product.Photos.OrderBy(p => p.SortOrder) .Select(p => p.FilePath)); sb.AppendLine($"\"{product.Name}\",\"{product.Description}\",{product.Price:F2},{product.Weight:F2},{product.WeightUnit},{product.StockQuantity},\"{product.CategoryName}\",{product.IsActive.ToString().ToLower()},\"{variationsText}\",\"{photoUrlsText}\""); } return sb.ToString(); } private string[] ParseCsvLine(string line) { var values = new List(); var inQuotes = false; var currentValue = new StringBuilder(); for (int i = 0; i < line.Length; i++) { var c = line[i]; if (c == '"') { inQuotes = !inQuotes; } else if (c == ',' && !inQuotes) { values.Add(currentValue.ToString().Trim()); currentValue.Clear(); } else { currentValue.Append(c); } } values.Add(currentValue.ToString().Trim()); return values.ToArray(); } private string GetValue(string[] values, string[] headers, string columnName, string defaultValue) { var index = Array.FindIndex(headers, h => h.Equals(columnName, StringComparison.OrdinalIgnoreCase)); if (index >= 0 && index < values.Length) { var value = values[index].Trim('"', ' '); return string.IsNullOrEmpty(value) ? defaultValue : value; } return defaultValue; } // ===== HUMAN-READABLE TEXT FORMAT IMPORT ===== public async Task ImportFromHumanTextAsync(string textContent, bool replaceAll = false) { var result = new ProductImportResultDto(); // Replace all existing data if requested if (replaceAll) { _logger.LogWarning("REPLACE ALL: Deleting all existing products, variants, and categories"); var deletedCount = await DeleteAllProductsAndCategoriesAsync(); _logger.LogInformation("Deleted {Count} existing records", deletedCount); } if (string.IsNullOrWhiteSpace(textContent)) { result.Errors.Add(new ProductImportErrorDto { RowNumber = 0, ProductName = "File", ErrorMessages = { "Import text is empty" } }); return result; } // Get categories and variant collections for lookup var categories = await _categoryService.GetAllCategoriesAsync(); var categoryLookup = categories.ToDictionary(c => c.Name, c => c.Id, StringComparer.OrdinalIgnoreCase); var variantCollectionsList = await _context.VariantCollections .Where(vc => vc.IsActive) .ToListAsync(); var variantCollections = variantCollectionsList.ToDictionary(vc => vc.Name, vc => vc.Id, StringComparer.OrdinalIgnoreCase); // Split into product blocks (each starting with #) var productBlocks = SplitIntoProductBlocks(textContent); result.TotalRows = productBlocks.Count; for (int i = 0; i < productBlocks.Count; i++) { try { await ParseAndImportProductBlock(productBlocks[i], categoryLookup, variantCollections, result, i + 1); } catch (Exception ex) { result.Errors.Add(new ProductImportErrorDto { RowNumber = i + 1, ProductName = "Parse Error", ErrorMessages = { ex.Message } }); result.FailedImports++; } } _logger.LogInformation("Human text import completed: {Success} successful, {Failed} failed", result.SuccessfulImports, result.FailedImports); return result; } private List SplitIntoProductBlocks(string textContent) { var blocks = new List(); var lines = textContent.Split('\n'); var currentBlock = new StringBuilder(); foreach (var line in lines) { if (line.TrimStart().StartsWith("#") && currentBlock.Length > 0) { // Start of new product block, save current block blocks.Add(currentBlock.ToString()); currentBlock.Clear(); } currentBlock.AppendLine(line); } // Add final block if (currentBlock.Length > 0) { blocks.Add(currentBlock.ToString()); } return blocks; } private async Task ParseAndImportProductBlock( string blockText, Dictionary categoryLookup, Dictionary variantCollectionLookup, ProductImportResultDto result, int blockNumber) { var lines = blockText.Split('\n', StringSplitOptions.RemoveEmptyEntries); var errors = new List(); // Parse product header (first line starting with #) var headerLine = lines.FirstOrDefault(l => l.TrimStart().StartsWith("#")); if (headerLine == null) { errors.Add("Product block must start with # followed by product name"); result.Errors.Add(new ProductImportErrorDto { RowNumber = blockNumber, ProductName = "Unknown", ErrorMessages = errors }); result.FailedImports++; return; } // Extract product name and optional variant collection var headerParts = headerLine.TrimStart('#').Split(';'); var productName = headerParts[0].Trim(); string? variantCollectionName = headerParts.Length > 1 ? headerParts[1].Trim() : null; // Extract description (between and ) var description = ExtractTextBlock(blockText); // Parse key-value properties var properties = ParseKeyValueProperties(lines); // Extract category if (!properties.TryGetValue("category", out var categoryName)) { errors.Add("Missing required 'category:' field"); } else if (!categoryLookup.TryGetValue(categoryName, out var categoryId)) { errors.Add($"Category '{categoryName}' not found"); } // Validate variant collection if specified Guid? variantCollectionId = null; if (!string.IsNullOrEmpty(variantCollectionName)) { if (!variantCollectionLookup.TryGetValue(variantCollectionName, out var vcId)) { errors.Add($"Variant collection '{variantCollectionName}' not found"); } else { variantCollectionId = vcId; } } // Parse numeric properties with defaults var price = ParseDecimal(properties.GetValueOrDefault("price", "0"), "price"); var weight = ParseDecimal(properties.GetValueOrDefault("weight", "0"), "weight"); var stock = ParseInt(properties.GetValueOrDefault("stock", "0"), "stock"); var weightUnitStr = properties.GetValueOrDefault("unit", "Grams"); if (!Enum.TryParse(weightUnitStr, true, out var weightUnit)) { errors.Add($"Invalid weight unit: {weightUnitStr}. Valid values: Grams, Kilograms, Pounds, Ounces"); } if (errors.Any()) { result.Errors.Add(new ProductImportErrorDto { RowNumber = blockNumber, ProductName = productName, ErrorMessages = errors }); result.FailedImports++; return; } // Create product var createProductDto = new CreateProductDto { Name = productName, Description = description ?? "", Price = price, Weight = weight, WeightUnit = weightUnit, StockQuantity = stock, CategoryId = categoryLookup[categoryName], VariantCollectionId = variantCollectionId }; var product = await _productService.CreateProductAsync(createProductDto); result.ImportedProducts.Add(product); // Parse and import variants (lines starting with -) var variantLines = lines.Where(l => l.TrimStart().StartsWith("-")).ToList(); for (int i = 0; i < variantLines.Count; i++) { await ParseAndImportVariantLine(product.Id, variantLines[i], i); } // Parse and import multi-buys (lines starting with +) var multiBuyLines = lines.Where(l => l.TrimStart().StartsWith("+")).ToList(); for (int i = 0; i < multiBuyLines.Count; i++) { await ParseAndImportMultiBuyLine(product.Id, multiBuyLines[i], i); } result.SuccessfulImports++; } private string? ExtractTextBlock(string blockText) { var startTag = ""; var endTag = ""; var startIndex = blockText.IndexOf(startTag, StringComparison.OrdinalIgnoreCase); if (startIndex == -1) return null; startIndex += startTag.Length; var endIndex = blockText.IndexOf(endTag, startIndex, StringComparison.OrdinalIgnoreCase); if (endIndex == -1) return null; return blockText.Substring(startIndex, endIndex - startIndex).Trim(); } private Dictionary ParseKeyValueProperties(string[] lines) { var properties = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var line in lines) { var trimmed = line.Trim(); if (string.IsNullOrEmpty(trimmed) || trimmed.StartsWith("#") || trimmed.StartsWith("<") || trimmed.StartsWith("-") || trimmed.StartsWith("+")) continue; var colonIndex = trimmed.IndexOf(':'); if (colonIndex > 0) { var key = trimmed.Substring(0, colonIndex).Trim(); var value = trimmed.Substring(colonIndex + 1).Trim(); properties[key] = value; } } return properties; } private async Task ParseAndImportVariantLine(Guid productId, string line, int sortOrder) { // Format: - VariantName; Price; OptionalStockLevel // Example: - Small; 8.00; 50 var content = line.TrimStart('-').Trim(); var parts = content.Split(';').Select(p => p.Trim()).ToArray(); if (parts.Length < 2) return; // Need at least name and price var variantName = parts[0]; var price = ParseDecimal(parts[1], "variant price"); var stockLevel = parts.Length > 2 ? ParseInt(parts[2], "variant stock") : 0; var variantDto = new CreateProductVariantDto { ProductId = productId, Name = variantName, Price = price, StockLevel = stockLevel, SortOrder = sortOrder, VariantType = "Standard" }; await _productService.CreateProductVariantAsync(variantDto); } private async Task ParseAndImportMultiBuyLine(Guid productId, string line, int sortOrder) { // Format: + Name; Quantity; Price // Example: + Twin Pack; 2; 19.00 var content = line.TrimStart('+').Trim(); var parts = content.Split(';').Select(p => p.Trim()).ToArray(); if (parts.Length < 3) return; // Need name, quantity, and price var name = parts[0]; var quantity = ParseInt(parts[1], "multi-buy quantity"); var price = ParseDecimal(parts[2], "multi-buy price"); var multiBuyDto = new CreateProductMultiBuyDto { ProductId = productId, Name = name, Description = "", Quantity = quantity, Price = price, SortOrder = sortOrder }; await _productService.CreateProductMultiBuyAsync(multiBuyDto); } private decimal ParseDecimal(string value, string fieldName) { if (decimal.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) return result; throw new FormatException($"Invalid decimal value for {fieldName}: {value}"); } private int ParseInt(string value, string fieldName) { if (int.TryParse(value, out var result)) return result; throw new FormatException($"Invalid integer value for {fieldName}: {value}"); } public string GenerateTemplateAsHumanText() { return @"# Organic Coffee Beans; Size Options Premium organic coffee beans sourced from sustainable farms. Perfect for espresso or filter coffee. Freshly roasted to order. category: Beverages price: 12.00 weight: 250 unit: Grams stock: 100 - 250g; 12.00; 50 - 500g; 22.00; 30 - 1kg; 40.00; 20 + Single Bag; 1; 12.00 + Twin Pack; 2; 22.00 + Family Pack; 3; 32.00 # Artisan Chocolate Bar Handcrafted dark chocolate made with 70% cacao. Vegan and gluten-free. category: Confectionery price: 3.50 weight: 100 unit: Grams stock: 200 + Single Bar; 1; 3.50 + Box of 6; 6; 18.00 # Premium Tea Blend category: Beverages price: 8.50 weight: 50 unit: Grams stock: 75 - 50g Tin; 8.50; 40 - 100g Tin; 15.00; 25 - 250g Bulk; 32.00; 10 "; } public async Task DeleteAllProductsAndCategoriesAsync() { var deletedCount = 0; try { // Use ExecuteSqlRaw to avoid EF tracking issues and optimistic concurrency errors // Order matters: delete child records first, then parents // Delete order items (references products) var orderItemsDeleted = await _context.Database.ExecuteSqlRawAsync("DELETE FROM OrderItems"); deletedCount += orderItemsDeleted; _logger.LogInformation("Deleted {Count} order items", orderItemsDeleted); // Delete product photos var photosDeleted = await _context.Database.ExecuteSqlRawAsync("DELETE FROM ProductPhotos"); deletedCount += photosDeleted; _logger.LogInformation("Deleted {Count} product photos", photosDeleted); // Delete product variants var variantsDeleted = await _context.Database.ExecuteSqlRawAsync("DELETE FROM ProductVariants"); deletedCount += variantsDeleted; _logger.LogInformation("Deleted {Count} product variants", variantsDeleted); // Delete product multi-buys var multiBuysDeleted = await _context.Database.ExecuteSqlRawAsync("DELETE FROM ProductMultiBuys"); deletedCount += multiBuysDeleted; _logger.LogInformation("Deleted {Count} product multi-buys", multiBuysDeleted); // Delete products var productsDeleted = await _context.Database.ExecuteSqlRawAsync("DELETE FROM Products"); deletedCount += productsDeleted; _logger.LogInformation("Deleted {Count} products", productsDeleted); // Delete categories var categoriesDeleted = await _context.Database.ExecuteSqlRawAsync("DELETE FROM Categories"); deletedCount += categoriesDeleted; _logger.LogInformation("Deleted {Count} categories", categoriesDeleted); _logger.LogInformation("Successfully deleted {TotalCount} total records", deletedCount); return deletedCount; } catch (Exception ex) { _logger.LogError(ex, "Failed to delete products and categories"); throw new InvalidOperationException($"Failed to delete data: {ex.Message}", ex); } } public async Task DeleteAllOrdersAndSalesDataAsync() { var deletedCount = 0; try { _logger.LogWarning("DELETING ALL ORDERS AND SALES DATA - This action is irreversible"); // Delete in proper order (child tables first) // Delete customer messages (references customers and orders) var messagesDeleted = await _context.Database.ExecuteSqlRawAsync("DELETE FROM CustomerMessages"); deletedCount += messagesDeleted; _logger.LogInformation("Deleted {Count} customer messages", messagesDeleted); // Delete crypto payments (references orders) var paymentsDeleted = await _context.Database.ExecuteSqlRawAsync("DELETE FROM CryptoPayments"); deletedCount += paymentsDeleted; _logger.LogInformation("Deleted {Count} crypto payments", paymentsDeleted); // Delete order items (references orders and products) var orderItemsDeleted = await _context.Database.ExecuteSqlRawAsync("DELETE FROM OrderItems"); deletedCount += orderItemsDeleted; _logger.LogInformation("Deleted {Count} order items", orderItemsDeleted); // Delete orders (parent table) var ordersDeleted = await _context.Database.ExecuteSqlRawAsync("DELETE FROM Orders"); deletedCount += ordersDeleted; _logger.LogInformation("Deleted {Count} orders", ordersDeleted); // Delete reviews (sales feedback data) var reviewsDeleted = await _context.Database.ExecuteSqlRawAsync("DELETE FROM Reviews"); deletedCount += reviewsDeleted; _logger.LogInformation("Deleted {Count} reviews", reviewsDeleted); // Delete push subscriptions (user notification data) var subscriptionsDeleted = await _context.Database.ExecuteSqlRawAsync("DELETE FROM PushSubscriptions"); deletedCount += subscriptionsDeleted; _logger.LogInformation("Deleted {Count} push subscriptions", subscriptionsDeleted); // Delete bot contacts (telegram bot interaction data) var contactsDeleted = await _context.Database.ExecuteSqlRawAsync("DELETE FROM BotContacts"); deletedCount += contactsDeleted; _logger.LogInformation("Deleted {Count} bot contacts", contactsDeleted); // Delete bot metrics (analytics data) var metricsDeleted = await _context.Database.ExecuteSqlRawAsync("DELETE FROM BotMetrics"); deletedCount += metricsDeleted; _logger.LogInformation("Deleted {Count} bot metrics", metricsDeleted); // Delete customers (will cascade any remaining references) var customersDeleted = await _context.Database.ExecuteSqlRawAsync("DELETE FROM Customers"); deletedCount += customersDeleted; _logger.LogInformation("Deleted {Count} customers", customersDeleted); _logger.LogInformation("✅ Successfully deleted {TotalCount} total sales records", deletedCount); return deletedCount; } catch (Exception ex) { _logger.LogError(ex, "Failed to delete orders and sales data"); throw new InvalidOperationException($"Failed to delete sales data: {ex.Message}", ex); } } }