diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs index 22dbcb1af1..f2f38ee997 100644 --- a/crates/router/src/compatibility/stripe/errors.rs +++ b/crates/router/src/compatibility/stripe/errors.rs @@ -146,6 +146,8 @@ pub(crate) enum ErrorCode { current_value: String, states: String, }, + #[error(error_type = StripeErrorType::InvalidRequestError, code = "", message = "The mandate information is invalid. {message}")] + PaymentIntentMandateInvalid { message: String }, // TODO: Some day implement all stripe error codes https://stripe.com/docs/error-codes // AccountCountryInvalidAddress, // AccountErrorCountryChangeRequiresAdditionalSteps, @@ -227,7 +229,6 @@ pub(crate) enum ErrorCode { // PaymentIntentIncompatiblePaymentMethod, // PaymentIntentInvalidParameter, // PaymentIntentKonbiniRejectedConfirmationNumber, - // PaymentIntentMandateInvalid, // PaymentIntentPaymentAttemptExpired, // PaymentIntentUnexpectedState, // PaymentMethodBankAccountAlreadyVerified, @@ -350,6 +351,9 @@ impl From for ErrorCode { ErrorCode::MerchantConnectorAccountNotFound } ApiErrorResponse::MandateNotFound => ErrorCode::MandateNotFound, + ApiErrorResponse::MandateValidationFailed { reason } => { + ErrorCode::PaymentIntentMandateInvalid { message: reason } + } ApiErrorResponse::ReturnUrlUnavailable => ErrorCode::ReturnUrlUnavailable, ApiErrorResponse::DuplicateMerchantAccount => ErrorCode::DuplicateMerchantAccount, ApiErrorResponse::DuplicateMerchantConnectorAccount => { @@ -427,6 +431,7 @@ impl actix_web::ResponseError for ErrorCode { | ErrorCode::SuccessfulPaymentNotFound | ErrorCode::AddressNotFound | ErrorCode::ResourceIdNotFound + | ErrorCode::PaymentIntentMandateInvalid { .. } | ErrorCode::PaymentIntentUnexpectedState { .. } => StatusCode::BAD_REQUEST, ErrorCode::RefundFailed | ErrorCode::InternalServerError => { StatusCode::INTERNAL_SERVER_ERROR diff --git a/crates/router/src/core/errors/api_error_response.rs b/crates/router/src/core/errors/api_error_response.rs index c79dcbe73a..88dbb652cb 100644 --- a/crates/router/src/core/errors/api_error_response.rs +++ b/crates/router/src/core/errors/api_error_response.rs @@ -120,6 +120,8 @@ pub enum ApiErrorResponse { SuccessfulPaymentNotFound, #[error(error_type = ErrorType::ObjectNotFound, code = "RE_05", message = "Address does not exist in our records.")] AddressNotFound, + #[error(error_type = ErrorType::ValidationError, code = "RE_03", message = "Mandate Validation Failed" )] + MandateValidationFailed { reason: String }, #[error(error_type = ErrorType::ServerNotAvailable, code = "IR_00", message = "This API is under development and will be made available soon.")] NotImplemented, } @@ -159,7 +161,8 @@ impl actix_web::ResponseError for ApiErrorResponse { | ApiErrorResponse::CardExpired { .. } | ApiErrorResponse::RefundFailed { .. } | ApiErrorResponse::VerificationFailed { .. } - | ApiErrorResponse::PaymentUnexpectedState { .. } => StatusCode::BAD_REQUEST, // 400 + | ApiErrorResponse::PaymentUnexpectedState { .. } + | ApiErrorResponse::MandateValidationFailed { .. } => StatusCode::BAD_REQUEST, // 400 ApiErrorResponse::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, // 500 ApiErrorResponse::DuplicateRefundRequest => StatusCode::BAD_REQUEST, // 400 diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs index 6dfbe33232..b2cb17fd76 100644 --- a/crates/router/src/core/payments/flows/authorize_flow.rs +++ b/crates/router/src/core/payments/flows/authorize_flow.rs @@ -1,10 +1,8 @@ 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}, @@ -17,7 +15,6 @@ use crate::{ storage::{self, enums as storage_enums}, PaymentsAuthorizeData, PaymentsAuthorizeRouterData, PaymentsResponseData, }, - utils, }; #[async_trait] @@ -124,7 +121,20 @@ impl PaymentsAuthorizeRouterData { ) .await .change_context(errors::ApiErrorResponse::MandateNotFound), - storage_enums::MandateType::MultiUse => Ok(mandate), + storage_enums::MandateType::MultiUse => state + .store + .update_mandate_by_merchant_id_mandate_id( + &resp.merchant_id, + mandate_id, + storage::MandateUpdate::CaptureAmountUpdate { + amount_captured: Some( + mandate.amount_captured.unwrap_or(0) + + self.request.amount, + ), + }, + ) + .await + .change_context(errors::ApiErrorResponse::MandateNotFound), }?; resp.payment_method_id = Some(mandate.payment_method_id); @@ -142,9 +152,13 @@ impl PaymentsAuthorizeRouterData { .payment_method_id; resp.payment_method_id = Some(payment_method_id.clone()); - if let Some(new_mandate_data) = - self.generate_mandate(maybe_customer, payment_method_id) - { + if let Some(new_mandate_data) = helpers::generate_mandate( + self.merchant_id.clone(), + self.connector.clone(), + None, + 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| { @@ -163,44 +177,4 @@ impl PaymentsAuthorizeRouterData { _ => Ok(self.clone()), } } - - fn generate_mandate( - &self, - customer: &Option, - payment_method_id: String, - ) -> Option { - match (self.request.setup_mandate_details.clone(), customer) { - (Some(data), Some(cus)) => { - let mandate_id = utils::generate_id(consts::ID_LENGTH, "man"); - - // The construction of the mandate new must be visible - let mut new_mandate = storage::MandateNew::default(); - - new_mandate - .set_mandate_id(mandate_id) - .set_customer_id(cus.customer_id.clone()) - .set_merchant_id(self.merchant_id.clone()) - .set_payment_method_id(payment_method_id) - .set_mandate_status(storage_enums::MandateStatus::Active) - .set_customer_ip_address( - data.customer_acceptance.get_ip_address().map(Secret::new), - ) - .set_customer_user_agent(data.customer_acceptance.get_user_agent()) - .set_customer_accepted_at(Some(data.customer_acceptance.get_accepted_at())); - - Some(match data.mandate_type { - api::MandateType::SingleUse(data) => new_mandate - .set_single_use_amount(Some(data.amount)) - .set_single_use_currency(Some(data.currency)) - .set_mandate_type(storage_enums::MandateType::SingleUse) - .to_owned(), - - api::MandateType::MultiUse => new_mandate - .set_mandate_type(storage_enums::MandateType::MultiUse) - .to_owned(), - }) - } - (_, _) => None, - } - } } diff --git a/crates/router/src/core/payments/flows/verfiy_flow.rs b/crates/router/src/core/payments/flows/verfiy_flow.rs index 41189e719b..3ab253348d 100644 --- a/crates/router/src/core/payments/flows/verfiy_flow.rs +++ b/crates/router/src/core/payments/flows/verfiy_flow.rs @@ -1,10 +1,8 @@ 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}, @@ -15,7 +13,6 @@ use crate::{ self, api, storage::{self, enums}, }, - utils, }; #[async_trait] @@ -108,8 +105,9 @@ impl types::VerifyRouterData { .payment_method_id; resp.payment_method_id = Some(payment_method_id.clone()); - if let Some(new_mandate_data) = generate_mandate( + if let Some(new_mandate_data) = helpers::generate_mandate( self.merchant_id.clone(), + self.connector.clone(), self.request.setup_mandate_details.clone(), maybe_customer, payment_method_id, @@ -132,29 +130,3 @@ impl types::VerifyRouterData { } } } - -fn generate_mandate( - merchant_id: String, - setup_mandate_details: Option, - customer: &Option, - payment_method_id: String, -) -> Option { - 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, - } -} diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index f805a2e807..2a7b231469 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -141,10 +141,7 @@ pub async fn get_token_for_recurring_mandate( .await .map_err(|error| error.to_not_found_response(errors::ApiErrorResponse::MandateNotFound))?; - utils::when( - mandate.mandate_status != storage_enums::MandateStatus::Active, - Err(errors::ApiErrorResponse::MandateNotFound), - )?; + // TODO: Make currency in payments request as Currency enum let customer = req.customer_id.clone().get_required_value("customer_id")?; @@ -159,8 +156,13 @@ pub async fn get_token_for_recurring_mandate( message: "mandate is not active".into() }))? }; - mandate.payment_method_id + mandate.payment_method_id.clone() }; + verify_mandate_details( + req.amount.get_required_value("amount")?.into(), + req.currency.clone().get_required_value("currency")?, + mandate.clone(), + )?; let payment_method = db .find_payment_method(payment_method_id.as_str()) @@ -346,6 +348,44 @@ fn validate_recurring_mandate(req: api::MandateValidationFields) -> RouterResult Ok(()) } +pub fn verify_mandate_details( + request_amount: i32, + request_currency: String, + mandate: storage::Mandate, +) -> RouterResult<()> { + match mandate.mandate_type { + storage_enums::MandateType::SingleUse => utils::when( + mandate + .mandate_amount + .map(|mandate_amount| request_amount > mandate_amount) + .unwrap_or(true), + Err(report!(errors::ApiErrorResponse::MandateValidationFailed { + reason: "request amount is greater than mandate amount".to_string() + })), + ), + storage::enums::MandateType::MultiUse => utils::when( + mandate + .mandate_amount + .map(|mandate_amount| { + (mandate.amount_captured.unwrap_or(0) + request_amount) > mandate_amount + }) + .unwrap_or(false), + Err(report!(errors::ApiErrorResponse::MandateValidationFailed { + reason: "request amount is greater than mandate amount".to_string() + })), + ), + }?; + utils::when( + mandate + .mandate_currency + .map(|mandate_currency| mandate_currency.to_string() != request_currency) + .unwrap_or(true), + Err(report!(errors::ApiErrorResponse::MandateValidationFailed { + reason: "cross currency mandates not supported".to_string() + })), + ) +} + #[instrument(skip_all)] pub fn payment_attempt_status_fsm( payment_method_data: &Option, @@ -1077,3 +1117,53 @@ pub fn hmac_sha256_sorted_query_params<'a>( pub fn check_if_operation_confirm(operations: Op) -> bool { format!("{:?}", operations) == "PaymentConfirm" } + +pub fn generate_mandate( + merchant_id: String, + connector: String, + setup_mandate_details: Option, + customer: &Option, + payment_method_id: String, +) -> Option { + match (setup_mandate_details, customer) { + (Some(data), Some(cus)) => { + let mandate_id = utils::generate_id(consts::ID_LENGTH, "man"); + + // The construction of the mandate new must be visible + let mut new_mandate = storage::MandateNew::default(); + + new_mandate + .set_mandate_id(mandate_id) + .set_customer_id(cus.customer_id.clone()) + .set_merchant_id(merchant_id) + .set_payment_method_id(payment_method_id) + .set_connector(connector) + .set_mandate_status(storage_enums::MandateStatus::Active) + .set_customer_ip_address( + data.customer_acceptance + .get_ip_address() + .map(masking::Secret::new), + ) + .set_customer_user_agent(data.customer_acceptance.get_user_agent()) + .set_customer_accepted_at(Some(data.customer_acceptance.get_accepted_at())); + + Some(match data.mandate_type { + api::MandateType::SingleUse(data) => new_mandate + .set_mandate_amount(Some(data.amount)) + .set_mandate_currency(Some(data.currency)) + .set_mandate_type(storage_enums::MandateType::SingleUse) + .to_owned(), + + api::MandateType::MultiUse(op_data) => match op_data { + Some(data) => new_mandate + .set_mandate_amount(Some(data.amount)) + .set_mandate_currency(Some(data.currency)), + None => &mut new_mandate, + } + .set_mandate_type(storage_enums::MandateType::MultiUse) + .to_owned(), + }) + } + (_, _) => None, + } +} diff --git a/crates/router/src/schema.rs b/crates/router/src/schema.rs index dbb330e0a6..c719ba473a 100644 --- a/crates/router/src/schema.rs +++ b/crates/router/src/schema.rs @@ -127,8 +127,11 @@ diesel::table! { network_transaction_id -> Nullable, previous_transaction_id -> Nullable, created_at -> Timestamp, - single_use_amount -> Nullable, - single_use_currency -> Nullable, + mandate_amount -> Nullable, + mandate_currency -> Nullable, + amount_captured -> Nullable, + connector -> Varchar, + connector_mandate_id -> Nullable, } } diff --git a/crates/router/src/types/api/payments.rs b/crates/router/src/types/api/payments.rs index e593afdfb5..e2b3b72327 100644 --- a/crates/router/src/types/api/payments.rs +++ b/crates/router/src/types/api/payments.rs @@ -154,11 +154,16 @@ pub struct MandateData { pub mandate_type: MandateType, } -#[derive(Default, Eq, PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone)] +#[derive(Eq, PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone)] pub enum MandateType { - SingleUse(storage::SingleUseMandate), - #[default] - MultiUse, + SingleUse(storage::MandateAmountData), + MultiUse(Option), +} + +impl Default for MandateType { + fn default() -> Self { + Self::MultiUse(None) + } } #[derive(Default, Eq, PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone)] diff --git a/crates/router/src/types/storage/mandate.rs b/crates/router/src/types/storage/mandate.rs index d9d251e4da..7d00d1e509 100644 --- a/crates/router/src/types/storage/mandate.rs +++ b/crates/router/src/types/storage/mandate.rs @@ -24,8 +24,11 @@ pub struct Mandate { pub network_transaction_id: Option, pub previous_transaction_id: Option, pub created_at: PrimitiveDateTime, - pub single_use_amount: Option, - pub single_use_currency: Option, + pub mandate_amount: Option, + pub mandate_currency: Option, + pub amount_captured: Option, + pub connector: String, + pub connector_mandate_id: Option, } #[derive( @@ -45,8 +48,11 @@ pub struct MandateNew { pub network_transaction_id: Option, pub previous_transaction_id: Option, pub created_at: Option, - pub single_use_amount: Option, - pub single_use_currency: Option, + pub mandate_amount: Option, + pub mandate_currency: Option, + pub amount_captured: Option, + pub connector: String, + pub connector_mandate_id: String, } #[derive(Debug)] @@ -54,10 +60,13 @@ pub enum MandateUpdate { StatusUpdate { mandate_status: storage_enums::MandateStatus, }, + CaptureAmountUpdate { + amount_captured: Option, + }, } #[derive(Clone, Eq, PartialEq, Copy, Debug, Default, serde::Serialize, serde::Deserialize)] -pub struct SingleUseMandate { +pub struct MandateAmountData { pub amount: i32, pub currency: storage_enums::Currency, } @@ -65,13 +74,21 @@ pub struct SingleUseMandate { #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] #[diesel(table_name = mandate)] pub(super) struct MandateUpdateInternal { - mandate_status: storage_enums::MandateStatus, + mandate_status: Option, + amount_captured: Option, } impl From for MandateUpdateInternal { fn from(mandate_update: MandateUpdate) -> Self { match mandate_update { - MandateUpdate::StatusUpdate { mandate_status } => Self { mandate_status }, + MandateUpdate::StatusUpdate { mandate_status } => Self { + mandate_status: Some(mandate_status), + amount_captured: None, + }, + MandateUpdate::CaptureAmountUpdate { amount_captured } => Self { + mandate_status: None, + amount_captured, + }, } } } diff --git a/migrations/2022-12-09-102635_mandate-connector-and-amount/down.sql b/migrations/2022-12-09-102635_mandate-connector-and-amount/down.sql new file mode 100644 index 0000000000..f0ef5bd2a0 --- /dev/null +++ b/migrations/2022-12-09-102635_mandate-connector-and-amount/down.sql @@ -0,0 +1,9 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE mandate +RENAME COLUMN mandate_amount TO single_use_amount; +ALTER TABLE mandate +RENAME COLUMN mandate_currency TO single_use_currency; +ALTER TABLE mandate +DROP COLUMN IF EXISTS amount_captured, +DROP COLUMN IF EXISTS connector, +DROP COLUMN IF EXISTS connector_mandate_id; \ No newline at end of file diff --git a/migrations/2022-12-09-102635_mandate-connector-and-amount/up.sql b/migrations/2022-12-09-102635_mandate-connector-and-amount/up.sql new file mode 100644 index 0000000000..b096618387 --- /dev/null +++ b/migrations/2022-12-09-102635_mandate-connector-and-amount/up.sql @@ -0,0 +1,9 @@ +-- Your SQL goes here +ALTER TABLE mandate +RENAME COLUMN single_use_amount TO mandate_amount; +ALTER TABLE mandate +RENAME COLUMN single_use_currency TO mandate_currency; +ALTER TABLE mandate +ADD IF NOT EXISTS amount_captured INTEGER DEFAULT NULL, +ADD IF NOT EXISTS connector VARCHAR(255) NOT NULL DEFAULT 'dummy', +ADD IF NOT EXISTS connector_mandate_id VARCHAR(255) DEFAULT NULL; \ No newline at end of file