From b171d2d1e2a4e226446723a2d3d5afe0c148a1df Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Tue, 2 Jun 2026 22:09:08 +0200 Subject: [PATCH] feat(application): wrapup generate/get/list use cases --- crates/application/src/context.rs | 3 +- crates/application/src/test_helpers.rs | 7 ++- crates/application/src/wrapup/commands.rs | 8 ++++ crates/application/src/wrapup/generate.rs | 47 +++++++++++++++++++ crates/application/src/wrapup/get_wrapup.rs | 9 ++++ crates/application/src/wrapup/list_wrapups.rs | 20 ++++++++ crates/application/src/wrapup/mod.rs | 4 ++ crates/presentation/src/main.rs | 1 + crates/presentation/src/tests/extractors.rs | 46 ++++++++++++++++++ crates/presentation/tests/api_test.rs | 2 + crates/worker/src/main.rs | 1 + 11 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 crates/application/src/wrapup/commands.rs create mode 100644 crates/application/src/wrapup/generate.rs create mode 100644 crates/application/src/wrapup/get_wrapup.rs create mode 100644 crates/application/src/wrapup/list_wrapups.rs diff --git a/crates/application/src/context.rs b/crates/application/src/context.rs index 4008ce3..c9121a9 100644 --- a/crates/application/src/context.rs +++ b/crates/application/src/context.rs @@ -6,7 +6,7 @@ use domain::ports::{ MovieRepository, PasswordHasher, PersonCommand, PersonQuery, PosterFetcherClient, RemoteWatchlistRepository, ReviewRepository, SearchCommand, SearchPort, SocialQueryPort, StatsRepository, UserProfileFieldsRepository, UserRepository, WatchEventRepository, - WatchlistRepository, WrapUpStatsQuery, WebhookTokenRepository, + WatchlistRepository, WrapUpRepository, WrapUpStatsQuery, WebhookTokenRepository, }; use crate::config::AppConfig; @@ -32,6 +32,7 @@ pub struct Repositories { pub remote_watchlist: Arc, pub social_query: Arc, pub wrapup_stats: Arc, + pub wrapup_repo: Arc, } #[derive(Clone)] diff --git a/crates/application/src/test_helpers.rs b/crates/application/src/test_helpers.rs index f45e8f0..9d0c667 100644 --- a/crates/application/src/test_helpers.rs +++ b/crates/application/src/test_helpers.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use domain::testing::{InMemoryWrapUpStatsQuery, NoopRemoteWatchlistRepository, NoopSocialQueryPort}; +use domain::testing::{InMemoryWrapUpRepository, InMemoryWrapUpStatsQuery, NoopRemoteWatchlistRepository, NoopSocialQueryPort}; use domain::{ ports::{ AuthService, DiaryExporter, DiaryRepository, DocumentParser, EventPublisher, ImageStorage, @@ -8,7 +8,7 @@ use domain::{ MovieRepository, PasswordHasher, PersonCommand, PersonQuery, PosterFetcherClient, ReviewRepository, SearchCommand, SearchPort, StatsRepository, UserProfileFieldsRepository, UserRepository, WatchEventRepository, WatchlistRepository, WebhookTokenRepository, - WrapUpStatsQuery, + WrapUpRepository, WrapUpStatsQuery, }, testing::{ FakeAuthService, FakeMetadataClient, FakePasswordHasher, InMemoryMovieRepository, @@ -52,6 +52,7 @@ pub struct TestContextBuilder { pub search_port: Arc, pub search_command: Arc, pub wrapup_stats: Arc, + pub wrapup_repo: Arc, pub config: AppConfig, } @@ -83,6 +84,7 @@ impl TestContextBuilder { search_port: Arc::new(PanicSearchPort), search_command: Arc::new(PanicSearchCommand), wrapup_stats: InMemoryWrapUpStatsQuery::new(), + wrapup_repo: InMemoryWrapUpRepository::new(), config: AppConfig { allow_registration: true, base_url: "http://localhost:3000".into(), @@ -153,6 +155,7 @@ impl TestContextBuilder { remote_watchlist: Arc::new(NoopRemoteWatchlistRepository), social_query: Arc::new(NoopSocialQueryPort), wrapup_stats: self.wrapup_stats, + wrapup_repo: self.wrapup_repo, }, services: Services { auth: self.auth_service, diff --git a/crates/application/src/wrapup/commands.rs b/crates/application/src/wrapup/commands.rs new file mode 100644 index 0000000..282a233 --- /dev/null +++ b/crates/application/src/wrapup/commands.rs @@ -0,0 +1,8 @@ +use chrono::NaiveDate; +use uuid::Uuid; + +pub struct RequestWrapUpCommand { + pub user_id: Option, + pub start_date: NaiveDate, + pub end_date: NaiveDate, +} diff --git a/crates/application/src/wrapup/generate.rs b/crates/application/src/wrapup/generate.rs new file mode 100644 index 0000000..3b598fa --- /dev/null +++ b/crates/application/src/wrapup/generate.rs @@ -0,0 +1,47 @@ +use domain::errors::DomainError; +use domain::events::DomainEvent; +use domain::models::wrapup::WrapUpStatus; +use domain::value_objects::{UserId, WrapUpId}; + +use crate::context::AppContext; +use crate::wrapup::commands::RequestWrapUpCommand; + +pub async fn execute(ctx: &AppContext, cmd: RequestWrapUpCommand) -> Result { + let existing = ctx + .repos + .wrapup_repo + .find_existing(cmd.user_id, cmd.start_date, cmd.end_date) + .await?; + + if let Some(ref rec) = existing { + if rec.status == WrapUpStatus::Ready || rec.status == WrapUpStatus::Generating { + return Ok(rec.id.clone()); + } + } + + let id = WrapUpId::generate(); + let record = domain::models::wrapup::WrapUpRecord { + id: id.clone(), + user_id: cmd.user_id, + start_date: cmd.start_date, + end_date: cmd.end_date, + status: WrapUpStatus::Pending, + report_json: None, + error_message: None, + created_at: chrono::Utc::now().naive_utc(), + completed_at: None, + }; + ctx.repos.wrapup_repo.create(&record).await?; + + ctx.services + .event_publisher + .publish(&DomainEvent::WrapUpRequested { + wrapup_id: id.clone(), + user_id: cmd.user_id.map(UserId::from_uuid), + start_date: cmd.start_date, + end_date: cmd.end_date, + }) + .await?; + + Ok(id) +} diff --git a/crates/application/src/wrapup/get_wrapup.rs b/crates/application/src/wrapup/get_wrapup.rs new file mode 100644 index 0000000..3b499cc --- /dev/null +++ b/crates/application/src/wrapup/get_wrapup.rs @@ -0,0 +1,9 @@ +use domain::errors::DomainError; +use domain::models::wrapup::WrapUpRecord; +use domain::value_objects::WrapUpId; + +use crate::context::AppContext; + +pub async fn execute(ctx: &AppContext, id: WrapUpId) -> Result, DomainError> { + ctx.repos.wrapup_repo.get_by_id(&id).await +} diff --git a/crates/application/src/wrapup/list_wrapups.rs b/crates/application/src/wrapup/list_wrapups.rs new file mode 100644 index 0000000..92deab7 --- /dev/null +++ b/crates/application/src/wrapup/list_wrapups.rs @@ -0,0 +1,20 @@ +use uuid::Uuid; + +use domain::errors::DomainError; +use domain::models::wrapup::WrapUpRecord; + +use crate::context::AppContext; + +pub struct ListWrapUpsQuery { + pub user_id: Option, +} + +pub async fn execute( + ctx: &AppContext, + query: ListWrapUpsQuery, +) -> Result, DomainError> { + match query.user_id { + Some(uid) => ctx.repos.wrapup_repo.list_for_user(uid).await, + None => ctx.repos.wrapup_repo.list_global().await, + } +} diff --git a/crates/application/src/wrapup/mod.rs b/crates/application/src/wrapup/mod.rs index 6196e90..076a360 100644 --- a/crates/application/src/wrapup/mod.rs +++ b/crates/application/src/wrapup/mod.rs @@ -1,2 +1,6 @@ +pub mod commands; pub mod compute; +pub mod generate; +pub mod get_wrapup; +pub mod list_wrapups; pub mod queries; diff --git a/crates/presentation/src/main.rs b/crates/presentation/src/main.rs index ec603d1..b52ada6 100644 --- a/crates/presentation/src/main.rs +++ b/crates/presentation/src/main.rs @@ -194,6 +194,7 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> { #[cfg(not(feature = "federation"))] social_query: Arc::new(domain::testing::NoopSocialQueryPort), wrapup_stats: Arc::new(domain::testing::PanicWrapUpStatsQuery) as Arc, + wrapup_repo: Arc::new(domain::testing::PanicWrapUpRepository) as Arc, }, services: Services { auth: auth_service, diff --git a/crates/presentation/src/tests/extractors.rs b/crates/presentation/src/tests/extractors.rs index ed2fc68..938ffd7 100644 --- a/crates/presentation/src/tests/extractors.rs +++ b/crates/presentation/src/tests/extractors.rs @@ -568,6 +568,50 @@ impl domain::ports::WebhookTokenRepository for Panic { } } +impl domain::ports::WrapUpStatsQuery for Panic { + async fn get_reviews_with_profiles( + &self, + _: &domain::models::wrapup::WrapUpScope, + _: &domain::models::wrapup::DateRange, + ) -> Result, DomainError> { + panic!() + } +} + +impl domain::ports::WrapUpRepository for Panic { + async fn create(&self, _: &domain::models::wrapup::WrapUpRecord) -> Result<(), DomainError> { + panic!() + } + async fn update_status( + &self, + _: &domain::value_objects::WrapUpId, + _: &domain::models::wrapup::WrapUpStatus, + _: Option<&str>, + ) -> Result<(), DomainError> { + panic!() + } + async fn set_complete(&self, _: &domain::value_objects::WrapUpId, _: &str) -> Result<(), DomainError> { + panic!() + } + async fn get_by_id(&self, _: &domain::value_objects::WrapUpId) -> Result, DomainError> { + panic!() + } + async fn list_for_user(&self, _: uuid::Uuid) -> Result, DomainError> { + panic!() + } + async fn list_global(&self) -> Result, DomainError> { + panic!() + } + async fn find_existing( + &self, + _: Option, + _: chrono::NaiveDate, + _: chrono::NaiveDate, + ) -> Result, DomainError> { + panic!() + } +} + // --- Single state factory — only auth_service varies --- pub fn make_test_state(auth_service: Arc) -> crate::state::AppState { @@ -593,6 +637,8 @@ pub fn make_test_state(auth_service: Arc) -> crate::state::AppS search_command: Arc::clone(&repo) as _, remote_watchlist: Arc::clone(&repo) as _, social_query: Arc::clone(&repo) as _, + wrapup_stats: Arc::clone(&repo) as _, + wrapup_repo: Arc::clone(&repo) as _, }, services: Services { auth: auth_service, diff --git a/crates/presentation/tests/api_test.rs b/crates/presentation/tests/api_test.rs index 40bc1ad..c5a2d2e 100644 --- a/crates/presentation/tests/api_test.rs +++ b/crates/presentation/tests/api_test.rs @@ -415,6 +415,8 @@ async fn test_app() -> Router { search_command: Arc::new(PanicSearchCommand), remote_watchlist: Arc::new(PanicRemoteWatchlist), social_query: Arc::new(PanicSocialQuery), + wrapup_stats: Arc::new(domain::testing::PanicWrapUpStatsQuery) as _, + wrapup_repo: Arc::new(domain::testing::PanicWrapUpRepository) as _, }, services: Services { auth: Arc::new(PanicAuth), diff --git a/crates/worker/src/main.rs b/crates/worker/src/main.rs index f27046c..ee34db3 100644 --- a/crates/worker/src/main.rs +++ b/crates/worker/src/main.rs @@ -93,6 +93,7 @@ async fn main() -> anyhow::Result<()> { #[cfg(not(feature = "federation"))] social_query: Arc::new(domain::testing::NoopSocialQueryPort), wrapup_stats: Arc::new(domain::testing::PanicWrapUpStatsQuery) as Arc, + wrapup_repo: Arc::new(domain::testing::PanicWrapUpRepository) as Arc, }, services: Services { auth: auth_service,