26
crates/application/src/metric/commands.rs
Normal file
26
crates/application/src/metric/commands.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use domain::entry::Date;
|
||||
use domain::metric::{MetricKind, MetricValue, Source};
|
||||
use domain::user::UserId;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MetricChange {
|
||||
Stated(MetricValue),
|
||||
Cleared(MetricKind),
|
||||
}
|
||||
|
||||
impl MetricChange {
|
||||
pub fn kind(&self) -> MetricKind {
|
||||
match self {
|
||||
Self::Stated(value) => value.kind(),
|
||||
Self::Cleared(kind) => *kind,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SetDailyMetricsCommand {
|
||||
pub user_id: UserId,
|
||||
pub date: Date,
|
||||
pub changes: Vec<MetricChange>,
|
||||
pub source: Source,
|
||||
}
|
||||
2
crates/application/src/metric/mod.rs
Normal file
2
crates/application/src/metric/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod commands;
|
||||
pub mod use_cases;
|
||||
@@ -0,0 +1,21 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::entry::DateSpan;
|
||||
use domain::metric::DailyMetric;
|
||||
use domain::ports::DailyMetricQueryPort;
|
||||
use domain::user::UserId;
|
||||
|
||||
use crate::errors::ApplicationError;
|
||||
|
||||
pub struct Deps {
|
||||
pub metrics: Arc<dyn DailyMetricQueryPort>,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(deps))]
|
||||
pub async fn execute(
|
||||
user_id: UserId,
|
||||
span: DateSpan,
|
||||
deps: &Deps,
|
||||
) -> Result<Vec<DailyMetric>, ApplicationError> {
|
||||
Ok(deps.metrics.find_by_span(&user_id, &span).await?)
|
||||
}
|
||||
2
crates/application/src/metric/use_cases/mod.rs
Normal file
2
crates/application/src/metric/use_cases/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod list_daily_metrics;
|
||||
pub mod set_daily_metrics;
|
||||
101
crates/application/src/metric/use_cases/set_daily_metrics.rs
Normal file
101
crates/application/src/metric/use_cases/set_daily_metrics.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::metric::{DailyMetric, MetricKind};
|
||||
use domain::ports::{DailyMetricCommandPort, UserQueryPort};
|
||||
|
||||
use crate::day::{timezone_for, today_in};
|
||||
use crate::errors::ApplicationError;
|
||||
use crate::metric::commands::{MetricChange, SetDailyMetricsCommand};
|
||||
|
||||
pub struct Deps {
|
||||
pub metrics: Arc<dyn DailyMetricCommandPort>,
|
||||
pub users: Arc<dyn UserQueryPort>,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(deps))]
|
||||
pub async fn execute(command: SetDailyMetricsCommand, deps: &Deps) -> Result<(), ApplicationError> {
|
||||
let timezone = timezone_for(&command.user_id, &deps.users).await?;
|
||||
|
||||
if command.date > today_in(&timezone) {
|
||||
return Err(ApplicationError::Validation(format!(
|
||||
"cannot record a metric for {}, which has not happened yet",
|
||||
command.date
|
||||
)));
|
||||
}
|
||||
|
||||
reject_a_kind_named_twice(&command.changes)?;
|
||||
reject_clearing_by_anyone_but_the_user(&command)?;
|
||||
|
||||
let cleared: Vec<MetricKind> = command
|
||||
.changes
|
||||
.iter()
|
||||
.filter_map(|change| match change {
|
||||
MetricChange::Cleared(kind) => Some(*kind),
|
||||
MetricChange::Stated(_) => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let stated: Vec<DailyMetric> = command
|
||||
.changes
|
||||
.iter()
|
||||
.filter_map(|change| match change {
|
||||
MetricChange::Stated(value) => Some(DailyMetric::new(
|
||||
command.user_id.clone(),
|
||||
command.date,
|
||||
*value,
|
||||
command.source.clone(),
|
||||
)),
|
||||
MetricChange::Cleared(_) => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !cleared.is_empty() {
|
||||
deps.metrics
|
||||
.delete(&command.user_id, &command.date, &cleared)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !stated.is_empty() {
|
||||
deps.metrics.save(&stated).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reject_clearing_by_anyone_but_the_user(
|
||||
command: &SetDailyMetricsCommand,
|
||||
) -> Result<(), ApplicationError> {
|
||||
if command.source.is_manual() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let clears = command
|
||||
.changes
|
||||
.iter()
|
||||
.any(|change| matches!(change, MetricChange::Cleared(_)));
|
||||
|
||||
if clears {
|
||||
return Err(ApplicationError::Validation(
|
||||
"a provider cannot clear a reading: only the user can".into(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reject_a_kind_named_twice(changes: &[MetricChange]) -> Result<(), ApplicationError> {
|
||||
for (position, change) in changes.iter().enumerate() {
|
||||
let named_again = changes[position + 1..]
|
||||
.iter()
|
||||
.any(|later| later.kind() == change.kind());
|
||||
|
||||
if named_again {
|
||||
return Err(ApplicationError::Validation(format!(
|
||||
"{} appears more than once in the same request",
|
||||
change.kind().name()
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user