mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-10 22:24:14 -04:00
feat(api): accept stock prices and archive cost summaries
This commit is contained in:
1 parent
5dbf887b68
commit
076b794dda
6 files changed
+83
-3
No files matched your search
@@ -5,6 +5,9 @@ namespace OpenNest.Api;
|
||||
/// <summary>One explicit physical-stock type for a whole nesting job.</summary>
|
||||
public class NestRequestPlate
|
||||
{
|
||||
/// <summary>Positive per-sheet cost in common units; omit on every row for area scoring.</summary>
|
||||
public double? Cost { get; init; }
|
||||
|
||||
public string Id { get; init; }
|
||||
public Size Size { get; init; }
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ public class NestResponse
|
||||
public IReadOnlyList<NestPartFulfillment> Fulfillment { get; init; } = [];
|
||||
public IReadOnlyList<NestStockUsage> StockUsage { get; init; } = [];
|
||||
public IReadOnlyList<NestPlateStockMapping> PlateStockMappings { get; init; } = [];
|
||||
public NestCostSummary Costs { get; init; }
|
||||
public Nest Nest { get; init; }
|
||||
public NestRequest Request { get; init; }
|
||||
|
||||
@@ -86,6 +87,7 @@ public class NestResponse
|
||||
Fulfillment = Fulfillment is null
|
||||
? []
|
||||
: new List<NestPartFulfillment>(Fulfillment),
|
||||
Costs = Costs,
|
||||
StockUsage = StockUsage is null ? [] : new List<NestStockUsage>(StockUsage),
|
||||
PlateStockMappings = PlateStockMappings is null
|
||||
? []
|
||||
@@ -171,6 +173,7 @@ public class NestResponse
|
||||
Fulfillment = hasStatusMetadata ? archive.Fulfillment ?? [] : [],
|
||||
StockUsage = hasStatusMetadata ? archive.StockUsage ?? [] : [],
|
||||
PlateStockMappings = hasStatusMetadata ? archive.PlateStockMappings ?? [] : [],
|
||||
Costs = archive.Costs,
|
||||
Nest = nest,
|
||||
Request = request,
|
||||
};
|
||||
@@ -178,6 +181,7 @@ public class NestResponse
|
||||
|
||||
private sealed class NestResponseArchiveDto
|
||||
{
|
||||
public NestCostSummary Costs { get; init; }
|
||||
public int SchemaVersion { get; init; }
|
||||
public int SheetCount { get; init; }
|
||||
public double Utilization { get; init; }
|
||||
|
||||
@@ -28,6 +28,7 @@ public static class NestRunner
|
||||
if (requestParts.Count == 0)
|
||||
throw new ArgumentException("Request must contain at least one part.", nameof(request));
|
||||
|
||||
var stock = CreateStock(request);
|
||||
var sw = Stopwatch.StartNew();
|
||||
var parts = IdentifyParts(requestParts);
|
||||
var importedByPath = new Dictionary<string, Drawing>(StringComparer.Ordinal);
|
||||
@@ -82,7 +83,6 @@ public static class NestRunner
|
||||
});
|
||||
}
|
||||
|
||||
var stock = CreateStock(request);
|
||||
var engineName = request.Engine ?? ResolvePlacementStrategy(request);
|
||||
var jobProgress = progress == null ? null : new JobProgressBridge(progress);
|
||||
var result = NestPipeline.Run(new NestPipelineRequest(
|
||||
@@ -91,6 +91,9 @@ public static class NestRunner
|
||||
// API returns a detached proposal, not an acceptance/commit to a caller's nest.
|
||||
// Invalid but representable proposals retain every pose and carry explicit validation status.
|
||||
var nest = new Nest { Thickness = request.Thickness, Material = new Material(request.Material) };
|
||||
if (request.Plates != null)
|
||||
nest.PlateOptions = stock.Where(s => s.Quantity != 0).Select(s => new PlateOption
|
||||
{ Width = s.Size.Width, Length = s.Size.Length, Cost = s.Cost ?? 0 }).ToList();
|
||||
foreach (var item in items)
|
||||
nest.Drawings.Add(item.Drawing);
|
||||
foreach (var proposed in result.Plates)
|
||||
@@ -144,6 +147,7 @@ public static class NestRunner
|
||||
Violations = result.Violations,
|
||||
Fulfillment = fulfillment,
|
||||
StockUsage = usage,
|
||||
Costs = result.CanKeep && result.IsValid ? NestCostSummary.FromAccepted(result) : null,
|
||||
PlateStockMappings = result.Plates.Select((value, index) =>
|
||||
new NestPlateStockMapping(index, value.Stock.Id)).ToArray(),
|
||||
Nest = nest,
|
||||
@@ -209,11 +213,13 @@ public static class NestRunner
|
||||
plate.Quantity,
|
||||
plate.PartSpacing,
|
||||
plate.EdgeSpacing,
|
||||
plate.Quadrant
|
||||
plate.Quadrant,
|
||||
plate.Cost
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
NestJobValidator.ValidateStockCosts(stock);
|
||||
return stock;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,59 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using OpenNest.Api;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
using OpenNest.Engine.Jobs;
|
||||
|
||||
namespace OpenNest.Tests.Api;
|
||||
|
||||
public class NestRunnerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ExplicitPricesSelectCheaperStockAndSurviveArchive()
|
||||
{
|
||||
var path = CreateTempSquareDxf(4, 4);
|
||||
var archive = Path.ChangeExtension(path, ".nestquote");
|
||||
try
|
||||
{
|
||||
var response = await NestRunner.RunAsync(new NestRequest
|
||||
{
|
||||
Engine = "Rectangles",
|
||||
Parts = [new NestRequestPart { DxfPath = path, Quantity = 1 }],
|
||||
Plates = [new NestRequestPlate { Id = "small", Size = new Size(10, 10), Quantity = 1, Cost = 50 },
|
||||
new NestRequestPlate { Id = "large", Size = new Size(12, 12), Quantity = 1, Cost = 7 }],
|
||||
});
|
||||
Assert.Equal(NestValidationStatus.Valid, response.ValidationStatus);
|
||||
Assert.Equal("large", Assert.Single(response.PlateStockMappings).StockId);
|
||||
Assert.Equal("supplied-cost", response.Costs.Basis);
|
||||
Assert.Equal(7, response.Costs.GrossTotal);
|
||||
Assert.Equal(7, response.Costs.NetScore);
|
||||
Assert.Equal(0, response.Costs.SalvageCredit);
|
||||
await response.SaveAsync(archive);
|
||||
var reopened = await NestResponse.LoadAsync(archive);
|
||||
Assert.Equal(7, reopened.Costs.GrossTotal);
|
||||
Assert.Equal(new[] { 50d, 7d }, reopened.Nest.PlateOptions.Select(p => p.Cost));
|
||||
Assert.Equal(7, reopened.Request.Plates[1].Cost);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
File.Delete(archive);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MixedCostsFailBeforeImport()
|
||||
{
|
||||
var error = await Assert.ThrowsAsync<ArgumentException>(() => NestRunner.RunAsync(new NestRequest
|
||||
{
|
||||
Parts = [new NestRequestPart { DxfPath = "missing.dxf", Quantity = 1 }],
|
||||
Plates = [new NestRequestPlate { Id = "priced", Size = new Size(10, 10), Cost = 2 },
|
||||
new NestRequestPlate { Id = "missing", Size = new Size(12, 12) }],
|
||||
}));
|
||||
Assert.Contains("Missing: missing", error.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_LegacySheetSize_UsesUnlimitedLegacyStockAndDerivedPartId()
|
||||
{
|
||||
|
||||
@@ -98,3 +98,20 @@ Response archive schema 3 persists validation status and violations. Older archi
|
||||
## Verification
|
||||
|
||||
`dotnet test OpenNest.FrontEnd.Tests/OpenNest.FrontEnd.Tests.csproj` exercises Console, MCP and API rejection/override, cancellation, physical-sheet settings, response IDs and archive round-trips. Its current target matches MCP's `net8.0-windows` marker but does not use WindowsDesktop and executes on Linux as well as Windows. It is included in the solution and Windows test workflow. Desktop interaction tests remain in Windows-only `OpenNest.WinForms.Tests`.
|
||||
|
||||
## Stock costs
|
||||
|
||||
API `NestRequestPlate.Cost` is an optional per-physical-sheet cost in caller-consistent
|
||||
units. Omit every cost for area scoring, or supply strictly positive finite costs
|
||||
for every available row. Explicit zero and mixed priced/unpriced available stock
|
||||
are rejected before import. Quantity-zero rows do not select the cost mode.
|
||||
|
||||
Validated proposals expose `Costs` (`Basis`, `Stock`, `GrossTotal`, `SalvageCredit`,
|
||||
`NetScore`) and persist them in quote archives. `area` identifies fallback scoring;
|
||||
`supplied-cost` identifies explicit prices. API salvage is disabled, so two sheets
|
||||
costing 25 each report gross and net totals of 50 with zero salvage credit. Totals
|
||||
exclude penalties for unplaced demand. Invalid proposals have no cost summary;
|
||||
overflow fails instead of becoming a score.
|
||||
|
||||
Returned nests preserve offered stock prices. These are cost-aware heuristics,
|
||||
not global cost optimizers; validation and fulfillment remain required.
|
||||
@@ -91,3 +91,8 @@ The existing `plateOptions` entries retain width, length and `cost`; no new `.ne
|
||||
format version is required. Legacy zero cost means unspecified, not a free sheet.
|
||||
Positive costs are generic per-sheet values in caller-consistent units. Solving
|
||||
requires all available options to be priced or all to be unspecified.
|
||||
|
||||
API returned nests retain offered options, excluding rows with zero available
|
||||
quantity because `.nest` options do not retain inventory. Options are offers, not
|
||||
a per-sheet purchase ledger. API quote archives additionally retain request costs
|
||||
and validated proposal cost summaries; old archives load with `Costs == null`.
|
||||
Reference in new issue
Block a user