fix: ensure bags are closed properly and closures are on the timeline
This commit is contained in:
parent
02c6f6d675
commit
144257c2c8
3 changed files with 162 additions and 16 deletions
|
|
@ -210,12 +210,18 @@ pub(crate) async fn update_bag(
|
|||
finished_at: None,
|
||||
});
|
||||
|
||||
let update = UpdateBag {
|
||||
let mut update = UpdateBag {
|
||||
remaining: body_update.remaining.or(update_params.remaining),
|
||||
closed: body_update.closed.or(update_params.closed),
|
||||
finished_at: body_update.finished_at.or(update_params.finished_at),
|
||||
};
|
||||
|
||||
if let Some(true) = update.closed {
|
||||
if update.finished_at.is_none() {
|
||||
update.finished_at = Some(chrono::Utc::now().date_naive());
|
||||
}
|
||||
}
|
||||
|
||||
let bag = state
|
||||
.bag_repo
|
||||
.update(id, update.clone())
|
||||
|
|
@ -224,14 +230,13 @@ pub(crate) async fn update_bag(
|
|||
|
||||
if let Some(true) = update.closed {
|
||||
// Fetch roast and roaster for timeline event
|
||||
if let Ok(roast) = state.roast_repo.get(bag.roast_id).await
|
||||
&& let Ok(roaster) = state.roaster_repo.get(roast.roaster_id).await
|
||||
{
|
||||
if let Ok(roast) = state.roast_repo.get(bag.roast_id).await {
|
||||
if let Ok(roaster) = state.roaster_repo.get(roast.roaster_id).await {
|
||||
let event = NewTimelineEvent {
|
||||
entity_type: "bag".to_string(),
|
||||
entity_id: bag.id.into_inner(),
|
||||
occurred_at: chrono::Utc::now(),
|
||||
title: roast.name.to_string(),
|
||||
title: format!("{}", roast.name),
|
||||
details: vec![TimelineEventDetail {
|
||||
label: "Roaster".to_string(),
|
||||
value: roaster.name,
|
||||
|
|
@ -241,6 +246,7 @@ pub(crate) async fn update_bag(
|
|||
let _ = state.timeline_repo.insert(event).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if is_datastar_request(&headers) {
|
||||
render_bag_list_fragment(state, request, true)
|
||||
|
|
|
|||
|
|
@ -244,3 +244,54 @@ async fn deleting_a_bag_returns_204() {
|
|||
|
||||
assert_eq!(get_response.status(), 404);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn closing_a_bag_automatically_sets_finished_at() {
|
||||
// Arrange
|
||||
let app = spawn_app_with_auth().await;
|
||||
let roaster = create_default_roaster(&app).await;
|
||||
let roast = create_default_roast(&app, roaster.id).await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let new_bag = NewBag {
|
||||
roast_id: roast.id,
|
||||
roast_date: None,
|
||||
amount: 250.0,
|
||||
};
|
||||
|
||||
let create_response = client
|
||||
.post(app.api_url("/bags"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&new_bag)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to create bag");
|
||||
|
||||
let created_bag: Bag = create_response
|
||||
.json()
|
||||
.await
|
||||
.expect("Failed to parse response");
|
||||
|
||||
let update_payload = serde_json::json!({
|
||||
"closed": true
|
||||
});
|
||||
|
||||
// Act
|
||||
let response = client
|
||||
.put(app.api_url(&format!("/bags/{}", created_bag.id)))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&update_payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
// Assert
|
||||
assert_eq!(response.status(), 200);
|
||||
let updated_bag: Bag = response.json().await.expect("Failed to parse response");
|
||||
assert!(updated_bag.closed);
|
||||
assert!(updated_bag.finished_at.is_some());
|
||||
assert_eq!(
|
||||
updated_bag.finished_at.unwrap(),
|
||||
chrono::Utc::now().date_naive()
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -326,3 +326,92 @@ async fn timeline_chunk_endpoint_serves_remaining_events() {
|
|||
"Expected chunk to clear next URL once exhausted"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn closing_a_bag_surfaces_on_the_timeline() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let client = Client::new();
|
||||
|
||||
let roaster_id = create_roaster_with_payload(
|
||||
&app,
|
||||
NewRoaster {
|
||||
name: "Bag Finish Timeline Roasters".to_string(),
|
||||
country: "UK".to_string(),
|
||||
city: Some("Bristol".to_string()),
|
||||
homepage: Some("https://example.com".to_string()),
|
||||
notes: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.id;
|
||||
|
||||
sleep(Duration::from_millis(5)).await;
|
||||
let roast_name = "Bag Finish Timeline Roast";
|
||||
create_roast(&app, roaster_id, roast_name).await;
|
||||
|
||||
// Fetch the roast to get its ID
|
||||
let roasts_response = client
|
||||
.get(app.api_url("/roasts"))
|
||||
.send()
|
||||
.await
|
||||
.expect("failed to fetch roasts");
|
||||
|
||||
let roasts: Vec<brewlog::domain::roasts::RoastWithRoaster> = roasts_response
|
||||
.json()
|
||||
.await
|
||||
.expect("failed to parse roasts");
|
||||
let roast_id = roasts.first().unwrap().roast.id;
|
||||
|
||||
// Create a bag
|
||||
let bag_submission = serde_json::json!({
|
||||
"roast_id": roast_id,
|
||||
"roast_date": "2023-01-01",
|
||||
"amount": 250.0
|
||||
});
|
||||
|
||||
let response = client
|
||||
.post(app.api_url("/bags"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&bag_submission)
|
||||
.send()
|
||||
.await
|
||||
.expect("failed to create bag");
|
||||
|
||||
let bag: brewlog::domain::bags::Bag = response.json().await.expect("failed to parse bag");
|
||||
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
|
||||
// Close the bag
|
||||
let update_submission = serde_json::json!({
|
||||
"closed": true
|
||||
});
|
||||
|
||||
let response = client
|
||||
.put(app.api_url(&format!("/bags/{}", bag.id)))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&update_submission)
|
||||
.send()
|
||||
.await
|
||||
.expect("failed to update bag");
|
||||
assert_eq!(response.status(), 200);
|
||||
|
||||
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("Bag Finished"),
|
||||
"Expected 'Bag Finished' badge in timeline HTML, got: {body}"
|
||||
);
|
||||
assert!(
|
||||
body.contains(&format!("Finished: {}", roast_name)),
|
||||
"Expected bag finished title to appear in timeline HTML, got: {body}"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue