feat(core): allow setting up status across payments, refunds and payouts for triggering webhooks in core resource flows (#8433)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Kashif
2025-06-30 12:38:04 +05:30
committed by GitHub
parent 78e837b171
commit d305fad2e6
20 changed files with 545 additions and 189 deletions

View File

@@ -55,10 +55,7 @@ use crate::{
logger,
routes::{metrics, SessionState},
services::{self, authentication::get_header_value_by_key},
types::{
self, domain,
transformers::{ForeignFrom, ForeignInto},
},
types::{self, domain, transformers::ForeignInto},
};
#[cfg(feature = "v1")]
use crate::{core::webhooks as webhooks_core, types::storage};
@@ -1156,25 +1153,21 @@ where
D: payments_core::OperationSessionGetters<F>,
{
let status = payment_data.get_payment_intent().status;
let payment_id = payment_data.get_payment_intent().get_id().to_owned();
let should_trigger_webhook = business_profile
.get_payment_webhook_statuses()
.contains(&status);
let captures = payment_data
.get_multiple_capture_data()
.map(|multiple_capture_data| {
multiple_capture_data
.get_all_captures()
.into_iter()
.cloned()
.collect()
});
if matches!(
status,
enums::IntentStatus::Succeeded
| enums::IntentStatus::Failed
| enums::IntentStatus::PartiallyCaptured
| enums::IntentStatus::RequiresMerchantAction
) {
if should_trigger_webhook {
let captures = payment_data
.get_multiple_capture_data()
.map(|multiple_capture_data| {
multiple_capture_data
.get_all_captures()
.into_iter()
.cloned()
.collect()
});
let payment_id = payment_data.get_payment_intent().get_id().to_owned();
let payments_response = crate::core::payments::transformers::payments_to_payments_response(
payment_data,
captures,
@@ -1188,7 +1181,7 @@ where
None,
)?;
let event_type = ForeignFrom::foreign_from(status);
let event_type = status.into();
if let services::ApplicationResponse::JsonWithHeaders((payments_response_json, _)) =
payments_response
@@ -1250,27 +1243,28 @@ pub async fn trigger_refund_outgoing_webhook(
profile_id: id_type::ProfileId,
) -> RouterResult<()> {
let refund_status = refund.refund_status;
if matches!(
refund_status,
enums::RefundStatus::Success
| enums::RefundStatus::Failure
| enums::RefundStatus::TransactionFailure
) {
let event_type = ForeignFrom::foreign_from(refund_status);
let key_manager_state = &(state).into();
let business_profile = state
.store
.find_business_profile_by_profile_id(
key_manager_state,
merchant_context.get_merchant_key_store(),
&profile_id,
)
.await
.to_not_found_response(errors::ApiErrorResponse::ProfileNotFound {
id: profile_id.get_string_repr().to_owned(),
})?;
let should_trigger_webhook = business_profile
.get_refund_webhook_statuses()
.contains(&refund_status);
if should_trigger_webhook {
let event_type = refund_status.into();
let refund_response: api_models::refunds::RefundResponse = refund.clone().foreign_into();
let key_manager_state = &(state).into();
let refund_id = refund_response.refund_id.clone();
let business_profile = state
.store
.find_business_profile_by_profile_id(
key_manager_state,
merchant_context.get_merchant_key_store(),
&profile_id,
)
.await
.to_not_found_response(errors::ApiErrorResponse::ProfileNotFound {
id: profile_id.get_string_repr().to_owned(),
})?;
let cloned_state = state.clone();
let cloned_merchant_context = merchant_context.clone();
let primary_object_created_at = refund_response.created_at;
@@ -1317,3 +1311,72 @@ pub fn get_locale_from_header(headers: &actix_web::http::header::HeaderMap) -> S
.map(|val| val.to_string())
.unwrap_or(common_utils::consts::DEFAULT_LOCALE.to_string())
}
#[cfg(all(feature = "payouts", feature = "v1"))]
pub async fn trigger_payouts_webhook(
state: &SessionState,
merchant_context: &domain::MerchantContext,
payout_response: &api_models::payouts::PayoutCreateResponse,
) -> RouterResult<()> {
let key_manager_state = &(state).into();
let profile_id = &payout_response.profile_id;
let business_profile = state
.store
.find_business_profile_by_profile_id(
key_manager_state,
merchant_context.get_merchant_key_store(),
profile_id,
)
.await
.to_not_found_response(errors::ApiErrorResponse::ProfileNotFound {
id: profile_id.get_string_repr().to_owned(),
})?;
let status = &payout_response.status;
let should_trigger_webhook = business_profile
.get_payout_webhook_statuses()
.contains(status);
if should_trigger_webhook {
let event_type = (*status).into();
if let Some(event_type) = event_type {
let cloned_merchant_context = merchant_context.clone();
let cloned_state = state.clone();
let cloned_response = payout_response.clone();
// This spawns this futures in a background thread, the exception inside this future won't affect
// the current thread and the lifecycle of spawn thread is not handled by runtime.
// So when server shutdown won't wait for this thread's completion.
tokio::spawn(
async move {
let primary_object_created_at = cloned_response.created;
Box::pin(webhooks_core::create_event_and_trigger_outgoing_webhook(
cloned_state,
cloned_merchant_context,
business_profile,
event_type,
diesel_models::enums::EventClass::Payouts,
cloned_response.payout_id.clone(),
diesel_models::enums::EventObjectType::PayoutDetails,
webhooks::OutgoingWebhookContent::PayoutDetails(Box::new(cloned_response)),
primary_object_created_at,
))
.await
}
.in_current_span(),
);
} else {
logger::warn!("Outgoing webhook not sent because of missing event type status mapping");
}
}
Ok(())
}
#[cfg(all(feature = "payouts", feature = "v2"))]
pub async fn trigger_payouts_webhook(
state: &SessionState,
merchant_context: &domain::MerchantContext,
payout_response: &api_models::payouts::PayoutCreateResponse,
) -> RouterResult<()> {
todo!()
}