Skip to main content

hammerwork/
job.rs

1//! Job types and utilities for representing work units in the job queue.
2//!
3//! This module provides the core [`Job`] struct and [`JobStatus`] enum that represent
4//! individual units of work to be processed by workers. Jobs contain all the metadata
5//! needed for scheduling, prioritization, retry logic, and lifecycle management.
6
7use crate::cron::CronSchedule;
8use crate::priority::JobPriority;
9use crate::retry::RetryStrategy;
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12use uuid::Uuid;
13
14#[cfg(any(feature = "postgres", feature = "mysql"))]
15use sqlx::{Decode, Encode, Type};
16
17#[cfg(feature = "postgres")]
18use sqlx::Postgres;
19
20#[cfg(feature = "mysql")]
21use sqlx::MySql;
22
23/// Unique identifier for a job.
24///
25/// Each job gets a unique UUID when created to enable tracking and management
26/// throughout its lifecycle.
27pub type JobId = Uuid;
28
29/// The current status of a job in its lifecycle.
30///
31/// Jobs progress through various states from creation to completion or failure.
32/// This enum tracks the current state to enable proper job management and statistics.
33///
34/// # Examples
35///
36/// ```rust
37/// use hammerwork::JobStatus;
38///
39/// // Check if a job is in a final state
40/// let status = JobStatus::Completed;
41/// let is_final = matches!(status, JobStatus::Completed | JobStatus::Dead | JobStatus::TimedOut | JobStatus::Archived);
42/// assert!(is_final);
43/// ```
44#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
45pub enum JobStatus {
46    /// Job is waiting to be processed by a worker.
47    Pending,
48    /// Job is currently being processed by a worker.
49    Running,
50    /// Job completed successfully.
51    Completed,
52    /// Job failed but may be retried if it hasn't exhausted retry attempts.
53    Failed,
54    /// Job failed permanently after exhausting all retry attempts.
55    Dead,
56    /// Job was terminated due to exceeding its timeout duration.
57    TimedOut,
58    /// Job failed but is scheduled for retry.
59    Retrying,
60    /// Job has been archived for long-term storage.
61    Archived,
62}
63
64impl JobStatus {
65    /// Returns the string representation of the job status.
66    pub fn as_str(&self) -> &'static str {
67        match self {
68            JobStatus::Pending => "Pending",
69            JobStatus::Running => "Running",
70            JobStatus::Completed => "Completed",
71            JobStatus::Failed => "Failed",
72            JobStatus::Dead => "Dead",
73            JobStatus::TimedOut => "TimedOut",
74            JobStatus::Retrying => "Retrying",
75            JobStatus::Archived => "Archived",
76        }
77    }
78}
79
80// SQLx implementations for JobStatus to handle database encoding/decoding
81
82#[cfg(feature = "postgres")]
83impl Type<Postgres> for JobStatus {
84    fn type_info() -> sqlx::postgres::PgTypeInfo {
85        <String as Type<Postgres>>::type_info()
86    }
87
88    // Accept every column type `String` decodes from (TEXT, VARCHAR, ...); the status
89    // columns are VARCHAR, which the default `compatible` (== TEXT) rejects.
90    fn compatible(ty: &sqlx::postgres::PgTypeInfo) -> bool {
91        <String as Type<Postgres>>::compatible(ty)
92    }
93}
94
95#[cfg(feature = "postgres")]
96impl Encode<'_, Postgres> for JobStatus {
97    fn encode_by_ref(
98        &self,
99        buf: &mut sqlx::postgres::PgArgumentBuffer,
100    ) -> Result<sqlx::encode::IsNull, Box<dyn std::error::Error + Send + Sync + 'static>> {
101        let status_str = match self {
102            JobStatus::Pending => "Pending",
103            JobStatus::Running => "Running",
104            JobStatus::Completed => "Completed",
105            JobStatus::Failed => "Failed",
106            JobStatus::Dead => "Dead",
107            JobStatus::TimedOut => "TimedOut",
108            JobStatus::Retrying => "Retrying",
109            JobStatus::Archived => "Archived",
110        };
111        <&str as Encode<'_, Postgres>>::encode_by_ref(&status_str, buf)
112    }
113}
114
115#[cfg(feature = "postgres")]
116impl Decode<'_, Postgres> for JobStatus {
117    fn decode(value: sqlx::postgres::PgValueRef<'_>) -> Result<Self, sqlx::error::BoxDynError> {
118        let status_str = <String as Decode<Postgres>>::decode(value)?;
119        // Handle both quoted (old format) and unquoted (new format) status values
120        let cleaned_str = status_str.trim_matches('"');
121        match cleaned_str {
122            "Pending" => Ok(JobStatus::Pending),
123            "Running" => Ok(JobStatus::Running),
124            "Completed" => Ok(JobStatus::Completed),
125            "Failed" => Ok(JobStatus::Failed),
126            "Dead" => Ok(JobStatus::Dead),
127            "TimedOut" => Ok(JobStatus::TimedOut),
128            "Retrying" => Ok(JobStatus::Retrying),
129            "Archived" => Ok(JobStatus::Archived),
130            _ => Err(format!("Unknown job status: {}", status_str).into()),
131        }
132    }
133}
134
135#[cfg(feature = "mysql")]
136impl Type<MySql> for JobStatus {
137    fn type_info() -> sqlx::mysql::MySqlTypeInfo {
138        <String as Type<MySql>>::type_info()
139    }
140
141    // Accept every column type `String` decodes from (TEXT, VARCHAR, ...); the status
142    // columns are VARCHAR, which the default `compatible` (== TEXT) rejects.
143    fn compatible(ty: &sqlx::mysql::MySqlTypeInfo) -> bool {
144        <String as Type<MySql>>::compatible(ty)
145    }
146}
147
148#[cfg(feature = "mysql")]
149impl Encode<'_, MySql> for JobStatus {
150    fn encode_by_ref(
151        &self,
152        buf: &mut Vec<u8>,
153    ) -> Result<sqlx::encode::IsNull, Box<dyn std::error::Error + Send + Sync + 'static>> {
154        let status_str = match self {
155            JobStatus::Pending => "Pending",
156            JobStatus::Running => "Running",
157            JobStatus::Completed => "Completed",
158            JobStatus::Failed => "Failed",
159            JobStatus::Dead => "Dead",
160            JobStatus::TimedOut => "TimedOut",
161            JobStatus::Retrying => "Retrying",
162            JobStatus::Archived => "Archived",
163        };
164        <&str as Encode<'_, MySql>>::encode_by_ref(&status_str, buf)
165    }
166}
167
168#[cfg(feature = "mysql")]
169impl Decode<'_, MySql> for JobStatus {
170    fn decode(value: sqlx::mysql::MySqlValueRef<'_>) -> Result<Self, sqlx::error::BoxDynError> {
171        let status_str = <String as Decode<MySql>>::decode(value)?;
172        // Handle both quoted (old format) and unquoted (new format) status values
173        let cleaned_str = status_str.trim_matches('"');
174        match cleaned_str {
175            "Pending" => Ok(JobStatus::Pending),
176            "Running" => Ok(JobStatus::Running),
177            "Completed" => Ok(JobStatus::Completed),
178            "Failed" => Ok(JobStatus::Failed),
179            "Dead" => Ok(JobStatus::Dead),
180            "TimedOut" => Ok(JobStatus::TimedOut),
181            "Retrying" => Ok(JobStatus::Retrying),
182            "Archived" => Ok(JobStatus::Archived),
183            _ => Err(format!("Unknown job status: {}", status_str).into()),
184        }
185    }
186}
187
188/// Configuration for job result storage.
189///
190/// This enum determines where and how job results are stored when jobs complete successfully.
191/// Different storage backends offer different trade-offs between performance, persistence,
192/// and resource usage.
193///
194/// # Examples
195///
196/// ```rust
197/// use hammerwork::job::ResultStorage;
198///
199/// // Store results in the database
200/// let db_storage = ResultStorage::Database;
201///
202/// // Store results in memory (faster but not persistent)
203/// let memory_storage = ResultStorage::Memory;
204///
205/// // Don't store results (default behavior)
206/// let no_storage = ResultStorage::None;
207/// ```
208#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
209pub enum ResultStorage {
210    /// Store results in the database (persistent across restarts).
211    Database,
212    /// Store results in memory (faster access but lost on restart).
213    Memory,
214    /// Don't store job results (default behavior).
215    #[default]
216    None,
217}
218
219/// Configuration for job result storage and management.
220///
221/// This struct contains settings that control how job results are stored,
222/// how long they're retained, and when they should be cleaned up.
223///
224/// # Examples
225///
226/// ```rust
227/// use hammerwork::job::{ResultConfig, ResultStorage};
228/// use std::time::Duration;
229///
230/// // Store results in database for 7 days
231/// let config = ResultConfig::new(ResultStorage::Database)
232///     .with_ttl(Duration::from_secs(7 * 24 * 60 * 60));
233///
234/// // Store results in memory for 1 hour
235/// let config = ResultConfig::new(ResultStorage::Memory)
236///     .with_ttl(Duration::from_secs(3600));
237/// ```
238#[derive(Debug, Clone, Serialize, Deserialize)]
239pub struct ResultConfig {
240    /// Where to store the job results.
241    pub storage: ResultStorage,
242    /// How long to keep results before expiring them.
243    pub ttl: Option<std::time::Duration>,
244    /// Maximum size of result data in bytes (for validation).
245    pub max_size_bytes: Option<usize>,
246}
247
248impl ResultConfig {
249    /// Creates a new result configuration with the specified storage backend.
250    ///
251    /// # Arguments
252    ///
253    /// * `storage` - The storage backend to use for results
254    ///
255    /// # Examples
256    ///
257    /// ```rust
258    /// use hammerwork::job::{ResultConfig, ResultStorage};
259    ///
260    /// let config = ResultConfig::new(ResultStorage::Database);
261    /// assert_eq!(config.storage, ResultStorage::Database);
262    /// assert!(config.ttl.is_none());
263    /// ```
264    pub fn new(storage: ResultStorage) -> Self {
265        Self {
266            storage,
267            ttl: None,
268            max_size_bytes: None,
269        }
270    }
271
272    /// Sets the time-to-live (TTL) for stored results.
273    ///
274    /// After this duration elapses, the result will be eligible for cleanup.
275    ///
276    /// # Arguments
277    ///
278    /// * `ttl` - How long to keep results before they expire
279    ///
280    /// # Examples
281    ///
282    /// ```rust
283    /// use hammerwork::job::{ResultConfig, ResultStorage};
284    /// use std::time::Duration;
285    ///
286    /// let config = ResultConfig::new(ResultStorage::Database)
287    ///     .with_ttl(Duration::from_secs(3600)); // 1 hour
288    /// ```
289    pub fn with_ttl(mut self, ttl: std::time::Duration) -> Self {
290        self.ttl = Some(ttl);
291        self
292    }
293
294    /// Sets the maximum size for result data.
295    ///
296    /// This is used to validate result data before storage to prevent
297    /// extremely large results from impacting system performance.
298    ///
299    /// # Arguments
300    ///
301    /// * `max_bytes` - Maximum allowed size for result data
302    ///
303    /// # Examples
304    ///
305    /// ```rust
306    /// use hammerwork::job::{ResultConfig, ResultStorage};
307    ///
308    /// let config = ResultConfig::new(ResultStorage::Database)
309    ///     .with_max_size(1024 * 1024); // 1MB limit
310    /// ```
311    pub fn with_max_size(mut self, max_bytes: usize) -> Self {
312        self.max_size_bytes = Some(max_bytes);
313        self
314    }
315}
316
317impl Default for ResultConfig {
318    fn default() -> Self {
319        Self::new(ResultStorage::None)
320    }
321}
322
323/// A unit of work to be processed by the job queue.
324///
325/// Jobs are the fundamental building blocks of the Hammerwork system. Each job contains:
326/// - A unique identifier for tracking
327/// - Queue name for routing to appropriate workers
328/// - JSON payload containing the work data
329/// - Scheduling and retry configuration
330/// - Priority level for queue ordering
331/// - Optional cron schedule for recurring jobs
332/// - Timeout configuration for automatic termination
333///
334/// # Examples
335///
336/// ## Basic Job Creation
337///
338/// ```rust
339/// use hammerwork::Job;
340/// use serde_json::json;
341///
342/// let job = Job::new("email_queue".to_string(), json!({
343///     "to": "user@example.com",
344///     "subject": "Welcome!",
345///     "body": "Thanks for signing up"
346/// }));
347///
348/// assert_eq!(job.queue_name, "email_queue");
349/// assert_eq!(job.max_attempts, 3); // Default retry attempts
350/// ```
351///
352/// ## Job with Priority and Timeout
353///
354/// ```rust
355/// use hammerwork::{Job, JobPriority};
356/// use serde_json::json;
357/// use std::time::Duration;
358///
359/// let job = Job::new("processing".to_string(), json!({"data": "important"}))
360///     .as_high_priority()
361///     .with_timeout(Duration::from_secs(300))
362///     .with_max_attempts(5);
363///
364/// assert_eq!(job.priority, JobPriority::High);
365/// assert_eq!(job.timeout, Some(Duration::from_secs(300)));
366/// assert_eq!(job.max_attempts, 5);
367/// ```
368///
369/// ## Delayed Job
370///
371/// ```rust
372/// use hammerwork::Job;
373/// use serde_json::json;
374/// use chrono::Duration;
375///
376/// let job = Job::with_delay(
377///     "notifications".to_string(),
378///     json!({"message": "Reminder"}),
379///     Duration::hours(1)
380/// );
381///
382/// // Job will be scheduled to run 1 hour from now
383/// assert!(job.scheduled_at > job.created_at);
384/// ```
385#[derive(Debug, Clone, Serialize, Deserialize)]
386pub struct Job {
387    /// Unique identifier for this job.
388    pub id: JobId,
389    /// Name of the queue this job belongs to.
390    pub queue_name: String,
391    /// JSON payload containing the work data.
392    pub payload: serde_json::Value,
393    /// Current status of the job.
394    pub status: JobStatus,
395    /// Number of times this job has been attempted.
396    pub attempts: i32,
397    /// Maximum number of attempts before marking the job as dead.
398    pub max_attempts: i32,
399    /// When the job was created.
400    pub created_at: DateTime<Utc>,
401    /// When the job should be processed (may be in the future for delayed jobs).
402    pub scheduled_at: DateTime<Utc>,
403    /// When the job started processing (if it has started).
404    pub started_at: Option<DateTime<Utc>>,
405    /// When the job completed successfully (if it completed).
406    pub completed_at: Option<DateTime<Utc>>,
407    /// When the job failed permanently (if it failed).
408    pub failed_at: Option<DateTime<Utc>>,
409    /// When the job timed out (if it timed out).
410    pub timed_out_at: Option<DateTime<Utc>>,
411    /// Maximum duration the job is allowed to run before timing out.
412    pub timeout: Option<std::time::Duration>,
413    /// Error message if the job failed.
414    pub error_message: Option<String>,
415    /// Priority level for queue ordering.
416    pub priority: JobPriority,
417    /// Cron expression for recurring jobs.
418    pub cron_schedule: Option<String>,
419    /// Next scheduled execution time for recurring jobs.
420    pub next_run_at: Option<DateTime<Utc>>,
421    /// Whether this is a recurring job.
422    pub recurring: bool,
423    /// Timezone for cron calculations.
424    pub timezone: Option<String>,
425    /// Batch ID if this job is part of a batch operation.
426    pub batch_id: Option<crate::batch::BatchId>,
427    /// Configuration for how job results should be stored.
428    pub result_config: ResultConfig,
429    /// The actual result data from job execution (if stored).
430    pub result_data: Option<serde_json::Value>,
431    /// When the result was stored (if applicable).
432    pub result_stored_at: Option<DateTime<Utc>>,
433    /// When the stored result will expire (if applicable).
434    pub result_expires_at: Option<DateTime<Utc>>,
435    /// Retry strategy for this job (overrides worker default if specified).
436    pub retry_strategy: Option<RetryStrategy>,
437    /// Job IDs this job depends on (must complete before this job can run).
438    pub depends_on: Vec<JobId>,
439    /// Job IDs that depend on this job (cached for performance).
440    pub dependents: Vec<JobId>,
441    /// Status of dependency resolution for this job.
442    pub dependency_status: crate::workflow::DependencyStatus,
443    /// ID of the workflow this job belongs to (if any).
444    pub workflow_id: Option<crate::workflow::WorkflowId>,
445    /// Name of the workflow this job belongs to (if any).
446    pub workflow_name: Option<String>,
447    /// Distributed trace identifier for cross-service tracing.
448    pub trace_id: Option<String>,
449    /// Business correlation identifier for grouping related operations.
450    pub correlation_id: Option<String>,
451    /// Parent span identifier for hierarchical tracing.
452    pub parent_span_id: Option<String>,
453    /// Serialized span context for trace propagation.
454    pub span_context: Option<String>,
455    /// Encryption configuration for this job's payload (if encrypted).
456    #[cfg(feature = "encryption")]
457    pub encryption_config: Option<crate::encryption::EncryptionConfig>,
458    /// List of field names that contain PII and should be encrypted.
459    pub pii_fields: Vec<String>,
460    /// Retention policy for encrypted data (overrides default if specified).
461    #[cfg(feature = "encryption")]
462    pub retention_policy: Option<crate::encryption::RetentionPolicy>,
463    /// Whether the payload is currently encrypted.
464    pub is_encrypted: bool,
465    /// Encrypted payload data (if job is encrypted).
466    #[cfg(feature = "encryption")]
467    pub encrypted_payload: Option<crate::encryption::EncryptedPayload>,
468}
469
470impl Job {
471    /// Creates a new job with default settings.
472    ///
473    /// The job will be created with:
474    /// - A unique UUID identifier
475    /// - Normal priority level
476    /// - 3 maximum retry attempts
477    /// - Scheduled to run immediately
478    /// - Pending status
479    ///
480    /// # Arguments
481    ///
482    /// * `queue_name` - The name of the queue this job should be processed by
483    /// * `payload` - JSON data containing the work to be performed
484    ///
485    /// # Examples
486    ///
487    /// ```rust
488    /// use hammerwork::{Job, JobStatus, JobPriority};
489    /// use serde_json::json;
490    ///
491    /// let job = Job::new("email_queue".to_string(), json!({
492    ///     "to": "user@example.com",
493    ///     "subject": "Welcome!",
494    ///     "template": "welcome"
495    /// }));
496    ///
497    /// assert_eq!(job.queue_name, "email_queue");
498    /// assert_eq!(job.status, JobStatus::Pending);
499    /// assert_eq!(job.priority, JobPriority::Normal);
500    /// assert_eq!(job.max_attempts, 3);
501    /// assert_eq!(job.attempts, 0);
502    /// assert!(!job.is_recurring());
503    /// ```
504    pub fn new(queue_name: String, payload: serde_json::Value) -> Self {
505        let now = Utc::now();
506        Self {
507            id: Uuid::new_v4(),
508            queue_name,
509            payload,
510            status: JobStatus::Pending,
511            attempts: 0,
512            max_attempts: 3,
513            created_at: now,
514            scheduled_at: now,
515            started_at: None,
516            completed_at: None,
517            failed_at: None,
518            timed_out_at: None,
519            timeout: None,
520            error_message: None,
521            priority: JobPriority::default(),
522            cron_schedule: None,
523            next_run_at: None,
524            recurring: false,
525            timezone: None,
526            batch_id: None,
527            result_config: ResultConfig::default(),
528            result_data: None,
529            result_stored_at: None,
530            result_expires_at: None,
531            retry_strategy: None,
532            depends_on: Vec::new(),
533            dependents: Vec::new(),
534            dependency_status: crate::workflow::DependencyStatus::None,
535            workflow_id: None,
536            workflow_name: None,
537            trace_id: None,
538            correlation_id: None,
539            parent_span_id: None,
540            span_context: None,
541            #[cfg(feature = "encryption")]
542            encryption_config: None,
543            pii_fields: Vec::new(),
544            #[cfg(feature = "encryption")]
545            retention_policy: None,
546            is_encrypted: false,
547            #[cfg(feature = "encryption")]
548            encrypted_payload: None,
549        }
550    }
551
552    /// Creates a new job scheduled to run after a delay.
553    ///
554    /// This is useful for implementing delayed notifications, retries with backoff,
555    /// or any work that should be performed at a specific time in the future.
556    ///
557    /// # Arguments
558    ///
559    /// * `queue_name` - The name of the queue this job should be processed by
560    /// * `payload` - JSON data containing the work to be performed
561    /// * `delay` - How long to wait before the job becomes eligible for processing
562    ///
563    /// # Examples
564    ///
565    /// ```rust
566    /// use hammerwork::Job;
567    /// use serde_json::json;
568    /// use chrono::Duration;
569    ///
570    /// // Send a reminder email in 24 hours
571    /// let job = Job::with_delay(
572    ///     "email_queue".to_string(),
573    ///     json!({
574    ///         "to": "user@example.com",
575    ///         "subject": "Don't forget to complete your profile",
576    ///         "template": "reminder"
577    ///     }),
578    ///     Duration::hours(24)
579    /// );
580    ///
581    /// // Job will be scheduled 24 hours from now
582    /// assert!(job.scheduled_at > job.created_at);
583    /// let delay_diff = job.scheduled_at - job.created_at;
584    /// assert_eq!(delay_diff, Duration::hours(24));
585    /// ```
586    pub fn with_delay(
587        queue_name: String,
588        payload: serde_json::Value,
589        delay: chrono::Duration,
590    ) -> Self {
591        let now = Utc::now();
592        Self {
593            id: Uuid::new_v4(),
594            queue_name,
595            payload,
596            status: JobStatus::Pending,
597            attempts: 0,
598            max_attempts: 3,
599            created_at: now,
600            scheduled_at: now + delay,
601            started_at: None,
602            completed_at: None,
603            failed_at: None,
604            timed_out_at: None,
605            timeout: None,
606            error_message: None,
607            priority: JobPriority::default(),
608            cron_schedule: None,
609            next_run_at: None,
610            recurring: false,
611            timezone: None,
612            batch_id: None,
613            result_config: ResultConfig::default(),
614            result_data: None,
615            result_stored_at: None,
616            result_expires_at: None,
617            retry_strategy: None,
618            depends_on: Vec::new(),
619            dependents: Vec::new(),
620            dependency_status: crate::workflow::DependencyStatus::None,
621            workflow_id: None,
622            workflow_name: None,
623            trace_id: None,
624            correlation_id: None,
625            parent_span_id: None,
626            span_context: None,
627            #[cfg(feature = "encryption")]
628            encryption_config: None,
629            pii_fields: Vec::new(),
630            #[cfg(feature = "encryption")]
631            retention_policy: None,
632            is_encrypted: false,
633            #[cfg(feature = "encryption")]
634            encrypted_payload: None,
635        }
636    }
637
638    /// Sets the maximum number of retry attempts for this job.
639    ///
640    /// When a job fails, it will be retried up to this many times before being
641    /// marked as dead. The default is 3 attempts.
642    ///
643    /// # Arguments
644    ///
645    /// * `max_attempts` - Maximum number of attempts (including the initial attempt)
646    ///
647    /// # Examples
648    ///
649    /// ```rust
650    /// use hammerwork::Job;
651    /// use serde_json::json;
652    ///
653    /// // Critical job that should be retried many times
654    /// let job = Job::new("critical_task".to_string(), json!({"task": "important"}))
655    ///     .with_max_attempts(10);
656    ///
657    /// assert_eq!(job.max_attempts, 10);
658    /// ```
659    pub fn with_max_attempts(mut self, max_attempts: i32) -> Self {
660        self.max_attempts = max_attempts;
661        self
662    }
663
664    /// Sets a timeout duration for this job.
665    ///
666    /// If the job takes longer than this duration to complete, it will be
667    /// automatically terminated and marked as timed out. Job-level timeouts
668    /// take precedence over worker-level default timeouts.
669    ///
670    /// The PostgreSQL and MySQL backends store the timeout in whole seconds, rounding a
671    /// fractional timeout up (`200ms` is stored as one second). `Duration::ZERO` means no
672    /// job-level timeout, so the worker's default applies.
673    ///
674    /// # Arguments
675    ///
676    /// * `timeout` - Maximum duration the job is allowed to run
677    ///
678    /// # Examples
679    ///
680    /// ```rust
681    /// use hammerwork::Job;
682    /// use serde_json::json;
683    /// use std::time::Duration;
684    ///
685    /// // API call that should timeout after 30 seconds
686    /// let job = Job::new("api_call".to_string(), json!({"url": "https://api.example.com"}))
687    ///     .with_timeout(Duration::from_secs(30));
688    ///
689    /// assert_eq!(job.timeout, Some(Duration::from_secs(30)));
690    /// ```
691    pub fn with_timeout(mut self, timeout: std::time::Duration) -> Self {
692        self.timeout = Some(timeout);
693        self
694    }
695
696    /// Sets the priority level for this job.
697    ///
698    /// Priority affects the order in which jobs are processed by workers.
699    /// Higher priority jobs are generally processed before lower priority ones.
700    ///
701    /// # Arguments
702    ///
703    /// * `priority` - The priority level to assign to this job
704    ///
705    /// # Examples
706    ///
707    /// ```rust
708    /// use hammerwork::{Job, JobPriority};
709    /// use serde_json::json;
710    ///
711    /// let job = Job::new("task".to_string(), json!({"data": "test"}))
712    ///     .with_priority(JobPriority::High);
713    ///
714    /// assert_eq!(job.priority, JobPriority::High);
715    /// assert!(job.is_high_priority());
716    /// ```
717    pub fn with_priority(mut self, priority: JobPriority) -> Self {
718        self.priority = priority;
719        self
720    }
721
722    /// Sets the job as critical priority (highest priority).
723    ///
724    /// Critical jobs are processed with the highest priority and should be used
725    /// sparingly for truly urgent work like system alerts or emergency responses.
726    ///
727    /// # Examples
728    ///
729    /// ```rust
730    /// use hammerwork::{Job, JobPriority};
731    /// use serde_json::json;
732    ///
733    /// let job = Job::new("system_alert".to_string(), json!({"alert": "system_down"}))
734    ///     .as_critical();
735    ///
736    /// assert_eq!(job.priority, JobPriority::Critical);
737    /// assert!(job.is_critical());
738    /// ```
739    pub fn as_critical(mut self) -> Self {
740        self.priority = JobPriority::Critical;
741        self
742    }
743
744    /// Sets the job as high priority.
745    ///
746    /// High priority jobs are processed before normal priority jobs but after
747    /// critical jobs. Suitable for user-facing operations or important business logic.
748    ///
749    /// # Examples
750    ///
751    /// ```rust
752    /// use hammerwork::{Job, JobPriority};
753    /// use serde_json::json;
754    ///
755    /// let job = Job::new("user_notification".to_string(), json!({"user_id": 123}))
756    ///     .as_high_priority();
757    ///
758    /// assert_eq!(job.priority, JobPriority::High);
759    /// assert!(job.is_high_priority());
760    /// ```
761    pub fn as_high_priority(mut self) -> Self {
762        self.priority = JobPriority::High;
763        self
764    }
765
766    /// Sets the job as low priority.
767    ///
768    /// Low priority jobs are processed after normal priority jobs but before
769    /// background jobs. Suitable for analytics, reporting, or non-urgent tasks.
770    ///
771    /// # Examples
772    ///
773    /// ```rust
774    /// use hammerwork::{Job, JobPriority};
775    /// use serde_json::json;
776    ///
777    /// let job = Job::new("analytics".to_string(), json!({"event": "page_view"}))
778    ///     .as_low_priority();
779    ///
780    /// assert_eq!(job.priority, JobPriority::Low);
781    /// assert!(job.is_low_priority());
782    /// ```
783    pub fn as_low_priority(mut self) -> Self {
784        self.priority = JobPriority::Low;
785        self
786    }
787
788    /// Sets the job as background priority (lowest priority).
789    ///
790    /// Background jobs are processed only when no higher priority jobs are available.
791    /// Suitable for cleanup tasks, maintenance, or work that can wait indefinitely.
792    ///
793    /// # Examples
794    ///
795    /// ```rust
796    /// use hammerwork::{Job, JobPriority};
797    /// use serde_json::json;
798    ///
799    /// let job = Job::new("cleanup".to_string(), json!({"type": "temp_files"}))
800    ///     .as_background();
801    ///
802    /// assert_eq!(job.priority, JobPriority::Background);
803    /// assert!(job.is_background());
804    /// ```
805    pub fn as_background(mut self) -> Self {
806        self.priority = JobPriority::Background;
807        self
808    }
809
810    /// Sets a custom retry strategy for this job.
811    ///
812    /// The retry strategy determines how long to wait between retry attempts
813    /// when the job fails. This overrides any default retry strategy configured
814    /// on the worker.
815    ///
816    /// # Arguments
817    ///
818    /// * `strategy` - The retry strategy to use for this job
819    ///
820    /// # Examples
821    ///
822    /// ```rust
823    /// use hammerwork::{Job, retry::RetryStrategy};
824    /// use serde_json::json;
825    /// use std::time::Duration;
826    ///
827    /// // Use exponential backoff for API calls
828    /// let job = Job::new("api_call".to_string(), json!({"url": "https://api.example.com"}))
829    ///     .with_retry_strategy(RetryStrategy::exponential(
830    ///         Duration::from_secs(1),
831    ///         2.0,
832    ///         Some(Duration::from_secs(10 * 60))
833    ///     ));
834    /// ```
835    pub fn with_retry_strategy(mut self, strategy: RetryStrategy) -> Self {
836        self.retry_strategy = Some(strategy);
837        self
838    }
839
840    /// Sets exponential backoff retry strategy for this job.
841    ///
842    /// This is a convenience method for the most common retry pattern.
843    /// Each retry attempt waits exponentially longer than the previous one.
844    ///
845    /// # Arguments
846    ///
847    /// * `base` - Base delay for the first retry attempt
848    /// * `multiplier` - Exponential growth multiplier (typically 2.0)
849    /// * `max_delay` - Maximum delay to cap exponential growth
850    ///
851    /// # Examples
852    ///
853    /// ```rust
854    /// use hammerwork::Job;
855    /// use serde_json::json;
856    /// use std::time::Duration;
857    ///
858    /// // Exponential backoff: 1s, 2s, 4s, 8s, 16s... (capped at 10 minutes)
859    /// let job = Job::new("network_request".to_string(), json!({"url": "https://example.com"}))
860    ///     .with_exponential_backoff(
861    ///         Duration::from_secs(1),
862    ///         2.0,
863    ///         Duration::from_secs(10 * 60)
864    ///     );
865    /// ```
866    pub fn with_exponential_backoff(
867        mut self,
868        base: std::time::Duration,
869        multiplier: f64,
870        max_delay: std::time::Duration,
871    ) -> Self {
872        self.retry_strategy = Some(RetryStrategy::exponential(
873            base,
874            multiplier,
875            Some(max_delay),
876        ));
877        self
878    }
879
880    /// Sets linear backoff retry strategy for this job.
881    ///
882    /// Each retry attempt waits longer than the previous by a fixed increment.
883    ///
884    /// # Arguments
885    ///
886    /// * `base` - Base delay for the first retry attempt
887    /// * `increment` - Amount to add for each subsequent attempt
888    /// * `max_delay` - Optional maximum delay to cap growth
889    ///
890    /// # Examples
891    ///
892    /// ```rust
893    /// use hammerwork::Job;
894    /// use serde_json::json;
895    /// use std::time::Duration;
896    ///
897    /// // Linear backoff: 10s, 20s, 30s, 40s... (capped at 2 minutes)
898    /// let job = Job::new("database_operation".to_string(), json!({"query": "SELECT ..."}))
899    ///     .with_linear_backoff(
900    ///         Duration::from_secs(10),
901    ///         Duration::from_secs(10),
902    ///         Some(Duration::from_secs(2 * 60))
903    ///     );
904    /// ```
905    pub fn with_linear_backoff(
906        mut self,
907        base: std::time::Duration,
908        increment: std::time::Duration,
909        max_delay: Option<std::time::Duration>,
910    ) -> Self {
911        self.retry_strategy = Some(RetryStrategy::linear(base, increment, max_delay));
912        self
913    }
914
915    /// Sets Fibonacci sequence backoff retry strategy for this job.
916    ///
917    /// Each retry waits according to the Fibonacci sequence multiplied by the base delay.
918    ///
919    /// # Arguments
920    ///
921    /// * `base` - Base delay multiplied by Fibonacci numbers
922    /// * `max_delay` - Optional maximum delay to cap growth
923    ///
924    /// # Examples
925    ///
926    /// ```rust
927    /// use hammerwork::Job;
928    /// use serde_json::json;
929    /// use std::time::Duration;
930    ///
931    /// // Fibonacci backoff: 2s, 2s, 4s, 6s, 10s, 16s, 26s...
932    /// let job = Job::new("file_processing".to_string(), json!({"file": "data.csv"}))
933    ///     .with_fibonacci_backoff(
934    ///         Duration::from_secs(2),
935    ///         Some(Duration::from_secs(5 * 60))
936    ///     );
937    /// ```
938    pub fn with_fibonacci_backoff(
939        mut self,
940        base: std::time::Duration,
941        max_delay: Option<std::time::Duration>,
942    ) -> Self {
943        self.retry_strategy = Some(RetryStrategy::fibonacci(base, max_delay));
944        self
945    }
946
947    /// Create a recurring job with a cron schedule
948    pub fn with_cron_schedule(
949        queue_name: String,
950        payload: serde_json::Value,
951        cron_schedule: CronSchedule,
952    ) -> Result<Self, crate::cron::CronError> {
953        let now = Utc::now();
954        let next_run = cron_schedule.next_execution_from_now();
955
956        Ok(Self {
957            id: Uuid::new_v4(),
958            queue_name,
959            payload,
960            status: JobStatus::Pending,
961            attempts: 0,
962            max_attempts: 3,
963            created_at: now,
964            scheduled_at: next_run.unwrap_or(now),
965            started_at: None,
966            completed_at: None,
967            failed_at: None,
968            timed_out_at: None,
969            timeout: None,
970            error_message: None,
971            priority: JobPriority::default(),
972            cron_schedule: Some(cron_schedule.expression.clone()),
973            next_run_at: next_run,
974            recurring: true,
975            timezone: Some(cron_schedule.timezone.clone()),
976            batch_id: None,
977            result_config: ResultConfig::default(),
978            result_data: None,
979            result_stored_at: None,
980            result_expires_at: None,
981            retry_strategy: None,
982            depends_on: Vec::new(),
983            dependents: Vec::new(),
984            dependency_status: crate::workflow::DependencyStatus::None,
985            workflow_id: None,
986            workflow_name: None,
987            trace_id: None,
988            correlation_id: None,
989            parent_span_id: None,
990            span_context: None,
991            #[cfg(feature = "encryption")]
992            encryption_config: None,
993            pii_fields: Vec::new(),
994            #[cfg(feature = "encryption")]
995            retention_policy: None,
996            is_encrypted: false,
997            #[cfg(feature = "encryption")]
998            encrypted_payload: None,
999        })
1000    }
1001
1002    /// Add a cron schedule to an existing job
1003    pub fn with_cron(
1004        mut self,
1005        cron_schedule: CronSchedule,
1006    ) -> Result<Self, crate::cron::CronError> {
1007        let next_run = cron_schedule.next_execution_from_now();
1008        self.cron_schedule = Some(cron_schedule.expression.clone());
1009        self.next_run_at = next_run;
1010        self.recurring = true;
1011        self.timezone = Some(cron_schedule.timezone.clone());
1012        self.scheduled_at = next_run.unwrap_or(self.scheduled_at);
1013        Ok(self)
1014    }
1015
1016    /// Schedule the job to run at `scheduled_at` instead of as soon as possible.
1017    ///
1018    /// # Examples
1019    ///
1020    /// ```rust
1021    /// use chrono::{Duration, Utc};
1022    /// use hammerwork::Job;
1023    /// use serde_json::json;
1024    ///
1025    /// let run_at = Utc::now() + Duration::days(7);
1026    /// let job = Job::new("reports".to_string(), json!({})).with_scheduled_at(run_at);
1027    /// assert_eq!(job.scheduled_at, run_at);
1028    /// ```
1029    pub fn with_scheduled_at(mut self, scheduled_at: DateTime<Utc>) -> Self {
1030        self.scheduled_at = scheduled_at;
1031        self
1032    }
1033
1034    /// Set the job as recurring without a cron schedule (for manual rescheduling)
1035    pub fn as_recurring(mut self) -> Self {
1036        self.recurring = true;
1037        self
1038    }
1039
1040    /// Set the timezone for the job
1041    pub fn with_timezone(mut self, timezone: String) -> Self {
1042        self.timezone = Some(timezone);
1043        self
1044    }
1045
1046    /// Configure how job results should be stored.
1047    ///
1048    /// This allows jobs to store their results for later retrieval by other systems.
1049    /// Results can be stored in the database, memory, or not stored at all.
1050    ///
1051    /// # Arguments
1052    ///
1053    /// * `storage` - The storage backend to use for results
1054    ///
1055    /// # Examples
1056    ///
1057    /// ```rust
1058    /// use hammerwork::{Job, job::ResultStorage};
1059    /// use serde_json::json;
1060    ///
1061    /// let job = Job::new("data_processing".to_string(), json!({"input": "data"}))
1062    ///     .with_result_storage(ResultStorage::Database);
1063    /// ```
1064    pub fn with_result_storage(mut self, storage: ResultStorage) -> Self {
1065        self.result_config.storage = storage;
1066        self
1067    }
1068
1069    /// Set the time-to-live (TTL) for stored job results.
1070    ///
1071    /// After this duration elapses, the result will be eligible for cleanup.
1072    /// This is useful for managing storage costs and compliance requirements.
1073    ///
1074    /// # Arguments
1075    ///
1076    /// * `ttl` - How long to keep results before they expire
1077    ///
1078    /// # Examples
1079    ///
1080    /// ```rust
1081    /// use hammerwork::Job;
1082    /// use serde_json::json;
1083    /// use std::time::Duration;
1084    ///
1085    /// let job = Job::new("report_generation".to_string(), json!({"type": "monthly"}))
1086    ///     .with_result_ttl(Duration::from_secs(7 * 24 * 60 * 60)); // 7 days
1087    /// ```
1088    pub fn with_result_ttl(mut self, ttl: std::time::Duration) -> Self {
1089        self.result_config.ttl = Some(ttl);
1090        self
1091    }
1092
1093    /// Configure complete result storage settings.
1094    ///
1095    /// This provides full control over how results are stored and managed.
1096    ///
1097    /// # Arguments
1098    ///
1099    /// * `config` - Complete result configuration
1100    ///
1101    /// # Examples
1102    ///
1103    /// ```rust
1104    /// use hammerwork::{Job, job::{ResultConfig, ResultStorage}};
1105    /// use serde_json::json;
1106    /// use std::time::Duration;
1107    ///
1108    /// let config = ResultConfig::new(ResultStorage::Database)
1109    ///     .with_ttl(Duration::from_secs(3600))
1110    ///     .with_max_size(1024 * 1024); // 1MB
1111    ///
1112    /// let job = Job::new("large_processing".to_string(), json!({"data": "..."}))
1113    ///     .with_result_config(config);
1114    /// ```
1115    pub fn with_result_config(mut self, config: ResultConfig) -> Self {
1116        self.result_config = config;
1117        self
1118    }
1119
1120    /// Check if the job has result storage configured.
1121    ///
1122    /// Returns `true` if the job is configured to store results in any backend
1123    /// other than `ResultStorage::None`.
1124    ///
1125    /// # Examples
1126    ///
1127    /// ```rust
1128    /// use hammerwork::{Job, job::ResultStorage};
1129    /// use serde_json::json;
1130    ///
1131    /// let job1 = Job::new("test".to_string(), json!({}));
1132    /// assert!(!job1.has_result_storage());
1133    ///
1134    /// let job2 = Job::new("test".to_string(), json!({}))
1135    ///     .with_result_storage(ResultStorage::Database);
1136    /// assert!(job2.has_result_storage());
1137    /// ```
1138    pub fn has_result_storage(&self) -> bool {
1139        self.result_config.storage != ResultStorage::None
1140    }
1141
1142    /// Check if the job has stored result data.
1143    ///
1144    /// Returns `true` if the job has result data available for retrieval.
1145    ///
1146    /// # Examples
1147    ///
1148    /// ```rust
1149    /// use hammerwork::Job;
1150    /// use serde_json::json;
1151    ///
1152    /// let job = Job::new("test".to_string(), json!({}));
1153    /// assert!(!job.has_result_data());
1154    /// ```
1155    pub fn has_result_data(&self) -> bool {
1156        self.result_data.is_some()
1157    }
1158
1159    /// Check if the job is dead (failed all retry attempts)
1160    pub fn is_dead(&self) -> bool {
1161        self.status == JobStatus::Dead
1162    }
1163
1164    /// Check if the job has timed out
1165    pub fn is_timed_out(&self) -> bool {
1166        self.status == JobStatus::TimedOut
1167    }
1168
1169    /// Check if the job is critical priority
1170    pub fn is_critical(&self) -> bool {
1171        self.priority == JobPriority::Critical
1172    }
1173
1174    /// Check if the job is high priority
1175    pub fn is_high_priority(&self) -> bool {
1176        self.priority == JobPriority::High
1177    }
1178
1179    /// Check if the job is normal priority
1180    pub fn is_normal_priority(&self) -> bool {
1181        self.priority == JobPriority::Normal
1182    }
1183
1184    /// Check if the job is low priority
1185    pub fn is_low_priority(&self) -> bool {
1186        self.priority == JobPriority::Low
1187    }
1188
1189    /// Check if the job is background priority
1190    pub fn is_background(&self) -> bool {
1191        self.priority == JobPriority::Background
1192    }
1193
1194    /// Get the priority level as a numeric value for comparison
1195    pub fn priority_value(&self) -> i32 {
1196        self.priority.as_i32()
1197    }
1198
1199    /// Check if the job has exhausted all retry attempts
1200    pub fn has_exhausted_retries(&self) -> bool {
1201        self.attempts >= self.max_attempts
1202    }
1203
1204    /// Checks if the job should timeout based on its start time and timeout setting.
1205    ///
1206    /// Returns `true` if the job has been running longer than its configured timeout
1207    /// duration. Returns `false` if the job hasn't started yet, has no timeout set,
1208    /// or is still within the timeout window.
1209    ///
1210    /// # Examples
1211    ///
1212    /// ```rust
1213    /// use hammerwork::Job;
1214    /// use serde_json::json;
1215    /// use std::time::Duration;
1216    /// use chrono::Utc;
1217    ///
1218    /// let mut job = Job::new("test".to_string(), json!({"data": "test"}))
1219    ///     .with_timeout(Duration::from_secs(30));
1220    ///
1221    /// // Job hasn't started yet, so it shouldn't timeout
1222    /// assert!(!job.should_timeout());
1223    ///
1224    /// // Simulate job starting 45 seconds ago
1225    /// job.started_at = Some(Utc::now() - chrono::Duration::seconds(45));
1226    ///
1227    /// // Job should timeout since 45s > 30s timeout
1228    /// assert!(job.should_timeout());
1229    /// ```
1230    pub fn should_timeout(&self) -> bool {
1231        if let (Some(started_at), Some(timeout)) = (self.started_at, self.timeout) {
1232            let elapsed = Utc::now() - started_at;
1233            let timeout_duration = chrono::Duration::from_std(timeout).unwrap_or_default();
1234            elapsed >= timeout_duration
1235        } else {
1236            false
1237        }
1238    }
1239
1240    /// Gets the duration since the job was created.
1241    ///
1242    /// This is useful for monitoring how long jobs have been in the system
1243    /// and identifying jobs that may be stuck or delayed.
1244    ///
1245    /// # Examples
1246    ///
1247    /// ```rust
1248    /// use hammerwork::Job;
1249    /// use serde_json::json;
1250    ///
1251    /// let job = Job::new("test".to_string(), json!({"data": "test"}));
1252    /// let age = job.age();
1253    ///
1254    /// // Job was just created, so age should be very small
1255    /// assert!(age.num_milliseconds() >= 0);
1256    /// assert!(age.num_seconds() < 1);
1257    /// ```
1258    pub fn age(&self) -> chrono::Duration {
1259        Utc::now() - self.created_at
1260    }
1261
1262    /// Gets the processing duration if the job has started.
1263    ///
1264    /// Returns the time between when the job started and when it finished
1265    /// (completed, failed, or timed out). If the job is still running,
1266    /// returns the time since it started. Returns `None` if the job hasn't
1267    /// started yet.
1268    ///
1269    /// # Examples
1270    ///
1271    /// ```rust
1272    /// use hammerwork::Job;
1273    /// use serde_json::json;
1274    /// use chrono::Utc;
1275    ///
1276    /// let mut job = Job::new("test".to_string(), json!({"data": "test"}));
1277    ///
1278    /// // Job hasn't started, so no processing duration
1279    /// assert!(job.processing_duration().is_none());
1280    ///
1281    /// // Simulate job that started and completed
1282    /// let start_time = Utc::now() - chrono::Duration::seconds(10);
1283    /// let end_time = start_time + chrono::Duration::seconds(5);
1284    /// job.started_at = Some(start_time);
1285    /// job.completed_at = Some(end_time);
1286    ///
1287    /// let duration = job.processing_duration().unwrap();
1288    /// assert_eq!(duration.num_seconds(), 5);
1289    /// ```
1290    pub fn processing_duration(&self) -> Option<chrono::Duration> {
1291        self.started_at.map(|started| {
1292            self.completed_at
1293                .or(self.failed_at)
1294                .or(self.timed_out_at)
1295                .unwrap_or_else(Utc::now)
1296                - started
1297        })
1298    }
1299
1300    /// Check if this is a recurring job
1301    pub fn is_recurring(&self) -> bool {
1302        self.recurring
1303    }
1304
1305    /// Check if this job has a cron schedule
1306    pub fn has_cron_schedule(&self) -> bool {
1307        self.cron_schedule.is_some()
1308    }
1309
1310    /// Get the cron schedule if it exists
1311    pub fn get_cron_schedule(&self) -> Option<Result<CronSchedule, crate::cron::CronError>> {
1312        self.cron_schedule
1313            .as_ref()
1314            .map(|expr| match &self.timezone {
1315                Some(tz) => CronSchedule::with_timezone(expr, tz),
1316                None => CronSchedule::new(expr),
1317            })
1318    }
1319
1320    /// Calculate the next run time for a recurring job
1321    pub fn calculate_next_run(&self) -> Option<DateTime<Utc>> {
1322        if !self.recurring {
1323            return None;
1324        }
1325
1326        if let Some(cron_schedule) = self.get_cron_schedule() {
1327            match cron_schedule {
1328                Ok(schedule) => schedule.next_execution_from_now(),
1329                Err(_) => None,
1330            }
1331        } else {
1332            None
1333        }
1334    }
1335
1336    /// Update the job for the next run (for recurring jobs)
1337    pub fn prepare_for_next_run(&mut self) -> Option<DateTime<Utc>> {
1338        if !self.recurring {
1339            return None;
1340        }
1341
1342        let next_run = self.calculate_next_run();
1343        if let Some(next_time) = next_run {
1344            self.status = JobStatus::Pending;
1345            self.attempts = 0;
1346            self.scheduled_at = next_time;
1347            self.next_run_at = Some(next_time);
1348            self.started_at = None;
1349            self.completed_at = None;
1350            self.failed_at = None;
1351            self.timed_out_at = None;
1352            self.error_message = None;
1353        }
1354        next_run
1355    }
1356
1357    /// Adds a dependency on another job.
1358    ///
1359    /// This job will not be executed until the specified job completes successfully.
1360    /// If the dependency job fails, this job's dependency status will be set to Failed.
1361    ///
1362    /// # Arguments
1363    ///
1364    /// * `job_id` - The ID of the job this job should depend on
1365    ///
1366    /// # Examples
1367    ///
1368    /// ```rust
1369    /// use hammerwork::Job;
1370    /// use serde_json::json;
1371    ///
1372    /// let job1 = Job::new("step1".to_string(), json!({"data": "step1"}));
1373    /// let job2 = Job::new("step2".to_string(), json!({"data": "step2"}))
1374    ///     .depends_on(&job1.id);
1375    /// assert!(job2.has_dependencies());
1376    /// ```
1377    pub fn depends_on(mut self, job_id: &JobId) -> Self {
1378        self.depends_on.push(*job_id);
1379        self.dependency_status = crate::workflow::DependencyStatus::Waiting;
1380        self
1381    }
1382
1383    /// Adds multiple dependencies on other jobs.
1384    ///
1385    /// This job will not be executed until all specified jobs complete successfully.
1386    ///
1387    /// # Arguments
1388    ///
1389    /// * `job_ids` - The IDs of the jobs this job should depend on
1390    ///
1391    /// # Examples
1392    ///
1393    /// ```rust
1394    /// use hammerwork::Job;
1395    /// use serde_json::json;
1396    ///
1397    /// let job1 = Job::new("step1".to_string(), json!({}));
1398    /// let job2 = Job::new("step2".to_string(), json!({}));
1399    /// let final_job = Job::new("final".to_string(), json!({}))
1400    ///     .depends_on_jobs(&[job1.id, job2.id]);
1401    /// assert_eq!(final_job.depends_on.len(), 2);
1402    /// ```
1403    pub fn depends_on_jobs(mut self, job_ids: &[JobId]) -> Self {
1404        self.depends_on.extend_from_slice(job_ids);
1405        if !job_ids.is_empty() {
1406            self.dependency_status = crate::workflow::DependencyStatus::Waiting;
1407        }
1408        self
1409    }
1410
1411    /// Sets the workflow this job belongs to.
1412    ///
1413    /// # Arguments
1414    ///
1415    /// * `workflow_id` - The ID of the workflow
1416    /// * `workflow_name` - The name of the workflow
1417    ///
1418    /// # Examples
1419    ///
1420    /// ```rust
1421    /// use hammerwork::Job;
1422    /// use serde_json::json;
1423    /// use uuid::Uuid;
1424    ///
1425    /// let workflow_id = Uuid::new_v4();
1426    /// let job = Job::new("test".to_string(), json!({}))
1427    ///     .with_workflow(workflow_id, "data_pipeline");
1428    ///
1429    /// assert_eq!(job.workflow_id, Some(workflow_id));
1430    /// assert_eq!(job.workflow_name, Some("data_pipeline".to_string()));
1431    /// ```
1432    pub fn with_workflow(
1433        mut self,
1434        workflow_id: crate::workflow::WorkflowId,
1435        workflow_name: impl Into<String>,
1436    ) -> Self {
1437        self.workflow_id = Some(workflow_id);
1438        self.workflow_name = Some(workflow_name.into());
1439        self
1440    }
1441
1442    /// Checks if this job has any dependencies.
1443    ///
1444    /// # Examples
1445    ///
1446    /// ```rust
1447    /// use hammerwork::Job;
1448    /// use serde_json::json;
1449    ///
1450    /// let job1 = Job::new("independent".to_string(), json!({}));
1451    /// assert!(!job1.has_dependencies());
1452    ///
1453    /// let job2 = Job::new("dependent".to_string(), json!({}))
1454    ///     .depends_on(&job1.id);
1455    /// assert!(job2.has_dependencies());
1456    /// ```
1457    pub fn has_dependencies(&self) -> bool {
1458        !self.depends_on.is_empty()
1459    }
1460
1461    /// Checks if this job is part of a workflow.
1462    ///
1463    /// # Examples
1464    ///
1465    /// ```rust
1466    /// use hammerwork::Job;
1467    /// use serde_json::json;
1468    /// use uuid::Uuid;
1469    ///
1470    /// let job1 = Job::new("standalone".to_string(), json!({}));
1471    /// assert!(!job1.is_part_of_workflow());
1472    ///
1473    /// let job2 = Job::new("workflow_job".to_string(), json!({}))
1474    ///     .with_workflow(Uuid::new_v4(), "test_workflow");
1475    /// assert!(job2.is_part_of_workflow());
1476    /// ```
1477    pub fn is_part_of_workflow(&self) -> bool {
1478        self.workflow_id.is_some()
1479    }
1480
1481    /// Checks if all dependencies for this job are satisfied.
1482    ///
1483    /// # Examples
1484    ///
1485    /// ```rust
1486    /// use hammerwork::Job;
1487    /// use serde_json::json;
1488    ///
1489    /// let job1 = Job::new("independent".to_string(), json!({}));
1490    /// assert!(job1.dependencies_satisfied());
1491    ///
1492    /// let job2 = Job::new("dependent".to_string(), json!({}))
1493    ///     .depends_on(&job1.id);
1494    /// assert!(!job2.dependencies_satisfied()); // Dependencies not satisfied yet
1495    /// ```
1496    pub fn dependencies_satisfied(&self) -> bool {
1497        matches!(
1498            self.dependency_status,
1499            crate::workflow::DependencyStatus::None | crate::workflow::DependencyStatus::Satisfied
1500        )
1501    }
1502
1503    /// Checks if any dependencies for this job have failed.
1504    ///
1505    /// # Examples
1506    ///
1507    /// ```rust
1508    /// use hammerwork::Job;
1509    /// use serde_json::json;
1510    ///
1511    /// let mut job = Job::new("test".to_string(), json!({}));
1512    /// job.dependency_status = hammerwork::workflow::DependencyStatus::Failed;
1513    /// assert!(job.dependencies_failed());
1514    /// ```
1515    pub fn dependencies_failed(&self) -> bool {
1516        matches!(
1517            self.dependency_status,
1518            crate::workflow::DependencyStatus::Failed
1519        )
1520    }
1521
1522    /// Gets the number of dependencies for this job.
1523    ///
1524    /// # Examples
1525    ///
1526    /// ```rust
1527    /// use hammerwork::Job;
1528    /// use serde_json::json;
1529    /// use uuid::Uuid;
1530    ///
1531    /// let job = Job::new("test".to_string(), json!({}))
1532    ///     .depends_on_jobs(&[Uuid::new_v4(), Uuid::new_v4()]);
1533    /// assert_eq!(job.dependency_count(), 2);
1534    /// ```
1535    pub fn dependency_count(&self) -> usize {
1536        self.depends_on.len()
1537    }
1538
1539    /// Gets the number of jobs that depend on this job.
1540    ///
1541    /// # Examples
1542    ///
1543    /// ```rust
1544    /// use hammerwork::Job;
1545    /// use serde_json::json;
1546    ///
1547    /// let job = Job::new("test".to_string(), json!({}));
1548    /// assert_eq!(job.dependent_count(), 0);
1549    /// ```
1550    pub fn dependent_count(&self) -> usize {
1551        self.dependents.len()
1552    }
1553
1554    /// Sets the distributed trace identifier for this job.
1555    ///
1556    /// The trace ID is used to track jobs across service boundaries in distributed
1557    /// systems. All related operations should share the same trace ID.
1558    ///
1559    /// # Arguments
1560    ///
1561    /// * `trace_id` - The distributed trace identifier
1562    ///
1563    /// # Examples
1564    ///
1565    /// ```rust
1566    /// use hammerwork::Job;
1567    /// use serde_json::json;
1568    ///
1569    /// let job = Job::new("service_call".to_string(), json!({"data": "test"}))
1570    ///     .with_trace_id("trace-123-456");
1571    ///
1572    /// assert_eq!(job.trace_id, Some("trace-123-456".to_string()));
1573    /// ```
1574    pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
1575        self.trace_id = Some(trace_id.into());
1576        self
1577    }
1578
1579    /// Sets the business correlation identifier for this job.
1580    ///
1581    /// The correlation ID groups related business operations together, even if they
1582    /// span multiple traces or services. Use this to correlate jobs that process
1583    /// the same business entity or workflow.
1584    ///
1585    /// # Arguments
1586    ///
1587    /// * `correlation_id` - The business correlation identifier
1588    ///
1589    /// # Examples
1590    ///
1591    /// ```rust
1592    /// use hammerwork::Job;
1593    /// use serde_json::json;
1594    ///
1595    /// let job = Job::new("order_processing".to_string(), json!({"order_id": 12345}))
1596    ///     .with_correlation_id("order-12345");
1597    ///
1598    /// assert_eq!(job.correlation_id, Some("order-12345".to_string()));
1599    /// ```
1600    pub fn with_correlation_id(mut self, correlation_id: impl Into<String>) -> Self {
1601        self.correlation_id = Some(correlation_id.into());
1602        self
1603    }
1604
1605    /// Sets the parent span identifier for hierarchical tracing.
1606    ///
1607    /// Use this to create a parent-child relationship between spans, enabling
1608    /// hierarchical trace visualization in tracing systems.
1609    ///
1610    /// # Arguments
1611    ///
1612    /// * `parent_span_id` - The parent span identifier
1613    ///
1614    /// # Examples
1615    ///
1616    /// ```rust
1617    /// use hammerwork::Job;
1618    /// use serde_json::json;
1619    ///
1620    /// let job = Job::new("child_task".to_string(), json!({"data": "test"}))
1621    ///     .with_parent_span_id("span-parent-123");
1622    ///
1623    /// assert_eq!(job.parent_span_id, Some("span-parent-123".to_string()));
1624    /// ```
1625    pub fn with_parent_span_id(mut self, parent_span_id: impl Into<String>) -> Self {
1626        self.parent_span_id = Some(parent_span_id.into());
1627        self
1628    }
1629
1630    /// Sets the serialized span context for trace propagation.
1631    ///
1632    /// The span context contains all the information needed to propagate tracing
1633    /// across service boundaries. This is typically a serialized representation
1634    /// of the current span context.
1635    ///
1636    /// # Arguments
1637    ///
1638    /// * `span_context` - The serialized span context
1639    ///
1640    /// # Examples
1641    ///
1642    /// ```rust
1643    /// use hammerwork::Job;
1644    /// use serde_json::json;
1645    ///
1646    /// let job = Job::new("distributed_task".to_string(), json!({"data": "test"}))
1647    ///     .with_span_context("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01");
1648    ///
1649    /// assert_eq!(job.span_context, Some("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01".to_string()));
1650    /// ```
1651    pub fn with_span_context(mut self, span_context: impl Into<String>) -> Self {
1652        self.span_context = Some(span_context.into());
1653        self
1654    }
1655
1656    /// Convenience method to set both trace ID and correlation ID.
1657    ///
1658    /// This is useful when the trace ID and correlation ID are the same,
1659    /// which is common in simple tracing scenarios.
1660    ///
1661    /// # Arguments
1662    ///
1663    /// * `id` - The identifier to use for both trace and correlation
1664    ///
1665    /// # Examples
1666    ///
1667    /// ```rust
1668    /// use hammerwork::Job;
1669    /// use serde_json::json;
1670    ///
1671    /// let job = Job::new("task".to_string(), json!({"data": "test"}))
1672    ///     .with_tracing_id("unified-id-123");
1673    ///
1674    /// assert_eq!(job.trace_id, Some("unified-id-123".to_string()));
1675    /// assert_eq!(job.correlation_id, Some("unified-id-123".to_string()));
1676    /// ```
1677    pub fn with_tracing_id(mut self, id: impl Into<String>) -> Self {
1678        let id_string = id.into();
1679        self.trace_id = Some(id_string.clone());
1680        self.correlation_id = Some(id_string);
1681        self
1682    }
1683
1684    /// Checks if this job has any tracing information.
1685    ///
1686    /// Returns `true` if any of the tracing fields (trace_id, correlation_id,
1687    /// parent_span_id, or span_context) are set.
1688    ///
1689    /// # Examples
1690    ///
1691    /// ```rust
1692    /// use hammerwork::Job;
1693    /// use serde_json::json;
1694    ///
1695    /// let job1 = Job::new("untraced".to_string(), json!({}));
1696    /// assert!(!job1.has_tracing_info());
1697    ///
1698    /// let job2 = Job::new("traced".to_string(), json!({}))
1699    ///     .with_trace_id("trace-123");
1700    /// assert!(job2.has_tracing_info());
1701    /// ```
1702    pub fn has_tracing_info(&self) -> bool {
1703        self.trace_id.is_some()
1704            || self.correlation_id.is_some()
1705            || self.parent_span_id.is_some()
1706            || self.span_context.is_some()
1707    }
1708
1709    /// Gets the trace ID if available.
1710    ///
1711    /// # Examples
1712    ///
1713    /// ```rust
1714    /// use hammerwork::Job;
1715    /// use serde_json::json;
1716    ///
1717    /// let job = Job::new("test".to_string(), json!({}))
1718    ///     .with_trace_id("trace-123");
1719    ///
1720    /// assert_eq!(job.get_trace_id(), Some("trace-123"));
1721    /// ```
1722    pub fn get_trace_id(&self) -> Option<&str> {
1723        self.trace_id.as_deref()
1724    }
1725
1726    /// Gets the correlation ID if available.
1727    ///
1728    /// # Examples
1729    ///
1730    /// ```rust
1731    /// use hammerwork::Job;
1732    /// use serde_json::json;
1733    ///
1734    /// let job = Job::new("test".to_string(), json!({}))
1735    ///     .with_correlation_id("corr-456");
1736    ///
1737    /// assert_eq!(job.get_correlation_id(), Some("corr-456"));
1738    /// ```
1739    pub fn get_correlation_id(&self) -> Option<&str> {
1740        self.correlation_id.as_deref()
1741    }
1742
1743    /// Sets the encryption configuration for this job.
1744    ///
1745    /// When encryption is configured, the job payload will be encrypted before
1746    /// being stored in the database and decrypted when retrieved.
1747    ///
1748    /// # Arguments
1749    ///
1750    /// * `config` - The encryption configuration to use
1751    ///
1752    /// # Examples
1753    ///
1754    /// ```rust,no_run
1755    /// # #[cfg(feature = "encryption")]
1756    /// # {
1757    /// use hammerwork::{Job, encryption::{EncryptionConfig, EncryptionAlgorithm}};
1758    /// use serde_json::json;
1759    ///
1760    /// let job = Job::new("secure_task".to_string(), json!({"data": "sensitive"}))
1761    ///     .with_encryption(EncryptionConfig::new(EncryptionAlgorithm::AES256GCM));
1762    ///
1763    /// assert!(job.has_encryption());
1764    /// # }
1765    /// ```
1766    #[cfg(feature = "encryption")]
1767    pub fn with_encryption(mut self, config: crate::encryption::EncryptionConfig) -> Self {
1768        self.encryption_config = Some(config);
1769        self
1770    }
1771
1772    /// Sets the PII fields for this job.
1773    ///
1774    /// PII (Personally Identifiable Information) fields are tracked separately
1775    /// for compliance and auditing purposes. When encryption is enabled, these
1776    /// fields receive special handling.
1777    ///
1778    /// # Arguments
1779    ///
1780    /// * `pii_fields` - List of field names that contain PII
1781    ///
1782    /// # Examples
1783    ///
1784    /// ```rust
1785    /// use hammerwork::Job;
1786    /// use serde_json::json;
1787    ///
1788    /// let job = Job::new("user_processing".to_string(), json!({
1789    ///     "user_id": "123",
1790    ///     "email": "user@example.com",
1791    ///     "ssn": "123-45-6789"
1792    /// }))
1793    /// .with_pii_fields(vec!["email", "ssn"]);
1794    ///
1795    /// assert_eq!(job.pii_fields.len(), 2);
1796    /// assert!(job.has_pii_fields());
1797    /// ```
1798    pub fn with_pii_fields(mut self, pii_fields: Vec<impl Into<String>>) -> Self {
1799        self.pii_fields = pii_fields.into_iter().map(|f| f.into()).collect();
1800        self
1801    }
1802
1803    /// Sets the retention policy for this job's encrypted data.
1804    ///
1805    /// The retention policy determines how long encrypted job data should be
1806    /// kept before automatic cleanup.
1807    ///
1808    /// # Arguments
1809    ///
1810    /// * `policy` - The retention policy to apply
1811    ///
1812    /// # Examples
1813    ///
1814    /// ```rust,no_run
1815    /// # #[cfg(feature = "encryption")]
1816    /// # {
1817    /// use hammerwork::{Job, encryption::RetentionPolicy};
1818    /// use serde_json::json;
1819    /// use std::time::Duration;
1820    ///
1821    /// let job = Job::new("temp_processing".to_string(), json!({"data": "temp"}))
1822    ///     .with_retention_policy(RetentionPolicy::DeleteAfter(Duration::from_secs(3600)));
1823    /// # }
1824    /// ```
1825    #[cfg(feature = "encryption")]
1826    pub fn with_retention_policy(mut self, policy: crate::encryption::RetentionPolicy) -> Self {
1827        self.retention_policy = Some(policy);
1828        self
1829    }
1830
1831    /// Checks if this job has encryption configured.
1832    ///
1833    /// # Examples
1834    ///
1835    /// ```rust
1836    /// use hammerwork::Job;
1837    /// use serde_json::json;
1838    ///
1839    /// let job1 = Job::new("plain".to_string(), json!({}));
1840    /// assert!(!job1.has_encryption());
1841    ///
1842    /// #[cfg(feature = "encryption")]
1843    /// {
1844    ///     use hammerwork::encryption::{EncryptionConfig, EncryptionAlgorithm};
1845    ///     let job2 = Job::new("encrypted".to_string(), json!({}))
1846    ///         .with_encryption(EncryptionConfig::new(EncryptionAlgorithm::AES256GCM));
1847    ///     assert!(job2.has_encryption());
1848    /// }
1849    /// ```
1850    pub fn has_encryption(&self) -> bool {
1851        #[cfg(feature = "encryption")]
1852        {
1853            self.encryption_config.is_some()
1854        }
1855        #[cfg(not(feature = "encryption"))]
1856        {
1857            false
1858        }
1859    }
1860
1861    /// Checks if this job has PII fields configured.
1862    ///
1863    /// # Examples
1864    ///
1865    /// ```rust
1866    /// use hammerwork::Job;
1867    /// use serde_json::json;
1868    ///
1869    /// let job1 = Job::new("normal".to_string(), json!({}));
1870    /// assert!(!job1.has_pii_fields());
1871    ///
1872    /// let job2 = Job::new("with_pii".to_string(), json!({}))
1873    ///     .with_pii_fields(vec!["email"]);
1874    /// assert!(job2.has_pii_fields());
1875    /// ```
1876    pub fn has_pii_fields(&self) -> bool {
1877        !self.pii_fields.is_empty()
1878    }
1879
1880    /// Checks if this job's payload is currently encrypted.
1881    ///
1882    /// # Examples
1883    ///
1884    /// ```rust
1885    /// use hammerwork::Job;
1886    /// use serde_json::json;
1887    ///
1888    /// let job = Job::new("test".to_string(), json!({}));
1889    /// assert!(!job.is_payload_encrypted());
1890    /// ```
1891    pub fn is_payload_encrypted(&self) -> bool {
1892        self.is_encrypted
1893    }
1894
1895    /// Gets the list of PII fields for this job.
1896    ///
1897    /// # Examples
1898    ///
1899    /// ```rust
1900    /// use hammerwork::Job;
1901    /// use serde_json::json;
1902    ///
1903    /// let job = Job::new("test".to_string(), json!({}))
1904    ///     .with_pii_fields(vec!["email", "ssn"]);
1905    ///
1906    /// let pii_fields = job.get_pii_fields();
1907    /// assert_eq!(pii_fields.len(), 2);
1908    /// assert!(pii_fields.contains(&"email".to_string()));
1909    /// ```
1910    pub fn get_pii_fields(&self) -> &[String] {
1911        &self.pii_fields
1912    }
1913
1914    /// Gets the encryption configuration if available.
1915    ///
1916    /// # Examples
1917    ///
1918    /// ```rust,no_run
1919    /// # #[cfg(feature = "encryption")]
1920    /// # {
1921    /// use hammerwork::{Job, encryption::{EncryptionConfig, EncryptionAlgorithm}};
1922    /// use serde_json::json;
1923    ///
1924    /// let config = EncryptionConfig::new(EncryptionAlgorithm::AES256GCM);
1925    /// let job = Job::new("test".to_string(), json!({}))
1926    ///     .with_encryption(config.clone());
1927    ///
1928    /// let retrieved_config = job.get_encryption_config().unwrap();
1929    /// assert_eq!(retrieved_config.algorithm, EncryptionAlgorithm::AES256GCM);
1930    /// # }
1931    /// ```
1932    #[cfg(feature = "encryption")]
1933    pub fn get_encryption_config(&self) -> Option<&crate::encryption::EncryptionConfig> {
1934        self.encryption_config.as_ref()
1935    }
1936
1937    /// Gets the retention policy if available.
1938    ///
1939    /// # Examples
1940    ///
1941    /// ```rust
1942    /// # #[cfg(feature = "encryption")]
1943    /// # {
1944    /// use hammerwork::{Job, encryption::RetentionPolicy};
1945    /// use serde_json::json;
1946    /// use std::time::Duration;
1947    ///
1948    /// let policy = RetentionPolicy::DeleteAfter(Duration::from_secs(3600));
1949    /// let job = Job::new("test".to_string(), json!({}))
1950    ///     .with_retention_policy(policy.clone());
1951    ///
1952    /// assert_eq!(job.get_retention_policy(), Some(&policy));
1953    /// # }
1954    /// ```
1955    #[cfg(feature = "encryption")]
1956    pub fn get_retention_policy(&self) -> Option<&crate::encryption::RetentionPolicy> {
1957        self.retention_policy.as_ref()
1958    }
1959
1960    /// Checks if this job should have its encrypted data cleaned up now.
1961    ///
1962    /// This method checks the retention policy and encrypted payload metadata
1963    /// to determine if the data has exceeded its retention period.
1964    ///
1965    /// # Examples
1966    ///
1967    /// ```rust
1968    /// use hammerwork::Job;
1969    /// use serde_json::json;
1970    ///
1971    /// let job = Job::new("test".to_string(), json!({}));
1972    /// assert!(!job.should_cleanup_encrypted_data());
1973    /// ```
1974    pub fn should_cleanup_encrypted_data(&self) -> bool {
1975        #[cfg(feature = "encryption")]
1976        {
1977            if let Some(encrypted_payload) = &self.encrypted_payload {
1978                encrypted_payload.should_delete_now()
1979            } else if let Some(retention_policy) = &self.retention_policy {
1980                retention_policy.should_delete_now(
1981                    self.created_at,
1982                    self.completed_at,
1983                    self.encryption_config
1984                        .as_ref()
1985                        .and_then(|c| c.default_retention),
1986                )
1987            } else {
1988                false
1989            }
1990        }
1991        #[cfg(not(feature = "encryption"))]
1992        {
1993            false
1994        }
1995    }
1996}
1997
1998#[cfg(test)]
1999mod tests {
2000    use super::*;
2001    use serde_json::json;
2002
2003    #[test]
2004    fn test_job_new() {
2005        let queue_name = "test_queue".to_string();
2006        let payload = json!({"key": "value"});
2007
2008        let job = Job::new(queue_name.clone(), payload.clone());
2009
2010        assert_eq!(job.queue_name, queue_name);
2011        assert_eq!(job.payload, payload);
2012        assert_eq!(job.status, JobStatus::Pending);
2013        assert_eq!(job.attempts, 0);
2014        assert_eq!(job.max_attempts, 3);
2015        assert!(job.started_at.is_none());
2016        assert!(job.completed_at.is_none());
2017        assert!(job.failed_at.is_none());
2018        assert!(job.error_message.is_none());
2019        assert_eq!(job.created_at, job.scheduled_at);
2020    }
2021
2022    #[test]
2023    fn test_job_with_delay() {
2024        let queue_name = "test_queue".to_string();
2025        let payload = json!({"key": "value"});
2026        let delay = chrono::Duration::minutes(5);
2027
2028        let job = Job::with_delay(queue_name.clone(), payload.clone(), delay);
2029
2030        assert_eq!(job.queue_name, queue_name);
2031        assert_eq!(job.payload, payload);
2032        assert_eq!(job.status, JobStatus::Pending);
2033        assert_eq!(job.attempts, 0);
2034        assert_eq!(job.max_attempts, 3);
2035        assert!(job.scheduled_at > job.created_at);
2036        assert_eq!(job.scheduled_at - job.created_at, delay);
2037    }
2038
2039    #[test]
2040    fn test_job_with_max_attempts() {
2041        let queue_name = "test_queue".to_string();
2042        let payload = json!({"key": "value"});
2043
2044        let job = Job::new(queue_name, payload).with_max_attempts(5);
2045
2046        assert_eq!(job.max_attempts, 5);
2047    }
2048
2049    #[test]
2050    fn test_job_with_delay_and_max_attempts() {
2051        let queue_name = "test_queue".to_string();
2052        let payload = json!({"key": "value"});
2053        let delay = chrono::Duration::hours(1);
2054
2055        let job = Job::with_delay(queue_name, payload, delay).with_max_attempts(10);
2056
2057        assert_eq!(job.max_attempts, 10);
2058        assert!(job.scheduled_at > job.created_at);
2059    }
2060
2061    #[test]
2062    fn test_job_status_equality() {
2063        assert_eq!(JobStatus::Pending, JobStatus::Pending);
2064        assert_eq!(JobStatus::Running, JobStatus::Running);
2065        assert_eq!(JobStatus::Completed, JobStatus::Completed);
2066        assert_eq!(JobStatus::Failed, JobStatus::Failed);
2067        assert_eq!(JobStatus::Dead, JobStatus::Dead);
2068        assert_eq!(JobStatus::TimedOut, JobStatus::TimedOut);
2069        assert_eq!(JobStatus::Retrying, JobStatus::Retrying);
2070
2071        assert_ne!(JobStatus::Pending, JobStatus::Running);
2072        assert_ne!(JobStatus::Completed, JobStatus::Failed);
2073        assert_ne!(JobStatus::Failed, JobStatus::Dead);
2074        assert_ne!(JobStatus::Dead, JobStatus::TimedOut);
2075        assert_ne!(JobStatus::TimedOut, JobStatus::Failed);
2076    }
2077
2078    #[test]
2079    fn test_job_serialization() {
2080        let job = Job::new("test".to_string(), json!({"data": "test"}));
2081
2082        let serialized = serde_json::to_string(&job).unwrap();
2083        let deserialized: Job = serde_json::from_str(&serialized).unwrap();
2084
2085        assert_eq!(job.id, deserialized.id);
2086        assert_eq!(job.queue_name, deserialized.queue_name);
2087        assert_eq!(job.payload, deserialized.payload);
2088        assert_eq!(job.status, deserialized.status);
2089        assert_eq!(job.attempts, deserialized.attempts);
2090        assert_eq!(job.max_attempts, deserialized.max_attempts);
2091    }
2092
2093    #[test]
2094    fn test_job_status_serialization() {
2095        let statuses = vec![
2096            JobStatus::Pending,
2097            JobStatus::Running,
2098            JobStatus::Completed,
2099            JobStatus::Failed,
2100            JobStatus::Dead,
2101            JobStatus::TimedOut,
2102            JobStatus::Retrying,
2103        ];
2104
2105        for status in statuses {
2106            let serialized = serde_json::to_string(&status).unwrap();
2107            let deserialized: JobStatus = serde_json::from_str(&serialized).unwrap();
2108            assert_eq!(status, deserialized);
2109        }
2110    }
2111
2112    #[test]
2113    fn test_job_dead_status_methods() {
2114        let mut job = Job::new("test".to_string(), json!({"data": "test"}));
2115
2116        // Initially not dead
2117        assert!(!job.is_dead());
2118        assert!(!job.has_exhausted_retries());
2119
2120        // Simulate exhausting retries
2121        job.attempts = 3;
2122        job.max_attempts = 3;
2123        assert!(job.has_exhausted_retries());
2124        assert!(!job.is_dead()); // Still not dead until status is set
2125
2126        // Mark as dead
2127        job.status = JobStatus::Dead;
2128        job.failed_at = Some(Utc::now());
2129        assert!(job.is_dead());
2130        assert!(job.has_exhausted_retries());
2131    }
2132
2133    #[test]
2134    fn test_job_processing_duration() {
2135        let mut job = Job::new("test".to_string(), json!({"data": "test"}));
2136
2137        // No processing duration when not started
2138        assert!(job.processing_duration().is_none());
2139
2140        // Set start time
2141        let start_time = Utc::now();
2142        job.started_at = Some(start_time);
2143
2144        // Should have some duration now (very small)
2145        let duration = job.processing_duration().unwrap();
2146        assert!(duration.num_milliseconds() >= 0);
2147
2148        // Set completion time
2149        let completion_time = start_time + chrono::Duration::seconds(5);
2150        job.completed_at = Some(completion_time);
2151
2152        let final_duration = job.processing_duration().unwrap();
2153        assert_eq!(final_duration.num_seconds(), 5);
2154    }
2155
2156    #[test]
2157    fn test_job_age() {
2158        let job = Job::new("test".to_string(), json!({"data": "test"}));
2159        let age = job.age();
2160
2161        // Age should be very small (just created)
2162        assert!(age.num_milliseconds() >= 0);
2163        assert!(age.num_seconds() < 1);
2164    }
2165
2166    #[test]
2167    fn test_job_with_timeout() {
2168        let timeout = std::time::Duration::from_secs(30);
2169        let job = Job::new("test".to_string(), json!({"data": "test"})).with_timeout(timeout);
2170
2171        assert_eq!(job.timeout, Some(timeout));
2172        assert!(!job.is_timed_out()); // Not timed out until status is set
2173    }
2174
2175    #[test]
2176    fn test_job_timeout_status_methods() {
2177        let mut job = Job::new("test".to_string(), json!({"data": "test"}));
2178
2179        // Initially not timed out
2180        assert!(!job.is_timed_out());
2181
2182        // Set timed out status
2183        job.status = JobStatus::TimedOut;
2184        job.timed_out_at = Some(Utc::now());
2185        assert!(job.is_timed_out());
2186    }
2187
2188    #[test]
2189    fn test_job_should_timeout() {
2190        let mut job = Job::new("test".to_string(), json!({"data": "test"}))
2191            .with_timeout(std::time::Duration::from_millis(100));
2192
2193        // Should not timeout before it starts
2194        assert!(!job.should_timeout());
2195
2196        // Set start time to simulate job starting
2197        job.started_at = Some(Utc::now() - chrono::Duration::milliseconds(200));
2198
2199        // Should timeout since 200ms > 100ms timeout
2200        assert!(job.should_timeout());
2201
2202        // Job without timeout should never timeout
2203        let mut job_no_timeout = Job::new("test".to_string(), json!({"data": "test"}));
2204        job_no_timeout.started_at = Some(Utc::now() - chrono::Duration::hours(1));
2205        assert!(!job_no_timeout.should_timeout());
2206    }
2207
2208    #[test]
2209    fn test_job_with_delay_and_timeout() {
2210        let delay = chrono::Duration::minutes(5);
2211        let timeout = std::time::Duration::from_secs(120);
2212
2213        let job = Job::with_delay("test".to_string(), json!({"data": "test"}), delay)
2214            .with_timeout(timeout)
2215            .with_max_attempts(5);
2216
2217        assert_eq!(job.timeout, Some(timeout));
2218        assert_eq!(job.max_attempts, 5);
2219        assert!(job.scheduled_at > job.created_at);
2220        assert_eq!(job.scheduled_at - job.created_at, delay);
2221    }
2222
2223    #[test]
2224    fn test_processing_duration_with_timeout() {
2225        let mut job = Job::new("test".to_string(), json!({"data": "test"}));
2226
2227        // Set start time and timed out time
2228        let start_time = Utc::now() - chrono::Duration::seconds(5);
2229        let timeout_time = start_time + chrono::Duration::seconds(3);
2230
2231        job.started_at = Some(start_time);
2232        job.timed_out_at = Some(timeout_time);
2233
2234        let duration = job.processing_duration().unwrap();
2235        assert_eq!(duration.num_seconds(), 3);
2236    }
2237
2238    #[test]
2239    fn test_timeout_builder_methods() {
2240        let job = Job::new("test".to_string(), json!({"key": "value"}))
2241            .with_timeout(std::time::Duration::from_secs(120))
2242            .with_max_attempts(5);
2243
2244        assert_eq!(job.timeout, Some(std::time::Duration::from_secs(120)));
2245        assert_eq!(job.max_attempts, 5);
2246        assert_eq!(job.queue_name, "test");
2247    }
2248
2249    #[test]
2250    fn test_job_timeout_edge_cases() {
2251        let mut job = Job::new("test".to_string(), json!({"data": "test"}));
2252
2253        // Job without timeout should never timeout
2254        assert!(!job.should_timeout());
2255
2256        // Job with timeout but not started should not timeout
2257        job.timeout = Some(std::time::Duration::from_millis(100));
2258        assert!(!job.should_timeout());
2259
2260        // Job with timeout and started but within timeout window should not timeout
2261        job.started_at = Some(Utc::now() - chrono::Duration::milliseconds(50));
2262        assert!(!job.should_timeout());
2263
2264        // Job with timeout and started beyond timeout window should timeout
2265        job.started_at = Some(Utc::now() - chrono::Duration::milliseconds(150));
2266        assert!(job.should_timeout());
2267    }
2268
2269    #[test]
2270    fn test_job_status_transitions_with_timeout() {
2271        let mut job = Job::new("test".to_string(), json!({"data": "test"}));
2272
2273        // Initial state
2274        assert_eq!(job.status, JobStatus::Pending);
2275        assert!(!job.is_timed_out());
2276
2277        // Simulate timeout
2278        job.status = JobStatus::TimedOut;
2279        job.timed_out_at = Some(Utc::now());
2280
2281        assert!(job.is_timed_out());
2282        assert!(!job.is_dead()); // TimedOut is different from Dead
2283    }
2284
2285    #[test]
2286    fn test_timeout_serialization_compatibility() {
2287        let original_job = Job::new("test_queue".to_string(), json!({"data": "test"}))
2288            .with_timeout(std::time::Duration::from_secs(300))
2289            .with_max_attempts(5);
2290
2291        // Serialize and deserialize
2292        let serialized = serde_json::to_string(&original_job).unwrap();
2293        let deserialized: Job = serde_json::from_str(&serialized).unwrap();
2294
2295        // Verify timeout field is preserved
2296        assert_eq!(original_job.timeout, deserialized.timeout);
2297        assert_eq!(original_job.timed_out_at, deserialized.timed_out_at);
2298        assert_eq!(original_job.status, deserialized.status);
2299    }
2300
2301    #[test]
2302    fn test_job_with_all_timeout_fields() {
2303        let timeout_duration = std::time::Duration::from_secs(60);
2304        let mut job = Job::new("comprehensive_test".to_string(), json!({"test": true}))
2305            .with_timeout(timeout_duration)
2306            .with_max_attempts(3);
2307
2308        // Simulate job lifecycle with timeout
2309        job.started_at = Some(Utc::now() - chrono::Duration::seconds(30));
2310        job.status = JobStatus::Running;
2311
2312        // Should not timeout yet (30s < 60s)
2313        assert!(!job.should_timeout());
2314
2315        // Simulate timeout occurring
2316        job.status = JobStatus::TimedOut;
2317        job.timed_out_at = Some(Utc::now());
2318        job.error_message = Some("Job timed out after 60s".to_string());
2319
2320        assert!(job.is_timed_out());
2321        assert_eq!(job.timeout, Some(timeout_duration));
2322        assert!(job.timed_out_at.is_some());
2323        assert!(job.error_message.is_some());
2324    }
2325
2326    #[test]
2327    fn test_job_status_backward_compatibility_string_matching() {
2328        // Test that our string matching logic handles both quoted and unquoted formats
2329        // This simulates what happens in the Decode implementation
2330        let test_cases = [
2331            // (input_string, expected_status)
2332            ("Pending", JobStatus::Pending),
2333            ("\"Pending\"", JobStatus::Pending),
2334            ("Running", JobStatus::Running),
2335            ("\"Running\"", JobStatus::Running),
2336            ("Completed", JobStatus::Completed),
2337            ("\"Completed\"", JobStatus::Completed),
2338            ("Failed", JobStatus::Failed),
2339            ("\"Failed\"", JobStatus::Failed),
2340            ("Dead", JobStatus::Dead),
2341            ("\"Dead\"", JobStatus::Dead),
2342            ("TimedOut", JobStatus::TimedOut),
2343            ("\"TimedOut\"", JobStatus::TimedOut),
2344            ("Retrying", JobStatus::Retrying),
2345            ("\"Retrying\"", JobStatus::Retrying),
2346            ("Archived", JobStatus::Archived),
2347            ("\"Archived\"", JobStatus::Archived),
2348        ];
2349
2350        for (input, expected) in &test_cases {
2351            // This is the same logic used in our Decode implementations
2352            let cleaned_str = input.trim_matches('"');
2353            let parsed_status = match cleaned_str {
2354                "Pending" => JobStatus::Pending,
2355                "Running" => JobStatus::Running,
2356                "Completed" => JobStatus::Completed,
2357                "Failed" => JobStatus::Failed,
2358                "Dead" => JobStatus::Dead,
2359                "TimedOut" => JobStatus::TimedOut,
2360                "Retrying" => JobStatus::Retrying,
2361                "Archived" => JobStatus::Archived,
2362                _ => panic!("Unknown job status: {}", input),
2363            };
2364
2365            assert_eq!(
2366                *expected, parsed_status,
2367                "Failed to parse '{}' correctly",
2368                input
2369            );
2370        }
2371    }
2372
2373    #[test]
2374    fn test_job_status_encoding_logic() {
2375        // Verify that our encoding logic produces the expected unquoted strings
2376        let statuses = [
2377            (JobStatus::Pending, "Pending"),
2378            (JobStatus::Running, "Running"),
2379            (JobStatus::Completed, "Completed"),
2380            (JobStatus::Failed, "Failed"),
2381            (JobStatus::Dead, "Dead"),
2382            (JobStatus::TimedOut, "TimedOut"),
2383            (JobStatus::Retrying, "Retrying"),
2384            (JobStatus::Archived, "Archived"),
2385        ];
2386
2387        for (status, expected_str) in &statuses {
2388            // This matches the logic in our Encode implementations
2389            let encoded_str = match status {
2390                JobStatus::Pending => "Pending",
2391                JobStatus::Running => "Running",
2392                JobStatus::Completed => "Completed",
2393                JobStatus::Failed => "Failed",
2394                JobStatus::Dead => "Dead",
2395                JobStatus::TimedOut => "TimedOut",
2396                JobStatus::Retrying => "Retrying",
2397                JobStatus::Archived => "Archived",
2398            };
2399
2400            assert_eq!(
2401                *expected_str, encoded_str,
2402                "Encoding mismatch for {:?}",
2403                status
2404            );
2405
2406            // Verify the encoded string does not have quotes
2407            assert!(
2408                !encoded_str.starts_with('"'),
2409                "Encoded string should not start with quotes: {}",
2410                encoded_str
2411            );
2412            assert!(
2413                !encoded_str.ends_with('"'),
2414                "Encoded string should not end with quotes: {}",
2415                encoded_str
2416            );
2417        }
2418    }
2419
2420    #[test]
2421    fn test_status_as_str_for_every_status() {
2422        for (status, name) in [
2423            (JobStatus::Pending, "Pending"),
2424            (JobStatus::Running, "Running"),
2425            (JobStatus::Completed, "Completed"),
2426            (JobStatus::Failed, "Failed"),
2427            (JobStatus::Dead, "Dead"),
2428            (JobStatus::TimedOut, "TimedOut"),
2429            (JobStatus::Retrying, "Retrying"),
2430            (JobStatus::Archived, "Archived"),
2431        ] {
2432            assert_eq!(status.as_str(), name);
2433        }
2434    }
2435
2436    #[test]
2437    fn test_result_config_builders() {
2438        let config = ResultConfig::new(ResultStorage::Database)
2439            .with_ttl(std::time::Duration::from_secs(60))
2440            .with_max_size(1024);
2441        assert_eq!(config.ttl, Some(std::time::Duration::from_secs(60)));
2442        assert_eq!(config.max_size_bytes, Some(1024));
2443        assert_eq!(ResultConfig::default().storage, ResultStorage::None);
2444
2445        let job = Job::new("q".to_string(), json!({}));
2446        assert!(!job.has_result_storage());
2447        assert!(!job.has_result_data());
2448        let mut job = job.with_result_config(config);
2449        assert!(job.has_result_storage());
2450        job.result_data = Some(json!({"ok": true}));
2451        assert!(job.has_result_data());
2452    }
2453
2454    #[test]
2455    fn test_backoff_builders_set_the_retry_strategy() {
2456        use std::time::Duration;
2457        let job = Job::new("q".to_string(), json!({})).with_exponential_backoff(
2458            Duration::from_secs(1),
2459            2.0,
2460            Duration::from_secs(60),
2461        );
2462        assert_eq!(
2463            job.retry_strategy,
2464            Some(RetryStrategy::exponential(
2465                Duration::from_secs(1),
2466                2.0,
2467                Some(Duration::from_secs(60))
2468            ))
2469        );
2470        let job = Job::new("q".to_string(), json!({})).with_linear_backoff(
2471            Duration::from_secs(1),
2472            Duration::from_secs(2),
2473            None,
2474        );
2475        assert_eq!(
2476            job.retry_strategy.as_ref().unwrap().calculate_delay(3),
2477            Duration::from_secs(7),
2478            "base + 3 * increment"
2479        );
2480        let job = Job::new("q".to_string(), json!({}))
2481            .with_fibonacci_backoff(Duration::from_secs(1), Some(Duration::from_secs(4)));
2482        assert_eq!(
2483            job.retry_strategy.as_ref().unwrap().calculate_delay(10),
2484            Duration::from_secs(4),
2485            "capped at max_delay"
2486        );
2487    }
2488
2489    #[test]
2490    fn test_dependency_and_workflow_predicates() {
2491        let a = Job::new("q".to_string(), json!({}));
2492        let b = Job::new("q".to_string(), json!({}));
2493        assert!(!a.has_dependencies());
2494        assert!(a.dependencies_satisfied());
2495        assert!(!a.dependencies_failed());
2496        assert_eq!(a.dependency_count(), 0);
2497        assert_eq!(a.dependent_count(), 0);
2498
2499        let mut c = Job::new("q".to_string(), json!({})).depends_on_jobs(&[a.id, b.id]);
2500        assert!(c.has_dependencies());
2501        assert_eq!(c.dependency_count(), 2);
2502        assert!(!c.dependencies_satisfied());
2503        c.dependency_status = crate::workflow::DependencyStatus::Satisfied;
2504        assert!(c.dependencies_satisfied());
2505        c.dependency_status = crate::workflow::DependencyStatus::Failed;
2506        assert!(c.dependencies_failed());
2507        assert!(!c.dependencies_satisfied());
2508
2509        // An empty list does not make a job wait.
2510        let d = Job::new("q".to_string(), json!({})).depends_on_jobs(&[]);
2511        assert!(d.dependencies_satisfied());
2512
2513        let mut e = Job::new("q".to_string(), json!({}));
2514        e.dependents.push(a.id);
2515        assert_eq!(e.dependent_count(), 1);
2516        assert!(!e.is_part_of_workflow());
2517        let e = e.with_workflow(uuid::Uuid::new_v4(), "flow");
2518        assert!(e.is_part_of_workflow());
2519        assert_eq!(e.workflow_name.as_deref(), Some("flow"));
2520    }
2521
2522    #[test]
2523    fn test_tracing_helpers() {
2524        let job = Job::new("q".to_string(), json!({}));
2525        assert!(!job.has_tracing_info());
2526        assert_eq!(job.get_trace_id(), None);
2527        assert_eq!(job.get_correlation_id(), None);
2528
2529        let job = job.with_tracing_id("op-1");
2530        assert!(job.has_tracing_info());
2531        assert_eq!(job.get_trace_id(), Some("op-1"));
2532        assert_eq!(job.get_correlation_id(), Some("op-1"));
2533
2534        for job in [
2535            Job::new("q".to_string(), json!({})).with_span_context("ctx"),
2536            Job::new("q".to_string(), json!({})).with_parent_span_id("p"),
2537            Job::new("q".to_string(), json!({})).with_correlation_id("c"),
2538        ] {
2539            assert!(job.has_tracing_info());
2540        }
2541        assert_eq!(
2542            Job::new("q".to_string(), json!({}))
2543                .with_span_context("ctx")
2544                .span_context
2545                .as_deref(),
2546            Some("ctx")
2547        );
2548    }
2549
2550    #[test]
2551    fn test_next_run_needs_a_valid_schedule_on_a_recurring_job() {
2552        let mut job = Job::new("q".to_string(), json!({}));
2553        assert_eq!(job.calculate_next_run(), None, "not recurring");
2554        assert_eq!(job.prepare_for_next_run(), None);
2555
2556        job.recurring = true;
2557        assert_eq!(job.calculate_next_run(), None, "no schedule");
2558        job.cron_schedule = Some("not a cron expression".to_string());
2559        assert!(matches!(job.get_cron_schedule(), Some(Err(_))));
2560        assert_eq!(job.calculate_next_run(), None, "invalid schedule");
2561
2562        job.cron_schedule = Some("0 0 * * * *".to_string());
2563        job.timezone = Some("America/New_York".to_string());
2564        let next = job.calculate_next_run().expect("next run");
2565        assert!(next > chrono::Utc::now());
2566    }
2567
2568    #[cfg(feature = "encryption")]
2569    #[test]
2570    fn test_encryption_accessors_and_cleanup() {
2571        use crate::encryption::{EncryptionAlgorithm, EncryptionConfig, RetentionPolicy};
2572
2573        let job = Job::new("q".to_string(), json!({}));
2574        assert!(!job.has_pii_fields());
2575        assert!(!job.is_payload_encrypted());
2576        assert!(job.get_pii_fields().is_empty());
2577        assert!(job.get_encryption_config().is_none());
2578        assert!(job.get_retention_policy().is_none());
2579        assert!(!job.should_cleanup_encrypted_data());
2580
2581        let config = EncryptionConfig::new(EncryptionAlgorithm::ChaCha20Poly1305);
2582        let job = Job::new("q".to_string(), json!({}))
2583            .with_encryption(config.clone())
2584            .with_pii_fields(vec!["ssn"])
2585            .with_retention_policy(RetentionPolicy::DeleteAt(
2586                chrono::Utc::now() - chrono::Duration::minutes(1),
2587            ));
2588        assert!(job.has_pii_fields());
2589        assert_eq!(job.get_pii_fields(), ["ssn".to_string()]);
2590        assert_eq!(
2591            job.get_encryption_config().unwrap().algorithm,
2592            EncryptionAlgorithm::ChaCha20Poly1305
2593        );
2594        assert!(job.should_cleanup_encrypted_data(), "retention ended");
2595
2596        let kept = Job::new("q".to_string(), json!({}))
2597            .with_retention_policy(RetentionPolicy::KeepIndefinitely);
2598        assert!(!kept.should_cleanup_encrypted_data());
2599        assert_eq!(
2600            kept.get_retention_policy(),
2601            Some(&RetentionPolicy::KeepIndefinitely)
2602        );
2603    }
2604}