diff --git a/apps/api/src/modules/group_buying/service.rs b/apps/api/src/modules/group_buying/service.rs index b175d44..7646efb 100644 --- a/apps/api/src/modules/group_buying/service.rs +++ b/apps/api/src/modules/group_buying/service.rs @@ -181,12 +181,15 @@ pub async fn link_opener(tx: &mut PgConnection, group_id: Uuid, order_id: Uuid) repo::set_leader(tx, group_id, order_id).await } -/// An unpaid opener cancelling closes a group that has no paid members. +/// An unpaid opener cancelling closes a group that has no paid members. The +/// expiry sweep runs first so an elapsed group records `expired` rather than +/// being relabelled `cancelled`. pub async fn cancel_opener_group( tx: &mut PgConnection, order_id: Uuid, group_id: Uuid, ) -> ApiResult<()> { + repo::expire_due_groups(&mut *tx).await?; repo::cancel_empty_opener_group(tx, group_id, order_id).await?; Ok(()) } diff --git a/apps/api/tests/group_buying.rs b/apps/api/tests/group_buying.rs index df7a451..21e7876 100644 --- a/apps/api/tests/group_buying.rs +++ b/apps/api/tests/group_buying.rs @@ -468,6 +468,35 @@ async fn activity_rejects_a_sku_with_an_overlapping_flash_sale() { assert_eq!(res.status(), 409); } +#[tokio::test] +#[serial] +async fn cancelling_an_elapsed_empty_group_records_expired() { + let app = spawn_app().await; + let admin = login_admin(&app).await; + let (owner, _shop, _product, sku) = setup_sellable(&app, &admin, "gb-precedence", 1000, 10).await; + let activity = create_activity(&app, &owner, &sku, 700, 3, 24).await; + + let (token, _) = register_customer(&app, "gb-precedence").await; + add_to_cart(&app, &token, &sku, 1).await; + let order = place(&app, &token, &sku, &activity, None).await; + let group_id = sqlx::query_scalar::<_, Uuid>("SELECT group_id FROM orders WHERE id = $1") + .bind(Uuid::parse_str(&order).unwrap()) + .fetch_one(&app.db) + .await + .unwrap(); + + // The lifetime ends before the opener cancels. + sqlx::query("UPDATE collective_groups SET expires_at = now() - interval '1 hour' WHERE id = $1") + .bind(group_id) + .execute(&app.db) + .await + .unwrap(); + assert_eq!(cancel(&app, &token, &order).await, 200); + + // Both conditions hold; an elapsed group is expired, not relabelled cancelled. + assert_eq!(group_state(&app, &group_id.to_string()).await.0, "expired"); +} + #[tokio::test] #[serial] async fn cancelling_does_not_roll_back_paid_seats() {