diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index 59e65c0605..cd1671b2b8 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -11,10 +11,10 @@ use crate::{ ExtendedCardInfoResponse, PaymentIdType, PaymentListConstraints, PaymentListFilterConstraints, PaymentListFilters, PaymentListFiltersV2, PaymentListResponse, PaymentListResponseV2, PaymentsApproveRequest, PaymentsCancelRequest, - PaymentsCaptureRequest, PaymentsExternalAuthenticationRequest, - PaymentsExternalAuthenticationResponse, PaymentsIncrementalAuthorizationRequest, - PaymentsRejectRequest, PaymentsRequest, PaymentsResponse, PaymentsRetrieveRequest, - PaymentsStartRequest, RedirectionResponse, + PaymentsCaptureRequest, PaymentsCompleteAuthorizeRequest, + PaymentsExternalAuthenticationRequest, PaymentsExternalAuthenticationResponse, + PaymentsIncrementalAuthorizationRequest, PaymentsRejectRequest, PaymentsRequest, + PaymentsResponse, PaymentsRetrieveRequest, PaymentsStartRequest, RedirectionResponse, }, }; impl ApiEventMetric for PaymentsRetrieveRequest { @@ -44,6 +44,14 @@ impl ApiEventMetric for PaymentsCaptureRequest { } } +impl ApiEventMetric for PaymentsCompleteAuthorizeRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payment { + payment_id: self.payment_id.clone(), + }) + } +} + impl ApiEventMetric for PaymentsCancelRequest { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::Payment { diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index f00be78a03..8f4fca59d2 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -4343,6 +4343,18 @@ pub struct PaymentRetrieveBodyWithCredentials { pub merchant_connector_details: Option, } +#[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] +pub struct PaymentsCompleteAuthorizeRequest { + /// The unique identifier for the payment + #[serde(skip_deserializing)] + pub payment_id: String, + /// The shipping address for the payment + pub shipping: Option
, + /// Client Secret + #[schema(value_type = String)] + pub client_secret: Secret, +} + #[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] pub struct PaymentsCancelRequest { /// The identifier for the payment diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index d65fa6868b..660db4d8b8 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -202,6 +202,9 @@ pub enum PaymentIntentUpdate { AuthorizationCountUpdate { authorization_count: i32, }, + CompleteAuthorizeUpdate { + shipping_address_id: Option, + }, } #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] @@ -503,6 +506,12 @@ impl From for PaymentIntentUpdateInternal { authorization_count: Some(authorization_count), ..Default::default() }, + PaymentIntentUpdate::CompleteAuthorizeUpdate { + shipping_address_id, + } => Self { + shipping_address_id, + ..Default::default() + }, } } } diff --git a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs index 84dc56403c..1322e66a14 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs @@ -203,6 +203,9 @@ pub enum PaymentIntentUpdate { AuthorizationCountUpdate { authorization_count: i32, }, + CompleteAuthorizeUpdate { + shipping_address_id: Option, + }, } #[derive(Clone, Debug, Default)] @@ -425,6 +428,12 @@ impl From for PaymentIntentUpdateInternal { authorization_count: Some(authorization_count), ..Default::default() }, + PaymentIntentUpdate::CompleteAuthorizeUpdate { + shipping_address_id, + } => Self { + shipping_address_id, + ..Default::default() + }, } } } diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index 5f5597387b..018ea34209 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -81,6 +81,7 @@ Never share your secret api keys. Keep them guarded and secure. routes::payments::payments_incremental_authorization, routes::payment_link::payment_link_retrieve, routes::payments::payments_external_authentication, + routes::payments::payments_complete_authorize, // Routes for refunds routes::refunds::refunds_create, @@ -402,6 +403,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::CaptureResponse, api_models::payments::PaymentsIncrementalAuthorizationRequest, api_models::payments::IncrementalAuthorizationResponse, + api_models::payments::PaymentsCompleteAuthorizeRequest, api_models::payments::PaymentsExternalAuthenticationRequest, api_models::payments::PaymentsExternalAuthenticationResponse, api_models::payments::SdkInformation, diff --git a/crates/openapi/src/routes/payments.rs b/crates/openapi/src/routes/payments.rs index 20d87b91e7..1273bde7a3 100644 --- a/crates/openapi/src/routes/payments.rs +++ b/crates/openapi/src/routes/payments.rs @@ -486,3 +486,23 @@ pub fn payments_incremental_authorization() {} security(("publishable_key" = [])) )] pub fn payments_external_authentication() {} + +/// Payments - Complete Authorize +/// +/// +#[utoipa::path( + post, + path = "/{payment_id}/complete_authorize", + request_body=PaymentsCompleteAuthorizeRequest, + params( + ("payment_id" =String, Path, description = "The identifier for payment") + ), + responses( + (status = 200, description = "Payments Complete Authorize Success", body = PaymentsResponse), + (status = 400, description = "Missing mandatory fields") + ), + tag = "Payments", + operation_id = "Complete Authorize a Payment", + security(("publishable_key" = [])) +)] +pub fn payments_complete_authorize() {} 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 a973651672..3f57ba2830 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -59,6 +59,8 @@ impl GetTracker, api::PaymentsRequest> for Co .setup_future_usage .or(payment_intent.setup_future_usage); + helpers::authenticate_client_secret(request.client_secret.as_ref(), &payment_intent)?; + helpers::validate_payment_status_against_not_allowed_statuses( &payment_intent.status, &[ @@ -173,16 +175,22 @@ impl GetTracker, api::PaymentsRequest> for Co .or_else(|| request.customer_id.clone()), )?; - let shipping_address = helpers::get_address_by_id( + let shipping_address = helpers::create_or_update_address_for_payment_by_request( db, - payment_intent.shipping_address_id.clone(), + request.shipping.as_ref(), + payment_intent.shipping_address_id.clone().as_deref(), + merchant_id.as_ref(), + payment_intent.customer_id.as_ref(), key_store, - &payment_intent.payment_id, - merchant_id, - merchant_account.storage_scheme, + payment_id.as_ref(), + storage_scheme, ) .await?; + payment_intent.shipping_address_id = shipping_address + .as_ref() + .map(|shipping_address| shipping_address.address_id.clone()); + let billing_address = helpers::get_address_by_id( db, payment_intent.billing_address_id.clone(), @@ -421,11 +429,11 @@ impl UpdateTracker, api::PaymentsRequest> for Comple #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - _state: &'b AppState, + state: &'b AppState, _req_state: ReqState, - payment_data: PaymentData, + mut payment_data: PaymentData, _customer: Option, - _storage_scheme: storage_enums::MerchantStorageScheme, + storage_scheme: storage_enums::MerchantStorageScheme, _updated_customer: Option, _merchant_key_store: &domain::MerchantKeyStore, _frm_suggestion: Option, @@ -434,6 +442,19 @@ impl UpdateTracker, api::PaymentsRequest> for Comple where F: 'b + Send, { + let payment_intent_update = hyperswitch_domain_models::payments::payment_intent::PaymentIntentUpdate::CompleteAuthorizeUpdate { + shipping_address_id: payment_data.payment_intent.shipping_address_id.clone() + }; + + let db = &*state.store; + let payment_intent = payment_data.payment_intent.clone(); + + let updated_payment_intent = db + .update_payment_intent(payment_intent, payment_intent_update, storage_scheme) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + payment_data.payment_intent = updated_payment_intent; Ok((Box::new(self), payment_data)) } } diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index f4eded24be..c00c0e17ff 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -386,7 +386,11 @@ impl Payments { ) .service( web::resource("/{payment_id}/{merchant_id}/redirect/complete/{connector}") - .route(web::get().to(payments_complete_authorize)) + .route(web::get().to(payments_complete_authorize_redirect)) + .route(web::post().to(payments_complete_authorize_redirect)), + ) + .service( + web::resource("/{payment_id}/complete_authorize") .route(web::post().to(payments_complete_authorize)), ) .service( diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 30b582079e..6a73643ce6 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -121,7 +121,8 @@ impl From for ApiIdentifier { | Flow::PaymentsIncrementalAuthorization | Flow::PaymentsExternalAuthentication | Flow::PaymentsAuthorize - | Flow::GetExtendedCardInfo => Self::Payments, + | Flow::GetExtendedCardInfo + | Flow::PaymentsCompleteAuthorize => Self::Payments, Flow::PayoutsCreate | Flow::PayoutsRetrieve diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 06bdd3ed05..f28cad8900 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -7,6 +7,7 @@ pub mod helpers; use actix_web::{web, Responder}; use api_models::payments::HeaderPayload; use error_stack::report; +use masking::PeekInterface; use router_env::{env, instrument, tracing, types, Flow}; use super::app::ReqState; @@ -749,7 +750,7 @@ pub async fn payments_redirect_response_with_creds_identifier( .await } #[instrument(skip_all, fields(flow =? Flow::PaymentsRedirect, payment_id))] -pub async fn payments_complete_authorize( +pub async fn payments_complete_authorize_redirect( state: web::Data, req: actix_web::HttpRequest, json_payload: Option>, @@ -792,6 +793,70 @@ pub async fn payments_complete_authorize( ) .await } + +/// Payments - Complete Authorize +#[instrument(skip_all, fields(flow =? Flow::PaymentsCompleteAuthorize, payment_id))] +pub async fn payments_complete_authorize( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + path: web::Path, +) -> impl Responder { + let flow = Flow::PaymentsCompleteAuthorize; + let mut payload = json_payload.into_inner(); + + let payment_id = path.into_inner(); + payload.payment_id.clone_from(&payment_id); + + tracing::Span::current().record("payment_id", &payment_id); + + let payment_confirm_req = payment_types::PaymentsRequest { + payment_id: Some(payment_types::PaymentIdType::PaymentIntentId( + payment_id.clone(), + )), + shipping: payload.shipping.clone(), + client_secret: Some(payload.client_secret.peek().clone()), + ..Default::default() + }; + + let (auth_type, auth_flow) = + match auth::check_client_secret_and_get_auth(req.headers(), &payment_confirm_req) { + Ok(auth) => auth, + Err(err) => return api::log_and_return_error_response(report!(err)), + }; + + let locking_action = payload.get_locking_input(flow.clone()); + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth, _req, req_state| { + payments::payments_core::< + api_types::CompleteAuthorize, + payment_types::PaymentsResponse, + _, + _, + _, + >( + state.clone(), + req_state, + auth.merchant_account, + auth.key_store, + payments::operations::payment_complete_authorize::CompleteAuthorize, + payment_confirm_req.clone(), + auth_flow, + payments::CallConnectorAction::Trigger, + None, + HeaderPayload::default(), + ) + }, + &*auth_type, + locking_action, + )) + .await +} + /// Payments - Cancel /// /// A Payment could can be cancelled when it is in one of these statuses: requires_payment_method, requires_capture, requires_confirmation, requires_customer_action @@ -1465,6 +1530,22 @@ impl GetLockingInput for payments::PaymentsRedirectResponseData { } } +impl GetLockingInput for payment_types::PaymentsCompleteAuthorizeRequest { + 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, + }, + } + } +} + impl GetLockingInput for payment_types::PaymentsCancelRequest { fn get_locking_input(&self, flow: F) -> api_locking::LockAction where diff --git a/crates/router/src/types/api/payments.rs b/crates/router/src/types/api/payments.rs index 9f68cac98c..a220193629 100644 --- a/crates/router/src/types/api/payments.rs +++ b/crates/router/src/types/api/payments.rs @@ -6,12 +6,13 @@ pub use api_models::payments::{ PaymentListFilters, PaymentListFiltersV2, PaymentListResponse, PaymentListResponseV2, PaymentMethodData, PaymentMethodDataRequest, PaymentMethodDataResponse, PaymentOp, PaymentRetrieveBody, PaymentRetrieveBodyWithCredentials, PaymentsApproveRequest, - PaymentsCancelRequest, PaymentsCaptureRequest, PaymentsExternalAuthenticationRequest, - PaymentsIncrementalAuthorizationRequest, PaymentsRedirectRequest, PaymentsRedirectionResponse, - PaymentsRejectRequest, PaymentsRequest, PaymentsResponse, PaymentsResponseForm, - PaymentsRetrieveRequest, PaymentsSessionRequest, PaymentsSessionResponse, PaymentsStartRequest, - PgRedirectResponse, PhoneDetails, RedirectionResponse, SessionToken, TimeRange, UrlDetails, - VerifyRequest, VerifyResponse, WalletData, + PaymentsCancelRequest, PaymentsCaptureRequest, PaymentsCompleteAuthorizeRequest, + PaymentsExternalAuthenticationRequest, PaymentsIncrementalAuthorizationRequest, + PaymentsRedirectRequest, PaymentsRedirectionResponse, PaymentsRejectRequest, PaymentsRequest, + PaymentsResponse, PaymentsResponseForm, PaymentsRetrieveRequest, PaymentsSessionRequest, + PaymentsSessionResponse, PaymentsStartRequest, PgRedirectResponse, PhoneDetails, + RedirectionResponse, SessionToken, TimeRange, UrlDetails, VerifyRequest, VerifyResponse, + WalletData, }; use error_stack::ResultExt; diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index ffa492bc60..07a45f5cdb 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -180,6 +180,8 @@ pub enum Flow { PayoutsAccounts, /// Payments Redirect flow. PaymentsRedirect, + /// Payemnts Complete Authorize Flow + PaymentsCompleteAuthorize, /// Refunds create flow. RefundsCreate, /// Refunds retrieve flow. diff --git a/crates/storage_impl/src/payments/payment_intent.rs b/crates/storage_impl/src/payments/payment_intent.rs index b3cfd8060c..0ac963ae81 100644 --- a/crates/storage_impl/src/payments/payment_intent.rs +++ b/crates/storage_impl/src/payments/payment_intent.rs @@ -1162,6 +1162,11 @@ impl DataModelExt for PaymentIntentUpdate { } => DieselPaymentIntentUpdate::AuthorizationCountUpdate { authorization_count, }, + Self::CompleteAuthorizeUpdate { + shipping_address_id, + } => DieselPaymentIntentUpdate::CompleteAuthorizeUpdate { + shipping_address_id, + }, } } diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index a3cd88d2ee..2365f165d6 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -853,6 +853,57 @@ ] } }, + "/{payment_id}/complete_authorize": { + "post": { + "tags": [ + "Payments" + ], + "summary": "Payments - Complete Authorize", + "description": "Payments - Complete Authorize\n\n", + "operationId": "Complete Authorize a Payment", + "parameters": [ + { + "name": "payment_id", + "in": "path", + "description": "The identifier for payment", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsCompleteAuthorizeRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Payments Complete Authorize Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsResponse" + } + } + } + }, + "400": { + "description": "Missing mandatory fields" + } + }, + "security": [ + { + "publishable_key": [] + } + ] + } + }, "/refunds": { "post": { "tags": [ @@ -13522,6 +13573,26 @@ } } }, + "PaymentsCompleteAuthorizeRequest": { + "type": "object", + "required": [ + "client_secret" + ], + "properties": { + "shipping": { + "allOf": [ + { + "$ref": "#/components/schemas/Address" + } + ], + "nullable": true + }, + "client_secret": { + "type": "string", + "description": "Client Secret" + } + } + }, "PaymentsConfirmRequest": { "type": "object", "properties": {