feat(connector): [PeachPayments] Add Cards Flow#9030
feat(connector): [PeachPayments] Add Cards Flow#9030likhinbopanna merged 16 commits intojuspay:mainfrom
Conversation
Changed Files
|
| _req: &PaymentsAuthorizeRouterData, | ||
| connectors: &Connectors, | ||
| ) -> CustomResult<String, errors::ConnectorError> { | ||
| Ok(format!("{}/transactions", self.base_url(connectors))) |
There was a problem hiding this comment.
| Ok(format!("{}/transactions", self.base_url(connectors))) | |
| Ok(format!("{}transactions", self.base_url(connectors))) |
Can we make this change across all flows since the base url already has a training / at the end?
| paystack.base_url = "https://api.paystack.co" | ||
| payu.base_url = "https://secure.snd.payu.com/" | ||
| peachpayments.base_url = "https://apitest.bankint.ppay.io/v/1" | ||
| placetopay.base_url = "https://test.placetopay.com/rest/gateway" |
There was a problem hiding this comment.
| placetopay.base_url = "https://test.placetopay.com/rest/gateway" | |
| placetopay.base_url = "https://test.placetopay.com/rest/gateway/" |
Can we add a trailing / since the same is done in deployments/production.toml file as well ?
Can we do the similar change for deployments/sandbox.toml, deployments/docker_compose.toml?
| #[serde(rename = "sendDateTime")] | ||
| pub send_date_time: String, | ||
| } | ||
|
|
There was a problem hiding this comment.
Instead of adding #[serde(rename = "chargeMethod")] on top of every field we can look at adding #[serde(rename_all = "camelCase")] at the top of the struct?
Can we do the same across all structs?
|
|
||
| #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] | ||
| pub struct AmountDetails { | ||
| pub amount: i64, |
There was a problem hiding this comment.
| pub amount: i64, | |
| pub amount: MinorUnit, |
Can we use custom user defined data type for Amount Fields?
| ); | ||
|
|
||
| Ok(Self { | ||
| payment_method: "ecommerce_card_payment_only".to_string(), |
There was a problem hiding this comment.
Can we make this field's type a enum and add ecommerce_card_payment_only as an enum variant?
| impl TryFrom<&ConnectorAuthType> for PeachpaymentsAuthType { | ||
| type Error = error_stack::Report<errors::ConnectorError>; | ||
| fn try_from(auth_type: &ConnectorAuthType) -> Result<Self, Self::Error> { | ||
| match auth_type { | ||
| ConnectorAuthType::BodyKey { api_key, key1: tenant_id } => { | ||
| Ok(Self { | ||
| api_key: api_key.to_owned(), | ||
| tenant_id: tenant_id.to_owned(), | ||
| }) | ||
| }, | ||
| ConnectorAuthType::MultiAuthKey { | ||
| api_key, | ||
| key1: tenant_id, | ||
| .. | ||
| } => { | ||
| Ok(Self { | ||
| api_key: api_key.to_owned(), | ||
| tenant_id: tenant_id.to_owned(), | ||
| }) | ||
| }, | ||
| _ => { | ||
| Err(errors::ConnectorError::FailedToObtainAuthType.into()) | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
| impl TryFrom<&ConnectorAuthType> for PeachpaymentsAuthType { | |
| type Error = error_stack::Report<errors::ConnectorError>; | |
| fn try_from(auth_type: &ConnectorAuthType) -> Result<Self, Self::Error> { | |
| match auth_type { | |
| ConnectorAuthType::BodyKey { api_key, key1: tenant_id } => { | |
| Ok(Self { | |
| api_key: api_key.to_owned(), | |
| tenant_id: tenant_id.to_owned(), | |
| }) | |
| }, | |
| ConnectorAuthType::MultiAuthKey { | |
| api_key, | |
| key1: tenant_id, | |
| .. | |
| } => { | |
| Ok(Self { | |
| api_key: api_key.to_owned(), | |
| tenant_id: tenant_id.to_owned(), | |
| }) | |
| }, | |
| _ => { | |
| Err(errors::ConnectorError::FailedToObtainAuthType.into()) | |
| } | |
| } | |
| } | |
| } | |
| impl TryFrom<&ConnectorAuthType> for PeachpaymentsAuthType { | |
| type Error = error_stack::Report<errors::ConnectorError>; | |
| fn try_from(auth_type: &ConnectorAuthType) -> Result<Self, Self::Error> { | |
| 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)? | |
| } | |
| } | |
| } |
Can we eliminate the handling of MultiAuthKey since the connector will always have a type of BodyKey?
|
|
||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||
| pub struct ResponseCode { | ||
| pub value: String, |
There was a problem hiding this comment.
can we make this type a enum and include variants like 00?
| } else { | ||
| Ok(PaymentsResponseData::TransactionResponse { | ||
| resource_id: ResponseId::ConnectorTransactionId(item.response.transaction_id.clone()), | ||
| redirection_data: Box::new(None), |
There was a problem hiding this comment.
Can we please get to know in case of 3ds payments, how would we get the redirect link for the customer to authenticate?
|
|
||
| // Extract card details from refund_connector_metadata | ||
| let card = if let Some(ref connector_metadata) = item.router_data.request.refund_connector_metadata { | ||
| let metadata_value = connector_metadata.clone().expose(); |
There was a problem hiding this comment.
Can we get to know why card details are again needed for Refund?
| impl TryFrom<ErrorResponse> for PeachpaymentsErrorResponse { | ||
| type Error = error_stack::Report<errors::ConnectorError>; | ||
|
|
||
| fn try_from(error_response: ErrorResponse) -> Result<Self, Self::Error> { | ||
| Ok(Self { | ||
| error_ref: error_response.code, | ||
| message: error_response.message, | ||
| error_details: serde_json::json!({}), | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
can we get to know where is this try_from being used?
4eb9d07 to
0a7dc52
Compare
…tion and API support - Added Peachpayments base URL to configuration files (development, example, docker compose, integration test, sandbox). - Introduced Peachpayments connector in the codebase, including payment and refund handling. - Implemented necessary transformers and request/response structures for Peachpayments. - Updated connector enums and validation to include Peachpayments. - Added tests for Peachpayments integration to ensure functionality.
…card networks, add amexId, remove defaults - Restrict supported card networks to only Mastercard, Visa, AmericanExpress across all config files - Add amexId field to routing configuration for AmEx transactions with optional serialization - Remove default values from transformer structs - omit keys when values not present instead - Update merchant_type to Select field with options: iso, sub - Update routing_route to Select field with options: exipay_emulator, absa_base24, nedbank_postbridge - Make MerchantInformation fields optional except client_merchant_reference_id - Make CardDetails cardholder_name and cvv optional for security compliance - Ensure routing_route is required in connector metadata with proper validation - Apply changes consistently across development, production, and sandbox configurations
…ments connector
- Add PeachpaymentsVoidRequest struct with payment_method, send_date_time, and failure_reason fields
- Implement TryFrom for PaymentsCancelRouterData with proper ISO 8601 timestamp generation
- Add complete void flow implementation in main connector with proper request/response handling
- Support POST /transactions/{transactionId}/void endpoint as per POC examples
- Use "timeout" as default failure_reason to match script behavior
- Reuse existing PeachpaymentsPaymentsResponse for void response handling
This brings the connector up to standard with all required flows: create, confirm, refund, sync, and void.
Comprehensive improvements to 3D Secure implementation: • Enhanced ThreeDSData struct with all required fields: - Added tavv field for Token Authentication Verification Value - Added xid field for Transaction Identifier - Improved field handling with proper optional serialization • Improved authentication status mapping: - ECI "05"/"06" → "Y" (fully authenticated) - ECI "07" → "A" (attempted authentication) - Other ECI values → "N" (not authenticated) • Better 3DS data extraction: - Enhanced CAVV validation and handling - Proper version string truncation for API compatibility - Improved ds_trans_id extraction with fallback logic • Comprehensive field validation: - All fields properly handle None values - skip_serializing_if applied consistently - Default ECI value "07" for attempted authentication Limitations documented: - Hyperswitch AuthenticationData struct missing tavv/xid fields - Code structured to accept these fields when available - Enhanced error handling for missing authentication data
…peachpayments - Updated comments to accurately reflect that tavv and xid are network token fields - tavv: Token Authentication Verification Value (network tokens) - xid: Legacy 3DS 1.x/network token Transaction Identifier - These fields are part of PeachPayments API spec but not standard 3DS fields
- Added proper validation for capture methods (Manual, Automatic, SequentialAutomatic) - Added validation for payment methods (Card only for now) - Added validation for payment method types (Credit, Debit) - Provides clear error messages for unsupported features - Follows Hyperswitch connector validation patterns
0a7dc52 to
789236d
Compare
| [[peachpayments.debit]] | ||
| payment_method_type = "Visa" | ||
| [[peachpayments.debit]] | ||
| payment_method_type = "AmericanExpress" |
There was a problem hiding this comment.
just making sure we don't want to include any other card networks for now?
There was a problem hiding this comment.
Confirmed with the Peachpayments team, they support Mastercard Visa and AmericanExpress.
| required=true | ||
| type="Text" | ||
|
|
||
|
|
There was a problem hiding this comment.
please remove the extra whitespaces
config/deployments/production.toml
Outdated
| paypal.base_url = "https://api-m.paypal.com/" | ||
| paysafe.base_url = "https://api.paysafe.com/paymenthub/" | ||
| paystack.base_url = "https://api.paystack.co" | ||
| peachpayments.base_url = "https://api.bankint.peachpayments.com" |
There was a problem hiding this comment.
This url already exists right ?
| // Truncate version to match API spec (e.g., "2.2.0" -> "2.2") | ||
| if version_str.len() > 3 && version_str.chars().nth(3) == Some('.') | ||
| { | ||
| version_str[..3].to_string() |
There was a problem hiding this comment.
using [..3] assumes a strict format (e.g., "2.2.0"). If version strings change,code may panic -
we can use something safer like split
let mut parts = v.to_string().split('.');
match (parts.next(), parts.next()) {
(Some(major), Some(minor)) => format!("{}.{}", major, minor),
_ => v.to_string(), // fallback if format unexpected
}
| .map(|reason| { | ||
| FailureReason::from_str(reason) | ||
| }) | ||
| .transpose()? |
There was a problem hiding this comment.
nit: Right now FromStr always returns Ok, so the Result / transpose()? isn’t really needed. We could make a helper like FailureReason::from_str(reason) that directly returns a FailureReason (defaulting to Timeout), which would make this block easier to read.
Type of Change
Description
This PR implements the complete Peachpayments connector integration for Hyperswitch, adding support for payment processing, 3DS authentication, void transactions, and comprehensive validation.
Key features implemented:
Additional Changes
Motivation and Context
This change adds Peachpayments as a new payment processor option in Hyperswitch, expanding payment gateway options for merchants. The implementation follows the connector integration pattern and includes all necessary features for production use including security validations and 3DS support.
How did you test it?
Postman Test
Request:
Response:
Request:
Response:
Request:
Response:
Request:
Response:
Request:
Response:
Checklist
cargo +nightly fmt --allcargo clippy