diff --git a/crates/domain/src/models/wrapup.rs b/crates/domain/src/models/wrapup.rs index ad304d2..b7d8375 100644 --- a/crates/domain/src/models/wrapup.rs +++ b/crates/domain/src/models/wrapup.rs @@ -1,6 +1,8 @@ -use chrono::NaiveDate; +use chrono::{NaiveDate, NaiveDateTime}; use uuid::Uuid; +use crate::value_objects::WrapUpId; + #[derive(Clone, Debug)] pub struct DateRange { pub start: NaiveDate, @@ -117,3 +119,24 @@ pub struct WrapUpReport { pub poster_paths: Vec, pub top_cast_profile_paths: Vec, } + +#[derive(Clone, Debug, PartialEq)] +pub enum WrapUpStatus { + Pending, + Generating, + Ready, + Failed, +} + +#[derive(Clone, Debug)] +pub struct WrapUpRecord { + pub id: WrapUpId, + pub user_id: Option, + pub start_date: NaiveDate, + pub end_date: NaiveDate, + pub status: WrapUpStatus, + pub report_json: Option, + pub error_message: Option, + pub created_at: NaiveDateTime, + pub completed_at: Option, +} diff --git a/crates/domain/src/ports.rs b/crates/domain/src/ports.rs index cfd39a8..8be7834 100644 --- a/crates/domain/src/ports.rs +++ b/crates/domain/src/ports.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use chrono::{DateTime, NaiveDateTime, Utc}; +use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc}; use uuid::Uuid; use crate::{ @@ -13,12 +13,12 @@ use crate::{ ReviewHistory, SearchQuery, SearchResults, User, UserStats, UserSummary, UserTrends, WatchEvent, WatchEventStatus, WatchlistEntry, WatchlistWithMovie, WebhookToken, collections::{self, PageParams, Paginated}, - wrapup::{DateRange, WrapUpScope}, + wrapup::{DateRange, WrapUpRecord, WrapUpScope, WrapUpStatus}, }, value_objects::{ Email, ExternalMetadataId, ImportProfileId, ImportSessionId, MovieId, MovieTitle, PasswordHash, PosterUrl, ReleaseYear, ReviewId, UserId, Username, WatchEventId, - WebhookTokenId, + WebhookTokenId, WrapUpId, }, }; @@ -471,6 +471,27 @@ pub trait WebhookTokenRepository: Send + Sync { async fn touch_last_used(&self, id: &WebhookTokenId) -> Result<(), DomainError>; } +#[async_trait] +pub trait WrapUpRepository: Send + Sync { + async fn create(&self, record: &WrapUpRecord) -> Result<(), DomainError>; + async fn update_status( + &self, + id: &WrapUpId, + status: &WrapUpStatus, + error: Option<&str>, + ) -> Result<(), DomainError>; + async fn set_complete(&self, id: &WrapUpId, report_json: &str) -> Result<(), DomainError>; + async fn get_by_id(&self, id: &WrapUpId) -> Result, DomainError>; + async fn list_for_user(&self, user_id: Uuid) -> Result, DomainError>; + async fn list_global(&self) -> Result, DomainError>; + async fn find_existing( + &self, + user_id: Option, + start: NaiveDate, + end: NaiveDate, + ) -> Result, DomainError>; +} + // ── Wrap-up / Year-in-Review ───────────────────────────────────────────────── #[derive(Clone, Debug)] diff --git a/crates/domain/src/testing.rs b/crates/domain/src/testing.rs index 7208e3d..1e1e4b9 100644 --- a/crates/domain/src/testing.rs +++ b/crates/domain/src/testing.rs @@ -24,11 +24,11 @@ use crate::{ ImportSessionRepository, MetadataClient, MetadataSearchCriteria, MovieProfileRepository, MovieRepository, PasswordHasher, PersonCommand, PersonQuery, PosterFetcherClient, ReviewRepository, SearchCommand, SearchPort, StatsRepository, UserProfileFieldsRepository, - UserRepository, WatchlistRepository, + UserRepository, WatchlistRepository, WrapUpRepository, }, value_objects::{ Email, ExternalMetadataId, ImportProfileId, ImportSessionId, MovieId, MovieTitle, - PasswordHash, PosterUrl, ReleaseYear, ReviewId, UserId, Username, + PasswordHash, PosterUrl, ReleaseYear, ReviewId, UserId, Username, WrapUpId, }, }; @@ -1050,3 +1050,143 @@ impl crate::ports::WrapUpStatsQuery for InMemoryWrapUpStatsQuery { Ok(filtered) } } + +// ── InMemoryWrapUpRepository ──────────────────────────────────────────────── + +pub struct InMemoryWrapUpRepository { + pub store: Mutex>, +} + +impl InMemoryWrapUpRepository { + pub fn new() -> Arc { + Arc::new(Self { + store: Mutex::new(Vec::new()), + }) + } +} + +#[async_trait] +impl WrapUpRepository for InMemoryWrapUpRepository { + async fn create( + &self, + record: &crate::models::wrapup::WrapUpRecord, + ) -> Result<(), DomainError> { + self.store.lock().unwrap().push(record.clone()); + Ok(()) + } + + async fn update_status( + &self, + id: &WrapUpId, + status: &crate::models::wrapup::WrapUpStatus, + error: Option<&str>, + ) -> Result<(), DomainError> { + let mut store = self.store.lock().unwrap(); + if let Some(rec) = store.iter_mut().find(|r| r.id == *id) { + rec.status = status.clone(); + rec.error_message = error.map(|s| s.to_string()); + Ok(()) + } else { + Err(DomainError::NotFound("wrapup record".into())) + } + } + + async fn set_complete(&self, id: &WrapUpId, report_json: &str) -> Result<(), DomainError> { + let mut store = self.store.lock().unwrap(); + if let Some(rec) = store.iter_mut().find(|r| r.id == *id) { + rec.status = crate::models::wrapup::WrapUpStatus::Ready; + rec.report_json = Some(report_json.to_string()); + rec.completed_at = Some(chrono::Utc::now().naive_utc()); + Ok(()) + } else { + Err(DomainError::NotFound("wrapup record".into())) + } + } + + async fn get_by_id( + &self, + id: &WrapUpId, + ) -> Result, DomainError> { + let store = self.store.lock().unwrap(); + Ok(store.iter().find(|r| r.id == *id).cloned()) + } + + async fn list_for_user( + &self, + user_id: Uuid, + ) -> Result, DomainError> { + let store = self.store.lock().unwrap(); + Ok(store + .iter() + .filter(|r| r.user_id == Some(user_id)) + .cloned() + .collect()) + } + + async fn list_global( + &self, + ) -> Result, DomainError> { + let store = self.store.lock().unwrap(); + Ok(store.iter().filter(|r| r.user_id.is_none()).cloned().collect()) + } + + async fn find_existing( + &self, + user_id: Option, + start: chrono::NaiveDate, + end: chrono::NaiveDate, + ) -> Result, DomainError> { + let store = self.store.lock().unwrap(); + Ok(store + .iter() + .find(|r| r.user_id == user_id && r.start_date == start && r.end_date == end) + .cloned()) + } +} + +// ── PanicWrapUpRepository ────────────────────────────────────────────────── + +pub struct PanicWrapUpRepository; + +#[async_trait] +impl WrapUpRepository for PanicWrapUpRepository { + async fn create(&self, _: &crate::models::wrapup::WrapUpRecord) -> Result<(), DomainError> { + panic!("PanicWrapUpRepository called") + } + async fn update_status( + &self, + _: &WrapUpId, + _: &crate::models::wrapup::WrapUpStatus, + _: Option<&str>, + ) -> Result<(), DomainError> { + panic!("PanicWrapUpRepository called") + } + async fn set_complete(&self, _: &WrapUpId, _: &str) -> Result<(), DomainError> { + panic!("PanicWrapUpRepository called") + } + async fn get_by_id( + &self, + _: &WrapUpId, + ) -> Result, DomainError> { + panic!("PanicWrapUpRepository called") + } + async fn list_for_user( + &self, + _: Uuid, + ) -> Result, DomainError> { + panic!("PanicWrapUpRepository called") + } + async fn list_global( + &self, + ) -> Result, DomainError> { + panic!("PanicWrapUpRepository called") + } + async fn find_existing( + &self, + _: Option, + _: chrono::NaiveDate, + _: chrono::NaiveDate, + ) -> Result, DomainError> { + panic!("PanicWrapUpRepository called") + } +} diff --git a/crates/domain/src/value_objects.rs b/crates/domain/src/value_objects.rs index f614066..a811a2e 100644 --- a/crates/domain/src/value_objects.rs +++ b/crates/domain/src/value_objects.rs @@ -28,6 +28,7 @@ uuid_id!(ImportProfileId); uuid_id!(WatchlistEntryId); uuid_id!(WatchEventId); uuid_id!(WebhookTokenId); +uuid_id!(WrapUpId); #[derive(Clone, Debug, PartialEq, Eq)] pub struct ExternalMetadataId(String);