feat(connector): Implement capture and webhook flow, fix some issues in ACI (#8349)

Co-authored-by: Anurag Singh <anurag.singh.001@MacBookPro.lan>
Co-authored-by: Anurag Singh <anurag.singh.001@Anurag-Singh-WPMHJ5619X.local>
This commit is contained in:
Anurag
2025-07-01 14:50:45 +05:30
committed by GitHub
parent cbf076db13
commit 1ae30247ca
6 changed files with 582 additions and 49 deletions

View File

@ -23,7 +23,7 @@ payout_connector_list = "nomupay,stripe,wise"
# base urls based on your need.
# Note: These are not optional attributes. hyperswitch request can fail due to invalid/empty values.
[connectors]
aci.base_url = "https://eu-test.oppwa.com/"
aci.base_url = "https://eu-prod.oppwa.com/"
adyen.base_url = "https://{{merchant_endpoint_prefix}}-checkout-live.adyenpayments.com/checkout/"
adyen.payout_base_url = "https://{{merchant_endpoint_prefix}}-pal-live.adyenpayments.com/"
adyen.dispute_base_url = "https://{{merchant_endpoint_prefix}}-ca-live.adyen.com/"

View File

@ -687,8 +687,8 @@ red_pagos = { country = "UY", currency = "UYU" }
local_bank_transfer = { country = "CN", currency = "CNY" }
[pm_filters.aci]
credit = { not_available_flows = { capture_method = "manual" }, country = "AD,AE,AT,BE,BG,CH,CN,CO,CR,CY,CZ,DE,DK,DO,EE,EG,ES,ET,FI,FR,GB,GH,GI,GR,GT,HN,HK,HR,HU,ID,IE,IS,IT,JP,KH,LA,LI,LT,LU,LY,MK,MM,MX,MY,MZ,NG,NZ,OM,PA,PE,PK,PL,PT,QA,RO,SA,SN,SE,SI,SK,SV,TH,UA,US,UY,VN,ZM", currency = "AED,ALL,ARS,BGN,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,EGP,EUR,GBP,GHS,HKD,HNL,HRK,HUF,IDR,ILS,ISK,JPY,KHR,KPW,LAK,LKR,MAD,MKD,MMK,MXN,MYR,MZN,NGN,NOK,NZD,OMR,PAB,PEN,PHP,PKR,PLN,QAR,RON,RSD,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,UYU,VND,ZAR,ZMW" }
debit = { not_available_flows = { capture_method = "manual" }, country = "AD,AE,AT,BE,BG,CH,CN,CO,CR,CY,CZ,DE,DK,DO,EE,EG,ES,ET,FI,FR,GB,GH,GI,GR,GT,HN,HK,HR,HU,ID,IE,IS,IT,JP,KH,LA,LI,LT,LU,LY,MK,MM,MX,MY,MZ,NG,NZ,OM,PA,PE,PK,PL,PT,QA,RO,SA,SN,SE,SI,SK,SV,TH,UA,US,UY,VN,ZM", currency = "AED,ALL,ARS,BGN,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,EGP,EUR,GBP,GHS,HKD,HNL,HRK,HUF,IDR,ILS,ISK,JPY,KHR,KPW,LAK,LKR,MAD,MKD,MMK,MXN,MYR,MZN,NGN,NOK,NZD,OMR,PAB,PEN,PHP,PKR,PLN,QAR,RON,RSD,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,UYU,VND,ZAR,ZMW" }
credit = { country = "AD,AE,AT,BE,BG,CH,CN,CO,CR,CY,CZ,DE,DK,DO,EE,EG,ES,ET,FI,FR,GB,GH,GI,GR,GT,HN,HK,HR,HU,ID,IE,IS,IT,JP,KH,LA,LI,LT,LU,LY,MK,MM,MX,MY,MZ,NG,NZ,OM,PA,PE,PK,PL,PT,QA,RO,SA,SN,SE,SI,SK,SV,TH,UA,US,UY,VN,ZM", currency = "AED,ALL,ARS,BGN,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,EGP,EUR,GBP,GHS,HKD,HNL,HRK,HUF,IDR,ILS,ISK,JPY,KHR,KPW,LAK,LKR,MAD,MKD,MMK,MXN,MYR,MZN,NGN,NOK,NZD,OMR,PAB,PEN,PHP,PKR,PLN,QAR,RON,RSD,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,UYU,VND,ZAR,ZMW" }
debit = { country = "AD,AE,AT,BE,BG,CH,CN,CO,CR,CY,CZ,DE,DK,DO,EE,EG,ES,ET,FI,FR,GB,GH,GI,GR,GT,HN,HK,HR,HU,ID,IE,IS,IT,JP,KH,LA,LI,LT,LU,LY,MK,MM,MX,MY,MZ,NG,NZ,OM,PA,PE,PK,PL,PT,QA,RO,SA,SN,SE,SI,SK,SV,TH,UA,US,UY,VN,ZM", currency = "AED,ALL,ARS,BGN,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,EGP,EUR,GBP,GHS,HKD,HNL,HRK,HUF,IDR,ILS,ISK,JPY,KHR,KPW,LAK,LKR,MAD,MKD,MMK,MXN,MYR,MZN,NGN,NOK,NZD,OMR,PAB,PEN,PHP,PKR,PLN,QAR,RON,RSD,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,UYU,VND,ZAR,ZMW" }
mb_way = { country = "EE,ES,PT", currency = "EUR" }
ali_pay = { country = "CN", currency = "CNY" }
eps = { country = "AT", currency = "EUR" }

View File

@ -101,6 +101,9 @@ pub enum CryptoError {
/// The provided IV length is invalid for the cryptographic algorithm
#[error("Invalid IV length")]
InvalidIvLength,
/// The provided authentication tag length is invalid for the cryptographic algorithm
#[error("Invalid authentication tag length")]
InvalidTagLength,
}
/// Errors for Qr code handling

View File

@ -6,7 +6,8 @@ use std::sync::LazyLock;
use api_models::webhooks::IncomingWebhookEvent;
use common_enums::enums;
use common_utils::{
errors::CustomResult,
crypto,
errors::{CryptoError, CustomResult},
ext_traits::BytesExt,
request::{Method, Request, RequestBuilder, RequestContent},
types::{AmountConvertor, StringMajorUnit, StringMajorUnitForConnector},
@ -30,8 +31,8 @@ use hyperswitch_domain_models::{
SupportedPaymentMethods, SupportedPaymentMethodsExt,
},
types::{
PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsSyncRouterData,
RefundsRouterData,
PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData,
PaymentsSyncRouterData, RefundsRouterData,
},
};
use hyperswitch_interfaces::{
@ -42,11 +43,13 @@ use hyperswitch_interfaces::{
errors,
events::connector_api_logs::ConnectorEvent,
types::{
PaymentsAuthorizeType, PaymentsSyncType, PaymentsVoidType, RefundExecuteType, Response,
PaymentsAuthorizeType, PaymentsCaptureType, PaymentsSyncType, PaymentsVoidType,
RefundExecuteType, Response,
},
webhooks::{IncomingWebhook, IncomingWebhookRequestDetails},
};
use masking::{Mask, PeekInterface};
use ring::aead::{self, UnboundKey};
use transformers as aci;
use crate::{
@ -181,8 +184,101 @@ impl ConnectorIntegration<SetupMandate, SetupMandateRequestData, PaymentsRespons
}
}
// TODO: Investigate unexplained error in capture flow from connector.
impl ConnectorIntegration<Capture, PaymentsCaptureData, PaymentsResponseData> for Aci {
// Not Implemented (R)
fn get_headers(
&self,
req: &PaymentsCaptureRouterData,
_connectors: &Connectors,
) -> CustomResult<Vec<(String, masking::Maskable<String>)>, errors::ConnectorError> {
let mut header = vec![(
headers::CONTENT_TYPE.to_string(),
PaymentsCaptureType::get_content_type(self)
.to_string()
.into(),
)];
let mut api_key = self.get_auth_header(&req.connector_auth_type)?;
header.append(&mut api_key);
Ok(header)
}
fn get_content_type(&self) -> &'static str {
self.common_get_content_type()
}
fn get_url(
&self,
req: &PaymentsCaptureRouterData,
connectors: &Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!(
"{}{}{}",
self.base_url(connectors),
"v1/payments/",
req.request.connector_transaction_id,
))
}
fn get_request_body(
&self,
req: &PaymentsCaptureRouterData,
_connectors: &Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let amount = convert_amount(
self.amount_converter,
req.request.minor_amount_to_capture,
req.request.currency,
)?;
let connector_router_data = aci::AciRouterData::from((amount, req));
let connector_req = aci::AciCaptureRequest::try_from(&connector_router_data)?;
Ok(RequestContent::FormUrlEncoded(Box::new(connector_req)))
}
fn build_request(
&self,
req: &PaymentsCaptureRouterData,
connectors: &Connectors,
) -> CustomResult<Option<Request>, errors::ConnectorError> {
Ok(Some(
RequestBuilder::new()
.method(Method::Post)
.url(&PaymentsCaptureType::get_url(self, req, connectors)?)
.attach_default_headers()
.headers(PaymentsCaptureType::get_headers(self, req, connectors)?)
.set_body(PaymentsCaptureType::get_request_body(
self, req, connectors,
)?)
.build(),
))
}
fn handle_response(
&self,
data: &PaymentsCaptureRouterData,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<PaymentsCaptureRouterData, errors::ConnectorError> {
let response: aci::AciCaptureResponse = res
.response
.parse_struct("AciCaptureResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
RouterData::try_from(ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res, event_builder)
}
}
impl ConnectorIntegration<PSync, PaymentsSyncData, PaymentsResponseData> for Aci {
@ -555,32 +651,252 @@ impl ConnectorIntegration<Execute, RefundsData, RefundsResponseData> for Aci {
impl ConnectorIntegration<RSync, RefundsData, RefundsResponseData> for Aci {}
/// Decrypts an AES-256-GCM encrypted payload where the IV, auth tag, and ciphertext
/// are provided separately as hex strings. This is specifically tailored for ACI webhooks.
///
/// # Arguments
/// * `hex_key`: The encryption key as a hex string (must decode to 32 bytes).
/// * `hex_iv`: The initialization vector (nonce) as a hex string (must decode to 12 bytes).
/// * `hex_auth_tag`: The authentication tag as a hex string (must decode to 16 bytes).
/// * `hex_encrypted_body`: The encrypted payload as a hex string.
fn decrypt_aci_webhook_payload(
hex_key: &str,
hex_iv: &str,
hex_auth_tag: &str,
hex_encrypted_body: &str,
) -> CustomResult<Vec<u8>, CryptoError> {
let key_bytes = hex::decode(hex_key)
.change_context(CryptoError::DecodingFailed)
.attach_printable("Failed to decode hex key")?;
let iv_bytes = hex::decode(hex_iv)
.change_context(CryptoError::DecodingFailed)
.attach_printable("Failed to decode hex IV")?;
let auth_tag_bytes = hex::decode(hex_auth_tag)
.change_context(CryptoError::DecodingFailed)
.attach_printable("Failed to decode hex auth tag")?;
let encrypted_body_bytes = hex::decode(hex_encrypted_body)
.change_context(CryptoError::DecodingFailed)
.attach_printable("Failed to decode hex encrypted body")?;
if key_bytes.len() != 32 {
return Err(CryptoError::InvalidKeyLength)
.attach_printable("Key must be 32 bytes for AES-256-GCM");
}
if iv_bytes.len() != aead::NONCE_LEN {
return Err(CryptoError::InvalidIvLength)
.attach_printable(format!("IV must be {} bytes for AES-GCM", aead::NONCE_LEN));
}
if auth_tag_bytes.len() != 16 {
return Err(CryptoError::InvalidTagLength)
.attach_printable("Auth tag must be 16 bytes for AES-256-GCM");
}
let unbound_key = UnboundKey::new(&aead::AES_256_GCM, &key_bytes)
.change_context(CryptoError::DecodingFailed)
.attach_printable("Failed to create unbound key")?;
let less_safe_key = aead::LessSafeKey::new(unbound_key);
let nonce_arr: [u8; aead::NONCE_LEN] = iv_bytes
.as_slice()
.try_into()
.map_err(|_| CryptoError::InvalidIvLength)
.attach_printable_lazy(|| {
format!(
"IV length is {} but expected {}",
iv_bytes.len(),
aead::NONCE_LEN
)
})?;
let nonce = aead::Nonce::assume_unique_for_key(nonce_arr);
let mut ciphertext_and_tag = encrypted_body_bytes;
ciphertext_and_tag.extend_from_slice(&auth_tag_bytes);
less_safe_key
.open_in_place(nonce, aead::Aad::empty(), &mut ciphertext_and_tag)
.change_context(CryptoError::DecodingFailed)
.attach_printable("Failed to decrypt payload using LessSafeKey")?;
let original_ciphertext_len = ciphertext_and_tag.len() - auth_tag_bytes.len();
ciphertext_and_tag.truncate(original_ciphertext_len);
Ok(ciphertext_and_tag)
}
// TODO: Test this webhook flow once dashboard access is available.
#[async_trait::async_trait]
impl IncomingWebhook for Aci {
fn get_webhook_object_reference_id(
fn get_webhook_source_verification_algorithm(
&self,
_request: &IncomingWebhookRequestDetails<'_>,
) -> CustomResult<Box<dyn crypto::VerifySignature + Send>, errors::ConnectorError> {
Ok(Box::new(crypto::NoAlgorithm))
}
fn get_webhook_source_verification_signature(
&self,
request: &IncomingWebhookRequestDetails<'_>,
_connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let header_value_str = request
.headers
.get("X-Authentication-Tag")
.ok_or(errors::ConnectorError::WebhookSignatureNotFound)
.attach_printable("Missing X-Authentication-Tag header")?
.to_str()
.map_err(|_| errors::ConnectorError::WebhookSignatureNotFound)
.attach_printable("Invalid X-Authentication-Tag header value (not UTF-8)")?;
Ok(header_value_str.as_bytes().to_vec())
}
fn get_webhook_source_verification_message(
&self,
request: &IncomingWebhookRequestDetails<'_>,
_merchant_id: &common_utils::id_type::MerchantId,
connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let webhook_secret_str = String::from_utf8(connector_webhook_secrets.secret.to_vec())
.map_err(|_| errors::ConnectorError::WebhookVerificationSecretInvalid)
.attach_printable("ACI webhook secret is not a valid UTF-8 string")?;
let iv_hex_str = request
.headers
.get("X-Initialization-Vector")
.ok_or(errors::ConnectorError::WebhookSourceVerificationFailed)
.attach_printable("Missing X-Initialization-Vector header")?
.to_str()
.map_err(|_| errors::ConnectorError::WebhookSourceVerificationFailed)
.attach_printable("Invalid X-Initialization-Vector header value (not UTF-8)")?;
let auth_tag_hex_str = request
.headers
.get("X-Authentication-Tag")
.ok_or(errors::ConnectorError::WebhookSourceVerificationFailed)
.attach_printable("Missing X-Authentication-Tag header")?
.to_str()
.map_err(|_| errors::ConnectorError::WebhookSourceVerificationFailed)
.attach_printable("Invalid X-Authentication-Tag header value (not UTF-8)")?;
let encrypted_body_hex = String::from_utf8(request.body.to_vec())
.map_err(|_| errors::ConnectorError::WebhookBodyDecodingFailed)
.attach_printable(
"Failed to read encrypted body as UTF-8 string for verification message",
)?;
decrypt_aci_webhook_payload(
&webhook_secret_str,
iv_hex_str,
auth_tag_hex_str,
&encrypted_body_hex,
)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)
.attach_printable("Failed to decrypt ACI webhook payload for verification")
}
fn get_webhook_object_reference_id(
&self,
request: &IncomingWebhookRequestDetails<'_>,
) -> CustomResult<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> {
Err(report!(errors::ConnectorError::WebhooksNotImplemented))
let aci_notification: aci::AciWebhookNotification =
serde_json::from_slice(request.body)
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)
.attach_printable("Failed to deserialize ACI webhook notification for ID extraction (expected decrypted payload)")?;
let id_value_str = aci_notification
.payload
.get("id")
.and_then(|id| id.as_str())
.ok_or_else(|| {
report!(errors::ConnectorError::WebhookResourceObjectNotFound)
.attach_printable("Missing 'id' in webhook payload for ID extraction")
})?;
let payment_type_str = aci_notification
.payload
.get("paymentType")
.and_then(|pt| pt.as_str());
if payment_type_str.is_some_and(|pt| pt.to_uppercase() == "RF") {
Ok(api_models::webhooks::ObjectReferenceId::RefundId(
api_models::webhooks::RefundIdType::ConnectorRefundId(id_value_str.to_string()),
))
} else {
Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
api_models::payments::PaymentIdType::ConnectorTransactionId(
id_value_str.to_string(),
),
))
}
}
fn get_webhook_event_type(
&self,
_request: &IncomingWebhookRequestDetails<'_>,
request: &IncomingWebhookRequestDetails<'_>,
) -> CustomResult<IncomingWebhookEvent, errors::ConnectorError> {
Ok(IncomingWebhookEvent::EventNotSupported)
let aci_notification: aci::AciWebhookNotification =
serde_json::from_slice(request.body)
.change_context(errors::ConnectorError::WebhookEventTypeNotFound)
.attach_printable("Failed to deserialize ACI webhook notification for event type (expected decrypted payload)")?;
match aci_notification.event_type {
aci::AciWebhookEventType::Payment => {
let payment_payload: aci::AciPaymentWebhookPayload =
serde_json::from_value(aci_notification.payload)
.change_context(errors::ConnectorError::WebhookEventTypeNotFound)
.attach_printable("Could not deserialize ACI payment webhook payload for event type determination")?;
let code = &payment_payload.result.code;
if aci_result_codes::SUCCESSFUL_CODES.contains(&code.as_str()) {
if payment_payload.payment_type.to_uppercase() == "RF" {
Ok(IncomingWebhookEvent::RefundSuccess)
} else {
Ok(IncomingWebhookEvent::PaymentIntentSuccess)
}
} else if aci_result_codes::PENDING_CODES.contains(&code.as_str()) {
if payment_payload.payment_type.to_uppercase() == "RF" {
Ok(IncomingWebhookEvent::EventNotSupported)
} else {
Ok(IncomingWebhookEvent::PaymentIntentProcessing)
}
} else if aci_result_codes::FAILURE_CODES.contains(&code.as_str()) {
if payment_payload.payment_type.to_uppercase() == "RF" {
Ok(IncomingWebhookEvent::RefundFailure)
} else {
Ok(IncomingWebhookEvent::PaymentIntentFailure)
}
} else {
Ok(IncomingWebhookEvent::EventNotSupported)
}
}
}
}
fn get_webhook_resource_object(
&self,
_request: &IncomingWebhookRequestDetails<'_>,
request: &IncomingWebhookRequestDetails<'_>,
) -> CustomResult<Box<dyn masking::ErasedMaskSerialize>, errors::ConnectorError> {
Err(report!(errors::ConnectorError::WebhooksNotImplemented))
let aci_notification: aci::AciWebhookNotification =
serde_json::from_slice(request.body)
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)
.attach_printable("Failed to deserialize ACI webhook notification for resource object (expected decrypted payload)")?;
match aci_notification.event_type {
aci::AciWebhookEventType::Payment => {
let payment_payload: aci::AciPaymentWebhookPayload =
serde_json::from_value(aci_notification.payload)
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)
.attach_printable("Failed to deserialize ACI payment webhook payload")?;
Ok(Box::new(payment_payload))
}
}
}
}
static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLock::new(|| {
let supported_capture_methods = vec![enums::CaptureMethod::Automatic];
let supported_capture_methods = vec![
enums::CaptureMethod::Automatic,
enums::CaptureMethod::Manual,
];
let supported_card_network = vec![
common_enums::CardNetwork::AmericanExpress,
@ -600,7 +916,7 @@ static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLo
enums::PaymentMethodType::MbWay,
PaymentMethodDetails {
mandates: enums::FeatureStatus::NotSupported,
refunds: enums::FeatureStatus::NotSupported,
refunds: enums::FeatureStatus::Supported,
supported_capture_methods: supported_capture_methods.clone(),
specific_features: None,
},
@ -611,7 +927,7 @@ static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLo
enums::PaymentMethodType::AliPay,
PaymentMethodDetails {
mandates: enums::FeatureStatus::NotSupported,
refunds: enums::FeatureStatus::NotSupported,
refunds: enums::FeatureStatus::Supported,
supported_capture_methods: supported_capture_methods.clone(),
specific_features: None,
},
@ -659,7 +975,7 @@ static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLo
enums::PaymentMethodType::Eps,
PaymentMethodDetails {
mandates: enums::FeatureStatus::NotSupported,
refunds: enums::FeatureStatus::NotSupported,
refunds: enums::FeatureStatus::Supported,
supported_capture_methods: supported_capture_methods.clone(),
specific_features: None,
},
@ -669,7 +985,7 @@ static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLo
enums::PaymentMethodType::Eft,
PaymentMethodDetails {
mandates: enums::FeatureStatus::NotSupported,
refunds: enums::FeatureStatus::NotSupported,
refunds: enums::FeatureStatus::Supported,
supported_capture_methods: supported_capture_methods.clone(),
specific_features: None,
},
@ -679,7 +995,7 @@ static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLo
enums::PaymentMethodType::Ideal,
PaymentMethodDetails {
mandates: enums::FeatureStatus::NotSupported,
refunds: enums::FeatureStatus::NotSupported,
refunds: enums::FeatureStatus::Supported,
supported_capture_methods: supported_capture_methods.clone(),
specific_features: None,
},
@ -689,7 +1005,7 @@ static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLo
enums::PaymentMethodType::Giropay,
PaymentMethodDetails {
mandates: enums::FeatureStatus::NotSupported,
refunds: enums::FeatureStatus::NotSupported,
refunds: enums::FeatureStatus::Supported,
supported_capture_methods: supported_capture_methods.clone(),
specific_features: None,
},
@ -699,7 +1015,7 @@ static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLo
enums::PaymentMethodType::Sofort,
PaymentMethodDetails {
mandates: enums::FeatureStatus::NotSupported,
refunds: enums::FeatureStatus::NotSupported,
refunds: enums::FeatureStatus::Supported,
supported_capture_methods: supported_capture_methods.clone(),
specific_features: None,
},
@ -709,7 +1025,7 @@ static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLo
enums::PaymentMethodType::Interac,
PaymentMethodDetails {
mandates: enums::FeatureStatus::NotSupported,
refunds: enums::FeatureStatus::NotSupported,
refunds: enums::FeatureStatus::Supported,
supported_capture_methods: supported_capture_methods.clone(),
specific_features: None,
},
@ -719,7 +1035,7 @@ static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLo
enums::PaymentMethodType::Przelewy24,
PaymentMethodDetails {
mandates: enums::FeatureStatus::NotSupported,
refunds: enums::FeatureStatus::NotSupported,
refunds: enums::FeatureStatus::Supported,
supported_capture_methods: supported_capture_methods.clone(),
specific_features: None,
},
@ -729,7 +1045,7 @@ static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLo
enums::PaymentMethodType::Trustly,
PaymentMethodDetails {
mandates: enums::FeatureStatus::NotSupported,
refunds: enums::FeatureStatus::NotSupported,
refunds: enums::FeatureStatus::Supported,
supported_capture_methods: supported_capture_methods.clone(),
specific_features: None,
},
@ -739,7 +1055,7 @@ static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLo
enums::PaymentMethodType::Klarna,
PaymentMethodDetails {
mandates: enums::FeatureStatus::NotSupported,
refunds: enums::FeatureStatus::NotSupported,
refunds: enums::FeatureStatus::Supported,
supported_capture_methods: supported_capture_methods.clone(),
specific_features: None,
},

View File

@ -6,11 +6,16 @@ use error_stack::report;
use hyperswitch_domain_models::{
payment_method_data::{BankRedirectData, Card, PayLaterData, PaymentMethodData, WalletData},
router_data::{ConnectorAuthType, RouterData},
router_request_types::ResponseId,
router_request_types::{
PaymentsAuthorizeData, PaymentsCancelData, PaymentsSyncData, ResponseId,
},
router_response_types::{
MandateReference, PaymentsResponseData, RedirectForm, RefundsResponseData,
},
types::{PaymentsAuthorizeRouterData, PaymentsCancelRouterData, RefundsRouterData},
types::{
PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData,
RefundsRouterData,
},
};
use hyperswitch_interfaces::errors;
use masking::{ExposeInterface, Secret};
@ -25,6 +30,28 @@ use crate::{
type Error = error_stack::Report<errors::ConnectorError>;
trait GetCaptureMethod {
fn get_capture_method(&self) -> Option<enums::CaptureMethod>;
}
impl GetCaptureMethod for PaymentsAuthorizeData {
fn get_capture_method(&self) -> Option<enums::CaptureMethod> {
self.capture_method
}
}
impl GetCaptureMethod for PaymentsSyncData {
fn get_capture_method(&self) -> Option<enums::CaptureMethod> {
self.capture_method
}
}
impl GetCaptureMethod for PaymentsCancelData {
fn get_capture_method(&self) -> Option<enums::CaptureMethod> {
None
}
}
#[derive(Debug, Serialize)]
pub struct AciRouterData<T> {
amount: StringMajorUnit,
@ -629,14 +656,18 @@ pub enum AciPaymentStatus {
RedirectShopper,
}
impl From<AciPaymentStatus> for enums::AttemptStatus {
fn from(item: AciPaymentStatus) -> Self {
match item {
AciPaymentStatus::Succeeded => Self::Charged,
AciPaymentStatus::Failed => Self::Failure,
AciPaymentStatus::Pending => Self::Authorizing,
AciPaymentStatus::RedirectShopper => Self::AuthenticationPending,
fn map_aci_attempt_status(item: AciPaymentStatus, auto_capture: bool) -> enums::AttemptStatus {
match item {
AciPaymentStatus::Succeeded => {
if auto_capture {
enums::AttemptStatus::Charged
} else {
enums::AttemptStatus::Authorized
}
}
AciPaymentStatus::Failed => enums::AttemptStatus::Failure,
AciPaymentStatus::Pending => enums::AttemptStatus::Authorizing,
AciPaymentStatus::RedirectShopper => enums::AttemptStatus::AuthenticationPending,
}
}
impl FromStr for AciPaymentStatus {
@ -708,12 +739,14 @@ pub struct ErrorParameters {
pub(super) message: String,
}
impl<F, T> TryFrom<ResponseRouterData<F, AciPaymentsResponse, T, PaymentsResponseData>>
for RouterData<F, T, PaymentsResponseData>
impl<F, Req> TryFrom<ResponseRouterData<F, AciPaymentsResponse, Req, PaymentsResponseData>>
for RouterData<F, Req, PaymentsResponseData>
where
Req: GetCaptureMethod,
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: ResponseRouterData<F, AciPaymentsResponse, T, PaymentsResponseData>,
item: ResponseRouterData<F, AciPaymentsResponse, Req, PaymentsResponseData>,
) -> Result<Self, Self::Error> {
let redirection_data = item.response.redirect.map(|data| {
let form_fields = std::collections::HashMap::<_, _>::from_iter(
@ -740,16 +773,22 @@ impl<F, T> TryFrom<ResponseRouterData<F, AciPaymentsResponse, T, PaymentsRespons
connector_mandate_request_reference_id: None,
});
let auto_capture = matches!(
item.data.request.get_capture_method(),
Some(enums::CaptureMethod::Automatic) | None
);
let status = if redirection_data.is_some() {
map_aci_attempt_status(AciPaymentStatus::RedirectShopper, auto_capture)
} else {
map_aci_attempt_status(
AciPaymentStatus::from_str(&item.response.result.code)?,
auto_capture,
)
};
Ok(Self {
status: {
if redirection_data.is_some() {
enums::AttemptStatus::from(AciPaymentStatus::RedirectShopper)
} else {
enums::AttemptStatus::from(AciPaymentStatus::from_str(
&item.response.result.code,
)?)
}
},
status,
response: Ok(PaymentsResponseData::TransactionResponse {
resource_id: ResponseId::ConnectorTransactionId(item.response.id.clone()),
redirection_data: Box::new(redirection_data),
@ -765,6 +804,95 @@ impl<F, T> TryFrom<ResponseRouterData<F, AciPaymentsResponse, T, PaymentsRespons
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AciCaptureRequest {
#[serde(flatten)]
pub txn_details: TransactionDetails,
}
impl TryFrom<&AciRouterData<&PaymentsCaptureRouterData>> for AciCaptureRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &AciRouterData<&PaymentsCaptureRouterData>) -> Result<Self, Self::Error> {
let auth = AciAuthType::try_from(&item.router_data.connector_auth_type)?;
Ok(Self {
txn_details: TransactionDetails {
entity_id: auth.entity_id,
amount: item.amount.to_owned(),
currency: item.router_data.request.currency.to_string(),
payment_type: AciPaymentType::Capture,
},
})
}
}
#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AciCaptureResponse {
id: String,
referenced_id: String,
payment_type: AciPaymentType,
amount: StringMajorUnit,
currency: String,
descriptor: String,
result: AciCaptureResult,
result_details: AciCaptureResultDetails,
build_number: String,
timestamp: String,
ndc: Secret<String>,
source: Secret<String>,
payment_method: String,
short_id: String,
}
#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AciCaptureResult {
code: String,
description: String,
}
#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct AciCaptureResultDetails {
extended_description: String,
#[serde(rename = "clearingInstituteName")]
clearing_institute_name: String,
connector_tx_id1: String,
connector_tx_id3: String,
connector_tx_id2: String,
acquirer_response: String,
}
impl<F, T> TryFrom<ResponseRouterData<F, AciCaptureResponse, T, PaymentsResponseData>>
for RouterData<F, T, PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: ResponseRouterData<F, AciCaptureResponse, T, PaymentsResponseData>,
) -> Result<Self, Self::Error> {
Ok(Self {
status: map_aci_attempt_status(
AciPaymentStatus::from_str(&item.response.result.code)?,
false,
),
reference_id: Some(item.response.referenced_id.clone()),
response: Ok(PaymentsResponseData::TransactionResponse {
resource_id: ResponseId::ConnectorTransactionId(item.response.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.referenced_id),
incremental_authorization_allowed: None,
charges: None,
}),
..item.data
})
}
}
#[derive(Default, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AciRefundRequest {
@ -854,3 +982,89 @@ impl<F> TryFrom<RefundsResponseRouterData<F, AciRefundResponse>> for RefundsRout
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub enum AciWebhookEventType {
Payment,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub enum AciWebhookAction {
Created,
Updated,
Deleted,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AciWebhookCardDetails {
pub bin: Option<String>,
#[serde(rename = "last4Digits")]
pub last4_digits: Option<String>,
pub holder: Option<String>,
pub expiry_month: Option<Secret<String>>,
pub expiry_year: Option<Secret<String>>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AciWebhookCustomerDetails {
#[serde(rename = "givenName")]
pub given_name: Option<Secret<String>>,
pub surname: Option<Secret<String>>,
#[serde(rename = "merchantCustomerId")]
pub merchant_customer_id: Option<Secret<String>>,
pub sex: Option<Secret<String>>,
pub email: Option<Email>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AciWebhookAuthenticationDetails {
#[serde(rename = "entityId")]
pub entity_id: Secret<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AciWebhookRiskDetails {
pub score: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AciPaymentWebhookPayload {
pub id: String,
pub payment_type: String,
pub payment_brand: String,
pub amount: StringMajorUnit,
pub currency: String,
pub presentation_amount: Option<StringMajorUnit>,
pub presentation_currency: Option<String>,
pub descriptor: Option<String>,
pub result: ResultCode,
pub authentication: Option<AciWebhookAuthenticationDetails>,
pub card: Option<AciWebhookCardDetails>,
pub customer: Option<AciWebhookCustomerDetails>,
#[serde(rename = "customParameters")]
pub custom_parameters: Option<serde_json::Value>,
pub risk: Option<AciWebhookRiskDetails>,
pub build_number: Option<String>,
pub timestamp: String,
pub ndc: String,
#[serde(rename = "channelName")]
pub channel_name: Option<String>,
pub source: Option<String>,
pub payment_method: Option<String>,
#[serde(rename = "shortId")]
pub short_id: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AciWebhookNotification {
#[serde(rename = "type")]
pub event_type: AciWebhookEventType,
pub action: Option<AciWebhookAction>,
pub payload: serde_json::Value,
}

View File

@ -502,8 +502,8 @@ credit = { country = "AD,AT,AU,BE,BG,CA,CH,CY,CZ,DE,DK,EE,ES,FI,FR,GB,GG,GI,GR,H
debit = { country = "AD,AT,AU,BE,BG,CA,CH,CY,CZ,DE,DK,EE,ES,FI,FR,GB,GG,GI,GR,HK,HR,HU,IE,IM,IT,JE,LI,LT,LU,LV,MT,MC,MY,NL,NO,NZ,PL,PT,RO,SE,SG,SI,SK,SM,US", currency = "AED,AMD,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BIF,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,ISK,JMD,JPY,KES,KGS,KHR,KMF,KRW,KYD,KZT,LAK,LBP,LKR,LRD,LSL,MAD,MDL,MKD,MNT,MOP,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLL,SOS,SRD,STD,SVC,SYP,SZL,THB,TJS,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL"}
[pm_filters.aci]
credit = { not_available_flows = { capture_method = "manual" }, country = "AD,AE,AT,BE,BG,CH,CN,CO,CR,CY,CZ,DE,DK,DO,EE,EG,ES,ET,FI,FR,GB,GH,GI,GR,GT,HN,HK,HR,HU,ID,IE,IS,IT,JP,KH,LA,LI,LT,LU,LY,MK,MM,MX,MY,MZ,NG,NZ,OM,PA,PE,PK,PL,PT,QA,RO,SA,SN,SE,SI,SK,SV,TH,UA,US,UY,VN,ZM", currency = "AED,ALL,ARS,BGN,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,EGP,EUR,GBP,GHS,HKD,HNL,HRK,HUF,IDR,ILS,ISK,JPY,KHR,KPW,LAK,LKR,MAD,MKD,MMK,MXN,MYR,MZN,NGN,NOK,NZD,OMR,PAB,PEN,PHP,PKR,PLN,QAR,RON,RSD,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,UYU,VND,ZAR,ZMW" }
debit = { not_available_flows = { capture_method = "manual" }, country = "AD,AE,AT,BE,BG,CH,CN,CO,CR,CY,CZ,DE,DK,DO,EE,EG,ES,ET,FI,FR,GB,GH,GI,GR,GT,HN,HK,HR,HU,ID,IE,IS,IT,JP,KH,LA,LI,LT,LU,LY,MK,MM,MX,MY,MZ,NG,NZ,OM,PA,PE,PK,PL,PT,QA,RO,SA,SN,SE,SI,SK,SV,TH,UA,US,UY,VN,ZM", currency = "AED,ALL,ARS,BGN,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,EGP,EUR,GBP,GHS,HKD,HNL,HRK,HUF,IDR,ILS,ISK,JPY,KHR,KPW,LAK,LKR,MAD,MKD,MMK,MXN,MYR,MZN,NGN,NOK,NZD,OMR,PAB,PEN,PHP,PKR,PLN,QAR,RON,RSD,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,UYU,VND,ZAR,ZMW" }
credit = { country = "AD,AE,AT,BE,BG,CH,CN,CO,CR,CY,CZ,DE,DK,DO,EE,EG,ES,ET,FI,FR,GB,GH,GI,GR,GT,HN,HK,HR,HU,ID,IE,IS,IT,JP,KH,LA,LI,LT,LU,LY,MK,MM,MX,MY,MZ,NG,NZ,OM,PA,PE,PK,PL,PT,QA,RO,SA,SN,SE,SI,SK,SV,TH,UA,US,UY,VN,ZM", currency = "AED,ALL,ARS,BGN,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,EGP,EUR,GBP,GHS,HKD,HNL,HRK,HUF,IDR,ILS,ISK,JPY,KHR,KPW,LAK,LKR,MAD,MKD,MMK,MXN,MYR,MZN,NGN,NOK,NZD,OMR,PAB,PEN,PHP,PKR,PLN,QAR,RON,RSD,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,UYU,VND,ZAR,ZMW" }
debit = { country = "AD,AE,AT,BE,BG,CH,CN,CO,CR,CY,CZ,DE,DK,DO,EE,EG,ES,ET,FI,FR,GB,GH,GI,GR,GT,HN,HK,HR,HU,ID,IE,IS,IT,JP,KH,LA,LI,LT,LU,LY,MK,MM,MX,MY,MZ,NG,NZ,OM,PA,PE,PK,PL,PT,QA,RO,SA,SN,SE,SI,SK,SV,TH,UA,US,UY,VN,ZM", currency = "AED,ALL,ARS,BGN,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,EGP,EUR,GBP,GHS,HKD,HNL,HRK,HUF,IDR,ILS,ISK,JPY,KHR,KPW,LAK,LKR,MAD,MKD,MMK,MXN,MYR,MZN,NGN,NOK,NZD,OMR,PAB,PEN,PHP,PKR,PLN,QAR,RON,RSD,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,UYU,VND,ZAR,ZMW" }
mb_way = { country = "EE,ES,PT", currency = "EUR" }
ali_pay = { country = "CN", currency = "CNY" }
eps = { country = "AT", currency = "EUR" }