brewlog/src/application/routes/api/checkin.rs
Jon Seager 58b551afac
refactor(routes): split account, checkin, and webauthn into api/app
Split each mixed file along the api/page boundary:
- account.rs: page handler + view types to app/, API handlers to api/
- checkin.rs: checkin_page to app/, submit_checkin to api/
- webauthn.rs: register_page + cli_callback_page to app/
2026-02-05 19:04:52 +00:00

97 lines
2.9 KiB
Rust

use axum::Json;
use axum::extract::State;
use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Redirect, Response};
use serde::Deserialize;
use crate::application::auth::AuthenticatedUser;
use crate::application::errors::{ApiError, AppError};
use crate::application::routes::support::{FlexiblePayload, PayloadSource, is_datastar_request};
use crate::application::server::AppState;
use crate::domain::cafes::NewCafe;
use crate::domain::cups::NewCup;
use crate::domain::ids::{CafeId, RoastId};
#[derive(Debug, Deserialize)]
pub(crate) struct CheckInSubmission {
#[serde(default)]
cafe_id: Option<String>,
#[serde(default)]
cafe_name: Option<String>,
#[serde(default)]
cafe_city: Option<String>,
#[serde(default)]
cafe_country: Option<String>,
#[serde(default)]
cafe_lat: f64,
#[serde(default)]
cafe_lng: f64,
#[serde(default)]
cafe_website: Option<String>,
roast_id: String,
}
#[tracing::instrument(skip(state, _auth_user, headers, payload))]
pub(crate) async fn submit_checkin(
State(state): State<AppState>,
_auth_user: AuthenticatedUser,
headers: HeaderMap,
payload: FlexiblePayload<CheckInSubmission>,
) -> Result<Response, ApiError> {
let (submission, source) = payload.into_parts();
let roast_id: i64 = submission
.roast_id
.parse()
.map_err(|_| AppError::validation("invalid roast ID"))?;
// Use existing cafe or create a new one
let cafe_id = if let Some(id) = submission.cafe_id.as_deref().filter(|s| !s.is_empty()) {
let parsed: i64 = id
.parse()
.map_err(|_| AppError::validation("invalid cafe ID"))?;
CafeId::from(parsed)
} else {
let name = submission
.cafe_name
.as_deref()
.filter(|s| !s.is_empty())
.ok_or_else(|| AppError::validation("cafe name is required"))?;
let new_cafe = NewCafe {
name: name.to_string(),
city: submission.cafe_city.unwrap_or_default(),
country: submission.cafe_country.unwrap_or_default(),
latitude: submission.cafe_lat,
longitude: submission.cafe_lng,
website: submission.cafe_website.filter(|s| !s.is_empty()),
}
.normalize();
let cafe = state
.cafe_repo
.insert(new_cafe)
.await
.map_err(AppError::from)?;
cafe.id
};
let new_cup = NewCup {
roast_id: RoastId::from(roast_id),
cafe_id,
};
let cup = state
.cup_repo
.insert(new_cup)
.await
.map_err(AppError::from)?;
if is_datastar_request(&headers) {
crate::application::routes::support::render_signals_json(&[]).map_err(ApiError::from)
} else if matches!(source, PayloadSource::Form) {
Ok(Redirect::to("/").into_response())
} else {
Ok((StatusCode::CREATED, Json(cup)).into_response())
}
}