From 766376be12799b5a71f577d6e8d8def9b5b1d612 Mon Sep 17 00:00:00 2001 From: Jon Seager Date: Sat, 7 Feb 2026 08:28:12 +0000 Subject: [PATCH] fix(checkin): use services instead of repos to record timeline events The check-in handler was calling cafe_repo.insert() and cup_repo.insert() directly, bypassing the service layer that records timeline events. Switch to cafe_service.create() and cup_service.create() so cafes and cups created via check-in appear on the timeline. Add timeline tests for cups (existing cafe, new cafe) and brews to prevent this regression. --- src/application/routes/api/checkin.rs | 8 +- tests/server/timeline.rs | 161 +++++++++++++++++++++++++- 2 files changed, 164 insertions(+), 5 deletions(-) diff --git a/src/application/routes/api/checkin.rs b/src/application/routes/api/checkin.rs index 7b2af4e..7d6ce0e 100644 --- a/src/application/routes/api/checkin.rs +++ b/src/application/routes/api/checkin.rs @@ -69,8 +69,8 @@ pub(crate) async fn submit_checkin( .normalize(); let cafe = state - .cafe_repo - .insert(new_cafe) + .cafe_service + .create(new_cafe) .await .map_err(AppError::from)?; cafe.id @@ -82,8 +82,8 @@ pub(crate) async fn submit_checkin( }; let cup = state - .cup_repo - .insert(new_cup) + .cup_service + .create(new_cup) .await .map_err(AppError::from)?; diff --git a/tests/server/timeline.rs b/tests/server/timeline.rs index 0da3b02..4c1a9db 100644 --- a/tests/server/timeline.rs +++ b/tests/server/timeline.rs @@ -1,4 +1,8 @@ -use crate::helpers::{create_cafe_with_payload, create_roaster_with_payload, spawn_app_with_auth}; +use crate::helpers::{ + create_cafe_with_payload, create_default_bag, create_default_cafe, create_default_gear, + create_default_roast, create_default_roaster, create_roaster_with_payload, spawn_app_with_auth, +}; +use brewlog::domain::brews::NewBrew; use brewlog::domain::cafes::NewCafe; use brewlog::domain::ids::RoasterId; use brewlog::domain::roasters::NewRoaster; @@ -499,3 +503,158 @@ async fn creating_a_cafe_surfaces_on_the_timeline() { "Expected cafe link in timeline HTML, got: {body}" ); } + +#[tokio::test] +async fn checkin_surfaces_cup_on_the_timeline() { + let app = spawn_app_with_auth().await; + let client = 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; + + sleep(Duration::from_millis(10)).await; + + let payload = serde_json::json!({ + "cafe_id": cafe.id.to_string(), + "roast_id": roast.id.to_string(), + }); + + let response = client + .post(app.api_url("/check-in")) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .json(&payload) + .send() + .await + .expect("failed to create check-in"); + assert_eq!(response.status(), 201); + + sleep(Duration::from_millis(10)).await; + + let response = client + .get(format!("{}/timeline", app.address)) + .send() + .await + .expect("failed to fetch timeline"); + + assert_eq!(response.status(), 200); + + let body = response.text().await.expect("failed to read response body"); + assert!( + body.contains("Cup Added"), + "Expected 'Cup Added' badge in timeline HTML, got: {body}" + ); + assert!( + body.contains("Test Roast"), + "Expected roast name to appear in cup timeline event, got: {body}" + ); +} + +#[tokio::test] +async fn checkin_with_new_cafe_surfaces_both_on_the_timeline() { + let app = spawn_app_with_auth().await; + let client = Client::new(); + + let roaster = create_default_roaster(&app).await; + let roast = create_default_roast(&app, roaster.id).await; + + sleep(Duration::from_millis(10)).await; + + let cafe_name = "Checkin Timeline Cafe"; + let payload = serde_json::json!({ + "roast_id": roast.id.to_string(), + "cafe_name": cafe_name, + "cafe_city": "London", + "cafe_country": "UK", + "cafe_lat": 51.5074, + "cafe_lng": -0.1278, + }); + + let response = client + .post(app.api_url("/check-in")) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .json(&payload) + .send() + .await + .expect("failed to create check-in"); + assert_eq!(response.status(), 201); + + sleep(Duration::from_millis(10)).await; + + let response = client + .get(format!("{}/timeline", app.address)) + .send() + .await + .expect("failed to fetch timeline"); + + assert_eq!(response.status(), 200); + + let body = response.text().await.expect("failed to read response body"); + assert!( + body.contains("Cafe Added"), + "Expected 'Cafe Added' badge in timeline HTML for new cafe, got: {body}" + ); + assert!( + body.contains("Cup Added"), + "Expected 'Cup Added' badge in timeline HTML, got: {body}" + ); + assert!( + body.contains(cafe_name), + "Expected cafe name to appear in timeline HTML, got: {body}" + ); +} + +#[tokio::test] +async fn creating_a_brew_surfaces_on_the_timeline() { + let app = spawn_app_with_auth().await; + let client = Client::new(); + + let roaster = create_default_roaster(&app).await; + let roast = create_default_roast(&app, roaster.id).await; + let bag = create_default_bag(&app, roast.id).await; + let grinder = create_default_gear(&app, "grinder", "Comandante", "C40 MK4").await; + let brewer = create_default_gear(&app, "brewer", "Hario", "V60 02").await; + + sleep(Duration::from_millis(10)).await; + + let new_brew = NewBrew { + bag_id: bag.id, + coffee_weight: 15.0, + grinder_id: grinder.id, + grind_setting: 24.0, + brewer_id: brewer.id, + filter_paper_id: None, + water_volume: 250, + water_temp: 92.0, + quick_notes: Vec::new(), + }; + + let response = client + .post(app.api_url("/brews")) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .json(&new_brew) + .send() + .await + .expect("failed to create brew"); + assert_eq!(response.status(), 201); + + sleep(Duration::from_millis(10)).await; + + let response = client + .get(format!("{}/timeline", app.address)) + .send() + .await + .expect("failed to fetch timeline"); + + assert_eq!(response.status(), 200); + + let body = response.text().await.expect("failed to read response body"); + assert!( + body.contains("Brew Added"), + "Expected 'Brew Added' badge in timeline HTML, got: {body}" + ); + assert!( + body.contains("Test Roast"), + "Expected roast name to appear in brew timeline event, got: {body}" + ); +}