refactor(core): restrict requires_customer_action in confirm (#3235)

This commit is contained in:
Narayan Bhat
2024-01-30 17:16:02 +05:30
committed by GitHub
parent b5bc8c4e7c
commit d2accdef41
18 changed files with 51 additions and 410 deletions

View File

@ -403,6 +403,15 @@ pub struct HeaderPayload {
pub x_hs_latency: Option<bool>,
}
impl HeaderPayload {
pub fn with_source(payment_confirm_source: api_enums::PaymentSource) -> Self {
Self {
payment_confirm_source: Some(payment_confirm_source),
..Default::default()
}
}
}
#[derive(
Default, Debug, serde::Serialize, Clone, PartialEq, ToSchema, router_derive::PolymorphicSchema,
)]

View File

@ -2069,6 +2069,7 @@ pub enum PaymentSource {
Postman,
Dashboard,
Sdk,
Webhook,
}
#[derive(

View File

@ -129,6 +129,7 @@ where
&merchant_account,
&key_store,
auth_flow,
header_payload.payment_confirm_source,
)
.await?;

View File

@ -110,6 +110,7 @@ pub trait GetTracker<F: Clone, D, R, Ctx: PaymentMethodRetrieve>: Send {
merchant_account: &domain::MerchantAccount,
mechant_key_store: &domain::MerchantKeyStore,
auth_flow: services::AuthFlow,
payment_confirm_source: Option<enums::PaymentSource>,
) -> RouterResult<GetTrackerResponse<'a, F, R, Ctx>>;
}

View File

@ -42,6 +42,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
merchant_account: &domain::MerchantAccount,
key_store: &domain::MerchantKeyStore,
_auth_flow: services::AuthFlow,
_payment_confirm_source: Option<common_enums::PaymentSource>,
) -> RouterResult<operations::GetTrackerResponse<'a, F, api::PaymentsCaptureRequest, Ctx>> {
let db = &*state.store;
let merchant_id = &merchant_account.merchant_id;

View File

@ -42,6 +42,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
merchant_account: &domain::MerchantAccount,
key_store: &domain::MerchantKeyStore,
_auth_flow: services::AuthFlow,
_payment_confirm_source: Option<common_enums::PaymentSource>,
) -> RouterResult<operations::GetTrackerResponse<'a, F, api::PaymentsCancelRequest, Ctx>> {
let db = &*state.store;
let merchant_id = &merchant_account.merchant_id;

View File

@ -41,6 +41,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
merchant_account: &domain::MerchantAccount,
key_store: &domain::MerchantKeyStore,
_auth_flow: services::AuthFlow,
_payment_confirm_source: Option<common_enums::PaymentSource>,
) -> RouterResult<operations::GetTrackerResponse<'a, F, api::PaymentsCaptureRequest, Ctx>> {
let db = &*state.store;
let merchant_id = &merchant_account.merchant_id;

View File

@ -44,6 +44,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
merchant_account: &domain::MerchantAccount,
key_store: &domain::MerchantKeyStore,
_auth_flow: services::AuthFlow,
_payment_confirm_source: Option<common_enums::PaymentSource>,
) -> RouterResult<operations::GetTrackerResponse<'a, F, api::PaymentsRequest, Ctx>> {
let db = &*state.store;
let merchant_id = &merchant_account.merchant_id;

View File

