diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index df5bfc44d851..120795bef9c5 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -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::("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 { + 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 { diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index 927c33277ab9..0092363523e5 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -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), PaypalRedirectsWebhooks(Box), PaypalRefundWebhooks(Box), + PaypalDisputeWebhooks(Box), +} + +#[derive(Deserialize, Debug, Serialize)] +pub struct PaypalDisputeWebhooks { + pub dispute_id: String, + pub dispute_transactions: Vec, + pub dispute_amount: OrderAmount, + pub dispute_outcome: DisputeOutcome, + pub dispute_life_cycle_stage: DisputeLifeCycleStage, + pub status: DisputeStatus, + pub reason: Option, + pub external_reason_code: Option, + pub seller_response_due_date: Option, + pub update_time: Option, + pub create_time: Option, +} + +#[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 for api::IncomingWebhookEvent { - fn from(event: PaypalWebhookEventType) -> Self { +impl ForeignFrom<(PaypalWebhookEventType, Option)> for api::IncomingWebhookEvent { + fn foreign_from((event, outcome): (PaypalWebhookEventType, Option)) -> Self { match event { PaypalWebhookEventType::PaymentCaptureCompleted | PaypalWebhookEventType::CheckoutOrderCompleted => Self::PaymentIntentSuccess, @@ -1491,14 +1558,49 @@ impl From 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 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 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 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 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 for PaypalOrderStatus { PaypalWebhookEventType::CheckoutOrderApproved | PaypalWebhookEventType::PaymentCaptureDeclined | PaypalWebhookEventType::PaymentCaptureRefunded + | PaypalWebhookEventType::CustomerDisputeCreated + | PaypalWebhookEventType::CustomerDisputeResolved + | PaypalWebhookEventType::CustomerDisputedUpdated + | PaypalWebhookEventType::RiskDisputeCreated | PaypalWebhookEventType::Unknown => { Err(errors::ConnectorError::WebhookEventTypeNotFound.into()) }