Fix payouts notifications not delivering (#5430)
* fix FK violation when inserting rows into `notifications_deliveries` * add test for FK violation when inserting into notifications_deliveries * sqlx prepare * add migration to prevent stale notifications from being dequeued all at once upon fix * Revert "add migration to prevent stale notifications from being dequeued all at once upon fix" This reverts commit 446f398752bbddb632196a549501f9ce0b2da67f.
This commit is contained in:
@@ -1393,3 +1393,179 @@ async fn check_balance_with_webhook(
|
||||
|
||||
Ok(result.ok().flatten())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test::{
|
||||
api_v3::ApiV3,
|
||||
database::USER_USER_ID_PARSED,
|
||||
environment::{TestEnvironment, with_test_environment},
|
||||
};
|
||||
use rust_decimal::dec;
|
||||
|
||||
async fn setup_payouts_values(
|
||||
db: &PgPool,
|
||||
entries: Vec<(i64, Decimal, DateTime<Utc>)>, // (user_id, amount, date_available)
|
||||
) {
|
||||
for (user_id, amount, date_available) in &entries {
|
||||
sqlx::query!(
|
||||
"INSERT INTO payouts_values (user_id, mod_id, amount, created, date_available)
|
||||
VALUES ($1, NULL, $2, NOW(), $3)",
|
||||
user_id,
|
||||
amount,
|
||||
date_available,
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
for (user_id, _amount, date_available) in &entries {
|
||||
sqlx::query!(
|
||||
"INSERT INTO payouts_values_notifications (date_available, user_id, notified)
|
||||
VALUES ($1, $2, FALSE)
|
||||
ON CONFLICT (date_available, user_id) DO NOTHING",
|
||||
date_available,
|
||||
user_id,
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// When a user's payout amount is below
|
||||
/// the $1.00 (100 cents) threshold, the `WHERE sum >= 100` filter in
|
||||
/// insert_many_payout_notifications skips the INSERT into notifications
|
||||
/// for that user, but the old code still passed ALL pre-generated
|
||||
/// notification_ids to insert_many_deliveries. Since
|
||||
/// notifications_deliveries.notification_id has a FK constraint on
|
||||
/// notifications(id), this caused a violation that failed
|
||||
/// the entire transaction. The notified flag was never set to `true`,
|
||||
/// so the rows accumulated and the same failure repeated every run.
|
||||
#[actix_rt::test]
|
||||
async fn test_payout_notification_below_threshold_does_not_fail() {
|
||||
with_test_environment(None, |env: TestEnvironment<ApiV3>| async move {
|
||||
let db = &env.db.pool;
|
||||
let redis = &env.db.redis_pool;
|
||||
|
||||
let user_id = USER_USER_ID_PARSED;
|
||||
|
||||
// date_available must be in the past so the notification query
|
||||
// picks it up (date_available <= NOW()).
|
||||
let date_available = Utc::now() - Duration::hours(1);
|
||||
|
||||
// Amount of $0.50 -- below the $1.00 threshold (sum < 100 cents).
|
||||
setup_payouts_values(
|
||||
db,
|
||||
vec![(user_id, dec!(0.50), date_available)],
|
||||
)
|
||||
.await;
|
||||
|
||||
// This should succeed, NOT return an error.
|
||||
// Before the fix, this would fail with a FK constraint violation.
|
||||
let result = index_payouts_notifications(db, redis).await;
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"index_payouts_notifications should succeed for below-threshold payouts, got: {:?}",
|
||||
result.err()
|
||||
);
|
||||
|
||||
// Verify the notification row was marked as notified (not stuck).
|
||||
let remaining = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM payouts_values_notifications WHERE notified = FALSE AND user_id = $1",
|
||||
user_id,
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_or(0);
|
||||
|
||||
assert_eq!(
|
||||
remaining, 0,
|
||||
"All payouts_values_notifications rows should be marked notified"
|
||||
);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
/// When there is a mix of users, some above and some below the
|
||||
/// threshold, the above-threshold users should get notifications
|
||||
/// while the below-threshold ones are silently skipped. The entire
|
||||
/// transaction must succeed (not fail due to the below-threshold user).
|
||||
#[actix_rt::test]
|
||||
async fn test_payout_notification_mixed_threshold_users() {
|
||||
with_test_environment(None, |env: TestEnvironment<ApiV3>| async move {
|
||||
let db = &env.db.pool;
|
||||
let redis = &env.db.redis_pool;
|
||||
|
||||
let above_user_id = USER_USER_ID_PARSED; // user 3
|
||||
let below_user_id = 4i64; // FRIEND_USER_ID
|
||||
let date_available = Utc::now() - Duration::hours(1);
|
||||
|
||||
setup_payouts_values(
|
||||
db,
|
||||
vec![
|
||||
// Above threshold
|
||||
(above_user_id, dec!(5.00), date_available),
|
||||
// Below threshold
|
||||
(below_user_id, dec!(0.25), date_available),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let result = index_payouts_notifications(db, redis).await;
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"index_payouts_notifications should succeed with mixed users, got: {:?}",
|
||||
result.err()
|
||||
);
|
||||
|
||||
// Above-threshold user should have a notification.
|
||||
let above_count = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM notifications WHERE user_id = $1 AND body->>'type' = 'payout_available'",
|
||||
above_user_id,
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_or(0);
|
||||
|
||||
assert!(
|
||||
above_count > 0,
|
||||
"Above-threshold user should have a payout notification"
|
||||
);
|
||||
|
||||
// Below-threshold user should NOT have a notification.
|
||||
let below_count = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM notifications WHERE user_id = $1 AND body->>'type' = 'payout_available'",
|
||||
below_user_id,
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_or(0);
|
||||
|
||||
assert_eq!(
|
||||
below_count, 0,
|
||||
"Below-threshold user should NOT have a payout notification"
|
||||
);
|
||||
|
||||
// Both should be marked notified.
|
||||
let remaining = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM payouts_values_notifications WHERE notified = FALSE",
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_or(0);
|
||||
|
||||
assert_eq!(
|
||||
remaining, 0,
|
||||
"All payouts_values_notifications rows should be marked notified"
|
||||
);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user