From a0f4bb771b583a8dad2a58158c64b7a8baff24d5 Mon Sep 17 00:00:00 2001 From: Kashif Date: Thu, 19 Sep 2024 18:55:12 +0530 Subject: [PATCH] feat(payout): add unified error code and messages along with translation (#5810) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference-v2/openapi_spec.json | 26 +- api-reference/openapi_spec.json | 26 +- crates/api_models/src/payouts.rs | 33 ++- crates/common_utils/src/consts.rs | 3 + crates/common_utils/src/types.rs | 106 ++++++++- crates/diesel_models/src/payout_attempt.rs | 19 ++ crates/diesel_models/src/schema.rs | 4 + crates/diesel_models/src/schema_v2.rs | 4 + .../src/payouts/payout_attempt.rs | 18 +- .../adyenplatform/transformers/payouts.rs | 10 +- .../generic_link/payout_link/status/script.js | 4 +- .../payout_link/status/styles.css | 3 +- crates/router/src/core/payout_link.rs | 19 +- crates/router/src/core/payouts.rs | 225 ++++++++++++++++-- crates/router/src/core/payouts/helpers.rs | 37 ++- crates/router/src/core/payouts/retry.rs | 13 +- .../router/src/core/payouts/transformers.rs | 4 +- crates/router/src/core/webhooks/incoming.rs | 16 +- crates/router/src/routes/payouts.rs | 60 ++++- .../attach_payout_account_workflow.rs | 16 +- .../src/workflows/outgoing_webhook_retry.rs | 19 +- .../src/payouts/payout_attempt.rs | 14 ++ .../down.sql | 3 + .../up.sql | 3 + 24 files changed, 605 insertions(+), 80 deletions(-) create mode 100644 migrations/2024-09-03-053218_add_unified_code_message_to_payout/down.sql create mode 100644 migrations/2024-09-03-053218_add_unified_code_message_to_payout/up.sql diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 558a319828..5465d90238 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -15606,13 +15606,17 @@ }, "unified_code": { "type": "string", - "description": "error code unified across the connectors is received here if there was an error while calling connector", - "nullable": true + "description": "(This field is not live yet)\nError code unified across the connectors is received here in case of errors while calling the underlying connector", + "example": "UE_000", + "nullable": true, + "maxLength": 255 }, "unified_message": { "type": "string", - "description": "error message unified across the connectors is received here if there was an error while calling connector", - "nullable": true + "description": "(This field is not live yet)\nError message unified across the connectors is received here in case of errors while calling the underlying connector", + "example": "Invalid card details", + "nullable": true, + "maxLength": 1024 } } }, @@ -16126,6 +16130,20 @@ "example": "+1", "nullable": true, "maxLength": 255 + }, + "unified_code": { + "type": "string", + "description": "(This field is not live yet)\nError code unified across the connectors is received here in case of errors while calling the underlying connector", + "example": "UE_000", + "nullable": true, + "maxLength": 255 + }, + "unified_message": { + "type": "string", + "description": "(This field is not live yet)\nError message unified across the connectors is received here in case of errors while calling the underlying connector", + "example": "Invalid card details", + "nullable": true, + "maxLength": 1024 } }, "additionalProperties": false diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 8214d3a10b..ff1a93454c 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -19417,13 +19417,17 @@ }, "unified_code": { "type": "string", - "description": "error code unified across the connectors is received here if there was an error while calling connector", - "nullable": true + "description": "(This field is not live yet)\nError code unified across the connectors is received here in case of errors while calling the underlying connector", + "example": "UE_000", + "nullable": true, + "maxLength": 255 }, "unified_message": { "type": "string", - "description": "error message unified across the connectors is received here if there was an error while calling connector", - "nullable": true + "description": "(This field is not live yet)\nError message unified across the connectors is received here in case of errors while calling the underlying connector", + "example": "Invalid card details", + "nullable": true, + "maxLength": 1024 } } }, @@ -19930,6 +19934,20 @@ "example": "+1", "nullable": true, "maxLength": 255 + }, + "unified_code": { + "type": "string", + "description": "(This field is not live yet)\nError code unified across the connectors is received here in case of errors while calling the underlying connector", + "example": "UE_000", + "nullable": true, + "maxLength": 255 + }, + "unified_message": { + "type": "string", + "description": "(This field is not live yet)\nError message unified across the connectors is received here in case of errors while calling the underlying connector", + "example": "Invalid card details", + "nullable": true, + "maxLength": 1024 } }, "additionalProperties": false diff --git a/crates/api_models/src/payouts.rs b/crates/api_models/src/payouts.rs index 01bb63642b..10771df4cc 100644 --- a/crates/api_models/src/payouts.rs +++ b/crates/api_models/src/payouts.rs @@ -5,6 +5,7 @@ use common_utils::{ consts::default_payouts_list_limit, crypto, id_type, link_utils, pii::{self, Email}, + types::{UnifiedCode, UnifiedMessage}, }; use masking::Secret; use router_derive::FlatStruct; @@ -383,7 +384,7 @@ pub struct Venmo { pub telephone_number: Option>, } -#[derive(Debug, ToSchema, Clone, Serialize)] +#[derive(Debug, ToSchema, Clone, Serialize, router_derive::PolymorphicSchema)] #[serde(deny_unknown_fields)] pub struct PayoutCreateResponse { /// Unique identifier for the payout. This ensures idempotency for multiple payouts @@ -535,6 +536,18 @@ pub struct PayoutCreateResponse { /// Customer's phone country code. _Deprecated: Use customer object instead._ #[schema(deprecated, max_length = 255, example = "+1")] pub phone_country_code: Option, + + /// (This field is not live yet) + /// Error code unified across the connectors is received here in case of errors while calling the underlying connector + #[remove_in(PayoutCreateResponse)] + #[schema(value_type = Option, max_length = 255, example = "UE_000")] + pub unified_code: Option, + + /// (This field is not live yet) + /// Error message unified across the connectors is received here in case of errors while calling the underlying connector + #[remove_in(PayoutCreateResponse)] + #[schema(value_type = Option, max_length = 1024, example = "Invalid card details")] + pub unified_message: Option, } #[derive( @@ -568,10 +581,16 @@ pub struct PayoutAttemptResponse { pub connector_transaction_id: Option, /// If the payout was cancelled the reason provided here pub cancellation_reason: Option, - /// error code unified across the connectors is received here if there was an error while calling connector - pub unified_code: Option, - /// error message unified across the connectors is received here if there was an error while calling connector - pub unified_message: Option, + /// (This field is not live yet) + /// Error code unified across the connectors is received here in case of errors while calling the underlying connector + #[remove_in(PayoutAttemptResponse)] + #[schema(value_type = Option, max_length = 255, example = "UE_000")] + pub unified_code: Option, + /// (This field is not live yet) + /// Error message unified across the connectors is received here in case of errors while calling the underlying connector + #[remove_in(PayoutAttemptResponse)] + #[schema(value_type = Option, max_length = 1024, example = "Invalid card details")] + pub unified_message: Option, } #[derive(Default, Debug, Clone, Deserialize, ToSchema)] @@ -819,8 +838,8 @@ pub struct PayoutLinkStatusDetails { pub session_expiry: PrimitiveDateTime, pub return_url: Option, pub status: api_enums::PayoutStatus, - pub error_code: Option, - pub error_message: Option, + pub error_code: Option, + pub error_message: Option, #[serde(flatten)] pub ui_config: link_utils::GenericLinkUiConfigFormData, pub test_mode: bool, diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index 299e70dc4c..70f00ad9b3 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -146,3 +146,6 @@ pub const ROLE_ID_ORGANIZATION_ADMIN: &str = "org_admin"; pub const ROLE_ID_INTERNAL_VIEW_ONLY_USER: &str = "internal_view_only"; /// Role ID for Internal Admin pub const ROLE_ID_INTERNAL_ADMIN: &str = "internal_admin"; + +/// Payout flow identifier used for performing GSM operations +pub const PAYOUT_FLOW_STR: &str = "payout_flow"; diff --git a/crates/common_utils/src/types.rs b/crates/common_utils/src/types.rs index 7f2f844b03..6acf5000ce 100644 --- a/crates/common_utils/src/types.rs +++ b/crates/common_utils/src/types.rs @@ -32,7 +32,7 @@ use utoipa::ToSchema; use crate::{ consts, - errors::{CustomResult, ParsingError, PercentageError}, + errors::{CustomResult, ParsingError, PercentageError, ValidationError}, }; /// Represents Percentage Value between 0 and 100 both inclusive #[derive(Clone, Default, Debug, PartialEq, Serialize)] @@ -767,3 +767,107 @@ where self.0.to_sql(out) } } + +/// Domain type for unified code +#[derive( + Debug, Clone, PartialEq, Eq, Queryable, serde::Deserialize, serde::Serialize, AsExpression, +)] +#[diesel(sql_type = sql_types::Text)] +pub struct UnifiedCode(pub String); + +impl TryFrom for UnifiedCode { + type Error = error_stack::Report; + fn try_from(src: String) -> Result { + if src.len() > 255 { + Err(report!(ValidationError::InvalidValue { + message: "unified_code's length should not exceed 255 characters".to_string() + })) + } else { + Ok(Self(src)) + } + } +} + +impl Queryable for UnifiedCode +where + DB: Backend, + Self: FromSql, +{ + type Row = Self; + + fn build(row: Self::Row) -> deserialize::Result { + Ok(row) + } +} +impl FromSql for UnifiedCode +where + DB: Backend, + String: FromSql, +{ + fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result { + let val = String::from_sql(bytes)?; + Ok(Self::try_from(val)?) + } +} + +impl ToSql for UnifiedCode +where + DB: Backend, + String: ToSql, +{ + fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> diesel::serialize::Result { + self.0.to_sql(out) + } +} + +/// Domain type for unified messages +#[derive( + Debug, Clone, PartialEq, Eq, Queryable, serde::Deserialize, serde::Serialize, AsExpression, +)] +#[diesel(sql_type = sql_types::Text)] +pub struct UnifiedMessage(pub String); + +impl TryFrom for UnifiedMessage { + type Error = error_stack::Report; + fn try_from(src: String) -> Result { + if src.len() > 1024 { + Err(report!(ValidationError::InvalidValue { + message: "unified_message's length should not exceed 1024 characters".to_string() + })) + } else { + Ok(Self(src)) + } + } +} + +impl Queryable for UnifiedMessage +where + DB: Backend, + Self: FromSql, +{ + type Row = Self; + + fn build(row: Self::Row) -> deserialize::Result { + Ok(row) + } +} +impl FromSql for UnifiedMessage +where + DB: Backend, + String: FromSql, +{ + fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result { + let val = String::from_sql(bytes)?; + Ok(Self::try_from(val)?) + } +} + +impl ToSql for UnifiedMessage +where + DB: Backend, + String: ToSql, +{ + fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> diesel::serialize::Result { + self.0.to_sql(out) + } +} diff --git a/crates/diesel_models/src/payout_attempt.rs b/crates/diesel_models/src/payout_attempt.rs index 619e694f41..edb60bc1f4 100644 --- a/crates/diesel_models/src/payout_attempt.rs +++ b/crates/diesel_models/src/payout_attempt.rs @@ -1,3 +1,4 @@ +use common_utils::types::{UnifiedCode, UnifiedMessage}; use diesel::{AsChangeset, Identifiable, Insertable, Queryable, Selectable}; use serde::{self, Deserialize, Serialize}; use time::PrimitiveDateTime; @@ -30,6 +31,8 @@ pub struct PayoutAttempt { pub profile_id: common_utils::id_type::ProfileId, pub merchant_connector_id: Option, pub routing_info: Option, + pub unified_code: Option, + pub unified_message: Option, } #[derive( @@ -66,6 +69,8 @@ pub struct PayoutAttemptNew { pub profile_id: common_utils::id_type::ProfileId, pub merchant_connector_id: Option, pub routing_info: Option, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -76,6 +81,8 @@ pub enum PayoutAttemptUpdate { error_message: Option, error_code: Option, is_eligible: Option, + unified_code: Option, + unified_message: Option, }, PayoutTokenUpdate { payout_token: String, @@ -110,6 +117,8 @@ pub struct PayoutAttemptUpdateInternal { pub address_id: Option, pub customer_id: Option, pub merchant_connector_id: Option, + pub unified_code: Option, + pub unified_message: Option, } impl Default for PayoutAttemptUpdateInternal { @@ -129,6 +138,8 @@ impl Default for PayoutAttemptUpdateInternal { last_modified_at: common_utils::date_time::now(), address_id: None, customer_id: None, + unified_code: None, + unified_message: None, } } } @@ -146,12 +157,16 @@ impl From for PayoutAttemptUpdateInternal { error_message, error_code, is_eligible, + unified_code, + unified_message, } => Self { connector_payout_id, status: Some(status), error_message, error_code, is_eligible, + unified_code, + unified_message, ..Default::default() }, PayoutAttemptUpdate::BusinessUpdate { @@ -197,6 +212,8 @@ impl PayoutAttemptUpdate { address_id, customer_id, merchant_connector_id, + unified_code, + unified_message, } = self.into(); PayoutAttempt { payout_token: payout_token.or(source.payout_token), @@ -213,6 +230,8 @@ impl PayoutAttemptUpdate { address_id: address_id.or(source.address_id), customer_id: customer_id.or(source.customer_id), merchant_connector_id: merchant_connector_id.or(source.merchant_connector_id), + unified_code: unified_code.or(source.unified_code), + unified_message: unified_message.or(source.unified_message), ..source } } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 239a2d5f2b..9ff7c958df 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -1054,6 +1054,10 @@ diesel::table! { #[max_length = 32] merchant_connector_id -> Nullable, routing_info -> Nullable, + #[max_length = 255] + unified_code -> Nullable, + #[max_length = 1024] + unified_message -> Nullable, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index ae3c388239..ca62d2c477 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -1010,6 +1010,10 @@ diesel::table! { #[max_length = 32] merchant_connector_id -> Nullable, routing_info -> Nullable, + #[max_length = 255] + unified_code -> Nullable, + #[max_length = 1024] + unified_message -> Nullable, } } diff --git a/crates/hyperswitch_domain_models/src/payouts/payout_attempt.rs b/crates/hyperswitch_domain_models/src/payouts/payout_attempt.rs index f747eb96c4..9c51614284 100644 --- a/crates/hyperswitch_domain_models/src/payouts/payout_attempt.rs +++ b/crates/hyperswitch_domain_models/src/payouts/payout_attempt.rs @@ -1,6 +1,9 @@ use api_models::enums::PayoutConnectors; use common_enums as storage_enums; -use common_utils::id_type; +use common_utils::{ + id_type, + types::{UnifiedCode, UnifiedMessage}, +}; use serde::{Deserialize, Serialize}; use storage_enums::MerchantStorageScheme; use time::PrimitiveDateTime; @@ -78,6 +81,8 @@ pub struct PayoutAttempt { pub profile_id: id_type::ProfileId, pub merchant_connector_id: Option, pub routing_info: Option, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Clone, Debug, PartialEq)] @@ -101,6 +106,8 @@ pub struct PayoutAttemptNew { pub profile_id: id_type::ProfileId, pub merchant_connector_id: Option, pub routing_info: Option, + pub unified_code: Option, + pub unified_message: Option, } #[derive(Debug, Clone)] @@ -111,6 +118,9 @@ pub enum PayoutAttemptUpdate { error_message: Option, error_code: Option, is_eligible: Option, + + unified_code: Option, + unified_message: Option, }, PayoutTokenUpdate { payout_token: String, @@ -143,6 +153,8 @@ pub struct PayoutAttemptUpdateInternal { pub address_id: Option, pub customer_id: Option, pub merchant_connector_id: Option, + pub unified_code: Option, + pub unified_message: Option, } impl From for PayoutAttemptUpdateInternal { @@ -158,12 +170,16 @@ impl From for PayoutAttemptUpdateInternal { error_message, error_code, is_eligible, + unified_code, + unified_message, } => Self { connector_payout_id, status: Some(status), error_message, error_code, is_eligible, + unified_code, + unified_message, ..Default::default() }, PayoutAttemptUpdate::BusinessUpdate { diff --git a/crates/router/src/connector/adyenplatform/transformers/payouts.rs b/crates/router/src/connector/adyenplatform/transformers/payouts.rs index e0ec8f9e18..09772fc87d 100644 --- a/crates/router/src/connector/adyenplatform/transformers/payouts.rs +++ b/crates/router/src/connector/adyenplatform/transformers/payouts.rs @@ -270,14 +270,20 @@ impl TryFrom> item: types::PayoutsResponseRouterData, ) -> Result { let response: AdyenTransferResponse = item.response; + let status = enums::PayoutStatus::from(response.status); + + let error_code = match status { + enums::PayoutStatus::Ineligible => Some(response.reason), + _ => None, + }; Ok(Self { response: Ok(types::PayoutsResponseData { - status: Some(enums::PayoutStatus::from(response.status)), + status: Some(status), connector_payout_id: Some(response.id), payout_eligible: None, should_add_next_step_to_process_tracker: false, - error_code: None, + error_code, error_message: None, }), ..item.data diff --git a/crates/router/src/core/generic_link/payout_link/status/script.js b/crates/router/src/core/generic_link/payout_link/status/script.js index 473af49b2e..819d7e63c3 100644 --- a/crates/router/src/core/generic_link/payout_link/status/script.js +++ b/crates/router/src/core/generic_link/payout_link/status/script.js @@ -121,10 +121,10 @@ function renderStatusDetails(payoutDetails) { "{{i18n_ref_id_text}}": payoutDetails.payout_id, }; if (typeof payoutDetails.error_code === "string") { - // resourceInfo["{{i18n_error_code_text}}"] = payoutDetails.error_code; + resourceInfo["{{i18n_error_code_text}}"] = payoutDetails.error_code; } if (typeof payoutDetails.error_message === "string") { - // resourceInfo["{{i18n_error_message}}"] = payoutDetails.error_message; + resourceInfo["{{i18n_error_message}}"] = payoutDetails.error_message; } var resourceNode = document.createElement("div"); resourceNode.id = "resource-info-container"; diff --git a/crates/router/src/core/generic_link/payout_link/status/styles.css b/crates/router/src/core/generic_link/payout_link/status/styles.css index a10bfe7e3c..11c6e68dc4 100644 --- a/crates/router/src/core/generic_link/payout_link/status/styles.css +++ b/crates/router/src/core/generic_link/payout_link/status/styles.css @@ -80,11 +80,12 @@ body { #resource-info-container { width: 100%; border-top: 1px solid rgb(231, 234, 241); - padding: 20px 10px; + padding: 20px 0; } #resource-info { display: flex; align-items: center; + margin: 0 2.5rem; } #info-key { text-align: right; diff --git a/crates/router/src/core/payout_link.rs b/crates/router/src/core/payout_link.rs index a153d07108..b175c8b7c0 100644 --- a/crates/router/src/core/payout_link.rs +++ b/crates/router/src/core/payout_link.rs @@ -17,7 +17,10 @@ use hyperswitch_domain_models::api::{GenericLinks, GenericLinksData}; use super::errors::{RouterResponse, StorageErrorExt}; use crate::{ configs::settings::{PaymentMethodFilterKey, PaymentMethodFilters}, - core::{payments::helpers, payouts::validator}, + core::{ + payments::helpers as payment_helpers, + payouts::{helpers as payout_helpers, validator}, + }, errors, routes::{app::StorageInterface, SessionState}, services, @@ -271,6 +274,14 @@ pub async fn initiate_payout_link( // Send back status page (_, link_utils::PayoutLinkStatus::Submitted) => { + let translated_unified_message = + payout_helpers::get_translated_unified_code_and_message( + &state, + payout_attempt.unified_code.as_ref(), + payout_attempt.unified_message.as_ref(), + &locale, + ) + .await?; let js_data = payouts::PayoutLinkStatusDetails { payout_link_id: payout_link.link_id, payout_id: payout_link.primary_reference, @@ -284,8 +295,8 @@ pub async fn initiate_payout_link( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to parse payout status link's return URL")?, status: payout.status, - error_code: payout_attempt.error_code, - error_message: payout_attempt.error_message, + error_code: payout_attempt.unified_code, + error_message: translated_unified_message, ui_config: ui_config_data, test_mode: link_data.test_mode.unwrap_or(false), }; @@ -338,7 +349,7 @@ pub async fn filter_payout_methods( .await .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; // Filter MCAs based on profile_id and connector_type - let filtered_mcas = helpers::filter_mca_based_on_profile_and_connector_type( + let filtered_mcas = payment_helpers::filter_mca_based_on_profile_and_connector_type( all_mcas, &payout.profile_id, common_enums::ConnectorType::PayoutProcessor, diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs index 85d5b78521..d0c0b2d3cd 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -16,7 +16,7 @@ use common_utils::{ ext_traits::{AsyncExt, ValueExt}, id_type::CustomerId, link_utils::{GenericLinkStatus, GenericLinkUiConfig, PayoutLinkData, PayoutLinkStatus}, - types::MinorUnit, + types::{MinorUnit, UnifiedCode, UnifiedMessage}, }; use diesel_models::{ enums as storage_enums, @@ -71,6 +71,7 @@ pub struct PayoutData { pub profile_id: common_utils::id_type::ProfileId, pub should_terminate: bool, pub payout_link: Option, + pub current_locale: String, } // ********************************************** CORE FLOWS ********************************************** @@ -358,7 +359,7 @@ pub async fn payouts_create_core( .await? }; - response_handler(&merchant_account, &payout_data).await + response_handler(&state, &merchant_account, &payout_data).await } #[instrument(skip_all)] @@ -367,6 +368,7 @@ pub async fn payouts_confirm_core( merchant_account: domain::MerchantAccount, key_store: domain::MerchantKeyStore, req: payouts::PayoutCreateRequest, + locale: &str, ) -> RouterResponse { let mut payout_data = make_payout_data( &state, @@ -374,6 +376,7 @@ pub async fn payouts_confirm_core( None, &key_store, &payouts::PayoutRequest::PayoutCreateRequest(req.to_owned()), + locale, ) .await?; let payout_attempt = payout_data.payout_attempt.to_owned(); @@ -429,7 +432,7 @@ pub async fn payouts_confirm_core( ) .await?; - response_handler(&merchant_account, &payout_data).await + response_handler(&state, &merchant_account, &payout_data).await } pub async fn payouts_update_core( @@ -437,6 +440,7 @@ pub async fn payouts_update_core( merchant_account: domain::MerchantAccount, key_store: domain::MerchantKeyStore, req: payouts::PayoutCreateRequest, + locale: &str, ) -> RouterResponse { let payout_id = req.payout_id.clone().get_required_value("payout_id")?; let mut payout_data = make_payout_data( @@ -445,6 +449,7 @@ pub async fn payouts_update_core( None, &key_store, &payouts::PayoutRequest::PayoutCreateRequest(req.to_owned()), + locale, ) .await?; @@ -509,7 +514,7 @@ pub async fn payouts_update_core( .await?; } - response_handler(&merchant_account, &payout_data).await + response_handler(&state, &merchant_account, &payout_data).await } #[instrument(skip_all)] @@ -519,6 +524,7 @@ pub async fn payouts_retrieve_core( profile_id: Option, key_store: domain::MerchantKeyStore, req: payouts::PayoutRetrieveRequest, + locale: &str, ) -> RouterResponse { let mut payout_data = make_payout_data( &state, @@ -526,6 +532,7 @@ pub async fn payouts_retrieve_core( profile_id, &key_store, &payouts::PayoutRequest::PayoutRetrieveRequest(req.to_owned()), + locale, ) .await?; let payout_attempt = payout_data.payout_attempt.to_owned(); @@ -553,7 +560,7 @@ pub async fn payouts_retrieve_core( .await?; } - response_handler(&merchant_account, &payout_data).await + response_handler(&state, &merchant_account, &payout_data).await } #[instrument(skip_all)] @@ -562,6 +569,7 @@ pub async fn payouts_cancel_core( merchant_account: domain::MerchantAccount, key_store: domain::MerchantKeyStore, req: payouts::PayoutActionRequest, + locale: &str, ) -> RouterResponse { let mut payout_data = make_payout_data( &state, @@ -569,6 +577,7 @@ pub async fn payouts_cancel_core( None, &key_store, &payouts::PayoutRequest::PayoutActionRequest(req.to_owned()), + locale, ) .await?; @@ -593,6 +602,8 @@ pub async fn payouts_cancel_core( error_message: Some("Cancelled by user".to_string()), error_code: None, is_eligible: None, + unified_code: None, + unified_message: None, }; payout_data.payout_attempt = state .store @@ -643,7 +654,7 @@ pub async fn payouts_cancel_core( .attach_printable("Payout cancellation failed for given Payout request")?; } - response_handler(&merchant_account, &payout_data).await + response_handler(&state, &merchant_account, &payout_data).await } #[instrument(skip_all)] @@ -652,6 +663,7 @@ pub async fn payouts_fulfill_core( merchant_account: domain::MerchantAccount, key_store: domain::MerchantKeyStore, req: payouts::PayoutActionRequest, + locale: &str, ) -> RouterResponse { let mut payout_data = make_payout_data( &state, @@ -659,6 +671,7 @@ pub async fn payouts_fulfill_core( None, &key_store, &payouts::PayoutRequest::PayoutActionRequest(req.to_owned()), + locale, ) .await?; @@ -735,7 +748,7 @@ pub async fn payouts_fulfill_core( })); } - response_handler(&merchant_account, &payout_data).await + response_handler(&state, &merchant_account, &payout_data).await } #[cfg(all(feature = "olap", feature = "v2", feature = "customer_v2"))] @@ -745,6 +758,7 @@ pub async fn payouts_list_core( _profile_id_list: Option>, _key_store: domain::MerchantKeyStore, _constraints: payouts::PayoutListConstraints, + _locale: &str, ) -> RouterResponse { todo!() } @@ -760,6 +774,7 @@ pub async fn payouts_list_core( profile_id_list: Option>, key_store: domain::MerchantKeyStore, constraints: payouts::PayoutListConstraints, + _locale: &str, ) -> RouterResponse { validator::validate_payout_list_request(&constraints)?; let merchant_id = merchant_account.get_id(); @@ -878,6 +893,7 @@ pub async fn payouts_filtered_list_core( profile_id_list: Option>, key_store: domain::MerchantKeyStore, filters: payouts::PayoutListFilterConstraints, + _locale: &str, ) -> RouterResponse { let limit = &filters.limit; validator::validate_payout_list_request_for_joins(*limit)?; @@ -981,6 +997,7 @@ pub async fn payouts_list_available_filters_core( merchant_account: domain::MerchantAccount, profile_id_list: Option>, time_range: api::TimeRange, + _locale: &str, ) -> RouterResponse { let db = state.store.as_ref(); let payouts = db @@ -1293,6 +1310,8 @@ pub async fn create_recipient( error_code: None, error_message: None, is_eligible: recipient_create_data.payout_eligible, + unified_code: None, + unified_message: None, }; payout_data.payout_attempt = db .update_payout_attempt( @@ -1406,6 +1425,8 @@ pub async fn check_payout_eligibility( error_code: None, error_message: None, is_eligible: payout_response_data.payout_eligible, + unified_code: None, + unified_message: None, }; payout_data.payout_attempt = db .update_payout_attempt( @@ -1437,12 +1458,34 @@ pub async fn check_payout_eligibility( } Err(err) => { let status = storage_enums::PayoutStatus::Failed; + let (error_code, error_message) = (Some(err.code), Some(err.message)); + let (unified_code, unified_message) = helpers::get_gsm_record( + state, + error_code.clone(), + error_message.clone(), + payout_data.payout_attempt.connector.clone(), + consts::PAYOUT_FLOW_STR, + ) + .await + .map_or((None, None), |gsm| (gsm.unified_code, gsm.unified_message)); let updated_payout_attempt = storage::PayoutAttemptUpdate::StatusUpdate { connector_payout_id: payout_data.payout_attempt.connector_payout_id.to_owned(), status, - error_code: Some(err.code), - error_message: Some(err.message), + error_code, + error_message, is_eligible: Some(false), + unified_code: unified_code + .map(UnifiedCode::try_from) + .transpose() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "unified_code", + })?, + unified_message: unified_message + .map(UnifiedMessage::try_from) + .transpose() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "unified_message", + })?, }; payout_data.payout_attempt = db .update_payout_attempt( @@ -1497,6 +1540,8 @@ pub async fn complete_create_payout( error_code: None, error_message: None, is_eligible: None, + unified_code: None, + unified_message: None, }; payout_data.payout_attempt = db .update_payout_attempt( @@ -1591,6 +1636,8 @@ pub async fn create_payout( error_code: None, error_message: None, is_eligible: payout_response_data.payout_eligible, + unified_code: None, + unified_message: None, }; payout_data.payout_attempt = db .update_payout_attempt( @@ -1622,12 +1669,34 @@ pub async fn create_payout( } Err(err) => { let status = storage_enums::PayoutStatus::Failed; + let (error_code, error_message) = (Some(err.code), Some(err.message)); + let (unified_code, unified_message) = helpers::get_gsm_record( + state, + error_code.clone(), + error_message.clone(), + payout_data.payout_attempt.connector.clone(), + consts::PAYOUT_FLOW_STR, + ) + .await + .map_or((None, None), |gsm| (gsm.unified_code, gsm.unified_message)); let updated_payout_attempt = storage::PayoutAttemptUpdate::StatusUpdate { connector_payout_id: payout_data.payout_attempt.connector_payout_id.to_owned(), status, - error_code: Some(err.code), - error_message: Some(err.message), + error_code, + error_message, is_eligible: None, + unified_code: unified_code + .map(UnifiedCode::try_from) + .transpose() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "unified_code", + })?, + unified_message: unified_message + .map(UnifiedMessage::try_from) + .transpose() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "unified_message", + })?, }; payout_data.payout_attempt = db .update_payout_attempt( @@ -1774,12 +1843,37 @@ pub async fn update_retrieve_payout_tracker( .unwrap_or(payout_attempt.status.to_owned()); let updated_payout_attempt = if helpers::is_payout_err_state(status) { + let (error_code, error_message) = ( + payout_response_data.error_code.clone(), + payout_response_data.error_message.clone(), + ); + let (unified_code, unified_message) = helpers::get_gsm_record( + state, + error_code.clone(), + error_message.clone(), + payout_data.payout_attempt.connector.clone(), + consts::PAYOUT_FLOW_STR, + ) + .await + .map_or((None, None), |gsm| (gsm.unified_code, gsm.unified_message)); storage::PayoutAttemptUpdate::StatusUpdate { connector_payout_id: payout_response_data.connector_payout_id.clone(), status, - error_code: payout_response_data.error_code.clone(), - error_message: payout_response_data.error_message.clone(), + error_code, + error_message, is_eligible: payout_response_data.payout_eligible, + unified_code: unified_code + .map(UnifiedCode::try_from) + .transpose() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "unified_code", + })?, + unified_message: unified_message + .map(UnifiedMessage::try_from) + .transpose() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "unified_message", + })?, } } else { storage::PayoutAttemptUpdate::StatusUpdate { @@ -1788,6 +1882,8 @@ pub async fn update_retrieve_payout_tracker( error_code: None, error_message: None, is_eligible: payout_response_data.payout_eligible, + unified_code: None, + unified_message: None, } }; @@ -1886,6 +1982,8 @@ pub async fn create_recipient_disburse_account( error_code: None, error_message: None, is_eligible: payout_response_data.payout_eligible, + unified_code: None, + unified_message: None, }; payout_data.payout_attempt = db .update_payout_attempt( @@ -1899,12 +1997,34 @@ pub async fn create_recipient_disburse_account( .attach_printable("Error updating payout_attempt in db")?; } Err(err) => { + let (error_code, error_message) = (Some(err.code), Some(err.message)); + let (unified_code, unified_message) = helpers::get_gsm_record( + state, + error_code.clone(), + error_message.clone(), + payout_data.payout_attempt.connector.clone(), + consts::PAYOUT_FLOW_STR, + ) + .await + .map_or((None, None), |gsm| (gsm.unified_code, gsm.unified_message)); let updated_payout_attempt = storage::PayoutAttemptUpdate::StatusUpdate { connector_payout_id: payout_data.payout_attempt.connector_payout_id.to_owned(), status: storage_enums::PayoutStatus::Failed, - error_code: Some(err.code), - error_message: Some(err.message), + error_code, + error_message, is_eligible: None, + unified_code: unified_code + .map(UnifiedCode::try_from) + .transpose() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "unified_code", + })?, + unified_message: unified_message + .map(UnifiedMessage::try_from) + .transpose() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "unified_message", + })?, }; payout_data.payout_attempt = db .update_payout_attempt( @@ -1964,6 +2084,8 @@ pub async fn cancel_payout( error_code: None, error_message: None, is_eligible: payout_response_data.payout_eligible, + unified_code: None, + unified_message: None, }; payout_data.payout_attempt = db .update_payout_attempt( @@ -1988,12 +2110,34 @@ pub async fn cancel_payout( } Err(err) => { let status = storage_enums::PayoutStatus::Failed; + let (error_code, error_message) = (Some(err.code), Some(err.message)); + let (unified_code, unified_message) = helpers::get_gsm_record( + state, + error_code.clone(), + error_message.clone(), + payout_data.payout_attempt.connector.clone(), + consts::PAYOUT_FLOW_STR, + ) + .await + .map_or((None, None), |gsm| (gsm.unified_code, gsm.unified_message)); let updated_payout_attempt = storage::PayoutAttemptUpdate::StatusUpdate { connector_payout_id: payout_data.payout_attempt.connector_payout_id.to_owned(), status, - error_code: Some(err.code), - error_message: Some(err.message), + error_code, + error_message, is_eligible: None, + unified_code: unified_code + .map(UnifiedCode::try_from) + .transpose() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "unified_code", + })?, + unified_message: unified_message + .map(UnifiedMessage::try_from) + .transpose() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "unified_message", + })?, }; payout_data.payout_attempt = db .update_payout_attempt( @@ -2075,6 +2219,8 @@ pub async fn fulfill_payout( error_code: None, error_message: None, is_eligible: payout_response_data.payout_eligible, + unified_code: None, + unified_message: None, }; payout_data.payout_attempt = db .update_payout_attempt( @@ -2131,12 +2277,34 @@ pub async fn fulfill_payout( } Err(err) => { let status = storage_enums::PayoutStatus::Failed; + let (error_code, error_message) = (Some(err.code), Some(err.message)); + let (unified_code, unified_message) = helpers::get_gsm_record( + state, + error_code.clone(), + error_message.clone(), + payout_data.payout_attempt.connector.clone(), + consts::PAYOUT_FLOW_STR, + ) + .await + .map_or((None, None), |gsm| (gsm.unified_code, gsm.unified_message)); let updated_payout_attempt = storage::PayoutAttemptUpdate::StatusUpdate { connector_payout_id: payout_data.payout_attempt.connector_payout_id.to_owned(), status, - error_code: Some(err.code), - error_message: Some(err.message), + error_code, + error_message, is_eligible: None, + unified_code: unified_code + .map(UnifiedCode::try_from) + .transpose() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "unified_code", + })?, + unified_message: unified_message + .map(UnifiedMessage::try_from) + .transpose() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "unified_message", + })?, }; payout_data.payout_attempt = db .update_payout_attempt( @@ -2165,6 +2333,7 @@ pub async fn fulfill_payout( } pub async fn response_handler( + state: &SessionState, merchant_account: &domain::MerchantAccount, payout_data: &PayoutData, ) -> RouterResponse { @@ -2176,12 +2345,20 @@ pub async fn response_handler( let customer_id = payouts.customer_id; let billing = billing_address.as_ref().map(From::from); + let translated_unified_message = helpers::get_translated_unified_code_and_message( + state, + payout_attempt.unified_code.as_ref(), + payout_attempt.unified_message.as_ref(), + &payout_data.current_locale, + ) + .await?; + let response = api::PayoutCreateResponse { payout_id: payouts.payout_id.to_owned(), merchant_id: merchant_account.get_id().to_owned(), amount: payouts.amount, currency: payouts.destination_currency.to_owned(), - connector: payout_attempt.connector.to_owned(), + connector: payout_attempt.connector, payout_type: payouts.payout_type.to_owned(), billing, auto_fulfill: payouts.auto_fulfill, @@ -2212,6 +2389,8 @@ pub async fn response_handler( connector_transaction_id: payout_attempt.connector_payout_id, priority: payouts.priority, attempts: None, + unified_code: payout_attempt.unified_code, + unified_message: translated_unified_message, payout_link: payout_link .map(|payout_link| { url::Url::parse(payout_link.url.peek()).map(|link| PayoutLinkResponse { @@ -2389,6 +2568,8 @@ pub async fn payout_create_db_entries( last_modified_at: common_utils::date_time::now(), merchant_connector_id: None, routing_info: None, + unified_code: None, + unified_message: None, }; let payout_attempt = db .insert_payout_attempt( @@ -2418,6 +2599,7 @@ pub async fn payout_create_db_entries( should_terminate: false, profile_id: profile_id.to_owned(), payout_link, + current_locale: locale.to_string(), }) } @@ -2428,6 +2610,7 @@ pub async fn make_payout_data( _auth_profile_id: Option, _key_store: &domain::MerchantKeyStore, _req: &payouts::PayoutRequest, + locale: &str, ) -> RouterResult { todo!() } @@ -2439,6 +2622,7 @@ pub async fn make_payout_data( auth_profile_id: Option, key_store: &domain::MerchantKeyStore, req: &payouts::PayoutRequest, + locale: &str, ) -> RouterResult { let db = &*state.store; let merchant_id = merchant_account.get_id(); @@ -2594,6 +2778,7 @@ pub async fn make_payout_data( should_terminate: false, profile_id, payout_link, + current_locale: locale.to_string(), }) } diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index 913f8ec761..ab13fa4c02 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -8,7 +8,7 @@ use common_utils::{ fp_utils, id_type, type_name, types::{ keymanager::{Identifier, KeyManagerState}, - MinorUnit, + MinorUnit, UnifiedCode, UnifiedMessage, }, }; #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] @@ -948,12 +948,13 @@ pub async fn get_gsm_record( error_code: Option, error_message: Option, connector_name: Option, - flow: String, + flow: &str, ) -> Option { + let connector_name = connector_name.unwrap_or_default(); let get_gsm = || async { state.store.find_gsm_rule( - connector_name.clone().unwrap_or_default(), - flow.clone(), + connector_name.clone(), + flow.to_string(), "sub_flow".to_string(), error_code.clone().unwrap_or_default(), // TODO: make changes in connector to get a mandatory code in case of success or error response error_message.clone().unwrap_or_default(), @@ -963,7 +964,7 @@ pub async fn get_gsm_record( if err.current_context().is_db_not_found() { logger::warn!( "GSM miss for connector - {}, flow - {}, error_code - {:?}, error_message - {:?}", - connector_name.unwrap_or_default(), + connector_name, flow, error_code, error_message @@ -1269,3 +1270,29 @@ pub(super) fn get_customer_details_from_request( phone_country_code: customer_phone_code, } } + +pub async fn get_translated_unified_code_and_message( + state: &SessionState, + unified_code: Option<&UnifiedCode>, + unified_message: Option<&UnifiedMessage>, + locale: &str, +) -> CustomResult, errors::ApiErrorResponse> { + Ok(unified_code + .zip(unified_message) + .async_and_then(|(code, message)| async { + payment_helpers::get_unified_translation( + state, + code.0.clone(), + message.0.clone(), + locale.to_string(), + ) + .await + .map(UnifiedMessage::try_from) + }) + .await + .transpose() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "unified_message", + })? + .or_else(|| unified_message.cloned())) +} diff --git a/crates/router/src/core/payouts/retry.rs b/crates/router/src/core/payouts/retry.rs index e190e627bd..00c25b0d67 100644 --- a/crates/router/src/core/payouts/retry.rs +++ b/crates/router/src/core/payouts/retry.rs @@ -190,12 +190,15 @@ pub async fn get_gsm( let error_code = payout_data.payout_attempt.error_code.to_owned(); let error_message = payout_data.payout_attempt.error_message.to_owned(); let connector_name = Some(original_connector_data.connector_name.to_string()); - let flow = "payout_flow".to_string(); - Ok( - payouts::helpers::get_gsm_record(state, error_code, error_message, connector_name, flow) - .await, + Ok(payouts::helpers::get_gsm_record( + state, + error_code, + error_message, + connector_name, + common_utils::consts::PAYOUT_FLOW_STR, ) + .await) } #[instrument(skip_all)] @@ -295,6 +298,8 @@ pub async fn modify_trackers( last_modified_at: common_utils::date_time::now(), merchant_connector_id: None, routing_info: None, + unified_code: None, + unified_message: None, }; payout_data.payout_attempt = db .insert_payout_attempt( diff --git a/crates/router/src/core/payouts/transformers.rs b/crates/router/src/core/payouts/transformers.rs index ed17eb71af..080cf775b2 100644 --- a/crates/router/src/core/payouts/transformers.rs +++ b/crates/router/src/core/payouts/transformers.rs @@ -98,10 +98,12 @@ impl created: Some(payout.created_at), connector_transaction_id: attempt.connector_transaction_id.clone(), priority: payout.priority, - attempts: Some(vec![attempt]), billing: address, client_secret: None, payout_link: None, + unified_code: attempt.unified_code.clone(), + unified_message: attempt.unified_message.clone(), + attempts: Some(vec![attempt]), email: customer .as_ref() .and_then(|customer| customer.email.clone()), diff --git a/crates/router/src/core/webhooks/incoming.rs b/crates/router/src/core/webhooks/incoming.rs index 8aa04a64a8..19d5591d18 100644 --- a/crates/router/src/core/webhooks/incoming.rs +++ b/crates/router/src/core/webhooks/incoming.rs @@ -689,6 +689,8 @@ async fn payouts_incoming_webhook_flow( error_message: None, error_code: None, is_eligible: payout_attempt.is_eligible, + unified_code: None, + unified_message: None, }; let action_req = @@ -696,9 +698,15 @@ async fn payouts_incoming_webhook_flow( payout_id: payouts.payout_id.clone(), }); - let payout_data = - payouts::make_payout_data(&state, &merchant_account, None, &key_store, &action_req) - .await?; + let payout_data = payouts::make_payout_data( + &state, + &merchant_account, + None, + &key_store, + &action_req, + common_utils::consts::DEFAULT_LOCALE, + ) + .await?; let updated_payout_attempt = db .update_payout_attempt( @@ -721,7 +729,7 @@ async fn payouts_incoming_webhook_flow( // If event is NOT an UnsupportedEvent, trigger Outgoing Webhook if let Some(outgoing_event_type) = event_type { let router_response = - payouts::response_handler(&merchant_account, &payout_data).await?; + payouts::response_handler(&state, &merchant_account, &payout_data).await?; let payout_create_response: payout_models::PayoutCreateResponse = match router_response { diff --git a/crates/router/src/routes/payouts.rs b/crates/router/src/routes/payouts.rs index 50c18d71f2..1b614330eb 100644 --- a/crates/router/src/routes/payouts.rs +++ b/crates/router/src/routes/payouts.rs @@ -1,5 +1,6 @@ use actix_web::{ body::{BoxBody, MessageBody}, + http::header::HeaderMap, web, HttpRequest, HttpResponse, Responder, }; use common_enums::EntityType; @@ -20,6 +21,14 @@ use crate::{ types::api::payouts as payout_types, }; +fn get_locale_from_header(headers: &HeaderMap) -> String { + get_header_value_by_key(ACCEPT_LANGUAGE.into(), headers) + .ok() + .flatten() + .map(|val| val.to_string()) + .unwrap_or(consts::DEFAULT_LOCALE.to_string()) +} + /// Payouts - Create #[instrument(skip_all, fields(flow = ?Flow::PayoutsCreate))] pub async fn payouts_create( @@ -28,11 +37,8 @@ pub async fn payouts_create( json_payload: web::Json, ) -> HttpResponse { let flow = Flow::PayoutsCreate; - let locale = get_header_value_by_key(ACCEPT_LANGUAGE.into(), req.headers()) - .ok() - .flatten() - .map(|val| val.to_string()) - .unwrap_or(consts::DEFAULT_LOCALE.to_string()); + let locale = get_locale_from_header(req.headers()); + Box::pin(api::server_wrap( flow, state, @@ -60,6 +66,8 @@ pub async fn payouts_retrieve( merchant_id: query_params.merchant_id.to_owned(), }; let flow = Flow::PayoutsRetrieve; + let locale = get_locale_from_header(req.headers()); + Box::pin(api::server_wrap( flow, state, @@ -72,6 +80,7 @@ pub async fn payouts_retrieve( auth.profile_id, auth.key_store, req, + &locale, ) }, auth::auth_type( @@ -95,6 +104,7 @@ pub async fn payouts_update( json_payload: web::Json, ) -> HttpResponse { let flow = Flow::PayoutsUpdate; + let locale = get_locale_from_header(req.headers()); let payout_id = path.into_inner(); let mut payout_update_payload = json_payload.into_inner(); payout_update_payload.payout_id = Some(payout_id); @@ -104,7 +114,7 @@ pub async fn payouts_update( &req, payout_update_payload, |state, auth, req, _| { - payouts_update_core(state, auth.merchant_account, auth.key_store, req) + payouts_update_core(state, auth.merchant_account, auth.key_store, req, &locale) }, &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, @@ -130,6 +140,7 @@ pub async fn payouts_confirm( Ok(auth) => auth, Err(e) => return api::log_and_return_error_response(e), }; + let locale = get_locale_from_header(req.headers()); Box::pin(api::server_wrap( flow, @@ -137,7 +148,7 @@ pub async fn payouts_confirm( &req, payload, |state, auth, req, _| { - payouts_confirm_core(state, auth.merchant_account, auth.key_store, req) + payouts_confirm_core(state, auth.merchant_account, auth.key_store, req, &locale) }, &*auth_type, api_locking::LockAction::NotApplicable, @@ -156,6 +167,7 @@ pub async fn payouts_cancel( let flow = Flow::PayoutsCancel; let mut payload = json_payload.into_inner(); payload.payout_id = path.into_inner(); + let locale = get_locale_from_header(req.headers()); Box::pin(api::server_wrap( flow, @@ -163,7 +175,7 @@ pub async fn payouts_cancel( &req, payload, |state, auth, req, _| { - payouts_cancel_core(state, auth.merchant_account, auth.key_store, req) + payouts_cancel_core(state, auth.merchant_account, auth.key_store, req, &locale) }, &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, @@ -181,6 +193,7 @@ pub async fn payouts_fulfill( let flow = Flow::PayoutsFulfill; let mut payload = json_payload.into_inner(); payload.payout_id = path.into_inner(); + let locale = get_locale_from_header(req.headers()); Box::pin(api::server_wrap( flow, @@ -188,7 +201,7 @@ pub async fn payouts_fulfill( &req, payload, |state, auth, req, _| { - payouts_fulfill_core(state, auth.merchant_account, auth.key_store, req) + payouts_fulfill_core(state, auth.merchant_account, auth.key_store, req, &locale) }, &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, @@ -206,6 +219,7 @@ pub async fn payouts_list( ) -> HttpResponse { let flow = Flow::PayoutsList; let payload = json_payload.into_inner(); + let locale = get_locale_from_header(req.headers()); Box::pin(api::server_wrap( flow, @@ -213,7 +227,14 @@ pub async fn payouts_list( &req, payload, |state, auth, req, _| { - payouts_list_core(state, auth.merchant_account, None, auth.key_store, req) + payouts_list_core( + state, + auth.merchant_account, + None, + auth.key_store, + req, + &locale, + ) }, auth::auth_type( &auth::HeaderAuth(auth::ApiKeyAuth), @@ -238,6 +259,7 @@ pub async fn payouts_list_profile( ) -> HttpResponse { let flow = Flow::PayoutsList; let payload = json_payload.into_inner(); + let locale = get_locale_from_header(req.headers()); Box::pin(api::server_wrap( flow, @@ -251,6 +273,7 @@ pub async fn payouts_list_profile( auth.profile_id.map(|profile_id| vec![profile_id]), auth.key_store, req, + &locale, ) }, auth::auth_type( @@ -276,6 +299,7 @@ pub async fn payouts_list_by_filter( ) -> HttpResponse { let flow = Flow::PayoutsList; let payload = json_payload.into_inner(); + let locale = get_locale_from_header(req.headers()); Box::pin(api::server_wrap( flow, @@ -283,7 +307,14 @@ pub async fn payouts_list_by_filter( &req, payload, |state, auth, req, _| { - payouts_filtered_list_core(state, auth.merchant_account, None, auth.key_store, req) + payouts_filtered_list_core( + state, + auth.merchant_account, + None, + auth.key_store, + req, + &locale, + ) }, auth::auth_type( &auth::HeaderAuth(auth::ApiKeyAuth), @@ -308,6 +339,7 @@ pub async fn payouts_list_by_filter_profile( ) -> HttpResponse { let flow = Flow::PayoutsList; let payload = json_payload.into_inner(); + let locale = get_locale_from_header(req.headers()); Box::pin(api::server_wrap( flow, @@ -321,6 +353,7 @@ pub async fn payouts_list_by_filter_profile( auth.profile_id.map(|profile_id| vec![profile_id]), auth.key_store, req, + &locale, ) }, auth::auth_type( @@ -346,6 +379,7 @@ pub async fn payouts_list_available_filters_for_merchant( ) -> HttpResponse { let flow = Flow::PayoutsFilter; let payload = json_payload.into_inner(); + let locale = get_locale_from_header(req.headers()); Box::pin(api::server_wrap( flow, @@ -353,7 +387,7 @@ pub async fn payouts_list_available_filters_for_merchant( &req, payload, |state, auth, req, _| { - payouts_list_available_filters_core(state, auth.merchant_account, None, req) + payouts_list_available_filters_core(state, auth.merchant_account, None, req, &locale) }, auth::auth_type( &auth::HeaderAuth(auth::ApiKeyAuth), @@ -378,6 +412,7 @@ pub async fn payouts_list_available_filters_for_profile( ) -> HttpResponse { let flow = Flow::PayoutsFilter; let payload = json_payload.into_inner(); + let locale = get_locale_from_header(req.headers()); Box::pin(api::server_wrap( flow, @@ -390,6 +425,7 @@ pub async fn payouts_list_available_filters_for_profile( auth.merchant_account, auth.profile_id.map(|profile_id| vec![profile_id]), req, + &locale, ) }, auth::auth_type( diff --git a/crates/router/src/workflows/attach_payout_account_workflow.rs b/crates/router/src/workflows/attach_payout_account_workflow.rs index fa481fdb9d..b29a15725b 100644 --- a/crates/router/src/workflows/attach_payout_account_workflow.rs +++ b/crates/router/src/workflows/attach_payout_account_workflow.rs @@ -1,4 +1,7 @@ -use common_utils::ext_traits::{OptionExt, ValueExt}; +use common_utils::{ + consts::DEFAULT_LOCALE, + ext_traits::{OptionExt, ValueExt}, +}; use scheduler::{ consumer::{self, workflows::ProcessTrackerWorkflow}, errors, @@ -46,8 +49,15 @@ impl ProcessTrackerWorkflow for AttachPayoutAccountWorkflow { let request = api::payouts::PayoutRequest::PayoutRetrieveRequest(tracking_data); - let mut payout_data = - payouts::make_payout_data(state, &merchant_account, None, &key_store, &request).await?; + let mut payout_data = payouts::make_payout_data( + state, + &merchant_account, + None, + &key_store, + &request, + DEFAULT_LOCALE, + ) + .await?; payouts::payouts_core( state, diff --git a/crates/router/src/workflows/outgoing_webhook_retry.rs b/crates/router/src/workflows/outgoing_webhook_retry.rs index 05b277f4f8..89fd3200d7 100644 --- a/crates/router/src/workflows/outgoing_webhook_retry.rs +++ b/crates/router/src/workflows/outgoing_webhook_retry.rs @@ -5,7 +5,10 @@ use api_models::{ webhook_events::OutgoingWebhookRequestContent, webhooks::{OutgoingWebhook, OutgoingWebhookContent}, }; -use common_utils::ext_traits::{StringExt, ValueExt}; +use common_utils::{ + consts::DEFAULT_LOCALE, + ext_traits::{StringExt, ValueExt}, +}; use diesel_models::process_tracker::business_status; use error_stack::ResultExt; use masking::PeekInterface; @@ -521,12 +524,18 @@ async fn get_outgoing_webhook_content_and_event_type( payout_models::PayoutActionRequest { payout_id }, ); - let payout_data = - payouts::make_payout_data(&state, &merchant_account, None, &key_store, &request) - .await?; + let payout_data = payouts::make_payout_data( + &state, + &merchant_account, + None, + &key_store, + &request, + DEFAULT_LOCALE, + ) + .await?; let router_response = - payouts::response_handler(&merchant_account, &payout_data).await?; + payouts::response_handler(&state, &merchant_account, &payout_data).await?; let payout_create_response: payout_models::PayoutCreateResponse = match router_response { diff --git a/crates/storage_impl/src/payouts/payout_attempt.rs b/crates/storage_impl/src/payouts/payout_attempt.rs index 697b5d91c9..e5bfe29af5 100644 --- a/crates/storage_impl/src/payouts/payout_attempt.rs +++ b/crates/storage_impl/src/payouts/payout_attempt.rs @@ -84,6 +84,8 @@ impl PayoutAttemptInterface for KVRouterStore { profile_id: new_payout_attempt.profile_id.clone(), merchant_connector_id: new_payout_attempt.merchant_connector_id.clone(), routing_info: new_payout_attempt.routing_info.clone(), + unified_code: new_payout_attempt.unified_code.clone(), + unified_message: new_payout_attempt.unified_message.clone(), }; let redis_entry = kv::TypedSql { @@ -525,6 +527,8 @@ impl DataModelExt for PayoutAttempt { profile_id: self.profile_id, merchant_connector_id: self.merchant_connector_id, routing_info: self.routing_info, + unified_code: self.unified_code, + unified_message: self.unified_message, } } @@ -549,6 +553,8 @@ impl DataModelExt for PayoutAttempt { profile_id: storage_model.profile_id, merchant_connector_id: storage_model.merchant_connector_id, routing_info: storage_model.routing_info, + unified_code: storage_model.unified_code, + unified_message: storage_model.unified_message, } } } @@ -576,6 +582,8 @@ impl DataModelExt for PayoutAttemptNew { profile_id: self.profile_id, merchant_connector_id: self.merchant_connector_id, routing_info: self.routing_info, + unified_code: self.unified_code, + unified_message: self.unified_message, } } @@ -600,6 +608,8 @@ impl DataModelExt for PayoutAttemptNew { profile_id: storage_model.profile_id, merchant_connector_id: storage_model.merchant_connector_id, routing_info: storage_model.routing_info, + unified_code: storage_model.unified_code, + unified_message: storage_model.unified_message, } } } @@ -613,12 +623,16 @@ impl DataModelExt for PayoutAttemptUpdate { error_message, error_code, is_eligible, + unified_code, + unified_message, } => DieselPayoutAttemptUpdate::StatusUpdate { connector_payout_id, status, error_message, error_code, is_eligible, + unified_code, + unified_message, }, Self::PayoutTokenUpdate { payout_token } => { DieselPayoutAttemptUpdate::PayoutTokenUpdate { payout_token } diff --git a/migrations/2024-09-03-053218_add_unified_code_message_to_payout/down.sql b/migrations/2024-09-03-053218_add_unified_code_message_to_payout/down.sql new file mode 100644 index 0000000000..b785cad78f --- /dev/null +++ b/migrations/2024-09-03-053218_add_unified_code_message_to_payout/down.sql @@ -0,0 +1,3 @@ +ALTER TABLE payout_attempt +DROP COLUMN IF EXISTS unified_code, +DROP COLUMN IF EXISTS unified_message; \ No newline at end of file diff --git a/migrations/2024-09-03-053218_add_unified_code_message_to_payout/up.sql b/migrations/2024-09-03-053218_add_unified_code_message_to_payout/up.sql new file mode 100644 index 0000000000..b4ccfb8e52 --- /dev/null +++ b/migrations/2024-09-03-053218_add_unified_code_message_to_payout/up.sql @@ -0,0 +1,3 @@ +ALTER TABLE payout_attempt +ADD COLUMN IF NOT EXISTS unified_code VARCHAR(255) DEFAULT NULL, +ADD COLUMN IF NOT EXISTS unified_message VARCHAR(1024) DEFAULT NULL; \ No newline at end of file