mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-04 05:59:48 +08:00
feat(router): added incoming dispute webhooks flow (#769)
Co-authored-by: Sangamesh <sangamesh.kulkarni@juspay.in> Co-authored-by: sai harsha <sai.harsha@sai.harsha-MacBookPro> Co-authored-by: Arun Raj M <jarnura47@gmail.com>
This commit is contained in:
@ -2,6 +2,7 @@ mod transformers;
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
use api_models::webhooks::IncomingWebhookEvent;
|
||||
use base64::Engine;
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
use router_env::{instrument, tracing};
|
||||
@ -17,6 +18,7 @@ use crate::{
|
||||
types::{
|
||||
self,
|
||||
api::{self, ConnectorCommon},
|
||||
transformers::ForeignFrom,
|
||||
},
|
||||
utils::{self, crypto, ByteSliceExt, BytesExt, OptionExt},
|
||||
};
|
||||
@ -719,27 +721,38 @@ impl api::IncomingWebhook for Adyen {
|
||||
) -> CustomResult<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> {
|
||||
let notif = get_webhook_object_from_body(request.body)
|
||||
.change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?;
|
||||
match notif.event_code {
|
||||
adyen::WebhookEventCode::Authorisation => {
|
||||
Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
|
||||
api_models::payments::PaymentIdType::ConnectorTransactionId(
|
||||
notif.psp_reference,
|
||||
),
|
||||
))
|
||||
}
|
||||
_ => Ok(api_models::webhooks::ObjectReferenceId::RefundId(
|
||||
api_models::webhooks::RefundIdType::ConnectorRefundId(notif.psp_reference),
|
||||
)),
|
||||
if adyen::is_transaction_event(¬if.event_code) {
|
||||
return Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
|
||||
api_models::payments::PaymentIdType::ConnectorTransactionId(notif.psp_reference),
|
||||
));
|
||||
}
|
||||
if adyen::is_refund_event(¬if.event_code) {
|
||||
return Ok(api_models::webhooks::ObjectReferenceId::RefundId(
|
||||
api_models::webhooks::RefundIdType::ConnectorRefundId(notif.psp_reference),
|
||||
));
|
||||
}
|
||||
if adyen::is_chargeback_event(¬if.event_code) {
|
||||
return Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
|
||||
api_models::payments::PaymentIdType::ConnectorTransactionId(
|
||||
notif
|
||||
.original_reference
|
||||
.ok_or(errors::ConnectorError::WebhookReferenceIdNotFound)?,
|
||||
),
|
||||
));
|
||||
}
|
||||
Err(errors::ConnectorError::WebhookReferenceIdNotFound).into_report()
|
||||
}
|
||||
|
||||
fn get_webhook_event_type(
|
||||
&self,
|
||||
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
|
||||
) -> CustomResult<IncomingWebhookEvent, errors::ConnectorError> {
|
||||
let notif = get_webhook_object_from_body(request.body)
|
||||
.change_context(errors::ConnectorError::WebhookEventTypeNotFound)?;
|
||||
Ok(notif.event_code.into())
|
||||
Ok(IncomingWebhookEvent::foreign_from((
|
||||
notif.event_code,
|
||||
notif.additional_data.dispute_status,
|
||||
)))
|
||||
}
|
||||
|
||||
fn get_webhook_resource_object(
|
||||
@ -767,4 +780,24 @@ impl api::IncomingWebhook for Adyen {
|
||||
"[accepted]".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn get_dispute_details(
|
||||
&self,
|
||||
request: &api_models::webhooks::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<api::disputes::DisputePayload, errors::ConnectorError> {
|
||||
let notif = get_webhook_object_from_body(request.body)
|
||||
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
|
||||
Ok(api::disputes::DisputePayload {
|
||||
amount: notif.amount.value.to_string(),
|
||||
currency: notif.amount.currency,
|
||||
dispute_stage: api_models::enums::DisputeStage::from(notif.event_code.clone()),
|
||||
connector_dispute_id: notif.psp_reference,
|
||||
connector_reason: notif.reason,
|
||||
connector_reason_code: notif.additional_data.chargeback_reason_code,
|
||||
challenge_required_by: notif.additional_data.defense_period_ends_at,
|
||||
connector_status: notif.event_code.to_string(),
|
||||
created_at: notif.event_date.clone(),
|
||||
updated_at: notif.event_date,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use api_models::webhooks::IncomingWebhookEvent;
|
||||
use api_models::{enums::DisputeStage, webhooks::IncomingWebhookEvent};
|
||||
use masking::PeekInterface;
|
||||
use reqwest::Url;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -13,6 +13,7 @@ use crate::{
|
||||
self,
|
||||
api::{self, enums as api_enums},
|
||||
storage::enums as storage_enums,
|
||||
transformers::ForeignFrom,
|
||||
},
|
||||
};
|
||||
|
||||
@ -1148,10 +1149,22 @@ pub struct ErrorResponse {
|
||||
// }
|
||||
// }
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub enum DisputeStatus {
|
||||
Undefended,
|
||||
Pending,
|
||||
Lost,
|
||||
Accepted,
|
||||
Won,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdyenAdditionalDataWH {
|
||||
pub hmac_signature: String,
|
||||
pub dispute_status: Option<DisputeStatus>,
|
||||
pub chargeback_reason_code: Option<String>,
|
||||
pub defense_period_ends_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@ -1160,7 +1173,7 @@ pub struct AdyenAmountWH {
|
||||
pub currency: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, strum::Display)]
|
||||
#[derive(Clone, Debug, Deserialize, strum::Display)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum WebhookEventCode {
|
||||
@ -1168,15 +1181,73 @@ pub enum WebhookEventCode {
|
||||
Refund,
|
||||
CancelOrRefund,
|
||||
RefundFailed,
|
||||
NotificationOfChargeback,
|
||||
Chargeback,
|
||||
ChargebackReversed,
|
||||
SecondChargeback,
|
||||
PrearbitrationWon,
|
||||
PrearbitrationLost,
|
||||
}
|
||||
|
||||
impl From<WebhookEventCode> for IncomingWebhookEvent {
|
||||
pub fn is_transaction_event(event_code: &WebhookEventCode) -> bool {
|
||||
matches!(event_code, WebhookEventCode::Authorisation)
|
||||
}
|
||||
|
||||
pub fn is_refund_event(event_code: &WebhookEventCode) -> bool {
|
||||
matches!(
|
||||
event_code,
|
||||
WebhookEventCode::Refund
|
||||
| WebhookEventCode::CancelOrRefund
|
||||
| WebhookEventCode::RefundFailed
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_chargeback_event(event_code: &WebhookEventCode) -> bool {
|
||||
matches!(
|
||||
event_code,
|
||||
WebhookEventCode::NotificationOfChargeback
|
||||
| WebhookEventCode::Chargeback
|
||||
| WebhookEventCode::ChargebackReversed
|
||||
| WebhookEventCode::SecondChargeback
|
||||
| WebhookEventCode::PrearbitrationWon
|
||||
| WebhookEventCode::PrearbitrationLost
|
||||
)
|
||||
}
|
||||
|
||||
impl ForeignFrom<(WebhookEventCode, Option<DisputeStatus>)> for IncomingWebhookEvent {
|
||||
fn foreign_from((code, status): (WebhookEventCode, Option<DisputeStatus>)) -> Self {
|
||||
match (code, status) {
|
||||
(WebhookEventCode::Authorisation, _) => Self::PaymentIntentSuccess,
|
||||
(WebhookEventCode::Refund, _) => Self::RefundSuccess,
|
||||
(WebhookEventCode::CancelOrRefund, _) => Self::RefundSuccess,
|
||||
(WebhookEventCode::RefundFailed, _) => Self::RefundFailure,
|
||||
(WebhookEventCode::NotificationOfChargeback, _) => Self::DisputeOpened,
|
||||
(WebhookEventCode::Chargeback, None) => Self::DisputeLost,
|
||||
(WebhookEventCode::Chargeback, Some(DisputeStatus::Won)) => Self::DisputeWon,
|
||||
(WebhookEventCode::Chargeback, Some(DisputeStatus::Lost)) => Self::DisputeLost,
|
||||
(WebhookEventCode::Chargeback, Some(_)) => Self::DisputeOpened,
|
||||
(WebhookEventCode::ChargebackReversed, Some(DisputeStatus::Pending)) => {
|
||||
Self::DisputeChallenged
|
||||
}
|
||||
(WebhookEventCode::ChargebackReversed, _) => Self::DisputeWon,
|
||||
(WebhookEventCode::SecondChargeback, _) => Self::DisputeLost,
|
||||
(WebhookEventCode::PrearbitrationWon, Some(DisputeStatus::Pending)) => {
|
||||
Self::DisputeOpened
|
||||
}
|
||||
(WebhookEventCode::PrearbitrationWon, _) => Self::DisputeWon,
|
||||
(WebhookEventCode::PrearbitrationLost, _) => Self::DisputeLost,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WebhookEventCode> for DisputeStage {
|
||||
fn from(code: WebhookEventCode) -> Self {
|
||||
match code {
|
||||
WebhookEventCode::Authorisation => Self::PaymentIntentSuccess,
|
||||
WebhookEventCode::Refund => Self::RefundSuccess,
|
||||
WebhookEventCode::CancelOrRefund => Self::RefundSuccess,
|
||||
WebhookEventCode::RefundFailed => Self::RefundFailure,
|
||||
WebhookEventCode::NotificationOfChargeback => Self::PreDispute,
|
||||
WebhookEventCode::SecondChargeback => Self::PreArbitration,
|
||||
WebhookEventCode::PrearbitrationWon => Self::PreArbitration,
|
||||
WebhookEventCode::PrearbitrationLost => Self::PreArbitration,
|
||||
_ => Self::Dispute,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1192,6 +1263,8 @@ pub struct AdyenNotificationRequestItemWH {
|
||||
pub merchant_account_code: String,
|
||||
pub merchant_reference: String,
|
||||
pub success: String,
|
||||
pub reason: Option<String>,
|
||||
pub event_date: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
||||
@ -598,7 +598,7 @@ impl api::IncomingWebhook for Trustpay {
|
||||
match details.payment_information.credit_debit_indicator {
|
||||
trustpay::CreditDebitIndicator::Crdt => {
|
||||
Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
|
||||
api_models::payments::PaymentIdType::PaymentIntentId(
|
||||
api_models::payments::PaymentIdType::PaymentAttemptId(
|
||||
details.payment_information.references.merchant_reference,
|
||||
),
|
||||
))
|
||||
@ -606,7 +606,7 @@ impl api::IncomingWebhook for Trustpay {
|
||||
trustpay::CreditDebitIndicator::Dbit => {
|
||||
if details.payment_information.status == trustpay::WebhookStatus::Chargebacked {
|
||||
Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
|
||||
api_models::payments::PaymentIdType::PaymentIntentId(
|
||||
api_models::payments::PaymentIdType::PaymentAttemptId(
|
||||
details.payment_information.references.merchant_reference,
|
||||
),
|
||||
))
|
||||
@ -648,6 +648,9 @@ impl api::IncomingWebhook for Trustpay {
|
||||
(trustpay::CreditDebitIndicator::Dbit, trustpay::WebhookStatus::Rejected) => {
|
||||
Ok(api_models::webhooks::IncomingWebhookEvent::RefundFailure)
|
||||
}
|
||||
(trustpay::CreditDebitIndicator::Dbit, trustpay::WebhookStatus::Chargebacked) => {
|
||||
Ok(api_models::webhooks::IncomingWebhookEvent::DisputeLost)
|
||||
}
|
||||
_ => Err(errors::ConnectorError::WebhookEventTypeNotFound).into_report()?,
|
||||
}
|
||||
}
|
||||
@ -716,6 +719,30 @@ impl api::IncomingWebhook for Trustpay {
|
||||
.change_context(errors::ConnectorError::WebhookVerificationSecretNotFound)?;
|
||||
Ok(secret)
|
||||
}
|
||||
|
||||
fn get_dispute_details(
|
||||
&self,
|
||||
request: &api_models::webhooks::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<api::disputes::DisputePayload, errors::ConnectorError> {
|
||||
let trustpay_response: trustpay::TrustpayWebhookResponse = request
|
||||
.body
|
||||
.parse_struct("TrustpayWebhookResponse")
|
||||
.switch()?;
|
||||
let payment_info = trustpay_response.payment_information;
|
||||
let reason = payment_info.status_reason_information.unwrap_or_default();
|
||||
Ok(api::disputes::DisputePayload {
|
||||
amount: payment_info.amount.amount.to_string(),
|
||||
currency: payment_info.amount.currency,
|
||||
dispute_stage: api_models::enums::DisputeStage::Dispute,
|
||||
connector_dispute_id: payment_info.references.payment_id,
|
||||
connector_reason: reason.reason.reject_reason,
|
||||
connector_reason_code: Some(reason.reason.code),
|
||||
challenge_required_by: None,
|
||||
connector_status: payment_info.status.to_string(),
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl services::ConnectorRedirectResponse for Trustpay {
|
||||
|
||||
@ -219,7 +219,7 @@ fn get_card_request_data(
|
||||
cvv: ccard.card_cvc.clone(),
|
||||
expiry_date: ccard.get_card_expiry_month_year_2_digit_with_delimiter("/".to_owned()),
|
||||
cardholder: ccard.card_holder_name.clone(),
|
||||
reference: item.payment_id.clone(),
|
||||
reference: item.attempt_id.clone(),
|
||||
redirect_url: return_url,
|
||||
billing_city: params.billing_city,
|
||||
billing_country: params.billing_country,
|
||||
@ -260,7 +260,7 @@ fn get_bank_redirection_request_data(
|
||||
currency: item.request.currency.to_string(),
|
||||
},
|
||||
references: References {
|
||||
merchant_reference: item.payment_id.clone(),
|
||||
merchant_reference: item.attempt_id.clone(),
|
||||
},
|
||||
},
|
||||
callback_urls: CallbackURLs {
|
||||
@ -1115,12 +1115,11 @@ pub enum CreditDebitIndicator {
|
||||
Dbit,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(strum::Display, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum WebhookStatus {
|
||||
Paid,
|
||||
Rejected,
|
||||
Refunded,
|
||||
// TODO (Handle Chargebacks)
|
||||
Chargebacked,
|
||||
}
|
||||
|
||||
@ -1151,15 +1150,25 @@ impl TryFrom<WebhookStatus> for storage_models::enums::RefundStatus {
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct WebhookReferences {
|
||||
pub merchant_reference: String,
|
||||
pub payment_id: String,
|
||||
pub payment_request_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct WebhookAmount {
|
||||
pub amount: f64,
|
||||
pub currency: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct WebhookPaymentInformation {
|
||||
pub credit_debit_indicator: CreditDebitIndicator,
|
||||
pub references: WebhookReferences,
|
||||
pub status: WebhookStatus,
|
||||
pub amount: WebhookAmount,
|
||||
pub status_reason_information: Option<StatusReasonInformation>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
|
||||
Reference in New Issue
Block a user