From ac033859a814998aaacc953caf70dcc6f5993d2f Mon Sep 17 00:00:00 2001 From: rex-bot Date: Mon, 27 Apr 2026 18:24:52 -0400 Subject: [PATCH] feat(CUB-28): [Extrudex] Define filament inventory database entities Add storage_location and is_archived fields to Spool entity to complete the filament inventory entity definition per CUB-28 requirements. Changes: - Spool entity: add IsArchived (bool, default false) and StorageLocation (nullable string, max 200) for physical inventory tracking - SpoolConfiguration: add snake_case column mappings, defaults, and indexes (ix_spools_is_archived, ix_spools_active_archived composite) - FilamentDtos: add IsArchived + StorageLocation to Response, Create, Update - FilamentQueryDtos: add IncludeArchived and StorageLocation query filters - FilamentsController: wire new fields into query, create, update, mapping - FilamentValidators: add StorageLocation max-length validation Build: PASS (0 errors) --- .../API/Controllers/FilamentsController.cs | 23 ++++++++++++++++--- backend/API/DTOs/Filaments/FilamentDtos.cs | 22 ++++++++++++++++++ .../API/DTOs/Filaments/FilamentQueryDtos.cs | 7 ++++++ backend/API/Validators/FilamentValidators.cs | 12 ++++++++++ backend/Domain/Entities/Spool.cs | 14 +++++++++++ .../Data/Configurations/SpoolConfiguration.cs | 17 ++++++++++++++ 6 files changed, 92 insertions(+), 3 deletions(-) diff --git a/backend/API/Controllers/FilamentsController.cs b/backend/API/Controllers/FilamentsController.cs index 5c62fec..5bb951e 100644 --- a/backend/API/Controllers/FilamentsController.cs +++ b/backend/API/Controllers/FilamentsController.cs @@ -46,9 +46,11 @@ public class FilamentsController : ControllerBase _logger.LogDebug( "Getting filaments: pageNumber={PageNumber}, pageSize={PageSize}, " + "materialBaseId={MaterialBaseId}, materialFinishId={MaterialFinishId}, " + - "materialModifierId={MaterialModifierId}, brand={Brand}, isActive={IsActive}", + "materialModifierId={MaterialModifierId}, brand={Brand}, isActive={IsActive}, " + + "includeArchived={IncludeArchived}, storageLocation={StorageLocation}", query.PageNumber, query.PageSize, query.MaterialBaseId, - query.MaterialFinishId, query.MaterialModifierId, query.Brand, query.IsActive); + query.MaterialFinishId, query.MaterialModifierId, query.Brand, query.IsActive, + query.IncludeArchived, query.StorageLocation); // Clamp pagination values var pageNumber = Math.Max(1, query.PageNumber); @@ -77,6 +79,15 @@ public class FilamentsController : ControllerBase if (query.IsActive.HasValue) spoolQuery = spoolQuery.Where(s => s.IsActive == query.IsActive.Value); + // Exclude archived spools by default; include when explicitly requested + if (query.IncludeArchived != true) + spoolQuery = spoolQuery.Where(s => !s.IsArchived); + + if (!string.IsNullOrWhiteSpace(query.StorageLocation)) + spoolQuery = spoolQuery.Where(s => + s.StorageLocation != null && + s.StorageLocation.ToLower().Contains(query.StorageLocation.ToLower())); + var totalCount = await spoolQuery.CountAsync(); var items = await spoolQuery @@ -185,7 +196,9 @@ public class FilamentsController : ControllerBase SpoolSerial = request.SpoolSerial, PurchasePrice = request.PurchasePrice, PurchaseDate = request.PurchaseDate, - IsActive = request.IsActive + IsActive = request.IsActive, + IsArchived = request.IsArchived, + StorageLocation = request.StorageLocation }; _dbContext.Spools.Add(entity); @@ -267,6 +280,8 @@ public class FilamentsController : ControllerBase entity.PurchasePrice = request.PurchasePrice; entity.PurchaseDate = request.PurchaseDate; entity.IsActive = request.IsActive; + entity.IsArchived = request.IsArchived; + entity.StorageLocation = request.StorageLocation; await _dbContext.SaveChangesAsync(); @@ -307,6 +322,8 @@ public class FilamentsController : ControllerBase PurchasePrice = s.PurchasePrice, PurchaseDate = s.PurchaseDate, IsActive = s.IsActive, + IsArchived = s.IsArchived, + StorageLocation = s.StorageLocation, CreatedAt = s.CreatedAt, UpdatedAt = s.UpdatedAt, QrCodeUrl = $"/api/qr/spool/{s.Id}" diff --git a/backend/API/DTOs/Filaments/FilamentDtos.cs b/backend/API/DTOs/Filaments/FilamentDtos.cs index 3b1b91b..75f9247 100644 --- a/backend/API/DTOs/Filaments/FilamentDtos.cs +++ b/backend/API/DTOs/Filaments/FilamentDtos.cs @@ -59,6 +59,12 @@ public class FilamentResponse /// Whether the spool is currently active and available. public bool IsActive { get; set; } + /// Whether the spool has been archived (removed from active inventory). + public bool IsArchived { get; set; } + + /// Physical storage location (e.g., "Shelf A", "Drawer 3"). Null if unset. + public string? StorageLocation { get; set; } + /// Timestamp when this record was created (UTC). public DateTime CreatedAt { get; set; } @@ -133,6 +139,15 @@ public class CreateFilamentRequest /// Whether the spool is active. Defaults to true. public bool IsActive { get; set; } = true; + + /// Whether the spool is archived. Defaults to false. + /// + public bool IsArchived { get; set; } = false; + + /// Physical storage location (e.g., "Shelf A", "Drawer 3"). Optional. + /// + [StringLength(200, ErrorMessage = "StorageLocation must not exceed 200 characters.")] + public string? StorageLocation { get; set; } } /// @@ -196,4 +211,11 @@ public class UpdateFilamentRequest /// Whether the spool is active. public bool IsActive { get; set; } = true; + + /// Whether the spool is archived. Defaults to false. + public bool IsArchived { get; set; } = false; + + /// Physical storage location (e.g., "Shelf A", "Drawer 3"). Optional. + [StringLength(200, ErrorMessage = "StorageLocation must not exceed 200 characters.")] + public string? StorageLocation { get; set; } } \ No newline at end of file diff --git a/backend/API/DTOs/Filaments/FilamentQueryDtos.cs b/backend/API/DTOs/Filaments/FilamentQueryDtos.cs index 98c9bd6..43936e7 100644 --- a/backend/API/DTOs/Filaments/FilamentQueryDtos.cs +++ b/backend/API/DTOs/Filaments/FilamentQueryDtos.cs @@ -30,4 +30,11 @@ public class FilamentQueryParameters /// Optional filter by active status. True = active only, False = inactive only. public bool? IsActive { get; set; } + + /// Whether to include archived spools in results. Defaults to false (excludes archived). + /// + public bool? IncludeArchived { get; set; } + + /// Optional filter by storage location (case-insensitive partial match). + public string? StorageLocation { get; set; } } \ No newline at end of file diff --git a/backend/API/Validators/FilamentValidators.cs b/backend/API/Validators/FilamentValidators.cs index 8fe0f18..9578b95 100644 --- a/backend/API/Validators/FilamentValidators.cs +++ b/backend/API/Validators/FilamentValidators.cs @@ -52,6 +52,12 @@ public class CreateFilamentRequestValidator : AbstractValidator x.PurchasePrice!.Value) .GreaterThanOrEqualTo(0).WithMessage("Purchase price must be non-negative."); }); + + When(x => x.StorageLocation != null, () => + { + RuleFor(x => x.StorageLocation!) + .MaximumLength(200).WithMessage("StorageLocation must not exceed 200 characters."); + }); } } @@ -104,5 +110,11 @@ public class UpdateFilamentRequestValidator : AbstractValidator x.PurchasePrice!.Value) .GreaterThanOrEqualTo(0).WithMessage("Purchase price must be non-negative."); }); + + When(x => x.StorageLocation != null, () => + { + RuleFor(x => x.StorageLocation!) + .MaximumLength(200).WithMessage("StorageLocation must not exceed 200 characters."); + }); } } \ No newline at end of file diff --git a/backend/Domain/Entities/Spool.cs b/backend/Domain/Entities/Spool.cs index 5084a7b..fd17d26 100644 --- a/backend/Domain/Entities/Spool.cs +++ b/backend/Domain/Entities/Spool.cs @@ -93,6 +93,20 @@ public class Spool : AuditableEntity /// public bool IsActive { get; set; } = true; + /// + /// Whether the spool has been archived (removed from active inventory). + /// Archived spools are retained for historical records but hidden from + /// default inventory views. Distinguishes long-term archival from + /// temporary inactivity (e.g., spool swapped out of AMS). + /// + public bool IsArchived { get; set; } = false; + + /// + /// Physical storage location of the spool (e.g., "Shelf A", "Drawer 3", "AMS Tray 2"). + /// Optional — not every spool has a designated storage location. + /// + public string? StorageLocation { get; set; } + /// /// Navigation collection of AMS slots where this spool is loaded. /// diff --git a/backend/Infrastructure/Data/Configurations/SpoolConfiguration.cs b/backend/Infrastructure/Data/Configurations/SpoolConfiguration.cs index a426906..19eff6d 100644 --- a/backend/Infrastructure/Data/Configurations/SpoolConfiguration.cs +++ b/backend/Infrastructure/Data/Configurations/SpoolConfiguration.cs @@ -68,6 +68,15 @@ public class SpoolConfiguration : BaseEntityConfiguration .HasDefaultValue(true) .IsRequired(); + builder.Property(e => e.IsArchived) + .HasColumnName("is_archived") + .HasDefaultValue(false) + .IsRequired(); + + builder.Property(e => e.StorageLocation) + .HasColumnName("storage_location") + .HasMaxLength(200); + // Unique index on spool_serial — critical for barcode/QR scanning builder.HasIndex(e => e.SpoolSerial) .IsUnique() @@ -89,6 +98,14 @@ public class SpoolConfiguration : BaseEntityConfiguration builder.HasIndex(e => e.IsActive) .HasDatabaseName("ix_spools_is_active"); + // Index on is_archived for inventory filtering (exclude archived from default views) + builder.HasIndex(e => e.IsArchived) + .HasDatabaseName("ix_spools_is_archived"); + + // Composite index on is_active + is_archived for common inventory queries + builder.HasIndex(e => new { e.IsActive, e.IsArchived }) + .HasDatabaseName("ix_spools_active_archived"); + // Relationships builder.HasOne(e => e.MaterialBase) .WithMany(e => e.Spools)