Skip to content
27 changes: 26 additions & 1 deletion crates/diesel_models/src/payment_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use diesel::{AsChangeset, Identifiable, Insertable, Queryable, Selectable};
any(feature = "v1", feature = "v2"),
not(feature = "payment_methods_v2")
))]
use masking::Secret;
use masking::{ExposeInterface, Secret};
use serde::{Deserialize, Serialize};
use time::PrimitiveDateTime;

Expand Down Expand Up @@ -253,6 +253,10 @@ pub enum PaymentMethodUpdate {
network_token_locker_id: Option<String>,
network_token_payment_method_data: Option<Encryption>,
},
ConnectorNetworkTransactionIdAndMandateDetailsUpdate {
connector_mandate_details: Option<pii::SecretSerdeValue>,
network_transaction_id: Option<Secret<String>>,
},
}

#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
Expand Down Expand Up @@ -646,6 +650,27 @@ impl From<PaymentMethodUpdate> for PaymentMethodUpdateInternal {
network_token_locker_id,
network_token_payment_method_data,
},
PaymentMethodUpdate::ConnectorNetworkTransactionIdAndMandateDetailsUpdate {
connector_mandate_details,
network_transaction_id,
} => Self {
connector_mandate_details: connector_mandate_details
.map(|mandate_details| mandate_details.expose()),
network_transaction_id: network_transaction_id.map(|txn_id| txn_id.expose()),
last_modified: common_utils::date_time::now(),
status: None,
metadata: None,
payment_method_data: None,
last_used_at: None,
locker_id: None,
payment_method: None,
updated_by: None,
payment_method_issuer: None,
payment_method_type: None,
network_token_requestor_reference_id: None,
network_token_locker_id: None,
network_token_payment_method_data: None,
},
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,15 @@ pub struct VerifyWebhookSource;
pub struct ConnectorMandateDetails {
pub connector_mandate_id: masking::Secret<String>,
}

#[derive(Debug, Clone, Serialize)]
pub struct ConnectorNetworkTxnId(masking::Secret<String>);

impl ConnectorNetworkTxnId {
pub fn new(txn_id: masking::Secret<String>) -> Self {
Self(txn_id)
}
pub fn get_id(&self) -> &masking::Secret<String> {
&self.0
}
}
11 changes: 11 additions & 0 deletions crates/hyperswitch_interfaces/src/webhooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,4 +264,15 @@ pub trait IncomingWebhook: ConnectorCommon + Sync {
> {
Ok(None)
}

/// fn get_network_txn_id
fn get_network_txn_id(
&self,
_request: &IncomingWebhookRequestDetails<'_>,
) -> CustomResult<
Option<hyperswitch_domain_models::router_flow_types::ConnectorNetworkTxnId>,
errors::ConnectorError,
> {
Ok(None)
}
}
21 changes: 21 additions & 0 deletions crates/router/src/connector/adyen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1929,6 +1929,27 @@ impl api::IncomingWebhook for Adyen {
});
Ok(mandate_reference)
}

fn get_network_txn_id(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<
Option<hyperswitch_domain_models::router_flow_types::ConnectorNetworkTxnId>,
errors::ConnectorError,
> {
let notif = get_webhook_object_from_body(request.body)
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
let optional_network_txn_id =
notif
.additional_data
.network_tx_reference
.map(|network_txn_id| {
hyperswitch_domain_models::router_flow_types::ConnectorNetworkTxnId::new(
network_txn_id,
)
});
Ok(optional_network_txn_id)
}
}

impl api::Dispute for Adyen {}
Expand Down
1 change: 1 addition & 0 deletions crates/router/src/connector/adyen/transformers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4264,6 +4264,7 @@ pub struct AdyenAdditionalDataWH {
/// Enable recurring details in dashboard to receive this ID, https://docs.adyen.com/online-payments/tokenization/create-and-use-tokens#test-and-go-live
#[serde(rename = "recurring.recurringDetailReference")]
pub recurring_detail_reference: Option<Secret<String>>,
pub network_tx_reference: Option<Secret<String>>,
}

