Skip to content

Commit 0688972

Browse files
srujanchikkeChikke Srujanhyperswitch-bot[bot]
authored
feat(connector): Add support for passive churn recovery webhooks (juspay#7109)
Co-authored-by: Chikke Srujan <[email protected]> Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
1 parent 11ff437 commit 0688972

File tree

32 files changed

+992
-36
lines changed

32 files changed

+992
-36
lines changed

api-reference-v2/openapi_spec.json

+44-4
Original file line numberDiff line numberDiff line change
@@ -13397,6 +13397,19 @@
1339713397
}
1339813398
}
1339913399
},
13400+
"PaymentAttemptFeatureMetadata": {
13401+
"type": "object",
13402+
"properties": {
13403+
"revenue_recovery": {
13404+
"allOf": [
13405+
{
13406+
"$ref": "#/components/schemas/PaymentAttemptRevenueRecoveryData"
13407+
}
13408+
],
13409+
"nullable": true
13410+
}
13411+
}
13412+
},
1340013413
"PaymentAttemptResponse": {
1340113414
"type": "object",
1340213415
"required": [
@@ -13405,7 +13418,8 @@
1340513418
"amount",
1340613419
"authentication_type",
1340713420
"created_at",
13408-
"modified_at"
13421+
"modified_at",
13422+
"connector_payment_id"
1340913423
],
1341013424
"properties": {
1341113425
"id": {
@@ -13501,9 +13515,7 @@
1350113515
},
1350213516
"connector_payment_id": {
1350313517
"type": "string",
13504-
"description": "A unique identifier for a payment provided by the connector",
13505-
"example": "993672945374576J",
13506-
"nullable": true
13518+
"description": "A unique identifier for a payment provided by the connector"
1350713519
},
1350813520
"payment_method_id": {
1350913521
"type": "string",
@@ -13520,6 +13532,27 @@
1352013532
"type": "string",
1352113533
"description": "Value passed in X-CLIENT-VERSION header during payments confirm request by the client",
1352213534
"nullable": true
13535+
},
13536+
"feature_metadata": {
13537+
"allOf": [
13538+
{
13539+
"$ref": "#/components/schemas/PaymentAttemptFeatureMetadata"
13540+
}
13541+
],
13542+
"nullable": true
13543+
}
13544+
}
13545+
},
13546+
"PaymentAttemptRevenueRecoveryData": {
13547+
"type": "object",
13548+
"properties": {
13549+
"attempt_triggered_by": {
13550+
"allOf": [
13551+
{
13552+
"$ref": "#/components/schemas/TriggeredBy"
13553+
}
13554+
],
13555+
"nullable": true
1352313556
}
1352413557
}
1352513558
},
@@ -21526,6 +21559,13 @@
2152621559
"payout"
2152721560
]
2152821561
},
21562+
"TriggeredBy": {
21563+
"type": "string",
21564+
"enum": [
21565+
"internal",
21566+
"external"
21567+
]
21568+
},
2152921569
"UIWidgetFormLayout": {
2153021570
"type": "string",
2153121571
"enum": [

api-reference/openapi_spec.json

+7
Original file line numberDiff line numberDiff line change
@@ -26144,6 +26144,13 @@
2614426144
"payout"
2614526145
]
2614626146
},
26147+
"TriggeredBy": {
26148+
"type": "string",
26149+
"enum": [
26150+
"internal",
26151+
"external"
26152+
]
26153+
},
2614726154
"UIWidgetFormLayout": {
2614826155
"type": "string",
2614926156
"enum": [

crates/api_models/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ customer_v2 = ["common_utils/customer_v2"]
2222
payment_methods_v2 = ["common_utils/payment_methods_v2"]
2323
dynamic_routing = []
2424
control_center_theme = ["dep:actix-web", "dep:actix-multipart"]
25+
revenue_recovery = []
2526

2627
[dependencies]
2728
actix-multipart = { version = "0.6.1", optional = true }

crates/api_models/src/payments.rs

+40-2
Original file line numberDiff line numberDiff line change
@@ -1511,8 +1511,8 @@ pub struct PaymentAttemptResponse {
15111511
pub payment_method_subtype: Option<api_enums::PaymentMethodType>,
15121512

15131513
/// A unique identifier for a payment provided by the connector
1514-
#[schema(value_type = Option<String>, example = "993672945374576J")]
1515-
pub connector_payment_id: Option<String>,
1514+
#[schema(value_type = String)]
1515+
pub connector_payment_id: Option<common_utils::types::ConnectorTransactionId>,
15161516

15171517
/// Identifier for Payment Method used for the payment attempt
15181518
#[schema(value_type = Option<String>, example = "12345_pm_01926c58bc6e77c09e809964e72af8c8")]
@@ -1522,6 +1522,24 @@ pub struct PaymentAttemptResponse {
15221522
pub client_source: Option<String>,
15231523
/// Value passed in X-CLIENT-VERSION header during payments confirm request by the client
15241524
pub client_version: Option<String>,
1525+
1526+
/// Additional data that might be required by hyperswitch, to enable some specific features.
1527+
pub feature_metadata: Option<PaymentAttemptFeatureMetadata>,
1528+
}
1529+
1530+
#[cfg(feature = "v2")]
1531+
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, ToSchema)]
1532+
pub struct PaymentAttemptFeatureMetadata {
1533+
/// Revenue recovery metadata that might be required by hyperswitch.
1534+
pub revenue_recovery: Option<PaymentAttemptRevenueRecoveryData>,
1535+
}
1536+
1537+
#[cfg(feature = "v2")]
1538+
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, ToSchema)]
1539+
pub struct PaymentAttemptRevenueRecoveryData {
1540+
/// Flag to find out whether an attempt was created by external or internal system.
1541+
#[schema(value_type = Option<TriggeredBy>, example = "internal")]
1542+
pub attempt_triggered_by: common_enums::TriggeredBy,
15251543
}
15261544

