feat(domain): add WrapUpRecord, WrapUpRepository port

This commit is contained in:
2026-06-02 21:59:47 +02:00
parent 4c75113c4f
commit a95d831fd1
4 changed files with 191 additions and 6 deletions

View File

@@ -1,6 +1,8 @@
use chrono::NaiveDate; use chrono::{NaiveDate, NaiveDateTime};
use uuid::Uuid; use uuid::Uuid;
use crate::value_objects::WrapUpId;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct DateRange { pub struct DateRange {
pub start: NaiveDate, pub start: NaiveDate,
@@ -117,3 +119,24 @@ pub struct WrapUpReport {
pub poster_paths: Vec<String>, pub poster_paths: Vec<String>,
pub top_cast_profile_paths: Vec<String>, pub top_cast_profile_paths: Vec<String>,
} }
#[derive(Clone, Debug, PartialEq)]
pub enum WrapUpStatus {
Pending,
Generating,
Ready,
Failed,
}
#[derive(Clone, Debug)]
pub struct WrapUpRecord {
pub id: WrapUpId,
pub user_id: Option<Uuid>,
pub start_date: NaiveDate,
pub end_date: NaiveDate,
pub status: WrapUpStatus,
pub report_json: Option<String>,
pub error_message: Option<String>,
pub created_at: NaiveDateTime,
pub completed_at: Option<NaiveDateTime>,
}

View File

@@ -1,5 +1,5 @@
use async_trait::async_trait; use async_trait::async_trait;
use chrono::{DateTime, NaiveDateTime, Utc}; use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc};
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
@@ -13,12 +13,12 @@ use crate::{
ReviewHistory, SearchQuery, SearchResults, User, UserStats, UserSummary, UserTrends, ReviewHistory, SearchQuery, SearchResults, User, UserStats, UserSummary, UserTrends,
WatchEvent, WatchEventStatus, WatchlistEntry, WatchlistWithMovie, WebhookToken, WatchEvent, WatchEventStatus, WatchlistEntry, WatchlistWithMovie, WebhookToken,
collections::{self, PageParams, Paginated}, collections::{self, PageParams, Paginated},
wrapup::{DateRange, WrapUpScope}, wrapup::{DateRange, WrapUpRecord, WrapUpScope, WrapUpStatus},
}, },
value_objects::{ value_objects::{
Email, ExternalMetadataId, ImportProfileId, ImportSessionId, MovieId, MovieTitle, Email, ExternalMetadataId, ImportProfileId, ImportSessionId, MovieId, MovieTitle,
PasswordHash, PosterUrl, ReleaseYear, ReviewId, UserId, Username, WatchEventId, 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 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<Option<WrapUpRecord>, DomainError>;
async fn list_for_user(&self, user_id: Uuid) -> Result<Vec<WrapUpRecord>, DomainError>;
async fn list_global(&self) -> Result<Vec<WrapUpRecord>, DomainError>;
async fn find_existing(
&self,
user_id: Option<Uuid>,
start: NaiveDate,
end: NaiveDate,
) -> Result<Option<WrapUpRecord>, DomainError>;
}
// ── Wrap-up / Year-in-Review ───────────────────────────────────────────────── // ── Wrap-up / Year-in-Review ─────────────────────────────────────────────────
#[derive(Clone, Debug)] #[derive(Clone, Debug)]

View File

@@ -24,11 +24,11 @@ use crate::{
ImportSessionRepository, MetadataClient, MetadataSearchCriteria, MovieProfileRepository, ImportSessionRepository, MetadataClient, MetadataSearchCriteria, MovieProfileRepository,
MovieRepository, PasswordHasher, PersonCommand, PersonQuery, PosterFetcherClient, MovieRepository, PasswordHasher, PersonCommand, PersonQuery, PosterFetcherClient,
ReviewRepository, SearchCommand, SearchPort, StatsRepository, UserProfileFieldsRepository, ReviewRepository, SearchCommand, SearchPort, StatsRepository, UserProfileFieldsRepository,
UserRepository, WatchlistRepository, UserRepository, WatchlistRepository, WrapUpRepository,
}, },
value_objects::{ value_objects::{
Email, ExternalMetadataId, ImportProfileId, ImportSessionId, MovieId, MovieTitle, 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) Ok(filtered)
} }
} }
// ── InMemoryWrapUpRepository ────────────────────────────────────────────────
pub struct InMemoryWrapUpRepository {
pub store: Mutex<Vec<crate::models::wrapup::WrapUpRecord>>,
}
impl InMemoryWrapUpRepository {
pub fn new() -> Arc<Self> {
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<Option<crate::models::wrapup::WrapUpRecord>, 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<Vec<crate::models::wrapup::WrapUpRecord>, 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<Vec<crate::models::wrapup::WrapUpRecord>, 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<Uuid>,
start: chrono::NaiveDate,
end: chrono::NaiveDate,
) -> Result<Option<crate::models::wrapup::WrapUpRecord>, 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<Option<crate::models::wrapup::WrapUpRecord>, DomainError> {
panic!("PanicWrapUpRepository called")
}
async fn list_for_user(
&self,
_: Uuid,
) -> Result<Vec<crate::models::wrapup::WrapUpRecord>, DomainError> {
panic!("PanicWrapUpRepository called")
}
async fn list_global(
&self,
) -> Result<Vec<crate::models::wrapup::WrapUpRecord>, DomainError> {
panic!("PanicWrapUpRepository called")
}
async fn find_existing(
&self,
_: Option<Uuid>,
_: chrono::NaiveDate,
_: chrono::NaiveDate,
) -> Result<Option<crate::models::wrapup::WrapUpRecord>, DomainError> {
panic!("PanicWrapUpRepository called")
}
}

View File

@@ -28,6 +28,7 @@ uuid_id!(ImportProfileId);
uuid_id!(WatchlistEntryId); uuid_id!(WatchlistEntryId);
uuid_id!(WatchEventId); uuid_id!(WatchEventId);
uuid_id!(WebhookTokenId); uuid_id!(WebhookTokenId);
uuid_id!(WrapUpId);
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExternalMetadataId(String); pub struct ExternalMetadataId(String);