mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-01 11:06:50 +08:00
feat(webhooks): adyen - consume and update connector's network_transaction_id in payment_methods (#6738)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -7,7 +7,7 @@ use diesel::{AsChangeset, Identifiable, Insertable, Queryable, Selectable};
|
|||||||
any(feature = "v1", feature = "v2"),
|
any(feature = "v1", feature = "v2"),
|
||||||
not(feature = "payment_methods_v2")
|
not(feature = "payment_methods_v2")
|
||||||
))]
|
))]
|
||||||
use masking::Secret;
|
use masking::{ExposeInterface, Secret};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use time::PrimitiveDateTime;
|
use time::PrimitiveDateTime;
|
||||||
|
|
||||||
@ -253,6 +253,10 @@ pub enum PaymentMethodUpdate {
|
|||||||
network_token_locker_id: Option<String>,
|
network_token_locker_id: Option<String>,
|
||||||
network_token_payment_method_data: Option<Encryption>,
|
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"))]
|
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
|
||||||
@ -646,6 +650,27 @@ impl From<PaymentMethodUpdate> for PaymentMethodUpdateInternal {
|
|||||||
network_token_locker_id,
|
network_token_locker_id,
|
||||||
network_token_payment_method_data,
|
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,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,3 +7,15 @@ pub struct VerifyWebhookSource;
|
|||||||
pub struct ConnectorMandateDetails {
|
pub struct ConnectorMandateDetails {
|
||||||
pub connector_mandate_id: masking::Secret<String>,
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -264,4 +264,15 @@ pub trait IncomingWebhook: ConnectorCommon + Sync {
|
|||||||
> {
|
> {
|
||||||
Ok(None)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1929,6 +1929,27 @@ impl api::IncomingWebhook for Adyen {
|
|||||||
});
|
});
|
||||||
Ok(mandate_reference)
|
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 {}
|
impl api::Dispute for Adyen {}
|
||||||
|
|||||||
@ -4268,6 +4268,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
|
/// 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")]
|
#[serde(rename = "recurring.recurringDetailReference")]
|
||||||
pub recurring_detail_reference: Option<Secret<String>>,
|
pub recurring_detail_reference: Option<Secret<String>>,
|
||||||
|
pub network_tx_reference: Option<Secret<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|||||||
@ -1817,11 +1817,20 @@ async fn update_connector_mandate_details(
|
|||||||
.switch()
|
.switch()
|
||||||
.attach_printable("Could not find connector mandate details in incoming webhook body")?;
|
.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 =
|
let payment_attempt =
|
||||||
get_payment_attempt_from_object_reference_id(state, object_ref_id, merchant_account)
|
get_payment_attempt_from_object_reference_id(state, object_ref_id, merchant_account)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if let Some(ref payment_method_id) = payment_attempt.payment_method_id {
|
if let Some(ref payment_method_id) = payment_attempt.payment_method_id {
|
||||||
let key_manager_state = &state.into();
|
let key_manager_state = &state.into();
|
||||||
let payment_method_info = state
|
let payment_method_info = state
|
||||||
@ -1835,95 +1844,107 @@ async fn update_connector_mandate_details(
|
|||||||
.await
|
.await
|
||||||
.to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?;
|
.to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?;
|
||||||
|
|
||||||
let mandate_details = payment_method_info
|
// Update connector's mandate details
|
||||||
.connector_mandate_details
|
let updated_connector_mandate_details =
|
||||||
.clone()
|
if let Some(webhook_mandate_details) = webhook_connector_mandate_details {
|
||||||
.map(|val| {
|
let mandate_details = payment_method_info
|
||||||
val.parse_value::<diesel_models::PaymentsMandateReference>(
|
.connector_mandate_details
|
||||||
"PaymentsMandateReference",
|
.clone()
|
||||||
)
|
.map(|val| {
|
||||||
})
|
val.parse_value::<diesel_models::PaymentsMandateReference>(
|
||||||
.transpose()
|
"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,
|
||||||
|
};
|
||||||
|
|
||||||
|
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_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 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_method(
|
||||||
|
key_manager_state,
|
||||||
|
key_store,
|
||||||
|
payment_method_info,
|
||||||
|
pm_update,
|
||||||
|
merchant_account.storage_scheme,
|
||||||
|
)
|
||||||
|
.await
|
||||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
.attach_printable("Failed to deserialize to Payment Mandate Reference")?;
|
.attach_printable("Failed to update payment method in db")?;
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
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_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."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@ -321,6 +321,19 @@ impl api::IncomingWebhook for ConnectorEnum {
|
|||||||
Self::New(connector) => connector.get_mandate_details(request),
|
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 {
|
impl api::ConnectorTransactionId for ConnectorEnum {
|
||||||
|
|||||||
Reference in New Issue
Block a user