From 86e30d720347c2007591447c20b7386c8853ccd8 Mon Sep 17 00:00:00 2001 From: SysAdmin Date: Wed, 8 Oct 2025 15:00:51 +0100 Subject: [PATCH] Fix: Import concurrency errors + Add sales data cleanup - Fix optimistic concurrency errors in product import by using ExecuteSqlRaw instead of EF tracking - Add DeleteAllOrdersAndSalesDataAsync() method to clear orders, payments, customers, and messages - Add DeleteAllSalesData endpoint to ProductsController for admin access - Proper deletion order to avoid foreign key violations - Enhanced logging for troubleshooting data cleanup operations Resolves: Import errors with 'database operation expected to affect 1 row(s)' --- .../Admin/Controllers/ProductsController.cs | 17 +++ LittleShop/Services/ProductImportService.cs | 125 +++++++++++++++--- 2 files changed, 120 insertions(+), 22 deletions(-) diff --git a/LittleShop/Areas/Admin/Controllers/ProductsController.cs b/LittleShop/Areas/Admin/Controllers/ProductsController.cs index a0a59fb..6d123b8 100644 --- a/LittleShop/Areas/Admin/Controllers/ProductsController.cs +++ b/LittleShop/Areas/Admin/Controllers/ProductsController.cs @@ -306,4 +306,21 @@ public class ProductsController : Controller } return Json(collection); } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task DeleteAllSalesData() + { + try + { + var deletedCount = await _importService.DeleteAllOrdersAndSalesDataAsync(); + TempData["SuccessMessage"] = $"✅ Successfully deleted {deletedCount} sales records (orders, payments, customers, messages)"; + return RedirectToAction(nameof(Index)); + } + catch (Exception ex) + { + TempData["ErrorMessage"] = $"Failed to delete sales data: {ex.Message}"; + return RedirectToAction(nameof(Index)); + } + } } \ No newline at end of file diff --git a/LittleShop/Services/ProductImportService.cs b/LittleShop/Services/ProductImportService.cs index 5cf104a..acb35d7 100644 --- a/LittleShop/Services/ProductImportService.cs +++ b/LittleShop/Services/ProductImportService.cs @@ -14,6 +14,7 @@ public interface IProductImportService Task ImportFromTextAsync(string csvText, bool replaceAll = false); Task ImportFromHumanTextAsync(string textContent, bool replaceAll = false); Task DeleteAllProductsAndCategoriesAsync(); + Task DeleteAllOrdersAndSalesDataAsync(); string GenerateTemplateAsCsv(); string GenerateTemplateAsHumanText(); Task ExportProductsAsCsvAsync(); @@ -731,33 +732,113 @@ stock: 75 { var deletedCount = 0; - // Delete all product photos - var photos = await _context.ProductPhotos.ToListAsync(); - _context.ProductPhotos.RemoveRange(photos); - deletedCount += photos.Count; + try + { + // Use ExecuteSqlRaw to avoid EF tracking issues and optimistic concurrency errors + // Order matters: delete child records first, then parents - // Delete all product variants - var variants = await _context.ProductVariants.ToListAsync(); - _context.ProductVariants.RemoveRange(variants); - deletedCount += variants.Count; + // Delete order items (references products) + var orderItemsDeleted = await _context.Database.ExecuteSqlRawAsync("DELETE FROM OrderItems"); + deletedCount += orderItemsDeleted; + _logger.LogInformation("Deleted {Count} order items", orderItemsDeleted); - // Delete all product multi-buys - var multiBuys = await _context.ProductMultiBuys.ToListAsync(); - _context.ProductMultiBuys.RemoveRange(multiBuys); - deletedCount += multiBuys.Count; + // Delete product photos + var photosDeleted = await _context.Database.ExecuteSqlRawAsync("DELETE FROM ProductPhotos"); + deletedCount += photosDeleted; + _logger.LogInformation("Deleted {Count} product photos", photosDeleted); - // Delete all products - var products = await _context.Products.ToListAsync(); - _context.Products.RemoveRange(products); - deletedCount += products.Count; + // Delete product variants + var variantsDeleted = await _context.Database.ExecuteSqlRawAsync("DELETE FROM ProductVariants"); + deletedCount += variantsDeleted; + _logger.LogInformation("Deleted {Count} product variants", variantsDeleted); - // Delete all categories - var categories = await _context.Categories.ToListAsync(); - _context.Categories.RemoveRange(categories); - deletedCount += categories.Count; + // Delete product multi-buys + var multiBuysDeleted = await _context.Database.ExecuteSqlRawAsync("DELETE FROM ProductMultiBuys"); + deletedCount += multiBuysDeleted; + _logger.LogInformation("Deleted {Count} product multi-buys", multiBuysDeleted); - await _context.SaveChangesAsync(); + // Delete products + var productsDeleted = await _context.Database.ExecuteSqlRawAsync("DELETE FROM Products"); + deletedCount += productsDeleted; + _logger.LogInformation("Deleted {Count} products", productsDeleted); - return deletedCount; + // 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); + } } } \ No newline at end of file