Skip to content

Commit a733eaf

Browse files
saiharsha-juspaySangamesh26sai harshajarnura
authored
feat(router): added incoming dispute webhooks flow (#769)
Co-authored-by: Sangamesh <[email protected]> Co-authored-by: sai harsha <[email protected]> Co-authored-by: Arun Raj M <[email protected]>
1 parent fb66a0e commit a733eaf

File tree

28 files changed

+1138
-31
lines changed

28 files changed

+1138
-31
lines changed

crates/api_models/src/disputes.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,38 @@
1+
use masking::Serialize;
2+
use utoipa::ToSchema;
13

4+
use super::enums::{DisputeStage, DisputeStatus};
5+
6+
#[derive(Default, Clone, Debug, Serialize, ToSchema)]
7+
pub struct DisputeResponse {
8+
/// The identifier for dispute
9+
pub dispute_id: String,
10+
/// The identifier for payment_intent
11+
pub payment_id: String,
12+
/// The identifier for payment_attempt
13+
pub attempt_id: String,
14+
/// The dispute amount
15+
pub amount: String,
16+
/// The three-letter ISO currency code
17+
pub currency: String,
18+
/// Stage of the dispute
19+
pub dispute_stage: DisputeStage,
20+
/// Status of the dispute
21+
pub dispute_status: DisputeStatus,
22+
/// Status of the dispute sent by connector
23+
pub connector_status: String,
24+
/// Dispute id sent by connector
25+
pub connector_dispute_id: String,
26+
/// Reason of dispute sent by connector
27+
pub connector_reason: Option<String>,
28+
/// Reason code of dispute sent by connector
29+
pub connector_reason_code: Option<String>,
30+
/// Evidence deadline of dispute sent by connector
31+
pub challenge_required_by: Option<String>,
32+
/// Dispute created time sent by connector
33+
pub created_at: Option<String>,
34+
/// Dispute updated time sent by connector
35+
pub updated_at: Option<String>,
36+
/// Time at which dispute is received
37+
pub received_at: String,
38+
}

crates/api_models/src/enums.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,13 @@ pub enum EventType {
267267
PaymentSucceeded,
268268
RefundSucceeded,
269269
RefundFailed,
270+
DisputeOpened,
271+
DisputeExpired,
272+
DisputeAccepted,
273+
DisputeCancelled,
274+
DisputeChallenged,
275+
DisputeWon,
276+
DisputeLost,
270277
}
271278

272279
#[derive(
@@ -767,3 +774,51 @@ impl From<AttemptStatus> for IntentStatus {
767774
}
768775
}
769776
}
777+
778+
#[derive(
779+
Clone,
780+
Default,
781+
Debug,
782+
Eq,
783+
Hash,
784+
PartialEq,
785+
serde::Deserialize,
786+
serde::Serialize,
787+
strum::Display,
788+
strum::EnumString,
789+
frunk::LabelledGeneric,
790+
ToSchema,
791+
)]
792+
pub enum DisputeStage {
793+
PreDispute,
794+
#[default]
795+
Dispute,
796+
PreArbitration,
797+
}
798+
799+
#[derive(
800+
Clone,
801+
Debug,
802+
Default,
803+
Eq,
804+
Hash,
805+
PartialEq,
806+
serde::Deserialize,
807+
serde::Serialize,
808+
strum::Display,
809+
strum::EnumString,
810+
frunk::LabelledGeneric,
811+
ToSchema,
812+
)]
813+
pub enum DisputeStatus {
814+
#[default]
815+
DisputeOpened,
816+
DisputeExpired,
817+
DisputeAccepted,
818+
DisputeCancelled,
819+
DisputeChallenged,
820+
// dispute has been successfully challenged by the merchant
821+
DisputeWon,
822+
// dispute has been unsuccessfully challenged
823+
DisputeLost,
824+
}

crates/api_models/src/webhooks.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use common_utils::custom_serde;
22
use serde::{Deserialize, Serialize};
33
use time::PrimitiveDateTime;
44

5-
use crate::{enums as api_enums, payments, refunds};
5+
use crate::{disputes, enums as api_enums, payments, refunds};
66

77
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
88
#[serde(rename_all = "snake_case")]
@@ -11,12 +11,22 @@ pub enum IncomingWebhookEvent {
1111
PaymentIntentSuccess,
1212
RefundFailure,
1313
RefundSuccess,
14+
DisputeOpened,
15+
DisputeExpired,
16+
DisputeAccepted,
17+
DisputeCancelled,
18+
DisputeChallenged,
19+
// dispute has been successfully challenged by the merchant
20+
DisputeWon,
21+
// dispute has been unsuccessfully challenged
22+
DisputeLost,
1423
EndpointVerification,
1524
}
1625

