mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-10-31 18:17:13 +08:00 
			
		
		
		
	feat(Connector): [Paypal] add support for dispute webhooks for paypal connector (#2353)
This commit is contained in:
		| @ -8,7 +8,7 @@ use error_stack::{IntoReport, ResultExt}; | ||||
| use masking::PeekInterface; | ||||
| use transformers as paypal; | ||||
|  | ||||
| use self::transformers::{PaypalAuthResponse, PaypalMeta}; | ||||
| use self::transformers::{PaypalAuthResponse, PaypalMeta, PaypalWebhookEventType}; | ||||
| use super::utils::PaymentsCompleteAuthorizeRequestData; | ||||
| use crate::{ | ||||
|     configs::settings, | ||||
| @ -30,6 +30,7 @@ use crate::{ | ||||
|     types::{ | ||||
|         self, | ||||
|         api::{self, CompleteAuthorize, ConnectorCommon, ConnectorCommonExt, VerifyWebhookSource}, | ||||
|         transformers::ForeignFrom, | ||||
|         ErrorResponse, Response, | ||||
|     }, | ||||
|     utils::{self, BytesExt}, | ||||
| @ -1113,6 +1114,17 @@ impl api::IncomingWebhook for Paypal { | ||||
|                     api_models::webhooks::RefundIdType::ConnectorRefundId(resource.id), | ||||
|                 )) | ||||
|             } | ||||
|             paypal::PaypalResource::PaypalDisputeWebhooks(resource) => { | ||||
|                 Ok(api_models::webhooks::ObjectReferenceId::PaymentId( | ||||
|                     api_models::payments::PaymentIdType::PaymentAttemptId( | ||||
|                         resource | ||||
|                             .dispute_transactions | ||||
|                             .first() | ||||
|                             .map(|transaction| transaction.reference_id.clone()) | ||||
|                             .ok_or(errors::ConnectorError::WebhookReferenceIdNotFound)?, | ||||
|                     ), | ||||
|                 )) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -1124,7 +1136,33 @@ impl api::IncomingWebhook for Paypal { | ||||
|             .body | ||||
|             .parse_struct("PaypalWebooksEventType") | ||||
|             .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; | ||||
|         Ok(api::IncomingWebhookEvent::from(payload.event_type)) | ||||
|         let outcome = match payload.event_type { | ||||
|             PaypalWebhookEventType::CustomerDisputeCreated | ||||
|             | PaypalWebhookEventType::CustomerDisputeResolved | ||||
|             | PaypalWebhookEventType::CustomerDisputedUpdated | ||||
|             | PaypalWebhookEventType::RiskDisputeCreated => Some( | ||||
|                 request | ||||
|                     .body | ||||
|                     .parse_struct::<paypal::DisputeOutcome>("PaypalWebooksEventType") | ||||
|                     .change_context(errors::ConnectorError::WebhookEventTypeNotFound)? | ||||
|                     .outcome_code, | ||||
|             ), | ||||
|             PaypalWebhookEventType::PaymentAuthorizationCreated | ||||
|             | PaypalWebhookEventType::PaymentAuthorizationVoided | ||||
|             | PaypalWebhookEventType::PaymentCaptureDeclined | ||||
|             | PaypalWebhookEventType::PaymentCaptureCompleted | ||||
|             | PaypalWebhookEventType::PaymentCapturePending | ||||
|             | PaypalWebhookEventType::PaymentCaptureRefunded | ||||
|             | PaypalWebhookEventType::CheckoutOrderApproved | ||||
|             | PaypalWebhookEventType::CheckoutOrderCompleted | ||||
|             | PaypalWebhookEventType::CheckoutOrderProcessed | ||||
|             | PaypalWebhookEventType::Unknown => None, | ||||
|         }; | ||||
|  | ||||
|         Ok(api::IncomingWebhookEvent::foreign_from(( | ||||
|             payload.event_type, | ||||
|             outcome, | ||||
|         ))) | ||||
|     } | ||||
|  | ||||
|     fn get_webhook_resource_object( | ||||
| @ -1152,9 +1190,39 @@ impl api::IncomingWebhook for Paypal { | ||||
|             ) | ||||
|             .into_report() | ||||
|             .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?, | ||||
|             paypal::PaypalResource::PaypalDisputeWebhooks(_) => serde_json::to_value(details) | ||||
|                 .into_report() | ||||
|                 .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?, | ||||
|         }; | ||||
|         Ok(sync_payload) | ||||
|     } | ||||
|  | ||||
|     fn get_dispute_details( | ||||
|         &self, | ||||
|         request: &api::IncomingWebhookRequestDetails<'_>, | ||||
|     ) -> CustomResult<api::disputes::DisputePayload, errors::ConnectorError> { | ||||
|         let payload: paypal::PaypalDisputeWebhooks = request | ||||
|             .body | ||||
|             .parse_struct("PaypalDisputeWebhooks") | ||||
|             .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; | ||||
|         Ok(api::disputes::DisputePayload { | ||||
|             amount: connector_utils::to_currency_lower_unit( | ||||
|                 payload.dispute_amount.value, | ||||
|                 payload.dispute_amount.currency_code, | ||||
|             )?, | ||||
|             currency: payload.dispute_amount.currency_code.to_string(), | ||||
|             dispute_stage: api_models::enums::DisputeStage::from( | ||||
|                 payload.dispute_life_cycle_stage.clone(), | ||||
|             ), | ||||
|             connector_status: payload.status.to_string(), | ||||
|             connector_dispute_id: payload.dispute_id, | ||||
|             connector_reason: payload.reason, | ||||
|             connector_reason_code: payload.external_reason_code, | ||||
|             challenge_required_by: payload.seller_response_due_date, | ||||
|             created_at: payload.create_time, | ||||
|             updated_at: payload.update_time, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl services::ConnectorRedirectResponse for Paypal { | ||||
|  | ||||
| @ -1,8 +1,9 @@ | ||||
| use api_models::payments::BankRedirectData; | ||||
| use api_models::{enums, payments::BankRedirectData}; | ||||
| use common_utils::errors::CustomResult; | ||||
| use error_stack::{IntoReport, ResultExt}; | ||||
| use masking::Secret; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use time::PrimitiveDateTime; | ||||
| use url::Url; | ||||
|  | ||||
| use crate::{ | ||||
| @ -67,8 +68,8 @@ pub enum PaypalPaymentIntent { | ||||
|  | ||||
| #[derive(Default, Debug, Clone, Serialize, Eq, PartialEq, Deserialize)] | ||||
| pub struct OrderAmount { | ||||
|     currency_code: storage_enums::Currency, | ||||
|     value: String, | ||||
|     pub currency_code: storage_enums::Currency, | ||||
|     pub value: String, | ||||
| } | ||||
|  | ||||
| #[derive(Default, Debug, Serialize, Eq, PartialEq)] | ||||
| @ -1403,7 +1404,7 @@ pub struct PaypalWebhooksBody { | ||||
|     pub resource: PaypalResource, | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize, Debug, Serialize)] | ||||
| #[derive(Clone, Deserialize, Debug, strum::Display, Serialize)] | ||||
| pub enum PaypalWebhookEventType { | ||||
|     #[serde(rename = "PAYMENT.AUTHORIZATION.CREATED")] | ||||
|     PaymentAuthorizationCreated, | ||||
| @ -1423,6 +1424,14 @@ pub enum PaypalWebhookEventType { | ||||
|     CheckoutOrderCompleted, | ||||
|     #[serde(rename = "CHECKOUT.ORDER.PROCESSED")] | ||||
|     CheckoutOrderProcessed, | ||||
|     #[serde(rename = "CUSTOMER.DISPUTE.CREATED")] | ||||
|     CustomerDisputeCreated, | ||||
|     #[serde(rename = "CUSTOMER.DISPUTE.RESOLVED")] | ||||
|     CustomerDisputeResolved, | ||||
|     #[serde(rename = "CUSTOMER.DISPUTE.UPDATED")] | ||||
|     CustomerDisputedUpdated, | ||||
|     #[serde(rename = "RISK.DISPUTE.CREATED")] | ||||
|     RiskDisputeCreated, | ||||
|     #[serde(other)] | ||||
|     Unknown, | ||||
| } | ||||
| @ -1433,6 +1442,64 @@ pub enum PaypalResource { | ||||
|     PaypalCardWebhooks(Box<PaypalCardWebhooks>), | ||||
|     PaypalRedirectsWebhooks(Box<PaypalRedirectsWebhooks>), | ||||
|     PaypalRefundWebhooks(Box<PaypalRefundWebhooks>), | ||||
|     PaypalDisputeWebhooks(Box<PaypalDisputeWebhooks>), | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize, Debug, Serialize)] | ||||
| pub struct PaypalDisputeWebhooks { | ||||
|     pub dispute_id: String, | ||||
|     pub dispute_transactions: Vec<DisputeTransaction>, | ||||
|     pub dispute_amount: OrderAmount, | ||||
|     pub dispute_outcome: DisputeOutcome, | ||||
|     pub dispute_life_cycle_stage: DisputeLifeCycleStage, | ||||
|     pub status: DisputeStatus, | ||||
|     pub reason: Option<String>, | ||||
|     pub external_reason_code: Option<String>, | ||||
|     pub seller_response_due_date: Option<PrimitiveDateTime>, | ||||
|     pub update_time: Option<PrimitiveDateTime>, | ||||
|     pub create_time: Option<PrimitiveDateTime>, | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize, Debug, Serialize)] | ||||
| pub struct DisputeTransaction { | ||||
|     pub reference_id: String, | ||||
| } | ||||
|  | ||||
| #[derive(Clone, Deserialize, Debug, strum::Display, Serialize)] | ||||
| #[serde(rename_all = "SCREAMING_SNAKE_CASE")] | ||||
| pub enum DisputeLifeCycleStage { | ||||
|     Inquiry, | ||||
|     Chargeback, | ||||
|     PreArbitration, | ||||
|     Arbitration, | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize, Debug, strum::Display, Serialize)] | ||||
| #[serde(rename_all = "SCREAMING_SNAKE_CASE")] | ||||
| pub enum DisputeStatus { | ||||
|     Open, | ||||
|     WaitingForBuyerResponse, | ||||
|     WaitingForSellerResponse, | ||||
|     UnderReview, | ||||
|     Resolved, | ||||
|     Other, | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize, Debug, Serialize)] | ||||
| pub struct DisputeOutcome { | ||||
|     pub outcome_code: OutcomeCode, | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize, Debug, Serialize)] | ||||
| #[serde(rename_all = "SCREAMING_SNAKE_CASE")] | ||||
| pub enum OutcomeCode { | ||||
|     ResolvedBuyerFavour, | ||||
|     ResolvedSellerFavour, | ||||
|     ResolvedWithPayout, | ||||
|     CanceledByBuyer, | ||||
|     ACCEPTED, | ||||
|     DENIED, | ||||
|     NONE, | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize, Debug, Serialize)] | ||||
| @ -1482,8 +1549,8 @@ pub struct PaypalWebooksEventType { | ||||
|     pub event_type: PaypalWebhookEventType, | ||||
| } | ||||
|  | ||||
| impl From<PaypalWebhookEventType> for api::IncomingWebhookEvent { | ||||
|     fn from(event: PaypalWebhookEventType) -> Self { | ||||
| impl ForeignFrom<(PaypalWebhookEventType, Option<OutcomeCode>)> for api::IncomingWebhookEvent { | ||||
|     fn foreign_from((event, outcome): (PaypalWebhookEventType, Option<OutcomeCode>)) -> Self { | ||||
|         match event { | ||||
|             PaypalWebhookEventType::PaymentCaptureCompleted | ||||
|             | PaypalWebhookEventType::CheckoutOrderCompleted => Self::PaymentIntentSuccess, | ||||
| @ -1491,14 +1558,49 @@ impl From<PaypalWebhookEventType> for api::IncomingWebhookEvent { | ||||
|             | PaypalWebhookEventType::CheckoutOrderProcessed => Self::PaymentIntentProcessing, | ||||
|             PaypalWebhookEventType::PaymentCaptureDeclined => Self::PaymentIntentFailure, | ||||
|             PaypalWebhookEventType::PaymentCaptureRefunded => Self::RefundSuccess, | ||||
|             PaypalWebhookEventType::CustomerDisputeCreated => Self::DisputeOpened, | ||||
|             PaypalWebhookEventType::RiskDisputeCreated => Self::DisputeAccepted, | ||||
|             PaypalWebhookEventType::CustomerDisputeResolved => { | ||||
|                 if let Some(outcome_code) = outcome { | ||||
|                     Self::from(outcome_code) | ||||
|                 } else { | ||||
|                     Self::EventNotSupported | ||||
|                 } | ||||
|             } | ||||
|             PaypalWebhookEventType::PaymentAuthorizationCreated | ||||
|             | PaypalWebhookEventType::PaymentAuthorizationVoided | ||||
|             | PaypalWebhookEventType::CheckoutOrderApproved | ||||
|             | PaypalWebhookEventType::CustomerDisputedUpdated | ||||
|             | PaypalWebhookEventType::Unknown => Self::EventNotSupported, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<OutcomeCode> for api::IncomingWebhookEvent { | ||||
|     fn from(outcome_code: OutcomeCode) -> Self { | ||||
|         match outcome_code { | ||||
|             OutcomeCode::ResolvedBuyerFavour => Self::DisputeLost, | ||||
|             OutcomeCode::ResolvedSellerFavour => Self::DisputeWon, | ||||
|             OutcomeCode::CanceledByBuyer => Self::DisputeCancelled, | ||||
|             OutcomeCode::ACCEPTED => Self::DisputeAccepted, | ||||
|             OutcomeCode::DENIED => Self::DisputeCancelled, | ||||
|             OutcomeCode::NONE => Self::DisputeCancelled, | ||||
|             OutcomeCode::ResolvedWithPayout => Self::EventNotSupported, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<DisputeLifeCycleStage> for enums::DisputeStage { | ||||
|     fn from(dispute_life_cycle_stage: DisputeLifeCycleStage) -> Self { | ||||
|         match dispute_life_cycle_stage { | ||||
|             DisputeLifeCycleStage::Inquiry => Self::PreDispute, | ||||
|             DisputeLifeCycleStage::Chargeback => Self::Dispute, | ||||
|             DisputeLifeCycleStage::PreArbitration => Self::PreArbitration, | ||||
|             DisputeLifeCycleStage::Arbitration => Self::PreArbitration, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize, Serialize, Debug)] | ||||
| pub struct PaypalSourceVerificationRequest { | ||||
|     pub transmission_id: String, | ||||
| @ -1617,7 +1719,11 @@ impl TryFrom<PaypalWebhookEventType> for PaypalPaymentStatus { | ||||
|             | PaypalWebhookEventType::CheckoutOrderProcessed => Ok(Self::Pending), | ||||
|             PaypalWebhookEventType::PaymentAuthorizationCreated => Ok(Self::Created), | ||||
|             PaypalWebhookEventType::PaymentCaptureRefunded => Ok(Self::Refunded), | ||||
|             PaypalWebhookEventType::Unknown => { | ||||
|             PaypalWebhookEventType::CustomerDisputeCreated | ||||
|             | PaypalWebhookEventType::CustomerDisputeResolved | ||||
|             | PaypalWebhookEventType::CustomerDisputedUpdated | ||||
|             | PaypalWebhookEventType::RiskDisputeCreated | ||||
|             | PaypalWebhookEventType::Unknown => { | ||||
|                 Err(errors::ConnectorError::WebhookEventTypeNotFound.into()) | ||||
|             } | ||||
|         } | ||||
| @ -1637,6 +1743,10 @@ impl TryFrom<PaypalWebhookEventType> for RefundStatus { | ||||
|             | PaypalWebhookEventType::CheckoutOrderApproved | ||||
|             | PaypalWebhookEventType::CheckoutOrderCompleted | ||||
|             | PaypalWebhookEventType::CheckoutOrderProcessed | ||||
|             | PaypalWebhookEventType::CustomerDisputeCreated | ||||
|             | PaypalWebhookEventType::CustomerDisputeResolved | ||||
|             | PaypalWebhookEventType::CustomerDisputedUpdated | ||||
|             | PaypalWebhookEventType::RiskDisputeCreated | ||||
|             | PaypalWebhookEventType::Unknown => { | ||||
|                 Err(errors::ConnectorError::WebhookEventTypeNotFound.into()) | ||||
|             } | ||||
| @ -1657,6 +1767,10 @@ impl TryFrom<PaypalWebhookEventType> for PaypalOrderStatus { | ||||
|             PaypalWebhookEventType::CheckoutOrderApproved | ||||
|             | PaypalWebhookEventType::PaymentCaptureDeclined | ||||
|             | PaypalWebhookEventType::PaymentCaptureRefunded | ||||
|             | PaypalWebhookEventType::CustomerDisputeCreated | ||||
|             | PaypalWebhookEventType::CustomerDisputeResolved | ||||
|             | PaypalWebhookEventType::CustomerDisputedUpdated | ||||
|             | PaypalWebhookEventType::RiskDisputeCreated | ||||
|             | PaypalWebhookEventType::Unknown => { | ||||
|                 Err(errors::ConnectorError::WebhookEventTypeNotFound.into()) | ||||
|             } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Swangi Kumari
					Swangi Kumari