diff --git a/config/development.toml b/config/development.toml index 33e7606756..329b86f43c 100644 --- a/config/development.toml +++ b/config/development.toml @@ -58,6 +58,7 @@ psync = "GW_TXN_SYNC" connectors_with_webhook_source_verification_call = "paypal, truelayer" [connectors] +citigate.base_url = "https://gw-test.cgate.tech" grabpay.base_url = "https://partner-api.grab.com/grabpay/partner/v2" maya.base_url = "https://pg-sandbox.paymaya.com" boost.base_url = "https://stage-api.boostconnect.biz/gateway" diff --git a/config/production.toml b/config/production.toml index 0bbd88d995..0eced30009 100644 --- a/config/production.toml +++ b/config/production.toml @@ -23,6 +23,7 @@ connector_request_timeout = 30 bypass_urls = ["localhost", "local"] [connectors] +citigate.base_url = "https://gw.cgate.tech" grabpay.base_url = "https://partner-api.grab.com/grabpay/partner/v2" maya.base_url = "https://pg.maya.ph" boost.base_url = "https://api.boostconnect.biz/gateway" diff --git a/config/sandbox.toml b/config/sandbox.toml index cb000a3a7b..2cd82ae38a 100644 --- a/config/sandbox.toml +++ b/config/sandbox.toml @@ -23,6 +23,7 @@ connector_request_timeout = 30 bypass_urls = ["localhost", "local"] [connectors] +citigate.base_url = "https://gw-test.cgate.tech" grabpay.base_url = "https://partner-api.stg-myteksi.com/grabpay/partner/v2" maya.base_url = "https://pg-sandbox.paymaya.com" boost.base_url = "https://stage-api.boostconnect.biz/gateway" diff --git a/crates/integrations/connector-integration/src/connectors.rs b/crates/integrations/connector-integration/src/connectors.rs index a9d41561d4..701f68929b 100644 --- a/crates/integrations/connector-integration/src/connectors.rs +++ b/crates/integrations/connector-integration/src/connectors.rs @@ -302,3 +302,6 @@ pub use self::tesouro::Tesouro; pub mod boost; pub use self::boost::Boost; + +pub mod citigate; +pub use self::citigate::Citigate; diff --git a/crates/integrations/connector-integration/src/connectors/citigate.rs b/crates/integrations/connector-integration/src/connectors/citigate.rs new file mode 100644 index 0000000000..b240775e2b --- /dev/null +++ b/crates/integrations/connector-integration/src/connectors/citigate.rs @@ -0,0 +1,497 @@ +//! Citigate connector. +//! +//! Citigate is a single-endpoint, verb-in-body JSON gateway: every operation is a +//! `POST` to `/orion/interface/json.ashx` and the operation is selected by the +//! `TransTypeID` body field, never by the URL or HTTP method. Credentials +//! (`MerchantName` / `MerchantPassword`) travel in the body too, so +//! [`ConnectorCommon::get_auth_header`] contributes no headers. +//! +//! Implemented scope: Card / Authorize (Purchase), one-time, non-3DS and the 3DS +//! user-redirect path; the Transaction Status Check (`TransTypeID = 8`), which +//! serves as both PSync and RSync; and the post-authorization operations Capture +//! (`3`), Void / Cancel (`4`) and Refund (`5`). + +pub mod transformers; + +use std::fmt::Debug; + +use common_enums::CurrencyUnit; +use common_utils::{errors::CustomResult, events, ext_traits::ByteSliceExt}; +use domain_types::{ + connector_flow::{Authorize, Capture, PSync, RSync, Refund, Void}, + connector_types::{ + PaymentFlowData, PaymentVoidData, PaymentsAuthorizeData, PaymentsCaptureData, + PaymentsResponseData, PaymentsSyncData, RefundFlowData, RefundSyncData, RefundsData, + RefundsResponseData, + }, + errors::{ConnectorError, IntegrationError}, + payment_method_data::PaymentMethodDataTypes, + router_data::{ConnectorSpecificConfig, ErrorResponse}, + router_data_v2::RouterDataV2, + router_response_types::Response, + types::Connectors, +}; +use error_stack::ResultExt; +use hyperswitch_masking::Maskable; +use interfaces::{ + api::ConnectorCommon, connector_integration_v2::ConnectorIntegrationV2, connector_types, + decode::BodyDecoding, +}; +use serde::Serialize; +use transformers::{ + self as citigate, CitigateCaptureRequest, CitigateCaptureResponse, CitigatePaymentsRequest, + CitigatePaymentsResponse, CitigateRefundRequest, CitigateRefundResponse, + CitigateRefundSyncRequest, CitigateRefundSyncResponse, CitigateSyncRequest, + CitigateSyncResponse, CitigateVoidRequest, CitigateVoidResponse, +}; + +use super::macros; +use crate::types::ResponseRouterData; +use crate::with_error_response_body; + +pub(crate) mod headers { + pub(crate) const CONTENT_TYPE: &str = "Content-Type"; +} + +/// Path shared by every Citigate operation, appended to the configured base URL. +const CITIGATE_JSON_INTERFACE_PATH: &str = "/orion/interface/json.ashx"; + +// Citigate expects the amount in the smallest denomination of the currency with no +// decimal point, and quotes it as a JSON string. +macros::create_amount_converter_wrapper!(connector_name: Citigate, amount_type: StringMinorUnit); + +// ===== MACRO PREREQUISITES ===== +macros::create_all_prerequisites!( + connector_name: Citigate, + generic_type: T, + api: [ + ( + flow: Authorize, + request_body: CitigatePaymentsRequest, + response_body: CitigatePaymentsResponse, + router_data: RouterDataV2, PaymentsResponseData>, + ), + ( + flow: PSync, + request_body: CitigateSyncRequest, + response_body: CitigateSyncResponse, + router_data: RouterDataV2, + ), + ( + flow: Capture, + request_body: CitigateCaptureRequest, + response_body: CitigateCaptureResponse, + router_data: RouterDataV2, + ), + ( + flow: Void, + request_body: CitigateVoidRequest, + response_body: CitigateVoidResponse, + router_data: RouterDataV2, + ), + ( + flow: Refund, + request_body: CitigateRefundRequest, + response_body: CitigateRefundResponse, + router_data: RouterDataV2, + ), + ( + flow: RSync, + request_body: CitigateRefundSyncRequest, + response_body: CitigateRefundSyncResponse, + router_data: RouterDataV2, + ) + ], + amount_converters: [], + member_functions: { + pub fn build_headers( + &self, + _req: &RouterDataV2, + ) -> CustomResult)>, IntegrationError> { + // Citigate has no authentication headers: MerchantName / MerchantPassword + // are members of the request body. + Ok(vec![( + headers::CONTENT_TYPE.to_string(), + self.common_get_content_type().to_string().into(), + )]) + } + + pub fn connector_base_url_payments<'a, F, Req, Res>( + &self, + req: &'a RouterDataV2, + ) -> &'a str { + &req.resource_common_data.connectors.citigate.base_url + } + } +); + +// ===== CONNECTOR COMMON IMPLEMENTATION ===== +impl ConnectorCommon + for Citigate +{ + fn id(&self) -> &'static str { + "citigate" + } + + fn get_currency_unit(&self) -> CurrencyUnit { + CurrencyUnit::Minor + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + + fn base_url<'a>(&self, connectors: &'a Connectors) -> &'a str { + &connectors.citigate.base_url + } + + fn get_auth_header( + &self, + auth_type: &ConnectorSpecificConfig, + ) -> CustomResult)>, IntegrationError> { + // Validate that the configured credentials have the shape Citigate needs, but + // emit no headers: the credentials are injected into the request body. + citigate::CitigateAuthType::try_from(auth_type)?; + Ok(Vec::new()) + } + + fn build_error_response( + &self, + res: Response, + event_builder: Option<&mut events::Event>, + _connector_config: &ConnectorSpecificConfig, + ) -> CustomResult { + // Citigate answers every outcome with HTTP 200 and the same envelope, so the + // error path parses the very same struct as the success path. + let response: CitigatePaymentsResponse = res + .response + .parse_struct("CitigatePaymentsResponse") + .change_context(crate::utils::response_deserialization_fail( + res.status_code, + "citigate: response body did not match the expected TransactionResponse format.", + ))?; + + with_error_response_body!(event_builder, response); + + Ok(response.to_error_response(res.status_code)) + } +} + +// ===== FLOW-SPECIFIC CONNECTOR INTEGRATION IMPLEMENTATIONS ===== + +// Authorize Flow — Purchase, PaymentTypeID = 1, TransTypeID = 0. +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Citigate, + curl_request: Json(CitigatePaymentsRequest), + curl_response: CitigatePaymentsResponse, + 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)>, IntegrationError> { + self.build_headers(req) + } + + fn get_url( + &self, + req: &RouterDataV2, PaymentsResponseData>, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.connector_base_url_payments(req), + CITIGATE_JSON_INTERFACE_PATH + )) + } + } +); + +// PSync Flow — Transaction Status Check, PaymentTypeID = 1, TransTypeID = 8. +// Same endpoint and same MID credentials as the Authorize that created the +// payment; the lookup key is MerchantRef, not TransactionID. +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Citigate, + curl_request: Json(CitigateSyncRequest), + curl_response: CitigateSyncResponse, + flow_name: PSync, + resource_common_data: PaymentFlowData, + flow_request: PaymentsSyncData, + flow_response: PaymentsResponseData, + http_method: Post, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize], + other_functions: { + fn get_headers( + &self, + req: &RouterDataV2, + ) -> CustomResult)>, IntegrationError> { + self.build_headers(req) + } + + fn get_url( + &self, + req: &RouterDataV2, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.connector_base_url_payments(req), + CITIGATE_JSON_INTERFACE_PATH + )) + } + } +); + +// Capture Flow — settle an open authorisation, PaymentTypeID = 1, TransTypeID = 3. +// Full capture only: the request has no Amount field. +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Citigate, + curl_request: Json(CitigateCaptureRequest), + curl_response: CitigateCaptureResponse, + 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)>, IntegrationError> { + self.build_headers(req) + } + + fn get_url( + &self, + req: &RouterDataV2, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.connector_base_url_payments(req), + CITIGATE_JSON_INTERFACE_PATH + )) + } + } +); + +// Void Flow — cancel an open authorisation, PaymentTypeID = 1, TransTypeID = 4. +// Full void only, and the cancellation reason cannot be transmitted. +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Citigate, + curl_request: Json(CitigateVoidRequest), + curl_response: CitigateVoidResponse, + 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)>, IntegrationError> { + self.build_headers(req) + } + + fn get_url( + &self, + req: &RouterDataV2, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.connector_base_url_payments(req), + CITIGATE_JSON_INTERFACE_PATH + )) + } + } +); + +// Refund Flow — PaymentTypeID = 1, TransTypeID = 5. Refunds carry RefundFlowData +// rather than PaymentFlowData, so the base URL is read directly here. +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Citigate, + curl_request: Json(CitigateRefundRequest), + curl_response: CitigateRefundResponse, + flow_name: Refund, + resource_common_data: RefundFlowData, + flow_request: RefundsData, + flow_response: RefundsResponseData, + http_method: Post, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize], + other_functions: { + fn get_headers( + &self, + req: &RouterDataV2, + ) -> CustomResult)>, IntegrationError> { + self.build_headers(req) + } + + fn get_url( + &self, + req: &RouterDataV2, + ) -> CustomResult { + Ok(format!( + "{}{}", + req.resource_common_data.connectors.citigate.base_url, + CITIGATE_JSON_INTERFACE_PATH + )) + } + } +); + +// RSync Flow — the same Transaction Status Check as PSync (TransTypeID = 8), but +// keyed on the refund leg's MerchantRef rather than the payment's. +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Citigate, + curl_request: Json(CitigateRefundSyncRequest), + curl_response: CitigateRefundSyncResponse, + flow_name: RSync, + resource_common_data: RefundFlowData, + flow_request: RefundSyncData, + flow_response: RefundsResponseData, + http_method: Post, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize], + other_functions: { + fn get_headers( + &self, + req: &RouterDataV2, + ) -> CustomResult)>, IntegrationError> { + self.build_headers(req) + } + + fn get_url( + &self, + req: &RouterDataV2, + ) -> CustomResult { + Ok(format!( + "{}{}", + req.resource_common_data.connectors.citigate.base_url, + CITIGATE_JSON_INTERFACE_PATH + )) + } + } +); + +// ===== CONNECTOR SERVICE TRAIT IMPLEMENTATION ===== +// Aggregate trait - composes all other connector traits. +impl + connector_types::ConnectorServiceTrait for Citigate +{ +} + +// ===== PAYMENT FLOW TRAIT IMPLEMENTATIONS ===== +impl + connector_types::PaymentAuthorizeV2 for Citigate +{ +} + +impl + connector_types::PaymentSyncV2 for Citigate +{ +} + +impl + connector_types::PaymentCapture for Citigate +{ +} + +impl + connector_types::PaymentVoidV2 for Citigate +{ +} + +impl + connector_types::RefundV2 for Citigate +{ +} + +impl + connector_types::RefundSyncV2 for Citigate +{ +} + +// ===== BASE (NON-FLOW) TRAIT IMPLEMENTATIONS ===== +impl + connector_types::ValidationTrait for Citigate +{ +} + +impl + connector_types::IncomingWebhook for Citigate +{ +} + +impl + connector_types::VerifyRedirectResponse for Citigate +{ +} + +// ===== SOURCE VERIFICATION IMPLEMENTATION ===== +// Citigate signs the 3DS redirect callback and the opt-in refund/fraud +// notification with SHA-1, but neither callback is consumed here: the 3DS outcome +// is resolved by polling the Transaction Status Check (PSync) instead. +impl + interfaces::verification::SourceVerification for Citigate +{ +} + +// ===== BODY DECODING IMPLEMENTATION ===== +impl BodyDecoding + for Citigate +{ +} + +// ===== PAYOUT TRAIT IMPLEMENTATIONS ===== +macros::macro_connector_payout_implementation!( + connector: Citigate, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize] +); + +// ===== FLOW STATUS IMPLEMENTATIONS ===== +// Every flow other than Authorize, PSync, Capture, Void, Refund and RSync is +// stubbed: the Citigate JSON interface exposes no other operation on the card +// payment type. +macros::macro_connector_flow_status_impls!( + connector: Citigate, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize], + not_implemented: [ + Accept, + ClientAuthenticationToken, + CreateConnectorCustomer, + DefendDispute, + MandateRevoke, + Authenticate, + IncrementalAuthorization, + CreateOrder, + PostAuthenticate, + PreAuthenticate, + PaymentMethodToken, + VoidPC, + RepeatPayment, + ServerAuthenticationToken, + ServerSessionAuthenticationToken, + SetupMandate, + SubmitEvidence, + GetConnectorCustomer, + VoidPostRefund + ], +); diff --git a/crates/integrations/connector-integration/src/connectors/citigate/transformers.rs b/crates/integrations/connector-integration/src/connectors/citigate/transformers.rs new file mode 100644 index 0000000000..37abee720a --- /dev/null +++ b/crates/integrations/connector-integration/src/connectors/citigate/transformers.rs @@ -0,0 +1,1364 @@ +//! Citigate transformers. +//! +//! Citigate exposes a single-endpoint, verb-in-body JSON API. Every operation is a +//! `POST` to `/orion/interface/json.ashx`; the operation is selected by the +//! `TransTypeID` body field. Authentication (`MerchantName` / `MerchantPassword`) is +//! also carried in the body, so there are no auth headers. +//! +//! Scope of this module: Card / Authorize (Purchase, `TransTypeID = 0`), one-time, +//! non-3DS **and** the 3DS user-redirect path, the Transaction Status Check +//! (`TransTypeID = 8`) used for both PSync and RSync, and the post-authorization +//! operations Capture (`3`), Void / Cancel (`4`) and Refund (`5`). + +use std::collections::HashMap; + +use common_enums::{AttemptStatus, AuthenticationType, CardNetwork, RefundStatus}; +use common_utils::{pii::Email, types::StringMinorUnit, Method}; +use domain_types::{ + connector_flow::{Authorize, Capture, PSync, RSync, Refund, Void}, + connector_types::{ + PaymentFlowData, PaymentVoidData, PaymentsAuthorizeData, PaymentsCaptureData, + PaymentsResponseData, PaymentsSyncData, RefundFlowData, RefundSyncData, RefundsData, + RefundsResponseData, ResponseId, + }, + errors::{ConnectorError, IntegrationError, IntegrationErrorContext}, + payment_method_data::{Card, PaymentMethodData, PaymentMethodDataTypes, RawCardNumber}, + router_data::{ConnectorSpecificConfig, ErrorResponse, FlowStatus}, + router_data_v2::RouterDataV2, + router_response_types::RedirectForm, + utils::{get_card_issuer, CardIssuer}, +}; +use hyperswitch_masking::{PeekInterface, Secret}; +use serde::{Deserialize, Deserializer, Serialize}; + +use crate::connectors::citigate::{CitigateAmountConvertor, CitigateRouterData}; +use crate::types::ResponseRouterData; + +/// `PaymentTypeID` for a card payment. Card is the only payment type in scope. +const PAYMENT_TYPE_ID_CARD: &str = "1"; +/// `TransTypeID` for a purchase (UCS `Authorize`). +const TRANS_TYPE_ID_PURCHASE: &str = "0"; +/// `TransTypeID` for settling an open authorisation (UCS `Capture`). +const TRANS_TYPE_ID_CAPTURE: &str = "3"; +/// `TransTypeID` for cancelling an open authorisation (UCS `Void`). +const TRANS_TYPE_ID_CANCEL: &str = "4"; +/// `TransTypeID` for refunding a sale or a captured authorisation (UCS `Refund`). +const TRANS_TYPE_ID_REFUND: &str = "5"; +/// `TransTypeID` for a transaction status check (UCS `PSync` **and** `RSync`). +const TRANS_TYPE_ID_STATUS_CHECK: &str = "8"; + +/// `ResponseCode` returned for an approved transaction. +const RESPONSE_CODE_APPROVED: &str = "0"; +/// `ResponseCode` returned when the cardholder failed 3D authentication at the ACS. +const RESPONSE_CODE_3D_AUTH_FAILURE: &str = "103"; +/// `ResponseCode` returned when the cardholder abandoned the transaction. +const RESPONSE_CODE_USER_ABORTED: &str = "106"; +/// `ResponseCode` returned when the cardholder never reached the redirect target. +const RESPONSE_CODE_NO_USER_REDIRECT: &str = "700"; +/// `ResponseCode` returned when the cardholder never came back from the redirect. +const RESPONSE_CODE_NO_USER_RETURN: &str = "800"; +/// `ResponseCode` returned when a cardholder redirect (3DS) is required. +const RESPONSE_CODE_REDIRECT_REQUIRED: &str = "600"; +/// Undocumented `ResponseCode` observed on Status Check responses; discriminated on +/// the response `TransTypeID`. +const RESPONSE_CODE_UNDOCUMENTED: &str = "999"; + +/// Response `TransTypeID` values (Transaction Key 4). +const RESP_TRANS_TYPE_SALE: &str = "1"; +const RESP_TRANS_TYPE_AUTHORISE: &str = "2"; +const RESP_TRANS_TYPE_CAPTURE: &str = "3"; +const RESP_TRANS_TYPE_CANCEL: &str = "4"; +const RESP_TRANS_TYPE_REFUND: &str = "5"; +const RESP_TRANS_TYPE_PENDING: &str = "6"; + +/// `TransactionID` value Citigate returns when no transaction was created. +const NO_TRANSACTION_ID: &str = "0"; + +// ============================================================================= +// AUTH +// ============================================================================= + +/// Citigate body-key credentials. +/// +/// `api_key` carries `MerchantName` and `key1` carries `MerchantPassword`; both are +/// injected into every request body rather than into headers. +#[derive(Debug, Clone)] +pub struct CitigateAuthType { + pub merchant_name: Secret, + pub merchant_password: Secret, +} + +impl TryFrom<&ConnectorSpecificConfig> for CitigateAuthType { + type Error = error_stack::Report; + + fn try_from(auth_type: &ConnectorSpecificConfig) -> Result { + match auth_type { + ConnectorSpecificConfig::Citigate { api_key, key1, .. } => Ok(Self { + merchant_name: api_key.to_owned(), + merchant_password: key1.to_owned(), + }), + _ => Err(error_stack::report!( + IntegrationError::FailedToObtainAuthType { + context: IntegrationErrorContext::default() + } + )), + } + } +} + +// ============================================================================= +// CARD BRAND +// ============================================================================= + +/// The `Brand` values Citigate accepts. Any other network is rejected before the +/// request is built rather than being sent and refused with `ResponseCode` 567. +#[derive(Debug, Clone, Copy, Serialize)] +pub enum CitigateBrand { + #[serde(rename = "VISA")] + Visa, + #[serde(rename = "MASTERCARD")] + Mastercard, + #[serde(rename = "AMEX")] + Amex, + #[serde(rename = "DINERS")] + Diners, + #[serde(rename = "MAESTRO")] + Maestro, +} + +fn not_supported(detail: String) -> error_stack::Report { + error_stack::report!(IntegrationError::NotSupported { + message: detail, + connector: "citigate", + context: IntegrationErrorContext::default(), + }) +} + +/// `MerchantRef` is mandatory on every Citigate request and is the *only* key a +/// Transaction Status Check can be resolved by, so an empty reference is rejected +/// up front rather than sent and refused by the gateway. +fn required_merchant_ref(reference: &str) -> Result> { + if reference.is_empty() { + return Err(error_stack::report!( + IntegrationError::MissingRequiredField { + field_name: "merchant_transaction_id", + context: IntegrationErrorContext::default(), + } + )); + } + Ok(reference.to_string()) +} + +/// Resolve the Citigate `Brand` from the supplied card network, falling back to BIN +/// detection when the network is not provided by the caller. +fn get_citigate_brand( + card: &Card, +) -> Result> { + if let Some(network) = card.card_network.as_ref() { + return match network { + CardNetwork::Visa => Ok(CitigateBrand::Visa), + CardNetwork::Mastercard => Ok(CitigateBrand::Mastercard), + CardNetwork::AmericanExpress => Ok(CitigateBrand::Amex), + CardNetwork::DinersClub => Ok(CitigateBrand::Diners), + CardNetwork::Maestro => Ok(CitigateBrand::Maestro), + other => Err(not_supported(format!("Card network {other:?}"))), + }; + } + + match get_card_issuer(card.card_number.peek())? { + CardIssuer::Visa => Ok(CitigateBrand::Visa), + CardIssuer::Master => Ok(CitigateBrand::Mastercard), + CardIssuer::AmericanExpress => Ok(CitigateBrand::Amex), + CardIssuer::DinersClub => Ok(CitigateBrand::Diners), + CardIssuer::Maestro => Ok(CitigateBrand::Maestro), + other => Err(not_supported(format!("Card issuer {other:?}"))), + } +} + +// ============================================================================= +// REQUEST +// ============================================================================= + +/// Purchase request (`TransTypeID = 0`). +/// +/// Citigate's field names are neither `camelCase` nor plain `PascalCase` +/// (`CardNo`, `CVV`, `UserIP`, `StreetLine1`, ...), so every field carries an +/// explicit `#[serde(rename = ...)]`. +#[derive(Debug, Serialize)] +pub struct CitigatePaymentsRequest { + #[serde(rename = "PaymentTypeID")] + pub payment_type_id: String, + #[serde(rename = "TransTypeID")] + pub trans_type_id: String, + #[serde(rename = "MerchantName")] + pub merchant_name: Secret, + #[serde(rename = "MerchantPassword")] + pub merchant_password: Secret, + #[serde(rename = "MerchantRef")] + pub merchant_ref: String, + #[serde(rename = "Currency")] + pub currency: common_enums::Currency, + #[serde(rename = "Amount")] + pub amount: StringMinorUnit, + #[serde(rename = "Brand")] + pub brand: CitigateBrand, + #[serde(rename = "CardholderName")] + pub cardholder_name: Secret, + #[serde(rename = "CardNo")] + pub card_no: RawCardNumber, + #[serde(rename = "ExpiryYear")] + pub expiry_year: Secret, + #[serde(rename = "ExpiryMonth")] + pub expiry_month: Secret, + #[serde(rename = "CVV")] + pub cvv: Secret, + #[serde(rename = "Firstname")] + pub firstname: Secret, + #[serde(rename = "Surname")] + pub surname: Secret, + #[serde(rename = "StreetLine1")] + pub street_line1: Secret, + #[serde(rename = "StreetLine2", skip_serializing_if = "Option::is_none")] + pub street_line2: Option>, + #[serde(rename = "City")] + pub city: Secret, + #[serde(rename = "PostalCode")] + pub postal_code: Secret, + #[serde(rename = "StateProvince", skip_serializing_if = "Option::is_none")] + pub state_province: Option>, + #[serde(rename = "Country")] + pub country: common_enums::CountryAlpha2, + #[serde(rename = "Email")] + pub email: Email, + #[serde(rename = "Telephone", skip_serializing_if = "Option::is_none")] + pub telephone: Option>, + /// `R` — and "Mandatory for Country = "US"" per the field table. Always `None`: + /// UCS carries no billing/customer date of birth on the card Authorize path + /// (`date_of_birth` exists only on `MifinityData` and the airline passenger + /// model), so there is nothing to source it from. Declared rather than omitted + /// so the gap is visible here instead of silently missing from the wire format. + #[serde(rename = "DateOfBirth", skip_serializing_if = "Option::is_none")] + pub date_of_birth: Option>, + #[serde(rename = "UserIP")] + pub user_ip: Secret, + /// `Y**` — mandatory on 3D-Secure-enabled MIDs, ignored elsewhere. Citigate + /// POSTs the cardholder back here once the redirect transaction is approved. + #[serde(rename = "SuccessURL", skip_serializing_if = "Option::is_none")] + pub success_url: Option, + /// `Y**` — the declined counterpart of `SuccessURL`. UCS has a single return + /// URL and re-derives the outcome with PSync rather than from the landing URL, + /// so both carry the same value. + #[serde(rename = "FailURL", skip_serializing_if = "Option::is_none")] + pub fail_url: Option, + /// `Y**` — receives the server-side POST that carries the final result. + #[serde(rename = "CallbackURL", skip_serializing_if = "Option::is_none")] + pub callback_url: Option, +} + +type AuthorizeRouterData = + RouterDataV2, PaymentsResponseData>; + +impl + TryFrom, T>> for CitigatePaymentsRequest +{ + type Error = error_stack::Report; + + fn try_from(item: CitigateRouterData, T>) -> Result { + let router_data = &item.router_data; + let card = match &router_data.request.payment_method_data { + PaymentMethodData::Card(card) => card, + _ => { + return Err(error_stack::report!(IntegrationError::NotImplemented( + "Only card payments are supported by citigate".to_string(), + IntegrationErrorContext::default(), + ))) + } + }; + + // The gateway predates 3DS2: there is no field anywhere in the interface to + // carry an externally obtained CAVV / ECI / dsTransId, so merchant-provided + // authentication data cannot be honoured. + if router_data.request.authentication_data.is_some() { + return Err(error_stack::report!(IntegrationError::NotSupported { + message: "External/merchant-provided 3DS authentication data".to_string(), + connector: "citigate", + context: IntegrationErrorContext::default(), + })); + } + + let auth = CitigateAuthType::try_from(&router_data.connector_config)?; + let common = &router_data.resource_common_data; + + let amount = CitigateAmountConvertor::convert( + router_data.request.minor_amount, + router_data.request.currency, + )?; + + let firstname = common.get_billing_first_name()?; + let surname = common.get_billing_last_name()?; + let cardholder_name = match card.get_optional_cardholder_name() { + Some(name) => name, + None => Secret::new(format!("{} {}", firstname.peek(), surname.peek())), + }; + + let email = match router_data.request.email.clone() { + Some(email) => email, + None => common.get_billing_email()?, + }; + + // `UserIP` is documented as mandatory (`Y`). Fail loudly rather than let the + // gateway reject the transaction with ResponseCode 535. + let user_ip = router_data.request.get_ip_address()?; + + // The field table qualifies three fields with "Mandatory for Country = "US"" + // on top of their `Y`/`R` flag, so the country has to be resolved before the + // request is assembled. Outside the US the documented flags stand on their + // own: `StateProvince` and `Telephone` stay optional, because most countries + // have no state and the gateway does not ask for one. + let country = common.get_billing_country()?; + let is_us_billing = country == common_enums::CountryAlpha2::US; + + let state_province = if is_us_billing { + Some(common.get_billing_state()?) + } else { + common.get_optional_billing_state() + }; + + let telephone = if is_us_billing { + Some(common.get_billing_phone_number()?) + } else { + common.get_optional_billing_phone_number() + }; + + // 3DS is an attribute of the MID, not of the transaction: there is no + // `3ds`/`no_3ds` request flag anywhere in the interface, and the gateway may + // ask for a redirect "irrespective of MID type". The three `Y**` URLs are + // therefore forwarded whenever the caller supplied them — a non-3D MID + // simply ignores them — but are required up front when the caller did ask + // for 3DS, so a missing URL surfaces here instead of as ResponseCode + // 584 / 585 / 586. + let return_url = router_data + .request + .router_return_url + .clone() + .or_else(|| router_data.request.complete_authorize_url.clone()) + .or_else(|| common.return_url.clone()); + let callback_url = router_data.request.webhook_url.clone(); + + if common.auth_type == AuthenticationType::ThreeDs { + if return_url.is_none() { + return Err(error_stack::report!( + IntegrationError::MissingRequiredField { + field_name: "router_return_url", + context: IntegrationErrorContext::default(), + } + )); + } + if callback_url.is_none() { + return Err(error_stack::report!( + IntegrationError::MissingRequiredField { + field_name: "webhook_url", + context: IntegrationErrorContext::default(), + } + )); + } + } + + Ok(Self { + payment_type_id: PAYMENT_TYPE_ID_CARD.to_string(), + trans_type_id: TRANS_TYPE_ID_PURCHASE.to_string(), + merchant_name: auth.merchant_name, + merchant_password: auth.merchant_password, + merchant_ref: common.connector_request_reference_id.clone(), + currency: router_data.request.currency, + amount, + brand: get_citigate_brand(card)?, + cardholder_name, + card_no: card.card_number.clone(), + expiry_year: card.get_expiry_year_4_digit(), + expiry_month: card.get_card_expiry_month_2_digit()?, + cvv: card.card_cvc.clone(), + firstname, + surname, + street_line1: common.get_billing_line1()?, + street_line2: common.get_optional_billing_line2(), + city: common.get_billing_city()?, + postal_code: common.get_billing_zip()?, + state_province, + country, + email, + telephone, + // No domain source on the card path — see the field's doc comment. + date_of_birth: None, + user_ip: Secret::new(user_ip.peek().to_string()), + success_url: return_url.clone(), + fail_url: return_url, + callback_url, + }) + } +} + +// ============================================================================= +// RESPONSE +// ============================================================================= + +/// Citigate's .NET JSON serializer emits an **empty array** (`[]`) for empty string +/// fields, and quotes numeric fields inconsistently. This collapses string, number +/// and `[]`/`null` forms into `Option`; a plain `Option` would fail +/// to deserialize `[]`. +fn deserialize_tolerant_string<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let value = Option::::deserialize(deserializer)?; + Ok(match value { + Some(serde_json::Value::String(string)) if !string.is_empty() => Some(string), + Some(serde_json::Value::Number(number)) => Some(number.to_string()), + Some(serde_json::Value::Bool(boolean)) => Some(boolean.to_string()), + // `[]`, `{}`, `""`, `null` and absent all mean "no value". + _ => None, + }) +} + +/// The single response envelope Citigate returns for every flow and every outcome +/// (approval, bank decline and gateway rejection all arrive as HTTP 200). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CitigatePaymentsResponse { + #[serde( + rename = "TransactionID", + default, + deserialize_with = "deserialize_tolerant_string" + )] + pub transaction_id: Option, + #[serde( + rename = "MerchantRef", + default, + deserialize_with = "deserialize_tolerant_string" + )] + pub merchant_ref: Option, + #[serde( + rename = "TransTypeID", + default, + deserialize_with = "deserialize_tolerant_string" + )] + pub trans_type_id: Option, + #[serde( + rename = "Currency", + default, + deserialize_with = "deserialize_tolerant_string" + )] + pub currency: Option, + #[serde( + rename = "Amount", + default, + deserialize_with = "deserialize_tolerant_string" + )] + pub amount: Option, + #[serde( + rename = "BusinessCase", + default, + deserialize_with = "deserialize_tolerant_string" + )] + pub business_case: Option, + #[serde( + rename = "Descriptor", + default, + deserialize_with = "deserialize_tolerant_string" + )] + pub descriptor: Option, + #[serde( + rename = "Bank", + default, + deserialize_with = "deserialize_tolerant_string" + )] + pub bank: Option, + #[serde( + rename = "ResponseCode", + default, + deserialize_with = "deserialize_tolerant_string" + )] + pub response_code: Option, + #[serde( + rename = "ResponseDescription", + default, + deserialize_with = "deserialize_tolerant_string" + )] + pub response_description: Option, + #[serde( + rename = "BankCode", + default, + deserialize_with = "deserialize_tolerant_string" + )] + pub bank_code: Option, + #[serde( + rename = "BankDescription", + default, + deserialize_with = "deserialize_tolerant_string" + )] + pub bank_description: Option, + #[serde( + rename = "RedirectURL", + default, + deserialize_with = "deserialize_tolerant_string" + )] + pub redirect_url: Option, +} + +impl CitigatePaymentsResponse { + /// `ResponseCode` is the primary status driver; the response `TransTypeID` + /// refines an approval into authorized vs. charged. + fn response_code(&self) -> &str { + self.response_code.as_deref().unwrap_or_default() + } + + fn trans_type_id(&self) -> &str { + self.trans_type_id.as_deref().unwrap_or_default() + } + + /// `true` when the gateway approved the transaction, asked for a cardholder + /// redirect (`600`), or reported it as still pending (`999` + `TransTypeID 6`). + fn is_success(&self) -> bool { + match self.response_code() { + RESPONSE_CODE_APPROVED => true, + // `600` is the normal first answer on a 3D-enabled MID and must travel + // through the success arm — but only if the gateway actually told us + // where to send the cardholder. + RESPONSE_CODE_REDIRECT_REQUIRED => self.redirect_form().is_some(), + RESPONSE_CODE_UNDOCUMENTED => self.trans_type_id() == RESP_TRANS_TYPE_PENDING, + _ => false, + } + } + + /// Status for an approved / redirecting / pending Authorize response. + fn attempt_status(&self) -> AttemptStatus { + match self.response_code() { + RESPONSE_CODE_APPROVED => match self.trans_type_id() { + // Bank supports sales only: authorized *and* captured in one step. + RESP_TRANS_TYPE_SALE | RESP_TRANS_TYPE_CAPTURE => AttemptStatus::Charged, + // Pre-auth performed; capture outstanding (Citigate auto-captures + // open auths after 48-96h). + RESP_TRANS_TYPE_AUTHORISE => AttemptStatus::Authorized, + _ => AttemptStatus::Pending, + }, + // Cardholder must be sent to the ACS page; the response `TransTypeID` + // is not yet meaningful and only settles once the redirect completes. + RESPONSE_CODE_REDIRECT_REQUIRED => AttemptStatus::AuthenticationPending, + _ => AttemptStatus::Pending, + } + } + + /// Status attached to a failed Authorize response. + fn failure_status(&self) -> AttemptStatus { + match self.response_code() { + // Cardholder failed / abandoned authentication at the ACS, never made it + // to the ACS (`700`) or never came back from it (`800`), or the gateway + // asked for a redirect without telling us where to. Every one of these + // fails at the authentication stage, before the bank was ever asked to + // authorize, so none of them should be reported as an authorization + // failure. + RESPONSE_CODE_3D_AUTH_FAILURE + | RESPONSE_CODE_USER_ABORTED + | RESPONSE_CODE_NO_USER_REDIRECT + | RESPONSE_CODE_NO_USER_RETURN + | RESPONSE_CODE_REDIRECT_REQUIRED => AttemptStatus::AuthenticationFailed, + _ => AttemptStatus::AuthorizationFailed, + } + } + + /// Where to send the cardholder when the gateway demanded a redirect. + /// + /// `RedirectURL` is an absolute URL whose query string is an opaque token, so + /// it is handed over verbatim as a `GET` with no form fields — there is no + /// `PaReq`/`creq` to post. It is only populated on `ResponseCode 600`, and the + /// PDF's own sample carries a stray leading space, hence the `trim`. + fn redirect_form(&self) -> Option { + if self.response_code() != RESPONSE_CODE_REDIRECT_REQUIRED { + return None; + } + self.redirect_url + .as_deref() + .map(str::trim) + .filter(|url| !url.is_empty()) + .map(|url| RedirectForm::Form { + endpoint: url.to_string(), + method: Method::Get, + form_fields: HashMap::new(), + }) + } + + /// `true` when a Status Check answered with a usable state. `600` cannot be + /// acted upon in a pull-based sync (there is no browser to redirect), so it is + /// reported as "redirect not consumed yet", i.e. still pending. + fn sync_is_success(&self) -> bool { + match self.response_code() { + RESPONSE_CODE_APPROVED | RESPONSE_CODE_REDIRECT_REQUIRED => true, + RESPONSE_CODE_UNDOCUMENTED => self.trans_type_id() == RESP_TRANS_TYPE_PENDING, + _ => false, + } + } + + /// Status Check status. The response `TransTypeID` describes the *original* + /// transaction, so it is what decides the terminal state after a 3DS redirect. + fn sync_attempt_status(&self) -> AttemptStatus { + match self.response_code() { + RESPONSE_CODE_APPROVED => match self.trans_type_id() { + // A 3D MID runs a sale, so a completed 3DS payment is charged. + RESP_TRANS_TYPE_SALE | RESP_TRANS_TYPE_CAPTURE | RESP_TRANS_TYPE_REFUND => { + AttemptStatus::Charged + } + RESP_TRANS_TYPE_AUTHORISE => AttemptStatus::Authorized, + RESP_TRANS_TYPE_CANCEL => AttemptStatus::Voided, + _ => AttemptStatus::Pending, + }, + // `6` (still processing) and an unconsumed `600` both mean "keep polling". + _ => AttemptStatus::Pending, + } + } + + /// Status attached to a failed Status Check. `TransTypeID 99` means the + /// `MerchantName` + `MerchantPassword` + `MerchantRef` triple matched nothing. + fn sync_failure_status(&self) -> AttemptStatus { + match self.response_code() { + RESPONSE_CODE_3D_AUTH_FAILURE + | RESPONSE_CODE_USER_ABORTED + | RESPONSE_CODE_NO_USER_REDIRECT + | RESPONSE_CODE_NO_USER_RETURN => AttemptStatus::AuthenticationFailed, + _ => AttemptStatus::Failure, + } + } + + /// Bank declines (`ResponseCode < 500`) carry bank-specific detail in + /// `BankCode` / `BankDescription`; gateway rejections (`> 500`) do not. + fn is_bank_decline(&self) -> bool { + self.response_code() + .parse::() + .map(|code| code != 0 && code < 500) + .unwrap_or(false) + } + + /// `TransactionID` is `"0"` when no transaction was created (gateway rejection). + fn connector_transaction_id(&self) -> Option { + self.transaction_id + .as_ref() + .filter(|id| id.as_str() != NO_TRANSACTION_ID) + .cloned() + } + + /// The MID and descriptor actually used, which vary on load-balanced master MIDs. + fn connector_metadata(&self) -> Option { + if self.business_case.is_none() && self.descriptor.is_none() && self.bank.is_none() { + return None; + } + Some(serde_json::json!({ + "business_case": self.business_case, + "descriptor": self.descriptor, + "bank": self.bank, + })) + } + + /// Metadata for Capture and Void, whose response `TransactionID` identifies a + /// **new** leg (auth `310` -> capture `312`) rather than the payment. UCS's + /// `resource_id` must keep pointing at the original authorisation, so the leg id + /// is retained here instead. + fn leg_connector_metadata(&self) -> Option { + let leg_transaction_id = self.connector_transaction_id(); + if leg_transaction_id.is_none() + && self.business_case.is_none() + && self.descriptor.is_none() + && self.bank.is_none() + { + return None; + } + Some(serde_json::json!({ + "business_case": self.business_case, + "descriptor": self.descriptor, + "bank": self.bank, + "leg_transaction_id": leg_transaction_id, + })) + } + + /// `true` when the gateway approved a post-authorization operation + /// (Capture / Void / Refund). These flows have neither a redirect (`600`) nor a + /// pending state, so `ResponseCode` alone decides the outcome. + fn is_approved(&self) -> bool { + self.response_code() == RESPONSE_CODE_APPROVED + } + + /// Capture status. On approval the response echoes `TransTypeID 3` and carries a + /// **new** capture-leg `TransactionID`. + fn capture_attempt_status(&self) -> AttemptStatus { + if self.is_approved() { + AttemptStatus::Charged + } else { + // Includes `561` — the auth was already captured, quite possibly by + // Citigate's own 48-96h auto-capture service, which cannot be disabled. + AttemptStatus::CaptureFailed + } + } + + /// Void / Cancel status. On approval the response echoes `TransTypeID 4`. + fn void_attempt_status(&self) -> AttemptStatus { + if self.is_approved() { + AttemptStatus::Voided + } else { + // `561` here means the auth is no longer open and a Refund is the + // correct operation instead. + AttemptStatus::VoidFailed + } + } + + /// Refund status. `605` ("Bank does not support API refunds") is a failure of + /// the API call even though Citigate logs a manual refund out of band; nothing + /// in this flow can resolve that, so it is reported as a failure verbatim. + fn refund_status(&self) -> RefundStatus { + if self.is_approved() { + RefundStatus::Success + } else { + RefundStatus::Failure + } + } + + /// RSync status. The Status Check reports the `TransTypeID` of the transaction + /// the `MerchantRef` resolved to, so only a refund leg (`5`) may be reported as + /// a settled refund. + fn refund_sync_status(&self) -> RefundStatus { + match (self.response_code(), self.trans_type_id()) { + (RESPONSE_CODE_APPROVED, RESP_TRANS_TYPE_REFUND) => RefundStatus::Success, + // The refund leg exists but has not reached a terminal state yet. + (RESPONSE_CODE_APPROVED, _) | (RESPONSE_CODE_UNDOCUMENTED, RESP_TRANS_TYPE_PENDING) => { + RefundStatus::Pending + } + // `999` + `TransTypeID 99` is "MerchantRef not found". + _ => RefundStatus::Failure, + } + } + + pub fn to_error_response(&self, http_code: u16) -> ErrorResponse { + self.to_error_response_with_status(http_code, self.failure_status()) + } + + fn to_error_response_with_status( + &self, + http_code: u16, + attempt_status: AttemptStatus, + ) -> ErrorResponse { + self.to_flow_error_response(http_code, FlowStatus::Payment(attempt_status)) + } + + fn to_refund_error_response( + &self, + http_code: u16, + refund_status: RefundStatus, + ) -> ErrorResponse { + self.to_flow_error_response(http_code, FlowStatus::Refund(refund_status)) + } + + fn to_flow_error_response(&self, http_code: u16, flow_status: FlowStatus) -> ErrorResponse { + let message = self + .response_description + .clone() + .unwrap_or_else(|| "Citigate transaction failed".to_string()); + let reason = self + .bank_description + .clone() + .or_else(|| self.response_description.clone()); + let is_bank_decline = self.is_bank_decline(); + + ErrorResponse { + status_code: http_code, + code: self + .response_code + .clone() + .unwrap_or_else(|| "NO_RESPONSE_CODE".to_string()), + message, + reason, + attempt_status: Some(flow_status), + connector_transaction_id: self.connector_transaction_id(), + network_decline_code: is_bank_decline.then(|| self.bank_code.clone()).flatten(), + network_advice_code: None, + network_error_message: is_bank_decline + .then(|| self.bank_description.clone()) + .flatten(), + typed_connector_response: None, + raw_connector_response: None, + raw_connector_request: None, + typed_connector_request: None, + } + } +} + +impl TryFrom> + for RouterDataV2, PaymentsResponseData> +{ + type Error = error_stack::Report; + + fn try_from( + item: ResponseRouterData, + ) -> Result { + let response = item.response; + + if !response.is_success() { + return Ok(Self { + response: Err(response.to_error_response(item.http_code)), + resource_common_data: PaymentFlowData { + status: response.failure_status(), + ..item.router_data.resource_common_data + }, + ..item.router_data + }); + } + + let resource_id = match response.connector_transaction_id() { + Some(transaction_id) => ResponseId::ConnectorTransactionId(transaction_id), + None => ResponseId::NoResponseId, + }; + + Ok(Self { + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id, + redirection_data: response.redirect_form().map(Box::new), + mandate_reference: None, + connector_metadata: response.connector_metadata(), + network_txn_id: None, + network_txn_link_id: None, + connector_response_reference_id: response.merchant_ref.clone(), + incremental_authorization_allowed: None, + splits: None, + status_code: item.http_code, + }), + resource_common_data: PaymentFlowData { + status: response.attempt_status(), + ..item.router_data.resource_common_data + }, + ..item.router_data + }) + } +} + +// ============================================================================= +// PSYNC — TRANSACTION STATUS CHECK (`TransTypeID = 8`) +// ============================================================================= + +/// Transaction Status Check request. +/// +/// The lookup key is the `MerchantName` + `MerchantPassword` + `MerchantRef` +/// triple — **not** the `TransactionID`. Querying with a different MID than the +/// one that created the payment therefore reports a perfectly good transaction as +/// `TransTypeID 99` / "MerchantRef not found". +#[derive(Debug, Serialize)] +pub struct CitigateSyncRequest { + #[serde(rename = "PaymentTypeID")] + pub payment_type_id: String, + #[serde(rename = "TransTypeID")] + pub trans_type_id: String, + #[serde(rename = "MerchantName")] + pub merchant_name: Secret, + #[serde(rename = "MerchantPassword")] + pub merchant_password: Secret, + #[serde(rename = "MerchantRef")] + pub merchant_ref: String, +} + +type SyncRouterData = RouterDataV2; + +impl + TryFrom> for CitigateSyncRequest +{ + type Error = error_stack::Report; + + fn try_from(item: CitigateRouterData) -> Result { + let router_data = &item.router_data; + let auth = CitigateAuthType::try_from(&router_data.connector_config)?; + + Ok(Self { + payment_type_id: PAYMENT_TYPE_ID_CARD.to_string(), + trans_type_id: TRANS_TYPE_ID_STATUS_CHECK.to_string(), + merchant_name: auth.merchant_name, + merchant_password: auth.merchant_password, + merchant_ref: required_merchant_ref( + &router_data + .resource_common_data + .connector_request_reference_id, + )?, + }) + } +} + +/// Status Check answers with the very same envelope as a purchase (minus +/// `RedirectURL`); the newtype exists only to give the macro framework a distinct +/// response type per flow. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(transparent)] +pub struct CitigateSyncResponse(pub CitigatePaymentsResponse); + +impl TryFrom> for SyncRouterData { + type Error = error_stack::Report; + + fn try_from(item: ResponseRouterData) -> Result { + let response = item.response.0; + + if !response.sync_is_success() { + return Ok(Self { + response: Err(response + .to_error_response_with_status(item.http_code, response.sync_failure_status())), + resource_common_data: PaymentFlowData { + status: response.sync_failure_status(), + ..item.router_data.resource_common_data + }, + ..item.router_data + }); + } + + let resource_id = match response.connector_transaction_id() { + Some(transaction_id) => ResponseId::ConnectorTransactionId(transaction_id), + None => item.router_data.request.connector_transaction_id.clone(), + }; + + Ok(Self { + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id, + // A sync has no browser to redirect: the redirect instruction, if + // any, was already handed out by Authorize. + redirection_data: None, + mandate_reference: None, + connector_metadata: response.connector_metadata(), + network_txn_id: None, + network_txn_link_id: None, + connector_response_reference_id: response.merchant_ref.clone(), + incremental_authorization_allowed: None, + splits: None, + status_code: item.http_code, + }), + resource_common_data: PaymentFlowData { + status: response.sync_attempt_status(), + ..item.router_data.resource_common_data + }, + ..item.router_data + }) + } +} + +// ============================================================================= +// CAPTURE (`TransTypeID = 3`) +// ============================================================================= + +/// Capture request — settles an open authorisation. +/// +/// The documented field list has exactly six entries and **no `Amount`**: Citigate +/// cannot perform a partial, multiple or incremental capture. +#[derive(Debug, Serialize)] +pub struct CitigateCaptureRequest { + #[serde(rename = "PaymentTypeID")] + pub payment_type_id: String, + #[serde(rename = "TransTypeID")] + pub trans_type_id: String, + #[serde(rename = "MerchantName")] + pub merchant_name: Secret, + #[serde(rename = "MerchantPassword")] + pub merchant_password: Secret, + #[serde(rename = "MerchantRef")] + pub merchant_ref: String, + /// The transaction reference from the original authorisation. + #[serde(rename = "TransactionID")] + pub transaction_id: String, +} + +type CaptureRouterData = + RouterDataV2; + +impl + TryFrom> for CitigateCaptureRequest +{ + type Error = error_stack::Report; + + fn try_from(item: CitigateRouterData) -> Result { + let router_data = &item.router_data; + let request = &router_data.request; + + // Fail fast rather than silently capturing an amount the caller did not ask + // for: the wire format simply has nowhere to put one. + if request.is_multiple_capture() { + return Err(not_supported("Multiple partial captures".to_string())); + } + if let Some(authorized) = router_data.resource_common_data.minor_amount_authorized { + if authorized != request.minor_amount_to_capture { + return Err(not_supported("Partial capture".to_string())); + } + } + + let auth = CitigateAuthType::try_from(&router_data.connector_config)?; + + Ok(Self { + payment_type_id: PAYMENT_TYPE_ID_CARD.to_string(), + trans_type_id: TRANS_TYPE_ID_CAPTURE.to_string(), + merchant_name: auth.merchant_name, + merchant_password: auth.merchant_password, + merchant_ref: required_merchant_ref( + &router_data + .resource_common_data + .connector_request_reference_id, + )?, + transaction_id: request.get_connector_transaction_id()?, + }) + } +} + +/// Capture answers with the shared response envelope; the newtype exists only to +/// give the macro framework a distinct response type per flow. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(transparent)] +pub struct CitigateCaptureResponse(pub CitigatePaymentsResponse); + +impl TryFrom> for CaptureRouterData { + type Error = error_stack::Report; + + fn try_from( + item: ResponseRouterData, + ) -> Result { + let response = item.response.0; + let status = response.capture_attempt_status(); + + if !response.is_approved() { + return Ok(Self { + response: Err(response.to_error_response_with_status(item.http_code, status)), + resource_common_data: PaymentFlowData { + status, + ..item.router_data.resource_common_data + }, + ..item.router_data + }); + } + + Ok(Self { + response: Ok(PaymentsResponseData::TransactionResponse { + // The response `TransactionID` is the new capture leg, not the + // payment — keep the id every later operation and PSync key off. + resource_id: item.router_data.request.connector_transaction_id.clone(), + redirection_data: None, + mandate_reference: None, + connector_metadata: response.leg_connector_metadata(), + network_txn_id: None, + network_txn_link_id: None, + connector_response_reference_id: response.merchant_ref.clone(), + incremental_authorization_allowed: None, + splits: None, + status_code: item.http_code, + }), + resource_common_data: PaymentFlowData { + status, + ..item.router_data.resource_common_data + }, + ..item.router_data + }) + } +} + +// ============================================================================= +// VOID / CANCEL (`TransTypeID = 4`) +// ============================================================================= + +/// Cancel request — voids an open authorisation. +/// +/// Same six fields as Capture. There is no `Amount` (a void always cancels the +/// whole authorisation) and no field able to carry a cancellation reason. +#[derive(Debug, Serialize)] +pub struct CitigateVoidRequest { + #[serde(rename = "PaymentTypeID")] + pub payment_type_id: String, + #[serde(rename = "TransTypeID")] + pub trans_type_id: String, + #[serde(rename = "MerchantName")] + pub merchant_name: Secret, + #[serde(rename = "MerchantPassword")] + pub merchant_password: Secret, + #[serde(rename = "MerchantRef")] + pub merchant_ref: String, + /// The transaction reference from the original authorisation. + #[serde(rename = "TransactionID")] + pub transaction_id: String, +} + +type VoidRouterData = RouterDataV2; + +impl + TryFrom> for CitigateVoidRequest +{ + type Error = error_stack::Report; + + fn try_from(item: CitigateRouterData) -> Result { + let router_data = &item.router_data; + let request = &router_data.request; + + if let (Some(requested), Some(authorized)) = ( + request.amount, + router_data.resource_common_data.minor_amount_authorized, + ) { + if requested != authorized { + return Err(not_supported("Partial void".to_string())); + } + } + + let auth = CitigateAuthType::try_from(&router_data.connector_config)?; + + Ok(Self { + payment_type_id: PAYMENT_TYPE_ID_CARD.to_string(), + trans_type_id: TRANS_TYPE_ID_CANCEL.to_string(), + merchant_name: auth.merchant_name, + merchant_password: auth.merchant_password, + merchant_ref: required_merchant_ref( + &router_data + .resource_common_data + .connector_request_reference_id, + )?, + transaction_id: request.connector_transaction_id.clone(), + }) + } +} + +/// Cancel answers with the shared response envelope. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(transparent)] +pub struct CitigateVoidResponse(pub CitigatePaymentsResponse); + +impl TryFrom> for VoidRouterData { + type Error = error_stack::Report; + + fn try_from(item: ResponseRouterData) -> Result { + let response = item.response.0; + let status = response.void_attempt_status(); + + if !response.is_approved() { + return Ok(Self { + response: Err(response.to_error_response_with_status(item.http_code, status)), + resource_common_data: PaymentFlowData { + status, + ..item.router_data.resource_common_data + }, + ..item.router_data + }); + } + + Ok(Self { + response: Ok(PaymentsResponseData::TransactionResponse { + // As with Capture, the response `TransactionID` is the new cancel + // leg and must not replace the payment's id. + resource_id: ResponseId::ConnectorTransactionId( + item.router_data.request.connector_transaction_id.clone(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: response.leg_connector_metadata(), + network_txn_id: None, + network_txn_link_id: None, + connector_response_reference_id: response.merchant_ref.clone(), + incremental_authorization_allowed: None, + splits: None, + status_code: item.http_code, + }), + resource_common_data: PaymentFlowData { + status, + ..item.router_data.resource_common_data + }, + ..item.router_data + }) + } +} + +// ============================================================================= +// REFUND (`TransTypeID = 5`) +// ============================================================================= + +/// Refund request — refunds a sale, or an authorisation that has been captured +/// (including one auto-captured by Citigate, in which case the *authorisation's* +/// `TransactionID` is still the correct value to send). +#[derive(Debug, Serialize)] +pub struct CitigateRefundRequest { + #[serde(rename = "PaymentTypeID")] + pub payment_type_id: String, + #[serde(rename = "TransTypeID")] + pub trans_type_id: String, + #[serde(rename = "MerchantName")] + pub merchant_name: Secret, + #[serde(rename = "MerchantPassword")] + pub merchant_password: Secret, + /// Also the RSync lookup key — the Status Check cannot be keyed on anything else. + #[serde(rename = "MerchantRef")] + pub merchant_ref: String, + #[serde(rename = "TransactionID")] + pub transaction_id: String, + /// Omitted for a full refund, which is both the documented default and the only + /// form that works on an account where partial refunds have not been enabled + /// (the default; sending an `Amount` there is rejected with `629`). + #[serde(rename = "Amount", skip_serializing_if = "Option::is_none")] + pub amount: Option, +} + +type RefundRouterData = RouterDataV2; + +impl + TryFrom> for CitigateRefundRequest +{ + type Error = error_stack::Report; + + fn try_from(item: CitigateRouterData) -> Result { + let router_data = &item.router_data; + let request = &router_data.request; + let auth = CitigateAuthType::try_from(&router_data.connector_config)?; + + let amount = if request.minor_refund_amount == request.minor_payment_amount { + None + } else { + Some(CitigateAmountConvertor::convert( + request.minor_refund_amount, + request.currency, + )?) + }; + + Ok(Self { + payment_type_id: PAYMENT_TYPE_ID_CARD.to_string(), + trans_type_id: TRANS_TYPE_ID_REFUND.to_string(), + merchant_name: auth.merchant_name, + merchant_password: auth.merchant_password, + merchant_ref: required_merchant_ref( + &router_data + .resource_common_data + .connector_request_reference_id, + )?, + transaction_id: request.connector_transaction_id.clone(), + amount, + }) + } +} + +/// Refund answers with the shared response envelope. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(transparent)] +pub struct CitigateRefundResponse(pub CitigatePaymentsResponse); + +impl TryFrom> for RefundRouterData { + type Error = error_stack::Report; + + fn try_from( + item: ResponseRouterData, + ) -> Result { + let response = item.response.0; + let refund_status = response.refund_status(); + + if !response.is_approved() { + return Ok(Self { + response: Err(response.to_refund_error_response(item.http_code, refund_status)), + resource_common_data: RefundFlowData { + status: refund_status, + ..item.router_data.resource_common_data + }, + ..item.router_data + }); + } + + // Here the new leg id *is* the refund, so it becomes `connector_refund_id`. + let connector_refund_id = response + .connector_transaction_id() + .unwrap_or_else(|| item.router_data.request.refund_id.clone()); + + Ok(Self { + response: Ok(RefundsResponseData { + connector_refund_id, + refund_status, + status_code: item.http_code, + acquirer_reference_number: None, + }), + resource_common_data: RefundFlowData { + status: refund_status, + ..item.router_data.resource_common_data + }, + ..item.router_data + }) + } +} + +// ============================================================================= +// RSYNC — TRANSACTION STATUS CHECK ON THE REFUND'S `MerchantRef` +// ============================================================================= + +/// Refund status check. Byte-identical on the wire to [`CitigateSyncRequest`] — the +/// only thing that makes a `TransTypeID = 8` call an RSync rather than a PSync is +/// that the `MerchantRef` is the **refund's** reference. Keying it on +/// `connector_refund_id` (a Citigate `TransactionID`, which the status check has no +/// field for) or on the Authorize `MerchantRef` would silently resolve the payment +/// leg instead. +#[derive(Debug, Serialize)] +#[serde(transparent)] +pub struct CitigateRefundSyncRequest(pub CitigateSyncRequest); + +type RefundSyncRouterData = + RouterDataV2; + +impl + TryFrom> for CitigateRefundSyncRequest +{ + type Error = error_stack::Report; + + fn try_from(item: CitigateRouterData) -> Result { + let router_data = &item.router_data; + let auth = CitigateAuthType::try_from(&router_data.connector_config)?; + + Ok(Self(CitigateSyncRequest { + payment_type_id: PAYMENT_TYPE_ID_CARD.to_string(), + trans_type_id: TRANS_TYPE_ID_STATUS_CHECK.to_string(), + merchant_name: auth.merchant_name, + merchant_password: auth.merchant_password, + // The RefundSync RouterData carries the same + // `connector_request_reference_id` the Refund request used. + merchant_ref: required_merchant_ref( + &router_data + .resource_common_data + .connector_request_reference_id, + )?, + })) + } +} + +/// The refund status check answers with the shared response envelope. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(transparent)] +pub struct CitigateRefundSyncResponse(pub CitigatePaymentsResponse); + +impl TryFrom> for RefundSyncRouterData { + type Error = error_stack::Report; + + fn try_from( + item: ResponseRouterData, + ) -> Result { + let response = item.response.0; + let refund_status = response.refund_sync_status(); + + if refund_status == RefundStatus::Failure { + return Ok(Self { + response: Err(response.to_refund_error_response(item.http_code, refund_status)), + resource_common_data: RefundFlowData { + status: refund_status, + ..item.router_data.resource_common_data + }, + ..item.router_data + }); + } + + let connector_refund_id = response + .connector_transaction_id() + .unwrap_or_else(|| item.router_data.request.connector_refund_id.clone()); + + Ok(Self { + response: Ok(RefundsResponseData { + connector_refund_id, + refund_status, + status_code: item.http_code, + acquirer_reference_number: None, + }), + resource_common_data: RefundFlowData { + status: refund_status, + ..item.router_data.resource_common_data + }, + ..item.router_data + }) + } +} diff --git a/crates/integrations/connector-integration/src/default_implementations.rs b/crates/integrations/connector-integration/src/default_implementations.rs index fb944d7f79..e139f733ec 100644 --- a/crates/integrations/connector-integration/src/default_implementations.rs +++ b/crates/integrations/connector-integration/src/default_implementations.rs @@ -291,6 +291,7 @@ default_impl_verify_webhook_source_v2!( Grabpay, Tesouro, Boost, + Citigate, ], ); // PayPal has its own implementation in paypal.rs @@ -367,6 +368,7 @@ macro_rules! default_impl_recharge_v2 { } default_impl_recharge_v2!( + Citigate, Boost, Tesouro, AbsaSanlam, @@ -592,6 +594,7 @@ macro_rules! default_impl_get_payment_method_v2 { // Same connector universe as default_impl_recharge_v2! above. default_impl_create_payment_method_v2!( + Citigate, Boost, Tesouro, AbsaSanlam, @@ -695,6 +698,7 @@ default_impl_create_payment_method_v2!( ); default_impl_get_payment_method_v2!( + Citigate, Boost, Tesouro, AbsaSanlam, @@ -799,6 +803,7 @@ default_impl_get_payment_method_v2!( default_impl_payment_method_eligibility_v2!( not_supported: [ + Citigate, Boost, Tesouro, Adyen, @@ -964,6 +969,7 @@ macro_rules! default_impl_refresh_payment_method_v2 { } default_impl_refresh_payment_method_v2!( + Citigate, Boost, AbsaSanlam, Aci, diff --git a/crates/integrations/connector-integration/src/types.rs b/crates/integrations/connector-integration/src/types.rs index b8cd3f9ae4..15a353b181 100644 --- a/crates/integrations/connector-integration/src/types.rs +++ b/crates/integrations/connector-integration/src/types.rs @@ -142,6 +142,7 @@ impl Box::new(connectors::Grabpay::::new()), ConnectorEnum::Tesouro => Box::new(connectors::Tesouro::::new()), ConnectorEnum::Boost => Box::new(connectors::Boost::::new()), + ConnectorEnum::Citigate => Box::new(connectors::Citigate::::new()), } } } diff --git a/crates/internal/field-probe/src/auth.rs b/crates/internal/field-probe/src/auth.rs index 641c45e5e6..4cb785803a 100644 --- a/crates/internal/field-probe/src/auth.rs +++ b/crates/internal/field-probe/src/auth.rs @@ -792,5 +792,10 @@ pub(crate) fn dummy_auth(connector: &ConnectorEnum) -> ConnectorSpecificConfig { merchant_secret: k(), base_url: None, }, + ConnectorEnum::Citigate => ConnectorSpecificConfig::Citigate { + api_key: k(), + key1: k(), + base_url: None, + }, } } diff --git a/crates/internal/integration-tests/src/connector_specs/citigate/specs.json b/crates/internal/integration-tests/src/connector_specs/citigate/specs.json new file mode 100644 index 0000000000..3fe703d6ee --- /dev/null +++ b/crates/internal/integration-tests/src/connector_specs/citigate/specs.json @@ -0,0 +1,11 @@ +{ + "connector": "citigate", + "supported_suites": [ + "PaymentService/Authorize", + "PaymentService/Get", + "PaymentService/Capture", + "PaymentService/Void", + "PaymentService/Refund", + "RefundService/Get" + ] +} diff --git a/crates/types-traits/domain_types/src/connector_types.rs b/crates/types-traits/domain_types/src/connector_types.rs index 722c32c63b..ef8af08741 100644 --- a/crates/types-traits/domain_types/src/connector_types.rs +++ b/crates/types-traits/domain_types/src/connector_types.rs @@ -163,6 +163,7 @@ pub enum ConnectorEnum { Grabpay, Tesouro, Boost, + Citigate, } // snake case for enum variants @@ -521,6 +522,7 @@ impl ForeignTryFrom for ConnectorEnum { grpc_api_types::payments::Connector::Givepayments => Ok(Self::Givepayments), grpc_api_types::payments::Connector::Boost => Ok(Self::Boost), grpc_api_types::payments::Connector::Grabpay => Ok(Self::Grabpay), + grpc_api_types::payments::Connector::Citigate => Ok(Self::Citigate), grpc_api_types::payments::Connector::Unspecified => { Err(IntegrationError::InvalidDataFormat { field_name: "connector", @@ -5669,6 +5671,7 @@ impl ForeignTryFrom AuthType::Maya(_) => Ok(Self::Payment(ConnectorEnum::Maya)), AuthType::Tesouro(_) => Ok(Self::Payment(ConnectorEnum::Tesouro)), AuthType::Boost(_) => Ok(Self::Payment(ConnectorEnum::Boost)), + AuthType::Citigate(_) => Ok(Self::Payment(ConnectorEnum::Citigate)), AuthType::Imerchantsolutions(_) => Ok(Self::Payment(ConnectorEnum::Imerchantsolutions)), AuthType::TsysTransit(_) => Ok(Self::Payment(ConnectorEnum::TsysTransit)), AuthType::TwocTwopPaco(_) => Ok(Self::Payment(ConnectorEnum::TwocTwopPaco)), diff --git a/crates/types-traits/domain_types/src/router_data.rs b/crates/types-traits/domain_types/src/router_data.rs index c1f7470711..0defa7bc39 100644 --- a/crates/types-traits/domain_types/src/router_data.rs +++ b/crates/types-traits/domain_types/src/router_data.rs @@ -966,6 +966,11 @@ pub enum ConnectorSpecificConfig { merchant_secret: Secret, base_url: Option, }, + Citigate { + api_key: Secret, + key1: Secret, + base_url: Option, + }, } impl ConnectorSpecificConfig { @@ -1313,6 +1318,7 @@ impl ConnectorSpecificConfig { api_secret }, Boost { api_key }, + Citigate { api_key, key1 }, Imerchantsolutions { api_key }, Interpayments { api_key }, TwocTwopPaco { @@ -1783,6 +1789,7 @@ impl ConnectorSpecificConfig { api_secret }, Boost { api_key }, + Citigate { api_key, key1 }, Imerchantsolutions { api_key }, Interpayments { api_key }, TwocTwopPaco { @@ -2405,6 +2412,11 @@ impl ForeignTryFrom for Conne merchant_secret: boost.merchant_secret.ok_or_else(err)?, base_url: boost.base_url, }), + AuthType::Citigate(citigate) => Ok(Self::Citigate { + api_key: citigate.api_key.ok_or_else(err)?, + key1: citigate.key1.ok_or_else(err)?, + base_url: citigate.base_url, + }), AuthType::Imerchantsolutions(imerchantsolutions) => Ok(Self::Imerchantsolutions { api_key: imerchantsolutions.api_key.ok_or_else(err)?, merchant_id: imerchantsolutions.merchant_id, @@ -3613,6 +3625,14 @@ impl ForeignTryFrom<(&ConnectorAuthType, &connector_types::ConnectorVariant)> }), _ => Err(err().into()), }, + ConnectorEnum::Citigate => match auth { + ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self::Citigate { + api_key: api_key.clone(), + key1: key1.clone(), + base_url: None, + }), + _ => Err(err().into()), + }, ConnectorEnum::PinelabsOnline => match auth { ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self::PinelabsOnline { client_id: api_key.clone(), diff --git a/crates/types-traits/domain_types/src/types.rs b/crates/types-traits/domain_types/src/types.rs index 7f6d64bbd3..735345db94 100644 --- a/crates/types-traits/domain_types/src/types.rs +++ b/crates/types-traits/domain_types/src/types.rs @@ -428,6 +428,7 @@ pub struct Connectors { pub tesouro: ConnectorParams, pub boost: ConnectorParams, pub santander: ConnectorParams, + pub citigate: ConnectorParams, } #[derive(Clone, Deserialize, Serialize, Debug, Default, PartialEq, config_patch_derive::Patch)] diff --git a/crates/types-traits/grpc-api-types/proto/payment.proto b/crates/types-traits/grpc-api-types/proto/payment.proto index 5fbeea121b..58bba3fa4e 100644 --- a/crates/types-traits/grpc-api-types/proto/payment.proto +++ b/crates/types-traits/grpc-api-types/proto/payment.proto @@ -900,6 +900,7 @@ enum Connector { MAYA = 134; GRABPAY = 135; BOOST = 136; + CITIGATE = 137; } // Payment method types @@ -5650,6 +5651,12 @@ message BoostConfig { optional string base_url = 50; } +message CitigateConfig { + SecretString api_key = 1; + SecretString key1 = 2; + optional string base_url = 50; +} + // ConnectorSpecificConfig message with oneof containing all connector // configurations. Comment above each field (e.g. "// PAYPAL = 62") is the // Connector enum value from this file; @@ -5966,6 +5973,8 @@ message ConnectorSpecificConfig { GrabpayConfig grabpay = 144; // BOOST = 136 BoostConfig boost = 145; + // CITIGATE = 137 + CitigateConfig citigate = 146; } } diff --git a/data/field_probe/citigate.json b/data/field_probe/citigate.json new file mode 100644 index 0000000000..00f1c35ea3 --- /dev/null +++ b/data/field_probe/citigate.json @@ -0,0 +1,807 @@ +{ + "connector": "citigate", + "flows": { + "authenticate": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: authenticate flow for citigate" + } + }, + "authorize": { + "Ach": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "AchBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Affirm": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Afterpay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Alfamart": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "AliPayRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "AmazonPayRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "ApplePay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "ApplePayDecrypted": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "ApplePayThirdPartySdk": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Bacs": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "BacsBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "BancontactCard": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "BcaBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Becs": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "BillDeskRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Bizum": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Blik": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Bluecode": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "BniVaBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Boleto": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "BriVaBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Card": { + "status": "supported", + "proto_request": { + "merchant_transaction_id": "probe_txn_001", + "amount": { + "minor_amount": 1000, + "currency": "USD" + }, + "payment_method": { + "card": { + "card_number": "4111111111111111", + "card_exp_month": "03", + "card_exp_year": "2030", + "card_cvc": "737", + "card_holder_name": "John Doe" + } + }, + "capture_method": "AUTOMATIC", + "address": { + "billing_address": { + "first_name": "John", + "last_name": "Doe", + "line1": "123 Main St", + "city": "Seattle", + "state": "WA", + "zip_code": "98101", + "country_alpha2_code": "US", + "email": "test@example.com", + "phone_number": "4155552671", + "phone_country_code": "+1" + } + }, + "auth_type": "NO_THREE_DS", + "return_url": "https://example.com/return", + "browser_info": { + "ip_address": "1.2.3.4" + } + }, + "sample": { + "url": "https://gw-test.cgate.tech/orion/interface/json.ashx", + "method": "Post", + "headers": { + "content-type": "application/json", + "via": "HyperSwitch" + }, + "body": "{\"PaymentTypeID\":\"1\",\"TransTypeID\":\"0\",\"MerchantName\":\"probe_key\",\"MerchantPassword\":\"probe_key\",\"MerchantRef\":\"probe_txn_001\",\"Currency\":\"USD\",\"Amount\":\"1000\",\"Brand\":\"VISA\",\"CardholderName\":\"John Doe\",\"CardNo\":\"4111111111111111\",\"ExpiryYear\":\"2030\",\"ExpiryMonth\":\"03\",\"CVV\":\"737\",\"Firstname\":\"John\",\"Surname\":\"Doe\",\"StreetLine1\":\"123 Main St\",\"City\":\"Seattle\",\"PostalCode\":\"98101\",\"StateProvince\":\"WA\",\"Country\":\"US\",\"Email\":\"test@example.com\",\"Telephone\":\"+14155552671\",\"UserIP\":\"1.2.3.4\",\"SuccessURL\":\"https://example.com/return\",\"FailURL\":\"https://example.com/return\"}" + } + }, + "CashappQr": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "CashfreeRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "CimbVaBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "ClassicReward": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Crypto": { + "status": "not_supported", + "error": "Invalid data format: payment_method. The provided payment method variant is empty or not supported by this flow" + }, + "Dana": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "DanamonVaBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "DuitNow": { + "status": "not_supported", + "error": "Invalid data format: payment_method. The provided payment method variant is empty or not supported by this flow" + }, + "EVoucher": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "EaseBuzzRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Efecty": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Eft": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Eps": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "FamilyMart": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "GCash": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Giropay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Givex": { + "status": "not_supported", + "error": "Invalid data format: payment_method. The provided payment method variant is empty or not supported by this flow" + }, + "GoPay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "GooglePay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "GooglePayDecrypted": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "GooglePayThirdPartySdk": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Ideal": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Indomaret": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "IndonesianBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "InstantBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "InstantBankTransferFinland": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "InstantBankTransferPoland": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Interac": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "KakaoPay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Klarna": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Lawson": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "LazyPayRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "LocalBankRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "LocalBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "MandiriVaBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "MbWay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Mifinity": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "MiniStop": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "MobilePayRedirect": { + "status": "not_supported", + "error": "Invalid data format: payment_method. The provided payment method variant is empty or not supported by this flow" + }, + "Momo": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "MultibancoBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Netbanking": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "OnlineBankingCzechRepublic": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "OnlineBankingFinland": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "OnlineBankingFpx": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "OnlineBankingPoland": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "OnlineBankingSlovakia": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "OnlineBankingThailand": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "OpenBanking": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "OpenBankingPis": { + "status": "not_supported", + "error": "Invalid data format: payment_method. The provided payment method variant is empty or not supported by this flow" + }, + "OpenBankingUk": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Oxxo": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "PagoEfectivo": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "PayEasy": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "PaySafeCard": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "PayURedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "PaypalRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "PaypalSdk": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Paysera": { + "status": "not_supported", + "error": "Invalid data format: payment_method. The provided payment method variant is empty or not supported by this flow" + }, + "Paze": { + "status": "not_supported", + "error": "Invalid data format: payment_method. The provided payment method variant is empty or not supported by this flow" + }, + "PermataBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "PhonePeRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Pix": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Przelewy24": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Pse": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "RedCompra": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "RedPagos": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "RevolutPay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "SamsungPay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Satispay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Seicomart": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Sepa": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "SepaBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "SepaGuaranteedDebit": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "SevenEleven": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Skrill": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Sofort": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Swish": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "TouchNGo": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Trustly": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Twint": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "UpiCollect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "UpiIntent": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "UpiQr": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Vipps": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "WeChatPayQr": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Wero": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + } + }, + "capture": { + "default": { + "status": "supported", + "proto_request": { + "merchant_capture_id": "probe_capture_001", + "connector_transaction_id": "probe_connector_txn_001", + "amount_to_capture": { + "minor_amount": 1000, + "currency": "USD" + } + }, + "sample": { + "url": "https://gw-test.cgate.tech/orion/interface/json.ashx", + "method": "Post", + "headers": { + "content-type": "application/json", + "via": "HyperSwitch" + }, + "body": "{\"PaymentTypeID\":\"1\",\"TransTypeID\":\"3\",\"MerchantName\":\"probe_key\",\"MerchantPassword\":\"probe_key\",\"MerchantRef\":\"probe_capture_001\",\"TransactionID\":\"probe_connector_txn_001\"}" + } + } + }, + "create_client_authentication_token": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: client_authentication_token flow for citigate" + } + }, + "create_order": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: create_order flow for citigate" + } + }, + "create_server_authentication_token": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: server_authentication_token flow for citigate" + } + }, + "create_server_session_authentication_token": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: server_session_authentication_token flow for citigate" + } + }, + "customer_create": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: create_connector_customer flow for citigate" + } + }, + "customer_get": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: get_connector_customer flow for citigate" + } + }, + "dispute_accept": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: accept_dispute flow for citigate" + } + }, + "dispute_defend": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: defend_dispute flow for citigate" + } + }, + "dispute_get": { + "default": { + "status": "not_implemented" + } + }, + "dispute_submit_evidence": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: submit_evidence flow for citigate" + } + }, + "eligibility": { + "default": { + "status": "not_supported", + "error": "eligibility flow not supported by citigate connector" + } + }, + "get": { + "default": { + "status": "supported", + "proto_request": { + "merchant_transaction_id": "probe_merchant_txn_001", + "connector_transaction_id": "probe_connector_txn_001", + "amount": { + "minor_amount": 1000, + "currency": "USD" + } + }, + "sample": { + "url": "https://gw-test.cgate.tech/orion/interface/json.ashx", + "method": "Post", + "headers": { + "content-type": "application/json", + "via": "HyperSwitch" + }, + "body": "{\"PaymentTypeID\":\"1\",\"TransTypeID\":\"8\",\"MerchantName\":\"probe_key\",\"MerchantPassword\":\"probe_key\",\"MerchantRef\":\"probe_merchant_txn_001\"}" + } + } + }, + "handle_event": { + "default": { + "status": "not_implemented" + } + }, + "incremental_authorization": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: incremental_authorization flow for citigate" + } + }, + "parse_event": { + "default": { + "status": "not_implemented" + } + }, + "payment_method_eligibility": { + "default": { + "status": "not_implemented" + } + }, + "post_authenticate": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: post_authenticate flow for citigate" + } + }, + "pre_authenticate": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: pre_authenticate flow for citigate" + } + }, + "proxy_authorize": { + "default": { + "status": "supported", + "proto_request": { + "merchant_transaction_id": "probe_proxy_txn_001", + "amount": { + "minor_amount": 1000, + "currency": "USD" + }, + "card_proxy": { + "card_number": "4111111111111111", + "card_exp_month": "03", + "card_exp_year": "2030", + "card_cvc": "123", + "card_holder_name": "John Doe", + "card_network": "VISA" + }, + "address": { + "billing_address": { + "first_name": "John", + "last_name": "Doe", + "line1": "123 Main St", + "city": "Seattle", + "state": "WA", + "zip_code": "98101", + "country_alpha2_code": "US", + "email": "test@example.com", + "phone_number": "4155552671", + "phone_country_code": "+1" + } + }, + "capture_method": "AUTOMATIC", + "auth_type": "NO_THREE_DS", + "return_url": "https://example.com/return", + "browser_info": { + "ip_address": "1.2.3.4" + } + }, + "sample": { + "url": "https://gw-test.cgate.tech/orion/interface/json.ashx", + "method": "Post", + "headers": { + "content-type": "application/json", + "via": "HyperSwitch" + }, + "body": "{\"PaymentTypeID\":\"1\",\"TransTypeID\":\"0\",\"MerchantName\":\"probe_key\",\"MerchantPassword\":\"probe_key\",\"MerchantRef\":\"probe_proxy_txn_001\",\"Currency\":\"USD\",\"Amount\":\"1000\",\"Brand\":\"VISA\",\"CardholderName\":\"John Doe\",\"CardNo\":\"{{$card_number}}\",\"ExpiryYear\":\"2030\",\"ExpiryMonth\":\"03\",\"CVV\":\"{{$card_cvc}}\",\"Firstname\":\"John\",\"Surname\":\"Doe\",\"StreetLine1\":\"123 Main St\",\"City\":\"Seattle\",\"PostalCode\":\"98101\",\"StateProvince\":\"WA\",\"Country\":\"US\",\"Email\":\"test@example.com\",\"Telephone\":\"+14155552671\",\"UserIP\":\"1.2.3.4\",\"SuccessURL\":\"https://example.com/return\",\"FailURL\":\"https://example.com/return\"}" + } + } + }, + "proxy_setup_recurring": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: setup_mandate flow for citigate" + } + }, + "recurring_charge": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: repeat_payment flow for citigate" + } + }, + "recurring_revoke": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: mandate_revoke flow for citigate" + } + }, + "refresh": { + "default": { + "status": "not_implemented" + } + }, + "refund": { + "default": { + "status": "supported", + "proto_request": { + "merchant_refund_id": "probe_refund_001", + "connector_transaction_id": "probe_connector_txn_001", + "payment_amount": 1000, + "refund_amount": { + "minor_amount": 1000, + "currency": "USD" + }, + "reason": "customer_request" + }, + "sample": { + "url": "https://gw-test.cgate.tech/orion/interface/json.ashx", + "method": "Post", + "headers": { + "content-type": "application/json", + "via": "HyperSwitch" + }, + "body": "{\"PaymentTypeID\":\"1\",\"TransTypeID\":\"5\",\"MerchantName\":\"probe_key\",\"MerchantPassword\":\"probe_key\",\"MerchantRef\":\"probe_refund_001\",\"TransactionID\":\"probe_connector_txn_001\"}" + } + } + }, + "refund_get": { + "default": { + "status": "supported", + "proto_request": { + "merchant_refund_id": "probe_refund_001", + "connector_transaction_id": "probe_connector_txn_001", + "refund_id": "probe_refund_id_001" + }, + "sample": { + "url": "https://gw-test.cgate.tech/orion/interface/json.ashx", + "method": "Post", + "headers": { + "content-type": "application/json", + "via": "HyperSwitch" + }, + "body": "{\"PaymentTypeID\":\"1\",\"TransTypeID\":\"8\",\"MerchantName\":\"probe_key\",\"MerchantPassword\":\"probe_key\",\"MerchantRef\":\"probe_refund_001\"}" + } + } + }, + "reverse": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: void_post_capture flow for citigate" + } + }, + "setup_recurring": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: setup_mandate flow for citigate" + } + }, + "token_authorize": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + } + }, + "token_setup_recurring": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: setup_mandate flow for citigate" + } + }, + "tokenize": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: payment_method_token flow for citigate" + } + }, + "verify_redirect": { + "default": { + "status": "not_implemented" + } + }, + "void": { + "default": { + "status": "supported", + "proto_request": { + "merchant_void_id": "probe_void_001", + "connector_transaction_id": "probe_connector_txn_001" + }, + "sample": { + "url": "https://gw-test.cgate.tech/orion/interface/json.ashx", + "method": "Post", + "headers": { + "content-type": "application/json", + "via": "HyperSwitch" + }, + "body": "{\"PaymentTypeID\":\"1\",\"TransTypeID\":\"4\",\"MerchantName\":\"probe_key\",\"MerchantPassword\":\"probe_key\",\"MerchantRef\":\"probe_void_001\",\"TransactionID\":\"probe_connector_txn_001\"}" + } + } + } + } +} \ No newline at end of file diff --git a/docs-generated/all_connector.md b/docs-generated/all_connector.md index 22c10cb027..d2634365ae 100644 --- a/docs-generated/all_connector.md +++ b/docs-generated/all_connector.md @@ -44,6 +44,7 @@ Authorize a payment amount on a payment method. This reserves funds without capt | [CashtoCode](connectors/cashtocode.md) | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | | [Celero](connectors/celero.md) | ✓ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | | [Checkout.com](connectors/checkout.md) | ✓ | ⚠ | ⚠ | ✓ | ⚠ | ? | ✓ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ✓ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | +| [Citigate](connectors/citigate.md) | ✓ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | | [CryptoPay](connectors/cryptopay.md) | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | | [CyberSource](connectors/cybersource.md) | ✓ | x | ✓ | ✓ | x | ✓ | ✓ | x | x | x | x | x | x | x | x | x | x | x | ✓ | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | ✓ | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | | [Datatrans](connectors/datatrans.md) | ✓ | ⚠ | ? | ? | ⚠ | ? | ? | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | @@ -158,6 +159,7 @@ Consolidated view of Get, Void, Refund, Capture, Reverse, CreateOrder, and other | [CashtoCode](connectors/cashtocode.md) | ⚠ | x | x | x | x | ⚠ | x | ⚠ | ⚠ | x | ⚠ | x | ⚠ | ⚠ | x | ⚠ | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | ⚠ | x | x | ✓ | ✓ | x | | [Celero](connectors/celero.md) | ✓ | ✓ | ⚠ | ✓ | x | ✓ | x | ⚠ | ⚠ | ⚠ | ⚠ | ✓ | ⚠ | ⚠ | ⚠ | ✓ | x | ⚠ | ⚠ | ⚠ | x | x | x | x | x | x | x | x | ⚠ | ⚠ | ⚠ | x | ⚠ | x | x | ⚠ | ⚠ | x | | [Checkout.com](connectors/checkout.md) | ✓ | ✓ | x | ✓ | ⚠ | ✓ | ⚠ | ⚠ | ✓ | ⚠ | ⚠ | ✓ | ✓ | ✓ | x | ✓ | x | ⚠ | ⚠ | ⚠ | x | x | x | x | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | +| [Citigate](connectors/citigate.md) | ✓ | ✓ | ⚠ | ✓ | ⚠ | ✓ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ✓ | ⚠ | ⚠ | ⚠ | ✓ | x | ⚠ | ⚠ | ⚠ | x | x | x | x | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | | [CryptoPay](connectors/cryptopay.md) | ✓ | x | x | x | x | ⚠ | x | ⚠ | ⚠ | x | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | x | x | x | x | x | x | x | x | x | x | x | x | x | ⚠ | x | x | ✓ | ✓ | x | | [CyberSource](connectors/cybersource.md) | ✓ | ✓ | ✓ | ✓ | ⚠ | ✓ | ✓ | ⚠ | ? | ✓ | ? | ✓ | ? | ✓ | ✓ | ✓ | x | ⚠ | ⚠ | ⚠ | x | x | x | x | x | ⚠ | ⚠ | ? | ✓ | ✓ | ✓ | x | ⚠ | x | x | ⚠ | ⚠ | x | | [Datatrans](connectors/datatrans.md) | ✓ | ✓ | ✓ | ✓ | ⚠ | ✓ | ⚠ | ⚠ | ✓ | ✓ | ⚠ | ✓ | ✓ | ? | ⚠ | ✓ | x | ⚠ | ⚠ | ⚠ | x | x | x | x | x | ⚠ | ⚠ | ✓ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | diff --git a/docs-generated/connectors/citigate.md b/docs-generated/connectors/citigate.md new file mode 100644 index 0000000000..9687accf6e --- /dev/null +++ b/docs-generated/connectors/citigate.md @@ -0,0 +1,376 @@ +# Citigate + + + +## SDK Configuration + +Use this config for all flows in this connector. Replace `YOUR_API_KEY` with your actual credentials. + + + + + + + + + +
PythonJavaScriptKotlinRust
+ +
Python + +```python +from payments.generated import sdk_config_pb2, payment_pb2, payment_methods_pb2 + +config = sdk_config_pb2.ConnectorConfig( + options=sdk_config_pb2.SdkOptions(environment=sdk_config_pb2.Environment.SANDBOX), + connector_config=payment_pb2.ConnectorSpecificConfig( + citigate=payment_pb2.CitigateConfig( + api_key=payment_methods_pb2.SecretString(value="YOUR_API_KEY"), + key1=payment_methods_pb2.SecretString(value="YOUR_KEY1"), + base_url="YOUR_BASE_URL", + ), + ), +) + +``` + +
+ +
+ +
JavaScript + +```javascript +const { PaymentClient } = require('hyperswitch-prism'); +const { ConnectorConfig, Environment, Connector } = require('hyperswitch-prism').types; + +const config = ConnectorConfig.create({ + connector: Connector.CITIGATE, + environment: Environment.SANDBOX, + auth: { + citigate: { + apiKey: { value: 'YOUR_API_KEY' }, + key1: { value: 'YOUR_KEY1' }, + baseUrl: 'YOUR_BASE_URL', + } + }, +}); +``` + +
+ +
+ +
Kotlin + +```kotlin +val config = ConnectorConfig.newBuilder() + .setOptions(SdkOptions.newBuilder().setEnvironment(Environment.SANDBOX).build()) + .setConnectorConfig( + ConnectorSpecificConfig.newBuilder() + .setCitigate(CitigateConfig.newBuilder() + .setApiKey(SecretString.newBuilder().setValue("YOUR_API_KEY").build()) + .setKey1(SecretString.newBuilder().setValue("YOUR_KEY1").build()) + .setBaseUrl("YOUR_BASE_URL") + .build()) + .build() + ) + .build() +``` + +
+ +
+ +
Rust + +```rust +use grpc_api_types::payments::*; +use grpc_api_types::payments::connector_specific_config; + +let config = ConnectorConfig { + connector_config: Some(ConnectorSpecificConfig { + config: Some(connector_specific_config::Config::Citigate(CitigateConfig { + api_key: Some(hyperswitch_masking::Secret::new("YOUR_API_KEY".to_string())), // Authentication credential + key1: Some(hyperswitch_masking::Secret::new("YOUR_KEY1".to_string())), // Authentication credential + base_url: Some("https://sandbox.example.com".to_string()), // Base URL for API calls + ..Default::default() + })), + }), + options: Some(SdkOptions { + environment: Environment::Sandbox.into(), + }), +}; +``` + +
+ +
+ +## Integration Scenarios + +Complete, runnable examples for common integration patterns. Each example shows the full flow with status handling. Copy-paste into your app and replace placeholder values. + +### One-step Payment (Authorize + Capture) + +Simple payment that authorizes and captures in one call. Use for immediate charges. + +**Response status handling:** + +| Status | Recommended action | +|--------|-------------------| +| `AUTHORIZED` | Payment authorized and captured — funds will be settled automatically | +| `PENDING` | Payment processing — await webhook for final status before fulfilling | +| `FAILED` | Payment declined — surface error to customer, do not retry without new details | + +**Examples:** [Python](../../examples/citigate/citigate.py#L149) · [JavaScript](../../examples/citigate/citigate.js) · [Kotlin](../../examples/citigate/citigate.kt#L126) · [Rust](../../examples/citigate/citigate.rs#L185) + +### Card Payment (Authorize + Capture) + +Two-step card payment. First authorize, then capture. Use when you need to verify funds before finalizing. + +**Response status handling:** + +| Status | Recommended action | +|--------|-------------------| +| `AUTHORIZED` | Funds reserved — proceed to Capture to settle | +| `PENDING` | Awaiting async confirmation — wait for webhook before capturing | +| `FAILED` | Payment declined — surface error to customer, do not retry without new details | + +**Examples:** [Python](../../examples/citigate/citigate.py#L168) · [JavaScript](../../examples/citigate/citigate.js) · [Kotlin](../../examples/citigate/citigate.kt#L142) · [Rust](../../examples/citigate/citigate.rs#L201) + +### Refund + +Return funds to the customer for a completed payment. + +**Examples:** [Python](../../examples/citigate/citigate.py#L193) · [JavaScript](../../examples/citigate/citigate.js) · [Kotlin](../../examples/citigate/citigate.kt#L164) · [Rust](../../examples/citigate/citigate.rs#L224) + +### Void Payment + +Cancel an authorized but not-yet-captured payment. + +**Examples:** [Python](../../examples/citigate/citigate.py#L218) · [JavaScript](../../examples/citigate/citigate.js) · [Kotlin](../../examples/citigate/citigate.kt#L186) · [Rust](../../examples/citigate/citigate.rs#L247) + +### Get Payment Status + +Retrieve current payment status from the connector. + +**Examples:** [Python](../../examples/citigate/citigate.py#L240) · [JavaScript](../../examples/citigate/citigate.js) · [Kotlin](../../examples/citigate/citigate.kt#L205) · [Rust](../../examples/citigate/citigate.rs#L266) + +## API Reference + +| Flow (Service.RPC) | Category | gRPC Request Message | +|--------------------|----------|----------------------| +| [PaymentService.Authorize](#paymentserviceauthorize) | Payments | `PaymentServiceAuthorizeRequest` | +| [PaymentService.Capture](#paymentservicecapture) | Payments | `PaymentServiceCaptureRequest` | +| [PaymentService.Get](#paymentserviceget) | Payments | `PaymentServiceGetRequest` | +| [PaymentService.ProxyAuthorize](#paymentserviceproxyauthorize) | Payments | `PaymentServiceProxyAuthorizeRequest` | +| [PaymentService.Refund](#paymentservicerefund) | Payments | `PaymentServiceRefundRequest` | +| [RefundService.Get](#refundserviceget) | Refunds | `RefundServiceGetRequest` | +| [PaymentService.Void](#paymentservicevoid) | Payments | `PaymentServiceVoidRequest` | + +### Payments + +#### PaymentService.Authorize + +Authorize a payment amount on a payment method. This reserves funds without capturing them, essential for verifying availability before finalizing. + +| | Message | +|---|---------| +| **Request** | `PaymentServiceAuthorizeRequest` | +| **Response** | `PaymentServiceAuthorizeResponse` | + +**Supported payment method types:** + +| Payment Method | Supported | +|----------------|:---------:| +| Card | ✓ | +| Bancontact | ⚠ | +| Apple Pay | ⚠ | +| Apple Pay Dec | ⚠ | +| Apple Pay SDK | ⚠ | +| Google Pay | ⚠ | +| Google Pay Dec | ⚠ | +| Google Pay SDK | ⚠ | +| PayPal SDK | ⚠ | +| Amazon Pay | ⚠ | +| Cash App | ⚠ | +| PayPal | ⚠ | +| WeChat Pay | ⚠ | +| Alipay | ⚠ | +| Revolut Pay | ⚠ | +| MiFinity | ⚠ | +| Bluecode | ⚠ | +| Paze | x | +| Samsung Pay | ⚠ | +| MB Way | ⚠ | +| Satispay | ⚠ | +| Wero | ⚠ | +| GoPay | ⚠ | +| GCash | ⚠ | +| Momo | ⚠ | +| Dana | ⚠ | +| Kakao Pay | ⚠ | +| Touch 'n Go | ⚠ | +| Twint | ⚠ | +| Vipps | ⚠ | +| Swish | ⚠ | +| Affirm | ⚠ | +| Afterpay | ⚠ | +| Klarna | ⚠ | +| UPI Collect | ⚠ | +| UPI Intent | ⚠ | +| UPI QR | ⚠ | +| Thailand | ⚠ | +| Czech | ⚠ | +| Finland | ⚠ | +| FPX | ⚠ | +| Poland | ⚠ | +| Slovakia | ⚠ | +| UK | ⚠ | +| PIS | x | +| Generic | ⚠ | +| Local | ⚠ | +| iDEAL | ⚠ | +| Sofort | ⚠ | +| Trustly | ⚠ | +| Giropay | ⚠ | +| EPS | ⚠ | +| Przelewy24 | ⚠ | +| PSE | ⚠ | +| BLIK | ⚠ | +| Interac | ⚠ | +| Bizum | ⚠ | +| EFT | ⚠ | +| DuitNow | x | +| ACH | ⚠ | +| SEPA | ⚠ | +| BACS | ⚠ | +| Multibanco | ⚠ | +| Instant | ⚠ | +| Instant FI | ⚠ | +| Instant PL | ⚠ | +| Pix | ⚠ | +| Permata | ⚠ | +| BCA | ⚠ | +| BNI VA | ⚠ | +| BRI VA | ⚠ | +| CIMB VA | ⚠ | +| Danamon VA | ⚠ | +| Mandiri VA | ⚠ | +| Local | ⚠ | +| Indonesian | ⚠ | +| ACH | ⚠ | +| SEPA | ⚠ | +| BACS | ⚠ | +| BECS | ⚠ | +| SEPA Guaranteed | ⚠ | +| Crypto | x | +| Reward | ⚠ | +| Givex | x | +| PaySafeCard | ⚠ | +| E-Voucher | ⚠ | +| Boleto | ⚠ | +| Efecty | ⚠ | +| Pago Efectivo | ⚠ | +| Red Compra | ⚠ | +| Red Pagos | ⚠ | +| Alfamart | ⚠ | +| Indomaret | ⚠ | +| Oxxo | ⚠ | +| 7-Eleven | ⚠ | +| Lawson | ⚠ | +| Mini Stop | ⚠ | +| Family Mart | ⚠ | +| Seicomart | ⚠ | +| Pay Easy | ⚠ | + +**Payment method objects** — use these in the `payment_method` field of the Authorize request. + +##### Card (Raw PAN) + +```python +"payment_method": { + "card": { + "card_number": "4111111111111111", + "card_exp_month": "03", + "card_exp_year": "2030", + "card_cvc": "737", + "card_holder_name": "John Doe" + } +} +``` + +**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts#L271) · [Kotlin](../../examples/citigate/citigate.kt#L223) · [Rust](../../examples/citigate/citigate.rs) + +#### PaymentService.Capture + +Finalize an authorized payment by transferring funds. Captures the authorized amount to complete the transaction and move funds to your merchant account. + +| | Message | +|---|---------| +| **Request** | `PaymentServiceCaptureRequest` | +| **Response** | `PaymentServiceCaptureResponse` | + +**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts#L280) · [Kotlin](../../examples/citigate/citigate.kt#L235) · [Rust](../../examples/citigate/citigate.rs) + +#### PaymentService.Get + +Retrieve current payment status from the payment processor. Enables synchronization between your system and payment processors for accurate state tracking. + +| | Message | +|---|---------| +| **Request** | `PaymentServiceGetRequest` | +| **Response** | `PaymentServiceGetResponse` | + +**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts#L289) · [Kotlin](../../examples/citigate/citigate.kt#L245) · [Rust](../../examples/citigate/citigate.rs) + +#### PaymentService.ProxyAuthorize + +Authorize using vault-aliased card data. Proxy substitutes before connector. + +| | Message | +|---|---------| +| **Request** | `PaymentServiceProxyAuthorizeRequest` | +| **Response** | `PaymentServiceAuthorizeResponse` | + +**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts#L298) · [Kotlin](../../examples/citigate/citigate.kt#L253) · [Rust](../../examples/citigate/citigate.rs) + +#### PaymentService.Refund + +Process a partial or full refund for a captured payment. Returns funds to the customer when goods are returned or services are cancelled. + +| | Message | +|---|---------| +| **Request** | `PaymentServiceRefundRequest` | +| **Response** | `RefundResponse` | + +**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts#L307) · [Kotlin](../../examples/citigate/citigate.kt#L295) · [Rust](../../examples/citigate/citigate.rs) + +#### PaymentService.Void + +Cancel an authorized payment that has not been captured. Releases held funds back to the customer's payment method when a transaction cannot be completed. + +| | Message | +|---|---------| +| **Request** | `PaymentServiceVoidRequest` | +| **Response** | `PaymentServiceVoidResponse` | + +**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts) · [Kotlin](../../examples/citigate/citigate.kt#L317) · [Rust](../../examples/citigate/citigate.rs) + +### Refunds + +#### RefundService.Get + +Retrieve refund status from the payment processor. Tracks refund progress through processor settlement for accurate customer communication. + +| | Message | +|---|---------| +| **Request** | `RefundServiceGetRequest` | +| **Response** | `RefundResponse` | + +**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts#L316) · [Kotlin](../../examples/citigate/citigate.kt#L305) · [Rust](../../examples/citigate/citigate.rs) diff --git a/docs-generated/llms.txt b/docs-generated/llms.txt index 8a88682a0b..8a3b313207 100644 --- a/docs-generated/llms.txt +++ b/docs-generated/llms.txt @@ -1,5 +1,5 @@ # Connector Service — LLM Navigation Index -# Connectors: 105 +# Connectors: 106 # # This file helps AI coding assistants navigate connector-service documentation. # Each connector block lists: doc path, scenarios, supported payment methods, @@ -8,7 +8,7 @@ # Usage: fetch this file first, then fetch the specific connector doc or example. overview: - total_connectors: 105 + total_connectors: 106 docs_root: docs-generated/connectors/ examples_root: examples/ all_connectors_matrix: docs-generated/all_connector.md @@ -190,6 +190,14 @@ payment_methods: Ach, ApplePayDecrypted, Card, GooglePayDecrypted flows: authorize, capture, get, proxy_authorize, proxy_setup_recurring, recurring_charge, refund, refund_get, setup_recurring, void examples_python: examples/checkout/checkout.py +## Citigate +connector_id: citigate +doc: docs/connectors/citigate.md +scenarios: checkout_autocapture, checkout_card, refund, void_payment, get_payment +payment_methods: Card +flows: authorize, capture, get, proxy_authorize, refund, refund_get, void +examples_python: examples/citigate/citigate.py + ## CryptoPay connector_id: cryptopay doc: docs/connectors/cryptopay.md diff --git a/examples/citigate/citigate.kt b/examples/citigate/citigate.kt new file mode 100644 index 0000000000..b8158a1b0d --- /dev/null +++ b/examples/citigate/citigate.kt @@ -0,0 +1,345 @@ +// This file is auto-generated. Do not edit manually. +// Replace YOUR_API_KEY and placeholder values with real data. +// Regenerate: python3 scripts/generate-connector-docs.py citigate +// +// Citigate — all scenarios and flows in one file. +// Run a scenario: ./gradlew run --args="citigate processCheckoutCard" + +package examples.citigate + +import types.Payment.* +import types.PaymentMethods.* +import payments.PaymentClient +import payments.RefundClient +import payments.AuthenticationType +import payments.CaptureMethod +import payments.CardNetwork +import payments.CountryAlpha2 +import payments.Currency +import payments.ConnectorConfig +import payments.SdkOptions +import payments.Environment +import payments.ConnectorSpecificConfig +import types.Payment.CitigateConfig +import payments.SecretString + +val SUPPORTED_FLOWS = listOf("authorize", "capture", "get", "proxy_authorize", "refund", "refund_get", "void") + +val _defaultConfig: ConnectorConfig = ConnectorConfig.newBuilder() + .setOptions(SdkOptions.newBuilder().setEnvironment(Environment.SANDBOX).build()) + .setConnectorConfig( + ConnectorSpecificConfig.newBuilder() + .setCitigate(CitigateConfig.newBuilder() + .setApiKey(SecretString.newBuilder().setValue("YOUR_API_KEY").build()) + .setKey1(SecretString.newBuilder().setValue("YOUR_KEY1").build()) + .setBaseUrl("YOUR_BASE_URL") + .build()) + .build() + ) + .build() + + + +private fun buildAuthorizeRequest(captureMethodStr: String): PaymentServiceAuthorizeRequest { + return PaymentServiceAuthorizeRequest.newBuilder().apply { + merchantTransactionId = "probe_txn_001" // Identification. + amountBuilder.apply { // The amount for the payment. + minorAmount = 1000L // Amount in minor units (e.g., 1000 = $10.00). + currency = Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + } + paymentMethodBuilder.apply { // Payment method to be used. + cardBuilder.apply { // Generic card payment. + cardNumberBuilder.value = "4111111111111111" // Card Identification. + cardExpMonthBuilder.value = "03" + cardExpYearBuilder.value = "2030" + cardCvcBuilder.value = "737" + cardHolderNameBuilder.value = "John Doe" // Cardholder Information. + } + } + captureMethod = CaptureMethod.valueOf(captureMethodStr) // Method for capturing the payment. + addressBuilder.apply { // Address Information. + billingAddressBuilder.apply { + firstNameBuilder.value = "John" // Personal Information. + lastNameBuilder.value = "Doe" + line1Builder.value = "123 Main St" // Address Details. + cityBuilder.value = "Seattle" + stateBuilder.value = "WA" + zipCodeBuilder.value = "98101" + countryAlpha2Code = CountryAlpha2.US + emailBuilder.value = "test@example.com" // Contact Information. + phoneNumberBuilder.value = "4155552671" + phoneCountryCode = "+1" + } + } + authType = AuthenticationType.NO_THREE_DS // Authentication Details. + returnUrl = "https://example.com/return" // URLs for Redirection and Webhooks. + browserInfoBuilder.apply { + ipAddress = "1.2.3.4" // Device Information. + } + }.build() +} + +private fun buildCaptureRequest(connectorTransactionIdStr: String): PaymentServiceCaptureRequest { + return PaymentServiceCaptureRequest.newBuilder().apply { + merchantCaptureId = "probe_capture_001" // Identification. + connectorTransactionId = connectorTransactionIdStr + amountToCaptureBuilder.apply { // Capture Details. + minorAmount = 1000L // Amount in minor units (e.g., 1000 = $10.00). + currency = Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + } + }.build() +} + +private fun buildGetRequest(connectorTransactionIdStr: String): PaymentServiceGetRequest { + return PaymentServiceGetRequest.newBuilder().apply { + merchantTransactionId = "probe_merchant_txn_001" // Identification. + connectorTransactionId = connectorTransactionIdStr + amountBuilder.apply { // Amount Information. + minorAmount = 1000L // Amount in minor units (e.g., 1000 = $10.00). + currency = Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + } + }.build() +} + +private fun buildRefundRequest(connectorTransactionIdStr: String): PaymentServiceRefundRequest { + return PaymentServiceRefundRequest.newBuilder().apply { + merchantRefundId = "probe_refund_001" // Identification. + connectorTransactionId = connectorTransactionIdStr + paymentAmount = 1000L // Amount Information. + refundAmountBuilder.apply { + minorAmount = 1000L // Amount in minor units (e.g., 1000 = $10.00). + currency = Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + } + reason = "customer_request" // Reason for the refund. + }.build() +} + +private fun buildVoidRequest(connectorTransactionIdStr: String): PaymentServiceVoidRequest { + return PaymentServiceVoidRequest.newBuilder().apply { + merchantVoidId = "probe_void_001" // Identification. + connectorTransactionId = connectorTransactionIdStr + }.build() +} + +// Scenario: One-step Payment (Authorize + Capture) +// Simple payment that authorizes and captures in one call. Use for immediate charges. +fun processCheckoutAutocapture(txnId: String, config: ConnectorConfig = _defaultConfig): Map { + val paymentClient = PaymentClient(config) + + // Step 1: Authorize — reserve funds on the payment method + val authorizeResponse = paymentClient.authorize(buildAuthorizeRequest("AUTOMATIC")) + + when (authorizeResponse.status.name) { + "FAILED" -> throw RuntimeException("Payment failed: ${authorizeResponse.error.unifiedDetails.message}") + "PENDING" -> return mapOf("status" to "PENDING") // await webhook before proceeding + } + + return mapOf("status" to authorizeResponse.status.name, "transactionId" to authorizeResponse.connectorTransactionId, "error" to authorizeResponse.error) +} + +// Scenario: Card Payment (Authorize + Capture) +// Two-step card payment. First authorize, then capture. Use when you need to verify funds before finalizing. +fun processCheckoutCard(txnId: String, config: ConnectorConfig = _defaultConfig): Map { + val paymentClient = PaymentClient(config) + + // Step 1: Authorize — reserve funds on the payment method + val authorizeResponse = paymentClient.authorize(buildAuthorizeRequest("MANUAL")) + + when (authorizeResponse.status.name) { + "FAILED" -> throw RuntimeException("Payment failed: ${authorizeResponse.error.unifiedDetails.message}") + "PENDING" -> return mapOf("status" to "PENDING") // await webhook before proceeding + } + + // Step 2: Capture — settle the reserved funds + val captureResponse = paymentClient.capture(buildCaptureRequest(authorizeResponse.connectorTransactionId ?: "")) + + if (captureResponse.status.name == "FAILED") + throw RuntimeException("Capture failed: ${captureResponse.error.unifiedDetails.message}") + + return mapOf("status" to captureResponse.status.name, "transactionId" to authorizeResponse.connectorTransactionId, "error" to authorizeResponse.error) +} + +// Scenario: Refund +// Return funds to the customer for a completed payment. +fun processRefund(txnId: String, config: ConnectorConfig = _defaultConfig): Map { + val paymentClient = PaymentClient(config) + + // Step 1: Authorize — reserve funds on the payment method + val authorizeResponse = paymentClient.authorize(buildAuthorizeRequest("AUTOMATIC")) + + when (authorizeResponse.status.name) { + "FAILED" -> throw RuntimeException("Payment failed: ${authorizeResponse.error.unifiedDetails.message}") + "PENDING" -> return mapOf("status" to "PENDING") // await webhook before proceeding + } + + // Step 2: Refund — return funds to the customer + val refundResponse = paymentClient.refund(buildRefundRequest(authorizeResponse.connectorTransactionId ?: "")) + + if (refundResponse.status.name == "FAILED") + throw RuntimeException("Refund failed: ${refundResponse.error.unifiedDetails.message}") + + return mapOf("status" to refundResponse.status.name, "error" to refundResponse.error) +} + +// Scenario: Void Payment +// Cancel an authorized but not-yet-captured payment. +fun processVoidPayment(txnId: String, config: ConnectorConfig = _defaultConfig): Map { + val paymentClient = PaymentClient(config) + + // Step 1: Authorize — reserve funds on the payment method + val authorizeResponse = paymentClient.authorize(buildAuthorizeRequest("MANUAL")) + + when (authorizeResponse.status.name) { + "FAILED" -> throw RuntimeException("Payment failed: ${authorizeResponse.error.unifiedDetails.message}") + "PENDING" -> return mapOf("status" to "PENDING") // await webhook before proceeding + } + + // Step 2: Void — release reserved funds (cancel authorization) + val voidResponse = paymentClient.void(buildVoidRequest(authorizeResponse.connectorTransactionId ?: "")) + + return mapOf("status" to voidResponse.status.name, "transactionId" to authorizeResponse.connectorTransactionId, "error" to voidResponse.error) +} + +// Scenario: Get Payment Status +// Retrieve current payment status from the connector. +fun processGetPayment(txnId: String, config: ConnectorConfig = _defaultConfig): Map { + val paymentClient = PaymentClient(config) + + // Step 1: Authorize — reserve funds on the payment method + val authorizeResponse = paymentClient.authorize(buildAuthorizeRequest("MANUAL")) + + when (authorizeResponse.status.name) { + "FAILED" -> throw RuntimeException("Payment failed: ${authorizeResponse.error.unifiedDetails.message}") + "PENDING" -> return mapOf("status" to "PENDING") // await webhook before proceeding + } + + // Step 2: Get — retrieve current payment status from the connector + val getResponse = paymentClient.get(buildGetRequest(authorizeResponse.connectorTransactionId ?: "")) + + return mapOf("status" to getResponse.status.name, "transactionId" to getResponse.connectorTransactionId, "error" to getResponse.error) +} + +// Flow: PaymentService.Authorize (Card) +fun authorize(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = PaymentClient(config) + val request = buildAuthorizeRequest("AUTOMATIC") + val response = client.authorize(request) + when (response.status.name) { + "FAILED" -> throw RuntimeException("Authorize failed: ${response.error.unifiedDetails.message}") + "PENDING" -> println("Pending — await webhook before proceeding") + else -> println("Authorized: ${response.connectorTransactionId}") + } +} + +// Flow: PaymentService.Capture +fun capture(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = PaymentClient(config) + val request = buildCaptureRequest("probe_connector_txn_001") + val response = client.capture(request) + if (response.status.name == "FAILED") + throw RuntimeException("Capture failed: ${response.error.unifiedDetails.message}") + println("Done: ${response.status.name}") +} + +// Flow: PaymentService.Get +fun get(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = PaymentClient(config) + val request = buildGetRequest("probe_connector_txn_001") + val response = client.get(request) + println("Status: ${response.status.name}") +} + +// Flow: PaymentService.ProxyAuthorize +fun proxyAuthorize(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = PaymentClient(config) + val request = PaymentServiceProxyAuthorizeRequest.newBuilder().apply { + merchantTransactionId = "probe_proxy_txn_001" + amountBuilder.apply { + minorAmount = 1000L // Amount in minor units (e.g., 1000 = $10.00). + currency = Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + } + cardProxyBuilder.apply { // Card proxy for vault-aliased payments (VGS, Basis Theory, Spreedly). Real card values are substituted by the proxy before reaching the connector. + cardNumberBuilder.value = "4111111111111111" // Card Identification. + cardExpMonthBuilder.value = "03" + cardExpYearBuilder.value = "2030" + cardCvcBuilder.value = "123" + cardHolderNameBuilder.value = "John Doe" // Cardholder Information. + cardNetwork = CardNetwork.VISA + } + addressBuilder.apply { + billingAddressBuilder.apply { + firstNameBuilder.value = "John" // Personal Information. + lastNameBuilder.value = "Doe" + line1Builder.value = "123 Main St" // Address Details. + cityBuilder.value = "Seattle" + stateBuilder.value = "WA" + zipCodeBuilder.value = "98101" + countryAlpha2Code = CountryAlpha2.US + emailBuilder.value = "test@example.com" // Contact Information. + phoneNumberBuilder.value = "4155552671" + phoneCountryCode = "+1" + } + } + captureMethod = CaptureMethod.AUTOMATIC + authType = AuthenticationType.NO_THREE_DS + returnUrl = "https://example.com/return" + browserInfoBuilder.apply { + ipAddress = "1.2.3.4" // Device Information. + } + }.build() + val response = client.proxy_authorize(request) + println("Status: ${response.status.name}") +} + +// Flow: PaymentService.Refund +fun refund(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = PaymentClient(config) + val request = buildRefundRequest("probe_connector_txn_001") + val response = client.refund(request) + if (response.status.name == "FAILED") + throw RuntimeException("Refund failed: ${response.error.unifiedDetails.message}") + println("Done: ${response.status.name}") +} + +// Flow: RefundService.Get +fun refundGet(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = RefundClient(config) + val request = RefundServiceGetRequest.newBuilder().apply { + merchantRefundId = "probe_refund_001" // Identification. + connectorTransactionId = "probe_connector_txn_001" + refundId = "probe_refund_id_001" // Deprecated. + }.build() + val response = client.refund_get(request) + println("Status: ${response.status.name}") +} + +// Flow: PaymentService.Void +fun void(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = PaymentClient(config) + val request = buildVoidRequest("probe_connector_txn_001") + val response = client.void(request) + if (response.status.name == "FAILED") + throw RuntimeException("Void failed: ${response.error.unifiedDetails.message}") + println("Done: ${response.status.name}") +} + + +fun main(args: Array) { + val txnId = "order_001" + val flow = args.firstOrNull() ?: "processCheckoutAutocapture" + when (flow) { + "processCheckoutAutocapture" -> processCheckoutAutocapture(txnId) + "processCheckoutCard" -> processCheckoutCard(txnId) + "processRefund" -> processRefund(txnId) + "processVoidPayment" -> processVoidPayment(txnId) + "processGetPayment" -> processGetPayment(txnId) + "authorize" -> authorize(txnId) + "capture" -> capture(txnId) + "get" -> get(txnId) + "proxyAuthorize" -> proxyAuthorize(txnId) + "refund" -> refund(txnId) + "refundGet" -> refundGet(txnId) + "void" -> void(txnId) + else -> System.err.println("Unknown flow: $flow. Available: processCheckoutAutocapture, processCheckoutCard, processRefund, processVoidPayment, processGetPayment, authorize, capture, get, proxyAuthorize, refund, refundGet, void") + } +} diff --git a/examples/citigate/citigate.py b/examples/citigate/citigate.py new file mode 100644 index 0000000000..fd81ec7976 --- /dev/null +++ b/examples/citigate/citigate.py @@ -0,0 +1,322 @@ +# This file is auto-generated. Do not edit manually. +# Replace YOUR_API_KEY and placeholder values with real data. +# Regenerate: python3 scripts/generate-connector-docs.py citigate +# +# Citigate — all integration scenarios and flows in one file. +# Run a scenario: python3 citigate.py checkout_card + +import asyncio +import sys +from payments import PaymentClient +from payments import RefundClient +from payments.generated import sdk_config_pb2, payment_pb2, payment_methods_pb2 + +SUPPORTED_FLOWS = ["authorize", "capture", "get", "proxy_authorize", "refund", "refund_get", "void"] + +_default_config = sdk_config_pb2.ConnectorConfig( + options=sdk_config_pb2.SdkOptions(environment=sdk_config_pb2.Environment.SANDBOX), + connector_config=payment_pb2.ConnectorSpecificConfig( + citigate=payment_pb2.CitigateConfig( + api_key=payment_methods_pb2.SecretString(value="YOUR_API_KEY"), + key1=payment_methods_pb2.SecretString(value="YOUR_KEY1"), + base_url="YOUR_BASE_URL", + ), + ), +) + + + + +def _build_authorize_request(capture_method: str): + return payment_pb2.PaymentServiceAuthorizeRequest( + merchant_transaction_id="probe_txn_001", # Identification. + amount=payment_pb2.Money( # The amount for the payment. + minor_amount=1000, # Amount in minor units (e.g., 1000 = $10.00). + currency=payment_pb2.Currency.Value("USD"), # ISO 4217 currency code (e.g., "USD", "EUR"). + ), + payment_method=payment_methods_pb2.PaymentMethod( # Payment method to be used. + card=payment_methods_pb2.CardDetails( + card_number=payment_methods_pb2.CardNumberType(value="4111111111111111"), # Card Identification. + card_exp_month=payment_methods_pb2.SecretString(value="03"), + card_exp_year=payment_methods_pb2.SecretString(value="2030"), + card_cvc=payment_methods_pb2.SecretString(value="737"), + card_holder_name=payment_methods_pb2.SecretString(value="John Doe"), # Cardholder Information. + ), + ), + capture_method=payment_pb2.CaptureMethod.Value(capture_method), # Method for capturing the payment. + address=payment_pb2.PaymentAddress( # Address Information. + billing_address=payment_pb2.Address( + first_name=payment_methods_pb2.SecretString(value="John"), # Personal Information. + last_name=payment_methods_pb2.SecretString(value="Doe"), + line1=payment_methods_pb2.SecretString(value="123 Main St"), # Address Details. + city=payment_methods_pb2.SecretString(value="Seattle"), + state=payment_methods_pb2.SecretString(value="WA"), + zip_code=payment_methods_pb2.SecretString(value="98101"), + country_alpha2_code=payment_methods_pb2.CountryAlpha2.Value("US"), + email=payment_methods_pb2.SecretString(value="test@example.com"), # Contact Information. + phone_number=payment_methods_pb2.SecretString(value="4155552671"), + phone_country_code="+1", + ), + ), + auth_type=payment_pb2.AuthenticationType.Value("NO_THREE_DS"), # Authentication Details. + return_url="https://example.com/return", # URLs for Redirection and Webhooks. + browser_info=payment_pb2.BrowserInformation( + ip_address="1.2.3.4", # Device Information. + ), + ) + +def _build_capture_request(connector_transaction_id: str): + return payment_pb2.PaymentServiceCaptureRequest( + merchant_capture_id="probe_capture_001", # Identification. + connector_transaction_id=connector_transaction_id, + amount_to_capture=payment_pb2.Money( # Capture Details. + minor_amount=1000, # Amount in minor units (e.g., 1000 = $10.00). + currency=payment_pb2.Currency.Value("USD"), # ISO 4217 currency code (e.g., "USD", "EUR"). + ), + ) + +def _build_get_request(connector_transaction_id: str): + return payment_pb2.PaymentServiceGetRequest( + merchant_transaction_id="probe_merchant_txn_001", # Identification. + connector_transaction_id=connector_transaction_id, + amount=payment_pb2.Money( # Amount Information. + minor_amount=1000, # Amount in minor units (e.g., 1000 = $10.00). + currency=payment_pb2.Currency.Value("USD"), # ISO 4217 currency code (e.g., "USD", "EUR"). + ), + ) + +def _build_proxy_authorize_request(): + return payment_pb2.PaymentServiceProxyAuthorizeRequest( + merchant_transaction_id="probe_proxy_txn_001", + amount=payment_pb2.Money( + minor_amount=1000, # Amount in minor units (e.g., 1000 = $10.00). + currency=payment_pb2.Currency.Value("USD"), # ISO 4217 currency code (e.g., "USD", "EUR"). + ), + card_proxy=payment_methods_pb2.ProxyCardDetails( # Card proxy for vault-aliased payments (VGS, Basis Theory, Spreedly). Real card values are substituted by the proxy before reaching the connector. + card_number=payment_methods_pb2.SecretString(value="4111111111111111"), # Card Identification. + card_exp_month=payment_methods_pb2.SecretString(value="03"), + card_exp_year=payment_methods_pb2.SecretString(value="2030"), + card_cvc=payment_methods_pb2.SecretString(value="123"), + card_holder_name=payment_methods_pb2.SecretString(value="John Doe"), # Cardholder Information. + card_network=payment_methods_pb2.CardNetwork.Value("VISA"), + ), + address=payment_pb2.PaymentAddress( + billing_address=payment_pb2.Address( + first_name=payment_methods_pb2.SecretString(value="John"), # Personal Information. + last_name=payment_methods_pb2.SecretString(value="Doe"), + line1=payment_methods_pb2.SecretString(value="123 Main St"), # Address Details. + city=payment_methods_pb2.SecretString(value="Seattle"), + state=payment_methods_pb2.SecretString(value="WA"), + zip_code=payment_methods_pb2.SecretString(value="98101"), + country_alpha2_code=payment_methods_pb2.CountryAlpha2.Value("US"), + email=payment_methods_pb2.SecretString(value="test@example.com"), # Contact Information. + phone_number=payment_methods_pb2.SecretString(value="4155552671"), + phone_country_code="+1", + ), + ), + capture_method=payment_pb2.CaptureMethod.Value("AUTOMATIC"), + auth_type=payment_pb2.AuthenticationType.Value("NO_THREE_DS"), + return_url="https://example.com/return", + browser_info=payment_pb2.BrowserInformation( + ip_address="1.2.3.4", # Device Information. + ), + ) + +def _build_refund_request(connector_transaction_id: str): + return payment_pb2.PaymentServiceRefundRequest( + merchant_refund_id="probe_refund_001", # Identification. + connector_transaction_id=connector_transaction_id, + payment_amount=1000, # Amount Information. + refund_amount=payment_pb2.Money( + minor_amount=1000, # Amount in minor units (e.g., 1000 = $10.00). + currency=payment_pb2.Currency.Value("USD"), # ISO 4217 currency code (e.g., "USD", "EUR"). + ), + reason="customer_request", # Reason for the refund. + ) + +def _build_refund_get_request(): + return payment_pb2.RefundServiceGetRequest( + merchant_refund_id="probe_refund_001", # Identification. + connector_transaction_id="probe_connector_txn_001", + refund_id="probe_refund_id_001", # Deprecated. + ) + +def _build_void_request(connector_transaction_id: str): + return payment_pb2.PaymentServiceVoidRequest( + merchant_void_id="probe_void_001", # Identification. + connector_transaction_id=connector_transaction_id, + ) +async def process_checkout_autocapture(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """One-step Payment (Authorize + Capture) + + Simple payment that authorizes and captures in one call. Use for immediate charges. + """ + payment_client = PaymentClient(config) + + # Step 1: Authorize — reserve funds on the payment method + authorize_response = await payment_client.authorize(_build_authorize_request("AUTOMATIC")) + + if authorize_response.status == "FAILED": + raise RuntimeError(f"Payment failed: {authorize_response.error}") + if authorize_response.status == "PENDING": + # Awaiting async confirmation — handle via webhook + return {"status": "pending", "transaction_id": authorize_response.connector_transaction_id} + + return {"status": getattr(authorize_response, "status", ""), "transaction_id": getattr(authorize_response, "connector_transaction_id", ""), "error": getattr(authorize_response, "error", None)} + + +async def process_checkout_card(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Card Payment (Authorize + Capture) + + Two-step card payment. First authorize, then capture. Use when you need to verify funds before finalizing. + """ + payment_client = PaymentClient(config) + + # Step 1: Authorize — reserve funds on the payment method + authorize_response = await payment_client.authorize(_build_authorize_request("MANUAL")) + + if authorize_response.status == "FAILED": + raise RuntimeError(f"Payment failed: {authorize_response.error}") + if authorize_response.status == "PENDING": + # Awaiting async confirmation — handle via webhook + return {"status": "pending", "transaction_id": authorize_response.connector_transaction_id} + + # Step 2: Capture — settle the reserved funds + capture_response = await payment_client.capture(_build_capture_request(authorize_response.connector_transaction_id)) + + if capture_response.status == "FAILED": + raise RuntimeError(f"Capture failed: {capture_response.error}") + + return {"status": getattr(capture_response, "status", ""), "transaction_id": getattr(authorize_response, "connector_transaction_id", ""), "error": getattr(capture_response, "error", None)} + + +async def process_refund(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Refund + + Return funds to the customer for a completed payment. + """ + payment_client = PaymentClient(config) + + # Step 1: Authorize — reserve funds on the payment method + authorize_response = await payment_client.authorize(_build_authorize_request("AUTOMATIC")) + + if authorize_response.status == "FAILED": + raise RuntimeError(f"Payment failed: {authorize_response.error}") + if authorize_response.status == "PENDING": + # Awaiting async confirmation — handle via webhook + return {"status": "pending", "transaction_id": authorize_response.connector_transaction_id} + + # Step 2: Refund — return funds to the customer + refund_response = await payment_client.refund(_build_refund_request(authorize_response.connector_transaction_id)) + + if refund_response.status == "FAILED": + raise RuntimeError(f"Refund failed: {refund_response.error}") + + return {"status": getattr(refund_response, "status", ""), "error": getattr(refund_response, "error", None)} + + +async def process_void_payment(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Void Payment + + Cancel an authorized but not-yet-captured payment. + """ + payment_client = PaymentClient(config) + + # Step 1: Authorize — reserve funds on the payment method + authorize_response = await payment_client.authorize(_build_authorize_request("MANUAL")) + + if authorize_response.status == "FAILED": + raise RuntimeError(f"Payment failed: {authorize_response.error}") + if authorize_response.status == "PENDING": + # Awaiting async confirmation — handle via webhook + return {"status": "pending", "transaction_id": authorize_response.connector_transaction_id} + + # Step 2: Void — release reserved funds (cancel authorization) + void_response = await payment_client.void(_build_void_request(authorize_response.connector_transaction_id)) + + return {"status": getattr(void_response, "status", ""), "transaction_id": getattr(authorize_response, "connector_transaction_id", ""), "error": getattr(void_response, "error", None)} + + +async def process_get_payment(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Get Payment Status + + Retrieve current payment status from the connector. + """ + payment_client = PaymentClient(config) + + # Step 1: Authorize — reserve funds on the payment method + authorize_response = await payment_client.authorize(_build_authorize_request("MANUAL")) + + if authorize_response.status == "FAILED": + raise RuntimeError(f"Payment failed: {authorize_response.error}") + if authorize_response.status == "PENDING": + # Awaiting async confirmation — handle via webhook + return {"status": "pending", "transaction_id": authorize_response.connector_transaction_id} + + # Step 2: Get — retrieve current payment status from the connector + get_response = await payment_client.get(_build_get_request(authorize_response.connector_transaction_id)) + + return {"status": getattr(get_response, "status", ""), "transaction_id": getattr(get_response, "connector_transaction_id", ""), "error": getattr(get_response, "error", None)} + + +async def process_authorize(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Flow: PaymentService.Authorize (Card)""" + payment_client = PaymentClient(config) + + authorize_response = await payment_client.authorize(_build_authorize_request("AUTOMATIC")) + + return {"status": authorize_response.status, "transaction_id": authorize_response.connector_transaction_id} + + +async def process_capture(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Flow: PaymentService.Capture""" + payment_client = PaymentClient(config) + + capture_response = await payment_client.capture(_build_capture_request("probe_connector_txn_001")) + + return {"status": capture_response.status} + + +async def process_get(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Flow: PaymentService.Get""" + payment_client = PaymentClient(config) + + get_response = await payment_client.get(_build_get_request("probe_connector_txn_001")) + + return {"status": get_response.status} + + +async def process_proxy_authorize(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Flow: PaymentService.ProxyAuthorize""" + payment_client = PaymentClient(config) + + proxy_response = await payment_client.proxy_authorize(_build_proxy_authorize_request()) + + return {"status": proxy_response.status} + + +async def process_refund_get(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Flow: RefundService.Get""" + refund_client = RefundClient(config) + + refund_response = await refund_client.refund_get(_build_refund_get_request()) + + return {"status": refund_response.status} + + +async def process_void(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Flow: PaymentService.Void""" + payment_client = PaymentClient(config) + + void_response = await payment_client.void(_build_void_request("probe_connector_txn_001")) + + return {"status": void_response.status} + +if __name__ == "__main__": + scenario = sys.argv[1] if len(sys.argv) > 1 else "checkout_autocapture" + fn = globals().get(f"process_{scenario}") + if not fn: + available = [k[8:] for k in globals() if k.startswith("process_")] + print(f"Unknown scenario: {scenario}. Available: {available}", file=sys.stderr) + sys.exit(1) + asyncio.run(fn("order_001")) diff --git a/examples/citigate/citigate.rs b/examples/citigate/citigate.rs new file mode 100644 index 0000000000..22751a8f2a --- /dev/null +++ b/examples/citigate/citigate.rs @@ -0,0 +1,514 @@ +// This file is auto-generated. Do not edit manually. +// Replace YOUR_API_KEY and placeholder values with real data. +// Regenerate: python3 scripts/generate-connector-docs.py citigate +// +// Citigate — all scenarios and flows in one file. +// Run a scenario: cargo run --example citigate -- process_checkout_card +use cards::CardNumber; +use grpc_api_types::payments::connector_specific_config; +use grpc_api_types::payments::payment_method; +use grpc_api_types::payments::*; +use hyperswitch_masking::Secret; +use hyperswitch_payments_client::ConnectorClient; +use std::collections::HashMap; +use std::str::FromStr; + +#[allow(dead_code)] +pub const SUPPORTED_FLOWS: &[&str] = &[ + "authorize", + "capture", + "get", + "proxy_authorize", + "refund", + "refund_get", + "void", +]; + +#[allow(dead_code)] +fn build_client() -> ConnectorClient { + // Configure the connector with authentication + let config = ConnectorConfig { + connector_config: Some(ConnectorSpecificConfig { + config: Some(connector_specific_config::Config::Citigate( + CitigateConfig { + api_key: Some(hyperswitch_masking::Secret::new("YOUR_API_KEY".to_string())), // Authentication credential + key1: Some(hyperswitch_masking::Secret::new("YOUR_KEY1".to_string())), // Authentication credential + base_url: Some("https://sandbox.example.com".to_string()), // Base URL for API calls + ..Default::default() + }, + )), + }), + options: Some(SdkOptions { + environment: Environment::Sandbox.into(), + }), + }; + ConnectorClient::new(config, None).unwrap() +} + +pub fn build_authorize_request(capture_method: &str) -> PaymentServiceAuthorizeRequest { + PaymentServiceAuthorizeRequest { + merchant_transaction_id: Some("probe_txn_001".to_string()), // Identification. + amount: Some(Money { + // The amount for the payment. + minor_amount: 1000, // Amount in minor units (e.g., 1000 = $10.00). + currency: Currency::Usd.into(), // ISO 4217 currency code (e.g., "USD", "EUR"). + }), + payment_method: Some(PaymentMethod { + // Payment method to be used. + payment_method: Some(payment_method::PaymentMethod::Card(CardDetails { + card_number: Some(CardNumber::from_str("4111111111111111").unwrap()), // Card Identification. + card_exp_month: Some(Secret::new("03".to_string())), + card_exp_year: Some(Secret::new("2030".to_string())), + card_cvc: Some(Secret::new("737".to_string())), + card_holder_name: Some(Secret::new("John Doe".to_string())), // Cardholder Information. + ..Default::default() + })), + ..Default::default() + }), + capture_method: Some( + CaptureMethod::from_str_name(capture_method) + .unwrap_or_default() + .into(), + ), // Method for capturing the payment. + address: Some(PaymentAddress { + // Address Information. + billing_address: Some(Address { + first_name: Some(Secret::new("John".to_string())), // Personal Information. + last_name: Some(Secret::new("Doe".to_string())), + line1: Some(Secret::new("123 Main St".to_string())), // Address Details. + city: Some(Secret::new("Seattle".to_string())), + state: Some(Secret::new("WA".to_string())), + zip_code: Some(Secret::new("98101".to_string())), + country_alpha2_code: Some(CountryAlpha2::Us.into()), + email: Some(Secret::new("test@example.com".to_string())), // Contact Information. + phone_number: Some(Secret::new("4155552671".to_string())), + phone_country_code: Some("+1".to_string()), + ..Default::default() + }), + ..Default::default() + }), + auth_type: AuthenticationType::NoThreeDs.into(), // Authentication Details. + return_url: Some("https://example.com/return".to_string()), // URLs for Redirection and Webhooks. + browser_info: Some(BrowserInformation { + ip_address: Some("1.2.3.4".to_string()), // Device Information. + ..Default::default() + }), + ..Default::default() + } +} + +pub fn build_capture_request(connector_transaction_id: &str) -> PaymentServiceCaptureRequest { + PaymentServiceCaptureRequest { + merchant_capture_id: Some("probe_capture_001".to_string()), // Identification. + connector_transaction_id: connector_transaction_id.to_string(), + amount_to_capture: Some(Money { + // Capture Details. + minor_amount: 1000, // Amount in minor units (e.g., 1000 = $10.00). + currency: Currency::Usd.into(), // ISO 4217 currency code (e.g., "USD", "EUR"). + }), + ..Default::default() + } +} + +pub fn build_get_request(connector_transaction_id: &str) -> PaymentServiceGetRequest { + PaymentServiceGetRequest { + merchant_transaction_id: Some("probe_merchant_txn_001".to_string()), // Identification. + connector_transaction_id: connector_transaction_id.to_string(), + amount: Some(Money { + // Amount Information. + minor_amount: 1000, // Amount in minor units (e.g., 1000 = $10.00). + currency: Currency::Usd.into(), // ISO 4217 currency code (e.g., "USD", "EUR"). + }), + ..Default::default() + } +} + +pub fn build_proxy_authorize_request() -> PaymentServiceProxyAuthorizeRequest { + PaymentServiceProxyAuthorizeRequest { + merchant_transaction_id: Some("probe_proxy_txn_001".to_string()), + amount: Some(Money { + minor_amount: 1000, // Amount in minor units (e.g., 1000 = $10.00). + currency: Currency::Usd.into(), // ISO 4217 currency code (e.g., "USD", "EUR"). + }), + card_proxy: Some(ProxyCardDetails { + // Card proxy for vault-aliased payments (VGS, Basis Theory, Spreedly). Real card values are substituted by the proxy before reaching the connector. + card_number: Some(Secret::new("4111111111111111".to_string())), // Card Identification. + card_exp_month: Some(Secret::new("03".to_string())), + card_exp_year: Some(Secret::new("2030".to_string())), + card_cvc: Some(Secret::new("123".to_string())), + card_holder_name: Some(Secret::new("John Doe".to_string())), // Cardholder Information. + card_network: Some(CardNetwork::Visa.into()), + ..Default::default() + }), + address: Some(PaymentAddress { + billing_address: Some(Address { + first_name: Some(Secret::new("John".to_string())), // Personal Information. + last_name: Some(Secret::new("Doe".to_string())), + line1: Some(Secret::new("123 Main St".to_string())), // Address Details. + city: Some(Secret::new("Seattle".to_string())), + state: Some(Secret::new("WA".to_string())), + zip_code: Some(Secret::new("98101".to_string())), + country_alpha2_code: Some(CountryAlpha2::Us.into()), + email: Some(Secret::new("test@example.com".to_string())), // Contact Information. + phone_number: Some(Secret::new("4155552671".to_string())), + phone_country_code: Some("+1".to_string()), + ..Default::default() + }), + ..Default::default() + }), + capture_method: Some(CaptureMethod::Automatic.into()), + auth_type: AuthenticationType::NoThreeDs.into(), + return_url: Some("https://example.com/return".to_string()), + browser_info: Some(BrowserInformation { + ip_address: Some("1.2.3.4".to_string()), // Device Information. + ..Default::default() + }), + ..Default::default() + } +} + +pub fn build_refund_request(connector_transaction_id: &str) -> PaymentServiceRefundRequest { + PaymentServiceRefundRequest { + merchant_refund_id: Some("probe_refund_001".to_string()), // Identification. + connector_transaction_id: connector_transaction_id.to_string(), + payment_amount: 1000, // Amount Information. + refund_amount: Some(Money { + minor_amount: 1000, // Amount in minor units (e.g., 1000 = $10.00). + currency: Currency::Usd.into(), // ISO 4217 currency code (e.g., "USD", "EUR"). + }), + reason: Some("customer_request".to_string()), // Reason for the refund. + ..Default::default() + } +} + +pub fn build_refund_get_request() -> RefundServiceGetRequest { + RefundServiceGetRequest { + merchant_refund_id: Some("probe_refund_001".to_string()), // Identification. + connector_transaction_id: "probe_connector_txn_001".to_string(), + refund_id: "probe_refund_id_001".to_string(), // Deprecated. + ..Default::default() + } +} + +pub fn build_void_request(connector_transaction_id: &str) -> PaymentServiceVoidRequest { + PaymentServiceVoidRequest { + merchant_void_id: Some("probe_void_001".to_string()), // Identification. + connector_transaction_id: connector_transaction_id.to_string(), + ..Default::default() + } +} + +// Scenario: One-step Payment (Authorize + Capture) +// Simple payment that authorizes and captures in one call. Use for immediate charges. +#[allow(dead_code)] +pub async fn process_checkout_autocapture( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + // Step 1: Authorize — reserve funds on the payment method + let authorize_response = client + .authorize(build_authorize_request("AUTOMATIC"), &HashMap::new(), None) + .await?; + + match authorize_response.status() { + PaymentStatus::Failure | PaymentStatus::AuthorizationFailed => { + return Err(format!("Payment failed: {:?}", authorize_response.error).into()) + } + PaymentStatus::Pending => return Ok("pending — awaiting webhook".to_string()), + _ => {} + } + + Ok(format!( + "Payment: {:?} — {}", + authorize_response.status(), + authorize_response + .connector_transaction_id + .as_deref() + .unwrap_or("") + )) +} + +// Scenario: Card Payment (Authorize + Capture) +// Two-step card payment. First authorize, then capture. Use when you need to verify funds before finalizing. +#[allow(dead_code)] +pub async fn process_checkout_card( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + // Step 1: Authorize — reserve funds on the payment method + let authorize_response = client + .authorize(build_authorize_request("MANUAL"), &HashMap::new(), None) + .await?; + + match authorize_response.status() { + PaymentStatus::Failure | PaymentStatus::AuthorizationFailed => { + return Err(format!("Payment failed: {:?}", authorize_response.error).into()) + } + PaymentStatus::Pending => return Ok("pending — awaiting webhook".to_string()), + _ => {} + } + + // Step 2: Capture — settle the reserved funds + let capture_response = client + .capture( + build_capture_request( + authorize_response + .connector_transaction_id + .as_deref() + .unwrap_or(""), + ), + &HashMap::new(), + None, + ) + .await?; + + if capture_response.status() == PaymentStatus::Failure { + return Err(format!("Capture failed: {:?}", capture_response.error).into()); + } + + Ok(format!( + "Payment completed: {}", + authorize_response + .connector_transaction_id + .as_deref() + .unwrap_or("") + )) +} + +// Scenario: Refund +// Return funds to the customer for a completed payment. +#[allow(dead_code)] +pub async fn process_refund( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + // Step 1: Authorize — reserve funds on the payment method + let authorize_response = client + .authorize(build_authorize_request("AUTOMATIC"), &HashMap::new(), None) + .await?; + + match authorize_response.status() { + PaymentStatus::Failure | PaymentStatus::AuthorizationFailed => { + return Err(format!("Payment failed: {:?}", authorize_response.error).into()) + } + PaymentStatus::Pending => return Ok("pending — awaiting webhook".to_string()), + _ => {} + } + + // Step 2: Refund — return funds to the customer + let refund_response = client + .refund( + build_refund_request( + authorize_response + .connector_transaction_id + .as_deref() + .unwrap_or(""), + ), + &HashMap::new(), + None, + ) + .await?; + + if refund_response.status() == RefundStatus::RefundFailure { + return Err(format!("Refund failed: {:?}", refund_response.error).into()); + } + + Ok(format!("Refunded: {:?}", refund_response.status())) +} + +// Scenario: Void Payment +// Cancel an authorized but not-yet-captured payment. +#[allow(dead_code)] +pub async fn process_void_payment( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + // Step 1: Authorize — reserve funds on the payment method + let authorize_response = client + .authorize(build_authorize_request("MANUAL"), &HashMap::new(), None) + .await?; + + match authorize_response.status() { + PaymentStatus::Failure | PaymentStatus::AuthorizationFailed => { + return Err(format!("Payment failed: {:?}", authorize_response.error).into()) + } + PaymentStatus::Pending => return Ok("pending — awaiting webhook".to_string()), + _ => {} + } + + // Step 2: Void — release reserved funds (cancel authorization) + let void_response = client + .void( + build_void_request( + authorize_response + .connector_transaction_id + .as_deref() + .unwrap_or(""), + ), + &HashMap::new(), + None, + ) + .await?; + + Ok(format!("Voided: {:?}", void_response.status())) +} + +// Scenario: Get Payment Status +// Retrieve current payment status from the connector. +#[allow(dead_code)] +pub async fn process_get_payment( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + // Step 1: Authorize — reserve funds on the payment method + let authorize_response = client + .authorize(build_authorize_request("MANUAL"), &HashMap::new(), None) + .await?; + + match authorize_response.status() { + PaymentStatus::Failure | PaymentStatus::AuthorizationFailed => { + return Err(format!("Payment failed: {:?}", authorize_response.error).into()) + } + PaymentStatus::Pending => return Ok("pending — awaiting webhook".to_string()), + _ => {} + } + + // Step 2: Get — retrieve current payment status from the connector + let get_response = client + .get( + build_get_request( + authorize_response + .connector_transaction_id + .as_deref() + .unwrap_or(""), + ), + &HashMap::new(), + None, + ) + .await?; + + Ok(format!("Status: {:?}", get_response.status())) +} + +// Flow: PaymentService.Authorize (Card) +#[allow(dead_code)] +pub async fn process_authorize( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + let response = client + .authorize(build_authorize_request("AUTOMATIC"), &HashMap::new(), None) + .await?; + match response.status() { + PaymentStatus::Failure | PaymentStatus::AuthorizationFailed => { + Err(format!("Authorize failed: {:?}", response.error).into()) + } + PaymentStatus::Pending => Ok("pending — await webhook".to_string()), + _ => Ok(format!( + "Authorized: {}", + response.connector_transaction_id.as_deref().unwrap_or("") + )), + } +} + +// Flow: PaymentService.Capture +#[allow(dead_code)] +pub async fn process_capture( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + let response = client + .capture( + build_capture_request("probe_connector_txn_001"), + &HashMap::new(), + None, + ) + .await?; + Ok(format!("status: {:?}", response.status())) +} + +// Flow: PaymentService.Get +#[allow(dead_code)] +pub async fn process_get( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + let response = client + .get( + build_get_request("probe_connector_txn_001"), + &HashMap::new(), + None, + ) + .await?; + Ok(format!("status: {:?}", response.status())) +} + +// Flow: PaymentService.ProxyAuthorize +#[allow(dead_code)] +pub async fn process_proxy_authorize( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + let response = client + .proxy_authorize(build_proxy_authorize_request(), &HashMap::new(), None) + .await?; + Ok(format!("status: {:?}", response.status())) +} + +// Flow: RefundService.Get +#[allow(dead_code)] +pub async fn process_refund_get( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + let response = client + .refund_get(build_refund_get_request(), &HashMap::new(), None) + .await?; + Ok(format!("status: {:?}", response.status())) +} + +// Flow: PaymentService.Void +#[allow(dead_code)] +pub async fn process_void( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + let response = client + .void( + build_void_request("probe_connector_txn_001"), + &HashMap::new(), + None, + ) + .await?; + Ok(format!("status: {:?}", response.status())) +} + +#[allow(dead_code)] +#[tokio::main] +async fn main() { + let client = build_client(); + let flow = std::env::args() + .nth(1) + .unwrap_or_else(|| "process_checkout_autocapture".to_string()); + let result: Result> = match flow.as_str() { + "process_checkout_autocapture" => process_checkout_autocapture(&client, "order_001").await, + "process_checkout_card" => process_checkout_card(&client, "order_001").await, + "process_refund" => process_refund(&client, "order_001").await, + "process_void_payment" => process_void_payment(&client, "order_001").await, + "process_get_payment" => process_get_payment(&client, "order_001").await, + "process_authorize" => process_authorize(&client, "txn_001").await, + "process_capture" => process_capture(&client, "txn_001").await, + "process_get" => process_get(&client, "txn_001").await, + "process_proxy_authorize" => process_proxy_authorize(&client, "txn_001").await, + "process_refund_get" => process_refund_get(&client, "txn_001").await, + "process_void" => process_void(&client, "txn_001").await, + _ => { + eprintln!("Unknown flow: {}. Available: process_checkout_autocapture, process_checkout_card, process_refund, process_void_payment, process_get_payment, process_authorize, process_capture, process_get, process_proxy_authorize, process_refund_get, process_void", flow); + return; + } + }; + match result { + Ok(msg) => println!("✓ {msg}"), + Err(e) => eprintln!("✗ {e}"), + } +} diff --git a/examples/citigate/citigate.ts b/examples/citigate/citigate.ts new file mode 100644 index 0000000000..3151497cf7 --- /dev/null +++ b/examples/citigate/citigate.ts @@ -0,0 +1,352 @@ +// This file is auto-generated. Do not edit manually. +// Replace YOUR_API_KEY and placeholder values with real data. +// Regenerate: python3 scripts/generate-connector-docs.py citigate +// +// Citigate — all integration scenarios and flows in one file. +// Run a scenario: npx tsx citigate.ts checkout_autocapture + +import { PaymentClient, RefundClient, types } from 'hyperswitch-prism'; +const { Environment, AuthenticationType, CaptureMethod, CardNetwork, CountryAlpha2, Currency } = types; +export const SUPPORTED_FLOWS = ["authorize", "capture", "get", "proxy_authorize", "refund", "refund_get", "void"]; + +const _defaultConfig: types.IConnectorConfig = { + options: { + environment: Environment.SANDBOX, + }, + connectorConfig: { + citigate: { + apiKey: { value: 'YOUR_API_KEY' }, + key1: { value: 'YOUR_KEY1' }, + baseUrl: 'YOUR_BASE_URL', + } + }, +}; + + +function _buildAuthorizeRequest(captureMethod: types.CaptureMethod): types.IPaymentServiceAuthorizeRequest { + return { + "merchantTransactionId": "probe_txn_001", // Identification. + "amount": { // The amount for the payment. + "minorAmount": 1000, // Amount in minor units (e.g., 1000 = $10.00). + "currency": Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + }, + "paymentMethod": { // Payment method to be used. + "card": { // Generic card payment. + "cardNumber": {"value": "4111111111111111"}, // Card Identification. + "cardExpMonth": {"value": "03"}, + "cardExpYear": {"value": "2030"}, + "cardCvc": {"value": "737"}, + "cardHolderName": {"value": "John Doe"} // Cardholder Information. + } + }, + "captureMethod": captureMethod, // Method for capturing the payment. + "address": { // Address Information. + "billingAddress": { + "firstName": {"value": "John"}, // Personal Information. + "lastName": {"value": "Doe"}, + "line1": {"value": "123 Main St"}, // Address Details. + "city": {"value": "Seattle"}, + "state": {"value": "WA"}, + "zipCode": {"value": "98101"}, + "countryAlpha2Code": CountryAlpha2.US, + "email": {"value": "test@example.com"}, // Contact Information. + "phoneNumber": {"value": "4155552671"}, + "phoneCountryCode": "+1" + } + }, + "authType": AuthenticationType.NO_THREE_DS, // Authentication Details. + "returnUrl": "https://example.com/return", // URLs for Redirection and Webhooks. + "browserInfo": { + "ipAddress": "1.2.3.4" // Device Information. + } + }; +} + +function _buildCaptureRequest(connectorTransactionId: string): types.IPaymentServiceCaptureRequest { + return { + "merchantCaptureId": "probe_capture_001", // Identification. + "connectorTransactionId": connectorTransactionId, + "amountToCapture": { // Capture Details. + "minorAmount": 1000, // Amount in minor units (e.g., 1000 = $10.00). + "currency": Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + } + }; +} + +function _buildGetRequest(connectorTransactionId: string): types.IPaymentServiceGetRequest { + return { + "merchantTransactionId": "probe_merchant_txn_001", // Identification. + "connectorTransactionId": connectorTransactionId, + "amount": { // Amount Information. + "minorAmount": 1000, // Amount in minor units (e.g., 1000 = $10.00). + "currency": Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + } + }; +} + +function _buildProxyAuthorizeRequest(): types.IPaymentServiceProxyAuthorizeRequest { + return { + "merchantTransactionId": "probe_proxy_txn_001", + "amount": { + "minorAmount": 1000, // Amount in minor units (e.g., 1000 = $10.00). + "currency": Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + }, + "cardProxy": { // Card proxy for vault-aliased payments (VGS, Basis Theory, Spreedly). Real card values are substituted by the proxy before reaching the connector. + "cardNumber": {"value": "4111111111111111"}, // Card Identification. + "cardExpMonth": {"value": "03"}, + "cardExpYear": {"value": "2030"}, + "cardCvc": {"value": "123"}, + "cardHolderName": {"value": "John Doe"}, // Cardholder Information. + "cardNetwork": CardNetwork.VISA + }, + "address": { + "billingAddress": { + "firstName": {"value": "John"}, // Personal Information. + "lastName": {"value": "Doe"}, + "line1": {"value": "123 Main St"}, // Address Details. + "city": {"value": "Seattle"}, + "state": {"value": "WA"}, + "zipCode": {"value": "98101"}, + "countryAlpha2Code": CountryAlpha2.US, + "email": {"value": "test@example.com"}, // Contact Information. + "phoneNumber": {"value": "4155552671"}, + "phoneCountryCode": "+1" + } + }, + "captureMethod": CaptureMethod.AUTOMATIC, + "authType": AuthenticationType.NO_THREE_DS, + "returnUrl": "https://example.com/return", + "browserInfo": { + "ipAddress": "1.2.3.4" // Device Information. + } + }; +} + +function _buildRefundRequest(connectorTransactionId: string): types.IPaymentServiceRefundRequest { + return { + "merchantRefundId": "probe_refund_001", // Identification. + "connectorTransactionId": connectorTransactionId, + "paymentAmount": 1000, // Amount Information. + "refundAmount": { + "minorAmount": 1000, // Amount in minor units (e.g., 1000 = $10.00). + "currency": Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + }, + "reason": "customer_request" // Reason for the refund. + }; +} + +function _buildRefundGetRequest(): types.IRefundServiceGetRequest { + return { + "merchantRefundId": "probe_refund_001", // Identification. + "connectorTransactionId": "probe_connector_txn_001", + "refundId": "probe_refund_id_001" // Deprecated. + }; +} + +function _buildVoidRequest(connectorTransactionId: string): types.IPaymentServiceVoidRequest { + return { + "merchantVoidId": "probe_void_001", // Identification. + "connectorTransactionId": connectorTransactionId + }; +} + + +// ANCHOR: scenario_functions +// One-step Payment (Authorize + Capture) +// Simple payment that authorizes and captures in one call. Use for immediate charges. +async function processCheckoutAutocapture(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + // Step 1: Authorize — reserve funds on the payment method + const authorizeResponse = await paymentClient.authorize(_buildAuthorizeRequest(CaptureMethod.AUTOMATIC)); + + if (authorizeResponse.status === types.PaymentStatus.FAILURE) { + throw new Error(`Payment failed: ${JSON.stringify(authorizeResponse.error)}`); + } + if (authorizeResponse.status === types.PaymentStatus.PENDING) { + // Awaiting async confirmation — handle via webhook + return { status: 'pending', connectorTransactionId: authorizeResponse.connectorTransactionId }; + } + + return { status: authorizeResponse.status, transactionId: authorizeResponse.connectorTransactionId!, error: authorizeResponse.error } as any; +} + +// Card Payment (Authorize + Capture) +// Two-step card payment. First authorize, then capture. Use when you need to verify funds before finalizing. +async function processCheckoutCard(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + // Step 1: Authorize — reserve funds on the payment method + const authorizeResponse = await paymentClient.authorize(_buildAuthorizeRequest(CaptureMethod.MANUAL)); + + if (authorizeResponse.status === types.PaymentStatus.FAILURE) { + throw new Error(`Payment failed: ${JSON.stringify(authorizeResponse.error)}`); + } + if (authorizeResponse.status === types.PaymentStatus.PENDING) { + // Awaiting async confirmation — handle via webhook + return { status: 'pending', connectorTransactionId: authorizeResponse.connectorTransactionId }; + } + + // Step 2: Capture — settle the reserved funds + const captureResponse = await paymentClient.capture(_buildCaptureRequest(authorizeResponse.connectorTransactionId!)); + + if (captureResponse.status === types.PaymentStatus.FAILURE) { + throw new Error(`Capture failed: ${JSON.stringify(captureResponse.error)}`); + } + + return { status: captureResponse.status, transactionId: authorizeResponse.connectorTransactionId!, error: authorizeResponse.error } as any; +} + +// Refund +// Return funds to the customer for a completed payment. +async function processRefund(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + // Step 1: Authorize — reserve funds on the payment method + const authorizeResponse = await paymentClient.authorize(_buildAuthorizeRequest(CaptureMethod.AUTOMATIC)); + + if (authorizeResponse.status === types.PaymentStatus.FAILURE) { + throw new Error(`Payment failed: ${JSON.stringify(authorizeResponse.error)}`); + } + if (authorizeResponse.status === types.PaymentStatus.PENDING) { + // Awaiting async confirmation — handle via webhook + return { status: 'pending', connectorTransactionId: authorizeResponse.connectorTransactionId }; + } + + // Step 2: Refund — return funds to the customer + const refundResponse = await paymentClient.refund(_buildRefundRequest(authorizeResponse.connectorTransactionId!)); + + if (refundResponse.status === types.RefundStatus.REFUND_FAILURE) { + throw new Error(`Refund failed: ${JSON.stringify(refundResponse.error)}`); + } + + return { status: refundResponse.status, error: refundResponse.error } as any; +} + +// Void Payment +// Cancel an authorized but not-yet-captured payment. +async function processVoidPayment(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + // Step 1: Authorize — reserve funds on the payment method + const authorizeResponse = await paymentClient.authorize(_buildAuthorizeRequest(CaptureMethod.MANUAL)); + + if (authorizeResponse.status === types.PaymentStatus.FAILURE) { + throw new Error(`Payment failed: ${JSON.stringify(authorizeResponse.error)}`); + } + if (authorizeResponse.status === types.PaymentStatus.PENDING) { + // Awaiting async confirmation — handle via webhook + return { status: 'pending', connectorTransactionId: authorizeResponse.connectorTransactionId }; + } + + // Step 2: Void — release reserved funds (cancel authorization) + const voidResponse = await paymentClient.void(_buildVoidRequest(authorizeResponse.connectorTransactionId!)); + + return { status: voidResponse.status, transactionId: authorizeResponse.connectorTransactionId!, error: voidResponse.error } as any; +} + +// Get Payment Status +// Retrieve current payment status from the connector. +async function processGetPayment(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + // Step 1: Authorize — reserve funds on the payment method + const authorizeResponse = await paymentClient.authorize(_buildAuthorizeRequest(CaptureMethod.MANUAL)); + + if (authorizeResponse.status === types.PaymentStatus.FAILURE) { + throw new Error(`Payment failed: ${JSON.stringify(authorizeResponse.error)}`); + } + if (authorizeResponse.status === types.PaymentStatus.PENDING) { + // Awaiting async confirmation — handle via webhook + return { status: 'pending', connectorTransactionId: authorizeResponse.connectorTransactionId }; + } + + // Step 2: Get — retrieve current payment status from the connector + const getResponse = await paymentClient.get(_buildGetRequest(authorizeResponse.connectorTransactionId!)); + + return { status: getResponse.status, transactionId: getResponse.connectorTransactionId!, error: getResponse.error } as any; +} + +// Flow: PaymentService.Authorize (Card) +async function authorize(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + const authorizeResponse = await paymentClient.authorize(_buildAuthorizeRequest(CaptureMethod.AUTOMATIC)); + + return authorizeResponse; +} + +// Flow: PaymentService.Capture +async function capture(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + const captureResponse = await paymentClient.capture(_buildCaptureRequest('probe_connector_txn_001')); + + return captureResponse; +} + +// Flow: PaymentService.Get +async function get(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + const getResponse = await paymentClient.get(_buildGetRequest('probe_connector_txn_001')); + + return getResponse; +} + +// Flow: PaymentService.ProxyAuthorize +async function proxyAuthorize(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + const proxyResponse = await paymentClient.proxyAuthorize(_buildProxyAuthorizeRequest()); + + return proxyResponse; +} + +// Flow: PaymentService.Refund +async function refund(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + const refundResponse = await paymentClient.refund(_buildRefundRequest('probe_connector_txn_001')); + + return refundResponse; +} + +// Flow: RefundService.Get +async function refundGet(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const refundClient = new RefundClient(config); + + const refundResponse = await refundClient.refundGet(_buildRefundGetRequest()); + + return refundResponse; +} + +// Flow: PaymentService.Void +async function voidPayment(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + const voidResponse = await paymentClient.void(_buildVoidRequest('probe_connector_txn_001')); + + return voidResponse; +} + + +// Export all process* functions for the smoke test +export { + processCheckoutAutocapture, processCheckoutCard, processRefund, processVoidPayment, processGetPayment, authorize, capture, get, proxyAuthorize, refund, refundGet, voidPayment, _buildAuthorizeRequest, _buildCaptureRequest, _buildGetRequest, _buildProxyAuthorizeRequest, _buildRefundRequest, _buildRefundGetRequest, _buildVoidRequest +}; + +// CLI runner +if (require.main === module) { + const scenario = process.argv[2] || 'checkout_autocapture'; + const key = 'process' + scenario.replace(/_([a-z])/g, (_, l) => l.toUpperCase()).replace(/^(.)/, c => c.toUpperCase()); + const fn = (globalThis as any)[key] || (exports as any)[key]; + if (!fn) { + const available = Object.keys(exports).map(k => + k.replace(/^process/, '').replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '') + ); + console.error(`Unknown scenario: ${scenario}. Available: ${available.join(', ')}`); + process.exit(1); + } + fn('order_001').catch(console.error); +} diff --git a/sdk/javascript/src/payments/_generated_grpc_client.ts b/sdk/javascript/src/payments/_generated_grpc_client.ts index c80079b807..7866fb5621 100644 --- a/sdk/javascript/src/payments/_generated_grpc_client.ts +++ b/sdk/javascript/src/payments/_generated_grpc_client.ts @@ -324,6 +324,7 @@ const _SECRET_STRING_FIELDS: Record = { GrabpayConfig: ["partnerId", "partnerSecret", "clientId", "clientSecret", "merchantId"], TesouroConfig: ["apiKey", "key1", "apiSecret"], BoostConfig: ["clientId", "merchantSecret"], + CitigateConfig: ["apiKey", "key1"], PaymentServiceTokenAuthorizeRequest: ["connectorToken", "metadata", "connectorFeatureData"], PaymentServiceTokenSetupRecurringRequest: ["connectorToken", "metadata", "connectorFeatureData"], PaymentServiceProxyAuthorizeRequest: ["metadata", "connectorFeatureData"], @@ -515,7 +516,7 @@ const _MSG_FIELD_TYPES: Record> = { CashtocodeConfig: { "authKeyMap": "AuthKeyMapEntry" }, AuthKeyMapEntry: { "value": "PayloadCurrencyAuthData" }, PayloadConfig: { "authKeyMap": "AuthKeyMapEntry" }, - ConnectorSpecificConfig: { "adyen": "AdyenConfig", "airwallex": "AirwallexConfig", "bambora": "BamboraConfig", "bankofamerica": "BankOfAmericaConfig", "billwerk": "BillwerkConfig", "bluesnap": "BluesnapConfig", "braintree": "BraintreeConfig", "cashtocode": "CashtocodeConfig", "cryptopay": "CryptopayConfig", "cybersource": "CybersourceConfig", "datatrans": "DatatransConfig", "dlocal": "DlocalConfig", "elavon": "ElavonConfig", "fiserv": "FiservConfig", "fiservemea": "FiservemeaConfig", "forte": "ForteConfig", "getnet": "GetnetConfig", "globalpay": "GlobalpayConfig", "hipay": "HipayConfig", "helcim": "HelcimConfig", "iatapay": "IatapayConfig", "jpmorgan": "JpmorganConfig", "mifinity": "MifinityConfig", "mollie": "MollieConfig", "multisafepay": "MultisafepayConfig", "nexinets": "NexinetsConfig", "nexixpay": "NexixpayConfig", "nmi": "NmiConfig", "noon": "NoonConfig", "novalnet": "NovalnetConfig", "nuvei": "NuveiConfig", "paybox": "PayboxConfig", "payme": "PaymeConfig", "payu": "PayuConfig", "powertranz": "PowertranzConfig", "rapyd": "RapydConfig", "redsys": "RedsysConfig", "shift4": "Shift4Config", "stax": "StaxConfig", "stripe": "StripeConfig", "trustpay": "TrustpayConfig", "tsys": "TsysConfig", "volt": "VoltConfig", "wellsfargo": "WellsfargoConfig", "worldpay": "WorldpayConfig", "worldpayvantiv": "WorldpayvantivConfig", "xendit": "XenditConfig", "phonepe": "PhonepeConfig", "cashfree": "CashfreeConfig", "paytm": "PaytmConfig", "calida": "CalidaConfig", "payload": "PayloadConfig", "authipay": "AuthipayConfig", "silverflow": "SilverflowConfig", "celero": "CeleroConfig", "trustpayments": "TrustpaymentsConfig", "paysafe": "PaysafeConfig", "barclaycard": "BarclaycardConfig", "worldpayxml": "WorldpayxmlConfig", "revolut": "RevolutConfig", "loonio": "LoonioConfig", "gigadat": "GigadatConfig", "hyperpg": "HyperpgConfig", "zift": "ZiftConfig", "screenstream": "ScreenstreamConfig", "ebanx": "EbanxConfig", "fiuu": "FiuuConfig", "globepay": "GlobepayConfig", "coinbase": "CoinbaseConfig", "coingate": "CoingateConfig", "revolv3": "Revolv3Config", "authorizedotnet": "AuthorizedotnetConfig", "peachpayments": "PeachpaymentsConfig", "paypal": "PaypalConfig", "truelayer": "TruelayerConfig", "fiservcommercehub": "FiservcommercehubConfig", "itaubank": "ItaubankConfig", "ppro": "PproConfig", "trustly": "TrustlyConfig", "absaSanlam": "AbsaSanlamConfig", "pinelabsOnline": "PinelabsOnlineConfig", "imerchantsolutions": "ImerchantsolutionsConfig", "axisbank": "AxisbankConfig", "easebuzz": "EasebuzzConfig", "twocTwopPaco": "TwocTwopPacoConfig", "bamboraapac": "BamboraapacConfig", "placetopay": "PlacetopayConfig", "finix": "FinixConfig", "aci": "AciConfig", "interpayments": "InterpaymentsConfig", "juspay": "JuspayConfig", "tamara": "TamaraConfig", "payconex": "PayconexConfig", "qwikcilver": "QwikcilverConfig", "checkout": "CheckoutConfig", "hyperswitch": "HyperswitchConfig", "tsysTransit": "TsysTransitConfig", "kount": "KountConfig", "affirm": "AffirmConfig", "flywire": "FlywireConfig", "glomopay": "GlomopayConfig", "givepayments": "GivepaymentsConfig", "tesouro": "TesouroConfig", "deutschebank": "DeutschebankConfig", "plaid": "PlaidConfig", "santander": "SantanderConfig", "maya": "MayaConfig", "grabpay": "GrabpayConfig", "boost": "BoostConfig" }, + ConnectorSpecificConfig: { "adyen": "AdyenConfig", "airwallex": "AirwallexConfig", "bambora": "BamboraConfig", "bankofamerica": "BankOfAmericaConfig", "billwerk": "BillwerkConfig", "bluesnap": "BluesnapConfig", "braintree": "BraintreeConfig", "cashtocode": "CashtocodeConfig", "cryptopay": "CryptopayConfig", "cybersource": "CybersourceConfig", "datatrans": "DatatransConfig", "dlocal": "DlocalConfig", "elavon": "ElavonConfig", "fiserv": "FiservConfig", "fiservemea": "FiservemeaConfig", "forte": "ForteConfig", "getnet": "GetnetConfig", "globalpay": "GlobalpayConfig", "hipay": "HipayConfig", "helcim": "HelcimConfig", "iatapay": "IatapayConfig", "jpmorgan": "JpmorganConfig", "mifinity": "MifinityConfig", "mollie": "MollieConfig", "multisafepay": "MultisafepayConfig", "nexinets": "NexinetsConfig", "nexixpay": "NexixpayConfig", "nmi": "NmiConfig", "noon": "NoonConfig", "novalnet": "NovalnetConfig", "nuvei": "NuveiConfig", "paybox": "PayboxConfig", "payme": "PaymeConfig", "payu": "PayuConfig", "powertranz": "PowertranzConfig", "rapyd": "RapydConfig", "redsys": "RedsysConfig", "shift4": "Shift4Config", "stax": "StaxConfig", "stripe": "StripeConfig", "trustpay": "TrustpayConfig", "tsys": "TsysConfig", "volt": "VoltConfig", "wellsfargo": "WellsfargoConfig", "worldpay": "WorldpayConfig", "worldpayvantiv": "WorldpayvantivConfig", "xendit": "XenditConfig", "phonepe": "PhonepeConfig", "cashfree": "CashfreeConfig", "paytm": "PaytmConfig", "calida": "CalidaConfig", "payload": "PayloadConfig", "authipay": "AuthipayConfig", "silverflow": "SilverflowConfig", "celero": "CeleroConfig", "trustpayments": "TrustpaymentsConfig", "paysafe": "PaysafeConfig", "barclaycard": "BarclaycardConfig", "worldpayxml": "WorldpayxmlConfig", "revolut": "RevolutConfig", "loonio": "LoonioConfig", "gigadat": "GigadatConfig", "hyperpg": "HyperpgConfig", "zift": "ZiftConfig", "screenstream": "ScreenstreamConfig", "ebanx": "EbanxConfig", "fiuu": "FiuuConfig", "globepay": "GlobepayConfig", "coinbase": "CoinbaseConfig", "coingate": "CoingateConfig", "revolv3": "Revolv3Config", "authorizedotnet": "AuthorizedotnetConfig", "peachpayments": "PeachpaymentsConfig", "paypal": "PaypalConfig", "truelayer": "TruelayerConfig", "fiservcommercehub": "FiservcommercehubConfig", "itaubank": "ItaubankConfig", "ppro": "PproConfig", "trustly": "TrustlyConfig", "absaSanlam": "AbsaSanlamConfig", "pinelabsOnline": "PinelabsOnlineConfig", "imerchantsolutions": "ImerchantsolutionsConfig", "axisbank": "AxisbankConfig", "easebuzz": "EasebuzzConfig", "twocTwopPaco": "TwocTwopPacoConfig", "bamboraapac": "BamboraapacConfig", "placetopay": "PlacetopayConfig", "finix": "FinixConfig", "aci": "AciConfig", "interpayments": "InterpaymentsConfig", "juspay": "JuspayConfig", "tamara": "TamaraConfig", "payconex": "PayconexConfig", "qwikcilver": "QwikcilverConfig", "checkout": "CheckoutConfig", "hyperswitch": "HyperswitchConfig", "tsysTransit": "TsysTransitConfig", "kount": "KountConfig", "affirm": "AffirmConfig", "flywire": "FlywireConfig", "glomopay": "GlomopayConfig", "givepayments": "GivepaymentsConfig", "tesouro": "TesouroConfig", "deutschebank": "DeutschebankConfig", "plaid": "PlaidConfig", "santander": "SantanderConfig", "maya": "MayaConfig", "grabpay": "GrabpayConfig", "boost": "BoostConfig", "citigate": "CitigateConfig" }, PaymentServiceTokenAuthorizeRequest: { "amount": "Money", "customer": "Customer", "address": "PaymentAddress", "browserInfo": "BrowserInformation", "state": "ConnectorState", "billingDescriptor": "BillingDescriptor", "l2L3Data": "L2L3Data", "customerAcceptance": "CustomerAcceptance" }, PaymentServiceTokenSetupRecurringRequest: { "amount": "Money", "customer": "Customer", "address": "PaymentAddress", "state": "ConnectorState", "customerAcceptance": "CustomerAcceptance", "setupMandateDetails": "SetupMandateDetails", "billingDescriptor": "BillingDescriptor" }, PaymentServiceProxyAuthorizeRequest: { "amount": "Money", "cardProxy": "ProxyCardDetails", "customer": "Customer", "address": "PaymentAddress", "authenticationData": "AuthenticationData", "browserInfo": "BrowserInformation", "state": "ConnectorState", "setupMandateDetails": "SetupMandateDetails", "billingDescriptor": "BillingDescriptor", "redirectionResponse": "RedirectionResponse", "l2L3Data": "L2L3Data", "customerAcceptance": "CustomerAcceptance", "domainData": "DomainData" },