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)
This commit is contained in:
@@ -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}"
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
@@ -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.");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user