mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 09:07:09 +08:00
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:
@ -53,6 +53,7 @@ impl api::PaymentVoid for Braintree {}
|
||||
impl api::PaymentCapture for Braintree {}
|
||||
impl api::PreVerify for Braintree {}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl
|
||||
services::ConnectorIntegration<
|
||||
api::Verify,
|
||||
@ -60,6 +61,7 @@ impl
|
||||
types::PaymentsResponseData,
|
||||
> for Braintree
|
||||
{
|
||||
// Not Implemented (R)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
||||
@ -20,7 +20,7 @@ use crate::{
|
||||
routes::AppState,
|
||||
services,
|
||||
types::{
|
||||
api::{self, PgRedirectResponse},
|
||||
api,
|
||||
storage::{self, enums},
|
||||
},
|
||||
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() {
|
||||
Some(api::MandateTxnType::NewMandateTxn) => {
|
||||
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")?;
|
||||
|
||||
if !confirm {
|
||||
@ -302,8 +305,7 @@ pub fn create_redirect_url(server: &Server, payment_attempt: &storage::PaymentAt
|
||||
payment_attempt.connector
|
||||
)
|
||||
}
|
||||
|
||||
fn validate_recurring_mandate(req: &api::PaymentsRequest) -> RouterResult<()> {
|
||||
fn validate_recurring_mandate(req: api::MandateValidationFields) -> RouterResult<()> {
|
||||
req.mandate_id.check_value_present("mandate_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(
|
||||
merchant_account: &storage::MerchantAccount,
|
||||
redirection_response: PgRedirectResponse,
|
||||
redirection_response: api::PgRedirectResponse,
|
||||
request_return_url: Option<&String>,
|
||||
) -> RouterResult<String> {
|
||||
// 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,
|
||||
response: &api::PaymentsResponse,
|
||||
connector: String,
|
||||
) -> PgRedirectResponse {
|
||||
PgRedirectResponse {
|
||||
) -> api::PgRedirectResponse {
|
||||
api::PgRedirectResponse {
|
||||
payment_id,
|
||||
status: response.status,
|
||||
gateway_id: connector,
|
||||
|
||||
@ -2,6 +2,7 @@ mod payment_cancel;
|
||||
mod payment_capture;
|
||||
mod payment_confirm;
|
||||
mod payment_create;
|
||||
mod payment_method_validate;
|
||||
mod payment_response;
|
||||
mod payment_session;
|
||||
mod payment_start;
|
||||
|
||||
@ -364,7 +364,7 @@ impl PaymentCreate {
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
fn make_connector_response(
|
||||
pub fn make_connector_response(
|
||||
payment_attempt: &storage::PaymentAttempt,
|
||||
) -> storage::ConnectorResponseNew {
|
||||
storage::ConnectorResponseNew {
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,6 @@ use masking::{PeekInterface, Secret};
|
||||
use router_derive::Setter;
|
||||
use time::PrimitiveDateTime;
|
||||
|
||||
use super::{ConnectorCommon, RefundResponse};
|
||||
use crate::{
|
||||
core::errors,
|
||||
pii,
|
||||
@ -89,9 +88,9 @@ pub struct VerifyRequest {
|
||||
pub phone_country_code: Option<String>,
|
||||
pub payment_method: Option<enums::PaymentMethodType>,
|
||||
pub payment_method_data: Option<PaymentMethod>,
|
||||
pub payment_token: Option<i32>,
|
||||
pub payment_token: Option<String>,
|
||||
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 client_secret: Option<String>,
|
||||
}
|
||||
@ -338,7 +337,7 @@ pub struct PaymentsResponse {
|
||||
pub currency: String,
|
||||
pub customer_id: 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_data: Option<MandateData>,
|
||||
pub setup_future_usage: Option<enums::FutureUsage>,
|
||||
@ -426,6 +425,51 @@ pub struct PaymentsRedirectionResponse {
|
||||
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 {
|
||||
pub fn new(redirect_url: &str) -> Self {
|
||||
Self {
|
||||
@ -659,7 +703,12 @@ pub trait PreVerify:
|
||||
}
|
||||
|
||||
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)]
|
||||
|
||||
@ -15,6 +15,7 @@ enum Derives {
|
||||
Canceldata,
|
||||
Capturedata,
|
||||
Start,
|
||||
Verify,
|
||||
Session,
|
||||
}
|
||||
|
||||
@ -30,6 +31,7 @@ impl From<String> for Derives {
|
||||
"capture" => Self::Capture,
|
||||
"capturedata" => Self::Capturedata,
|
||||
"start" => Self::Start,
|
||||
"verify" => Self::Verify,
|
||||
"session" => Self::Session,
|
||||
_ => Self::Authorize,
|
||||
}
|
||||
@ -100,6 +102,7 @@ impl Conversion {
|
||||
Derives::Capture => syn::Ident::new("PaymentsCaptureRequest", Span::call_site()),
|
||||
Derives::Capturedata => syn::Ident::new("PaymentsCaptureData", 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()),
|
||||
}
|
||||
}
|
||||
@ -288,7 +291,8 @@ pub fn operation_derive_inner(token: proc_macro::TokenStream) -> proc_macro::Tok
|
||||
PaymentsRetrieveRequest,
|
||||
PaymentsRequest,
|
||||
PaymentsStartRequest,
|
||||
PaymentsSessionRequest
|
||||
PaymentsSessionRequest,
|
||||
VerifyRequest
|
||||
}
|
||||
};
|
||||
#trait_derive
|
||||
|
||||
Reference in New Issue
Block a user