@ -56,6 +56,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
merchant_account: &domain::MerchantAccount,
key_store: &domain::MerchantKeyStore,
auth_flow: services::AuthFlow,
payment_confirm_source: Option<common_enums::PaymentSource>,
) -> RouterResult<operations::GetTrackerResponse<'a, F, api::PaymentsRequest, Ctx>> {
let merchant_id = &merchant_account.merchant_id;
let storage_scheme = merchant_account.storage_scheme;
@ -117,17 +118,32 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
helpers::validate_customer_access(&payment_intent, auth_flow, request)?;
helpers::validate_payment_status_against_not_allowed_statuses(
&payment_intent.status,
&[
storage_enums::IntentStatus::Cancelled,
storage_enums::IntentStatus::Succeeded,
storage_enums::IntentStatus::Processing,
storage_enums::IntentStatus::RequiresCapture,
storage_enums::IntentStatus::RequiresMerchantAction,
],
"confirm",
)?;
if let Some(common_enums::PaymentSource::Webhook) = payment_confirm_source {
helpers::validate_payment_status_against_not_allowed_statuses(
&payment_intent.status,
&[
storage_enums::IntentStatus::Cancelled,
storage_enums::IntentStatus::Succeeded,
storage_enums::IntentStatus::Processing,
storage_enums::IntentStatus::RequiresCapture,
storage_enums::IntentStatus::RequiresMerchantAction,
],
"confirm",
)?;
} else {
helpers::validate_payment_status_against_not_allowed_statuses(
&payment_intent.status,
&[
storage_enums::IntentStatus::Cancelled,
storage_enums::IntentStatus::Succeeded,
storage_enums::IntentStatus::Processing,
storage_enums::IntentStatus::RequiresCapture,
storage_enums::IntentStatus::RequiresMerchantAction,
storage_enums::IntentStatus::RequiresCustomerAction,
],
"confirm",
)?;
}
helpers::authenticate_client_secret(request.client_secret.as_ref(), &payment_intent)?;

View File

@ -59,6 +59,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
merchant_account: &domain::MerchantAccount,
merchant_key_store: &domain::MerchantKeyStore,
_auth_flow: services::AuthFlow,
_payment_confirm_source: Option<common_enums::PaymentSource>,
) -> RouterResult<operations::GetTrackerResponse<'a, F, api::PaymentsRequest, Ctx>> {
let db = &*state.store;
let ephemeral_key = Self::get_ephemeral_key(request, state, merchant_account).await;

View File

