Skip to content

Commit 1cf8b6c

Browse files
feat(router): add mandates incoming webhooks flow (#2464)
1 parent 3f0d927 commit 1cf8b6c

File tree

19 files changed

+321
-8
lines changed

19 files changed

+321
-8
lines changed

crates/api_models/src/mandates.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pub struct MandateRevokedResponse {
1919
pub status: api_enums::MandateStatus,
2020
}
2121

22-
#[derive(Default, Debug, Deserialize, Serialize, ToSchema)]
22+
#[derive(Default, Debug, Deserialize, Serialize, ToSchema, Clone)]
2323
pub struct MandateResponse {
2424
/// The identifier for mandate
2525
pub mandate_id: String,
@@ -37,7 +37,7 @@ pub struct MandateResponse {
3737
pub customer_acceptance: Option<payments::CustomerAcceptance>,
3838
}
3939

40-
#[derive(Default, Debug, Deserialize, Serialize, ToSchema)]
40+
#[derive(Default, Debug, Deserialize, Serialize, ToSchema, Clone)]
4141
pub struct MandateCardDetails {
4242
/// The last 4 digits of card
4343
pub last4_digits: Option<String>,

crates/api_models/src/webhooks.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
33
use time::PrimitiveDateTime;
44
use utoipa::ToSchema;
55

6-
use crate::{disputes, enums as api_enums, payments, refunds};
6+
use crate::{disputes, enums as api_enums, mandates, payments, refunds};
77

88
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Copy)]
99
#[serde(rename_all = "snake_case")]
@@ -27,6 +27,8 @@ pub enum IncomingWebhookEvent {
2727
DisputeWon,
2828
// dispute has been unsuccessfully challenged
2929
DisputeLost,
30+
MandateActive,
31+
MandateRevoked,
3032
EndpointVerification,
3133
}
3234

@@ -37,6 +39,7 @@ pub enum WebhookFlow {
3739
Subscription,
3840
ReturnResponse,
3941
BankTransfer,
42+
Mandate,
4043
}
4144

4245
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
@@ -56,6 +59,10 @@ pub enum WebhookResponseTracker {
5659
payment_id: String,
5760
status: common_enums::DisputeStatus,
5861
},
62+
Mandate {
63+
mandate_id: String,
64+
status: common_enums::MandateStatus,
65+
},
5966
NoEffect,
6067
}
6168

