Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(router): add expand attempts support in payments retrieve response #1678

Merged
merged 9 commits into from
Jul 14, 2023
59 changes: 59 additions & 0 deletions crates/api_models/src/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,56 @@ pub struct PaymentsRequest {
pub feature_metadata: Option<FeatureMetadata>,
}

#[derive(
Default, Debug, serde::Serialize, Clone, PartialEq, ToSchema, router_derive::PolymorphicSchema,
)]
pub struct PaymentAttemptResponse {
/// Unique identifier for the attempt
pub attempt_id: String,
sai-harsha-vardhan marked this conversation as resolved.
Show resolved Hide resolved
/// The status of the attempt
#[schema(value_type = AttemptStatus, example = "charged")]
pub status: enums::AttemptStatus,
/// The payment attempt amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.,
pub amount: i64,
/// The currency of the amount of the payment attempt
#[schema(value_type = Option<Currency>, example = "usd")]
pub currency: Option<enums::Currency>,
/// The connector used for the payment
pub connector: Option<String>,
/// If there was an error while calling the connector the error message is received here
pub error_message: Option<String>,
/// The payment method that is to be used
#[schema(value_type = Option<PaymentMethod>, example = "bank_transfer")]
pub payment_method: Option<enums::PaymentMethod>,
/// A unique identifier for a payment provided by the connector
pub connector_transaction_id: Option<String>,
/// This is the instruction for capture/ debit the money from the users' card. On the other hand authorization refers to blocking the amount on the users' payment method.
#[schema(value_type = Option<CaptureMethod>, example = "scheduled")]
pub capture_method: Option<enums::CaptureMethod>,
/// The transaction authentication can be set to undergo payer authentication. By default, the authentication will be marked as NO_THREE_DS
#[schema(value_type = Option<AuthenticationType>, example = "no_three_ds", default = "three_ds")]
pub authentication_type: Option<enums::AuthenticationType>,
/// If the payment was cancelled the reason provided here
pub cancellation_reason: Option<String>,
/// A unique identifier to link the payment to a mandate, can be use instead of payment_method_data
pub mandate_id: Option<String>,
/// If there was an error while calling the connectors the code is received here
pub error_code: Option<String>,
/// Provide a reference to a stored payment method
pub payment_token: Option<String>,
/// additional data related to some connectors
pub connector_metadata: Option<serde_json::Value>,
/// Payment Experience for the current payment
#[schema(value_type = Option<PaymentExperience>, example = "redirect_to_url")]
pub payment_experience: Option<enums::PaymentExperience>,
/// Payment Method Type
#[schema(value_type = Option<PaymentMethodType>, example = "google_pay")]
pub payment_method_type: Option<enums::PaymentMethodType>,
/// reference to the payment at connector side
#[schema(value_type = Option<String>, example = "993672945374576J")]
pub reference_id: Option<String>,
}

