diff --git a/backend/connector-integration/src/connectors.rs b/backend/connector-integration/src/connectors.rs index 468f0aa8b..429961ffe 100644 --- a/backend/connector-integration/src/connectors.rs +++ b/backend/connector-integration/src/connectors.rs @@ -85,3 +85,6 @@ pub use self::stripe::Stripe; pub mod cybersource; pub use self::cybersource::Cybersource; + +pub mod peachpayments; +pub use self::peachpayments::Peachpayments; diff --git a/backend/connector-integration/src/connectors/peachpayments.rs b/backend/connector-integration/src/connectors/peachpayments.rs new file mode 100644 index 000000000..f108b03f6 --- /dev/null +++ b/backend/connector-integration/src/connectors/peachpayments.rs @@ -0,0 +1,798 @@ +pub mod transformers; + +use common_enums::CurrencyUnit; +use common_utils::{errors::CustomResult, ext_traits::BytesExt}; +use domain_types::{ + connector_flow::{ + Accept, Authenticate, Authorize, Capture, CreateAccessToken, CreateConnectorCustomer, + CreateOrder, CreateSessionToken, DefendDispute, PSync, PaymentMethodToken, + PostAuthenticate, PreAuthenticate, RSync, Refund, RepeatPayment, SetupMandate, + SubmitEvidence, Void, + }, + connector_types::{ + AcceptDisputeData, AccessTokenRequestData, AccessTokenResponseData, ConnectorCustomerData, + ConnectorCustomerResponse, DisputeDefendData, DisputeFlowData, DisputeResponseData, + PaymentCreateOrderData, PaymentCreateOrderResponse, PaymentFlowData, + PaymentMethodTokenResponse, PaymentMethodTokenizationData, PaymentVoidData, + PaymentsAuthenticateData, PaymentsAuthorizeData, PaymentsCaptureData, + PaymentsPostAuthenticateData, PaymentsPreAuthenticateData, PaymentsResponseData, + PaymentsSyncData, RefundFlowData, RefundSyncData, RefundsData, RefundsResponseData, + RepeatPaymentData, SessionTokenRequestData, SessionTokenResponseData, + SetupMandateRequestData, SubmitEvidenceData, + }, + errors, + payment_method_data::PaymentMethodDataTypes, + router_data::{ConnectorAuthType, ErrorResponse}, + router_data_v2::RouterDataV2, + router_response_types::Response, + types::Connectors, +}; +use hyperswitch_masking::{ExposeInterface, Mask, Maskable}; +use interfaces::{ + api::ConnectorCommon, connector_integration_v2::ConnectorIntegrationV2, connector_types, + events::connector_api_logs::ConnectorEvent, +}; +use serde::Serialize; +use std::fmt::Debug; +use transformers::{ + self as peachpayments, PeachpaymentsConfirmRequest, PeachpaymentsConfirmResponse, + PeachpaymentsPaymentsRequest, PeachpaymentsPaymentsResponse, + PeachpaymentsPaymentsResponse as PeachpaymentsSyncResponse, + PeachpaymentsPaymentsResponse as PeachpaymentsVoidResponse, PeachpaymentsVoidRequest, +}; + +use super::macros; +use crate::{types::ResponseRouterData, with_error_response_body}; + +use error_stack::ResultExt; + +// Trait implementations with generic type parameters +impl + connector_types::ConnectorServiceTrait for Peachpayments +{ +} +impl + connector_types::PaymentAuthorizeV2 for Peachpayments +{ +} +impl + connector_types::PaymentSyncV2 for Peachpayments +{ +} +impl + connector_types::PaymentVoidV2 for Peachpayments +{ +} +impl + connector_types::RefundSyncV2 for Peachpayments +{ +} +impl + connector_types::RefundV2 for Peachpayments +{ +} +impl + connector_types::PaymentCapture for Peachpayments +{ +} +impl + connector_types::ValidationTrait for Peachpayments +{ +} +impl + connector_types::PaymentOrderCreate for Peachpayments +{ +} +impl + connector_types::SetupMandateV2 for Peachpayments +{ +} +impl + connector_types::RepeatPaymentV2 for Peachpayments +{ +} +impl + connector_types::AcceptDispute for Peachpayments +{ +} +impl + connector_types::SubmitEvidenceV2 for Peachpayments +{ +} +impl + connector_types::DisputeDefend for Peachpayments +{ +} +impl + connector_types::IncomingWebhook for Peachpayments +{ +} +impl + connector_types::PaymentSessionToken for Peachpayments +{ +} +impl + connector_types::PaymentAccessToken for Peachpayments +{ +} +impl< + T: PaymentMethodDataTypes + + std::fmt::Debug + + std::marker::Sync + + std::marker::Send + + 'static + + Serialize, + > connector_types::CreateConnectorCustomer for Peachpayments +{ +} +impl + connector_types::PaymentTokenV2 for Peachpayments +{ +} + +impl + connector_types::PaymentPreAuthenticateV2 for Peachpayments +{ +} + +impl + connector_types::PaymentAuthenticateV2 for Peachpayments +{ +} + +impl + connector_types::PaymentPostAuthenticateV2 for Peachpayments +{ +} + +pub(crate) mod headers { + pub(crate) const CONTENT_TYPE: &str = "Content-Type"; +} + +macros::create_all_prerequisites!( + connector_name: Peachpayments, + generic_type: T, + api: [ + ( + flow: Authorize, + request_body: PeachpaymentsPaymentsRequest, + response_body: PeachpaymentsPaymentsResponse, + router_data: RouterDataV2, PaymentsResponseData>, + ), + ( + flow: PSync, + response_body: PeachpaymentsSyncResponse, + router_data: RouterDataV2, + ), + ( + flow: Capture, + request_body: PeachpaymentsConfirmRequest, + response_body: PeachpaymentsConfirmResponse, + router_data: RouterDataV2, + ), + ( + flow: Void, + request_body: PeachpaymentsVoidRequest, + response_body: PeachpaymentsVoidResponse, + router_data: RouterDataV2, + ) + ], + amount_converters: [], + member_functions: { + pub fn build_headers( + &self, + req: &RouterDataV2, + ) -> CustomResult)>, errors::ConnectorError> + where + Self: ConnectorIntegrationV2, + { + let mut header = vec![( + headers::CONTENT_TYPE.to_string(), + self.get_content_type().to_string().into(), + )]; + let mut api_key = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut api_key); + Ok(header) + } + + pub fn connector_base_url_payments<'a, F, Req, Res>( + &self, + req: &'a RouterDataV2, + ) -> &'a str { + &req.resource_common_data.connectors.peachpayments.base_url + } + + pub fn connector_base_url_refunds<'a, F, Req, Res>( + &self, + req: &'a RouterDataV2, + ) -> &'a str { + &req.resource_common_data.connectors.peachpayments.base_url + } + } +); + +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Peachpayments, + curl_request: Json(PeachpaymentsPaymentsRequest), + curl_response: PeachpaymentsPaymentsResponse, + flow_name: Authorize, + resource_common_data: PaymentFlowData, + flow_request: PaymentsAuthorizeData, + flow_response: PaymentsResponseData, + http_method: Post, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize], + other_functions: { + fn get_headers( + &self, + req: &RouterDataV2, PaymentsResponseData>, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req) + } + fn get_url( + &self, + req: &RouterDataV2, PaymentsResponseData>, + ) -> CustomResult { + Ok(format!("{}/transactions", self.connector_base_url_payments(req))) + } + } +); + +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Peachpayments, + curl_response: PeachpaymentsSyncResponse, + flow_name: PSync, + resource_common_data: PaymentFlowData, + flow_request: PaymentsSyncData, + flow_response: PaymentsResponseData, + http_method: Get, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize], + other_functions: { + fn get_headers( + &self, + req: &RouterDataV2, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req) + } + fn get_url( + &self, + req: &RouterDataV2, + ) -> CustomResult { + let connector_transaction_id = req + .request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + Ok(format!( + "{}/transactions/{}", + self.connector_base_url_payments(req), + connector_transaction_id + )) + } + } +); + +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Peachpayments, + curl_request: Json(PeachpaymentsConfirmRequest), + curl_response: PeachpaymentsConfirmResponse, + flow_name: Capture, + resource_common_data: PaymentFlowData, + flow_request: PaymentsCaptureData, + flow_response: PaymentsResponseData, + http_method: Post, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize], + other_functions: { + fn get_headers( + &self, + req: &RouterDataV2, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req) + } + fn get_url( + &self, + req: &RouterDataV2, + ) -> CustomResult { + let connector_transaction_id = req + .request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + Ok(format!( + "{}/transactions/{}/confirm", + self.connector_base_url_payments(req), + connector_transaction_id + )) + } + } +); + +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Peachpayments, + curl_request: Json(PeachpaymentsVoidRequest), + curl_response: PeachpaymentsVoidResponse, + flow_name: Void, + resource_common_data: PaymentFlowData, + flow_request: PaymentVoidData, + flow_response: PaymentsResponseData, + http_method: Post, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize], + other_functions: { + fn get_headers( + &self, + req: &RouterDataV2, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req) + } + fn get_url( + &self, + req: &RouterDataV2, + ) -> CustomResult { + let connector_transaction_id = &req.request.connector_transaction_id; + Ok(format!( + "{}/transactions/{}/reverse", + self.connector_base_url_payments(req), + connector_transaction_id + )) + } + } +); + +impl ConnectorCommon + for Peachpayments +{ + fn id(&self) -> &'static str { + "peachpayments" + } + + fn get_currency_unit(&self) -> CurrencyUnit { + // PeachPayments Card Gateway accepts amounts in cents (minor unit) + CurrencyUnit::Minor + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + + fn base_url<'a>(&self, connectors: &'a Connectors) -> &'a str { + connectors.peachpayments.base_url.as_ref() + } + + fn get_auth_header( + &self, + auth_type: &ConnectorAuthType, + ) -> CustomResult)>, errors::ConnectorError> { + let auth = peachpayments::PeachpaymentsAuthType::try_from(auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + Ok(vec![ + ("x-api-key".to_string(), auth.api_key.expose().into_masked()), + ( + "x-tenant-id".to_string(), + auth.tenant_id.expose().into_masked(), + ), + ("x-exi-auth-ver".to_string(), "v1".to_string().into_masked()), + ]) + } + + fn build_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + let response: peachpayments::PeachpaymentsErrorResponse = res + .response + .parse_struct("PeachpaymentsErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + with_error_response_body!(event_builder, response); + + Ok(ErrorResponse { + status_code: res.status_code, + code: response.error_ref.clone(), + message: response.message.clone(), + reason: Some(response.message.clone()), + attempt_status: None, + connector_transaction_id: None, + network_advice_code: None, + network_decline_code: None, + network_error_message: None, + }) + } +} + +// Stub implementations for unsupported flows + +impl< + T: PaymentMethodDataTypes + + std::fmt::Debug + + std::marker::Sync + + std::marker::Send + + 'static + + Serialize, + > ConnectorIntegrationV2 + for Peachpayments +{ +} + +impl< + T: PaymentMethodDataTypes + + std::fmt::Debug + + std::marker::Sync + + std::marker::Send + + 'static + + Serialize, + > ConnectorIntegrationV2 + for Peachpayments +{ +} + +impl< + T: PaymentMethodDataTypes + + std::fmt::Debug + + std::marker::Sync + + std::marker::Send + + 'static + + Serialize, + > + ConnectorIntegrationV2< + CreateOrder, + PaymentFlowData, + PaymentCreateOrderData, + PaymentCreateOrderResponse, + > for Peachpayments +{ +} + +impl< + T: PaymentMethodDataTypes + + std::fmt::Debug + + std::marker::Sync + + std::marker::Send + + 'static + + Serialize, + > + ConnectorIntegrationV2 + for Peachpayments +{ +} + +impl< + T: PaymentMethodDataTypes + + std::fmt::Debug + + std::marker::Sync + + std::marker::Send + + 'static + + Serialize, + > ConnectorIntegrationV2 + for Peachpayments +{ +} + +impl< + T: PaymentMethodDataTypes + + std::fmt::Debug + + std::marker::Sync + + std::marker::Send + + 'static + + Serialize, + > ConnectorIntegrationV2 + for Peachpayments +{ +} + +impl< + T: PaymentMethodDataTypes + + std::fmt::Debug + + std::marker::Sync + + std::marker::Send + + 'static + + Serialize, + > + ConnectorIntegrationV2< + SetupMandate, + PaymentFlowData, + SetupMandateRequestData, + PaymentsResponseData, + > for Peachpayments +{ +} + +impl< + T: PaymentMethodDataTypes + + std::fmt::Debug + + std::marker::Sync + + std::marker::Send + + 'static + + Serialize, + > + ConnectorIntegrationV2 + for Peachpayments +{ +} + +impl< + T: PaymentMethodDataTypes + + std::fmt::Debug + + std::marker::Sync + + std::marker::Send + + 'static + + Serialize, + > + ConnectorIntegrationV2< + CreateSessionToken, + PaymentFlowData, + SessionTokenRequestData, + SessionTokenResponseData, + > for Peachpayments +{ +} + +impl + ConnectorIntegrationV2< + PreAuthenticate, + PaymentFlowData, + PaymentsPreAuthenticateData, + PaymentsResponseData, + > for Peachpayments +{ +} + +impl + ConnectorIntegrationV2< + Authenticate, + PaymentFlowData, + PaymentsAuthenticateData, + PaymentsResponseData, + > for Peachpayments +{ +} + +impl + ConnectorIntegrationV2< + PostAuthenticate, + PaymentFlowData, + PaymentsPostAuthenticateData, + PaymentsResponseData, + > for Peachpayments +{ +} + +impl + ConnectorIntegrationV2< + CreateConnectorCustomer, + PaymentFlowData, + ConnectorCustomerData, + ConnectorCustomerResponse, + > for Peachpayments +{ +} + +// SourceVerification implementations for all flows +impl + interfaces::verification::SourceVerification< + Authorize, + PaymentFlowData, + PaymentsAuthorizeData, + PaymentsResponseData, + > for Peachpayments +{ +} + +impl + interfaces::verification::SourceVerification< + PSync, + PaymentFlowData, + PaymentsSyncData, + PaymentsResponseData, + > for Peachpayments +{ +} + +impl + interfaces::verification::SourceVerification< + Capture, + PaymentFlowData, + PaymentsCaptureData, + PaymentsResponseData, + > for Peachpayments +{ +} + +impl + interfaces::verification::SourceVerification< + Void, + PaymentFlowData, + PaymentVoidData, + PaymentsResponseData, + > for Peachpayments +{ +} + +impl + interfaces::verification::SourceVerification< + Refund, + RefundFlowData, + RefundsData, + RefundsResponseData, + > for Peachpayments +{ +} + +impl + interfaces::verification::SourceVerification< + RSync, + RefundFlowData, + RefundSyncData, + RefundsResponseData, + > for Peachpayments +{ +} + +impl + interfaces::verification::SourceVerification< + SetupMandate, + PaymentFlowData, + SetupMandateRequestData, + PaymentsResponseData, + > for Peachpayments +{ +} + +impl + interfaces::verification::SourceVerification< + Accept, + DisputeFlowData, + AcceptDisputeData, + DisputeResponseData, + > for Peachpayments +{ +} + +impl + interfaces::verification::SourceVerification< + SubmitEvidence, + DisputeFlowData, + SubmitEvidenceData, + DisputeResponseData, + > for Peachpayments +{ +} + +impl + interfaces::verification::SourceVerification< + DefendDispute, + DisputeFlowData, + DisputeDefendData, + DisputeResponseData, + > for Peachpayments +{ +} + +impl + interfaces::verification::SourceVerification< + CreateOrder, + PaymentFlowData, + PaymentCreateOrderData, + PaymentCreateOrderResponse, + > for Peachpayments +{ +} + +impl + interfaces::verification::SourceVerification< + RepeatPayment, + PaymentFlowData, + RepeatPaymentData, + PaymentsResponseData, + > for Peachpayments +{ +} + +impl + interfaces::verification::SourceVerification< + CreateSessionToken, + PaymentFlowData, + SessionTokenRequestData, + SessionTokenResponseData, + > for Peachpayments +{ +} + +impl + interfaces::verification::SourceVerification< + CreateAccessToken, + PaymentFlowData, + AccessTokenRequestData, + AccessTokenResponseData, + > for Peachpayments +{ +} +impl< + T: PaymentMethodDataTypes + + std::fmt::Debug + + std::marker::Sync + + std::marker::Send + + 'static + + Serialize, + > + interfaces::verification::SourceVerification< + PaymentMethodToken, + PaymentFlowData, + PaymentMethodTokenizationData, + PaymentMethodTokenResponse, + > for Peachpayments +{ +} + +impl + ConnectorIntegrationV2< + CreateAccessToken, + PaymentFlowData, + AccessTokenRequestData, + AccessTokenResponseData, + > for Peachpayments +{ +} +impl< + T: PaymentMethodDataTypes + + std::fmt::Debug + + std::marker::Sync + + std::marker::Send + + 'static + + Serialize, + > + ConnectorIntegrationV2< + PaymentMethodToken, + PaymentFlowData, + PaymentMethodTokenizationData, + PaymentMethodTokenResponse, + > for Peachpayments +{ +} + +impl + interfaces::verification::SourceVerification< + PreAuthenticate, + PaymentFlowData, + PaymentsPreAuthenticateData, + PaymentsResponseData, + > for Peachpayments +{ +} + +impl + interfaces::verification::SourceVerification< + Authenticate, + PaymentFlowData, + PaymentsAuthenticateData, + PaymentsResponseData, + > for Peachpayments +{ +} + +impl + interfaces::verification::SourceVerification< + PostAuthenticate, + PaymentFlowData, + PaymentsPostAuthenticateData, + PaymentsResponseData, + > for Peachpayments +{ +} + +impl + interfaces::verification::SourceVerification< + CreateConnectorCustomer, + PaymentFlowData, + ConnectorCustomerData, + ConnectorCustomerResponse, + > for Peachpayments +{ +} diff --git a/backend/connector-integration/src/connectors/peachpayments/transformers.rs b/backend/connector-integration/src/connectors/peachpayments/transformers.rs new file mode 100644 index 000000000..b73cad253 --- /dev/null +++ b/backend/connector-integration/src/connectors/peachpayments/transformers.rs @@ -0,0 +1,885 @@ +use crate::utils; +use common_enums::AttemptStatus; +use common_utils::{ + consts::{NO_ERROR_CODE, NO_ERROR_MESSAGE}, + pii, + types::MinorUnit, +}; +use domain_types::{ + connector_flow::{Authorize, Capture, PSync, Void}, + connector_types::{ + PaymentFlowData, PaymentVoidData, PaymentsAuthorizeData, PaymentsCaptureData, + PaymentsResponseData, PaymentsSyncData, ResponseId, + }, + errors::{self, ConnectorError}, + payment_method_data::{PaymentMethodData, PaymentMethodDataTypes, RawCardNumber}, + router_data::{ConnectorAuthType, ErrorResponse}, + router_data_v2::RouterDataV2, +}; +use error_stack::ResultExt; +use hyperswitch_masking::{PeekInterface, Secret}; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; +use time::OffsetDateTime; + +use super::PeachpaymentsRouterData; +use crate::types::ResponseRouterData; + +impl TryFrom<&Option> for PeachPaymentsConnectorMetadataObject { + type Error = error_stack::Report; + fn try_from(meta_data: &Option) -> Result { + let metadata = utils::to_connector_meta_from_secret::(meta_data.clone()) + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "metadata", + })?; + Ok(metadata) + } +} + +// Card Gateway API Transaction Request +#[derive(Debug, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PeachpaymentsPaymentsRequest< + T: PaymentMethodDataTypes + + std::fmt::Debug + + std::marker::Sync + + std::marker::Send + + 'static + + Serialize, +> { + pub charge_method: PaymentMethod, + pub reference_id: String, + pub ecommerce_card_payment_only_transaction_data: EcommerceCardPaymentOnlyTransactionData, + #[serde(skip_serializing_if = "Option::is_none")] + pub pos_data: Option, + pub send_date_time: String, +} + +#[derive(Debug, Clone, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct EcommerceCardPaymentOnlyTransactionData< + T: PaymentMethodDataTypes + + std::fmt::Debug + + std::marker::Sync + + std::marker::Send + + 'static + + Serialize, +> { + pub merchant_information: MerchantInformation, + pub routing: Routing, + pub card: CardDetails, + pub amount: AmountDetails, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct MerchantInformation { + pub client_merchant_reference_id: Secret, + pub name: Secret, + pub mcc: Secret, + #[serde(skip_serializing_if = "Option::is_none")] + pub phone: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub email: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub mobile: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub address: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub city: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub postal_code: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub region_code: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub merchant_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub website_url: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum MerchantType { + Standard, + Sub, + Iso, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Routing { + pub route: Route, + pub mid: Secret, + pub tid: Secret, + #[serde(skip_serializing_if = "Option::is_none")] + pub visa_payment_facilitator_id: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub master_card_payment_facilitator_id: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub sub_mid: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub amex_id: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +#[serde(rename_all = "snake_case")] +pub enum Route { + #[default] + ExipayEmulator, + AbsaBase24, + NedbankPostbridge, + AbsaPostbridgeEcentric, + PostbridgeDirecttransact, + PostbridgeEfficacy, + FiservLloyds, + NfsIzwe, + AbsaHpsZambia, + EcentricEcommerce, + UnitTestEmptyConfig, +} + +#[derive(Debug, Clone, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct CardDetails< + T: PaymentMethodDataTypes + + std::fmt::Debug + + std::marker::Sync + + std::marker::Send + + 'static + + Serialize, +> { + pub pan: RawCardNumber, + #[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, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct AmountDetails { + pub amount: MinorUnit, + pub currency_code: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub display_amount: Option, +} + +// Confirm Transaction Request (for capture) +#[derive(Debug, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PeachpaymentsConfirmRequest { + pub ecommerce_card_payment_only_confirmation_data: EcommerceCardPaymentOnlyConfirmationData, +} + +#[derive(Debug, Clone, Serialize, PartialEq)] +pub struct EcommerceCardPaymentOnlyConfirmationData { + pub amount: AmountDetails, +} + +// Void Transaction Request +#[derive(Debug, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PeachpaymentsVoidRequest { + pub payment_method: PaymentMethod, + pub send_date_time: String, + pub failure_reason: FailureReason, +} + +#[derive(Debug, Serialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum PaymentMethod { + EcommerceCardPaymentOnly, +} + +#[derive(Default, Debug, Serialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum FailureReason { + UnableToSend, + #[default] + Timeout, + SecurityError, + IssuerUnavailable, + TooLateResponse, + Malfunction, + UnableToComplete, + OnlineDeclined, + SuspectedFraud, + CardDeclined, + Partial, + OfflineDeclined, + CustomerCancel, +} + +impl FromStr for FailureReason { + type Err = error_stack::Report; + + fn from_str(value: &str) -> Result { + match value.to_lowercase().as_str() { + "unable_to_send" => Ok(Self::UnableToSend), + "timeout" => Ok(Self::Timeout), + "security_error" => Ok(Self::SecurityError), + "issuer_unavailable" => Ok(Self::IssuerUnavailable), + "too_late_response" => Ok(Self::TooLateResponse), + "malfunction" => Ok(Self::Malfunction), + "unable_to_complete" => Ok(Self::UnableToComplete), + "online_declined" => Ok(Self::OnlineDeclined), + "suspected_fraud" => Ok(Self::SuspectedFraud), + "card_declined" => Ok(Self::CardDeclined), + "partial" => Ok(Self::Partial), + "offline_declined" => Ok(Self::OfflineDeclined), + "customer_cancel" => Ok(Self::CustomerCancel), + _ => Ok(Self::Timeout), + } + } +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct PeachPaymentsConnectorMetadataObject { + pub client_merchant_reference_id: Secret, + pub name: Secret, + pub mcc: Secret, + #[serde(skip_serializing_if = "Option::is_none")] + pub phone: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub email: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub mobile: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub address: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub city: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub postal_code: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub region_code: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub merchant_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + 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>, + #[serde(skip_serializing_if = "Option::is_none")] + pub master_card_payment_facilitator_id: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub sub_mid: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub amex_id: Option>, +} + +fn get_card_expiry_year_2_digit( + expiry_year: Secret, +) -> Result, errors::ConnectorError> { + let year = expiry_year.peek(); + if year.len() < 2 { + return Err(errors::ConnectorError::RequestEncodingFailed); + } + Ok(Secret::new(year[year.len() - 2..].to_string())) +} + +impl< + T: PaymentMethodDataTypes + + std::fmt::Debug + + std::marker::Sync + + std::marker::Send + + 'static + + Serialize, + > + TryFrom< + PeachpaymentsRouterData< + RouterDataV2< + Authorize, + PaymentFlowData, + PaymentsAuthorizeData, + PaymentsResponseData, + >, + T, + >, + > for PeachpaymentsPaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + item: PeachpaymentsRouterData< + RouterDataV2< + Authorize, + PaymentFlowData, + PaymentsAuthorizeData, + PaymentsResponseData, + >, + T, + >, + ) -> Result { + match item.router_data.request.payment_method_data.clone() { + PaymentMethodData::Card(req_card) => { + let amount_in_cents = item.router_data.request.minor_amount; + + let connector_merchant_config = PeachPaymentsConnectorMetadataObject::try_from( + &item.router_data.resource_common_data.connector_meta_data, + )?; + + let merchant_information = MerchantInformation { + client_merchant_reference_id: connector_merchant_config + .client_merchant_reference_id, + name: connector_merchant_config.name, + mcc: connector_merchant_config.mcc, + phone: connector_merchant_config.phone, + email: connector_merchant_config.email, + mobile: connector_merchant_config.mobile, + address: connector_merchant_config.address, + city: connector_merchant_config.city, + postal_code: connector_merchant_config.postal_code, + region_code: connector_merchant_config.region_code, + merchant_type: connector_merchant_config.merchant_type, + website_url: connector_merchant_config.website_url, + }; + + // Get routing configuration from metadata + let routing = Routing { + route: connector_merchant_config.route, + mid: connector_merchant_config.mid, + tid: connector_merchant_config.tid, + visa_payment_facilitator_id: connector_merchant_config + .visa_payment_facilitator_id, + master_card_payment_facilitator_id: connector_merchant_config + .master_card_payment_facilitator_id, + sub_mid: connector_merchant_config.sub_mid, + amex_id: connector_merchant_config.amex_id, + }; + + let card = CardDetails { + pan: req_card.card_number.clone(), + cardholder_name: req_card.card_holder_name.clone(), + expiry_year: get_card_expiry_year_2_digit(req_card.card_exp_year.clone())?, + expiry_month: req_card.card_exp_month.clone(), + cvv: Some(req_card.card_cvc.clone()), + }; + + let amount = AmountDetails { + amount: amount_in_cents, + currency_code: item.router_data.request.currency.to_string(), + display_amount: None, + }; + + let ecommerce_data = EcommerceCardPaymentOnlyTransactionData { + merchant_information, + routing, + card, + amount, + }; + + // Generate current timestamp for sendDateTime (ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ) + let send_date_time = OffsetDateTime::now_utc() + .format(&time::format_description::well_known::Iso8601::DEFAULT) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + + Ok(Self { + charge_method: PaymentMethod::EcommerceCardPaymentOnly, + reference_id: item + .router_data + .resource_common_data + .connector_request_reference_id, + ecommerce_card_payment_only_transaction_data: ecommerce_data, + pos_data: None, + send_date_time, + }) + } + _ => Err(errors::ConnectorError::NotImplemented("Payment method".to_string()).into()), + } + } +} + +impl< + T: PaymentMethodDataTypes + + std::fmt::Debug + + std::marker::Sync + + std::marker::Send + + 'static + + Serialize, + > + TryFrom< + PeachpaymentsRouterData< + RouterDataV2, + T, + >, + > for PeachpaymentsVoidRequest +{ + type Error = error_stack::Report; + fn try_from( + item: PeachpaymentsRouterData< + RouterDataV2, + T, + >, + ) -> Result { + let send_date_time = OffsetDateTime::now_utc() + .format(&time::format_description::well_known::Iso8601::DEFAULT) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Self { + payment_method: PaymentMethod::EcommerceCardPaymentOnly, + send_date_time, + failure_reason: item + .router_data + .request + .cancellation_reason + .as_ref() + .map(|reason| FailureReason::from_str(reason)) + .transpose()? + .unwrap_or(FailureReason::Timeout), + }) + } +} + +// Auth Struct for Card Gateway API +pub struct PeachpaymentsAuthType { + pub(crate) api_key: Secret, + pub(crate) tenant_id: Secret, +} + +impl TryFrom<&ConnectorAuthType> for PeachpaymentsAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &ConnectorAuthType) -> Result { + if let ConnectorAuthType::BodyKey { api_key, key1 } = auth_type { + Ok(Self { + api_key: api_key.clone(), + tenant_id: key1.clone(), + }) + } else { + Err(errors::ConnectorError::FailedToObtainAuthType)? + } + } +} +// Card Gateway API Response +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum PeachpaymentsPaymentStatus { + Successful, + Pending, + Authorized, + Approved, + ApprovedConfirmed, + Declined, + Failed, + Reversed, + ThreedsRequired, + Voided, +} + +impl From for common_enums::AttemptStatus { + fn from(item: PeachpaymentsPaymentStatus) -> Self { + match item { + // PENDING means authorized but not yet captured - requires confirmation + PeachpaymentsPaymentStatus::Pending + | PeachpaymentsPaymentStatus::Authorized + | PeachpaymentsPaymentStatus::Approved => Self::Authorized, + PeachpaymentsPaymentStatus::Declined | PeachpaymentsPaymentStatus::Failed => { + Self::Failure + } + PeachpaymentsPaymentStatus::Voided | PeachpaymentsPaymentStatus::Reversed => { + Self::Voided + } + PeachpaymentsPaymentStatus::ThreedsRequired => Self::AuthenticationPending, + PeachpaymentsPaymentStatus::ApprovedConfirmed + | PeachpaymentsPaymentStatus::Successful => Self::Charged, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PeachpaymentsPaymentsResponse { + pub transaction_id: String, + pub response_code: Option, + pub transaction_result: PeachpaymentsPaymentStatus, + pub ecommerce_card_payment_only_transaction_data: Option, +} + +// Confirm Transaction Response +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PeachpaymentsConfirmResponse { + pub transaction_id: String, + pub response_code: Option, + pub transaction_result: PeachpaymentsPaymentStatus, + pub authorization_code: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +pub enum ResponseCode { + Text(String), + Structured { + value: String, + description: String, + terminal_outcome_string: Option, + receipt_string: Option, + }, +} + +impl ResponseCode { + pub fn value(&self) -> Option<&String> { + match self { + Self::Structured { value, .. } => Some(value), + _ => None, + } + } + + pub fn description(&self) -> Option<&String> { + match self { + Self::Structured { description, .. } => Some(description), + _ => None, + } + } + + pub fn as_text(&self) -> Option<&String> { + match self { + Self::Text(s) => Some(s), + _ => None, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +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 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< + T: PaymentMethodDataTypes + + std::fmt::Debug + + std::marker::Sync + + std::marker::Send + + 'static + + Serialize, + > + TryFrom< + ResponseRouterData< + PeachpaymentsPaymentsResponse, + RouterDataV2< + Authorize, + PaymentFlowData, + PaymentsAuthorizeData, + PaymentsResponseData, + >, + >, + > for RouterDataV2, PaymentsResponseData> +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData< + PeachpaymentsPaymentsResponse, + RouterDataV2< + Authorize, + PaymentFlowData, + PaymentsAuthorizeData, + PaymentsResponseData, + >, + >, + ) -> Result { + let status = common_enums::AttemptStatus::from(item.response.transaction_result); + + // Check if it's an error response + let response = if status == AttemptStatus::Failure { + 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 + .as_ref() + .and_then(|data| data.description.clone()), + 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, + }) + } else { + Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId( + item.response.transaction_id.clone(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(item.response.transaction_id), + incremental_authorization_allowed: None, + status_code: item.http_code, + }) + }; + + Ok(Self { + resource_common_data: PaymentFlowData { + status, + ..item.router_data.resource_common_data + }, + response, + ..item.router_data + }) + } +} + +impl< + T: PaymentMethodDataTypes + + std::fmt::Debug + + std::marker::Sync + + std::marker::Send + + 'static + + Serialize, + > + TryFrom< + PeachpaymentsRouterData< + RouterDataV2, + T, + >, + > for PeachpaymentsConfirmRequest +{ + type Error = error_stack::Report; + fn try_from( + item: PeachpaymentsRouterData< + RouterDataV2, + T, + >, + ) -> Result { + let amount_in_cents = item.router_data.request.minor_amount_to_capture; + + let amount = AmountDetails { + amount: amount_in_cents, + currency_code: item.router_data.request.currency.to_string(), + display_amount: None, + }; + + let confirmation_data = EcommerceCardPaymentOnlyConfirmationData { amount }; + + Ok(Self { + ecommerce_card_payment_only_confirmation_data: confirmation_data, + }) + } +} + +impl TryFrom> + for RouterDataV2 +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData, + ) -> Result { + let status = common_enums::AttemptStatus::from(item.response.transaction_result); + + // Check if it's an error response + let response = if status == AttemptStatus::Failure { + Err(ErrorResponse { + code: get_error_code(item.response.response_code.as_ref()), + message: get_error_message(item.response.response_code.as_ref()), + reason: None, + 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, + }) + } else { + Ok(PaymentsResponseData::TransactionResponse { + status_code: item.http_code, + resource_id: ResponseId::ConnectorTransactionId( + item.response.transaction_id.clone(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: item.response.authorization_code.map(|auth_code| { + serde_json::json!({ + "authorization_code": auth_code + }) + }), + network_txn_id: None, + connector_response_reference_id: Some(item.response.transaction_id), + incremental_authorization_allowed: None, + }) + }; + + Ok(Self { + resource_common_data: PaymentFlowData { + status, + ..item.router_data.resource_common_data + }, + response, + ..item.router_data + }) + } +} + +impl + TryFrom< + ResponseRouterData< + PeachpaymentsPaymentsResponse, + RouterDataV2, + >, + > for RouterDataV2 +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData< + PeachpaymentsPaymentsResponse, + RouterDataV2, + >, + ) -> Result { + let status = common_enums::AttemptStatus::from(item.response.transaction_result); + + // Check if it's an error response + let response = if status == AttemptStatus::Failure { + 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 + .as_ref() + .and_then(|data| data.description.clone()), + 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, + }) + } else { + Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId( + item.response.transaction_id.clone(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(item.response.transaction_id), + incremental_authorization_allowed: None, + status_code: item.http_code, + }) + }; + + Ok(Self { + resource_common_data: PaymentFlowData { + status, + ..item.router_data.resource_common_data + }, + response, + ..item.router_data + }) + } +} + +impl + TryFrom< + ResponseRouterData< + PeachpaymentsPaymentsResponse, + RouterDataV2, + >, + > for RouterDataV2 +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData< + PeachpaymentsPaymentsResponse, + RouterDataV2, + >, + ) -> Result { + let status = common_enums::AttemptStatus::from(item.response.transaction_result); + + // Check if it's an error response + let response = if status == AttemptStatus::Failure { + 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 + .as_ref() + .and_then(|data| data.description.clone()), + 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, + }) + } else { + Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId( + item.response.transaction_id.clone(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(item.response.transaction_id), + incremental_authorization_allowed: None, + status_code: item.http_code, + }) + }; + + Ok(Self { + resource_common_data: PaymentFlowData { + status, + ..item.router_data.resource_common_data + }, + response, + ..item.router_data + }) + } +} + +// Error Response +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PeachpaymentsErrorResponse { + pub error_ref: String, + pub message: String, +} + +impl TryFrom for PeachpaymentsErrorResponse { + type Error = error_stack::Report; + + fn try_from(error_response: ErrorResponse) -> Result { + Ok(Self { + error_ref: error_response.code, + message: error_response.message, + }) + } +} diff --git a/backend/connector-integration/src/types.rs b/backend/connector-integration/src/types.rs index d3849735d..279665f2b 100644 --- a/backend/connector-integration/src/types.rs +++ b/backend/connector-integration/src/types.rs @@ -6,7 +6,8 @@ use interfaces::connector_types::BoxedConnector; use crate::connectors::{ Aci, Adyen, Authorizedotnet, Bluecode, Braintree, Cashfree, Cashtocode, Checkout, Cryptopay, Cybersource, Dlocal, Elavon, Fiserv, Fiuu, Helcim, Mifinity, Nexinets, Noon, Novalnet, Paytm, - Payu, Phonepe, Placetopay, Rapyd, Razorpay, RazorpayV2, Stripe, Trustpay, Volt, Xendit, + Payu, Peachpayments, Phonepe, Placetopay, Rapyd, Razorpay, RazorpayV2, Stripe, Trustpay, Volt, + Xendit, }; #[derive(Clone)] @@ -58,6 +59,7 @@ impl Box::new(Trustpay::new()), ConnectorEnum::Stripe => Box::new(Stripe::new()), ConnectorEnum::Cybersource => Box::new(Cybersource::new()), + ConnectorEnum::Peachpayments => Box::new(Peachpayments::new()), } } } diff --git a/backend/domain_types/src/connector_types.rs b/backend/domain_types/src/connector_types.rs index 33d4513be..dadc998fe 100644 --- a/backend/domain_types/src/connector_types.rs +++ b/backend/domain_types/src/connector_types.rs @@ -72,6 +72,7 @@ pub enum ConnectorEnum { Trustpay, Stripe, Cybersource, + Peachpayments, } impl ForeignTryFrom for ConnectorEnum { @@ -110,6 +111,7 @@ impl ForeignTryFrom for ConnectorEnum { grpc_api_types::payments::Connector::Trustpay => Ok(Self::Trustpay), grpc_api_types::payments::Connector::Stripe => Ok(Self::Stripe), grpc_api_types::payments::Connector::Cybersource => Ok(Self::Cybersource), + grpc_api_types::payments::Connector::Peachpayments => Ok(Self::Peachpayments), grpc_api_types::payments::Connector::Unspecified => { Err(ApplicationErrorResponse::BadRequest(ApiError { sub_code: "UNSPECIFIED_CONNECTOR".to_owned(), diff --git a/backend/domain_types/src/types.rs b/backend/domain_types/src/types.rs index d667ebc2b..b84bc2dde 100644 --- a/backend/domain_types/src/types.rs +++ b/backend/domain_types/src/types.rs @@ -124,6 +124,7 @@ pub struct Connectors { pub trustpay: ConnectorParamsWithMoreUrls, pub stripe: ConnectorParams, pub cybersource: ConnectorParams, + pub peachpayments: ConnectorParams, } #[derive(Clone, serde::Deserialize, Debug, Default)] diff --git a/backend/grpc-api-types/proto/payment.proto b/backend/grpc-api-types/proto/payment.proto index 8ade4a644..5abaf13ab 100644 --- a/backend/grpc-api-types/proto/payment.proto +++ b/backend/grpc-api-types/proto/payment.proto @@ -456,6 +456,7 @@ enum Connector { CASHFREE = 92; PAYTM = 93; BLUECODE = 94; + PEACHPAYMENTS = 95; } // Payment method types. diff --git a/backend/grpc-server/tests/peachpayments_payment_flows_test.rs b/backend/grpc-server/tests/peachpayments_payment_flows_test.rs new file mode 100644 index 000000000..9a038639d --- /dev/null +++ b/backend/grpc-server/tests/peachpayments_payment_flows_test.rs @@ -0,0 +1,355 @@ +#![allow(clippy::expect_used)] +#![allow(clippy::unwrap_used)] +#![allow(clippy::panic)] + +use cards::CardNumber; +use grpc_server::{app, configs}; +mod common; +use std::{ + collections::HashMap, + env, + str::FromStr, + time::{SystemTime, UNIX_EPOCH}, +}; + +use grpc_api_types::{ + health_check::{health_client::HealthClient, HealthCheckRequest}, + payments::{ + card_payment_method_type, identifier::IdType, payment_method, + payment_service_client::PaymentServiceClient, AuthenticationType, CaptureMethod, + CardDetails, CardPaymentMethodType, Currency, Identifier, PaymentMethod, + PaymentServiceAuthorizeRequest, PaymentServiceAuthorizeResponse, + PaymentServiceCaptureRequest, PaymentServiceGetRequest, PaymentServiceVoidRequest, + PaymentStatus, + }, +}; +use hyperswitch_masking::Secret; +use serde_json::json; +use tonic::{transport::Channel, Request}; + +// Constants for Peachpayments connector +const CONNECTOR_NAME: &str = "peachpayments"; +const AUTH_TYPE: &str = "body-key"; +const MERCHANT_ID: &str = "merchant_1758520172"; + +// Environment variable names for API credentials (can be set or overridden with +// provided values) +const PEACHPAYMENTS_API_KEY_ENV: &str = "TEST_PEACHPAYMENTS_API_KEY"; +const PEACHPAYMENTS_KEY1_ENV: &str = "TEST_PEACHPAYMENTS_KEY1"; + +// Test card data +const TEST_AMOUNT: i64 = 1000; +const TEST_CARD_NUMBER: &str = "4242424242424242"; // Valid test card for Peachpayments +const TEST_CARD_EXP_MONTH: &str = "10"; +const TEST_CARD_EXP_YEAR: &str = "25"; +const TEST_CARD_CVC: &str = "123"; +const TEST_CARD_HOLDER: &str = "Test User"; +const TEST_EMAIL: &str = "customer@example.com"; + +// Helper function to get current timestamp +fn get_timestamp() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() +} + +// Helper function to add Peachpayments metadata headers to a request +fn add_peachpayments_metadata(request: &mut Request) { + // Get API credentials from environment variables - throw error if not set + let api_key = env::var(PEACHPAYMENTS_API_KEY_ENV) + .expect("TEST_PEACHPAYMENTS_API_KEY environment variable is required"); + let key1 = env::var(PEACHPAYMENTS_KEY1_ENV) + .expect("TEST_PEACHPAYMENTS_KEY1 environment variable is required"); + + request.metadata_mut().append( + "x-connector", + CONNECTOR_NAME.parse().expect("Failed to parse x-connector"), + ); + request + .metadata_mut() + .append("x-auth", AUTH_TYPE.parse().expect("Failed to parse x-auth")); + + request.metadata_mut().append( + "x-api-key", + api_key.parse().expect("Failed to parse x-api-key"), + ); + request + .metadata_mut() + .append("x-key1", key1.parse().expect("Failed to parse x-key1")); + request.metadata_mut().append( + "x-merchant-id", + MERCHANT_ID.parse().expect("Failed to parse x-merchant-id"), + ); + request.metadata_mut().append( + "x-request-id", + format!("test_request_{}", get_timestamp()) + .parse() + .expect("Failed to parse x-request-id"), + ); +} + +// Helper function to extract connector transaction ID from response +fn extract_transaction_id(response: &PaymentServiceAuthorizeResponse) -> String { + match &response.transaction_id { + Some(id) => match id.id_type.as_ref().unwrap() { + IdType::Id(id) => id.clone(), + _ => panic!("Expected connector transaction ID"), + }, + None => panic!("Resource ID is None"), + } +} + +// Helper function to create a payment authorize request +fn create_payment_authorize_request( + capture_method: CaptureMethod, +) -> PaymentServiceAuthorizeRequest { + let card_details = card_payment_method_type::CardType::Credit(CardDetails { + card_number: Some(CardNumber::from_str(TEST_CARD_NUMBER).unwrap()), + card_exp_month: Some(Secret::new(TEST_CARD_EXP_MONTH.to_string())), + card_exp_year: Some(Secret::new(TEST_CARD_EXP_YEAR.to_string())), + card_cvc: Some(Secret::new(TEST_CARD_CVC.to_string())), + card_holder_name: Some(Secret::new(TEST_CARD_HOLDER.to_string())), + card_network: Some(1), + card_issuer: None, + card_type: None, + card_issuing_country_alpha2: None, + bank_code: None, + nick_name: None, + }); + let mut metadata: HashMap = HashMap::new(); + + let client_merchant_reference_id = + env::var("TEST_CLIENT_MERCHANT_REFERENCE_ID").expect("missing env var"); + let merchant_name = env::var("TEST_MERCHANT_NAME").expect("missing env var"); + let mcc = env::var("TEST_MCC").expect("missing env var"); + let route = env::var("TEST_ROUTE").expect("missing env var"); + let merchant_id = env::var("TEST_PEACHPAYMENTS_MERCHANT_ID").expect("missing env var"); + + let tid = env::var("TEST_TID").expect("missing env var"); + let connector_meta_data = json!({ + "client_merchant_reference_id": client_merchant_reference_id, + "name": merchant_name, + "mcc": mcc, + "route": route, + "mid": merchant_id, + "tid": tid, + }) + .to_string(); + + metadata.insert("connector_meta_data".to_string(), connector_meta_data); + + PaymentServiceAuthorizeRequest { + amount: TEST_AMOUNT, + minor_amount: TEST_AMOUNT, + currency: i32::from(Currency::Usd), + payment_method: Some(PaymentMethod { + payment_method: Some(payment_method::PaymentMethod::Card(CardPaymentMethodType { + card_type: Some(card_details), + })), + }), + return_url: Some("https://duck.com".to_string()), + email: Some(TEST_EMAIL.to_string().into()), + address: Some(grpc_api_types::payments::PaymentAddress::default()), + auth_type: i32::from(AuthenticationType::NoThreeDs), + request_ref_id: Some(Identifier { + id_type: Some(IdType::Id(format!("ref_{}", get_timestamp()))), + }), + enrolled_for_3ds: false, + request_incremental_authorization: false, + capture_method: Some(i32::from(capture_method)), + metadata, + ..Default::default() + } +} + +// Helper function to create a payment sync request +fn create_payment_sync_request(transaction_id: &str) -> PaymentServiceGetRequest { + PaymentServiceGetRequest { + transaction_id: Some(Identifier { + id_type: Some(IdType::Id(transaction_id.to_string())), + }), + request_ref_id: None, + access_token: None, + // all_keys_required: None, + capture_method: None, + handle_response: None, + amount: TEST_AMOUNT, + currency: i32::from(Currency::Eur), + } +} + +// Helper function to create a payment capture request +fn create_payment_capture_request(transaction_id: &str) -> PaymentServiceCaptureRequest { + PaymentServiceCaptureRequest { + transaction_id: Some(Identifier { + id_type: Some(IdType::Id(transaction_id.to_string())), + }), + amount_to_capture: TEST_AMOUNT, + currency: i32::from(Currency::Usd), + multiple_capture_data: None, + request_ref_id: None, + ..Default::default() + } +} + +// Helper function to create a payment void request +fn create_payment_void_request(transaction_id: &str) -> PaymentServiceVoidRequest { + PaymentServiceVoidRequest { + transaction_id: Some(Identifier { + id_type: Some(IdType::Id(transaction_id.to_string())), + }), + cancellation_reason: Some("requested by customer".to_string()), + request_ref_id: Some(Identifier { + id_type: Some(IdType::Id(format!("void_ref_{}", get_timestamp()))), + }), + all_keys_required: None, + browser_info: None, + access_token: None, + amount: None, + currency: Some(i32::from(Currency::Zar)), + } +} + +// Test for basic health check +#[tokio::test] +async fn test_health() { + grpc_test!(client, HealthClient, { + let response = client + .check(Request::new(HealthCheckRequest { + service: "connector_service".to_string(), + })) + .await + .expect("Failed to call health check") + .into_inner(); + + assert_eq!( + response.status(), + grpc_api_types::health_check::health_check_response::ServingStatus::Serving + ); + }); +} + +// Test payment authorization with manual capture +#[tokio::test] +async fn test_payment_authorization_manual_capture() { + grpc_test!(client, PaymentServiceClient, { + // Add delay of 4 seconds + tokio::time::sleep(std::time::Duration::from_secs(4)).await; + + // Create the payment authorization request with manual capture + let auth_request = create_payment_authorize_request(CaptureMethod::Manual); + + // Add metadata headers for auth request + let mut auth_grpc_request = Request::new(auth_request); + add_peachpayments_metadata(&mut auth_grpc_request); + + // Send the auth request + let auth_response = client + .authorize(auth_grpc_request) + .await + .expect("gRPC authorize call failed") + .into_inner(); + + // Verify payment status + assert!( + auth_response.status == i32::from(PaymentStatus::AuthenticationPending) + || auth_response.status == i32::from(PaymentStatus::Pending) + || auth_response.status == i32::from(PaymentStatus::Authorized), + "Payment should be in AuthenticationPending or Pending state" + ); + + // Extract the transaction ID + let transaction_id = extract_transaction_id(&auth_response); + + // Create capture request + let capture_request = create_payment_capture_request(&transaction_id); + + // Add metadata headers for capture request - make sure they include the terminal_id + let mut capture_grpc_request = Request::new(capture_request); + add_peachpayments_metadata(&mut capture_grpc_request); + + // Send the capture request + let capture_response = client + .capture(capture_grpc_request) + .await + .expect("gRPC payment_capture call failed") + .into_inner(); + + // Verify payment status is charged after capture + assert!( + capture_response.status == i32::from(PaymentStatus::Charged), + "Payment should be in CHARGED state after capture" + ); + }); +} + +// Test payment void +#[tokio::test] +async fn test_payment_void() { + grpc_test!(client, PaymentServiceClient, { + // Add delay of 12 seconds + tokio::time::sleep(std::time::Duration::from_secs(12)).await; + + // First create a payment with manual capture to void + let auth_request = create_payment_authorize_request(CaptureMethod::Manual); + + // Add metadata headers for auth request + let mut auth_grpc_request = Request::new(auth_request); + add_peachpayments_metadata(&mut auth_grpc_request); + + // Send the auth request + let auth_response = client + .authorize(auth_grpc_request) + .await + .expect("gRPC payment_authorize call failed") + .into_inner(); + + // Extract the transaction ID + let transaction_id = extract_transaction_id(&auth_response); + + // Verify payment status + assert!( + auth_response.status == i32::from(PaymentStatus::Authorized), + "Payment should be in AUTHORIZED state before voiding" + ); + + // Create void request with a unique reference ID + let void_request = create_payment_void_request(&transaction_id); + + // Add metadata headers for void request + let mut void_grpc_request = Request::new(void_request); + add_peachpayments_metadata(&mut void_grpc_request); + + // Send the void request + let void_response = client + .void(void_grpc_request) + .await + .expect("gRPC void_payment call failed") + .into_inner(); + + // Verify the void response + assert!( + void_response.status == i32::from(PaymentStatus::Voided), + "Payment should be in VOIDED state after void" + ); + + // Verify the payment status with a sync operation + let sync_request = create_payment_sync_request(&transaction_id); + let mut sync_grpc_request = Request::new(sync_request.clone()); + add_peachpayments_metadata(&mut sync_grpc_request); + + // Send the sync request to verify void status + let sync_response = client + .get(sync_grpc_request) + .await + .expect("gRPC payment_sync call failed") + .into_inner(); + + // Verify the payment is properly voided + assert!( + sync_response.status == i32::from(PaymentStatus::Voided), + "Payment should be in VOIDED state after void sync" + ); + }); +} diff --git a/config/development.toml b/config/development.toml index 393ae7178..704b9b285 100644 --- a/config/development.toml +++ b/config/development.toml @@ -61,6 +61,7 @@ trustpay.base_url = "https://test-tpgw.trustpay.eu/" trustpay.base_url_bank_redirects = "https://aapi.trustpay.eu/" stripe.base_url = "https://api.stripe.com/" cybersource.base_url = "https://apitest.cybersource.com/" +peachpayments.base_url = "https://apitest.bankint.ppay.io/v/1" # Generic Events Configuration [events] diff --git a/config/production.toml b/config/production.toml index 1bcdf696f..f38affe53 100644 --- a/config/production.toml +++ b/config/production.toml @@ -53,6 +53,7 @@ trustpay.base_url = "https://tpgw.trustpay.eu/" trustpay.base_url_bank_redirects = "https://aapi.trustpay.eu/" stripe.base_url = "https://api.stripe.com/" cybersource.base_url = "https://api.cybersource.com/" +peachpayments.base_url = "https://api.bankint.peachpayments.com" # Generic Events Configuration [events] diff --git a/config/sandbox.toml b/config/sandbox.toml index ca44e0436..b2edd9c69 100644 --- a/config/sandbox.toml +++ b/config/sandbox.toml @@ -53,6 +53,7 @@ trustpay.base_url = "https://test-tpgw.trustpay.eu/" trustpay.base_url_bank_redirects = "https://aapi.trustpay.eu/" stripe.base_url = "https://api.stripe.com/" cybersource.base_url = "https://apitest.cybersource.com/" +peachpayments.base_url = "https://apitest.bankint.ppay.io/v/1" # Generic Events Configuration [events]