feat(payment_method_validate): add new operation to payments_operation_core for payment method validation (#53)

Co-authored-by: Arun Raj M <jarnura47@gmail.com>
This commit is contained in:
Nishant Joshi
2022-12-07 17:23:06 +05:30
committed by GitHub
parent b72e0df512
commit ff561bddb6
7 changed files with 380 additions and 15 deletions

View File

@ -53,6 +53,7 @@ impl api::PaymentVoid for Braintree {}
impl api::PaymentCapture for Braintree {} impl api::PaymentCapture for Braintree {}
impl api::PreVerify for Braintree {} impl api::PreVerify for Braintree {}
#[allow(dead_code)]
impl impl
services::ConnectorIntegration< services::ConnectorIntegration<
api::Verify, api::Verify,
@ -60,6 +61,7 @@ impl
types::PaymentsResponseData, types::PaymentsResponseData,
> for Braintree > for Braintree
{ {
// Not Implemented (R)
} }
#[allow(dead_code)] #[allow(dead_code)]

View File

@ -20,7 +20,7 @@ use crate::{
routes::AppState, routes::AppState,
services, services,
types::{ types::{
api::{self, PgRedirectResponse}, api,
storage::{self, enums}, storage::{self, enums},
}, },
utils::{ utils::{
@ -226,7 +226,10 @@ pub fn validate_request_amount_and_amount_to_capture(
) )
} }
pub fn validate_mandate(req: &api::PaymentsRequest) -> RouterResult<Option<api::MandateTxnType>> { pub fn validate_mandate(
req: impl Into<api::MandateValidationFields>,
) -> RouterResult<Option<api::MandateTxnType>> {
let req: api::MandateValidationFields = req.into();
match req.is_mandate() { match req.is_mandate() {
Some(api::MandateTxnType::NewMandateTxn) => { Some(api::MandateTxnType::NewMandateTxn) => {
validate_new_mandate_request(req)?; validate_new_mandate_request(req)?;
@ -240,7 +243,7 @@ pub fn validate_mandate(req: &api::PaymentsRequest) -> RouterResult<Option<api::
} }
} }
fn validate_new_mandate_request(req: &api::PaymentsRequest) -> RouterResult<()> { fn validate_new_mandate_request(req: api::MandateValidationFields) -> RouterResult<()> {
let confirm = req.confirm.get_required_value("confirm")?; let confirm = req.confirm.get_required_value("confirm")?;
if !confirm { if !confirm {
@ -302,8 +305,7 @@ pub fn create_redirect_url(server: &Server, payment_attempt: &storage::PaymentAt
payment_attempt.connector payment_attempt.connector
) )
} }
fn validate_recurring_mandate(req: api::MandateValidationFields) -> RouterResult<()> {
fn validate_recurring_mandate(req: &api::PaymentsRequest) -> RouterResult<()> {
req.mandate_id.check_value_present("mandate_id")?; req.mandate_id.check_value_present("mandate_id")?;
req.customer_id.check_value_present("customer_id")?; req.customer_id.check_value_present("customer_id")?;
@ -844,7 +846,7 @@ pub fn get_handle_response_url(
pub fn make_merchant_url_with_response( pub fn make_merchant_url_with_response(
merchant_account: &storage::MerchantAccount, merchant_account: &storage::MerchantAccount,
redirection_response: PgRedirectResponse, redirection_response: api::PgRedirectResponse,
request_return_url: Option<&String>, request_return_url: Option<&String>,
) -> RouterResult<String> { ) -> RouterResult<String> {
// take return url if provided in the request else use merchant return url // take return url if provided in the request else use merchant return url
@ -889,8 +891,8 @@ pub fn make_pg_redirect_response(
payment_id: String, payment_id: String,
response: &api::PaymentsResponse, response: &api::PaymentsResponse,
connector: String, connector: String,
) -> PgRedirectResponse { ) -> api::PgRedirectResponse {
PgRedirectResponse { api::PgRedirectResponse {
payment_id, payment_id,
status: response.status, status: response.status,
gateway_id: connector, gateway_id: connector,

View File

@ -2,6 +2,7 @@ mod payment_cancel;
mod payment_capture; mod payment_capture;
mod payment_confirm; mod payment_confirm;
mod payment_create; mod payment_create;
mod payment_method_validate;
mod payment_response; mod payment_response;
mod payment_session; mod payment_session;
mod payment_start; mod payment_start;

View File

@ -364,7 +364,7 @@ impl PaymentCreate {
} }
#[instrument(skip_all)] #[instrument(skip_all)]
fn make_connector_response( pub fn make_connector_response(
payment_attempt: &storage::PaymentAttempt, payment_attempt: &storage::PaymentAttempt,
) -> storage::ConnectorResponseNew { ) -> storage::ConnectorResponseNew {
storage::ConnectorResponseNew { storage::ConnectorResponseNew {

View File

@ -0,0 +1,307 @@
use std::marker::PhantomData;
use async_trait::async_trait;
use common_utils::{date_time, errors::CustomResult};
use error_stack::ResultExt;
use router_derive::PaymentOperation;
use router_env::{instrument, tracing};
use uuid::Uuid;
use super::{BoxedOperation, Domain, GetTracker, PaymentCreate, UpdateTracker, ValidateRequest};
use crate::{
consts,
core::{
errors::{self, RouterResult, StorageErrorExt},
payments::{self, helpers, Operation, PaymentData},
utils as core_utils,
},
db::StorageInterface,
routes::AppState,
types::{
self, api,
storage::{self, enums},
},
utils,
};
#[derive(Debug, Clone, Copy, PaymentOperation)]
#[operation(ops = "all", flow = "verify")]
pub struct PaymentMethodValidate;
impl<F: Send + Clone> ValidateRequest<F, api::VerifyRequest> for PaymentMethodValidate {
#[instrument(skip_all)]
fn validate_request<'a, 'b>(
&'b self,
request: &api::VerifyRequest,
merchant_account: &'a types::storage::MerchantAccount,
) -> RouterResult<(
BoxedOperation<'b, F, api::VerifyRequest>,
&'a str,
api::PaymentIdType,
Option<api::MandateTxnType>,
)> {
let request_merchant_id = Some(&request.merchant_id[..]);
helpers::validate_merchant_id(&merchant_account.merchant_id, request_merchant_id)
.change_context(errors::ApiErrorResponse::MerchantAccountNotFound)?;
let mandate_type = helpers::validate_mandate(request)?;
let validation_id = core_utils::get_or_generate_id("validation_id", &None, "val")?;
Ok((
Box::new(self),
&merchant_account.merchant_id,
api::PaymentIdType::PaymentIntentId(validation_id),
mandate_type,
))
}
}
#[async_trait]
impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::VerifyRequest> for PaymentMethodValidate {
#[instrument(skip_all)]
async fn get_trackers<'a>(
&'a self,
state: &'a AppState,
payment_id: &api::PaymentIdType,
merchant_id: &str,
connector: types::Connector,
request: &api::VerifyRequest,
_mandate_type: Option<api::MandateTxnType>,
) -> RouterResult<(
BoxedOperation<'a, F, api::VerifyRequest>,
PaymentData<F>,
Option<payments::CustomerDetails>,
)> {
let db = &state.store;
let (payment_intent, payment_attempt, connector_response);
let payment_id = payment_id
.get_payment_intent_id()
.change_context(errors::ApiErrorResponse::InternalServerError)?;
payment_attempt = match db
.insert_payment_attempt(Self::make_payment_attempt(
&payment_id,
merchant_id,
connector,
request.payment_method,
request,
))
.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,
connector,
request,
))
.await
{
Ok(payment_intent) => Ok(payment_intent),
Err(err) => {
Err(err.change_context(errors::ApiErrorResponse::VerificationFailed { data: None }))
}
}?;
connector_response = match db
.insert_connector_response(PaymentCreate::make_connector_response(&payment_attempt))
.await
{
Ok(connector_resp) => Ok(connector_resp),
Err(err) => {
Err(err.change_context(errors::ApiErrorResponse::VerificationFailed { data: None }))
}
}?;
Ok((
Box::new(self),
PaymentData {
flow: PhantomData,
payment_intent,
payment_attempt,
/// currency and amount are irrelevant in this scenario
currency: enums::Currency::default(),
amount: 0,
mandate_id: None,
setup_mandate: request.mandate_data.clone(),
token: request.payment_token.clone(),
connector_response,
payment_method_data: request.payment_method_data.clone(),
confirm: Some(true),
address: types::PaymentAddress::default(),
force_sync: None,
refunds: vec![],
},
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> UpdateTracker<F, PaymentData<F>, api::VerifyRequest> for PaymentMethodValidate {
#[instrument(skip_all)]
async fn update_trackers<'b>(
&'b self,
db: &dyn StorageInterface,
_payment_id: &api::PaymentIdType,
mut payment_data: PaymentData<F>,
_customer: Option<storage::Customer>,
) -> RouterResult<(BoxedOperation<'b, F, api::VerifyRequest>, 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(enums::IntentStatus::Processing);
let customer_id = payment_data.payment_intent.customer_id.clone();
payment_data.payment_intent = db
.update_payment_intent(
payment_data.payment_intent,
storage::PaymentIntentUpdate::ReturnUrlUpdate {
return_url: None,
status,
customer_id,
shipping_address_id: None,
billing_address_id: None,
},
)
.await
.map_err(|err| {
err.to_not_found_response(errors::ApiErrorResponse::VerificationFailed {
data: None,
})
})?;
Ok((Box::new(self), payment_data))
}
}
#[async_trait]
impl<F, Op> Domain<F, api::VerifyRequest> for Op
where
F: Clone + Send,
Op: Send + Sync + Operation<F, api::VerifyRequest>,
for<'a> &'a Op: Operation<F, api::VerifyRequest>,
{
#[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>,
merchant_id: &str,
) -> CustomResult<
(
BoxedOperation<'a, F, api::VerifyRequest>,
Option<api::CustomerResponse>,
),
errors::StorageError,
> {
helpers::create_customer_if_not_exist(
Box::new(self),
db,
payment_data,
request,
merchant_id,
)
.await
}
#[instrument(skip_all)]
async fn make_pm_data<'a>(
&'a self,
state: &'a AppState,
payment_method: Option<enums::PaymentMethodType>,
txn_id: &str,
payment_attempt: &storage::PaymentAttempt,
request: &Option<api::PaymentMethod>,
token: &Option<String>,
) -> RouterResult<(
BoxedOperation<'a, F, api::VerifyRequest>,
Option<api::PaymentMethod>,
)> {
helpers::make_pm_data(
Box::new(self),
state,
payment_method,
txn_id,
payment_attempt,
request,
token,
)
.await
}
}
impl PaymentMethodValidate {
#[instrument(skip_all)]
fn make_payment_attempt(
payment_id: &str,
merchant_id: &str,
connector: types::Connector,
payment_method: Option<enums::PaymentMethodType>,
_request: &api::VerifyRequest,
) -> storage::PaymentAttemptNew {
let created_at @ modified_at @ last_synced = Some(date_time::now());
let status = enums::AttemptStatus::Pending;
storage::PaymentAttemptNew {
payment_id: payment_id.to_string(),
merchant_id: merchant_id.to_string(),
txn_id: Uuid::new_v4().to_string(),
status,
// Amount & Currency will be zero in this case
amount: 0,
currency: Default::default(),
connector: connector.to_string(),
payment_method,
confirm: true,
created_at,
modified_at,
last_synced,
..Default::default()
}
}
fn make_payment_intent(
payment_id: &str,
merchant_id: &str,
connector: types::Connector,
request: &api::VerifyRequest,
) -> 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!("{}_secret", payment_id).as_str());
storage::PaymentIntentNew {
payment_id: payment_id.to_string(),
merchant_id: merchant_id.to_string(),
status,
amount: 0,
currency: Default::default(),
connector_id: Some(connector.to_string()),
created_at,
modified_at,
last_synced,
client_secret: Some(client_secret),
setup_future_usage: request.setup_future_usage,
off_session: request.off_session,
..Default::default()
}
}
}

View File

@ -3,7 +3,6 @@ use masking::{PeekInterface, Secret};
use router_derive::Setter; use router_derive::Setter;
use time::PrimitiveDateTime; use time::PrimitiveDateTime;
use super::{ConnectorCommon, RefundResponse};
use crate::{ use crate::{
core::errors, core::errors,
pii, pii,
@ -89,9 +88,9 @@ pub struct VerifyRequest {
pub phone_country_code: Option<String>, pub phone_country_code: Option<String>,
pub payment_method: Option<enums::PaymentMethodType>, pub payment_method: Option<enums::PaymentMethodType>,
pub payment_method_data: Option<PaymentMethod>, pub payment_method_data: Option<PaymentMethod>,
pub payment_token: Option<i32>, pub payment_token: Option<String>,
pub mandate_data: Option<MandateData>, pub mandate_data: Option<MandateData>,
pub setup_future_usage: Option<super::FutureUsage>, pub setup_future_usage: Option<api_types::FutureUsage>,
pub off_session: Option<bool>, pub off_session: Option<bool>,
pub client_secret: Option<String>, pub client_secret: Option<String>,
} }
@ -338,7 +337,7 @@ pub struct PaymentsResponse {
pub currency: String, pub currency: String,
pub customer_id: Option<String>, pub customer_id: Option<String>,
pub description: Option<String>, pub description: Option<String>,
pub refunds: Option<Vec<RefundResponse>>, pub refunds: Option<Vec<api_types::RefundResponse>>,
pub mandate_id: Option<String>, pub mandate_id: Option<String>,
pub mandate_data: Option<MandateData>, pub mandate_data: Option<MandateData>,
pub setup_future_usage: Option<enums::FutureUsage>, pub setup_future_usage: Option<enums::FutureUsage>,
@ -426,6 +425,51 @@ pub struct PaymentsRedirectionResponse {
pub redirect_url: String, pub redirect_url: String,
} }
pub struct MandateValidationFields {
pub mandate_id: Option<String>,
pub confirm: Option<bool>,
pub customer_id: Option<String>,
pub mandate_data: Option<MandateData>,
pub setup_future_usage: Option<api_types::FutureUsage>,
pub off_session: Option<bool>,
}
impl MandateValidationFields {
pub fn is_mandate(&self) -> Option<MandateTxnType> {
match (&self.mandate_data, &self.mandate_id) {
(None, None) => None,
(_, Some(_)) => Some(MandateTxnType::RecurringMandateTxn),
(Some(_), _) => Some(MandateTxnType::NewMandateTxn),
}
}
}
impl From<&PaymentsRequest> for MandateValidationFields {
fn from(req: &PaymentsRequest) -> Self {
Self {
mandate_id: req.mandate_id.clone(),
confirm: req.confirm,
customer_id: req.customer_id.clone(),
mandate_data: req.mandate_data.clone(),
setup_future_usage: req.setup_future_usage,
off_session: req.off_session,
}
}
}
impl From<&VerifyRequest> for MandateValidationFields {
fn from(req: &VerifyRequest) -> Self {
Self {
mandate_id: None,
confirm: Some(true),
customer_id: req.customer_id.clone(),
mandate_data: req.mandate_data.clone(),
off_session: req.off_session,
setup_future_usage: req.setup_future_usage,
}
}
}
impl PaymentsRedirectionResponse { impl PaymentsRedirectionResponse {
pub fn new(redirect_url: &str) -> Self { pub fn new(redirect_url: &str) -> Self {
Self { Self {
@ -659,7 +703,12 @@ pub trait PreVerify:
} }
pub trait Payment: pub trait Payment:
ConnectorCommon + PaymentAuthorize + PaymentSync + PaymentCapture + PaymentVoid + PreVerify api_types::ConnectorCommon
+ PaymentAuthorize
+ PaymentSync
+ PaymentCapture
+ PaymentVoid
+ PreVerify
{ {
} }
#[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone)] #[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone)]

View File

@ -15,6 +15,7 @@ enum Derives {
Canceldata, Canceldata,
Capturedata, Capturedata,
Start, Start,
Verify,
Session, Session,
} }
@ -30,6 +31,7 @@ impl From<String> for Derives {
"capture" => Self::Capture, "capture" => Self::Capture,
"capturedata" => Self::Capturedata, "capturedata" => Self::Capturedata,
"start" => Self::Start, "start" => Self::Start,
"verify" => Self::Verify,
"session" => Self::Session, "session" => Self::Session,
_ => Self::Authorize, _ => Self::Authorize,
} }
@ -100,6 +102,7 @@ impl Conversion {
Derives::Capture => syn::Ident::new("PaymentsCaptureRequest", Span::call_site()), Derives::Capture => syn::Ident::new("PaymentsCaptureRequest", Span::call_site()),
Derives::Capturedata => syn::Ident::new("PaymentsCaptureData", Span::call_site()), Derives::Capturedata => syn::Ident::new("PaymentsCaptureData", Span::call_site()),
Derives::Start => syn::Ident::new("PaymentsStartRequest", Span::call_site()), Derives::Start => syn::Ident::new("PaymentsStartRequest", Span::call_site()),
Derives::Verify => syn::Ident::new("VerifyRequest", Span::call_site()),
Derives::Session => syn::Ident::new("PaymentsSessionRequest", Span::call_site()), Derives::Session => syn::Ident::new("PaymentsSessionRequest", Span::call_site()),
} }
} }
@ -288,7 +291,8 @@ pub fn operation_derive_inner(token: proc_macro::TokenStream) -> proc_macro::Tok
PaymentsRetrieveRequest, PaymentsRetrieveRequest,
PaymentsRequest, PaymentsRequest,
PaymentsStartRequest, PaymentsStartRequest,
PaymentsSessionRequest PaymentsSessionRequest,
VerifyRequest
} }
}; };
#trait_derive #trait_derive