34
crates/application/src/entry/use_cases/create_entry.rs
Normal file
34
crates/application/src/entry/use_cases/create_entry.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::entry::MoodEntry;
|
||||
use domain::events::{DomainEvent, EventEnvelope};
|
||||
use domain::ports::{EventPublisherPort, MoodEntryCommandPort};
|
||||
|
||||
use crate::errors::ApplicationError;
|
||||
|
||||
use super::super::commands::CreateEntryCommand;
|
||||
|
||||
pub struct Deps {
|
||||
pub entries: Arc<dyn MoodEntryCommandPort>,
|
||||
pub events: Arc<dyn EventPublisherPort>,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(deps))]
|
||||
pub async fn execute(cmd: CreateEntryCommand, deps: &Deps) -> Result<MoodEntry, ApplicationError> {
|
||||
let mut entry = MoodEntry::new(cmd.user_id, cmd.mood, cmd.logged_at);
|
||||
|
||||
entry.set_content(cmd.content);
|
||||
entry.set_activities(cmd.activities);
|
||||
entry.set_photos(cmd.photos);
|
||||
entry.set_voice_memos(cmd.voice_memos);
|
||||
|
||||
deps.entries.save(&entry).await?;
|
||||
|
||||
let event = DomainEvent::EntryCreated {
|
||||
entry_id: entry.id().clone(),
|
||||
user_id: entry.user_id().clone(),
|
||||
};
|
||||
deps.events.publish(EventEnvelope::wrap(event)).await?;
|
||||
|
||||
Ok(entry)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::entry::DateRange;
|
||||
use domain::ports::{CascadeDeletePort, MediaStoragePort};
|
||||
use domain::user::UserId;
|
||||
|
||||
use crate::errors::ApplicationError;
|
||||
|
||||
pub struct Deps {
|
||||
pub cascade: Arc<dyn CascadeDeletePort>,
|
||||
pub media_storage: Arc<dyn MediaStoragePort>,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(deps))]
|
||||
pub async fn execute(
|
||||
user_id: UserId,
|
||||
range: &DateRange,
|
||||
deps: &Deps,
|
||||
) -> Result<u64, ApplicationError> {
|
||||
let entries = deps
|
||||
.cascade
|
||||
.delete_entries_in_range(&user_id, range)
|
||||
.await?;
|
||||
|
||||
for entry in &entries {
|
||||
for photo_id in entry.photos() {
|
||||
if let Err(e) = deps.media_storage.delete_photo(photo_id).await {
|
||||
tracing::warn!(%e, "failed to delete photo blob");
|
||||
}
|
||||
}
|
||||
for memo_id in entry.voice_memos() {
|
||||
if let Err(e) = deps.media_storage.delete_voice_memo(memo_id).await {
|
||||
tracing::warn!(%e, "failed to delete voice memo blob");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(entries.len() as u64)
|
||||
}
|
||||
55
crates/application/src/entry/use_cases/delete_entry.rs
Normal file
55
crates/application/src/entry/use_cases/delete_entry.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::entry::MoodEntryId;
|
||||
use domain::errors::DomainError;
|
||||
use domain::events::{DomainEvent, EventEnvelope};
|
||||
use domain::ports::{
|
||||
EventPublisherPort, MediaStoragePort, MoodEntryCommandPort, MoodEntryQueryPort,
|
||||
};
|
||||
use domain::user::UserId;
|
||||
|
||||
use crate::authorization::verify_ownership;
|
||||
use crate::errors::ApplicationError;
|
||||
|
||||
pub struct Deps {
|
||||
pub command: Arc<dyn MoodEntryCommandPort>,
|
||||
pub query: Arc<dyn MoodEntryQueryPort>,
|
||||
pub events: Arc<dyn EventPublisherPort>,
|
||||
pub media_storage: Arc<dyn MediaStoragePort>,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(deps))]
|
||||
pub async fn execute(
|
||||
entry_id: MoodEntryId,
|
||||
caller_id: UserId,
|
||||
deps: &Deps,
|
||||
) -> Result<(), ApplicationError> {
|
||||
let entry = deps
|
||||
.query
|
||||
.find_by_id(&entry_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("mood entry not found".into()))?;
|
||||
|
||||
verify_ownership(entry.user_id(), &caller_id)?;
|
||||
|
||||
for photo_id in entry.photos() {
|
||||
if let Err(e) = deps.media_storage.delete_photo(photo_id).await {
|
||||
tracing::warn!(%e, "failed to delete photo blob");
|
||||
}
|
||||
}
|
||||
for memo_id in entry.voice_memos() {
|
||||
if let Err(e) = deps.media_storage.delete_voice_memo(memo_id).await {
|
||||
tracing::warn!(%e, "failed to delete voice memo blob");
|
||||
}
|
||||
}
|
||||
|
||||
deps.command.delete(&entry_id).await?;
|
||||
|
||||
let event = DomainEvent::EntryDeleted {
|
||||
entry_id,
|
||||
user_id: entry.user_id().clone(),
|
||||
};
|
||||
deps.events.publish(EventEnvelope::wrap(event)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
24
crates/application/src/entry/use_cases/filter_by_activity.rs
Normal file
24
crates/application/src/entry/use_cases/filter_by_activity.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::entry::MoodEntry;
|
||||
use domain::ports::MoodEntryQueryPort;
|
||||
|
||||
use crate::errors::ApplicationError;
|
||||
|
||||
use super::super::queries::FilterByActivityQuery;
|
||||
|
||||
pub struct Deps {
|
||||
pub query: Arc<dyn MoodEntryQueryPort>,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(deps))]
|
||||
pub async fn execute(
|
||||
query: FilterByActivityQuery,
|
||||
deps: &Deps,
|
||||
) -> Result<Vec<MoodEntry>, ApplicationError> {
|
||||
let entries = deps
|
||||
.query
|
||||
.find_by_activity(&query.user_id, &query.activity_id)
|
||||
.await?;
|
||||
Ok(entries)
|
||||
}
|
||||
21
crates/application/src/entry/use_cases/filter_by_mood.rs
Normal file
21
crates/application/src/entry/use_cases/filter_by_mood.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::entry::MoodEntry;
|
||||
use domain::ports::MoodEntryQueryPort;
|
||||
|
||||
use crate::errors::ApplicationError;
|
||||
|
||||
use super::super::queries::FilterByMoodQuery;
|
||||
|
||||
pub struct Deps {
|
||||
pub query: Arc<dyn MoodEntryQueryPort>,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(deps))]
|
||||
pub async fn execute(
|
||||
query: FilterByMoodQuery,
|
||||
deps: &Deps,
|
||||
) -> Result<Vec<MoodEntry>, ApplicationError> {
|
||||
let entries = deps.query.find_by_mood(&query.user_id, query.mood).await?;
|
||||
Ok(entries)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::activity::ActivityId;
|
||||
use domain::entry::DateRange;
|
||||
use domain::ports::MoodEntryQueryPort;
|
||||
use domain::services::MoodAnalyzerService;
|
||||
use domain::user::UserId;
|
||||
|
||||
use crate::errors::ApplicationError;
|
||||
|
||||
pub struct Deps {
|
||||
pub query: Arc<dyn MoodEntryQueryPort>,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(deps))]
|
||||
pub async fn execute(
|
||||
user_id: UserId,
|
||||
activity_id: ActivityId,
|
||||
range: Option<DateRange>,
|
||||
deps: &Deps,
|
||||
) -> Result<Option<f64>, ApplicationError> {
|
||||
let entries = match range {
|
||||
Some(range) => deps.query.find_by_date_range(&user_id, &range).await?,
|
||||
None => deps.query.find_by_user(&user_id, None, None).await?,
|
||||
};
|
||||
|
||||
Ok(MoodAnalyzerService::activity_mood_correlation(
|
||||
&entries,
|
||||
&activity_id,
|
||||
))
|
||||
}
|
||||
68
crates/application/src/entry/use_cases/get_calendar.rs
Normal file
68
crates/application/src/entry/use_cases/get_calendar.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::NaiveDate;
|
||||
|
||||
use domain::entry::{DateRange, Mood, MoodEntry};
|
||||
use domain::ports::MoodEntryQueryPort;
|
||||
use domain::user::UserId;
|
||||
|
||||
use crate::errors::ApplicationError;
|
||||
|
||||
pub struct CalendarDay {
|
||||
pub date: NaiveDate,
|
||||
pub entries: Vec<MoodEntry>,
|
||||
pub dominant_mood: Option<Mood>,
|
||||
}
|
||||
|
||||
pub struct Deps {
|
||||
pub query: Arc<dyn MoodEntryQueryPort>,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(deps))]
|
||||
pub async fn execute(
|
||||
user_id: UserId,
|
||||
range: DateRange,
|
||||
deps: &Deps,
|
||||
) -> Result<Vec<CalendarDay>, ApplicationError> {
|
||||
let entries = deps.query.find_by_date_range(&user_id, &range).await?;
|
||||
|
||||
let mut by_date: BTreeMap<NaiveDate, Vec<MoodEntry>> = BTreeMap::new();
|
||||
for entry in entries {
|
||||
let date = entry.logged_at().date_naive();
|
||||
by_date.entry(date).or_default().push(entry);
|
||||
}
|
||||
|
||||
let days = by_date
|
||||
.into_iter()
|
||||
.map(|(date, entries)| {
|
||||
let dominant_mood = find_dominant_mood(&entries);
|
||||
CalendarDay {
|
||||
date,
|
||||
entries,
|
||||
dominant_mood,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(days)
|
||||
}
|
||||
|
||||
fn find_dominant_mood(entries: &[MoodEntry]) -> Option<Mood> {
|
||||
if entries.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut counts = [0u32; 5];
|
||||
for entry in entries {
|
||||
counts[entry.mood().value() as usize - 1] += 1;
|
||||
}
|
||||
|
||||
let max_index = counts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.max_by_key(|(_, count)| *count)
|
||||
.map(|(i, _)| i)?;
|
||||
|
||||
Mood::try_from(max_index as u8 + 1).ok()
|
||||
}
|
||||
29
crates/application/src/entry/use_cases/get_entry.rs
Normal file
29
crates/application/src/entry/use_cases/get_entry.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::entry::{MoodEntry, MoodEntryId};
|
||||
use domain::errors::DomainError;
|
||||
use domain::ports::MoodEntryQueryPort;
|
||||
use domain::user::UserId;
|
||||
|
||||
use crate::authorization::verify_ownership;
|
||||
use crate::errors::ApplicationError;
|
||||
|
||||
pub struct Deps {
|
||||
pub query: Arc<dyn MoodEntryQueryPort>,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(deps))]
|
||||
pub async fn execute(
|
||||
entry_id: MoodEntryId,
|
||||
caller_id: UserId,
|
||||
deps: &Deps,
|
||||
) -> Result<MoodEntry, ApplicationError> {
|
||||
let entry = deps
|
||||
.query
|
||||
.find_by_id(&entry_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("mood entry not found".into()))?;
|
||||
|
||||
verify_ownership(entry.user_id(), &caller_id)?;
|
||||
Ok(entry)
|
||||
}
|
||||
39
crates/application/src/entry/use_cases/get_mood_stats.rs
Normal file
39
crates/application/src/entry/use_cases/get_mood_stats.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::entry::Mood;
|
||||
use domain::ports::MoodEntryQueryPort;
|
||||
use domain::services::MoodAnalyzerService;
|
||||
|
||||
use crate::errors::ApplicationError;
|
||||
|
||||
use super::super::queries::MoodStatsQuery;
|
||||
|
||||
pub struct MoodStats {
|
||||
pub average: Option<f64>,
|
||||
pub frequency: Vec<(Mood, usize)>,
|
||||
pub current_streak: usize,
|
||||
pub total_entries: usize,
|
||||
}
|
||||
|
||||
pub struct Deps {
|
||||
pub query: Arc<dyn MoodEntryQueryPort>,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(deps))]
|
||||
pub async fn execute(query: MoodStatsQuery, deps: &Deps) -> Result<MoodStats, ApplicationError> {
|
||||
let entries = match query.range {
|
||||
Some(range) => {
|
||||
deps.query
|
||||
.find_by_date_range(&query.user_id, &range)
|
||||
.await?
|
||||
}
|
||||
None => deps.query.find_by_user(&query.user_id, None, None).await?,
|
||||
};
|
||||
|
||||
Ok(MoodStats {
|
||||
average: MoodAnalyzerService::average_mood(&entries),
|
||||
frequency: MoodAnalyzerService::mood_frequency(&entries),
|
||||
current_streak: MoodAnalyzerService::current_streak(&entries),
|
||||
total_entries: entries.len(),
|
||||
})
|
||||
}
|
||||
33
crates/application/src/entry/use_cases/list_entries.rs
Normal file
33
crates/application/src/entry/use_cases/list_entries.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::entry::MoodEntry;
|
||||
use domain::ports::MoodEntryQueryPort;
|
||||
|
||||
use crate::errors::ApplicationError;
|
||||
|
||||
use super::super::queries::ListEntriesQuery;
|
||||
|
||||
pub struct Deps {
|
||||
pub query: Arc<dyn MoodEntryQueryPort>,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(deps))]
|
||||
pub async fn execute(
|
||||
query: ListEntriesQuery,
|
||||
deps: &Deps,
|
||||
) -> Result<Vec<MoodEntry>, ApplicationError> {
|
||||
let entries = match query.range {
|
||||
Some(range) => {
|
||||
deps.query
|
||||
.find_by_date_range(&query.user_id, &range)
|
||||
.await?
|
||||
}
|
||||
None => {
|
||||
deps.query
|
||||
.find_by_user(&query.user_id, query.limit, query.offset)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
12
crates/application/src/entry/use_cases/mod.rs
Normal file
12
crates/application/src/entry/use_cases/mod.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
pub mod create_entry;
|
||||
pub mod delete_entries_by_date_range;
|
||||
pub mod delete_entry;
|
||||
pub mod filter_by_activity;
|
||||
pub mod filter_by_mood;
|
||||
pub mod get_activity_correlation;
|
||||
pub mod get_calendar;
|
||||
pub mod get_entry;
|
||||
pub mod get_mood_stats;
|
||||
pub mod list_entries;
|
||||
pub mod replace_activity;
|
||||
pub mod update_entry;
|
||||
37
crates/application/src/entry/use_cases/replace_activity.rs
Normal file
37
crates/application/src/entry/use_cases/replace_activity.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::activity::ActivityId;
|
||||
use domain::errors::DomainError;
|
||||
use domain::ports::{ActivityQueryPort, MoodEntryCommandPort};
|
||||
use domain::user::UserId;
|
||||
|
||||
use crate::authorization::verify_ownership;
|
||||
use crate::errors::ApplicationError;
|
||||
|
||||
pub struct Deps {
|
||||
pub entry_command: Arc<dyn MoodEntryCommandPort>,
|
||||
pub activity_query: Arc<dyn ActivityQueryPort>,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(deps))]
|
||||
pub async fn execute(
|
||||
user_id: UserId,
|
||||
old_activity_id: ActivityId,
|
||||
new_activity_id: ActivityId,
|
||||
deps: &Deps,
|
||||
) -> Result<u64, ApplicationError> {
|
||||
let new_activity = deps
|
||||
.activity_query
|
||||
.find_by_id(&new_activity_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("target activity not found".into()))?;
|
||||
|
||||
verify_ownership(new_activity.user_id(), &user_id)?;
|
||||
|
||||
let updated = deps
|
||||
.entry_command
|
||||
.replace_activity(&user_id, &old_activity_id, &new_activity_id)
|
||||
.await?;
|
||||
|
||||
Ok(updated)
|
||||
}
|
||||
71
crates/application/src/entry/use_cases/update_entry.rs
Normal file
71
crates/application/src/entry/use_cases/update_entry.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::entry::MoodEntry;
|
||||
use domain::errors::DomainError;
|
||||
use domain::events::{DomainEvent, EventEnvelope};
|
||||
use domain::ports::{
|
||||
EventPublisherPort, MediaStoragePort, MoodEntryCommandPort, MoodEntryQueryPort,
|
||||
};
|
||||
use domain::user::UserId;
|
||||
|
||||
use crate::authorization::verify_ownership;
|
||||
use crate::errors::ApplicationError;
|
||||
|
||||
use super::super::commands::UpdateEntryCommand;
|
||||
|
||||
pub struct Deps {
|
||||
pub command: Arc<dyn MoodEntryCommandPort>,
|
||||
pub query: Arc<dyn MoodEntryQueryPort>,
|
||||
pub media_storage: Arc<dyn MediaStoragePort>,
|
||||
pub events: Arc<dyn EventPublisherPort>,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(deps))]
|
||||
pub async fn execute(
|
||||
cmd: UpdateEntryCommand,
|
||||
caller_id: UserId,
|
||||
deps: &Deps,
|
||||
) -> Result<MoodEntry, ApplicationError> {
|
||||
let mut entry = deps
|
||||
.query
|
||||
.find_by_id(&cmd.entry_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("mood entry not found".into()))?;
|
||||
|
||||
verify_ownership(entry.user_id(), &caller_id)?;
|
||||
|
||||
let old_photos = entry.photos().to_vec();
|
||||
let old_memos = entry.voice_memos().to_vec();
|
||||
|
||||
entry.update_mood(cmd.mood);
|
||||
entry.update_logged_at(cmd.logged_at);
|
||||
entry.set_content(cmd.content);
|
||||
entry.set_activities(cmd.activities);
|
||||
entry.set_photos(cmd.photos);
|
||||
entry.set_voice_memos(cmd.voice_memos);
|
||||
|
||||
for photo_id in &old_photos {
|
||||
if !entry.photos().contains(photo_id)
|
||||
&& let Err(e) = deps.media_storage.delete_photo(photo_id).await
|
||||
{
|
||||
tracing::warn!(%e, "failed to delete removed photo blob");
|
||||
}
|
||||
}
|
||||
for memo_id in &old_memos {
|
||||
if !entry.voice_memos().contains(memo_id)
|
||||
&& let Err(e) = deps.media_storage.delete_voice_memo(memo_id).await
|
||||
{
|
||||
tracing::warn!(%e, "failed to delete removed voice memo blob");
|
||||
}
|
||||
}
|
||||
|
||||
deps.command.save(&entry).await?;
|
||||
|
||||
let event = DomainEvent::EntryUpdated {
|
||||
entry_id: entry.id().clone(),
|
||||
user_id: entry.user_id().clone(),
|
||||
};
|
||||
deps.events.publish(EventEnvelope::wrap(event)).await?;
|
||||
|
||||
Ok(entry)
|
||||
}
|
||||
Reference in New Issue
Block a user