@@ -65,7 +72,7 @@ impl WebhookResponseTracker {
6572
Self::Payment { payment_id, .. }
6673
| Self::Refund { payment_id, .. }
6774
| Self::Dispute { payment_id, .. } => Some(payment_id.to_string()),
68-
Self::NoEffect => None,
75+
Self::NoEffect | Self::Mandate { .. } => None,
6976
}
7077
}
7178
}
@@ -82,6 +89,9 @@ impl From<IncomingWebhookEvent> for WebhookFlow {
8289
IncomingWebhookEvent::RefundSuccess | IncomingWebhookEvent::RefundFailure => {
8390
Self::Refund
8491
}
92+
IncomingWebhookEvent::MandateActive | IncomingWebhookEvent::MandateRevoked => {
93+
Self::Mandate
94+
}
8595
IncomingWebhookEvent::DisputeOpened
8696
| IncomingWebhookEvent::DisputeAccepted
8797
| IncomingWebhookEvent::DisputeExpired
@@ -104,10 +114,17 @@ pub enum RefundIdType {
104114
ConnectorRefundId(String),
105115
}
106116

117+
#[derive(Clone)]
118+
pub enum MandateIdType {
119+
MandateId(String),
120+
ConnectorMandateId(String),
121+
}
122+
107123
#[derive(Clone)]
108124
pub enum ObjectReferenceId {
109125
PaymentId(payments::PaymentIdType),
110126
RefundId(RefundIdType),
127+
MandateId(MandateIdType),
111128
}
112129

113130
pub struct IncomingWebhookDetails {
@@ -144,6 +161,8 @@ pub enum OutgoingWebhookContent {
144161
RefundDetails(refunds::RefundResponse),
145162
#[schema(value_type = DisputeResponse)]
146163
DisputeDetails(Box<disputes::DisputeResponse>),
164+
#[schema(value_type = MandateResponse)]
165+
MandateDetails(Box<mandates::MandateResponse>),
147166
}
148167

149168
#[derive(Debug, Clone, Serialize)]

crates/common_enums/src/enums.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,8 @@ pub enum EventType {
804804
DisputeChallenged,
805805
DisputeWon,
806806
DisputeLost,
807+
MandateActive,
808+
MandateRevoked,
807809
}
808810

809811
#[derive(

crates/diesel_models/src/enums.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ pub enum EventClass {
3939
Payments,
4040
Refunds,
4141
Disputes,
42+
Mandates,
4243
}
4344

4445
#[derive(
@@ -59,6 +60,7 @@ pub enum EventObjectType {
5960
PaymentDetails,
6061
RefundDetails,
6162
DisputeDetails,
63+
MandateDetails,
6264
}
6365

6466
#[derive(

crates/diesel_models/src/mandate.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ pub struct Mandate {
3030
pub end_date: Option<PrimitiveDateTime>,
3131
pub metadata: Option<pii::SecretSerdeValue>,
3232
pub connector_mandate_ids: Option<pii::SecretSerdeValue>,
33+
pub original_payment_id: Option<String>,
3334
}
3435

3536
#[derive(
@@ -58,6 +59,7 @@ pub struct MandateNew {
5859
pub end_date: Option<PrimitiveDateTime>,
5960
pub metadata: Option<pii::SecretSerdeValue>,
6061
pub connector_mandate_ids: Option<pii::SecretSerdeValue>,
62+
pub original_payment_id: Option<String>,
6163
}
6264

6365
#[derive(Debug)]

crates/diesel_models/src/query/mandate.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,20 @@ impl Mandate {
2727
.await
2828
}
2929

30+
pub async fn find_by_merchant_id_connector_mandate_id(
31+
conn: &PgPooledConn,
32+
merchant_id: &str,
33+
connector_mandate_id: &str,
34+
) -> StorageResult<Self> {
35+
generics::generic_find_one::<<Self as HasTable>::Table, _, _>(
36+
conn,
37+
dsl::merchant_id
38+
.eq(merchant_id.to_owned())
39+
.and(dsl::connector_mandate_id.eq(connector_mandate_id.to_owned())),
40+
)
41+
.await
42+
}
43+
3044
pub async fn find_by_merchant_id_customer_id(
3145
conn: &PgPooledConn,
3246
merchant_id: &str,

crates/diesel_models/src/schema.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,8 @@ diesel::table! {
398398
end_date -> Nullable<Timestamp>,
399399
metadata -> Nullable<Jsonb>,
400400
connector_mandate_ids -> Nullable<Jsonb>,
401+
#[max_length = 64]
402+
original_payment_id -> Nullable<Varchar>,
401403
}
402404
}
403405

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

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use api_models::{
2-
enums::DisputeStatus,
2+
enums::{DisputeStatus, MandateStatus},
33
webhooks::{self as api},
44
};
55
use common_utils::{crypto::SignMessage, date_time, ext_traits};
@@ -73,6 +73,7 @@ pub enum StripeWebhookObject {
7373
PaymentIntent(StripePaymentIntentResponse),
7474
Refund(StripeRefundResponse),
7575
Dispute(StripeDisputeResponse),
76+
Mandate(StripeMandateResponse),
7677
}
7778

7879
#[derive(Serialize, Debug)]
@@ -85,6 +86,22 @@ pub struct StripeDisputeResponse {
8586
pub status: StripeDisputeStatus,
8687
}
8788

89+
#[derive(Serialize, Debug)]
90+
pub struct StripeMandateResponse {
91+
pub mandate_id: String,
92+
pub status: StripeMandateStatus,
93+
pub payment_method_id: String,
94+
pub payment_method: String,
95+
}
96+
97+
#[derive(Serialize, Debug)]
98+
#[serde(rename_all = "snake_case")]
99+
pub enum StripeMandateStatus {
100+
Active,
101+
Inactive,
102+
Pending,
103+
}
104+
88105
#[derive(Serialize, Debug)]
89106
#[serde(rename_all = "snake_case")]
90107
pub enum StripeDisputeStatus {
@@ -111,6 +128,27 @@ impl From<api_models::disputes::DisputeResponse> for StripeDisputeResponse {
111128
}
112129
}
113130

131+
impl From<api_models::mandates::MandateResponse> for StripeMandateResponse {
132+
fn from(res: api_models::mandates::MandateResponse) -> Self {
133+
Self {
134+
mandate_id: res.mandate_id,
135+
payment_method: res.payment_method,
136+
payment_method_id: res.payment_method_id,
137+
status: StripeMandateStatus::from(res.status),
138+
}
139+
}
140+
}
141+
142+
impl From<MandateStatus> for StripeMandateStatus {
143+
fn from(status: MandateStatus) -> Self {
144+
match status {
145+
MandateStatus::Active => Self::Active,
146+
MandateStatus::Inactive | MandateStatus::Revoked => Self::Inactive,
147+
MandateStatus::Pending => Self::Pending,
148+
}
149+
}
150+
}
151+
114152
impl From<DisputeStatus> for StripeDisputeStatus {
115153
fn from(status: DisputeStatus) -> Self {
116154
match status {
@@ -142,6 +180,8 @@ fn get_stripe_event_type(event_type: api_models::enums::EventType) -> &'static s
142180
api_models::enums::EventType::DisputeChallenged => "dispute.challenged",
143181
api_models::enums::EventType::DisputeWon => "dispute.won",
144182
api_models::enums::EventType::DisputeLost => "dispute.lost",
183+
api_models::enums::EventType::MandateActive => "mandate.active",
184+
api_models::enums::EventType::MandateRevoked => "mandate.revoked",
145185
}
146186
}
147187

@@ -179,6 +219,9 @@ impl From<api::OutgoingWebhookContent> for StripeWebhookObject {
179219
api::OutgoingWebhookContent::DisputeDetails(dispute) => {
180220
Self::Dispute((*dispute).into())
181221
}
222+
api::OutgoingWebhookContent::MandateDetails(mandate) => {
223+
Self::Mandate((*mandate).into())
224+
}
182225
}
183226
}
184227
}

crates/router/src/core/mandate.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ where
218218

219219
if let Some(new_mandate_data) = helpers::generate_mandate(
220220
resp.merchant_id.clone(),
221+
resp.payment_id.clone(),
221222
resp.connector.clone(),
222223
resp.request.get_setup_mandate_details().map(Clone::clone),
223224
maybe_customer,

crates/router/src/core/payments/helpers.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2115,6 +2115,7 @@ pub fn check_if_operation_confirm<Op: std::fmt::Debug>(operations: Op) -> bool {
21152115
#[allow(clippy::too_many_arguments)]
21162116
pub fn generate_mandate(
21172117
merchant_id: String,
2118+
payment_id: String,
21182119
connector: String,
21192120
setup_mandate_details: Option<MandateData>,
21202121
customer: &Option<domain::Customer>,
@@ -2137,6 +2138,7 @@ pub fn generate_mandate(
21372138
.set_mandate_id(mandate_id)
21382139
.set_customer_id(cus.customer_id.clone())
21392140
.set_merchant_id(merchant_id)
2141+
.set_original_payment_id(Some(payment_id))
21402142
.set_payment_method_id(payment_method_id)
21412143
.set_connector(connector)
21422144
.set_mandate_status(storage_enums::MandateStatus::Active)

0 commit comments

Comments
 (0)