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

@ -363,7 +363,7 @@ pub async fn payouts_create_core(
.await?
};
response_handler(&state, &merchant_context, &payout_data).await
trigger_webhook_and_handle_response(&state, &merchant_context, &payout_data).await
}
#[instrument(skip_all)]
@ -426,7 +426,7 @@ pub async fn payouts_confirm_core(
)
.await?;
response_handler(&state, &merchant_context, &payout_data).await
trigger_webhook_and_handle_response(&state, &merchant_context, &payout_data).await
}
pub async fn payouts_update_core(
@ -498,7 +498,7 @@ pub async fn payouts_update_core(
.await?;
}
response_handler(&state, &merchant_context, &payout_data).await
trigger_webhook_and_handle_response(&state, &merchant_context, &payout_data).await
}
#[cfg(all(feature = "payouts", feature = "v1"))]
@ -541,7 +541,9 @@ pub async fn payouts_retrieve_core(
.await?;
}
response_handler(&state, &merchant_context, &payout_data).await
Ok(services::ApplicationResponse::Json(
response_handler(&state, &merchant_context, &payout_data).await?,
))
}
#[instrument(skip_all)]
@ -632,7 +634,9 @@ pub async fn payouts_cancel_core(
.attach_printable("Payout cancellation failed for given Payout request")?;
}
response_handler(&state, &merchant_context, &payout_data).await
Ok(services::ApplicationResponse::Json(
response_handler(&state, &merchant_context, &payout_data).await?,
))
}
#[instrument(skip_all)]
@ -722,7 +726,7 @@ pub async fn payouts_fulfill_core(
}));
}
response_handler(&state, &merchant_context, &payout_data).await
trigger_webhook_and_handle_response(&state, &merchant_context, &payout_data).await
}
#[cfg(all(feature = "olap", feature = "v2"))]
@ -2481,11 +2485,21 @@ pub async fn fulfill_payout(
Ok(())
}
pub async fn response_handler(
pub async fn trigger_webhook_and_handle_response(
state: &SessionState,
merchant_context: &domain::MerchantContext,
payout_data: &PayoutData,
) -> RouterResponse<payouts::PayoutCreateResponse> {
let response = response_handler(state, merchant_context, payout_data).await?;
utils::trigger_payouts_webhook(state, merchant_context, &response).await?;
Ok(services::ApplicationResponse::Json(response))
}
pub async fn response_handler(
state: &SessionState,
merchant_context: &domain::MerchantContext,
payout_data: &PayoutData,
) -> RouterResult<payouts::PayoutCreateResponse> {
let payout_attempt = payout_data.payout_attempt.to_owned();
let payouts = payout_data.payouts.to_owned();
@ -2574,7 +2588,8 @@ pub async fn response_handler(
.attach_printable("Failed to parse payout link's URL")?,
payout_method_id,
};
Ok(services::ApplicationResponse::Json(response))
Ok(response)
}
#[cfg(feature = "v2")]

View File

@ -864,7 +864,7 @@ async fn payments_incoming_webhook_flow(
let status = payments_response.status;
let event_type: Option<enums::EventType> = payments_response.status.foreign_into();
let event_type: Option<enums::EventType> = payments_response.status.into();
// If event is NOT an UnsupportedEvent, trigger Outgoing Webhook
if let Some(outgoing_event_type) = event_type {
@ -984,20 +984,13 @@ async fn payouts_incoming_webhook_flow(
)
})?;
let event_type: Option<enums::EventType> = updated_payout_attempt.status.foreign_into();
let event_type: Option<enums::EventType> = updated_payout_attempt.status.into();
// If event is NOT an UnsupportedEvent, trigger Outgoing Webhook
if let Some(outgoing_event_type) = event_type {
let router_response =
let payout_create_response =
payouts::response_handler(&state, &merchant_context, &payout_data).await?;
let payout_create_response: payout_models::PayoutCreateResponse = match router_response
{
services::ApplicationResponse::Json(response) => response,
_ => Err(errors::ApiErrorResponse::WebhookResourceNotFound)
.attach_printable("Failed to fetch the payout create response")?,
};
Box::pin(super::create_event_and_trigger_outgoing_webhook(
state,
merchant_context,
@ -1192,7 +1185,7 @@ async fn refunds_incoming_webhook_flow(
.await
.attach_printable_lazy(|| format!("Failed while updating refund: refund_id: {refund_id}"))?
};
let event_type: Option<enums::EventType> = updated_refund.refund_status.foreign_into();
let event_type: Option<enums::EventType> = updated_refund.refund_status.into();
// If event is NOT an UnsupportedEvent, trigger Outgoing Webhook
if let Some(outgoing_event_type) = event_type {
@ -1508,8 +1501,7 @@ async fn external_authentication_incoming_webhook_flow(
let payment_id = payments_response.payment_id.clone();
let status = payments_response.status;
let event_type: Option<enums::EventType> =
payments_response.status.foreign_into();
let event_type: Option<enums::EventType> = payments_response.status.into();
// Set poll_id as completed in redis to allow the fetch status of poll through retrieve_poll_status api from client
let poll_id = core_utils::get_poll_id(
merchant_context.get_merchant_account().get_id(),
@ -1627,7 +1619,7 @@ async fn mandates_incoming_webhook_flow(
)
.await?,
);
let event_type: Option<enums::EventType> = updated_mandate.mandate_status.foreign_into();
let event_type: Option<enums::EventType> = updated_mandate.mandate_status.into();
if let Some(outgoing_event_type) = event_type {
Box::pin(super::create_event_and_trigger_outgoing_webhook(
state,
@ -1730,7 +1722,7 @@ async fn frm_incoming_webhook_flow(
services::ApplicationResponse::JsonWithHeaders((payments_response, _)) => {
let payment_id = payments_response.payment_id.clone();
let status = payments_response.status;
let event_type: Option<enums::EventType> = payments_response.status.foreign_into();
let event_type: Option<enums::EventType> = payments_response.status.into();
if let Some(outgoing_event_type) = event_type {
let primary_object_created_at = payments_response.created;
Box::pin(super::create_event_and_trigger_outgoing_webhook(
@ -1804,7 +1796,7 @@ async fn disputes_incoming_webhook_flow(
)
.await?;
let disputes_response = Box::new(dispute_object.clone().foreign_into());
let event_type: enums::EventType = dispute_object.dispute_status.foreign_into();
let event_type: enums::EventType = dispute_object.dispute_status.into();
Box::pin(super::create_event_and_trigger_outgoing_webhook(
state,
@ -1886,7 +1878,7 @@ async fn bank_transfer_webhook_flow(
services::ApplicationResponse::JsonWithHeaders((payments_response, _)) => {
let payment_id = payments_response.payment_id.clone();
let event_type: Option<enums::EventType> = payments_response.status.foreign_into();
let event_type: Option<enums::EventType> = payments_response.status.into();
let status = payments_response.status;
// If event is NOT an UnsupportedEvent, trigger Outgoing Webhook

View File

@ -535,7 +535,7 @@ async fn payments_incoming_webhook_flow(
let status = payments_response.status;
let event_type: Option<enums::EventType> = payments_response.status.foreign_into();
let event_type: Option<enums::EventType> = payments_response.status.into();
// If event is NOT an UnsupportedEvent, trigger Outgoing Webhook
if let Some(outgoing_event_type) = event_type {

View File

@ -3,7 +3,7 @@ use router_env::{instrument, tracing, Flow};
use super::app::AppState;
use crate::{
core::{admin::*, api_locking},
core::{admin::*, api_locking, errors},
services::{api, authentication as auth, authorization::permissions::Permission},
types::{api::admin, domain},
};
@ -186,11 +186,25 @@ pub async fn merchant_account_create(
json_payload: web::Json<admin::MerchantAccountCreate>,
) -> HttpResponse {
let flow = Flow::MerchantsAccountCreate;
let payload = json_payload.into_inner();
if let Err(api_error) = payload
.webhook_details
.as_ref()
.map(|details| {
details
.validate()
.map_err(|message| errors::ApiErrorResponse::InvalidRequestData { message })
})
.transpose()
{
return api::log_and_return_error_response(api_error.into());
}
Box::pin(api::server_wrap(
flow,
state,
&req,
json_payload.into_inner(),
payload,
|state, auth, req, _| create_merchant_account(state, req, auth),
&auth::PlatformOrgAdminAuth {
is_admin_auth_allowed: true,

View File

@ -3,7 +3,7 @@ use router_env::{instrument, tracing, Flow};
use super::app::AppState;
use crate::{
core::{admin::*, api_locking},
core::{admin::*, api_locking, errors},
services::{api, authentication as auth, authorization::permissions},
types::{api::admin, domain},
};
@ -19,6 +19,18 @@ pub async fn profile_create(
let flow = Flow::ProfileCreate;
let payload = json_payload.into_inner();
let merchant_id = path.into_inner();
if let Err(api_error) = payload
.webhook_details
.as_ref()
.map(|details| {
details
.validate()
.map_err(|message| errors::ApiErrorResponse::InvalidRequestData { message })
})
.transpose()
{
return api::log_and_return_error_response(api_error.into());
}
Box::pin(api::server_wrap(
flow,
@ -53,6 +65,18 @@ pub async fn profile_create(
) -> HttpResponse {
let flow = Flow::ProfileCreate;
let payload = json_payload.into_inner();
if let Err(api_error) = payload
.webhook_details
.as_ref()
.map(|details| {
details
.validate()
.map_err(|message| errors::ApiErrorResponse::InvalidRequestData { message })
})
.transpose()
{
return api::log_and_return_error_response(api_error.into());
}
Box::pin(api::server_wrap(
flow,
@ -158,12 +182,25 @@ pub async fn profile_update(
) -> HttpResponse {
let flow = Flow::ProfileUpdate;
let (merchant_id, profile_id) = path.into_inner();
let payload = json_payload.into_inner();
if let Err(api_error) = payload
.webhook_details
.as_ref()
.map(|details| {
details
.validate()
.map_err(|message| errors::ApiErrorResponse::InvalidRequestData { message })
})
.transpose()
{
return api::log_and_return_error_response(api_error.into());
}
Box::pin(api::server_wrap(
flow,
state,
&req,
json_payload.into_inner(),
payload,
|state, auth_data, req, _| update_profile(state, &profile_id, auth_data.key_store, req),
auth::auth_type(
&auth::HeaderAuth(auth::ApiKeyAuthWithMerchantIdFromRoute(merchant_id.clone())),
@ -189,12 +226,25 @@ pub async fn profile_update(
) -> HttpResponse {
let flow = Flow::ProfileUpdate;
let profile_id = path.into_inner();
let payload = json_payload.into_inner();
if let Err(api_error) = payload
.webhook_details
.as_ref()
.map(|details| {
details
.validate()
.map_err(|message| errors::ApiErrorResponse::InvalidRequestData { message })
})
.transpose()
{
return api::log_and_return_error_response(api_error.into());
}
Box::pin(api::server_wrap(
flow,
state,
&req,
json_payload.into_inner(),
payload,
|state, auth::AuthenticationDataWithoutProfile { key_store, .. }, req, _| {
update_profile(state, &profile_id, key_store, req)
},

View File

@ -443,31 +443,6 @@ impl ForeignFrom<payments::MandateAmountData> for storage_enums::MandateAmountDa
}
}
impl ForeignFrom<api_enums::IntentStatus> for Option<storage_enums::EventType> {
fn foreign_from(value: api_enums::IntentStatus) -> Self {
match value {
api_enums::IntentStatus::Succeeded => Some(storage_enums::EventType::PaymentSucceeded),
api_enums::IntentStatus::Failed => Some(storage_enums::EventType::PaymentFailed),
api_enums::IntentStatus::Processing => {
Some(storage_enums::EventType::PaymentProcessing)
}
api_enums::IntentStatus::RequiresMerchantAction
| api_enums::IntentStatus::RequiresCustomerAction
| api_enums::IntentStatus::Conflicted => Some(storage_enums::EventType::ActionRequired),
api_enums::IntentStatus::Cancelled => Some(storage_enums::EventType::PaymentCancelled),
api_enums::IntentStatus::PartiallyCaptured
| api_enums::IntentStatus::PartiallyCapturedAndCapturable => {
Some(storage_enums::EventType::PaymentCaptured)
}
api_enums::IntentStatus::RequiresCapture => {
Some(storage_enums::EventType::PaymentAuthorized)
}
api_enums::IntentStatus::RequiresPaymentMethod
| api_enums::IntentStatus::RequiresConfirmation => None,
}
}
}
impl ForeignFrom<api_enums::PaymentMethodType> for api_enums::PaymentMethod {
fn foreign_from(payment_method_type: api_enums::PaymentMethodType) -> Self {
match payment_method_type {
@ -614,66 +589,6 @@ impl ForeignTryFrom<payments::PaymentMethodData> for api_enums::PaymentMethod {
}
}
impl ForeignFrom<storage_enums::RefundStatus> for Option<storage_enums::EventType> {
fn foreign_from(value: storage_enums::RefundStatus) -> Self {
match value {
storage_enums::RefundStatus::Success => Some(storage_enums::EventType::RefundSucceeded),
storage_enums::RefundStatus::Failure => Some(storage_enums::EventType::RefundFailed),
api_enums::RefundStatus::ManualReview
| api_enums::RefundStatus::Pending
| api_enums::RefundStatus::TransactionFailure => None,
}
}
}
impl ForeignFrom<storage_enums::PayoutStatus> for Option<storage_enums::EventType> {
fn foreign_from(value: storage_enums::PayoutStatus) -> Self {
match value {
storage_enums::PayoutStatus::Success => Some(storage_enums::EventType::PayoutSuccess),
storage_enums::PayoutStatus::Failed => Some(storage_enums::EventType::PayoutFailed),
storage_enums::PayoutStatus::Cancelled => {
Some(storage_enums::EventType::PayoutCancelled)
}
storage_enums::PayoutStatus::Initiated => {
Some(storage_enums::EventType::PayoutInitiated)
}
storage_enums::PayoutStatus::Expired => Some(storage_enums::EventType::PayoutExpired),
storage_enums::PayoutStatus::Reversed => Some(storage_enums::EventType::PayoutReversed),
storage_enums::PayoutStatus::Ineligible
| storage_enums::PayoutStatus::Pending
| storage_enums::PayoutStatus::RequiresCreation
| storage_enums::PayoutStatus::RequiresFulfillment
| storage_enums::PayoutStatus::RequiresPayoutMethodData
| storage_enums::PayoutStatus::RequiresVendorAccountCreation
| storage_enums::PayoutStatus::RequiresConfirmation => None,
}
}
}
impl ForeignFrom<storage_enums::DisputeStatus> for storage_enums::EventType {
fn foreign_from(value: storage_enums::DisputeStatus) -> Self {
match value {
storage_enums::DisputeStatus::DisputeOpened => Self::DisputeOpened,
storage_enums::DisputeStatus::DisputeExpired => Self::DisputeExpired,
storage_enums::DisputeStatus::DisputeAccepted => Self::DisputeAccepted,
storage_enums::DisputeStatus::DisputeCancelled => Self::DisputeCancelled,
storage_enums::DisputeStatus::DisputeChallenged => Self::DisputeChallenged,
storage_enums::DisputeStatus::DisputeWon => Self::DisputeWon,
storage_enums::DisputeStatus::DisputeLost => Self::DisputeLost,
}
}
}
impl ForeignFrom<storage_enums::MandateStatus> for Option<storage_enums::EventType> {
fn foreign_from(value: storage_enums::MandateStatus) -> Self {
match value {
storage_enums::MandateStatus::Active => Some(storage_enums::EventType::MandateActive),
storage_enums::MandateStatus::Revoked => Some(storage_enums::EventType::MandateRevoked),
storage_enums::MandateStatus::Inactive | storage_enums::MandateStatus::Pending => None,
}
}
}
impl ForeignTryFrom<api_models::webhooks::IncomingWebhookEvent> for storage_enums::RefundStatus {
type Error = errors::ValidationError;
@ -2135,8 +2050,11 @@ impl ForeignFrom<api_models::admin::WebhookDetails>
webhook_password: item.webhook_password,
webhook_url: item.webhook_url,
payment_created_enabled: item.payment_created_enabled,
payment_succeeded_enabled: item.payment_succeeded_enabled,
payment_failed_enabled: item.payment_failed_enabled,
payment_succeeded_enabled: item.payment_succeeded_enabled,
payment_statuses_enabled: item.payment_statuses_enabled,
refund_statuses_enabled: item.refund_statuses_enabled,
payout_statuses_enabled: item.payout_statuses_enabled,
}
}
}
@ -2151,8 +2069,11 @@ impl ForeignFrom<diesel_models::business_profile::WebhookDetails>
webhook_password: item.webhook_password,
webhook_url: item.webhook_url,
payment_created_enabled: item.payment_created_enabled,
payment_succeeded_enabled: item.payment_succeeded_enabled,
payment_failed_enabled: item.payment_failed_enabled,
payment_succeeded_enabled: item.payment_succeeded_enabled,
payment_statuses_enabled: item.payment_statuses_enabled,
refund_statuses_enabled: item.refund_statuses_enabled,
payout_statuses_enabled: item.payout_statuses_enabled,
}
}
}

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!()
}

View File

@ -438,7 +438,7 @@ async fn get_outgoing_webhook_content_and_event_type(
})
}
}?;
let event_type = Option::<EventType>::foreign_from(payments_response.status);
let event_type: Option<EventType> = payments_response.status.into();
logger::debug!(current_resource_status=%payments_response.status);
Ok((
@ -462,7 +462,7 @@ async fn get_outgoing_webhook_content_and_event_type(
request,
))
.await?;
let event_type = Option::<EventType>::foreign_from(refund.refund_status);
let event_type: Option<EventType> = refund.refund_status.into();
logger::debug!(current_resource_status=%refund.refund_status);
let refund_response = RefundResponse::foreign_from(refund);
@ -495,7 +495,7 @@ async fn get_outgoing_webhook_content_and_event_type(
}
}
.map(Box::new)?;
let event_type = Some(EventType::foreign_from(dispute_response.dispute_status));
let event_type = Some(EventType::from(dispute_response.dispute_status));
logger::debug!(current_resource_status=%dispute_response.dispute_status);
Ok((
@ -527,7 +527,7 @@ async fn get_outgoing_webhook_content_and_event_type(
}
}
.map(Box::new)?;
let event_type = Option::<EventType>::foreign_from(mandate_response.status);
let event_type: Option<EventType> = mandate_response.status.into();
logger::debug!(current_resource_status=%mandate_response.status);
Ok((
@ -551,17 +551,10 @@ async fn get_outgoing_webhook_content_and_event_type(
))
.await?;
let router_response =
let payout_create_response =
payouts::response_handler(&state, &merchant_context, &payout_data).await?;
let payout_create_response: payout_models::PayoutCreateResponse = match router_response
{
ApplicationResponse::Json(response) => response,
_ => Err(errors::ApiErrorResponse::WebhookResourceNotFound)
.attach_printable("Failed to fetch the payout create response")?,
};
let event_type = Option::<EventType>::foreign_from(payout_data.payout_attempt.status);
let event_type: Option<EventType> = payout_data.payout_attempt.status.into();
logger::debug!(current_resource_status=%payout_data.payout_attempt.status);
Ok((