diff --git a/crates/router/src/connector/braintree.rs b/crates/router/src/connector/braintree.rs index 55de2cb4ec..798d29b84b 100644 --- a/crates/router/src/connector/braintree.rs +++ b/crates/router/src/connector/braintree.rs @@ -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)] diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 7a0448d599..459140795e 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -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> { +pub fn validate_mandate( + req: impl Into, +) -> RouterResult> { + 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 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 { // 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, diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index 4d73ed45ab..dcfdfdffc8 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -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; diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 8959bcdb3f..7e6e2ffb1a 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -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 { diff --git a/crates/router/src/core/payments/operations/payment_method_validate.rs b/crates/router/src/core/payments/operations/payment_method_validate.rs new file mode 100644 index 0000000000..4c4e957c06 --- /dev/null +++ b/crates/router/src/core/payments/operations/payment_method_validate.rs @@ -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 ValidateRequest 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, + )> { + 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 GetTracker, 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, + ) -> RouterResult<( + BoxedOperation<'a, F, api::VerifyRequest>, + PaymentData, + Option, + )> { + 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 UpdateTracker, 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, + _customer: Option, + ) -> RouterResult<(BoxedOperation<'b, F, api::VerifyRequest>, PaymentData)> + 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 Domain for Op +where + F: Clone + Send, + Op: Send + Sync + Operation, + for<'a> &'a Op: Operation, +{ + #[instrument(skip_all)] + async fn get_or_create_customer_details<'a>( + &'a self, + db: &dyn StorageInterface, + payment_data: &mut PaymentData, + request: Option, + merchant_id: &str, + ) -> CustomResult< + ( + BoxedOperation<'a, F, api::VerifyRequest>, + Option, + ), + 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, + txn_id: &str, + payment_attempt: &storage::PaymentAttempt, + request: &Option, + token: &Option, + ) -> RouterResult<( + BoxedOperation<'a, F, api::VerifyRequest>, + Option, + )> { + 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, + _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() + } + } +} diff --git a/crates/router/src/types/api/payments.rs b/crates/router/src/types/api/payments.rs index d779c5a5d9..c383673bfd 100644 --- a/crates/router/src/types/api/payments.rs +++ b/crates/router/src/types/api/payments.rs @@ -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, pub payment_method: Option, pub payment_method_data: Option, - pub payment_token: Option, + pub payment_token: Option, pub mandate_data: Option, - pub setup_future_usage: Option, + pub setup_future_usage: Option, pub off_session: Option, pub client_secret: Option, } @@ -338,7 +337,7 @@ pub struct PaymentsResponse { pub currency: String, pub customer_id: Option, pub description: Option, - pub refunds: Option>, + pub refunds: Option>, pub mandate_id: Option, pub mandate_data: Option, pub setup_future_usage: Option, @@ -426,6 +425,51 @@ pub struct PaymentsRedirectionResponse { pub redirect_url: String, } +pub struct MandateValidationFields { + pub mandate_id: Option, + pub confirm: Option, + pub customer_id: Option, + pub mandate_data: Option, + pub setup_future_usage: Option, + pub off_session: Option, +} + +impl MandateValidationFields { + pub fn is_mandate(&self) -> Option { + 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)] diff --git a/crates/router_derive/src/macros/operation.rs b/crates/router_derive/src/macros/operation.rs index 2674682668..078c405e8f 100644 --- a/crates/router_derive/src/macros/operation.rs +++ b/crates/router_derive/src/macros/operation.rs @@ -15,6 +15,7 @@ enum Derives { Canceldata, Capturedata, Start, + Verify, Session, } @@ -30,6 +31,7 @@ impl From 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