feat(core): added multiple payment_attempt support for payment_intent (#439)

Co-authored-by: Nishant Joshi <nishant.joshi@juspay.in>
This commit is contained in:
Abhishek
2023-03-30 23:49:51 +05:30
committed by GitHub
parent 4d1013c611
commit 35d3e27724
29 changed files with 374 additions and 509 deletions

8
Cargo.lock generated
View File

@@ -4369,9 +4369,9 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.19.7"
version = "0.19.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc18466501acd8ac6a3f615dd29a3438f8ca6bb3b19537138b3106e575621274"
checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13"
dependencies = [
"indexmap",
"serde",
@@ -5024,9 +5024,9 @@ checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
[[package]]
name = "winnow"
version = "0.3.6"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d020b441f92996c80d94ae9166e8501e59c7bb56121189dc9eab3bd8216966"
checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28"
dependencies = [
"memchr",
]

View File

@@ -193,7 +193,7 @@ async fn drainer(
}
kv::Updateable::PaymentAttemptUpdate(a) => {
macro_util::handle_resp!(
a.orig.update(&conn, a.update_data).await,
a.orig.update_with_attempt_id(&conn, a.update_data).await,
update_op,
payment_attempt
)

View File

@@ -880,7 +880,7 @@ impl<F, T>
.transaction_id
.map_or(response.order_id, Some) // For paypal there will be no transaction_id, only order_id will be present
.map(types::ResponseId::ConnectorTransactionId)
.ok_or_else(|| errors::ConnectorError::MissingConnectorTransactionID)?,
.ok_or(errors::ConnectorError::MissingConnectorTransactionID)?,
redirection_data,
mandate_reference: None,
// we don't need to save session token for capture, void flow so ignoring if it is not present

View File

@@ -817,9 +817,10 @@ pub async fn list_payment_methods(
let payment_attempt = payment_intent
.as_ref()
.async_map(|pi| async {
db.find_payment_attempt_by_payment_id_merchant_id(
db.find_payment_attempt_by_payment_id_merchant_id_attempt_id(
&pi.payment_id,
&pi.merchant_id,
&pi.active_attempt_id,
merchant_account.storage_scheme,
)
.await

View File

@@ -6,7 +6,6 @@ pub mod transformers;
use std::{fmt::Debug, marker::PhantomData, time::Instant};
use common_utils::ext_traits::AsyncExt;
use error_stack::{IntoReport, ResultExt};
use futures::future::join_all;
use router_env::{instrument, tracing};
@@ -127,17 +126,29 @@ where
payment_data = match connector_details {
api::ConnectorCallType::Single(connector) => {
call_connector_service(
let router_data = call_connector_service(
state,
&merchant_account,
&validate_result.payment_id,
connector,
&operation,
payment_data,
&payment_data,
&customer,
call_connector_action,
)
.await?
.await?;
let operation = Box::new(PaymentResponse);
let db = &*state.store;
operation
.to_post_update_tracker()?
.update_tracker(
db,
&validate_result.payment_id,
payment_data,
router_data,
merchant_account.storage_scheme,
)
.await?
}
api::ConnectorCallType::Multiple(connectors) => {
@@ -367,13 +378,12 @@ impl PaymentRedirectFlow for PaymentRedirectSync {
pub async fn call_connector_service<F, Op, Req>(
state: &AppState,
merchant_account: &storage::MerchantAccount,
payment_id: &api::PaymentIdType,
connector: api::ConnectorData,
_operation: &Op,
payment_data: PaymentData<F>,
payment_data: &PaymentData<F>,
customer: &Option<storage::Customer>,
call_connector_action: CallConnectorAction,
) -> RouterResult<PaymentData<F>>
) -> RouterResult<types::RouterData<F, Req, types::PaymentsResponseData>>
where
Op: Debug + Sync,
F: Send + Clone,
@@ -388,8 +398,6 @@ where
// To perform router related operation for PaymentResponse
PaymentResponse: Operation<F, Req>,
{
let db = &*state.store;
let stime_connector = Instant::now();
let mut router_data = payment_data
@@ -420,28 +428,11 @@ where
Ok(router_data)
};
let response = router_data_res
.async_and_then(|response| async {
let operation = helpers::response_operation::<F, Req>();
let payment_data = operation
.to_post_update_tracker()?
.update_tracker(
db,
payment_id,
payment_data,
response,
merchant_account.storage_scheme,
)
.await?;
Ok(payment_data)
})
.await?;
let etime_connector = Instant::now();
let duration_connector = etime_connector.saturating_duration_since(stime_connector);
tracing::info!(duration = format!("Duration taken: {}", duration_connector.as_millis()));
Ok(response)
router_data_res
}
pub async fn call_multiple_connectors_service<F, Op, Req>(
@@ -668,9 +659,10 @@ pub async fn list_payments(
let pi = futures::stream::iter(payment_intents)
.filter_map(|pi| async {
let pa = db
.find_payment_attempt_by_payment_id_merchant_id(
.find_payment_attempt_by_payment_id_merchant_id_attempt_id(
&pi.payment_id,
merchant_id,
&pi.active_attempt_id,
// since OLAP doesn't have KV. Force to get the data from PSQL.
storage_enums::MerchantStorageScheme::PostgresOnly,
)

View File

@@ -56,9 +56,10 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsCancelRequest>
})?;
let mut payment_attempt = db
.find_payment_attempt_by_payment_id_merchant_id(
&payment_id,
.find_payment_attempt_by_payment_id_merchant_id_attempt_id(
payment_intent.payment_id.as_str(),
merchant_id,
payment_intent.active_attempt_id.as_str(),
storage_scheme,
)
.await
@@ -175,7 +176,7 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsCancelRequest> for
{
let cancellation_reason = payment_data.payment_attempt.cancellation_reason.clone();
payment_data.payment_attempt = db
.update_payment_attempt(
.update_payment_attempt_with_attempt_id(
payment_data.payment_attempt,
storage::PaymentAttemptUpdate::VoidUpdate {
status: enums::AttemptStatus::VoidInitiated,

View File

@@ -63,9 +63,10 @@ impl<F: Send + Clone> GetTracker<F, payments::PaymentData<F>, api::PaymentsCaptu
helpers::validate_amount_to_capture(payment_intent.amount, request.amount_to_capture)?;
payment_attempt = db
.find_payment_attempt_by_payment_id_merchant_id(
&payment_id,
.find_payment_attempt_by_payment_id_merchant_id_attempt_id(
payment_intent.payment_id.as_str(),
merchant_id,
payment_intent.active_attempt_id.as_str(),
storage_scheme,
)
.await

View File

@@ -94,9 +94,10 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Co
})?;
payment_attempt = db
.find_payment_attempt_by_payment_id_merchant_id(
&payment_id,
.find_payment_attempt_by_payment_id_merchant_id_attempt_id(
&payment_intent.payment_id,
merchant_id,
&payment_intent.active_attempt_id,
storage_scheme,
)
.await

View File

@@ -96,9 +96,10 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
})?;
payment_attempt = db
.find_payment_attempt_by_payment_id_merchant_id(
&payment_id,
.find_payment_attempt_by_payment_id_merchant_id_attempt_id(
payment_intent.payment_id.as_str(),
merchant_id,
payment_intent.active_attempt_id.as_str(),
storage_scheme,
)
.await
@@ -338,7 +339,7 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
.attach_printable("Failed to encode additional pm data")?;
payment_data.payment_attempt = db
.update_payment_attempt(
.update_payment_attempt_with_attempt_id(
payment_data.payment_attempt,
storage::PaymentAttemptUpdate::ConfirmUpdate {
amount: payment_data.amount.into(),

View File

@@ -127,6 +127,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
request,
shipping_address.clone().map(|x| x.address_id),
billing_address.clone().map(|x| x.address_id),
payment_attempt.attempt_id.to_owned(),
)?,
storage_scheme,
)
@@ -316,7 +317,7 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
let connector = payment_data.payment_attempt.connector.clone();
payment_data.payment_attempt = db
.update_payment_attempt(
.update_payment_attempt_with_attempt_id(
payment_data.payment_attempt,
storage::PaymentAttemptUpdate::UpdateTrackers {
payment_token,
@@ -479,6 +480,7 @@ impl PaymentCreate {
request: &api::PaymentsRequest,
shipping_address_id: Option<String>,
billing_address_id: Option<String>,
active_attempt_id: String,
) -> RouterResult<storage::PaymentIntentNew> {
let created_at @ modified_at @ last_synced = Some(common_utils::date_time::now());
let status =
@@ -512,6 +514,7 @@ impl PaymentCreate {
statement_descriptor_name: request.statement_descriptor_name.clone(),
statement_descriptor_suffix: request.statement_descriptor_suffix.clone(),
metadata: metadata.map(masking::Secret::new),
active_attempt_id,
..storage::PaymentIntentNew::default()
})
}

View File

@@ -106,7 +106,12 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::VerifyRequest> for Paym
payment_intent = match db
.insert_payment_intent(
Self::make_payment_intent(&payment_id, merchant_id, request),
Self::make_payment_intent(
&payment_id,
merchant_id,
request,
payment_attempt.attempt_id.to_owned(),
),
storage_scheme,
)
.await
@@ -312,6 +317,7 @@ impl PaymentMethodValidate {
payment_id: &str,
merchant_id: &str,
request: &api::VerifyRequest,
active_attempt_id: String,
) -> storage::PaymentIntentNew {
let created_at @ modified_at @ last_synced = Some(date_time::now());
let status = helpers::payment_intent_status_fsm(&request.payment_method_data, Some(true));
@@ -331,6 +337,7 @@ impl PaymentMethodValidate {
client_secret: Some(client_secret),
setup_future_usage: request.setup_future_usage.map(ForeignInto::foreign_into),
off_session: request.off_session,
active_attempt_id,
..Default::default()
}
}

View File

@@ -361,7 +361,7 @@ async fn payment_response_update_tracker<F: Clone, T>(
payment_data.payment_attempt = match payment_attempt_update {
Some(payment_attempt_update) => db
.update_payment_attempt(
.update_payment_attempt_with_attempt_id(
payment_data.payment_attempt,
payment_attempt_update,
storage_scheme,

View File

@@ -71,9 +71,10 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsSessionRequest>
)?;
let mut payment_attempt = db
.find_payment_attempt_by_payment_id_merchant_id(
&payment_id,
.find_payment_attempt_by_payment_id_merchant_id_attempt_id(
payment_intent.payment_id.as_str(),
merchant_id,
payment_intent.active_attempt_id.as_str(),
storage_scheme,
)
.await

View File

@@ -68,9 +68,10 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsStartRequest> f
)?;
payment_attempt = db
.find_payment_attempt_by_payment_id_merchant_id(
&payment_id,
.find_payment_attempt_by_payment_id_merchant_id_attempt_id(
payment_intent.payment_id.as_str(),
merchant_id,
payment_intent.active_attempt_id.as_str(),
storage_scheme,
)
.await

View File

@@ -191,27 +191,11 @@ async fn get_tracker_for_sync<
)> {
let (payment_intent, payment_attempt, currency, amount);
payment_attempt = match payment_id {
api::PaymentIdType::PaymentIntentId(ref id) => {
db.find_payment_attempt_by_payment_id_merchant_id(id, merchant_id, storage_scheme)
}
api::PaymentIdType::ConnectorTransactionId(ref id) => {
db.find_payment_attempt_by_merchant_id_connector_txn_id(merchant_id, id, storage_scheme)
}
api::PaymentIdType::PaymentAttemptId(ref id) => {
db.find_payment_attempt_by_merchant_id_attempt_id(merchant_id, id, storage_scheme)
}
}
.await
.map_err(|error| error.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound))?;
(payment_intent, payment_attempt) =
get_payment_intent_payment_attempt(db, payment_id, merchant_id, storage_scheme).await?;
let payment_id_str = payment_attempt.payment_id.clone();
payment_intent = db
.find_payment_intent_by_payment_id_merchant_id(&payment_id_str, merchant_id, storage_scheme)
.await
.map_err(|error| error.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound))?;
let mut connector_response = db
.find_connector_response_by_payment_id_merchant_id_attempt_id(
&payment_intent.payment_id,
@@ -319,3 +303,63 @@ impl<F: Send + Clone> ValidateRequest<F, api::PaymentsRetrieveRequest> for Payme
))
}
}
#[inline]
pub async fn get_payment_intent_payment_attempt(
db: &dyn StorageInterface,
payment_id: &api::PaymentIdType,
merchant_id: &str,
storage_scheme: enums::MerchantStorageScheme,
) -> RouterResult<(storage::PaymentIntent, storage::PaymentAttempt)> {
(|| async {
let (pi, pa);
match payment_id {
api_models::payments::PaymentIdType::PaymentIntentId(ref id) => {
pi = db
.find_payment_intent_by_payment_id_merchant_id(id, merchant_id, storage_scheme)
.await?;
pa = db
.find_payment_attempt_by_payment_id_merchant_id_attempt_id(
pi.payment_id.as_str(),
merchant_id,
pi.active_attempt_id.as_str(),
storage_scheme,
)
.await?;
}
api_models::payments::PaymentIdType::ConnectorTransactionId(ref id) => {
pa = db
.find_payment_attempt_by_merchant_id_connector_txn_id(
merchant_id,
id,
storage_scheme,
)
.await?;
pi = db
.find_payment_intent_by_payment_id_merchant_id(
pa.payment_id.as_str(),
merchant_id,
storage_scheme,
)
.await?;
}
api_models::payments::PaymentIdType::PaymentAttemptId(ref id) => {
pa = db
.find_payment_attempt_by_attempt_id_merchant_id(id, merchant_id, storage_scheme)
.await?;
pi = db
.find_payment_intent_by_payment_id_merchant_id(
pa.payment_id.as_str(),
merchant_id,
storage_scheme,
)
.await?;
}
}
Ok((pi, pa))
})()
.await
.map_err(|error: error_stack::Report<errors::StorageError>| {
error.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)
})
}

View File

@@ -87,10 +87,18 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
)
.await?;
payment_intent = db
.find_payment_intent_by_payment_id_merchant_id(&payment_id, merchant_id, storage_scheme)
.await
.map_err(|error| {
error.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)
})?;
payment_attempt = db
.find_payment_attempt_by_payment_id_merchant_id(
&payment_id,
.find_payment_attempt_by_payment_id_merchant_id_attempt_id(
payment_intent.payment_id.as_str(),
merchant_id,
payment_intent.active_attempt_id.as_str(),
storage_scheme,
)
.await
@@ -364,7 +372,7 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
let payment_method_type = payment_data.payment_attempt.payment_method_type.clone();
let payment_experience = payment_data.payment_attempt.payment_experience.clone();
payment_data.payment_attempt = db
.update_payment_attempt(
.update_payment_attempt_with_attempt_id(
payment_data.payment_attempt,
storage::PaymentAttemptUpdate::Update {
amount: payment_data.amount.into(),

View File

@@ -37,26 +37,6 @@ pub async fn refund_create_core(
merchant_id = &merchant_account.merchant_id;
payment_attempt = db
.find_payment_attempt_last_successful_attempt_by_payment_id_merchant_id(
&req.payment_id,
merchant_id,
merchant_account.storage_scheme,
)
.await
.change_context(errors::ApiErrorResponse::SuccessfulPaymentNotFound)?;
// Amount is not passed in request refer from payment attempt.
amount = req.amount.unwrap_or(payment_attempt.amount); // [#298]: Need to that capture amount
//[#299]: Can we change the flow based on some workflow idea
utils::when(amount <= 0, || {
Err(report!(errors::ApiErrorResponse::InvalidDataFormat {
field_name: "amount".to_string(),
expected_format: "positive integer".to_string()
})
.attach_printable("amount less than zero"))
})?;
payment_intent = db
.find_payment_intent_by_payment_id_merchant_id(
&req.payment_id,
@@ -74,6 +54,32 @@ pub async fn refund_create_core(
},
)?;
// Amount is not passed in request refer from payment attempt.
amount = req.amount.unwrap_or(
payment_intent
.amount_captured
.ok_or(errors::ApiErrorResponse::InternalServerError)
.into_report()
.attach_printable("amount captured is none in a successful payment")?,
);
//[#299]: Can we change the flow based on some workflow idea
utils::when(amount <= 0, || {
Err(report!(errors::ApiErrorResponse::InvalidDataFormat {
field_name: "amount".to_string(),
expected_format: "positive integer".to_string()
})
.attach_printable("amount less than zero"))
})?;
payment_attempt = db
.find_payment_attempt_last_successful_attempt_by_payment_id_merchant_id(
&req.payment_id,
merchant_id,
merchant_account.storage_scheme,
)
.await
.change_context(errors::ApiErrorResponse::SuccessfulPaymentNotFound)?;
let creds_identifier = req
.merchant_connector_details
.as_ref()

View File

@@ -214,9 +214,9 @@ async fn get_payment_attempt_from_object_reference_id(
.await
.change_context(errors::WebhooksFlowError::ResourceNotFound),
api::ObjectReferenceId::PaymentId(api::PaymentIdType::PaymentAttemptId(ref id)) => db
.find_payment_attempt_by_merchant_id_attempt_id(
&merchant_account.merchant_id,
.find_payment_attempt_by_attempt_id_merchant_id(
id,
&merchant_account.merchant_id,
merchant_account.storage_scheme,
)
.await

View File

@@ -12,20 +12,13 @@ pub trait PaymentAttemptInterface {
storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<types::PaymentAttempt, errors::StorageError>;
async fn update_payment_attempt(
async fn update_payment_attempt_with_attempt_id(
&self,
this: types::PaymentAttempt,
payment_attempt: types::PaymentAttemptUpdate,
storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<types::PaymentAttempt, errors::StorageError>;
async fn find_payment_attempt_by_payment_id_merchant_id(
&self,
payment_id: &str,
merchant_id: &str,
storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<types::PaymentAttempt, errors::StorageError>;
async fn find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id(
&self,
connector_transaction_id: &str,
@@ -48,12 +41,20 @@ pub trait PaymentAttemptInterface {
storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<types::PaymentAttempt, errors::StorageError>;
async fn find_payment_attempt_by_merchant_id_attempt_id(
async fn find_payment_attempt_by_payment_id_merchant_id_attempt_id(
&self,
payment_id: &str,
merchant_id: &str,
attempt_id: &str,
storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<types::PaymentAttempt, errors::StorageError>;
async fn find_payment_attempt_by_attempt_id_merchant_id(
&self,
attempt_id: &str,
merchant_id: &str,
storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<types::PaymentAttempt, errors::StorageError>;
}
#[cfg(not(feature = "kv_store"))]
@@ -83,27 +84,14 @@ mod storage {
.into_report()
}
async fn update_payment_attempt(
async fn update_payment_attempt_with_attempt_id(
&self,
this: PaymentAttempt,
payment_attempt: PaymentAttemptUpdate,
_storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<PaymentAttempt, errors::StorageError> {
let conn = connection::pg_connection_write(self).await?;
this.update(&conn, payment_attempt)
.await
.map_err(Into::into)
.into_report()
}
async fn find_payment_attempt_by_payment_id_merchant_id(
&self,
payment_id: &str,
merchant_id: &str,
_storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<PaymentAttempt, errors::StorageError> {
let conn = connection::pg_connection_read(self).await?;
PaymentAttempt::find_by_payment_id_merchant_id(&conn, payment_id, merchant_id)
let conn = connection::pg_connection_write(&self).await?;
this.update_with_attempt_id(&conn, payment_attempt)
.await
.map_err(Into::into)
.into_report()
@@ -162,7 +150,26 @@ mod storage {
.into_report()
}
async fn find_payment_attempt_by_merchant_id_attempt_id(
async fn find_payment_attempt_by_payment_id_merchant_id_attempt_id(
&self,
payment_id: &str,
merchant_id: &str,
attempt_id: &str,
_storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<PaymentAttempt, errors::StorageError> {
let conn = connection::pg_connection_read(self).await?;
PaymentAttempt::find_by_payment_id_merchant_id_attempt_id(
&conn,
payment_id,
merchant_id,
attempt_id,
)
.await
.map_err(Into::into)
.into_report()
}
async fn find_payment_attempt_by_attempt_id_merchant_id(
&self,
merchant_id: &str,
attempt_id: &str,
@@ -180,8 +187,9 @@ mod storage {
#[async_trait::async_trait]
impl PaymentAttemptInterface for MockDb {
async fn find_payment_attempt_by_merchant_id_attempt_id(
async fn find_payment_attempt_by_payment_id_merchant_id_attempt_id(
&self,
_payment_id: &str,
_merchant_id: &str,
_attempt_id: &str,
_storage_scheme: enums::MerchantStorageScheme,
@@ -190,6 +198,16 @@ impl PaymentAttemptInterface for MockDb {
Err(errors::StorageError::MockDbError)?
}
async fn find_payment_attempt_by_attempt_id_merchant_id(
&self,
_attempt_id: &str,
_merchant_id: &str,
_storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<types::PaymentAttempt, errors::StorageError> {
// [#172]: Implement function for `MockDb`
Err(errors::StorageError::MockDbError)?
}
async fn find_payment_attempt_by_merchant_id_connector_txn_id(
&self,
_merchant_id: &str,
@@ -252,7 +270,7 @@ impl PaymentAttemptInterface for MockDb {
// safety: only used for testing
#[allow(clippy::unwrap_used)]
async fn update_payment_attempt(
async fn update_payment_attempt_with_attempt_id(
&self,
this: types::PaymentAttempt,
payment_attempt: types::PaymentAttemptUpdate,
@@ -262,7 +280,7 @@ impl PaymentAttemptInterface for MockDb {
let item = payment_attempts
.iter_mut()
.find(|item| item.id == this.id)
.find(|item| item.attempt_id == this.attempt_id)
.unwrap();
*item = payment_attempt.apply_changeset(this);
@@ -270,16 +288,6 @@ impl PaymentAttemptInterface for MockDb {
Ok(item.clone())
}
async fn find_payment_attempt_by_payment_id_merchant_id(
&self,
_payment_id: &str,
_merchant_id: &str,
_storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<types::PaymentAttempt, errors::StorageError> {
// [#172]: Implement function for `MockDb`
Err(errors::StorageError::MockDbError)?
}
async fn find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id(
&self,
_connector_transaction_id: &str,
@@ -317,6 +325,7 @@ mod storage {
use common_utils::date_time;
use error_stack::{IntoReport, ResultExt};
use redis_interface::HsetnxReply;
use storage_models::reverse_lookup::ReverseLookup;
use super::PaymentAttemptInterface;
use crate::{
@@ -406,9 +415,7 @@ mod storage {
ReverseLookupNew {
lookup_id: format!(
"{}_{}",
&created_attempt.merchant_id,
// [#439]: Change this to `attempt_id`
&created_attempt.payment_id,
&created_attempt.merchant_id, &created_attempt.attempt_id,
),
pk_id: key,
sk_id: field,
@@ -440,7 +447,7 @@ mod storage {
}
}
async fn update_payment_attempt(
async fn update_payment_attempt_with_attempt_id(
&self,
this: PaymentAttempt,
payment_attempt: PaymentAttemptUpdate,
@@ -449,7 +456,7 @@ mod storage {
match storage_scheme {
enums::MerchantStorageScheme::PostgresOnly => {
let conn = connection::pg_connection_write(self).await?;
this.update(&conn, payment_attempt)
this.update_with_attempt_id(&conn, payment_attempt)
.await
.map_err(Into::into)
.into_report()
@@ -465,33 +472,39 @@ mod storage {
.change_context(errors::StorageError::KVError)?;
let field = format!("pa_{}", updated_attempt.attempt_id);
let updated_attempt = self
.redis_conn()
.map_err(Into::<errors::StorageError>::into)?
.redis_conn
.set_hash_fields(&key, (&field, &redis_value))
.await
.map(|_| updated_attempt)
.change_context(errors::StorageError::KVError)?;
let conn = connection::pg_connection_write(self).await?;
// Reverse lookup for connector_transaction_id
if let (None, Some(connector_transaction_id)) = (
match (
old_connector_transaction_id,
&updated_attempt.connector_transaction_id,
) {
let field = format!("pa_{}", updated_attempt.attempt_id);
ReverseLookupNew {
lookup_id: format!(
"{}_{}",
&updated_attempt.merchant_id, connector_transaction_id
),
pk_id: key.clone(),
sk_id: field.clone(),
source: "payment_attempt".to_string(),
(None, Some(connector_transaction_id)) => {
add_connector_txn_id_to_reverse_lookup(
self,
key.as_str(),
this.merchant_id.as_str(),
updated_attempt.attempt_id.as_str(),
connector_transaction_id.as_str(),
)
.await?;
}
.insert(&conn)
.await
.map_err(Into::<errors::StorageError>::into)
.into_report()?;
(Some(old_connector_transaction_id), Some(connector_transaction_id)) => {
if old_connector_transaction_id.ne(connector_transaction_id) {
add_connector_txn_id_to_reverse_lookup(
self,
key.as_str(),
this.merchant_id.as_str(),
updated_attempt.attempt_id.as_str(),
connector_transaction_id.as_str(),
)
.await?;
}
}
(_, _) => {}
}
let redis_entry = kv::TypedSql {
@@ -517,41 +530,6 @@ mod storage {
}
}
async fn find_payment_attempt_by_payment_id_merchant_id(
&self,
payment_id: &str,
merchant_id: &str,
storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<PaymentAttempt, errors::StorageError> {
let database_call = || async {
let conn = connection::pg_connection_read(self).await?;
PaymentAttempt::find_by_payment_id_merchant_id(&conn, payment_id, merchant_id)
.await
.map_err(Into::into)
.into_report()
};
match storage_scheme {
enums::MerchantStorageScheme::PostgresOnly => database_call().await,
enums::MerchantStorageScheme::RedisKv => {
// [#439]: get the attempt_id from payment_intent
let key = format!("{merchant_id}_{payment_id}");
let lookup = self.get_lookup_by_lookup_id(&key).await?;
db_utils::try_redis_get_else_try_database_get(
self.redis_conn()
.map_err(Into::<errors::StorageError>::into)?
.get_hash_field_and_deserialize(
&lookup.pk_id,
&lookup.sk_id,
"PaymentAttempt",
),
database_call,
)
.await
}
}
}
async fn find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id(
&self,
connector_transaction_id: &str,
@@ -594,21 +572,17 @@ mod storage {
&self,
payment_id: &str,
merchant_id: &str,
storage_scheme: enums::MerchantStorageScheme,
_storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<PaymentAttempt, errors::StorageError> {
self.find_payment_attempt_by_payment_id_merchant_id(
let conn = connection::pg_connection_read(self).await?;
PaymentAttempt::find_last_successful_attempt_by_payment_id_merchant_id(
&conn,
payment_id,
merchant_id,
storage_scheme,
)
.await
.and_then(|attempt| match attempt.status {
enums::AttemptStatus::Charged => Ok(attempt),
_ => Err(errors::StorageError::ValueNotFound(format!(
"Successful payment attempt does not exist for {payment_id}_{merchant_id}"
)))
.into_report(),
})
.map_err(Into::into)
.into_report()
}
async fn find_payment_attempt_by_merchant_id_connector_txn_id(
@@ -647,10 +621,10 @@ mod storage {
}
}
async fn find_payment_attempt_by_merchant_id_attempt_id(
async fn find_payment_attempt_by_attempt_id_merchant_id(
&self,
merchant_id: &str,
attempt_id: &str,
merchant_id: &str,
storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<PaymentAttempt, errors::StorageError> {
let database_call = || async {
@@ -677,5 +651,64 @@ mod storage {
}
}
}
async fn find_payment_attempt_by_payment_id_merchant_id_attempt_id(
&self,
payment_id: &str,
merchant_id: &str,
attempt_id: &str,
storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<PaymentAttempt, errors::StorageError> {
let database_call = || async {
let conn = connection::pg_connection_read(self).await?;
PaymentAttempt::find_by_payment_id_merchant_id_attempt_id(
&conn,
payment_id,
merchant_id,
attempt_id,
)
.await
.map_err(Into::into)
.into_report()
};
match storage_scheme {
enums::MerchantStorageScheme::PostgresOnly => database_call().await,
enums::MerchantStorageScheme::RedisKv => {
let lookup_id = format!("{merchant_id}_{attempt_id}");
let lookup = self.get_lookup_by_lookup_id(&lookup_id).await?;
let key = &lookup.pk_id;
db_utils::try_redis_get_else_try_database_get(
self.redis_conn()
.map_err(Into::<errors::StorageError>::into)?
.get_hash_field_and_deserialize(key, &lookup.sk_id, "PaymentAttempt"),
database_call,
)
.await
}
}
}
}
#[inline]
async fn add_connector_txn_id_to_reverse_lookup(
store: &Store,
key: &str,
merchant_id: &str,
updated_attempt_attempt_id: &str,
connector_transaction_id: &str,
) -> CustomResult<ReverseLookup, errors::StorageError> {
let conn = connection::pg_connection_write(store).await?;
let field = format!("pa_{}", updated_attempt_attempt_id);
ReverseLookupNew {
lookup_id: format!("{}_{}", merchant_id, connector_transaction_id),
pk_id: key.to_owned(),
sk_id: field.clone(),
source: "payment_attempt".to_string(),
}
.insert(&conn)
.await
.map_err(Into::<errors::StorageError>::into)
.into_report()
}
}

View File

@@ -92,6 +92,7 @@ mod storage {
setup_future_usage: new.setup_future_usage,
off_session: new.off_session,
client_secret: new.client_secret.clone(),
active_attempt_id: new.active_attempt_id.to_owned(),
};
match self
@@ -347,6 +348,7 @@ impl PaymentIntentInterface for MockDb {
setup_future_usage: new.setup_future_usage,
off_session: new.off_session,
client_secret: new.client_secret,
active_attempt_id: new.active_attempt_id.to_owned(),
};
payment_intents.push(payment_intent.clone());
Ok(payment_intent)

View File

@@ -1,279 +0,0 @@
use error_stack::{FutureExt, IntoReport, ResultExt};
use futures::{
future::{join_all, try_join, try_join_all},
join,
};
use router_derive::Setter;
use storage_models::enums::MerchantStorageScheme;
use crate::{
core::errors::{self, RouterResult, StorageErrorExt},
db::StorageInterface,
types::storage::{self as storage_types},
};
pub enum PaymentAttemptDbCall {
Query {
merchant_id: String,
payment_id: String,
},
Insert(storage_types::PaymentAttemptNew),
Update {
current_payment_attempt: storage_types::PaymentAttempt,
updated_payment_attempt: storage_types::PaymentAttemptUpdate,
},
}
impl PaymentAttemptDbCall {
async fn get_db_call(
self,
db: &dyn StorageInterface,
storage_scheme: MerchantStorageScheme,
) -> Result<storage_types::PaymentAttempt, error_stack::Report<errors::ApiErrorResponse>> {
match self {
PaymentAttemptDbCall::Query {
merchant_id,
payment_id,
} => db
.find_payment_attempt_by_payment_id_merchant_id(
&payment_id,
&merchant_id,
storage_scheme,
)
.await
.change_context(errors::ApiErrorResponse::PaymentNotFound),
PaymentAttemptDbCall::Insert(payment_attempt_new) => {
let payment_id = payment_attempt_new.payment_id.clone();
db.insert_payment_attempt(payment_attempt_new, storage_scheme)
.await
.change_context(errors::ApiErrorResponse::DuplicatePayment { payment_id })
}
PaymentAttemptDbCall::Update {
current_payment_attempt,
updated_payment_attempt,
} => db
.update_payment_attempt(
current_payment_attempt,
updated_payment_attempt,
storage_scheme,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to update payment attempt"),
}
}
}
pub enum PaymentIntentDbCall {
Insert(storage_types::PaymentIntentNew),
}
impl PaymentIntentDbCall {
async fn get_db_call(
self,
db: &dyn StorageInterface,
storage_scheme: MerchantStorageScheme,
) -> Result<storage_types::PaymentIntent, error_stack::Report<errors::ApiErrorResponse>> {
match self {
PaymentIntentDbCall::Insert(payment_intent_new) => {
let payment_id = payment_intent_new.payment_id.clone();
db.insert_payment_intent(payment_intent_new, storage_scheme)
.await
.change_context(errors::ApiErrorResponse::DuplicatePayment { payment_id })
}
}
}
}
pub enum ConnectorResponseDbCall {
Query {
merchant_id: String,
payment_id: String,
attempt_id: String,
},
Insert(storage_types::ConnectorResponseNew),
Update(storage_types::ConnectorResponseUpdate),
}
pub enum AddressDbCall {
Query { address_id: String },
Insert(storage_types::AddressNew),
Update(storage_types::AddressUpdate),
}
pub struct DbCall {
payment_intent: PaymentIntentDbCall,
payment_attempt: PaymentAttemptDbCall,
connector_response: ConnectorResponseDbCall,
shipping_address: Option<AddressDbCall>,
billing_address: Option<AddressDbCall>,
}
pub enum EntityRequest {
PaymentIntent {},
PaymentAttempt {
payment_id: String,
merchant_id: String,
},
Address {
address_id: String,
},
ConnectorResponse {
payment_id: String,
attempt_id: String,
merchant_id: String,
},
}
#[derive(Debug)]
pub enum Entity {
PaymentIntent(
Result<storage_types::PaymentIntent, error_stack::Report<errors::ApiErrorResponse>>,
),
PaymentAttempt(
Result<storage_types::PaymentAttempt, error_stack::Report<errors::ApiErrorResponse>>,
),
Address(Result<storage_types::Address, error_stack::Report<errors::ApiErrorResponse>>),
ConnectorResponse(
Result<storage_types::ConnectorResponse, error_stack::Report<errors::ApiErrorResponse>>,
),
None, //FIXME: for testing purposes only
}
#[derive(Setter)]
pub struct EntityResult {
pub payment_intent:
Result<storage_types::PaymentIntent, error_stack::Report<errors::ApiErrorResponse>>,
pub payment_attempt:
Result<storage_types::PaymentAttempt, error_stack::Report<errors::ApiErrorResponse>>,
pub connector_response:
Result<storage_types::ConnectorResponse, error_stack::Report<errors::ApiErrorResponse>>,
pub billing_address:
Result<Option<storage_types::Address>, error_stack::Report<errors::ApiErrorResponse>>,
pub shipping_address:
Result<Option<storage_types::Address>, error_stack::Report<errors::ApiErrorResponse>>,
}
// #[derive(Setter)]
// pub struct DbCallRequest<T, F: Fn() -> Result<T, errors::ApiErrorResponse>> {
// pub payment_attempt: F,
// pub connector_response:
// Result<storage_types::ConnectorResponse, error_stack::Report<errors::ApiErrorResponse>>,
// pub billing_address:
// Result<Option<storage_types::Address>, error_stack::Report<errors::ApiErrorResponse>>,
// pub shipping_address:
// Result<Option<storage_types::Address>, error_stack::Report<errors::ApiErrorResponse>>,
// pub mandate:
// Result<Option<storage_types::Mandate>, error_stack::Report<errors::ApiErrorResponse>>,
// }
impl EntityResult {
fn new() -> Self {
Self {
payment_intent: Err(error_stack::report!(
errors::ApiErrorResponse::PaymentNotFound
)),
payment_attempt: Err(error_stack::report!(
errors::ApiErrorResponse::PaymentNotFound
)),
connector_response: Err(error_stack::report!(
errors::ApiErrorResponse::PaymentNotFound
)),
billing_address: Ok(None),
shipping_address: Ok(None),
}
}
}
impl Default for EntityResult {
fn default() -> Self {
Self::new()
}
}
#[async_trait::async_trait]
pub trait QueryEntity {
async fn query_entity(
&self,
db: &dyn StorageInterface,
storage_scheme: MerchantStorageScheme,
) -> Entity;
}
#[async_trait::async_trait]
impl QueryEntity for EntityRequest {
async fn query_entity(
&self,
db: &dyn StorageInterface,
storage_scheme: MerchantStorageScheme,
) -> Entity {
match self {
EntityRequest::PaymentIntent {
payment_id,
merchant_id,
} => Entity::PaymentIntent(
db.find_payment_intent_by_payment_id_merchant_id(
payment_id,
merchant_id,
storage_scheme,
)
.await
.change_context(errors::ApiErrorResponse::PaymentNotFound),
),
EntityRequest::PaymentAttempt {
payment_id,
merchant_id,
} => Entity::PaymentAttempt(
db.find_payment_attempt_by_payment_id_merchant_id(
payment_id,
merchant_id,
storage_scheme,
)
.await
.change_context(errors::ApiErrorResponse::PaymentNotFound),
),
EntityRequest::Address { address_id } => Entity::Address(
db.find_address(address_id)
.await
.change_context(errors::ApiErrorResponse::AddressNotFound), //FIXME: do not change context
),
EntityRequest::ConnectorResponse {
payment_id,
attempt_id,
merchant_id,
} => Entity::ConnectorResponse(
db.find_connector_response_by_payment_id_merchant_id_attempt_id(
&payment_id,
&merchant_id,
&attempt_id,
storage_scheme,
)
.await
.change_context(errors::ApiErrorResponse::PaymentNotFound),
),
}
}
}
pub async fn make_parallel_db_call(
db: &dyn StorageInterface,
db_calls: DbCall,
storage_scheme: MerchantStorageScheme,
) -> EntityResult {
let (payment_intent_res) = join!(
db_calls.payment_attempt.get_db_call(db, storage_scheme),
db_calls.payment_intent.get_db_call(db, storage_scheme)
);
let mut entities_result = EntityResult::new();
for entity in combined_res {
match entity {
Entity::PaymentIntent(pi_res) => entities_result.set_payment_intent(pi_res),
Entity::PaymentAttempt(pa_res) => entities_result.set_payment_attempt(pa_res),
_ => &mut entities_result,
};
}
entities_result
}

View File

@@ -95,6 +95,7 @@ mod tests {
let current_time = common_utils::date_time::now();
let payment_id = Uuid::new_v4().to_string();
let attempt_id = Uuid::new_v4().to_string();
let merchant_id = Uuid::new_v4().to_string();
let connector = types::Connector::Dummy.to_string();
@@ -106,6 +107,7 @@ mod tests {
})),
created_at: current_time.into(),
modified_at: current_time.into(),
attempt_id: attempt_id.clone(),
..PaymentAttemptNew::default()
};
state
@@ -116,9 +118,10 @@ mod tests {
let response = state
.store
.find_payment_attempt_by_payment_id_merchant_id(
.find_payment_attempt_by_payment_id_merchant_id_attempt_id(
&payment_id,
&merchant_id,
&attempt_id,
enums::MerchantStorageScheme::PostgresOnly,
)
.await
@@ -150,6 +153,7 @@ mod tests {
modified_at: current_time.into(),
// Adding a mandate_id
mandate_id: Some("man_121212".to_string()),
attempt_id: uuid.clone(),
..PaymentAttemptNew::default()
};
state
@@ -160,9 +164,10 @@ mod tests {
let response = state
.store
.find_payment_attempt_by_payment_id_merchant_id(
.find_payment_attempt_by_payment_id_merchant_id_attempt_id(
&uuid,
"1",
&uuid,
enums::MerchantStorageScheme::PostgresOnly,
)
.await

View File

@@ -33,6 +33,7 @@ pub struct PaymentIntent {
pub setup_future_usage: Option<storage_enums::FutureUsage>,
pub off_session: Option<bool>,
pub client_secret: Option<String>,
pub active_attempt_id: String,
}
#[derive(
@@ -72,6 +73,7 @@ pub struct PaymentIntentNew {
pub client_secret: Option<String>,
pub setup_future_usage: Option<storage_enums::FutureUsage>,
pub off_session: Option<bool>,
pub active_attempt_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -109,6 +111,9 @@ pub enum PaymentIntentUpdate {
billing_address_id: Option<String>,
return_url: Option<String>,
},
PaymentAttemptUpdate {
active_attempt_id: String,
},
}
#[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)]
@@ -128,6 +133,7 @@ pub struct PaymentIntentUpdateInternal {
pub billing_address_id: Option<String>,
pub shipping_address_id: Option<String>,
pub modified_at: Option<PrimitiveDateTime>,
pub active_attempt_id: Option<String>,
}
impl PaymentIntentUpdate {
@@ -242,6 +248,10 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
modified_at: Some(common_utils::date_time::now()),
..Default::default()
},
PaymentIntentUpdate::PaymentAttemptUpdate { active_attempt_id } => Self {
active_attempt_id: Some(active_attempt_id),
..Default::default()
},
}
}
}

View File

@@ -21,15 +21,20 @@ impl PaymentAttemptNew {
impl PaymentAttempt {
#[instrument(skip(conn))]
pub async fn update(
pub async fn update_with_attempt_id(
self,
conn: &PgPooledConn,
payment_attempt: PaymentAttemptUpdate,
) -> StorageResult<Self> {
match generics::generic_update_with_results::<<Self as HasTable>::Table, _, _, _>(
match generics::generic_update_with_unique_predicate_get_result::<
<Self as HasTable>::Table,
_,
_,
_,
>(
conn,
dsl::payment_id
.eq(self.payment_id.to_owned())
dsl::attempt_id
.eq(self.attempt_id.to_owned())
.and(dsl::merchant_id.eq(self.merchant_id.to_owned())),
PaymentAttemptUpdateInternal::from(payment_attempt),
)
@@ -39,27 +44,10 @@ impl PaymentAttempt {
errors::DatabaseError::NoFieldsToUpdate => Ok(self),
_ => Err(error),
},
Ok(mut payment_attempts) => payment_attempts
.pop()
.ok_or(error_stack::report!(errors::DatabaseError::NotFound)),
result => result,
}
}
#[instrument(skip(conn))]
pub async fn find_by_payment_id_merchant_id(
conn: &PgPooledConn,
payment_id: &str,
merchant_id: &str,
) -> StorageResult<Self> {
generics::generic_find_one::<<Self as HasTable>::Table, _, _>(
conn,
dsl::merchant_id
.eq(merchant_id.to_owned())
.and(dsl::payment_id.eq(payment_id.to_owned())),
)
.await
}
#[instrument(skip(conn))]
pub async fn find_optional_by_payment_id_merchant_id(
conn: &PgPooledConn,
@@ -118,7 +106,7 @@ impl PaymentAttempt {
.fold(
Err(errors::DatabaseError::NotFound).into_report(),
|acc, cur| match acc {
Ok(value) if value.created_at > cur.created_at => Ok(value),
Ok(value) if value.modified_at > cur.modified_at => Ok(value),
_ => Ok(cur),
},
)
@@ -153,4 +141,22 @@ impl PaymentAttempt {
)
.await
}
#[instrument(skip(conn))]
pub async fn find_by_payment_id_merchant_id_attempt_id(
conn: &PgPooledConn,
payment_id: &str,
merchant_id: &str,
attempt_id: &str,
) -> StorageResult<Self> {
generics::generic_find_one::<<Self as HasTable>::Table, _, _>(
conn,
dsl::payment_id.eq(payment_id.to_owned()).and(
dsl::merchant_id
.eq(merchant_id.to_owned())
.and(dsl::attempt_id.eq(attempt_id.to_owned())),
),
)
.await
}
}

View File

@@ -312,6 +312,7 @@ diesel::table! {
setup_future_usage -> Nullable<FutureUsage>,
off_session -> Nullable<Bool>,
client_secret -> Nullable<Varchar>,
active_attempt_id -> Varchar,
}
}

View File

@@ -0,0 +1,5 @@
-- This file should undo anything in `up.sql`
DROP INDEX payment_attempt_payment_id_merchant_id_index;
CREATE UNIQUE INDEX payment_attempt_payment_id_merchant_id_index ON payment_attempt (payment_id, merchant_id);
DROP INDEX payment_attempt_payment_id_merchant_id_attempt_id_index;
ALTER TABLE PAYMENT_INTENT DROP COLUMN attempt_id;

View File

@@ -0,0 +1,11 @@
-- Your SQL goes here
ALTER TABLE payment_intent ADD COLUMN active_attempt_id VARCHAR(64) NOT NULL DEFAULT 'xxx';
UPDATE payment_intent SET active_attempt_id = payment_attempt.attempt_id from payment_attempt where payment_intent.active_attempt_id = payment_attempt.payment_id;
CREATE UNIQUE INDEX payment_attempt_payment_id_merchant_id_attempt_id_index ON payment_attempt (payment_id, merchant_id, attempt_id);
-- Because payment_attempt table can have rows with same payment_id and merchant_id, this index is dropped.
DROP index payment_attempt_payment_id_merchant_id_index;
CREATE INDEX payment_attempt_payment_id_merchant_id_index ON payment_attempt (payment_id, merchant_id);

View File

@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP INDEX payment_attempt_connector_transaction_id_merchant_id_index;

View File

@@ -0,0 +1,2 @@
-- Your SQL goes here
CREATE INDEX payment_attempt_connector_transaction_id_merchant_id_index ON payment_attempt (connector_transaction_id, merchant_id);