From db011f3d7690458c64c8bba75920b0646b502646 Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Wed, 28 Jun 2023 19:37:17 +0530 Subject: [PATCH] feat(connector): Add refund and dispute webhooks for Rapyd (#1313) --- .../src/connector/checkout/transformers.rs | 6 +- crates/router/src/connector/rapyd.rs | 90 ++++++++++++++++--- .../src/connector/rapyd/transformers.rs | 56 +++++++++--- 3 files changed, 124 insertions(+), 28 deletions(-) diff --git a/crates/router/src/connector/checkout/transformers.rs b/crates/router/src/connector/checkout/transformers.rs index d5f4bb01d4..a8909cca66 100644 --- a/crates/router/src/connector/checkout/transformers.rs +++ b/crates/router/src/connector/checkout/transformers.rs @@ -757,10 +757,10 @@ pub struct CheckoutDisputeWebhookData { pub action_id: Option, pub amount: i32, pub currency: String, - #[serde(with = "common_utils::custom_serde::iso8601::option")] + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub evidence_required_by: Option, pub reason_code: Option, - #[serde(with = "common_utils::custom_serde::iso8601::option")] + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub date: Option, } #[derive(Debug, Deserialize)] @@ -768,7 +768,7 @@ pub struct CheckoutDisputeWebhookBody { #[serde(rename = "type")] pub transaction_type: CheckoutTransactionType, pub data: CheckoutDisputeWebhookData, - #[serde(with = "common_utils::custom_serde::iso8601::option")] + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub created_on: Option, } #[derive(Debug, Deserialize, strum::Display, Clone)] diff --git a/crates/router/src/connector/rapyd.rs b/crates/router/src/connector/rapyd.rs index 43337c9ced..118a5f7858 100644 --- a/crates/router/src/connector/rapyd.rs +++ b/crates/router/src/connector/rapyd.rs @@ -813,16 +813,23 @@ impl api::IncomingWebhook for Rapyd { .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; Ok(match webhook.data { - transformers::WebhookData::PaymentData(payment_data) => { + transformers::WebhookData::Payment(payment_data) => { api_models::webhooks::ObjectReferenceId::PaymentId( api_models::payments::PaymentIdType::ConnectorTransactionId(payment_data.id), ) } - transformers::WebhookData::RefundData(refund_data) => { + transformers::WebhookData::Refund(refund_data) => { api_models::webhooks::ObjectReferenceId::RefundId( api_models::webhooks::RefundIdType::ConnectorRefundId(refund_data.id), ) } + transformers::WebhookData::Dispute(dispute_data) => { + api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::ConnectorTransactionId( + dispute_data.original_transaction_id, + ), + ) + } }) } @@ -834,8 +841,32 @@ impl api::IncomingWebhook for Rapyd { .body .parse_struct("RapydIncomingWebhook") .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; - - webhook.webhook_type.try_into() + Ok(match webhook.webhook_type { + rapyd::RapydWebhookObjectEventType::PaymentCompleted + | rapyd::RapydWebhookObjectEventType::PaymentCaptured => { + api::IncomingWebhookEvent::PaymentIntentSuccess + } + rapyd::RapydWebhookObjectEventType::PaymentFailed => { + api::IncomingWebhookEvent::PaymentIntentFailure + } + rapyd::RapydWebhookObjectEventType::PaymentRefundFailed + | rapyd::RapydWebhookObjectEventType::PaymentRefundRejected => { + api::IncomingWebhookEvent::RefundFailure + } + rapyd::RapydWebhookObjectEventType::RefundCompleted => { + api::IncomingWebhookEvent::RefundSuccess + } + rapyd::RapydWebhookObjectEventType::PaymentDisputeCreated => { + api::IncomingWebhookEvent::DisputeOpened + } + rapyd::RapydWebhookObjectEventType::Unknown => { + api::IncomingWebhookEvent::EventNotSupported + } + rapyd::RapydWebhookObjectEventType::PaymentDisputeUpdated => match webhook.data { + rapyd::WebhookData::Dispute(data) => api::IncomingWebhookEvent::from(data.status), + _ => api::IncomingWebhookEvent::EventNotSupported, + }, + }) } fn get_webhook_resource_object( @@ -846,17 +877,50 @@ impl api::IncomingWebhook for Rapyd { .body .parse_struct("RapydIncomingWebhook") .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; - let response = match webhook.data { - transformers::WebhookData::PaymentData(payment_data) => { + let res_json = match webhook.data { + transformers::WebhookData::Payment(payment_data) => { let rapyd_response: transformers::RapydPaymentsResponse = payment_data.into(); - Ok(rapyd_response) - } - _ => Err(errors::ConnectorError::WebhookEventTypeNotFound), - }?; - let res_json = - utils::Encode::::encode_to_value(&response) - .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; + utils::Encode::::encode_to_value( + &rapyd_response, + ) + .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)? + } + transformers::WebhookData::Refund(refund_data) => { + utils::Encode::::encode_to_value(&refund_data) + .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)? + } + transformers::WebhookData::Dispute(dispute_data) => { + utils::Encode::::encode_to_value(&dispute_data) + .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)? + } + }; Ok(res_json) } + + fn get_dispute_details( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + let webhook: transformers::RapydIncomingWebhook = request + .body + .parse_struct("RapydIncomingWebhook") + .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; + let webhook_dispute_data = match webhook.data { + transformers::WebhookData::Dispute(dispute_data) => Ok(dispute_data), + _ => Err(errors::ConnectorError::WebhookBodyDecodingFailed), + }?; + Ok(api::disputes::DisputePayload { + amount: webhook_dispute_data.amount.to_string(), + currency: webhook_dispute_data.currency.to_string(), + dispute_stage: api_models::enums::DisputeStage::Dispute, + connector_dispute_id: webhook_dispute_data.token, + connector_reason: Some(webhook_dispute_data.dispute_reason_description), + connector_reason_code: None, + challenge_required_by: webhook_dispute_data.due_date, + connector_status: webhook_dispute_data.status.to_string(), + created_at: webhook_dispute_data.created_at, + updated_at: webhook_dispute_data.updated_at, + }) + } } diff --git a/crates/router/src/connector/rapyd/transformers.rs b/crates/router/src/connector/rapyd/transformers.rs index 7f8483a564..f11e1c76b7 100644 --- a/crates/router/src/connector/rapyd/transformers.rs +++ b/crates/router/src/connector/rapyd/transformers.rs @@ -1,5 +1,6 @@ use error_stack::{IntoReport, ResultExt}; use serde::{Deserialize, Serialize}; +use time::PrimitiveDateTime; use url::Url; use crate::{ @@ -245,6 +246,23 @@ pub struct ResponseData { pub failure_message: Option, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct DisputeResponseData { + pub id: String, + pub amount: i64, + pub currency: api_models::enums::Currency, + pub token: String, + pub dispute_reason_description: String, + #[serde(default, with = "common_utils::custom_serde::timestamp::option")] + pub due_date: Option, + pub status: RapydWebhookDisputeStatus, + #[serde(default, with = "common_utils::custom_serde::timestamp::option")] + pub created_at: Option, + #[serde(default, with = "common_utils::custom_serde::timestamp::option")] + pub updated_at: Option, + pub original_transaction_id: String, +} + #[derive(Default, Debug, Serialize)] pub struct RapydRefundRequest { pub payment: String, @@ -463,21 +481,34 @@ pub enum RapydWebhookObjectEventType { RefundCompleted, PaymentRefundRejected, PaymentRefundFailed, + PaymentDisputeCreated, + PaymentDisputeUpdated, #[serde(other)] Unknown, } -impl TryFrom for api::IncomingWebhookEvent { - type Error = error_stack::Report; - fn try_from(value: RapydWebhookObjectEventType) -> Result { +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, strum::Display)] +pub enum RapydWebhookDisputeStatus { + #[serde(rename = "ACT")] + Active, + #[serde(rename = "RVW")] + Review, + #[serde(rename = "LOS")] + Lose, + #[serde(rename = "WIN")] + Win, + #[serde(other)] + Unknown, +} + +impl From for api_models::webhooks::IncomingWebhookEvent { + fn from(value: RapydWebhookDisputeStatus) -> Self { match value { - RapydWebhookObjectEventType::PaymentCompleted => Ok(Self::PaymentIntentSuccess), - RapydWebhookObjectEventType::PaymentCaptured => Ok(Self::PaymentIntentSuccess), - RapydWebhookObjectEventType::PaymentFailed => Ok(Self::PaymentIntentFailure), - RapydWebhookObjectEventType::Unknown - | RapydWebhookObjectEventType::RefundCompleted - | RapydWebhookObjectEventType::PaymentRefundRejected - | RapydWebhookObjectEventType::PaymentRefundFailed => Ok(Self::EventNotSupported), + RapydWebhookDisputeStatus::Active => Self::DisputeOpened, + RapydWebhookDisputeStatus::Review => Self::DisputeChallenged, + RapydWebhookDisputeStatus::Lose => Self::DisputeLost, + RapydWebhookDisputeStatus::Win => Self::DisputeWon, + RapydWebhookDisputeStatus::Unknown => Self::EventNotSupported, } } } @@ -485,8 +516,9 @@ impl TryFrom for api::IncomingWebhookEvent { #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum WebhookData { - PaymentData(ResponseData), - RefundData(RefundResponseData), + Payment(ResponseData), + Refund(RefundResponseData), + Dispute(DisputeResponseData), } impl From for RapydPaymentsResponse {