refactor(cups): remove rating field

Drop the rating column from cups. This removes the field from the
domain model, repository, API routes (including the PUT/update
endpoint), CLI, and all related tests.
This commit is contained in:
Jon Seager 2026-02-05 17:02:42 +00:00
parent 33c27ed0d0
commit 6d12b71482
No known key found for this signature in database
13 changed files with 36 additions and 319 deletions

View file

@ -137,7 +137,7 @@ brewlog bag <cmd> Manage bags of coffee
brewlog gear <cmd> Manage brewing gear (grinders, brewers, filter papers)
brewlog brew <cmd> Manage brews (add, list, get, delete — no update)
brewlog cafe <cmd> Manage cafes
brewlog cup <cmd> Manage cups (cafe visits with ratings)
brewlog cup <cmd> Manage cups (cafe visits)
brewlog token <cmd> Manage API tokens (create, list, revoke)
brewlog backup Export all data to JSON on stdout (requires BREWLOG_TOKEN)
brewlog restore --file F Restore data from a JSON backup into an empty database (requires BREWLOG_TOKEN)

View file

@ -0,0 +1 @@
ALTER TABLE cups DROP COLUMN rating;

View file

@ -503,40 +503,33 @@ fi
# Monmouth Coffee (London) - 2 cups
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Red Brick Espresso") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Monmouth Coffee") | .id')" \
--rating 4
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Monmouth Coffee") | .id')"
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Kamwangi") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Monmouth Coffee") | .id')" \
--rating 5
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Monmouth Coffee") | .id')"
# Prufrock Coffee (London) - 3 cups
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Kochere") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Prufrock Coffee") | .id')" \
--rating 5
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Prufrock Coffee") | .id')"
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="La Laja") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Prufrock Coffee") | .id')" \
--rating 4
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Prufrock Coffee") | .id')"
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Ben Saïd Natural") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Prufrock Coffee") | .id')" \
--rating 5
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Prufrock Coffee") | .id')"
# Hola Coffee (Madrid) - 2 cups
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Cerro Azul") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Hola Coffee") | .id')" \
--rating 4
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Hola Coffee") | .id')"
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Guji Highland") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Hola Coffee") | .id')" \
--rating 3
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Hola Coffee") | .id')"
# Toma Café (Madrid) - 1 cup
./target/debug/brewlog cup add \
@ -546,68 +539,56 @@ fi
# The Barn (Berlin) - 3 cups
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Gatomboya") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="The Barn") | .id')" \
--rating 4
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="The Barn") | .id')"
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Los Pirineos") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="The Barn") | .id')" \
--rating 4
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="The Barn") | .id')"
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Halo Beriti") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="The Barn") | .id')" \
--rating 5
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="The Barn") | .id')"
# Companion Coffee (Berlin) - 2 cups
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="El Paraiso 92 Anaerobic") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Companion Coffee") | .id')" \
--rating 5
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Companion Coffee") | .id')"
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Oreti SL28") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Companion Coffee") | .id')" \
--rating 4
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Companion Coffee") | .id')"
# Man Versus Machine (Munich) - 2 cups
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Finca Tamana Washed") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Man Versus Machine") | .id')" \
--rating 4
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Man Versus Machine") | .id')"
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="La Esmeralda Geisha") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Man Versus Machine") | .id')" \
--rating 5
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Man Versus Machine") | .id')"
# Vits der Kaffee (Munich) - 1 cup
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="San Fermin") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Vits der Kaffee") | .id')" \
--rating 3
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Vits der Kaffee") | .id')"
# Full Court Press (Bristol) - 2 cups
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Suke Quto") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Full Court Press") | .id')" \
--rating 5
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Full Court Press") | .id')"
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Death to Decaf") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Full Court Press") | .id')" \
--rating 3
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Full Court Press") | .id')"
# Small Street Espresso (Bristol) - 2 cups
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Simbi") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Small Street Espresso") | .id')" \
--rating 4
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Small Street Espresso") | .id')"
./target/debug/brewlog cup add \
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="La Chumeca") | .id')" \
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Small Street Espresso") | .id')" \
--rating 4
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Small Street Espresso") | .id')"
# ============================================================================
# Bags - Various bags from different roasters with amounts ranging 100g-500g

