feat: atomic goal, watchlist, profile writes

This commit is contained in:
2026-10-26 00:49:28 +01:00
parent c8a1e8bfe4
commit 62ad9f4017
48 changed files with 1296 additions and 264 deletions

View File

@@ -94,19 +94,17 @@ impl ReviewLogger for DefaultReviewLogger {
events.push(review_event);
self.review_repo.save_review(&review, &events).await?;
let was_on_watchlist = self
.watchlist_repo
.remove_if_present(review.user_id(), review.movie_id())
.await?;
if was_on_watchlist {
let _ = self
.event_publisher
.publish(&DomainEvent::WatchlistEntryRemoved {
// The removal event is recorded only if an entry was actually removed.
self.watchlist_repo
.remove_if_present(
review.user_id(),
review.movie_id(),
&[DomainEvent::WatchlistEntryRemoved {
user_id: review.user_id().clone(),
movie_id: review.movie_id().clone(),
})
.await;
}
}],
)
.await?;
Ok(())
}

View File

@@ -29,19 +29,16 @@ pub async fn execute(
cmd.target_count,
GoalType::Movies,
)?;
deps.goal_command.save(&g).await?;
let event = DomainEvent::GoalCreated {
goal_id: g.id().clone(),
user_id: user_id.clone(),
year: cmd.year,
target_count: cmd.target_count,
};
deps.goal_command.save(&g, &[event]).await?;
let current_count = deps.stats.count_reviews_in_year(&user_id, cmd.year).await?;
deps.event_publisher
.publish(&DomainEvent::GoalCreated {
goal_id: g.id().clone(),
user_id,
year: cmd.year,
target_count: cmd.target_count,
})
.await?;
Ok(GoalWithProgress {
goal: g,
current_count,

View File

@@ -11,15 +11,12 @@ pub async fn execute(deps: &GoalCommandDeps, cmd: DeleteGoalCommand) -> Result<(
.await?
.ok_or_else(|| DomainError::NotFound(format!("Goal for year {}", cmd.year)))?;
deps.goal_command.delete(g.id(), &user_id).await?;
deps.event_publisher
.publish(&DomainEvent::GoalDeleted {
goal_id: g.id().clone(),
user_id,
year: cmd.year,
})
.await?;
let event = DomainEvent::GoalDeleted {
goal_id: g.id().clone(),
user_id: user_id.clone(),
year: cmd.year,
};
deps.goal_command.delete(g.id(), &user_id, &[event]).await?;
Ok(())
}

View File

@@ -1,12 +1,11 @@
use std::sync::Arc;
use domain::ports::{EventPublisher, GoalCommand, GoalQuery, StatsRepository};
use domain::ports::{GoalCommand, GoalQuery, StatsRepository};
pub struct GoalCommandDeps {
pub goal_command: Arc<dyn GoalCommand>,
pub goal_query: Arc<dyn GoalQuery>,
pub stats: Arc<dyn StatsRepository>,
pub event_publisher: Arc<dyn EventPublisher>,
}
pub struct GoalQueryDeps {

View File

@@ -17,19 +17,16 @@ pub async fn execute(
.ok_or_else(|| DomainError::NotFound(format!("Goal for year {}", cmd.year)))?;
g.update_target(cmd.target_count)?;
deps.goal_command.update(&g).await?;
let event = DomainEvent::GoalUpdated {
goal_id: g.id().clone(),
user_id: user_id.clone(),
year: cmd.year,
target_count: cmd.target_count,
};
deps.goal_command.update(&g, &[event]).await?;
let current_count = deps.stats.count_reviews_in_year(&user_id, cmd.year).await?;
deps.event_publisher
.publish(&DomainEvent::GoalUpdated {
goal_id: g.id().clone(),
user_id,
year: cmd.year,
target_count: cmd.target_count,
})
.await?;
Ok(GoalWithProgress {
goal: g,
current_count,

View File

@@ -58,7 +58,6 @@ pub struct GetCurrentProfileDeps {
pub struct UpdateProfileFieldsDeps {
pub profile_fields: Arc<dyn UserProfileFieldsRepository>,
pub event_publisher: Arc<dyn EventPublisher>,
}
pub struct GetSettingsDeps {

View File

@@ -91,6 +91,16 @@ pub async fn execute(
}
});
let mut events = vec![DomainEvent::UserUpdated {
user_id: user_id.clone(),
}];
if let Some(new_actor_url) = moved_to {
events.push(DomainEvent::UserAccountMoved {
user_id: user_id.clone(),
new_actor_url,
});
}
deps.user
.update_profile(
&user_id,
@@ -102,24 +112,9 @@ pub async fn execute(
also_known_as: cmd.also_known_as,
profile_fields: vec![],
},
&events,
)
.await?;
deps.event_publisher
.publish(&DomainEvent::UserUpdated {
user_id: user_id.clone(),
})
.await?;
if let Some(new_actor_url) = moved_to {
let _ = deps
.event_publisher
.publish(&DomainEvent::UserAccountMoved {
user_id,
new_actor_url,
})
.await;
}
Ok(())
}

View File

@@ -11,9 +11,10 @@ pub async fn execute(
) -> Result<(), DomainError> {
UserProfile::validate_custom_fields(&cmd.fields)?;
let user_id = UserId::from_uuid(cmd.user_id);
deps.profile_fields.set_fields(&user_id, cmd.fields).await?;
deps.event_publisher
.publish(&DomainEvent::UserUpdated { user_id })
.await?;
Ok(())
let event = DomainEvent::UserUpdated {
user_id: user_id.clone(),
};
deps.profile_fields
.set_fields(&user_id, cmd.fields, &[event])
.await
}

View File

@@ -23,19 +23,15 @@ pub async fn execute(
.await?;
let entry = WatchlistEntry::new(user_id.clone(), movie.id().clone());
deps.watchlist.add(&entry).await?;
let _ = deps
.event_publisher
.publish(&DomainEvent::WatchlistEntryAdded {
user_id,
movie_id: movie.id().clone(),
movie_title: movie.title().value().to_string(),
release_year: movie.release_year().value(),
external_metadata_id: movie.external_metadata_id().map(|e| e.value().to_string()),
added_at: entry.added_at,
})
.await;
let event = DomainEvent::WatchlistEntryAdded {
user_id,
movie_id: movie.id().clone(),
movie_title: movie.title().value().to_string(),
release_year: movie.release_year().value(),
external_metadata_id: movie.external_metadata_id().map(|e| e.value().to_string()),
added_at: entry.added_at,
};
deps.watchlist.add(&entry, &[event]).await?;
Ok(())
}

View File

@@ -29,5 +29,4 @@ pub struct IsOnWatchlistDeps {
pub struct RemoveFromWatchlistDeps {
pub watchlist: Arc<dyn WatchlistRepository>,
pub event_publisher: Arc<dyn EventPublisher>,
}

View File

@@ -13,12 +13,11 @@ pub async fn execute(
) -> Result<(), DomainError> {
let user_id = UserId::from_uuid(cmd.user_id);
let movie_id = MovieId::from_uuid(cmd.movie_id);
deps.watchlist.remove(&user_id, &movie_id).await?;
let _ = deps
.event_publisher
.publish(&DomainEvent::WatchlistEntryRemoved { user_id, movie_id })
.await;
let event = DomainEvent::WatchlistEntryRemoved {
user_id: user_id.clone(),
movie_id: movie_id.clone(),
};
deps.watchlist.remove(&user_id, &movie_id, &[event]).await?;
Ok(())
}