@ -1,398 +0,0 @@
use std::marker::PhantomData;
use api_models::enums::FrmSuggestion;
use async_trait::async_trait;
use common_utils::{date_time, errors::CustomResult, ext_traits::AsyncExt};
use error_stack::ResultExt;
use router_derive::PaymentOperation;
use router_env::{instrument, tracing};
use super::{BoxedOperation, Domain, GetTracker, UpdateTracker, ValidateRequest};
use crate::{
consts,
core::{
errors::{self, RouterResult, StorageErrorExt},
payment_methods::PaymentMethodRetrieve,
payments::{self, helpers, operations, Operation, PaymentData},
utils as core_utils,
},
db::StorageInterface,
routes::AppState,
services,
types::{
self,
api::{self, enums as api_enums, PaymentIdTypeExt},
domain,
storage::{self, enums as storage_enums},
},
utils,
};
#[derive(Debug, Clone, Copy, PaymentOperation)]
#[operation(operations = "all", flow = "verify")]
pub struct PaymentMethodValidate;
impl<F: Send + Clone, Ctx: PaymentMethodRetrieve> ValidateRequest<F, api::VerifyRequest, Ctx>
for PaymentMethodValidate
{
#[instrument(skip_all)]
fn validate_request<'a, 'b>(
&'b self,
request: &api::VerifyRequest,
merchant_account: &'a domain::MerchantAccount,
) -> RouterResult<(
BoxedOperation<'b, F, api::VerifyRequest, Ctx>,
operations::ValidateResult<'a>,
)> {
let request_merchant_id = request.merchant_id.as_deref();
helpers::validate_merchant_id(&merchant_account.merchant_id, request_merchant_id)
.change_context(errors::ApiErrorResponse::MerchantAccountNotFound)?;
let mandate_type =
helpers::validate_mandate(request, payments::is_operation_confirm(self))?;
let validation_id = core_utils::get_or_generate_id("validation_id", &None, "val")?;
Ok((
Box::new(self),
operations::ValidateResult {
merchant_id: &merchant_account.merchant_id,
payment_id: api::PaymentIdType::PaymentIntentId(validation_id),
mandate_type,
storage_scheme: merchant_account.storage_scheme,
requeue: false,
},
))
}
}
#[async_trait]
impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
GetTracker<F, PaymentData<F>, api::VerifyRequest, Ctx> for PaymentMethodValidate
{
#[instrument(skip_all)]
async fn get_trackers<'a>(
&'a self,
state: &'a AppState,
payment_id: &api::PaymentIdType,
request: &api::VerifyRequest,
_mandate_type: Option<api::MandateTransactionType>,
merchant_account: &domain::MerchantAccount,
_mechant_key_store: &domain::MerchantKeyStore,
_auth_flow: services::AuthFlow,
) -> RouterResult<(
BoxedOperation<'a, F, api::VerifyRequest, Ctx>,
PaymentData<F>,
Option<payments::CustomerDetails>,
)> {
let db = &*state.store;
let merchant_id = &merchant_account.merchant_id;
let storage_scheme = merchant_account.storage_scheme;
let (payment_intent, payment_attempt);
let payment_id = payment_id
.get_payment_intent_id()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed while getting payment_intent_id from PaymentIdType")?;
payment_attempt = match db
.insert_payment_attempt(
Self::make_payment_attempt(
&payment_id,
merchant_id,
request.payment_method,
request,
state,
merchant_account.storage_scheme,
),
storage_scheme,
)
.await
{
Ok(payment_attempt) => Ok(payment_attempt),
Err(err) => {
Err(err.change_context(errors::ApiErrorResponse::VerificationFailed { data: None }))
}
}?;
payment_intent = match db
.insert_payment_intent(
Self::make_payment_intent(
&payment_id,
merchant_id,
request,
payment_attempt.attempt_id.clone(),
merchant_account.storage_scheme,
),
storage_scheme,
)
.await
{
Ok(payment_intent) => Ok(payment_intent),
Err(err) => {
Err(err.change_context(errors::ApiErrorResponse::VerificationFailed { data: None }))
}
}?;
let creds_identifier = request
.merchant_connector_details
.as_ref()
.map(|mcd| mcd.creds_identifier.to_owned());
request
.merchant_connector_details
.to_owned()
.async_map(|mcd| async {
helpers::insert_merchant_connector_creds_to_config(
db,
merchant_account.merchant_id.as_str(),
mcd,
)
.await
})
.await
.transpose()?;
Ok((
Box::new(self),
PaymentData {
flow: PhantomData,
payment_intent,
payment_attempt,
/// currency and amount are irrelevant in this scenario
currency: storage_enums::Currency::default(),
amount: api::Amount::Zero,
email: None,
mandate_id: None,
mandate_connector: None,
setup_mandate: request.mandate_data.clone().map(Into::into),
token: request.payment_token.clone(),
payment_method_data: request.payment_method_data.clone(),
confirm: Some(true),
address: types::PaymentAddress::default(),
force_sync: None,
refunds: vec![],
disputes: vec![],
attempts: None,
sessions_token: vec![],
card_cvc: None,
creds_identifier,
pm_token: None,
connector_customer_id: None,
recurring_mandate_payment_data: None,
ephemeral_key: None,
multiple_capture_data: None,
redirect_response: None,
surcharge_details: None,
frm_message: None,
payment_link_data: None,
frm_metadata: None,
},
Some(payments::CustomerDetails {
customer_id: request.customer_id.clone(),
name: request.name.clone(),
email: request.email.clone(),
phone: request.phone.clone(),
phone_country_code: request.phone_country_code.clone(),
}),
))
}
}
#[async_trait]
impl<F: Clone, Ctx: PaymentMethodRetrieve> UpdateTracker<F, PaymentData<F>, api::VerifyRequest, Ctx>
for PaymentMethodValidate
{
#[instrument(skip_all)]
async fn update_trackers<'b>(
&'b self,
state: &'b AppState,
mut payment_data: PaymentData<F>,
_customer: Option<domain::Customer>,
storage_scheme: storage_enums::MerchantStorageScheme,
_updated_customer: Option<storage::CustomerUpdate>,
_mechant_key_store: &domain::MerchantKeyStore,
_frm_suggestion: Option<FrmSuggestion>,
_header_payload: api::HeaderPayload,
) -> RouterResult<(
BoxedOperation<'b, F, api::VerifyRequest, Ctx>,
PaymentData<F>,
)>
where
F: 'b + Send,
{
// There is no fsm involved in this operation all the change of states must happen in a single request
let status = Some(storage_enums::IntentStatus::Processing);
let customer_id = payment_data.payment_intent.customer_id.clone();
payment_data.payment_intent = state
.store
.update_payment_intent(
payment_data.payment_intent,
storage::PaymentIntentUpdate::ReturnUrlUpdate {
return_url: None,
status,
customer_id,
shipping_address_id: None,
billing_address_id: None,
updated_by: storage_scheme.to_string(),
},
storage_scheme,
)
.await
.to_not_found_response(errors::ApiErrorResponse::VerificationFailed { data: None })?;
Ok((Box::new(self), payment_data))
}
}
#[async_trait]
impl<F, Op, Ctx: PaymentMethodRetrieve> Domain<F, api::VerifyRequest, Ctx> for Op
where
F: Clone + Send,
Op: Send + Sync + Operation<F, api::VerifyRequest, Ctx>,
for<'a> &'a Op: Operation<F, api::VerifyRequest, Ctx>,
{
#[instrument(skip_all)]
async fn get_or_create_customer_details<'a>(
&'a self,
db: &dyn StorageInterface,
payment_data: &mut PaymentData<F>,
request: Option<payments::CustomerDetails>,
key_store: &domain::MerchantKeyStore,
) -> CustomResult<
(
BoxedOperation<'a, F, api::VerifyRequest, Ctx>,
Option<domain::Customer>,
),
errors::StorageError,
> {
helpers::create_customer_if_not_exist(
Box::new(self),
db,
payment_data,
request,
&key_store.merchant_id,
key_store,
)
.await
}
#[instrument(skip_all)]
async fn make_pm_data<'a>(
&'a self,
state: &'a AppState,
payment_data: &mut PaymentData<F>,
_storage_scheme: storage_enums::MerchantStorageScheme,
merchant_key_store: &domain::MerchantKeyStore,
) -> RouterResult<(
BoxedOperation<'a, F, api::VerifyRequest, Ctx>,
Option<api::PaymentMethodData>,
)> {
helpers::make_pm_data(Box::new(self), state, payment_data, merchant_key_store).await
}
async fn get_connector<'a>(
&'a self,
_merchant_account: &domain::MerchantAccount,
state: &AppState,
_request: &api::VerifyRequest,
_payment_intent: &storage::PaymentIntent,
_mechant_key_store: &domain::MerchantKeyStore,
) -> CustomResult<api::ConnectorChoice, errors::ApiErrorResponse> {
helpers::get_connector_default(state, None).await
}
}
impl PaymentMethodValidate {
#[instrument(skip_all)]
fn make_payment_attempt(
payment_id: &str,
merchant_id: &str,
payment_method: Option<api_enums::PaymentMethod>,
_request: &api::VerifyRequest,
state: &AppState,
storage_scheme: storage_enums::MerchantStorageScheme,
) -> storage::PaymentAttemptNew {
let created_at @ modified_at @ last_synced = Some(date_time::now());
let status = storage_enums::AttemptStatus::Pending;
let attempt_id = if core_utils::is_merchant_enabled_for_payment_id_as_connector_request_id(
&state.conf,
merchant_id,
) {
payment_id.to_string()
} else {
utils::get_payment_attempt_id(payment_id, 1)
};
storage::PaymentAttemptNew {
payment_id: payment_id.to_string(),
merchant_id: merchant_id.to_string(),
attempt_id,
status,
// Amount & Currency will be zero in this case
amount: 0,
currency: Default::default(),
connector: None,
payment_method,
confirm: true,
created_at,
modified_at,
last_synced,
updated_by: storage_scheme.to_string(),
..Default::default()
}
}
fn make_payment_intent(
payment_id: &str,
merchant_id: &str,
request: &api::VerifyRequest,
active_attempt_id: String,
storage_scheme: storage_enums::MerchantStorageScheme,
) -> storage::PaymentIntentNew {
let created_at @ modified_at @ last_synced = Some(date_time::now());
let status = helpers::payment_intent_status_fsm(&request.payment_method_data, Some(true));
let client_secret =
utils::generate_id(consts::ID_LENGTH, format!("{payment_id}_secret").as_str());
storage::PaymentIntentNew {
payment_id: payment_id.to_string(),
merchant_id: merchant_id.to_string(),
status,
amount: 0,
currency: Default::default(),
connector_id: None,
created_at,
modified_at,
last_synced,
client_secret: Some(client_secret),
setup_future_usage: request.setup_future_usage,
off_session: request.off_session,
active_attempt: data_models::RemoteStorageObject::ForeignID(active_attempt_id),
attempt_count: 1,
amount_captured: Default::default(),
customer_id: Default::default(),
description: Default::default(),
return_url: Default::default(),
metadata: Default::default(),
shipping_address_id: Default::default(),
billing_address_id: Default::default(),
statement_descriptor_name: Default::default(),
statement_descriptor_suffix: Default::default(),
business_country: Default::default(),
business_label: Default::default(),
order_details: Default::default(),
allowed_payment_method_types: Default::default(),
connector_metadata: Default::default(),
feature_metadata: Default::default(),
profile_id: Default::default(),
merchant_decision: Default::default(),
payment_confirm_source: Default::default(),
surcharge_applicable: Default::default(),
payment_link_id: Default::default(),
updated_by: storage_scheme.to_string(),
}
}
}

