Files
movies-diary/crates/presentation/src/handlers/goals.rs
Gabriel Kaszewski 6a9b4e5c00 refactor: LOW+MEDIUM cleanups — CQRS DiaryQuery/GoalCommand+GoalQuery,
test-helpers out of production, decompose get_user_profile_html,
dedup secure_flag, remove vestigial social_query, drop
PersistedImportSession, LoginCommand rename, DeleteAccountDeps,
PersonId macro, ReviewSortBy rename, TUI Command for fs::read,
generic PaginatedResponse<T>, noop ports for production fallbacks
2026-07-10 05:29:02 +02:00

234 lines
7.4 KiB
Rust

use axum::{
Json,
extract::{Path, State},
http::StatusCode,
};
use uuid::Uuid;
use crate::{errors::ApiError, extractors::AuthenticatedUser, state::AppState};
use api_types::{
CreateGoalRequest, GoalDto, GoalsResponse, UpdateGoalRequest, UpdateUserSettingsRequest,
UserSettingsDto,
};
use application::goals::deps::{GoalCommandDeps, GoalQueryDeps};
// ── Shared mapper ────────────────────────────────────────────────────────────
pub fn goal_with_progress_to_dto(g: &domain::models::GoalWithProgress) -> GoalDto {
GoalDto {
year: g.goal.year(),
target_count: g.goal.target_count(),
current_count: g.current_count,
percentage: g.percentage(),
is_complete: g.is_complete(),
goal_type: g.goal.goal_type().clone(),
}
}
// ── Goals API ────────────────────────────────────────────────────────────────
#[utoipa::path(
get, path = "/api/v1/goals",
responses(
(status = 200, body = GoalsResponse),
(status = 401, description = "Unauthorized"),
),
security(("bearer_auth" = []))
)]
pub async fn list_goals(
State(state): State<AppState>,
user: AuthenticatedUser,
) -> Result<Json<GoalsResponse>, ApiError> {
let deps = GoalQueryDeps {
goal_query: state.app_ctx.repos.goal_query.clone(),
stats: state.app_ctx.repos.stats.clone(),
};
let goals = application::goals::list::execute(
&deps,
application::goals::queries::ListGoalsQuery {
user_id: user.0.value(),
},
)
.await?;
Ok(Json(GoalsResponse {
goals: goals.iter().map(goal_with_progress_to_dto).collect(),
}))
}
#[utoipa::path(
post, path = "/api/v1/goals",
request_body = CreateGoalRequest,
responses(
(status = 200, body = GoalDto),
(status = 401, description = "Unauthorized"),
),
security(("bearer_auth" = []))
)]
pub async fn create_goal(
State(state): State<AppState>,
user: AuthenticatedUser,
Json(req): Json<CreateGoalRequest>,
) -> Result<Json<GoalDto>, ApiError> {
let deps = GoalCommandDeps {
goal_command: state.app_ctx.repos.goal_command.clone(),
goal_query: state.app_ctx.repos.goal_query.clone(),
stats: state.app_ctx.repos.stats.clone(),
event_publisher: state.app_ctx.services.event_publisher.clone(),
};
let g = application::goals::create::execute(
&deps,
application::goals::commands::CreateGoalCommand {
user_id: user.0.value(),
year: req.year,
target_count: req.target_count,
},
)
.await?;
Ok(Json(goal_with_progress_to_dto(&g)))
}
#[utoipa::path(
put, path = "/api/v1/goals/{year}",
request_body = UpdateGoalRequest,
responses(
(status = 200, body = GoalDto),
(status = 401, description = "Unauthorized"),
(status = 404, description = "Goal not found"),
),
security(("bearer_auth" = []))
)]
pub async fn update_goal(
State(state): State<AppState>,
user: AuthenticatedUser,
Path(year): Path<u16>,
Json(req): Json<UpdateGoalRequest>,
) -> Result<Json<GoalDto>, ApiError> {
let deps = GoalCommandDeps {
goal_command: state.app_ctx.repos.goal_command.clone(),
goal_query: state.app_ctx.repos.goal_query.clone(),
stats: state.app_ctx.repos.stats.clone(),
event_publisher: state.app_ctx.services.event_publisher.clone(),
};
let g = application::goals::update::execute(
&deps,
application::goals::commands::UpdateGoalCommand {
user_id: user.0.value(),
year,
target_count: req.target_count,
},
)
.await?;
Ok(Json(goal_with_progress_to_dto(&g)))
}
#[utoipa::path(
delete, path = "/api/v1/goals/{year}",
responses(
(status = 204, description = "Goal deleted"),
(status = 401, description = "Unauthorized"),
(status = 404, description = "Goal not found"),
),
security(("bearer_auth" = []))
)]
pub async fn delete_goal(
State(state): State<AppState>,
user: AuthenticatedUser,
Path(year): Path<u16>,
) -> Result<StatusCode, ApiError> {
let deps = GoalCommandDeps {
goal_command: state.app_ctx.repos.goal_command.clone(),
goal_query: state.app_ctx.repos.goal_query.clone(),
stats: state.app_ctx.repos.stats.clone(),
event_publisher: state.app_ctx.services.event_publisher.clone(),
};
application::goals::delete::execute(
&deps,
application::goals::commands::DeleteGoalCommand {
user_id: user.0.value(),
year,
},
)
.await?;
Ok(StatusCode::NO_CONTENT)
}
#[utoipa::path(
get, path = "/api/v1/users/{id}/goals",
responses(
(status = 200, body = GoalsResponse),
(status = 401, description = "Unauthorized"),
),
security(("bearer_auth" = []))
)]
pub async fn get_user_goals(
State(state): State<AppState>,
AuthenticatedUser(_viewer): AuthenticatedUser,
Path(user_id): Path<Uuid>,
) -> Result<Json<GoalsResponse>, ApiError> {
let deps = GoalQueryDeps {
goal_query: state.app_ctx.repos.goal_query.clone(),
stats: state.app_ctx.repos.stats.clone(),
};
let goals = application::goals::list::execute(
&deps,
application::goals::queries::ListGoalsQuery { user_id },
)
.await?;
Ok(Json(GoalsResponse {
goals: goals.iter().map(goal_with_progress_to_dto).collect(),
}))
}
// ── User Settings ────────────────────────────────────────────────────────────
#[utoipa::path(
get, path = "/api/v1/settings",
responses(
(status = 200, body = UserSettingsDto),
(status = 401, description = "Unauthorized"),
),
security(("bearer_auth" = []))
)]
pub async fn get_settings(
State(state): State<AppState>,
user: AuthenticatedUser,
) -> Result<Json<UserSettingsDto>, ApiError> {
let settings = application::users::get_settings::execute(
state.app_ctx.repos.user_settings.clone(),
user.0.value(),
)
.await?;
Ok(Json(UserSettingsDto {
federate_goals: settings.federate_goals(),
federate_reviews: settings.federate_reviews(),
federate_watchlist: settings.federate_watchlist(),
}))
}
#[utoipa::path(
put, path = "/api/v1/settings",
request_body = UpdateUserSettingsRequest,
responses(
(status = 204, description = "Settings updated"),
(status = 401, description = "Unauthorized"),
),
security(("bearer_auth" = []))
)]
pub async fn update_settings(
State(state): State<AppState>,
user: AuthenticatedUser,
Json(req): Json<UpdateUserSettingsRequest>,
) -> Result<StatusCode, ApiError> {
application::users::update_settings::execute(
state.app_ctx.repos.user_settings.clone(),
application::users::update_settings::UpdateUserSettingsCommand {
user_id: user.0.value(),
federate_goals: req.federate_goals,
federate_reviews: req.federate_reviews,
federate_watchlist: req.federate_watchlist,
},
)
.await?;
Ok(StatusCode::NO_CONTENT)
}