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:
saiharsha-juspay
2023-03-30 04:25:54 +05:30
committed by GitHub
parent fb66a0e0f2
commit a733eafbbe
28 changed files with 1139 additions and 32 deletions

View File

@ -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(&notif.event_code) {
return Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
api_models::payments::PaymentIdType::ConnectorTransactionId(notif.psp_reference),
));
}
if adyen::is_refund_event(&notif.event_code) {
return Ok(api_models::webhooks::ObjectReferenceId::RefundId(
api_models::webhooks::RefundIdType::ConnectorRefundId(notif.psp_reference),
));
}
if adyen::is_chargeback_event(&notif.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,
})
}
}

View File

@ -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)]

View File

@ -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 {

View File

@ -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)]