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

@ -5,7 +5,7 @@ use utoipa::ToSchema;
use crate::{disputes, enums as api_enums, payments, refunds}; use crate::{disputes, enums as api_enums, payments, refunds};
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Copy)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum IncomingWebhookEvent { pub enum IncomingWebhookEvent {
PaymentIntentFailure, PaymentIntentFailure,
@ -39,6 +39,26 @@ pub enum WebhookFlow {
BankTransfer, BankTransfer,
} }
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
/// This enum tells about the affect a webhook had on an object
pub enum WebhookResponseTracker {
Payment {
payment_id: String,
status: common_enums::IntentStatus,
},
Refund {
payment_id: String,
refund_id: String,
status: common_enums::RefundStatus,
},
Dispute {
dispute_id: String,
payment_id: String,
status: common_enums::DisputeStatus,
},
NoEffect,
}
impl From<IncomingWebhookEvent> for WebhookFlow { impl From<IncomingWebhookEvent> for WebhookFlow {
fn from(evt: IncomingWebhookEvent) -> Self { fn from(evt: IncomingWebhookEvent) -> Self {
match evt { match evt {

View File

@ -3,7 +3,7 @@ pub mod utils;
use std::str::FromStr; use std::str::FromStr;
use api_models::payments::HeaderPayload; use api_models::{payments::HeaderPayload, webhooks::WebhookResponseTracker};
use common_utils::errors::ReportSwitchExt; use common_utils::errors::ReportSwitchExt;
use error_stack::{report, IntoReport, ResultExt}; use error_stack::{report, IntoReport, ResultExt};
use masking::ExposeInterface; use masking::ExposeInterface;
@ -40,7 +40,7 @@ pub async fn payments_incoming_webhook_flow<W: types::OutgoingWebhookType>(
key_store: domain::MerchantKeyStore, key_store: domain::MerchantKeyStore,
webhook_details: api::IncomingWebhookDetails, webhook_details: api::IncomingWebhookDetails,
source_verified: bool, source_verified: bool,
) -> CustomResult<(), errors::ApiErrorResponse> { ) -> CustomResult<WebhookResponseTracker, errors::ApiErrorResponse> {
let consume_or_trigger_flow = if source_verified { let consume_or_trigger_flow = if source_verified {
payments::CallConnectorAction::HandleResponse(webhook_details.resource_object) payments::CallConnectorAction::HandleResponse(webhook_details.resource_object)
} else { } else {
@ -111,9 +111,12 @@ pub async fn payments_incoming_webhook_flow<W: types::OutgoingWebhookType>(
metrics::WEBHOOK_PAYMENT_NOT_FOUND.add( metrics::WEBHOOK_PAYMENT_NOT_FOUND.add(
&metrics::CONTEXT, &metrics::CONTEXT,
1, 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?, error @ Err(_) => error?,
} }
@ -134,6 +137,8 @@ pub async fn payments_incoming_webhook_flow<W: types::OutgoingWebhookType>(
.change_context(errors::ApiErrorResponse::WebhookProcessingFailure) .change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
.attach_printable("payment id not received from payments core")?; .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(); let event_type: Option<enums::EventType> = payments_response.status.foreign_into();
// If event is NOT an UnsupportedEvent, trigger Outgoing Webhook // 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, outgoing_event_type,
enums::EventClass::Payments, enums::EventClass::Payments,
None, None,
payment_id, payment_id.clone(),
enums::EventObjectType::PaymentDetails, enums::EventObjectType::PaymentDetails,
api::OutgoingWebhookContent::PaymentDetails(payments_response), api::OutgoingWebhookContent::PaymentDetails(payments_response),
) )
.await?; .await?;
} };
let response = WebhookResponseTracker::Payment { payment_id, status };
Ok(response)
} }
_ => Err(errors::ApiErrorResponse::WebhookProcessingFailure) _ => Err(errors::ApiErrorResponse::WebhookProcessingFailure)
.into_report() .into_report()
.attach_printable("received non-json response from payments core")?, .attach_printable("received non-json response from payments core")?,
} }
Ok(())
} }
#[instrument(skip_all)] #[instrument(skip_all)]
@ -169,7 +176,7 @@ pub async fn refunds_incoming_webhook_flow<W: types::OutgoingWebhookType>(
connector_name: &str, connector_name: &str,
source_verified: bool, source_verified: bool,
event_type: api_models::webhooks::IncomingWebhookEvent, event_type: api_models::webhooks::IncomingWebhookEvent,
) -> CustomResult<(), errors::ApiErrorResponse> { ) -> CustomResult<WebhookResponseTracker, errors::ApiErrorResponse> {
let db = &*state.store; let db = &*state.store;
//find refund by connector refund id //find refund by connector refund id
let refund = match webhook_details.object_reference_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 event is NOT an UnsupportedEvent, trigger Outgoing Webhook
if let Some(outgoing_event_type) = event_type { 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>( create_event_and_trigger_outgoing_webhook::<W>(
state, state,
merchant_account, merchant_account,
@ -260,7 +268,11 @@ pub async fn refunds_incoming_webhook_flow<W: types::OutgoingWebhookType>(
.await?; .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( 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), connector: &(dyn api::Connector + Sync),
request_details: &api::IncomingWebhookRequestDetails<'_>, request_details: &api::IncomingWebhookRequestDetails<'_>,
event_type: api_models::webhooks::IncomingWebhookEvent, event_type: api_models::webhooks::IncomingWebhookEvent,
) -> CustomResult<(), errors::ApiErrorResponse> { ) -> CustomResult<WebhookResponseTracker, errors::ApiErrorResponse> {
metrics::INCOMING_DISPUTE_WEBHOOK_METRIC.add(&metrics::CONTEXT, 1, &[]); metrics::INCOMING_DISPUTE_WEBHOOK_METRIC.add(&metrics::CONTEXT, 1, &[]);
if source_verified { if source_verified {
let db = &*state.store; let db = &*state.store;
@ -411,7 +423,7 @@ pub async fn disputes_incoming_webhook_flow<W: types::OutgoingWebhookType>(
dispute_details, dispute_details,
&merchant_account.merchant_id, &merchant_account.merchant_id,
&payment_attempt, &payment_attempt,
event_type.clone(), event_type,
connector.id(), connector.id(),
) )
.await?; .await?;
@ -424,13 +436,17 @@ pub async fn disputes_incoming_webhook_flow<W: types::OutgoingWebhookType>(
event_type, event_type,
enums::EventClass::Disputes, enums::EventClass::Disputes,
None, None,
dispute_object.dispute_id, dispute_object.dispute_id.clone(),
enums::EventObjectType::DisputeDetails, enums::EventObjectType::DisputeDetails,
api::OutgoingWebhookContent::DisputeDetails(disputes_response), api::OutgoingWebhookContent::DisputeDetails(disputes_response),
) )
.await?; .await?;
metrics::INCOMING_DISPUTE_WEBHOOK_MERCHANT_NOTIFIED_METRIC.add(&metrics::CONTEXT, 1, &[]); 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 { } else {
metrics::INCOMING_DISPUTE_WEBHOOK_SIGNATURE_FAILURE_METRIC.add(&metrics::CONTEXT, 1, &[]); metrics::INCOMING_DISPUTE_WEBHOOK_SIGNATURE_FAILURE_METRIC.add(&metrics::CONTEXT, 1, &[]);
Err(errors::ApiErrorResponse::WebhookAuthenticationFailed).into_report() Err(errors::ApiErrorResponse::WebhookAuthenticationFailed).into_report()
@ -443,7 +459,7 @@ async fn bank_transfer_webhook_flow<W: types::OutgoingWebhookType>(
key_store: domain::MerchantKeyStore, key_store: domain::MerchantKeyStore,
webhook_details: api::IncomingWebhookDetails, webhook_details: api::IncomingWebhookDetails,
source_verified: bool, source_verified: bool,
) -> CustomResult<(), errors::ApiErrorResponse> { ) -> CustomResult<WebhookResponseTracker, errors::ApiErrorResponse> {
let response = if source_verified { let response = if source_verified {
let payment_attempt = get_payment_attempt_from_object_reference_id( let payment_attempt = get_payment_attempt_from_object_reference_id(
&state, &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")?; .attach_printable("did not receive payment id from payments core response")?;
let event_type: Option<enums::EventType> = payments_response.status.foreign_into(); 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 event is NOT an UnsupportedEvent, trigger Outgoing Webhook
if let Some(outgoing_event_type) = event_type { if let Some(outgoing_event_type) = event_type {
@ -495,20 +512,20 @@ async fn bank_transfer_webhook_flow<W: types::OutgoingWebhookType>(
outgoing_event_type, outgoing_event_type,
enums::EventClass::Payments, enums::EventClass::Payments,
None, None,
payment_id, payment_id.clone(),
enums::EventObjectType::PaymentDetails, enums::EventObjectType::PaymentDetails,
api::OutgoingWebhookContent::PaymentDetails(payments_response), api::OutgoingWebhookContent::PaymentDetails(payments_response),
) )
.await?; .await?;
} }
Ok(WebhookResponseTracker::Payment { payment_id, status })
} }
_ => Err(errors::ApiErrorResponse::WebhookProcessingFailure) _ => Err(errors::ApiErrorResponse::WebhookProcessingFailure)
.into_report() .into_report()
.attach_printable("received non-json response from payments core")?, .attach_printable("received non-json response from payments core")?,
} }
Ok(())
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@ -729,6 +746,27 @@ pub async fn trigger_webhook_to_merchant<W: types::OutgoingWebhookType>(
Ok(()) 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)] #[instrument(skip_all)]
pub async fn webhooks_core<W: types::OutgoingWebhookType>( pub async fn webhooks_core<W: types::OutgoingWebhookType>(
state: AppState, state: AppState,
@ -737,7 +775,10 @@ pub async fn webhooks_core<W: types::OutgoingWebhookType>(
key_store: domain::MerchantKeyStore, key_store: domain::MerchantKeyStore,
connector_name_or_mca_id: &str, connector_name_or_mca_id: &str,
body: actix_web::web::Bytes, body: actix_web::web::Bytes,
) -> RouterResponse<serde_json::Value> { ) -> errors::RouterResult<(
services::ApplicationResponse<serde_json::Value>,
WebhookResponseTracker,
)> {
metrics::WEBHOOK_INCOMING_COUNT.add( metrics::WEBHOOK_INCOMING_COUNT.add(
&metrics::CONTEXT, &metrics::CONTEXT,
1, 1,
@ -754,8 +795,11 @@ pub async fn webhooks_core<W: types::OutgoingWebhookType>(
body: &body, 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( let (merchant_connector_account, connector) = fetch_mca_and_connector(
state.clone(), &state,
&merchant_account, &merchant_account,
connector_name_or_mca_id, connector_name_or_mca_id,
&key_store, &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) .get_webhook_api_response(&request_details)
.switch() .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); logger::info!(event_type=?event_type);
let flow_type: api::WebhookFlow = event_type.to_owned().into(); 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 let object_ref_id = connector
.get_webhook_object_reference_id(&request_details) .get_webhook_object_reference_id(&request_details)
.switch() .switch()
@ -962,7 +1010,7 @@ pub async fn webhooks_core<W: types::OutgoingWebhookType>(
.await .await
.attach_printable("Incoming bank-transfer webhook flow failed")?, .attach_printable("Incoming bank-transfer webhook flow failed")?,
api::WebhookFlow::ReturnResponse => {} api::WebhookFlow::ReturnResponse => WebhookResponseTracker::NoEffect,
_ => Err(errors::ApiErrorResponse::InternalServerError) _ => Err(errors::ApiErrorResponse::InternalServerError)
.into_report() .into_report()
@ -977,14 +1025,15 @@ pub async fn webhooks_core<W: types::OutgoingWebhookType>(
merchant_account.merchant_id.clone(), merchant_account.merchant_id.clone(),
)], )],
); );
} WebhookResponseTracker::NoEffect
};
let response = connector let response = connector
.get_webhook_api_response(&request_details) .get_webhook_api_response(&request_details)
.switch() .switch()
.attach_printable("Could not get incoming webhook api response from connector")?; .attach_printable("Could not get incoming webhook api response from connector")?;
Ok(response) Ok((response, webhook_effect))
} }
#[inline] #[inline]
@ -1026,7 +1075,7 @@ pub async fn get_payment_id(
} }
async fn fetch_mca_and_connector( async fn fetch_mca_and_connector(
state: AppState, state: &AppState,
merchant_account: &domain::MerchantAccount, merchant_account: &domain::MerchantAccount,
connector_name_or_mca_id: &str, connector_name_or_mca_id: &str,
key_store: &domain::MerchantKeyStore, key_store: &domain::MerchantKeyStore,

View File

@ -29,6 +29,9 @@ const IRRELEVANT_ATTEMPT_ID_IN_SOURCE_VERIFICATION_FLOW: &str =
const IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_SOURCE_VERIFICATION_FLOW: &str = const IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_SOURCE_VERIFICATION_FLOW: &str =
"irrelevant_connector_request_reference_id_in_source_verification_flow"; "irrelevant_connector_request_reference_id_in_source_verification_flow";
/// Check whether the merchant has configured to process the webhook `event` for the `connector`
/// First check for the key "whconf_{merchant_id}_{connector_id}" in redis,
/// if not found, fetch from configs table in database, if not found use default
pub async fn lookup_webhook_event( pub async fn lookup_webhook_event(
db: &dyn StorageInterface, db: &dyn StorageInterface,
connector_id: &str, connector_id: &str,

View File

@ -26,8 +26,8 @@ pub async fn receive_incoming_webhook<W: types::OutgoingWebhookType>(
&req, &req,
body, body,
|state, auth, body| { |state, auth, body| {
webhooks::webhooks_core::<W>( webhooks::webhooks_wrapper::<W>(
state, state.to_owned(),
&req, &req,
auth.merchant_account, auth.merchant_account,
auth.key_store, auth.key_store,