using System.Net; using System.Net.Http.Json; using System.Text.Json; using DongfangHydro.Dashboard.Api.Data; using DongfangHydro.Dashboard.Api.Domain; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Xunit; namespace DongfangHydro.Dashboard.Tests; public sealed class BomDataApiTests(DashboardApiFactory factory) : IClassFixture { [Fact] public async Task MaterialQuotas_supports_category_keyword_and_paging() { using var client = factory.CreateClient(); Guid orderId; await using (var scope = factory.Services.CreateAsyncScope()) { var dbContext = scope.ServiceProvider.GetRequiredService(); orderId = await dbContext.ProductionOrders.Select(order => order.Id).FirstAsync(); dbContext.MaterialQuotas.AddRange( CreateQuota(orderId, "钢板", "1010100182", "钢板/Q235B", 9), CreateQuota(orderId, "标准件", "1110100001", "螺栓/8.8级", 188)); await dbContext.SaveChangesAsync(); } var response = await client.GetAsync( $"/api/orders/{orderId}/material-quotas?category=钢板&keyword=Q235B&page=1&pageSize=10"); var body = await response.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(1, body.GetProperty("total").GetInt32()); var item = Assert.Single(body.GetProperty("items").EnumerateArray()); Assert.Equal("钢板", item.GetProperty("category").GetString()); Assert.Equal("1010100182", item.GetProperty("materialCode").GetString()); Assert.Equal(33.61m, item.GetProperty("netWeight").GetDecimal()); Assert.Equal(35.6266m, item.GetProperty("consumptionQuota").GetDecimal()); Assert.Equal("33.61", item.GetProperty("netWeightText").GetString()); Assert.Equal("35.6266", item.GetProperty("consumptionQuotaText").GetString()); Assert.Equal(9, item.GetProperty("sourceRow").GetInt32()); } [Fact] public async Task Tree_exposes_imported_bom_fields() { using var client = factory.CreateClient(); Guid orderId; Guid nodeId; await using (var scope = factory.Services.CreateAsyncScope()) { var dbContext = scope.ServiceProvider.GetRequiredService(); var node = await dbContext.ProductionNodes.FirstAsync(item => item.NodeType == "process"); orderId = node.OrderId; nodeId = node.Id; node.Specification = "M16"; node.Material = "标准件"; node.UnitWeight = 0.2m; node.TotalWeight = 1.6m; node.UnitWeightText = "0.2"; node.TotalWeightText = "1.6"; node.BomQuantity = -8m; node.BomQuantityText = "-8"; node.Remark = "镀锌"; node.SourceSequence = "1.1.2"; node.SourceSheet = "明细"; node.SourceRow = 10; await dbContext.SaveChangesAsync(); } var root = await client.GetFromJsonAsync($"/api/orders/{orderId}/tree"); var nodeJson = FindNode(root, nodeId); Assert.Equal("M16", nodeJson.GetProperty("specification").GetString()); Assert.Equal("标准件", nodeJson.GetProperty("material").GetString()); Assert.Equal(0.2m, nodeJson.GetProperty("unitWeight").GetDecimal()); Assert.Equal(1.6m, nodeJson.GetProperty("totalWeight").GetDecimal()); Assert.Equal("0.2", nodeJson.GetProperty("unitWeightText").GetString()); Assert.Equal("1.6", nodeJson.GetProperty("totalWeightText").GetString()); Assert.Equal(-8m, nodeJson.GetProperty("bomQuantity").GetDecimal()); Assert.Equal("-8", nodeJson.GetProperty("bomQuantityText").GetString()); Assert.Equal("镀锌", nodeJson.GetProperty("remark").GetString()); Assert.Equal("1.1.2", nodeJson.GetProperty("sourceSequence").GetString()); Assert.Equal("明细", nodeJson.GetProperty("sourceSheet").GetString()); Assert.Equal(10, nodeJson.GetProperty("sourceRow").GetInt32()); } [Fact] public async Task Material_leaf_accepts_direct_progress_updates() { using var client = factory.CreateClient(); var orderId = Guid.NewGuid(); var rootId = Guid.NewGuid(); var materialId = Guid.NewGuid(); await using (var scope = factory.Services.CreateAsyncScope()) { var dbContext = scope.ServiceProvider.GetRequiredService(); dbContext.ProductionOrders.Add(new ProductionOrder { Id = orderId, Code = $"ZZ-MAT-{orderId:N}"[..24], ProductName = "物料报工测试", BatchQty = 1, RequiredQty = 10, Status = "waiting", RiskLevel = "normal", DataSource = "test", CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow, RowVersion = [1], }); dbContext.ProductionNodes.AddRange( new ProductionNode { Id = rootId, OrderId = orderId, Code = "ROOT", Name = "测试订单", NodeType = "order", SupplyType = "self_made", RequiredQty = 10, Status = "waiting", RiskLevel = "normal", UpdatedAt = DateTimeOffset.UtcNow, RowVersion = [1], }, new ProductionNode { Id = materialId, OrderId = orderId, ParentNodeId = rootId, Code = "MAT-01", Name = "钢板", Level = 1, NodeType = "material", SupplyType = "purchased", RequiredQty = 10, Status = "waiting", RiskLevel = "normal", UpdatedAt = DateTimeOffset.UtcNow, RowVersion = [1], }); await dbContext.SaveChangesAsync(); } var tree = await client.GetFromJsonAsync($"/api/orders/{orderId}/tree"); var material = FindNode(tree, materialId); var response = await client.PatchAsJsonAsync( $"/api/nodes/{materialId}/progress", new { completedQty = 10, expectedVersion = material.GetProperty("version").GetString(), status = "done", riskLevel = "normal", }); var body = await response.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal("material", body.GetProperty("node").GetProperty("nodeType").GetString()); Assert.Equal(100, body.GetProperty("node").GetProperty("progress").GetInt32()); } private static MaterialQuota CreateQuota( Guid orderId, string category, string materialCode, string materialName, int sourceRow) { return new MaterialQuota { Id = Guid.NewGuid(), OrderId = orderId, Category = category, SourceSequence = "1", MaterialCode = materialCode, MaterialName = materialName, Specification = "δ2-1500", Unit = "kg", NetWeight = 33.61m, ConsumptionQuota = 35.6266m, NetWeightText = "33.61", ConsumptionQuotaText = "35.6266", Brand = "无", Remark = string.Empty, SourceSheet = "材料定额", SourceRow = sourceRow, }; } private static JsonElement FindNode(JsonElement node, Guid nodeId) { if (node.GetProperty("id").GetGuid() == nodeId) { return node; } foreach (var child in node.GetProperty("children").EnumerateArray()) { var match = FindNode(child, nodeId); if (match.ValueKind != JsonValueKind.Undefined) { return match; } } return default; } }