#[derive(Debug, Deserialize)]
Expand Down
193 changes: 107 additions & 86 deletions crates/router/src/core/webhooks/incoming.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1817,11 +1817,20 @@ async fn update_connector_mandate_details(
.switch()
.attach_printable("Could not find connector mandate details in incoming webhook body")?;

if let Some(webhook_mandate_details) = webhook_connector_mandate_details {
let webhook_connector_network_transaction_id = connector
.get_network_txn_id(request_details)
.switch()
.attach_printable(
"Could not find connector network transaction id in incoming webhook body",
)?;

// Either one OR both of the fields are present
if webhook_connector_mandate_details.is_some()
|| webhook_connector_network_transaction_id.is_some()
{
let payment_attempt =
get_payment_attempt_from_object_reference_id(state, object_ref_id, merchant_account)
.await?;

if let Some(ref payment_method_id) = payment_attempt.payment_method_id {
let key_manager_state = &state.into();
let payment_method_info = state
Expand All @@ -1835,95 +1844,107 @@ async fn update_connector_mandate_details(
.await
.to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?;

let mandate_details = payment_method_info
.connector_mandate_details
.clone()
.map(|val| {
val.parse_value::<diesel_models::PaymentsMandateReference>(
"PaymentsMandateReference",
)
})
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to deserialize to Payment Mandate Reference")?;

let merchant_connector_account_id = payment_attempt
.merchant_connector_id
.clone()
.get_required_value("merchant_connector_id")?;
// Update connector's mandate details
let updated_connector_mandate_details =
if let Some(webhook_mandate_details) = webhook_connector_mandate_details {
let mandate_details = payment_method_info
.connector_mandate_details
.clone()
.map(|val| {
val.parse_value::<diesel_models::PaymentsMandateReference>(
"PaymentsMandateReference",
)
})
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to deserialize to Payment Mandate Reference")?;

let merchant_connector_account_id = payment_attempt
.merchant_connector_id
.clone()
.get_required_value("merchant_connector_id")?;

if mandate_details
.as_ref()
.map(|details: &diesel_models::PaymentsMandateReference| {
!details.0.contains_key(&merchant_connector_account_id)
})
.unwrap_or(true)
{
// Update the payment attempt to maintain consistency across tables.
let (mandate_metadata, connector_mandate_request_reference_id) =
payment_attempt
.connector_mandate_detail
.as_ref()
.map(|details| {
(
details.mandate_metadata.clone(),
details.connector_mandate_request_reference_id.clone(),
)
})
.unwrap_or((None, None));

let connector_mandate_reference_id = ConnectorMandateReferenceId {
connector_mandate_id: Some(
webhook_mandate_details
.connector_mandate_id
.peek()
.to_string(),
),
payment_method_id: Some(payment_method_id.to_string()),
mandate_metadata,
connector_mandate_request_reference_id,
};

if mandate_details
.as_ref()
.map(|details: &diesel_models::PaymentsMandateReference| {
!details.0.contains_key(&merchant_connector_account_id)
})
.unwrap_or(true)
{
let updated_connector_mandate_details = insert_mandate_details(
&payment_attempt,
&webhook_mandate_details,
mandate_details,
)?;
let pm_update = diesel_models::PaymentMethodUpdate::ConnectorMandateDetailsUpdate {
connector_mandate_details: updated_connector_mandate_details,
};
let attempt_update =
storage::PaymentAttemptUpdate::ConnectorMandateDetailUpdate {
connector_mandate_detail: Some(connector_mandate_reference_id),
updated_by: merchant_account.storage_scheme.to_string(),
};

state
.store
.update_payment_method(
key_manager_state,
key_store,
payment_method_info,
pm_update,
merchant_account.storage_scheme,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to update payment method in db")?;
// Update the payment attempt to maintain consistency across tables.

let (mandate_metadata, connector_mandate_request_reference_id) = payment_attempt
.connector_mandate_detail
.as_ref()
.map(|details| {
(
details.mandate_metadata.clone(),
details.connector_mandate_request_reference_id.clone(),
)
})
.unwrap_or((None, None));

let connector_mandate_reference_id = ConnectorMandateReferenceId {
connector_mandate_id: Some(
webhook_mandate_details
.connector_mandate_id
.peek()
.to_string(),
),
payment_method_id: Some(payment_method_id.to_string()),
mandate_metadata,
connector_mandate_request_reference_id,
state
.store
.update_payment_attempt_with_attempt_id(
payment_attempt.clone(),
attempt_update,
merchant_account.storage_scheme,
)
.await
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;

insert_mandate_details(
&payment_attempt,
&webhook_mandate_details,
mandate_details,
)?
} else {
logger::info!(
"Skipping connector mandate details update since they are already present."
);
None
}
} else {
None
};

let attempt_update = storage::PaymentAttemptUpdate::ConnectorMandateDetailUpdate {
connector_mandate_detail: Some(connector_mandate_reference_id),
updated_by: merchant_account.storage_scheme.to_string(),
};
let pm_update = diesel_models::PaymentMethodUpdate::ConnectorNetworkTransactionIdAndMandateDetailsUpdate {
connector_mandate_details: updated_connector_mandate_details.map(masking::Secret::new),
network_transaction_id: webhook_connector_network_transaction_id
.map(|webhook_network_transaction_id| webhook_network_transaction_id.get_id().clone()),
};

state
.store
.update_payment_attempt_with_attempt_id(
payment_attempt.clone(),
attempt_update,
merchant_account.storage_scheme,
)
.await
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
} else {
logger::info!(
"Skipping connector mandate details update since they are already present."
);
}
state
.store
.update_payment_method(
key_manager_state,
key_store,
payment_method_info,
pm_update,
merchant_account.storage_scheme,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to update payment method in db")?;
}
}
Ok(())
Expand Down
13 changes: 13 additions & 0 deletions crates/router/src/services/connector_integration_interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,19 @@ impl api::IncomingWebhook for ConnectorEnum {
Self::New(connector) => connector.get_mandate_details(request),
}
}

fn get_network_txn_id(
&self,
request: &IncomingWebhookRequestDetails<'_>,
) -> CustomResult<
Option<hyperswitch_domain_models::router_flow_types::ConnectorNetworkTxnId>,
errors::ConnectorError,
> {
match self {
Self::Old(connector) => connector.get_network_txn_id(request),
Self::New(connector) => connector.get_network_txn_id(request),
}
}
}

impl api::ConnectorTransactionId for ConnectorEnum {
Expand Down
Loading