impl PaymentsRequest {
pub fn get_feature_metadata_as_value(
&self,
Expand Down Expand Up @@ -1362,6 +1412,11 @@ pub struct PaymentsResponse {
#[schema(value_type = Option<Vec<DisputeResponsePaymentsRetrieve>>)]
pub disputes: Option<Vec<disputes::DisputeResponsePaymentsRetrieve>>,

/// List of attempts that happened on this intent
#[schema(value_type = Option<Vec<PaymentAttemptResponse>>)]
#[serde(skip_serializing_if = "Option::is_none")]
pub attempts: Option<Vec<PaymentAttemptResponse>>,

/// A unique identifier to link the payment to a mandate, can be use instead of payment_method_data
#[schema(max_length = 255, example = "mandate_iwer89rnjef349dni3")]
pub mandate_id: Option<String>,
Expand Down Expand Up @@ -1771,6 +1826,8 @@ pub struct PaymentsRetrieveRequest {
pub merchant_connector_details: Option<admin::MerchantConnectorDetailsWrap>,
/// This is a token which expires after 15 minutes, used from the client to authenticate and create sessions from the SDK
pub client_secret: Option<String>,
/// If enabled provides list of attempts linked to payment intent
pub expand_attempts: Option<bool>,
}

#[derive(Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)]
Expand Down Expand Up @@ -2164,6 +2221,8 @@ pub struct PaymentRetrieveBody {
pub force_sync: Option<bool>,
/// This is a token which expires after 15 minutes, used from the client to authenticate and create sessions from the SDK
pub client_secret: Option<String>,
/// If enabled provides list of attempts linked to payment intent
pub expand_attempts: Option<bool>,
}

#[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)]
Expand Down
1 change: 1 addition & 0 deletions crates/common_enums/src/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub mod diesel_exports {
serde::Serialize,
strum::Display,
strum::EnumString,
ToSchema,
)]
#[router_derive::diesel_enum(storage_type = "pg_enum")]
#[serde(rename_all = "snake_case")]
Expand Down
24 changes: 24 additions & 0 deletions crates/diesel_models/src/query/payment_attempt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,30 @@ impl PaymentAttempt {
)
.await
}

#[instrument(skip(conn))]
pub async fn find_by_merchant_id_payment_id(
conn: &PgPooledConn,
merchant_id: &str,
payment_id: &str,
) -> StorageResult<Vec<Self>> {
generics::generic_filter::<
<Self as HasTable>::Table,
_,
<<Self as HasTable>::Table as Table>::PrimaryKey,
_,
>(
conn,
dsl::merchant_id
.eq(merchant_id.to_owned())
.and(dsl::payment_id.eq(payment_id.to_owned())),
None,
None,
None,
)
.await
}

pub async fn get_filters_for_payments(
conn: &PgPooledConn,
pi: &[PaymentIntent],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ pub async fn payment_intents_retrieve(
param: None,
merchant_connector_details: None,
client_secret: query_payload.client_secret.clone(),
expand_attempts: None,
};

let (auth_type, auth_flow) =
Expand Down
1 change: 1 addition & 0 deletions crates/router/src/compatibility/stripe/setup_intents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ pub async fn setup_intents_retrieve(
param: None,
merchant_connector_details: None,
client_secret: query_payload.client_secret.clone(),
expand_attempts: None,
};

let (auth_type, auth_flow) =
Expand Down
2 changes: 2 additions & 0 deletions crates/router/src/core/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,7 @@ impl PaymentRedirectFlow for PaymentRedirectSync {
}
}),
client_secret: None,
expand_attempts: None,
};
payments_core::<api::PSync, api::PaymentsResponse, _, _, _>(
state,
Expand Down Expand Up @@ -1081,6 +1082,7 @@ where
pub payment_method_data: Option<api::PaymentMethodData>,
pub refunds: Vec<storage::Refund>,
pub disputes: Vec<storage::Dispute>,
pub attempts: Option<Vec<storage::PaymentAttempt>>,
pub sessions_token: Vec<api::SessionToken>,
pub card_cvc: Option<Secret<String>>,
pub email: Option<pii::Email>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsCancelRequest>
force_sync: None,
refunds: vec![],
disputes: vec![],
attempts: None,
connector_response,
sessions_token: vec![],
card_cvc: None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ impl<F: Send + Clone> GetTracker<F, payments::PaymentData<F>, api::PaymentsCaptu
payment_method_data: None,
refunds: vec![],
disputes: vec![],
attempts: None,
connector_response,
sessions_token: vec![],
card_cvc: None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Co
force_sync: None,
refunds: vec![],
disputes: vec![],
attempts: None,
sessions_token: vec![],
card_cvc: request.card_cvc.clone(),
creds_identifier: None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
force_sync: None,
refunds: vec![],
disputes: vec![],
attempts: None,
sessions_token: vec![],
card_cvc: request.card_cvc.clone(),
creds_identifier,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
payment_method_data: request.payment_method_data.clone(),
refunds: vec![],
disputes: vec![],
attempts: None,
force_sync: None,
connector_response,
sessions_token: vec![],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::VerifyRequest> for Paym
force_sync: None,
refunds: vec![],
disputes: vec![],
attempts: None,
sessions_token: vec![],
card_cvc: None,
creds_identifier,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsSessionRequest>
force_sync: None,
refunds: vec![],
disputes: vec![],
attempts: None,
sessions_token: vec![],
connector_response,
card_cvc: None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsStartRequest> f
force_sync: None,
refunds: vec![],
disputes: vec![],
attempts: None,
sessions_token: vec![],
card_cvc: None,
creds_identifier: None,
Expand Down
14 changes: 14 additions & 0 deletions crates/router/src/core/payments/operations/payment_status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,19 @@ async fn get_tracker_for_sync<
)
.await?;

let attempts = match request.expand_attempts {
Some(true) => {
Some(db
.find_attempts_by_merchant_id_payment_id(merchant_id, &payment_id_str, storage_scheme)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable_lazy(|| {
format!("Error while retrieving attempt list for, merchant_id: {merchant_id}, payment_id: {payment_id_str}")
})?)
},
_ => None,
};

