From 7a0bc41873daddbc5abb4b83da7c01d5ed954fab Mon Sep 17 00:00:00 2001 From: shuklatushar226 Date: Thu, 13 Aug 2026 19:31:46 +0530 Subject: [PATCH 01/11] feat(connector): implement Authorize for citigate --- config/development.toml | 1 + config/production.toml | 1 + config/sandbox.toml | 1 + .../connector-integration/src/connectors.rs | 3 + .../src/connectors/citigate.rs | 258 +++++++++ .../src/connectors/citigate/transformers.rs | 527 ++++++++++++++++++ .../src/default_implementations.rs | 6 + .../connector-integration/src/types.rs | 1 + crates/internal/field-probe/src/auth.rs | 5 + .../domain_types/src/connector_types.rs | 3 + .../domain_types/src/router_data.rs | 20 + crates/types-traits/domain_types/src/types.rs | 1 + .../grpc-api-types/proto/payment.proto | 9 + 13 files changed, 836 insertions(+) create mode 100644 crates/integrations/connector-integration/src/connectors/citigate.rs create mode 100644 crates/integrations/connector-integration/src/connectors/citigate/transformers.rs 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..ba7d54ec89 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-test.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..d99aad3002 --- /dev/null +++ b/crates/integrations/connector-integration/src/connectors/citigate.rs @@ -0,0 +1,258 @@ +//! 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. + +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, + connector_types::{PaymentFlowData, PaymentsAuthorizeData, PaymentsResponseData}, + 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, CitigatePaymentsRequest, CitigatePaymentsResponse}; + +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>, + ) + ], + 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 + )) + } + } +); + +// ===== 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 +{ +} + +// ===== 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's only signed callbacks belong to the 3DS redirect and the opt-in +// refund/fraud notification services, both out of scope for this integration. +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 is stubbed: the Citigate JSON interface does +// support capture / cancel / refund / status-check on the same endpoint, but they +// are out of scope for this integration. +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, + Capture, + IncrementalAuthorization, + CreateOrder, + PostAuthenticate, + PreAuthenticate, + PSync, + PaymentMethodToken, + VoidPC, + Void, + RSync, + Refund, + 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..adaee99085 --- /dev/null +++ b/crates/integrations/connector-integration/src/connectors/citigate/transformers.rs @@ -0,0 +1,527 @@ +//! 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. + +use common_enums::{AttemptStatus, CardNetwork}; +use common_utils::{pii::Email, types::StringMinorUnit}; +use domain_types::{ + connector_flow::Authorize, + connector_types::{PaymentFlowData, PaymentsAuthorizeData, PaymentsResponseData, ResponseId}, + errors::{ConnectorError, IntegrationError, IntegrationErrorContext}, + payment_method_data::{Card, PaymentMethodData, PaymentMethodDataTypes, RawCardNumber}, + router_data::{ConnectorSpecificConfig, ErrorResponse, FlowStatus}, + router_data_v2::RouterDataV2, + 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"; + +/// `ResponseCode` returned for an approved transaction. +const RESPONSE_CODE_APPROVED: &str = "0"; +/// `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_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 unsupported_brand(detail: String) -> error_stack::Report { + error_stack::report!(IntegrationError::NotSupported { + message: detail, + connector: "citigate", + context: IntegrationErrorContext::default(), + }) +} + +/// 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(unsupported_brand(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(unsupported_brand(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>, + #[serde(rename = "UserIP")] + pub user_ip: Secret, +} + +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(), + ))) + } + }; + + 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()?; + + 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: common.get_optional_billing_state(), + country: common.get_billing_country()?, + email, + telephone: common.get_optional_billing_phone_number(), + user_ip: Secret::new(user_ip.peek().to_string()), + }) + } +} + +// ============================================================================= +// 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, or reported it as still + /// pending (`999` + `TransTypeID 6`). + fn is_success(&self) -> bool { + match self.response_code() { + RESPONSE_CODE_APPROVED => true, + RESPONSE_CODE_UNDOCUMENTED => self.trans_type_id() == RESP_TRANS_TYPE_PENDING, + _ => false, + } + } + + /// Status for an approved / 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, + }, + _ => AttemptStatus::Pending, + } + } + + /// Status attached to a failed Authorize response. + fn failure_status(&self) -> AttemptStatus { + match self.response_code() { + // Redirect (3DS) is out of scope for this integration: surface it as an + // authentication failure rather than silently reporting a plain failure. + RESPONSE_CODE_REDIRECT_REQUIRED => AttemptStatus::AuthenticationFailed, + _ => AttemptStatus::AuthorizationFailed, + } + } + + /// 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, + })) + } + + pub fn to_error_response(&self, http_code: u16) -> 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(FlowStatus::Payment(self.failure_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(), + } + } +} + +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: 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.attempt_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 e233f58ad1..18c77b5a94 100644 --- a/crates/internal/field-probe/src/auth.rs +++ b/crates/internal/field-probe/src/auth.rs @@ -791,5 +791,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/types-traits/domain_types/src/connector_types.rs b/crates/types-traits/domain_types/src/connector_types.rs index ea042f94f9..a36d5c2adc 100644 --- a/crates/types-traits/domain_types/src/connector_types.rs +++ b/crates/types-traits/domain_types/src/connector_types.rs @@ -162,6 +162,7 @@ pub enum ConnectorEnum { Grabpay, Tesouro, Boost, + Citigate, } // snake case for enum variants @@ -520,6 +521,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", @@ -5667,6 +5669,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 f1e4f55885..18beb1efb8 100644 --- a/crates/types-traits/domain_types/src/router_data.rs +++ b/crates/types-traits/domain_types/src/router_data.rs @@ -965,6 +965,11 @@ pub enum ConnectorSpecificConfig { merchant_secret: Secret, base_url: Option, }, + Citigate { + api_key: Secret, + key1: Secret, + base_url: Option, + }, } impl ConnectorSpecificConfig { @@ -1312,6 +1317,7 @@ impl ConnectorSpecificConfig { api_secret }, Boost { api_key }, + Citigate { api_key, key1 }, Imerchantsolutions { api_key }, Interpayments { api_key }, TwocTwopPaco { @@ -1782,6 +1788,7 @@ impl ConnectorSpecificConfig { api_secret }, Boost { api_key }, + Citigate { api_key, key1 }, Imerchantsolutions { api_key }, Interpayments { api_key }, TwocTwopPaco { @@ -2403,6 +2410,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, @@ -3610,6 +3622,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 914ecfbb47..dbf75b3e6c 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 82b810553e..b82e38c268 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 @@ -5644,6 +5645,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; @@ -5960,6 +5967,8 @@ message ConnectorSpecificConfig { GrabpayConfig grabpay = 144; // BOOST = 136 BoostConfig boost = 145; + // CITIGATE = 137 + CitigateConfig citigate = 146; } } From ea13cab8611c8a262ef2ab762bfbe9a008c89ccd Mon Sep 17 00:00:00 2001 From: "hyperswitch-bot[bot]" <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:23:40 +0000 Subject: [PATCH 02/11] chore: auto-fix formatting and generated code Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions. --- data/field_probe/citigate.json | 726 ++++++++++++++++++ docs-generated/all_connector.md | 2 + docs-generated/connectors/citigate.md | 282 +++++++ docs-generated/llms.txt | 12 +- examples/citigate/citigate.kt | 155 ++++ examples/citigate/citigate.py | 140 ++++ examples/citigate/citigate.rs | 213 +++++ examples/citigate/citigate.ts | 155 ++++ .../src/payments/_generated_grpc_client.ts | 3 +- 9 files changed, 1685 insertions(+), 3 deletions(-) create mode 100644 data/field_probe/citigate.json create mode 100644 docs-generated/connectors/citigate.md create mode 100644 examples/citigate/citigate.kt create mode 100644 examples/citigate/citigate.py create mode 100644 examples/citigate/citigate.rs create mode 100644 examples/citigate/citigate.ts diff --git a/data/field_probe/citigate.json b/data/field_probe/citigate.json new file mode 100644 index 0000000000..0d7593e5b3 --- /dev/null +++ b/data/field_probe/citigate.json @@ -0,0 +1,726 @@ +{ + "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", + "zip_code": "98101", + "country_alpha2_code": "US", + "email": "test@example.com" + } + }, + "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\",\"Country\":\"US\",\"Email\":\"test@example.com\",\"UserIP\":\"1.2.3.4\"}" + } + }, + "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": "not_implemented", + "error": "This feature is not implemented: capture flow for citigate" + } + }, + "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": "not_implemented", + "error": "This feature is not implemented: payment_sync flow for citigate" + } + }, + "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", + "zip_code": "98101", + "country_alpha2_code": "US", + "email": "test@example.com" + } + }, + "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\",\"Country\":\"US\",\"Email\":\"test@example.com\",\"UserIP\":\"1.2.3.4\"}" + } + } + }, + "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": "not_implemented", + "error": "This feature is not implemented: refund flow for citigate" + } + }, + "refund_get": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: refund_sync flow for citigate" + } + }, + "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": "not_implemented", + "error": "This feature is not implemented: void flow for citigate" + } + } + } +} \ No newline at end of file diff --git a/docs-generated/all_connector.md b/docs-generated/all_connector.md index 22c10cb027..b049396f5f 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..129959637e --- /dev/null +++ b/docs-generated/connectors/citigate.md @@ -0,0 +1,282 @@ +# 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#L97) · [JavaScript](../../examples/citigate/citigate.js) · [Kotlin](../../examples/citigate/citigate.kt#L80) · [Rust](../../examples/citigate/citigate.rs#L124) + +## API Reference + +| Flow (Service.RPC) | Category | gRPC Request Message | +|--------------------|----------|----------------------| +| [PaymentService.Authorize](#paymentserviceauthorize) | Payments | `PaymentServiceAuthorizeRequest` | +| [PaymentService.ProxyAuthorize](#paymentserviceproxyauthorize) | Payments | `PaymentServiceProxyAuthorizeRequest` | + +### 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#L119) · [Kotlin](../../examples/citigate/citigate.kt#L95) · [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#L128) · [Kotlin](../../examples/citigate/citigate.kt#L107) · [Rust](../../examples/citigate/citigate.rs) diff --git a/docs-generated/llms.txt b/docs-generated/llms.txt index 8a88682a0b..6f97e885aa 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 +payment_methods: Card +flows: authorize, proxy_authorize +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..069ad4b747 --- /dev/null +++ b/examples/citigate/citigate.kt @@ -0,0 +1,155 @@ +// 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.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", "proxy_authorize") + +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" + zipCodeBuilder.value = "98101" + countryAlpha2Code = CountryAlpha2.US + emailBuilder.value = "test@example.com" // Contact Information. + } + } + 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() +} + +// 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) +} + +// 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.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" + zipCodeBuilder.value = "98101" + countryAlpha2Code = CountryAlpha2.US + emailBuilder.value = "test@example.com" // Contact Information. + } + } + 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}") +} + + +fun main(args: Array) { + val txnId = "order_001" + val flow = args.firstOrNull() ?: "processCheckoutAutocapture" + when (flow) { + "processCheckoutAutocapture" -> processCheckoutAutocapture(txnId) + "authorize" -> authorize(txnId) + "proxyAuthorize" -> proxyAuthorize(txnId) + else -> System.err.println("Unknown flow: $flow. Available: processCheckoutAutocapture, authorize, proxyAuthorize") + } +} diff --git a/examples/citigate/citigate.py b/examples/citigate/citigate.py new file mode 100644 index 0000000000..f266bde572 --- /dev/null +++ b/examples/citigate/citigate.py @@ -0,0 +1,140 @@ +# 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.generated import sdk_config_pb2, payment_pb2, payment_methods_pb2 + +SUPPORTED_FLOWS = ["authorize", "proxy_authorize"] + +_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"), + 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. + ), + ), + 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_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"), + 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. + ), + ), + 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. + ), + ) +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_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_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} + +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..4ba7ee437c --- /dev/null +++ b/examples/citigate/citigate.rs @@ -0,0 +1,213 @@ +// 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", "proxy_authorize"]; + +#[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())), + 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. + ..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_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())), + 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. + ..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() + } +} + +// 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("") + )) +} + +// 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.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())) +} + +#[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_authorize" => process_authorize(&client, "txn_001").await, + "process_proxy_authorize" => process_proxy_authorize(&client, "txn_001").await, + _ => { + eprintln!("Unknown flow: {}. Available: process_checkout_autocapture, process_authorize, process_proxy_authorize", 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..479910d13e --- /dev/null +++ b/examples/citigate/citigate.ts @@ -0,0 +1,155 @@ +// 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, types } from 'hyperswitch-prism'; +const { Environment, AuthenticationType, CaptureMethod, CardNetwork, CountryAlpha2, Currency } = types; +export const SUPPORTED_FLOWS = ["authorize", "proxy_authorize"]; + +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"}, + "zipCode": {"value": "98101"}, + "countryAlpha2Code": CountryAlpha2.US, + "email": {"value": "test@example.com"} // Contact Information. + } + }, + "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 _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"}, + "zipCode": {"value": "98101"}, + "countryAlpha2Code": CountryAlpha2.US, + "email": {"value": "test@example.com"} // Contact Information. + } + }, + "captureMethod": CaptureMethod.AUTOMATIC, + "authType": AuthenticationType.NO_THREE_DS, + "returnUrl": "https://example.com/return", + "browserInfo": { + "ipAddress": "1.2.3.4" // Device Information. + } + }; +} + + +// 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; +} + +// 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.ProxyAuthorize +async function proxyAuthorize(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + const proxyResponse = await paymentClient.proxyAuthorize(_buildProxyAuthorizeRequest()); + + return proxyResponse; +} + + +// Export all process* functions for the smoke test +export { + processCheckoutAutocapture, authorize, proxyAuthorize, _buildAuthorizeRequest, _buildProxyAuthorizeRequest +}; + +// 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 db790fbf2f..685e3fd701 100644 --- a/sdk/javascript/src/payments/_generated_grpc_client.ts +++ b/sdk/javascript/src/payments/_generated_grpc_client.ts @@ -323,6 +323,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"], @@ -514,7 +515,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" }, From 7c6bf5bcb02b5288933e5052147893397b4389fc Mon Sep 17 00:00:00 2001 From: shuklatushar226 Date: Thu, 13 Aug 2026 20:03:13 +0530 Subject: [PATCH 03/11] feat(connector): implement Authorize (Card, 3DS redirect) + PSync for citigate --- .../src/connectors/citigate.rs | 72 ++++- .../src/connectors/citigate/transformers.rs | 286 +++++++++++++++++- 2 files changed, 336 insertions(+), 22 deletions(-) diff --git a/crates/integrations/connector-integration/src/connectors/citigate.rs b/crates/integrations/connector-integration/src/connectors/citigate.rs index d99aad3002..1f593e92b8 100644 --- a/crates/integrations/connector-integration/src/connectors/citigate.rs +++ b/crates/integrations/connector-integration/src/connectors/citigate.rs @@ -6,7 +6,9 @@ //! (`MerchantName` / `MerchantPassword`) travel in the body too, so //! [`ConnectorCommon::get_auth_header`] contributes no headers. //! -//! Implemented scope: Card / Authorize (Purchase), one-time, non-3DS. +//! Implemented scope: Card / Authorize (Purchase), one-time, non-3DS and the 3DS +//! user-redirect path, plus the Transaction Status Check (`TransTypeID = 8`) that +//! resolves the payment once the cardholder returns from the ACS page. pub mod transformers; @@ -15,8 +17,10 @@ use std::fmt::Debug; use common_enums::CurrencyUnit; use common_utils::{errors::CustomResult, events, ext_traits::ByteSliceExt}; use domain_types::{ - connector_flow::Authorize, - connector_types::{PaymentFlowData, PaymentsAuthorizeData, PaymentsResponseData}, + connector_flow::{Authorize, PSync}, + connector_types::{ + PaymentFlowData, PaymentsAuthorizeData, PaymentsResponseData, PaymentsSyncData, + }, errors::{ConnectorError, IntegrationError}, payment_method_data::PaymentMethodDataTypes, router_data::{ConnectorSpecificConfig, ErrorResponse}, @@ -31,7 +35,10 @@ use interfaces::{ decode::BodyDecoding, }; use serde::Serialize; -use transformers::{self as citigate, CitigatePaymentsRequest, CitigatePaymentsResponse}; +use transformers::{ + self as citigate, CitigatePaymentsRequest, CitigatePaymentsResponse, CitigateSyncRequest, + CitigateSyncResponse, +}; use super::macros; use crate::types::ResponseRouterData; @@ -58,6 +65,12 @@ macros::create_all_prerequisites!( request_body: CitigatePaymentsRequest, response_body: CitigatePaymentsResponse, router_data: RouterDataV2, PaymentsResponseData>, + ), + ( + flow: PSync, + request_body: CitigateSyncRequest, + response_body: CitigateSyncResponse, + router_data: RouterDataV2, ) ], amount_converters: [], @@ -171,6 +184,42 @@ macros::macro_connector_implementation!( } ); +// 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 + )) + } + } +); + // ===== CONNECTOR SERVICE TRAIT IMPLEMENTATION ===== // Aggregate trait - composes all other connector traits. impl @@ -184,6 +233,11 @@ impl { } +impl + connector_types::PaymentSyncV2 for Citigate +{ +} + // ===== BASE (NON-FLOW) TRAIT IMPLEMENTATIONS ===== impl connector_types::ValidationTrait for Citigate @@ -201,8 +255,9 @@ impl } // ===== SOURCE VERIFICATION IMPLEMENTATION ===== -// Citigate's only signed callbacks belong to the 3DS redirect and the opt-in -// refund/fraud notification services, both out of scope for this integration. +// 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 { @@ -222,8 +277,8 @@ macros::macro_connector_payout_implementation!( ); // ===== FLOW STATUS IMPLEMENTATIONS ===== -// Every flow other than Authorize is stubbed: the Citigate JSON interface does -// support capture / cancel / refund / status-check on the same endpoint, but they +// Every flow other than Authorize and PSync is stubbed: the Citigate JSON +// interface does support capture / cancel / refund on the same endpoint, but they // are out of scope for this integration. macros::macro_connector_flow_status_impls!( connector: Citigate, @@ -241,7 +296,6 @@ macros::macro_connector_flow_status_impls!( CreateOrder, PostAuthenticate, PreAuthenticate, - PSync, PaymentMethodToken, VoidPC, Void, diff --git a/crates/integrations/connector-integration/src/connectors/citigate/transformers.rs b/crates/integrations/connector-integration/src/connectors/citigate/transformers.rs index adaee99085..5b53f60e1a 100644 --- a/crates/integrations/connector-integration/src/connectors/citigate/transformers.rs +++ b/crates/integrations/connector-integration/src/connectors/citigate/transformers.rs @@ -6,17 +6,23 @@ //! also carried in the body, so there are no auth headers. //! //! Scope of this module: Card / Authorize (Purchase, `TransTypeID = 0`), one-time, -//! non-3DS. +//! non-3DS **and** the 3DS user-redirect path, plus the Transaction Status Check +//! (`TransTypeID = 8`) that resolves the payment after the cardholder returns. -use common_enums::{AttemptStatus, CardNetwork}; -use common_utils::{pii::Email, types::StringMinorUnit}; +use std::collections::HashMap; + +use common_enums::{AttemptStatus, AuthenticationType, CardNetwork}; +use common_utils::{pii::Email, types::StringMinorUnit, Method}; use domain_types::{ - connector_flow::Authorize, - connector_types::{PaymentFlowData, PaymentsAuthorizeData, PaymentsResponseData, ResponseId}, + connector_flow::{Authorize, PSync}, + connector_types::{ + PaymentFlowData, PaymentsAuthorizeData, PaymentsResponseData, PaymentsSyncData, 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}; @@ -29,9 +35,13 @@ use crate::types::ResponseRouterData; const PAYMENT_TYPE_ID_CARD: &str = "1"; /// `TransTypeID` for a purchase (UCS `Authorize`). const TRANS_TYPE_ID_PURCHASE: &str = "0"; +/// `TransTypeID` for a transaction status check (UCS `PSync`). +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 a cardholder redirect (3DS) is required. const RESPONSE_CODE_REDIRECT_REQUIRED: &str = "600"; /// Undocumented `ResponseCode` observed on Status Check responses; discriminated on @@ -42,6 +52,8 @@ const RESPONSE_CODE_UNDOCUMENTED: &str = "999"; 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. @@ -192,6 +204,18 @@ pub struct CitigatePaymentsRequest { pub telephone: 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 = @@ -214,6 +238,17 @@ impl 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 / pending Authorize response. + /// 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() { @@ -405,6 +481,9 @@ impl CitigatePaymentsResponse { 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, } } @@ -412,13 +491,74 @@ impl CitigatePaymentsResponse { /// Status attached to a failed Authorize response. fn failure_status(&self) -> AttemptStatus { match self.response_code() { - // Redirect (3DS) is out of scope for this integration: surface it as an - // authentication failure rather than silently reporting a plain failure. - RESPONSE_CODE_REDIRECT_REQUIRED => AttemptStatus::AuthenticationFailed, + // Cardholder failed / abandoned authentication at the ACS, or the + // gateway asked for a redirect without telling us where to. + RESPONSE_CODE_3D_AUTH_FAILURE | 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 => 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 { @@ -449,6 +589,14 @@ impl CitigatePaymentsResponse { } 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 { let message = self .response_description .clone() @@ -467,7 +615,7 @@ impl CitigatePaymentsResponse { .unwrap_or_else(|| "NO_RESPONSE_CODE".to_string()), message, reason, - attempt_status: Some(FlowStatus::Payment(self.failure_status())), + attempt_status: Some(FlowStatus::Payment(attempt_status)), connector_transaction_id: self.connector_transaction_id(), network_decline_code: is_bank_decline.then(|| self.bank_code.clone()).flatten(), network_advice_code: None, @@ -507,7 +655,7 @@ impl TryFrom TryFrom, + #[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)?; + + let merchant_ref = router_data + .resource_common_data + .connector_request_reference_id + .clone(); + if merchant_ref.is_empty() { + return Err(error_stack::report!( + IntegrationError::MissingRequiredField { + field_name: "merchant_transaction_id", + context: IntegrationErrorContext::default(), + } + )); + } + + 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, + }) + } +} + +/// 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 + }) + } +} From 5dfad311f36b73c6d6ae0f346784b98692a08d54 Mon Sep 17 00:00:00 2001 From: "hyperswitch-bot[bot]" <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:55:36 +0000 Subject: [PATCH 04/11] chore: auto-fix formatting and generated code Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions. --- data/field_probe/citigate.json | 24 +++++++-- docs-generated/all_connector.md | 2 +- docs-generated/connectors/citigate.md | 24 +++++++-- docs-generated/llms.txt | 4 +- examples/citigate/citigate.kt | 44 +++++++++++++++- examples/citigate/citigate.py | 43 +++++++++++++++- examples/citigate/citigate.rs | 72 ++++++++++++++++++++++++++- examples/citigate/citigate.ts | 46 ++++++++++++++++- 8 files changed, 242 insertions(+), 17 deletions(-) diff --git a/data/field_probe/citigate.json b/data/field_probe/citigate.json index 0d7593e5b3..039692b267 100644 --- a/data/field_probe/citigate.json +++ b/data/field_probe/citigate.json @@ -138,7 +138,7 @@ "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\",\"Country\":\"US\",\"Email\":\"test@example.com\",\"UserIP\":\"1.2.3.4\"}" + "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\",\"Country\":\"US\",\"Email\":\"test@example.com\",\"UserIP\":\"1.2.3.4\",\"SuccessURL\":\"https://example.com/return\",\"FailURL\":\"https://example.com/return\"}" } }, "CashappQr": { @@ -563,8 +563,24 @@ }, "get": { "default": { - "status": "not_implemented", - "error": "This feature is not implemented: payment_sync flow for citigate" + "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": { @@ -642,7 +658,7 @@ "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\",\"Country\":\"US\",\"Email\":\"test@example.com\",\"UserIP\":\"1.2.3.4\"}" + "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\",\"Country\":\"US\",\"Email\":\"test@example.com\",\"UserIP\":\"1.2.3.4\",\"SuccessURL\":\"https://example.com/return\",\"FailURL\":\"https://example.com/return\"}" } } }, diff --git a/docs-generated/all_connector.md b/docs-generated/all_connector.md index b049396f5f..d4bbdad1e9 100644 --- a/docs-generated/all_connector.md +++ b/docs-generated/all_connector.md @@ -159,7 +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 | +| [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 index 129959637e..53fb345bcc 100644 --- a/docs-generated/connectors/citigate.md +++ b/docs-generated/connectors/citigate.md @@ -127,13 +127,20 @@ Simple payment that authorizes and captures in one call. Use for immediate charg | `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#L97) · [JavaScript](../../examples/citigate/citigate.js) · [Kotlin](../../examples/citigate/citigate.kt#L80) · [Rust](../../examples/citigate/citigate.rs#L124) +**Examples:** [Python](../../examples/citigate/citigate.py#L107) · [JavaScript](../../examples/citigate/citigate.js) · [Kotlin](../../examples/citigate/citigate.kt#L91) · [Rust](../../examples/citigate/citigate.rs#L136) + +### Get Payment Status + +Retrieve current payment status from the connector. + +**Examples:** [Python](../../examples/citigate/citigate.py#L126) · [JavaScript](../../examples/citigate/citigate.js) · [Kotlin](../../examples/citigate/citigate.kt#L107) · [Rust](../../examples/citigate/citigate.rs#L152) ## API Reference | Flow (Service.RPC) | Category | gRPC Request Message | |--------------------|----------|----------------------| | [PaymentService.Authorize](#paymentserviceauthorize) | Payments | `PaymentServiceAuthorizeRequest` | +| [PaymentService.Get](#paymentserviceget) | Payments | `PaymentServiceGetRequest` | | [PaymentService.ProxyAuthorize](#paymentserviceproxyauthorize) | Payments | `PaymentServiceProxyAuthorizeRequest` | ### Payments @@ -268,7 +275,18 @@ Authorize a payment amount on a payment method. This reserves funds without capt } ``` -**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts#L119) · [Kotlin](../../examples/citigate/citigate.kt#L95) · [Rust](../../examples/citigate/citigate.rs) +**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts#L152) · [Kotlin](../../examples/citigate/citigate.kt#L125) · [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#L161) · [Kotlin](../../examples/citigate/citigate.kt#L137) · [Rust](../../examples/citigate/citigate.rs) #### PaymentService.ProxyAuthorize @@ -279,4 +297,4 @@ Authorize using vault-aliased card data. Proxy substitutes before connector. | **Request** | `PaymentServiceProxyAuthorizeRequest` | | **Response** | `PaymentServiceAuthorizeResponse` | -**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts#L128) · [Kotlin](../../examples/citigate/citigate.kt#L107) · [Rust](../../examples/citigate/citigate.rs) +**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts#L170) · [Kotlin](../../examples/citigate/citigate.kt#L145) · [Rust](../../examples/citigate/citigate.rs) diff --git a/docs-generated/llms.txt b/docs-generated/llms.txt index 6f97e885aa..9d767305e5 100644 --- a/docs-generated/llms.txt +++ b/docs-generated/llms.txt @@ -193,9 +193,9 @@ examples_python: examples/checkout/checkout.py ## Citigate connector_id: citigate doc: docs/connectors/citigate.md -scenarios: checkout_autocapture +scenarios: checkout_autocapture, get_payment payment_methods: Card -flows: authorize, proxy_authorize +flows: authorize, get, proxy_authorize examples_python: examples/citigate/citigate.py ## CryptoPay diff --git a/examples/citigate/citigate.kt b/examples/citigate/citigate.kt index 069ad4b747..f26d4a8890 100644 --- a/examples/citigate/citigate.kt +++ b/examples/citigate/citigate.kt @@ -22,7 +22,7 @@ import payments.ConnectorSpecificConfig import types.Payment.CitigateConfig import payments.SecretString -val SUPPORTED_FLOWS = listOf("authorize", "proxy_authorize") +val SUPPORTED_FLOWS = listOf("authorize", "get", "proxy_authorize") val _defaultConfig: ConnectorConfig = ConnectorConfig.newBuilder() .setOptions(SdkOptions.newBuilder().setEnvironment(Environment.SANDBOX).build()) @@ -75,6 +75,17 @@ private fun buildAuthorizeRequest(captureMethodStr: String): PaymentServiceAutho }.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() +} + // 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 { @@ -91,6 +102,25 @@ fun processCheckoutAutocapture(txnId: String, config: ConnectorConfig = _default return mapOf("status" to authorizeResponse.status.name, "transactionId" to authorizeResponse.connectorTransactionId, "error" to authorizeResponse.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) @@ -103,6 +133,14 @@ fun authorize(txnId: String, config: ConnectorConfig = _defaultConfig) { } } +// 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) @@ -148,8 +186,10 @@ fun main(args: Array) { val flow = args.firstOrNull() ?: "processCheckoutAutocapture" when (flow) { "processCheckoutAutocapture" -> processCheckoutAutocapture(txnId) + "processGetPayment" -> processGetPayment(txnId) "authorize" -> authorize(txnId) + "get" -> get(txnId) "proxyAuthorize" -> proxyAuthorize(txnId) - else -> System.err.println("Unknown flow: $flow. Available: processCheckoutAutocapture, authorize, proxyAuthorize") + else -> System.err.println("Unknown flow: $flow. Available: processCheckoutAutocapture, processGetPayment, authorize, get, proxyAuthorize") } } diff --git a/examples/citigate/citigate.py b/examples/citigate/citigate.py index f266bde572..0e0ab0221d 100644 --- a/examples/citigate/citigate.py +++ b/examples/citigate/citigate.py @@ -10,7 +10,7 @@ from payments import PaymentClient from payments.generated import sdk_config_pb2, payment_pb2, payment_methods_pb2 -SUPPORTED_FLOWS = ["authorize", "proxy_authorize"] +SUPPORTED_FLOWS = ["authorize", "get", "proxy_authorize"] _default_config = sdk_config_pb2.ConnectorConfig( options=sdk_config_pb2.SdkOptions(environment=sdk_config_pb2.Environment.SANDBOX), @@ -61,6 +61,16 @@ def _build_authorize_request(capture_method: str): ), ) +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", @@ -113,6 +123,28 @@ async def process_checkout_autocapture(merchant_transaction_id: str, config: sdk return {"status": getattr(authorize_response, "status", ""), "transaction_id": getattr(authorize_response, "connector_transaction_id", ""), "error": getattr(authorize_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) @@ -122,6 +154,15 @@ async def process_authorize(merchant_transaction_id: str, config: sdk_config_pb2 return {"status": authorize_response.status, "transaction_id": authorize_response.connector_transaction_id} +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) diff --git a/examples/citigate/citigate.rs b/examples/citigate/citigate.rs index 4ba7ee437c..8455a76b6a 100644 --- a/examples/citigate/citigate.rs +++ b/examples/citigate/citigate.rs @@ -14,7 +14,7 @@ use std::collections::HashMap; use std::str::FromStr; #[allow(dead_code)] -pub const SUPPORTED_FLOWS: &[&str] = &["authorize", "proxy_authorize"]; +pub const SUPPORTED_FLOWS: &[&str] = &["authorize", "get", "proxy_authorize"]; #[allow(dead_code)] fn build_client() -> ConnectorClient { @@ -86,6 +86,19 @@ pub fn build_authorize_request(capture_method: &str) -> PaymentServiceAuthorizeR } } +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()), @@ -157,6 +170,43 @@ pub async fn process_checkout_autocapture( )) } +// 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( @@ -178,6 +228,22 @@ pub async fn process_authorize( } } +// 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( @@ -199,10 +265,12 @@ async fn main() { .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_get_payment" => process_get_payment(&client, "order_001").await, "process_authorize" => process_authorize(&client, "txn_001").await, + "process_get" => process_get(&client, "txn_001").await, "process_proxy_authorize" => process_proxy_authorize(&client, "txn_001").await, _ => { - eprintln!("Unknown flow: {}. Available: process_checkout_autocapture, process_authorize, process_proxy_authorize", flow); + eprintln!("Unknown flow: {}. Available: process_checkout_autocapture, process_get_payment, process_authorize, process_get, process_proxy_authorize", flow); return; } }; diff --git a/examples/citigate/citigate.ts b/examples/citigate/citigate.ts index 479910d13e..b16b965d74 100644 --- a/examples/citigate/citigate.ts +++ b/examples/citigate/citigate.ts @@ -7,7 +7,7 @@ import { PaymentClient, types } from 'hyperswitch-prism'; const { Environment, AuthenticationType, CaptureMethod, CardNetwork, CountryAlpha2, Currency } = types; -export const SUPPORTED_FLOWS = ["authorize", "proxy_authorize"]; +export const SUPPORTED_FLOWS = ["authorize", "get", "proxy_authorize"]; const _defaultConfig: types.IConnectorConfig = { options: { @@ -59,6 +59,17 @@ function _buildAuthorizeRequest(captureMethod: types.CaptureMethod): types.IPaym }; } +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", @@ -115,6 +126,28 @@ async function processCheckoutAutocapture(merchantTransactionId: string, config: return { status: authorizeResponse.status, transactionId: authorizeResponse.connectorTransactionId!, error: authorizeResponse.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); @@ -124,6 +157,15 @@ async function authorize(merchantTransactionId: string, config: types.IConnector return authorizeResponse; } +// 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); @@ -136,7 +178,7 @@ async function proxyAuthorize(merchantTransactionId: string, config: types.IConn // Export all process* functions for the smoke test export { - processCheckoutAutocapture, authorize, proxyAuthorize, _buildAuthorizeRequest, _buildProxyAuthorizeRequest + processCheckoutAutocapture, processGetPayment, authorize, get, proxyAuthorize, _buildAuthorizeRequest, _buildGetRequest, _buildProxyAuthorizeRequest }; // CLI runner From d65fcd31d3fb37ca31548a3bde226f492e4e65cc Mon Sep 17 00:00:00 2001 From: shuklatushar226 Date: Fri, 14 Aug 2026 01:53:07 +0530 Subject: [PATCH 05/11] feat(connector): implement Capture, Void, Refund and RSync for citigate Adds the post-authorization operations on the Citigate JSON interface: Capture (TransTypeID 3), Void/Cancel (4), Refund (5) and RSync, which reuses the Transaction Status Check (8) keyed on the refund leg's MerchantRef. Refund and RSync are verified live against the sandbox gateway (approved, ResponseCode 0), including end to end through the Hyperswitch REST API. Capture and Void success paths are not exercisable on the sandbox MID, which auto-captures and returns TransTypeID 1 (Sale) even for a manual capture request, so no open authorisation ever exists; only their wire format and the gateway 560 failure mapping were validated live. --- .../src/connectors/citigate.rs | 211 ++++++- .../src/connectors/citigate/transformers.rs | 581 +++++++++++++++++- 2 files changed, 755 insertions(+), 37 deletions(-) diff --git a/crates/integrations/connector-integration/src/connectors/citigate.rs b/crates/integrations/connector-integration/src/connectors/citigate.rs index 1f593e92b8..b240775e2b 100644 --- a/crates/integrations/connector-integration/src/connectors/citigate.rs +++ b/crates/integrations/connector-integration/src/connectors/citigate.rs @@ -7,8 +7,9 @@ //! [`ConnectorCommon::get_auth_header`] contributes no headers. //! //! Implemented scope: Card / Authorize (Purchase), one-time, non-3DS and the 3DS -//! user-redirect path, plus the Transaction Status Check (`TransTypeID = 8`) that -//! resolves the payment once the cardholder returns from the ACS page. +//! 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; @@ -17,9 +18,11 @@ use std::fmt::Debug; use common_enums::CurrencyUnit; use common_utils::{errors::CustomResult, events, ext_traits::ByteSliceExt}; use domain_types::{ - connector_flow::{Authorize, PSync}, + connector_flow::{Authorize, Capture, PSync, RSync, Refund, Void}, connector_types::{ - PaymentFlowData, PaymentsAuthorizeData, PaymentsResponseData, PaymentsSyncData, + PaymentFlowData, PaymentVoidData, PaymentsAuthorizeData, PaymentsCaptureData, + PaymentsResponseData, PaymentsSyncData, RefundFlowData, RefundSyncData, RefundsData, + RefundsResponseData, }, errors::{ConnectorError, IntegrationError}, payment_method_data::PaymentMethodDataTypes, @@ -36,8 +39,10 @@ use interfaces::{ }; use serde::Serialize; use transformers::{ - self as citigate, CitigatePaymentsRequest, CitigatePaymentsResponse, CitigateSyncRequest, - CitigateSyncResponse, + self as citigate, CitigateCaptureRequest, CitigateCaptureResponse, CitigatePaymentsRequest, + CitigatePaymentsResponse, CitigateRefundRequest, CitigateRefundResponse, + CitigateRefundSyncRequest, CitigateRefundSyncResponse, CitigateSyncRequest, + CitigateSyncResponse, CitigateVoidRequest, CitigateVoidResponse, }; use super::macros; @@ -71,6 +76,30 @@ macros::create_all_prerequisites!( 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: [], @@ -220,6 +249,146 @@ macros::macro_connector_implementation!( } ); +// 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 @@ -238,6 +407,26 @@ impl { } +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 @@ -277,9 +466,9 @@ macros::macro_connector_payout_implementation!( ); // ===== FLOW STATUS IMPLEMENTATIONS ===== -// Every flow other than Authorize and PSync is stubbed: the Citigate JSON -// interface does support capture / cancel / refund on the same endpoint, but they -// are out of scope for this integration. +// 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, @@ -291,16 +480,12 @@ macros::macro_connector_flow_status_impls!( DefendDispute, MandateRevoke, Authenticate, - Capture, IncrementalAuthorization, CreateOrder, PostAuthenticate, PreAuthenticate, PaymentMethodToken, VoidPC, - Void, - RSync, - Refund, RepeatPayment, ServerAuthenticationToken, ServerSessionAuthenticationToken, diff --git a/crates/integrations/connector-integration/src/connectors/citigate/transformers.rs b/crates/integrations/connector-integration/src/connectors/citigate/transformers.rs index 5b53f60e1a..8851e0e0d3 100644 --- a/crates/integrations/connector-integration/src/connectors/citigate/transformers.rs +++ b/crates/integrations/connector-integration/src/connectors/citigate/transformers.rs @@ -6,17 +6,20 @@ //! 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, plus the Transaction Status Check -//! (`TransTypeID = 8`) that resolves the payment after the cardholder returns. +//! 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}; +use common_enums::{AttemptStatus, AuthenticationType, CardNetwork, RefundStatus}; use common_utils::{pii::Email, types::StringMinorUnit, Method}; use domain_types::{ - connector_flow::{Authorize, PSync}, + connector_flow::{Authorize, Capture, PSync, RSync, Refund, Void}, connector_types::{ - PaymentFlowData, PaymentsAuthorizeData, PaymentsResponseData, PaymentsSyncData, ResponseId, + PaymentFlowData, PaymentVoidData, PaymentsAuthorizeData, PaymentsCaptureData, + PaymentsResponseData, PaymentsSyncData, RefundFlowData, RefundSyncData, RefundsData, + RefundsResponseData, ResponseId, }, errors::{ConnectorError, IntegrationError, IntegrationErrorContext}, payment_method_data::{Card, PaymentMethodData, PaymentMethodDataTypes, RawCardNumber}, @@ -35,7 +38,13 @@ use crate::types::ResponseRouterData; const PAYMENT_TYPE_ID_CARD: &str = "1"; /// `TransTypeID` for a purchase (UCS `Authorize`). const TRANS_TYPE_ID_PURCHASE: &str = "0"; -/// `TransTypeID` for a transaction status check (UCS `PSync`). +/// `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. @@ -111,7 +120,7 @@ pub enum CitigateBrand { Maestro, } -fn unsupported_brand(detail: String) -> error_stack::Report { +fn not_supported(detail: String) -> error_stack::Report { error_stack::report!(IntegrationError::NotSupported { message: detail, connector: "citigate", @@ -119,6 +128,21 @@ fn unsupported_brand(detail: String) -> error_stack::Report { }) } +/// `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( @@ -131,7 +155,7 @@ fn get_citigate_brand( CardNetwork::AmericanExpress => Ok(CitigateBrand::Amex), CardNetwork::DinersClub => Ok(CitigateBrand::Diners), CardNetwork::Maestro => Ok(CitigateBrand::Maestro), - other => Err(unsupported_brand(format!("Card network {other:?}"))), + other => Err(not_supported(format!("Card network {other:?}"))), }; } @@ -141,7 +165,7 @@ fn get_citigate_brand( CardIssuer::AmericanExpress => Ok(CitigateBrand::Amex), CardIssuer::DinersClub => Ok(CitigateBrand::Diners), CardIssuer::Maestro => Ok(CitigateBrand::Maestro), - other => Err(unsupported_brand(format!("Card issuer {other:?}"))), + other => Err(not_supported(format!("Card issuer {other:?}"))), } } @@ -588,6 +612,83 @@ impl CitigatePaymentsResponse { })) } + /// 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()) } @@ -597,6 +698,18 @@ impl CitigatePaymentsResponse { 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() @@ -615,13 +728,17 @@ impl CitigatePaymentsResponse { .unwrap_or_else(|| "NO_RESPONSE_CODE".to_string()), message, reason, - attempt_status: Some(FlowStatus::Payment(attempt_status)), + 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, } } } @@ -709,25 +826,16 @@ impl> for SyncRouterData }) } } + +// ============================================================================= +// 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 + }) + } +} From 64398bdaa5c0a1958ea544261b646b24b5a92ba8 Mon Sep 17 00:00:00 2001 From: "hyperswitch-bot[bot]" <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Date: Thu, 13 Aug 2026 20:44:16 +0000 Subject: [PATCH 06/11] chore: auto-fix formatting and generated code Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions. --- data/field_probe/citigate.json | 75 ++++++++- docs-generated/all_connector.md | 2 +- docs-generated/connectors/citigate.md | 86 +++++++++- docs-generated/llms.txt | 4 +- examples/citigate/citigate.kt | 148 ++++++++++++++++- examples/citigate/citigate.py | 137 ++++++++++++++- examples/citigate/citigate.rs | 231 +++++++++++++++++++++++++- examples/citigate/citigate.ts | 155 ++++++++++++++++- 8 files changed, 814 insertions(+), 24 deletions(-) diff --git a/data/field_probe/citigate.json b/data/field_probe/citigate.json index 039692b267..f56450a56a 100644 --- a/data/field_probe/citigate.json +++ b/data/field_probe/citigate.json @@ -492,8 +492,24 @@ }, "capture": { "default": { - "status": "not_implemented", - "error": "This feature is not implemented: capture flow for citigate" + "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": { @@ -687,14 +703,45 @@ }, "refund": { "default": { - "status": "not_implemented", - "error": "This feature is not implemented: refund flow for citigate" + "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": "not_implemented", - "error": "This feature is not implemented: refund_sync flow for citigate" + "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": { @@ -734,8 +781,20 @@ }, "void": { "default": { - "status": "not_implemented", - "error": "This feature is not implemented: void flow for citigate" + "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\"}" + } } } } diff --git a/docs-generated/all_connector.md b/docs-generated/all_connector.md index d4bbdad1e9..d2634365ae 100644 --- a/docs-generated/all_connector.md +++ b/docs-generated/all_connector.md @@ -159,7 +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 | +| [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 index 53fb345bcc..bbf8949c38 100644 --- a/docs-generated/connectors/citigate.md +++ b/docs-generated/connectors/citigate.md @@ -127,21 +127,51 @@ Simple payment that authorizes and captures in one call. Use for immediate charg | `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#L107) · [JavaScript](../../examples/citigate/citigate.js) · [Kotlin](../../examples/citigate/citigate.kt#L91) · [Rust](../../examples/citigate/citigate.rs#L136) +**Examples:** [Python](../../examples/citigate/citigate.py#L143) · [JavaScript](../../examples/citigate/citigate.js) · [Kotlin](../../examples/citigate/citigate.kt#L123) · [Rust](../../examples/citigate/citigate.rs#L179) + +### 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#L162) · [JavaScript](../../examples/citigate/citigate.js) · [Kotlin](../../examples/citigate/citigate.kt#L139) · [Rust](../../examples/citigate/citigate.rs#L195) + +### Refund + +Return funds to the customer for a completed payment. + +**Examples:** [Python](../../examples/citigate/citigate.py#L187) · [JavaScript](../../examples/citigate/citigate.js) · [Kotlin](../../examples/citigate/citigate.kt#L161) · [Rust](../../examples/citigate/citigate.rs#L218) + +### Void Payment + +Cancel an authorized but not-yet-captured payment. + +**Examples:** [Python](../../examples/citigate/citigate.py#L212) · [JavaScript](../../examples/citigate/citigate.js) · [Kotlin](../../examples/citigate/citigate.kt#L183) · [Rust](../../examples/citigate/citigate.rs#L241) ### Get Payment Status Retrieve current payment status from the connector. -**Examples:** [Python](../../examples/citigate/citigate.py#L126) · [JavaScript](../../examples/citigate/citigate.js) · [Kotlin](../../examples/citigate/citigate.kt#L107) · [Rust](../../examples/citigate/citigate.rs#L152) +**Examples:** [Python](../../examples/citigate/citigate.py#L234) · [JavaScript](../../examples/citigate/citigate.js) · [Kotlin](../../examples/citigate/citigate.kt#L202) · [Rust](../../examples/citigate/citigate.rs#L260) ## 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 @@ -275,7 +305,18 @@ Authorize a payment amount on a payment method. This reserves funds without capt } ``` -**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts#L152) · [Kotlin](../../examples/citigate/citigate.kt#L125) · [Rust](../../examples/citigate/citigate.rs) +**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts#L265) · [Kotlin](../../examples/citigate/citigate.kt#L220) · [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#L274) · [Kotlin](../../examples/citigate/citigate.kt#L232) · [Rust](../../examples/citigate/citigate.rs) #### PaymentService.Get @@ -286,7 +327,7 @@ Retrieve current payment status from the payment processor. Enables synchronizat | **Request** | `PaymentServiceGetRequest` | | **Response** | `PaymentServiceGetResponse` | -**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts#L161) · [Kotlin](../../examples/citigate/citigate.kt#L137) · [Rust](../../examples/citigate/citigate.rs) +**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts#L283) · [Kotlin](../../examples/citigate/citigate.kt#L242) · [Rust](../../examples/citigate/citigate.rs) #### PaymentService.ProxyAuthorize @@ -297,4 +338,39 @@ Authorize using vault-aliased card data. Proxy substitutes before connector. | **Request** | `PaymentServiceProxyAuthorizeRequest` | | **Response** | `PaymentServiceAuthorizeResponse` | -**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts#L170) · [Kotlin](../../examples/citigate/citigate.kt#L145) · [Rust](../../examples/citigate/citigate.rs) +**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts#L292) · [Kotlin](../../examples/citigate/citigate.kt#L250) · [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#L301) · [Kotlin](../../examples/citigate/citigate.kt#L289) · [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#L311) · [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#L310) · [Kotlin](../../examples/citigate/citigate.kt#L299) · [Rust](../../examples/citigate/citigate.rs) diff --git a/docs-generated/llms.txt b/docs-generated/llms.txt index 9d767305e5..8a3b313207 100644 --- a/docs-generated/llms.txt +++ b/docs-generated/llms.txt @@ -193,9 +193,9 @@ examples_python: examples/checkout/checkout.py ## Citigate connector_id: citigate doc: docs/connectors/citigate.md -scenarios: checkout_autocapture, get_payment +scenarios: checkout_autocapture, checkout_card, refund, void_payment, get_payment payment_methods: Card -flows: authorize, get, proxy_authorize +flows: authorize, capture, get, proxy_authorize, refund, refund_get, void examples_python: examples/citigate/citigate.py ## CryptoPay diff --git a/examples/citigate/citigate.kt b/examples/citigate/citigate.kt index f26d4a8890..dec3e136cf 100644 --- a/examples/citigate/citigate.kt +++ b/examples/citigate/citigate.kt @@ -10,6 +10,7 @@ package examples.citigate import types.Payment.* import types.PaymentMethods.* import payments.PaymentClient +import payments.RefundClient import payments.AuthenticationType import payments.CaptureMethod import payments.CardNetwork @@ -22,7 +23,7 @@ import payments.ConnectorSpecificConfig import types.Payment.CitigateConfig import payments.SecretString -val SUPPORTED_FLOWS = listOf("authorize", "get", "proxy_authorize") +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()) @@ -75,6 +76,17 @@ private fun buildAuthorizeRequest(captureMethodStr: String): PaymentServiceAutho }.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. @@ -86,6 +98,26 @@ private fun buildGetRequest(connectorTransactionIdStr: String): PaymentServiceGe }.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 { @@ -102,6 +134,69 @@ fun processCheckoutAutocapture(txnId: String, config: ConnectorConfig = _default 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 { @@ -133,6 +228,16 @@ fun authorize(txnId: String, config: ConnectorConfig = _defaultConfig) { } } +// 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) @@ -180,16 +285,55 @@ fun proxyAuthorize(txnId: String, config: ConnectorConfig = _defaultConfig) { 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) - else -> System.err.println("Unknown flow: $flow. Available: processCheckoutAutocapture, processGetPayment, authorize, get, proxyAuthorize") + "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 index 0e0ab0221d..2ed68d80ca 100644 --- a/examples/citigate/citigate.py +++ b/examples/citigate/citigate.py @@ -8,9 +8,10 @@ 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", "get", "proxy_authorize"] +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), @@ -61,6 +62,16 @@ def _build_authorize_request(capture_method: str): ), ) +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. @@ -104,6 +115,31 @@ def _build_proxy_authorize_request(): 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) @@ -123,6 +159,78 @@ async def process_checkout_autocapture(merchant_transaction_id: str, config: sdk 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 @@ -154,6 +262,15 @@ async def process_authorize(merchant_transaction_id: str, config: sdk_config_pb2 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) @@ -171,6 +288,24 @@ async def process_proxy_authorize(merchant_transaction_id: str, config: sdk_conf 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}") diff --git a/examples/citigate/citigate.rs b/examples/citigate/citigate.rs index 8455a76b6a..e6a36f5bc3 100644 --- a/examples/citigate/citigate.rs +++ b/examples/citigate/citigate.rs @@ -14,7 +14,15 @@ use std::collections::HashMap; use std::str::FromStr; #[allow(dead_code)] -pub const SUPPORTED_FLOWS: &[&str] = &["authorize", "get", "proxy_authorize"]; +pub const SUPPORTED_FLOWS: &[&str] = &[ + "authorize", + "capture", + "get", + "proxy_authorize", + "refund", + "refund_get", + "void", +]; #[allow(dead_code)] fn build_client() -> ConnectorClient { @@ -86,6 +94,19 @@ pub fn build_authorize_request(capture_method: &str) -> PaymentServiceAuthorizeR } } +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. @@ -140,6 +161,37 @@ pub fn build_proxy_authorize_request() -> PaymentServiceProxyAuthorizeRequest { } } +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)] @@ -170,6 +222,131 @@ pub async fn process_checkout_autocapture( )) } +// 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)] @@ -228,6 +405,22 @@ pub async fn process_authorize( } } +// 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( @@ -256,6 +449,34 @@ pub async fn process_proxy_authorize( 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() { @@ -265,12 +486,18 @@ async fn main() { .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_get_payment, process_authorize, process_get, process_proxy_authorize", flow); + 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; } }; diff --git a/examples/citigate/citigate.ts b/examples/citigate/citigate.ts index b16b965d74..b2c2d6468d 100644 --- a/examples/citigate/citigate.ts +++ b/examples/citigate/citigate.ts @@ -5,9 +5,9 @@ // Citigate — all integration scenarios and flows in one file. // Run a scenario: npx tsx citigate.ts checkout_autocapture -import { PaymentClient, types } from 'hyperswitch-prism'; +import { PaymentClient, RefundClient, types } from 'hyperswitch-prism'; const { Environment, AuthenticationType, CaptureMethod, CardNetwork, CountryAlpha2, Currency } = types; -export const SUPPORTED_FLOWS = ["authorize", "get", "proxy_authorize"]; +export const SUPPORTED_FLOWS = ["authorize", "capture", "get", "proxy_authorize", "refund", "refund_get", "void"]; const _defaultConfig: types.IConnectorConfig = { options: { @@ -59,6 +59,17 @@ function _buildAuthorizeRequest(captureMethod: types.CaptureMethod): types.IPaym }; } +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. @@ -105,6 +116,34 @@ function _buildProxyAuthorizeRequest(): types.IPaymentServiceProxyAuthorizeReque }; } +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) @@ -126,6 +165,80 @@ async function processCheckoutAutocapture(merchantTransactionId: string, config: 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) { @@ -157,6 +270,15 @@ async function authorize(merchantTransactionId: string, config: types.IConnector 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); @@ -175,10 +297,37 @@ async function proxyAuthorize(merchantTransactionId: string, config: types.IConn 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, processGetPayment, authorize, get, proxyAuthorize, _buildAuthorizeRequest, _buildGetRequest, _buildProxyAuthorizeRequest + processCheckoutAutocapture, processCheckoutCard, processRefund, processVoidPayment, processGetPayment, authorize, capture, get, proxyAuthorize, refund, refundGet, voidPayment, _buildAuthorizeRequest, _buildCaptureRequest, _buildGetRequest, _buildProxyAuthorizeRequest, _buildRefundRequest, _buildRefundGetRequest, _buildVoidRequest }; // CLI runner From 51e8dbcd202487405a305cca7db44abd2ef2df42 Mon Sep 17 00:00:00 2001 From: shuklatushar226 Date: Fri, 14 Aug 2026 03:22:57 +0530 Subject: [PATCH 07/11] chore(connector): add citigate connector_specs manifest The connector list parity check (check_connector_specs, Phase 1) requires every connector with integration code to have a connector_specs/ directory. citigate was missing one, so the Compilation Check job failed with '1 missing spec dir(s)'. Declares the six suites citigate implements: Authorize, Get (PSync), Capture, Void, Refund and RefundService/Get (RSync). --- .../src/connector_specs/citigate/specs.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 crates/internal/integration-tests/src/connector_specs/citigate/specs.json 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" + ] +} From 5aba4ec69581de6a8fc1e35df39c4688b453d82c Mon Sep 17 00:00:00 2001 From: Tushar Shukla Date: Fri, 14 Aug 2026 19:34:57 +0530 Subject: [PATCH 08/11] =?UTF-8?q?fix(connector):=20citigate=20=E2=80=94=20?= =?UTF-8?q?enforce=20US-conditional=20fields,=20fix=20auth-stage=20statuse?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The API Card v1.27 field table (p. 14) qualifies three purchase-request fields with "Mandatory for Country = "US"" on top of their Y/R flag. A plain Option cannot express that, so all three conditions were unenforced: - StateProvince (Y) and Telephone (R) are now resolved against the billing country and use the required-variant helpers when it is US, so an incomplete US address fails in UCS with MissingRequiredField instead of being rejected by the gateway. Non-US billing keeps the previous optional behaviour, since most countries have no state. - DateOfBirth (R) is declared but always None: UCS carries no billing date of birth on the card Authorize path, so there is nothing to source it from. Declaring it keeps the gap visible at the field rather than silently absent from the wire format. Wiring it needs a billing-DOB on the proto/domain. Separately, ResponseCodes 106 (user aborted), 700 (no user redirect) and 800 (no user return) all fail at the authentication stage, before the bank is asked to authorize, but were reported as AuthorizationFailed/Failure. They now map to AuthenticationFailed alongside 103, in both failure_status and sync_failure_status. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/connectors/citigate/transformers.rs | 62 ++++++++++++++++--- 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/crates/integrations/connector-integration/src/connectors/citigate/transformers.rs b/crates/integrations/connector-integration/src/connectors/citigate/transformers.rs index 8851e0e0d3..37abee720a 100644 --- a/crates/integrations/connector-integration/src/connectors/citigate/transformers.rs +++ b/crates/integrations/connector-integration/src/connectors/citigate/transformers.rs @@ -51,6 +51,12 @@ const TRANS_TYPE_ID_STATUS_CHECK: &str = "8"; 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 @@ -226,6 +232,13 @@ pub struct CitigatePaymentsRequest { 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 @@ -297,6 +310,26 @@ impl AttemptStatus { match self.response_code() { - // Cardholder failed / abandoned authentication at the ACS, or the - // gateway asked for a redirect without telling us where to. - RESPONSE_CODE_3D_AUTH_FAILURE | RESPONSE_CODE_REDIRECT_REQUIRED => { - AttemptStatus::AuthenticationFailed - } + // 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, } } @@ -578,7 +619,10 @@ impl CitigatePaymentsResponse { /// `MerchantName` + `MerchantPassword` + `MerchantRef` triple matched nothing. fn sync_failure_status(&self) -> AttemptStatus { match self.response_code() { - RESPONSE_CODE_3D_AUTH_FAILURE => AttemptStatus::AuthenticationFailed, + RESPONSE_CODE_3D_AUTH_FAILURE + | RESPONSE_CODE_USER_ABORTED + | RESPONSE_CODE_NO_USER_REDIRECT + | RESPONSE_CODE_NO_USER_RETURN => AttemptStatus::AuthenticationFailed, _ => AttemptStatus::Failure, } } From 7cdb5ba05e5bd040db280305f3267aaf3ea6e29f Mon Sep 17 00:00:00 2001 From: "hyperswitch-bot[bot]" <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 14:26:06 +0000 Subject: [PATCH 09/11] chore: auto-fix formatting and generated code Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions. --- data/field_probe/citigate.json | 14 ++++++++++---- docs-generated/connectors/citigate.md | 24 ++++++++++++------------ examples/citigate/citigate.kt | 6 ++++++ examples/citigate/citigate.py | 6 ++++++ examples/citigate/citigate.rs | 6 ++++++ examples/citigate/citigate.ts | 10 ++++++++-- 6 files changed, 48 insertions(+), 18 deletions(-) diff --git a/data/field_probe/citigate.json b/data/field_probe/citigate.json index f56450a56a..00f1c35ea3 100644 --- a/data/field_probe/citigate.json +++ b/data/field_probe/citigate.json @@ -120,9 +120,12 @@ "last_name": "Doe", "line1": "123 Main St", "city": "Seattle", + "state": "WA", "zip_code": "98101", "country_alpha2_code": "US", - "email": "test@example.com" + "email": "test@example.com", + "phone_number": "4155552671", + "phone_country_code": "+1" } }, "auth_type": "NO_THREE_DS", @@ -138,7 +141,7 @@ "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\",\"Country\":\"US\",\"Email\":\"test@example.com\",\"UserIP\":\"1.2.3.4\",\"SuccessURL\":\"https://example.com/return\",\"FailURL\":\"https://example.com/return\"}" + "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": { @@ -655,9 +658,12 @@ "last_name": "Doe", "line1": "123 Main St", "city": "Seattle", + "state": "WA", "zip_code": "98101", "country_alpha2_code": "US", - "email": "test@example.com" + "email": "test@example.com", + "phone_number": "4155552671", + "phone_country_code": "+1" } }, "capture_method": "AUTOMATIC", @@ -674,7 +680,7 @@ "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\",\"Country\":\"US\",\"Email\":\"test@example.com\",\"UserIP\":\"1.2.3.4\",\"SuccessURL\":\"https://example.com/return\",\"FailURL\":\"https://example.com/return\"}" + "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\"}" } } }, diff --git a/docs-generated/connectors/citigate.md b/docs-generated/connectors/citigate.md index bbf8949c38..9687accf6e 100644 --- a/docs-generated/connectors/citigate.md +++ b/docs-generated/connectors/citigate.md @@ -127,7 +127,7 @@ Simple payment that authorizes and captures in one call. Use for immediate charg | `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#L143) · [JavaScript](../../examples/citigate/citigate.js) · [Kotlin](../../examples/citigate/citigate.kt#L123) · [Rust](../../examples/citigate/citigate.rs#L179) +**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) @@ -141,25 +141,25 @@ Two-step card payment. First authorize, then capture. Use when you need to verif | `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#L162) · [JavaScript](../../examples/citigate/citigate.js) · [Kotlin](../../examples/citigate/citigate.kt#L139) · [Rust](../../examples/citigate/citigate.rs#L195) +**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#L187) · [JavaScript](../../examples/citigate/citigate.js) · [Kotlin](../../examples/citigate/citigate.kt#L161) · [Rust](../../examples/citigate/citigate.rs#L218) +**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#L212) · [JavaScript](../../examples/citigate/citigate.js) · [Kotlin](../../examples/citigate/citigate.kt#L183) · [Rust](../../examples/citigate/citigate.rs#L241) +**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#L234) · [JavaScript](../../examples/citigate/citigate.js) · [Kotlin](../../examples/citigate/citigate.kt#L202) · [Rust](../../examples/citigate/citigate.rs#L260) +**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 @@ -305,7 +305,7 @@ Authorize a payment amount on a payment method. This reserves funds without capt } ``` -**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts#L265) · [Kotlin](../../examples/citigate/citigate.kt#L220) · [Rust](../../examples/citigate/citigate.rs) +**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 @@ -316,7 +316,7 @@ Finalize an authorized payment by transferring funds. Captures the authorized am | **Request** | `PaymentServiceCaptureRequest` | | **Response** | `PaymentServiceCaptureResponse` | -**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts#L274) · [Kotlin](../../examples/citigate/citigate.kt#L232) · [Rust](../../examples/citigate/citigate.rs) +**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 @@ -327,7 +327,7 @@ Retrieve current payment status from the payment processor. Enables synchronizat | **Request** | `PaymentServiceGetRequest` | | **Response** | `PaymentServiceGetResponse` | -**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts#L283) · [Kotlin](../../examples/citigate/citigate.kt#L242) · [Rust](../../examples/citigate/citigate.rs) +**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 @@ -338,7 +338,7 @@ Authorize using vault-aliased card data. Proxy substitutes before connector. | **Request** | `PaymentServiceProxyAuthorizeRequest` | | **Response** | `PaymentServiceAuthorizeResponse` | -**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts#L292) · [Kotlin](../../examples/citigate/citigate.kt#L250) · [Rust](../../examples/citigate/citigate.rs) +**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 @@ -349,7 +349,7 @@ Process a partial or full refund for a captured payment. Returns funds to the cu | **Request** | `PaymentServiceRefundRequest` | | **Response** | `RefundResponse` | -**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts#L301) · [Kotlin](../../examples/citigate/citigate.kt#L289) · [Rust](../../examples/citigate/citigate.rs) +**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 @@ -360,7 +360,7 @@ Cancel an authorized payment that has not been captured. Releases held funds bac | **Request** | `PaymentServiceVoidRequest` | | **Response** | `PaymentServiceVoidResponse` | -**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts) · [Kotlin](../../examples/citigate/citigate.kt#L311) · [Rust](../../examples/citigate/citigate.rs) +**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts) · [Kotlin](../../examples/citigate/citigate.kt#L317) · [Rust](../../examples/citigate/citigate.rs) ### Refunds @@ -373,4 +373,4 @@ Retrieve refund status from the payment processor. Tracks refund progress throug | **Request** | `RefundServiceGetRequest` | | **Response** | `RefundResponse` | -**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts#L310) · [Kotlin](../../examples/citigate/citigate.kt#L299) · [Rust](../../examples/citigate/citigate.rs) +**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/examples/citigate/citigate.kt b/examples/citigate/citigate.kt index dec3e136cf..b8158a1b0d 100644 --- a/examples/citigate/citigate.kt +++ b/examples/citigate/citigate.kt @@ -63,9 +63,12 @@ private fun buildAuthorizeRequest(captureMethodStr: String): PaymentServiceAutho 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. @@ -269,9 +272,12 @@ fun proxyAuthorize(txnId: String, config: ConnectorConfig = _defaultConfig) { 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 diff --git a/examples/citigate/citigate.py b/examples/citigate/citigate.py index 2ed68d80ca..fd81ec7976 100644 --- a/examples/citigate/citigate.py +++ b/examples/citigate/citigate.py @@ -50,9 +50,12 @@ def _build_authorize_request(capture_method: str): 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. @@ -103,9 +106,12 @@ def _build_proxy_authorize_request(): 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"), diff --git a/examples/citigate/citigate.rs b/examples/citigate/citigate.rs index e6a36f5bc3..22751a8f2a 100644 --- a/examples/citigate/citigate.rs +++ b/examples/citigate/citigate.rs @@ -77,9 +77,12 @@ pub fn build_authorize_request(capture_method: &str) -> PaymentServiceAuthorizeR 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() @@ -143,9 +146,12 @@ pub fn build_proxy_authorize_request() -> PaymentServiceProxyAuthorizeRequest { 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() diff --git a/examples/citigate/citigate.ts b/examples/citigate/citigate.ts index b2c2d6468d..3151497cf7 100644 --- a/examples/citigate/citigate.ts +++ b/examples/citigate/citigate.ts @@ -46,9 +46,12 @@ function _buildAuthorizeRequest(captureMethod: types.CaptureMethod): types.IPaym "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. + "email": {"value": "test@example.com"}, // Contact Information. + "phoneNumber": {"value": "4155552671"}, + "phoneCountryCode": "+1" } }, "authType": AuthenticationType.NO_THREE_DS, // Authentication Details. @@ -102,9 +105,12 @@ function _buildProxyAuthorizeRequest(): types.IPaymentServiceProxyAuthorizeReque "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. + "email": {"value": "test@example.com"}, // Contact Information. + "phoneNumber": {"value": "4155552671"}, + "phoneCountryCode": "+1" } }, "captureMethod": CaptureMethod.AUTOMATIC, From bbd0bc090f0bf3b3f550145ce2737e85918ee11b Mon Sep 17 00:00:00 2001 From: Tushar Shukla Date: Sun, 16 Aug 2026 03:16:21 +0530 Subject: [PATCH 10/11] =?UTF-8?q?fix(config):=20citigate=20=E2=80=94=20poi?= =?UTF-8?q?nt=20production=20at=20the=20live=20gateway,=20not=20the=20sand?= =?UTF-8?q?box?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit config/production.toml carried the sandbox host, so production traffic would have been posted to https://gw-test.cgate.tech/orion/interface/json.ashx. The two hosts are separate infrastructure (gw.cgate.tech 37.35.89.132, gw-test.cgate.tech 20.68.195.195), so this was a real misroute rather than an alias. Hyperswitch's own config/deployments/production.toml already carries https://gw.cgate.tech, but that entry is inert for citigate: the connector is in ucs_only_connectors, so HS never issues the HTTP call, and it builds the UCS config as Citigate { api_key, key1, base_url: None }. With no override, base_url_override() yields None, no connector patch is applied, and the transformer reads UCS's own TOML. That made this file the authoritative one. sandbox.toml and development.toml keep gw-test.cgate.tech, which is correct. Note: the API Card v1.27 does not document a production interface URL — Appendix 2 lists test URLs only, and gw.cgate.tech appears solely inside sample RedirectURL response values. This aligns UCS with the host Hyperswitch already assumes; the value should be confirmed with Citigate before production traffic is enabled. Co-Authored-By: Claude Opus 5 (1M context) --- config/production.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/production.toml b/config/production.toml index ba7d54ec89..0eced30009 100644 --- a/config/production.toml +++ b/config/production.toml @@ -23,7 +23,7 @@ connector_request_timeout = 30 bypass_urls = ["localhost", "local"] [connectors] -citigate.base_url = "https://gw-test.cgate.tech" +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" From 5e49f2eece3dcb0ec0dadb63c25223d87a7111f3 Mon Sep 17 00:00:00 2001 From: Tushar Shukla Date: Mon, 17 Aug 2026 19:24:54 +0530 Subject: [PATCH 11/11] =?UTF-8?q?fix(connector):=20citigate=20=E2=80=94=20?= =?UTF-8?q?replace=20dead=20partial=20capture/void=20guards?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both guards read `PaymentFlowData::minor_amount_authorized`, which is a response-reporting field: all 23 request-path constructors in `domain_types::types` set it to `None`, and no proto request message carries an authorised amount. Neither guard could ever fire, so a partial capture or void was silently settled/voided in full and reported as success. Capture now rejects the partial/multi-capture intents that do survive the wire — `multiple_capture_data` (already handled) and a `capture_method` of `manual_multiple` or `scheduled`. Fail-closed on the authorised amount is not an option: since the field is unconditionally `None`, it would reject every capture including full ones. The remaining gap — a lone `manual` capture for less than the authorisation — is not detectable at this layer and is documented as such at both call sites. Closing it needs the authorised amount added to `PaymentServiceCaptureRequest`. --- .../src/connectors/citigate/transformers.rs | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/crates/integrations/connector-integration/src/connectors/citigate/transformers.rs b/crates/integrations/connector-integration/src/connectors/citigate/transformers.rs index 37abee720a..cd38ce6325 100644 --- a/crates/integrations/connector-integration/src/connectors/citigate/transformers.rs +++ b/crates/integrations/connector-integration/src/connectors/citigate/transformers.rs @@ -12,7 +12,7 @@ use std::collections::HashMap; -use common_enums::{AttemptStatus, AuthenticationType, CardNetwork, RefundStatus}; +use common_enums::{AttemptStatus, AuthenticationType, CaptureMethod, CardNetwork, RefundStatus}; use common_utils::{pii::Email, types::StringMinorUnit, Method}; use domain_types::{ connector_flow::{Authorize, Capture, PSync, RSync, Refund, Void}, @@ -975,15 +975,24 @@ impl