feat(payments): add structured error details to payment attempts (#10646)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Ayush Anand
2025-12-30 17:06:06 +05:30
committed by GitHub
parent 48fbc4e415
commit fff565e8fb
8 changed files with 125 additions and 0 deletions

View File

@@ -343,6 +343,36 @@ pub enum GsmDecision {
DoDefault,
}
#[derive(
Clone,
Copy,
Debug,
strum::Display,
PartialEq,
Eq,
serde::Serialize,
serde::Deserialize,
strum::EnumString,
ToSchema,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
#[router_derive::diesel_enum(storage_type = "text")]
pub enum RecommendedAction {
DoNotRetry,
RetryAfter10Days,
RetryAfter1Hour,
RetryAfter24Hours,
RetryAfter2Days,
RetryAfter4Days,
RetryAfter6Days,
RetryAfter8Days,
RetryAfterInstrumentUpdate,
RetryLater,
RetryWithDifferentPaymentMethodData,
StopRecurring,
}
#[derive(
Clone,
Copy,

View File

@@ -47,6 +47,49 @@ pub struct NetworkDetails {
pub network_advice_code: Option<String>,
}
// ErrorDetails nested structs for V1 payment_attempt
common_utils::impl_to_sql_from_sql_json!(ErrorDetails);
#[derive(
Clone, Default, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, diesel::AsExpression,
)]
#[diesel(sql_type = diesel::sql_types::Jsonb)]
pub struct ErrorDetails {
pub unified_details: Option<UnifiedErrorDetails>,
pub issuer_details: Option<IssuerErrorDetails>,
pub connector_details: Option<ConnectorErrorDetails>,
}
#[derive(Clone, Default, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
pub struct UnifiedErrorDetails {
pub category: Option<String>,
pub message: Option<String>,
pub standardised_code: Option<storage_enums::StandardisedCode>,
pub description: Option<String>,
pub user_guidance_message: Option<String>,
pub recommended_action: Option<storage_enums::RecommendedAction>,
}
#[derive(Clone, Default, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
pub struct IssuerErrorDetails {
pub code: Option<String>,
pub message: Option<String>,
pub network_details: Option<NetworkErrorDetails>,
}
#[derive(Clone, Default, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
pub struct NetworkErrorDetails {
pub name: Option<storage_enums::CardNetwork>,
pub advice_code: Option<String>,
pub advice_message: Option<String>,
}
#[derive(Clone, Default, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
pub struct ConnectorErrorDetails {
pub code: Option<String>,
pub message: Option<String>,
pub reason: Option<String>,
}
#[cfg(feature = "v2")]
#[derive(
Clone, Debug, Eq, PartialEq, Identifiable, Queryable, Serialize, Deserialize, Selectable,
@@ -118,6 +161,7 @@ pub struct PaymentAttempt {
pub extended_authorization_last_applied_at: Option<PrimitiveDateTime>,
pub tokenization: Option<common_enums::Tokenization>,
pub encrypted_payment_method_data: Option<common_utils::encryption::Encryption>,
pub error_details: Option<ErrorDetails>,
#[diesel(deserialize_as = RequiredFromNullable<storage_enums::PaymentMethod>)]
pub payment_method_type_v2: storage_enums::PaymentMethod,
pub connector_payment_id: Option<ConnectorTransactionId>,
@@ -246,6 +290,7 @@ pub struct PaymentAttempt {
pub extended_authorization_last_applied_at: Option<PrimitiveDateTime>,
pub tokenization: Option<common_enums::Tokenization>,
pub encrypted_payment_method_data: Option<common_utils::encryption::Encryption>,
pub error_details: Option<ErrorDetails>,
}
#[cfg(feature = "v1")]
@@ -396,6 +441,7 @@ pub struct PaymentAttemptNew {
/// Amount captured for this payment attempt
pub amount_captured: Option<MinorUnit>,
pub encrypted_payment_method_data: Option<common_utils::encryption::Encryption>,
pub error_details: Option<ErrorDetails>,
}
#[cfg(feature = "v1")]
@@ -486,6 +532,7 @@ pub struct PaymentAttemptNew {
pub extended_authorization_last_applied_at: Option<PrimitiveDateTime>,
pub tokenization: Option<common_enums::Tokenization>,
pub encrypted_payment_method_data: Option<common_utils::encryption::Encryption>,
pub error_details: Option<ErrorDetails>,
}
#[cfg(feature = "v1")]
@@ -625,6 +672,7 @@ pub enum PaymentAttemptUpdate {
setup_future_usage_applied: Option<storage_enums::FutureUsage>,
is_overcapture_enabled: Option<OvercaptureEnabledBool>,
authorized_amount: Option<MinorUnit>,
error_details: Box<Option<ErrorDetails>>,
},
UnresolvedResponseUpdate {
status: storage_enums::AttemptStatus,
@@ -636,6 +684,7 @@ pub enum PaymentAttemptUpdate {
error_reason: Option<Option<String>>,
connector_response_reference_id: Option<String>,
updated_by: String,
error_details: Box<Option<ErrorDetails>>,
},
StatusUpdate {
status: storage_enums::AttemptStatus,
@@ -658,6 +707,7 @@ pub enum PaymentAttemptUpdate {
issuer_error_code: Option<String>,
issuer_error_message: Option<String>,
network_details: Option<NetworkDetails>,
error_details: Box<Option<ErrorDetails>>,
},
CaptureUpdate {
amount_to_capture: Option<MinorUnit>,
@@ -1072,6 +1122,7 @@ impl PaymentAttemptUpdateInternal {
authorized_amount: source.authorized_amount,
amount_captured: amount_captured.or(source.amount_captured),
encrypted_payment_method_data: source.encrypted_payment_method_data,
error_details: source.error_details,
}
}
}
@@ -1147,6 +1198,7 @@ pub struct PaymentAttemptUpdateInternal {
pub is_stored_credential: Option<bool>,
pub request_extended_authorization: Option<RequestExtendedAuthorizationBool>,
pub authorized_amount: Option<MinorUnit>,
pub error_details: Option<ErrorDetails>,
}
#[cfg(feature = "v1")]
@@ -1347,6 +1399,7 @@ impl PaymentAttemptUpdate {
is_stored_credential,
request_extended_authorization,
authorized_amount,
error_details,
} = PaymentAttemptUpdateInternal::from(self).populate_derived_fields(&source);
PaymentAttempt {
amount: amount.unwrap_or(source.amount),
@@ -1428,6 +1481,7 @@ impl PaymentAttemptUpdate {
.or(source.request_extended_authorization),
authorized_amount: authorized_amount.or(source.authorized_amount),
tokenization: tokenization.or(source.tokenization),
error_details: error_details.or(source.error_details),
..source
}
}
@@ -2783,6 +2837,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
request_extended_authorization: None,
authorized_amount: None,
encrypted_payment_method_data: None,
error_details: None,
},
PaymentAttemptUpdate::AuthenticationTypeUpdate {
authentication_type,
@@ -2856,6 +2911,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
request_extended_authorization: None,
authorized_amount: None,
encrypted_payment_method_data: None,
error_details: None,
},
PaymentAttemptUpdate::ConfirmUpdate {
amount,
@@ -2966,6 +3022,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
request_extended_authorization,
authorized_amount: None,
encrypted_payment_method_data: None,
error_details: None,
},
PaymentAttemptUpdate::VoidUpdate {
status,
@@ -3040,6 +3097,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
request_extended_authorization: None,
authorized_amount: None,
encrypted_payment_method_data: None,
error_details: None,
},
PaymentAttemptUpdate::RejectUpdate {
status,
@@ -3115,6 +3173,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
request_extended_authorization: None,
authorized_amount: None,
encrypted_payment_method_data: None,
error_details: None,
},
PaymentAttemptUpdate::BlocklistUpdate {
status,
@@ -3190,6 +3249,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
request_extended_authorization: None,
authorized_amount: None,
encrypted_payment_method_data: None,
error_details: None,
},
PaymentAttemptUpdate::ConnectorMandateDetailUpdate {
connector_mandate_detail,
@@ -3264,6 +3324,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
request_extended_authorization: None,
authorized_amount: None,
encrypted_payment_method_data: None,
error_details: None,
},
PaymentAttemptUpdate::PaymentMethodDetailsUpdate {
payment_method_id,
@@ -3337,6 +3398,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
request_extended_authorization: None,
authorized_amount: None,
encrypted_payment_method_data: None,
error_details: None,
},
PaymentAttemptUpdate::ResponseUpdate {
status,
@@ -3369,7 +3431,9 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
network_transaction_id,
is_overcapture_enabled,
authorized_amount,
error_details: boxed_error_details,
} => {
let error_details = *boxed_error_details;
let (connector_transaction_id, processor_transaction_data) =
connector_transaction_id
.map(ConnectorTransactionId::form_id_and_data)
@@ -3444,6 +3508,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
request_extended_authorization: None,
authorized_amount,
encrypted_payment_method_data,
error_details,
}
}
PaymentAttemptUpdate::ErrorUpdate {
@@ -3463,7 +3528,9 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
issuer_error_code,
issuer_error_message,
network_details,
error_details: boxed_error_details,
} => {
let error_details = *boxed_error_details;
let (connector_transaction_id, processor_transaction_data) =
connector_transaction_id
.map(ConnectorTransactionId::form_id_and_data)
@@ -3538,6 +3605,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
request_extended_authorization: None,
authorized_amount: None,
encrypted_payment_method_data,
error_details,
}
}
PaymentAttemptUpdate::StatusUpdate { status, updated_by } => Self {
@@ -3609,6 +3677,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
request_extended_authorization: None,
authorized_amount: None,
encrypted_payment_method_data: None,
error_details: None,
},
PaymentAttemptUpdate::UpdateTrackers {
payment_token,
@@ -3690,6 +3759,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
request_extended_authorization: None,
authorized_amount: None,
encrypted_payment_method_data: None,
error_details: None,
},
PaymentAttemptUpdate::UnresolvedResponseUpdate {
status,
@@ -3701,7 +3771,9 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
error_reason,
connector_response_reference_id,
updated_by,
error_details: boxed_error_details,
} => {
let error_details = *boxed_error_details;
let (connector_transaction_id, processor_transaction_data) =
connector_transaction_id
.map(ConnectorTransactionId::form_id_and_data)
@@ -3776,6 +3848,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
request_extended_authorization: None,
authorized_amount: None,
encrypted_payment_method_data: None,
error_details,
}
}
PaymentAttemptUpdate::PreprocessingUpdate {
@@ -3861,6 +3934,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
request_extended_authorization: None,
authorized_amount: None,
encrypted_payment_method_data: None,
error_details: None,
}
}
PaymentAttemptUpdate::CaptureUpdate {
@@ -3936,6 +4010,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
request_extended_authorization: None,
authorized_amount: None,
encrypted_payment_method_data: None,
error_details: None,
},
PaymentAttemptUpdate::AmountToCaptureUpdate {
status,
@@ -4010,6 +4085,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
request_extended_authorization: None,
authorized_amount: None,
encrypted_payment_method_data: None,
error_details: None,
},
PaymentAttemptUpdate::ConnectorResponse {
authentication_data,
@@ -4093,6 +4169,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
request_extended_authorization: None,
authorized_amount: None,
encrypted_payment_method_data: None,
error_details: None,
}
}
PaymentAttemptUpdate::IncrementalAuthorizationAmountUpdate {
@@ -4167,6 +4244,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
request_extended_authorization: None,
authorized_amount: None,
encrypted_payment_method_data: None,
error_details: None,
},
PaymentAttemptUpdate::AuthenticationUpdate {
status,
@@ -4243,6 +4321,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
request_extended_authorization: None,
authorized_amount: None,
encrypted_payment_method_data: None,
error_details: None,
},
PaymentAttemptUpdate::ManualUpdate {
status,
@@ -4329,6 +4408,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
request_extended_authorization: None,
authorized_amount: None,
encrypted_payment_method_data: None,
error_details: None,
}
}
PaymentAttemptUpdate::PostSessionTokensUpdate {
@@ -4403,6 +4483,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
request_extended_authorization: None,
authorized_amount: None,
encrypted_payment_method_data: None,
error_details: None,
},
}
}

