32
crates/application/src/reminder/use_cases/create_reminder.rs
Normal file
32
crates/application/src/reminder/use_cases/create_reminder.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::events::{DomainEvent, EventEnvelope};
|
||||
use domain::ports::{EventPublisherPort, ReminderCommandPort};
|
||||
use domain::reminder::Reminder;
|
||||
|
||||
use crate::errors::ApplicationError;
|
||||
|
||||
use super::super::commands::CreateReminderCommand;
|
||||
|
||||
pub struct Deps {
|
||||
pub reminders: Arc<dyn ReminderCommandPort>,
|
||||
pub events: Arc<dyn EventPublisherPort>,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(deps))]
|
||||
pub async fn execute(
|
||||
cmd: CreateReminderCommand,
|
||||
deps: &Deps,
|
||||
) -> Result<Reminder, ApplicationError> {
|
||||
let reminder = Reminder::new(cmd.user_id, cmd.schedule);
|
||||
|
||||
deps.reminders.save(&reminder).await?;
|
||||
|
||||
let event = DomainEvent::ReminderCreated {
|
||||
reminder_id: reminder.id().clone(),
|
||||
user_id: reminder.user_id().clone(),
|
||||
};
|
||||
deps.events.publish(EventEnvelope::wrap(event)).await?;
|
||||
|
||||
Ok(reminder)
|
||||
}
|
||||
41
crates/application/src/reminder/use_cases/delete_reminder.rs
Normal file
41
crates/application/src/reminder/use_cases/delete_reminder.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::errors::DomainError;
|
||||
use domain::events::{DomainEvent, EventEnvelope};
|
||||
use domain::ports::{EventPublisherPort, ReminderCommandPort, ReminderQueryPort};
|
||||
use domain::reminder::ReminderId;
|
||||
use domain::user::UserId;
|
||||
|
||||
use crate::authorization::verify_ownership;
|
||||
use crate::errors::ApplicationError;
|
||||
|
||||
pub struct Deps {
|
||||
pub command: Arc<dyn ReminderCommandPort>,
|
||||
pub query: Arc<dyn ReminderQueryPort>,
|
||||
pub events: Arc<dyn EventPublisherPort>,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(deps))]
|
||||
pub async fn execute(
|
||||
reminder_id: ReminderId,
|
||||
caller_id: UserId,
|
||||
deps: &Deps,
|
||||
) -> Result<(), ApplicationError> {
|
||||
let reminder = deps
|
||||
.query
|
||||
.find_by_id(&reminder_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("reminder not found".into()))?;
|
||||
|
||||
verify_ownership(reminder.user_id(), &caller_id)?;
|
||||
|
||||
deps.command.delete(&reminder_id).await?;
|
||||
|
||||
let event = DomainEvent::ReminderDeleted {
|
||||
reminder_id,
|
||||
user_id: reminder.user_id().clone(),
|
||||
};
|
||||
deps.events.publish(EventEnvelope::wrap(event)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
29
crates/application/src/reminder/use_cases/get_reminder.rs
Normal file
29
crates/application/src/reminder/use_cases/get_reminder.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::errors::DomainError;
|
||||
use domain::ports::ReminderQueryPort;
|
||||
use domain::reminder::{Reminder, ReminderId};
|
||||
use domain::user::UserId;
|
||||
|
||||
use crate::authorization::verify_ownership;
|
||||
use crate::errors::ApplicationError;
|
||||
|
||||
pub struct Deps {
|
||||
pub query: Arc<dyn ReminderQueryPort>,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(deps))]
|
||||
pub async fn execute(
|
||||
reminder_id: ReminderId,
|
||||
caller_id: UserId,
|
||||
deps: &Deps,
|
||||
) -> Result<Reminder, ApplicationError> {
|
||||
let reminder = deps
|
||||
.query
|
||||
.find_by_id(&reminder_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("reminder not found".into()))?;
|
||||
|
||||
verify_ownership(reminder.user_id(), &caller_id)?;
|
||||
Ok(reminder)
|
||||
}
|
||||
16
crates/application/src/reminder/use_cases/list_reminders.rs
Normal file
16
crates/application/src/reminder/use_cases/list_reminders.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::ports::ReminderQueryPort;
|
||||
use domain::reminder::Reminder;
|
||||
use domain::user::UserId;
|
||||
|
||||
use crate::errors::ApplicationError;
|
||||
|
||||
pub struct Deps {
|
||||
pub query: Arc<dyn ReminderQueryPort>,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(deps))]
|
||||
pub async fn execute(user_id: UserId, deps: &Deps) -> Result<Vec<Reminder>, ApplicationError> {
|
||||
Ok(deps.query.find_by_user(&user_id).await?)
|
||||
}
|
||||
6
crates/application/src/reminder/use_cases/mod.rs
Normal file
6
crates/application/src/reminder/use_cases/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub mod create_reminder;
|
||||
pub mod delete_reminder;
|
||||
pub mod get_reminder;
|
||||
pub mod list_reminders;
|
||||
pub mod process_due_reminders;
|
||||
pub mod update_reminder;
|
||||
@@ -0,0 +1,86 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::{Datelike, NaiveTime, Utc};
|
||||
|
||||
use domain::ports::{ReminderQueryPort, ReminderSenderPort, UserQueryPort};
|
||||
use domain::reminder::Reminder;
|
||||
|
||||
use crate::errors::ApplicationError;
|
||||
|
||||
pub struct Deps {
|
||||
pub reminder_query: Arc<dyn ReminderQueryPort>,
|
||||
pub user_query: Arc<dyn UserQueryPort>,
|
||||
pub sender: Arc<dyn ReminderSenderPort>,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(deps))]
|
||||
pub async fn execute(deps: &Deps) -> Result<u64, ApplicationError> {
|
||||
let reminders = deps.reminder_query.find_all_enabled().await?;
|
||||
tracing::debug!(
|
||||
reminder_count = reminders.len(),
|
||||
"checking enabled reminders"
|
||||
);
|
||||
|
||||
let mut sent_count = 0u64;
|
||||
|
||||
for reminder in &reminders {
|
||||
if should_send(reminder, &deps.user_query).await? {
|
||||
tracing::info!(user_id = %reminder.user_id(), "sending reminder");
|
||||
deps.sender.send_reminder(reminder.user_id()).await?;
|
||||
sent_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!(sent_count, "reminder processing completed");
|
||||
Ok(sent_count)
|
||||
}
|
||||
|
||||
async fn should_send(
|
||||
reminder: &Reminder,
|
||||
user_query: &Arc<dyn UserQueryPort>,
|
||||
) -> Result<bool, ApplicationError> {
|
||||
let user = user_query.find_by_id(reminder.user_id()).await?;
|
||||
let user = match user {
|
||||
Some(u) => u,
|
||||
None => {
|
||||
tracing::warn!(user_id = %reminder.user_id(), "reminder references nonexistent user");
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
|
||||
let now = match user.timezone() {
|
||||
Some(tz) => {
|
||||
let tz: chrono_tz::Tz = match tz.value().parse() {
|
||||
Ok(tz) => tz,
|
||||
Err(_) => {
|
||||
tracing::warn!(
|
||||
user_id = %reminder.user_id(),
|
||||
timezone = tz.value(),
|
||||
"invalid timezone for user"
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
Utc::now().with_timezone(&tz)
|
||||
}
|
||||
None => {
|
||||
tracing::debug!(user_id = %reminder.user_id(), "user has no timezone set, skipping reminder");
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
|
||||
let weekday = now.weekday();
|
||||
let scheduled_time = match reminder.schedule().time_for(weekday) {
|
||||
Some(time) => time,
|
||||
None => return Ok(false),
|
||||
};
|
||||
|
||||
let current_time = now.time();
|
||||
Ok(is_within_window(current_time, scheduled_time))
|
||||
}
|
||||
|
||||
fn is_within_window(current: NaiveTime, scheduled: NaiveTime) -> bool {
|
||||
let diff = current.signed_duration_since(scheduled);
|
||||
let minutes = diff.num_minutes();
|
||||
(0..5).contains(&minutes)
|
||||
}
|
||||
55
crates/application/src/reminder/use_cases/update_reminder.rs
Normal file
55
crates/application/src/reminder/use_cases/update_reminder.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::errors::DomainError;
|
||||
use domain::events::{DomainEvent, EventEnvelope};
|
||||
use domain::ports::{EventPublisherPort, ReminderCommandPort, ReminderQueryPort};
|
||||
use domain::reminder::Reminder;
|
||||
use domain::user::UserId;
|
||||
|
||||
use crate::authorization::verify_ownership;
|
||||
use crate::errors::ApplicationError;
|
||||
|
||||
use super::super::commands::UpdateReminderCommand;
|
||||
|
||||
pub struct Deps {
|
||||
pub command: Arc<dyn ReminderCommandPort>,
|
||||
pub query: Arc<dyn ReminderQueryPort>,
|
||||
pub events: Arc<dyn EventPublisherPort>,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(deps))]
|
||||
pub async fn execute(
|
||||
cmd: UpdateReminderCommand,
|
||||
caller_id: UserId,
|
||||
deps: &Deps,
|
||||
) -> Result<Reminder, ApplicationError> {
|
||||
let mut reminder = deps
|
||||
.query
|
||||
.find_by_id(&cmd.reminder_id)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("reminder not found".into()))?;
|
||||
|
||||
verify_ownership(reminder.user_id(), &caller_id)?;
|
||||
|
||||
if let Some(schedule) = cmd.schedule {
|
||||
reminder.update_schedule(schedule);
|
||||
}
|
||||
|
||||
if let Some(enabled) = cmd.enabled {
|
||||
if enabled {
|
||||
reminder.enable();
|
||||
} else {
|
||||
reminder.disable();
|
||||
}
|
||||
}
|
||||
|
||||
deps.command.save(&reminder).await?;
|
||||
|
||||
let event = DomainEvent::ReminderUpdated {
|
||||
reminder_id: reminder.id().clone(),
|
||||
user_id: reminder.user_id().clone(),
|
||||
};
|
||||
deps.events.publish(EventEnvelope::wrap(event)).await?;
|
||||
|
||||
Ok(reminder)
|
||||
}
|
||||
Reference in New Issue
Block a user