using DongfangHydro.Dashboard.Api.Data; using DongfangHydro.Dashboard.Api.Domain; using DongfangHydro.Dashboard.Api.Importing; using Microsoft.EntityFrameworkCore; using Xunit; namespace DongfangHydro.Dashboard.Tests; public sealed class BomImportServiceTests { [Fact] public void Order_code_index_is_not_unique_because_source_document_is_the_import_identity() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase($"bom-model-{Guid.NewGuid():N}") .Options; using var dbContext = new DashboardDbContext(options); var orderType = dbContext.Model.FindEntityType(typeof(ProductionOrder))!; var codeIndex = Assert.Single(orderType.GetIndexes(), index => index.Properties.Count == 1 && index.Properties[0].Name == nameof(ProductionOrder.Code)); Assert.False(codeIndex.IsUnique); } [Fact] public async Task Import_replaces_matching_source_purges_demo_and_is_idempotent() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase($"bom-import-{Guid.NewGuid():N}") .Options; await using var dbContext = new DashboardDbContext(options); await dbContext.Database.EnsureCreatedAsync(); dbContext.ProductionOrders.AddRange( CreateOrder("MO-DEMO", "demo", string.Empty), CreateOrder("2437-OLD", "excel", "SG2026-06-033"), CreateOrder("KEEP-001", "manual", "OTHER-DOC")); await dbContext.SaveChangesAsync(); var service = new BomImportService(dbContext); var document = CreateDocument(); var first = await service.ImportAsync(document, purgeDemo: true, CancellationToken.None); var progressedNode = await dbContext.ProductionNodes.SingleAsync(node => node.SourceRow == 6); progressedNode.CompletedQty = 1; progressedNode.Progress = 50; progressedNode.Status = "in_progress"; await dbContext.SaveChangesAsync(); var second = await service.ImportAsync(document, purgeDemo: true, CancellationToken.None); Assert.Equal(1, first.RemovedDemoOrders); Assert.Equal(1, first.ReplacedOrders); Assert.Equal(0, second.ReplacedOrders); Assert.Equal(2, second.TotalOrders); Assert.Equal(2, await dbContext.ProductionOrders.CountAsync()); Assert.DoesNotContain(await dbContext.ProductionOrders.ToListAsync(), order => order.DataSource == "demo"); Assert.Contains(await dbContext.ProductionOrders.ToListAsync(), order => order.Code == "KEEP-001"); var importedOrder = await dbContext.ProductionOrders.SingleAsync( order => order.SourceDocumentCode == "SG2026-06-033"); Assert.Equal("2437-QM0201", importedOrder.Code); Assert.Equal("excel", importedOrder.DataSource); Assert.Null(importedOrder.PlannedStart); Assert.Null(importedOrder.PlannedEnd); var nodes = await dbContext.ProductionNodes .Where(node => node.OrderId == importedOrder.Id) .OrderBy(node => node.Level) .ThenBy(node => node.SourceRow) .ToListAsync(); Assert.Equal(3, nodes.Count); Assert.Equal("order", nodes[0].NodeType); Assert.Equal("part", nodes[1].NodeType); Assert.Equal("material", nodes[2].NodeType); Assert.Equal(nodes[0].Id, nodes[1].ParentNodeId); Assert.Equal(nodes[1].Id, nodes[2].ParentNodeId); Assert.Equal(1, nodes.Single(node => node.SourceRow == 6).CompletedQty); Assert.Equal(50, nodes.Single(node => node.SourceRow == 6).Progress); Assert.Equal("in_progress", nodes.Single(node => node.SourceRow == 6).Status); var quota = await dbContext.MaterialQuotas.SingleAsync(item => item.OrderId == importedOrder.Id); Assert.Equal("钢板", quota.Category); Assert.Equal(33.61m, quota.NetWeight); Assert.Equal(9, quota.SourceRow); var forced = await service.ImportAsync( document, purgeDemo: true, CancellationToken.None, forceReplace: true); Assert.Equal(1, forced.ReplacedOrders); Assert.False(forced.SkippedUnchanged); Assert.Equal(0, (await dbContext.ProductionNodes.SingleAsync(node => node.SourceRow == 6)).Progress); } private static ProductionOrder CreateOrder(string code, string dataSource, string sourceDocumentCode) { return new ProductionOrder { Id = Guid.NewGuid(), Code = code, ProductName = code, DataSource = dataSource, SourceDocumentCode = sourceDocumentCode, BatchQty = 1, RequiredQty = 1, Status = "waiting", RiskLevel = "normal", CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow, }; } private static BomImportDocument CreateDocument() { return new BomImportDocument( new BomImportMetadata( "SG2026-06-033", "2437-QM0201", "青峪口水库工程", "青峪口尾水单向门机 2×160/10", "电站尾水2×160kN/100kN单向门式启闭机", 1, "bom.xlsx", new string('a', 64)), [ new BomImportNode(2, null, "1", "P-01", "主起升机构", "", 1, "部件", 10, 10, "", 1, "part", "self_made"), new BomImportNode(6, 2, "1.1", "GB/T700", "钢板", "δ10", 2, "Q235B", 5, 10, "", 2, "material", "purchased"), ], [ new MaterialQuotaImportRow(9, "钢板", "1", "1010100182", "钢板/Q235B", "δ2-1500", "kg", null, 33.61m, 35.6266m, "无", ""), ], 2, []); } }