diff --git a/crates/hyperswitch_connectors/src/connectors/peachpayments.rs b/crates/hyperswitch_connectors/src/connectors/peachpayments.rs index dd5c4869cf..2e95b9e8a6 100644 --- a/crates/hyperswitch_connectors/src/connectors/peachpayments.rs +++ b/crates/hyperswitch_connectors/src/connectors/peachpayments.rs @@ -5,11 +5,12 @@ use std::sync::LazyLock; use common_enums::{self, enums}; use common_utils::{ errors::CustomResult, - ext_traits::BytesExt, + ext_traits::{ByteSliceExt, BytesExt}, + id_type, request::{Method, Request, RequestBuilder, RequestContent}, types::{AmountConvertor, MinorUnit, MinorUnitForConnector}, }; -use error_stack::{report, ResultExt}; +use error_stack::ResultExt; use hyperswitch_domain_models::{ router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData}, router_flow_types::{ @@ -42,7 +43,7 @@ use hyperswitch_interfaces::{ types::{self, Response}, webhooks, }; -use masking::{ExposeInterface, Mask}; +use masking::{ExposeInterface, Mask, Secret}; use transformers as peachpayments; use crate::{constants::headers, types::ResponseRouterData, utils}; @@ -542,23 +543,90 @@ impl ConnectorIntegration for Peachpaym impl webhooks::IncomingWebhook for Peachpayments { fn get_webhook_object_reference_id( &self, - _request: &webhooks::IncomingWebhookRequestDetails<'_>, + request: &webhooks::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + let webhook_body: peachpayments::PeachpaymentsIncomingWebhook = request + .body + .parse_struct("PeachpaymentsIncomingWebhook") + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + + let reference_id = webhook_body + .transaction + .as_ref() + .map(|txn| txn.reference_id.clone()) + .ok_or(errors::ConnectorError::WebhookReferenceIdNotFound)?; + + Ok(api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::PaymentAttemptId(reference_id), + )) } fn get_webhook_event_type( &self, - _request: &webhooks::IncomingWebhookRequestDetails<'_>, + request: &webhooks::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + let webhook_body: peachpayments::PeachpaymentsIncomingWebhook = request + .body + .parse_struct("PeachpaymentsIncomingWebhook") + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + + match webhook_body.webhook_type.as_str() { + "transaction" => { + if let Some(transaction) = webhook_body.transaction { + match transaction.transaction_result { + peachpayments::PeachpaymentsPaymentStatus::Successful + | peachpayments::PeachpaymentsPaymentStatus::ApprovedConfirmed => { + Ok(api_models::webhooks::IncomingWebhookEvent::PaymentIntentSuccess) + } + peachpayments::PeachpaymentsPaymentStatus::Authorized + | peachpayments::PeachpaymentsPaymentStatus::Approved => { + Ok(api_models::webhooks::IncomingWebhookEvent::PaymentIntentAuthorizationSuccess) + } + peachpayments::PeachpaymentsPaymentStatus::Pending => { + Ok(api_models::webhooks::IncomingWebhookEvent::PaymentIntentProcessing) + } + peachpayments::PeachpaymentsPaymentStatus::Declined + | peachpayments::PeachpaymentsPaymentStatus::Failed => { + Ok(api_models::webhooks::IncomingWebhookEvent::PaymentIntentFailure) + } + peachpayments::PeachpaymentsPaymentStatus::Voided + | peachpayments::PeachpaymentsPaymentStatus::Reversed => { + Ok(api_models::webhooks::IncomingWebhookEvent::PaymentIntentCancelled) + } + peachpayments::PeachpaymentsPaymentStatus::ThreedsRequired => { + Ok(api_models::webhooks::IncomingWebhookEvent::PaymentActionRequired) + } + } + } else { + Err(errors::ConnectorError::WebhookEventTypeNotFound) + } + } + _ => Err(errors::ConnectorError::WebhookEventTypeNotFound), + } + .change_context(errors::ConnectorError::WebhookEventTypeNotFound) } fn get_webhook_resource_object( &self, - _request: &webhooks::IncomingWebhookRequestDetails<'_>, + request: &webhooks::IncomingWebhookRequestDetails<'_>, ) -> CustomResult, errors::ConnectorError> { - Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + let webhook_body: peachpayments::PeachpaymentsIncomingWebhook = request + .body + .parse_struct("PeachpaymentsIncomingWebhook") + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + + Ok(Box::new(webhook_body)) + } + + async fn verify_webhook_source( + &self, + _request: &webhooks::IncomingWebhookRequestDetails<'_>, + _merchant_id: &id_type::MerchantId, + _connector_webhook_details: Option, + _connector_account_details: common_utils::crypto::Encryptable>, + _connector_name: &str, + ) -> CustomResult { + Ok(false) } } @@ -625,7 +693,8 @@ static PEACHPAYMENTS_CONNECTOR_INFO: ConnectorInfo = ConnectorInfo { integration_status: enums::ConnectorIntegrationStatus::Beta, }; -static PEACHPAYMENTS_SUPPORTED_WEBHOOK_FLOWS: [enums::EventClass; 0] = []; +static PEACHPAYMENTS_SUPPORTED_WEBHOOK_FLOWS: [enums::EventClass; 1] = + [enums::EventClass::Payments]; impl ConnectorSpecifications for Peachpayments { fn get_connector_about(&self) -> Option<&'static ConnectorInfo> { diff --git a/crates/hyperswitch_connectors/src/connectors/peachpayments/transformers.rs b/crates/hyperswitch_connectors/src/connectors/peachpayments/transformers.rs index b66112f363..349af2ede6 100644 --- a/crates/hyperswitch_connectors/src/connectors/peachpayments/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/peachpayments/transformers.rs @@ -1,7 +1,8 @@ use std::str::FromStr; use cards::CardNumber; -use common_utils::{pii, types::MinorUnit}; +use common_enums::enums as storage_enums; +use common_utils::{errors::CustomResult, pii, types::MinorUnit}; use error_stack::ResultExt; use hyperswitch_domain_models::{ network_tokenization::NetworkTokenNumber, @@ -99,6 +100,7 @@ pub struct EcommerceCardPaymentOnlyTransactionData { pub routing: Routing, pub card: CardDetails, pub amount: AmountDetails, + pub rrn: Option, } #[derive(Debug, Serialize, PartialEq)] @@ -581,6 +583,7 @@ impl TryFrom<(&PeachpaymentsRouterData<&PaymentsAuthorizeRouterData>, Card)> routing, card, amount, + rrn: item.router_data.request.merchant_order_reference_id.clone(), }); // Generate current timestamp for sendDateTime (ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ) @@ -652,9 +655,16 @@ impl From for common_enums::AttemptStatus { } } +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum PeachpaymentsPaymentsResponse { + Response(Box), + WebhookResponse(Box), +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde[rename_all = "camelCase"]] -pub struct PeachpaymentsPaymentsResponse { +pub struct PeachpaymentsPaymentsData { pub transaction_id: String, pub response_code: Option, pub transaction_result: PeachpaymentsPaymentStatus, @@ -751,6 +761,105 @@ fn get_error_message(response_code: Option<&ResponseCode>) -> String { ) } +pub fn get_peachpayments_response( + response: PeachpaymentsPaymentsData, + status_code: u16, +) -> CustomResult< + ( + storage_enums::AttemptStatus, + Result, + ), + errors::ConnectorError, +> { + let status = common_enums::AttemptStatus::from(response.transaction_result); + let payments_response = if !is_payment_success( + response + .response_code + .as_ref() + .and_then(|code| code.value()), + ) { + Err(ErrorResponse { + code: get_error_code(response.response_code.as_ref()), + message: get_error_message(response.response_code.as_ref()), + reason: response + .ecommerce_card_payment_only_transaction_data + .and_then(|data| data.description), + status_code, + attempt_status: Some(status), + connector_transaction_id: Some(response.transaction_id.clone()), + network_advice_code: None, + network_decline_code: None, + network_error_message: None, + connector_metadata: None, + }) + } else { + Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(response.transaction_id.clone()), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(response.transaction_id), + incremental_authorization_allowed: None, + charges: None, + }) + }; + Ok((status, payments_response)) +} + +pub fn get_webhook_response( + response: PeachpaymentsIncomingWebhook, + status_code: u16, +) -> CustomResult< + ( + storage_enums::AttemptStatus, + Result, + ), + errors::ConnectorError, +> { + let transaction = response + .transaction + .ok_or(errors::ConnectorError::WebhookResourceObjectNotFound)?; + let status = common_enums::AttemptStatus::from(transaction.transaction_result); + let webhook_response = if !is_payment_success( + transaction + .response_code + .as_ref() + .and_then(|code| code.value()), + ) { + Err(ErrorResponse { + code: get_error_code(transaction.response_code.as_ref()), + message: get_error_message(transaction.response_code.as_ref()), + reason: transaction + .ecommerce_card_payment_only_transaction_data + .and_then(|data| data.description), + status_code, + attempt_status: Some(status), + connector_transaction_id: Some(transaction.transaction_id.clone()), + network_advice_code: None, + network_decline_code: None, + network_error_message: None, + connector_metadata: None, + }) + } else { + Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId( + transaction + .original_transaction_id + .unwrap_or(transaction.transaction_id.clone()), + ), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(transaction.transaction_id.clone()), + incremental_authorization_allowed: None, + charges: None, + }) + }; + Ok((status, webhook_response)) +} + impl TryFrom> for RouterData { @@ -758,43 +867,13 @@ impl TryFrom, ) -> Result { - let status = common_enums::AttemptStatus::from(item.response.transaction_result); - - // Check if it's an error response - let response = if !is_payment_success( - item.response - .response_code - .as_ref() - .and_then(|code| code.value()), - ) { - Err(ErrorResponse { - code: get_error_code(item.response.response_code.as_ref()), - message: get_error_message(item.response.response_code.as_ref()), - reason: item - .response - .ecommerce_card_payment_only_transaction_data - .and_then(|data| data.description), - status_code: item.http_code, - attempt_status: Some(status), - connector_transaction_id: Some(item.response.transaction_id.clone()), - network_advice_code: None, - network_decline_code: None, - network_error_message: None, - connector_metadata: None, - }) - } else { - Ok(PaymentsResponseData::TransactionResponse { - resource_id: ResponseId::ConnectorTransactionId( - item.response.transaction_id.clone(), - ), - redirection_data: Box::new(None), - mandate_reference: Box::new(None), - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: Some(item.response.transaction_id), - incremental_authorization_allowed: None, - charges: None, - }) + let (status, response) = match item.response { + PeachpaymentsPaymentsResponse::Response(response) => { + get_peachpayments_response(*response, item.http_code)? + } + PeachpaymentsPaymentsResponse::WebhookResponse(response) => { + get_webhook_response(*response, item.http_code)? + } }; Ok(Self { @@ -884,6 +963,28 @@ impl TryFrom<&PeachpaymentsRouterData<&PaymentsAuthorizeRouterData>> } } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PeachpaymentsIncomingWebhook { + pub webhook_id: String, + pub webhook_type: String, + pub reversal_failure_reason: Option, + pub transaction: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct WebhookTransaction { + pub transaction_id: String, + pub original_transaction_id: Option, + pub reference_id: String, + pub transaction_result: PeachpaymentsPaymentStatus, + pub error_message: Option, + pub response_code: Option, + pub ecommerce_card_payment_only_transaction_data: Option, + pub payment_method: Secret, +} + // Error Response #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")]