let refunds = db
.find_refund_by_payment_id_merchant_id(&payment_id_str, merchant_id, storage_scheme)
.await
Expand Down Expand Up @@ -300,6 +313,7 @@ async fn get_tracker_for_sync<
payment_attempt,
refunds,
disputes,
attempts,
sessions_token: vec![],
card_cvc: None,
creds_identifier,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
force_sync: None,
refunds: vec![],
disputes: vec![],
attempts: None,
connector_response,
sessions_token: vec![],
card_cvc: request.card_cvc.clone(),
Expand Down
10 changes: 10 additions & 0 deletions crates/router/src/core/payments/transformers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ where
payment_data.payment_intent,
payment_data.refunds,
payment_data.disputes,
payment_data.attempts,
payment_data.payment_method_data,
customer,
auth_flow,
Expand Down Expand Up @@ -258,6 +259,7 @@ pub fn payments_to_payments_response<R, Op>(
payment_intent: storage::PaymentIntent,
refunds: Vec<storage::Refund>,
disputes: Vec<storage::Dispute>,
option_attempts: Option<Vec<storage::PaymentAttempt>>,
payment_method_data: Option<api::PaymentMethodData>,
customer: Option<domain::Customer>,
auth_flow: services::AuthFlow,
Expand Down Expand Up @@ -296,6 +298,12 @@ where
.collect(),
)
};
let attempts_response = option_attempts.map(|attempts| {
attempts
.into_iter()
.map(ForeignInto::foreign_into)
.collect()
});
let merchant_id = payment_attempt.merchant_id.to_owned();
let payment_method_type = payment_attempt
.payment_method_type
Expand Down Expand Up @@ -407,6 +415,7 @@ where
.set_description(payment_intent.description)
.set_refunds(refunds_response) // refunds.iter().map(refund_to_refund_response),
.set_disputes(disputes_response)
.set_attempts(attempts_response)
.set_payment_method(
payment_attempt.payment_method,
auth_flow == services::AuthFlow::Merchant,
Expand Down Expand Up @@ -466,6 +475,7 @@ where
description: payment_intent.description,
refunds: refunds_response,
disputes: disputes_response,
attempts: attempts_response,
payment_method: payment_attempt.payment_method,
capture_method: payment_attempt.capture_method,
error_message: payment_attempt.error_message,
Expand Down
1 change: 1 addition & 0 deletions crates/router/src/core/webhooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ pub async fn payments_incoming_webhook_flow<W: api::OutgoingWebhookType>(
param: None,
merchant_connector_details: None,
client_secret: None,
expand_attempts: None,
},
services::AuthFlow::Merchant,
consume_or_trigger_flow,
Expand Down
59 changes: 59 additions & 0 deletions crates/router/src/db/payment_attempt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ pub trait PaymentAttemptInterface {
storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<types::PaymentAttempt, errors::StorageError>;

async fn find_attempts_by_merchant_id_payment_id(
&self,
merchant_id: &str,
payment_id: &str,
storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<Vec<types::PaymentAttempt>, errors::StorageError>;

async fn get_filters_for_payments(
&self,
pi: &[diesel_models::payment_intent::PaymentIntent],
Expand Down Expand Up @@ -216,6 +223,19 @@ mod storage {
.into_report()
}

async fn find_attempts_by_merchant_id_payment_id(
&self,
merchant_id: &str,
payment_id: &str,
_storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<Vec<PaymentAttempt>, errors::StorageError> {
let conn = connection::pg_connection_read(self).await?;
PaymentAttempt::find_by_merchant_id_payment_id(&conn, merchant_id, payment_id)
.await
.map_err(Into::into)
.into_report()
}

async fn find_payment_attempt_by_attempt_id_merchant_id(
&self,
merchant_id: &str,
Expand Down Expand Up @@ -285,6 +305,16 @@ impl PaymentAttemptInterface for MockDb {
Err(errors::StorageError::MockDbError)?
}

async fn find_attempts_by_merchant_id_payment_id(
&self,
_merchant_id: &str,
_payment_id: &str,
_storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<Vec<types::PaymentAttempt>, errors::StorageError> {
// [#172]: Implement function for `MockDb`
Err(errors::StorageError::MockDbError)?
}

#[allow(clippy::panic)]
async fn insert_payment_attempt(
&self,
Expand Down Expand Up @@ -832,6 +862,35 @@ mod storage {
}
}

async fn find_attempts_by_merchant_id_payment_id(
&self,
merchant_id: &str,
payment_id: &str,
storage_scheme: enums::MerchantStorageScheme,
) -> CustomResult<Vec<PaymentAttempt>, errors::StorageError> {
match storage_scheme {
enums::MerchantStorageScheme::PostgresOnly => {
let conn = connection::pg_connection_read(self).await?;
PaymentAttempt::find_by_merchant_id_payment_id(&conn, merchant_id, payment_id)
.await
.map_err(Into::into)
.into_report()
}
enums::MerchantStorageScheme::RedisKv => {
let key = format!("{merchant_id}_{payment_id}");
let lookup = self.get_lookup_by_lookup_id(&key).await?;

let pattern = db_utils::generate_hscan_pattern_for_attempt(&lookup.sk_id);

self.redis_conn()
.map_err(Into::<errors::StorageError>::into)?
.hscan_and_deserialize(&key, &pattern, None)
.await
.change_context(errors::StorageError::KVError)
}
}
}

async fn get_filters_for_payments(
&self,
pi: &[diesel_models::payment_intent::PaymentIntent],
Expand Down
Loading