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:
Kashif
2024-12-05 17:23:44 +05:30
committed by GitHub
parent 4bfabdfa24
commit 871a36379d
7 changed files with 195 additions and 91 deletions

View File

@ -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;
@ -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"))]
@ -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,
},
}
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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 {}

View File

@ -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
#[serde(rename = "recurring.recurringDetailReference")]
pub recurring_detail_reference: Option<Secret<String>>,
pub network_tx_reference: Option<Secret<String>>,
}
#[derive(Debug, Deserialize)]

View File

@ -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
@ -1835,6 +1844,9 @@ async fn update_connector_mandate_details(
.await
.to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?;
// 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()
@ -1859,30 +1871,9 @@ async fn update_connector_mandate_details(
})
.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
let (mandate_metadata, connector_mandate_request_reference_id) =
payment_attempt
.connector_mandate_detail
.as_ref()
.map(|details| {
@ -1905,7 +1896,8 @@ async fn update_connector_mandate_details(
connector_mandate_request_reference_id,
};
let attempt_update = storage::PaymentAttemptUpdate::ConnectorMandateDetailUpdate {
let attempt_update =
storage::PaymentAttemptUpdate::ConnectorMandateDetailUpdate {
connector_mandate_detail: Some(connector_mandate_reference_id),
updated_by: merchant_account.storage_scheme.to_string(),
};
@ -1919,11 +1911,40 @@ async fn update_connector_mandate_details(
)
.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)
.attach_printable("Failed to update payment method in db")?;
}
}
Ok(())

View File

@ -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 {