diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index f639e093e6..4ed4e00666 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -14,8 +14,9 @@ use crate::{ PaymentListResponse, PaymentListResponseV2, PaymentsApproveRequest, PaymentsCancelRequest, PaymentsCaptureRequest, PaymentsCompleteAuthorizeRequest, PaymentsExternalAuthenticationRequest, PaymentsExternalAuthenticationResponse, - PaymentsIncrementalAuthorizationRequest, PaymentsRejectRequest, PaymentsRequest, - PaymentsResponse, PaymentsRetrieveRequest, PaymentsStartRequest, RedirectionResponse, + PaymentsIncrementalAuthorizationRequest, PaymentsManualUpdateRequest, + PaymentsRejectRequest, PaymentsRequest, PaymentsResponse, PaymentsRetrieveRequest, + PaymentsStartRequest, RedirectionResponse, }, }; impl ApiEventMetric for PaymentsRetrieveRequest { @@ -239,3 +240,11 @@ impl ApiEventMetric for PaymentsExternalAuthenticationRequest { } impl ApiEventMetric for ExtendedCardInfoResponse {} + +impl ApiEventMetric for PaymentsManualUpdateRequest { + 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 1d4418bee1..777e876b1b 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -4653,6 +4653,25 @@ pub struct PaymentsExternalAuthenticationRequest { pub threeds_method_comp_ind: ThreeDsCompletionIndicator, } +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, ToSchema)] +pub struct PaymentsManualUpdateRequest { + /// The identifier for the payment + #[serde(skip)] + pub payment_id: String, + /// The identifier for the payment attempt + pub attempt_id: String, + /// Merchant ID + pub merchant_id: String, + /// The status of the attempt + pub attempt_status: Option, + /// Error code of the connector + pub error_code: Option, + /// Error message of the connector + pub error_message: Option, + /// Error reason of the connector + pub error_reason: Option, +} + #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, ToSchema)] pub enum ThreeDsCompletionIndicator { /// 3DS method successfully completed diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 1b62473b9a..0aeef652d0 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -351,6 +351,15 @@ pub enum PaymentAttemptUpdate { authentication_id: Option, updated_by: String, }, + ManualUpdate { + status: Option, + error_code: Option, + error_message: Option, + error_reason: Option, + updated_by: String, + unified_code: Option, + unified_message: Option, + }, } #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] @@ -884,6 +893,24 @@ impl From for PaymentAttemptUpdateInternal { updated_by, ..Default::default() }, + PaymentAttemptUpdate::ManualUpdate { + status, + error_code, + error_message, + error_reason, + updated_by, + unified_code, + unified_message, + } => Self { + status, + error_code: error_code.map(Some), + error_message: error_message.map(Some), + error_reason: error_reason.map(Some), + updated_by, + unified_code: unified_code.map(Some), + unified_message: unified_message.map(Some), + ..Default::default() + }, } } } diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 7fad83e7a0..6326eeef64 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -206,6 +206,10 @@ pub enum PaymentIntentUpdate { CompleteAuthorizeUpdate { shipping_address_id: Option, }, + ManualUpdate { + status: Option, + updated_by: String, + }, } #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] @@ -508,6 +512,11 @@ impl From for PaymentIntentUpdateInternal { shipping_address_id, ..Default::default() }, + PaymentIntentUpdate::ManualUpdate { status, updated_by } => Self { + status, + updated_by, + ..Default::default() + }, } } } diff --git a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs index d8b5288c85..4f50948b48 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs @@ -454,6 +454,15 @@ pub enum PaymentAttemptUpdate { authentication_id: Option, updated_by: String, }, + ManualUpdate { + status: Option, + error_code: Option, + error_message: Option, + error_reason: Option, + updated_by: String, + unified_code: Option, + unified_message: Option, + }, } impl ForeignIDRef for PaymentAttempt { diff --git a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs index 2c99360d4f..66ecf7ec57 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs @@ -214,6 +214,10 @@ pub enum PaymentIntentUpdate { CompleteAuthorizeUpdate { shipping_address_id: Option, }, + ManualUpdate { + status: Option, + updated_by: String, + }, } #[derive(Clone, Debug, Default)] @@ -442,6 +446,12 @@ impl From for PaymentIntentUpdateInternal { shipping_address_id, ..Default::default() }, + PaymentIntentUpdate::ManualUpdate { status, updated_by } => Self { + status, + modified_at: Some(common_utils::date_time::now()), + updated_by, + ..Default::default() + }, } } } @@ -611,6 +621,9 @@ impl From for DieselPaymentIntentUpdate { } => Self::CompleteAuthorizeUpdate { shipping_address_id, }, + PaymentIntentUpdate::ManualUpdate { status, updated_by } => { + Self::ManualUpdate { status, updated_by } + } } } } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 78c9ca5cd5..459ada8fb4 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -4134,3 +4134,115 @@ pub async fn get_extended_card_info( payments_api::ExtendedCardInfoResponse { payload }, )) } + +#[cfg(feature = "olap")] +pub async fn payments_manual_update( + state: SessionState, + req: api_models::payments::PaymentsManualUpdateRequest, +) -> RouterResponse { + let api_models::payments::PaymentsManualUpdateRequest { + payment_id, + attempt_id, + merchant_id, + attempt_status, + error_code, + error_message, + error_reason, + } = req; + let key_store = state + .store + .get_merchant_key_store_by_merchant_id( + &merchant_id, + &state.store.get_master_key().to_vec().into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound) + .attach_printable("Error while fetching the key store by merchant_id")?; + let merchant_account = state + .store + .find_merchant_account_by_merchant_id(&merchant_id, &key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound) + .attach_printable("Error while fetching the merchant_account by merchant_id")?; + let payment_attempt = state + .store + .find_payment_attempt_by_payment_id_merchant_id_attempt_id( + &payment_id, + &merchant_id, + &attempt_id.clone(), + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) + .attach_printable( + "Error while fetching the payment_attempt by payment_id, merchant_id and attempt_id", + )?; + let payment_intent = state + .store + .find_payment_intent_by_payment_id_merchant_id( + &payment_id, + &merchant_account.merchant_id, + &key_store, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) + .attach_printable("Error while fetching the payment_intent by payment_id, merchant_id")?; + let option_gsm = if let Some(((code, message), connector_name)) = error_code + .as_ref() + .zip(error_message.as_ref()) + .zip(payment_attempt.connector.as_ref()) + { + helpers::get_gsm_record( + &state, + Some(code.to_string()), + Some(message.to_string()), + connector_name.to_string(), + // We need to get the unified_code and unified_message of the Authorize flow + "Authorize".to_string(), + ) + .await + } else { + None + }; + // Update the payment_attempt + let attempt_update = storage::PaymentAttemptUpdate::ManualUpdate { + status: attempt_status, + error_code, + error_message, + error_reason, + updated_by: merchant_account.storage_scheme.to_string(), + unified_code: option_gsm.as_ref().and_then(|gsm| gsm.unified_code.clone()), + unified_message: option_gsm.and_then(|gsm| gsm.unified_message), + }; + let updated_payment_attempt = state + .store + .update_payment_attempt_with_attempt_id( + payment_attempt.clone(), + attempt_update, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) + .attach_printable("Error while updating the payment_attempt")?; + // If the payment_attempt is active attempt for an intent, update the intent status + if payment_intent.active_attempt.get_id() == payment_attempt.attempt_id { + let intent_status = enums::IntentStatus::foreign_from(updated_payment_attempt.status); + let payment_intent_update = storage::PaymentIntentUpdate::ManualUpdate { + status: Some(intent_status), + updated_by: merchant_account.storage_scheme.to_string(), + }; + state + .store + .update_payment_intent( + payment_intent, + payment_intent_update, + &key_store, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) + .attach_printable("Error while updating payment_intent")?; + } + Ok(services::ApplicationResponse::StatusOk) +} diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index b91129f457..0124f6ae6a 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -460,6 +460,10 @@ impl Payments { ) .service(web::resource("/filter").route(web::post().to(get_filters_for_payments))) .service(web::resource("/v2/filter").route(web::get().to(get_payment_filters))) + .service( + web::resource("/{payment_id}/manual-update") + .route(web::put().to(payments_manual_update)), + ) } #[cfg(feature = "oltp")] { diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index b18720652c..e5c4f1c8e9 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -124,7 +124,8 @@ impl From for ApiIdentifier { | Flow::PaymentsExternalAuthentication | Flow::PaymentsAuthorize | Flow::GetExtendedCardInfo - | Flow::PaymentsCompleteAuthorize => Self::Payments, + | Flow::PaymentsCompleteAuthorize + | Flow::PaymentsManualUpdate => Self::Payments, Flow::PayoutsCreate | Flow::PayoutsRetrieve diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index e6e4d23867..912421f465 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -1394,6 +1394,35 @@ pub async fn post_3ds_payments_authorize( .await } +#[cfg(feature = "olap")] +pub async fn payments_manual_update( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + path: web::Path, +) -> impl Responder { + let flow = Flow::PaymentsManualUpdate; + let mut payload = json_payload.into_inner(); + let payment_id = path.into_inner(); + + let locking_action = payload.get_locking_input(flow.clone()); + + tracing::Span::current().record("payment_id", &payment_id); + + payload.payment_id = payment_id; + + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, _auth, req, _req_state| payments::payments_manual_update(state, req), + &auth::AdminApiAuth, + locking_action, + )) + .await +} + /// Retrieve endpoint for merchant to fetch the encrypted customer payment method data #[instrument(skip_all, fields(flow = ?Flow::GetExtendedCardInfo, payment_id))] pub async fn retrieve_extended_card_info( @@ -1654,3 +1683,19 @@ impl GetLockingInput for payment_types::PaymentsExternalAuthenticationRequest { } } } + +impl GetLockingInput for payment_types::PaymentsManualUpdateRequest { + 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/types/api/payments.rs b/crates/router/src/types/api/payments.rs index d830383122..e1dbf452ef 100644 --- a/crates/router/src/types/api/payments.rs +++ b/crates/router/src/types/api/payments.rs @@ -8,11 +8,11 @@ pub use api_models::payments::{ PaymentRetrieveBody, PaymentRetrieveBodyWithCredentials, PaymentsApproveRequest, PaymentsCancelRequest, PaymentsCaptureRequest, PaymentsCompleteAuthorizeRequest, PaymentsExternalAuthenticationRequest, PaymentsIncrementalAuthorizationRequest, - PaymentsRedirectRequest, PaymentsRedirectionResponse, PaymentsRejectRequest, PaymentsRequest, - PaymentsResponse, PaymentsResponseForm, PaymentsRetrieveRequest, PaymentsSessionRequest, - PaymentsSessionResponse, PaymentsStartRequest, PgRedirectResponse, PhoneDetails, - RedirectionResponse, SessionToken, TimeRange, UrlDetails, VerifyRequest, VerifyResponse, - WalletData, + PaymentsManualUpdateRequest, PaymentsRedirectRequest, PaymentsRedirectionResponse, + PaymentsRejectRequest, PaymentsRequest, PaymentsResponse, PaymentsResponseForm, + PaymentsRetrieveRequest, PaymentsSessionRequest, PaymentsSessionResponse, PaymentsStartRequest, + PgRedirectResponse, PhoneDetails, RedirectionResponse, SessionToken, TimeRange, UrlDetails, + VerifyRequest, VerifyResponse, WalletData, }; use error_stack::ResultExt; pub use hyperswitch_domain_models::router_flow_types::payments::{ diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 0d146a9eb2..46bb93f06e 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -446,6 +446,8 @@ pub enum Flow { ToggleConnectorAgnosticMit, /// Get the extended card info associated to a payment_id GetExtendedCardInfo, + /// Manually update the payment details like status, error code, error message etc. + PaymentsManualUpdate, } /// diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index 4828c8b370..f464ef66a1 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -1748,6 +1748,23 @@ impl DataModelExt for PaymentAttemptUpdate { authentication_id, updated_by, }, + Self::ManualUpdate { + status, + error_code, + error_message, + error_reason, + updated_by, + unified_code, + unified_message, + } => DieselPaymentAttemptUpdate::ManualUpdate { + status, + error_code, + error_message, + error_reason, + updated_by, + unified_code, + unified_message, + }, } } @@ -2079,6 +2096,23 @@ impl DataModelExt for PaymentAttemptUpdate { authentication_id, updated_by, }, + DieselPaymentAttemptUpdate::ManualUpdate { + status, + error_code, + error_message, + error_reason, + updated_by, + unified_code, + unified_message, + } => Self::ManualUpdate { + status, + error_code, + error_message, + error_reason, + updated_by, + unified_code, + unified_message, + }, } } }