feat(webhooks): webhooks effect tracker (#2260)

This commit is contained in:
Narayan Bhat
2023-10-03 14:57:34 +05:30
committed by GitHub
parent abfdea20b0
commit 5048d248e5
4 changed files with 103 additions and 31 deletions

View File

@ -3,7 +3,7 @@ pub mod utils;
use std::str::FromStr;
use api_models::payments::HeaderPayload;
use api_models::{payments::HeaderPayload, webhooks::WebhookResponseTracker};
use common_utils::errors::ReportSwitchExt;
use error_stack::{report, IntoReport, ResultExt};
use masking::ExposeInterface;
@ -40,7 +40,7 @@ pub async fn payments_incoming_webhook_flow<W: types::OutgoingWebhookType>(
key_store: domain::MerchantKeyStore,
webhook_details: api::IncomingWebhookDetails,
source_verified: bool,
) -> CustomResult<(), errors::ApiErrorResponse> {
) -> CustomResult<WebhookResponseTracker, errors::ApiErrorResponse> {
let consume_or_trigger_flow = if source_verified {
payments::CallConnectorAction::HandleResponse(webhook_details.resource_object)
} else {
@ -111,9 +111,12 @@ pub async fn payments_incoming_webhook_flow<W: types::OutgoingWebhookType>(
metrics::WEBHOOK_PAYMENT_NOT_FOUND.add(
&metrics::CONTEXT,
1,
&[add_attributes("merchant_id", merchant_account.merchant_id)],
&[add_attributes(
"merchant_id",
merchant_account.merchant_id.clone(),
)],
);
return Ok(());
return Ok(WebhookResponseTracker::NoEffect);
}
error @ Err(_) => error?,
}
@ -134,6 +137,8 @@ pub async fn payments_incoming_webhook_flow<W: types::OutgoingWebhookType>(
.change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
.attach_printable("payment id not received from payments core")?;
let status = payments_response.status;
let event_type: Option<enums::EventType> = payments_response.status.foreign_into();
// If event is NOT an UnsupportedEvent, trigger Outgoing Webhook
@ -144,20 +149,22 @@ pub async fn payments_incoming_webhook_flow<W: types::OutgoingWebhookType>(
outgoing_event_type,
enums::EventClass::Payments,
None,
payment_id,
payment_id.clone(),
enums::EventObjectType::PaymentDetails,
api::OutgoingWebhookContent::PaymentDetails(payments_response),
)
.await?;
}
};
let response = WebhookResponseTracker::Payment { payment_id, status };
Ok(response)
}
_ => Err(errors::ApiErrorResponse::WebhookProcessingFailure)
.into_report()
.attach_printable("received non-json response from payments core")?,
}
Ok(())
}
#[instrument(skip_all)]
@ -169,7 +176,7 @@ pub async fn refunds_incoming_webhook_flow<W: types::OutgoingWebhookType>(
connector_name: &str,
source_verified: bool,
event_type: api_models::webhooks::IncomingWebhookEvent,
) -> CustomResult<(), errors::ApiErrorResponse> {
) -> CustomResult<WebhookResponseTracker, errors::ApiErrorResponse> {
let db = &*state.store;
//find refund by connector refund id
let refund = match webhook_details.object_reference_id {
@ -246,7 +253,8 @@ pub async fn refunds_incoming_webhook_flow<W: types::OutgoingWebhookType>(
// If event is NOT an UnsupportedEvent, trigger Outgoing Webhook
if let Some(outgoing_event_type) = event_type {
let refund_response: api_models::refunds::RefundResponse = updated_refund.foreign_into();
let refund_response: api_models::refunds::RefundResponse =
updated_refund.clone().foreign_into();
create_event_and_trigger_outgoing_webhook::<W>(
state,
merchant_account,
@ -260,7 +268,11 @@ pub async fn refunds_incoming_webhook_flow<W: types::OutgoingWebhookType>(
.await?;
}
Ok(())
Ok(WebhookResponseTracker::Refund {
payment_id: updated_refund.payment_id,
refund_id: updated_refund.refund_id,
status: updated_refund.refund_status,
})
}
pub async fn get_payment_attempt_from_object_reference_id(
@ -386,7 +398,7 @@ pub async fn disputes_incoming_webhook_flow<W: types::OutgoingWebhookType>(
connector: &(dyn api::Connector + Sync),
request_details: &api::IncomingWebhookRequestDetails<'_>,
event_type: api_models::webhooks::IncomingWebhookEvent,
) -> CustomResult<(), errors::ApiErrorResponse> {
) -> CustomResult<WebhookResponseTracker, errors::ApiErrorResponse> {
metrics::INCOMING_DISPUTE_WEBHOOK_METRIC.add(&metrics::CONTEXT, 1, &[]);
if source_verified {
let db = &*state.store;
@ -411,7 +423,7 @@ pub async fn disputes_incoming_webhook_flow<W: types::OutgoingWebhookType>(
dispute_details,
&merchant_account.merchant_id,
&payment_attempt,
event_type.clone(),
event_type,
connector.id(),
)
.await?;
@ -424,13 +436,17 @@ pub async fn disputes_incoming_webhook_flow<W: types::OutgoingWebhookType>(
event_type,
enums::EventClass::Disputes,
None,
dispute_object.dispute_id,
dispute_object.dispute_id.clone(),
enums::EventObjectType::DisputeDetails,
api::OutgoingWebhookContent::DisputeDetails(disputes_response),
)
.await?;
metrics::INCOMING_DISPUTE_WEBHOOK_MERCHANT_NOTIFIED_METRIC.add(&metrics::CONTEXT, 1, &[]);
Ok(())
Ok(WebhookResponseTracker::Dispute {
dispute_id: dispute_object.dispute_id,
payment_id: dispute_object.payment_id,
status: dispute_object.dispute_status,
})
} else {
metrics::INCOMING_DISPUTE_WEBHOOK_SIGNATURE_FAILURE_METRIC.add(&metrics::CONTEXT, 1, &[]);
Err(errors::ApiErrorResponse::WebhookAuthenticationFailed).into_report()
@ -443,7 +459,7 @@ async fn bank_transfer_webhook_flow<W: types::OutgoingWebhookType>(
key_store: domain::MerchantKeyStore,
webhook_details: api::IncomingWebhookDetails,
source_verified: bool,
) -> CustomResult<(), errors::ApiErrorResponse> {
) -> CustomResult<WebhookResponseTracker, errors::ApiErrorResponse> {
let response = if source_verified {
let payment_attempt = get_payment_attempt_from_object_reference_id(
&state,
@ -486,6 +502,7 @@ async fn bank_transfer_webhook_flow<W: types::OutgoingWebhookType>(
.attach_printable("did not receive payment id from payments core response")?;
let event_type: Option<enums::EventType> = payments_response.status.foreign_into();
let status = payments_response.status;
// If event is NOT an UnsupportedEvent, trigger Outgoing Webhook
if let Some(outgoing_event_type) = event_type {
@ -495,20 +512,20 @@ async fn bank_transfer_webhook_flow<W: types::OutgoingWebhookType>(
outgoing_event_type,
enums::EventClass::Payments,
None,
payment_id,
payment_id.clone(),
enums::EventObjectType::PaymentDetails,
api::OutgoingWebhookContent::PaymentDetails(payments_response),
)
.await?;
}
Ok(WebhookResponseTracker::Payment { payment_id, status })
}
_ => Err(errors::ApiErrorResponse::WebhookProcessingFailure)
.into_report()
.attach_printable("received non-json response from payments core")?,
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
@ -729,6 +746,27 @@ pub async fn trigger_webhook_to_merchant<W: types::OutgoingWebhookType>(
Ok(())
}
pub async fn webhooks_wrapper<W: types::OutgoingWebhookType>(
state: AppState,
req: &actix_web::HttpRequest,
merchant_account: domain::MerchantAccount,
key_store: domain::MerchantKeyStore,
connector_name_or_mca_id: &str,
body: actix_web::web::Bytes,
) -> RouterResponse<serde_json::Value> {
let (application_response, _webhooks_response_tracker) = webhooks_core::<W>(
state,
req,
merchant_account,
key_store,
connector_name_or_mca_id,
body,
)
.await?;
Ok(application_response)
}
#[instrument(skip_all)]
pub async fn webhooks_core<W: types::OutgoingWebhookType>(
state: AppState,
@ -737,7 +775,10 @@ pub async fn webhooks_core<W: types::OutgoingWebhookType>(
key_store: domain::MerchantKeyStore,
connector_name_or_mca_id: &str,
body: actix_web::web::Bytes,
) -> RouterResponse<serde_json::Value> {
) -> errors::RouterResult<(
services::ApplicationResponse<serde_json::Value>,
WebhookResponseTracker,
)> {
metrics::WEBHOOK_INCOMING_COUNT.add(
&metrics::CONTEXT,
1,
@ -754,8 +795,11 @@ pub async fn webhooks_core<W: types::OutgoingWebhookType>(
body: &body,
};
// Fetch the merchant connector account to get the webhooks source secret
// `webhooks source secret` is a secret shared between the merchant and connector
// This is used for source verification and webhooks integrity
let (merchant_connector_account, connector) = fetch_mca_and_connector(
state.clone(),
&state,
&merchant_account,
connector_name_or_mca_id,
&key_store,
@ -810,10 +854,12 @@ pub async fn webhooks_core<W: types::OutgoingWebhookType>(
],
);
return connector
let response = connector
.get_webhook_api_response(&request_details)
.switch()
.attach_printable("Failed while early return in case of event type parsing");
.attach_printable("Failed while early return in case of event type parsing")?;
return Ok((response, WebhookResponseTracker::NoEffect));
}
};
@ -829,7 +875,9 @@ pub async fn webhooks_core<W: types::OutgoingWebhookType>(
logger::info!(event_type=?event_type);
let flow_type: api::WebhookFlow = event_type.to_owned().into();
if process_webhook_further && !matches!(flow_type, api::WebhookFlow::ReturnResponse) {
let webhook_effect = if process_webhook_further
&& !matches!(flow_type, api::WebhookFlow::ReturnResponse)
{
let object_ref_id = connector
.get_webhook_object_reference_id(&request_details)
.switch()
@ -962,7 +1010,7 @@ pub async fn webhooks_core<W: types::OutgoingWebhookType>(
.await
.attach_printable("Incoming bank-transfer webhook flow failed")?,
api::WebhookFlow::ReturnResponse => {}
api::WebhookFlow::ReturnResponse => WebhookResponseTracker::NoEffect,
_ => Err(errors::ApiErrorResponse::InternalServerError)
.into_report()
@ -977,14 +1025,15 @@ pub async fn webhooks_core<W: types::OutgoingWebhookType>(
merchant_account.merchant_id.clone(),
)],
);
}
WebhookResponseTracker::NoEffect
};
let response = connector
.get_webhook_api_response(&request_details)
.switch()
.attach_printable("Could not get incoming webhook api response from connector")?;
Ok(response)
Ok((response, webhook_effect))
}
#[inline]
@ -1026,7 +1075,7 @@ pub async fn get_payment_id(
}
async fn fetch_mca_and_connector(
state: AppState,
state: &AppState,
merchant_account: &domain::MerchantAccount,
connector_name_or_mca_id: &str,
key_store: &domain::MerchantKeyStore,