using System.ComponentModel.DataAnnotations; namespace Extrudex.API.DTOs.PrintJobs; /// /// 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. /// public class PrintJobResponse { /// Unique identifier for the print job. public Guid Id { get; set; } /// Foreign key to the printer that executed this job. public Guid PrinterId { get; set; } /// Name of the printer that executed this job. public string PrinterName { get; set; } = string.Empty; /// Foreign key to the spool that provided filament. public Guid SpoolId { get; set; } /// Serial number of the spool that provided filament. public string SpoolSerial { get; set; } = string.Empty; /// Human-readable name or identifier for the print job. public string PrintName { get; set; } = string.Empty; /// Path or filename of the G-code file. public string? GcodeFilePath { get; set; } /// Total millimeters of filament extruded during this print. public decimal MmExtruded { get; set; } /// /// 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. /// public decimal GramsDerived { get; set; } /// Calculated cost of goods sold (COGS) for this print job. public decimal? CostPerPrint { get; set; } /// Timestamp when the print job started (UTC). public DateTime? StartedAt { get; set; } /// Timestamp when the print job completed or failed (UTC). public DateTime? CompletedAt { get; set; } /// Current status of the print job (Queued, Printing, Completed, Cancelled, Failed). public string Status { get; set; } = string.Empty; /// Data source that provided this job (Mqtt, Moonraker, Manual). public string DataSource { get; set; } = string.Empty; /// Audit snapshot: filament diameter (mm) recorded at time of print. public decimal FilamentDiameterAtPrintMm { get; set; } /// Audit snapshot: material density (g/cm³) recorded at time of print. public decimal MaterialDensityAtPrint { get; set; } /// Optional notes about the print job. public string? Notes { get; set; } /// Timestamp when this record was created (UTC). public DateTime CreatedAt { get; set; } /// Timestamp when this record was last updated (UTC). public DateTime UpdatedAt { get; set; } } /// /// 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. /// public class CreatePrintJobRequest { /// Foreign key to the printer that will execute this job. Required. [Required(ErrorMessage = "PrinterId is required.")] public Guid PrinterId { get; set; } /// Foreign key to the spool providing filament. Required. [Required(ErrorMessage = "SpoolId is required.")] public Guid SpoolId { get; set; } /// Human-readable name for the print job. Required, max 200 characters. [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; /// Optional path or filename of the G-code file. Max 500 characters. [StringLength(500, ErrorMessage = "GcodeFilePath must not exceed 500 characters.")] public string? GcodeFilePath { get; set; } /// Total millimeters of filament extruded. Must be non-negative. Defaults to 0. [Range(0, double.MaxValue, ErrorMessage = "MmExtruded must be non-negative.")] public decimal MmExtruded { get; set; } /// /// 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. /// [Range(0, double.MaxValue, ErrorMessage = "GramsDerived must be non-negative.")] public decimal GramsDerived { get; set; } /// Optional calculated COGS. Must be non-negative if provided. [Range(0, double.MaxValue, ErrorMessage = "CostPerPrint must be non-negative.")] public decimal? CostPerPrint { get; set; } /// Optional timestamp when the job started (UTC). public DateTime? StartedAt { get; set; } /// /// Data source for this job. Must be "Mqtt", "Moonraker", or "Manual". /// Defaults to "Manual". /// [Required(ErrorMessage = "DataSource is required.")] [RegularExpression("^(Mqtt|Moonraker|Manual)$", ErrorMessage = "DataSource must be 'Mqtt', 'Moonraker', or 'Manual'.")] public string DataSource { get; set; } = "Manual"; /// /// 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. /// [Range(0.01, 100, ErrorMessage = "FilamentDiameterAtPrintMm must be between 0.01 and 100 mm.")] public decimal FilamentDiameterAtPrintMm { get; set; } = 1.75m; /// /// 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. /// [Range(0.001, 100, ErrorMessage = "MaterialDensityAtPrint must be between 0.001 and 100 g/cm³.")] public decimal MaterialDensityAtPrint { get; set; } /// Optional notes about the print job. Max 2000 characters. [StringLength(2000, ErrorMessage = "Notes must not exceed 2000 characters.")] public string? Notes { get; set; } /// /// When true, the server auto-derives GramsDerived, FilamentDiameterAtPrintMm, /// and MaterialDensityAtPrint from the spool's material data. /// MmExtruded must still be provided. Overrides manual GramsDerived. /// public bool AutoDeriveGrams { get; set; } } /// /// 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. /// public class UpdatePrintJobRequest { /// Foreign key to the printer. Required. [Required(ErrorMessage = "PrinterId is required.")] public Guid PrinterId { get; set; } /// Foreign key to the spool. Required. [Required(ErrorMessage = "SpoolId is required.")] public Guid SpoolId { get; set; } /// Human-readable name for the print job. Required, max 200 characters. [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; /// Optional path or filename of the G-code file. Max 500 characters. [StringLength(500, ErrorMessage = "GcodeFilePath must not exceed 500 characters.")] public string? GcodeFilePath { get; set; } /// Total millimeters of filament extruded. Must be non-negative. [Range(0, double.MaxValue, ErrorMessage = "MmExtruded must be non-negative.")] public decimal MmExtruded { get; set; } /// /// Derived grams consumed. If AutoDeriveGrams is true, this is recomputed /// server-side and the provided value is ignored. /// [Range(0, double.MaxValue, ErrorMessage = "GramsDerived must be non-negative.")] public decimal GramsDerived { get; set; } /// Optional calculated COGS. Must be non-negative if provided. [Range(0, double.MaxValue, ErrorMessage = "CostPerPrint must be non-negative.")] public decimal? CostPerPrint { get; set; } /// Optional notes about the print job. Max 2000 characters. [StringLength(2000, ErrorMessage = "Notes must not exceed 2000 characters.")] public string? Notes { get; set; } /// /// When true, the server recomputes GramsDerived, FilamentDiameterAtPrintMm, /// and MaterialDensityAtPrint from the spool's current material data. /// MmExtruded must still be provided. /// public bool AutoDeriveGrams { get; set; } } /// /// 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. /// public class UpdatePrintJobStatusRequest { /// /// New status for the print job. Must be one of: Queued, Printing, Completed, Cancelled, Failed. /// Case-insensitive. /// [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; }