feat(Connector): [Paypal] add support for dispute webhooks for paypal connector (#2353)

This commit is contained in:
Swangi Kumari
2023-10-18 16:54:56 +05:30
committed by GitHub
parent 1dad7455c4
commit 6cf8f0582c
2 changed files with 191 additions and 9 deletions

View File

@ -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 {

View File

@ -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())
}