fix(api): record an elapsed group as expired when its opener cancels
Cancelling the opening unpaid order ran the expiry sweep only implicitly, so a group whose lifetime had already ended was relabelled cancelled instead of expired. Both conditions hold in that case; sweep first so the lifetime ending wins. The behaviour was invisible to shoppers (neither state is joinable) but it matters to a future refund flow reading expired paid groups. Add an integration test that reproduces the precedence.
This commit is contained in:
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user