diff --git a/src/application/routes/api/coffee/bags.rs b/src/application/routes/api/coffee/bags.rs index 22c704c..e58aed4 100644 --- a/src/application/routes/api/coffee/bags.rs +++ b/src/application/routes/api/coffee/bags.rs @@ -170,7 +170,7 @@ pub(crate) async fn update_bag( let (submission, source) = payload.into_parts(); let (body_update, image_data_url) = submission.into_parts(); - let update = UpdateBag { + let mut update = UpdateBag { roast_id: body_update.roast_id.or(update_params.roast_id), roast_date: body_update.roast_date.or(update_params.roast_date), amount: body_update.amount.or(update_params.amount), @@ -180,6 +180,15 @@ pub(crate) async fn update_bag( created_at: body_update.created_at, }; + // When the bag amount changes, recompute remaining based on how much has been consumed. + if let Some(new_amount) = update.amount + && update.remaining.is_none() + { + let current = state.bag_repo.get(id).await.map_err(AppError::from)?; + let consumed = current.amount - current.remaining; + update.remaining = Some((new_amount - consumed).max(0.0)); + } + let bag = if let Some(true) = update.closed { state .bag_service diff --git a/tests/server/bags_api.rs b/tests/server/bags_api.rs index a661d6f..dd11705 100644 --- a/tests/server/bags_api.rs +++ b/tests/server/bags_api.rs @@ -1,5 +1,6 @@ use crate::helpers::{ - create_default_roast, create_default_roaster, spawn_app, spawn_app_with_auth, + create_default_bag, create_default_gear, create_default_roast, create_default_roaster, + spawn_app, spawn_app_with_auth, }; use brewlog::domain::bags::{Bag, BagWithRoast, NewBag, UpdateBag}; use chrono::NaiveDate; @@ -302,3 +303,58 @@ async fn closing_a_bag_automatically_sets_finished_at() { chrono::Utc::now().date_naive() ); } + +#[tokio::test] +async fn updating_bag_amount_recomputes_remaining() { + // Arrange: create a bag and brew from it to consume some coffee + let app = spawn_app_with_auth().await; + 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; + let client = reqwest::Client::new(); + + // Brew 15g from the 250g bag → remaining should be 235g + let new_brew = brewlog::domain::brews::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(), + brew_time: None, + created_at: None, + }; + + client + .post(app.api_url("/brews")) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .json(&new_brew) + .send() + .await + .expect("Failed to create brew"); + + // Act: update the bag amount from 250g to 500g + let update_payload = UpdateBag { + amount: Some(500.0), + ..Default::default() + }; + + let response = client + .put(app.api_url(&format!("/bags/{}", bag.id))) + .bearer_auth(app.auth_token.as_ref().unwrap()) + .json(&update_payload) + .send() + .await + .expect("Failed to execute request"); + + // Assert: remaining should be 500 - 15 = 485 (recomputed from consumed) + assert_eq!(response.status(), 200); + let updated_bag: Bag = response.json().await.expect("Failed to parse response"); + assert_eq!(updated_bag.amount, 500.0); + assert_eq!(updated_bag.remaining, 485.0); +}