mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-02 04:04:43 +08:00
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:
@ -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/"
|
||||
|
||||
@ -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" }
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user