fix(router/webhooks): use api error response for returning errors from webhooks core (#1305)

This commit is contained in:
ItsMeShashank
2023-05-29 19:28:38 +05:30
committed by GitHub
parent 864d85534f
commit cd0cf40fe2
6 changed files with 212 additions and 109 deletions

10
Cargo.lock generated
View File

@ -3058,7 +3058,7 @@ dependencies = [
[[package]]
name = "opentelemetry"
version = "0.18.0"
source = "git+https://github.com/open-telemetry/opentelemetry-rust/?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658"
source = "git+https://github.com/open-telemetry/opentelemetry-rust?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658"
dependencies = [
"opentelemetry_api",
"opentelemetry_sdk",
@ -3067,7 +3067,7 @@ dependencies = [
[[package]]
name = "opentelemetry-otlp"
version = "0.11.0"
source = "git+https://github.com/open-telemetry/opentelemetry-rust/?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658"
source = "git+https://github.com/open-telemetry/opentelemetry-rust?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658"
dependencies = [
"async-trait",
"futures",
@ -3084,7 +3084,7 @@ dependencies = [
[[package]]
name = "opentelemetry-proto"
version = "0.1.0"
source = "git+https://github.com/open-telemetry/opentelemetry-rust/?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658"
source = "git+https://github.com/open-telemetry/opentelemetry-rust?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658"
dependencies = [
"futures",
"futures-util",
@ -3096,7 +3096,7 @@ dependencies = [
[[package]]
name = "opentelemetry_api"
version = "0.18.0"
source = "git+https://github.com/open-telemetry/opentelemetry-rust/?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658"
source = "git+https://github.com/open-telemetry/opentelemetry-rust?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658"
dependencies = [
"fnv",
"futures-channel",
@ -3111,7 +3111,7 @@ dependencies = [
[[package]]
name = "opentelemetry_sdk"
version = "0.18.0"
source = "git+https://github.com/open-telemetry/opentelemetry-rust/?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658"
source = "git+https://github.com/open-telemetry/opentelemetry-rust?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658"
dependencies = [
"async-trait",
"crossbeam-channel",

View File

@ -199,6 +199,8 @@ pub enum StripeErrorCode {
FileNotFound,
#[error(error_type = StripeErrorType::HyperswitchError, code = "", message = "File not available")]
FileNotAvailable,
#[error(error_type = StripeErrorType::HyperswitchError, code = "", message = "There was an issue with processing webhooks")]
WebhookProcessingError,
// [#216]: https://github.com/juspay/hyperswitch/issues/216
// Implement the remaining stripe error codes
@ -498,6 +500,10 @@ impl From<errors::ApiErrorResponse> for StripeErrorCode {
Self::MerchantConnectorAccountDisabled
}
errors::ApiErrorResponse::NotSupported { .. } => Self::InternalServerError,
errors::ApiErrorResponse::WebhookBadRequest
| errors::ApiErrorResponse::WebhookResourceNotFound
| errors::ApiErrorResponse::WebhookProcessingFailure
| errors::ApiErrorResponse::WebhookAuthenticationFailed => Self::WebhookProcessingError,
}
}
}
@ -557,7 +563,8 @@ impl actix_web::ResponseError for StripeErrorCode {
Self::RefundFailed
| Self::InternalServerError
| Self::MandateActive
| Self::CustomerRedacted => StatusCode::INTERNAL_SERVER_ERROR,
| Self::CustomerRedacted
| Self::WebhookProcessingError => StatusCode::INTERNAL_SERVER_ERROR,
Self::ReturnUrlUnavailable => StatusCode::SERVICE_UNAVAILABLE,
Self::ExternalConnectorError { status_code, .. } => {
StatusCode::from_u16(*status_code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)

View File

@ -678,34 +678,59 @@ impl From<CheckoutRedirectResponseStatus> for enums::AttemptStatus {
}
}
pub fn is_refund_event(event_code: &CheckoutTransactionType) -> bool {
pub fn is_refund_event(event_code: &CheckoutWebhookEventType) -> bool {
matches!(
event_code,
CheckoutTransactionType::PaymentRefunded | CheckoutTransactionType::PaymentRefundDeclined
CheckoutWebhookEventType::PaymentRefunded | CheckoutWebhookEventType::PaymentRefundDeclined
)
}
pub fn is_chargeback_event(event_code: &CheckoutTransactionType) -> bool {
pub fn is_chargeback_event(event_code: &CheckoutWebhookEventType) -> bool {
matches!(
event_code,
CheckoutTransactionType::DisputeReceived
| CheckoutTransactionType::DisputeExpired
| CheckoutTransactionType::DisputeAccepted
| CheckoutTransactionType::DisputeCanceled
| CheckoutTransactionType::DisputeEvidenceSubmitted
| CheckoutTransactionType::DisputeEvidenceAcknowledgedByScheme
| CheckoutTransactionType::DisputeEvidenceRequired
| CheckoutTransactionType::DisputeArbitrationLost
| CheckoutTransactionType::DisputeArbitrationWon
| CheckoutTransactionType::DisputeWon
| CheckoutTransactionType::DisputeLost
CheckoutWebhookEventType::DisputeReceived
| CheckoutWebhookEventType::DisputeExpired
| CheckoutWebhookEventType::DisputeAccepted
| CheckoutWebhookEventType::DisputeCanceled
| CheckoutWebhookEventType::DisputeEvidenceSubmitted
| CheckoutWebhookEventType::DisputeEvidenceAcknowledgedByScheme
| CheckoutWebhookEventType::DisputeEvidenceRequired
| CheckoutWebhookEventType::DisputeArbitrationLost
| CheckoutWebhookEventType::DisputeArbitrationWon
| CheckoutWebhookEventType::DisputeWon
| CheckoutWebhookEventType::DisputeLost
)
}
#[derive(Debug, Deserialize, strum::Display, Clone)]
#[serde(rename_all = "snake_case")]
pub enum CheckoutWebhookEventType {
AuthenticationStarted,
AuthenticationApproved,
PaymentApproved,
PaymentCaptured,
PaymentDeclined,
PaymentRefunded,
PaymentRefundDeclined,
DisputeReceived,
DisputeExpired,
DisputeAccepted,
DisputeCanceled,
DisputeEvidenceSubmitted,
DisputeEvidenceAcknowledgedByScheme,
DisputeEvidenceRequired,
DisputeArbitrationLost,
DisputeArbitrationWon,
DisputeWon,
DisputeLost,
#[serde(other)]
Unknown,
}
#[derive(Debug, Deserialize)]
pub struct CheckoutWebhookEventTypeBody {
#[serde(rename = "type")]
pub transaction_type: CheckoutTransactionType,
pub transaction_type: CheckoutWebhookEventType,
}
#[derive(Debug, Deserialize)]
@ -720,7 +745,7 @@ pub struct CheckoutWebhookData {
#[derive(Debug, Deserialize)]
pub struct CheckoutWebhookBody {
#[serde(rename = "type")]
pub transaction_type: CheckoutTransactionType,
pub transaction_type: CheckoutWebhookEventType,
pub data: CheckoutWebhookData,
}
@ -768,29 +793,30 @@ pub enum CheckoutTransactionType {
DisputeLost,
}
impl From<CheckoutTransactionType> for api::IncomingWebhookEvent {
fn from(transaction_type: CheckoutTransactionType) -> Self {
impl From<CheckoutWebhookEventType> for api::IncomingWebhookEvent {
fn from(transaction_type: CheckoutWebhookEventType) -> Self {
match transaction_type {
CheckoutTransactionType::AuthenticationStarted => Self::EventNotSupported,
CheckoutTransactionType::AuthenticationApproved => Self::EventNotSupported,
CheckoutTransactionType::PaymentApproved => Self::EventNotSupported,
CheckoutTransactionType::PaymentCaptured => Self::PaymentIntentSuccess,
CheckoutTransactionType::PaymentDeclined => Self::PaymentIntentFailure,
CheckoutTransactionType::PaymentRefunded => Self::RefundSuccess,
CheckoutTransactionType::PaymentRefundDeclined => Self::RefundFailure,
CheckoutTransactionType::DisputeReceived
| CheckoutTransactionType::DisputeEvidenceRequired => Self::DisputeOpened,
CheckoutTransactionType::DisputeExpired => Self::DisputeExpired,
CheckoutTransactionType::DisputeAccepted => Self::DisputeAccepted,
CheckoutTransactionType::DisputeCanceled => Self::DisputeCancelled,
CheckoutTransactionType::DisputeEvidenceSubmitted
| CheckoutTransactionType::DisputeEvidenceAcknowledgedByScheme => {
CheckoutWebhookEventType::AuthenticationStarted => Self::EventNotSupported,
CheckoutWebhookEventType::AuthenticationApproved => Self::EventNotSupported,
CheckoutWebhookEventType::PaymentApproved => Self::EventNotSupported,
CheckoutWebhookEventType::PaymentCaptured => Self::PaymentIntentSuccess,
CheckoutWebhookEventType::PaymentDeclined => Self::PaymentIntentFailure,
CheckoutWebhookEventType::PaymentRefunded => Self::RefundSuccess,
CheckoutWebhookEventType::PaymentRefundDeclined => Self::RefundFailure,
CheckoutWebhookEventType::DisputeReceived
| CheckoutWebhookEventType::DisputeEvidenceRequired => Self::DisputeOpened,
CheckoutWebhookEventType::DisputeExpired => Self::DisputeExpired,
CheckoutWebhookEventType::DisputeAccepted => Self::DisputeAccepted,
CheckoutWebhookEventType::DisputeCanceled => Self::DisputeCancelled,
CheckoutWebhookEventType::DisputeEvidenceSubmitted
| CheckoutWebhookEventType::DisputeEvidenceAcknowledgedByScheme => {
Self::DisputeChallenged
}
CheckoutTransactionType::DisputeWon
| CheckoutTransactionType::DisputeArbitrationWon => Self::DisputeWon,
CheckoutTransactionType::DisputeLost
| CheckoutTransactionType::DisputeArbitrationLost => Self::DisputeLost,
CheckoutWebhookEventType::DisputeWon
| CheckoutWebhookEventType::DisputeArbitrationWon => Self::DisputeWon,
CheckoutWebhookEventType::DisputeLost
| CheckoutWebhookEventType::DisputeArbitrationLost => Self::DisputeLost,
CheckoutWebhookEventType::Unknown => Self::EventNotSupported,
}
}
}

View File

@ -184,6 +184,14 @@ pub enum ApiErrorResponse {
MissingFilePurpose,
#[error(error_type = ErrorType::InvalidRequestError, code = "HE_04", message = "File content type not found / valid")]
MissingFileContentType,
#[error(error_type = ErrorType::InvalidRequestError, code = "WE_01", message = "Failed to authenticate the webhook")]
WebhookAuthenticationFailed,
#[error(error_type = ErrorType::ObjectNotFound, code = "WE_04", message = "Webhook resource not found")]
WebhookResourceNotFound,
#[error(error_type = ErrorType::InvalidRequestError, code = "WE_02", message = "Bad request received in webhook")]
WebhookBadRequest,
#[error(error_type = ErrorType::RouterError, code = "WE_03", message = "There was some issue processing the webhook")]
WebhookProcessingFailure,
}
#[derive(Clone)]
@ -222,11 +230,12 @@ impl actix_web::ResponseError for ApiErrorResponse {
Self::Unauthorized
| Self::InvalidEphemeralKey
| Self::InvalidJwtToken
| Self::GenericUnauthorized { .. } => StatusCode::UNAUTHORIZED, // 401
| Self::GenericUnauthorized { .. }
| Self::WebhookAuthenticationFailed => StatusCode::UNAUTHORIZED, // 401
Self::ExternalConnectorError { status_code, .. } => {
StatusCode::from_u16(*status_code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
}
Self::InvalidRequestUrl => StatusCode::NOT_FOUND, // 404
Self::InvalidRequestUrl | Self::WebhookResourceNotFound => StatusCode::NOT_FOUND, // 404
Self::InvalidHttpMethod => StatusCode::METHOD_NOT_ALLOWED, // 405
Self::MissingRequiredField { .. }
| Self::InvalidDataValue { .. }
@ -250,9 +259,9 @@ impl actix_web::ResponseError for ApiErrorResponse {
| Self::PaymentUnexpectedState { .. }
| Self::MandateValidationFailed { .. } => StatusCode::BAD_REQUEST, // 400
Self::MandateUpdateFailed | Self::InternalServerError => {
StatusCode::INTERNAL_SERVER_ERROR
} // 500
Self::MandateUpdateFailed
| Self::InternalServerError
| Self::WebhookProcessingFailure => StatusCode::INTERNAL_SERVER_ERROR, // 500
Self::DuplicateRefundRequest | Self::DuplicatePayment { .. } => StatusCode::BAD_REQUEST, // 400
Self::RefundNotFound
| Self::CustomerNotFound
@ -275,7 +284,8 @@ impl actix_web::ResponseError for ApiErrorResponse {
| Self::NotSupported { .. }
| Self::FlowNotSupported { .. }
| Self::ApiKeyNotFound
| Self::DisputeStatusValidationFailed { .. } => StatusCode::BAD_REQUEST, // 400
| Self::DisputeStatusValidationFailed { .. }
| Self::WebhookBadRequest => StatusCode::BAD_REQUEST, // 400
Self::DuplicateMerchantAccount
| Self::DuplicateMerchantConnectorAccount { .. }
| Self::DuplicatePaymentMethod
@ -510,6 +520,18 @@ impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorRespon
Self::MissingDisputeId => {
AER::BadRequest(ApiError::new("HE", 2, "Dispute id not found in the request", None))
}
Self::WebhookAuthenticationFailed => {
AER::Unauthorized(ApiError::new("WE", 1, "Webhook authentication failed", None))
}
Self::WebhookResourceNotFound => {
AER::NotFound(ApiError::new("WE", 4, "Webhook resource was not found", None))
}
Self::WebhookBadRequest => {
AER::BadRequest(ApiError::new("WE", 2, "Bad request body received", None))
}
Self::WebhookProcessingFailure => {
AER::InternalServerError(ApiError::new("WE", 3, "There was an issue processing the webhook", None))
}
}
}
}

View File

@ -5,8 +5,9 @@ use common_utils::{crypto::SignMessage, ext_traits};
use error_stack::{report, IntoReport, ResultExt};
use masking::ExposeInterface;
use router_env::{instrument, tracing};
use utils::WebhookApiErrorSwitch;
use super::metrics;
use super::{errors::StorageErrorExt, metrics};
use crate::{
consts,
core::{
@ -32,7 +33,7 @@ pub async fn payments_incoming_webhook_flow<W: api::OutgoingWebhookType>(
merchant_account: storage::MerchantAccount,
webhook_details: api::IncomingWebhookDetails,
source_verified: bool,
) -> CustomResult<(), errors::WebhooksFlowError> {
) -> CustomResult<(), errors::ApiErrorResponse> {
let consume_or_trigger_flow = if source_verified {
payments::CallConnectorAction::HandleResponse(webhook_details.resource_object)
} else {
@ -56,10 +57,13 @@ pub async fn payments_incoming_webhook_flow<W: api::OutgoingWebhookType>(
services::AuthFlow::Merchant,
consume_or_trigger_flow,
)
.await
.change_context(errors::WebhooksFlowError::PaymentsCoreFailed)?
.await?
}
_ => Err(errors::WebhooksFlowError::PaymentsCoreFailed).into_report()?,
_ => Err(errors::ApiErrorResponse::WebhookProcessingFailure)
.into_report()
.attach_printable(
"Did not get payment id as object reference id in webhook payments flow",
)?,
};
match payments_response {
@ -68,13 +72,15 @@ pub async fn payments_incoming_webhook_flow<W: api::OutgoingWebhookType>(
.payment_id
.clone()
.get_required_value("payment_id")
.change_context(errors::WebhooksFlowError::PaymentsCoreFailed)?;
.change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
.attach_printable("payment id not received from payments core")?;
let event_type: enums::EventType = payments_response
.status
.foreign_try_into()
.into_report()
.change_context(errors::WebhooksFlowError::PaymentsCoreFailed)?;
.change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
.attach_printable("payment event type mapping failed")?;
create_event_and_trigger_outgoing_webhook::<W>(
state,
@ -89,7 +95,9 @@ pub async fn payments_incoming_webhook_flow<W: api::OutgoingWebhookType>(
.await?;
}
_ => Err(errors::WebhooksFlowError::PaymentsCoreFailed).into_report()?,
_ => Err(errors::ApiErrorResponse::WebhookProcessingFailure)
.into_report()
.attach_printable("received non-json response from payments core")?,
}
Ok(())
@ -103,7 +111,7 @@ pub async fn refunds_incoming_webhook_flow<W: api::OutgoingWebhookType>(
connector_name: &str,
source_verified: bool,
event_type: api_models::webhooks::IncomingWebhookEvent,
) -> CustomResult<(), errors::WebhooksFlowError> {
) -> CustomResult<(), errors::ApiErrorResponse> {
let db = &*state.store;
//find refund by connector refund id
let refund = match webhook_details.object_reference_id {
@ -117,7 +125,7 @@ pub async fn refunds_incoming_webhook_flow<W: api::OutgoingWebhookType>(
merchant_account.storage_scheme,
)
.await
.change_context(errors::WebhooksFlowError::ResourceNotFound)
.change_context(errors::ApiErrorResponse::WebhookResourceNotFound)
.attach_printable_lazy(|| "Failed fetching the refund")?,
api_models::webhooks::ObjectReferenceId::RefundId(
api_models::webhooks::RefundIdType::RefundId(id),
@ -128,9 +136,11 @@ pub async fn refunds_incoming_webhook_flow<W: api::OutgoingWebhookType>(
merchant_account.storage_scheme,
)
.await
.change_context(errors::WebhooksFlowError::ResourceNotFound)
.change_context(errors::ApiErrorResponse::WebhookResourceNotFound)
.attach_printable_lazy(|| "Failed fetching the refund")?,
_ => Err(errors::WebhooksFlowError::RefundsCoreFailed).into_report()?,
_ => Err(errors::ApiErrorResponse::WebhookProcessingFailure)
.into_report()
.attach_printable("received a non-refund id when processing refund webhooks")?,
};
let refund_id = refund.refund_id.to_owned();
//if source verified then update refund status else trigger refund sync
@ -141,7 +151,8 @@ pub async fn refunds_incoming_webhook_flow<W: api::OutgoingWebhookType>(
refund_status: event_type
.foreign_try_into()
.into_report()
.change_context(errors::WebhooksFlowError::RefundsCoreFailed)?,
.change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
.attach_printable("failed refund status mapping from event type")?,
};
state
.store
@ -151,7 +162,7 @@ pub async fn refunds_incoming_webhook_flow<W: api::OutgoingWebhookType>(
merchant_account.storage_scheme,
)
.await
.change_context(errors::WebhooksFlowError::RefundsCoreFailed)
.to_not_found_response(errors::ApiErrorResponse::WebhookResourceNotFound)
.attach_printable_lazy(|| {
format!(
"Failed while updating refund: refund_id: {}",
@ -169,7 +180,6 @@ pub async fn refunds_incoming_webhook_flow<W: api::OutgoingWebhookType>(
},
)
.await
.change_context(errors::WebhooksFlowError::RefundsCoreFailed)
.attach_printable_lazy(|| {
format!(
"Failed while updating refund: refund_id: {}",
@ -181,7 +191,8 @@ pub async fn refunds_incoming_webhook_flow<W: api::OutgoingWebhookType>(
.refund_status
.foreign_try_into()
.into_report()
.change_context(errors::WebhooksFlowError::RefundsCoreFailed)?;
.change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
.attach_printable("refund status to event type mapping failed")?;
let refund_response: api_models::refunds::RefundResponse = updated_refund.foreign_into();
create_event_and_trigger_outgoing_webhook::<W>(
state,
@ -201,7 +212,7 @@ pub async fn get_payment_attempt_from_object_reference_id(
state: &AppState,
object_reference_id: api_models::webhooks::ObjectReferenceId,
merchant_account: &storage::MerchantAccount,
) -> CustomResult<storage_models::payment_attempt::PaymentAttempt, errors::WebhooksFlowError> {
) -> CustomResult<storage_models::payment_attempt::PaymentAttempt, errors::ApiErrorResponse> {
let db = &*state.store;
match object_reference_id {
api::ObjectReferenceId::PaymentId(api::PaymentIdType::ConnectorTransactionId(ref id)) => db
@ -211,7 +222,7 @@ pub async fn get_payment_attempt_from_object_reference_id(
merchant_account.storage_scheme,
)
.await
.change_context(errors::WebhooksFlowError::ResourceNotFound),
.to_not_found_response(errors::ApiErrorResponse::WebhookResourceNotFound),
api::ObjectReferenceId::PaymentId(api::PaymentIdType::PaymentAttemptId(ref id)) => db
.find_payment_attempt_by_attempt_id_merchant_id(
id,
@ -219,7 +230,7 @@ pub async fn get_payment_attempt_from_object_reference_id(
merchant_account.storage_scheme,
)
.await
.change_context(errors::WebhooksFlowError::ResourceNotFound),
.to_not_found_response(errors::ApiErrorResponse::WebhookResourceNotFound),
api::ObjectReferenceId::PaymentId(api::PaymentIdType::PreprocessingId(ref id)) => db
.find_payment_attempt_by_preprocessing_id_merchant_id(
id,
@ -227,8 +238,10 @@ pub async fn get_payment_attempt_from_object_reference_id(
merchant_account.storage_scheme,
)
.await
.change_context(errors::WebhooksFlowError::ResourceNotFound),
_ => Err(errors::WebhooksFlowError::ResourceNotFound).into_report(),
.to_not_found_response(errors::ApiErrorResponse::WebhookResourceNotFound),
_ => Err(errors::ApiErrorResponse::WebhookProcessingFailure)
.into_report()
.attach_printable("received a non-payment id for retrieving payment")?,
}
}
@ -240,7 +253,7 @@ pub async fn get_or_update_dispute_object(
payment_attempt: &storage_models::payment_attempt::PaymentAttempt,
event_type: api_models::webhooks::IncomingWebhookEvent,
connector_name: &str,
) -> CustomResult<storage_models::dispute::Dispute, errors::WebhooksFlowError> {
) -> CustomResult<storage_models::dispute::Dispute, errors::ApiErrorResponse> {
let db = &*state.store;
match option_dispute {
None => {
@ -254,7 +267,8 @@ pub async fn get_or_update_dispute_object(
dispute_status: event_type
.foreign_try_into()
.into_report()
.change_context(errors::WebhooksFlowError::DisputeCoreFailed)?,
.change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
.attach_printable("event type to dispute status mapping failed")?,
payment_id: payment_attempt.payment_id.to_owned(),
connector: connector_name.to_owned(),
attempt_id: payment_attempt.attempt_id.to_owned(),
@ -272,7 +286,7 @@ pub async fn get_or_update_dispute_object(
.store
.insert_dispute(new_dispute.clone())
.await
.change_context(errors::WebhooksFlowError::WebhookEventCreationFailed)
.to_not_found_response(errors::ApiErrorResponse::WebhookResourceNotFound)
}
Some(dispute) => {
logger::info!("Dispute Already exists, Updating the dispute details");
@ -280,13 +294,16 @@ pub async fn get_or_update_dispute_object(
let dispute_status: storage_models::enums::DisputeStatus = event_type
.foreign_try_into()
.into_report()
.change_context(errors::WebhooksFlowError::DisputeCoreFailed)?;
.change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
.attach_printable("event type to dispute state conversion failure")?;
crate::core::utils::validate_dispute_stage_and_dispute_status(
dispute.dispute_stage.foreign_into(),
dispute.dispute_status.foreign_into(),
dispute_details.dispute_stage.clone(),
dispute_status.foreign_into(),
)?;
)
.change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
.attach_printable("dispute stage and status validation failed")?;
let update_dispute = storage_models::dispute::DisputeUpdate::Update {
dispute_stage: dispute_details.dispute_stage.foreign_into(),
dispute_status,
@ -298,7 +315,7 @@ pub async fn get_or_update_dispute_object(
};
db.update_dispute(dispute, update_dispute)
.await
.change_context(errors::WebhooksFlowError::ResourceNotFound)
.to_not_found_response(errors::ApiErrorResponse::WebhookResourceNotFound)
}
}
}
@ -312,13 +329,11 @@ pub async fn disputes_incoming_webhook_flow<W: api::OutgoingWebhookType>(
connector: &(dyn api::Connector + Sync),
request_details: &api::IncomingWebhookRequestDetails<'_>,
event_type: api_models::webhooks::IncomingWebhookEvent,
) -> CustomResult<(), errors::WebhooksFlowError> {
) -> CustomResult<(), errors::ApiErrorResponse> {
metrics::INCOMING_DISPUTE_WEBHOOK_METRIC.add(&metrics::CONTEXT, 1, &[]);
if source_verified {
let db = &*state.store;
let dispute_details = connector
.get_dispute_details(request_details)
.change_context(errors::WebhooksFlowError::WebhookEventObjectCreationFailed)?;
let dispute_details = connector.get_dispute_details(request_details).switch()?;
let payment_attempt = get_payment_attempt_from_object_reference_id(
&state,
webhook_details.object_reference_id,
@ -332,7 +347,7 @@ pub async fn disputes_incoming_webhook_flow<W: api::OutgoingWebhookType>(
&dispute_details.connector_dispute_id,
)
.await
.change_context(errors::WebhooksFlowError::ResourceNotFound)?;
.to_not_found_response(errors::ApiErrorResponse::WebhookResourceNotFound)?;
let dispute_object = get_or_update_dispute_object(
state.clone(),
option_dispute,
@ -348,7 +363,8 @@ pub async fn disputes_incoming_webhook_flow<W: api::OutgoingWebhookType>(
.dispute_status
.foreign_try_into()
.into_report()
.change_context(errors::WebhooksFlowError::DisputeCoreFailed)?;
.change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
.attach_printable("failed to map dispute status to event type")?;
create_event_and_trigger_outgoing_webhook::<W>(
state,
merchant_account,
@ -364,7 +380,7 @@ pub async fn disputes_incoming_webhook_flow<W: api::OutgoingWebhookType>(
Ok(())
} else {
metrics::INCOMING_DISPUTE_WEBHOOK_SIGNATURE_FAILURE_METRIC.add(&metrics::CONTEXT, 1, &[]);
Err(errors::WebhooksFlowError::WebhookSourceVerificationFailed).into_report()
Err(errors::ApiErrorResponse::WebhookAuthenticationFailed).into_report()
}
}
@ -373,7 +389,7 @@ async fn bank_transfer_webhook_flow<W: api::OutgoingWebhookType>(
merchant_account: storage::MerchantAccount,
webhook_details: api::IncomingWebhookDetails,
source_verified: bool,
) -> CustomResult<(), errors::WebhooksFlowError> {
) -> CustomResult<(), errors::ApiErrorResponse> {
let response = if source_verified {
let payment_attempt = get_payment_attempt_from_object_reference_id(
&state,
@ -398,10 +414,9 @@ async fn bank_transfer_webhook_flow<W: api::OutgoingWebhookType>(
payments::CallConnectorAction::Trigger,
)
.await
.change_context(errors::WebhooksFlowError::PaymentsCoreFailed)
} else {
Err(report!(
errors::WebhooksFlowError::WebhookSourceVerificationFailed
errors::ApiErrorResponse::WebhookAuthenticationFailed
))
};
@ -411,13 +426,15 @@ async fn bank_transfer_webhook_flow<W: api::OutgoingWebhookType>(
.payment_id
.clone()
.get_required_value("payment_id")
.change_context(errors::WebhooksFlowError::PaymentsCoreFailed)?;
.change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
.attach_printable("did not receive payment id from payments core response")?;
let event_type: enums::EventType = payments_response
.status
.foreign_try_into()
.into_report()
.change_context(errors::WebhooksFlowError::PaymentsCoreFailed)?;
.change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
.attach_printable("error mapping payments response status to event type")?;
create_event_and_trigger_outgoing_webhook::<W>(
state,
@ -432,7 +449,9 @@ async fn bank_transfer_webhook_flow<W: api::OutgoingWebhookType>(
.await?;
}
_ => Err(errors::WebhooksFlowError::PaymentsCoreFailed).into_report()?,
_ => Err(errors::ApiErrorResponse::WebhookProcessingFailure)
.into_report()
.attach_printable("received non-json response from payments core")?,
}
Ok(())
@ -449,7 +468,7 @@ pub async fn create_event_and_trigger_outgoing_webhook<W: api::OutgoingWebhookTy
primary_object_id: String,
primary_object_type: enums::EventObjectType,
content: api::OutgoingWebhookContent,
) -> CustomResult<(), errors::WebhooksFlowError> {
) -> CustomResult<(), errors::ApiErrorResponse> {
let new_event = storage::EventNew {
event_id: generate_id(consts::ID_LENGTH, "evt"),
event_type,
@ -464,12 +483,14 @@ pub async fn create_event_and_trigger_outgoing_webhook<W: api::OutgoingWebhookTy
.store
.insert_event(new_event)
.await
.change_context(errors::WebhooksFlowError::WebhookEventCreationFailed)?;
.change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
.attach_printable("event insertion failure")?;
if state.conf.webhooks.outgoing_enabled {
let arbiter = actix::Arbiter::try_current()
.ok_or(errors::WebhooksFlowError::ForkFlowFailed)
.into_report()?;
.ok_or(errors::ApiErrorResponse::WebhookProcessingFailure)
.into_report()
.attach_printable("arbiter retrieval failure")?;
let outgoing_webhook = api::OutgoingWebhook {
merchant_id: merchant_account.merchant_id.clone(),
@ -481,7 +502,8 @@ pub async fn create_event_and_trigger_outgoing_webhook<W: api::OutgoingWebhookTy
let webhook_signature_payload =
ext_traits::Encode::<serde_json::Value>::encode_to_string_of_json(&outgoing_webhook)
.change_context(errors::WebhooksFlowError::OutgoingWebhookEncodingFailed)?;
.change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
.attach_printable("failed encoding outgoing webhook payload")?;
let outgoing_webhooks_signature = merchant_account
.payment_response_hash_key
@ -494,7 +516,7 @@ pub async fn create_event_and_trigger_outgoing_webhook<W: api::OutgoingWebhookTy
)
})
.transpose()
.change_context(errors::WebhooksFlowError::OutgoingWebhookSigningFailed)
.change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
.attach_printable("Failed to sign the message")?
.map(hex::encode);
@ -625,7 +647,7 @@ pub async fn webhooks_core<W: api::OutgoingWebhookType>(
&merchant_account.merchant_id,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.switch()
.attach_printable("There was an error in incoming webhook body decoding")?;
request_details.body = &decoded_body;
@ -646,7 +668,8 @@ pub async fn webhooks_core<W: api::OutgoingWebhookType>(
logger::info!(process_webhook=?process_webhook_further);
logger::info!(event_type=?event_type);
if process_webhook_further {
let flow_type: api::WebhookFlow = event_type.to_owned().into();
if process_webhook_further && !matches!(flow_type, api::WebhookFlow::ReturnResponse) {
let source_verified = connector
.verify_webhook_source(
&*state.store,
@ -654,17 +677,17 @@ pub async fn webhooks_core<W: api::OutgoingWebhookType>(
&merchant_account.merchant_id,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.switch()
.attach_printable("There was an issue in incoming webhook source verification")?;
let object_ref_id = connector
.get_webhook_object_reference_id(&request_details)
.change_context(errors::ApiErrorResponse::InternalServerError)
.switch()
.attach_printable("Could not find object reference id in incoming webhook body")?;
let event_object = connector
.get_webhook_resource_object(&request_details)
.change_context(errors::ApiErrorResponse::InternalServerError)
.switch()
.attach_printable("Could not find resource object in incoming webhook body")?;
let webhook_details = api::IncomingWebhookDetails {
@ -676,7 +699,6 @@ pub async fn webhooks_core<W: api::OutgoingWebhookType>(
)?,
};
let flow_type: api::WebhookFlow = event_type.to_owned().into();
match flow_type {
api::WebhookFlow::Payment => payments_incoming_webhook_flow::<W>(
state.clone(),
@ -685,7 +707,6 @@ pub async fn webhooks_core<W: api::OutgoingWebhookType>(
source_verified,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Incoming webhook flow for payments failed")?,
api::WebhookFlow::Refund => refunds_incoming_webhook_flow::<W>(
@ -697,7 +718,6 @@ pub async fn webhooks_core<W: api::OutgoingWebhookType>(
event_type,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Incoming webhook flow for refunds failed")?,
api::WebhookFlow::Dispute => disputes_incoming_webhook_flow::<W>(
@ -710,7 +730,6 @@ pub async fn webhooks_core<W: api::OutgoingWebhookType>(
event_type,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Incoming webhook flow for disputes failed")?,
api::WebhookFlow::BankTransfer => bank_transfer_webhook_flow::<W>(
@ -720,7 +739,6 @@ pub async fn webhooks_core<W: api::OutgoingWebhookType>(
source_verified,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Incoming bank-transfer webhook flow failed")?,
api::WebhookFlow::ReturnResponse => {}
@ -733,7 +751,7 @@ pub async fn webhooks_core<W: api::OutgoingWebhookType>(
let response = connector
.get_webhook_api_response(&request_details)
.change_context(errors::ApiErrorResponse::InternalServerError)
.switch()
.attach_printable("Could not get incoming webhook api response from connector")?;
Ok(response)

View File

@ -1,4 +1,7 @@
use error_stack::ResultExt;
use crate::{
core::errors,
db::{get_and_deserialize_key, StorageInterface},
types::api,
};
@ -45,3 +48,30 @@ pub async fn lookup_webhook_event(
}
}
}
pub trait WebhookApiErrorSwitch<T> {
fn switch(self) -> errors::RouterResult<T>;
}
impl<T> WebhookApiErrorSwitch<T> for errors::CustomResult<T, errors::ConnectorError> {
fn switch(self) -> errors::RouterResult<T> {
match self {
Ok(res) => Ok(res),
Err(e) => match e.current_context() {
errors::ConnectorError::WebhookSourceVerificationFailed => {
Err(e).change_context(errors::ApiErrorResponse::WebhookAuthenticationFailed)
}
errors::ConnectorError::WebhookSignatureNotFound
| errors::ConnectorError::WebhookReferenceIdNotFound
| errors::ConnectorError::WebhookEventTypeNotFound
| errors::ConnectorError::WebhookResourceObjectNotFound
| errors::ConnectorError::WebhookBodyDecodingFailed => {
Err(e).change_context(errors::ApiErrorResponse::WebhookBadRequest)
}
_ => Err(e).change_context(errors::ApiErrorResponse::InternalServerError),
},
}
}
}