View file

@ -56,8 +56,6 @@ pub(crate) struct CheckInSubmission {
#[serde(default)]
cafe_website: Option<String>,
roast_id: String,
#[serde(default)]
rating: i32,
}
#[tracing::instrument(skip(state, _auth_user, headers, payload))]
@ -105,16 +103,9 @@ pub(crate) async fn submit_checkin(
cafe.id
};
let rating = if (1..=5).contains(&submission.rating) {
Some(submission.rating)
} else {
None
};
let new_cup = NewCup {
roast_id: RoastId::from(roast_id),
cafe_id,
rating,
};
let cup = state

View file

@ -1,5 +1,5 @@
use axum::Json;
use axum::extract::{Path, Query, State};
use axum::extract::{Query, State};
use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Redirect, Response};
@ -12,7 +12,7 @@ use crate::application::routes::support::{
FlexiblePayload, ListQuery, PayloadSource, is_datastar_request,
};
use crate::application::server::AppState;
use crate::domain::cups::{Cup, CupFilter, CupSortKey, CupWithDetails, NewCup, UpdateCup};
use crate::domain::cups::{CupFilter, CupSortKey, CupWithDetails, NewCup};
use crate::domain::ids::CupId;
use crate::domain::listing::{ListRequest, SortDirection};
use crate::presentation::web::templates::CupListTemplate;
@ -54,12 +54,6 @@ pub(crate) async fn create_cup(
let (request, search) = query.into_request_and_search::<CupSortKey>();
let (new_cup, source) = payload.into_parts();
if let Some(rating) = new_cup.rating
&& !(1..=5).contains(&rating)
{
return Err(AppError::validation("rating must be between 1 and 5").into());
}
let cup = state
.cup_repo
.insert(new_cup)
@ -94,33 +88,6 @@ pub(crate) async fn list_cups(
define_enriched_get_handler!(get_cup, CupId, CupWithDetails, cup_repo, get_with_details);
#[tracing::instrument(skip(state, _auth_user))]
pub(crate) async fn update_cup(
State(state): State<AppState>,
_auth_user: AuthenticatedUser,
Path(id): Path<CupId>,
Json(payload): Json<UpdateCup>,
) -> Result<Json<Cup>, ApiError> {
let has_changes = payload.rating.is_some();
if !has_changes {
return Err(AppError::validation("no changes provided").into());
}
if let Some(rating) = payload.rating.as_ref()
&& !(1..=5).contains(rating)
{
return Err(AppError::validation("rating must be between 1 and 5").into());
}
let cup = state
.cup_repo
.update(id, payload)
.await
.map_err(AppError::from)?;
Ok(Json(cup))
}
define_delete_handler!(
delete_cup,
CupId,

View file

@ -91,12 +91,7 @@ pub fn app_router(state: AppState) -> axum::Router {
.route("/scan", post(scan::submit_scan))
.route("/check-in", post(checkin::submit_checkin))
.route("/cups", get(cups::list_cups).post(cups::create_cup))
.route(
"/cups/:id",
get(cups::get_cup)
.put(cups::update_cup)
.delete(cups::delete_cup),
)
.route("/cups/:id", get(cups::get_cup).delete(cups::delete_cup))
.route(
"/tokens",
post(tokens::create_token).get(tokens::list_tokens),

View file

@ -9,7 +9,6 @@ pub struct Cup {
pub id: CupId,
pub roast_id: RoastId,
pub cafe_id: CafeId,
pub rating: Option<i32>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@ -30,12 +29,6 @@ pub struct CupWithDetails {
pub struct NewCup {
pub roast_id: RoastId,
pub cafe_id: CafeId,
pub rating: Option<i32>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct UpdateCup {
pub rating: Option<i32>,
}
/// Filter criteria for cup queries.
@ -73,7 +66,6 @@ pub enum CupSortKey {
CreatedAt,
CafeName,
RoastName,
Rating,
}
impl SortKey for CupSortKey {
@ -86,7 +78,6 @@ impl SortKey for CupSortKey {
"created-at" => Some(CupSortKey::CreatedAt),
"cafe" => Some(CupSortKey::CafeName),
"roast" => Some(CupSortKey::RoastName),
"rating" => Some(CupSortKey::Rating),
_ => None,
}
}
@ -96,13 +87,12 @@ impl SortKey for CupSortKey {
CupSortKey::CreatedAt => "created-at",
CupSortKey::CafeName => "cafe",
CupSortKey::RoastName => "roast",
CupSortKey::Rating => "rating",
}
}
fn default_direction(self) -> SortDirection {
match self {
CupSortKey::CreatedAt | CupSortKey::Rating => SortDirection::Desc,
CupSortKey::CreatedAt => SortDirection::Desc,
_ => SortDirection::Asc,
}
}

View file

@ -5,7 +5,7 @@ use crate::domain::listing::{ListRequest, Page, SortDirection, SortKey};
use crate::domain::bags::{Bag, BagFilter, BagSortKey, BagWithRoast, NewBag, UpdateBag};
use crate::domain::brews::{Brew, BrewFilter, BrewSortKey, BrewWithDetails, NewBrew};
use crate::domain::cafes::{Cafe, CafeSortKey, NewCafe, UpdateCafe};
use crate::domain::cups::{Cup, CupFilter, CupSortKey, CupWithDetails, NewCup, UpdateCup};
use crate::domain::cups::{Cup, CupFilter, CupSortKey, CupWithDetails, NewCup};
use crate::domain::gear::{Gear, GearFilter, GearSortKey, NewGear, UpdateGear};
use crate::domain::ids::{
BagId, BrewId, CafeId, CupId, GearId, PasskeyCredentialId, RegistrationTokenId, RoastId,
@ -222,7 +222,6 @@ pub trait CupRepository: Send + Sync {
request: &ListRequest<CupSortKey>,
search: Option<&str>,
) -> Result<Page<CupWithDetails>, RepositoryError>;
async fn update(&self, id: CupId, changes: UpdateCup) -> Result<Cup, RepositoryError>;
async fn delete(&self, id: CupId) -> Result<(), RepositoryError>;
}

View file

@ -1,7 +1,7 @@
use anyhow::{Context, Result};
use reqwest::StatusCode;
use crate::domain::cups::{Cup, CupWithDetails, NewCup, UpdateCup};
use crate::domain::cups::{Cup, CupWithDetails, NewCup};
use crate::domain::ids::CupId;
use super::BrewlogClient;
@ -52,19 +52,6 @@ impl<'a> CupsClient<'a> {
self.inner.handle_response(response).await
}
pub async fn update(&self, id: CupId, payload: &UpdateCup) -> Result<Cup> {
let url = self.inner.endpoint(&format!("api/v1/cups/{id}"))?;
let response = self
.inner
.request(reqwest::Method::PUT, url)
.json(payload)
.send()
.await
.context("failed to issue update cup request")?;
self.inner.handle_response(response).await
}
pub async fn delete(&self, id: CupId) -> Result<()> {
let url = self.inner.endpoint(&format!("api/v1/cups/{id}"))?;
let response = self

View file

@ -1,10 +1,9 @@
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use sqlx::{QueryBuilder, query, query_as};
use sqlx::{query, query_as};
use super::macros::push_update_field;
use crate::domain::RepositoryError;
use crate::domain::cups::{Cup, CupFilter, CupSortKey, CupWithDetails, NewCup, UpdateCup};
use crate::domain::cups::{Cup, CupFilter, CupSortKey, CupWithDetails, NewCup};
use crate::domain::ids::{CafeId, CupId, RoastId};
use crate::domain::listing::{ListRequest, Page, SortDirection};
use crate::domain::repositories::CupRepository;
@ -13,7 +12,7 @@ use crate::infrastructure::database::DatabasePool;
const BASE_SELECT: &str = r"
SELECT
c.id, c.roast_id, c.cafe_id, c.rating,
c.id, c.roast_id, c.cafe_id,
c.created_at, c.updated_at,
r.name as roast_name, r.slug as roast_slug,
rr.name as roaster_name, rr.slug as roaster_slug,
@ -48,7 +47,6 @@ impl SqlCupRepository {
CupSortKey::RoastName => {
format!("LOWER(r.name) {dir_sql}, c.created_at DESC")
}
CupSortKey::Rating => format!("c.rating {dir_sql}, c.created_at DESC"),
}
}
@ -57,7 +55,6 @@ impl SqlCupRepository {
id: CupId::new(record.id),
roast_id: RoastId::new(record.roast_id),
cafe_id: CafeId::new(record.cafe_id),
rating: record.rating,
created_at: record.created_at,
updated_at: record.updated_at,
}
@ -69,7 +66,6 @@ impl SqlCupRepository {
id: CupId::new(record.id),
roast_id: RoastId::new(record.roast_id),
cafe_id: CafeId::new(record.cafe_id),
rating: record.rating,
created_at: record.created_at,
updated_at: record.updated_at,
},
@ -101,7 +97,7 @@ impl SqlCupRepository {
}
fn details_for_cup(cup_with_details: &CupWithDetails) -> Result<String, RepositoryError> {
let mut details = vec![
let details = vec![
TimelineEventDetail {
label: "Coffee".to_string(),
value: cup_with_details.roast_name.clone(),
@ -116,13 +112,6 @@ impl SqlCupRepository {
},
];
if let Some(rating) = cup_with_details.cup.rating {
details.push(TimelineEventDetail {
label: "Rating".to_string(),
value: format!("{rating}/5"),
});
}
serde_json::to_string(&details).map_err(|err| {
RepositoryError::unexpected(format!("failed to encode timeline event details: {err}"))
})
@ -139,12 +128,11 @@ impl CupRepository for SqlCupRepository {
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
let record = query_as::<_, CupRecord>(
"INSERT INTO cups (roast_id, cafe_id, rating) VALUES (?, ?, ?) \
RETURNING id, roast_id, cafe_id, rating, created_at, updated_at",
"INSERT INTO cups (roast_id, cafe_id) VALUES (?, ?) \
RETURNING id, roast_id, cafe_id, created_at, updated_at",
)
.bind(new_cup.roast_id.into_inner())
.bind(new_cup.cafe_id.into_inner())
.bind(new_cup.rating)
.fetch_one(&mut *tx)
.await
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
@ -190,7 +178,7 @@ impl CupRepository for SqlCupRepository {
async fn get(&self, id: CupId) -> Result<Cup, RepositoryError> {
let record = query_as::<_, CupRecord>(
"SELECT id, roast_id, cafe_id, rating, created_at, updated_at FROM cups WHERE id = ?",
"SELECT id, roast_id, cafe_id, created_at, updated_at FROM cups WHERE id = ?",
)
.bind(i64::from(id))
.fetch_optional(&self.pool)
@ -258,29 +246,6 @@ impl CupRepository for SqlCupRepository {
.await
}
async fn update(&self, id: CupId, changes: UpdateCup) -> Result<Cup, RepositoryError> {
let mut builder = QueryBuilder::new("UPDATE cups SET updated_at = CURRENT_TIMESTAMP");
let mut sep = true;
push_update_field!(builder, sep, "rating", changes.rating);
let _ = sep;
builder.push(" WHERE id = ");
builder.push_bind(i64::from(id));
let result = builder
.build()
.execute(&self.pool)
.await
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
if result.rows_affected() == 0 {
return Err(RepositoryError::NotFound);
}
self.get(id).await
}
async fn delete(&self, id: CupId) -> Result<(), RepositoryError> {
let result = query("DELETE FROM cups WHERE id = ?")
.bind(i64::from(id))
@ -301,7 +266,6 @@ struct CupRecord {
id: i64,
roast_id: i64,
cafe_id: i64,
rating: Option<i32>,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
@ -311,7 +275,6 @@ struct CupWithDetailsRecord {
id: i64,
roast_id: i64,
cafe_id: i64,
rating: Option<i32>,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
roast_name: String,

View file

@ -3,7 +3,7 @@ use clap::{Args, Subcommand};
use super::macros::{define_delete_command, define_get_command};
use super::print_json;
use crate::domain::cups::{NewCup, UpdateCup};
use crate::domain::cups::NewCup;
use crate::domain::ids::{CafeId, CupId, RoastId};
use crate::infrastructure::client::BrewlogClient;
@ -15,8 +15,6 @@ pub enum CupCommands {
List,
/// Get a cup by ID
Get(GetCupCommand),
/// Update a cup
Update(UpdateCupCommand),
/// Delete a cup
Delete(DeleteCupCommand),
}
@ -26,7 +24,6 @@ pub async fn run(client: &BrewlogClient, cmd: CupCommands) -> Result<()> {
CupCommands::Add(c) => add_cup(client, c).await,
CupCommands::List => list_cups(client).await,
CupCommands::Get(c) => get_cup(client, c).await,
CupCommands::Update(c) => update_cup(client, c).await,
CupCommands::Delete(c) => delete_cup(client, c).await,
}
}
@ -37,15 +34,12 @@ pub struct AddCupCommand {
pub roast_id: i64,
#[arg(long)]
pub cafe_id: i64,
#[arg(long)]
pub rating: Option<i32>,
}
pub async fn add_cup(client: &BrewlogClient, command: AddCupCommand) -> Result<()> {
let payload = NewCup {
roast_id: RoastId::new(command.roast_id),
cafe_id: CafeId::new(command.cafe_id),
rating: command.rating,
};
let cup = client.cups().create(&payload).await?;
@ -58,25 +52,4 @@ pub async fn list_cups(client: &BrewlogClient) -> Result<()> {
}
define_get_command!(GetCupCommand, get_cup, CupId, cups);
#[derive(Debug, Args)]
pub struct UpdateCupCommand {
#[arg(long)]
pub id: i64,
#[arg(long)]
pub rating: Option<i32>,
}
pub async fn update_cup(client: &BrewlogClient, command: UpdateCupCommand) -> Result<()> {
let payload = UpdateCup {
rating: command.rating,
};
let cup = client
.cups()
.update(CupId::new(command.id), &payload)
.await?;
print_json(&cup)
}
define_delete_command!(DeleteCupCommand, delete_cup, CupId, cups, "cup");

View file

@ -9,19 +9,11 @@ pub struct CupView {
pub roaster_slug: String,
pub cafe_name: String,
pub cafe_slug: String,
pub rating: String,
pub has_rating: bool,
pub created_at: String,
}
impl CupView {
pub fn from_domain(cup: CupWithDetails) -> Self {
let rating = cup
.cup
.rating
.map_or_else(|| "".to_string(), |r| format!("{r}/5"));
let has_rating = cup.cup.rating.is_some();
Self {
id: cup.cup.id.to_string(),
roast_name: cup.roast_name,
@ -30,8 +22,6 @@ impl CupView {
roaster_slug: cup.roaster_slug,
cafe_name: cup.cafe_name,
cafe_slug: cup.cafe_slug,
rating,
has_rating,
created_at: cup.cup.created_at.format("%Y-%m-%d %H:%M").to_string(),
}
}

View file

@ -1,7 +1,7 @@
use crate::helpers::{
create_default_cafe, create_default_roast, create_default_roaster, spawn_app_with_auth,
};
use brewlog::domain::cups::{Cup, CupWithDetails, NewCup, UpdateCup};
use brewlog::domain::cups::{Cup, CupWithDetails, NewCup};
use brewlog::domain::ids::{CafeId, RoastId};
#[tokio::test]
@ -16,7 +16,6 @@ async fn creating_a_cup_returns_a_201_for_valid_data() {
let new_cup = NewCup {
roast_id: roast.id,
cafe_id: cafe.id,
rating: Some(5),
};
let response = client
@ -32,36 +31,6 @@ async fn creating_a_cup_returns_a_201_for_valid_data() {
let cup: Cup = response.json().await.expect("Failed to parse response");
assert_eq!(cup.roast_id, roast.id);
assert_eq!(cup.cafe_id, cafe.id);
assert_eq!(cup.rating, Some(5));
}
#[tokio::test]
async fn creating_a_cup_without_optional_fields_returns_a_201() {
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
let roaster = create_default_roaster(&app).await;
let roast = create_default_roast(&app, roaster.id).await;
let cafe = create_default_cafe(&app).await;
let new_cup = NewCup {
roast_id: roast.id,
cafe_id: cafe.id,
rating: None,
};
let response = client
.post(app.api_url("/cups"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&new_cup)
.send()
.await
.expect("Failed to execute request");
assert_eq!(response.status(), 201);
let cup: Cup = response.json().await.expect("Failed to parse response");
assert_eq!(cup.rating, None);
}
#[tokio::test]
@ -72,7 +41,6 @@ async fn creating_a_cup_requires_authentication() {
let new_cup = NewCup {
roast_id: RoastId::new(1),
cafe_id: CafeId::new(1),
rating: None,
};
let response = client
@ -114,7 +82,6 @@ async fn listing_cups_returns_a_200_with_enriched_data() {
let new_cup = NewCup {
roast_id: roast.id,
cafe_id: cafe.id,
rating: Some(4),
};
client
@ -152,7 +119,6 @@ async fn getting_a_cup_returns_a_200_with_details() {
let new_cup = NewCup {
roast_id: roast.id,
cafe_id: cafe.id,
rating: Some(3),
};
let create_response = client
@ -196,91 +162,6 @@ async fn getting_a_nonexistent_cup_returns_a_404() {
assert_eq!(response.status(), 404);
}
#[tokio::test]
async fn updating_a_cup_returns_a_200_for_valid_data() {
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
let roaster = create_default_roaster(&app).await;
let roast = create_default_roast(&app, roaster.id).await;
let cafe = create_default_cafe(&app).await;
let new_cup = NewCup {
roast_id: roast.id,
cafe_id: cafe.id,
rating: None,
};
let create_response = client
.post(app.api_url("/cups"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&new_cup)
.send()
.await
.expect("Failed to create cup");
let cup: Cup = create_response
.json()
.await
.expect("Failed to parse response");
let update = UpdateCup { rating: Some(4) };
let response = client
.put(app.api_url(&format!("/cups/{}", cup.id)))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&update)
.send()
.await
.expect("Failed to execute request");
assert_eq!(response.status(), 200);
let updated: Cup = response.json().await.expect("Failed to parse response");
assert_eq!(updated.rating, Some(4));
}
#[tokio::test]
async fn updating_a_cup_with_no_changes_returns_a_400() {
let app = spawn_app_with_auth().await;
let client = reqwest::Client::new();
let roaster = create_default_roaster(&app).await;
let roast = create_default_roast(&app, roaster.id).await;
let cafe = create_default_cafe(&app).await;
let new_cup = NewCup {
roast_id: roast.id,
cafe_id: cafe.id,
rating: None,
};
let create_response = client
.post(app.api_url("/cups"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&new_cup)
.send()
.await
.expect("Failed to create cup");
let cup: Cup = create_response
.json()
.await
.expect("Failed to parse response");
let update = UpdateCup { rating: None };
let response = client
.put(app.api_url(&format!("/cups/{}", cup.id)))
.bearer_auth(app.auth_token.as_ref().unwrap())
.json(&update)
.send()
.await
.expect("Failed to execute request");
assert_eq!(response.status(), 400);
}
#[tokio::test]
async fn deleting_a_cup_returns_a_204_for_valid_id() {
let app = spawn_app_with_auth().await;
@ -293,7 +174,6 @@ async fn deleting_a_cup_returns_a_204_for_valid_id() {
let new_cup = NewCup {
roast_id: roast.id,
cafe_id: cafe.id,
rating: None,
};
let create_response = client