View File

@ -41,6 +41,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
merchant_account: &domain::MerchantAccount,
key_store: &domain::MerchantKeyStore,
_auth_flow: services::AuthFlow,
_payment_confirm_source: Option<common_enums::PaymentSource>,
) -> RouterResult<operations::GetTrackerResponse<'a, F, PaymentsCancelRequest, Ctx>> {
let db = &*state.store;
let merchant_id = &merchant_account.merchant_id;

View File

@ -43,6 +43,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
merchant_account: &domain::MerchantAccount,
key_store: &domain::MerchantKeyStore,
_auth_flow: services::AuthFlow,
_payment_confirm_source: Option<common_enums::PaymentSource>,
) -> RouterResult<operations::GetTrackerResponse<'a, F, api::PaymentsSessionRequest, Ctx>> {
let payment_id = payment_id
.get_payment_intent_id()

View File

@ -42,6 +42,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
merchant_account: &domain::MerchantAccount,
mechant_key_store: &domain::MerchantKeyStore,
_auth_flow: services::AuthFlow,
_payment_confirm_source: Option<common_enums::PaymentSource>,
) -> RouterResult<operations::GetTrackerResponse<'a, F, api::PaymentsStartRequest, Ctx>> {
let (mut payment_intent, payment_attempt, currency, amount);
let db = &*state.store;

