Fix: CSV import now creates ProductVariants instead of ProductMultiBuys

CRITICAL BUG FIX: Orders were totaling £0 because imported products had:
- Base price set to £0
- Variants stored as ProductMultiBuys (not ProductVariants)
- Order creation couldn't find variant prices → used base price of £0

Changes:
- CSV import now detects format: semicolons (;) = variants, colons (:) = multi-buys
- Added ImportProductVariantsAsync() to handle pipe-delimited variant format
- Format: "value; price; stock|value; price; stock" (e.g., "10; 30.00; 100|25; 70.00; 50")
- Updated CSV export to prioritize ProductVariants over MultiBuys
- Updated CSV template with correct variant format examples
- Added comprehensive documentation in VARIANT_CSV_FORMAT.md

Migration Required:
- Existing products with incorrect MultiBuy data need re-import
- Convert format from "name:qty:price;name:qty:price" to "value; price; stock|value; price; stock"
- Ensure base price is 0 for variant-based products

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-08 18:31:46 +01:00
parent 9f7b2840af
commit 062916d5ce
2 changed files with 167 additions and 8 deletions

View File

@@ -233,7 +233,17 @@ public class ProductImportService : IProductImportService
// Import variations if provided
if (!string.IsNullOrEmpty(importDto.Variations))
{
await ImportProductMultiBuysAsync(product.Id, importDto.Variations);
// Check format: if it contains semicolons with 3 parts, treat as variants
// Format: "10; 30.00; 100|25; 70.00; 50" (value; price; stock)
// Old multi-buy format: "Single Item:1:10.00;Twin Pack:2:19.00" (name:quantity:price)
if (importDto.Variations.Contains(';'))
{
await ImportProductVariantsAsync(product.Id, importDto.Variations);
}
else
{
await ImportProductMultiBuysAsync(product.Id, importDto.Variations);
}
}
// Import photos if provided
@@ -256,6 +266,42 @@ public class ProductImportService : IProductImportService
}
}
private async Task ImportProductVariantsAsync(Guid productId, string variationsText)
{
// Format: "10; 30.00; 100|25; 70.00; 50|50; 130.00; 25" (value; price; stock separated by |)
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 variantValue = parts[0].Trim();
var priceOverride = decimal.Parse(parts[1].Trim(), CultureInfo.InvariantCulture);
var stockQuantity = int.Parse(parts[2].Trim());
// Determine variant type based on the value format
string variantType = variantValue.ToLower().Contains("g") ? "Weight" : "Quantity";
var variantDto = new CreateProductVariantDto
{
ProductId = productId,
Name = variantValue,
Price = priceOverride,
StockLevel = stockQuantity,
SortOrder = i,
VariantType = variantType
};
_logger.LogInformation(
"CSV Import: Creating variant for product {ProductId}: Name={Name}, Price={Price}, Stock={Stock}",
productId, variantValue, priceOverride, stockQuantity);
await _productService.CreateProductVariantAsync(variantDto);
}
}
}
private async Task ImportProductMultiBuysAsync(Guid productId, string variationsText)
{
// Format: "Single Item:1:10.00;Twin Pack:2:19.00;Triple Pack:3:25.00"
@@ -306,10 +352,10 @@ public class ProductImportService : IProductImportService
// 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,\"\",\"\"");
// Example rows with ProductVariant format (value; price; stock | separated)
sb.AppendLine("\"Example Product 1\",\"High-quality product with quantity-based variants\",0,150,Grams,0,Electronics,true,\"10; 30.00; 100|25; 70.00; 50|50; 130.00; 25\",\"https://example.com/photo1.jpg\"");
sb.AppendLine("\"Example Product 2\",\"Product with weight-based variants\",0,200,Grams,0,Clothing,true,\"3.5g; 35.00; 10|7g; 65.00; 8|14g; 120.00; 5\",\"\"");
sb.AppendLine("\"Simple Product\",\"Basic product without variations (set price and stock)\",9.99,100,Grams,100,Books,true,\"\",\"\"");
return sb.ToString();
}
@@ -324,9 +370,21 @@ public class ProductImportService : IProductImportService
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 variations string - prioritize ProductVariants over MultiBuys
string variationsText = "";
if (product.Variants != null && product.Variants.Any())
{
// Export ProductVariants format: "value; price; stock|value; price; stock"
variationsText = string.Join("|", product.Variants.OrderBy(v => v.SortOrder)
.Select(v => $"{v.Name}; {v.Price:F2}; {v.StockLevel}"));
}
else if (product.MultiBuys != null && product.MultiBuys.Any())
{
// Legacy MultiBuys format: "name:quantity:price;name:quantity:price"
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)