1726
pub enum WebhookFlow {
1827
Payment,
1928
Refund,
29+
Dispute,
2030
Subscription,
2131
ReturnResponse,
2232
}
@@ -28,6 +38,13 @@ impl From<IncomingWebhookEvent> for WebhookFlow {
2838
IncomingWebhookEvent::PaymentIntentSuccess => Self::Payment,
2939
IncomingWebhookEvent::RefundSuccess => Self::Refund,
3040
IncomingWebhookEvent::RefundFailure => Self::Refund,
41+
IncomingWebhookEvent::DisputeOpened => Self::Dispute,
42+
IncomingWebhookEvent::DisputeAccepted => Self::Dispute,
43+
IncomingWebhookEvent::DisputeExpired => Self::Dispute,
44+
IncomingWebhookEvent::DisputeCancelled => Self::Dispute,
45+
IncomingWebhookEvent::DisputeChallenged => Self::Dispute,
46+
IncomingWebhookEvent::DisputeWon => Self::Dispute,
47+
IncomingWebhookEvent::DisputeLost => Self::Dispute,
3148
IncomingWebhookEvent::EndpointVerification => Self::ReturnResponse,
3249
}
3350
}
@@ -73,6 +90,7 @@ pub struct OutgoingWebhook {
7390
pub enum OutgoingWebhookContent {
7491
PaymentDetails(payments::PaymentsResponse),
7592
RefundDetails(refunds::RefundResponse),
93+
DisputeDetails(Box<disputes::DisputeResponse>),
7694
}
7795

7896
pub trait OutgoingWebhookType: Serialize + From<OutgoingWebhook> + Sync + Send {}

crates/router/src/compatibility/stripe/webhooks.rs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use api_models::webhooks::{self as api};
1+
use api_models::{
2+
enums::DisputeStatus,
3+
webhooks::{self as api},
4+
};
25
use serde::Serialize;
36

47
use super::{
@@ -20,6 +23,57 @@ impl api::OutgoingWebhookType for StripeOutgoingWebhook {}
2023
pub enum StripeWebhookObject {
2124
PaymentIntent(StripePaymentIntentResponse),
2225
Refund(StripeRefundResponse),
26+
Dispute(StripeDisputeResponse),
27+
}
28+
29+
#[derive(Serialize)]
30+
pub struct StripeDisputeResponse {
31+
pub id: String,
32+
pub amount: String,
33+
pub currency: String,
34+
pub payment_intent: String,
35+
pub reason: Option<String>,
36+
pub status: StripeDisputeStatus,
37+
}
38+
39+
#[derive(Serialize)]
40+
#[serde(rename_all = "snake_case")]
41+
pub enum StripeDisputeStatus {
42+
WarningNeedsResponse,
43+
WarningUnderReview,
44+
WarningClosed,
45+
NeedsResponse,
46+
UnderReview,
47+
ChargeRefunded,
48+
Won,
49+
Lost,
50+
}
51+
52+
impl From<api_models::disputes::DisputeResponse> for StripeDisputeResponse {
53+
fn from(res: api_models::disputes::DisputeResponse) -> Self {
54+
Self {
55+
id: res.dispute_id,
56+
amount: res.amount,
57+
currency: res.currency,
58+
payment_intent: res.payment_id,
59+
reason: res.connector_reason,
60+
status: StripeDisputeStatus::from(res.dispute_status),
61+
}
62+
}
63+
}
64+
65+
impl From<DisputeStatus> for StripeDisputeStatus {
66+
fn from(status: DisputeStatus) -> Self {
67+
match status {
68+
DisputeStatus::DisputeOpened => Self::WarningNeedsResponse,
69+
DisputeStatus::DisputeExpired => Self::Lost,
70+
DisputeStatus::DisputeAccepted => Self::Lost,
71+
DisputeStatus::DisputeCancelled => Self::WarningClosed,
72+
DisputeStatus::DisputeChallenged => Self::WarningUnderReview,
73+
DisputeStatus::DisputeWon => Self::Won,
74+
DisputeStatus::DisputeLost => Self::Lost,
75+
}
76+
}
2377
}
2478

2579
impl From<api::OutgoingWebhook> for StripeOutgoingWebhook {
@@ -40,6 +94,9 @@ impl From<api::OutgoingWebhookContent> for StripeWebhookObject {
4094
Self::PaymentIntent(payment.into())
4195
}
4296
api::OutgoingWebhookContent::RefundDetails(refund) => Self::Refund(refund.into()),
97+
api::OutgoingWebhookContent::DisputeDetails(dispute) => {
98+
Self::Dispute((*dispute).into())
99+
}
43100
}
44101
}
45102
}
@@ -49,6 +106,7 @@ impl StripeWebhookObject {
49106
match self {
50107
Self::PaymentIntent(p) => p.id.to_owned(),
51108
Self::Refund(r) => Some(r.id.to_owned()),
109+
Self::Dispute(d) => Some(d.id.to_owned()),
52110
}
53111
}
54112
}

