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/payments.rs b/crates/api_models/src/payments.rs index eaf0937ef2a2..0da6822f1501 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -2209,6 +2209,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)] @@ -2277,6 +2283,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 @@ -2985,6 +3009,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/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/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 40d8fd92caeb..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( @@ -110,6 +111,7 @@ pub struct PaymentIntentNew { pub surcharge_applicable: Option, pub request_incremental_authorization: RequestIncrementalAuthorization, pub incremental_authorization_allowed: Option, + pub authorization_count: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -187,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)] @@ -220,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 { @@ -251,6 +260,7 @@ impl PaymentIntentUpdate { updated_by, surcharge_applicable, incremental_authorization_allowed, + authorization_count, } = self.into(); PaymentIntent { amount: amount.unwrap_or(source.amount), @@ -282,6 +292,7 @@ impl PaymentIntentUpdate { surcharge_applicable: surcharge_applicable.or(source.surcharge_applicable), incremental_authorization_allowed, + authorization_count, ..source } } @@ -448,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/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index 1de107af086d..631b2f8c97ed 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 {} @@ -872,6 +876,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 953f82c76a83..d3f542d2013a 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 { @@ -654,6 +746,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/payments.rs b/crates/router/src/core/payments.rs index 67b1dbcc98b2..8bc251f9c3d3 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -32,8 +32,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, @@ -1884,6 +1885,16 @@ where 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)] @@ -1984,6 +1995,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 59b2b53c654d..3e01a7b193d0 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -2417,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], @@ -2612,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); @@ -2665,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); @@ -2718,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); diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index 809c9e925de0..93db8f03ff5c 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::{ 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..91dd5be40f64 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -479,6 +479,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_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index bb7d0a931e1b..f3cd726a17c7 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -323,6 +323,8 @@ impl surcharge_details, frm_message: None, payment_link_data, + incremental_authorization_details: None, + authorizations: vec![], }; let get_trackers_response = operations::GetTrackerResponse { @@ -757,6 +759,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 68f064c539d1..e1c373171682 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -373,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 { diff --git a/crates/router/src/core/payments/operations/payments_incremental_authorization.rs b/crates/router/src/core/payments/operations/payments_incremental_authorization.rs new file mode 100644 index 000000000000..7346c46df120 --- /dev/null +++ b/crates/router/src/core/payments/operations/payments_incremental_authorization.rs @@ -0,0 +1,328 @@ +use std::marker::PhantomData; + +use api_models::{enums::FrmSuggestion, payments::PaymentsIncrementalAuthorizationRequest}; +use async_trait::async_trait; +use common_utils::errors::CustomResult; +use diesel_models::authorization::AuthorizationNew; +use error_stack::{report, IntoReport, ResultExt}; +use router_env::{instrument, tracing}; + +use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; +use crate::{ + core::{ + errors::{self, RouterResult, StorageErrorExt}, + payment_methods::PaymentMethodRetrieve, + payments::{ + self, helpers, operations, CustomerDetails, IncrementalAuthorizationDetails, + PaymentAddress, + }, + }, + routes::{app::StorageInterface, AppState}, + services, + types::{ + api::{self, PaymentIdTypeExt}, + domain, + storage::{self, enums}, + }, + utils::OptionExt, +}; + +#[derive(Debug, Clone, Copy, router_derive::PaymentOperation)] +#[operation(operations = "all", flow = "incremental_authorization")] +pub struct PaymentIncrementalAuthorization; + +#[async_trait] +impl + 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/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 88806b565d3a..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 diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index b32dbe3d4b6a..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 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/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 595f487e7079..08cbb36952e3 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -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, @@ -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 { @@ -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/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/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/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 b1e313f15baa..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": [ @@ -6419,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": [ @@ -10540,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 } } },