diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index 2f3336fc2777..f718dc1ca4dd 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -8,8 +8,9 @@ use crate::{ payments::{ PaymentIdType, PaymentListConstraints, PaymentListFilterConstraints, PaymentListFilters, PaymentListResponse, PaymentListResponseV2, PaymentsApproveRequest, PaymentsCancelRequest, - PaymentsCaptureRequest, PaymentsRejectRequest, PaymentsRequest, PaymentsResponse, - PaymentsRetrieveRequest, PaymentsStartRequest, RedirectionResponse, + PaymentsCaptureRequest, PaymentsIncrementalAuthorizationRequest, PaymentsRejectRequest, + PaymentsRequest, PaymentsResponse, PaymentsRetrieveRequest, PaymentsStartRequest, + RedirectionResponse, }, }; impl ApiEventMetric for PaymentsRetrieveRequest { @@ -149,3 +150,11 @@ impl ApiEventMetric for PaymentListResponseV2 { } impl ApiEventMetric for RedirectionResponse {} + +impl ApiEventMetric for PaymentsIncrementalAuthorizationRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payment { + payment_id: self.payment_id.clone(), + }) + } +} diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index 8b7cd02c9350..3634b51e0cc0 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -6,11 +6,12 @@ use crate::user::{ dashboard_metadata::{ GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest, }, - ChangePasswordRequest, ConnectAccountRequest, ConnectAccountResponse, - CreateInternalUserRequest, GetUsersResponse, SwitchMerchantIdRequest, UserMerchantCreate, + AuthorizeResponse, ChangePasswordRequest, ConnectAccountRequest, CreateInternalUserRequest, + DashboardEntryResponse, GetUsersResponse, SignUpRequest, SignUpWithMerchantIdRequest, + SwitchMerchantIdRequest, UserMerchantCreate, }; -impl ApiEventMetric for ConnectAccountResponse { +impl ApiEventMetric for DashboardEntryResponse { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::User { merchant_id: self.merchant_id.clone(), @@ -19,9 +20,9 @@ impl ApiEventMetric for ConnectAccountResponse { } } -impl ApiEventMetric for ConnectAccountRequest {} - common_utils::impl_misc_api_event_type!( + SignUpRequest, + SignUpWithMerchantIdRequest, ChangePasswordRequest, GetMultipleMetaDataPayload, GetMetaDataResponse, @@ -30,7 +31,9 @@ common_utils::impl_misc_api_event_type!( SwitchMerchantIdRequest, CreateInternalUserRequest, UserMerchantCreate, - GetUsersResponse + GetUsersResponse, + AuthorizeResponse, + ConnectAccountRequest ); #[cfg(feature = "dummy_connector")] diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index dfb8e8999771..b3c6b049d5d9 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -2,8 +2,10 @@ use std::collections::HashMap; use cards::CardNumber; use common_utils::{ - consts::SURCHARGE_PERCENTAGE_PRECISION_LENGTH, crypto::OptionalEncryptableName, pii, - types::Percentage, + consts::SURCHARGE_PERCENTAGE_PRECISION_LENGTH, + crypto::OptionalEncryptableName, + pii, + types::{Percentage, Surcharge}, }; use serde::de; use utoipa::ToSchema; @@ -14,7 +16,7 @@ use crate::{ admin, customers::CustomerId, enums as api_enums, - payments::{self, BankCodeResponse, RequestSurchargeDetails}, + payments::{self, BankCodeResponse}, }; #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] @@ -337,117 +339,13 @@ pub struct SurchargeDetailsResponse { /// tax on surcharge value pub tax_on_surcharge: Option>, /// surcharge amount for this payment - pub surcharge_amount: i64, + pub display_surcharge_amount: f64, /// tax on surcharge amount for this payment - pub tax_on_surcharge_amount: i64, + pub display_tax_on_surcharge_amount: f64, + /// sum of display_surcharge_amount and display_tax_on_surcharge_amount + pub display_total_surcharge_amount: f64, /// sum of original amount, - pub final_amount: i64, -} - -impl SurchargeDetailsResponse { - pub fn is_request_surcharge_matching( - &self, - request_surcharge_details: RequestSurchargeDetails, - ) -> bool { - request_surcharge_details.surcharge_amount == self.surcharge_amount - && request_surcharge_details.tax_amount.unwrap_or(0) == self.tax_on_surcharge_amount - } - pub fn get_total_surcharge_amount(&self) -> i64 { - self.surcharge_amount + self.tax_on_surcharge_amount - } -} - -#[derive(Clone, Debug)] -pub struct SurchargeMetadata { - surcharge_results: HashMap< - ( - common_enums::PaymentMethod, - common_enums::PaymentMethodType, - Option, - ), - SurchargeDetailsResponse, - >, - pub payment_attempt_id: String, -} - -impl SurchargeMetadata { - pub fn new(payment_attempt_id: String) -> Self { - Self { - surcharge_results: HashMap::new(), - payment_attempt_id, - } - } - pub fn is_empty_result(&self) -> bool { - self.surcharge_results.is_empty() - } - pub fn get_surcharge_results_size(&self) -> usize { - self.surcharge_results.len() - } - pub fn insert_surcharge_details( - &mut self, - payment_method: &common_enums::PaymentMethod, - payment_method_type: &common_enums::PaymentMethodType, - card_network: Option<&common_enums::CardNetwork>, - surcharge_details: SurchargeDetailsResponse, - ) { - let key = ( - payment_method.to_owned(), - payment_method_type.to_owned(), - card_network.cloned(), - ); - self.surcharge_results.insert(key, surcharge_details); - } - pub fn get_surcharge_details( - &self, - payment_method: &common_enums::PaymentMethod, - payment_method_type: &common_enums::PaymentMethodType, - card_network: Option<&common_enums::CardNetwork>, - ) -> Option<&SurchargeDetailsResponse> { - let key = &( - payment_method.to_owned(), - payment_method_type.to_owned(), - card_network.cloned(), - ); - self.surcharge_results.get(key) - } - pub fn get_surcharge_metadata_redis_key(payment_attempt_id: &str) -> String { - format!("surcharge_metadata_{}", payment_attempt_id) - } - pub fn get_individual_surcharge_key_value_pairs( - &self, - ) -> Vec<(String, SurchargeDetailsResponse)> { - self.surcharge_results - .iter() - .map(|((pm, pmt, card_network), surcharge_details)| { - let key = - Self::get_surcharge_details_redis_hashset_key(pm, pmt, card_network.as_ref()); - (key, surcharge_details.to_owned()) - }) - .collect() - } - pub fn get_surcharge_details_redis_hashset_key( - payment_method: &common_enums::PaymentMethod, - payment_method_type: &common_enums::PaymentMethodType, - card_network: Option<&common_enums::CardNetwork>, - ) -> String { - if let Some(card_network) = card_network { - format!( - "{}_{}_{}", - payment_method, payment_method_type, card_network - ) - } else { - format!("{}_{}", payment_method, payment_method_type) - } - } -} - -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] -#[serde(rename_all = "snake_case", tag = "type", content = "value")] -pub enum Surcharge { - /// Fixed Surcharge value - Fixed(i64), - /// Surcharge percentage - Rate(Percentage), + pub display_final_amount: f64, } /// Required fields info used while listing the payment_method_data diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index fe5ed417f350..93c97cbd443c 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -16,7 +16,6 @@ use crate::{ admin, disputes, enums::{self as api_enums}, ephemeral_key::EphemeralKeyCreateResponse, - payment_methods::{Surcharge, SurchargeDetailsResponse}, refunds, }; @@ -204,8 +203,9 @@ pub struct PaymentsRequest { #[schema(example = "187282ab-40ef-47a9-9206-5099ba31e432")] pub payment_token: Option, - /// This is used when payment is to be confirmed and the card is not saved - #[schema(value_type = Option)] + /// This is used when payment is to be confirmed and the card is not saved. + /// This field will be deprecated soon, use the CardToken object instead + #[schema(value_type = Option, deprecated)] pub card_cvc: Option>, /// The shipping address for the payment @@ -339,17 +339,6 @@ impl RequestSurchargeDetails { pub fn is_surcharge_zero(&self) -> bool { self.surcharge_amount == 0 && self.tax_amount.unwrap_or(0) == 0 } - pub fn get_surcharge_details_object(&self, original_amount: i64) -> SurchargeDetailsResponse { - let surcharge_amount = self.surcharge_amount; - let tax_on_surcharge_amount = self.tax_amount.unwrap_or(0); - SurchargeDetailsResponse { - surcharge: Surcharge::Fixed(self.surcharge_amount), - tax_on_surcharge: None, - surcharge_amount, - tax_on_surcharge_amount, - final_amount: original_amount + surcharge_amount + tax_on_surcharge_amount, - } - } pub fn get_total_surcharge_amount(&self) -> i64 { self.surcharge_amount + self.tax_amount.unwrap_or(0) } @@ -720,12 +709,43 @@ pub struct Card { pub nick_name: Option>, } -#[derive(Eq, PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] +impl Card { + fn apply_additional_card_info(&self, additional_card_info: AdditionalCardInfo) -> Self { + Self { + card_number: self.card_number.clone(), + card_exp_month: self.card_exp_month.clone(), + card_exp_year: self.card_exp_year.clone(), + card_holder_name: self.card_holder_name.clone(), + card_cvc: self.card_cvc.clone(), + card_issuer: self + .card_issuer + .clone() + .or(additional_card_info.card_issuer), + card_network: self + .card_network + .clone() + .or(additional_card_info.card_network), + card_type: self.card_type.clone().or(additional_card_info.card_type), + card_issuing_country: self + .card_issuing_country + .clone() + .or(additional_card_info.card_issuing_country), + bank_code: self.bank_code.clone().or(additional_card_info.bank_code), + nick_name: self.nick_name.clone(), + } + } +} + +#[derive(Eq, PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema, Default)] #[serde(rename_all = "snake_case")] pub struct CardToken { /// The card holder's name #[schema(value_type = String, example = "John Test")] pub card_holder_name: Option>, + + /// The CVC number for the card + #[schema(value_type = Option)] + pub card_cvc: Option>, } #[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] @@ -889,6 +909,21 @@ impl PaymentMethodData { | Self::CardToken(_) => None, } } + pub fn apply_additional_payment_data( + &self, + additional_payment_data: AdditionalPaymentData, + ) -> Self { + if let AdditionalPaymentData::Card(additional_card_info) = additional_payment_data { + match self { + Self::Card(card) => { + Self::Card(card.apply_additional_card_info(*additional_card_info)) + } + _ => self.to_owned(), + } + } else { + self.to_owned() + } + } } pub trait GetPaymentMethodType { @@ -2216,6 +2251,12 @@ pub struct PaymentsResponse { /// If true incremental authorization can be performed on this payment pub incremental_authorization_allowed: Option, + + /// Total number of authorizations happened in an incremental_authorization payment + pub authorization_count: Option, + + /// List of incremental authorizations happened to the payment + pub incremental_authorizations: Option>, } #[derive(Clone, Debug, serde::Deserialize, ToSchema, serde::Serialize)] @@ -2284,6 +2325,24 @@ pub struct PaymentListResponse { // The list of payments response objects pub data: Vec, } + +#[derive(Setter, Clone, Default, Debug, PartialEq, serde::Serialize, ToSchema)] +pub struct IncrementalAuthorizationResponse { + /// The unique identifier of authorization + pub authorization_id: String, + /// Amount the authorization has been made for + pub amount: i64, + #[schema(value_type= AuthorizationStatus)] + /// The status of the authorization + pub status: common_enums::AuthorizationStatus, + /// Error code sent by the connector for authorization + pub error_code: Option, + /// Error message sent by the connector for authorization + pub error_message: Option, + /// Previously authorized amount for the payment + pub previously_authorized_amount: i64, +} + #[derive(Clone, Debug, serde::Serialize)] pub struct PaymentListResponseV2 { /// The number of payments included in the list for given constraints @@ -2992,6 +3051,18 @@ pub struct PaymentsCancelRequest { pub merchant_connector_details: Option, } +#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone, ToSchema)] +pub struct PaymentsIncrementalAuthorizationRequest { + /// The identifier for the payment + #[serde(skip)] + pub payment_id: String, + /// The total amount including previously authorized amount and additional amount + #[schema(value_type = i64, example = 6540)] + pub amount: i64, + /// Reason for incremental authorization + pub reason: Option, +} + #[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] pub struct PaymentsApproveRequest { /// The identifier for the payment diff --git a/crates/api_models/src/surcharge_decision_configs.rs b/crates/api_models/src/surcharge_decision_configs.rs index 3ebf8f42744e..0777bde85de0 100644 --- a/crates/api_models/src/surcharge_decision_configs.rs +++ b/crates/api_models/src/surcharge_decision_configs.rs @@ -7,21 +7,21 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] -pub struct SurchargeDetails { - pub surcharge: Surcharge, +pub struct SurchargeDetailsOutput { + pub surcharge: SurchargeOutput, pub tax_on_surcharge: Option>, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case", tag = "type", content = "value")] -pub enum Surcharge { - Fixed(i64), +pub enum SurchargeOutput { + Fixed { amount: i64 }, Rate(Percentage), } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct SurchargeDecisionConfigs { - pub surcharge_details: Option, + pub surcharge_details: Option, } impl EuclidDirFilter for SurchargeDecisionConfigs { const ALLOWED: &'static [DirKeyKind] = &[ @@ -30,7 +30,6 @@ impl EuclidDirFilter for SurchargeDecisionConfigs { DirKeyKind::PaymentAmount, DirKeyKind::PaymentCurrency, DirKeyKind::BillingCountry, - DirKeyKind::CardType, DirKeyKind::CardNetwork, DirKeyKind::PayLaterType, DirKeyKind::WalletType, diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index 36d730f5118e..287c377eb46a 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -7,13 +7,25 @@ pub mod dashboard_metadata; pub mod sample_data; #[derive(serde::Deserialize, Debug, Clone, serde::Serialize)] -pub struct ConnectAccountRequest { +pub struct SignUpWithMerchantIdRequest { + pub name: Secret, + pub email: pii::Email, + pub password: Secret, + pub company_name: String, +} + +pub type SignUpWithMerchantIdResponse = AuthorizeResponse; + +#[derive(serde::Deserialize, Debug, Clone, serde::Serialize)] +pub struct SignUpRequest { pub email: pii::Email, pub password: Secret, } +pub type SignUpResponse = DashboardEntryResponse; + #[derive(serde::Serialize, Debug, Clone)] -pub struct ConnectAccountResponse { +pub struct DashboardEntryResponse { pub token: Secret, pub merchant_id: String, pub name: Secret, @@ -25,6 +37,28 @@ pub struct ConnectAccountResponse { pub user_id: String, } +pub type SignInRequest = SignUpRequest; + +pub type SignInResponse = DashboardEntryResponse; + +#[derive(serde::Deserialize, Debug, Clone, serde::Serialize)] +pub struct ConnectAccountRequest { + pub email: pii::Email, +} + +pub type ConnectAccountResponse = AuthorizeResponse; + +#[derive(serde::Serialize, Debug, Clone)] +pub struct AuthorizeResponse { + pub is_email_sent: bool, + //this field is added for audit/debug reasons + #[serde(skip_serializing)] + pub user_id: String, + //this field is added for audit/debug reasons + #[serde(skip_serializing)] + pub merchant_id: String, +} + #[derive(serde::Deserialize, Debug, serde::Serialize)] pub struct ChangePasswordRequest { pub new_password: Secret, @@ -36,6 +70,8 @@ pub struct SwitchMerchantIdRequest { pub merchant_id: String, } +pub type SwitchMerchantResponse = DashboardEntryResponse; + #[derive(serde::Deserialize, Debug, serde::Serialize)] pub struct CreateInternalUserRequest { pub name: Secret, diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 8da4a2da54cc..7615c0cc8804 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -246,6 +246,32 @@ pub enum CaptureStatus { Failed, } +#[derive( + Default, + Clone, + Debug, + Eq, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, + ToSchema, + Hash, +)] +#[router_derive::diesel_enum(storage_type = "text")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum AuthorizationStatus { + Success, + Failure, + // Processing state is before calling connector + #[default] + Processing, + // Requires merchant action + Unresolved, +} + #[derive( Clone, Copy, diff --git a/crates/common_utils/src/request.rs b/crates/common_utils/src/request.rs index 64bce8649d97..d6d9281a4a05 100644 --- a/crates/common_utils/src/request.rs +++ b/crates/common_utils/src/request.rs @@ -17,6 +17,7 @@ pub enum Method { Post, Put, Delete, + Patch, } #[derive(Deserialize, Serialize, Debug)] diff --git a/crates/common_utils/src/types.rs b/crates/common_utils/src/types.rs index 111f0f43c0f2..cf94f2fe26ce 100644 --- a/crates/common_utils/src/types.rs +++ b/crates/common_utils/src/types.rs @@ -2,7 +2,10 @@ use error_stack::{IntoReport, ResultExt}; use serde::{de::Visitor, Deserialize, Deserializer}; -use crate::errors::{CustomResult, PercentageError}; +use crate::{ + consts, + errors::{CustomResult, PercentageError}, +}; /// Represents Percentage Value between 0 and 100 both inclusive #[derive(Clone, Default, Debug, PartialEq, serde::Serialize)] @@ -136,3 +139,13 @@ impl<'de, const PRECISION: u8> Deserialize<'de> for Percentage { data.deserialize_map(PercentageVisitor:: {}) } } + +/// represents surcharge type and value +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case", tag = "type", content = "value")] +pub enum Surcharge { + /// Fixed Surcharge value + Fixed(i64), + /// Surcharge percentage + Rate(Percentage<{ consts::SURCHARGE_PERCENTAGE_PRECISION_LENGTH }>), +} diff --git a/crates/data_models/src/payments.rs b/crates/data_models/src/payments.rs index af2076bfa10d..7a4787fcf0a0 100644 --- a/crates/data_models/src/payments.rs +++ b/crates/data_models/src/payments.rs @@ -52,4 +52,5 @@ pub struct PaymentIntent { pub surcharge_applicable: Option, pub request_incremental_authorization: storage_enums::RequestIncrementalAuthorization, pub incremental_authorization_allowed: Option, + pub authorization_count: Option, } diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index 44aa48b142ad..f7b849f1d4e1 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -359,6 +359,10 @@ pub enum PaymentAttemptUpdate { connector: Option, updated_by: String, }, + IncrementalAuthorizationAmountUpdate { + amount: i64, + amount_capturable: i64, + }, } impl ForeignIDRef for PaymentAttempt { diff --git a/crates/data_models/src/payments/payment_intent.rs b/crates/data_models/src/payments/payment_intent.rs index d8f927a4e2c5..d7edcfdf1791 100644 --- a/crates/data_models/src/payments/payment_intent.rs +++ b/crates/data_models/src/payments/payment_intent.rs @@ -109,6 +109,7 @@ pub struct PaymentIntentNew { pub surcharge_applicable: Option, pub request_incremental_authorization: storage_enums::RequestIncrementalAuthorization, pub incremental_authorization_allowed: Option, + pub authorization_count: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -186,6 +187,12 @@ pub enum PaymentIntentUpdate { surcharge_applicable: bool, updated_by: String, }, + IncrementalAuthorizationAmountUpdate { + amount: i64, + }, + AuthorizationCountUpdate { + authorization_count: i32, + }, } #[derive(Clone, Debug, Default)] @@ -218,6 +225,7 @@ pub struct PaymentIntentUpdateInternal { pub updated_by: String, pub surcharge_applicable: Option, pub incremental_authorization_allowed: Option, + pub authorization_count: Option, } impl From for PaymentIntentUpdateInternal { @@ -381,6 +389,16 @@ impl From for PaymentIntentUpdateInternal { updated_by, ..Default::default() }, + PaymentIntentUpdate::IncrementalAuthorizationAmountUpdate { amount } => Self { + amount: Some(amount), + ..Default::default() + }, + PaymentIntentUpdate::AuthorizationCountUpdate { + authorization_count, + } => Self { + authorization_count: Some(authorization_count), + ..Default::default() + }, } } } diff --git a/crates/diesel_models/src/authorization.rs b/crates/diesel_models/src/authorization.rs new file mode 100644 index 000000000000..64fd1c65187d --- /dev/null +++ b/crates/diesel_models/src/authorization.rs @@ -0,0 +1,78 @@ +use diesel::{AsChangeset, Identifiable, Insertable, Queryable}; +use serde::{Deserialize, Serialize}; +use time::PrimitiveDateTime; + +use crate::{enums as storage_enums, schema::incremental_authorization}; + +#[derive(Clone, Debug, Eq, PartialEq, Identifiable, Queryable, Serialize, Deserialize, Hash)] +#[diesel(table_name = incremental_authorization)] +#[diesel(primary_key(authorization_id, merchant_id))] +pub struct Authorization { + pub authorization_id: String, + pub merchant_id: String, + pub payment_id: String, + pub amount: i64, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub modified_at: PrimitiveDateTime, + pub status: storage_enums::AuthorizationStatus, + pub error_code: Option, + pub error_message: Option, + pub connector_authorization_id: Option, + pub previously_authorized_amount: i64, +} + +#[derive(Clone, Debug, Insertable, router_derive::DebugAsDisplay, Serialize, Deserialize)] +#[diesel(table_name = incremental_authorization)] +pub struct AuthorizationNew { + pub authorization_id: String, + pub merchant_id: String, + pub payment_id: String, + pub amount: i64, + pub status: storage_enums::AuthorizationStatus, + pub error_code: Option, + pub error_message: Option, + pub connector_authorization_id: Option, + pub previously_authorized_amount: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum AuthorizationUpdate { + StatusUpdate { + status: storage_enums::AuthorizationStatus, + error_code: Option, + error_message: Option, + connector_authorization_id: Option, + }, +} + +#[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] +#[diesel(table_name = incremental_authorization)] +pub struct AuthorizationUpdateInternal { + pub status: Option, + pub error_code: Option, + pub error_message: Option, + pub modified_at: Option, + pub connector_authorization_id: Option, +} + +impl From for AuthorizationUpdateInternal { + fn from(authorization_child_update: AuthorizationUpdate) -> Self { + let now = Some(common_utils::date_time::now()); + match authorization_child_update { + AuthorizationUpdate::StatusUpdate { + status, + error_code, + error_message, + connector_authorization_id, + } => Self { + status: Some(status), + error_code, + error_message, + connector_authorization_id, + modified_at: now, + }, + } + } +} diff --git a/crates/diesel_models/src/lib.rs b/crates/diesel_models/src/lib.rs index 781099662a50..fa32fb84a15d 100644 --- a/crates/diesel_models/src/lib.rs +++ b/crates/diesel_models/src/lib.rs @@ -5,6 +5,7 @@ pub mod capture; pub mod cards_info; pub mod configs; +pub mod authorization; pub mod customers; pub mod dispute; pub mod encryption; diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 216801fa8fb1..b1e8e144a9e3 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -269,6 +269,10 @@ pub enum PaymentAttemptUpdate { connector: Option, updated_by: String, }, + IncrementalAuthorizationAmountUpdate { + amount: i64, + amount_capturable: i64, + }, } #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] @@ -679,6 +683,14 @@ impl From for PaymentAttemptUpdateInternal { updated_by, ..Default::default() }, + PaymentAttemptUpdate::IncrementalAuthorizationAmountUpdate { + amount, + amount_capturable, + } => Self { + amount: Some(amount), + amount_capturable: Some(amount_capturable), + ..Default::default() + }, } } } diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 8d752466103e..1bd5c73a96ca 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -54,6 +54,7 @@ pub struct PaymentIntent { pub surcharge_applicable: Option, pub request_incremental_authorization: RequestIncrementalAuthorization, pub incremental_authorization_allowed: Option, + pub authorization_count: Option, } #[derive( @@ -106,11 +107,11 @@ pub struct PaymentIntentNew { pub merchant_decision: Option, pub payment_link_id: Option, pub payment_confirm_source: Option, - pub updated_by: String, pub surcharge_applicable: Option, pub request_incremental_authorization: RequestIncrementalAuthorization, pub incremental_authorization_allowed: Option, + pub authorization_count: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -188,6 +189,12 @@ pub enum PaymentIntentUpdate { surcharge_applicable: Option, updated_by: String, }, + IncrementalAuthorizationAmountUpdate { + amount: i64, + }, + AuthorizationCountUpdate { + authorization_count: i32, + }, } #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] @@ -221,6 +228,7 @@ pub struct PaymentIntentUpdateInternal { pub updated_by: String, pub surcharge_applicable: Option, pub incremental_authorization_allowed: Option, + pub authorization_count: Option, } impl PaymentIntentUpdate { @@ -252,6 +260,7 @@ impl PaymentIntentUpdate { updated_by, surcharge_applicable, incremental_authorization_allowed, + authorization_count, } = self.into(); PaymentIntent { amount: amount.unwrap_or(source.amount), @@ -283,6 +292,7 @@ impl PaymentIntentUpdate { surcharge_applicable: surcharge_applicable.or(source.surcharge_applicable), incremental_authorization_allowed, + authorization_count, ..source } } @@ -449,6 +459,16 @@ impl From for PaymentIntentUpdateInternal { updated_by, ..Default::default() }, + PaymentIntentUpdate::IncrementalAuthorizationAmountUpdate { amount } => Self { + amount: Some(amount), + ..Default::default() + }, + PaymentIntentUpdate::AuthorizationCountUpdate { + authorization_count, + } => Self { + authorization_count: Some(authorization_count), + ..Default::default() + }, } } } diff --git a/crates/diesel_models/src/query.rs b/crates/diesel_models/src/query.rs index b0537d0a287b..3a3dee47a854 100644 --- a/crates/diesel_models/src/query.rs +++ b/crates/diesel_models/src/query.rs @@ -5,6 +5,7 @@ mod capture; pub mod cards_info; pub mod configs; +pub mod authorization; pub mod customers; pub mod dashboard_metadata; pub mod dispute; diff --git a/crates/diesel_models/src/query/authorization.rs b/crates/diesel_models/src/query/authorization.rs new file mode 100644 index 000000000000..dc9515bda55e --- /dev/null +++ b/crates/diesel_models/src/query/authorization.rs @@ -0,0 +1,79 @@ +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; +use router_env::{instrument, tracing}; + +use super::generics; +use crate::{ + authorization::{ + Authorization, AuthorizationNew, AuthorizationUpdate, AuthorizationUpdateInternal, + }, + errors, + schema::incremental_authorization::dsl, + PgPooledConn, StorageResult, +}; + +impl AuthorizationNew { + #[instrument(skip(conn))] + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl Authorization { + #[instrument(skip(conn))] + pub async fn update_by_merchant_id_authorization_id( + conn: &PgPooledConn, + merchant_id: String, + authorization_id: String, + authorization_update: AuthorizationUpdate, + ) -> StorageResult { + match generics::generic_update_with_unique_predicate_get_result::< + ::Table, + _, + _, + _, + >( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::authorization_id.eq(authorization_id.to_owned())), + AuthorizationUpdateInternal::from(authorization_update), + ) + .await + { + Err(error) => match error.current_context() { + errors::DatabaseError::NotFound => Err(error.attach_printable( + "Authorization with the given Authorization ID does not exist", + )), + errors::DatabaseError::NoFieldsToUpdate => { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::authorization_id.eq(authorization_id.to_owned())), + ) + .await + } + _ => Err(error), + }, + result => result, + } + } + + #[instrument(skip(conn))] + pub async fn find_by_merchant_id_payment_id( + conn: &PgPooledConn, + merchant_id: &str, + payment_id: &str, + ) -> StorageResult> { + generics::generic_filter::<::Table, _, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::payment_id.eq(payment_id.to_owned())), + None, + None, + Some(dsl::created_at.asc()), + ) + .await + } +} diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 13b001ecc6d1..9baf613d9233 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -362,6 +362,31 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + incremental_authorization (authorization_id, merchant_id) { + #[max_length = 64] + authorization_id -> Varchar, + #[max_length = 64] + merchant_id -> Varchar, + #[max_length = 64] + payment_id -> Varchar, + amount -> Int8, + created_at -> Timestamp, + modified_at -> Timestamp, + #[max_length = 64] + status -> Varchar, + #[max_length = 255] + error_code -> Nullable, + error_message -> Nullable, + #[max_length = 64] + connector_authorization_id -> Nullable, + previously_authorized_amount -> Int8, + } +} + diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; @@ -680,6 +705,7 @@ diesel::table! { surcharge_applicable -> Nullable, request_incremental_authorization -> RequestIncrementalAuthorization, incremental_authorization_allowed -> Nullable, + authorization_count -> Nullable, } } @@ -997,6 +1023,7 @@ diesel::allow_tables_to_appear_in_same_query!( file_metadata, fraud_check, gateway_status_map, + incremental_authorization, locker_mock_up, mandate, merchant_account, diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index f9bfcae1ca10..d529ae034a86 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -4106,6 +4106,58 @@ impl Default for super::settings::RequiredFields { ( enums::PaymentMethod::BankRedirect, PaymentMethodType(HashMap::from([ + ( + enums::PaymentMethodType::OpenBankingUk, + ConnectorFields { + fields: HashMap::from([ + ( + enums::Connector::Volt, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from([ + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "billing_first_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "billing_last_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ]), + common: HashMap::new(), + } + ), + ( + enums::Connector::Adyen, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap:: from([ + ( + "payment_method_data.bank_redirect.open_banking_uk.issuer".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.open_banking_uk.issuer".to_string(), + display_name: "issuer".to_string(), + field_type: enums::FieldType::UserBank, + value: None, + } + ) + ]), + common: HashMap::new(), + } + ) + ]), + }, + ), ( enums::PaymentMethodType::Przelewy24, ConnectorFields { @@ -4241,24 +4293,6 @@ impl Default for super::settings::RequiredFields { mandate: HashMap::new(), non_mandate: HashMap::new(), common: HashMap::from([ - ( - "billing.address.first_name".to_string(), - RequiredFieldInfo { - required_field: "billing.address.first_name".to_string(), - display_name: "billing_first_name".to_string(), - field_type: enums::FieldType::UserBillingName, - value: None, - } - ), - ( - "billing.address.last_name".to_string(), - RequiredFieldInfo { - required_field: "billing.address.last_name".to_string(), - display_name: "billing_last_name".to_string(), - field_type: enums::FieldType::UserBillingName, - value: None, - } - ), ( "email".to_string(), RequiredFieldInfo { @@ -4272,7 +4306,7 @@ impl Default for super::settings::RequiredFields { "billing.address.first_name".to_string(), RequiredFieldInfo { required_field: "billing.address.first_name".to_string(), - display_name: "card_holder_name".to_string(), + display_name: "billing_first_name".to_string(), field_type: enums::FieldType::UserBillingName, value: None, } @@ -4281,7 +4315,7 @@ impl Default for super::settings::RequiredFields { "billing.address.last_name".to_string(), RequiredFieldInfo { required_field: "billing.address.last_name".to_string(), - display_name: "card_holder_name".to_string(), + display_name: "billing_last_name".to_string(), field_type: enums::FieldType::UserBillingName, value: None, } diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index 7521189a3401..2405ba103ca6 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -217,7 +217,10 @@ where ("Host".to_string(), host.to_string().into()), ("Signature".to_string(), signature.into_masked()), ]; - if matches!(http_method, services::Method::Post | services::Method::Put) { + if matches!( + http_method, + services::Method::Post | services::Method::Put | services::Method::Patch + ) { headers.push(( "Digest".to_string(), format!("SHA-256={sha256}").into_masked(), @@ -232,6 +235,7 @@ impl api::PaymentAuthorize for Cybersource {} impl api::PaymentSync for Cybersource {} impl api::PaymentVoid for Cybersource {} impl api::PaymentCapture for Cybersource {} +impl api::PaymentIncrementalAuthorization for Cybersource {} impl api::MandateSetup for Cybersource {} impl api::ConnectorAccessToken for Cybersource {} impl api::PaymentToken for Cybersource {} @@ -951,6 +955,116 @@ impl ConnectorIntegration for Cybersource +{ + fn get_headers( + &self, + req: &types::PaymentsIncrementalAuthorizationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &types::PaymentsIncrementalAuthorizationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let connector_payment_id = req.request.connector_transaction_id.clone(); + Ok(format!( + "{}pts/v2/payments/{}", + self.base_url(connectors), + connector_payment_id + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsIncrementalAuthorizationRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let connector_router_data = cybersource::CybersourceRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.additional_amount, + req, + ))?; + let connector_request = + cybersource::CybersourcePaymentsIncrementalAuthorizationRequest::try_from( + &connector_router_data, + )?; + let cybersource_payments_incremental_authorization_request = + types::RequestBody::log_and_get_request_body( + &connector_request, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(cybersource_payments_incremental_authorization_request)) + } + fn build_request( + &self, + req: &types::PaymentsIncrementalAuthorizationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Patch) + .url(&types::IncrementalAuthorizationType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::IncrementalAuthorizationType::get_headers( + self, req, connectors, + )?) + .body(types::IncrementalAuthorizationType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + fn handle_response( + &self, + data: &types::PaymentsIncrementalAuthorizationRouterData, + res: types::Response, + ) -> CustomResult< + types::RouterData< + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + >, + errors::ConnectorError, + > { + let response: cybersource::CybersourcePaymentsIncrementalAuthorizationResponse = res + .response + .parse_struct("Cybersource PaymentResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(( + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }, + true, + )) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + fn get_error_response( + &self, + res: types::Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + #[async_trait::async_trait] impl api::IncomingWebhook for Cybersource { fn get_webhook_object_reference_id( diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 4b73296e12d8..74fe323e493e 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -77,9 +77,11 @@ impl TryFrom<&types::SetupMandateRouterData> for CybersourceZeroMandateRequest { Some(vec![CybersourceActionsTokenType::InstrumentIdentifier]), Some(CybersourceAuthorizationOptions { initiator: CybersourcePaymentInitiator { - initiator_type: CybersourcePaymentInitiatorTypes::Customer, - credential_stored_on_file: true, + initiator_type: Some(CybersourcePaymentInitiatorTypes::Customer), + credential_stored_on_file: Some(true), + stored_credential_used: None, }, + merchant_intitiated_transaction: None, }), ); @@ -158,14 +160,22 @@ pub enum CybersourceActionsTokenType { #[serde(rename_all = "camelCase")] pub struct CybersourceAuthorizationOptions { initiator: CybersourcePaymentInitiator, + merchant_intitiated_transaction: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MerchantInitiatedTransaction { + reason: String, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CybersourcePaymentInitiator { #[serde(rename = "type")] - initiator_type: CybersourcePaymentInitiatorTypes, - credential_stored_on_file: bool, + initiator_type: Option, + credential_stored_on_file: Option, + stored_credential_used: Option, } #[derive(Debug, Serialize)] @@ -229,6 +239,12 @@ pub struct OrderInformationWithBill { bill_to: Option, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct OrderInformationIncrementalAuthorization { + amount_details: AdditionalAmount, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct OrderInformation { @@ -242,6 +258,13 @@ pub struct Amount { currency: String, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AdditionalAmount { + additional_amount: String, + currency: String, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct BillTo { @@ -305,9 +328,11 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> Some(vec![CybersourceActionsTokenType::InstrumentIdentifier]), Some(CybersourceAuthorizationOptions { initiator: CybersourcePaymentInitiator { - initiator_type: CybersourcePaymentInitiatorTypes::Customer, - credential_stored_on_file: true, + initiator_type: Some(CybersourcePaymentInitiatorTypes::Customer), + credential_stored_on_file: Some(true), + stored_credential_used: None, }, + merchant_intitiated_transaction: None, }), ) } else { @@ -390,6 +415,13 @@ pub struct CybersourcePaymentsCaptureRequest { order_information: OrderInformationWithBill, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourcePaymentsIncrementalAuthorizationRequest { + processing_information: ProcessingInformation, + order_information: OrderInformationIncrementalAuthorization, +} + impl TryFrom<&CybersourceRouterData<&types::PaymentsCaptureRouterData>> for CybersourcePaymentsCaptureRequest { @@ -420,6 +452,41 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsCaptureRouterData>> } } +impl TryFrom<&CybersourceRouterData<&types::PaymentsIncrementalAuthorizationRouterData>> + for CybersourcePaymentsIncrementalAuthorizationRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &CybersourceRouterData<&types::PaymentsIncrementalAuthorizationRouterData>, + ) -> Result { + Ok(Self { + processing_information: ProcessingInformation { + action_list: None, + action_token_types: None, + authorization_options: Some(CybersourceAuthorizationOptions { + initiator: CybersourcePaymentInitiator { + initiator_type: None, + credential_stored_on_file: None, + stored_credential_used: Some(true), + }, + merchant_intitiated_transaction: Some(MerchantInitiatedTransaction { + reason: "5".to_owned(), + }), + }), + commerce_indicator: CybersourceCommerceIndicator::Internet, + capture: None, + capture_options: None, + }, + order_information: OrderInformationIncrementalAuthorization { + amount_details: AdditionalAmount { + additional_amount: item.amount.clone(), + currency: item.router_data.request.currency.to_string(), + }, + }, + }) + } +} + pub struct CybersourceAuthType { pub(super) api_key: Secret, pub(super) merchant_account: Secret, @@ -461,6 +528,14 @@ pub enum CybersourcePaymentStatus { Processing, } +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum CybersourceIncrementalAuthorizationStatus { + Authorized, + Declined, + AuthorizedPendingReview, +} + impl From for enums::AttemptStatus { fn from(item: CybersourcePaymentStatus) -> Self { match item { @@ -477,6 +552,16 @@ impl From for enums::AttemptStatus { } } +impl From for common_enums::AuthorizationStatus { + fn from(item: CybersourceIncrementalAuthorizationStatus) -> Self { + match item { + CybersourceIncrementalAuthorizationStatus::Authorized + | CybersourceIncrementalAuthorizationStatus::AuthorizedPendingReview => Self::Success, + CybersourceIncrementalAuthorizationStatus::Declined => Self::Failure, + } + } +} + impl From for enums::RefundStatus { fn from(item: CybersourcePaymentStatus) -> Self { match item { @@ -499,6 +584,13 @@ pub struct CybersourcePaymentsResponse { token_information: Option, } +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourcePaymentsIncrementalAuthorizationResponse { + status: CybersourceIncrementalAuthorizationStatus, + error_information: Option, +} + #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CybersourceSetupMandatesResponse { @@ -661,6 +753,54 @@ impl } } +impl + TryFrom<( + types::ResponseRouterData< + F, + CybersourcePaymentsIncrementalAuthorizationResponse, + T, + types::PaymentsResponseData, + >, + bool, + )> for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + data: ( + types::ResponseRouterData< + F, + CybersourcePaymentsIncrementalAuthorizationResponse, + T, + types::PaymentsResponseData, + >, + bool, + ), + ) -> Result { + let item = data.0; + Ok(Self { + response: match item.response.error_information { + Some(error) => Ok( + types::PaymentsResponseData::IncrementalAuthorizationResponse { + status: common_enums::AuthorizationStatus::Failure, + error_code: Some(error.reason), + error_message: Some(error.message), + connector_authorization_id: None, + }, + ), + _ => Ok( + types::PaymentsResponseData::IncrementalAuthorizationResponse { + status: item.response.status.into(), + error_code: None, + error_message: None, + connector_authorization_id: None, + }, + ), + }, + ..item.data + }) + } +} + #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CybersourceTransactionResponse { diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index 1049137a9470..a2dbfb1480c4 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -42,7 +42,6 @@ pub trait PaymentMethodRetrieve { key_store: &domain::MerchantKeyStore, token: &storage::PaymentTokenData, payment_intent: &PaymentIntent, - card_cvc: Option>, card_token_data: Option<&CardToken>, ) -> RouterResult>; } @@ -126,7 +125,6 @@ impl PaymentMethodRetrieve for Oss { merchant_key_store: &domain::MerchantKeyStore, token_data: &storage::PaymentTokenData, payment_intent: &PaymentIntent, - card_cvc: Option>, card_token_data: Option<&CardToken>, ) -> RouterResult> { match token_data { @@ -135,7 +133,6 @@ impl PaymentMethodRetrieve for Oss { state, &generic_token.token, payment_intent, - card_cvc, merchant_key_store, card_token_data, ) @@ -147,7 +144,6 @@ impl PaymentMethodRetrieve for Oss { state, &generic_token.token, payment_intent, - card_cvc, merchant_key_store, card_token_data, ) @@ -159,7 +155,6 @@ impl PaymentMethodRetrieve for Oss { state, &card_token.token, payment_intent, - card_cvc, card_token_data, ) .await @@ -171,7 +166,6 @@ impl PaymentMethodRetrieve for Oss { state, &card_token.token, payment_intent, - card_cvc, card_token_data, ) .await diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 044e270a7ea9..545733e298ab 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -225,12 +225,21 @@ pub async fn add_card_to_locker( ) .await .map_err(|error| { - metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); + metrics::CARD_LOCKER_FAILURES.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "basilisk"), + router_env::opentelemetry::KeyValue::new("operation", "add"), + ], + ); error }) }, &metrics::CARD_ADD_TIME, - &[], + &[router_env::opentelemetry::KeyValue::new( + "locker", "basilisk", + )], ) .await?; logger::debug!("card added to basilisk locker"); @@ -248,22 +257,45 @@ pub async fn add_card_to_locker( ) .await .map_err(|error| { - metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); + metrics::CARD_LOCKER_FAILURES.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "rust"), + router_env::opentelemetry::KeyValue::new("operation", "add"), + ], + ); error }) }, &metrics::CARD_ADD_TIME, - &[], + &[router_env::opentelemetry::KeyValue::new("locker", "rust")], ) .await; match add_card_to_rs_resp { value @ Ok(_) => { - logger::debug!("Card added successfully"); + logger::debug!("card added to rust locker"); + let _ = &metrics::CARD_LOCKER_SUCCESSFUL_RESPONSE.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "rust"), + router_env::opentelemetry::KeyValue::new("operation", "add"), + ], + ); value } Err(err) => { - logger::debug!(error =? err,"failed to add card"); + logger::debug!(error =? err,"failed to add card to rust locker"); + let _ = &metrics::CARD_LOCKER_SUCCESSFUL_RESPONSE.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "basilisk"), + router_env::opentelemetry::KeyValue::new("operation", "add"), + ], + ); Ok(add_card_to_hs_resp) } } @@ -290,12 +322,19 @@ pub async fn get_card_from_locker( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed while getting card from basilisk_hs") .map_err(|error| { - metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); + metrics::CARD_LOCKER_FAILURES.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "rust"), + router_env::opentelemetry::KeyValue::new("operation", "get"), + ], + ); error }) }, &metrics::CARD_GET_TIME, - &[], + &[router_env::opentelemetry::KeyValue::new("locker", "rust")], ) .await; @@ -313,20 +352,45 @@ pub async fn get_card_from_locker( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed while getting card from basilisk_hs") .map_err(|error| { - metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); + metrics::CARD_LOCKER_FAILURES.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "basilisk"), + router_env::opentelemetry::KeyValue::new("operation", "get"), + ], + ); error }) }, &metrics::CARD_GET_TIME, - &[], + &[router_env::opentelemetry::KeyValue::new( + "locker", "basilisk", + )], ) .await .map(|inner_card| { logger::debug!("card retrieved from basilisk locker"); + let _ = &metrics::CARD_LOCKER_SUCCESSFUL_RESPONSE.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "basilisk"), + router_env::opentelemetry::KeyValue::new("operation", "get"), + ], + ); inner_card }), Ok(_) => { logger::debug!("card retrieved from rust locker"); + let _ = &metrics::CARD_LOCKER_SUCCESSFUL_RESPONSE.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "rust"), + router_env::opentelemetry::KeyValue::new("operation", "get"), + ], + ); get_card_from_rs_locker_resp } } diff --git a/crates/router/src/core/payment_methods/surcharge_decision_configs.rs b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs index 9a65ec76f2a5..38ae71754b87 100644 --- a/crates/router/src/core/payment_methods/surcharge_decision_configs.rs +++ b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs @@ -1,12 +1,10 @@ use api_models::{ - payment_methods::{self, SurchargeDetailsResponse, SurchargeMetadata}, + payment_methods::SurchargeDetailsResponse, payments::Address, routing, - surcharge_decision_configs::{ - self, SurchargeDecisionConfigs, SurchargeDecisionManagerRecord, SurchargeDetails, - }, + surcharge_decision_configs::{self, SurchargeDecisionConfigs, SurchargeDecisionManagerRecord}, }; -use common_utils::{ext_traits::StringExt, static_cache::StaticCache}; +use common_utils::{ext_traits::StringExt, static_cache::StaticCache, types as common_utils_types}; use error_stack::{self, IntoReport, ResultExt}; use euclid::{ backend, @@ -14,7 +12,11 @@ use euclid::{ }; use router_env::{instrument, tracing}; -use crate::{core::payments::PaymentData, db::StorageInterface, types::storage as oss_storage}; +use crate::{ + core::payments::{types, PaymentData}, + db::StorageInterface, + types::{storage as oss_storage, transformers::ForeignTryFrom}, +}; static CONF_CACHE: StaticCache = StaticCache::new(); use crate::{ core::{ @@ -55,10 +57,10 @@ pub async fn perform_surcharge_decision_management_for_payment_method_list( billing_address: Option
, response_payment_method_types: &mut [api_models::payment_methods::ResponsePaymentMethodsEnabled], ) -> ConditionalConfigResult<( - SurchargeMetadata, + types::SurchargeMetadata, surcharge_decision_configs::MerchantSurchargeConfigs, )> { - let mut surcharge_metadata = SurchargeMetadata::new(payment_attempt.attempt_id.clone()); + let mut surcharge_metadata = types::SurchargeMetadata::new(payment_attempt.attempt_id.clone()); let algorithm_id = if let Some(id) = algorithm_ref.surcharge_config_algo_id { id } else { @@ -101,20 +103,27 @@ pub async fn perform_surcharge_decision_management_for_payment_method_list( Some(card_network_type.card_network.clone()); let surcharge_output = execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?; + // let surcharge_details = card_network_type.surcharge_details = surcharge_output .surcharge_details .map(|surcharge_details| { - get_surcharge_details_response(surcharge_details, payment_attempt).map( - |surcharge_details_response| { - surcharge_metadata.insert_surcharge_details( - &payment_methods_enabled.payment_method, - &payment_method_type_response.payment_method_type, - Some(&card_network_type.card_network), - surcharge_details_response.clone(), - ); - surcharge_details_response - }, - ) + let surcharge_details = get_surcharge_details_from_surcharge_output( + surcharge_details, + payment_attempt, + )?; + surcharge_metadata.insert_surcharge_details( + &payment_methods_enabled.payment_method, + &payment_method_type_response.payment_method_type, + Some(&card_network_type.card_network), + surcharge_details.clone(), + ); + SurchargeDetailsResponse::foreign_try_from(( + &surcharge_details, + payment_attempt, + )) + .into_report() + .change_context(ConfigError::DslExecutionError) + .attach_printable("Error while constructing Surcharge response type") }) .transpose()?; } @@ -124,17 +133,23 @@ pub async fn perform_surcharge_decision_management_for_payment_method_list( payment_method_type_response.surcharge_details = surcharge_output .surcharge_details .map(|surcharge_details| { - get_surcharge_details_response(surcharge_details, payment_attempt).map( - |surcharge_details_response| { - surcharge_metadata.insert_surcharge_details( - &payment_methods_enabled.payment_method, - &payment_method_type_response.payment_method_type, - None, - surcharge_details_response.clone(), - ); - surcharge_details_response - }, - ) + let surcharge_details = get_surcharge_details_from_surcharge_output( + surcharge_details, + payment_attempt, + )?; + surcharge_metadata.insert_surcharge_details( + &payment_methods_enabled.payment_method, + &payment_method_type_response.payment_method_type, + None, + surcharge_details.clone(), + ); + SurchargeDetailsResponse::foreign_try_from(( + &surcharge_details, + payment_attempt, + )) + .into_report() + .change_context(ConfigError::DslExecutionError) + .attach_printable("Error while constructing Surcharge response type") }) .transpose()?; } @@ -148,12 +163,12 @@ pub async fn perform_surcharge_decision_management_for_session_flow( algorithm_ref: routing::RoutingAlgorithmRef, payment_data: &mut PaymentData, payment_method_type_list: &Vec, -) -> ConditionalConfigResult +) -> ConditionalConfigResult where O: Send + Clone, { let mut surcharge_metadata = - SurchargeMetadata::new(payment_data.payment_attempt.attempt_id.clone()); + types::SurchargeMetadata::new(payment_data.payment_attempt.attempt_id.clone()); let algorithm_id = if let Some(id) = algorithm_ref.surcharge_config_algo_id { id } else { @@ -186,8 +201,10 @@ where let surcharge_output = execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?; if let Some(surcharge_details) = surcharge_output.surcharge_details { - let surcharge_details_response = - get_surcharge_details_response(surcharge_details, &payment_data.payment_attempt)?; + let surcharge_details_response = get_surcharge_details_from_surcharge_output( + surcharge_details, + &payment_data.payment_attempt, + )?; surcharge_metadata.insert_surcharge_details( &payment_method_type.to_owned().into(), payment_method_type, @@ -199,13 +216,13 @@ where Ok(surcharge_metadata) } -fn get_surcharge_details_response( - surcharge_details: SurchargeDetails, +fn get_surcharge_details_from_surcharge_output( + surcharge_details: surcharge_decision_configs::SurchargeDetailsOutput, payment_attempt: &oss_storage::PaymentAttempt, -) -> ConditionalConfigResult { +) -> ConditionalConfigResult { let surcharge_amount = match surcharge_details.surcharge.clone() { - surcharge_decision_configs::Surcharge::Fixed(value) => value, - surcharge_decision_configs::Surcharge::Rate(percentage) => percentage + surcharge_decision_configs::SurchargeOutput::Fixed { amount } => amount, + surcharge_decision_configs::SurchargeOutput::Rate(percentage) => percentage .apply_and_ceil_result(payment_attempt.amount) .change_context(ConfigError::DslExecutionError) .attach_printable("Failed to Calculate surcharge amount by applying percentage")?, @@ -221,13 +238,13 @@ fn get_surcharge_details_response( }) .transpose()? .unwrap_or(0); - Ok(SurchargeDetailsResponse { + Ok(types::SurchargeDetails { surcharge: match surcharge_details.surcharge { - surcharge_decision_configs::Surcharge::Fixed(surcharge_amount) => { - payment_methods::Surcharge::Fixed(surcharge_amount) + surcharge_decision_configs::SurchargeOutput::Fixed { amount } => { + common_utils_types::Surcharge::Fixed(amount) } - surcharge_decision_configs::Surcharge::Rate(percentage) => { - payment_methods::Surcharge::Rate(percentage) + surcharge_decision_configs::SurchargeOutput::Rate(percentage) => { + common_utils_types::Surcharge::Rate(percentage) } }, tax_on_surcharge: surcharge_details.tax_on_surcharge, diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 33afa29397e1..2a3ed0f1596f 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -13,12 +13,8 @@ pub mod types; use std::{fmt::Debug, marker::PhantomData, ops::Deref, time::Instant, vec::IntoIter}; -use api_models::{ - self, enums, - payment_methods::{Surcharge, SurchargeDetailsResponse}, - payments::{self, HeaderPayload}, -}; -use common_utils::{ext_traits::AsyncExt, pii}; +use api_models::{self, enums, payments::HeaderPayload}; +use common_utils::{ext_traits::AsyncExt, pii, types::Surcharge}; use data_models::mandates::MandateData; use diesel_models::{ephemeral_key, fraud_check::FraudCheck}; use error_stack::{IntoReport, ResultExt}; @@ -33,8 +29,9 @@ use scheduler::{db::process_tracker::ProcessTrackerExt, errors as sch_errors, ut use time; pub use self::operations::{ - PaymentApprove, PaymentCancel, PaymentCapture, PaymentConfirm, PaymentCreate, PaymentReject, - PaymentResponse, PaymentSession, PaymentStatus, PaymentUpdate, + PaymentApprove, PaymentCancel, PaymentCapture, PaymentConfirm, PaymentCreate, + PaymentIncrementalAuthorization, PaymentReject, PaymentResponse, PaymentSession, PaymentStatus, + PaymentUpdate, }; use self::{ conditional_configs::perform_decision_management, @@ -42,6 +39,7 @@ use self::{ helpers::get_key_params_for_surcharge_details, operations::{payment_complete_authorize, BoxedOperation, Operation}, routing::{self as self_routing, SessionFlowRoutingInput}, + types::SurchargeDetails, }; use super::{ errors::StorageErrorExt, payment_methods::surcharge_decision_configs, utils as core_utils, @@ -175,10 +173,6 @@ where let mut connector_http_status_code = None; let mut external_latency = None; if let Some(connector_details) = connector { - operation - .to_domain()? - .populate_payment_data(state, &mut payment_data, &req, &merchant_account) - .await?; payment_data = match connector_details { api::ConnectorCallType::PreDetermined(connector) => { let schedule_time = if should_add_task_to_process_tracker { @@ -405,7 +399,6 @@ where async fn populate_surcharge_details( state: &AppState, payment_data: &mut PaymentData, - request: &payments::PaymentsRequest, ) -> RouterResult<()> where F: Send + Clone, @@ -415,7 +408,7 @@ where .surcharge_applicable .unwrap_or(false) { - let payment_method_data = request + let payment_method_data = payment_data .payment_method_data .clone() .get_required_value("payment_method_data")?; @@ -436,47 +429,14 @@ where Err(err) => Err(err).change_context(errors::ApiErrorResponse::InternalServerError)?, }; - let request_surcharge_details = request.surcharge_details; - - match (request_surcharge_details, calculated_surcharge_details) { - (Some(request_surcharge_details), Some(calculated_surcharge_details)) => { - if calculated_surcharge_details - .is_request_surcharge_matching(request_surcharge_details) - { - payment_data.surcharge_details = Some(calculated_surcharge_details); - } else { - return Err(errors::ApiErrorResponse::InvalidRequestData { - message: "Invalid value provided: 'surcharge_details'. surcharge details provided do not match with surcharge details sent in payment_methods list response".to_string(), - } - .into()); - } - } - (None, Some(_calculated_surcharge_details)) => { - return Err(errors::ApiErrorResponse::MissingRequiredField { - field_name: "surcharge_details", - } - .into()); - } - (Some(request_surcharge_details), None) => { - if request_surcharge_details.is_surcharge_zero() { - return Ok(()); - } else { - return Err(errors::ApiErrorResponse::InvalidRequestData { - message: "Invalid value provided: 'surcharge_details'. surcharge details provided do not match with surcharge details sent in payment_methods list response".to_string(), - } - .into()); - } - } - (None, None) => return Ok(()), - }; + payment_data.surcharge_details = calculated_surcharge_details; } else { let surcharge_details = payment_data .payment_attempt .get_surcharge_details() .map(|surcharge_details| { - surcharge_details - .get_surcharge_details_object(payment_data.payment_attempt.amount) + SurchargeDetails::from((&surcharge_details, &payment_data.payment_attempt)) }); payment_data.surcharge_details = surcharge_details; } @@ -509,7 +469,7 @@ where let final_amount = payment_data.payment_attempt.amount + surcharge_amount + tax_on_surcharge_amount; Ok(Some(api::SessionSurchargeDetails::PreDetermined( - SurchargeDetailsResponse { + SurchargeDetails { surcharge: Surcharge::Fixed(surcharge_amount), tax_on_surcharge: None, surcharge_amount, @@ -978,6 +938,10 @@ where payment_data, ) .await?; + operation + .to_domain()? + .populate_payment_data(state, payment_data, merchant_account) + .await?; let mut router_data = payment_data .construct_router_data( @@ -1882,9 +1846,19 @@ where pub recurring_mandate_payment_data: Option, pub ephemeral_key: Option, pub redirect_response: Option, - pub surcharge_details: Option, + pub surcharge_details: Option, pub frm_message: Option, pub payment_link_data: Option, + pub incremental_authorization_details: Option, + pub authorizations: Vec, +} + +#[derive(Debug, Default, Clone)] +pub struct IncrementalAuthorizationDetails { + pub additional_amount: i64, + pub total_amount: i64, + pub reason: Option, + pub authorization_id: Option, } #[derive(Debug, Default, Clone)] @@ -1985,6 +1959,10 @@ pub fn should_call_connector( "CompleteAuthorize" => true, "PaymentApprove" => true, "PaymentSession" => true, + "PaymentIncrementalAuthorization" => matches!( + payment_data.payment_intent.status, + storage_enums::IntentStatus::RequiresCapture + ), _ => false, } } diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 9be6f5905b8b..94b8bc1ff5d4 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -3,6 +3,7 @@ pub mod authorize_flow; pub mod cancel_flow; pub mod capture_flow; pub mod complete_authorize_flow; +pub mod incremental_authorization_flow; pub mod psync_flow; pub mod reject_flow; pub mod session_flow; @@ -1577,3 +1578,82 @@ default_imp_for_reject!( connector::Worldpay, connector::Zen ); + +macro_rules! default_imp_for_incremental_authorization { + ($($path:ident::$connector:ident),*) => { + $( + impl api::PaymentIncrementalAuthorization for $path::$connector {} + impl + services::ConnectorIntegration< + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + > for $path::$connector + {} + )* + }; +} + +#[cfg(feature = "dummy_connector")] +impl api::PaymentIncrementalAuthorization for connector::DummyConnector {} +#[cfg(feature = "dummy_connector")] +impl + services::ConnectorIntegration< + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + > for connector::DummyConnector +{ +} + +default_imp_for_incremental_authorization!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Authorizedotnet, + connector::Bambora, + connector::Bankofamerica, + connector::Bitpay, + connector::Bluesnap, + connector::Boku, + connector::Braintree, + connector::Cashtocode, + connector::Checkout, + connector::Cryptopay, + connector::Coinbase, + connector::Dlocal, + connector::Fiserv, + connector::Forte, + connector::Globalpay, + connector::Globepay, + connector::Gocardless, + connector::Helcim, + connector::Iatapay, + connector::Klarna, + connector::Mollie, + connector::Multisafepay, + connector::Nexinets, + connector::Nmi, + connector::Noon, + connector::Nuvei, + connector::Opayo, + connector::Opennode, + connector::Payeezy, + connector::Payme, + connector::Paypal, + connector::Payu, + connector::Powertranz, + connector::Prophetpay, + connector::Rapyd, + connector::Square, + connector::Stax, + connector::Stripe, + connector::Shift4, + connector::Trustpay, + connector::Tsys, + connector::Volt, + connector::Wise, + connector::Worldline, + connector::Worldpay, + connector::Zen +); diff --git a/crates/router/src/core/payments/flows/incremental_authorization_flow.rs b/crates/router/src/core/payments/flows/incremental_authorization_flow.rs new file mode 100644 index 000000000000..387916bab7c9 --- /dev/null +++ b/crates/router/src/core/payments/flows/incremental_authorization_flow.rs @@ -0,0 +1,118 @@ +use async_trait::async_trait; + +use super::ConstructFlowSpecificData; +use crate::{ + core::{ + errors::{ConnectorErrorExt, RouterResult}, + payments::{self, access_token, helpers, transformers, Feature, PaymentData}, + }, + routes::AppState, + services, + types::{self, api, domain}, +}; + +#[async_trait] +impl + ConstructFlowSpecificData< + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + > for PaymentData +{ + async fn construct_router_data<'a>( + &self, + state: &AppState, + connector_id: &str, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + customer: &Option, + merchant_connector_account: &helpers::MerchantConnectorAccountType, + ) -> RouterResult { + Box::pin(transformers::construct_payment_router_data::< + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + >( + state, + self.clone(), + connector_id, + merchant_account, + key_store, + customer, + merchant_connector_account, + )) + .await + } +} + +#[async_trait] +impl Feature + for types::RouterData< + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + > +{ + async fn decide_flows<'a>( + self, + state: &AppState, + connector: &api::ConnectorData, + _customer: &Option, + call_connector_action: payments::CallConnectorAction, + _merchant_account: &domain::MerchantAccount, + connector_request: Option, + _key_store: &domain::MerchantKeyStore, + ) -> RouterResult { + let connector_integration: services::BoxedConnectorIntegration< + '_, + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + > = connector.connector.get_connector_integration(); + + let resp = services::execute_connector_processing_step( + state, + connector_integration, + &self, + call_connector_action, + connector_request, + ) + .await + .to_payment_failed_response()?; + + Ok(resp) + } + + async fn add_access_token<'a>( + &self, + state: &AppState, + connector: &api::ConnectorData, + merchant_account: &domain::MerchantAccount, + ) -> RouterResult { + access_token::add_access_token(state, connector, merchant_account, self).await + } + + async fn build_flow_specific_connector_request( + &mut self, + state: &AppState, + connector: &api::ConnectorData, + call_connector_action: payments::CallConnectorAction, + ) -> RouterResult<(Option, bool)> { + let request = match call_connector_action { + payments::CallConnectorAction::Trigger => { + let connector_integration: services::BoxedConnectorIntegration< + '_, + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + > = connector.connector.get_connector_integration(); + + connector_integration + .build_request(self, &state.conf.connectors) + .to_payment_failed_response()? + } + _ => None, + }; + + Ok((request, true)) + } +} diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 4d11f6400f44..c9ab77c6a332 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use api_models::payments::{CardToken, GetPaymentMethodType}; +use api_models::payments::{CardToken, GetPaymentMethodType, RequestSurchargeDetails}; use base64::Engine; use common_utils::{ ext_traits::{AsyncExt, ByteSliceExt, ValueExt}, @@ -572,6 +572,7 @@ pub fn validate_merchant_id( pub fn validate_request_amount_and_amount_to_capture( op_amount: Option, op_amount_to_capture: Option, + surcharge_details: Option, ) -> CustomResult<(), errors::ApiErrorResponse> { match (op_amount, op_amount_to_capture) { (None, _) => Ok(()), @@ -581,7 +582,11 @@ pub fn validate_request_amount_and_amount_to_capture( api::Amount::Value(amount_inner) => { // If both amount and amount to capture is present // then amount to be capture should be less than or equal to request amount - utils::when(!amount_to_capture.le(&amount_inner.get()), || { + let total_capturable_amount = amount_inner.get() + + surcharge_details + .map(|surcharge_details| surcharge_details.get_total_surcharge_amount()) + .unwrap_or(0); + utils::when(!amount_to_capture.le(&total_capturable_amount), || { Err(report!(errors::ApiErrorResponse::PreconditionFailed { message: format!( "amount_to_capture is greater than amount capture_amount: {amount_to_capture:?} request_amount: {amount:?}" @@ -603,13 +608,34 @@ pub fn validate_request_amount_and_amount_to_capture( /// if capture method = automatic, amount_to_capture(if provided) must be equal to amount #[instrument(skip_all)] -pub fn validate_amount_to_capture_in_create_call_request( +pub fn validate_amount_to_capture_and_capture_method( + payment_attempt: Option<&PaymentAttempt>, request: &api_models::payments::PaymentsRequest, ) -> CustomResult<(), errors::ApiErrorResponse> { - if request.capture_method.unwrap_or_default() == api_enums::CaptureMethod::Automatic { - let total_capturable_amount = request.get_total_capturable_amount(); - if let Some((amount_to_capture, total_capturable_amount)) = - request.amount_to_capture.zip(total_capturable_amount) + let capture_method = request + .capture_method + .or(payment_attempt + .map(|payment_attempt| payment_attempt.capture_method.unwrap_or_default())) + .unwrap_or_default(); + if capture_method == api_enums::CaptureMethod::Automatic { + let original_amount = request + .amount + .map(|amount| amount.into()) + .or(payment_attempt.map(|payment_attempt| payment_attempt.amount)); + let surcharge_amount = request + .surcharge_details + .map(|surcharge_details| surcharge_details.get_total_surcharge_amount()) + .or_else(|| { + payment_attempt.map(|payment_attempt| { + payment_attempt.surcharge_amount.unwrap_or(0) + + payment_attempt.tax_amount.unwrap_or(0) + }) + }) + .unwrap_or(0); + let total_capturable_amount = + original_amount.map(|original_amount| original_amount + surcharge_amount); + if let Some((total_capturable_amount, amount_to_capture)) = + total_capturable_amount.zip(request.amount_to_capture) { utils::when(amount_to_capture != total_capturable_amount, || { Err(report!(errors::ApiErrorResponse::PreconditionFailed { @@ -1354,7 +1380,6 @@ pub async fn retrieve_payment_method_with_temporary_token( state: &AppState, token: &str, payment_intent: &PaymentIntent, - card_cvc: Option>, merchant_key_store: &domain::MerchantKeyStore, card_token_data: Option<&CardToken>, ) -> RouterResult> { @@ -1395,10 +1420,13 @@ pub async fn retrieve_payment_method_with_temporary_token( updated_card.card_holder_name = name_on_card; } - if let Some(cvc) = card_cvc { - is_card_updated = true; - updated_card.card_cvc = cvc; + if let Some(token_data) = card_token_data { + if let Some(cvc) = token_data.card_cvc.clone() { + is_card_updated = true; + updated_card.card_cvc = cvc; + } } + if is_card_updated { let updated_pm = api::PaymentMethodData::Card(updated_card); vault::Vault::store_payment_method_data_in_locker( @@ -1444,7 +1472,6 @@ pub async fn retrieve_card_with_permanent_token( state: &AppState, token: &str, payment_intent: &PaymentIntent, - card_cvc: Option>, card_token_data: Option<&CardToken>, ) -> RouterResult { let customer_id = payment_intent @@ -1479,7 +1506,11 @@ pub async fn retrieve_card_with_permanent_token( card_holder_name: name_on_card.unwrap_or(masking::Secret::from("".to_string())), card_exp_month: card.card_exp_month, card_exp_year: card.card_exp_year, - card_cvc: card_cvc.unwrap_or_default(), + card_cvc: card_token_data + .cloned() + .unwrap_or_default() + .card_cvc + .unwrap_or_default(), card_issuer: card.card_brand, nick_name: card.nick_name.map(masking::Secret::new), card_network: None, @@ -1501,6 +1532,22 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( Option, )> { let request = &payment_data.payment_method_data.clone(); + + let mut card_token_data = payment_data + .payment_method_data + .clone() + .and_then(|pmd| match pmd { + api_models::payments::PaymentMethodData::CardToken(token_data) => Some(token_data), + _ => None, + }) + .or(Some(CardToken::default())); + + if let Some(cvc) = payment_data.card_cvc.clone() { + if let Some(token_data) = card_token_data.as_mut() { + token_data.card_cvc = Some(cvc); + } + } + let token = payment_data.token.clone(); let hyperswitch_token = match payment_data.mandate_id { @@ -1560,13 +1607,6 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( } }; - let card_cvc = payment_data.card_cvc.clone(); - - let card_token_data = request.as_ref().and_then(|pmd| match pmd { - api_models::payments::PaymentMethodData::CardToken(token_data) => Some(token_data), - _ => None, - }); - // TODO: Handle case where payment method and token both are present in request properly. let payment_method = match (request, hyperswitch_token) { (_, Some(hyperswitch_token)) => { @@ -1575,8 +1615,7 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( merchant_key_store, &hyperswitch_token, &payment_data.payment_intent, - card_cvc, - card_token_data, + card_token_data.as_ref(), ) .await .attach_printable("in 'make_pm_data'")?; @@ -2378,6 +2417,20 @@ pub async fn get_merchant_fullfillment_time( } } +pub(crate) fn validate_payment_status_against_allowed_statuses( + intent_status: &storage_enums::IntentStatus, + allowed_statuses: &[storage_enums::IntentStatus], + action: &'static str, +) -> Result<(), errors::ApiErrorResponse> { + fp_utils::when(!allowed_statuses.contains(intent_status), || { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: format!( + "You cannot {action} this payment because it has status {intent_status}", + ), + }) + }) +} + pub(crate) fn validate_payment_status_against_not_allowed_statuses( intent_status: &storage_enums::IntentStatus, not_allowed_statuses: &[storage_enums::IntentStatus], @@ -2573,6 +2626,7 @@ mod tests { request_incremental_authorization: common_enums::RequestIncrementalAuthorization::default(), incremental_authorization_allowed: None, + authorization_count: None, }; let req_cs = Some("1".to_string()); let merchant_fulfillment_time = Some(900); @@ -2626,6 +2680,7 @@ mod tests { request_incremental_authorization: common_enums::RequestIncrementalAuthorization::default(), incremental_authorization_allowed: None, + authorization_count: None, }; let req_cs = Some("1".to_string()); let merchant_fulfillment_time = Some(10); @@ -2679,6 +2734,7 @@ mod tests { request_incremental_authorization: common_enums::RequestIncrementalAuthorization::default(), incremental_authorization_allowed: None, + authorization_count: None, }; let req_cs = Some("1".to_string()); let merchant_fulfillment_time = Some(10); @@ -3578,31 +3634,16 @@ pub fn get_key_params_for_surcharge_details( )> { match payment_method_data { api_models::payments::PaymentMethodData::Card(card) => { - let card_type = card - .card_type - .get_required_value("payment_method_data.card.card_type")?; let card_network = card .card_network .get_required_value("payment_method_data.card.card_network")?; - match card_type.to_lowercase().as_str() { - "credit" => Ok(( - common_enums::PaymentMethod::Card, - common_enums::PaymentMethodType::Credit, - Some(card_network), - )), - "debit" => Ok(( - common_enums::PaymentMethod::Card, - common_enums::PaymentMethodType::Debit, - Some(card_network), - )), - _ => { - logger::debug!("Invalid Card type found in payment confirm call, hence surcharge not applicable"); - Err(errors::ApiErrorResponse::InvalidDataValue { - field_name: "payment_method_data.card.card_type", - } - .into()) - } - } + // surcharge generated will always be same for credit as well as debit + // since surcharge conditions cannot be defined on card_type + Ok(( + common_enums::PaymentMethod::Card, + common_enums::PaymentMethodType::Credit, + Some(card_network), + )) } api_models::payments::PaymentMethodData::CardRedirect(card_redirect_data) => Ok(( common_enums::PaymentMethod::CardRedirect, diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index 809c9e925de0..cf0c0ab294a8 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -10,6 +10,7 @@ pub mod payment_session; pub mod payment_start; pub mod payment_status; pub mod payment_update; +pub mod payments_incremental_authorization; use api_models::enums::FrmSuggestion; use async_trait::async_trait; @@ -22,6 +23,7 @@ pub use self::{ payment_create::PaymentCreate, payment_reject::PaymentReject, payment_response::PaymentResponse, payment_session::PaymentSession, payment_start::PaymentStart, payment_status::PaymentStatus, payment_update::PaymentUpdate, + payments_incremental_authorization::PaymentIncrementalAuthorization, }; use super::{helpers, CustomerDetails, PaymentData}; use crate::{ @@ -157,7 +159,6 @@ pub trait Domain: Send + Sync { &'a self, _state: &AppState, _payment_data: &mut PaymentData, - _request: &R, _merchant_account: &domain::MerchantAccount, ) -> CustomResult<(), errors::ApiErrorResponse> { Ok(()) diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index f51d7a93ee5e..fee0326df03c 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -251,6 +251,8 @@ impl surcharge_details: None, frm_message: frm_response.ok(), payment_link_data: None, + incremental_authorization_details: None, + authorizations: vec![], }; let customer_details = Some(CustomerDetails { diff --git a/crates/router/src/core/payments/operations/payment_cancel.rs b/crates/router/src/core/payments/operations/payment_cancel.rs index ae7810971896..7c8fbcc34979 100644 --- a/crates/router/src/core/payments/operations/payment_cancel.rs +++ b/crates/router/src/core/payments/operations/payment_cancel.rs @@ -171,6 +171,8 @@ impl surcharge_details: None, frm_message: None, payment_link_data: None, + incremental_authorization_details: None, + authorizations: vec![], }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index 5b89cfdbcf0b..65b91f0401cf 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -215,6 +215,8 @@ impl surcharge_details: None, frm_message: None, payment_link_data: None, + incremental_authorization_details: None, + authorizations: vec![], }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index 8b264edbb3d1..17b71172a349 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -245,6 +245,8 @@ impl surcharge_details: None, frm_message: None, payment_link_data: None, + incremental_authorization_details: None, + authorizations: vec![], }; let customer_details = Some(CustomerDetails { diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index d718db79a6d0..578395860c9a 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -446,6 +446,21 @@ impl ) .await?; + let additional_pm_data = request + .payment_method_data + .as_ref() + .async_map(|payment_method_data| async { + helpers::get_additional_payment_data(payment_method_data, &*state.store).await + }) + .await; + let payment_method_data_after_card_bin_call = request + .payment_method_data + .as_ref() + .zip(additional_pm_data) + .map(|(payment_method_data, additional_payment_data)| { + payment_method_data.apply_additional_payment_data(additional_payment_data) + }); + let payment_data = PaymentData { flow: PhantomData, payment_intent, @@ -462,7 +477,7 @@ impl billing: billing_address.as_ref().map(|a| a.into()), }, confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), + payment_method_data: payment_method_data_after_card_bin_call, force_sync: None, refunds: vec![], disputes: vec![], @@ -479,6 +494,8 @@ impl surcharge_details: None, frm_message: None, payment_link_data: None, + incremental_authorization_details: None, + authorizations: vec![], }; let get_trackers_response = operations::GetTrackerResponse { @@ -591,10 +608,9 @@ impl Domain, - request: &api::PaymentsRequest, _merchant_account: &domain::MerchantAccount, ) -> CustomResult<(), errors::ApiErrorResponse> { - populate_surcharge_details(state, payment_data, request).await + populate_surcharge_details(state, payment_data).await } } diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index ac387076d1d1..287e4945951b 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -167,7 +167,7 @@ impl ) .await?; - let payment_attempt_new = Self::make_payment_attempt( + let (payment_attempt_new, additional_payment_data) = Self::make_payment_attempt( &payment_id, merchant_id, money, @@ -286,10 +286,18 @@ impl // The operation merges mandate data from both request and payment_attempt let setup_mandate = setup_mandate.map(MandateData::from); - let surcharge_details = request.surcharge_details.map(|surcharge_details| { - surcharge_details.get_surcharge_details_object(payment_attempt.amount) + let surcharge_details = request.surcharge_details.map(|request_surcharge_details| { + payments::SurchargeDetails::from((&request_surcharge_details, &payment_attempt)) }); + let payment_method_data_after_card_bin_call = request + .payment_method_data + .as_ref() + .zip(additional_payment_data) + .map(|(payment_method_data, additional_payment_data)| { + payment_method_data.apply_additional_payment_data(additional_payment_data) + }); + let payment_data = PaymentData { flow: PhantomData, payment_intent, @@ -306,7 +314,7 @@ impl billing: billing_address.as_ref().map(|a| a.into()), }, confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), + payment_method_data: payment_method_data_after_card_bin_call, refunds: vec![], disputes: vec![], attempts: None, @@ -323,6 +331,8 @@ impl surcharge_details, frm_message: None, payment_link_data, + incremental_authorization_details: None, + authorizations: vec![], }; let get_trackers_response = operations::GetTrackerResponse { @@ -540,14 +550,14 @@ impl ValidateRequest, state: &AppState, - ) -> RouterResult { + ) -> RouterResult<( + storage::PaymentAttemptNew, + Option, + )> { let created_at @ modified_at @ last_synced = Some(common_utils::date_time::now()); let status = helpers::payment_attempt_status_fsm(&request.payment_method_data, request.confirm); @@ -614,7 +627,8 @@ impl PaymentCreate { .async_map(|payment_method_data| async { helpers::get_additional_payment_data(payment_method_data, &*state.store).await }) - .await + .await; + let additional_pm_data_value = additional_pm_data .as_ref() .map(Encode::::encode_to_value) .transpose() @@ -629,35 +643,38 @@ impl PaymentCreate { utils::get_payment_attempt_id(payment_id, 1) }; - Ok(storage::PaymentAttemptNew { - payment_id: payment_id.to_string(), - merchant_id: merchant_id.to_string(), - attempt_id, - status, - currency, - amount: amount.into(), - payment_method, - capture_method: request.capture_method, - capture_on: request.capture_on, - confirm: request.confirm.unwrap_or(false), - created_at, - modified_at, - last_synced, - authentication_type: request.authentication_type, - browser_info, - payment_experience: request.payment_experience, - payment_method_type, - payment_method_data: additional_pm_data, - amount_to_capture: request.amount_to_capture, - payment_token: request.payment_token.clone(), - mandate_id: request.mandate_id.clone(), - business_sub_label: request.business_sub_label.clone(), - mandate_details: request - .mandate_data - .as_ref() - .and_then(|inner| inner.mandate_type.clone().map(Into::into)), - ..storage::PaymentAttemptNew::default() - }) + Ok(( + storage::PaymentAttemptNew { + payment_id: payment_id.to_string(), + merchant_id: merchant_id.to_string(), + attempt_id, + status, + currency, + amount: amount.into(), + payment_method, + capture_method: request.capture_method, + capture_on: request.capture_on, + confirm: request.confirm.unwrap_or(false), + created_at, + modified_at, + last_synced, + authentication_type: request.authentication_type, + browser_info, + payment_experience: request.payment_experience, + payment_method_type, + payment_method_data: additional_pm_data_value, + amount_to_capture: request.amount_to_capture, + payment_token: request.payment_token.clone(), + mandate_id: request.mandate_id.clone(), + business_sub_label: request.business_sub_label.clone(), + mandate_details: request + .mandate_data + .as_ref() + .and_then(|inner| inner.mandate_type.clone().map(Into::into)), + ..storage::PaymentAttemptNew::default() + }, + additional_pm_data, + )) } #[instrument(skip_all)] @@ -757,6 +774,7 @@ impl PaymentCreate { updated_by: merchant_account.storage_scheme.to_string(), request_incremental_authorization, incremental_authorization_allowed: None, + authorization_count: None, }) } diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index ae606187a0a1..03bf6dd46b60 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -158,6 +158,8 @@ impl surcharge_details: None, frm_message: frm_response.ok(), payment_link_data: None, + incremental_authorization_details: None, + authorizations: vec![], }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 9781ad651ee2..f92487d74a7b 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -1,8 +1,9 @@ use std::collections::HashMap; use async_trait::async_trait; +use common_enums::AuthorizationStatus; use data_models::payments::payment_attempt::PaymentAttempt; -use error_stack::ResultExt; +use error_stack::{report, IntoReport, ResultExt}; use futures::FutureExt; use router_derive; use router_env::{instrument, tracing}; @@ -36,7 +37,7 @@ use crate::{ #[derive(Debug, Clone, Copy, router_derive::PaymentOperation)] #[operation( operations = "post_update_tracker", - flow = "sync_data, authorize_data, cancel_data, capture_data, complete_authorize_data, approve_data, reject_data, setup_mandate_data, session_data" + flow = "sync_data, authorize_data, cancel_data, capture_data, complete_authorize_data, approve_data, reject_data, setup_mandate_data, session_data,incremental_authorization_data" )] pub struct PaymentResponse; @@ -76,6 +77,138 @@ impl PostUpdateTracker, types::PaymentsAuthorizeData } } +#[async_trait] +impl PostUpdateTracker, types::PaymentsIncrementalAuthorizationData> + for PaymentResponse +{ + async fn update_tracker<'b>( + &'b self, + db: &'b AppState, + _payment_id: &api::PaymentIdType, + mut payment_data: PaymentData, + router_data: types::RouterData< + F, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + >, + storage_scheme: enums::MerchantStorageScheme, + ) -> RouterResult> + where + F: 'b + Send, + { + let incremental_authorization_details = payment_data + .incremental_authorization_details + .clone() + .ok_or_else(|| { + report!(errors::ApiErrorResponse::InternalServerError) + .attach_printable("missing incremental_authorization_details in payment_data") + })?; + // Update payment_intent and payment_attempt 'amount' if incremental_authorization is successful + let (option_payment_attempt_update, option_payment_intent_update) = + match router_data.response.clone() { + Err(_) => (None, None), + Ok(types::PaymentsResponseData::IncrementalAuthorizationResponse { + status, + .. + }) => { + if status == AuthorizationStatus::Success { + (Some( + storage::PaymentAttemptUpdate::IncrementalAuthorizationAmountUpdate { + amount: incremental_authorization_details.total_amount, + amount_capturable: incremental_authorization_details.total_amount, + }, + ), Some( + storage::PaymentIntentUpdate::IncrementalAuthorizationAmountUpdate { + amount: incremental_authorization_details.total_amount, + }, + )) + } else { + (None, None) + } + } + _ => Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("unexpected response in incremental_authorization flow")?, + }; + //payment_attempt update + if let Some(payment_attempt_update) = option_payment_attempt_update { + payment_data.payment_attempt = db + .store + .update_payment_attempt_with_attempt_id( + payment_data.payment_attempt.clone(), + payment_attempt_update, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + } + // payment_intent update + if let Some(payment_intent_update) = option_payment_intent_update { + payment_data.payment_intent = db + .store + .update_payment_intent( + payment_data.payment_intent.clone(), + payment_intent_update, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + } + // Update the status of authorization record + let authorization_update = match &router_data.response { + Err(res) => Ok(storage::AuthorizationUpdate::StatusUpdate { + status: AuthorizationStatus::Failure, + error_code: Some(res.code.clone()), + error_message: Some(res.message.clone()), + connector_authorization_id: None, + }), + Ok(types::PaymentsResponseData::IncrementalAuthorizationResponse { + status, + error_code, + error_message, + connector_authorization_id, + }) => Ok(storage::AuthorizationUpdate::StatusUpdate { + status: status.clone(), + error_code: error_code.clone(), + error_message: error_message.clone(), + connector_authorization_id: connector_authorization_id.clone(), + }), + Ok(_) => Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("unexpected response in incremental_authorization flow"), + }?; + let authorization_id = incremental_authorization_details + .authorization_id + .clone() + .ok_or( + report!(errors::ApiErrorResponse::InternalServerError).attach_printable( + "missing authorization_id in incremental_authorization_details in payment_data", + ), + )?; + db.store + .update_authorization_by_merchant_id_authorization_id( + router_data.merchant_id.clone(), + authorization_id, + authorization_update, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed while updating authorization")?; + //Fetch all the authorizations of the payment and send in incremental authorization response + let authorizations = db + .store + .find_all_authorizations_by_merchant_id_payment_id( + &router_data.merchant_id, + &payment_data.payment_intent.payment_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed while retrieving authorizations")?; + payment_data.authorizations = authorizations; + Ok(payment_data) + } +} + #[async_trait] impl PostUpdateTracker, types::PaymentsSyncData> for PaymentResponse { async fn update_tracker<'b>( @@ -544,6 +677,7 @@ async fn payment_response_update_tracker( types::PaymentsResponseData::TokenizationResponse { .. } => (None, None), types::PaymentsResponseData::ConnectorCustomerResponse { .. } => (None, None), types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. } => (None, None), + types::PaymentsResponseData::IncrementalAuthorizationResponse { .. } => (None, None), types::PaymentsResponseData::MultipleCaptureResponse { capture_sync_response_list, } => match payment_data.multiple_capture_data { @@ -637,6 +771,7 @@ async fn payment_response_update_tracker( payment_data.payment_attempt.status, ), updated_by: storage_scheme.to_string(), + // make this false only if initial payment fails, if incremental authorization call fails don't make it false incremental_authorization_allowed: Some(false), }, Ok(_) => storage::PaymentIntentUpdate::ResponseUpdate { diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index 6097a5e430ce..572bc710b963 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -195,6 +195,8 @@ impl surcharge_details: None, frm_message: None, payment_link_data: None, + incremental_authorization_details: None, + authorizations: vec![], }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 3a4ae2c2e0de..887edd030d13 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -169,6 +169,8 @@ impl surcharge_details: None, frm_message: None, payment_link_data: None, + incremental_authorization_details: None, + authorizations: vec![], }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index d0cd4b32d3c2..0320cf50663e 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -314,6 +314,20 @@ async fn get_tracker_for_sync< ) })?; + let authorizations = db + .find_all_authorizations_by_merchant_id_payment_id( + &merchant_account.merchant_id, + &payment_id_str, + ) + .await + .change_context(errors::ApiErrorResponse::PaymentNotFound) + .attach_printable_lazy(|| { + format!( + "Failed while getting authorizations list for, payment_id: {}, merchant_id: {}", + &payment_id_str, merchant_account.merchant_id + ) + })?; + let disputes = db .find_disputes_by_merchant_id_payment_id(&merchant_account.merchant_id, &payment_id_str) .await @@ -407,6 +421,8 @@ async fn get_tracker_for_sync< payment_link_data: None, surcharge_details: None, frm_message: frm_response.ok(), + incremental_authorization_details: None, + authorizations, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 1176eeb1dd3f..e1c373171682 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -21,7 +21,7 @@ use crate::{ types::{ api::{self, PaymentIdTypeExt}, domain, - storage::{self, enums as storage_enums}, + storage::{self, enums as storage_enums, payment_attempt::PaymentAttemptExt}, }, utils::OptionExt, }; @@ -80,6 +80,7 @@ impl &[ storage_enums::IntentStatus::Failed, storage_enums::IntentStatus::Succeeded, + storage_enums::IntentStatus::PartiallyCaptured, storage_enums::IntentStatus::RequiresCapture, ], "update", @@ -134,6 +135,20 @@ impl .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + helpers::validate_amount_to_capture_and_capture_method(Some(&payment_attempt), request)?; + + helpers::validate_request_amount_and_amount_to_capture( + request.amount, + request.amount_to_capture, + request + .surcharge_details + .or(payment_attempt.get_surcharge_details()), + ) + .change_context(errors::ApiErrorResponse::InvalidDataFormat { + field_name: "amount_to_capture".to_string(), + expected_format: "amount_to_capture lesser than or equal to amount".to_string(), + })?; + currency = request .currency .or(payment_attempt.currency) @@ -322,7 +337,7 @@ impl })?; let surcharge_details = request.surcharge_details.map(|request_surcharge_details| { - request_surcharge_details.get_surcharge_details_object(payment_attempt.amount) + payments::SurchargeDetails::from((&request_surcharge_details, &payment_attempt)) }); let payment_data = PaymentData { @@ -358,6 +373,8 @@ impl surcharge_details, frm_message: None, payment_link_data: None, + incremental_authorization_details: None, + authorizations: vec![], }; let get_trackers_response = operations::GetTrackerResponse { @@ -629,6 +646,7 @@ impl ValidateRequest + GetTracker, PaymentsIncrementalAuthorizationRequest, Ctx> + for PaymentIncrementalAuthorization +{ + #[instrument(skip_all)] + async fn get_trackers<'a>( + &'a self, + state: &'a AppState, + payment_id: &api::PaymentIdType, + request: &PaymentsIncrementalAuthorizationRequest, + _mandate_type: Option, + merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + _auth_flow: services::AuthFlow, + ) -> RouterResult< + operations::GetTrackerResponse<'a, F, PaymentsIncrementalAuthorizationRequest, Ctx>, + > { + let db = &*state.store; + let merchant_id = &merchant_account.merchant_id; + let storage_scheme = merchant_account.storage_scheme; + let payment_id = payment_id + .get_payment_intent_id() + .change_context(errors::ApiErrorResponse::PaymentNotFound)?; + + let payment_intent = db + .find_payment_intent_by_payment_id_merchant_id(&payment_id, merchant_id, storage_scheme) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + helpers::validate_payment_status_against_allowed_statuses( + &payment_intent.status, + &[enums::IntentStatus::RequiresCapture], + "increment authorization", + )?; + + if payment_intent.incremental_authorization_allowed != Some(true) { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: + "You cannot increment authorization this payment because it is not allowed for incremental_authorization".to_owned(), + })? + } + + if request.amount < payment_intent.amount { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Amount should be greater than original authorized amount".to_owned(), + })? + } + + let attempt_id = payment_intent.active_attempt.get_id().clone(); + let payment_attempt = db + .find_payment_attempt_by_payment_id_merchant_id_attempt_id( + payment_intent.payment_id.as_str(), + merchant_id, + attempt_id.clone().as_str(), + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + let currency = payment_attempt.currency.get_required_value("currency")?; + let amount = payment_attempt.amount; + + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = state + .store + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = payments::PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount: amount.into(), + email: None, + mandate_id: None, + mandate_connector: None, + setup_mandate: None, + token: None, + address: PaymentAddress { + billing: None, + shipping: None, + }, + confirm: None, + payment_method_data: None, + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: None, + creds_identifier: None, + 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, + incremental_authorization_details: Some(IncrementalAuthorizationDetails { + additional_amount: request.amount - amount, + total_amount: request.amount, + reason: request.reason.clone(), + authorization_id: None, + }), + authorizations: vec![], + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: None, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) + } +} + +#[async_trait] +impl + UpdateTracker, PaymentsIncrementalAuthorizationRequest, Ctx> + for PaymentIncrementalAuthorization +{ + #[instrument(skip_all)] + async fn update_trackers<'b>( + &'b self, + db: &'b AppState, + mut payment_data: payments::PaymentData, + _customer: Option, + storage_scheme: enums::MerchantStorageScheme, + _updated_customer: Option, + _mechant_key_store: &domain::MerchantKeyStore, + _frm_suggestion: Option, + _header_payload: api::HeaderPayload, + ) -> RouterResult<( + BoxedOperation<'b, F, PaymentsIncrementalAuthorizationRequest, Ctx>, + payments::PaymentData, + )> + where + F: 'b + Send, + { + let new_authorization_count = payment_data + .payment_intent + .authorization_count + .map(|count| count + 1) + .unwrap_or(1); + // Create new authorization record + let authorization_new = AuthorizationNew { + authorization_id: format!( + "{}_{}", + common_utils::generate_id_with_default_len("auth"), + new_authorization_count + ), + merchant_id: payment_data.payment_intent.merchant_id.clone(), + payment_id: payment_data.payment_intent.payment_id.clone(), + amount: payment_data + .incremental_authorization_details + .clone() + .map(|details| details.total_amount) + .ok_or( + report!(errors::ApiErrorResponse::InternalServerError).attach_printable( + "missing incremental_authorization_details in payment_data", + ), + )?, + status: common_enums::AuthorizationStatus::Processing, + error_code: None, + error_message: None, + connector_authorization_id: None, + previously_authorized_amount: payment_data.payment_intent.amount, + }; + let authorization = db + .store + .insert_authorization(authorization_new.clone()) + .await + .to_duplicate_response(errors::ApiErrorResponse::GenericDuplicateError { + message: format!( + "Authorization with authorization_id {} already exists", + authorization_new.authorization_id + ), + }) + .attach_printable("failed while inserting new authorization")?; + // Update authorization_count in payment_intent + payment_data.payment_intent = db + .store + .update_payment_intent( + payment_data.payment_intent.clone(), + storage::PaymentIntentUpdate::AuthorizationCountUpdate { + authorization_count: new_authorization_count, + }, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) + .attach_printable("Failed to update authorization_count in Payment Intent")?; + match &payment_data.incremental_authorization_details { + Some(details) => { + payment_data.incremental_authorization_details = + Some(IncrementalAuthorizationDetails { + authorization_id: Some(authorization.authorization_id), + ..details.clone() + }); + } + None => Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("missing incremental_authorization_details in payment_data")?, + } + Ok((Box::new(self), payment_data)) + } +} + +impl + ValidateRequest + for PaymentIncrementalAuthorization +{ + #[instrument(skip_all)] + fn validate_request<'a, 'b>( + &'b self, + request: &PaymentsIncrementalAuthorizationRequest, + merchant_account: &'a domain::MerchantAccount, + ) -> RouterResult<( + BoxedOperation<'b, F, PaymentsIncrementalAuthorizationRequest, Ctx>, + operations::ValidateResult<'a>, + )> { + Ok(( + Box::new(self), + operations::ValidateResult { + merchant_id: &merchant_account.merchant_id, + payment_id: api::PaymentIdType::PaymentIntentId(request.payment_id.to_owned()), + mandate_type: None, + storage_scheme: merchant_account.storage_scheme, + requeue: false, + }, + )) + } +} + +#[async_trait] +impl + Domain for PaymentIncrementalAuthorization +{ + #[instrument(skip_all)] + async fn get_or_create_customer_details<'a>( + &'a self, + _db: &dyn StorageInterface, + _payment_data: &mut payments::PaymentData, + _request: Option, + _merchant_key_store: &domain::MerchantKeyStore, + ) -> CustomResult< + ( + BoxedOperation<'a, F, PaymentsIncrementalAuthorizationRequest, Ctx>, + Option, + ), + errors::StorageError, + > { + Ok((Box::new(self), None)) + } + + #[instrument(skip_all)] + async fn make_pm_data<'a>( + &'a self, + _state: &'a AppState, + _payment_data: &mut payments::PaymentData, + _storage_scheme: enums::MerchantStorageScheme, + _merchant_key_store: &domain::MerchantKeyStore, + ) -> RouterResult<( + BoxedOperation<'a, F, PaymentsIncrementalAuthorizationRequest, Ctx>, + Option, + )> { + Ok((Box::new(self), None)) + } + + async fn get_connector<'a>( + &'a self, + _merchant_account: &domain::MerchantAccount, + state: &AppState, + _request: &PaymentsIncrementalAuthorizationRequest, + _payment_intent: &storage::PaymentIntent, + _merchant_key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + helpers::get_connector_default(state, None).await + } +} diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 51e139c97988..bd6d03e5625a 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -4,7 +4,7 @@ use api_models::payments::{FrmMessage, RequestSurchargeDetails}; use common_enums::RequestIncrementalAuthorization; use common_utils::{consts::X_HS_LATENCY, fp_utils}; use diesel_models::ephemeral_key; -use error_stack::{IntoReport, ResultExt}; +use error_stack::{report, IntoReport, ResultExt}; use router_env::{instrument, tracing}; use super::{flows::Feature, PaymentData}; @@ -392,6 +392,18 @@ where ) }; + let incremental_authorizations_response = if payment_data.authorizations.is_empty() { + None + } else { + Some( + payment_data + .authorizations + .into_iter() + .map(ForeignInto::foreign_into) + .collect(), + ) + }; + let attempts_response = payment_data.attempts.map(|attempts| { attempts .into_iter() @@ -692,6 +704,8 @@ where .set_incremental_authorization_allowed( payment_intent.incremental_authorization_allowed, ) + .set_authorization_count(payment_intent.authorization_count) + .set_incremental_authorizations(incremental_authorizations_response) .to_owned(), headers, )) @@ -755,6 +769,8 @@ where unified_code: payment_attempt.unified_code, unified_message: payment_attempt.unified_message, incremental_authorization_allowed: payment_intent.incremental_authorization_allowed, + authorization_count: payment_intent.authorization_count, + incremental_authorizations: incremental_authorizations_response, ..Default::default() }, headers, @@ -1078,6 +1094,50 @@ impl TryFrom> for types::PaymentsSyncData } } +impl TryFrom> + for types::PaymentsIncrementalAuthorizationData +{ + type Error = error_stack::Report; + + fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result { + let payment_data = additional_data.payment_data; + let connector = api::ConnectorData::get_connector_by_name( + &additional_data.state.conf.connectors, + &additional_data.connector_name, + api::GetToken::Connector, + payment_data.payment_attempt.merchant_connector_id.clone(), + )?; + Ok(Self { + total_amount: payment_data + .incremental_authorization_details + .clone() + .map(|details| details.total_amount) + .ok_or( + report!(errors::ApiErrorResponse::InternalServerError).attach_printable( + "missing incremental_authorization_details in payment_data", + ), + )?, + additional_amount: payment_data + .incremental_authorization_details + .clone() + .map(|details| details.additional_amount) + .ok_or( + report!(errors::ApiErrorResponse::InternalServerError).attach_printable( + "missing incremental_authorization_details in payment_data", + ), + )?, + reason: payment_data + .incremental_authorization_details + .and_then(|details| details.reason), + currency: payment_data.currency, + connector_transaction_id: connector + .connector + .connector_transaction_id(payment_data.payment_attempt.clone())? + .ok_or(errors::ApiErrorResponse::ResourceIdNotFound)?, + }) + } +} + impl api::ConnectorTransactionId for Helcim { fn connector_transaction_id( &self, diff --git a/crates/router/src/core/payments/types.rs b/crates/router/src/core/payments/types.rs index 5e150a33d5c5..f97cdc17d724 100644 --- a/crates/router/src/core/payments/types.rs +++ b/crates/router/src/core/payments/types.rs @@ -1,10 +1,16 @@ -use std::collections::HashMap; +use std::{collections::HashMap, num::TryFromIntError}; +use api_models::{payment_methods::SurchargeDetailsResponse, payments::RequestSurchargeDetails}; +use common_utils::{consts, types as common_types}; +use data_models::payments::payment_attempt::PaymentAttempt; use error_stack::{IntoReport, ResultExt}; use crate::{ core::errors::{self, RouterResult}, - types::storage::{self, enums as storage_enums}, + types::{ + storage::{self, enums as storage_enums}, + transformers::ForeignTryFrom, + }, }; #[derive(Clone, Debug)] @@ -164,3 +170,153 @@ impl MultipleCaptureData { .collect() } } + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct SurchargeDetails { + /// surcharge value + pub surcharge: common_types::Surcharge, + /// tax on surcharge value + pub tax_on_surcharge: + Option>, + /// surcharge amount for this payment + pub surcharge_amount: i64, + /// tax on surcharge amount for this payment + pub tax_on_surcharge_amount: i64, + /// sum of original amount, + pub final_amount: i64, +} + +impl From<(&RequestSurchargeDetails, &PaymentAttempt)> for SurchargeDetails { + fn from( + (request_surcharge_details, payment_attempt): (&RequestSurchargeDetails, &PaymentAttempt), + ) -> Self { + let surcharge_amount = request_surcharge_details.surcharge_amount; + let tax_on_surcharge_amount = request_surcharge_details.tax_amount.unwrap_or(0); + Self { + surcharge: common_types::Surcharge::Fixed(request_surcharge_details.surcharge_amount), + tax_on_surcharge: None, + surcharge_amount, + tax_on_surcharge_amount, + final_amount: payment_attempt.amount + surcharge_amount + tax_on_surcharge_amount, + } + } +} + +impl ForeignTryFrom<(&SurchargeDetails, &PaymentAttempt)> for SurchargeDetailsResponse { + type Error = TryFromIntError; + fn foreign_try_from( + (surcharge_details, payment_attempt): (&SurchargeDetails, &PaymentAttempt), + ) -> Result { + let currency = payment_attempt.currency.unwrap_or_default(); + let display_surcharge_amount = + currency.to_currency_base_unit_asf64(surcharge_details.surcharge_amount)?; + let display_tax_on_surcharge_amount = + currency.to_currency_base_unit_asf64(surcharge_details.tax_on_surcharge_amount)?; + let display_final_amount = + currency.to_currency_base_unit_asf64(surcharge_details.final_amount)?; + Ok(Self { + surcharge: surcharge_details.surcharge.clone(), + tax_on_surcharge: surcharge_details.tax_on_surcharge.clone(), + display_surcharge_amount, + display_tax_on_surcharge_amount, + display_total_surcharge_amount: display_surcharge_amount + + display_tax_on_surcharge_amount, + display_final_amount, + }) + } +} + +impl SurchargeDetails { + pub fn is_request_surcharge_matching( + &self, + request_surcharge_details: RequestSurchargeDetails, + ) -> bool { + request_surcharge_details.surcharge_amount == self.surcharge_amount + && request_surcharge_details.tax_amount.unwrap_or(0) == self.tax_on_surcharge_amount + } + pub fn get_total_surcharge_amount(&self) -> i64 { + self.surcharge_amount + self.tax_on_surcharge_amount + } +} + +#[derive(Clone, Debug)] +pub struct SurchargeMetadata { + surcharge_results: HashMap< + ( + common_enums::PaymentMethod, + common_enums::PaymentMethodType, + Option, + ), + SurchargeDetails, + >, + pub payment_attempt_id: String, +} + +impl SurchargeMetadata { + pub fn new(payment_attempt_id: String) -> Self { + Self { + surcharge_results: HashMap::new(), + payment_attempt_id, + } + } + pub fn is_empty_result(&self) -> bool { + self.surcharge_results.is_empty() + } + pub fn get_surcharge_results_size(&self) -> usize { + self.surcharge_results.len() + } + pub fn insert_surcharge_details( + &mut self, + payment_method: &common_enums::PaymentMethod, + payment_method_type: &common_enums::PaymentMethodType, + card_network: Option<&common_enums::CardNetwork>, + surcharge_details: SurchargeDetails, + ) { + let key = ( + payment_method.to_owned(), + payment_method_type.to_owned(), + card_network.cloned(), + ); + self.surcharge_results.insert(key, surcharge_details); + } + pub fn get_surcharge_details( + &self, + payment_method: &common_enums::PaymentMethod, + payment_method_type: &common_enums::PaymentMethodType, + card_network: Option<&common_enums::CardNetwork>, + ) -> Option<&SurchargeDetails> { + let key = &( + payment_method.to_owned(), + payment_method_type.to_owned(), + card_network.cloned(), + ); + self.surcharge_results.get(key) + } + pub fn get_surcharge_metadata_redis_key(payment_attempt_id: &str) -> String { + format!("surcharge_metadata_{}", payment_attempt_id) + } + pub fn get_individual_surcharge_key_value_pairs(&self) -> Vec<(String, SurchargeDetails)> { + self.surcharge_results + .iter() + .map(|((pm, pmt, card_network), surcharge_details)| { + let key = + Self::get_surcharge_details_redis_hashset_key(pm, pmt, card_network.as_ref()); + (key, surcharge_details.to_owned()) + }) + .collect() + } + pub fn get_surcharge_details_redis_hashset_key( + payment_method: &common_enums::PaymentMethod, + payment_method_type: &common_enums::PaymentMethodType, + card_network: Option<&common_enums::CardNetwork>, + ) -> String { + if let Some(card_network) = card_network { + format!( + "{}_{}_{}", + payment_method, payment_method_type, card_network + ) + } else { + format!("{}_{}", payment_method, payment_method_type) + } + } +} diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 7d0d599cc4ed..c868530f81af 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -1,10 +1,17 @@ use api_models::user as user_api; use diesel_models::{enums::UserStatus, user as storage_user}; -use error_stack::{IntoReport, ResultExt}; -use masking::{ExposeInterface, Secret}; +#[cfg(feature = "email")] +use error_stack::IntoReport; +use error_stack::ResultExt; +use masking::ExposeInterface; +#[cfg(feature = "email")] use router_env::env; +#[cfg(feature = "email")] +use router_env::logger; use super::errors::{UserErrors, UserResponse}; +#[cfg(feature = "email")] +use crate::services::email::types as email_types; use crate::{ consts, db::user::UserInterface, @@ -13,11 +20,112 @@ use crate::{ types::domain, utils, }; +pub mod dashboard_metadata; #[cfg(feature = "dummy_connector")] pub mod sample_data; -pub mod dashboard_metadata; +#[cfg(feature = "email")] +pub async fn signup_with_merchant_id( + state: AppState, + request: user_api::SignUpWithMerchantIdRequest, +) -> UserResponse { + let new_user = domain::NewUser::try_from(request.clone())?; + new_user + .get_new_merchant() + .get_new_organization() + .insert_org_in_db(state.clone()) + .await?; + + let user_from_db = new_user + .insert_user_and_merchant_in_db(state.clone()) + .await?; + + let user_role = new_user + .insert_user_role_in_db( + state.clone(), + consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(), + UserStatus::Active, + ) + .await?; + + let email_contents = email_types::ResetPassword { + recipient_email: user_from_db.get_email().try_into()?, + user_name: domain::UserName::new(user_from_db.get_name())?, + settings: state.conf.clone(), + subject: "Get back to Hyperswitch - Reset Your Password Now", + }; + + let send_email_result = state + .email_client + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) + .await; + + logger::info!(?send_email_result); + Ok(ApplicationResponse::Json(user_api::AuthorizeResponse { + is_email_sent: send_email_result.is_ok(), + user_id: user_from_db.get_user_id().to_string(), + merchant_id: user_role.merchant_id, + })) +} + +pub async fn signup( + state: AppState, + request: user_api::SignUpRequest, +) -> UserResponse { + let new_user = domain::NewUser::try_from(request)?; + new_user + .get_new_merchant() + .get_new_organization() + .insert_org_in_db(state.clone()) + .await?; + let user_from_db = new_user + .insert_user_and_merchant_in_db(state.clone()) + .await?; + let user_role = new_user + .insert_user_role_in_db( + state.clone(), + consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(), + UserStatus::Active, + ) + .await?; + let token = utils::user::generate_jwt_auth_token(state, &user_from_db, &user_role).await?; + Ok(ApplicationResponse::Json( + utils::user::get_dashboard_entry_response(user_from_db, user_role, token), + )) +} + +pub async fn signin( + state: AppState, + request: user_api::SignInRequest, +) -> UserResponse { + let user_from_db: domain::UserFromStorage = state + .store + .find_user_by_email(request.email.clone().expose().expose().as_str()) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::InvalidCredentials) + } else { + e.change_context(UserErrors::InternalServerError) + } + })? + .into(); + + user_from_db.compare_password(request.password)?; + + let user_role = user_from_db.get_role_from_db(state.clone()).await?; + let token = utils::user::generate_jwt_auth_token(state, &user_from_db, &user_role).await?; + + Ok(ApplicationResponse::Json( + utils::user::get_dashboard_entry_response(user_from_db, user_role, token), + )) +} + +#[cfg(feature = "email")] pub async fn connect_account( state: AppState, request: user_api::ConnectAccountRequest, @@ -29,26 +137,34 @@ pub async fn connect_account( if let Ok(found_user) = find_user { let user_from_db: domain::UserFromStorage = found_user.into(); + let user_role = user_from_db.get_role_from_db(state.clone()).await?; - user_from_db.compare_password(request.password)?; + let email_contents = email_types::MagicLink { + recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?, + settings: state.conf.clone(), + user_name: domain::UserName::new(user_from_db.get_name())?, + subject: "Unlock Hyperswitch: Use Your Magic Link to Sign In", + }; + + let send_email_result = state + .email_client + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) + .await; - let user_role = user_from_db.get_role_from_db(state.clone()).await?; - let jwt_token = user_from_db - .get_jwt_auth_token(state.clone(), user_role.org_id) - .await?; + logger::info!(?send_email_result); return Ok(ApplicationResponse::Json( user_api::ConnectAccountResponse { - token: Secret::new(jwt_token), - merchant_id: user_role.merchant_id, - name: user_from_db.get_name(), - email: user_from_db.get_email(), - verification_days_left: None, - user_role: user_role.role_id, + is_email_sent: send_email_result.is_ok(), user_id: user_from_db.get_user_id().to_string(), + merchant_id: user_role.merchant_id, }, )); } else if find_user + .as_ref() .map_err(|e| e.current_context().is_db_not_found()) .err() .unwrap_or(false) @@ -73,46 +189,35 @@ pub async fn connect_account( UserStatus::Active, ) .await?; - let jwt_token = user_from_db - .get_jwt_auth_token(state.clone(), user_role.org_id) - .await?; - - #[cfg(feature = "email")] - { - use router_env::logger; - - use crate::services::email::types as email_types; - - let email_contents = email_types::VerifyEmail { - recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?, - settings: state.conf.clone(), - subject: "Welcome to the Hyperswitch community!", - }; - let send_email_result = state - .email_client - .compose_and_send_email( - Box::new(email_contents), - state.conf.proxy.https_url.as_ref(), - ) - .await; + let email_contents = email_types::VerifyEmail { + recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?, + settings: state.conf.clone(), + subject: "Welcome to the Hyperswitch community!", + }; + + let send_email_result = state + .email_client + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) + .await; - logger::info!(?send_email_result); - } + logger::info!(?send_email_result); return Ok(ApplicationResponse::Json( user_api::ConnectAccountResponse { - token: Secret::new(jwt_token), - merchant_id: user_role.merchant_id, - name: user_from_db.get_name(), - email: user_from_db.get_email(), - verification_days_left: None, - user_role: user_role.role_id, + is_email_sent: send_email_result.is_ok(), user_id: user_from_db.get_user_id().to_string(), + merchant_id: user_role.merchant_id, }, )); } else { - Err(UserErrors::InternalServerError.into()) + Err(find_user + .err() + .map(|e| e.change_context(UserErrors::InternalServerError)) + .unwrap_or(UserErrors::InternalServerError.into())) } } @@ -215,7 +320,7 @@ pub async fn switch_merchant_id( state: AppState, request: user_api::SwitchMerchantIdRequest, user_from_token: auth::UserFromToken, -) -> UserResponse { +) -> UserResponse { if !utils::user_role::is_internal_role(&user_from_token.role_id) { let merchant_list = utils::user_role::get_merchant_ids_for_user(state.clone(), &user_from_token.user_id) @@ -252,7 +357,7 @@ pub async fn switch_merchant_id( } })?; - let org_id = state + let _org_id = state .store .find_merchant_account_by_merchant_id(request.merchant_id.as_str(), &key_store) .await @@ -272,23 +377,23 @@ pub async fn switch_merchant_id( .await .change_context(UserErrors::InternalServerError)?; - let token = Box::pin(user.get_jwt_auth_token_with_custom_merchant_id( - state.clone(), + let token = utils::user::generate_jwt_auth_token_with_custom_merchant_id( + state, + &user, + &user_role, request.merchant_id.clone(), - org_id, - )) - .await? - .into(); + ) + .await?; Ok(ApplicationResponse::Json( - user_api::ConnectAccountResponse { - merchant_id: request.merchant_id, + user_api::SwitchMerchantResponse { token, name: user.get_name(), email: user.get_email(), user_id: user.get_user_id().to_string(), verification_days_left: None, user_role: user_role.role_id, + merchant_id: user_role.merchant_id, }, )) } diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 670c25c814ed..6d82c44d803a 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -1,9 +1,6 @@ use std::{marker::PhantomData, str::FromStr}; -use api_models::{ - enums::{DisputeStage, DisputeStatus}, - payment_methods::{SurchargeDetailsResponse, SurchargeMetadata}, -}; +use api_models::enums::{DisputeStage, DisputeStatus}; use common_enums::RequestIncrementalAuthorization; #[cfg(feature = "payouts")] use common_utils::{crypto::Encryptable, pii::Email}; @@ -17,7 +14,7 @@ use redis_interface::errors::RedisError; use router_env::{instrument, tracing}; use uuid::Uuid; -use super::payments::{helpers, PaymentAddress}; +use super::payments::{helpers, types as payments_types, PaymentAddress}; #[cfg(feature = "payouts")] use super::payouts::PayoutData; #[cfg(feature = "payouts")] @@ -1075,7 +1072,7 @@ pub fn get_flow_name() -> RouterResult { pub async fn persist_individual_surcharge_details_in_redis( state: &AppState, merchant_account: &domain::MerchantAccount, - surcharge_metadata: &SurchargeMetadata, + surcharge_metadata: &payments_types::SurchargeMetadata, ) -> RouterResult<()> { if !surcharge_metadata.is_empty_result() { let redis_conn = state @@ -1083,7 +1080,7 @@ pub async fn persist_individual_surcharge_details_in_redis( .get_redis_conn() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to get redis connection")?; - let redis_key = SurchargeMetadata::get_surcharge_metadata_redis_key( + let redis_key = payments_types::SurchargeMetadata::get_surcharge_metadata_redis_key( &surcharge_metadata.payment_attempt_id, ); @@ -1094,7 +1091,7 @@ pub async fn persist_individual_surcharge_details_in_redis( { value_list.push(( key, - Encode::::encode_to_string_of_json(&value) + Encode::::encode_to_string_of_json(&value) .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to encode to string of json")?, )); @@ -1118,20 +1115,21 @@ pub async fn get_individual_surcharge_detail_from_redis( payment_method_type: &euclid_enums::PaymentMethodType, card_network: Option, payment_attempt_id: &str, -) -> CustomResult { +) -> CustomResult { let redis_conn = state .store .get_redis_conn() .attach_printable("Failed to get redis connection")?; - let redis_key = SurchargeMetadata::get_surcharge_metadata_redis_key(payment_attempt_id); - let value_key = SurchargeMetadata::get_surcharge_details_redis_hashset_key( + let redis_key = + payments_types::SurchargeMetadata::get_surcharge_metadata_redis_key(payment_attempt_id); + let value_key = payments_types::SurchargeMetadata::get_surcharge_details_redis_hashset_key( payment_method, payment_method_type, card_network.as_ref(), ); redis_conn - .get_hash_field_and_deserialize(&redis_key, &value_key, "SurchargeDetailsResponse") + .get_hash_field_and_deserialize(&redis_key, &value_key, "SurchargeDetails") .await } diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index 6558cc6ace50..0cd4cb218810 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -1,5 +1,6 @@ pub mod address; pub mod api_keys; +pub mod authorization; pub mod business_profile; pub mod cache; pub mod capture; @@ -100,6 +101,7 @@ pub trait StorageInterface: + gsm::GsmInterface + user::UserInterface + user_role::UserRoleInterface + + authorization::AuthorizationInterface + user::sample_data::BatchSampleDataInterface + 'static { diff --git a/crates/router/src/db/authorization.rs b/crates/router/src/db/authorization.rs new file mode 100644 index 000000000000..f24daaf718ad --- /dev/null +++ b/crates/router/src/db/authorization.rs @@ -0,0 +1,104 @@ +use error_stack::IntoReport; + +use super::{MockDb, Store}; +use crate::{ + connection, + core::errors::{self, CustomResult}, + types::storage, +}; + +#[async_trait::async_trait] +pub trait AuthorizationInterface { + async fn insert_authorization( + &self, + authorization: storage::AuthorizationNew, + ) -> CustomResult; + + async fn find_all_authorizations_by_merchant_id_payment_id( + &self, + merchant_id: &str, + payment_id: &str, + ) -> CustomResult, errors::StorageError>; + + async fn update_authorization_by_merchant_id_authorization_id( + &self, + merchant_id: String, + authorization_id: String, + authorization: storage::AuthorizationUpdate, + ) -> CustomResult; +} + +#[async_trait::async_trait] +impl AuthorizationInterface for Store { + async fn insert_authorization( + &self, + authorization: storage::AuthorizationNew, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + authorization + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_all_authorizations_by_merchant_id_payment_id( + &self, + merchant_id: &str, + payment_id: &str, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_read(self).await?; + storage::Authorization::find_by_merchant_id_payment_id(&conn, merchant_id, payment_id) + .await + .map_err(Into::into) + .into_report() + } + + async fn update_authorization_by_merchant_id_authorization_id( + &self, + merchant_id: String, + authorization_id: String, + authorization: storage::AuthorizationUpdate, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::Authorization::update_by_merchant_id_authorization_id( + &conn, + merchant_id, + authorization_id, + authorization, + ) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl AuthorizationInterface for MockDb { + async fn insert_authorization( + &self, + _authorization: storage::AuthorizationNew, + ) -> CustomResult { + // TODO: Implement function for `MockDb` + Err(errors::StorageError::MockDbError)? + } + + async fn find_all_authorizations_by_merchant_id_payment_id( + &self, + _merchant_id: &str, + _payment_id: &str, + ) -> CustomResult, errors::StorageError> { + // TODO: Implement function for `MockDb` + Err(errors::StorageError::MockDbError)? + } + + async fn update_authorization_by_merchant_id_authorization_id( + &self, + _merchant_id: String, + _authorization_id: String, + _authorization: storage::AuthorizationUpdate, + ) -> CustomResult { + // TODO: Implement function for `MockDb` + Err(errors::StorageError::MockDbError)? + } +} diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 32548e36b6fb..db94c1bcbca9 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -32,6 +32,7 @@ use crate::{ db::{ address::AddressInterface, api_keys::ApiKeyInterface, + authorization::AuthorizationInterface, business_profile::BusinessProfileInterface, capture::CaptureInterface, cards_info::CardsInfoInterface, @@ -2095,3 +2096,38 @@ impl BatchSampleDataInterface for KafkaStore { Ok(refunds_list) } } + +#[async_trait::async_trait] +impl AuthorizationInterface for KafkaStore { + async fn insert_authorization( + &self, + authorization: storage::AuthorizationNew, + ) -> CustomResult { + self.diesel_store.insert_authorization(authorization).await + } + + async fn find_all_authorizations_by_merchant_id_payment_id( + &self, + merchant_id: &str, + payment_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_all_authorizations_by_merchant_id_payment_id(merchant_id, payment_id) + .await + } + + async fn update_authorization_by_merchant_id_authorization_id( + &self, + merchant_id: String, + authorization_id: String, + authorization: storage::AuthorizationUpdate, + ) -> CustomResult { + self.diesel_store + .update_authorization_by_merchant_id_authorization_id( + merchant_id, + authorization_id, + authorization, + ) + .await + } +} diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index cfb0268a9f80..82c98304c62b 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -175,6 +175,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::enums::CaptureStatus, api_models::enums::ReconStatus, api_models::enums::ConnectorStatus, + api_models::enums::AuthorizationStatus, api_models::admin::MerchantConnectorCreate, api_models::admin::MerchantConnectorUpdate, api_models::admin::PrimaryBusinessDetails, @@ -315,6 +316,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::RequestSurchargeDetails, api_models::payments::PaymentAttemptResponse, api_models::payments::CaptureResponse, + api_models::payments::IncrementalAuthorizationResponse, api_models::payment_methods::RequiredFieldInfo, api_models::payment_methods::MaskedBankDetails, api_models::refunds::RefundListRequest, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index a145f3e7e5d7..5d14d1219d32 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -343,6 +343,9 @@ impl Payments { web::resource("/{payment_id}/{merchant_id}/redirect/complete/{connector}") .route(web::get().to(payments_complete_authorize)) .route(web::post().to(payments_complete_authorize)), + ) + .service( + web::resource("/{payment_id}/incremental_authorization").route(web::post().to(payments_incremental_authorization)), ); } route @@ -823,10 +826,7 @@ impl User { let mut route = web::scope("/user").app_data(web::Data::new(state)); route = route - .service(web::resource("/signin").route(web::post().to(user_connect_account))) - .service(web::resource("/signup").route(web::post().to(user_connect_account))) - .service(web::resource("/v2/signin").route(web::post().to(user_connect_account))) - .service(web::resource("/v2/signup").route(web::post().to(user_connect_account))) + .service(web::resource("/signin").route(web::post().to(user_signin))) .service(web::resource("/change_password").route(web::post().to(change_password))) .service( web::resource("/data/merchant") @@ -841,7 +841,6 @@ impl User { ) .service(web::resource("/switch/list").route(web::get().to(list_merchant_ids_for_user))) .service(web::resource("/user/list").route(web::get().to(get_user_details))) - // User Role APIs .service(web::resource("/permission_info").route(web::get().to(get_authorization_info))) .service(web::resource("/user/update_role").route(web::post().to(update_user_role))) .service(web::resource("/role/list").route(web::get().to(list_roles))) @@ -855,6 +854,21 @@ impl User { .route(web::delete().to(delete_sample_data)), ) } + #[cfg(feature = "email")] + { + route = route + .service( + web::resource("/connect_account").route(web::post().to(user_connect_account)), + ) + .service( + web::resource("/signup_with_merchant_id") + .route(web::post().to(user_signup_with_merchant_id)), + ); + } + #[cfg(not(feature = "email"))] + { + route = route.service(web::resource("/signup").route(web::post().to(user_signup))) + } route } } diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 6aa2bbad0b15..3592506f522b 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -96,7 +96,8 @@ impl From for ApiIdentifier { | Flow::PaymentsSessionToken | Flow::PaymentsStart | Flow::PaymentsList - | Flow::PaymentsRedirect => Self::Payments, + | Flow::PaymentsRedirect + | Flow::PaymentsIncrementalAuthorization => Self::Payments, Flow::PayoutsCreate | Flow::PayoutsRetrieve @@ -149,6 +150,8 @@ impl From for ApiIdentifier { | Flow::GsmRuleDelete => Self::Gsm, Flow::UserConnectAccount + | Flow::UserSignUp + | Flow::UserSignIn | Flow::ChangePassword | Flow::SetDashboardMetadata | Flow::GetMutltipleDashboardMetadata @@ -159,7 +162,8 @@ impl From for ApiIdentifier { | Flow::GenerateSampleData | Flow::DeleteSampleData | Flow::UserMerchantAccountList - | Flow::GetUserDetails => Self::User, + | Flow::GetUserDetails + | Flow::UserSignUpWithMerchantId => Self::User, Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => { Self::UserRole diff --git a/crates/router/src/routes/metrics.rs b/crates/router/src/routes/metrics.rs index a8e6f9d2a892..192df1a09298 100644 --- a/crates/router/src/routes/metrics.rs +++ b/crates/router/src/routes/metrics.rs @@ -85,6 +85,7 @@ counter_metric!(CONNECTOR_HTTP_STATUS_CODE_5XX_COUNT, GLOBAL_METER); // Service Level counter_metric!(CARD_LOCKER_FAILURES, GLOBAL_METER); +counter_metric!(CARD_LOCKER_SUCCESSFUL_RESPONSE, GLOBAL_METER); counter_metric!(TEMP_LOCKER_FAILURES, GLOBAL_METER); histogram_metric!(CARD_ADD_TIME, GLOBAL_METER); histogram_metric!(CARD_GET_TIME, GLOBAL_METER); diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 979b15a3d7f2..e424e93c78ed 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -986,6 +986,67 @@ where } } +/// Payments - Incremental Authorization +/// +/// Authorized amount for a payment can be incremented if it is in status: requires_capture +#[utoipa::path( + post, + path = "/payments/{payment_id}/incremental_authorization", + request_body=PaymentsIncrementalAuthorizationRequest, + params( + ("payment_id" = String, Path, description = "The identifier for payment") + ), + responses( + (status = 200, description = "Payment authorized amount incremented"), + (status = 400, description = "Missing mandatory fields") + ), + tag = "Payments", + operation_id = "Increment authorized amount for a Payment", + security(("api_key" = [])) +)] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsIncrementalAuthorization))] +pub async fn payments_incremental_authorization( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + path: web::Path, +) -> impl Responder { + let flow = Flow::PaymentsIncrementalAuthorization; + let mut payload = json_payload.into_inner(); + let payment_id = path.into_inner(); + payload.payment_id = payment_id; + let locking_action = payload.get_locking_input(flow.clone()); + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth, req| { + payments::payments_core::< + api_types::IncrementalAuthorization, + payment_types::PaymentsResponse, + _, + _, + _, + Oss, + >( + state, + auth.merchant_account, + auth.key_store, + payments::PaymentIncrementalAuthorization, + req, + api::AuthFlow::Merchant, + payments::CallConnectorAction::Trigger, + None, + HeaderPayload::default(), + ) + }, + &auth::ApiKeyAuth, + locking_action, + )) + .await +} + pub fn get_or_generate_payment_id( payload: &mut payment_types::PaymentsRequest, ) -> errors::RouterResult<()> { @@ -1135,3 +1196,19 @@ impl GetLockingInput for payment_types::PaymentsCaptureRequest { } } } + +impl GetLockingInput for payment_types::PaymentsIncrementalAuthorizationRequest { + fn get_locking_input(&self, flow: F) -> api_locking::LockAction + where + F: types::FlowMetric, + lock_utils::ApiIdentifier: From, + { + api_locking::LockAction::Hold { + input: api_locking::LockingInput { + unique_locking_key: self.payment_id.to_owned(), + api_identifier: lock_utils::ApiIdentifier::from(flow), + override_lock_retries: None, + }, + } + } +} diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 97bd7054da9e..45fa0ba35c59 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -19,6 +19,65 @@ use crate::{ utils::user::dashboard_metadata::{parse_string_to_enums, set_ip_address_if_required}, }; +#[cfg(feature = "email")] +pub async fn user_signup_with_merchant_id( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::UserSignUpWithMerchantId; + let req_payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + req_payload.clone(), + |state, _, req_body| user_core::signup_with_merchant_id(state, req_body), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn user_signup( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::UserSignUp; + let req_payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + req_payload.clone(), + |state, _, req_body| user_core::signup(state, req_body), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn user_signin( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::UserSignIn; + let req_payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + req_payload.clone(), + |state, _, req_body| user_core::signin(state, req_body), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "email")] pub async fn user_connect_account( state: web::Data, http_req: HttpRequest, diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 1ff46474db59..ee5727bbda90 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -545,6 +545,9 @@ pub async fn send_request( Method::Put => client .put(url) .body(request.payload.expose_option().unwrap_or_default()), // If payload needs processing the body cannot have default + Method::Patch => client + .patch(url) + .body(request.payload.expose_option().unwrap_or_default()), Method::Delete => client.delete(url), } .add_headers(headers) @@ -1186,6 +1189,7 @@ impl Authenticate for api_models::payments::PaymentsSessionRequest { impl Authenticate for api_models::payments::PaymentsRetrieveRequest {} impl Authenticate for api_models::payments::PaymentsCancelRequest {} impl Authenticate for api_models::payments::PaymentsCaptureRequest {} +impl Authenticate for api_models::payments::PaymentsIncrementalAuthorizationRequest {} impl Authenticate for api_models::payments::PaymentsStartRequest {} pub fn build_redirection_form( diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index c267a54cc57b..08cbb36952e3 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -30,7 +30,7 @@ use crate::core::utils::IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_DISPUTE_FLO use crate::{ core::{ errors::{self, RouterResult}, - payments::{PaymentData, RecurringMandatePaymentData}, + payments::{types, PaymentData, RecurringMandatePaymentData}, }, services, types::{storage::payment_attempt::PaymentAttemptExt, transformers::ForeignFrom}, @@ -52,6 +52,11 @@ pub type PaymentsBalanceRouterData = pub type PaymentsSyncRouterData = RouterData; pub type PaymentsCaptureRouterData = RouterData; +pub type PaymentsIncrementalAuthorizationRouterData = RouterData< + api::IncrementalAuthorization, + PaymentsIncrementalAuthorizationData, + PaymentsResponseData, +>; pub type PaymentsCancelRouterData = RouterData; pub type PaymentsRejectRouterData = RouterData; @@ -142,6 +147,11 @@ pub type TokenizationType = dyn services::ConnectorIntegration< PaymentMethodTokenizationData, PaymentsResponseData, >; +pub type IncrementalAuthorizationType = dyn services::ConnectorIntegration< + api::IncrementalAuthorization, + PaymentsIncrementalAuthorizationData, + PaymentsResponseData, +>; pub type ConnectorCustomerType = dyn services::ConnectorIntegration< api::CreateConnectorCustomer, @@ -379,7 +389,7 @@ pub struct PaymentsAuthorizeData { pub related_transaction_id: Option, pub payment_experience: Option, pub payment_method_type: Option, - pub surcharge_details: Option, + pub surcharge_details: Option, pub customer_id: Option, pub request_incremental_authorization: bool, } @@ -395,6 +405,15 @@ pub struct PaymentsCaptureData { pub browser_info: Option, } +#[derive(Debug, Clone, Default)] +pub struct PaymentsIncrementalAuthorizationData { + pub total_amount: i64, + pub additional_amount: i64, + pub currency: storage_enums::Currency, + pub reason: Option, + pub connector_transaction_id: String, +} + #[allow(dead_code)] #[derive(Debug, Clone, Default)] pub struct MultipleCaptureRequestData { @@ -441,7 +460,7 @@ pub struct PaymentsPreProcessingData { pub router_return_url: Option, pub webhook_url: Option, pub complete_authorize_url: Option, - pub surcharge_details: Option, + pub surcharge_details: Option, pub browser_info: Option, pub connector_transaction_id: Option, } @@ -517,7 +536,7 @@ pub struct PaymentsSessionData { pub amount: i64, pub currency: storage_enums::Currency, pub country: Option, - pub surcharge_details: Option, + pub surcharge_details: Option, pub order_details: Option>, } @@ -599,6 +618,7 @@ impl Capturable for PaymentsCancelData { impl Capturable for PaymentsApproveData {} impl Capturable for PaymentsRejectData {} impl Capturable for PaymentsSessionData {} +impl Capturable for PaymentsIncrementalAuthorizationData {} impl Capturable for PaymentsSyncData { fn get_capture_amount(&self, payment_data: &PaymentData) -> Option where @@ -707,6 +727,12 @@ pub enum PaymentsResponseData { session_token: Option, connector_response_reference_id: Option, }, + IncrementalAuthorizationResponse { + status: common_enums::AuthorizationStatus, + connector_authorization_id: Option, + error_code: Option, + error_message: Option, + }, } #[derive(Debug, Clone)] diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 96bcaca3ed5d..ea2ea8b701da 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -19,7 +19,6 @@ pub mod webhooks; use std::{fmt::Debug, str::FromStr}; -use api_models::payment_methods::{SurchargeDetailsResponse, SurchargeMetadata}; use error_stack::{report, IntoReport, ResultExt}; pub use self::{ @@ -30,7 +29,10 @@ use super::ErrorResponse; use crate::{ configs::settings::Connectors, connector, consts, - core::errors::{self, CustomResult}, + core::{ + errors::{self, CustomResult}, + payments::types as payments_types, + }, services::{request, ConnectorIntegration, ConnectorRedirectResponse, ConnectorValidation}, types::{self, api::enums as api_enums}, }; @@ -222,9 +224,9 @@ pub struct SessionConnectorData { /// Session Surcharge type pub enum SessionSurchargeDetails { /// Surcharge is calculated by hyperswitch - Calculated(SurchargeMetadata), + Calculated(payments_types::SurchargeMetadata), /// Surcharge is sent by merchant - PreDetermined(SurchargeDetailsResponse), + PreDetermined(payments_types::SurchargeDetails), } impl SessionSurchargeDetails { @@ -233,7 +235,7 @@ impl SessionSurchargeDetails { payment_method: &enums::PaymentMethod, payment_method_type: &enums::PaymentMethodType, card_network: Option<&enums::CardNetwork>, - ) -> Option { + ) -> Option { match self { Self::Calculated(surcharge_metadata) => surcharge_metadata .get_surcharge_details(payment_method, payment_method_type, card_network) diff --git a/crates/router/src/types/api/payments.rs b/crates/router/src/types/api/payments.rs index b00a7f0cbdac..2acf42fa479d 100644 --- a/crates/router/src/types/api/payments.rs +++ b/crates/router/src/types/api/payments.rs @@ -5,11 +5,12 @@ pub use api_models::payments::{ PayLaterData, PaymentIdType, PaymentListConstraints, PaymentListFilterConstraints, PaymentListFilters, PaymentListResponse, PaymentListResponseV2, PaymentMethodData, PaymentMethodDataResponse, PaymentOp, PaymentRetrieveBody, PaymentRetrieveBodyWithCredentials, - PaymentsApproveRequest, PaymentsCancelRequest, PaymentsCaptureRequest, PaymentsRedirectRequest, - PaymentsRedirectionResponse, PaymentsRejectRequest, PaymentsRequest, PaymentsResponse, - PaymentsResponseForm, PaymentsRetrieveRequest, PaymentsSessionRequest, PaymentsSessionResponse, - PaymentsStartRequest, PgRedirectResponse, PhoneDetails, RedirectionResponse, SessionToken, - TimeRange, UrlDetails, VerifyRequest, VerifyResponse, WalletData, + PaymentsApproveRequest, PaymentsCancelRequest, PaymentsCaptureRequest, + PaymentsIncrementalAuthorizationRequest, PaymentsRedirectRequest, PaymentsRedirectionResponse, + PaymentsRejectRequest, PaymentsRequest, PaymentsResponse, PaymentsResponseForm, + PaymentsRetrieveRequest, PaymentsSessionRequest, PaymentsSessionResponse, PaymentsStartRequest, + PgRedirectResponse, PhoneDetails, RedirectionResponse, SessionToken, TimeRange, UrlDetails, + VerifyRequest, VerifyResponse, WalletData, }; use error_stack::{IntoReport, ResultExt}; @@ -81,6 +82,9 @@ pub struct SetupMandate; #[derive(Debug, Clone)] pub struct PreProcessing; +#[derive(Debug, Clone)] +pub struct IncrementalAuthorization; + pub trait PaymentIdTypeExt { fn get_payment_intent_id(&self) -> errors::CustomResult; } @@ -164,6 +168,15 @@ pub trait MandateSetup: { } +pub trait PaymentIncrementalAuthorization: + api::ConnectorIntegration< + IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, +> +{ +} + pub trait PaymentsCompleteAuthorize: api::ConnectorIntegration< CompleteAuthorize, @@ -215,6 +228,7 @@ pub trait Payment: + PaymentToken + PaymentsPreProcessing + ConnectorCustomer + + PaymentIncrementalAuthorization { } diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 082b29d80941..592195922493 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -26,7 +26,7 @@ use crate::{ db::StorageInterface, routes::AppState, services::{ - authentication::{AuthToken, UserFromToken}, + authentication::UserFromToken, authorization::{info, predefined_permissions}, }, types::transformers::ForeignFrom, @@ -215,6 +215,25 @@ impl NewUserOrganization { } } +impl TryFrom for NewUserOrganization { + type Error = error_stack::Report; + fn try_from(value: user_api::SignUpWithMerchantIdRequest) -> UserResult { + let new_organization = api_org::OrganizationNew::new(Some( + UserCompanyName::new(value.company_name)?.get_secret(), + )); + let db_organization = ForeignFrom::foreign_from(new_organization); + Ok(Self(db_organization)) + } +} + +impl From for NewUserOrganization { + fn from(_value: user_api::SignUpRequest) -> Self { + let new_organization = api_org::OrganizationNew::new(None); + let db_organization = ForeignFrom::foreign_from(new_organization); + Self(db_organization) + } +} + impl From for NewUserOrganization { fn from(_value: user_api::ConnectAccountRequest) -> Self { let new_organization = api_org::OrganizationNew::new(None); @@ -334,6 +353,24 @@ impl NewUserMerchant { } } +impl TryFrom for NewUserMerchant { + type Error = error_stack::Report; + + fn try_from(value: user_api::SignUpRequest) -> UserResult { + let merchant_id = MerchantId::new(format!( + "merchant_{}", + common_utils::date_time::now_unix_timestamp() + ))?; + let new_organization = NewUserOrganization::from(value); + + Ok(Self { + company_name: None, + merchant_id, + new_organization, + }) + } +} + impl TryFrom for NewUserMerchant { type Error = error_stack::Report; @@ -352,6 +389,21 @@ impl TryFrom for NewUserMerchant { } } +impl TryFrom for NewUserMerchant { + type Error = error_stack::Report; + fn try_from(value: user_api::SignUpWithMerchantIdRequest) -> UserResult { + let company_name = Some(UserCompanyName::new(value.company_name.clone())?); + let merchant_id = MerchantId::new(value.company_name.clone())?; + let new_organization = NewUserOrganization::try_from(value)?; + + Ok(Self { + company_name, + merchant_id, + new_organization, + }) + } +} + impl TryFrom for NewUserMerchant { type Error = error_stack::Report; @@ -434,10 +486,23 @@ impl NewUser { .attach_printable("Error while inserting user") } + pub async fn check_if_already_exists_in_db(&self, state: AppState) -> UserResult<()> { + if state + .store + .find_user_by_email(self.get_email().into_inner().expose().expose().as_str()) + .await + .is_ok() + { + return Err(UserErrors::UserExists).into_report(); + } + Ok(()) + } + pub async fn insert_user_and_merchant_in_db( &self, state: AppState, ) -> UserResult { + self.check_if_already_exists_in_db(state.clone()).await?; let db = state.store.as_ref(); let merchant_id = self.get_new_merchant().get_merchant_id(); self.new_merchant @@ -495,6 +560,46 @@ impl TryFrom for storage_user::UserNew { } } +impl TryFrom for NewUser { + type Error = error_stack::Report; + + fn try_from(value: user_api::SignUpWithMerchantIdRequest) -> UserResult { + let email = value.email.clone().try_into()?; + let name = UserName::new(value.name.clone())?; + let password = UserPassword::new(value.password.clone())?; + let user_id = uuid::Uuid::new_v4().to_string(); + let new_merchant = NewUserMerchant::try_from(value)?; + + Ok(Self { + name, + email, + password, + user_id, + new_merchant, + }) + } +} + +impl TryFrom for NewUser { + type Error = error_stack::Report; + + fn try_from(value: user_api::SignUpRequest) -> UserResult { + let user_id = uuid::Uuid::new_v4().to_string(); + let email = value.email.clone().try_into()?; + let name = UserName::try_from(value.email.clone())?; + let password = UserPassword::new(value.password.clone())?; + let new_merchant = NewUserMerchant::try_from(value)?; + + Ok(Self { + user_id, + name, + email, + password, + new_merchant, + }) + } +} + impl TryFrom for NewUser { type Error = error_stack::Report; @@ -502,7 +607,7 @@ impl TryFrom for NewUser { let user_id = uuid::Uuid::new_v4().to_string(); let email = value.email.clone().try_into()?; let name = UserName::try_from(value.email.clone())?; - let password = UserPassword::new(value.password.clone())?; + let password = UserPassword::new(uuid::Uuid::new_v4().to_string().into())?; let new_merchant = NewUserMerchant::try_from(value)?; Ok(Self { @@ -582,41 +687,6 @@ impl UserFromStorage { self.0.email.clone() } - pub async fn get_jwt_auth_token(&self, state: AppState, org_id: String) -> UserResult { - let role_id = self.get_role_from_db(state.clone()).await?.role_id; - let merchant_id = state - .store - .find_user_role_by_user_id(self.get_user_id()) - .await - .change_context(UserErrors::InternalServerError)? - .merchant_id; - AuthToken::new_token( - self.0.user_id.clone(), - merchant_id, - role_id, - &state.conf, - org_id, - ) - .await - } - - pub async fn get_jwt_auth_token_with_custom_merchant_id( - &self, - state: AppState, - merchant_id: String, - org_id: String, - ) -> UserResult { - let role_id = self.get_role_from_db(state.clone()).await?.role_id; - AuthToken::new_token( - self.0.user_id.clone(), - merchant_id, - role_id, - &state.conf, - org_id, - ) - .await - } - pub async fn get_role_from_db(&self, state: AppState) -> UserResult { state .store diff --git a/crates/router/src/types/storage.rs b/crates/router/src/types/storage.rs index a83a405f3554..c8cc7f9c010f 100644 --- a/crates/router/src/types/storage.rs +++ b/crates/router/src/types/storage.rs @@ -1,5 +1,6 @@ pub mod address; pub mod api_keys; +pub mod authorization; pub mod business_profile; pub mod capture; pub mod cards_info; @@ -43,7 +44,7 @@ pub use data_models::payments::{ }; pub use self::{ - address::*, api_keys::*, capture::*, cards_info::*, configs::*, customers::*, + address::*, api_keys::*, authorization::*, capture::*, cards_info::*, configs::*, customers::*, dashboard_metadata::*, dispute::*, ephemeral_key::*, events::*, file::*, gsm::*, locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*, merchant_key_store::*, payment_link::*, payment_method::*, payout_attempt::*, payouts::*, diff --git a/crates/router/src/types/storage/authorization.rs b/crates/router/src/types/storage/authorization.rs new file mode 100644 index 000000000000..678cd64f8810 --- /dev/null +++ b/crates/router/src/types/storage/authorization.rs @@ -0,0 +1 @@ +pub use diesel_models::authorization::{Authorization, AuthorizationNew, AuthorizationUpdate}; diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 99096864a000..0244d8dc18ef 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -685,6 +685,19 @@ impl ForeignFrom for api_models::disputes::DisputeResponse { } } +impl ForeignFrom for payments::IncrementalAuthorizationResponse { + fn foreign_from(authorization: storage::Authorization) -> Self { + Self { + authorization_id: authorization.authorization_id, + amount: authorization.amount, + status: authorization.status, + error_code: authorization.error_code, + error_message: authorization.error_message, + previously_authorized_amount: authorization.previously_authorized_amount, + } + } +} + impl ForeignFrom for api_models::disputes::DisputeResponsePaymentsRetrieve { fn foreign_from(dispute: storage::Dispute) -> Self { Self { diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index 696aa4090044..0403d9b453d0 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -1,11 +1,13 @@ -use diesel_models::enums::UserStatus; +use api_models::user as user_api; +use diesel_models::{enums::UserStatus, user_role::UserRole}; use error_stack::ResultExt; +use masking::Secret; use crate::{ core::errors::{UserErrors, UserResult}, routes::AppState, - services::authentication::UserFromToken, - types::domain::MerchantAccount, + services::authentication::{AuthToken, UserFromToken}, + types::domain::{MerchantAccount, UserFromStorage}, }; pub mod dashboard_metadata; @@ -68,3 +70,52 @@ pub async fn get_merchant_ids_for_user(state: AppState, user_id: &str) -> UserRe }) .collect()) } + +pub async fn generate_jwt_auth_token( + state: AppState, + user: &UserFromStorage, + user_role: &UserRole, +) -> UserResult> { + let token = AuthToken::new_token( + user.get_user_id().to_string(), + user_role.merchant_id.clone(), + user_role.role_id.clone(), + &state.conf, + user_role.org_id.clone(), + ) + .await?; + Ok(Secret::new(token)) +} + +pub async fn generate_jwt_auth_token_with_custom_merchant_id( + state: AppState, + user: &UserFromStorage, + user_role: &UserRole, + merchant_id: String, +) -> UserResult> { + let token = AuthToken::new_token( + user.get_user_id().to_string(), + merchant_id, + user_role.role_id.clone(), + &state.conf, + user_role.org_id.to_owned(), + ) + .await?; + Ok(Secret::new(token)) +} + +pub fn get_dashboard_entry_response( + user: UserFromStorage, + user_role: UserRole, + token: Secret, +) -> user_api::DashboardEntryResponse { + user_api::DashboardEntryResponse { + merchant_id: user_role.merchant_id, + token, + name: user.get_name(), + email: user.get_email(), + user_id: user.get_user_id().to_string(), + verification_days_left: None, + user_role: user_role.role_id, + } +} diff --git a/crates/router/src/utils/user/sample_data.rs b/crates/router/src/utils/user/sample_data.rs index 7a9cf6d2b7db..543e3cd2aa5f 100644 --- a/crates/router/src/utils/user/sample_data.rs +++ b/crates/router/src/utils/user/sample_data.rs @@ -196,6 +196,7 @@ pub async fn generate_sample_data( surcharge_applicable: Default::default(), request_incremental_authorization: Default::default(), incremental_authorization_allowed: Default::default(), + authorization_count: Default::default(), }; let payment_attempt = PaymentAttemptBatchNew { attempt_id: attempt_id.clone(), diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 823b3eae497d..7e5cfeb43974 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -542,6 +542,7 @@ pub trait ConnectorActions: Connector { Ok(types::PaymentsResponseData::PreProcessingResponse { .. }) => None, Ok(types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. }) => None, Ok(types::PaymentsResponseData::MultipleCaptureResponse { .. }) => None, + Ok(types::PaymentsResponseData::IncrementalAuthorizationResponse { .. }) => None, Err(_) => None, } } @@ -1029,6 +1030,7 @@ pub fn get_connector_transaction_id( Ok(types::PaymentsResponseData::ConnectorCustomerResponse { .. }) => None, Ok(types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. }) => None, Ok(types::PaymentsResponseData::MultipleCaptureResponse { .. }) => None, + Ok(types::PaymentsResponseData::IncrementalAuthorizationResponse { .. }) => None, Err(_) => None, } } diff --git a/crates/router_derive/src/macros/operation.rs b/crates/router_derive/src/macros/operation.rs index 370e03b984ba..e743a2d9cc52 100644 --- a/crates/router_derive/src/macros/operation.rs +++ b/crates/router_derive/src/macros/operation.rs @@ -27,6 +27,8 @@ pub enum Derives { Verify, Session, SessionData, + IncrementalAuthorization, + IncrementalAuthorizationData, } impl Derives { @@ -95,6 +97,12 @@ impl Conversion { } Derives::Session => syn::Ident::new("PaymentsSessionRequest", Span::call_site()), Derives::SessionData => syn::Ident::new("PaymentsSessionData", Span::call_site()), + Derives::IncrementalAuthorization => { + syn::Ident::new("PaymentsIncrementalAuthorizationRequest", Span::call_site()) + } + Derives::IncrementalAuthorizationData => { + syn::Ident::new("PaymentsIncrementalAuthorizationData", Span::call_site()) + } } } @@ -414,6 +422,7 @@ pub fn operation_derive_inner(input: DeriveInput) -> syn::Result syn::Result DieselPaymentAttemptUpdate::IncrementalAuthorizationAmountUpdate { + amount, + amount_capturable, + }, } } @@ -1728,6 +1735,13 @@ impl DataModelExt for PaymentAttemptUpdate { connector, updated_by, }, + DieselPaymentAttemptUpdate::IncrementalAuthorizationAmountUpdate { + amount, + amount_capturable, + } => Self::IncrementalAuthorizationAmountUpdate { + amount, + amount_capturable, + }, } } } diff --git a/crates/storage_impl/src/payments/payment_intent.rs b/crates/storage_impl/src/payments/payment_intent.rs index fdf9875bc1ff..90bb21190c39 100644 --- a/crates/storage_impl/src/payments/payment_intent.rs +++ b/crates/storage_impl/src/payments/payment_intent.rs @@ -99,6 +99,7 @@ impl PaymentIntentInterface for KVRouterStore { surcharge_applicable: new.surcharge_applicable, request_incremental_authorization: new.request_incremental_authorization, incremental_authorization_allowed: new.incremental_authorization_allowed, + authorization_count: new.authorization_count, }; let redis_entry = kv::TypedSql { op: kv::DBOperation::Insert { @@ -762,6 +763,7 @@ impl DataModelExt for PaymentIntentNew { surcharge_applicable: self.surcharge_applicable, request_incremental_authorization: self.request_incremental_authorization, incremental_authorization_allowed: self.incremental_authorization_allowed, + authorization_count: self.authorization_count, } } @@ -804,6 +806,7 @@ impl DataModelExt for PaymentIntentNew { surcharge_applicable: storage_model.surcharge_applicable, request_incremental_authorization: storage_model.request_incremental_authorization, incremental_authorization_allowed: storage_model.incremental_authorization_allowed, + authorization_count: storage_model.authorization_count, } } } @@ -851,6 +854,7 @@ impl DataModelExt for PaymentIntent { surcharge_applicable: self.surcharge_applicable, request_incremental_authorization: self.request_incremental_authorization, incremental_authorization_allowed: self.incremental_authorization_allowed, + authorization_count: self.authorization_count, } } @@ -894,6 +898,7 @@ impl DataModelExt for PaymentIntent { surcharge_applicable: storage_model.surcharge_applicable, request_incremental_authorization: storage_model.request_incremental_authorization, incremental_authorization_allowed: storage_model.incremental_authorization_allowed, + authorization_count: storage_model.authorization_count, } } } @@ -1038,6 +1043,14 @@ impl DataModelExt for PaymentIntentUpdate { surcharge_applicable: Some(surcharge_applicable), updated_by, }, + Self::IncrementalAuthorizationAmountUpdate { amount } => { + DieselPaymentIntentUpdate::IncrementalAuthorizationAmountUpdate { amount } + } + Self::AuthorizationCountUpdate { + authorization_count, + } => DieselPaymentIntentUpdate::AuthorizationCountUpdate { + authorization_count, + }, } } diff --git a/migrations/2023-11-30-170902_add-authorizations-table/down.sql b/migrations/2023-11-30-170902_add-authorizations-table/down.sql new file mode 100644 index 000000000000..476f16a52aab --- /dev/null +++ b/migrations/2023-11-30-170902_add-authorizations-table/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE IF EXISTS incremental_authorization; \ No newline at end of file diff --git a/migrations/2023-11-30-170902_add-authorizations-table/up.sql b/migrations/2023-11-30-170902_add-authorizations-table/up.sql new file mode 100644 index 000000000000..ade615877dc2 --- /dev/null +++ b/migrations/2023-11-30-170902_add-authorizations-table/up.sql @@ -0,0 +1,16 @@ +-- Your SQL goes here + +CREATE TABLE IF NOT EXISTS incremental_authorization ( + authorization_id VARCHAR(64) NOT NULL, + merchant_id VARCHAR(64) NOT NULL, + payment_id VARCHAR(64) NOT NULL, + amount BIGINT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP, + modified_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP, + status VARCHAR(64) NOT NULL, + error_code VARCHAR(255), + error_message TEXT, + connector_authorization_id VARCHAR(64), + previously_authorized_amount BIGINT NOT NULL, + PRIMARY KEY (authorization_id, merchant_id) +); \ No newline at end of file diff --git a/migrations/2023-12-01-090834_add-authorization_count-in-payment_intent/down.sql b/migrations/2023-12-01-090834_add-authorization_count-in-payment_intent/down.sql new file mode 100644 index 000000000000..8d4d0e6d81d2 --- /dev/null +++ b/migrations/2023-12-01-090834_add-authorization_count-in-payment_intent/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_intent DROP COLUMN IF EXISTS authorization_count; \ No newline at end of file diff --git a/migrations/2023-12-01-090834_add-authorization_count-in-payment_intent/up.sql b/migrations/2023-12-01-090834_add-authorization_count-in-payment_intent/up.sql new file mode 100644 index 000000000000..741135c6a1a3 --- /dev/null +++ b/migrations/2023-12-01-090834_add-authorization_count-in-payment_intent/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE payment_intent ADD COLUMN IF NOT EXISTS authorization_count INTEGER; diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index f5ad99f05752..4f26589ba029 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -2888,6 +2888,15 @@ "no_three_ds" ] }, + "AuthorizationStatus": { + "type": "string", + "enum": [ + "success", + "failure", + "processing", + "unresolved" + ] + }, "BacsBankTransfer": { "type": "object", "required": [ @@ -4316,6 +4325,11 @@ "type": "string", "description": "The card holder's name", "example": "John Test" + }, + "card_cvc": { + "type": "string", + "description": "The CVC number for the card", + "nullable": true } } }, @@ -6414,6 +6428,44 @@ } } }, + "IncrementalAuthorizationResponse": { + "type": "object", + "required": [ + "authorization_id", + "amount", + "status", + "previously_authorized_amount" + ], + "properties": { + "authorization_id": { + "type": "string", + "description": "The unique identifier of authorization" + }, + "amount": { + "type": "integer", + "format": "int64", + "description": "Amount the authorization has been made for" + }, + "status": { + "$ref": "#/components/schemas/AuthorizationStatus" + }, + "error_code": { + "type": "string", + "description": "Error code sent by the connector for authorization", + "nullable": true + }, + "error_message": { + "type": "string", + "description": "Error message sent by the connector for authorization", + "nullable": true + }, + "previously_authorized_amount": { + "type": "integer", + "format": "int64", + "description": "Previously authorized amount for the payment" + } + } + }, "IndomaretVoucherData": { "type": "object", "required": [ @@ -9545,7 +9597,8 @@ }, "card_cvc": { "type": "string", - "description": "This is used when payment is to be confirmed and the card is not saved", + "description": "This is used when payment is to be confirmed and the card is not saved.\nThis field will be deprecated soon, use the CardToken object instead", + "deprecated": true, "nullable": true }, "shipping": { @@ -9914,7 +9967,8 @@ }, "card_cvc": { "type": "string", - "description": "This is used when payment is to be confirmed and the card is not saved", + "description": "This is used when payment is to be confirmed and the card is not saved.\nThis field will be deprecated soon, use the CardToken object instead", + "deprecated": true, "nullable": true }, "shipping": { @@ -10533,6 +10587,20 @@ "type": "boolean", "description": "If true incremental authorization can be performed on this payment", "nullable": true + }, + "authorization_count": { + "type": "integer", + "format": "int32", + "description": "Total number of authorizations happened in an incremental_authorization payment", + "nullable": true + }, + "incremental_authorizations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IncrementalAuthorizationResponse" + }, + "description": "List of incremental authorizations happened to the payment", + "nullable": true } } },