feat(stripe): add setup intent in connector integration (stripe) (#50)

Co-authored-by: Arun Raj M <jarnura47@gmail.com>
This commit is contained in:
Nishant Joshi
2022-12-07 11:59:40 +05:30
committed by GitHub
parent cbbba37909
commit c208cd2be7
15 changed files with 586 additions and 38 deletions

View File

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

View File

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

View File

@ -2,6 +2,7 @@ mod authorize_flow;
mod cancel_flow;
mod capture_flow;
mod psync_flow;
mod verfiy_flow;
use async_trait::async_trait;

View 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,
}
}

View File

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