15271545
#[derive(
@@ -5563,6 +5581,26 @@ pub struct PaymentsRetrieveResponse {
55635581
pub attempts: Option<Vec<PaymentAttemptResponse>>,
55645582
}
55655583

5584+
#[cfg(feature = "v2")]
5585+
impl PaymentsRetrieveResponse {
5586+
pub fn find_attempt_in_attempts_list_using_connector_transaction_id(
5587+
self,
5588+
connector_transaction_id: &common_utils::types::ConnectorTransactionId,
5589+
) -> Option<PaymentAttemptResponse> {
5590+
self.attempts
5591+
.as_ref()
5592+
.and_then(|attempts| {
5593+
attempts.iter().find(|attempt| {
5594+
attempt
5595+
.connector_payment_id
5596+
.as_ref()
5597+
.is_some_and(|txn_id| txn_id == connector_transaction_id)
5598+
})
5599+
})
5600+
.cloned()
5601+
}
5602+
}
5603+
55665604
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
55675605
#[cfg(feature = "v2")]
55685606
pub struct PaymentStartRedirectionRequest {

crates/api_models/src/webhooks.rs

+35
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ pub enum IncomingWebhookEvent {
5757
PayoutExpired,
5858
#[cfg(feature = "payouts")]
5959
PayoutReversed,
60+
#[cfg(all(feature = "revenue_recovery", feature = "v2"))]
61+
RecoveryPaymentFailure,
62+
#[cfg(all(feature = "revenue_recovery", feature = "v2"))]
63+
RecoveryPaymentSuccess,
64+
#[cfg(all(feature = "revenue_recovery", feature = "v2"))]
65+
RecoveryPaymentPending,
66+
#[cfg(all(feature = "revenue_recovery", feature = "v2"))]
67+
RecoveryInvoiceCancel,
6068
}
6169

6270
pub enum WebhookFlow {
@@ -71,6 +79,8 @@ pub enum WebhookFlow {
7179
Mandate,
7280
ExternalAuthentication,
7381
FraudCheck,
82+
#[cfg(all(feature = "revenue_recovery", feature = "v2"))]
83+
Recovery,
7484
}
7585

7686
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
@@ -197,6 +207,11 @@ impl From<IncomingWebhookEvent> for WebhookFlow {
197207
| IncomingWebhookEvent::PayoutCreated
198208
| IncomingWebhookEvent::PayoutExpired
199209
| IncomingWebhookEvent::PayoutReversed => Self::Payout,
210+
#[cfg(all(feature = "revenue_recovery", feature = "v2"))]
211+
IncomingWebhookEvent::RecoveryInvoiceCancel
212+
| IncomingWebhookEvent::RecoveryPaymentFailure
213+
| IncomingWebhookEvent::RecoveryPaymentPending
214+
| IncomingWebhookEvent::RecoveryPaymentSuccess => Self::Recovery,
200215
}
201216
}
202217
}
@@ -236,6 +251,14 @@ pub enum ObjectReferenceId {
236251
ExternalAuthenticationID(AuthenticationIdType),
237252
#[cfg(feature = "payouts")]
238253
PayoutId(PayoutIdType),
254+
#[cfg(all(feature = "revenue_recovery", feature = "v2"))]
255+
InvoiceId(InvoiceIdType),
256+
}
257+
258+
#[cfg(all(feature = "revenue_recovery", feature = "v2"))]
259+
#[derive(Clone)]
260+
pub enum InvoiceIdType {
261+
ConnectorInvoiceId(String),
239262
}
240263

