# FabWorks.Api Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Create a standalone ASP.NET Web API (`FabWorks.Api`) that owns all fabrication data — export records, BOM items, cut templates, form programs — so ExportDXF and other tools communicate via HTTP instead of direct database access. **Architecture:** FabWorks.Api is a minimal ASP.NET Web API targeting net8.0. It uses EF Core with SQL Server (same database ExportDXF already uses). A shared class library (`FabWorks.Core`) holds the entity models and DbContext so both the API and ExportDXF can share the schema during the migration period. CincyLib's press brake models are copied into FabWorks.Core as a net8.0 port (just the PressBrake folder — pure XML parsing, no dependencies). The API exposes RESTful endpoints for export records, BOM items, cut templates, and form programs. **Tech Stack:** ASP.NET Web API (net8.0), EF Core 8 + SQL Server, CincyLib PressBrake parser (ported to net8.0), xUnit for tests --- ## Project Layout ``` ExportDXF/ (solution root) ├── ExportDXF.sln (add new projects here) ├── ExportDXF/ (existing WinForms app) ├── FabWorks.Core/ ← NEW shared library │ ├── FabWorks.Core.csproj │ ├── Data/ │ │ └── FabWorksDbContext.cs │ ├── Models/ │ │ ├── ExportRecord.cs │ │ ├── BomItem.cs │ │ ├── CutTemplate.cs │ │ └── FormProgram.cs ← NEW entity │ └── PressBrake/ ← Ported from CincyLib │ ├── Program.cs │ ├── ProgramReader.cs │ ├── Step.cs │ ├── ToolSetup.cs │ ├── SegEntry.cs │ ├── MatType.cs │ └── Extensions.cs ├── FabWorks.Api/ ← NEW web API │ ├── FabWorks.Api.csproj │ ├── Program.cs │ ├── Controllers/ │ │ ├── ExportsController.cs │ │ ├── BomItemsController.cs │ │ └── FormProgramsController.cs │ ├── DTOs/ │ │ ├── CreateExportRequest.cs │ │ ├── ExportDetailDto.cs │ │ ├── BomItemDto.cs │ │ └── FormProgramDto.cs │ └── Services/ │ └── FormProgramService.cs └── FabWorks.Tests/ ← NEW test project ├── FabWorks.Tests.csproj ├── FormProgramServiceTests.cs ├── ExportsControllerTests.cs └── PressBrake/ └── ProgramReaderTests.cs ``` --- ## Task 1: Create FabWorks.Core with Shared Models **Files:** - Create: `FabWorks.Core/FabWorks.Core.csproj` - Create: `FabWorks.Core/Models/ExportRecord.cs` - Create: `FabWorks.Core/Models/BomItem.cs` - Create: `FabWorks.Core/Models/CutTemplate.cs` - Create: `FabWorks.Core/Models/FormProgram.cs` - Create: `FabWorks.Core/Data/FabWorksDbContext.cs` **Step 1: Create the class library project** ```bash cd C:\Users\aisaacs\Desktop\Projects\ExportDXF dotnet new classlib -n FabWorks.Core --framework net8.0 dotnet sln ExportDXF.sln add FabWorks.Core/FabWorks.Core.csproj ``` **Step 2: Add EF Core package to FabWorks.Core** ```bash cd C:\Users\aisaacs\Desktop\Projects\ExportDXF\FabWorks.Core dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 8.0.11 dotnet add package Microsoft.EntityFrameworkCore.Tools --version 8.0.11 ``` **Step 3: Create entity models** Move/recreate the existing entity models from `ExportDXF/Models/` into `FabWorks.Core/Models/` under namespace `FabWorks.Core.Models`. Keep the exact same schema so the existing database is compatible. `FabWorks.Core/Models/ExportRecord.cs`: ```csharp using System; using System.Collections.Generic; namespace FabWorks.Core.Models { public class ExportRecord { public int Id { get; set; } public string DrawingNumber { get; set; } public string SourceFilePath { get; set; } public string OutputFolder { get; set; } public DateTime ExportedAt { get; set; } public string ExportedBy { get; set; } public string PdfContentHash { get; set; } public virtual ICollection BomItems { get; set; } = new List(); } } ``` `FabWorks.Core/Models/BomItem.cs`: ```csharp namespace FabWorks.Core.Models { public class BomItem { public int ID { get; set; } public string ItemNo { get; set; } = ""; public string PartNo { get; set; } = ""; public int SortOrder { get; set; } public int? Qty { get; set; } public int? TotalQty { get; set; } public string Description { get; set; } = ""; public string PartName { get; set; } = ""; public string ConfigurationName { get; set; } = ""; public string Material { get; set; } = ""; public int ExportRecordId { get; set; } public virtual ExportRecord ExportRecord { get; set; } public virtual CutTemplate CutTemplate { get; set; } public virtual FormProgram FormProgram { get; set; } } } ``` `FabWorks.Core/Models/CutTemplate.cs`: ```csharp using System; namespace FabWorks.Core.Models { public class CutTemplate { public int Id { get; set; } public string DxfFilePath { get; set; } = ""; public string ContentHash { get; set; } public string CutTemplateName { get; set; } = ""; private double? _thickness; public double? Thickness { get => _thickness; set => _thickness = value.HasValue ? Math.Round(value.Value, 8) : null; } public double? KFactor { get; set; } private double? _defaultBendRadius; public double? DefaultBendRadius { get => _defaultBendRadius; set => _defaultBendRadius = value.HasValue ? Math.Round(value.Value, 8) : null; } public int BomItemId { get; set; } public virtual BomItem BomItem { get; set; } } } ``` `FabWorks.Core/Models/FormProgram.cs` (NEW): ```csharp namespace FabWorks.Core.Models { public class FormProgram { public int Id { get; set; } public string ProgramFilePath { get; set; } = ""; public string ContentHash { get; set; } public string ProgramName { get; set; } = ""; public double? Thickness { get; set; } public string MaterialType { get; set; } = ""; public double? KFactor { get; set; } public int BendCount { get; set; } public string UpperToolNames { get; set; } = ""; public string LowerToolNames { get; set; } = ""; public string SetupNotes { get; set; } = ""; public int BomItemId { get; set; } public virtual BomItem BomItem { get; set; } } } ``` **Step 4: Create FabWorksDbContext** `FabWorks.Core/Data/FabWorksDbContext.cs`: ```csharp using FabWorks.Core.Models; using Microsoft.EntityFrameworkCore; namespace FabWorks.Core.Data { public class FabWorksDbContext : DbContext { public DbSet ExportRecords { get; set; } public DbSet BomItems { get; set; } public DbSet CutTemplates { get; set; } public DbSet FormPrograms { get; set; } public FabWorksDbContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.DrawingNumber).HasMaxLength(100); entity.Property(e => e.SourceFilePath).HasMaxLength(500); entity.Property(e => e.OutputFolder).HasMaxLength(500); entity.Property(e => e.ExportedBy).HasMaxLength(100); entity.Property(e => e.PdfContentHash).HasMaxLength(64); entity.HasMany(e => e.BomItems) .WithOne(b => b.ExportRecord) .HasForeignKey(b => b.ExportRecordId) .OnDelete(DeleteBehavior.Cascade); }); modelBuilder.Entity(entity => { entity.HasKey(e => e.ID); entity.Property(e => e.ItemNo).HasMaxLength(50); entity.Property(e => e.PartNo).HasMaxLength(100); entity.Property(e => e.Description).HasMaxLength(500); entity.Property(e => e.PartName).HasMaxLength(200); entity.Property(e => e.ConfigurationName).HasMaxLength(100); entity.Property(e => e.Material).HasMaxLength(100); entity.HasOne(e => e.CutTemplate) .WithOne(ct => ct.BomItem) .HasForeignKey(ct => ct.BomItemId) .OnDelete(DeleteBehavior.Cascade); entity.HasOne(e => e.FormProgram) .WithOne(fp => fp.BomItem) .HasForeignKey(fp => fp.BomItemId) .OnDelete(DeleteBehavior.Cascade); }); modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.DxfFilePath).HasMaxLength(500); entity.Property(e => e.CutTemplateName).HasMaxLength(100); entity.Property(e => e.ContentHash).HasMaxLength(64); }); modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); entity.Property(e => e.ProgramFilePath).HasMaxLength(500); entity.Property(e => e.ContentHash).HasMaxLength(64); entity.Property(e => e.ProgramName).HasMaxLength(200); entity.Property(e => e.MaterialType).HasMaxLength(50); entity.Property(e => e.UpperToolNames).HasMaxLength(500); entity.Property(e => e.LowerToolNames).HasMaxLength(500); entity.Property(e => e.SetupNotes).HasMaxLength(2000); }); } } } ``` **Step 5: Delete auto-generated Class1.cs and build** ```bash del FabWorks.Core\Class1.cs dotnet build FabWorks.Core/FabWorks.Core.csproj ``` **Step 6: Commit** ```bash git add FabWorks.Core/ git commit -m "feat: add FabWorks.Core shared library with entity models and FormProgram" ``` --- ## Task 2: Port CincyLib PressBrake Parser into FabWorks.Core **Files:** - Create: `FabWorks.Core/PressBrake/Program.cs` - Create: `FabWorks.Core/PressBrake/ProgramReader.cs` - Create: `FabWorks.Core/PressBrake/Step.cs` - Create: `FabWorks.Core/PressBrake/ToolSetup.cs` - Create: `FabWorks.Core/PressBrake/SegEntry.cs` - Create: `FabWorks.Core/PressBrake/MatType.cs` - Create: `FabWorks.Core/PressBrake/Extensions.cs` **Step 1: Copy CincyLib PressBrake files** Copy the following files from `C:\Users\aisaacs\Desktop\Projects\CincyLib\CincyLib\PressBrake\` into `FabWorks.Core\PressBrake\`: - `Program.cs` - `ProgramReader.cs` - `Step.cs` - `ToolSetup.cs` - `SegEntry.cs` - `MatType.cs` Also copy `C:\Users\aisaacs\Desktop\Projects\CincyLib\CincyLib\Extensions.cs` into `FabWorks.Core\PressBrake\Extensions.cs`. **Step 2: Update namespaces** Change all `namespace CincyLib.PressBrake` to `namespace FabWorks.Core.PressBrake`. Change `namespace CincyLib` (in Extensions.cs) to `namespace FabWorks.Core.PressBrake`. No other code changes needed — these files only use `System`, `System.IO`, `System.Linq`, `System.Xml.Linq`, `System.Collections.Generic`, all available in net8.0. **Step 3: Build to verify port** ```bash dotnet build FabWorks.Core/FabWorks.Core.csproj ``` **Step 4: Commit** ```bash git add FabWorks.Core/PressBrake/ git commit -m "feat: port CincyLib PressBrake parser to FabWorks.Core (net8.0)" ``` --- ## Task 3: Create FabWorks.Tests and Validate PressBrake Parser **Files:** - Create: `FabWorks.Tests/FabWorks.Tests.csproj` - Create: `FabWorks.Tests/PressBrake/ProgramReaderTests.cs` - Create: `FabWorks.Tests/TestData/` (copy a sample .pgm file) **Step 1: Create test project** ```bash cd C:\Users\aisaacs\Desktop\Projects\ExportDXF dotnet new xunit -n FabWorks.Tests --framework net8.0 dotnet sln ExportDXF.sln add FabWorks.Tests/FabWorks.Tests.csproj cd FabWorks.Tests dotnet add reference ../FabWorks.Core/FabWorks.Core.csproj ``` **Step 2: Copy test data** Copy a sample `.pgm` file into `FabWorks.Tests/TestData/sample.pgm`. Use the file from `S:\4980 GMCH Lockport\4980 A05 Degreaser Casing\4980 A05-1 Access door\Forms\4980 A05-1 PT02.pgm`. Add it to the csproj as ``. **Step 3: Write ProgramReader test** `FabWorks.Tests/PressBrake/ProgramReaderTests.cs`: ```csharp using FabWorks.Core.PressBrake; using Xunit; namespace FabWorks.Tests.PressBrake { public class ProgramReaderTests { [Fact] public void Load_SamplePgm_ParsesProgramName() { var pgm = Program.Load("TestData/sample.pgm"); Assert.False(string.IsNullOrEmpty(pgm.ProgName)); } [Fact] public void Load_SamplePgm_ParsesThickness() { var pgm = Program.Load("TestData/sample.pgm"); Assert.True(pgm.MatThick > 0); } [Fact] public void Load_SamplePgm_ParsesSteps() { var pgm = Program.Load("TestData/sample.pgm"); Assert.NotEmpty(pgm.Steps); } [Fact] public void Load_SamplePgm_ParsesToolSetups() { var pgm = Program.Load("TestData/sample.pgm"); Assert.NotEmpty(pgm.UpperToolSets); Assert.NotEmpty(pgm.LowerToolSets); } [Fact] public void Load_SamplePgm_ResolvesStepToolReferences() { var pgm = Program.Load("TestData/sample.pgm"); var step = pgm.Steps[0]; Assert.NotNull(step.UpperTool); Assert.NotNull(step.LowerTool); } } } ``` **Step 4: Run tests** ```bash dotnet test FabWorks.Tests/FabWorks.Tests.csproj -v normal ``` Expected: All 5 tests PASS. **Step 5: Commit** ```bash git add FabWorks.Tests/ git commit -m "test: add ProgramReader tests validating CincyLib port" ``` --- ## Task 4: Create FabWorks.Api Project with Exports Endpoint **Files:** - Create: `FabWorks.Api/FabWorks.Api.csproj` - Create: `FabWorks.Api/Program.cs` - Create: `FabWorks.Api/Controllers/ExportsController.cs` - Create: `FabWorks.Api/DTOs/CreateExportRequest.cs` - Create: `FabWorks.Api/DTOs/ExportDetailDto.cs` **Step 1: Create web API project** ```bash cd C:\Users\aisaacs\Desktop\Projects\ExportDXF dotnet new webapi -n FabWorks.Api --framework net8.0 --no-openapi dotnet sln ExportDXF.sln add FabWorks.Api/FabWorks.Api.csproj cd FabWorks.Api dotnet add reference ../FabWorks.Core/FabWorks.Core.csproj ``` **Step 2: Configure Program.cs with EF Core** `FabWorks.Api/Program.cs`: ```csharp using FabWorks.Core.Data; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddDbContext(options => options.UseSqlServer(builder.Configuration.GetConnectionString("FabWorksDb"))); var app = builder.Build(); app.MapControllers(); app.Run(); ``` Add connection string to `appsettings.json`: ```json { "ConnectionStrings": { "FabWorksDb": "Server=localhost;Database=ExportDxfDb;Trusted_Connection=True;TrustServerCertificate=True;" } } ``` Note: Points at the same `ExportDxfDb` database. Same schema, just adding the FormPrograms table. **Step 3: Create DTOs** `FabWorks.Api/DTOs/CreateExportRequest.cs`: ```csharp namespace FabWorks.Api.DTOs { public class CreateExportRequest { public string DrawingNumber { get; set; } public string SourceFilePath { get; set; } public string OutputFolder { get; set; } } } ``` `FabWorks.Api/DTOs/ExportDetailDto.cs`: ```csharp using System; using System.Collections.Generic; namespace FabWorks.Api.DTOs { public class ExportDetailDto { public int Id { get; set; } public string DrawingNumber { get; set; } public string SourceFilePath { get; set; } public string OutputFolder { get; set; } public DateTime ExportedAt { get; set; } public string ExportedBy { get; set; } public string PdfContentHash { get; set; } public List BomItems { get; set; } = new(); } public class BomItemDto { public int ID { get; set; } public string ItemNo { get; set; } public string PartNo { get; set; } public int SortOrder { get; set; } public int? Qty { get; set; } public int? TotalQty { get; set; } public string Description { get; set; } public string PartName { get; set; } public string ConfigurationName { get; set; } public string Material { get; set; } public CutTemplateDto CutTemplate { get; set; } public FormProgramDto FormProgram { get; set; } } public class CutTemplateDto { public int Id { get; set; } public string DxfFilePath { get; set; } public string ContentHash { get; set; } public double? Thickness { get; set; } public double? KFactor { get; set; } public double? DefaultBendRadius { get; set; } } public class FormProgramDto { public int Id { get; set; } public string ProgramFilePath { get; set; } public string ContentHash { get; set; } public string ProgramName { get; set; } public double? Thickness { get; set; } public string MaterialType { get; set; } public double? KFactor { get; set; } public int BendCount { get; set; } public string UpperToolNames { get; set; } public string LowerToolNames { get; set; } public string SetupNotes { get; set; } } } ``` **Step 4: Create ExportsController** `FabWorks.Api/Controllers/ExportsController.cs`: ```csharp using FabWorks.Api.DTOs; using FabWorks.Core.Data; using FabWorks.Core.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace FabWorks.Api.Controllers { [ApiController] [Route("api/[controller]")] public class ExportsController : ControllerBase { private readonly FabWorksDbContext _db; public ExportsController(FabWorksDbContext db) { _db = db; } [HttpPost] public async Task> Create(CreateExportRequest request) { var record = new ExportRecord { DrawingNumber = request.DrawingNumber, SourceFilePath = request.SourceFilePath, OutputFolder = request.OutputFolder, ExportedAt = DateTime.Now, ExportedBy = Environment.UserName }; _db.ExportRecords.Add(record); await _db.SaveChangesAsync(); return CreatedAtAction(nameof(GetById), new { id = record.Id }, MapToDto(record)); } [HttpGet("{id}")] public async Task> GetById(int id) { var record = await _db.ExportRecords .Include(r => r.BomItems).ThenInclude(b => b.CutTemplate) .Include(r => r.BomItems).ThenInclude(b => b.FormProgram) .FirstOrDefaultAsync(r => r.Id == id); if (record == null) return NotFound(); return MapToDto(record); } [HttpGet("by-source")] public async Task> GetBySourceFile([FromQuery] string path) { var record = await _db.ExportRecords .Where(r => r.SourceFilePath.ToLower() == path.ToLower() && !string.IsNullOrEmpty(r.DrawingNumber)) .OrderByDescending(r => r.Id) .FirstOrDefaultAsync(); if (record == null) return NotFound(); return MapToDto(record); } [HttpGet("by-drawing")] public async Task>> GetByDrawing([FromQuery] string drawingNumber) { var records = await _db.ExportRecords .Include(r => r.BomItems).ThenInclude(b => b.CutTemplate) .Include(r => r.BomItems).ThenInclude(b => b.FormProgram) .Where(r => r.DrawingNumber == drawingNumber) .OrderByDescending(r => r.ExportedAt) .ToListAsync(); return records.Select(MapToDto).ToList(); } [HttpGet("next-item-number")] public async Task> GetNextItemNumber([FromQuery] string drawingNumber) { if (string.IsNullOrEmpty(drawingNumber)) return "1"; var existingItems = await _db.ExportRecords .Where(r => r.DrawingNumber == drawingNumber) .SelectMany(r => r.BomItems) .Select(b => b.ItemNo) .ToListAsync(); int maxNum = 0; foreach (var itemNo in existingItems) { if (int.TryParse(itemNo, out var num) && num > maxNum) maxNum = num; } return (maxNum + 1).ToString(); } private static ExportDetailDto MapToDto(ExportRecord r) => new() { Id = r.Id, DrawingNumber = r.DrawingNumber, SourceFilePath = r.SourceFilePath, OutputFolder = r.OutputFolder, ExportedAt = r.ExportedAt, ExportedBy = r.ExportedBy, PdfContentHash = r.PdfContentHash, BomItems = r.BomItems?.Select(b => new BomItemDto { ID = b.ID, ItemNo = b.ItemNo, PartNo = b.PartNo, SortOrder = b.SortOrder, Qty = b.Qty, TotalQty = b.TotalQty, Description = b.Description, PartName = b.PartName, ConfigurationName = b.ConfigurationName, Material = b.Material, CutTemplate = b.CutTemplate == null ? null : new CutTemplateDto { Id = b.CutTemplate.Id, DxfFilePath = b.CutTemplate.DxfFilePath, ContentHash = b.CutTemplate.ContentHash, Thickness = b.CutTemplate.Thickness, KFactor = b.CutTemplate.KFactor, DefaultBendRadius = b.CutTemplate.DefaultBendRadius }, FormProgram = b.FormProgram == null ? null : new FormProgramDto { Id = b.FormProgram.Id, ProgramFilePath = b.FormProgram.ProgramFilePath, ContentHash = b.FormProgram.ContentHash, ProgramName = b.FormProgram.ProgramName, Thickness = b.FormProgram.Thickness, MaterialType = b.FormProgram.MaterialType, KFactor = b.FormProgram.KFactor, BendCount = b.FormProgram.BendCount, UpperToolNames = b.FormProgram.UpperToolNames, LowerToolNames = b.FormProgram.LowerToolNames, SetupNotes = b.FormProgram.SetupNotes } }).ToList() ?? new() }; } } ``` **Step 5: Clean up auto-generated files, build** Delete the auto-generated `WeatherForecast.cs` and `Controllers/WeatherForecastController.cs` if present. ```bash dotnet build FabWorks.Api/FabWorks.Api.csproj ``` **Step 6: Commit** ```bash git add FabWorks.Api/ git commit -m "feat: add FabWorks.Api with ExportsController and DTOs" ``` --- ## Task 5: Add BomItems and FormPrograms Controllers **Files:** - Create: `FabWorks.Api/Controllers/BomItemsController.cs` - Create: `FabWorks.Api/Controllers/FormProgramsController.cs` - Create: `FabWorks.Api/Services/FormProgramService.cs` **Step 1: Create FormProgramService (wraps CincyLib parser)** `FabWorks.Api/Services/FormProgramService.cs`: ```csharp using FabWorks.Core.Models; using FabWorks.Core.PressBrake; using System.Security.Cryptography; namespace FabWorks.Api.Services { public class FormProgramService { /// /// Parse a .pgm file and return a FormProgram entity with metadata populated. /// public FormProgram ParseFromFile(string filePath) { var pgm = Program.Load(filePath); var hash = ComputeFileHash(filePath); return new FormProgram { ProgramFilePath = filePath, ContentHash = hash, ProgramName = pgm.ProgName ?? "", Thickness = pgm.MatThick > 0 ? pgm.MatThick : null, MaterialType = pgm.MatType.ToString(), KFactor = pgm.KFactor > 0 ? pgm.KFactor : null, BendCount = pgm.Steps.Count, UpperToolNames = string.Join(", ", pgm.UpperToolSets .Select(t => t.Name).Where(n => !string.IsNullOrEmpty(n)).Distinct()), LowerToolNames = string.Join(", ", pgm.LowerToolSets .Select(t => t.Name).Where(n => !string.IsNullOrEmpty(n)).Distinct()), SetupNotes = pgm.SetupNotes ?? "" }; } private static string ComputeFileHash(string filePath) { using var sha = SHA256.Create(); using var stream = File.OpenRead(filePath); var bytes = sha.ComputeHash(stream); return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant(); } } } ``` **Step 2: Register FormProgramService in DI** In `FabWorks.Api/Program.cs`, add: ```csharp builder.Services.AddSingleton(); ``` **Step 3: Create BomItemsController** `FabWorks.Api/Controllers/BomItemsController.cs`: ```csharp using FabWorks.Api.DTOs; using FabWorks.Core.Data; using FabWorks.Core.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace FabWorks.Api.Controllers { [ApiController] [Route("api/exports/{exportId}/bom-items")] public class BomItemsController : ControllerBase { private readonly FabWorksDbContext _db; public BomItemsController(FabWorksDbContext db) => _db = db; [HttpGet] public async Task>> GetByExport(int exportId) { var items = await _db.BomItems .Include(b => b.CutTemplate) .Include(b => b.FormProgram) .Where(b => b.ExportRecordId == exportId) .OrderBy(b => b.SortOrder) .ToListAsync(); return items.Select(MapToDto).ToList(); } [HttpPost] public async Task> Create(int exportId, BomItemDto dto) { var export = await _db.ExportRecords.FindAsync(exportId); if (export == null) return NotFound("Export record not found"); var item = new BomItem { ExportRecordId = exportId, ItemNo = dto.ItemNo ?? "", PartNo = dto.PartNo ?? "", SortOrder = dto.SortOrder, Qty = dto.Qty, TotalQty = dto.TotalQty, Description = dto.Description ?? "", PartName = dto.PartName ?? "", ConfigurationName = dto.ConfigurationName ?? "", Material = dto.Material ?? "" }; if (dto.CutTemplate != null) { item.CutTemplate = new CutTemplate { DxfFilePath = dto.CutTemplate.DxfFilePath ?? "", ContentHash = dto.CutTemplate.ContentHash, Thickness = dto.CutTemplate.Thickness, KFactor = dto.CutTemplate.KFactor, DefaultBendRadius = dto.CutTemplate.DefaultBendRadius }; } if (dto.FormProgram != null) { item.FormProgram = new FormProgram { ProgramFilePath = dto.FormProgram.ProgramFilePath ?? "", ContentHash = dto.FormProgram.ContentHash, ProgramName = dto.FormProgram.ProgramName ?? "", Thickness = dto.FormProgram.Thickness, MaterialType = dto.FormProgram.MaterialType ?? "", KFactor = dto.FormProgram.KFactor, BendCount = dto.FormProgram.BendCount, UpperToolNames = dto.FormProgram.UpperToolNames ?? "", LowerToolNames = dto.FormProgram.LowerToolNames ?? "", SetupNotes = dto.FormProgram.SetupNotes ?? "" }; } _db.BomItems.Add(item); await _db.SaveChangesAsync(); return CreatedAtAction(nameof(GetByExport), new { exportId }, MapToDto(item)); } private static BomItemDto MapToDto(BomItem b) => new() { ID = b.ID, ItemNo = b.ItemNo, PartNo = b.PartNo, SortOrder = b.SortOrder, Qty = b.Qty, TotalQty = b.TotalQty, Description = b.Description, PartName = b.PartName, ConfigurationName = b.ConfigurationName, Material = b.Material, CutTemplate = b.CutTemplate == null ? null : new CutTemplateDto { Id = b.CutTemplate.Id, DxfFilePath = b.CutTemplate.DxfFilePath, ContentHash = b.CutTemplate.ContentHash, Thickness = b.CutTemplate.Thickness, KFactor = b.CutTemplate.KFactor, DefaultBendRadius = b.CutTemplate.DefaultBendRadius }, FormProgram = b.FormProgram == null ? null : new FormProgramDto { Id = b.FormProgram.Id, ProgramFilePath = b.FormProgram.ProgramFilePath, ContentHash = b.FormProgram.ContentHash, ProgramName = b.FormProgram.ProgramName, Thickness = b.FormProgram.Thickness, MaterialType = b.FormProgram.MaterialType, KFactor = b.FormProgram.KFactor, BendCount = b.FormProgram.BendCount, UpperToolNames = b.FormProgram.UpperToolNames, LowerToolNames = b.FormProgram.LowerToolNames, SetupNotes = b.FormProgram.SetupNotes } }; } } ``` **Step 4: Create FormProgramsController** `FabWorks.Api/Controllers/FormProgramsController.cs`: ```csharp using FabWorks.Api.DTOs; using FabWorks.Api.Services; using FabWorks.Core.Data; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace FabWorks.Api.Controllers { [ApiController] [Route("api/form-programs")] public class FormProgramsController : ControllerBase { private readonly FabWorksDbContext _db; private readonly FormProgramService _formService; public FormProgramsController(FabWorksDbContext db, FormProgramService formService) { _db = db; _formService = formService; } [HttpGet("by-drawing")] public async Task>> GetByDrawing([FromQuery] string drawingNumber) { var programs = await _db.FormPrograms .Include(fp => fp.BomItem) .ThenInclude(b => b.ExportRecord) .Where(fp => fp.BomItem.ExportRecord.DrawingNumber == drawingNumber) .ToListAsync(); return programs.Select(fp => new FormProgramDto { Id = fp.Id, ProgramFilePath = fp.ProgramFilePath, ContentHash = fp.ContentHash, ProgramName = fp.ProgramName, Thickness = fp.Thickness, MaterialType = fp.MaterialType, KFactor = fp.KFactor, BendCount = fp.BendCount, UpperToolNames = fp.UpperToolNames, LowerToolNames = fp.LowerToolNames, SetupNotes = fp.SetupNotes }).ToList(); } [HttpPost("parse")] public ActionResult Parse([FromQuery] string filePath) { if (!System.IO.File.Exists(filePath)) return NotFound($"File not found: {filePath}"); var fp = _formService.ParseFromFile(filePath); return new FormProgramDto { ProgramFilePath = fp.ProgramFilePath, ContentHash = fp.ContentHash, ProgramName = fp.ProgramName, Thickness = fp.Thickness, MaterialType = fp.MaterialType, KFactor = fp.KFactor, BendCount = fp.BendCount, UpperToolNames = fp.UpperToolNames, LowerToolNames = fp.LowerToolNames, SetupNotes = fp.SetupNotes }; } [HttpPost("{bomItemId}")] public async Task> AttachToItem(int bomItemId, [FromQuery] string filePath) { var bomItem = await _db.BomItems .Include(b => b.FormProgram) .FirstOrDefaultAsync(b => b.ID == bomItemId); if (bomItem == null) return NotFound("BOM item not found"); if (!System.IO.File.Exists(filePath)) return NotFound($"File not found: {filePath}"); var fp = _formService.ParseFromFile(filePath); fp.BomItemId = bomItemId; // Replace existing if present if (bomItem.FormProgram != null) _db.FormPrograms.Remove(bomItem.FormProgram); bomItem.FormProgram = fp; await _db.SaveChangesAsync(); return new FormProgramDto { Id = fp.Id, ProgramFilePath = fp.ProgramFilePath, ContentHash = fp.ContentHash, ProgramName = fp.ProgramName, Thickness = fp.Thickness, MaterialType = fp.MaterialType, KFactor = fp.KFactor, BendCount = fp.BendCount, UpperToolNames = fp.UpperToolNames, LowerToolNames = fp.LowerToolNames, SetupNotes = fp.SetupNotes }; } } } ``` **Step 5: Build** ```bash dotnet build FabWorks.Api/FabWorks.Api.csproj ``` **Step 6: Commit** ```bash git add FabWorks.Api/ git commit -m "feat: add BomItems and FormPrograms controllers with parse service" ``` --- ## Task 6: Add EF Core Migration for FormPrograms Table **Step 1: Add migration from FabWorks.Api** ```bash cd C:\Users\aisaacs\Desktop\Projects\ExportDXF\FabWorks.Api dotnet ef migrations add AddFormPrograms --project ../FabWorks.Core/FabWorks.Core.csproj --startup-project . ``` Note: If the migration tooling complains about the existing tables, we may need a baseline migration. The existing ExportDxfDb already has ExportRecords, BomItems, CutTemplates tables. The migration should only add the FormPrograms table and the FK on BomItems. **Step 2: Apply migration** ```bash dotnet ef database update --project ../FabWorks.Core/FabWorks.Core.csproj --startup-project . ``` **Step 3: Commit** ```bash git add FabWorks.Core/Migrations/ git commit -m "feat: add EF migration for FormPrograms table" ``` --- ## Task 7: Add FormProgramService Tests **Files:** - Create: `FabWorks.Tests/FormProgramServiceTests.cs` **Step 1: Write tests** `FabWorks.Tests/FormProgramServiceTests.cs`: ```csharp using FabWorks.Api.Services; using Xunit; namespace FabWorks.Tests { public class FormProgramServiceTests { [Fact] public void ParseFromFile_SamplePgm_PopulatesProgramName() { var service = new FormProgramService(); var fp = service.ParseFromFile("TestData/sample.pgm"); Assert.False(string.IsNullOrEmpty(fp.ProgramName)); } [Fact] public void ParseFromFile_SamplePgm_PopulatesThickness() { var service = new FormProgramService(); var fp = service.ParseFromFile("TestData/sample.pgm"); Assert.NotNull(fp.Thickness); Assert.True(fp.Thickness > 0); } [Fact] public void ParseFromFile_SamplePgm_PopulatesBendCount() { var service = new FormProgramService(); var fp = service.ParseFromFile("TestData/sample.pgm"); Assert.True(fp.BendCount > 0); } [Fact] public void ParseFromFile_SamplePgm_PopulatesToolNames() { var service = new FormProgramService(); var fp = service.ParseFromFile("TestData/sample.pgm"); Assert.False(string.IsNullOrEmpty(fp.UpperToolNames)); Assert.False(string.IsNullOrEmpty(fp.LowerToolNames)); } [Fact] public void ParseFromFile_SamplePgm_ComputesContentHash() { var service = new FormProgramService(); var fp = service.ParseFromFile("TestData/sample.pgm"); Assert.NotNull(fp.ContentHash); Assert.Equal(64, fp.ContentHash.Length); // SHA256 hex = 64 chars } } } ``` **Step 2: Add project reference to FabWorks.Api** ```bash cd C:\Users\aisaacs\Desktop\Projects\ExportDXF\FabWorks.Tests dotnet add reference ../FabWorks.Api/FabWorks.Api.csproj ``` **Step 3: Run all tests** ```bash dotnet test FabWorks.Tests/FabWorks.Tests.csproj -v normal ``` Expected: All 10 tests PASS. **Step 4: Commit** ```bash git add FabWorks.Tests/ git commit -m "test: add FormProgramService tests" ``` --- ## Task 8: Smoke Test — Run API and Hit Endpoints **Step 1: Run the API** ```bash cd C:\Users\aisaacs\Desktop\Projects\ExportDXF\FabWorks.Api dotnet run ``` **Step 2: Test endpoints with curl/Invoke-WebRequest** ```powershell # Create export record Invoke-RestMethod -Uri "http://localhost:5000/api/exports" -Method Post -ContentType "application/json" -Body '{"drawingNumber":"4980 A05-1","sourceFilePath":"C:\\test.slddrw","outputFolder":"C:\\output"}' # Get by ID Invoke-RestMethod -Uri "http://localhost:5000/api/exports/1" # Lookup by source file Invoke-RestMethod -Uri "http://localhost:5000/api/exports/by-source?path=C:\test.slddrw" # Parse a .pgm file Invoke-RestMethod -Uri "http://localhost:5000/api/form-programs/parse?filePath=S:\4980 GMCH Lockport\4980 A05 Degreaser Casing\4980 A05-1 Access door\Forms\4980 A05-1 PT02.pgm" ``` **Step 3: Verify JSON responses look correct** **Step 4: Commit any fixes needed** --- ## Future Tasks (Not In This Plan) These are follow-up items after the API is proven out: 1. **Wire ExportDXF to call FabWorks.Api** — Add `IFabWorksClient` interface, replace `_dbContextFactory` usage in `DxfExportService` with HTTP calls 2. **Deploy as Windows Service** — Add `Microsoft.Extensions.Hosting.WindowsServices`, create `deploy.ps1` per deploy script guide 3. **Add Swagger/OpenAPI** — For discoverability 4. **LaserQuote integration** — Point LaserQuote at FabWorks.Api for BOM/material lookups 5. **Batch form program scanner** — Endpoint to scan a directory tree and auto-link .pgm files to BOM items by naming convention