View File

@ -198,6 +198,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
merchant_account: &domain::MerchantAccount,
key_store: &domain::MerchantKeyStore,
_auth_flow: services::AuthFlow,
_payment_confirm_source: Option<common_enums::PaymentSource>,
) -> RouterResult<operations::GetTrackerResponse<'a, F, api::PaymentsRetrieveRequest, Ctx>>
{
get_tracker_for_sync(

View File

@ -44,6 +44,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
merchant_account: &domain::MerchantAccount,
key_store: &domain::MerchantKeyStore,
auth_flow: services::AuthFlow,
_payment_confirm_source: Option<common_enums::PaymentSource>,
) -> RouterResult<operations::GetTrackerResponse<'a, F, api::PaymentsRequest, Ctx>> {
let (mut payment_intent, mut payment_attempt, currency): (_, _, storage_enums::Currency);

View File

@ -46,6 +46,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
merchant_account: &domain::MerchantAccount,
_key_store: &domain::MerchantKeyStore,
_auth_flow: services::AuthFlow,
_payment_confirm_source: Option<common_enums::PaymentSource>,
) -> RouterResult<
operations::GetTrackerResponse<'a, F, PaymentsIncrementalAuthorizationRequest, Ctx>,
> {

View File

@ -603,7 +603,7 @@ async fn bank_transfer_webhook_flow<W: types::OutgoingWebhookType, Ctx: PaymentM
services::api::AuthFlow::Merchant,
payments::CallConnectorAction::Trigger,
None,
HeaderPayload::default(),
HeaderPayload::with_source(common_enums::PaymentSource::Webhook),
))
.await
} else {