241264
pub struct IncomingWebhookDetails {
@@ -303,3 +326,15 @@ pub struct ConnectorWebhookSecrets {
303326
pub secret: Vec<u8>,
304327
pub additional_secret: Option<masking::Secret<String>>,
305328
}
329+
330+
#[cfg(all(feature = "v2", feature = "revenue_recovery"))]
331+
impl IncomingWebhookEvent {
332+
pub fn is_recovery_transaction_event(&self) -> bool {
333+
matches!(
334+
self,
335+
Self::RecoveryPaymentFailure
336+
| Self::RecoveryPaymentSuccess
337+
| Self::RecoveryPaymentPending
338+
)
339+
}
340+
}

crates/common_enums/src/enums.rs

+25
Original file line numberDiff line numberDiff line change
@@ -7591,3 +7591,28 @@ pub enum PaymentConnectorTransmission {
75917591
/// Payment Connector call succeeded
75927592
ConnectorCallSucceeded,
75937593
}
7594+
7595+
#[derive(
7596+
Clone,
7597+
Copy,
7598+
Debug,
7599+
Default,
7600+
Eq,
7601+
Hash,
7602+
PartialEq,
7603+
serde::Deserialize,
7604+
serde::Serialize,
7605+
strum::Display,
7606+
strum::EnumString,
7607+
ToSchema,
7608+
)]
7609+
#[router_derive::diesel_enum(storage_type = "db_enum")]
7610+
#[strum(serialize_all = "snake_case")]
7611+
#[serde(rename_all = "snake_case")]
7612+
pub enum TriggeredBy {
7613+
/// Denotes payment attempt is been created by internal system.
7614+
#[default]
7615+
Internal,
7616+
/// Denotes payment attempt is been created by external system.
7617+
External,
7618+
}

crates/common_utils/src/ext_traits.rs

+43-9
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
use error_stack::ResultExt;
55
use masking::{ExposeInterface, PeekInterface, Secret, Strategy};
66
use quick_xml::de;
7+
#[cfg(all(feature = "logs", feature = "async_ext"))]
8+
use router_env::logger;
79
use serde::{Deserialize, Serialize};
810

