mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-03 05:17:02 +08:00
feat(stripe): add setup intent in connector integration (stripe) (#50)
Co-authored-by: Arun Raj M <jarnura47@gmail.com>
This commit is contained in:
@ -79,6 +79,8 @@ pub enum ApiErrorResponse {
|
||||
CardExpired { data: Option<serde_json::Value> },
|
||||
#[error(error_type = ErrorType::ProcessingError, code = "CE_06", message = "Refund failed while processing with connector. Retry refund.")]
|
||||
RefundFailed { data: Option<serde_json::Value> },
|
||||
#[error(error_type = ErrorType::ProcessingError, code = "CE_01", message = "Verification failed while processing with connector. Retry operation.")]
|
||||
VerificationFailed { data: Option<serde_json::Value> },
|
||||
|
||||
#[error(error_type = ErrorType::ServerNotAvailable, code = "RE_00", message = "Something went wrong.")]
|
||||
InternalServerError,
|
||||
@ -154,6 +156,7 @@ impl actix_web::ResponseError for ApiErrorResponse {
|
||||
| ApiErrorResponse::InvalidCardData { .. }
|
||||
| ApiErrorResponse::CardExpired { .. }
|
||||
| ApiErrorResponse::RefundFailed { .. }
|
||||
| ApiErrorResponse::VerificationFailed { .. }
|
||||
| ApiErrorResponse::PaymentUnexpectedState { .. } => StatusCode::BAD_REQUEST, // 400
|
||||
|
||||
ApiErrorResponse::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, // 500
|
||||
|
||||
@ -1,74 +1,76 @@
|
||||
use super::DatabaseError;
|
||||
use crate::logger;
|
||||
use crate::{core::errors, logger};
|
||||
|
||||
pub(crate) trait StorageErrorExt {
|
||||
fn to_not_found_response(
|
||||
self,
|
||||
not_found_response: super::ApiErrorResponse,
|
||||
) -> error_stack::Report<super::ApiErrorResponse>;
|
||||
not_found_response: errors::ApiErrorResponse,
|
||||
) -> error_stack::Report<errors::ApiErrorResponse>;
|
||||
|
||||
fn to_duplicate_response(
|
||||
self,
|
||||
duplicate_response: super::ApiErrorResponse,
|
||||
) -> error_stack::Report<super::ApiErrorResponse>;
|
||||
duplicate_response: errors::ApiErrorResponse,
|
||||
) -> error_stack::Report<errors::ApiErrorResponse>;
|
||||
}
|
||||
|
||||
impl StorageErrorExt for error_stack::Report<super::StorageError> {
|
||||
impl StorageErrorExt for error_stack::Report<errors::StorageError> {
|
||||
fn to_not_found_response(
|
||||
self,
|
||||
not_found_response: super::ApiErrorResponse,
|
||||
) -> error_stack::Report<super::ApiErrorResponse> {
|
||||
not_found_response: errors::ApiErrorResponse,
|
||||
) -> error_stack::Report<errors::ApiErrorResponse> {
|
||||
match self.current_context() {
|
||||
super::StorageError::DatabaseError(DatabaseError::NotFound) => {
|
||||
errors::StorageError::DatabaseError(DatabaseError::NotFound) => {
|
||||
self.change_context(not_found_response)
|
||||
}
|
||||
_ => self.change_context(super::ApiErrorResponse::InternalServerError),
|
||||
_ => self.change_context(errors::ApiErrorResponse::InternalServerError),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_duplicate_response(
|
||||
self,
|
||||
duplicate_response: super::ApiErrorResponse,
|
||||
) -> error_stack::Report<super::ApiErrorResponse> {
|
||||
duplicate_response: errors::ApiErrorResponse,
|
||||
) -> error_stack::Report<errors::ApiErrorResponse> {
|
||||
match self.current_context() {
|
||||
super::StorageError::DatabaseError(DatabaseError::UniqueViolation) => {
|
||||
errors::StorageError::DatabaseError(DatabaseError::UniqueViolation) => {
|
||||
self.change_context(duplicate_response)
|
||||
}
|
||||
_ => self.change_context(super::ApiErrorResponse::InternalServerError),
|
||||
_ => self.change_context(errors::ApiErrorResponse::InternalServerError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait ApiClientErrorExt {
|
||||
fn to_unsuccessful_processing_step_response(self)
|
||||
-> error_stack::Report<super::ConnectorError>;
|
||||
}
|
||||
|
||||
impl ApiClientErrorExt for error_stack::Report<super::ApiClientError> {
|
||||
fn to_unsuccessful_processing_step_response(
|
||||
self,
|
||||
) -> error_stack::Report<super::ConnectorError> {
|
||||
) -> error_stack::Report<errors::ConnectorError>;
|
||||
}
|
||||
|
||||
impl ApiClientErrorExt for error_stack::Report<errors::ApiClientError> {
|
||||
fn to_unsuccessful_processing_step_response(
|
||||
self,
|
||||
) -> error_stack::Report<errors::ConnectorError> {
|
||||
let data = match self.current_context() {
|
||||
super::ApiClientError::BadRequestReceived(bytes)
|
||||
| super::ApiClientError::UnauthorizedReceived(bytes)
|
||||
| super::ApiClientError::NotFoundReceived(bytes)
|
||||
| super::ApiClientError::UnprocessableEntityReceived(bytes) => Some(bytes.clone()),
|
||||
errors::ApiClientError::BadRequestReceived(bytes)
|
||||
| errors::ApiClientError::UnauthorizedReceived(bytes)
|
||||
| errors::ApiClientError::NotFoundReceived(bytes)
|
||||
| errors::ApiClientError::UnprocessableEntityReceived(bytes) => Some(bytes.clone()),
|
||||
_ => None,
|
||||
};
|
||||
self.change_context(super::ConnectorError::ProcessingStepFailed(data))
|
||||
self.change_context(errors::ConnectorError::ProcessingStepFailed(data))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait ConnectorErrorExt {
|
||||
fn to_refund_failed_response(self) -> error_stack::Report<super::ApiErrorResponse>;
|
||||
fn to_payment_failed_response(self) -> error_stack::Report<super::ApiErrorResponse>;
|
||||
fn to_refund_failed_response(self) -> error_stack::Report<errors::ApiErrorResponse>;
|
||||
fn to_payment_failed_response(self) -> error_stack::Report<errors::ApiErrorResponse>;
|
||||
fn to_verify_failed_response(self) -> error_stack::Report<errors::ApiErrorResponse>;
|
||||
}
|
||||
|
||||
// FIXME: The implementation can be improved by handling BOM maybe?
|
||||
impl ConnectorErrorExt for error_stack::Report<super::ConnectorError> {
|
||||
fn to_refund_failed_response(self) -> error_stack::Report<super::ApiErrorResponse> {
|
||||
impl ConnectorErrorExt for error_stack::Report<errors::ConnectorError> {
|
||||
fn to_refund_failed_response(self) -> error_stack::Report<errors::ApiErrorResponse> {
|
||||
let data = match self.current_context() {
|
||||
super::ConnectorError::ProcessingStepFailed(Some(bytes)) => {
|
||||
errors::ConnectorError::ProcessingStepFailed(Some(bytes)) => {
|
||||
let response_str = std::str::from_utf8(bytes);
|
||||
match response_str {
|
||||
Ok(s) => serde_json::from_str(s)
|
||||
@ -84,12 +86,12 @@ impl ConnectorErrorExt for error_stack::Report<super::ConnectorError> {
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
self.change_context(super::ApiErrorResponse::RefundFailed { data })
|
||||
self.change_context(errors::ApiErrorResponse::RefundFailed { data })
|
||||
}
|
||||
|
||||
fn to_payment_failed_response(self) -> error_stack::Report<super::ApiErrorResponse> {
|
||||
fn to_payment_failed_response(self) -> error_stack::Report<errors::ApiErrorResponse> {
|
||||
let data = match self.current_context() {
|
||||
super::ConnectorError::ProcessingStepFailed(Some(bytes)) => {
|
||||
errors::ConnectorError::ProcessingStepFailed(Some(bytes)) => {
|
||||
let response_str = std::str::from_utf8(bytes);
|
||||
match response_str {
|
||||
Ok(s) => serde_json::from_str(s)
|
||||
@ -105,6 +107,25 @@ impl ConnectorErrorExt for error_stack::Report<super::ConnectorError> {
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
self.change_context(super::ApiErrorResponse::PaymentAuthorizationFailed { data })
|
||||
self.change_context(errors::ApiErrorResponse::PaymentAuthorizationFailed { data })
|
||||
}
|
||||
|
||||
fn to_verify_failed_response(self) -> error_stack::Report<errors::ApiErrorResponse> {
|
||||
let data = match self.current_context() {
|
||||
errors::ConnectorError::ProcessingStepFailed(Some(bytes)) => {
|
||||
let response_str = std::str::from_utf8(bytes);
|
||||
match response_str {
|
||||
Ok(s) => serde_json::from_str(s)
|
||||
.map_err(|err| logger::error!(%err, "Failed to convert response to JSON"))
|
||||
.ok(),
|
||||
Err(err) => {
|
||||
logger::error!(%err, "Failed to convert response to UTF8 string");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
self.change_context(errors::ApiErrorResponse::PaymentAuthorizationFailed { data })
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ mod authorize_flow;
|
||||
mod cancel_flow;
|
||||
mod capture_flow;
|
||||
mod psync_flow;
|
||||
mod verfiy_flow;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
|
||||
175
crates/router/src/core/payments/flows/verfiy_flow.rs
Normal file
175
crates/router/src/core/payments/flows/verfiy_flow.rs
Normal file
@ -0,0 +1,175 @@
|
||||
use async_trait::async_trait;
|
||||
use error_stack::ResultExt;
|
||||
use masking::Secret;
|
||||
|
||||
use super::{ConstructFlowSpecificData, Feature};
|
||||
use crate::{
|
||||
consts,
|
||||
core::{
|
||||
errors::{self, ConnectorErrorExt, RouterResult, StorageErrorExt},
|
||||
payments::{self, helpers, transformers, PaymentData},
|
||||
},
|
||||
routes::AppState,
|
||||
services,
|
||||
types::{
|
||||
self, api,
|
||||
storage::{self, enums},
|
||||
},
|
||||
utils,
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl ConstructFlowSpecificData<api::Verify, types::VerifyRequestData, types::PaymentsResponseData>
|
||||
for PaymentData<api::Verify>
|
||||
{
|
||||
async fn construct_r_d<'a>(
|
||||
&self,
|
||||
state: &AppState,
|
||||
connector_id: &str,
|
||||
merchant_account: &storage::MerchantAccount,
|
||||
) -> RouterResult<types::VerifyRouterData> {
|
||||
let (_, router_data) = transformers::construct_payment_router_data::<
|
||||
api::Verify,
|
||||
types::VerifyRequestData,
|
||||
>(state, self.clone(), connector_id, merchant_account)
|
||||
.await?;
|
||||
|
||||
Ok(router_data)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Feature<api::Verify, types::VerifyRequestData> for types::VerifyRouterData {
|
||||
async fn decide_flows<'a>(
|
||||
self,
|
||||
state: &AppState,
|
||||
connector: api::ConnectorData,
|
||||
customer: &Option<api::CustomerResponse>,
|
||||
payment_data: PaymentData<api::Verify>,
|
||||
call_connector_action: payments::CallConnectorAction,
|
||||
) -> (RouterResult<Self>, PaymentData<api::Verify>)
|
||||
where
|
||||
dyn api::Connector: services::ConnectorIntegration<
|
||||
api::Verify,
|
||||
types::VerifyRequestData,
|
||||
types::PaymentsResponseData,
|
||||
>,
|
||||
{
|
||||
let resp = self
|
||||
.decide_flow(
|
||||
state,
|
||||
connector,
|
||||
customer,
|
||||
Some(true),
|
||||
call_connector_action,
|
||||
)
|
||||
.await;
|
||||
|
||||
(resp, payment_data)
|
||||
}
|
||||
}
|
||||
|
||||
impl types::VerifyRouterData {
|
||||
pub async fn decide_flow<'a, 'b>(
|
||||
&'b self,
|
||||
state: &'a AppState,
|
||||
connector: api::ConnectorData,
|
||||
maybe_customer: &Option<api::CustomerResponse>,
|
||||
confirm: Option<bool>,
|
||||
call_connector_action: payments::CallConnectorAction,
|
||||
) -> RouterResult<Self>
|
||||
where
|
||||
dyn api::Connector + Sync: services::ConnectorIntegration<
|
||||
api::Verify,
|
||||
types::VerifyRequestData,
|
||||
types::PaymentsResponseData,
|
||||
>,
|
||||
{
|
||||
match confirm {
|
||||
Some(true) => {
|
||||
let connector_integration: services::BoxedConnectorIntegration<
|
||||
api::Verify,
|
||||
types::VerifyRequestData,
|
||||
types::PaymentsResponseData,
|
||||
> = connector.connector.get_connector_integration();
|
||||
let mut resp = services::execute_connector_processing_step(
|
||||
state,
|
||||
connector_integration,
|
||||
self,
|
||||
call_connector_action,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| err.to_verify_failed_response())?;
|
||||
|
||||
match &self.request.mandate_id {
|
||||
Some(mandate_id) => {
|
||||
let mandate = state
|
||||
.store
|
||||
.find_mandate_by_merchant_id_mandate_id(&resp.merchant_id, mandate_id)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::MandateNotFound)?;
|
||||
resp.payment_method_id = Some(mandate.payment_method_id);
|
||||
}
|
||||
None => {
|
||||
if self.request.setup_future_usage.is_some() {
|
||||
let payment_method_id = helpers::call_payment_method(
|
||||
state,
|
||||
&self.merchant_id,
|
||||
Some(&self.request.payment_method_data),
|
||||
Some(self.payment_method),
|
||||
maybe_customer,
|
||||
)
|
||||
.await?
|
||||
.payment_method_id;
|
||||
|
||||
resp.payment_method_id = Some(payment_method_id.clone());
|
||||
if let Some(new_mandate_data) = generate_mandate(
|
||||
self.merchant_id.clone(),
|
||||
self.request.setup_mandate_details.clone(),
|
||||
maybe_customer,
|
||||
payment_method_id,
|
||||
) {
|
||||
resp.request.mandate_id = Some(new_mandate_data.mandate_id.clone());
|
||||
state.store.insert_mandate(new_mandate_data).await.map_err(
|
||||
|err| {
|
||||
err.to_duplicate_response(
|
||||
errors::ApiErrorResponse::DuplicateMandate,
|
||||
)
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(resp)
|
||||
}
|
||||
_ => Ok(self.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_mandate(
|
||||
merchant_id: String,
|
||||
setup_mandate_details: Option<api::MandateData>,
|
||||
customer: &Option<api::CustomerResponse>,
|
||||
payment_method_id: String,
|
||||
) -> Option<storage::MandateNew> {
|
||||
match (setup_mandate_details, customer) {
|
||||
(Some(data), Some(cus)) => {
|
||||
let mandate_id = utils::generate_id(consts::ID_LENGTH, "man");
|
||||
Some(storage::MandateNew {
|
||||
mandate_id,
|
||||
customer_id: cus.customer_id.clone(),
|
||||
merchant_id,
|
||||
payment_method_id,
|
||||
mandate_status: enums::MandateStatus::Active,
|
||||
mandate_type: enums::MandateType::MultiUse,
|
||||
customer_ip_address: data.customer_acceptance.get_ip_address().map(Secret::new),
|
||||
customer_user_agent: data.customer_acceptance.get_user_agent(),
|
||||
customer_accepted_at: Some(data.customer_acceptance.get_accepted_at()),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
(_, _) => None,
|
||||
}
|
||||
}
|
||||
@ -328,3 +328,31 @@ impl<F: Clone> TryFrom<PaymentData<F>> for types::PaymentsCancelData {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: Clone> TryFrom<PaymentData<F>> for types::VerifyRequestData {
|
||||
type Error = error_stack::Report<errors::ApiErrorResponse>;
|
||||
|
||||
fn try_from(payment_data: PaymentData<F>) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
confirm: true,
|
||||
payment_method_data: {
|
||||
let payment_method_type = payment_data
|
||||
.payment_attempt
|
||||
.payment_method
|
||||
.get_required_value("payment_method_type")?;
|
||||
|
||||
match payment_method_type {
|
||||
enums::PaymentMethodType::Paypal => api::PaymentMethod::Paypal,
|
||||
_ => payment_data
|
||||
.payment_method_data
|
||||
.get_required_value("payment_method_data")?,
|
||||
}
|
||||
},
|
||||
statement_descriptor_suffix: payment_data.payment_intent.statement_descriptor_suffix,
|
||||
setup_future_usage: payment_data.payment_intent.setup_future_usage,
|
||||
mandate_id: payment_data.mandate_id.clone(),
|
||||
off_session: payment_data.mandate_id.as_ref().map(|_| true),
|
||||
setup_mandate_details: payment_data.setup_mandate,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user