Skip to main content

backbone_bucket/application/service/
conversion_service.rs

1//! Conversion Service
2//!
3//! Hand-written — NOT generated. This file is safe from regeneration.
4//!
5//! Manages file format conversion jobs (image → webp, document → pdf, etc.).
6//! Creates ConversionJob records and tracks progress.
7
8use std::sync::Arc;
9
10use chrono::Utc;
11use uuid::Uuid;
12
13use super::error::{ServiceError, ServiceResult};
14use crate::domain::entity::{ConversionJob, ConversionStatus};
15use crate::infrastructure::persistence::ConversionJobRepository;
16use crate::infrastructure::persistence::StoredFileRepository;
17
18/// Service for managing file format conversions.
19pub struct ConversionService {
20    conversion_repo: Arc<ConversionJobRepository>,
21    file_repo: Arc<StoredFileRepository>,
22}
23
24impl ConversionService {
25    pub fn new(
26        conversion_repo: Arc<ConversionJobRepository>,
27        file_repo: Arc<StoredFileRepository>,
28    ) -> Self {
29        Self { conversion_repo, file_repo }
30    }
31
32    /// Request a format conversion for a file.
33    pub async fn request_conversion(
34        &self,
35        source_file_id: Uuid,
36        target_format: &str,
37        options: Option<serde_json::Value>,
38    ) -> ServiceResult<ConversionJob> {
39        let _file = self.file_repo
40            .find_by_id(&source_file_id.to_string())
41            .await
42            .map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?
43            .ok_or(ServiceError::NotFound)?;
44
45        let mut builder = ConversionJob::builder()
46            .source_file_id(source_file_id)
47            .target_format(target_format.to_string())
48            .status(ConversionStatus::Pending)
49            .progress(0);
50
51        if let Some(opts) = options {
52            builder = builder.conversion_options(opts);
53        }
54
55        let job = builder.build()
56            .map_err(|e| ServiceError::Validation(e))?;
57
58        let created = self.conversion_repo
59            .create(&job)
60            .await
61            .map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?;
62
63        Ok(created)
64    }
65
66    /// Mark a conversion job as started.
67    pub async fn mark_started(&self, job_id: Uuid) -> ServiceResult<ConversionJob> {
68        let mut job = self.find_job(job_id).await?;
69
70        if job.status != ConversionStatus::Pending {
71            return Err(ServiceError::Validation(format!("Job {} is not in pending state", job_id)));
72        }
73
74        job.status = ConversionStatus::Processing;
75        job.started_at = Some(Utc::now());
76        job.metadata.touch();
77
78        let id_str = job.id.to_string();
79        self.conversion_repo
80            .update(&id_str, &job)
81            .await
82            .map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?
83            .ok_or(ServiceError::NotFound)
84    }
85
86    /// Update conversion progress (0-100).
87    pub async fn update_progress(&self, job_id: Uuid, progress: i32) -> ServiceResult<ConversionJob> {
88        let mut job = self.find_job(job_id).await?;
89
90        if progress < 0 || progress > 100 {
91            return Err(ServiceError::Validation("Progress must be between 0 and 100".into()));
92        }
93
94        job.progress = progress;
95        job.metadata.touch();
96
97        let id_str = job.id.to_string();
98        self.conversion_repo
99            .update(&id_str, &job)
100            .await
101            .map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?
102            .ok_or(ServiceError::NotFound)
103    }
104
105    /// Mark a conversion job as completed with the result file.
106    pub async fn mark_completed(
107        &self,
108        job_id: Uuid,
109        result_file_id: Uuid,
110    ) -> ServiceResult<ConversionJob> {
111        let mut job = self.find_job(job_id).await?;
112
113        job.status = ConversionStatus::Completed;
114        job.result_file_id = Some(result_file_id);
115        job.progress = 100;
116        job.completed_at = Some(Utc::now());
117        job.metadata.touch();
118
119        let id_str = job.id.to_string();
120        self.conversion_repo
121            .update(&id_str, &job)
122            .await
123            .map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?
124            .ok_or(ServiceError::NotFound)
125    }
126
127    /// Mark a conversion job as failed with an error message.
128    pub async fn mark_failed(
129        &self,
130        job_id: Uuid,
131        error_message: &str,
132    ) -> ServiceResult<ConversionJob> {
133        let mut job = self.find_job(job_id).await?;
134
135        job.status = ConversionStatus::Failed;
136        job.error_message = Some(error_message.to_string());
137        job.completed_at = Some(Utc::now());
138        job.metadata.touch();
139
140        let id_str = job.id.to_string();
141        self.conversion_repo
142            .update(&id_str, &job)
143            .await
144            .map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?
145            .ok_or(ServiceError::NotFound)
146    }
147
148    // ---- helpers ----
149
150    async fn find_job(&self, job_id: Uuid) -> ServiceResult<ConversionJob> {
151        self.conversion_repo
152            .find_by_id(&job_id.to_string())
153            .await
154            .map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?
155            .ok_or(ServiceError::NotFound)
156    }
157}