feat(domain): add WrapUpRecord, WrapUpRepository port
This commit is contained in:
@@ -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>,
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user