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

View File

@ -193,7 +193,7 @@ async fn drainer(
} }
kv::Updateable::PaymentAttemptUpdate(a) => { kv::Updateable::PaymentAttemptUpdate(a) => {
macro_util::handle_resp!( 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, update_op,
payment_attempt payment_attempt
) )

View File

@ -880,7 +880,7 @@ impl<F, T>
.transaction_id .transaction_id
.map_or(response.order_id, Some) // For paypal there will be no transaction_id, only order_id will be present .map_or(response.order_id, Some) // For paypal there will be no transaction_id, only order_id will be present
.map(types::ResponseId::ConnectorTransactionId) .map(types::ResponseId::ConnectorTransactionId)
.ok_or_else(|| errors::ConnectorError::MissingConnectorTransactionID)?, .ok_or(errors::ConnectorError::MissingConnectorTransactionID)?,
redirection_data, redirection_data,
mandate_reference: None, mandate_reference: None,
// we don't need to save session token for capture, void flow so ignoring if it is not present // 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 let payment_attempt = payment_intent
.as_ref() .as_ref()
.async_map(|pi| async { .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.payment_id,
&pi.merchant_id, &pi.merchant_id,
&pi.active_attempt_id,
merchant_account.storage_scheme, merchant_account.storage_scheme,
) )
.await .await

View File

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

View File

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

View File

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

View File

@ -127,6 +127,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
request, request,
shipping_address.clone().map(|x| x.address_id), shipping_address.clone().map(|x| x.address_id),
billing_address.clone().map(|x| x.address_id), billing_address.clone().map(|x| x.address_id),
payment_attempt.attempt_id.to_owned(),
)?, )?,
storage_scheme, 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(); let connector = payment_data.payment_attempt.connector.clone();
payment_data.payment_attempt = db payment_data.payment_attempt = db
.update_payment_attempt( .update_payment_attempt_with_attempt_id(
payment_data.payment_attempt, payment_data.payment_attempt,
storage::PaymentAttemptUpdate::UpdateTrackers { storage::PaymentAttemptUpdate::UpdateTrackers {
payment_token, payment_token,
@ -479,6 +480,7 @@ impl PaymentCreate {
request: &api::PaymentsRequest, request: &api::PaymentsRequest,
shipping_address_id: Option<String>, shipping_address_id: Option<String>,
billing_address_id: Option<String>, billing_address_id: Option<String>,
active_attempt_id: String,
) -> RouterResult<storage::PaymentIntentNew> { ) -> RouterResult<storage::PaymentIntentNew> {
let created_at @ modified_at @ last_synced = Some(common_utils::date_time::now()); let created_at @ modified_at @ last_synced = Some(common_utils::date_time::now());
let status = let status =
@ -512,6 +514,7 @@ impl PaymentCreate {
statement_descriptor_name: request.statement_descriptor_name.clone(), statement_descriptor_name: request.statement_descriptor_name.clone(),
statement_descriptor_suffix: request.statement_descriptor_suffix.clone(), statement_descriptor_suffix: request.statement_descriptor_suffix.clone(),
metadata: metadata.map(masking::Secret::new), metadata: metadata.map(masking::Secret::new),
active_attempt_id,
..storage::PaymentIntentNew::default() ..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 payment_intent = match db
.insert_payment_intent( .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, storage_scheme,
) )
.await .await
@ -312,6 +317,7 @@ impl PaymentMethodValidate {
payment_id: &str, payment_id: &str,
merchant_id: &str, merchant_id: &str,
request: &api::VerifyRequest, request: &api::VerifyRequest,
active_attempt_id: String,
) -> storage::PaymentIntentNew { ) -> storage::PaymentIntentNew {
let created_at @ modified_at @ last_synced = Some(date_time::now()); let created_at @ modified_at @ last_synced = Some(date_time::now());
let status = helpers::payment_intent_status_fsm(&request.payment_method_data, Some(true)); let status = helpers::payment_intent_status_fsm(&request.payment_method_data, Some(true));
@ -331,6 +337,7 @@ impl PaymentMethodValidate {
client_secret: Some(client_secret), client_secret: Some(client_secret),
setup_future_usage: request.setup_future_usage.map(ForeignInto::foreign_into), setup_future_usage: request.setup_future_usage.map(ForeignInto::foreign_into),
off_session: request.off_session, off_session: request.off_session,
active_attempt_id,
..Default::default() ..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 { payment_data.payment_attempt = match payment_attempt_update {
Some(payment_attempt_update) => db Some(payment_attempt_update) => db
.update_payment_attempt( .update_payment_attempt_with_attempt_id(
payment_data.payment_attempt, payment_data.payment_attempt,
payment_attempt_update, payment_attempt_update,
storage_scheme, storage_scheme,

View File

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

View File

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

View File

@ -191,27 +191,11 @@ async fn get_tracker_for_sync<
)> { )> {
let (payment_intent, payment_attempt, currency, amount); let (payment_intent, payment_attempt, currency, amount);
payment_attempt = match payment_id { (payment_intent, payment_attempt) =
api::PaymentIdType::PaymentIntentId(ref id) => { get_payment_intent_payment_attempt(db, payment_id, merchant_id, storage_scheme).await?;
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))?;
let payment_id_str = payment_attempt.payment_id.clone(); 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 let mut connector_response = db
.find_connector_response_by_payment_id_merchant_id_attempt_id( .find_connector_response_by_payment_id_merchant_id_attempt_id(
&payment_intent.payment_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?; .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 payment_attempt = db
.find_payment_attempt_by_payment_id_merchant_id( .find_payment_attempt_by_payment_id_merchant_id_attempt_id(
&payment_id, payment_intent.payment_id.as_str(),
merchant_id, merchant_id,
payment_intent.active_attempt_id.as_str(),
storage_scheme, storage_scheme,
) )
.await .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_method_type = payment_data.payment_attempt.payment_method_type.clone();
let payment_experience = payment_data.payment_attempt.payment_experience.clone(); let payment_experience = payment_data.payment_attempt.payment_experience.clone();
payment_data.payment_attempt = db payment_data.payment_attempt = db
.update_payment_attempt( .update_payment_attempt_with_attempt_id(
payment_data.payment_attempt, payment_data.payment_attempt,
storage::PaymentAttemptUpdate::Update { storage::PaymentAttemptUpdate::Update {
amount: payment_data.amount.into(), amount: payment_data.amount.into(),

View File

@ -37,26 +37,6 @@ pub async fn refund_create_core(
merchant_id = &merchant_account.merchant_id; 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 payment_intent = db
.find_payment_intent_by_payment_id_merchant_id( .find_payment_intent_by_payment_id_merchant_id(
&req.payment_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 let creds_identifier = req
.merchant_connector_details .merchant_connector_details
.as_ref() .as_ref()

View File

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

View File

@ -12,20 +12,13 @@ pub trait PaymentAttemptInterface {
storage_scheme: enums::MerchantStorageScheme, storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<types::PaymentAttempt, errors::StorageError>; ) -> CustomResult<types::PaymentAttempt, errors::StorageError>;
async fn update_payment_attempt( async fn update_payment_attempt_with_attempt_id(
&self, &self,
this: types::PaymentAttempt, this: types::PaymentAttempt,
payment_attempt: types::PaymentAttemptUpdate, payment_attempt: types::PaymentAttemptUpdate,
storage_scheme: enums::MerchantStorageScheme, storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<types::PaymentAttempt, errors::StorageError>; ) -> 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( async fn find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id(
&self, &self,
connector_transaction_id: &str, connector_transaction_id: &str,
@ -48,12 +41,20 @@ pub trait PaymentAttemptInterface {
storage_scheme: enums::MerchantStorageScheme, storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<types::PaymentAttempt, errors::StorageError>; ) -> 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, &self,
payment_id: &str,
merchant_id: &str, merchant_id: &str,
attempt_id: &str, attempt_id: &str,
storage_scheme: enums::MerchantStorageScheme, storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<types::PaymentAttempt, errors::StorageError>; ) -> 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"))] #[cfg(not(feature = "kv_store"))]
@ -83,27 +84,14 @@ mod storage {
.into_report() .into_report()
} }
async fn update_payment_attempt( async fn update_payment_attempt_with_attempt_id(
&self, &self,
this: PaymentAttempt, this: PaymentAttempt,
payment_attempt: PaymentAttemptUpdate, payment_attempt: PaymentAttemptUpdate,
_storage_scheme: enums::MerchantStorageScheme, _storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<PaymentAttempt, errors::StorageError> { ) -> CustomResult<PaymentAttempt, errors::StorageError> {
let conn = connection::pg_connection_write(self).await?; 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()
}
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)
.await .await
.map_err(Into::into) .map_err(Into::into)
.into_report() .into_report()
@ -162,7 +150,26 @@ mod storage {
.into_report() .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, &self,
merchant_id: &str, merchant_id: &str,
attempt_id: &str, attempt_id: &str,
@ -180,8 +187,9 @@ mod storage {
#[async_trait::async_trait] #[async_trait::async_trait]
impl PaymentAttemptInterface for MockDb { 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, &self,
_payment_id: &str,
_merchant_id: &str, _merchant_id: &str,
_attempt_id: &str, _attempt_id: &str,
_storage_scheme: enums::MerchantStorageScheme, _storage_scheme: enums::MerchantStorageScheme,
@ -190,6 +198,16 @@ impl PaymentAttemptInterface for MockDb {
Err(errors::StorageError::MockDbError)? 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( async fn find_payment_attempt_by_merchant_id_connector_txn_id(
&self, &self,
_merchant_id: &str, _merchant_id: &str,
@ -252,7 +270,7 @@ impl PaymentAttemptInterface for MockDb {
// safety: only used for testing // safety: only used for testing
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
async fn update_payment_attempt( async fn update_payment_attempt_with_attempt_id(
&self, &self,
this: types::PaymentAttempt, this: types::PaymentAttempt,
payment_attempt: types::PaymentAttemptUpdate, payment_attempt: types::PaymentAttemptUpdate,
@ -262,7 +280,7 @@ impl PaymentAttemptInterface for MockDb {
let item = payment_attempts let item = payment_attempts
.iter_mut() .iter_mut()
.find(|item| item.id == this.id) .find(|item| item.attempt_id == this.attempt_id)
.unwrap(); .unwrap();
*item = payment_attempt.apply_changeset(this); *item = payment_attempt.apply_changeset(this);
@ -270,16 +288,6 @@ impl PaymentAttemptInterface for MockDb {
Ok(item.clone()) 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( async fn find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id(
&self, &self,
_connector_transaction_id: &str, _connector_transaction_id: &str,
@ -317,6 +325,7 @@ mod storage {
use common_utils::date_time; use common_utils::date_time;
use error_stack::{IntoReport, ResultExt}; use error_stack::{IntoReport, ResultExt};
use redis_interface::HsetnxReply; use redis_interface::HsetnxReply;
use storage_models::reverse_lookup::ReverseLookup;
use super::PaymentAttemptInterface; use super::PaymentAttemptInterface;
use crate::{ use crate::{
@ -406,9 +415,7 @@ mod storage {
ReverseLookupNew { ReverseLookupNew {
lookup_id: format!( lookup_id: format!(
"{}_{}", "{}_{}",
&created_attempt.merchant_id, &created_attempt.merchant_id, &created_attempt.attempt_id,
// [#439]: Change this to `attempt_id`
&created_attempt.payment_id,
), ),
pk_id: key, pk_id: key,
sk_id: field, sk_id: field,
@ -440,7 +447,7 @@ mod storage {
} }
} }
async fn update_payment_attempt( async fn update_payment_attempt_with_attempt_id(
&self, &self,
this: PaymentAttempt, this: PaymentAttempt,
payment_attempt: PaymentAttemptUpdate, payment_attempt: PaymentAttemptUpdate,
@ -449,7 +456,7 @@ mod storage {
match storage_scheme { match storage_scheme {
enums::MerchantStorageScheme::PostgresOnly => { enums::MerchantStorageScheme::PostgresOnly => {
let conn = connection::pg_connection_write(self).await?; let conn = connection::pg_connection_write(self).await?;
this.update(&conn, payment_attempt) this.update_with_attempt_id(&conn, payment_attempt)
.await .await
.map_err(Into::into) .map_err(Into::into)
.into_report() .into_report()
@ -465,33 +472,39 @@ mod storage {
.change_context(errors::StorageError::KVError)?; .change_context(errors::StorageError::KVError)?;
let field = format!("pa_{}", updated_attempt.attempt_id); let field = format!("pa_{}", updated_attempt.attempt_id);
let updated_attempt = self let updated_attempt = self
.redis_conn() .redis_conn
.map_err(Into::<errors::StorageError>::into)?
.set_hash_fields(&key, (&field, &redis_value)) .set_hash_fields(&key, (&field, &redis_value))
.await .await
.map(|_| updated_attempt) .map(|_| updated_attempt)
.change_context(errors::StorageError::KVError)?; .change_context(errors::StorageError::KVError)?;
let conn = connection::pg_connection_write(self).await?; match (
// Reverse lookup for connector_transaction_id
if let (None, Some(connector_transaction_id)) = (
old_connector_transaction_id, old_connector_transaction_id,
&updated_attempt.connector_transaction_id, &updated_attempt.connector_transaction_id,
) { ) {
let field = format!("pa_{}", updated_attempt.attempt_id); (None, Some(connector_transaction_id)) => {
ReverseLookupNew { add_connector_txn_id_to_reverse_lookup(
lookup_id: format!( self,
"{}_{}", key.as_str(),
&updated_attempt.merchant_id, connector_transaction_id this.merchant_id.as_str(),
), updated_attempt.attempt_id.as_str(),
pk_id: key.clone(), connector_transaction_id.as_str(),
sk_id: field.clone(), )
source: "payment_attempt".to_string(), .await?;
} }
.insert(&conn) (Some(old_connector_transaction_id), Some(connector_transaction_id)) => {
.await if old_connector_transaction_id.ne(connector_transaction_id) {
.map_err(Into::<errors::StorageError>::into) add_connector_txn_id_to_reverse_lookup(
.into_report()?; 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 { 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( async fn find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id(
&self, &self,
connector_transaction_id: &str, connector_transaction_id: &str,
@ -594,21 +572,17 @@ mod storage {
&self, &self,
payment_id: &str, payment_id: &str,
merchant_id: &str, merchant_id: &str,
storage_scheme: enums::MerchantStorageScheme, _storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<PaymentAttempt, errors::StorageError> { ) -> 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, payment_id,
merchant_id, merchant_id,
storage_scheme,
) )
.await .await
.and_then(|attempt| match attempt.status { .map_err(Into::into)
enums::AttemptStatus::Charged => Ok(attempt), .into_report()
_ => Err(errors::StorageError::ValueNotFound(format!(
"Successful payment attempt does not exist for {payment_id}_{merchant_id}"
)))
.into_report(),
})
} }
async fn find_payment_attempt_by_merchant_id_connector_txn_id( 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, &self,
merchant_id: &str,
attempt_id: &str, attempt_id: &str,
merchant_id: &str,
storage_scheme: enums::MerchantStorageScheme, storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<PaymentAttempt, errors::StorageError> { ) -> CustomResult<PaymentAttempt, errors::StorageError> {
let database_call = || async { 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, setup_future_usage: new.setup_future_usage,
off_session: new.off_session, off_session: new.off_session,
client_secret: new.client_secret.clone(), client_secret: new.client_secret.clone(),
active_attempt_id: new.active_attempt_id.to_owned(),
}; };
match self match self
@ -347,6 +348,7 @@ impl PaymentIntentInterface for MockDb {
setup_future_usage: new.setup_future_usage, setup_future_usage: new.setup_future_usage,
off_session: new.off_session, off_session: new.off_session,
client_secret: new.client_secret, client_secret: new.client_secret,
active_attempt_id: new.active_attempt_id.to_owned(),
}; };
payment_intents.push(payment_intent.clone()); payment_intents.push(payment_intent.clone());
Ok(payment_intent) 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 current_time = common_utils::date_time::now();
let payment_id = Uuid::new_v4().to_string(); 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 merchant_id = Uuid::new_v4().to_string();
let connector = types::Connector::Dummy.to_string(); let connector = types::Connector::Dummy.to_string();
@ -106,6 +107,7 @@ mod tests {
})), })),
created_at: current_time.into(), created_at: current_time.into(),
modified_at: current_time.into(), modified_at: current_time.into(),
attempt_id: attempt_id.clone(),
..PaymentAttemptNew::default() ..PaymentAttemptNew::default()
}; };
state state
@ -116,9 +118,10 @@ mod tests {
let response = state let response = state
.store .store
.find_payment_attempt_by_payment_id_merchant_id( .find_payment_attempt_by_payment_id_merchant_id_attempt_id(
&payment_id, &payment_id,
&merchant_id, &merchant_id,
&attempt_id,
enums::MerchantStorageScheme::PostgresOnly, enums::MerchantStorageScheme::PostgresOnly,
) )
.await .await
@ -150,6 +153,7 @@ mod tests {
modified_at: current_time.into(), modified_at: current_time.into(),
// Adding a mandate_id // Adding a mandate_id
mandate_id: Some("man_121212".to_string()), mandate_id: Some("man_121212".to_string()),
attempt_id: uuid.clone(),
..PaymentAttemptNew::default() ..PaymentAttemptNew::default()
}; };
state state
@ -160,9 +164,10 @@ mod tests {
let response = state let response = state
.store .store
.find_payment_attempt_by_payment_id_merchant_id( .find_payment_attempt_by_payment_id_merchant_id_attempt_id(
&uuid, &uuid,
"1", "1",
&uuid,
enums::MerchantStorageScheme::PostgresOnly, enums::MerchantStorageScheme::PostgresOnly,
) )
.await .await

View File

@ -33,6 +33,7 @@ pub struct PaymentIntent {
pub setup_future_usage: Option<storage_enums::FutureUsage>, pub setup_future_usage: Option<storage_enums::FutureUsage>,
pub off_session: Option<bool>, pub off_session: Option<bool>,
pub client_secret: Option<String>, pub client_secret: Option<String>,
pub active_attempt_id: String,
} }
#[derive( #[derive(
@ -72,6 +73,7 @@ pub struct PaymentIntentNew {
pub client_secret: Option<String>, pub client_secret: Option<String>,
pub setup_future_usage: Option<storage_enums::FutureUsage>, pub setup_future_usage: Option<storage_enums::FutureUsage>,
pub off_session: Option<bool>, pub off_session: Option<bool>,
pub active_attempt_id: String,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -109,6 +111,9 @@ pub enum PaymentIntentUpdate {
billing_address_id: Option<String>, billing_address_id: Option<String>,
return_url: Option<String>, return_url: Option<String>,
}, },
PaymentAttemptUpdate {
active_attempt_id: String,
},
} }
#[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)]
@ -128,6 +133,7 @@ pub struct PaymentIntentUpdateInternal {
pub billing_address_id: Option<String>, pub billing_address_id: Option<String>,
pub shipping_address_id: Option<String>, pub shipping_address_id: Option<String>,
pub modified_at: Option<PrimitiveDateTime>, pub modified_at: Option<PrimitiveDateTime>,
pub active_attempt_id: Option<String>,
} }
impl PaymentIntentUpdate { impl PaymentIntentUpdate {
@ -242,6 +248,10 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
modified_at: Some(common_utils::date_time::now()), modified_at: Some(common_utils::date_time::now()),
..Default::default() ..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 { impl PaymentAttempt {
#[instrument(skip(conn))] #[instrument(skip(conn))]
pub async fn update( pub async fn update_with_attempt_id(
self, self,
conn: &PgPooledConn, conn: &PgPooledConn,
payment_attempt: PaymentAttemptUpdate, payment_attempt: PaymentAttemptUpdate,
) -> StorageResult<Self> { ) -> 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, conn,
dsl::payment_id dsl::attempt_id
.eq(self.payment_id.to_owned()) .eq(self.attempt_id.to_owned())
.and(dsl::merchant_id.eq(self.merchant_id.to_owned())), .and(dsl::merchant_id.eq(self.merchant_id.to_owned())),
PaymentAttemptUpdateInternal::from(payment_attempt), PaymentAttemptUpdateInternal::from(payment_attempt),
) )
@ -39,27 +44,10 @@ impl PaymentAttempt {
errors::DatabaseError::NoFieldsToUpdate => Ok(self), errors::DatabaseError::NoFieldsToUpdate => Ok(self),
_ => Err(error), _ => Err(error),
}, },
Ok(mut payment_attempts) => payment_attempts result => result,
.pop()
.ok_or(error_stack::report!(errors::DatabaseError::NotFound)),
} }
} }
#[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))] #[instrument(skip(conn))]
pub async fn find_optional_by_payment_id_merchant_id( pub async fn find_optional_by_payment_id_merchant_id(
conn: &PgPooledConn, conn: &PgPooledConn,
@ -118,7 +106,7 @@ impl PaymentAttempt {
.fold( .fold(
Err(errors::DatabaseError::NotFound).into_report(), Err(errors::DatabaseError::NotFound).into_report(),
|acc, cur| match acc { |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), _ => Ok(cur),
}, },
) )
@ -153,4 +141,22 @@ impl PaymentAttempt {
) )
.await .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>, setup_future_usage -> Nullable<FutureUsage>,
off_session -> Nullable<Bool>, off_session -> Nullable<Bool>,
client_secret -> Nullable<Varchar>, 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);