Merge pull request 'feat(CUB-28): [Extrudex] Define filament inventory database entities' (#24) from agent/hex/CUB-28-filament-inventory-entities into dev
Some checks failed
Dev Build / build-test (push) Failing after 54s
Dev Build / deploy-dev (push) Has been skipped
Dev Build / notify-success (push) Has been skipped
Dev Build / notify-failure (push) Successful in 3s

This commit was merged in pull request #24.
This commit is contained in:
2026-04-27 18:28:26 -04:00
6 changed files with 92 additions and 3 deletions

View File

@@ -46,9 +46,11 @@ public class FilamentsController : ControllerBase
_logger.LogDebug( _logger.LogDebug(
"Getting filaments: pageNumber={PageNumber}, pageSize={PageSize}, " + "Getting filaments: pageNumber={PageNumber}, pageSize={PageSize}, " +
"materialBaseId={MaterialBaseId}, materialFinishId={MaterialFinishId}, " + "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.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 // Clamp pagination values
var pageNumber = Math.Max(1, query.PageNumber); var pageNumber = Math.Max(1, query.PageNumber);
@@ -77,6 +79,15 @@ public class FilamentsController : ControllerBase
if (query.IsActive.HasValue) if (query.IsActive.HasValue)
spoolQuery = spoolQuery.Where(s => s.IsActive == query.IsActive.Value); 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 totalCount = await spoolQuery.CountAsync();
var items = await spoolQuery var items = await spoolQuery
@@ -185,7 +196,9 @@ public class FilamentsController : ControllerBase
SpoolSerial = request.SpoolSerial, SpoolSerial = request.SpoolSerial,
PurchasePrice = request.PurchasePrice, PurchasePrice = request.PurchasePrice,
PurchaseDate = request.PurchaseDate, PurchaseDate = request.PurchaseDate,
IsActive = request.IsActive IsActive = request.IsActive,
IsArchived = request.IsArchived,
StorageLocation = request.StorageLocation
}; };
_dbContext.Spools.Add(entity); _dbContext.Spools.Add(entity);
@@ -267,6 +280,8 @@ public class FilamentsController : ControllerBase
entity.PurchasePrice = request.PurchasePrice; entity.PurchasePrice = request.PurchasePrice;
entity.PurchaseDate = request.PurchaseDate; entity.PurchaseDate = request.PurchaseDate;
entity.IsActive = request.IsActive; entity.IsActive = request.IsActive;
entity.IsArchived = request.IsArchived;
entity.StorageLocation = request.StorageLocation;
await _dbContext.SaveChangesAsync(); await _dbContext.SaveChangesAsync();
@@ -307,6 +322,8 @@ public class FilamentsController : ControllerBase
PurchasePrice = s.PurchasePrice, PurchasePrice = s.PurchasePrice,
PurchaseDate = s.PurchaseDate, PurchaseDate = s.PurchaseDate,
IsActive = s.IsActive, IsActive = s.IsActive,
IsArchived = s.IsArchived,
StorageLocation = s.StorageLocation,
CreatedAt = s.CreatedAt, CreatedAt = s.CreatedAt,
UpdatedAt = s.UpdatedAt, UpdatedAt = s.UpdatedAt,
QrCodeUrl = $"/api/qr/spool/{s.Id}" QrCodeUrl = $"/api/qr/spool/{s.Id}"

View File

@@ -59,6 +59,12 @@ public class FilamentResponse
/// <summary>Whether the spool is currently active and available.</summary> /// <summary>Whether the spool is currently active and available.</summary>
public bool IsActive { get; set; } public bool IsActive { get; set; }
/// <summary>Whether the spool has been archived (removed from active inventory).</summary>
public bool IsArchived { get; set; }
/// <summary>Physical storage location (e.g., "Shelf A", "Drawer 3"). Null if unset.</summary>
public string? StorageLocation { get; set; }
/// <summary>Timestamp when this record was created (UTC).</summary> /// <summary>Timestamp when this record was created (UTC).</summary>
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
@@ -133,6 +139,15 @@ public class CreateFilamentRequest
/// <summary>Whether the spool is active. Defaults to true.</summary> /// <summary>Whether the spool is active. Defaults to true.</summary>
public bool IsActive { get; set; } = true; public bool IsActive { get; set; } = true;
/// <summary>Whether the spool is archived. Defaults to false.
/// </summary>
public bool IsArchived { get; set; } = false;
/// <summary>Physical storage location (e.g., "Shelf A", "Drawer 3"). Optional.
/// </summary>
[StringLength(200, ErrorMessage = "StorageLocation must not exceed 200 characters.")]
public string? StorageLocation { get; set; }
} }
/// <summary> /// <summary>
@@ -196,4 +211,11 @@ public class UpdateFilamentRequest
/// <summary>Whether the spool is active.</summary> /// <summary>Whether the spool is active.</summary>
public bool IsActive { get; set; } = true; public bool IsActive { get; set; } = true;
/// <summary>Whether the spool is archived. Defaults to false.</summary>
public bool IsArchived { get; set; } = false;
/// <summary>Physical storage location (e.g., "Shelf A", "Drawer 3"). Optional.</summary>
[StringLength(200, ErrorMessage = "StorageLocation must not exceed 200 characters.")]
public string? StorageLocation { get; set; }
} }

