diff --git a/crates/hyperswitch_connectors/src/connectors/peachpayments/transformers.rs b/crates/hyperswitch_connectors/src/connectors/peachpayments/transformers.rs index a5e34b416a..9b1776bf05 100644 --- a/crates/hyperswitch_connectors/src/connectors/peachpayments/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/peachpayments/transformers.rs @@ -16,7 +16,6 @@ use hyperswitch_interfaces::{ }; use masking::Secret; use serde::{Deserialize, Serialize}; -use strum::Display; use time::{format_description::well_known::Rfc3339, OffsetDateTime}; use crate::{ @@ -70,50 +69,72 @@ pub struct EcommerceCardPaymentOnlyTransactionData { pub routing: Routing, pub card: CardDetails, pub amount: AmountDetails, - #[serde(skip_serializing_if = "Option::is_none")] - pub three_ds_data: Option, } #[derive(Debug, Serialize, Deserialize, PartialEq)] #[serde[rename_all = "camelCase"]] pub struct MerchantInformation { - pub client_merchant_reference_id: String, - pub name: String, - pub mcc: String, + pub client_merchant_reference_id: Secret, + pub name: Secret, + pub mcc: Secret, #[serde(skip_serializing_if = "Option::is_none")] - pub phone: Option, + pub phone: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub email: Option, + pub email: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub mobile: Option, + pub mobile: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub address: Option, + pub address: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub city: Option, + pub city: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub postal_code: Option, + pub postal_code: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub region_code: Option, + pub region_code: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub merchant_type: Option, + pub merchant_type: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub website_url: Option, + pub website_url: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde[rename_all = "lowercase"]] +pub enum MerchantType { + Standard, + Sub, + Iso, } #[derive(Debug, Serialize, Deserialize, PartialEq)] #[serde[rename_all = "camelCase"]] pub struct Routing { - pub route: String, - pub mid: String, - pub tid: String, + pub route: Route, + pub mid: Secret, + pub tid: Secret, #[serde(skip_serializing_if = "Option::is_none")] - pub visa_payment_facilitator_id: Option, + pub visa_payment_facilitator_id: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub master_card_payment_facilitator_id: Option, + pub master_card_payment_facilitator_id: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub sub_mid: Option, + pub sub_mid: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub amex_id: Option, + pub amex_id: Option>, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde[rename_all = "snake_case"]] +pub enum Route { + ExipayEmulator, + AbsaBase24, + NedbankPostbridge, + AbsaPostbridgeEcentric, + PostbridgeDirecttransact, + PostbridgeEfficacy, + FiservLloyds, + NfsIzwe, + AbsaHpsZambia, + EcentricEcommerce, + UnitTestEmptyConfig, } #[derive(Debug, Serialize, PartialEq)] @@ -128,38 +149,13 @@ pub struct CardDetails { pub cvv: Option>, } -#[derive(Debug, Serialize, PartialEq)] -#[serde[rename_all = "camelCase"]] -pub struct RefundCardDetails { - pub pan: Secret, - #[serde(skip_serializing_if = "Option::is_none")] - pub cardholder_name: Option>, - pub expiry_year: Secret, - pub expiry_month: Secret, - #[serde(skip_serializing_if = "Option::is_none")] - pub cvv: Option>, -} - #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde[rename_all = "camelCase"]] pub struct AmountDetails { pub amount: MinorUnit, pub currency_code: String, -} - -#[derive(Debug, Serialize, PartialEq)] -#[serde[rename_all = "camelCase"]] -pub struct ThreeDSData { #[serde(skip_serializing_if = "Option::is_none")] - pub cavv: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub tavv: Option>, - pub eci: String, - pub ds_trans_id: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub xid: Option, - pub authentication_status: AuthenticationStatus, - pub three_ds_version: String, + pub display_amount: Option, } // Confirm Transaction Request (for capture) @@ -199,6 +195,7 @@ impl TryFrom<&PeachpaymentsRouterData<&PaymentsCaptureRouterData>> for Peachpaym let amount = AmountDetails { amount: amount_in_cents, currency_code: item.router_data.request.currency.to_string(), + display_amount: None, }; let confirmation_data = EcommerceCardPaymentOnlyConfirmationData { amount }; @@ -271,57 +268,40 @@ impl TryFrom<&PaymentsCancelRouterData> for PeachpaymentsVoidRequest { } } -#[derive(Debug, Serialize, PartialEq)] -pub enum AuthenticationStatus { - Y, // Fully authenticated - A, // Attempted authentication / liability shift - N, // Not authenticated / failed -} - -impl From<&str> for AuthenticationStatus { - fn from(eci: &str) -> Self { - match eci { - "05" | "06" => Self::Y, - "07" => Self::A, - _ => Self::N, - } - } -} - #[derive(Debug, Serialize, Deserialize)] pub struct PeachPaymentsConnectorMetadataObject { - pub client_merchant_reference_id: String, - pub name: String, - pub mcc: String, + pub client_merchant_reference_id: Secret, + pub name: Secret, + pub mcc: Secret, #[serde(skip_serializing_if = "Option::is_none")] - pub phone: Option, + pub phone: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub email: Option, + pub email: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub mobile: Option, + pub mobile: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub address: Option, + pub address: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub city: Option, + pub city: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub postal_code: Option, + pub postal_code: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub region_code: Option, + pub region_code: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub merchant_type: Option, + pub merchant_type: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub website_url: Option, - pub route: String, - pub mid: String, - pub tid: String, + pub website_url: Option, + pub route: Route, + pub mid: Secret, + pub tid: Secret, #[serde(skip_serializing_if = "Option::is_none")] - pub visa_payment_facilitator_id: Option, + pub visa_payment_facilitator_id: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub master_card_payment_facilitator_id: Option, + pub master_card_payment_facilitator_id: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub sub_mid: Option, + pub sub_mid: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub amex_id: Option, + pub amex_id: Option>, } impl TryFrom<&PeachpaymentsRouterData<&PaymentsAuthorizeRouterData>> @@ -379,55 +359,14 @@ impl TryFrom<&PeachpaymentsRouterData<&PaymentsAuthorizeRouterData>> let amount = AmountDetails { amount: amount_in_cents, currency_code: item.router_data.request.currency.to_string(), + display_amount: None, }; - // Extract 3DS data if available - let three_ds_data = item - .router_data - .request - .authentication_data - .as_ref() - .and_then(|auth_data| { - // Only include 3DS data if we have essential fields (ECI is most critical) - if let Some(eci) = &auth_data.eci { - let ds_trans_id = auth_data - .ds_trans_id - .clone() - .or_else(|| auth_data.threeds_server_transaction_id.clone())?; - - // Determine authentication status based on ECI value - let authentication_status = eci.as_str().into(); - - // Convert message version to string, handling None case - let three_ds_version = auth_data.message_version.as_ref().map(|v| { - let version_str = v.to_string(); - let mut parts = version_str.split('.'); - match (parts.next(), parts.next()) { - (Some(major), Some(minor)) => format!("{}.{}", major, minor), - _ => v.to_string(), // fallback if format unexpected - } - })?; - - Some(ThreeDSData { - cavv: Some(auth_data.cavv.clone()), - tavv: None, // Network token field - not available in Hyperswitch AuthenticationData - eci: eci.clone(), - ds_trans_id, - xid: None, // Legacy 3DS 1.x/network token field - not available in Hyperswitch AuthenticationData - authentication_status, - three_ds_version, - }) - } else { - None - } - }); - let ecommerce_data = EcommerceCardPaymentOnlyTransactionData { merchant_information, routing, card, amount, - three_ds_data, }; // Generate current timestamp for sendDateTime (ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ) @@ -469,18 +408,16 @@ impl TryFrom<&ConnectorAuthType> for PeachpaymentsAuthType { } // Card Gateway API Response #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] +#[serde(rename_all = "snake_case")] pub enum PeachpaymentsPaymentStatus { Successful, Pending, Authorized, Approved, - #[serde(rename = "approved_confirmed")] ApprovedConfirmed, Declined, Failed, Reversed, - #[serde(rename = "threeds_required")] ThreedsRequired, Voided, } @@ -509,9 +446,8 @@ impl From for common_enums::AttemptStatus { #[serde[rename_all = "camelCase"]] pub struct PeachpaymentsPaymentsResponse { pub transaction_id: String, - pub response_code: ResponseCode, + pub response_code: Option, pub transaction_result: PeachpaymentsPaymentStatus, - #[serde(skip_serializing_if = "Option::is_none")] pub ecommerce_card_payment_only_transaction_data: Option, } @@ -520,24 +456,26 @@ pub struct PeachpaymentsPaymentsResponse { #[serde[rename_all = "camelCase"]] pub struct PeachpaymentsConfirmResponse { pub transaction_id: String, - pub response_code: ResponseCode, + pub response_code: Option, pub transaction_result: PeachpaymentsPaymentStatus, - #[serde(skip_serializing_if = "Option::is_none")] pub authorization_code: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde[rename_all = "camelCase"]] #[serde(untagged)] pub enum ResponseCode { Text(String), Structured { - value: ResponseCodeValue, + value: String, description: String, + terminal_outcome_string: Option, + receipt_string: Option, }, } impl ResponseCode { - pub fn value(&self) -> Option<&ResponseCodeValue> { + pub fn value(&self) -> Option<&String> { match self { Self::Structured { value, .. } => Some(value), _ => None, @@ -559,33 +497,48 @@ impl ResponseCode { } } -#[derive(Debug, Display, Clone, Serialize, Deserialize, PartialEq)] -pub enum ResponseCodeValue { - #[serde(rename = "00", alias = "08")] - Success, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct EcommerceCardPaymentOnlyResponseData { - pub card: Option, - pub amount: Option, - pub stan: Option, - pub rrn: Option, - #[serde(rename = "approvalCode")] - pub approval_code: Option, - #[serde(rename = "merchantAdviceCode")] - pub merchant_advice_code: Option, - pub description: Option, -} - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde[rename_all = "camelCase"]] -pub struct CardResponseData { - pub bin_number: Option, - pub masked_pan: Option, - pub cardholder_name: Option, - pub expiry_month: Option, - pub expiry_year: Option, +pub struct EcommerceCardPaymentOnlyResponseData { + pub amount: Option, + pub stan: Option>, + pub rrn: Option>, + pub approval_code: Option, + pub merchant_advice_code: Option, + pub description: Option, + pub trace_id: Option, +} + +fn is_payment_success(value: Option<&String>) -> bool { + if let Some(val) = value { + val == "00" || val == "08" || val == "X94" + } else { + false + } +} + +fn get_error_code(response_code: Option<&ResponseCode>) -> String { + response_code + .and_then(|code| code.value()) + .map(|val| val.to_string()) + .unwrap_or( + response_code + .and_then(|code| code.as_text()) + .map(|text| text.to_string()) + .unwrap_or(NO_ERROR_CODE.to_string()), + ) +} + +fn get_error_message(response_code: Option<&ResponseCode>) -> String { + response_code + .and_then(|code| code.description()) + .map(|desc| desc.to_string()) + .unwrap_or( + response_code + .and_then(|code| code.as_text()) + .map(|text| text.to_string()) + .unwrap_or(NO_ERROR_MESSAGE.to_string()), + ) } impl TryFrom> @@ -598,20 +551,15 @@ impl TryFrom TryFrom> let amount = AmountDetails { amount: amount_in_cents, currency_code: item.router_data.request.currency.to_string(), + display_amount: None, }; let confirmation_data = EcommerceCardPaymentOnlyConfirmationData { amount }; diff --git a/crates/payment_methods/src/configs/payment_connector_required_fields.rs b/crates/payment_methods/src/configs/payment_connector_required_fields.rs index 32a1c175ea..f61c789fe2 100644 --- a/crates/payment_methods/src/configs/payment_connector_required_fields.rs +++ b/crates/payment_methods/src/configs/payment_connector_required_fields.rs @@ -1538,6 +1538,10 @@ fn get_cards_required_fields() -> HashMap { ), (Connector::Paypal, fields(vec![], card_basic(), vec![])), (Connector::Payu, fields(vec![], card_basic(), vec![])), + ( + Connector::Peachpayments, + fields(vec![], vec![], card_with_name()), + ), ( Connector::Powertranz, fields(vec![], card_with_name(), vec![]),