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);
}
[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> ImportFromHumanTextAsync(string textContent, bool replaceAll = false);
Task<int> DeleteAllProductsAndCategoriesAsync();
Task<int> DeleteAllOrdersAndSalesDataAsync();
string GenerateTemplateAsCsv();
string GenerateTemplateAsHumanText();
Task<string> 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);
// 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<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);
}
}
}