crates/router/src/connector/adyen.rs

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ mod transformers;
22

33
use std::fmt::Debug;
44

5+
use api_models::webhooks::IncomingWebhookEvent;
56
use base64::Engine;
67
use error_stack::{IntoReport, ResultExt};
78
use router_env::{instrument, tracing};
@@ -17,6 +18,7 @@ use crate::{
1718
types::{
1819
self,
1920
api::{self, ConnectorCommon},
21+
transformers::ForeignFrom,
2022
},
2123
utils::{self, crypto, ByteSliceExt, BytesExt, OptionExt},
2224
};
@@ -719,27 +721,38 @@ impl api::IncomingWebhook for Adyen {
719721
) -> CustomResult<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> {
720722
let notif = get_webhook_object_from_body(request.body)
721723
.change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?;
722-
match notif.event_code {
723-
adyen::WebhookEventCode::Authorisation => {
724-
Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
725-
api_models::payments::PaymentIdType::ConnectorTransactionId(
726-
notif.psp_reference,
727-
),
728-
))
729-
}
730-
_ => Ok(api_models::webhooks::ObjectReferenceId::RefundId(
724+
if adyen::is_transaction_event(&notif.event_code) {
725+
return Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
726+
api_models::payments::PaymentIdType::ConnectorTransactionId(notif.psp_reference),
727+
));
728+
}
729+
if adyen::is_refund_event(&notif.event_code) {
730+
return Ok(api_models::webhooks::ObjectReferenceId::RefundId(
731731
api_models::webhooks::RefundIdType::ConnectorRefundId(notif.psp_reference),
732-
)),
732+
));
733733
}
734+
if adyen::is_chargeback_event(&notif.event_code) {
735+
return Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
736+
api_models::payments::PaymentIdType::ConnectorTransactionId(
737+
notif
738+
.original_reference
739+
.ok_or(errors::ConnectorError::WebhookReferenceIdNotFound)?,
740+
),
741+
));
742+
}
743+
Err(errors::ConnectorError::WebhookReferenceIdNotFound).into_report()
734744
}
735745

736746
fn get_webhook_event_type(
737747
&self,
738748
request: &api::IncomingWebhookRequestDetails<'_>,
739-
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
749+
) -> CustomResult<IncomingWebhookEvent, errors::ConnectorError> {
740750
let notif = get_webhook_object_from_body(request.body)
741751
.change_context(errors::ConnectorError::WebhookEventTypeNotFound)?;
742-
Ok(notif.event_code.into())
752+
Ok(IncomingWebhookEvent::foreign_from((
753+
notif.event_code,
754+
notif.additional_data.dispute_status,
755+
)))
743756
}
744757

745758
fn get_webhook_resource_object(
@@ -767,4 +780,24 @@ impl api::IncomingWebhook for Adyen {
767780
"[accepted]".to_string(),
768781
))
769782
}
783+
784+
fn get_dispute_details(
785+
&self,
786+
request: &api_models::webhooks::IncomingWebhookRequestDetails<'_>,
787+
) -> CustomResult<api::disputes::DisputePayload, errors::ConnectorError> {
788+
let notif = get_webhook_object_from_body(request.body)
789+
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
790+
Ok(api::disputes::DisputePayload {
791+
amount: notif.amount.value.to_string(),
792+
currency: notif.amount.currency,
793+
dispute_stage: api_models::enums::DisputeStage::from(notif.event_code.clone()),
794+
connector_dispute_id: notif.psp_reference,
795+
connector_reason: notif.reason,
796+
connector_reason_code: notif.additional_data.chargeback_reason_code,
797+
challenge_required_by: notif.additional_data.defense_period_ends_at,
798+
connector_status: notif.event_code.to_string(),
799+
created_at: notif.event_date.clone(),
800+
updated_at: notif.event_date,
801+
})
802+
}
770803
}

0 commit comments

Comments
 (0)