View File

@@ -30,4 +30,11 @@ public class FilamentQueryParameters
/// <summary>Optional filter by active status. True = active only, False = inactive only.</summary> /// <summary>Optional filter by active status. True = active only, False = inactive only.</summary>
public bool? IsActive { get; set; } public bool? IsActive { get; set; }
/// <summary>Whether to include archived spools in results. Defaults to false (excludes archived).
/// </summary>
public bool? IncludeArchived { get; set; }
/// <summary>Optional filter by storage location (case-insensitive partial match).</summary>
public string? StorageLocation { get; set; }
} }

View File

@@ -52,6 +52,12 @@ public class CreateFilamentRequestValidator : AbstractValidator<CreateFilamentRe
RuleFor(x => x.PurchasePrice!.Value) RuleFor(x => x.PurchasePrice!.Value)
.GreaterThanOrEqualTo(0).WithMessage("Purchase price must be non-negative."); .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<UpdateFilamentRe
RuleFor(x => x.PurchasePrice!.Value) RuleFor(x => x.PurchasePrice!.Value)
.GreaterThanOrEqualTo(0).WithMessage("Purchase price must be non-negative."); .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.");
});
} }
} }

View File

@@ -93,6 +93,20 @@ public class Spool : AuditableEntity
/// </summary> /// </summary>
public bool IsActive { get; set; } = true; public bool IsActive { get; set; } = true;
/// <summary>
/// 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).
/// </summary>
public bool IsArchived { get; set; } = false;
/// <summary>
/// Physical storage location of the spool (e.g., "Shelf A", "Drawer 3", "AMS Tray 2").
/// Optional — not every spool has a designated storage location.
/// </summary>
public string? StorageLocation { get; set; }
/// <summary> /// <summary>
/// Navigation collection of AMS slots where this spool is loaded. /// Navigation collection of AMS slots where this spool is loaded.
/// </summary> /// </summary>

View File

@@ -68,6 +68,15 @@ public class SpoolConfiguration : BaseEntityConfiguration<Spool>
.HasDefaultValue(true) .HasDefaultValue(true)
.IsRequired(); .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 // Unique index on spool_serial — critical for barcode/QR scanning
builder.HasIndex(e => e.SpoolSerial) builder.HasIndex(e => e.SpoolSerial)
.IsUnique() .IsUnique()
@@ -89,6 +98,14 @@ public class SpoolConfiguration : BaseEntityConfiguration<Spool>
builder.HasIndex(e => e.IsActive) builder.HasIndex(e => e.IsActive)
.HasDatabaseName("ix_spools_is_active"); .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 // Relationships
builder.HasOne(e => e.MaterialBase) builder.HasOne(e => e.MaterialBase)
.WithMany(e => e.Spools) .WithMany(e => e.Spools)