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)'
This commit is contained in:
SysAdmin 2025-10-08 15:00:51 +01:00
parent 6c8106ff90
commit 86e30d7203
2 changed files with 120 additions and 22 deletions

View File

@ -306,4 +306,21 @@ public class ProductsController : Controller
} }
return Json(collection); return Json(collection);
} }
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> 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));
}
}
} }

View File

@ -14,6 +14,7 @@ public interface IProductImportService
Task<ProductImportResultDto> ImportFromTextAsync(string csvText, bool replaceAll = false); Task<ProductImportResultDto> ImportFromTextAsync(string csvText, bool replaceAll = false);
Task<ProductImportResultDto> ImportFromHumanTextAsync(string textContent, bool replaceAll = false); Task<ProductImportResultDto> ImportFromHumanTextAsync(string textContent, bool replaceAll = false);
Task<int> DeleteAllProductsAndCategoriesAsync(); Task<int> DeleteAllProductsAndCategoriesAsync();
Task<int> DeleteAllOrdersAndSalesDataAsync();
string GenerateTemplateAsCsv(); string GenerateTemplateAsCsv();
string GenerateTemplateAsHumanText(); string GenerateTemplateAsHumanText();
Task<string> ExportProductsAsCsvAsync(); Task<string> ExportProductsAsCsvAsync();
@ -731,33 +732,113 @@ stock: 75
{ {
var deletedCount = 0; var deletedCount = 0;
// Delete all product photos try
var photos = await _context.ProductPhotos.ToListAsync(); {
_context.ProductPhotos.RemoveRange(photos); // Use ExecuteSqlRaw to avoid EF tracking issues and optimistic concurrency errors
deletedCount += photos.Count; // Order matters: delete child records first, then parents
// Delete all product variants // Delete order items (references products)
var variants = await _context.ProductVariants.ToListAsync(); var orderItemsDeleted = await _context.Database.ExecuteSqlRawAsync("DELETE FROM OrderItems");
_context.ProductVariants.RemoveRange(variants); deletedCount += orderItemsDeleted;
deletedCount += variants.Count; _logger.LogInformation("Deleted {Count} order items", orderItemsDeleted);
// Delete all product multi-buys // Delete product photos
var multiBuys = await _context.ProductMultiBuys.ToListAsync(); var photosDeleted = await _context.Database.ExecuteSqlRawAsync("DELETE FROM ProductPhotos");
_context.ProductMultiBuys.RemoveRange(multiBuys); deletedCount += photosDeleted;
deletedCount += multiBuys.Count; _logger.LogInformation("Deleted {Count} product photos", photosDeleted);
// Delete all products // Delete product variants
var products = await _context.Products.ToListAsync(); var variantsDeleted = await _context.Database.ExecuteSqlRawAsync("DELETE FROM ProductVariants");
_context.Products.RemoveRange(products); deletedCount += variantsDeleted;
deletedCount += products.Count; _logger.LogInformation("Deleted {Count} product variants", variantsDeleted);
// Delete all categories // Delete product multi-buys
var categories = await _context.Categories.ToListAsync(); var multiBuysDeleted = await _context.Database.ExecuteSqlRawAsync("DELETE FROM ProductMultiBuys");
_context.Categories.RemoveRange(categories); deletedCount += multiBuysDeleted;
deletedCount += categories.Count; _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);
// 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; 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<int> 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);
}
}
} }