Files
saiharsha-juspay a733eafbbe 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>
2023-03-29 22:55:54 +00:00

804 lines
28 KiB
Rust

mod transformers;
use std::fmt::Debug;
use api_models::webhooks::IncomingWebhookEvent;
use base64::Engine;
use error_stack::{IntoReport, ResultExt};
use router_env::{instrument, tracing};
use storage_models::enums as storage_enums;
use self::transformers as adyen;
use crate::{
configs::settings,
consts,
core::errors::{self, CustomResult},
db::StorageInterface,
headers, logger, services,
types::{
self,
api::{self, ConnectorCommon},
transformers::ForeignFrom,
},
utils::{self, crypto, ByteSliceExt, BytesExt, OptionExt},
};
#[derive(Debug, Clone)]
pub struct Adyen;
impl ConnectorCommon for Adyen {
fn id(&self) -> &'static str {
"adyen"
}
fn get_auth_header(
&self,
auth_type: &types::ConnectorAuthType,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
let auth = adyen::AdyenAuthType::try_from(auth_type)
.change_context(errors::ConnectorError::FailedToObtainAuthType)?;
Ok(vec![(headers::X_API_KEY.to_string(), auth.api_key)])
}
fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str {
connectors.adyen.base_url.as_ref()
}
}
impl api::Payment for Adyen {}
impl api::PaymentAuthorize for Adyen {}
impl api::PaymentSync for Adyen {}
impl api::PaymentVoid for Adyen {}
impl api::PaymentCapture for Adyen {}
impl api::PreVerify for Adyen {}
impl api::ConnectorAccessToken for Adyen {}
impl
services::ConnectorIntegration<
api::AccessTokenAuth,
types::AccessTokenRequestData,
types::AccessToken,
> for Adyen
{
// Not Implemented (R)
}
impl
services::ConnectorIntegration<
api::Verify,
types::VerifyRequestData,
types::PaymentsResponseData,
> for Adyen
{
// Issue: #173
}
impl api::PaymentSession for Adyen {}
impl
services::ConnectorIntegration<
api::Session,
types::PaymentsSessionData,
types::PaymentsResponseData,
> for Adyen
{
// Not Implemented (R)
}
impl
services::ConnectorIntegration<
api::Capture,
types::PaymentsCaptureData,
types::PaymentsResponseData,
> for Adyen
{
fn get_headers(
&self,
req: &types::PaymentsCaptureRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
let mut header = vec![
(
headers::CONTENT_TYPE.to_string(),
self.common_get_content_type().to_string(),
),
(headers::X_ROUTER.to_string(), "test".to_string()),
];
let mut api_key = self.get_auth_header(&req.connector_auth_type)?;
header.append(&mut api_key);
Ok(header)
}
fn get_url(
&self,
req: &types::PaymentsCaptureRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
let id = req.request.connector_transaction_id.as_str();
Ok(format!(
"{}{}/{}/captures",
self.base_url(connectors),
"v68/payments",
id
))
}
fn get_request_body(
&self,
req: &types::PaymentsCaptureRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let connector_req = adyen::AdyenCaptureRequest::try_from(req)?;
let adyen_req =
utils::Encode::<adyen::AdyenCaptureRequest>::encode_to_string_of_json(&connector_req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(adyen_req))
}
fn build_request(
&self,
req: &types::PaymentsCaptureRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsCaptureType::get_url(self, req, connectors)?)
.headers(types::PaymentsCaptureType::get_headers(
self, req, connectors,
)?)
.body(types::PaymentsCaptureType::get_request_body(self, req)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsCaptureRouterData,
res: types::Response,
) -> CustomResult<types::PaymentsCaptureRouterData, errors::ConnectorError> {
let response: adyen::AdyenCaptureResponse = res
.response
.parse_struct("AdyenCaptureResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: types::Response,
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
let response: adyen::ErrorResponse = res
.response
.parse_struct("adyen::ErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
Ok(types::ErrorResponse {
status_code: res.status_code,
code: response.error_code,
message: response.message,
reason: None,
})
}
}
impl
services::ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
for Adyen
{
fn get_headers(
&self,
req: &types::RouterData<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>,
_connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
let mut header = vec![
(
headers::CONTENT_TYPE.to_string(),
types::PaymentsSyncType::get_content_type(self).to_string(),
),
(headers::X_ROUTER.to_string(), "test".to_string()),
];
let mut api_key = self.get_auth_header(&req.connector_auth_type)?;
header.append(&mut api_key);
Ok(header)
}
fn get_request_body(
&self,
req: &types::RouterData<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let encoded_data = req
.request
.encoded_data
.clone()
.get_required_value("encoded_data")
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
let adyen_redirection_type = serde_urlencoded::from_str::<
transformers::AdyenRedirectRequestTypes,
>(encoded_data.as_str())
.into_report()
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
let redirection_request = match adyen_redirection_type {
adyen::AdyenRedirectRequestTypes::AdyenRedirection(req) => {
adyen::AdyenRedirectRequest {
details: adyen::AdyenRedirectRequestTypes::AdyenRedirection(
adyen::AdyenRedirection {
redirect_result: req.redirect_result,
type_of_redirection_result: None,
result_code: None,
},
),
}
}
adyen::AdyenRedirectRequestTypes::AdyenThreeDS(req) => adyen::AdyenRedirectRequest {
details: adyen::AdyenRedirectRequestTypes::AdyenThreeDS(adyen::AdyenThreeDS {
three_ds_result: req.three_ds_result,
type_of_redirection_result: None,
result_code: None,
}),
},
};
let adyen_request = utils::Encode::<adyen::AdyenRedirectRequest>::encode_to_string_of_json(
&redirection_request,
)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(adyen_request))
}
fn get_url(
&self,
_req: &types::RouterData<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!(
"{}{}",
self.base_url(connectors),
"v68/payments/details"
))
}
fn build_request(
&self,
req: &types::RouterData<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsSyncType::get_url(self, req, connectors)?)
.headers(types::PaymentsSyncType::get_headers(self, req, connectors)?)
.header(headers::X_ROUTER, "test")
.body(types::PaymentsSyncType::get_request_body(self, req)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::RouterData<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>,
res: types::Response,
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
logger::debug!(payment_sync_response=?res);
let response: adyen::AdyenPaymentResponse = res
.response
.parse_struct("AdyenPaymentResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
let is_manual_capture =
data.request.capture_method == Some(storage_enums::CaptureMethod::Manual);
types::RouterData::try_from((
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
},
is_manual_capture,
))
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: types::Response,
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
let response: adyen::ErrorResponse = res
.response
.parse_struct("ErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
Ok(types::ErrorResponse {
status_code: res.status_code,
code: response.error_code,
message: response.message,
reason: None,
})
}
}
impl
services::ConnectorIntegration<
api::Authorize,
types::PaymentsAuthorizeData,
types::PaymentsResponseData,
> for Adyen
{
fn get_headers(
&self,
req: &types::PaymentsAuthorizeRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError>
where
Self: services::ConnectorIntegration<
api::Authorize,
types::PaymentsAuthorizeData,
types::PaymentsResponseData,
>,
{
let mut header = vec![
(
headers::CONTENT_TYPE.to_string(),
types::PaymentsAuthorizeType::get_content_type(self).to_string(),
),
(headers::X_ROUTER.to_string(), "test".to_string()),
];
let mut api_key = self.get_auth_header(&req.connector_auth_type)?;
header.append(&mut api_key);
Ok(header)
}
fn get_url(
&self,
_req: &types::PaymentsAuthorizeRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!("{}{}", self.base_url(connectors), "v68/payments"))
}
fn get_request_body(
&self,
req: &types::PaymentsAuthorizeRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let connector_req = adyen::AdyenPaymentRequest::try_from(req)?;
let adyen_req = utils::Encode::<adyen::AdyenPaymentRequest<'_>>::encode_to_string_of_json(
&connector_req,
)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(adyen_req))
}
fn build_request(
&self,
req: &types::RouterData<
api::Authorize,
types::PaymentsAuthorizeData,
types::PaymentsResponseData,
>,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsAuthorizeType::get_url(
self, req, connectors,
)?)
.headers(types::PaymentsAuthorizeType::get_headers(
self, req, connectors,
)?)
.header(headers::X_ROUTER, "test")
.body(types::PaymentsAuthorizeType::get_request_body(self, req)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsAuthorizeRouterData,
res: types::Response,
) -> CustomResult<types::PaymentsAuthorizeRouterData, errors::ConnectorError> {
let response: adyen::AdyenPaymentResponse = res
.response
.parse_struct("AdyenPaymentResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
let is_manual_capture =
data.request.capture_method == Some(storage_models::enums::CaptureMethod::Manual);
types::RouterData::try_from((
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
},
is_manual_capture,
))
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: types::Response,
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
let response: adyen::ErrorResponse = res
.response
.parse_struct("ErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
Ok(types::ErrorResponse {
status_code: res.status_code,
code: response.error_code,
message: response.message,
reason: None,
})
}
}
impl
services::ConnectorIntegration<
api::Void,
types::PaymentsCancelData,
types::PaymentsResponseData,
> for Adyen
{
fn get_headers(
&self,
req: &types::PaymentsCancelRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
let mut header = vec![
(
headers::CONTENT_TYPE.to_string(),
types::PaymentsAuthorizeType::get_content_type(self).to_string(),
),
(headers::X_ROUTER.to_string(), "test".to_string()),
];
let mut api_key = self.get_auth_header(&req.connector_auth_type)?;
header.append(&mut api_key);
Ok(header)
}
fn get_url(
&self,
req: &types::PaymentsCancelRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
let id = req.request.connector_transaction_id.as_str();
Ok(format!(
"{}v68/payments/{}/cancels",
self.base_url(connectors),
id
))
}
fn get_request_body(
&self,
req: &types::PaymentsCancelRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let connector_req = adyen::AdyenCancelRequest::try_from(req)?;
let adyen_req =
utils::Encode::<adyen::AdyenCancelRequest>::encode_to_string_of_json(&connector_req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(adyen_req))
}
fn build_request(
&self,
req: &types::PaymentsCancelRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsVoidType::get_url(self, req, connectors)?)
.headers(types::PaymentsVoidType::get_headers(self, req, connectors)?)
.header(headers::X_ROUTER, "test")
.body(types::PaymentsVoidType::get_request_body(self, req)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsCancelRouterData,
res: types::Response,
) -> CustomResult<types::PaymentsCancelRouterData, errors::ConnectorError> {
let response: adyen::AdyenCancelResponse = res
.response
.parse_struct("AdyenCancelResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: types::Response,
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
let response: adyen::ErrorResponse = res
.response
.parse_struct("ErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
logger::info!(response=?res);
Ok(types::ErrorResponse {
status_code: res.status_code,
code: response.error_code,
message: response.message,
reason: None,
})
}
}
impl api::Refund for Adyen {}
impl api::RefundExecute for Adyen {}
impl api::RefundSync for Adyen {}
impl services::ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsResponseData>
for Adyen
{
fn get_headers(
&self,
req: &types::RefundsRouterData<api::Execute>,
_connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
let mut header = vec![
(
headers::CONTENT_TYPE.to_string(),
types::RefundExecuteType::get_content_type(self).to_string(),
),
(headers::X_ROUTER.to_string(), "test".to_string()),
];
let mut api_header = self.get_auth_header(&req.connector_auth_type)?;
header.append(&mut api_header);
Ok(header)
}
fn get_url(
&self,
req: &types::RefundsRouterData<api::Execute>,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
let connector_payment_id = req.request.connector_transaction_id.clone();
Ok(format!(
"{}v68/payments/{}/refunds",
self.base_url(connectors),
connector_payment_id,
))
}
fn get_request_body(
&self,
req: &types::RefundsRouterData<api::Execute>,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let connector_req = adyen::AdyenRefundRequest::try_from(req)?;
let adyen_req =
utils::Encode::<adyen::AdyenRefundRequest>::encode_to_string_of_json(&connector_req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(adyen_req))
}
fn build_request(
&self,
req: &types::RefundsRouterData<api::Execute>,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::RefundExecuteType::get_url(self, req, connectors)?)
.headers(types::RefundExecuteType::get_headers(
self, req, connectors,
)?)
.body(types::RefundExecuteType::get_request_body(self, req)?)
.build(),
))
}
#[instrument(skip_all)]
fn handle_response(
&self,
data: &types::RefundsRouterData<api::Execute>,
res: types::Response,
) -> CustomResult<types::RefundsRouterData<api::Execute>, errors::ConnectorError> {
let response: adyen::AdyenRefundResponse = res
.response
.parse_struct("AdyenRefundResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
logger::info!(response=?res);
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: types::Response,
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
let response: adyen::ErrorResponse = res
.response
.parse_struct("ErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
logger::info!(response=?res);
Ok(types::ErrorResponse {
status_code: res.status_code,
code: response.error_code,
message: response.message,
reason: None,
})
}
}
impl services::ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponseData>
for Adyen
{
}
fn get_webhook_object_from_body(
body: &[u8],
) -> CustomResult<adyen::AdyenNotificationRequestItemWH, errors::ParsingError> {
let mut webhook: adyen::AdyenIncomingWebhook = body.parse_struct("AdyenIncomingWebhook")?;
let item_object = webhook
.notification_items
.drain(..)
.next()
.ok_or(errors::ParsingError)
.into_report()?;
Ok(item_object.notification_request_item)
}
#[async_trait::async_trait]
impl api::IncomingWebhook for Adyen {
fn get_webhook_source_verification_algorithm(
&self,
_request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<Box<dyn crypto::VerifySignature + Send>, errors::ConnectorError> {
Ok(Box::new(crypto::HmacSha256))
}
fn get_webhook_source_verification_signature(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let notif_item = get_webhook_object_from_body(request.body)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
let base64_signature = notif_item.additional_data.hmac_signature;
let signature = consts::BASE64_ENGINE
.decode(base64_signature.as_bytes())
.into_report()
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
Ok(signature)
}
fn get_webhook_source_verification_message(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
_merchant_id: &str,
_secret: &[u8],
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let notif = get_webhook_object_from_body(request.body)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
let message = format!(
"{}:{}:{}:{}:{}:{}:{}:{}",
notif.psp_reference,
notif.original_reference.unwrap_or_default(),
notif.merchant_account_code,
notif.merchant_reference,
notif.amount.value,
notif.amount.currency,
notif.event_code,
notif.success
);
Ok(message.into_bytes())
}
async fn get_webhook_source_verification_merchant_secret(
&self,
db: &dyn StorageInterface,
merchant_id: &str,
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let key = format!("whsec_verification_{}_{}", self.id(), merchant_id);
let secret = db
.get_key(&key)
.await
.change_context(errors::ConnectorError::WebhookVerificationSecretNotFound)?;
Ok(secret)
}
fn get_webhook_object_reference_id(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> {
let notif = get_webhook_object_from_body(request.body)
.change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?;
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<IncomingWebhookEvent, errors::ConnectorError> {
let notif = get_webhook_object_from_body(request.body)
.change_context(errors::ConnectorError::WebhookEventTypeNotFound)?;
Ok(IncomingWebhookEvent::foreign_from((
notif.event_code,
notif.additional_data.dispute_status,
)))
}
fn get_webhook_resource_object(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<serde_json::Value, errors::ConnectorError> {
let notif = get_webhook_object_from_body(request.body)
.change_context(errors::ConnectorError::WebhookEventTypeNotFound)?;
let response: adyen::AdyenResponse = notif.into();
let res_json = serde_json::to_value(response)
.into_report()
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?;
Ok(res_json)
}
fn get_webhook_api_response(
&self,
_request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<services::api::ApplicationResponse<serde_json::Value>, errors::ConnectorError>
{
Ok(services::api::ApplicationResponse::TextPlain(
"[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,
})
}
}