Merge remote-tracking branch 'origin/dev' into agent/dex/CUB-33-moonraker-usage-polling
Some checks failed
Dev Build / build-test (pull_request) Failing after 1m5s
Dev Build / deploy-dev (pull_request) Has been skipped
Dev Build / notify-success (pull_request) Has been skipped
Dev Build / notify-failure (pull_request) Successful in 3s

# Conflicts:
#	backend/Domain/Interfaces/IMoonrakerClient.cs
#	backend/Infrastructure/Services/MoonrakerClient.cs
This commit is contained in:
2026-04-27 20:22:36 -04:00
16 changed files with 1001 additions and 260 deletions

View File

@@ -40,15 +40,18 @@ public class FilamentsController : ControllerBase
/// <response code="200">Returns the paginated list of filament spools.</response>
[HttpGet]
[ProducesResponseType(typeof(PagedResponse<FilamentResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<PagedResponse<FilamentResponse>>> GetFilaments(
[FromQuery] FilamentQueryParameters query)
{
_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 +80,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 +197,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 +281,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 +323,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}"

View File

@@ -59,6 +59,12 @@ public class FilamentResponse
/// <summary>Whether the spool is currently active and available.</summary>
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>
public DateTime CreatedAt { get; set; }
@@ -133,6 +139,15 @@ public class CreateFilamentRequest
/// <summary>Whether the spool is active. Defaults to true.</summary>
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>
@@ -196,4 +211,11 @@ public class UpdateFilamentRequest
/// <summary>Whether the spool is active.</summary>
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>
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

@@ -10,6 +10,9 @@ namespace Extrudex.API.Validators;
/// </summary>
public class CreateFilamentRequestValidator : AbstractValidator<CreateFilamentRequest>
{
/// <summary>
/// Initializes validation rules for <see cref="CreateFilamentRequest"/>.
/// </summary>
public CreateFilamentRequestValidator()
{
RuleFor(x => x.MaterialBaseId)
@@ -52,6 +55,12 @@ public class CreateFilamentRequestValidator : AbstractValidator<CreateFilamentRe
RuleFor(x => 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.");
});
}
}
@@ -62,6 +71,9 @@ public class CreateFilamentRequestValidator : AbstractValidator<CreateFilamentRe
/// </summary>
public class UpdateFilamentRequestValidator : AbstractValidator<UpdateFilamentRequest>
{
/// <summary>
/// Initializes validation rules for <see cref="UpdateFilamentRequest"/>.
/// </summary>
public UpdateFilamentRequestValidator()
{
RuleFor(x => x.MaterialBaseId)
@@ -104,5 +116,11 @@ public class UpdateFilamentRequestValidator : AbstractValidator<UpdateFilamentRe
RuleFor(x => 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.");
});
}
}