Add `created_at: Option<DateTime<Utc>>` through all layers so CLI users can backdate entities at creation/update time. When omitted, falls back to `Utc::now()`. - Domain: add field to all New*/Update* structs with serde(default) - Domain: timeline events use entity created_at instead of Utc::now() - Repos: unify INSERT to explicit Rust-side created_at with unwrap_or_else - Repos: add created_at to UPDATE dynamic query builders - Routes: add field to submission structs and has_changes guards - Clients: pass created_at through manual JSON client methods - CLI: add --created-at flag with parse_created_at helper (RFC 3339 or YYYY-MM-DD)
139 lines
3.7 KiB
Rust
139 lines
3.7 KiB
Rust
use chrono::{DateTime, Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use super::ids::{CafeId, CupId, RoastId};
|
|
use super::listing::{SortDirection, SortKey};
|
|
use crate::domain::timeline::{NewTimelineEvent, TimelineEventDetail};
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Cup {
|
|
pub id: CupId,
|
|
pub roast_id: RoastId,
|
|
pub cafe_id: CafeId,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CupWithDetails {
|
|
#[serde(flatten)]
|
|
pub cup: Cup,
|
|
pub roast_name: String,
|
|
pub roaster_name: String,
|
|
pub roast_slug: String,
|
|
pub roaster_slug: String,
|
|
pub cafe_name: String,
|
|
pub cafe_slug: String,
|
|
pub cafe_city: String,
|
|
}
|
|
|
|
impl CupWithDetails {
|
|
pub fn to_timeline_event(&self) -> NewTimelineEvent {
|
|
NewTimelineEvent {
|
|
entity_type: "cup".to_string(),
|
|
entity_id: self.cup.id.into_inner(),
|
|
action: "added".to_string(),
|
|
occurred_at: self.cup.created_at,
|
|
title: self.roast_name.clone(),
|
|
details: vec![
|
|
TimelineEventDetail {
|
|
label: "Coffee".to_string(),
|
|
value: self.roast_name.clone(),
|
|
},
|
|
TimelineEventDetail {
|
|
label: "Roaster".to_string(),
|
|
value: self.roaster_name.clone(),
|
|
},
|
|
TimelineEventDetail {
|
|
label: "Cafe".to_string(),
|
|
value: self.cafe_name.clone(),
|
|
},
|
|
],
|
|
tasting_notes: vec![],
|
|
slug: Some(self.roast_slug.clone()),
|
|
roaster_slug: Some(self.roaster_slug.clone()),
|
|
brew_data: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct NewCup {
|
|
pub roast_id: RoastId,
|
|
pub cafe_id: CafeId,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub created_at: Option<DateTime<Utc>>,
|
|
}
|
|
|
|
/// Filter criteria for cup queries.
|
|
#[derive(Debug, Default, Clone)]
|
|
pub struct CupFilter {
|
|
pub cafe_id: Option<CafeId>,
|
|
pub roast_id: Option<RoastId>,
|
|
}
|
|
|
|
impl CupFilter {
|
|
/// No filter - returns all cups.
|
|
pub fn all() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Filter for cups at a specific cafe.
|
|
pub fn for_cafe(cafe_id: CafeId) -> Self {
|
|
Self {
|
|
cafe_id: Some(cafe_id),
|
|
..Self::default()
|
|
}
|
|
}
|
|
|
|
/// Filter for cups of a specific roast.
|
|
pub fn for_roast(roast_id: RoastId) -> Self {
|
|
Self {
|
|
roast_id: Some(roast_id),
|
|
..Self::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
|
pub enum CupSortKey {
|
|
CreatedAt,
|
|
CafeName,
|
|
CafeCity,
|
|
RoastName,
|
|
RoasterName,
|
|
}
|
|
|
|
impl SortKey for CupSortKey {
|
|
fn default() -> Self {
|
|
CupSortKey::CreatedAt
|
|
}
|
|
|
|
fn from_query(value: &str) -> Option<Self> {
|
|
match value {
|
|
"created-at" => Some(CupSortKey::CreatedAt),
|
|
"cafe" => Some(CupSortKey::CafeName),
|
|
"city" => Some(CupSortKey::CafeCity),
|
|
"roast" => Some(CupSortKey::RoastName),
|
|
"roaster" => Some(CupSortKey::RoasterName),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn query_value(self) -> &'static str {
|
|
match self {
|
|
CupSortKey::CreatedAt => "created-at",
|
|
CupSortKey::CafeName => "cafe",
|
|
CupSortKey::CafeCity => "city",
|
|
CupSortKey::RoastName => "roast",
|
|
CupSortKey::RoasterName => "roaster",
|
|
}
|
|
}
|
|
|
|
fn default_direction(self) -> SortDirection {
|
|
match self {
|
|
CupSortKey::CreatedAt => SortDirection::Desc,
|
|
_ => SortDirection::Asc,
|
|
}
|
|
}
|
|
}
|