911
use crate::{
@@ -295,28 +297,34 @@ impl<T> StringExt<T> for String {
295297
/// Extending functionalities of Wrapper types for idiomatic
296298
#[cfg(feature = "async_ext")]
297299
#[cfg_attr(feature = "async_ext", async_trait::async_trait)]
298-
pub trait AsyncExt<A, B> {
300+
pub trait AsyncExt<A> {
299301
/// Output type of the map function
300302
type WrappedSelf<T>;
301303

302304
/// Extending map by allowing functions which are async
303-
async fn async_map<F, Fut>(self, func: F) -> Self::WrappedSelf<B>
305+
async fn async_map<F, B, Fut>(self, func: F) -> Self::WrappedSelf<B>
304306
where
305307
F: FnOnce(A) -> Fut + Send,
306308
Fut: futures::Future<Output = B> + Send;
307309

308310
/// Extending the `and_then` by allowing functions which are async
309-
async fn async_and_then<F, Fut>(self, func: F) -> Self::WrappedSelf<B>
311+
async fn async_and_then<F, B, Fut>(self, func: F) -> Self::WrappedSelf<B>
310312
where
311313
F: FnOnce(A) -> Fut + Send,
312314
Fut: futures::Future<Output = Self::WrappedSelf<B>> + Send;
315+
316+
/// Extending `unwrap_or_else` to allow async fallback
317+
async fn async_unwrap_or_else<F, Fut>(self, func: F) -> A
318+
where
319+
F: FnOnce() -> Fut + Send,
320+
Fut: futures::Future<Output = A> + Send;
313321
}
314322

315323
#[cfg(feature = "async_ext")]
316324
#[cfg_attr(feature = "async_ext", async_trait::async_trait)]
317-
impl<A: Send, B, E: Send> AsyncExt<A, B> for Result<A, E> {
325+
impl<A: Send, E: Send + std::fmt::Debug> AsyncExt<A> for Result<A, E> {
318326
type WrappedSelf<T> = Result<T, E>;
319-
async fn async_and_then<F, Fut>(self, func: F) -> Self::WrappedSelf<B>
327+
async fn async_and_then<F, B, Fut>(self, func: F) -> Self::WrappedSelf<B>
320328
where
321329
F: FnOnce(A) -> Fut + Send,
322330
Fut: futures::Future<Output = Self::WrappedSelf<B>> + Send,
@@ -327,7 +335,7 @@ impl<A: Send, B, E: Send> AsyncExt<A, B> for Result<A, E> {
327335
}
328336
}
329337

330-
async fn async_map<F, Fut>(self, func: F) -> Self::WrappedSelf<B>
338+
async fn async_map<F, B, Fut>(self, func: F) -> Self::WrappedSelf<B>
331339
where
332340
F: FnOnce(A) -> Fut + Send,
333341
Fut: futures::Future<Output = B> + Send,
@@ -337,13 +345,28 @@ impl<A: Send, B, E: Send> AsyncExt<A, B> for Result<A, E> {
337345
Err(err) => Err(err),
338346
}
339347
}
348+
349+
async fn async_unwrap_or_else<F, Fut>(self, func: F) -> A
350+
where
351+
F: FnOnce() -> Fut + Send,
352+
Fut: futures::Future<Output = A> + Send,
353+
{
354+
match self {
355+
Ok(a) => a,
356+
Err(_err) => {
357+
#[cfg(feature = "logs")]
358+
logger::error!("Error: {:?}", _err);
359+
func().await
360+
}
361+
}
362+
}
340363
}
341364

342365
#[cfg(feature = "async_ext")]
343366
#[cfg_attr(feature = "async_ext", async_trait::async_trait)]
344-
impl<A: Send, B> AsyncExt<A, B> for Option<A> {
367+
impl<A: Send> AsyncExt<A> for Option<A> {
345368
type WrappedSelf<T> = Option<T>;
346-
async fn async_and_then<F, Fut>(self, func: F) -> Self::WrappedSelf<B>
369+
async fn async_and_then<F, B, Fut>(self, func: F) -> Self::WrappedSelf<B>
347370
where
348371
F: FnOnce(A) -> Fut + Send,
349372
Fut: futures::Future<Output = Self::WrappedSelf<B>> + Send,
@@ -354,7 +377,7 @@ impl<A: Send, B> AsyncExt<A, B> for Option<A> {
354377
}
355378
}
356379

357-
async fn async_map<F, Fut>(self, func: F) -> Self::WrappedSelf<B>
380+
async fn async_map<F, B, Fut>(self, func: F) -> Self::WrappedSelf<B>
358381
where
359382
F: FnOnce(A) -> Fut + Send,
360383
Fut: futures::Future<Output = B> + Send,
@@ -364,6 +387,17 @@ impl<A: Send, B> AsyncExt<A, B> for Option<A> {
364387
None => None,
365388
}
366389
}
390+
391+
async fn async_unwrap_or_else<F, Fut>(self, func: F) -> A
392+
where
393+
F: FnOnce() -> Fut + Send,
394+
Fut: futures::Future<Output = A> + Send,
395+
{
396+
match self {
397+
Some(a) => a,
398+
None => func().await,
399+
}
400+
}
367401
}
368402

369403
/// Extension trait for validating application configuration. This trait provides utilities to

0 commit comments

Comments
 (0)