223 lines
10 KiB
C#
223 lines
10 KiB
C#
using System.ComponentModel.DataAnnotations;
|
||
|
||
namespace Extrudex.API.DTOs.PrintJobs;
|
||
|
||
/// <summary>
|
||
/// Response DTO for PrintJob entity. Contains all job details including
|
||
/// denormalized printer name and spool serial for display.
|
||
/// Audit snapshots (filament diameter and material density) preserve COGS accuracy
|
||
/// even if the source data changes after the print.
|
||
/// </summary>
|
||
public class PrintJobResponse
|
||
{
|
||
/// <summary>Unique identifier for the print job.</summary>
|
||
public Guid Id { get; set; }
|
||
|
||
/// <summary>Foreign key to the printer that executed this job.</summary>
|
||
public Guid PrinterId { get; set; }
|
||
|
||
/// <summary>Name of the printer that executed this job.</summary>
|
||
public string PrinterName { get; set; } = string.Empty;
|
||
|
||
/// <summary>Foreign key to the spool that provided filament.</summary>
|
||
public Guid SpoolId { get; set; }
|
||
|
||
/// <summary>Serial number of the spool that provided filament.</summary>
|
||
public string SpoolSerial { get; set; } = string.Empty;
|
||
|
||
/// <summary>Human-readable name or identifier for the print job.</summary>
|
||
public string PrintName { get; set; } = string.Empty;
|
||
|
||
/// <summary>Path or filename of the G-code file.</summary>
|
||
public string? GcodeFilePath { get; set; }
|
||
|
||
/// <summary>Total millimeters of filament extruded during this print.</summary>
|
||
public decimal MmExtruded { get; set; }
|
||
|
||
/// <summary>
|
||
/// Derived grams consumed for this print, calculated as:
|
||
/// mm_extruded × cross_section_area × material_density.
|
||
/// Cross-section area = π × (filament_diameter / 2)² in mm².
|
||
/// Density converted from g/cm³ to g/mm³ by dividing by 1000.
|
||
/// </summary>
|
||
public decimal GramsDerived { get; set; }
|
||
|
||
/// <summary>Calculated cost of goods sold (COGS) for this print job.</summary>
|
||
public decimal? CostPerPrint { get; set; }
|
||
|
||
/// <summary>Timestamp when the print job started (UTC).</summary>
|
||
public DateTime? StartedAt { get; set; }
|
||
|
||
/// <summary>Timestamp when the print job completed or failed (UTC).</summary>
|
||
public DateTime? CompletedAt { get; set; }
|
||
|
||
/// <summary>Current status of the print job (Queued, Printing, Completed, Cancelled, Failed).</summary>
|
||
public string Status { get; set; } = string.Empty;
|
||
|
||
/// <summary>Data source that provided this job (Mqtt, Moonraker, Manual).</summary>
|
||
public string DataSource { get; set; } = string.Empty;
|
||
|
||
/// <summary>Audit snapshot: filament diameter (mm) recorded at time of print.</summary>
|
||
public decimal FilamentDiameterAtPrintMm { get; set; }
|
||
|
||
/// <summary>Audit snapshot: material density (g/cm³) recorded at time of print.</summary>
|
||
public decimal MaterialDensityAtPrint { get; set; }
|
||
|
||
/// <summary>Optional notes about the print job.</summary>
|
||
public string? Notes { get; set; }
|
||
|
||
/// <summary>Timestamp when this record was created (UTC).</summary>
|
||
public DateTime CreatedAt { get; set; }
|
||
|
||
/// <summary>Timestamp when this record was last updated (UTC).</summary>
|
||
public DateTime UpdatedAt { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Request DTO for creating a new print job. The gram derivation formula
|
||
/// (grams = mm_extruded × cross_section_area × material_density) can be
|
||
/// auto-computed server-side when MmExtruded, FilamentDiameterAtPrintMm,
|
||
/// and MaterialDensityAtPrint are provided. Alternatively, set AutoDeriveGrams
|
||
/// to true and provide a SpoolId to pull density from the material base.
|
||
/// </summary>
|
||
public class CreatePrintJobRequest
|
||
{
|
||
/// <summary>Foreign key to the printer that will execute this job. Required.</summary>
|
||
[Required(ErrorMessage = "PrinterId is required.")]
|
||
public Guid PrinterId { get; set; }
|
||
|
||
/// <summary>Foreign key to the spool providing filament. Required.</summary>
|
||
[Required(ErrorMessage = "SpoolId is required.")]
|
||
public Guid SpoolId { get; set; }
|
||
|
||
/// <summary>Human-readable name for the print job. Required, max 200 characters.</summary>
|
||
[Required(ErrorMessage = "PrintName is required.")]
|
||
[StringLength(200, MinimumLength = 1, ErrorMessage = "PrintName must be between 1 and 200 characters.")]
|
||
public string PrintName { get; set; } = string.Empty;
|
||
|
||
/// <summary>Optional path or filename of the G-code file. Max 500 characters.</summary>
|
||
[StringLength(500, ErrorMessage = "GcodeFilePath must not exceed 500 characters.")]
|
||
public string? GcodeFilePath { get; set; }
|
||
|
||
/// <summary>Total millimeters of filament extruded. Must be non-negative. Defaults to 0.</summary>
|
||
[Range(0, double.MaxValue, ErrorMessage = "MmExtruded must be non-negative.")]
|
||
public decimal MmExtruded { get; set; }
|
||
|
||
/// <summary>
|
||
/// Derived grams consumed. If AutoDeriveGrams is true, this is computed
|
||
/// server-side and the provided value is ignored. Must be non-negative if manually set.
|
||
/// </summary>
|
||
[Range(0, double.MaxValue, ErrorMessage = "GramsDerived must be non-negative.")]
|
||
public decimal GramsDerived { get; set; }
|
||
|
||
/// <summary>Optional calculated COGS. Must be non-negative if provided.</summary>
|
||
[Range(0, double.MaxValue, ErrorMessage = "CostPerPrint must be non-negative.")]
|
||
public decimal? CostPerPrint { get; set; }
|
||
|
||
/// <summary>Optional timestamp when the job started (UTC).</summary>
|
||
public DateTime? StartedAt { get; set; }
|
||
|
||
/// <summary>
|
||
/// Data source for this job. Must be "Mqtt", "Moonraker", or "Manual".
|
||
/// Defaults to "Manual".
|
||
/// </summary>
|
||
[Required(ErrorMessage = "DataSource is required.")]
|
||
[RegularExpression("^(Mqtt|Moonraker|Manual)$", ErrorMessage = "DataSource must be 'Mqtt', 'Moonraker', or 'Manual'.")]
|
||
public string DataSource { get; set; } = "Manual";
|
||
|
||
/// <summary>
|
||
/// Audit snapshot: filament diameter in mm at time of print. Must be greater than zero.
|
||
/// Defaults to 1.75mm if not specified and AutoDeriveGrams is false.
|
||
/// </summary>
|
||
[Range(0.01, 100, ErrorMessage = "FilamentDiameterAtPrintMm must be between 0.01 and 100 mm.")]
|
||
public decimal FilamentDiameterAtPrintMm { get; set; } = 1.75m;
|
||
|
||
/// <summary>
|
||
/// Audit snapshot: material density in g/cm³ at time of print. Must be greater than zero.
|
||
/// If AutoDeriveGrams is true, this is populated from the spool's material base.
|
||
/// </summary>
|
||
[Range(0.001, 100, ErrorMessage = "MaterialDensityAtPrint must be between 0.001 and 100 g/cm³.")]
|
||
public decimal MaterialDensityAtPrint { get; set; }
|
||
|
||
/// <summary>Optional notes about the print job. Max 2000 characters.</summary>
|
||
[StringLength(2000, ErrorMessage = "Notes must not exceed 2000 characters.")]
|
||
public string? Notes { get; set; }
|
||
|
||
/// <summary>
|
||
/// When true, the server auto-derives GramsDerived, FilamentDiameterAtPrintMm,
|
||
/// and MaterialDensityAtPrint from the spool's material data.
|
||
/// MmExtruded must still be provided. Overrides manual GramsDerived.
|
||
/// </summary>
|
||
public bool AutoDeriveGrams { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Request DTO for updating an existing print job. Full replacement semantics —
|
||
/// all required fields must be provided. Gram derivation can be recomputed
|
||
/// by setting AutoDeriveGrams to true.
|
||
/// </summary>
|
||
public class UpdatePrintJobRequest
|
||
{
|
||
/// <summary>Foreign key to the printer. Required.</summary>
|
||
[Required(ErrorMessage = "PrinterId is required.")]
|
||
public Guid PrinterId { get; set; }
|
||
|
||
/// <summary>Foreign key to the spool. Required.</summary>
|
||
[Required(ErrorMessage = "SpoolId is required.")]
|
||
public Guid SpoolId { get; set; }
|
||
|
||
/// <summary>Human-readable name for the print job. Required, max 200 characters.</summary>
|
||
[Required(ErrorMessage = "PrintName is required.")]
|
||
[StringLength(200, MinimumLength = 1, ErrorMessage = "PrintName must be between 1 and 200 characters.")]
|
||
public string PrintName { get; set; } = string.Empty;
|
||
|
||
/// <summary>Optional path or filename of the G-code file. Max 500 characters.</summary>
|
||
[StringLength(500, ErrorMessage = "GcodeFilePath must not exceed 500 characters.")]
|
||
public string? GcodeFilePath { get; set; }
|
||
|
||
/// <summary>Total millimeters of filament extruded. Must be non-negative.</summary>
|
||
[Range(0, double.MaxValue, ErrorMessage = "MmExtruded must be non-negative.")]
|
||
public decimal MmExtruded { get; set; }
|
||
|
||
/// <summary>
|
||
/// Derived grams consumed. If AutoDeriveGrams is true, this is recomputed
|
||
/// server-side and the provided value is ignored.
|
||
/// </summary>
|
||
[Range(0, double.MaxValue, ErrorMessage = "GramsDerived must be non-negative.")]
|
||
public decimal GramsDerived { get; set; }
|
||
|
||
/// <summary>Optional calculated COGS. Must be non-negative if provided.</summary>
|
||
[Range(0, double.MaxValue, ErrorMessage = "CostPerPrint must be non-negative.")]
|
||
public decimal? CostPerPrint { get; set; }
|
||
|
||
/// <summary>Optional notes about the print job. Max 2000 characters.</summary>
|
||
[StringLength(2000, ErrorMessage = "Notes must not exceed 2000 characters.")]
|
||
public string? Notes { get; set; }
|
||
|
||
/// <summary>
|
||
/// When true, the server recomputes GramsDerived, FilamentDiameterAtPrintMm,
|
||
/// and MaterialDensityAtPrint from the spool's current material data.
|
||
/// MmExtruded must still be provided.
|
||
/// </summary>
|
||
public bool AutoDeriveGrams { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Request DTO for the PATCH /api/printjobs/{id}/status endpoint.
|
||
/// Validates that the transition is allowed by business rules:
|
||
/// - Can always go to Cancelled or Failed from any state.
|
||
/// - Can move from Queued → Printing, Printing → Completed.
|
||
/// - Cannot move from Completed back to Printing or Queued.
|
||
/// - Cannot move from Cancelled back to any active state.
|
||
/// </summary>
|
||
public class UpdatePrintJobStatusRequest
|
||
{
|
||
/// <summary>
|
||
/// New status for the print job. Must be one of: Queued, Printing, Completed, Cancelled, Failed.
|
||
/// Case-insensitive.
|
||
/// </summary>
|
||
[Required(ErrorMessage = "Status is required.")]
|
||
[RegularExpression("^(Queued|Printing|Completed|Cancelled|Failed)$",
|
||
ErrorMessage = "Status must be one of: Queued, Printing, Completed, Cancelled, Failed.")]
|
||
public string Status { get; set; } = string.Empty;
|
||
} |