Files
OpenNest/OpenNest.Tests/Data/LedgerStockMapperTests.cs
T
ajandClaude Opus 5.5 ea1decc097 feat(autonest): load sheet and drop stock from MaterialLedger
Auto Nest's Stock Options gets a Load from ledger button that reads the nest
material's free sheets and drops from MaterialLedger's API-key-protected
GET /api/stock/opennest. Ledger rows are request-local: the engine sees them as
ordinary NestPlateStock rows with ledger:{n} ids and finite quantities, and they
are never written to Nest.PlateOptions.

- OpenNest.Data/MaterialLedger: settings file, read-only client (X-Api-Key,
  5 s timeout, closed LedgerStockResult set) and LedgerStockMapper (bundles and
  sheets one row each, same-size drops grouped with ordered item codes, unknown
  shapes and too-small items skipped, inch/mm conversion).
- Options: MaterialLedger URL and API key (DPAPI, CurrentUser) with Test.
- Auto Nest grid: Use, Qty and Source columns for every row; PlateOption gains
  request-local Quantity and Id, which NestStockBuilder passes to the engine.
  Ledger rows score by area; hand-typed costs stay saved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 17:40:44 -04:00

129 lines
5.0 KiB
C#

using OpenNest.Data.MaterialLedger;
using OpenNest.Geometry;
namespace OpenNest.Tests.Data;
public class LedgerStockMapperTests
{
[Fact]
public void BundlesAndSheetsAreOneRowEachAndDropsOfOneSizeShareARow()
{
var mapping = LedgerStockMapper.Map(new[]
{
Item("B-1", LedgerItemKinds.FullSheetBundle, 48, 96, quantity: 3),
Item("S-1", LedgerItemKinds.FullSheet, 48, 96, quantity: 1),
Item("D-2", LedgerItemKinds.Drop, 24, 36, location: "R2"),
Item("D-9", LedgerItemKinds.Drop, 30, 40, location: "R1"),
Item("D-1", LedgerItemKinds.Drop, 36.0004m, 24, location: "R1"),
Item("D-3", LedgerItemKinds.Drop, 24, 36, location: "R1"),
}, Units.Inches);
Assert.Equal(4, mapping.Rows.Count);
var bundle = mapping.Rows[0];
Assert.Equal("ledger:1", bundle.Id);
Assert.False(bundle.IsDrop);
Assert.Equal(3, bundle.Quantity);
Assert.Equal(new[] { "B-1", "B-1", "B-1" }, bundle.ItemCodes);
Assert.Equal(new Size(48, 96), bundle.Size);
Assert.Equal(1, mapping.Rows[1].Quantity);
// Width is the short side; drops matching after ML's 3-place rounding group, ordered by location then code.
var drops = mapping.Rows[2];
Assert.True(drops.IsDrop);
Assert.Equal(new Size(24, 36), drops.Size);
Assert.Equal(3, drops.Quantity);
Assert.Equal(new[] { "D-1", "D-3", "D-2" }, drops.ItemCodes);
Assert.Equal(new[] { "R1", "R2" }, drops.Locations);
Assert.Equal(new[] { "D-9" }, mapping.Rows[3].ItemCodes);
Assert.Equal(2, mapping.SheetRows);
Assert.Equal(4, mapping.Drops);
Assert.Equal("Ledger: 2 sheet sizes, 4 drops", mapping.Summary);
}
[Fact]
public void UnknownShapesAreSkippedAndCounted()
{
var mapping = LedgerStockMapper.Map(new[]
{
Item("U-1", LedgerItemKinds.Drop, 20, 30, shape: LedgerItemShapes.Unknown),
Item("U-2", LedgerItemKinds.Drop, 20, 30, shape: "SomeFutureShape"),
Item("S-1", LedgerItemKinds.FullSheet, 60, 120),
}, Units.Inches);
Assert.Equal("S-1", Assert.Single(Assert.Single(mapping.Rows).ItemCodes));
Assert.Equal(2, mapping.SkippedUnknownShape);
Assert.Equal("Ledger: 1 sheet size, 0 drops (2 skipped: 2 unknown shape)", mapping.Summary);
}
[Fact]
public void IrregularDropsAreMarkedAndKeptApartFromRectangularOnes()
{
var mapping = LedgerStockMapper.Map(new[]
{
Item("D-1", LedgerItemKinds.Drop, 20, 30),
Item("D-2", LedgerItemKinds.Drop, 20, 30, shape: LedgerItemShapes.IrregularConservativeRectangle),
}, Units.Inches);
Assert.Equal(2, mapping.Rows.Count);
Assert.False(mapping.Rows[0].IsIrregular);
Assert.True(mapping.Rows[1].IsIrregular);
}
[Fact]
public void ItemsNoPartFitsAreSkipped()
{
var mapping = LedgerStockMapper.Map(new[]
{
Item("D-1", LedgerItemKinds.Drop, 10, 50),
Item("D-2", LedgerItemKinds.Drop, 20, 25),
Item("D-3", LedgerItemKinds.Drop, 5, 5),
}, Units.Inches, new[] { new Size(22, 12), new Size(40, 30) });
Assert.Equal("D-2", Assert.Single(Assert.Single(mapping.Rows).ItemCodes));
Assert.Equal(2, mapping.SkippedTooSmall);
Assert.Contains("2 too small for any part", mapping.Summary);
}
[Fact]
public void HeldOutItemsAndUnknownKindsAreIgnored()
{
var mapping = LedgerStockMapper.Map(new[]
{
Item("S-1", LedgerItemKinds.FullSheet, 48, 96, quantity: 0),
Item("C-1", "Coil", 48, 96),
}, Units.Inches);
Assert.Empty(mapping.Rows);
}
[Fact]
public void MillimeterNestsGetMillimeterSizes()
{
var row = Assert.Single(LedgerStockMapper.Map(new[] { Item("S-1", LedgerItemKinds.FullSheet, 48, 96.5m) },
Units.Millimeters).Rows);
Assert.Equal(48 * 25.4, row.Size.Width, 9);
Assert.Equal(96.5 * 25.4, row.Size.Length, 9);
}
[Theory]
[InlineData(0.25, Units.Inches, 0.25)]
[InlineData(0.18751, Units.Inches, 0.1875)]
[InlineData(6.35, Units.Millimeters, 0.25)]
[InlineData(3, Units.Millimeters, 0.1181)]
public void ThicknessQueryIsInchesToFourPlaces(double thickness, Units units, double expected) =>
Assert.Equal(expected, LedgerStockMapper.ThicknessInches(thickness, units));
[Fact]
public void LedgerIdsAreRecognized()
{
Assert.True(LedgerStockMapper.IsLedgerId("ledger:3"));
Assert.False(LedgerStockMapper.IsLedgerId("option-0"));
Assert.False(LedgerStockMapper.IsLedgerId(null));
}
private static LedgerStockItem Item(string code, string kind, decimal width, decimal length, int quantity = 1,
string shape = LedgerItemShapes.Rectangular, string location = "R1") =>
new(code, 7, kind, width, length, quantity, shape, location, location, null, null, DateTime.UtcNow);
}