View File

@@ -1135,6 +1135,7 @@ diesel::table! {
#[max_length = 64]
tokenization -> Nullable<Varchar>,
encrypted_payment_method_data -> Nullable<Bytea>,
error_details -> Nullable<Jsonb>,
}
}

View File

@@ -1077,6 +1077,7 @@ diesel::table! {
#[max_length = 64]
tokenization -> Nullable<Varchar>,
encrypted_payment_method_data -> Nullable<Bytea>,
error_details -> Nullable<Jsonb>,
payment_method_type_v2 -> Nullable<Varchar>,
#[max_length = 128]
connector_payment_id -> Nullable<Varchar>,

View File

@@ -319,6 +319,7 @@ impl PaymentAttemptBatchNew {
is_stored_credential: self.is_stored_credential,
authorized_amount: self.authorized_amount,
tokenization: self.tokenization,
error_details: None,
}
}
}

View File

@@ -1927,6 +1927,7 @@ impl PaymentAttemptUpdate {
is_overcapture_enabled,
authorized_amount,
encrypted_payment_method_data: encrypted_payment_method_data.map(Encryption::from),
error_details: Box::new(None),
},
Self::UnresolvedResponseUpdate {
status,
@@ -1948,6 +1949,7 @@ impl PaymentAttemptUpdate {
error_reason,
connector_response_reference_id,
updated_by,
error_details: Box::new(None),
},
Self::StatusUpdate { status, updated_by } => {
DieselPaymentAttemptUpdate::StatusUpdate { status, updated_by }
@@ -1986,6 +1988,7 @@ impl PaymentAttemptUpdate {
issuer_error_message,
network_details,
encrypted_payment_method_data: encrypted_payment_method_data.map(Encryption::from),
error_details: Box::new(None),
},
Self::CaptureUpdate {
multiple_capture_count,
@@ -2323,6 +2326,7 @@ impl behaviour::Conversion for PaymentAttempt {
is_stored_credential: self.is_stored_credential,
authorized_amount: self.authorized_amount,
encrypted_payment_method_data: self.encrypted_payment_method_data.map(Encryption::from),
error_details: None,
})
}
@@ -2546,6 +2550,7 @@ impl behaviour::Conversion for PaymentAttempt {
is_stored_credential: self.is_stored_credential,
authorized_amount: self.authorized_amount,
encrypted_payment_method_data: self.encrypted_payment_method_data.map(Encryption::from),
error_details: None,
})
}
}
@@ -2727,6 +2732,7 @@ impl behaviour::Conversion for PaymentAttempt {
tokenization: None,
amount_captured,
encrypted_payment_method_data: None,
error_details: None,
})
}
@@ -3017,6 +3023,7 @@ impl behaviour::Conversion for PaymentAttempt {
authorized_amount,
amount_captured: amount_details.amount_captured,
encrypted_payment_method_data: None,
error_details: None,
})
}
}

View File

@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
ALTER TABLE payment_attempt DROP COLUMN IF EXISTS error_details;

View File

@@ -0,0 +1,2 @@
-- Your SQL goes here
ALTER TABLE payment_attempt ADD COLUMN IF NOT EXISTS error_details JSONB;