init
Some checks failed
CI / ci (push) Failing after 1m48s

This commit is contained in:
2026-08-25 23:24:36 +02:00
commit 95739892de
466 changed files with 33918 additions and 0 deletions

View 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)
}

View 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(())
}

View 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)
}

View 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?)
}

View 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;

View File

@@ -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)
}

View 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)
}