Skip to content

Commit

Permalink
feat(Connector): [Paypal] add support for dispute webhooks for paypal…
Browse files Browse the repository at this point in the history
… connector (#2353)
  • Loading branch information
swangi-kumari authored Oct 18, 2023
1 parent 1dad745 commit 6cf8f05
Show file tree
Hide file tree
Showing 2 changed files with 191 additions and 9 deletions.
72 changes: 70 additions & 2 deletions crates/router/src/connector/paypal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -30,6 +30,7 @@ use crate::{
types::{
self,
api::{self, CompleteAuthorize, ConnectorCommon, ConnectorCommonExt, VerifyWebhookSource},
transformers::ForeignFrom,
ErrorResponse, Response,
},
utils::{self, BytesExt},
Expand Down Expand Up @@ -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)?,
),
))
}
}
}

Expand All @@ -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(
Expand Down Expand Up @@ -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 {
Expand Down
128 changes: 121 additions & 7 deletions crates/router/src/connector/paypal/transformers.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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,
Expand All @@ -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,
}
Expand All @@ -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)]
Expand Down Expand Up @@ -1482,23 +1549,58 @@ 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,
PaypalWebhookEventType::PaymentCapturePending
| 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,
Expand Down Expand Up @@ -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())
}
}
Expand All @@ -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())
}
Expand All @@ -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())
}
Expand Down

0 comments on commit 6cf8f05

Please sign in to comment.