diff --git a/.typos.toml b/.typos.toml index 016a1f3f56..8d7880815a 100644 --- a/.typos.toml +++ b/.typos.toml @@ -24,7 +24,9 @@ caf = "caf" # part of café (URL-encoding / Unicode examples) Automatico = "Automatico" # Brazilian Portuguese — Globalgetnet's PIX_AUTOMATICO API method AUTOMATICO = "AUTOMATICO" # Brazilian Portuguese — Globalgetnet's PIX_AUTOMATICO API method UE = "UE" # Hyperswitch internal error code prefix (e.g. UE_9000) -Payed = "Payed" # Santander API returns "PAYED" as a status +Payed = "Payed" # Santander API returns "PAYED" as a status +Threed = "Threed" # Ilixium spells its 3DS-completion operation type "THREED_SECURE_COMPLETE" +THREED = "THREED" # Ilixium spells its 3DS-completion operation type "THREED_SECURE_COMPLETE" [default.extend-identifiers] ACI = "ACI" # Name of a connector diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ac058f850..babc4ed5e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,67 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - - - +## 2026.08.19.0 + +### Features + +- **connector:** Moneris Connector Integration ([#2127](https://github.com/juspay/connector-service/pull/2127)) ([`7aa0b8b`](https://github.com/juspay/connector-service/commit/7aa0b8b69cf23ffa5d721707b839e7a80add590d)) +- **shadow:** Add x-org-id header to outgoing shadow mode requests ([#2148](https://github.com/juspay/connector-service/pull/2148)) ([`d452e76`](https://github.com/juspay/connector-service/commit/d452e769c5acce8fca48ca1a45124d7b03fb0c4d)) + +**Full Changelog:** [`2026.08.18.1...2026.08.19.0`](https://github.com/juspay/connector-service/compare/2026.08.18.1...2026.08.19.0) + +- - - + +## 2026.08.18.1 + +### Features + +- **connector:** Implement Authorize (Card, non-3DS + 3DS redirect), PSync, Capture, Void, Refund and RSync for citigate ([#2124](https://github.com/juspay/connector-service/pull/2124)) ([`6afccff`](https://github.com/juspay/connector-service/commit/6afccff73895a7fdc752c180d665148bfe46a8b3)) +- **qwikcilver:** Add payment method eligibility check ([#2135](https://github.com/juspay/connector-service/pull/2135)) ([`91d0a3d`](https://github.com/juspay/connector-service/commit/91d0a3d9c11adf95b6c9ea4d7eff743632043203)) +- **tamara:** Override prod with sbx url ([#2147](https://github.com/juspay/connector-service/pull/2147)) ([`f88c0a3`](https://github.com/juspay/connector-service/commit/f88c0a3f07b76c426ed5dd7e6a10a35e5245f8fc)) + +### Documentation + +- **sdk/payout-service:** Add per-operation docs for java, node, pyth… ([#2102](https://github.com/juspay/connector-service/pull/2102)) ([`b18837d`](https://github.com/juspay/connector-service/commit/b18837da41957ab7d0d5f3d3147367f4f1859d0e)) + +**Full Changelog:** [`2026.08.18.0...2026.08.18.1`](https://github.com/juspay/connector-service/compare/2026.08.18.0...2026.08.18.1) + +- - - + +## 2026.08.18.0 + +### Features + +- **connector:** [Truelayer] Implementation ([#2047](https://github.com/juspay/connector-service/pull/2047)) ([`c108428`](https://github.com/juspay/connector-service/commit/c10842866847364522aa838db0f9fecc6ea01e0e)) +- **logs:** Euler-parity golden-line sources + mapping config (on #2075) ([#2076](https://github.com/juspay/connector-service/pull/2076)) ([`4d43c1f`](https://github.com/juspay/connector-service/commit/4d43c1f9f668e4802194e9678552e19f5ddd5337)) + +### Bug Fixes + +- **maya:** Capture missing response fields + surface raw connector response ([#2134](https://github.com/juspay/connector-service/pull/2134)) ([`317e347`](https://github.com/juspay/connector-service/commit/317e34743d61a5c87b59f32893194e8f7f93e51c)) +- **sync:** Return merchant_transaction_id on failed payment sync response ([#2141](https://github.com/juspay/connector-service/pull/2141)) ([`a5a726d`](https://github.com/juspay/connector-service/commit/a5a726d59c1f15082fc6ac56096944ad4bd5ac55)) + +### Refactors + +- **deps:** Make log_utils optional and fix default log format visibility ([#2137](https://github.com/juspay/connector-service/pull/2137)) ([`c470753`](https://github.com/juspay/connector-service/commit/c470753df1cd561d6c21eb3e5d3bebf973a300e0)) + +**Full Changelog:** [`2026.08.17.0...2026.08.18.0`](https://github.com/juspay/connector-service/compare/2026.08.17.0...2026.08.18.0) + +- - - + +## 2026.08.17.0 + +### Features + +- **connector:** [Truelayer] Add support for Returning Customer Flow ([#1458](https://github.com/juspay/connector-service/pull/1458)) ([`7bbd9e2`](https://github.com/juspay/connector-service/commit/7bbd9e26c2f2e05bb48d16ae37914777326bb580)) + +### Documentation + +- **grace:** Require superposition URL registration + URL patching for new connectors ([#2123](https://github.com/juspay/connector-service/pull/2123)) ([`00ff139`](https://github.com/juspay/connector-service/commit/00ff139ab81b7eaff6d26129db9da240bf5ff658)) + +**Full Changelog:** [`2026.08.14.1...2026.08.17.0`](https://github.com/juspay/connector-service/compare/2026.08.14.1...2026.08.17.0) + +- - - + ## 2026.08.14.1 ### Bug Fixes diff --git a/Cargo.lock b/Cargo.lock index e0458e5bf4..e02e9c8825 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -861,7 +861,7 @@ dependencies = [ [[package]] name = "build_info" version = "0.1.0" -source = "git+https://github.com/juspay/framework-libs-rs?rev=76613f5d0c72fd3d26a3403ae859c2a7545e8dd2#76613f5d0c72fd3d26a3403ae859c2a7545e8dd2" +source = "git+https://github.com/juspay/framework-libs-rs?rev=3cdcc24b461917b29641942b04f3aef8110b1adf#3cdcc24b461917b29641942b04f3aef8110b1adf" dependencies = [ "cargo_metadata 0.23.1", "vergen-gix", @@ -4570,7 +4570,7 @@ dependencies = [ [[package]] name = "log_utils" version = "0.1.0" -source = "git+https://github.com/juspay/framework-libs-rs?rev=76613f5d0c72fd3d26a3403ae859c2a7545e8dd2#76613f5d0c72fd3d26a3403ae859c2a7545e8dd2" +source = "git+https://github.com/juspay/framework-libs-rs?rev=3cdcc24b461917b29641942b04f3aef8110b1adf#3cdcc24b461917b29641942b04f3aef8110b1adf" dependencies = [ "gethostname 1.1.0", "rustc-hash 2.1.3", diff --git a/config/development.toml b/config/development.toml index 33e7606756..325a665d84 100644 --- a/config/development.toml +++ b/config/development.toml @@ -58,6 +58,8 @@ psync = "GW_TXN_SYNC" connectors_with_webhook_source_verification_call = "paypal, truelayer" [connectors] +citigate.base_url = "https://gw-test.cgate.tech" +ilixium.base_url = "https://prprocessing.ilixium.com/platform/ili" 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" @@ -138,6 +140,7 @@ novalnet.base_url = "https://payport.novalnet.de/v2" nexinets.base_url = "https://apitest.payengine.de/v1" noon.base_url = "https://api-test.noonpayments.com/" mifinity.base_url = "https://demo.mifinity.com/" +moneris.base_url = "https://api.sb.moneris.io" braintree.base_url = "https://payments.sandbox.braintree-api.com/graphql" volt.base_url = "https://gateway.sandbox.volt.io" volt.secondary_base_url = "https://api.sandbox.volt.io" diff --git a/config/production.toml b/config/production.toml index 0bbd88d995..8375c5a706 100644 --- a/config/production.toml +++ b/config/production.toml @@ -23,6 +23,8 @@ connector_request_timeout = 30 bypass_urls = ["localhost", "local"] [connectors] +citigate.base_url = "https://gw.cgate.tech" +ilixium.base_url = "https://prprocessing.ilixium.com/platform/ili" 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" @@ -100,6 +102,7 @@ novalnet.base_url = "https://payport.novalnet.de/v2" nexinets.base_url = "https://api.payengine.de/v1" noon.base_url = "https://api.noonpayments.com/" mifinity.base_url = "https://secure.mifinity.com/" +moneris.base_url = "https://api.moneris.io" braintree.base_url = "https://payments.braintree-api.com/graphql" volt.base_url = "https://gateway.volt.io" volt.secondary_base_url = "https://api.volt.io" diff --git a/config/sandbox.toml b/config/sandbox.toml index cb000a3a7b..65749164b8 100644 --- a/config/sandbox.toml +++ b/config/sandbox.toml @@ -23,6 +23,8 @@ connector_request_timeout = 30 bypass_urls = ["localhost", "local"] [connectors] +citigate.base_url = "https://gw-test.cgate.tech" +ilixium.base_url = "https://prprocessing.ilixium.com/platform/ili" 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" @@ -101,6 +103,7 @@ novalnet.base_url = "https://payport.novalnet.de/v2" nexinets.base_url = "https://apitest.payengine.de/v1" noon.base_url = "https://api-test.noonpayments.com/" mifinity.base_url = "https://demo.mifinity.com/" +moneris.base_url = "https://api.sb.moneris.io" braintree.base_url = "https://payments.sandbox.braintree-api.com/graphql" volt.base_url = "https://gateway.sandbox.volt.io" volt.secondary_base_url = "https://api.sandbox.volt.io" diff --git a/config/superposition.toml b/config/superposition.toml index a217782571..65f17b54fc 100644 --- a/config/superposition.toml +++ b/config/superposition.toml @@ -85,7 +85,7 @@ api_tags_tags_psync = { value = "", schema = { type = "string" } } environment = { position = 1, schema = { type = "string", enum = ["sandbox", "development", "production"] } } # Connector dimension - determines which payment processor URLs to use -connector = { position = 2, schema = { type = "string", enum = ["glomopay", "stripe", "adyen", "paypal", "razorpay", "checkout", "braintree", "worldpay", "cybersource", "payme", "bluesnap", "fiserv", "trustpay", "volt", "hipay", "xendit", "phonepe", "cashfree", "payu", "paytm", "dlocal", "rapyd", "aci", "nuvei", "forte", "authorizedotnet", "elavon", "bamboraapac", "billwerk", "paysafe", "worldpayvantiv", "worldpayxml", "tsys", "tsys_transit", "bankofamerica", "powertranz", "revolut", "airwallex", "bambora", "shift4", "multisafepay", "iatapay", "jpmorgan", "nmi", "nexixpay", "authipay", "stax", "trustpayments", "fiservemea", "datatrans", "silverflow", "celero", "globalpay", "razorpayv2", "fiuu", "payload", "cashtocode", "novalnet", "nexinets", "noon", "mifinity", "calida", "cryptopay", "helcim", "placetopay", "barclaycard", "gigadat", "loonio", "mollie", "absa_sanlam", "truelayer", "trustly", "twoc_twop_paco", "zift", "revolv3", "fiservcommercehub", "finix", "wellsfargo", "imerchantsolutions", "qwikcilver", "flywire", "boost", "plaid", "grabpay", "maya"] } } +connector = { position = 2, schema = { type = "string", enum = ["glomopay", "stripe", "adyen", "paypal", "razorpay", "checkout", "braintree", "worldpay", "cybersource", "payme", "bluesnap", "fiserv", "trustpay", "volt", "hipay", "xendit", "phonepe", "cashfree", "payu", "paytm", "dlocal", "rapyd", "aci", "nuvei", "forte", "authorizedotnet", "elavon", "bamboraapac", "billwerk", "paysafe", "worldpayvantiv", "worldpayxml", "tsys", "tsys_transit", "bankofamerica", "powertranz", "revolut", "airwallex", "bambora", "shift4", "multisafepay", "iatapay", "jpmorgan", "nmi", "nexixpay", "authipay", "stax", "trustpayments", "fiservemea", "datatrans", "silverflow", "celero", "globalpay", "razorpayv2", "fiuu", "payload", "cashtocode", "novalnet", "nexinets", "noon", "mifinity", "calida", "cryptopay", "helcim", "placetopay", "barclaycard", "gigadat", "loonio", "mollie", "moneris", "absa_sanlam", "truelayer", "trustly", "twoc_twop_paco", "zift", "revolv3", "fiservcommercehub", "finix", "wellsfargo", "imerchantsolutions", "qwikcilver", "flywire", "boost", "plaid", "grabpay", "maya", "tamara", "ilixium"] } } # ============================================================================ # Development Environment Overrides @@ -342,6 +342,11 @@ connector_base_url = "https://sandbox-merchant.revolut.com/" _context_ = { connector = "airwallex" } connector_base_url = "https://api-demo.airwallex.com/api/v1" +# Tamara +[[overrides]] +_context_ = { connector = "tamara" } +connector_base_url = "https://api-sandbox.tamara.co" + # Bambora [[overrides]] _context_ = { connector = "bambora" } @@ -580,6 +585,13 @@ connector_base_url = "https://api.zift.io/" _context_ = { connector = "boost" } connector_base_url = "https://stage-api.boostconnect.biz/gateway" +# Ilixium Direct API — sandbox host. Ilixium does not publish a production +# host; it is supplied per merchant at onboarding, so there is deliberately no +# production override here (see tech spec UNDECIDED #4). +[[overrides]] +_context_ = { connector = "ilixium" } +connector_base_url = "https://prprocessing.ilixium.com/platform/ili" + # ============================================================================ # Production Environment Overrides # ============================================================================ @@ -595,6 +607,11 @@ connector_dispute_base_url = "https://{{merchant_endpoint_prefix}}-ca-live.adyen _context_ = { connector = "paypal", environment = "production" } connector_base_url = "https://api-m.paypal.com/" +# Tamara Production +[[overrides]] +_context_ = { connector = "tamara", environment = "production" } +connector_base_url = "https://api.tamara.co" + # Braintree Production [[overrides]] _context_ = { connector = "braintree", environment = "production" } @@ -938,4 +955,14 @@ connector_base_url = "https://partner-api.grab.com/grabpay/partner/v2" # Maya Production [[overrides]] _context_ = { connector = "maya", environment = "production" } -connector_base_url = "https://pg.maya.ph" \ No newline at end of file +connector_base_url = "https://pg.maya.ph" + +# Moneris +[[overrides]] +_context_ = { connector = "moneris" } +connector_base_url = "https://api.sb.moneris.io" + +# Moneris Production +[[overrides]] +_context_ = { connector = "moneris", environment = "production" } +connector_base_url = "https://api.moneris.io" \ No newline at end of file diff --git a/crates/common/common_enums/src/enums.rs b/crates/common/common_enums/src/enums.rs index 528aedc53b..001bd3bbb5 100644 --- a/crates/common/common_enums/src/enums.rs +++ b/crates/common/common_enums/src/enums.rs @@ -1781,6 +1781,273 @@ pub enum CountryAlpha2 { US } +/// Full ISO country names. Ported from hyperswitch so connectors (e.g. Trustly's +/// ClearingHouse) that require the English country name can derive it from a +/// [`CountryAlpha2`] via [`Country::from_alpha2`]. +#[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + strum::Display, + strum::EnumString, + Serialize, + Deserialize, +)] +pub enum Country { + Afghanistan, + AlandIslands, + Albania, + Algeria, + AmericanSamoa, + Andorra, + Angola, + Anguilla, + Antarctica, + AntiguaAndBarbuda, + Argentina, + Armenia, + Aruba, + Australia, + Austria, + Azerbaijan, + Bahamas, + Bahrain, + Bangladesh, + Barbados, + Belarus, + Belgium, + Belize, + Benin, + Bermuda, + Bhutan, + BoliviaPlurinationalState, + BonaireSintEustatiusAndSaba, + BosniaAndHerzegovina, + Botswana, + BouvetIsland, + Brazil, + BritishIndianOceanTerritory, + BruneiDarussalam, + Bulgaria, + BurkinaFaso, + Burundi, + CaboVerde, + Cambodia, + Cameroon, + Canada, + CaymanIslands, + CentralAfricanRepublic, + Chad, + Chile, + China, + ChristmasIsland, + CocosKeelingIslands, + Colombia, + Comoros, + Congo, + CongoDemocraticRepublic, + CookIslands, + CostaRica, + CotedIvoire, + Croatia, + Cuba, + Curacao, + Cyprus, + Czechia, + Denmark, + Djibouti, + Dominica, + DominicanRepublic, + Ecuador, + Egypt, + ElSalvador, + EquatorialGuinea, + Eritrea, + Estonia, + Ethiopia, + FalklandIslandsMalvinas, + FaroeIslands, + Fiji, + Finland, + France, + FrenchGuiana, + FrenchPolynesia, + FrenchSouthernTerritories, + Gabon, + Gambia, + Georgia, + Germany, + Ghana, + Gibraltar, + Greece, + Greenland, + Grenada, + Guadeloupe, + Guam, + Guatemala, + Guernsey, + Guinea, + GuineaBissau, + Guyana, + Haiti, + HeardIslandAndMcDonaldIslands, + HolySee, + Honduras, + HongKong, + Hungary, + Iceland, + India, + Indonesia, + IranIslamicRepublic, + Iraq, + Ireland, + IsleOfMan, + Israel, + Italy, + Jamaica, + Japan, + Jersey, + Jordan, + Kazakhstan, + Kenya, + Kiribati, + KoreaDemocraticPeoplesRepublic, + KoreaRepublic, + Kuwait, + Kyrgyzstan, + LaoPeoplesDemocraticRepublic, + Latvia, + Lebanon, + Lesotho, + Liberia, + Libya, + Liechtenstein, + Lithuania, + Luxembourg, + Macao, + MacedoniaTheFormerYugoslavRepublic, + Madagascar, + Malawi, + Malaysia, + Maldives, + Mali, + Malta, + MarshallIslands, + Martinique, + Mauritania, + Mauritius, + Mayotte, + Mexico, + MicronesiaFederatedStates, + MoldovaRepublic, + Monaco, + Mongolia, + Montenegro, + Montserrat, + Morocco, + Mozambique, + Myanmar, + Namibia, + Nauru, + Nepal, + Netherlands, + NewCaledonia, + NewZealand, + Nicaragua, + Niger, + Nigeria, + Niue, + NorfolkIsland, + NorthernMarianaIslands, + Norway, + Oman, + Pakistan, + Palau, + PalestineState, + Panama, + PapuaNewGuinea, + Paraguay, + Peru, + Philippines, + Pitcairn, + Poland, + Portugal, + PuertoRico, + Qatar, + Reunion, + Romania, + RussianFederation, + Rwanda, + SaintBarthelemy, + SaintHelenaAscensionAndTristandaCunha, + SaintKittsAndNevis, + SaintLucia, + SaintMartinFrenchpart, + SaintPierreAndMiquelon, + SaintVincentAndTheGrenadines, + Samoa, + SanMarino, + SaoTomeAndPrincipe, + SaudiArabia, + Senegal, + Serbia, + Seychelles, + SierraLeone, + Singapore, + SintMaartenDutchpart, + Slovakia, + Slovenia, + SolomonIslands, + Somalia, + SouthAfrica, + SouthGeorgiaAndTheSouthSandwichIslands, + SouthSudan, + Spain, + SriLanka, + Sudan, + Suriname, + SvalbardAndJanMayen, + Swaziland, + Sweden, + Switzerland, + SyrianArabRepublic, + TaiwanProvinceOfChina, + Tajikistan, + TanzaniaUnitedRepublic, + Thailand, + TimorLeste, + Togo, + Tokelau, + Tonga, + TrinidadAndTobago, + Tunisia, + Turkey, + Turkmenistan, + TurksAndCaicosIslands, + Tuvalu, + Uganda, + Ukraine, + UnitedArabEmirates, + UnitedKingdomOfGreatBritainAndNorthernIreland, + UnitedStatesOfAmerica, + UnitedStatesMinorOutlyingIslands, + Uruguay, + Uzbekistan, + Vanuatu, + VenezuelaBolivarianRepublic, + Vietnam, + VirginIslandsBritish, + VirginIslandsUS, + WallisAndFutuna, + WesternSahara, + Yemen, + Zambia, + Zimbabwe, +} + #[derive(Clone, Copy, Eq, Hash, PartialEq, Debug, Serialize, Deserialize, strum::Display)] #[rustfmt::skip] pub enum CountryAlpha3 { diff --git a/crates/common/common_enums/src/transformers.rs b/crates/common/common_enums/src/transformers.rs index e634a7f46d..9b3ac42242 100644 --- a/crates/common/common_enums/src/transformers.rs +++ b/crates/common/common_enums/src/transformers.rs @@ -1,4 +1,4 @@ -use crate::enums::{CountryAlpha2, CountryAlpha3}; +use crate::enums::{Country, CountryAlpha2, CountryAlpha3}; impl CountryAlpha2 { pub const fn from_alpha2_to_alpha3(code: Self) -> CountryAlpha3 { @@ -510,3 +510,259 @@ impl CountryAlpha2 { } } } + +impl Country { + pub const fn from_alpha2(code: CountryAlpha2) -> Self { + match code { + CountryAlpha2::AF => Self::Afghanistan, + CountryAlpha2::AX => Self::AlandIslands, + CountryAlpha2::AL => Self::Albania, + CountryAlpha2::DZ => Self::Algeria, + CountryAlpha2::AS => Self::AmericanSamoa, + CountryAlpha2::AD => Self::Andorra, + CountryAlpha2::AO => Self::Angola, + CountryAlpha2::AI => Self::Anguilla, + CountryAlpha2::AQ => Self::Antarctica, + CountryAlpha2::AG => Self::AntiguaAndBarbuda, + CountryAlpha2::AR => Self::Argentina, + CountryAlpha2::AM => Self::Armenia, + CountryAlpha2::AW => Self::Aruba, + CountryAlpha2::AU => Self::Australia, + CountryAlpha2::AT => Self::Austria, + CountryAlpha2::AZ => Self::Azerbaijan, + CountryAlpha2::BS => Self::Bahamas, + CountryAlpha2::BH => Self::Bahrain, + CountryAlpha2::BD => Self::Bangladesh, + CountryAlpha2::BB => Self::Barbados, + CountryAlpha2::BY => Self::Belarus, + CountryAlpha2::BE => Self::Belgium, + CountryAlpha2::BZ => Self::Belize, + CountryAlpha2::BJ => Self::Benin, + CountryAlpha2::BM => Self::Bermuda, + CountryAlpha2::BT => Self::Bhutan, + CountryAlpha2::BO => Self::BoliviaPlurinationalState, + CountryAlpha2::BQ => Self::BonaireSintEustatiusAndSaba, + CountryAlpha2::BA => Self::BosniaAndHerzegovina, + CountryAlpha2::BW => Self::Botswana, + CountryAlpha2::BV => Self::BouvetIsland, + CountryAlpha2::BR => Self::Brazil, + CountryAlpha2::IO => Self::BritishIndianOceanTerritory, + CountryAlpha2::BN => Self::BruneiDarussalam, + CountryAlpha2::BG => Self::Bulgaria, + CountryAlpha2::BF => Self::BurkinaFaso, + CountryAlpha2::BI => Self::Burundi, + CountryAlpha2::CV => Self::CaboVerde, + CountryAlpha2::KH => Self::Cambodia, + CountryAlpha2::CM => Self::Cameroon, + CountryAlpha2::CA => Self::Canada, + CountryAlpha2::KY => Self::CaymanIslands, + CountryAlpha2::CF => Self::CentralAfricanRepublic, + CountryAlpha2::TD => Self::Chad, + CountryAlpha2::CL => Self::Chile, + CountryAlpha2::CN => Self::China, + CountryAlpha2::CX => Self::ChristmasIsland, + CountryAlpha2::CC => Self::CocosKeelingIslands, + CountryAlpha2::CO => Self::Colombia, + CountryAlpha2::KM => Self::Comoros, + CountryAlpha2::CG => Self::Congo, + CountryAlpha2::CD => Self::CongoDemocraticRepublic, + CountryAlpha2::CK => Self::CookIslands, + CountryAlpha2::CR => Self::CostaRica, + CountryAlpha2::CI => Self::CotedIvoire, + CountryAlpha2::HR => Self::Croatia, + CountryAlpha2::CU => Self::Cuba, + CountryAlpha2::CW => Self::Curacao, + CountryAlpha2::CY => Self::Cyprus, + CountryAlpha2::CZ => Self::Czechia, + CountryAlpha2::DK => Self::Denmark, + CountryAlpha2::DJ => Self::Djibouti, + CountryAlpha2::DM => Self::Dominica, + CountryAlpha2::DO => Self::DominicanRepublic, + CountryAlpha2::EC => Self::Ecuador, + CountryAlpha2::EG => Self::Egypt, + CountryAlpha2::SV => Self::ElSalvador, + CountryAlpha2::GQ => Self::EquatorialGuinea, + CountryAlpha2::ER => Self::Eritrea, + CountryAlpha2::EE => Self::Estonia, + CountryAlpha2::ET => Self::Ethiopia, + CountryAlpha2::FK => Self::FalklandIslandsMalvinas, + CountryAlpha2::FO => Self::FaroeIslands, + CountryAlpha2::FJ => Self::Fiji, + CountryAlpha2::FI => Self::Finland, + CountryAlpha2::FR => Self::France, + CountryAlpha2::GF => Self::FrenchGuiana, + CountryAlpha2::PF => Self::FrenchPolynesia, + CountryAlpha2::TF => Self::FrenchSouthernTerritories, + CountryAlpha2::GA => Self::Gabon, + CountryAlpha2::GM => Self::Gambia, + CountryAlpha2::GE => Self::Georgia, + CountryAlpha2::DE => Self::Germany, + CountryAlpha2::GH => Self::Ghana, + CountryAlpha2::GI => Self::Gibraltar, + CountryAlpha2::GR => Self::Greece, + CountryAlpha2::GL => Self::Greenland, + CountryAlpha2::GD => Self::Grenada, + CountryAlpha2::GP => Self::Guadeloupe, + CountryAlpha2::GU => Self::Guam, + CountryAlpha2::GT => Self::Guatemala, + CountryAlpha2::GG => Self::Guernsey, + CountryAlpha2::GN => Self::Guinea, + CountryAlpha2::GW => Self::GuineaBissau, + CountryAlpha2::GY => Self::Guyana, + CountryAlpha2::HT => Self::Haiti, + CountryAlpha2::HM => Self::HeardIslandAndMcDonaldIslands, + CountryAlpha2::VA => Self::HolySee, + CountryAlpha2::HN => Self::Honduras, + CountryAlpha2::HK => Self::HongKong, + CountryAlpha2::HU => Self::Hungary, + CountryAlpha2::IS => Self::Iceland, + CountryAlpha2::IN => Self::India, + CountryAlpha2::ID => Self::Indonesia, + CountryAlpha2::IR => Self::IranIslamicRepublic, + CountryAlpha2::IQ => Self::Iraq, + CountryAlpha2::IE => Self::Ireland, + CountryAlpha2::IM => Self::IsleOfMan, + CountryAlpha2::IL => Self::Israel, + CountryAlpha2::IT => Self::Italy, + CountryAlpha2::JM => Self::Jamaica, + CountryAlpha2::JP => Self::Japan, + CountryAlpha2::JE => Self::Jersey, + CountryAlpha2::JO => Self::Jordan, + CountryAlpha2::KZ => Self::Kazakhstan, + CountryAlpha2::KE => Self::Kenya, + CountryAlpha2::KI => Self::Kiribati, + CountryAlpha2::KP => Self::KoreaDemocraticPeoplesRepublic, + CountryAlpha2::KR => Self::KoreaRepublic, + CountryAlpha2::KW => Self::Kuwait, + CountryAlpha2::KG => Self::Kyrgyzstan, + CountryAlpha2::LA => Self::LaoPeoplesDemocraticRepublic, + CountryAlpha2::LV => Self::Latvia, + CountryAlpha2::LB => Self::Lebanon, + CountryAlpha2::LS => Self::Lesotho, + CountryAlpha2::LR => Self::Liberia, + CountryAlpha2::LY => Self::Libya, + CountryAlpha2::LI => Self::Liechtenstein, + CountryAlpha2::LT => Self::Lithuania, + CountryAlpha2::LU => Self::Luxembourg, + CountryAlpha2::MO => Self::Macao, + CountryAlpha2::MK => Self::MacedoniaTheFormerYugoslavRepublic, + CountryAlpha2::MG => Self::Madagascar, + CountryAlpha2::MW => Self::Malawi, + CountryAlpha2::MY => Self::Malaysia, + CountryAlpha2::MV => Self::Maldives, + CountryAlpha2::ML => Self::Mali, + CountryAlpha2::MT => Self::Malta, + CountryAlpha2::MH => Self::MarshallIslands, + CountryAlpha2::MQ => Self::Martinique, + CountryAlpha2::MR => Self::Mauritania, + CountryAlpha2::MU => Self::Mauritius, + CountryAlpha2::YT => Self::Mayotte, + CountryAlpha2::MX => Self::Mexico, + CountryAlpha2::FM => Self::MicronesiaFederatedStates, + CountryAlpha2::MD => Self::MoldovaRepublic, + CountryAlpha2::MC => Self::Monaco, + CountryAlpha2::MN => Self::Mongolia, + CountryAlpha2::ME => Self::Montenegro, + CountryAlpha2::MS => Self::Montserrat, + CountryAlpha2::MA => Self::Morocco, + CountryAlpha2::MZ => Self::Mozambique, + CountryAlpha2::MM => Self::Myanmar, + CountryAlpha2::NA => Self::Namibia, + CountryAlpha2::NR => Self::Nauru, + CountryAlpha2::NP => Self::Nepal, + CountryAlpha2::NL => Self::Netherlands, + CountryAlpha2::NC => Self::NewCaledonia, + CountryAlpha2::NZ => Self::NewZealand, + CountryAlpha2::NI => Self::Nicaragua, + CountryAlpha2::NE => Self::Niger, + CountryAlpha2::NG => Self::Nigeria, + CountryAlpha2::NU => Self::Niue, + CountryAlpha2::NF => Self::NorfolkIsland, + CountryAlpha2::MP => Self::NorthernMarianaIslands, + CountryAlpha2::NO => Self::Norway, + CountryAlpha2::OM => Self::Oman, + CountryAlpha2::PK => Self::Pakistan, + CountryAlpha2::PW => Self::Palau, + CountryAlpha2::PS => Self::PalestineState, + CountryAlpha2::PA => Self::Panama, + CountryAlpha2::PG => Self::PapuaNewGuinea, + CountryAlpha2::PY => Self::Paraguay, + CountryAlpha2::PE => Self::Peru, + CountryAlpha2::PH => Self::Philippines, + CountryAlpha2::PN => Self::Pitcairn, + CountryAlpha2::PL => Self::Poland, + CountryAlpha2::PT => Self::Portugal, + CountryAlpha2::PR => Self::PuertoRico, + CountryAlpha2::QA => Self::Qatar, + CountryAlpha2::RE => Self::Reunion, + CountryAlpha2::RO => Self::Romania, + CountryAlpha2::RU => Self::RussianFederation, + CountryAlpha2::RW => Self::Rwanda, + CountryAlpha2::BL => Self::SaintBarthelemy, + CountryAlpha2::SH => Self::SaintHelenaAscensionAndTristandaCunha, + CountryAlpha2::KN => Self::SaintKittsAndNevis, + CountryAlpha2::LC => Self::SaintLucia, + CountryAlpha2::MF => Self::SaintMartinFrenchpart, + CountryAlpha2::PM => Self::SaintPierreAndMiquelon, + CountryAlpha2::VC => Self::SaintVincentAndTheGrenadines, + CountryAlpha2::WS => Self::Samoa, + CountryAlpha2::SM => Self::SanMarino, + CountryAlpha2::ST => Self::SaoTomeAndPrincipe, + CountryAlpha2::SA => Self::SaudiArabia, + CountryAlpha2::SN => Self::Senegal, + CountryAlpha2::RS => Self::Serbia, + CountryAlpha2::SC => Self::Seychelles, + CountryAlpha2::SL => Self::SierraLeone, + CountryAlpha2::SG => Self::Singapore, + CountryAlpha2::SX => Self::SintMaartenDutchpart, + CountryAlpha2::SK => Self::Slovakia, + CountryAlpha2::SI => Self::Slovenia, + CountryAlpha2::SB => Self::SolomonIslands, + CountryAlpha2::SO => Self::Somalia, + CountryAlpha2::ZA => Self::SouthAfrica, + CountryAlpha2::GS => Self::SouthGeorgiaAndTheSouthSandwichIslands, + CountryAlpha2::SS => Self::SouthSudan, + CountryAlpha2::ES => Self::Spain, + CountryAlpha2::LK => Self::SriLanka, + CountryAlpha2::SD => Self::Sudan, + CountryAlpha2::SR => Self::Suriname, + CountryAlpha2::SJ => Self::SvalbardAndJanMayen, + CountryAlpha2::SZ => Self::Swaziland, + CountryAlpha2::SE => Self::Sweden, + CountryAlpha2::CH => Self::Switzerland, + CountryAlpha2::SY => Self::SyrianArabRepublic, + CountryAlpha2::TW => Self::TaiwanProvinceOfChina, + CountryAlpha2::TJ => Self::Tajikistan, + CountryAlpha2::TZ => Self::TanzaniaUnitedRepublic, + CountryAlpha2::TH => Self::Thailand, + CountryAlpha2::TL => Self::TimorLeste, + CountryAlpha2::TG => Self::Togo, + CountryAlpha2::TK => Self::Tokelau, + CountryAlpha2::TO => Self::Tonga, + CountryAlpha2::TT => Self::TrinidadAndTobago, + CountryAlpha2::TN => Self::Tunisia, + CountryAlpha2::TR => Self::Turkey, + CountryAlpha2::TM => Self::Turkmenistan, + CountryAlpha2::TC => Self::TurksAndCaicosIslands, + CountryAlpha2::TV => Self::Tuvalu, + CountryAlpha2::UG => Self::Uganda, + CountryAlpha2::UA => Self::Ukraine, + CountryAlpha2::AE => Self::UnitedArabEmirates, + CountryAlpha2::GB => Self::UnitedKingdomOfGreatBritainAndNorthernIreland, + CountryAlpha2::US => Self::UnitedStatesOfAmerica, + CountryAlpha2::UM => Self::UnitedStatesMinorOutlyingIslands, + CountryAlpha2::UY => Self::Uruguay, + CountryAlpha2::UZ => Self::Uzbekistan, + CountryAlpha2::VU => Self::Vanuatu, + CountryAlpha2::VE => Self::VenezuelaBolivarianRepublic, + CountryAlpha2::VN => Self::Vietnam, + CountryAlpha2::VG => Self::VirginIslandsBritish, + CountryAlpha2::VI => Self::VirginIslandsUS, + CountryAlpha2::WF => Self::WallisAndFutuna, + CountryAlpha2::EH => Self::WesternSahara, + CountryAlpha2::YE => Self::Yemen, + CountryAlpha2::ZM => Self::Zambia, + CountryAlpha2::ZW => Self::Zimbabwe, + } + } +} diff --git a/crates/common/common_utils/Cargo.toml b/crates/common/common_utils/Cargo.toml index 395e115e68..65e7241e93 100644 --- a/crates/common/common_utils/Cargo.toml +++ b/crates/common/common_utils/Cargo.toml @@ -12,7 +12,7 @@ anyhow = "1.0" rdkafka = { version = "0.36", optional = true } tracing-kafka = { path = "../tracing-kafka", optional = true } tracing = { workspace = true } -log_utils = { git = "https://github.com/juspay/framework-libs-rs", rev = "76613f5d0c72fd3d26a3403ae859c2a7545e8dd2", package = "log_utils", features = ["tracing", "tracing-storage-api"] } +log_utils = { git = "https://github.com/juspay/framework-libs-rs", rev = "3cdcc24b461917b29641942b04f3aef8110b1adf", package = "log_utils", features = ["tracing", "tracing-storage-api"], optional = true } once_cell = "1.19" chrono = "0.4.31" @@ -50,10 +50,11 @@ josekit = "0.8.7" # Optional features [features] default = [] +logging = ["dep:log_utils"] kafka = ["dep:rdkafka", "dep:tracing-kafka"] async_ext = ["dep:async-trait", "dep:futures"] superposition = ["dep:superposition_core", "dep:superposition_types"] -log-transformations = [] +log-transformations = ["logging"] [dependencies.async-trait] version = "0.1.74" diff --git a/crates/common/common_utils/src/consts.rs b/crates/common/common_utils/src/consts.rs index 32a1e9fcb9..2e60e9abee 100644 --- a/crates/common/common_utils/src/consts.rs +++ b/crates/common/common_utils/src/consts.rs @@ -47,6 +47,8 @@ pub const X_FRM_CONNECTOR_NAME: &str = "x-frm-connector"; pub const X_AUTHENTICATOR_CONNECTOR_NAME: &str = "x-auth-connector"; /// Header key for merchant identification pub const X_MERCHANT_ID: &str = "x-merchant-id"; +/// Header key for organization identification +pub const X_ORG_ID: &str = "x-org-id"; /// Header key for payment method identification pub const X_PAYMENT_METHOD: &str = "x-payment-method"; /// Header key for payment method type identification diff --git a/crates/common/common_utils/src/events.rs b/crates/common/common_utils/src/events.rs index 91b595bcda..2a17020644 100644 --- a/crates/common/common_utils/src/events.rs +++ b/crates/common/common_utils/src/events.rs @@ -698,16 +698,20 @@ pub fn maskable_headers_to_json<'a>( /// All reserved-key filtering happens **outside** the span lock to avoid /// deadlocks (the storage layer logs a warning for reserved keys, which would /// re-enter the subscriber). +#[cfg(feature = "logging")] pub fn record_json_fields_on_span(fields: Vec<(&'static str, serde_json::Value)>) { + // Filter reserved keys and log fields outside the span lock to avoid deadlocks + // (tracing::info!/warn! re-enters the subscriber). let fields: Vec<_> = fields .into_iter() - .filter(|(key, _)| { + .filter(|(key, value)| { if log_utils::Storage::is_reserved(key) { tracing::warn!( "Span field `{key}` is reserved by the logging infrastructure, skipping" ); false } else { + tracing::info!(%value, "{key}"); true } }) diff --git a/crates/common/external-services/Cargo.toml b/crates/common/external-services/Cargo.toml index 51b5b9774f..3cdc004fb5 100644 --- a/crates/common/external-services/Cargo.toml +++ b/crates/common/external-services/Cargo.toml @@ -47,6 +47,7 @@ domain_types = { path = "../../types-traits/domain_types" } interfaces = { path = "../../types-traits/interfaces" } common_utils = { path = "../common_utils", package = "ucs_common_utils", features = [ "async_ext", + "logging", ] } common_enums = { path = "../common_enums", package = "ucs_common_enums" } hyperswitch_masking = { version = "0.0.1", default-features = false, features = [ @@ -59,7 +60,7 @@ injector = { git = "https://github.com/juspay/hyperswitch", tag = "2026.06.18.0" connector_request_kafka = { path = "../connector_request_kafka", optional = true } url = "2.5.0" chrono = "0.4.31" -log_utils = { git = "https://github.com/juspay/framework-libs-rs", rev = "76613f5d0c72fd3d26a3403ae859c2a7545e8dd2", package = "log_utils", features = [ +log_utils = { git = "https://github.com/juspay/framework-libs-rs", rev = "3cdcc24b461917b29641942b04f3aef8110b1adf", package = "log_utils", features = [ "tracing", "tracing-storage-api", ] } diff --git a/crates/common/external-services/src/service.rs b/crates/common/external-services/src/service.rs index 2ba867575a..ef8ae9ac70 100644 --- a/crates/common/external-services/src/service.rs +++ b/crates/common/external-services/src/service.rs @@ -580,6 +580,7 @@ pub struct EventProcessingParams<'a> { pub proxy_name: Option<&'a str>, pub tenant_id: &'a str, pub merchant_id: &'a str, + pub org_id: &'a str, pub return_raw_connector_data: bool, pub connector_latency: ConnectorLatencyTracker, /// Runtime kill-switch for log field application. @@ -602,7 +603,10 @@ pub struct EventProcessingParams<'a> { response.error_message = Empty, response.status_code = Empty, message_ = "Golden Log Line (outgoing)", + // `latency` is the pre-existing human-readable string; `latency_ms` is the same + // duration as a plain number of milliseconds, for numeric downstream consumers. latency = Empty, + latency_ms = Empty, ) )] #[allow(clippy::too_many_arguments)] @@ -659,7 +663,7 @@ where } (common_enums::CallConnectorAction::Trigger, TransportType::Http) => { let mut connector_request = connector - .build_request_v2(&router_data.clone()) + .build_request_v2(&router_data) .map_err(report_connector_request_to_flow)?; let mut updated_router_data = router_data.clone(); @@ -716,6 +720,12 @@ where consts::X_MERCHANT_ID, Maskable::Masked(Secret::new(event_params.merchant_id.to_string())), ); + if !event_params.org_id.is_empty() { + req.add_header( + consts::X_ORG_ID, + Maskable::Masked(Secret::new(event_params.org_id.to_string())), + ); + } if let Some(payment_method) = router_data.resource_common_data.get_payment_method_header() { @@ -991,7 +1001,7 @@ where } (common_enums::CallConnectorAction::Trigger, TransportType::Kafka) => { let kafka_record = connector - .build_kafka_record(&router_data.clone()) + .build_kafka_record(&router_data) .map_err(report_connector_request_to_flow)?; match kafka_record { @@ -1148,6 +1158,8 @@ where let elapsed = start.elapsed().as_millis(); tracing::Span::current().record("latency", elapsed); + // Additive numeric latency alongside the existing string `latency`. + tracing::Span::current().record("latency_ms", u64::try_from(elapsed).unwrap_or_default()); // Apply outgoing log fields (transformations + static values) before emitting the golden log line #[cfg(feature = "log-transformations")] if event_params.log_fields_enabled { diff --git a/crates/common/tracing-kafka/Cargo.toml b/crates/common/tracing-kafka/Cargo.toml index 45f51a1f18..3b0c708773 100644 --- a/crates/common/tracing-kafka/Cargo.toml +++ b/crates/common/tracing-kafka/Cargo.toml @@ -11,7 +11,7 @@ rdkafka = "0.36" serde_json = { workspace = true } tokio = "1.0" thiserror = { workspace = true } -log_utils = { git = "https://github.com/juspay/framework-libs-rs", rev = "76613f5d0c72fd3d26a3403ae859c2a7545e8dd2", package = "log_utils", features = ["tracing"] } +log_utils = { git = "https://github.com/juspay/framework-libs-rs", rev = "3cdcc24b461917b29641942b04f3aef8110b1adf", package = "log_utils", features = ["tracing"] } # Optional dependencies for metrics prometheus = { version = "0.13", optional = true } diff --git a/crates/common/ucs_env/Cargo.toml b/crates/common/ucs_env/Cargo.toml index 700d778a39..f28ad6c978 100644 --- a/crates/common/ucs_env/Cargo.toml +++ b/crates/common/ucs_env/Cargo.toml @@ -49,16 +49,16 @@ tokio = { version = "1.48.0", features = [ tonic = { workspace = true } tonic-reflection = "0.14.0" http = "1.2.0" -log_utils = { git = "https://github.com/juspay/framework-libs-rs", rev = "76613f5d0c72fd3d26a3403ae859c2a7545e8dd2", package = "log_utils", features = [ +log_utils = { git = "https://github.com/juspay/framework-libs-rs", rev = "3cdcc24b461917b29641942b04f3aef8110b1adf", package = "log_utils", features = [ "tracing", ] } -build_info = { git = "https://github.com/juspay/framework-libs-rs", rev = "76613f5d0c72fd3d26a3403ae859c2a7545e8dd2", package = "build_info", features = [ +build_info = { git = "https://github.com/juspay/framework-libs-rs", rev = "3cdcc24b461917b29641942b04f3aef8110b1adf", package = "build_info", features = [ "cargo-workspace", "framework-libs-members-env", ] } [build-dependencies] -build_info = { git = "https://github.com/juspay/framework-libs-rs", rev = "76613f5d0c72fd3d26a3403ae859c2a7545e8dd2", package = "build_info", features = [ +build_info = { git = "https://github.com/juspay/framework-libs-rs", rev = "3cdcc24b461917b29641942b04f3aef8110b1adf", package = "build_info", features = [ "cargo-workspace-build", "vergen-gix-build", "framework-libs-members-env", @@ -71,4 +71,4 @@ otel = ["dep:opentelemetry", "dep:opentelemetry-otlp", "dep:opentelemetry_sdk"] log-transformations = ["common_utils/log-transformations"] [lints] -workspace = true \ No newline at end of file +workspace = true diff --git a/crates/grpc-server/grpc-server/Cargo.toml b/crates/grpc-server/grpc-server/Cargo.toml index bf1975e16b..4c19b85707 100644 --- a/crates/grpc-server/grpc-server/Cargo.toml +++ b/crates/grpc-server/grpc-server/Cargo.toml @@ -15,6 +15,7 @@ cards = { path = "../../types-traits/cards", package = "ucs_cards" } common_enums = { path = "../../common/common_enums", package = "ucs_common_enums" } common_utils = { path = "../../common/common_utils", package = "ucs_common_utils", features = [ "superposition", + "logging", ] } composite-service = { path = "../../internal/composite-service" } config_patch_derive = { path = "../../common/config_patch_derive" } @@ -66,11 +67,11 @@ prost-types = "0.14" rustc-hash = "2.0" gethostname = "0.5.0" once_cell = "1.19.0" -log_utils = { git = "https://github.com/juspay/framework-libs-rs", rev = "76613f5d0c72fd3d26a3403ae859c2a7545e8dd2", package = "log_utils", features = [ +log_utils = { git = "https://github.com/juspay/framework-libs-rs", rev = "3cdcc24b461917b29641942b04f3aef8110b1adf", package = "log_utils", features = [ "tracing", "tracing-storage-api", ] } -build_info = { git = "https://github.com/juspay/framework-libs-rs", rev = "76613f5d0c72fd3d26a3403ae859c2a7545e8dd2", package = "build_info", features = [ +build_info = { git = "https://github.com/juspay/framework-libs-rs", rev = "3cdcc24b461917b29641942b04f3aef8110b1adf", package = "build_info", features = [ "cargo-workspace", "framework-libs-members-env", ] } @@ -78,7 +79,7 @@ uuid = { workspace = true } chrono = "0.4.31" [build-dependencies] -build_info = { git = "https://github.com/juspay/framework-libs-rs", rev = "76613f5d0c72fd3d26a3403ae859c2a7545e8dd2", package = "build_info", features = [ +build_info = { git = "https://github.com/juspay/framework-libs-rs", rev = "3cdcc24b461917b29641942b04f3aef8110b1adf", package = "build_info", features = [ "cargo-workspace-build", "vergen-gix-build", "framework-libs-members-env", diff --git a/crates/grpc-server/grpc-server/src/http/error.rs b/crates/grpc-server/grpc-server/src/http/error.rs index 37421a5ace..f1175f1cf7 100644 --- a/crates/grpc-server/grpc-server/src/http/error.rs +++ b/crates/grpc-server/grpc-server/src/http/error.rs @@ -225,6 +225,15 @@ fn grpc_code_to_http_status(code: tonic::Code) -> StatusCode { } } +/// The HTTP status the caller receives for a gRPC error: the connector's exact 4xx/5xx when the +/// details carry one, else the gRPC-to-HTTP fallback. Shared by the HTTP response and the golden +/// line so both report the same status. +pub fn http_status_for_status(status: &tonic::Status) -> StatusCode { + let details = extract_error_details_from_status(status); + connector_http_status_from_error_details(details.as_ref()) + .unwrap_or_else(|| grpc_code_to_http_status(status.code())) +} + // Convert tonic::Status to HTTP error impl From for HttpError { fn from(status: tonic::Status) -> Self { diff --git a/crates/grpc-server/grpc-server/src/http/handlers/composite/payment_methods.rs b/crates/grpc-server/grpc-server/src/http/handlers/composite/payment_methods.rs index bcd2625512..a695c5e4ba 100644 --- a/crates/grpc-server/grpc-server/src/http/handlers/composite/payment_methods.rs +++ b/crates/grpc-server/grpc-server/src/http/handlers/composite/payment_methods.rs @@ -13,6 +13,7 @@ use crate::http::{ use grpc_api_types::payments::{ composite_payment_method_service_server::CompositePaymentMethodService, CompositePaymentMethodCreateRequest, CompositePaymentMethodCreateResponse, + CompositePaymentMethodEligibilityRequest, CompositePaymentMethodEligibilityResponse, CompositePaymentMethodGetRequest, CompositePaymentMethodGetResponse, CompositePaymentMethodRechargeRequest, CompositePaymentMethodRechargeResponse, }; @@ -40,3 +41,11 @@ http_handler!( recharge, composite_payment_method_service ); + +http_handler!( + eligibility, + CompositePaymentMethodEligibilityRequest, + CompositePaymentMethodEligibilityResponse, + eligibility, + composite_payment_method_service +); diff --git a/crates/grpc-server/grpc-server/src/http/router.rs b/crates/grpc-server/grpc-server/src/http/router.rs index a7bc618af8..421583061e 100644 --- a/crates/grpc-server/grpc-server/src/http/router.rs +++ b/crates/grpc-server/grpc-server/src/http/router.rs @@ -57,6 +57,10 @@ pub fn create_router(state: AppState) -> Router { "/composite/payment_methods/recharge", post(handlers::composite::payment_methods::recharge), ) + .route( + "/composite/payment_methods/eligibility", + post(handlers::composite::payment_methods::eligibility), + ) .route( "/composite/pre_authenticate", post(handlers::composite::payments::pre_authenticate), diff --git a/crates/grpc-server/grpc-server/src/request.rs b/crates/grpc-server/grpc-server/src/request.rs index dc2110f65b..e68925859e 100644 --- a/crates/grpc-server/grpc-server/src/request.rs +++ b/crates/grpc-server/grpc-server/src/request.rs @@ -13,8 +13,8 @@ use crate::utils::{ connector_and_config_from_metadata, connector_variant_from_metadata, MetadataPayload, }; use ucs_interface_common::metadata::{ - merchant_id_from_metadata, proxy_name_from_metadata, request_id_from_metadata, - tenant_id_from_metadata, + merchant_id_from_metadata, org_id_from_metadata, proxy_name_from_metadata, + request_id_from_metadata, tenant_id_from_metadata, }; /// Deprecated header carrying the typed connector config (kept for backward compatibility). @@ -123,11 +123,13 @@ fn extract_routing_metadata_only( .map(|s| s.to_string()); let proxy_name = proxy_name_from_metadata(metadata); + let org_id = org_id_from_metadata(metadata); Ok(MetadataPayload { tenant_id, request_id, merchant_id, + org_id, connector, lineage_ids: common_utils::lineage::LineageIds::empty(""), connector_config, diff --git a/crates/grpc-server/grpc-server/src/server/disputes.rs b/crates/grpc-server/grpc-server/src/server/disputes.rs index 9ed6c146ff..c3596bdab0 100644 --- a/crates/grpc-server/grpc-server/src/server/disputes.rs +++ b/crates/grpc-server/grpc-server/src/server/disputes.rs @@ -116,6 +116,7 @@ impl DisputeService for Disputes { proxy_name, tenant_id, merchant_id, + org_id, connector_latency, environment, .. @@ -183,6 +184,7 @@ impl DisputeService for Disputes { proxy_name: proxy_name.as_deref(), tenant_id: &tenant_id, merchant_id: merchant_id.as_str(), + org_id: org_id.as_str(), return_raw_connector_data: config.common.return_raw_connector_data, connector_latency, log_fields_enabled: config.log_fields.enabled, @@ -357,6 +359,7 @@ impl DisputeService for Disputes { proxy_name, tenant_id, merchant_id, + org_id, connector_latency, environment, .. @@ -425,6 +428,7 @@ impl DisputeService for Disputes { proxy_name: proxy_name.as_deref(), tenant_id: &tenant_id, merchant_id: merchant_id.as_str(), + org_id: org_id.as_str(), return_raw_connector_data: config.common.return_raw_connector_data, connector_latency, log_fields_enabled: config.log_fields.enabled, diff --git a/crates/grpc-server/grpc-server/src/server/events.rs b/crates/grpc-server/grpc-server/src/server/events.rs index 71b08332cd..47e49f0298 100644 --- a/crates/grpc-server/grpc-server/src/server/events.rs +++ b/crates/grpc-server/grpc-server/src/server/events.rs @@ -503,6 +503,7 @@ impl EventServiceImpl { proxy_name: metadata_payload.proxy_name.as_deref(), tenant_id: &metadata_payload.tenant_id, merchant_id: metadata_payload.merchant_id.as_str(), + org_id: metadata_payload.org_id.as_str(), return_raw_connector_data: config.common.return_raw_connector_data, connector_latency: metadata_payload.connector_latency.clone(), log_fields_enabled: config.log_fields.enabled, @@ -614,6 +615,7 @@ impl EventServiceImpl { proxy_name: metadata_payload.proxy_name.as_deref(), tenant_id: &metadata_payload.tenant_id, merchant_id: metadata_payload.merchant_id.as_str(), + org_id: metadata_payload.org_id.as_str(), return_raw_connector_data: config.common.return_raw_connector_data, connector_latency: metadata_payload.connector_latency.clone(), log_fields_enabled: config.log_fields.enabled, @@ -725,6 +727,7 @@ impl EventServiceImpl { proxy_name: metadata_payload.proxy_name.as_deref(), tenant_id: &metadata_payload.tenant_id, merchant_id: metadata_payload.merchant_id.as_str(), + org_id: metadata_payload.org_id.as_str(), return_raw_connector_data: config.common.return_raw_connector_data, connector_latency: metadata_payload.connector_latency.clone(), log_fields_enabled: config.log_fields.enabled, @@ -833,6 +836,7 @@ impl EventServiceImpl { proxy_name: metadata_payload.proxy_name.as_deref(), tenant_id: &metadata_payload.tenant_id, merchant_id: metadata_payload.merchant_id.as_str(), + org_id: metadata_payload.org_id.as_str(), return_raw_connector_data: config.common.return_raw_connector_data, connector_latency: metadata_payload.connector_latency.clone(), log_fields_enabled: config.log_fields.enabled, @@ -941,6 +945,7 @@ impl EventServiceImpl { proxy_name: metadata_payload.proxy_name.as_deref(), tenant_id: &metadata_payload.tenant_id, merchant_id: metadata_payload.merchant_id.as_str(), + org_id: metadata_payload.org_id.as_str(), return_raw_connector_data: config.common.return_raw_connector_data, connector_latency: metadata_payload.connector_latency.clone(), log_fields_enabled: config.log_fields.enabled, @@ -1040,6 +1045,7 @@ async fn verify_webhook_source_external( proxy_name: metadata_payload.proxy_name.as_deref(), tenant_id: &metadata_payload.tenant_id, merchant_id: metadata_payload.merchant_id.as_str(), + org_id: metadata_payload.org_id.as_str(), return_raw_connector_data: config.common.return_raw_connector_data, connector_latency: metadata_payload.connector_latency.clone(), log_fields_enabled: config.log_fields.enabled, diff --git a/crates/grpc-server/grpc-server/src/server/payments.rs b/crates/grpc-server/grpc-server/src/server/payments.rs index aad5e44271..201e3f28dd 100644 --- a/crates/grpc-server/grpc-server/src/server/payments.rs +++ b/crates/grpc-server/grpc-server/src/server/payments.rs @@ -123,6 +123,7 @@ struct EventParams<'a> { proxy_name: Option<&'a str>, tenant_id: &'a str, merchant_id: &'a str, + org_id: &'a str, connector_latency: common_utils::request_metrics::ConnectorLatencyTracker, } @@ -577,6 +578,7 @@ impl Payments { proxy_name: metadata_payload.proxy_name.as_deref(), tenant_id: &metadata_payload.tenant_id, merchant_id: metadata_payload.merchant_id.as_str(), + org_id: metadata_payload.org_id.as_str(), return_raw_connector_data: config.common.return_raw_connector_data, connector_latency: metadata_payload.connector_latency.clone(), log_fields_enabled: config.log_fields.enabled, @@ -717,6 +719,7 @@ impl Payments { proxy_name: metadata_payload.proxy_name.as_deref(), tenant_id: &metadata_payload.tenant_id, merchant_id: metadata_payload.merchant_id.as_str(), + org_id: metadata_payload.org_id.as_str(), return_raw_connector_data: config.common.return_raw_connector_data, connector_latency: metadata_payload.connector_latency.clone(), log_fields_enabled: config.log_fields.enabled, @@ -1134,6 +1137,7 @@ impl PaymentService for Payments { proxy_name: metadata_payload.proxy_name.as_deref(), tenant_id: &metadata_payload.tenant_id, merchant_id: metadata_payload.merchant_id.as_str(), + org_id: metadata_payload.org_id.as_str(), return_raw_connector_data: config.common.return_raw_connector_data, connector_latency: metadata_payload.connector_latency.clone(), log_fields_enabled: config.log_fields.enabled, @@ -2617,6 +2621,7 @@ impl PaymentMethod { proxy_name: metadata_payload.proxy_name.as_deref(), tenant_id: &metadata_payload.tenant_id, merchant_id: metadata_payload.merchant_id.as_str(), + org_id: metadata_payload.org_id.as_str(), return_raw_connector_data: config.common.return_raw_connector_data, connector_latency: metadata_payload.connector_latency.clone(), log_fields_enabled: config.log_fields.enabled, @@ -2755,6 +2760,7 @@ impl PaymentMethod { proxy_name: metadata_payload.proxy_name.as_deref(), tenant_id: &metadata_payload.tenant_id, merchant_id: metadata_payload.merchant_id.as_str(), + org_id: metadata_payload.org_id.as_str(), return_raw_connector_data: config.common.return_raw_connector_data, connector_latency: metadata_payload.connector_latency.clone(), runtime_metadata: &config.runtime_metadata, @@ -2875,6 +2881,7 @@ impl MerchantAuthentication { proxy_name: event_params.proxy_name, tenant_id: event_params.tenant_id, merchant_id: event_params.merchant_id, + org_id: event_params.org_id, return_raw_connector_data: config.common.return_raw_connector_data, connector_latency: event_params.connector_latency.clone(), log_fields_enabled: config.log_fields.enabled, @@ -3019,6 +3026,7 @@ impl MerchantAuthentication { proxy_name: event_params.proxy_name, tenant_id: event_params.tenant_id, merchant_id: event_params.merchant_id, + org_id: event_params.org_id, return_raw_connector_data: config.common.return_raw_connector_data, connector_latency: event_params.connector_latency.clone(), log_fields_enabled: config.log_fields.enabled, @@ -3249,6 +3257,7 @@ impl MerchantAuthenticationService for MerchantAuthentication { proxy_name: metadata_payload.proxy_name.as_deref(), tenant_id: &metadata_payload.tenant_id, merchant_id: metadata_payload.merchant_id.as_str(), + org_id: metadata_payload.org_id.as_str(), connector_latency: metadata_payload.connector_latency.clone(), }; @@ -3365,6 +3374,7 @@ impl MerchantAuthenticationService for MerchantAuthentication { proxy_name: metadata_payload.proxy_name.as_deref(), tenant_id: &metadata_payload.tenant_id, merchant_id: metadata_payload.merchant_id.as_str(), + org_id: metadata_payload.org_id.as_str(), connector_latency: metadata_payload.connector_latency.clone(), }; @@ -3601,6 +3611,7 @@ impl RecurringPaymentService for RecurringPayments { proxy_name: metadata_payload.proxy_name.as_deref(), tenant_id: &metadata_payload.tenant_id, merchant_id: metadata_payload.merchant_id.as_str(), + org_id: metadata_payload.org_id.as_str(), return_raw_connector_data: config.common.return_raw_connector_data, connector_latency: metadata_payload.connector_latency.clone(), log_fields_enabled: config.log_fields.enabled, diff --git a/crates/grpc-server/grpc-server/src/utils.rs b/crates/grpc-server/grpc-server/src/utils.rs index 82ce98f00e..438e04901a 100644 --- a/crates/grpc-server/grpc-server/src/utils.rs +++ b/crates/grpc-server/grpc-server/src/utils.rs @@ -41,6 +41,9 @@ pub fn record_fields_from_header(request: &Request) -> "request", uri = %url_path, version = ?request.version(), + // `action` = the real HTTP verb (GET/POST/…). gRPC-over-HTTP2 is always POST; + // the HTTP gateway carries the true verb. + action = %request.method(), tenant_id = tracing::field::Empty, request_id = tracing::field::Empty, execution_mode = tracing::field::Empty, @@ -276,14 +279,16 @@ where .. } = metadata_payload; let current_span = tracing::Span::current(); + let masked_body = hyperswitch_masking::masked_serialize(&request_data.payload) + .map_err(|e| tracing::error!("Masked serialization error: {:?}", e)) + .ok(); let connector_name = connector.get_connector_name(); current_span.record("service_name", service_name); - match hyperswitch_masking::masked_serialize(&request_data.payload) { - Ok(masked_value) => { - record_json_fields_on_span(vec![("request_body", masked_value)]); + match masked_body.as_ref() { + Some(masked_value) => { + record_json_fields_on_span(vec![("request_body", masked_value.clone())]); } - Err(e) => { - tracing::error!("Masked serialization error: {:?}", e); + None => { current_span.record("request_body", ""); } }; @@ -306,6 +311,10 @@ pub fn log_after_initialization( match &result { Ok(response) => { + // Additive numeric `res_code` (success). `status_code` is left untouched so + // existing consumers of the gRPC-code field don't break. + record_json_fields_on_span(vec![("res_code", Value::from(200_i64))]); + let res_ref = response.get_ref(); // Record response_body as structured JSON with masking @@ -340,7 +349,12 @@ pub fn log_after_initialization( } Err(status) => { current_span.record("error_message", status.message()); + // Backward-compatible: keep main's gRPC code-name string on `status_code`. current_span.record("status_code", status.code().to_string()); + // Additive numeric `res_code`: connector-aware HTTP status (e.g. 422) — matches the + // HTTP response the caller receives, not the coarse gRPC code. + let http_status = crate::http::error::http_status_for_status(status).as_u16(); + record_json_fields_on_span(vec![("res_code", Value::from(i64::from(http_status)))]); } } // Apply unified log fields (transformations + static values) before emitting the golden log line @@ -356,6 +370,15 @@ pub fn log_after_initialization( tracing::info!("Golden Log Line (incoming - response)"); } +/// Record the additive numeric `latency_ms` on the current span. Shared by the streaming and +/// non-streaming logging wrappers to avoid drift. +fn record_latency_ms(duration: u128) { + record_json_fields_on_span(vec![( + "latency_ms", + Value::from(u64::try_from(duration).unwrap_or_default()), + )]); +} + /// Generic gRPC logging wrapper that accepts a custom parser function. /// This allows different parsing strategies for different flow types /// (e.g., authenticated flows vs unauthenticated webhook flows). @@ -398,6 +421,8 @@ where let duration = start_time.elapsed().as_millis(); current_span.record("response_time", duration); + // Additive numeric latency alongside the existing `response_time`. + record_latency_ms(duration); result } .await; @@ -466,6 +491,8 @@ where let duration = start_time.elapsed().as_millis(); current_span.record("response_time", duration); + // Additive numeric latency alongside the existing `response_time`. + record_latency_ms(duration); result } .await; @@ -743,6 +770,7 @@ macro_rules! implement_connector_operation { proxy_name: metadata_payload.proxy_name.as_deref(), tenant_id: &metadata_payload.tenant_id, merchant_id: metadata_payload.merchant_id.as_str(), + org_id: metadata_payload.org_id.as_str(), return_raw_connector_data: config.common.return_raw_connector_data, connector_latency: metadata_payload.connector_latency.clone(), log_fields_enabled: config.log_fields.enabled, @@ -815,16 +843,18 @@ macro_rules! implement_connector_operation { }; let call_connector_action = connector_integration.get_call_connector_action(); - let response_result = external_services::service::execute_connector_processing_step( - proxy, - connector_integration, - router_data, - all_keys_required, - event_params, - token_data, - call_connector_action, - test_context, - api_tag, + let response_result = Box::pin( + external_services::service::execute_connector_processing_step( + proxy, + connector_integration, + router_data, + all_keys_required, + event_params, + token_data, + call_connector_action, + test_context, + api_tag, + ), ) .await .to_grpc_error()?; @@ -1106,22 +1136,25 @@ macro_rules! implement_connector_operation { proxy_name: metadata_payload.proxy_name.as_deref(), tenant_id: &metadata_payload.tenant_id, merchant_id: metadata_payload.merchant_id.as_str(), + org_id: metadata_payload.org_id.as_str(), return_raw_connector_data: config.common.return_raw_connector_data, connector_latency: metadata_payload.connector_latency.clone(), log_fields_enabled: config.log_fields.enabled, log_fields: &config.log_fields.outgoing, }; let call_connector_action = connector_integration.get_call_connector_action(); - let response_result = external_services::service::execute_connector_processing_step( - &config.proxy, - connector_integration, - router_data, - $all_keys_required, - event_params, - None, - call_connector_action, - test_context, - api_tag, + let response_result = Box::pin( + external_services::service::execute_connector_processing_step( + &config.proxy, + connector_integration, + router_data, + $all_keys_required, + event_params, + None, + call_connector_action, + test_context, + api_tag, + ), ) .await .to_grpc_error()?; diff --git a/crates/grpc-server/grpc-server/tests/common.rs b/crates/grpc-server/grpc-server/tests/common.rs index 8b494c61ec..6eb14019b4 100644 --- a/crates/grpc-server/grpc-server/tests/common.rs +++ b/crates/grpc-server/grpc-server/tests/common.rs @@ -296,7 +296,8 @@ macro_rules! grpc_test { common::server_and_client_stub::<$c_type>(server, base_config) .await .expect("Failed to create the server client pair"); - let response = async { $body }; + let server_fut = Box::pin(server_fut); + let response = Box::pin(async { $body }); tokio::select! { _ = server_fut => panic!("Server failed"), @@ -312,7 +313,8 @@ macro_rules! grpc_test { .await .expect("Failed to create the server channel"); $(let mut $client = <$c_type as common::AutoClient>::new(channel.clone());)+ - let response = async { $body }; + let server_fut = Box::pin(server_fut); + let response = Box::pin(async { $body }); tokio::select! { _ = server_fut => panic!("Server failed"), diff --git a/crates/integrations/connector-integration/src/authenticator_connectors/plaid/test.rs b/crates/integrations/connector-integration/src/authenticator_connectors/plaid/test.rs index 8755a749bb..d26405a149 100644 --- a/crates/integrations/connector-integration/src/authenticator_connectors/plaid/test.rs +++ b/crates/integrations/connector-integration/src/authenticator_connectors/plaid/test.rs @@ -247,6 +247,7 @@ mod tests { sender_payment_instrument_id: None, settlement_status: None, raw_connector_status: None, + connector_returned_payment_method_details: None, typed_connector_response: None, }, connector_config: ConnectorSpecificConfig::Plaid { @@ -408,6 +409,7 @@ mod tests { sender_payment_instrument_id: None, settlement_status: None, raw_connector_status: None, + connector_returned_payment_method_details: None, typed_connector_response: None, }, connector_config: ConnectorSpecificConfig::Plaid { diff --git a/crates/integrations/connector-integration/src/connectors.rs b/crates/integrations/connector-integration/src/connectors.rs index a9d41561d4..3a175c94cb 100644 --- a/crates/integrations/connector-integration/src/connectors.rs +++ b/crates/integrations/connector-integration/src/connectors.rs @@ -289,6 +289,9 @@ pub use self::flywire::Flywire; pub mod affirm; pub use self::affirm::Affirm; +pub mod moneris; +pub use self::moneris::Moneris; + pub mod kount; pub use self::kount::Kount; @@ -302,3 +305,8 @@ pub use self::tesouro::Tesouro; pub mod boost; pub use self::boost::Boost; + +pub mod citigate; +pub use self::citigate::Citigate; +pub mod ilixium; +pub use self::ilixium::Ilixium; diff --git a/crates/integrations/connector-integration/src/connectors/aci/transformers.rs b/crates/integrations/connector-integration/src/connectors/aci/transformers.rs index bf4750ab13..89558df8e7 100644 --- a/crates/integrations/connector-integration/src/connectors/aci/transformers.rs +++ b/crates/integrations/connector-integration/src/connectors/aci/transformers.rs @@ -424,7 +424,7 @@ impl | BankRedirectData::OnlineBankingThailand { .. } | BankRedirectData::LocalBankRedirect {} | BankRedirectData::OpenBankingUk { .. } - | BankRedirectData::OpenBanking {} + | BankRedirectData::OpenBanking { .. } | BankRedirectData::Netbanking { .. } => Err(IntegrationError::NotImplemented( "Payment method".to_string(), Default::default(), diff --git a/crates/integrations/connector-integration/src/connectors/adyen/test.rs b/crates/integrations/connector-integration/src/connectors/adyen/test.rs index a7beffc0b5..5c0d3f8b11 100644 --- a/crates/integrations/connector-integration/src/connectors/adyen/test.rs +++ b/crates/integrations/connector-integration/src/connectors/adyen/test.rs @@ -88,6 +88,7 @@ mod tests { l2_l3_data: None, merchant_request_id: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, typed_connector_response: None, }, @@ -298,6 +299,7 @@ mod tests { l2_l3_data: None, merchant_request_id: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, typed_connector_response: None, }, diff --git a/crates/integrations/connector-integration/src/connectors/calida/test.rs b/crates/integrations/connector-integration/src/connectors/calida/test.rs index 6028078639..0fb314606e 100644 --- a/crates/integrations/connector-integration/src/connectors/calida/test.rs +++ b/crates/integrations/connector-integration/src/connectors/calida/test.rs @@ -105,6 +105,7 @@ mod tests { l2_l3_data: None, merchant_request_id: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, }, connector_config: ConnectorSpecificConfig::Calida { @@ -295,6 +296,7 @@ mod tests { merchant_request_id: None, sender_payment_instrument_id: None, settlement_status: None, + connector_returned_payment_method_details: None, }, connector_config: ConnectorSpecificConfig::Calida { api_key: Secret::new(api_key), @@ -469,6 +471,7 @@ mod tests { l2_l3_data: None, merchant_request_id: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, }, connector_config: ConnectorSpecificConfig::Calida { diff --git a/crates/integrations/connector-integration/src/connectors/citigate.rs b/crates/integrations/connector-integration/src/connectors/citigate.rs new file mode 100644 index 0000000000..b240775e2b --- /dev/null +++ b/crates/integrations/connector-integration/src/connectors/citigate.rs @@ -0,0 +1,497 @@ +//! Citigate connector. +//! +//! Citigate is a single-endpoint, verb-in-body JSON gateway: every operation is a +//! `POST` to `/orion/interface/json.ashx` and the operation is selected by the +//! `TransTypeID` body field, never by the URL or HTTP method. Credentials +//! (`MerchantName` / `MerchantPassword`) travel in the body too, so +//! [`ConnectorCommon::get_auth_header`] contributes no headers. +//! +//! Implemented scope: Card / Authorize (Purchase), one-time, non-3DS and the 3DS +//! user-redirect path; the Transaction Status Check (`TransTypeID = 8`), which +//! serves as both PSync and RSync; and the post-authorization operations Capture +//! (`3`), Void / Cancel (`4`) and Refund (`5`). + +pub mod transformers; + +use std::fmt::Debug; + +use common_enums::CurrencyUnit; +use common_utils::{errors::CustomResult, events, ext_traits::ByteSliceExt}; +use domain_types::{ + connector_flow::{Authorize, Capture, PSync, RSync, Refund, Void}, + connector_types::{ + PaymentFlowData, PaymentVoidData, PaymentsAuthorizeData, PaymentsCaptureData, + PaymentsResponseData, PaymentsSyncData, RefundFlowData, RefundSyncData, RefundsData, + RefundsResponseData, + }, + errors::{ConnectorError, IntegrationError}, + payment_method_data::PaymentMethodDataTypes, + router_data::{ConnectorSpecificConfig, ErrorResponse}, + router_data_v2::RouterDataV2, + router_response_types::Response, + types::Connectors, +}; +use error_stack::ResultExt; +use hyperswitch_masking::Maskable; +use interfaces::{ + api::ConnectorCommon, connector_integration_v2::ConnectorIntegrationV2, connector_types, + decode::BodyDecoding, +}; +use serde::Serialize; +use transformers::{ + self as citigate, CitigateCaptureRequest, CitigateCaptureResponse, CitigatePaymentsRequest, + CitigatePaymentsResponse, CitigateRefundRequest, CitigateRefundResponse, + CitigateRefundSyncRequest, CitigateRefundSyncResponse, CitigateSyncRequest, + CitigateSyncResponse, CitigateVoidRequest, CitigateVoidResponse, +}; + +use super::macros; +use crate::types::ResponseRouterData; +use crate::with_error_response_body; + +pub(crate) mod headers { + pub(crate) const CONTENT_TYPE: &str = "Content-Type"; +} + +/// Path shared by every Citigate operation, appended to the configured base URL. +const CITIGATE_JSON_INTERFACE_PATH: &str = "/orion/interface/json.ashx"; + +// Citigate expects the amount in the smallest denomination of the currency with no +// decimal point, and quotes it as a JSON string. +macros::create_amount_converter_wrapper!(connector_name: Citigate, amount_type: StringMinorUnit); + +// ===== MACRO PREREQUISITES ===== +macros::create_all_prerequisites!( + connector_name: Citigate, + generic_type: T, + api: [ + ( + flow: Authorize, + request_body: CitigatePaymentsRequest, + response_body: CitigatePaymentsResponse, + router_data: RouterDataV2, PaymentsResponseData>, + ), + ( + flow: PSync, + request_body: CitigateSyncRequest, + response_body: CitigateSyncResponse, + router_data: RouterDataV2, + ), + ( + flow: Capture, + request_body: CitigateCaptureRequest, + response_body: CitigateCaptureResponse, + router_data: RouterDataV2, + ), + ( + flow: Void, + request_body: CitigateVoidRequest, + response_body: CitigateVoidResponse, + router_data: RouterDataV2, + ), + ( + flow: Refund, + request_body: CitigateRefundRequest, + response_body: CitigateRefundResponse, + router_data: RouterDataV2, + ), + ( + flow: RSync, + request_body: CitigateRefundSyncRequest, + response_body: CitigateRefundSyncResponse, + router_data: RouterDataV2, + ) + ], + amount_converters: [], + member_functions: { + pub fn build_headers( + &self, + _req: &RouterDataV2, + ) -> CustomResult)>, IntegrationError> { + // Citigate has no authentication headers: MerchantName / MerchantPassword + // are members of the request body. + Ok(vec![( + headers::CONTENT_TYPE.to_string(), + self.common_get_content_type().to_string().into(), + )]) + } + + pub fn connector_base_url_payments<'a, F, Req, Res>( + &self, + req: &'a RouterDataV2, + ) -> &'a str { + &req.resource_common_data.connectors.citigate.base_url + } + } +); + +// ===== CONNECTOR COMMON IMPLEMENTATION ===== +impl ConnectorCommon + for Citigate +{ + fn id(&self) -> &'static str { + "citigate" + } + + fn get_currency_unit(&self) -> CurrencyUnit { + CurrencyUnit::Minor + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + + fn base_url<'a>(&self, connectors: &'a Connectors) -> &'a str { + &connectors.citigate.base_url + } + + fn get_auth_header( + &self, + auth_type: &ConnectorSpecificConfig, + ) -> CustomResult)>, IntegrationError> { + // Validate that the configured credentials have the shape Citigate needs, but + // emit no headers: the credentials are injected into the request body. + citigate::CitigateAuthType::try_from(auth_type)?; + Ok(Vec::new()) + } + + fn build_error_response( + &self, + res: Response, + event_builder: Option<&mut events::Event>, + _connector_config: &ConnectorSpecificConfig, + ) -> CustomResult { + // Citigate answers every outcome with HTTP 200 and the same envelope, so the + // error path parses the very same struct as the success path. + let response: CitigatePaymentsResponse = res + .response + .parse_struct("CitigatePaymentsResponse") + .change_context(crate::utils::response_deserialization_fail( + res.status_code, + "citigate: response body did not match the expected TransactionResponse format.", + ))?; + + with_error_response_body!(event_builder, response); + + Ok(response.to_error_response(res.status_code)) + } +} + +// ===== FLOW-SPECIFIC CONNECTOR INTEGRATION IMPLEMENTATIONS ===== + +// Authorize Flow — Purchase, PaymentTypeID = 1, TransTypeID = 0. +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Citigate, + curl_request: Json(CitigatePaymentsRequest), + curl_response: CitigatePaymentsResponse, + flow_name: Authorize, + resource_common_data: PaymentFlowData, + flow_request: PaymentsAuthorizeData, + flow_response: PaymentsResponseData, + http_method: Post, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize], + other_functions: { + fn get_headers( + &self, + req: &RouterDataV2, PaymentsResponseData>, + ) -> CustomResult)>, IntegrationError> { + self.build_headers(req) + } + + fn get_url( + &self, + req: &RouterDataV2, PaymentsResponseData>, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.connector_base_url_payments(req), + CITIGATE_JSON_INTERFACE_PATH + )) + } + } +); + +// PSync Flow — Transaction Status Check, PaymentTypeID = 1, TransTypeID = 8. +// Same endpoint and same MID credentials as the Authorize that created the +// payment; the lookup key is MerchantRef, not TransactionID. +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Citigate, + curl_request: Json(CitigateSyncRequest), + curl_response: CitigateSyncResponse, + flow_name: PSync, + resource_common_data: PaymentFlowData, + flow_request: PaymentsSyncData, + flow_response: PaymentsResponseData, + http_method: Post, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize], + other_functions: { + fn get_headers( + &self, + req: &RouterDataV2, + ) -> CustomResult)>, IntegrationError> { + self.build_headers(req) + } + + fn get_url( + &self, + req: &RouterDataV2, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.connector_base_url_payments(req), + CITIGATE_JSON_INTERFACE_PATH + )) + } + } +); + +// Capture Flow — settle an open authorisation, PaymentTypeID = 1, TransTypeID = 3. +// Full capture only: the request has no Amount field. +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Citigate, + curl_request: Json(CitigateCaptureRequest), + curl_response: CitigateCaptureResponse, + flow_name: Capture, + resource_common_data: PaymentFlowData, + flow_request: PaymentsCaptureData, + flow_response: PaymentsResponseData, + http_method: Post, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize], + other_functions: { + fn get_headers( + &self, + req: &RouterDataV2, + ) -> CustomResult)>, IntegrationError> { + self.build_headers(req) + } + + fn get_url( + &self, + req: &RouterDataV2, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.connector_base_url_payments(req), + CITIGATE_JSON_INTERFACE_PATH + )) + } + } +); + +// Void Flow — cancel an open authorisation, PaymentTypeID = 1, TransTypeID = 4. +// Full void only, and the cancellation reason cannot be transmitted. +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Citigate, + curl_request: Json(CitigateVoidRequest), + curl_response: CitigateVoidResponse, + flow_name: Void, + resource_common_data: PaymentFlowData, + flow_request: PaymentVoidData, + flow_response: PaymentsResponseData, + http_method: Post, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize], + other_functions: { + fn get_headers( + &self, + req: &RouterDataV2, + ) -> CustomResult)>, IntegrationError> { + self.build_headers(req) + } + + fn get_url( + &self, + req: &RouterDataV2, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.connector_base_url_payments(req), + CITIGATE_JSON_INTERFACE_PATH + )) + } + } +); + +// Refund Flow — PaymentTypeID = 1, TransTypeID = 5. Refunds carry RefundFlowData +// rather than PaymentFlowData, so the base URL is read directly here. +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Citigate, + curl_request: Json(CitigateRefundRequest), + curl_response: CitigateRefundResponse, + flow_name: Refund, + resource_common_data: RefundFlowData, + flow_request: RefundsData, + flow_response: RefundsResponseData, + http_method: Post, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize], + other_functions: { + fn get_headers( + &self, + req: &RouterDataV2, + ) -> CustomResult)>, IntegrationError> { + self.build_headers(req) + } + + fn get_url( + &self, + req: &RouterDataV2, + ) -> CustomResult { + Ok(format!( + "{}{}", + req.resource_common_data.connectors.citigate.base_url, + CITIGATE_JSON_INTERFACE_PATH + )) + } + } +); + +// RSync Flow — the same Transaction Status Check as PSync (TransTypeID = 8), but +// keyed on the refund leg's MerchantRef rather than the payment's. +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Citigate, + curl_request: Json(CitigateRefundSyncRequest), + curl_response: CitigateRefundSyncResponse, + flow_name: RSync, + resource_common_data: RefundFlowData, + flow_request: RefundSyncData, + flow_response: RefundsResponseData, + http_method: Post, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize], + other_functions: { + fn get_headers( + &self, + req: &RouterDataV2, + ) -> CustomResult)>, IntegrationError> { + self.build_headers(req) + } + + fn get_url( + &self, + req: &RouterDataV2, + ) -> CustomResult { + Ok(format!( + "{}{}", + req.resource_common_data.connectors.citigate.base_url, + CITIGATE_JSON_INTERFACE_PATH + )) + } + } +); + +// ===== CONNECTOR SERVICE TRAIT IMPLEMENTATION ===== +// Aggregate trait - composes all other connector traits. +impl + connector_types::ConnectorServiceTrait for Citigate +{ +} + +// ===== PAYMENT FLOW TRAIT IMPLEMENTATIONS ===== +impl + connector_types::PaymentAuthorizeV2 for Citigate +{ +} + +impl + connector_types::PaymentSyncV2 for Citigate +{ +} + +impl + connector_types::PaymentCapture for Citigate +{ +} + +impl + connector_types::PaymentVoidV2 for Citigate +{ +} + +impl + connector_types::RefundV2 for Citigate +{ +} + +impl + connector_types::RefundSyncV2 for Citigate +{ +} + +// ===== BASE (NON-FLOW) TRAIT IMPLEMENTATIONS ===== +impl + connector_types::ValidationTrait for Citigate +{ +} + +impl + connector_types::IncomingWebhook for Citigate +{ +} + +impl + connector_types::VerifyRedirectResponse for Citigate +{ +} + +// ===== SOURCE VERIFICATION IMPLEMENTATION ===== +// Citigate signs the 3DS redirect callback and the opt-in refund/fraud +// notification with SHA-1, but neither callback is consumed here: the 3DS outcome +// is resolved by polling the Transaction Status Check (PSync) instead. +impl + interfaces::verification::SourceVerification for Citigate +{ +} + +// ===== BODY DECODING IMPLEMENTATION ===== +impl BodyDecoding + for Citigate +{ +} + +// ===== PAYOUT TRAIT IMPLEMENTATIONS ===== +macros::macro_connector_payout_implementation!( + connector: Citigate, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize] +); + +// ===== FLOW STATUS IMPLEMENTATIONS ===== +// Every flow other than Authorize, PSync, Capture, Void, Refund and RSync is +// stubbed: the Citigate JSON interface exposes no other operation on the card +// payment type. +macros::macro_connector_flow_status_impls!( + connector: Citigate, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize], + not_implemented: [ + Accept, + ClientAuthenticationToken, + CreateConnectorCustomer, + DefendDispute, + MandateRevoke, + Authenticate, + IncrementalAuthorization, + CreateOrder, + PostAuthenticate, + PreAuthenticate, + PaymentMethodToken, + VoidPC, + RepeatPayment, + ServerAuthenticationToken, + ServerSessionAuthenticationToken, + SetupMandate, + SubmitEvidence, + GetConnectorCustomer, + VoidPostRefund + ], +); diff --git a/crates/integrations/connector-integration/src/connectors/citigate/transformers.rs b/crates/integrations/connector-integration/src/connectors/citigate/transformers.rs new file mode 100644 index 0000000000..cd38ce6325 --- /dev/null +++ b/crates/integrations/connector-integration/src/connectors/citigate/transformers.rs @@ -0,0 +1,1369 @@ +//! Citigate transformers. +//! +//! Citigate exposes a single-endpoint, verb-in-body JSON API. Every operation is a +//! `POST` to `/orion/interface/json.ashx`; the operation is selected by the +//! `TransTypeID` body field. Authentication (`MerchantName` / `MerchantPassword`) is +//! also carried in the body, so there are no auth headers. +//! +//! Scope of this module: Card / Authorize (Purchase, `TransTypeID = 0`), one-time, +//! non-3DS **and** the 3DS user-redirect path, the Transaction Status Check +//! (`TransTypeID = 8`) used for both PSync and RSync, and the post-authorization +//! operations Capture (`3`), Void / Cancel (`4`) and Refund (`5`). + +use std::collections::HashMap; + +use common_enums::{AttemptStatus, AuthenticationType, CaptureMethod, CardNetwork, RefundStatus}; +use common_utils::{pii::Email, types::StringMinorUnit, Method}; +use domain_types::{ + connector_flow::{Authorize, Capture, PSync, RSync, Refund, Void}, + connector_types::{ + PaymentFlowData, PaymentVoidData, PaymentsAuthorizeData, PaymentsCaptureData, + PaymentsResponseData, PaymentsSyncData, RefundFlowData, RefundSyncData, RefundsData, + RefundsResponseData, ResponseId, + }, + errors::{ConnectorError, IntegrationError, IntegrationErrorContext}, + payment_method_data::{Card, PaymentMethodData, PaymentMethodDataTypes, RawCardNumber}, + router_data::{ConnectorSpecificConfig, ErrorResponse, FlowStatus}, + router_data_v2::RouterDataV2, + router_response_types::RedirectForm, + utils::{get_card_issuer, CardIssuer}, +}; +use hyperswitch_masking::{PeekInterface, Secret}; +use serde::{Deserialize, Deserializer, Serialize}; + +use crate::connectors::citigate::{CitigateAmountConvertor, CitigateRouterData}; +use crate::types::ResponseRouterData; + +/// `PaymentTypeID` for a card payment. Card is the only payment type in scope. +const PAYMENT_TYPE_ID_CARD: &str = "1"; +/// `TransTypeID` for a purchase (UCS `Authorize`). +const TRANS_TYPE_ID_PURCHASE: &str = "0"; +/// `TransTypeID` for settling an open authorisation (UCS `Capture`). +const TRANS_TYPE_ID_CAPTURE: &str = "3"; +/// `TransTypeID` for cancelling an open authorisation (UCS `Void`). +const TRANS_TYPE_ID_CANCEL: &str = "4"; +/// `TransTypeID` for refunding a sale or a captured authorisation (UCS `Refund`). +const TRANS_TYPE_ID_REFUND: &str = "5"; +/// `TransTypeID` for a transaction status check (UCS `PSync` **and** `RSync`). +const TRANS_TYPE_ID_STATUS_CHECK: &str = "8"; + +/// `ResponseCode` returned for an approved transaction. +const RESPONSE_CODE_APPROVED: &str = "0"; +/// `ResponseCode` returned when the cardholder failed 3D authentication at the ACS. +const RESPONSE_CODE_3D_AUTH_FAILURE: &str = "103"; +/// `ResponseCode` returned when the cardholder abandoned the transaction. +const RESPONSE_CODE_USER_ABORTED: &str = "106"; +/// `ResponseCode` returned when the cardholder never reached the redirect target. +const RESPONSE_CODE_NO_USER_REDIRECT: &str = "700"; +/// `ResponseCode` returned when the cardholder never came back from the redirect. +const RESPONSE_CODE_NO_USER_RETURN: &str = "800"; +/// `ResponseCode` returned when a cardholder redirect (3DS) is required. +const RESPONSE_CODE_REDIRECT_REQUIRED: &str = "600"; +/// Undocumented `ResponseCode` observed on Status Check responses; discriminated on +/// the response `TransTypeID`. +const RESPONSE_CODE_UNDOCUMENTED: &str = "999"; + +/// Response `TransTypeID` values (Transaction Key 4). +const RESP_TRANS_TYPE_SALE: &str = "1"; +const RESP_TRANS_TYPE_AUTHORISE: &str = "2"; +const RESP_TRANS_TYPE_CAPTURE: &str = "3"; +const RESP_TRANS_TYPE_CANCEL: &str = "4"; +const RESP_TRANS_TYPE_REFUND: &str = "5"; +const RESP_TRANS_TYPE_PENDING: &str = "6"; + +/// `TransactionID` value Citigate returns when no transaction was created. +const NO_TRANSACTION_ID: &str = "0"; + +// ============================================================================= +// AUTH +// ============================================================================= + +/// Citigate body-key credentials. +/// +/// `api_key` carries `MerchantName` and `key1` carries `MerchantPassword`; both are +/// injected into every request body rather than into headers. +#[derive(Debug, Clone)] +pub struct CitigateAuthType { + pub merchant_name: Secret, + pub merchant_password: Secret, +} + +impl TryFrom<&ConnectorSpecificConfig> for CitigateAuthType { + type Error = error_stack::Report; + + fn try_from(auth_type: &ConnectorSpecificConfig) -> Result { + match auth_type { + ConnectorSpecificConfig::Citigate { api_key, key1, .. } => Ok(Self { + merchant_name: api_key.to_owned(), + merchant_password: key1.to_owned(), + }), + _ => Err(error_stack::report!( + IntegrationError::FailedToObtainAuthType { + context: IntegrationErrorContext::default() + } + )), + } + } +} + +// ============================================================================= +// CARD BRAND +// ============================================================================= + +/// The `Brand` values Citigate accepts. Any other network is rejected before the +/// request is built rather than being sent and refused with `ResponseCode` 567. +#[derive(Debug, Clone, Copy, Serialize)] +pub enum CitigateBrand { + #[serde(rename = "VISA")] + Visa, + #[serde(rename = "MASTERCARD")] + Mastercard, + #[serde(rename = "AMEX")] + Amex, + #[serde(rename = "DINERS")] + Diners, + #[serde(rename = "MAESTRO")] + Maestro, +} + +fn not_supported(detail: String) -> error_stack::Report { + error_stack::report!(IntegrationError::NotSupported { + message: detail, + connector: "citigate", + context: IntegrationErrorContext::default(), + }) +} + +/// `MerchantRef` is mandatory on every Citigate request and is the *only* key a +/// Transaction Status Check can be resolved by, so an empty reference is rejected +/// up front rather than sent and refused by the gateway. +fn required_merchant_ref(reference: &str) -> Result> { + if reference.is_empty() { + return Err(error_stack::report!( + IntegrationError::MissingRequiredField { + field_name: "merchant_transaction_id", + context: IntegrationErrorContext::default(), + } + )); + } + Ok(reference.to_string()) +} + +/// Resolve the Citigate `Brand` from the supplied card network, falling back to BIN +/// detection when the network is not provided by the caller. +fn get_citigate_brand( + card: &Card, +) -> Result> { + if let Some(network) = card.card_network.as_ref() { + return match network { + CardNetwork::Visa => Ok(CitigateBrand::Visa), + CardNetwork::Mastercard => Ok(CitigateBrand::Mastercard), + CardNetwork::AmericanExpress => Ok(CitigateBrand::Amex), + CardNetwork::DinersClub => Ok(CitigateBrand::Diners), + CardNetwork::Maestro => Ok(CitigateBrand::Maestro), + other => Err(not_supported(format!("Card network {other:?}"))), + }; + } + + match get_card_issuer(card.card_number.peek())? { + CardIssuer::Visa => Ok(CitigateBrand::Visa), + CardIssuer::Master => Ok(CitigateBrand::Mastercard), + CardIssuer::AmericanExpress => Ok(CitigateBrand::Amex), + CardIssuer::DinersClub => Ok(CitigateBrand::Diners), + CardIssuer::Maestro => Ok(CitigateBrand::Maestro), + other => Err(not_supported(format!("Card issuer {other:?}"))), + } +} + +// ============================================================================= +// REQUEST +// ============================================================================= + +/// Purchase request (`TransTypeID = 0`). +/// +/// Citigate's field names are neither `camelCase` nor plain `PascalCase` +/// (`CardNo`, `CVV`, `UserIP`, `StreetLine1`, ...), so every field carries an +/// explicit `#[serde(rename = ...)]`. +#[derive(Debug, Serialize)] +pub struct CitigatePaymentsRequest { + #[serde(rename = "PaymentTypeID")] + pub payment_type_id: String, + #[serde(rename = "TransTypeID")] + pub trans_type_id: String, + #[serde(rename = "MerchantName")] + pub merchant_name: Secret, + #[serde(rename = "MerchantPassword")] + pub merchant_password: Secret, + #[serde(rename = "MerchantRef")] + pub merchant_ref: String, + #[serde(rename = "Currency")] + pub currency: common_enums::Currency, + #[serde(rename = "Amount")] + pub amount: StringMinorUnit, + #[serde(rename = "Brand")] + pub brand: CitigateBrand, + #[serde(rename = "CardholderName")] + pub cardholder_name: Secret, + #[serde(rename = "CardNo")] + pub card_no: RawCardNumber, + #[serde(rename = "ExpiryYear")] + pub expiry_year: Secret, + #[serde(rename = "ExpiryMonth")] + pub expiry_month: Secret, + #[serde(rename = "CVV")] + pub cvv: Secret, + #[serde(rename = "Firstname")] + pub firstname: Secret, + #[serde(rename = "Surname")] + pub surname: Secret, + #[serde(rename = "StreetLine1")] + pub street_line1: Secret, + #[serde(rename = "StreetLine2", skip_serializing_if = "Option::is_none")] + pub street_line2: Option>, + #[serde(rename = "City")] + pub city: Secret, + #[serde(rename = "PostalCode")] + pub postal_code: Secret, + #[serde(rename = "StateProvince", skip_serializing_if = "Option::is_none")] + pub state_province: Option>, + #[serde(rename = "Country")] + pub country: common_enums::CountryAlpha2, + #[serde(rename = "Email")] + pub email: Email, + #[serde(rename = "Telephone", skip_serializing_if = "Option::is_none")] + pub telephone: Option>, + /// `R` — and "Mandatory for Country = "US"" per the field table. Always `None`: + /// UCS carries no billing/customer date of birth on the card Authorize path + /// (`date_of_birth` exists only on `MifinityData` and the airline passenger + /// model), so there is nothing to source it from. Declared rather than omitted + /// so the gap is visible here instead of silently missing from the wire format. + #[serde(rename = "DateOfBirth", skip_serializing_if = "Option::is_none")] + pub date_of_birth: Option>, + #[serde(rename = "UserIP")] + pub user_ip: Secret, + /// `Y**` — mandatory on 3D-Secure-enabled MIDs, ignored elsewhere. Citigate + /// POSTs the cardholder back here once the redirect transaction is approved. + #[serde(rename = "SuccessURL", skip_serializing_if = "Option::is_none")] + pub success_url: Option, + /// `Y**` — the declined counterpart of `SuccessURL`. UCS has a single return + /// URL and re-derives the outcome with PSync rather than from the landing URL, + /// so both carry the same value. + #[serde(rename = "FailURL", skip_serializing_if = "Option::is_none")] + pub fail_url: Option, + /// `Y**` — receives the server-side POST that carries the final result. + #[serde(rename = "CallbackURL", skip_serializing_if = "Option::is_none")] + pub callback_url: Option, +} + +type AuthorizeRouterData = + RouterDataV2, PaymentsResponseData>; + +impl + TryFrom, T>> for CitigatePaymentsRequest +{ + type Error = error_stack::Report; + + fn try_from(item: CitigateRouterData, T>) -> Result { + let router_data = &item.router_data; + let card = match &router_data.request.payment_method_data { + PaymentMethodData::Card(card) => card, + _ => { + return Err(error_stack::report!(IntegrationError::NotImplemented( + "Only card payments are supported by citigate".to_string(), + IntegrationErrorContext::default(), + ))) + } + }; + + // The gateway predates 3DS2: there is no field anywhere in the interface to + // carry an externally obtained CAVV / ECI / dsTransId, so merchant-provided + // authentication data cannot be honoured. + if router_data.request.authentication_data.is_some() { + return Err(error_stack::report!(IntegrationError::NotSupported { + message: "External/merchant-provided 3DS authentication data".to_string(), + connector: "citigate", + context: IntegrationErrorContext::default(), + })); + } + + let auth = CitigateAuthType::try_from(&router_data.connector_config)?; + let common = &router_data.resource_common_data; + + let amount = CitigateAmountConvertor::convert( + router_data.request.minor_amount, + router_data.request.currency, + )?; + + let firstname = common.get_billing_first_name()?; + let surname = common.get_billing_last_name()?; + let cardholder_name = match card.get_optional_cardholder_name() { + Some(name) => name, + None => Secret::new(format!("{} {}", firstname.peek(), surname.peek())), + }; + + let email = match router_data.request.email.clone() { + Some(email) => email, + None => common.get_billing_email()?, + }; + + // `UserIP` is documented as mandatory (`Y`). Fail loudly rather than let the + // gateway reject the transaction with ResponseCode 535. + let user_ip = router_data.request.get_ip_address()?; + + // The field table qualifies three fields with "Mandatory for Country = "US"" + // on top of their `Y`/`R` flag, so the country has to be resolved before the + // request is assembled. Outside the US the documented flags stand on their + // own: `StateProvince` and `Telephone` stay optional, because most countries + // have no state and the gateway does not ask for one. + let country = common.get_billing_country()?; + let is_us_billing = country == common_enums::CountryAlpha2::US; + + let state_province = if is_us_billing { + Some(common.get_billing_state()?) + } else { + common.get_optional_billing_state() + }; + + let telephone = if is_us_billing { + Some(common.get_billing_phone_number()?) + } else { + common.get_optional_billing_phone_number() + }; + + // 3DS is an attribute of the MID, not of the transaction: there is no + // `3ds`/`no_3ds` request flag anywhere in the interface, and the gateway may + // ask for a redirect "irrespective of MID type". The three `Y**` URLs are + // therefore forwarded whenever the caller supplied them — a non-3D MID + // simply ignores them — but are required up front when the caller did ask + // for 3DS, so a missing URL surfaces here instead of as ResponseCode + // 584 / 585 / 586. + let return_url = router_data + .request + .router_return_url + .clone() + .or_else(|| router_data.request.complete_authorize_url.clone()) + .or_else(|| common.return_url.clone()); + let callback_url = router_data.request.webhook_url.clone(); + + if common.auth_type == AuthenticationType::ThreeDs { + if return_url.is_none() { + return Err(error_stack::report!( + IntegrationError::MissingRequiredField { + field_name: "router_return_url", + context: IntegrationErrorContext::default(), + } + )); + } + if callback_url.is_none() { + return Err(error_stack::report!( + IntegrationError::MissingRequiredField { + field_name: "webhook_url", + context: IntegrationErrorContext::default(), + } + )); + } + } + + Ok(Self { + payment_type_id: PAYMENT_TYPE_ID_CARD.to_string(), + trans_type_id: TRANS_TYPE_ID_PURCHASE.to_string(), + merchant_name: auth.merchant_name, + merchant_password: auth.merchant_password, + merchant_ref: common.connector_request_reference_id.clone(), + currency: router_data.request.currency, + amount, + brand: get_citigate_brand(card)?, + cardholder_name, + card_no: card.card_number.clone(), + expiry_year: card.get_expiry_year_4_digit(), + expiry_month: card.get_card_expiry_month_2_digit()?, + cvv: card.card_cvc.clone(), + firstname, + surname, + street_line1: common.get_billing_line1()?, + street_line2: common.get_optional_billing_line2(), + city: common.get_billing_city()?, + postal_code: common.get_billing_zip()?, + state_province, + country, + email, + telephone, + // No domain source on the card path — see the field's doc comment. + date_of_birth: None, + user_ip: Secret::new(user_ip.peek().to_string()), + success_url: return_url.clone(), + fail_url: return_url, + callback_url, + }) + } +} + +// ============================================================================= +// RESPONSE +// ============================================================================= + +/// Citigate's .NET JSON serializer emits an **empty array** (`[]`) for empty string +/// fields, and quotes numeric fields inconsistently. This collapses string, number +/// and `[]`/`null` forms into `Option`; a plain `Option` would fail +/// to deserialize `[]`. +fn deserialize_tolerant_string<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let value = Option::::deserialize(deserializer)?; + Ok(match value { + Some(serde_json::Value::String(string)) if !string.is_empty() => Some(string), + Some(serde_json::Value::Number(number)) => Some(number.to_string()), + Some(serde_json::Value::Bool(boolean)) => Some(boolean.to_string()), + // `[]`, `{}`, `""`, `null` and absent all mean "no value". + _ => None, + }) +} + +/// The single response envelope Citigate returns for every flow and every outcome +/// (approval, bank decline and gateway rejection all arrive as HTTP 200). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CitigatePaymentsResponse { + #[serde( + rename = "TransactionID", + default, + deserialize_with = "deserialize_tolerant_string" + )] + pub transaction_id: Option, + #[serde( + rename = "MerchantRef", + default, + deserialize_with = "deserialize_tolerant_string" + )] + pub merchant_ref: Option, + #[serde( + rename = "TransTypeID", + default, + deserialize_with = "deserialize_tolerant_string" + )] + pub trans_type_id: Option, + #[serde( + rename = "Currency", + default, + deserialize_with = "deserialize_tolerant_string" + )] + pub currency: Option, + #[serde( + rename = "Amount", + default, + deserialize_with = "deserialize_tolerant_string" + )] + pub amount: Option, + #[serde( + rename = "BusinessCase", + default, + deserialize_with = "deserialize_tolerant_string" + )] + pub business_case: Option, + #[serde( + rename = "Descriptor", + default, + deserialize_with = "deserialize_tolerant_string" + )] + pub descriptor: Option, + #[serde( + rename = "Bank", + default, + deserialize_with = "deserialize_tolerant_string" + )] + pub bank: Option, + #[serde( + rename = "ResponseCode", + default, + deserialize_with = "deserialize_tolerant_string" + )] + pub response_code: Option, + #[serde( + rename = "ResponseDescription", + default, + deserialize_with = "deserialize_tolerant_string" + )] + pub response_description: Option, + #[serde( + rename = "BankCode", + default, + deserialize_with = "deserialize_tolerant_string" + )] + pub bank_code: Option, + #[serde( + rename = "BankDescription", + default, + deserialize_with = "deserialize_tolerant_string" + )] + pub bank_description: Option, + #[serde( + rename = "RedirectURL", + default, + deserialize_with = "deserialize_tolerant_string" + )] + pub redirect_url: Option, +} + +impl CitigatePaymentsResponse { + /// `ResponseCode` is the primary status driver; the response `TransTypeID` + /// refines an approval into authorized vs. charged. + fn response_code(&self) -> &str { + self.response_code.as_deref().unwrap_or_default() + } + + fn trans_type_id(&self) -> &str { + self.trans_type_id.as_deref().unwrap_or_default() + } + + /// `true` when the gateway approved the transaction, asked for a cardholder + /// redirect (`600`), or reported it as still pending (`999` + `TransTypeID 6`). + fn is_success(&self) -> bool { + match self.response_code() { + RESPONSE_CODE_APPROVED => true, + // `600` is the normal first answer on a 3D-enabled MID and must travel + // through the success arm — but only if the gateway actually told us + // where to send the cardholder. + RESPONSE_CODE_REDIRECT_REQUIRED => self.redirect_form().is_some(), + RESPONSE_CODE_UNDOCUMENTED => self.trans_type_id() == RESP_TRANS_TYPE_PENDING, + _ => false, + } + } + + /// Status for an approved / redirecting / pending Authorize response. + fn attempt_status(&self) -> AttemptStatus { + match self.response_code() { + RESPONSE_CODE_APPROVED => match self.trans_type_id() { + // Bank supports sales only: authorized *and* captured in one step. + RESP_TRANS_TYPE_SALE | RESP_TRANS_TYPE_CAPTURE => AttemptStatus::Charged, + // Pre-auth performed; capture outstanding (Citigate auto-captures + // open auths after 48-96h). + RESP_TRANS_TYPE_AUTHORISE => AttemptStatus::Authorized, + _ => AttemptStatus::Pending, + }, + // Cardholder must be sent to the ACS page; the response `TransTypeID` + // is not yet meaningful and only settles once the redirect completes. + RESPONSE_CODE_REDIRECT_REQUIRED => AttemptStatus::AuthenticationPending, + _ => AttemptStatus::Pending, + } + } + + /// Status attached to a failed Authorize response. + fn failure_status(&self) -> AttemptStatus { + match self.response_code() { + // Cardholder failed / abandoned authentication at the ACS, never made it + // to the ACS (`700`) or never came back from it (`800`), or the gateway + // asked for a redirect without telling us where to. Every one of these + // fails at the authentication stage, before the bank was ever asked to + // authorize, so none of them should be reported as an authorization + // failure. + RESPONSE_CODE_3D_AUTH_FAILURE + | RESPONSE_CODE_USER_ABORTED + | RESPONSE_CODE_NO_USER_REDIRECT + | RESPONSE_CODE_NO_USER_RETURN + | RESPONSE_CODE_REDIRECT_REQUIRED => AttemptStatus::AuthenticationFailed, + _ => AttemptStatus::AuthorizationFailed, + } + } + + /// Where to send the cardholder when the gateway demanded a redirect. + /// + /// `RedirectURL` is an absolute URL whose query string is an opaque token, so + /// it is handed over verbatim as a `GET` with no form fields — there is no + /// `PaReq`/`creq` to post. It is only populated on `ResponseCode 600`, and the + /// PDF's own sample carries a stray leading space, hence the `trim`. + fn redirect_form(&self) -> Option { + if self.response_code() != RESPONSE_CODE_REDIRECT_REQUIRED { + return None; + } + self.redirect_url + .as_deref() + .map(str::trim) + .filter(|url| !url.is_empty()) + .map(|url| RedirectForm::Form { + endpoint: url.to_string(), + method: Method::Get, + form_fields: HashMap::new(), + }) + } + + /// `true` when a Status Check answered with a usable state. `600` cannot be + /// acted upon in a pull-based sync (there is no browser to redirect), so it is + /// reported as "redirect not consumed yet", i.e. still pending. + fn sync_is_success(&self) -> bool { + match self.response_code() { + RESPONSE_CODE_APPROVED | RESPONSE_CODE_REDIRECT_REQUIRED => true, + RESPONSE_CODE_UNDOCUMENTED => self.trans_type_id() == RESP_TRANS_TYPE_PENDING, + _ => false, + } + } + + /// Status Check status. The response `TransTypeID` describes the *original* + /// transaction, so it is what decides the terminal state after a 3DS redirect. + fn sync_attempt_status(&self) -> AttemptStatus { + match self.response_code() { + RESPONSE_CODE_APPROVED => match self.trans_type_id() { + // A 3D MID runs a sale, so a completed 3DS payment is charged. + RESP_TRANS_TYPE_SALE | RESP_TRANS_TYPE_CAPTURE | RESP_TRANS_TYPE_REFUND => { + AttemptStatus::Charged + } + RESP_TRANS_TYPE_AUTHORISE => AttemptStatus::Authorized, + RESP_TRANS_TYPE_CANCEL => AttemptStatus::Voided, + _ => AttemptStatus::Pending, + }, + // `6` (still processing) and an unconsumed `600` both mean "keep polling". + _ => AttemptStatus::Pending, + } + } + + /// Status attached to a failed Status Check. `TransTypeID 99` means the + /// `MerchantName` + `MerchantPassword` + `MerchantRef` triple matched nothing. + fn sync_failure_status(&self) -> AttemptStatus { + match self.response_code() { + RESPONSE_CODE_3D_AUTH_FAILURE + | RESPONSE_CODE_USER_ABORTED + | RESPONSE_CODE_NO_USER_REDIRECT + | RESPONSE_CODE_NO_USER_RETURN => AttemptStatus::AuthenticationFailed, + _ => AttemptStatus::Failure, + } + } + + /// Bank declines (`ResponseCode < 500`) carry bank-specific detail in + /// `BankCode` / `BankDescription`; gateway rejections (`> 500`) do not. + fn is_bank_decline(&self) -> bool { + self.response_code() + .parse::() + .map(|code| code != 0 && code < 500) + .unwrap_or(false) + } + + /// `TransactionID` is `"0"` when no transaction was created (gateway rejection). + fn connector_transaction_id(&self) -> Option { + self.transaction_id + .as_ref() + .filter(|id| id.as_str() != NO_TRANSACTION_ID) + .cloned() + } + + /// The MID and descriptor actually used, which vary on load-balanced master MIDs. + fn connector_metadata(&self) -> Option { + if self.business_case.is_none() && self.descriptor.is_none() && self.bank.is_none() { + return None; + } + Some(serde_json::json!({ + "business_case": self.business_case, + "descriptor": self.descriptor, + "bank": self.bank, + })) + } + + /// Metadata for Capture and Void, whose response `TransactionID` identifies a + /// **new** leg (auth `310` -> capture `312`) rather than the payment. UCS's + /// `resource_id` must keep pointing at the original authorisation, so the leg id + /// is retained here instead. + fn leg_connector_metadata(&self) -> Option { + let leg_transaction_id = self.connector_transaction_id(); + if leg_transaction_id.is_none() + && self.business_case.is_none() + && self.descriptor.is_none() + && self.bank.is_none() + { + return None; + } + Some(serde_json::json!({ + "business_case": self.business_case, + "descriptor": self.descriptor, + "bank": self.bank, + "leg_transaction_id": leg_transaction_id, + })) + } + + /// `true` when the gateway approved a post-authorization operation + /// (Capture / Void / Refund). These flows have neither a redirect (`600`) nor a + /// pending state, so `ResponseCode` alone decides the outcome. + fn is_approved(&self) -> bool { + self.response_code() == RESPONSE_CODE_APPROVED + } + + /// Capture status. On approval the response echoes `TransTypeID 3` and carries a + /// **new** capture-leg `TransactionID`. + fn capture_attempt_status(&self) -> AttemptStatus { + if self.is_approved() { + AttemptStatus::Charged + } else { + // Includes `561` — the auth was already captured, quite possibly by + // Citigate's own 48-96h auto-capture service, which cannot be disabled. + AttemptStatus::CaptureFailed + } + } + + /// Void / Cancel status. On approval the response echoes `TransTypeID 4`. + fn void_attempt_status(&self) -> AttemptStatus { + if self.is_approved() { + AttemptStatus::Voided + } else { + // `561` here means the auth is no longer open and a Refund is the + // correct operation instead. + AttemptStatus::VoidFailed + } + } + + /// Refund status. `605` ("Bank does not support API refunds") is a failure of + /// the API call even though Citigate logs a manual refund out of band; nothing + /// in this flow can resolve that, so it is reported as a failure verbatim. + fn refund_status(&self) -> RefundStatus { + if self.is_approved() { + RefundStatus::Success + } else { + RefundStatus::Failure + } + } + + /// RSync status. The Status Check reports the `TransTypeID` of the transaction + /// the `MerchantRef` resolved to, so only a refund leg (`5`) may be reported as + /// a settled refund. + fn refund_sync_status(&self) -> RefundStatus { + match (self.response_code(), self.trans_type_id()) { + (RESPONSE_CODE_APPROVED, RESP_TRANS_TYPE_REFUND) => RefundStatus::Success, + // The refund leg exists but has not reached a terminal state yet. + (RESPONSE_CODE_APPROVED, _) | (RESPONSE_CODE_UNDOCUMENTED, RESP_TRANS_TYPE_PENDING) => { + RefundStatus::Pending + } + // `999` + `TransTypeID 99` is "MerchantRef not found". + _ => RefundStatus::Failure, + } + } + + pub fn to_error_response(&self, http_code: u16) -> ErrorResponse { + self.to_error_response_with_status(http_code, self.failure_status()) + } + + fn to_error_response_with_status( + &self, + http_code: u16, + attempt_status: AttemptStatus, + ) -> ErrorResponse { + self.to_flow_error_response(http_code, FlowStatus::Payment(attempt_status)) + } + + fn to_refund_error_response( + &self, + http_code: u16, + refund_status: RefundStatus, + ) -> ErrorResponse { + self.to_flow_error_response(http_code, FlowStatus::Refund(refund_status)) + } + + fn to_flow_error_response(&self, http_code: u16, flow_status: FlowStatus) -> ErrorResponse { + let message = self + .response_description + .clone() + .unwrap_or_else(|| "Citigate transaction failed".to_string()); + let reason = self + .bank_description + .clone() + .or_else(|| self.response_description.clone()); + let is_bank_decline = self.is_bank_decline(); + + ErrorResponse { + status_code: http_code, + code: self + .response_code + .clone() + .unwrap_or_else(|| "NO_RESPONSE_CODE".to_string()), + message, + reason, + attempt_status: Some(flow_status), + connector_transaction_id: self.connector_transaction_id(), + network_decline_code: is_bank_decline.then(|| self.bank_code.clone()).flatten(), + network_advice_code: None, + network_error_message: is_bank_decline + .then(|| self.bank_description.clone()) + .flatten(), + typed_connector_response: None, + raw_connector_response: None, + raw_connector_request: None, + typed_connector_request: None, + } + } +} + +impl TryFrom> + for RouterDataV2, PaymentsResponseData> +{ + type Error = error_stack::Report; + + fn try_from( + item: ResponseRouterData, + ) -> Result { + let response = item.response; + + if !response.is_success() { + return Ok(Self { + response: Err(response.to_error_response(item.http_code)), + resource_common_data: PaymentFlowData { + status: response.failure_status(), + ..item.router_data.resource_common_data + }, + ..item.router_data + }); + } + + let resource_id = match response.connector_transaction_id() { + Some(transaction_id) => ResponseId::ConnectorTransactionId(transaction_id), + None => ResponseId::NoResponseId, + }; + + Ok(Self { + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id, + redirection_data: response.redirect_form().map(Box::new), + mandate_reference: None, + connector_metadata: response.connector_metadata(), + network_txn_id: None, + network_txn_link_id: None, + connector_response_reference_id: response.merchant_ref.clone(), + incremental_authorization_allowed: None, + splits: None, + status_code: item.http_code, + }), + resource_common_data: PaymentFlowData { + status: response.attempt_status(), + ..item.router_data.resource_common_data + }, + ..item.router_data + }) + } +} + +// ============================================================================= +// PSYNC — TRANSACTION STATUS CHECK (`TransTypeID = 8`) +// ============================================================================= + +/// Transaction Status Check request. +/// +/// The lookup key is the `MerchantName` + `MerchantPassword` + `MerchantRef` +/// triple — **not** the `TransactionID`. Querying with a different MID than the +/// one that created the payment therefore reports a perfectly good transaction as +/// `TransTypeID 99` / "MerchantRef not found". +#[derive(Debug, Serialize)] +pub struct CitigateSyncRequest { + #[serde(rename = "PaymentTypeID")] + pub payment_type_id: String, + #[serde(rename = "TransTypeID")] + pub trans_type_id: String, + #[serde(rename = "MerchantName")] + pub merchant_name: Secret, + #[serde(rename = "MerchantPassword")] + pub merchant_password: Secret, + #[serde(rename = "MerchantRef")] + pub merchant_ref: String, +} + +type SyncRouterData = RouterDataV2; + +impl + TryFrom> for CitigateSyncRequest +{ + type Error = error_stack::Report; + + fn try_from(item: CitigateRouterData) -> Result { + let router_data = &item.router_data; + let auth = CitigateAuthType::try_from(&router_data.connector_config)?; + + Ok(Self { + payment_type_id: PAYMENT_TYPE_ID_CARD.to_string(), + trans_type_id: TRANS_TYPE_ID_STATUS_CHECK.to_string(), + merchant_name: auth.merchant_name, + merchant_password: auth.merchant_password, + merchant_ref: required_merchant_ref( + &router_data + .resource_common_data + .connector_request_reference_id, + )?, + }) + } +} + +/// Status Check answers with the very same envelope as a purchase (minus +/// `RedirectURL`); the newtype exists only to give the macro framework a distinct +/// response type per flow. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(transparent)] +pub struct CitigateSyncResponse(pub CitigatePaymentsResponse); + +impl TryFrom> for SyncRouterData { + type Error = error_stack::Report; + + fn try_from(item: ResponseRouterData) -> Result { + let response = item.response.0; + + if !response.sync_is_success() { + return Ok(Self { + response: Err(response + .to_error_response_with_status(item.http_code, response.sync_failure_status())), + resource_common_data: PaymentFlowData { + status: response.sync_failure_status(), + ..item.router_data.resource_common_data + }, + ..item.router_data + }); + } + + let resource_id = match response.connector_transaction_id() { + Some(transaction_id) => ResponseId::ConnectorTransactionId(transaction_id), + None => item.router_data.request.connector_transaction_id.clone(), + }; + + Ok(Self { + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id, + // A sync has no browser to redirect: the redirect instruction, if + // any, was already handed out by Authorize. + redirection_data: None, + mandate_reference: None, + connector_metadata: response.connector_metadata(), + network_txn_id: None, + network_txn_link_id: None, + connector_response_reference_id: response.merchant_ref.clone(), + incremental_authorization_allowed: None, + splits: None, + status_code: item.http_code, + }), + resource_common_data: PaymentFlowData { + status: response.sync_attempt_status(), + ..item.router_data.resource_common_data + }, + ..item.router_data + }) + } +} + +// ============================================================================= +// CAPTURE (`TransTypeID = 3`) +// ============================================================================= + +/// Capture request — settles an open authorisation. +/// +/// The documented field list has exactly six entries and **no `Amount`**: Citigate +/// cannot perform a partial, multiple or incremental capture. +#[derive(Debug, Serialize)] +pub struct CitigateCaptureRequest { + #[serde(rename = "PaymentTypeID")] + pub payment_type_id: String, + #[serde(rename = "TransTypeID")] + pub trans_type_id: String, + #[serde(rename = "MerchantName")] + pub merchant_name: Secret, + #[serde(rename = "MerchantPassword")] + pub merchant_password: Secret, + #[serde(rename = "MerchantRef")] + pub merchant_ref: String, + /// The transaction reference from the original authorisation. + #[serde(rename = "TransactionID")] + pub transaction_id: String, +} + +type CaptureRouterData = + RouterDataV2; + +impl + TryFrom> for CitigateCaptureRequest +{ + type Error = error_stack::Report; + + fn try_from(item: CitigateRouterData) -> Result { + let router_data = &item.router_data; + let request = &router_data.request; + + // Citigate's capture body has no `Amount` field, so the gateway always settles the + // whole authorisation. Reject the partial- and multi-capture intents up front rather + // than sending a request whose amount the wire format cannot express. + // + // Known gap — a lone partial capture is not detectable here. The capture contract + // (`PaymentServiceCaptureRequest`) carries `amount_to_capture` but never the amount + // that was originally authorised, and `PaymentFlowData::minor_amount_authorized` is + // a response-reporting field: every request-path constructor sets it to `None`. So a + // single `Manual` capture for less than the authorisation is indistinguishable from a + // full one at this layer, and will settle in full. Closing that gap needs the + // authorised amount added to the capture contract; see the follow-up on this PR. + if request.is_multiple_capture() { + return Err(not_supported("Multiple partial captures".to_string())); + } + if let Some(method @ (CaptureMethod::ManualMultiple | CaptureMethod::Scheduled)) = + &request.capture_method + { + return Err(not_supported(format!("{method} capture"))); + } + + 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; + + // Same shape as Capture: the cancel body has no `Amount` field, so Citigate always + // voids the whole authorisation. `PaymentVoidData::amount` does reach us, but there + // is nothing to compare it against — `minor_amount_authorized` is `None` on every + // request path — so a partial void cannot be rejected here and will void in full. + + let auth = CitigateAuthType::try_from(&router_data.connector_config)?; + + Ok(Self { + payment_type_id: PAYMENT_TYPE_ID_CARD.to_string(), + trans_type_id: TRANS_TYPE_ID_CANCEL.to_string(), + merchant_name: auth.merchant_name, + merchant_password: auth.merchant_password, + merchant_ref: required_merchant_ref( + &router_data + .resource_common_data + .connector_request_reference_id, + )?, + transaction_id: request.connector_transaction_id.clone(), + }) + } +} + +/// Cancel answers with the shared response envelope. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(transparent)] +pub struct CitigateVoidResponse(pub CitigatePaymentsResponse); + +impl TryFrom> for VoidRouterData { + type Error = error_stack::Report; + + fn try_from(item: ResponseRouterData) -> Result { + let response = item.response.0; + let status = response.void_attempt_status(); + + if !response.is_approved() { + return Ok(Self { + response: Err(response.to_error_response_with_status(item.http_code, status)), + resource_common_data: PaymentFlowData { + status, + ..item.router_data.resource_common_data + }, + ..item.router_data + }); + } + + Ok(Self { + response: Ok(PaymentsResponseData::TransactionResponse { + // As with Capture, the response `TransactionID` is the new cancel + // leg and must not replace the payment's id. + resource_id: ResponseId::ConnectorTransactionId( + item.router_data.request.connector_transaction_id.clone(), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: response.leg_connector_metadata(), + network_txn_id: None, + network_txn_link_id: None, + connector_response_reference_id: response.merchant_ref.clone(), + incremental_authorization_allowed: None, + splits: None, + status_code: item.http_code, + }), + resource_common_data: PaymentFlowData { + status, + ..item.router_data.resource_common_data + }, + ..item.router_data + }) + } +} + +// ============================================================================= +// REFUND (`TransTypeID = 5`) +// ============================================================================= + +/// Refund request — refunds a sale, or an authorisation that has been captured +/// (including one auto-captured by Citigate, in which case the *authorisation's* +/// `TransactionID` is still the correct value to send). +#[derive(Debug, Serialize)] +pub struct CitigateRefundRequest { + #[serde(rename = "PaymentTypeID")] + pub payment_type_id: String, + #[serde(rename = "TransTypeID")] + pub trans_type_id: String, + #[serde(rename = "MerchantName")] + pub merchant_name: Secret, + #[serde(rename = "MerchantPassword")] + pub merchant_password: Secret, + /// Also the RSync lookup key — the Status Check cannot be keyed on anything else. + #[serde(rename = "MerchantRef")] + pub merchant_ref: String, + #[serde(rename = "TransactionID")] + pub transaction_id: String, + /// Omitted for a full refund, which is both the documented default and the only + /// form that works on an account where partial refunds have not been enabled + /// (the default; sending an `Amount` there is rejected with `629`). + #[serde(rename = "Amount", skip_serializing_if = "Option::is_none")] + pub amount: Option, +} + +type RefundRouterData = RouterDataV2; + +impl + TryFrom> for CitigateRefundRequest +{ + type Error = error_stack::Report; + + fn try_from(item: CitigateRouterData) -> Result { + let router_data = &item.router_data; + let request = &router_data.request; + let auth = CitigateAuthType::try_from(&router_data.connector_config)?; + + let amount = if request.minor_refund_amount == request.minor_payment_amount { + None + } else { + Some(CitigateAmountConvertor::convert( + request.minor_refund_amount, + request.currency, + )?) + }; + + Ok(Self { + payment_type_id: PAYMENT_TYPE_ID_CARD.to_string(), + trans_type_id: TRANS_TYPE_ID_REFUND.to_string(), + merchant_name: auth.merchant_name, + merchant_password: auth.merchant_password, + merchant_ref: required_merchant_ref( + &router_data + .resource_common_data + .connector_request_reference_id, + )?, + transaction_id: request.connector_transaction_id.clone(), + amount, + }) + } +} + +/// Refund answers with the shared response envelope. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(transparent)] +pub struct CitigateRefundResponse(pub CitigatePaymentsResponse); + +impl TryFrom> for RefundRouterData { + type Error = error_stack::Report; + + fn try_from( + item: ResponseRouterData, + ) -> Result { + let response = item.response.0; + let refund_status = response.refund_status(); + + if !response.is_approved() { + return Ok(Self { + response: Err(response.to_refund_error_response(item.http_code, refund_status)), + resource_common_data: RefundFlowData { + status: refund_status, + ..item.router_data.resource_common_data + }, + ..item.router_data + }); + } + + // Here the new leg id *is* the refund, so it becomes `connector_refund_id`. + let connector_refund_id = response + .connector_transaction_id() + .unwrap_or_else(|| item.router_data.request.refund_id.clone()); + + Ok(Self { + response: Ok(RefundsResponseData { + connector_refund_id, + refund_status, + status_code: item.http_code, + acquirer_reference_number: None, + }), + resource_common_data: RefundFlowData { + status: refund_status, + ..item.router_data.resource_common_data + }, + ..item.router_data + }) + } +} + +// ============================================================================= +// RSYNC — TRANSACTION STATUS CHECK ON THE REFUND'S `MerchantRef` +// ============================================================================= + +/// Refund status check. Byte-identical on the wire to [`CitigateSyncRequest`] — the +/// only thing that makes a `TransTypeID = 8` call an RSync rather than a PSync is +/// that the `MerchantRef` is the **refund's** reference. Keying it on +/// `connector_refund_id` (a Citigate `TransactionID`, which the status check has no +/// field for) or on the Authorize `MerchantRef` would silently resolve the payment +/// leg instead. +#[derive(Debug, Serialize)] +#[serde(transparent)] +pub struct CitigateRefundSyncRequest(pub CitigateSyncRequest); + +type RefundSyncRouterData = + RouterDataV2; + +impl + TryFrom> for CitigateRefundSyncRequest +{ + type Error = error_stack::Report; + + fn try_from(item: CitigateRouterData) -> Result { + let router_data = &item.router_data; + let auth = CitigateAuthType::try_from(&router_data.connector_config)?; + + Ok(Self(CitigateSyncRequest { + payment_type_id: PAYMENT_TYPE_ID_CARD.to_string(), + trans_type_id: TRANS_TYPE_ID_STATUS_CHECK.to_string(), + merchant_name: auth.merchant_name, + merchant_password: auth.merchant_password, + // The RefundSync RouterData carries the same + // `connector_request_reference_id` the Refund request used. + merchant_ref: required_merchant_ref( + &router_data + .resource_common_data + .connector_request_reference_id, + )?, + })) + } +} + +/// The refund status check answers with the shared response envelope. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(transparent)] +pub struct CitigateRefundSyncResponse(pub CitigatePaymentsResponse); + +impl TryFrom> for RefundSyncRouterData { + type Error = error_stack::Report; + + fn try_from( + item: ResponseRouterData, + ) -> Result { + let response = item.response.0; + let refund_status = response.refund_sync_status(); + + if refund_status == RefundStatus::Failure { + return Ok(Self { + response: Err(response.to_refund_error_response(item.http_code, refund_status)), + resource_common_data: RefundFlowData { + status: refund_status, + ..item.router_data.resource_common_data + }, + ..item.router_data + }); + } + + let connector_refund_id = response + .connector_transaction_id() + .unwrap_or_else(|| item.router_data.request.connector_refund_id.clone()); + + Ok(Self { + response: Ok(RefundsResponseData { + connector_refund_id, + refund_status, + status_code: item.http_code, + acquirer_reference_number: None, + }), + resource_common_data: RefundFlowData { + status: refund_status, + ..item.router_data.resource_common_data + }, + ..item.router_data + }) + } +} diff --git a/crates/integrations/connector-integration/src/connectors/fiuu/transformers.rs b/crates/integrations/connector-integration/src/connectors/fiuu/transformers.rs index c421275d82..920c91e24a 100644 --- a/crates/integrations/connector-integration/src/connectors/fiuu/transformers.rs +++ b/crates/integrations/connector-integration/src/connectors/fiuu/transformers.rs @@ -606,7 +606,7 @@ impl { Err(error_stack::report!(IntegrationError::NotSupported { message: utils::get_unimplemented_payment_method_error_message("fiuu"), @@ -979,7 +979,7 @@ impl { Err(error_stack::report!(IntegrationError::NotSupported { message: utils::get_unimplemented_payment_method_error_message("fiuu"), diff --git a/crates/integrations/connector-integration/src/connectors/ilixium.rs b/crates/integrations/connector-integration/src/connectors/ilixium.rs new file mode 100644 index 0000000000..c1e4657b70 --- /dev/null +++ b/crates/integrations/connector-integration/src/connectors/ilixium.rs @@ -0,0 +1,667 @@ +//! Ilixium — Direct API (card, one-time payments). +//! +//! Scope: the **PreAuthenticate**, **Authorize**, **Capture**, **Void**, **PSync**, **Refund** +//! and **RSync** flows, for **Card** payment methods. +//! +//! Ilixium has exactly **one** authorisation endpoint, and it performs 3-D Secure inside it +//! rather than through standalone authentication endpoints. A card payment is therefore one or +//! two round trips depending on whether the issuer challenges: +//! +//! * `POST /direct/auth` — the authorisation. Runs as **PreAuthenticate** when the payment is +//! 3DS (see the flow block below) and as **Authorize** when it is not. On a 3DS-enabled +//! account it *may* answer `status.code = PENDING` with the ACS redirect data instead of a +//! final result — but it may equally answer `SUCCESS`, because Ilixium decides at response +//! time whether a challenge is needed. A not-enrolled or frictionless card is charged by this +//! single call and has no second leg. +//! * `POST /direct/threedcomplete` — the finalisation, sent as **Authorize** when UCS re-invokes +//! the flow after the ACS posts `md`/`paRes` back to `TermUrl`. Selected by +//! [`transformers::is_three_ds_completion`], which is also what the request-body builder +//! uses, so the URL and the body cannot disagree about which leg is being sent. +//! +//! This is why `/direct/threedcomplete` is modelled as `Authorize` and not as +//! `PostAuthenticate`: it is what moves the money (the vendor is explicit that "no customer +//! funds have been ring-fenced prior to this stage"), and the authentication flows are defined +//! to produce authentication data for a *following* Authorize to spend. Modelling it as +//! `PostAuthenticate` would leave an Authorize still to run, re-sending `/direct/auth` and +//! earning response code 102, "Duplicate Merchant Ref". +//! +//! Capture and Void are each a single round trip: +//! +//! * `POST /direct/capture` — completes a deferred-capture authorisation. It is bound to the +//! original payment by `transaction.merchantRef` (the authorisation's own reference), not by +//! a gateway id, so the reference is recomputed from the same UCS value both legs used. +//! * `POST /direct/reversal` — Ilixium's name for a void. A separate endpoint, *not* a capture +//! for zero, with a body field-for-field identical to the capture body and the same +//! `merchantRef` binding. Partial reversals are not supported, so the amount must be the +//! original transaction's in full (see `transformers::IlixiumVoidRequest`). +//! +//! Refund is likewise a single round trip, and the first flow here built on `RefundFlowData`: +//! +//! * `POST /direct/refund` — variant A only (a refund that references a previous transaction), +//! bound to the payment by the same `transaction.merchantRef`. Partial refunds are supported +//! and accumulate; the payment must already be captured. `/direct/credit` is a payout, not a +//! refund, and is never called. Ilixium returns no refund identifier of any kind — see +//! `transformers::refund_identifier`. +//! +//! PSync and RSync are the outliers, and they are the *same* call: +//! +//! * `POST /history/operations` — note the path is **not** under `/direct/`. Ilixium publishes no +//! per-payment status endpoint and no refund-status endpoint at all, so a sync is a **bulk, +//! time-windowed reconciliation report** (max 24 hours, no server-side filter of any kind) that +//! the connector then filters client-side. It is a POST with a body, unlike the GET-based +//! PSync/RSync most connectors have. See `transformers::IlixiumHistoryRequest`. +//! +//! PSync keeps the entries whose `type` is a payment-lifecycle operation; RSync keeps those +//! whose `type` is `REFUND` — never `CREDIT`, which is a `/direct/credit` payout. Both filter on +//! the **original payment's** `transaction.merchantRef`, which on the refund side has to be +//! resolved out of `RefundSyncData` rather than out of the flow's common data. Refund entries +//! echo the payment's `merchantRef` *and* `gatewayRef`, so distinguishing one partial refund +//! from another is a known, documented limitation — see +//! `transformers::IlixiumHistoryResponse::match_refund_operation`. +//! +//! Two things about this API drive the whole design: +//! +//! 1. **Every** business failure — validation rejection, decline, 3DS-required, internal +//! error — comes back as **HTTP 200** with the outcome in `status.code`. Nothing here +//! branches on the HTTP status. +//! 2. Authentication is a body-derived digest (`X-MERCHANT-DIGEST`), not a static key, so it +//! is computed in `build_headers` from the exact serialised body rather than in +//! `ConnectorCommon::get_auth_header`. +//! +//! Reference: `grace/rulesbook/codegen/references/ilixium/technical_specification.md` + +pub mod transformers; + +use std::fmt::Debug; + +use common_enums::CurrencyUnit; +use common_utils::{ + errors::CustomResult, events, ext_traits::ByteSliceExt, request::Method, types::StringMinorUnit, +}; +use domain_types::{ + connector_flow::{Authorize, Capture, PSync, PreAuthenticate, RSync, Refund, Void}, + connector_types::*, + errors, + 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::{Mask, Maskable, PeekInterface}; +use interfaces::{ + api::ConnectorCommon, connector_integration_v2::ConnectorIntegrationV2, connector_types, + decode::BodyDecoding, +}; +use serde::Serialize; +use transformers::{ + self as ilixium, IlixiumAuthorizeRequest, IlixiumCaptureRequest, IlixiumCaptureResponse, + IlixiumHistoryRequest, IlixiumHistoryResponse, IlixiumPaymentResponse, + IlixiumPreAuthenticateRequest, IlixiumPreAuthenticateResponse, IlixiumRefundHistoryRequest, + IlixiumRefundHistoryResponse, IlixiumRefundRequest, IlixiumRefundResponse, IlixiumVoidRequest, + IlixiumVoidResponse, +}; + +use super::macros; +use crate::{types::ResponseRouterData, with_error_response_body}; + +pub(crate) mod headers { + pub(crate) const X_MERCHANT_DIGEST: &str = "X-MERCHANT-DIGEST"; + pub(crate) const CONTENT_TYPE: &str = "Content-Type"; + pub(crate) const ACCEPT: &str = "Accept"; +} + +/// The Direct API defaults to `text/xml`, so both `Content-Type` and `Accept` must be sent +/// explicitly for a JSON integration. +const ILIXIUM_JSON_MEDIA_TYPE: &str = "application/json"; + +const AUTH_ENDPOINT: &str = "/direct/auth"; +const THREE_DS_COMPLETE_ENDPOINT: &str = "/direct/threedcomplete"; +const CAPTURE_ENDPOINT: &str = "/direct/capture"; +/// Ilixium calls a void a *reversal*; there is no `void`/`cancel` endpoint on the Direct API. +const VOID_ENDPOINT: &str = "/direct/reversal"; +/// The refund endpoint. **Not** `/direct/credit`: that is a money-out *payout* to a payment +/// method (`creditRequest`, response `type: CREDIT`, its own error codes 126 and 205–208) which +/// happens to have an almost identical body, and it must never be used for a refund. +const REFUND_ENDPOINT: &str = "/direct/refund"; +/// The operation-history report, and the **only** query the Direct API offers — there is no +/// per-payment status endpoint (see the module docs). Note the path is `/history/…`, *not* under +/// `/direct/` like every other endpoint above. +const HISTORY_OPERATIONS_ENDPOINT: &str = "/history/operations"; + +impl ConnectorCommon + for Ilixium +{ + fn id(&self) -> &'static str { + "ilixium" + } + + fn get_currency_unit(&self) -> CurrencyUnit { + CurrencyUnit::Minor + } + + fn common_get_content_type(&self) -> &'static str { + ILIXIUM_JSON_MEDIA_TYPE + } + + fn base_url<'a>(&self, connectors: &'a Connectors) -> &'a str { + connectors.ilixium.base_url.as_ref() + } + + fn get_auth_header( + &self, + auth_type: &ConnectorSpecificConfig, + ) -> CustomResult)>, errors::IntegrationError> { + ilixium::IlixiumAuthType::try_from(auth_type)?; + Ok(Vec::new()) + } + + fn build_error_response( + &self, + res: Response, + event_builder: Option<&mut events::Event>, + _connector_config: &ConnectorSpecificConfig, + ) -> CustomResult { + match res + .response + .parse_struct::("IlixiumPaymentResponse") + { + Ok(response) => { + with_error_response_body!(event_builder, response); + + let reason_codes = response.reason_codes(); + Ok(ErrorResponse { + status_code: res.status_code, + code: reason_codes + .first() + .cloned() + .unwrap_or_else(|| format!("{:?}", response.status.code).to_uppercase()), + message: response + .status + .message + .clone() + .unwrap_or_else(|| common_utils::consts::NO_ERROR_MESSAGE.to_string()), + reason: if reason_codes.is_empty() { + response.status.message.clone() + } else { + Some(reason_codes.join(", ")) + }, + attempt_status: None, + connector_transaction_id: response.gateway_ref(), + network_decline_code: None, + network_advice_code: None, + network_error_message: None, + typed_connector_response: None, + raw_connector_response: None, + raw_connector_request: None, + typed_connector_request: None, + }) + } + Err(_) => { + let raw_body = String::from_utf8_lossy(&res.response).to_string(); + tracing::warn!( + status_code = res.status_code, + body = %raw_body, + "Ilixium returned a body that is not a paymentResponse envelope — the \ + Direct API answers HTTP 200 with a paymentResponse for every documented \ + outcome, so this is most likely an intermediary proxy/WAF/gateway response" + ); + Ok(ErrorResponse { + status_code: res.status_code, + code: common_utils::consts::NO_ERROR_CODE.to_string(), + message: common_utils::consts::NO_ERROR_MESSAGE.to_string(), + reason: Some(raw_body.chars().take(500).collect()), + attempt_status: None, + connector_transaction_id: None, + network_decline_code: None, + network_advice_code: None, + network_error_message: None, + typed_connector_response: None, + raw_connector_response: None, + raw_connector_request: None, + typed_connector_request: None, + }) + } + } + } +} + +impl BodyDecoding + for Ilixium +{ +} + +macros::create_amount_converter_wrapper!(connector_name: Ilixium, amount_type: StringMinorUnit); + +macros::create_all_prerequisites!( + connector_name: Ilixium, + generic_type: T, + api: [ + ( + flow: Authorize, + request_body: IlixiumAuthorizeRequest, + response_body: IlixiumPaymentResponse, + router_data: RouterDataV2, PaymentsResponseData>, + ), + ( + + flow: PreAuthenticate, + request_body: IlixiumPreAuthenticateRequest, + response_body: IlixiumPreAuthenticateResponse, + router_data: RouterDataV2, PaymentsResponseData>, + ), + ( + flow: Capture, + request_body: IlixiumCaptureRequest, + response_body: IlixiumCaptureResponse, + router_data: RouterDataV2, + ), + ( + flow: Void, + request_body: IlixiumVoidRequest, + response_body: IlixiumVoidResponse, + router_data: RouterDataV2, + ), + ( + flow: PSync, + request_body: IlixiumHistoryRequest, + response_body: IlixiumHistoryResponse, + router_data: RouterDataV2, + ), + ( + flow: Refund, + request_body: IlixiumRefundRequest, + response_body: IlixiumRefundResponse, + router_data: RouterDataV2, + ), + ( + flow: RSync, + request_body: IlixiumRefundHistoryRequest, + response_body: IlixiumRefundHistoryResponse, + router_data: RouterDataV2, + ) + ], + amount_converters: [ + amount_converter: StringMinorUnit + ], + member_functions: { + /// Builds the three Ilixium request headers. + /// + /// `X-MERCHANT-DIGEST` must be computed over the **exact** bytes that will be + /// transmitted, so the body is taken from `get_request_body` and digested verbatim. + /// That is the same value the framework puts on the wire: `RequestContent:: + /// get_body_bytes` serialises through `get_inner_value`, which is what + /// `content.content.get_inner_value()` returns here, so the digest can never drift + /// from the payload. + /// + /// Note the `.content` hop: `ConnectorRequestData` carries both the wire body + /// (`content`) and a *masked* typed copy kept for observability (`typed_request`). + /// The digest must be taken over the former — digesting the masked copy would sign + /// redacted JSON and every request would be rejected by the gateway. + /// + /// Generic over the flow-common-data type (`FCD`) rather than pinned to + /// `PaymentFlowData`: the digest is computed from the serialised body alone, so the + /// same code serves the payment flows and the refund flows (`RefundFlowData`) without + /// duplication. + pub fn build_headers( + &self, + req: &RouterDataV2, + ) -> CustomResult)>, errors::IntegrationError> + where + Self: ConnectorIntegrationV2, + { + let method = self.get_http_method(); + let body = match method { + Method::Get => String::default(), + Method::Post | Method::Put | Method::Delete | Method::Patch => self + .get_request_body(req)? + .map(|content| content.content.get_inner_value().peek().to_owned()) + .unwrap_or_default(), + }; + + let auth = ilixium::IlixiumAuthType::try_from(&req.connector_config)?; + let digest = auth.compute_merchant_digest(&body).change_context( + errors::IntegrationError::RequestEncodingFailed { + context: errors::IntegrationErrorContext { + suggested_action: None, + doc_url: Some( + "https://docs.ilixium.com/docs/direct/digest".to_string(), + ), + additional_context: Some( + "Failed to compute the X-MERCHANT-DIGEST authentication header \ + for an Ilixium payment request. An incorrect digest is rejected \ + by the platform with response code 101, 'Invalid Merchant \ + Credentials'." + .to_string(), + ), + }, + }, + )?; + + Ok(vec![ + ( + headers::CONTENT_TYPE.to_string(), + self.get_content_type().to_string().into(), + ), + ( + headers::ACCEPT.to_string(), + ILIXIUM_JSON_MEDIA_TYPE.to_string().into(), + ), + (headers::X_MERCHANT_DIGEST.to_string(), digest.into_masked()), + ]) + } + + pub fn connector_base_url_payments<'a, F, Req, Res>( + &self, + req: &'a RouterDataV2, + ) -> &'a str { + &req.resource_common_data.connectors.ilixium.base_url + } + + /// Refund-side counterpart of [`Self::connector_base_url_payments`]. Ilixium serves + /// every Direct API operation from one host, so both resolve to the same value; the + /// split exists only because `RefundFlowData` is a different common-data type. + pub fn connector_base_url_refunds<'a, F, Req, Res>( + &self, + req: &'a RouterDataV2, + ) -> &'a str { + &req.resource_common_data.connectors.ilixium.base_url + } + } +); + +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Ilixium, + curl_request: Json(IlixiumAuthorizeRequest), + curl_response: IlixiumPaymentResponse, + 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)>, errors::IntegrationError> { + self.build_headers(req) + } + fn get_url( + &self, + req: &RouterDataV2, PaymentsResponseData>, + ) -> CustomResult { + let endpoint = if ilixium::is_three_ds_completion(&req.request) { + THREE_DS_COMPLETE_ENDPOINT + } else { + AUTH_ENDPOINT + }; + Ok(format!("{}{}", self.connector_base_url_payments(req), endpoint)) + } + } +); + +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Ilixium, + curl_request: Json(IlixiumPreAuthenticateRequest), + curl_response: IlixiumPreAuthenticateResponse, + flow_name: PreAuthenticate, + resource_common_data: PaymentFlowData, + flow_request: PaymentsPreAuthenticateData, + 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)>, errors::IntegrationError> { + self.build_headers(req) + } + fn get_url( + &self, + req: &RouterDataV2, PaymentsResponseData>, + ) -> CustomResult { + Ok(format!("{}{}", self.connector_base_url_payments(req), AUTH_ENDPOINT)) + } + } +); + +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Ilixium, + curl_request: Json(IlixiumCaptureRequest), + curl_response: IlixiumCaptureResponse, + 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)>, errors::IntegrationError> { + self.build_headers(req) + } + fn get_url( + &self, + req: &RouterDataV2, + ) -> CustomResult { + Ok(format!("{}{}", self.connector_base_url_payments(req), CAPTURE_ENDPOINT)) + } + } +); + +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Ilixium, + curl_request: Json(IlixiumVoidRequest), + curl_response: IlixiumVoidResponse, + 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)>, errors::IntegrationError> { + self.build_headers(req) + } + fn get_url( + &self, + req: &RouterDataV2, + ) -> CustomResult { + Ok(format!("{}{}", self.connector_base_url_payments(req), VOID_ENDPOINT)) + } + } +); + +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Ilixium, + curl_request: Json(IlixiumHistoryRequest), + curl_response: IlixiumHistoryResponse, + 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)>, errors::IntegrationError> { + self.build_headers(req) + } + fn get_url( + &self, + req: &RouterDataV2, + ) -> CustomResult { + Ok(format!("{}{}", self.connector_base_url_payments(req), HISTORY_OPERATIONS_ENDPOINT)) + } + } +); + +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Ilixium, + curl_request: Json(IlixiumRefundRequest), + curl_response: IlixiumRefundResponse, + 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)>, errors::IntegrationError> { + self.build_headers(req) + } + fn get_url( + &self, + req: &RouterDataV2, + ) -> CustomResult { + Ok(format!("{}{}", self.connector_base_url_refunds(req), REFUND_ENDPOINT)) + } + } +); + +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Ilixium, + curl_request: Json(IlixiumRefundHistoryRequest), + curl_response: IlixiumRefundHistoryResponse, + 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)>, errors::IntegrationError> { + self.build_headers(req) + } + fn get_url( + &self, + req: &RouterDataV2, + ) -> CustomResult { + Ok(format!("{}{}", self.connector_base_url_refunds(req), HISTORY_OPERATIONS_ENDPOINT)) + } + } +); + +impl + connector_types::ConnectorServiceTrait for Ilixium +{ +} + +impl + connector_types::PaymentAuthorizeV2 for Ilixium +{ +} + +impl + connector_types::PaymentPreAuthenticateV2 for Ilixium +{ +} + +impl + connector_types::PaymentCapture for Ilixium +{ +} + +impl + connector_types::PaymentVoidV2 for Ilixium +{ +} + +impl + connector_types::PaymentSyncV2 for Ilixium +{ +} + +impl + connector_types::RefundV2 for Ilixium +{ +} + +impl + connector_types::RefundSyncV2 for Ilixium +{ +} + +impl + connector_types::ValidationTrait for Ilixium +{ +} + +impl + connector_types::IncomingWebhook for Ilixium +{ +} + +impl + connector_types::VerifyRedirectResponse for Ilixium +{ +} + +impl + interfaces::verification::SourceVerification for Ilixium +{ +} + +crate::connectors::macros::macro_connector_payout_implementation!( + connector: Ilixium, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize] +); + +crate::connectors::macros::macro_connector_flow_status_impls!( + connector: Ilixium, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize], + not_implemented: [ + PaymentMethodToken, + SetupMandate, + RepeatPayment + ], + not_supported: [ + VoidPC, + VoidPostRefund, + IncrementalAuthorization, + CreateOrder, + CreateConnectorCustomer, + GetConnectorCustomer, + MandateRevoke, + Authenticate, + PostAuthenticate, + ServerAuthenticationToken, + ServerSessionAuthenticationToken, + ClientAuthenticationToken, + Accept, + SubmitEvidence, + DefendDispute + ], +); diff --git a/crates/integrations/connector-integration/src/connectors/ilixium/transformers.rs b/crates/integrations/connector-integration/src/connectors/ilixium/transformers.rs new file mode 100644 index 0000000000..265e2c694f --- /dev/null +++ b/crates/integrations/connector-integration/src/connectors/ilixium/transformers.rs @@ -0,0 +1,3785 @@ +use base64::Engine; +use common_enums::{AttemptStatus, AuthenticationType, Currency, RefundStatus}; +use common_utils::{ + crypto::{self, GenerateDigest}, + pii::Email, + types::StringMinorUnit, +}; +use domain_types::{ + connector_flow::{Authorize, Capture, PSync, PreAuthenticate, RSync, Refund, Void}, + connector_types::{ + PaymentFlowData, PaymentVoidData, PaymentsAuthorizeData, PaymentsCaptureData, + PaymentsPreAuthenticateData, PaymentsResponseData, PaymentsSyncData, RawConnectorStatus, + RefundFlowData, RefundSyncData, RefundsData, RefundsResponseData, ResponseId, + }, + errors, + payment_method_data::{PaymentMethodData, PaymentMethodDataTypes, RawCardNumber}, + router_data::{ConnectorSpecificConfig, ErrorResponse}, + router_data_v2::RouterDataV2, + router_request_types::BrowserInformation, + router_response_types::RedirectForm, +}; +use error_stack::ResultExt; +use hyperswitch_masking::{ExposeInterface, PeekInterface, Secret}; +use serde::{Deserialize, Serialize}; +use time::{format_description::well_known::Rfc3339, Duration, OffsetDateTime, UtcOffset}; + +use super::IlixiumRouterData; + +pub const BASE64_ENGINE: base64::engine::GeneralPurpose = base64::engine::general_purpose::STANDARD; + +/// Message-level API version. The platform's `version` field is `2` for every +/// in-scope Direct API message (`/direct/auth`, `/direct/threedcomplete`). +const ILIXIUM_MESSAGE_VERSION: u8 = 2; + +/// `transaction.merchantRef` is constrained to `^(?!.*£)[\w]{4,20}$`. +const MERCHANT_REF_MIN_LEN: usize = 4; +const MERCHANT_REF_MAX_LEN: usize = 20; + +const CHALLENGE_WINDOW_SIZE_FULL_SCREEN: &str = "05"; + +/// The colour depths Ilixium accepts (`^1$|^4$|^8$|^15$|^16$|^24$|^32$|^48$`). +const ACCEPTED_COLOR_DEPTHS: [u8; 8] = [1, 4, 8, 15, 16, 24, 32, 48]; + +#[derive(Debug, Clone)] +pub struct IlixiumAuthType { + pub merchant_password: Secret, + pub merchant_id: Secret, + pub account_id: Secret, +} + +impl TryFrom<&ConnectorSpecificConfig> for IlixiumAuthType { + type Error = error_stack::Report; + + fn try_from(auth_type: &ConnectorSpecificConfig) -> Result { + match auth_type { + ConnectorSpecificConfig::Ilixium { + api_key, + key1, + api_secret, + .. + } => Ok(Self { + merchant_password: api_key.to_owned(), + merchant_id: key1.to_owned(), + account_id: api_secret.to_owned(), + }), + _ => Err(error_stack::report!( + errors::IntegrationError::FailedToObtainAuthType { + context: errors::IntegrationErrorContext { + suggested_action: Some( + "Configure this merchant account's Ilixium connector with a \ + SignatureKey auth type: api_key = Digest Calculation Password, \ + key1 = MerchantId, api_secret = AccountId." + .to_string(), + ), + doc_url: Some("https://docs.ilixium.com/docs/direct/digest".to_string()), + additional_context: Some( + "The connector_config passed to IlixiumAuthType::try_from was not \ + the ConnectorSpecificConfig::Ilixium variant — either a different \ + connector's config was routed to Ilixium, or Ilixium's three \ + credentials were never configured for this merchant account." + .to_string(), + ), + } + } + )), + } + } +} + +impl IlixiumAuthType { + pub fn compute_merchant_digest( + &self, + body: &str, + ) -> Result> { + let digest_error = |round: &str| errors::IntegrationError::RequestEncodingFailed { + context: errors::IntegrationErrorContext { + suggested_action: None, + doc_url: Some("https://docs.ilixium.com/docs/direct/digest".to_string()), + additional_context: Some(format!( + "SHA-512 hashing failed while computing {round} of Ilixium's \ + X-MERCHANT-DIGEST (two rounds of Base64(SHA-512(...)) over the request \ + body, with the Digest Calculation Password appended in round two)." + )), + }, + }; + + let round_one = BASE64_ENGINE.encode( + crypto::Sha512 + .generate_digest(body.as_bytes()) + .change_context(digest_error("round one"))?, + ); + let salted = format!("{}{}", round_one, self.merchant_password.peek()); + let round_two = BASE64_ENGINE.encode( + crypto::Sha512 + .generate_digest(salted.as_bytes()) + .change_context(digest_error("round two"))?, + ); + Ok(round_two) + } +} + +/// Whether a value already satisfies Ilixium's `transaction.merchantRef` pattern +/// (`^(?!.*£)[\w]{4,20}$`) and can therefore be sent verbatim. +/// +/// This is what makes [`derive_merchant_ref`] **idempotent**: a value it has already produced — +/// either an untouched caller reference or the 20-character hex digest — passes this test, so +/// re-deriving from it returns it unchanged. The Refund flow relies on that, because the caller +/// may legitimately supply *either* the original UCS payment reference *or* the `merchantRef` +/// Ilixium echoed back for it. +fn is_merchant_ref_verbatim_safe(reference: &str) -> bool { + (MERCHANT_REF_MIN_LEN..=MERCHANT_REF_MAX_LEN).contains(&reference.len()) + && reference + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_') +} + +/// Ilixium's `transaction.merchantRef` is 4–20 characters of `[\w]` (`[A-Za-z0-9_]`) and must +/// be unique per payment request (a repeat yields response code 102, Duplicate Merchant Ref). +/// A UCS `connector_request_reference_id` is routinely longer than 20 characters and usually +/// contains `-`, which is not in `[\w]`. +/// +/// Resolution (tech spec UNDECIDED #3, option (a) made collision-safe): +/// * If the reference already satisfies the pattern verbatim, send it unchanged so the value +/// stays human-traceable in Ilixium's back office. +/// * Otherwise derive `hex(SHA-512(reference))[..20]`. Hex is a subset of `[\w]`, the length +/// is exactly 20, and the derivation is **deterministic**, which is what makes the 3DS +/// second leg work: `/direct/threedcomplete` must quote the *same* `merchantRef` as the +/// original `/direct/auth`, and both legs recompute it from the same reference rather than +/// depending on any persisted mapping. +pub fn derive_merchant_ref( + reference: &str, +) -> Result> { + if is_merchant_ref_verbatim_safe(reference) { + return Ok(reference.to_string()); + } + + if reference.is_empty() { + return Err(error_stack::report!( + errors::IntegrationError::MissingRequiredField { + field_name: "connector_request_reference_id", + context: errors::IntegrationErrorContext { + suggested_action: Some( + "Ilixium requires a non-empty transaction.merchantRef on every \ + /direct/auth request." + .to_string(), + ), + doc_url: None, + additional_context: None, + }, + } + )); + } + + let digest = crypto::Sha512 + .generate_digest(reference.as_bytes()) + .change_context(errors::IntegrationError::RequestEncodingFailed { + context: errors::IntegrationErrorContext { + suggested_action: None, + doc_url: None, + additional_context: Some( + "SHA-512 hashing failed while deriving Ilixium's 20-character \ + transaction.merchantRef from the UCS payment reference." + .to_string(), + ), + }, + })?; + Ok(hex::encode(digest) + .chars() + .take(MERCHANT_REF_MAX_LEN) + .collect()) +} + +#[derive(Debug, Clone, Copy, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum IlixiumPaymentMethodType { + Card, +} + +#[derive(Debug, Clone, Copy, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum IlixiumTransactionType { + Ecommerce, +} + +#[derive(Debug, Clone, Serialize)] +pub struct IlixiumMerchant { + #[serde(rename = "merchantId")] + pub merchant_id: Secret, + #[serde(rename = "accountId")] + pub account_id: Secret, +} + +impl From<&IlixiumAuthType> for IlixiumMerchant { + fn from(auth: &IlixiumAuthType) -> Self { + Self { + merchant_id: auth.merchant_id.clone(), + account_id: auth.account_id.clone(), + } + } +} + +#[derive(Debug, Clone, Serialize)] +pub struct IlixiumTransaction { + #[serde(rename = "transactionType")] + pub transaction_type: IlixiumTransactionType, + #[serde(rename = "merchantRef")] + pub merchant_ref: String, + /// Minor units, digits only, serialised as a JSON **string** to match the schema's + /// `^[\d]{1,12}$` (the published examples emit a bare number, but the schema types it + /// as a string and both are accepted). + pub amount: StringMinorUnit, + /// ISO 4217 three-letter code (`GBP`); the schema also accepts the three-digit form. + pub currency: Currency, +} + +#[derive(Debug, Clone, Serialize)] +pub struct IlixiumCard { + #[serde(rename = "cardNumber")] + pub card_number: RawCardNumber, + /// `MMyyyy`, no separator (e.g. `012030`). + #[serde(rename = "expiryDate")] + pub expiry_date: Secret, + #[serde(rename = "securityCode")] + pub security_code: Secret, +} + +#[derive(Debug, Clone, Serialize)] +pub struct IlixiumAddress { + #[serde(rename = "addressLine1", skip_serializing_if = "Option::is_none")] + pub address_line1: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub city: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub province: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub postcode: Option>, + /// Always mandatory, even when the account is configured for Optional Address. + pub country: common_enums::CountryAlpha3, +} + +#[derive(Debug, Clone, Serialize)] +pub struct IlixiumCustomer { + #[serde(rename = "customerId")] + pub customer_id: String, + pub email: Email, + #[serde(rename = "firstName")] + pub first_name: Secret, + pub surname: Secret, + /// `ddmmyyyy`. Schema-mandatory but absent from the UCS payment model — see + /// [`extract_date_of_birth`] for how it is sourced and why it may be omitted. + #[serde(rename = "dateOfBirth", skip_serializing_if = "Option::is_none")] + pub date_of_birth: Option>, + pub address: IlixiumAddress, + #[serde(rename = "mobileNumber", skip_serializing_if = "Option::is_none")] + pub mobile_number: Option>, +} + +#[derive(Debug, Clone, Serialize)] +pub struct IlixiumPaymentInfo { + /// Cardholder browser IP — recommended by Ilixium whenever 3-D Secure is in play. + #[serde(rename = "ipAddress")] + pub ip_address: Secret, +} + +/// Mandatory when the merchant's account performs 3-D Secure; ignored (but harmless) when the +/// account is not configured for 3DS. `browserDetails` is the platform-performed-3DS branch of +/// the schema's `oneOf`; the `externalReferences` branch (merchant/third-party +/// pre-authentication) is out of scope here. +#[derive(Debug, Clone, Serialize)] +pub struct IlixiumEmvco3ds { + #[serde(rename = "browserDetails")] + pub browser_details: IlixiumBrowserDetails, +} + +/// All ten fields are mandatory. The schema types the numeric ones as strings. +#[derive(Debug, Clone, Serialize)] +pub struct IlixiumBrowserDetails { + #[serde(rename = "acceptHeader")] + pub accept_header: String, + #[serde(rename = "javaScriptEnabled")] + pub java_script_enabled: bool, + #[serde(rename = "javaEnabled")] + pub java_enabled: bool, + pub language: String, + #[serde(rename = "screenHeight")] + pub screen_height: String, + #[serde(rename = "screenWidth")] + pub screen_width: String, + /// `getTimezoneOffset()` in minutes. + #[serde(rename = "timeDifference")] + pub time_difference: String, + #[serde(rename = "userAgent")] + pub user_agent: String, + #[serde(rename = "colorDepth")] + pub color_depth: String, + #[serde(rename = "challengeWindowSize")] + pub challenge_window_size: String, +} + +/// Untagged request body for the Authorize flow. +/// +/// Leg 1 (`Auth`) is `POST /direct/auth`; leg 2 (`ThreeDsComplete`) is +/// `POST /direct/threedcomplete`. The leg is selected by [`is_three_ds_completion`], which is +/// also what `get_url` in `ilixium.rs` keys off, so the URL and the body can never disagree. +/// `untagged` so each variant serialises as its own bare body. +#[derive(Debug, Clone, Serialize)] +#[serde(untagged)] +pub enum IlixiumAuthorizeRequest { + Auth(Box>), + ThreeDsComplete(Box), +} + +#[derive(Debug, Clone, Serialize)] +pub struct IlixiumPaymentsRequest { + pub version: u8, + /// `false` (the default) captures the authorisation immediately — the response `type` + /// is then `AUTH_CAP`. `true` authorises only and requires a later Capture or Reversal. + #[serde(rename = "deferredCapture")] + pub deferred_capture: bool, + pub transaction: IlixiumTransaction, + #[serde(rename = "paymentMethodType")] + pub payment_method_type: IlixiumPaymentMethodType, + pub merchant: IlixiumMerchant, + pub card: IlixiumCard, + pub customer: IlixiumCustomer, + #[serde(rename = "paymentInfo", skip_serializing_if = "Option::is_none")] + pub payment_info: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub emvco3ds: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct IlixiumThreeDsTransactionRef { + /// Must be byte-identical to the `merchantRef` sent on the original `/direct/auth`. + #[serde(rename = "merchantRef")] + pub merchant_ref: String, +} + +#[derive(Debug, Clone, Serialize)] +pub struct IlixiumThreeDsData { + pub md: Secret, + #[serde(rename = "paRes")] + pub pa_res: Secret, +} + +#[derive(Debug, Clone, Serialize)] +pub struct IlixiumThreeDsCompleteRequest { + pub version: u8, + pub merchant: IlixiumMerchant, + pub transaction: IlixiumThreeDsTransactionRef, + #[serde(rename = "threeDSecure")] + pub three_d_secure: IlixiumThreeDsData, +} + +/// Whether this Authorize invocation is the 3DS return leg. +/// +/// UCS re-invokes `Authorize` after the ACS posts back to `TermUrl`, and it is the presence of +/// `redirect_response` that distinguishes that call from the initial authorisation. Used by +/// both the request builder below and `get_url` in `ilixium.rs`. +pub fn is_three_ds_completion( + request: &PaymentsAuthorizeData, +) -> bool { + request.redirect_response.is_some() +} + +/// `customer.dateOfBirth` is schema-mandatory (`ddmmyyyy`) but has no home in the UCS payment +/// model (tech spec UNDECIDED #1). Rather than fabricate a placeholder date — which would be +/// sent to the issuer and could distort Ilixium's own fraud checks — it is read from the +/// merchant-supplied `metadata` object under `ilixium_date_of_birth` (or `date_of_birth`) and +/// simply omitted when absent. Accounts that enforce the field will answer `VA8`, which +/// surfaces as a normal `REJECTED` error rather than a silently wrong value. +fn extract_date_of_birth( + metadata: Option<&common_utils::pii::SecretSerdeValue>, +) -> Option> { + let value = metadata?.clone().expose(); + ["ilixium_date_of_birth", "date_of_birth"] + .iter() + .find_map(|key| value.get(key).and_then(|v| v.as_str()).map(String::from)) + .map(Secret::new) +} + +/// Ilixium accepts only the colour depths in [`ACCEPTED_COLOR_DEPTHS`], while browsers report +/// whatever `screen.colorDepth` returns (30 on some X11 configurations, for instance). Snap +/// down to the nearest accepted value so a legitimate browser reading never turns into a +/// `VB88` rejection. +fn normalize_color_depth(reported: u8) -> u8 { + ACCEPTED_COLOR_DEPTHS + .iter() + .rev() + .find(|accepted| **accepted <= reported) + .copied() + .unwrap_or(ACCEPTED_COLOR_DEPTHS[0]) +} + +fn build_browser_details( + browser_info: &BrowserInformation, +) -> Result> { + let missing = |field: &'static str| { + error_stack::report!(errors::IntegrationError::MissingRequiredField { + field_name: field, + context: errors::IntegrationErrorContext { + suggested_action: Some( + "Ilixium requires all ten emvco3ds.browserDetails fields on a 3-D Secure \ + authorisation. Collect the full browser profile client-side before \ + confirming a three_ds card payment." + .to_string(), + ), + doc_url: Some("https://docs.ilixium.com/docs/direct/3dsecure".to_string()), + additional_context: None, + }, + }) + }; + + Ok(IlixiumBrowserDetails { + accept_header: browser_info + .accept_header + .clone() + .ok_or_else(|| missing("browser_info.accept_header"))?, + java_script_enabled: browser_info + .java_script_enabled + .ok_or_else(|| missing("browser_info.java_script_enabled"))?, + java_enabled: browser_info + .java_enabled + .ok_or_else(|| missing("browser_info.java_enabled"))?, + language: browser_info + .language + .clone() + .ok_or_else(|| missing("browser_info.language"))?, + screen_height: browser_info + .screen_height + .ok_or_else(|| missing("browser_info.screen_height"))? + .to_string(), + screen_width: browser_info + .screen_width + .ok_or_else(|| missing("browser_info.screen_width"))? + .to_string(), + time_difference: browser_info + .time_zone + .ok_or_else(|| missing("browser_info.time_zone"))? + .to_string(), + user_agent: browser_info + .user_agent + .clone() + .ok_or_else(|| missing("browser_info.user_agent"))?, + color_depth: normalize_color_depth( + browser_info + .color_depth + .ok_or_else(|| missing("browser_info.color_depth"))?, + ) + .to_string(), + challenge_window_size: CHALLENGE_WINDOW_SIZE_FULL_SCREEN.to_string(), + }) +} + +impl + TryFrom< + IlixiumRouterData< + RouterDataV2< + Authorize, + PaymentFlowData, + PaymentsAuthorizeData, + PaymentsResponseData, + >, + T, + >, + > for IlixiumAuthorizeRequest +{ + type Error = error_stack::Report; + + fn try_from( + item: IlixiumRouterData< + RouterDataV2< + Authorize, + PaymentFlowData, + PaymentsAuthorizeData, + PaymentsResponseData, + >, + T, + >, + ) -> Result { + if is_three_ds_completion(&item.router_data.request) { + Ok(Self::ThreeDsComplete(Box::new( + IlixiumThreeDsCompleteRequest::try_from(&item.router_data)?, + ))) + } else { + Ok(Self::Auth(Box::new(IlixiumPaymentsRequest::try_from( + &item, + )?))) + } + } +} + +/// Everything the `/direct/auth` body needs, normalised out of whichever flow request is driving +/// it. +/// +/// `/direct/auth` is reachable from two flows: `Authorize` (non-3DS payments, and the 3DS first +/// leg when the connector is not routed through PreAuthenticate) and `PreAuthenticate` (the 3DS +/// first leg). The two carry the same information under different types — and +/// [`PaymentsPreAuthenticateData`] carries strictly less of it — so both funnel through here +/// rather than duplicating a 130-line body builder. +/// +/// Fields the PreAuthenticate request cannot supply are `Option` and documented at the call site. +pub(super) struct IlixiumAuthBodyInputs<'a, T: PaymentMethodDataTypes> { + pub payment_method_data: &'a PaymentMethodData, + /// Already converted to Ilixium's minor-unit digit string by the caller, which owns the + /// flow-specific amount field (`minor_amount` vs `amount`). + pub amount: StringMinorUnit, + pub currency: Currency, + pub email: Option, + /// `None` on the PreAuthenticate leg — `PaymentsPreAuthenticateData` has no `customer_name`, + /// so the billing address is the only name source there. + pub customer_name: Option<&'a str>, + /// `None` on the PreAuthenticate leg — see [`extract_date_of_birth`]. + pub metadata: Option<&'a common_utils::pii::SecretSerdeValue>, + pub browser_info: Option<&'a BrowserInformation>, + pub is_auto_capture: bool, + pub is_three_ds: bool, +} + +/// Builds the `POST /direct/auth` body shared by the Authorize and PreAuthenticate flows. +pub(super) fn build_ilixium_payments_request( + inputs: IlixiumAuthBodyInputs<'_, T>, + common: &PaymentFlowData, + connector_config: &ConnectorSpecificConfig, +) -> Result, error_stack::Report> { + let card = match inputs.payment_method_data { + PaymentMethodData::Card(card_data) => IlixiumCard { + card_number: card_data.card_number.clone(), + expiry_date: Secret::new(format!( + "{}{}", + card_data.get_card_expiry_month_2_digit()?.peek(), + card_data.get_expiry_year_4_digit().peek() + )), + security_code: card_data.card_cvc.clone(), + }, + other => { + return Err(error_stack::report!( + errors::IntegrationError::NotImplemented( + "Ilixium supports only raw card payments on POST /direct/auth; wallets, \ + APMs, bank transfers and stored-card tokens are out of scope for this \ + connector implementation" + .to_string(), + errors::IntegrationErrorContext { + suggested_action: Some( + "Route this payment with PaymentMethodData::Card.".to_string(), + ), + doc_url: Some( + "https://docs.ilixium.com/docs/api/authorisation".to_string() + ), + additional_context: Some(format!( + "Unsupported payment_method_data variant for Ilixium \ + /direct/auth: {other:?}" + )), + }, + ) + )); + } + }; + + let auth = IlixiumAuthType::try_from(connector_config)?; + + let amount = inputs.amount; + + let country = common.get_optional_billing_country().ok_or_else(|| { + error_stack::report!(errors::IntegrationError::MissingRequiredField { + field_name: "payment_method_data.billing.address.country", + context: errors::IntegrationErrorContext { + suggested_action: Some( + "Ilixium always requires customer.address.country, even on accounts \ + configured for Optional Address." + .to_string(), + ), + doc_url: Some("https://docs.ilixium.com/docs/direct/optional-address".to_string()), + additional_context: None, + }, + }) + })?; + let country = common_enums::CountryAlpha2::from_alpha2_to_alpha3(country); + + let email = inputs + .email + .clone() + .or_else(|| common.get_optional_billing_email()) + .ok_or_else(|| { + error_stack::report!(errors::IntegrationError::MissingRequiredField { + field_name: "email", + context: errors::IntegrationErrorContext { + suggested_action: Some( + "Ilixium requires customer.email on every authorisation.".to_string(), + ), + doc_url: None, + additional_context: None, + }, + }) + })?; + + let (first_name, surname) = split_customer_name(common, inputs.customer_name)?; + + let customer_id = common + .customer_id + .as_ref() + .map(|id| id.get_string_repr().to_string()) + .unwrap_or_else(|| common.connector_request_reference_id.clone()); + + let ip_address = inputs + .browser_info + .and_then(|info| info.ip_address) + .map(|ip| Secret::new(ip.to_string())); + + // Ilixium keys emvco3ds off the *account*, not the transaction: an account configured for 3-D + // Secure rejects every auth that omits a full browserDetails block (VA80-VA89, one code per + // missing field), while an account not so configured ignores the element when it is present. + // So send it whenever the browser profile is complete; only a 3DS authorisation treats an + // absent or partial profile as an error. + let missing_browser_info = || { + error_stack::report!(errors::IntegrationError::MissingRequiredField { + field_name: "browser_info", + context: errors::IntegrationErrorContext { + suggested_action: Some( + "A 3-D Secure card authorisation must carry the browser profile \ + that populates emvco3ds.browserDetails." + .to_string(), + ), + doc_url: Some("https://docs.ilixium.com/docs/direct/3dsecure".to_string()), + additional_context: None, + }, + }) + }; + + let emvco3ds = match inputs.browser_info { + Some(browser_info) if inputs.is_three_ds => Some(IlixiumEmvco3ds { + browser_details: build_browser_details(browser_info)?, + }), + // Best-effort outside 3DS: a partial profile is omitted rather than fatal, so merchants on + // accounts that do not require 3-D Secure behave exactly as they did before. + Some(browser_info) => build_browser_details(browser_info) + .ok() + .map(|browser_details| IlixiumEmvco3ds { browser_details }), + None if inputs.is_three_ds => return Err(missing_browser_info()), + None => None, + }; + + let payment_info = ip_address.map(|ip_address| IlixiumPaymentInfo { ip_address }); + + Ok(IlixiumPaymentsRequest { + version: ILIXIUM_MESSAGE_VERSION, + deferred_capture: !inputs.is_auto_capture, + transaction: IlixiumTransaction { + transaction_type: IlixiumTransactionType::Ecommerce, + merchant_ref: derive_merchant_ref(&common.connector_request_reference_id)?, + amount, + currency: inputs.currency, + }, + payment_method_type: IlixiumPaymentMethodType::Card, + merchant: IlixiumMerchant::from(&auth), + card, + customer: IlixiumCustomer { + customer_id, + email, + first_name, + surname, + date_of_birth: extract_date_of_birth(inputs.metadata), + address: IlixiumAddress { + address_line1: common.get_optional_billing_line1(), + city: common.get_optional_billing_city(), + province: common.get_optional_billing_state(), + postcode: common.get_optional_billing_zip(), + country, + }, + mobile_number: common.get_billing_phone_number().ok(), + }, + payment_info, + emvco3ds, + }) +} + +impl + TryFrom< + &IlixiumRouterData< + RouterDataV2< + Authorize, + PaymentFlowData, + PaymentsAuthorizeData, + PaymentsResponseData, + >, + T, + >, + > for IlixiumPaymentsRequest +{ + type Error = error_stack::Report; + + fn try_from( + item: &IlixiumRouterData< + RouterDataV2< + Authorize, + PaymentFlowData, + PaymentsAuthorizeData, + PaymentsResponseData, + >, + T, + >, + ) -> Result { + let router_data = &item.router_data; + let request = &router_data.request; + let common = &router_data.resource_common_data; + + let amount = item + .connector + .amount_converter + .convert(request.minor_amount, request.currency) + .change_context(errors::IntegrationError::AmountConversionFailed { + context: errors::IntegrationErrorContext { + suggested_action: None, + doc_url: None, + additional_context: Some(format!( + "Failed to convert minor_amount {} {} into Ilixium's \ + transaction.amount (minor units, digits only, sent as a JSON string).", + request.minor_amount.get_amount_as_i64(), + request.currency + )), + }, + })?; + + build_ilixium_payments_request( + IlixiumAuthBodyInputs { + payment_method_data: &request.payment_method_data, + amount, + currency: request.currency, + email: request.email.clone(), + customer_name: request.customer_name.as_deref(), + metadata: request.metadata.as_ref(), + browser_info: request.browser_info.as_ref(), + is_auto_capture: request.is_auto_capture(), + is_three_ds: common.auth_type == AuthenticationType::ThreeDs, + }, + common, + &router_data.connector_config, + ) + } +} + +/// `POST /direct/auth` built from the **PreAuthenticate** leg. +/// +/// Same body as the Authorize path — Ilixium has one authorisation endpoint — but +/// [`PaymentsPreAuthenticateData`] is a narrower struct, so three things differ: +/// +/// * `payment_method_data` and `currency` are `Option` here and must be unwrapped; +/// * `amount` is a single `MinorUnit` (there is no separate `minor_amount`); +/// * `is_auto_capture()` returns a `Result` (it rejects `ManualMultiple`/`Scheduled`) rather than +/// a bare `bool`. +/// +/// One input is unavailable on this leg: `customer_name`, so the billing address is the only +/// name source (which [`split_customer_name`] already handles). `metadata` *is* available — +/// `PaymentsPreAuthenticateData` carries it so `customer.dateOfBirth` resolves here exactly as +/// it does on Authorize. +impl + TryFrom< + IlixiumRouterData< + RouterDataV2< + PreAuthenticate, + PaymentFlowData, + PaymentsPreAuthenticateData, + PaymentsResponseData, + >, + T, + >, + > for IlixiumPreAuthenticateRequest +{ + type Error = error_stack::Report; + + fn try_from( + item: IlixiumRouterData< + RouterDataV2< + PreAuthenticate, + PaymentFlowData, + PaymentsPreAuthenticateData, + PaymentsResponseData, + >, + T, + >, + ) -> Result { + let router_data = &item.router_data; + let request = &router_data.request; + let common = &router_data.resource_common_data; + + let missing = |field: &'static str| { + error_stack::report!(errors::IntegrationError::MissingRequiredField { + field_name: field, + context: errors::IntegrationErrorContext { + suggested_action: Some( + "Ilixium's PreAuthenticate leg sends the full POST /direct/auth \ + authorisation, so it needs the same card, amount and currency an \ + Authorize would carry." + .to_string(), + ), + doc_url: Some("https://docs.ilixium.com/docs/api/authorisation".to_string()), + additional_context: None, + }, + }) + }; + + let payment_method_data = request + .payment_method_data + .as_ref() + .ok_or_else(|| missing("payment_method_data"))?; + let currency = request.currency.ok_or_else(|| missing("currency"))?; + + let amount = item + .connector + .amount_converter + .convert(request.amount, currency) + .change_context(errors::IntegrationError::AmountConversionFailed { + context: errors::IntegrationErrorContext { + suggested_action: None, + doc_url: None, + additional_context: Some(format!( + "Failed to convert amount {} {} into Ilixium's transaction.amount \ + (minor units, digits only, sent as a JSON string).", + request.amount.get_amount_as_i64(), + currency + )), + }, + })?; + + build_ilixium_payments_request( + IlixiumAuthBodyInputs { + payment_method_data, + amount, + currency, + email: request.email.clone(), + customer_name: None, + metadata: request.metadata.as_ref(), + browser_info: request.browser_info.as_ref(), + is_auto_capture: request.is_auto_capture()?, + is_three_ds: true, + }, + common, + &router_data.connector_config, + ) + } +} + +/// Ilixium requires `firstName` and `surname` separately. Prefer the structured billing name, +/// then fall back to splitting the single `customer_name` on the first space. +fn split_customer_name( + common: &PaymentFlowData, + customer_name: Option<&str>, +) -> Result<(Secret, Secret), error_stack::Report> { + let missing = |field: &'static str| { + error_stack::report!(errors::IntegrationError::MissingRequiredField { + field_name: field, + context: errors::IntegrationErrorContext { + suggested_action: Some( + "Ilixium requires customer.firstName and customer.surname. Supply a \ + billing address with first_name/last_name, or a customer name containing \ + both parts." + .to_string(), + ), + doc_url: None, + additional_context: None, + }, + }) + }; + + let billing_first = common.get_optional_billing_first_name(); + let billing_last = common.get_optional_billing_last_name(); + if let (Some(first), Some(last)) = (billing_first.clone(), billing_last.clone()) { + return Ok((first, last)); + } + + let (split_first, split_last) = customer_name + .and_then(|name| { + name.trim() + .split_once(char::is_whitespace) + .map(|(first, last)| (first.to_string(), last.trim().to_string())) + }) + .map_or((None, None), |(first, last)| { + (Some(Secret::new(first)), Some(Secret::new(last))) + }); + + let first_name = billing_first + .or(split_first) + .ok_or_else(|| missing("payment_method_data.billing.address.first_name"))?; + let surname = billing_last + .or(split_last) + .ok_or_else(|| missing("payment_method_data.billing.address.last_name"))?; + Ok((first_name, surname)) +} + +impl + TryFrom< + &RouterDataV2, PaymentsResponseData>, + > for IlixiumThreeDsCompleteRequest +{ + type Error = error_stack::Report; + + fn try_from( + router_data: &RouterDataV2< + Authorize, + PaymentFlowData, + PaymentsAuthorizeData, + PaymentsResponseData, + >, + ) -> Result { + let auth = IlixiumAuthType::try_from(&router_data.connector_config)?; + + let payload = router_data + .request + .redirect_response + .as_ref() + .and_then(|response| response.payload.clone()) + .ok_or_else(|| { + error_stack::report!(errors::IntegrationError::MissingRequiredField { + field_name: "request.redirect_response.payload", + context: errors::IntegrationErrorContext { + suggested_action: Some( + "The ACS posts `md` and `paRes` back to TermUrl; both must be \ + forwarded into the Authorize completion call." + .to_string(), + ), + doc_url: Some( + "https://docs.ilixium.com/docs/api/threedcomplete".to_string() + ), + additional_context: None, + }, + }) + })? + .expose(); + + let pick = |candidates: &[&str], field_name: &'static str| { + candidates + .iter() + .find_map(|key| payload.get(*key).and_then(|value| value.as_str())) + .map(|value| Secret::new(value.to_string())) + .ok_or_else(|| { + error_stack::report!(errors::IntegrationError::MissingRequiredField { + field_name, + context: errors::IntegrationErrorContext { + suggested_action: Some( + "POST /direct/threedcomplete requires both threeDSecure.md and \ + threeDSecure.paRes exactly as returned by the ACS." + .to_string(), + ), + doc_url: Some( + "https://docs.ilixium.com/docs/api/threedcomplete".to_string() + ), + additional_context: None, + }, + }) + }) + }; + + Ok(Self { + version: ILIXIUM_MESSAGE_VERSION, + merchant: IlixiumMerchant::from(&auth), + transaction: IlixiumThreeDsTransactionRef { + merchant_ref: derive_merchant_ref( + &router_data + .resource_common_data + .connector_request_reference_id, + )?, + }, + three_d_secure: IlixiumThreeDsData { + md: pick(&["md", "MD", "Md"], "request.redirect_response.payload.md")?, + pa_res: pick( + &["paRes", "PaRes", "PARes", "pares"], + "request.redirect_response.payload.paRes", + )?, + }, + }) + } +} + +/// `captureRequest.transaction`. +/// +/// A deliberate second transaction struct rather than a reuse of [`IlixiumTransaction`]: +/// `captureRequest` requires exactly `amount`, `currency` and `merchantRef`, and Ilixium +/// ignores `transactionType` here (it is inherited from `transactionDetails` but is not part +/// of a capture), so sending it would be noise on a message that identifies the payment by +/// nothing but its reference. +#[derive(Debug, Clone, Serialize)] +pub struct IlixiumCaptureTransaction { + /// **The original authorisation's** `merchantRef` — this is the only thing binding the + /// capture to a payment; `captureRequest` carries no gateway id. A reference that matches + /// nothing yields response code 104. + #[serde(rename = "merchantRef")] + pub merchant_ref: String, + /// Minor units, digits only, serialised as a JSON string (`^[\d]{1,12}$`). + pub amount: StringMinorUnit, + /// Must equal the original transaction's currency; a mismatch yields response code 103. + pub currency: Currency, +} + +/// `POST /direct/capture` body. The schema's top-level properties are exactly `version`, +/// `transaction` and `merchant` — there is no card, token, customer or paymentMethodType on a +/// capture. +#[derive(Debug, Clone, Serialize)] +pub struct IlixiumCaptureRequest { + pub version: u8, + pub transaction: IlixiumCaptureTransaction, + pub merchant: IlixiumMerchant, +} + +impl + TryFrom< + IlixiumRouterData< + RouterDataV2, + T, + >, + > for IlixiumCaptureRequest +{ + type Error = error_stack::Report; + + fn try_from( + item: IlixiumRouterData< + RouterDataV2, + T, + >, + ) -> Result { + let router_data = &item.router_data; + let request = &router_data.request; + let common = &router_data.resource_common_data; + + let auth = IlixiumAuthType::try_from(&router_data.connector_config)?; + + let amount = item + .connector + .amount_converter + .convert(request.minor_amount_to_capture, request.currency) + .change_context(errors::IntegrationError::AmountConversionFailed { + context: errors::IntegrationErrorContext { + suggested_action: None, + doc_url: None, + additional_context: Some(format!( + "Failed to convert minor_amount_to_capture {} {} into Ilixium's \ + transaction.amount (minor units, digits only, sent as a JSON string).", + request.minor_amount_to_capture.get_amount_as_i64(), + request.currency + )), + }, + })?; + + Ok(Self { + version: ILIXIUM_MESSAGE_VERSION, + transaction: IlixiumCaptureTransaction { + merchant_ref: derive_merchant_ref(&common.connector_request_reference_id)?, + amount, + currency: request.currency, + }, + merchant: IlixiumMerchant::from(&auth), + }) + } +} + +/// `reversalRequest.transaction`. +/// +/// `reversalRequest` is field-for-field identical to `captureRequest` — same +/// `required: [merchant, transaction, version]`, same regexes, same length limits; only the +/// schema `title`/`description` differ. It is kept as a sibling of +/// [`IlixiumCaptureTransaction`] rather than reusing it because `amount` means something +/// materially different here: on a capture it may be *less* than the original (partial capture +/// is allowed), on a reversal it **must equal it exactly**. +#[derive(Debug, Clone, Serialize)] +pub struct IlixiumReversalTransaction { + /// **The original authorisation's** `merchantRef` — the only thing binding the reversal to + /// a payment; `reversalRequest` carries no gateway id. A reference that matches nothing + /// yields response code 104. + #[serde(rename = "merchantRef")] + pub merchant_ref: String, + /// Minor units, digits only, serialised as a JSON string (`^[\d]{1,12}$`). + /// + /// **Partial reversals are not supported.** This must equal the original transaction's + /// amount exactly; anything else — including `0` or a partial value — is rejected with + /// response code 142, "Reversal amount does not match original transaction". Nothing in + /// this connector ever synthesises the value: it is whatever the caller supplied, and the + /// request is refused locally when the caller supplied nothing. + pub amount: StringMinorUnit, + /// Must equal the original transaction's currency; a mismatch yields response code 103. + pub currency: Currency, +} + +/// `POST /direct/reversal` body. Top-level properties are exactly `version`, `transaction` and +/// `merchant` — the same three the capture body carries. +#[derive(Debug, Clone, Serialize)] +pub struct IlixiumVoidRequest { + pub version: u8, + pub transaction: IlixiumReversalTransaction, + pub merchant: IlixiumMerchant, +} + +/// Builds the reversal body from a UCS Void request. +/// +/// **The missing-amount problem.** `PaymentVoidData::amount` and `::currency` are both +/// `Option`, mirroring `PaymentServiceVoidRequest.amount`, which is an `optional Money` on the +/// wire — a caller may legitimately omit it. Ilixium, however, makes `transaction.amount` +/// mandatory *and* requires it to equal the original transaction's amount exactly. There is no +/// second source to fall back on: `PaymentFlowData` is built for the Void flow with `amount`, +/// `minor_amount_captured`, `minor_amount_capturable` and `minor_amount_authorized` all set to +/// `None`, so `resource_common_data` knows nothing about the payment's value either. +/// +/// Every way of papering over that would be worse than failing: +/// * sending `0` or a partial amount earns response code 142 — a remote rejection dressed up as +/// a local success path, and one that consumes an operation slot on the transaction (code +/// 117 serialises operations per transaction); +/// * fabricating an amount risks reversing the wrong figure if Ilixium ever relaxes the +/// equality check. +/// +/// So a Void with no amount is refused here with a `MissingRequiredField` naming `amount` (or +/// `currency`) and spelling out that the *original* transaction's values are required. +impl + TryFrom< + IlixiumRouterData< + RouterDataV2, + T, + >, + > for IlixiumVoidRequest +{ + type Error = error_stack::Report; + + fn try_from( + item: IlixiumRouterData< + RouterDataV2, + T, + >, + ) -> Result { + let router_data = &item.router_data; + let request = &router_data.request; + let common = &router_data.resource_common_data; + + let auth = IlixiumAuthType::try_from(&router_data.connector_config)?; + + let missing = |field: &'static str, what: &str| { + error_stack::report!(errors::IntegrationError::MissingRequiredField { + field_name: field, + context: errors::IntegrationErrorContext { + suggested_action: Some(format!( + "Ilixium does not support partial reversals: POST /direct/reversal \ + requires transaction.{what}, and it must match the original \ + transaction exactly. Send the void request with the original \ + authorisation's amount and currency (PaymentServiceVoidRequest.amount \ + carries both)." + )), + doc_url: Some("https://docs.ilixium.com/docs/api/reversal".to_string()), + additional_context: Some( + "PaymentVoidData carries amount/currency as Option because the gRPC \ + field is optional, and the Void flow's PaymentFlowData holds no \ + amount to fall back on. Rather than send 0 or a partial value — both \ + of which Ilixium rejects with response code 142, 'Reversal amount \ + does not match original transaction' — the request is refused here." + .to_string(), + ), + }, + }) + }; + + let minor_amount = request.amount.ok_or_else(|| missing("amount", "amount"))?; + let currency = request + .currency + .ok_or_else(|| missing("currency", "currency"))?; + + let amount = item + .connector + .amount_converter + .convert(minor_amount, currency) + .change_context(errors::IntegrationError::AmountConversionFailed { + context: errors::IntegrationErrorContext { + suggested_action: None, + doc_url: None, + additional_context: Some(format!( + "Failed to convert void amount {} {} into Ilixium's \ + transaction.amount (minor units, digits only, sent as a JSON string).", + minor_amount.get_amount_as_i64(), + currency + )), + }, + })?; + + Ok(Self { + version: ILIXIUM_MESSAGE_VERSION, + transaction: IlixiumReversalTransaction { + merchant_ref: derive_merchant_ref(&common.connector_request_reference_id)?, + amount, + currency, + }, + merchant: IlixiumMerchant::from(&auth), + }) + } +} + +/// `refundRequest.transaction`, **variant A** — a refund that references a previous +/// transaction. +/// +/// `refundRequest` is a single schema covering two variants. Variant A carries exactly the same +/// six fields as `captureRequest`/`reversalRequest`; variant B is a *standalone* refund that +/// additionally needs `paymentMethodType`, `token`-or-`card`, `customer` and `paymentInfo`. +/// Only variant A is implemented: a connector Refund flow always has a previous payment, and +/// the vendor states standalone refunds are account-gated ("during testing both types will be +/// accepted, although this may not be the case within the production environment"). +/// +/// A sibling of [`IlixiumCaptureTransaction`] / [`IlixiumReversalTransaction`] rather than a +/// reuse of either, because `amount` means a third thing again: on a capture it may be less than +/// the original (partial capture), on a reversal it **must** equal it exactly, and on a refund it +/// may be less than the original *and* accumulates across successive refunds — Ilixium rejects +/// the request with code 125 only once the **total** would exceed the original authorised amount. +#[derive(Debug, Clone, Serialize)] +pub struct IlixiumRefundTransaction { + /// **The original payment's** `merchantRef` — the only thing binding the refund to a + /// transaction; `refundRequest` carries no gateway id and mints no refund id. A reference + /// that matches nothing yields response code 104. + #[serde(rename = "merchantRef")] + pub merchant_ref: String, + /// Minor units, digits only, serialised as a JSON string (`^[\d]{1,12}$`). + /// + /// **Partial refunds are supported and cumulative.** This is + /// `RefundsData::minor_refund_amount` (the amount to refund now), never + /// `minor_payment_amount` (the original payment's total). + pub amount: StringMinorUnit, + /// Must equal the original transaction's currency; a mismatch yields response code 103. + pub currency: Currency, +} + +/// `POST /direct/refund` body, variant A. The three top-level properties are the same ones the +/// capture and reversal bodies carry — no card, token, customer, paymentInfo or +/// paymentMethodType, all of which belong to variant B only. +#[derive(Debug, Clone, Serialize)] +pub struct IlixiumRefundRequest { + pub version: u8, + pub transaction: IlixiumRefundTransaction, + pub merchant: IlixiumMerchant, +} + +/// Resolves the **original payment's** `merchantRef` for a refund-side flow. +/// +/// This is the one place where Refund and RSync cannot copy Capture and Void. Those flows read +/// `PaymentFlowData::connector_request_reference_id`, which UCS populates from +/// `merchant_capture_id` / `merchant_void_id` — i.e. from a caller-supplied reference that +/// *identifies the original payment*. On the refund-side flows the analogous field, +/// `RefundFlowData::connector_request_reference_id`, is populated from **`merchant_refund_id`**: +/// it identifies the *refund*, not the payment. Deriving a `merchantRef` from it would address a +/// transaction that does not exist — response code 104 on `/direct/refund`, and on RSync a +/// client-side filter over `/history/operations` that can never match anything. +/// +/// So the payment reference is taken from the flow's *request* data, in this order: +/// +/// 1. **`connector_order_id`** (`PaymentServiceRefundRequest.connector_order_id`, and its +/// `RefundSyncData` counterpart) — the field whose documented purpose is exactly this: +/// "connector-side identifier for the original payment that this refund targets". It is passed +/// through [`derive_merchant_ref`], which is idempotent (see [`is_merchant_ref_verbatim_safe`]), +/// so it works whether the caller supplies the original UCS payment reference or the +/// `merchantRef` Ilixium echoed back in `connector_reference_id`. +/// 2. **`connector_transaction_id`**, but only when it is already `merchantRef`-shaped. For +/// Ilixium this field normally holds `transaction.gatewayRef`, a 36-character dashed UUID that +/// is *not* a `merchantRef` and cannot be converted into one — hashing it would produce a +/// well-formed reference pointing at nothing. The shape test is what separates the two: a +/// caller that put the reference itself here is honoured, a gatewayRef is not. +/// 3. Otherwise the request is **refused locally**. Sending a derived-from-the-wrong-input +/// reference would be a guaranteed 104 dressed up as a local success path, and it consumes an +/// operation slot on the transaction (Ilixium serialises operations per transaction, code +/// 117). +/// +/// Taken as two plain values rather than as a `&RefundsData` so that RSync — which is handed a +/// [`RefundSyncData`] carrying the very same two fields — resolves the reference through exactly +/// this ladder rather than through a copy of it. +fn resolve_original_merchant_ref( + connector_order_id: Option<&str>, + connector_transaction_id: &str, +) -> Result> { + if let Some(order_id) = connector_order_id.filter(|value| !value.is_empty()) { + return derive_merchant_ref(order_id); + } + + if is_merchant_ref_verbatim_safe(connector_transaction_id) { + return Ok(connector_transaction_id.to_owned()); + } + + Err(error_stack::report!( + errors::IntegrationError::MissingRequiredField { + field_name: "connector_order_id", + context: errors::IntegrationErrorContext { + suggested_action: Some( + "Set connector_order_id on the refund (or refund-sync) request to the \ + original payment's reference — either the merchant_transaction_id the \ + payment was authorised with, or the connector_reference_id Ilixium returned \ + for it (both resolve to the same transaction.merchantRef). POST \ + /direct/refund binds a refund to a payment by merchantRef alone, and POST \ + /history/operations can only be filtered on it client-side." + .to_string(), + ), + doc_url: Some("https://docs.ilixium.com/docs/api/refund".to_string()), + additional_context: Some( + "RefundFlowData::connector_request_reference_id is derived from \ + merchant_refund_id, so it identifies the refund rather than the payment, and \ + connector_transaction_id holds Ilixium's gatewayRef (a dashed UUID) which is \ + not a merchantRef and cannot be converted into one. Rather than send a \ + reference that matches no transaction — response code 104, 'No Matching \ + Transaction', or a history filter that selects nothing — the request is \ + refused here." + .to_string(), + ), + }, + } + )) +} + +/// The refund's own reference, as supplied by UCS. +/// +/// Ilixium returns **no refund identifier** (see [`refund_identifier`]), so this value is the +/// connector's only stable handle on an individual refund. It is validated when the request is +/// built — before any money moves — rather than at response time, so that a refund Ilixium has +/// already accepted can never fail for want of an id. +/// +/// `RefundsData::refund_id` and `RefundFlowData::connector_request_reference_id` are both +/// `merchant_refund_id`; the second is checked only so that a caller who populated one and not +/// the other still works. +fn refund_reference(request: &RefundsData, common: &RefundFlowData) -> Option { + [ + request.refund_id.as_str(), + common.connector_request_reference_id.as_str(), + ] + .into_iter() + .find(|value| !value.is_empty()) + .map(str::to_string) +} + +/// [`refund_reference`] as a hard requirement, used when the request body is built. +fn resolve_refund_reference( + request: &RefundsData, + common: &RefundFlowData, +) -> Result> { + refund_reference(request, common).ok_or_else(|| { + error_stack::report!(errors::IntegrationError::MissingRequiredField { + field_name: "merchant_refund_id", + context: errors::IntegrationErrorContext { + suggested_action: Some( + "Send merchant_refund_id on every Ilixium refund request.".to_string(), + ), + doc_url: Some("https://docs.ilixium.com/docs/direct/reconciliation".to_string()), + additional_context: Some( + "POST /direct/refund mints no refund identifier: its response echoes the \ + original payment's merchantRef and gatewayRef, and status.operationRef — the \ + only refund-unique value — is documented as 'available soon'. When Ilixium \ + omits operationRef, merchant_refund_id is the only value that can identify \ + this refund, so the request is refused rather than performed with no way to \ + report a connector_refund_id." + .to_string(), + ), + }, + }) + }) +} + +impl + TryFrom< + IlixiumRouterData< + RouterDataV2, + T, + >, + > for IlixiumRefundRequest +{ + type Error = error_stack::Report; + + fn try_from( + item: IlixiumRouterData< + RouterDataV2, + T, + >, + ) -> Result { + let router_data = &item.router_data; + let request = &router_data.request; + let common = &router_data.resource_common_data; + + let auth = IlixiumAuthType::try_from(&router_data.connector_config)?; + + resolve_refund_reference(request, common)?; + + let amount = item + .connector + .amount_converter + .convert(request.minor_refund_amount, request.currency) + .change_context(errors::IntegrationError::AmountConversionFailed { + context: errors::IntegrationErrorContext { + suggested_action: None, + doc_url: None, + additional_context: Some(format!( + "Failed to convert minor_refund_amount {} {} into Ilixium's \ + transaction.amount (minor units, digits only, sent as a JSON string).", + request.minor_refund_amount.get_amount_as_i64(), + request.currency + )), + }, + })?; + + Ok(Self { + version: ILIXIUM_MESSAGE_VERSION, + transaction: IlixiumRefundTransaction { + merchant_ref: resolve_original_merchant_ref( + request.connector_order_id.as_deref(), + &request.connector_transaction_id, + )?, + amount, + currency: request.currency, + }, + merchant: IlixiumMerchant::from(&auth), + }) + } +} + +/// Request-level outcome. **Every** business failure is returned as HTTP 200 with one of these +/// codes, so this — never the HTTP status — is what the connector branches on. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum IlixiumStatusCode { + Success, + Declined, + Rejected, + Error, + Cancelled, + Pending, + Resubmission, + /// Any code Ilixium adds that this integration does not yet know about. + #[serde(other)] + Unknown, +} + +/// Operation type echoed in the response. `/direct/auth` answers `AUTH_CAP` when +/// `deferredCapture` was false and `AUTH` when it was true; `/direct/capture` answers +/// `CAPTURE` and `/direct/reversal` answers `REVERSAL`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum IlixiumOperationType { + Auth, + AuthCap, + Capture, + Reversal, + Refund, + Credit, + /// The wire value is pinned explicitly rather than derived from the identifier: the + /// enum's `rename_all = "SCREAMING_SNAKE_CASE"` would turn any respelling of "Threed" + /// into a *different* string (`THREE_D_SECURE_COMPLETE`), which Ilixium never sends, and + /// the variant would silently fall through to `Unknown` instead of failing loudly. + #[serde(rename = "THREED_SECURE_COMPLETE")] + ThreeDsSecureComplete, + Payment, + #[serde(other)] + Unknown, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum IlixiumAttemptCode { + Success, + Pending, + Rejected, + Error, + Cancelled, + Declined, + Cvv2CheckFailed, + AvsAddressCheckFailed, + AvsPostcodeCheckFailed, + #[serde(other)] + Unknown, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum IlixiumThreeDsStatus { + Pending, + AuthenticationSuccessful, + AuthenticationFailed, + NotEnrolled, + AuthenticationUnavailable, + AuthenticationAttempted, + AcsError, + InternalError, + #[serde(other)] + Unknown, +} + +/// A single entry of `status.reasons.reason`. The OpenAPI schema types these as strings +/// (`VA22`, `121`), but the legacy 3DS example emits a bare number (`502`), so accept both. +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum IlixiumReasonCode { + Text(String), + Numeric(i64), +} + +impl std::fmt::Display for IlixiumReasonCode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Text(value) => write!(f, "{value}"), + Self::Numeric(value) => write!(f, "{value}"), + } + } +} + +/// `status.reasons` shape. Tech spec CONFLICT: the OpenAPI schema and the `/docs/api/*` +/// examples wrap the codes in an object (`{"reason": ["VA22"]}`), while `/docs/response-codes/` +/// shows a bare array and the legacy 3DS page shows a scalar. The schema is authoritative, but +/// all three shapes are accepted here so a stale wire format can never turn a decline into an +/// unparsable response. +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum IlixiumReasons { + Wrapped { reason: IlixiumReasonList }, + Bare(IlixiumReasonList), +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum IlixiumReasonList { + Many(Vec), + One(IlixiumReasonCode), +} + +impl IlixiumReasonList { + fn codes(&self) -> Vec { + match self { + Self::Many(codes) => codes.iter().map(ToString::to_string).collect(), + Self::One(code) => vec![code.to_string()], + } + } +} + +impl IlixiumReasons { + pub fn codes(&self) -> Vec { + match self { + Self::Wrapped { reason } => reason.codes(), + Self::Bare(list) => list.codes(), + } + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct IlixiumStatus { + pub code: IlixiumStatusCode, + pub message: Option, + pub reasons: Option, + pub timestamp: Option, + #[serde(rename = "operationRef")] + pub operation_ref: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct IlixiumTransactionResponse { + #[serde(rename = "merchantRef")] + pub merchant_ref: Option, + /// Gateway-assigned unique reference — this is the connector transaction id. + #[serde(rename = "gatewayRef")] + pub gateway_ref: Option, + pub currency: Option, + #[serde(rename = "transactionType")] + pub transaction_type: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct IlixiumCardResponse { + #[serde(rename = "acquirerRef")] + pub acquirer_ref: Option, + #[serde(rename = "cardBin")] + pub card_bin: Option, + #[serde(rename = "cardLastFour")] + pub card_last_four: Option, + #[serde(rename = "cardType")] + pub card_type: Option, + /// Present only when the transaction was successfully authorised. + #[serde(rename = "authCode")] + pub auth_code: Option, + pub cvv: Option, + #[serde(rename = "avsAddress")] + pub avs_address: Option, + #[serde(rename = "avsPostcode")] + pub avs_postcode: Option, + #[serde(rename = "threeDSecureStatus")] + pub three_d_secure_status: Option, + /// The ACS form `action` for the browser redirect. + #[serde(rename = "threeDSecureAcsUrl")] + pub three_d_secure_acs_url: Option, + /// POSTed to the ACS as `MD`. + #[serde(rename = "threeDSecureMd")] + pub three_d_secure_md: Option>, + /// POSTed to the ACS as `PaReq`. + #[serde(rename = "threeDSecurePaReq")] + pub three_d_secure_pa_req: Option>, + #[serde(rename = "threeDSecureVersion")] + pub three_d_secure_version: Option, + pub iso8583code: Option, +} + +/// Deliberately omits `amount`/`currency`: they are unused by the Authorize mapping and the +/// vendor's examples type them inconsistently (quoted string in `transaction`, bare number +/// here), so parsing them would add a failure mode for no benefit. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct IlixiumPaymentAttempt { + pub order: Option, + pub timestamp: Option, + pub code: Option, + pub message: Option, + #[serde(rename = "operationRef")] + pub operation_ref: Option, + #[serde(rename = "paymentMethodType")] + pub payment_method_type: Option, + #[serde(rename = "cardResponse")] + pub card_response: Option, +} + +/// Tech spec CONFLICT: the OpenAPI schema types `paymentAttempt` as an array, but the legacy +/// 3DS documentation page renders it as a single object. Both are accepted. +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum IlixiumPaymentAttempts { + Many(Vec), + One(Box), +} + +impl IlixiumPaymentAttempts { + /// The most recent attempt: highest `order`, falling back to the last element when the + /// platform omits `order`. + pub fn latest(&self) -> Option<&IlixiumPaymentAttempt> { + match self { + Self::One(attempt) => Some(attempt.as_ref()), + Self::Many(attempts) => attempts + .iter() + .max_by_key(|attempt| attempt.order.unwrap_or_default()) + .or_else(|| attempts.last()), + } + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct IlixiumPaymentHistory { + #[serde(rename = "paymentAttempt")] + pub payment_attempt: Option, +} + +/// The shared `paymentResponse` envelope. `/direct/auth`, `/direct/threedcomplete` and +/// `/direct/capture` return byte-for-byte the same schema, so both Authorize legs and Capture +/// deserialise into this one type. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct IlixiumPaymentResponse { + pub version: Option, + #[serde(rename = "type")] + pub operation_type: Option, + pub transaction: Option, + pub status: IlixiumStatus, + #[serde(rename = "paymentHistory")] + pub payment_history: Option, +} + +/// `/direct/capture` returns the same `paymentResponse` envelope as `/direct/auth`. The alias +/// exists because the connector macros mint one `…Templating` marker type per named response +/// body, so two flows cannot both name `IlixiumPaymentResponse` directly. +pub type IlixiumCaptureResponse = IlixiumPaymentResponse; + +/// `/direct/reversal` returns the same `paymentResponse` envelope, with `type` = `REVERSAL`. +/// A distinct alias for the same reason [`IlixiumCaptureResponse`] is one. +pub type IlixiumVoidResponse = IlixiumPaymentResponse; + +/// The **PreAuthenticate** leg posts the very same `/direct/auth` body as Authorize does. +/// +/// An alias rather than a reuse of the ident: `create_all_prerequisites!` mints one +/// `Templating` marker struct per named request ident, so a second flow cannot name +/// `IlixiumPaymentsRequest` directly without defining that marker twice. +pub type IlixiumPreAuthenticateRequest = IlixiumPaymentsRequest; + +/// `/direct/auth` answers the same `paymentResponse` envelope on the PreAuthenticate leg as it +/// does on Authorize. A distinct alias for the same macro reason as +/// [`IlixiumPreAuthenticateRequest`]. +pub type IlixiumPreAuthenticateResponse = IlixiumPaymentResponse; + +/// `/direct/refund` returns the same `paymentResponse` envelope, with `type` = `REFUND`. +/// A distinct alias for the same reason [`IlixiumCaptureResponse`] is one. +/// +/// Note what this envelope does **not** contain: any refund-specific identifier. +/// `transaction.merchantRef` and `transaction.gatewayRef` both echo the *original payment* — +/// the vendor's own published history example shows an `AUTH_CAP` and a `REFUND` sharing both +/// values — and the schema has no `refundId` field. See [`refund_identifier`]. +pub type IlixiumRefundResponse = IlixiumPaymentResponse; + +impl IlixiumPaymentResponse { + pub fn latest_attempt(&self) -> Option<&IlixiumPaymentAttempt> { + self.payment_history + .as_ref() + .and_then(|history| history.payment_attempt.as_ref()) + .and_then(IlixiumPaymentAttempts::latest) + } + + pub fn card_response(&self) -> Option<&IlixiumCardResponse> { + self.latest_attempt() + .and_then(|attempt| attempt.card_response.as_ref()) + } + + pub fn gateway_ref(&self) -> Option { + self.transaction + .as_ref() + .and_then(|transaction| transaction.gateway_ref.clone()) + } + + pub fn merchant_ref(&self) -> Option { + self.transaction + .as_ref() + .and_then(|transaction| transaction.merchant_ref.clone()) + } + + pub fn reason_codes(&self) -> Vec { + self.status + .reasons + .as_ref() + .map(IlixiumReasons::codes) + .unwrap_or_default() + } + + /// The 3DS branch is gated on the *structure* of the response, never on a reason code. + /// + /// Tech spec CONFLICT #7: `/docs/response-codes/` documents `9` as "3D Secure Required" + /// while the 3DS guide's own PENDING example carries `502`. Both are unreliable, so the + /// decision here is `status.code == PENDING` **and** `threeDSecureStatus == PENDING` + /// **and** an ACS URL is actually present — the three things that must all hold for a + /// redirect to be possible at all. + pub fn three_ds_acs_url(&self) -> Option<&str> { + if self.status.code != IlixiumStatusCode::Pending { + return None; + } + let card_response = self.card_response()?; + if card_response.three_d_secure_status != Some(IlixiumThreeDsStatus::Pending) { + return None; + } + card_response + .three_d_secure_acs_url + .as_deref() + .filter(|url| !url.is_empty()) + } +} + +/// Maps the request-level `status.code` onto a UCS attempt status. +/// +/// `SUCCESS` splits on the operation type Ilixium echoes back: `AUTH_CAP` means the funds were +/// captured in the same call (`Charged`), `AUTH` means authorised-only (`Authorized`). When the +/// platform omits `type`, fall back to what the request asked for. +fn map_attempt_status( + response: &IlixiumPaymentResponse, + requested_auto_capture: bool, +) -> AttemptStatus { + match response.status.code { + IlixiumStatusCode::Success => match response.operation_type { + Some(IlixiumOperationType::AuthCap) => AttemptStatus::Charged, + Some(IlixiumOperationType::Auth) => AttemptStatus::Authorized, + _ => { + if requested_auto_capture { + AttemptStatus::Charged + } else { + AttemptStatus::Authorized + } + } + }, + IlixiumStatusCode::Pending => { + if response.three_ds_acs_url().is_some() { + AttemptStatus::AuthenticationPending + } else { + AttemptStatus::Pending + } + } + IlixiumStatusCode::Cancelled => AttemptStatus::Voided, + IlixiumStatusCode::Declined | IlixiumStatusCode::Rejected | IlixiumStatusCode::Error => { + AttemptStatus::Failure + } + IlixiumStatusCode::Resubmission | IlixiumStatusCode::Unknown => AttemptStatus::Pending, + } +} + +/// Maps a `/direct/capture` response onto a UCS attempt status. +/// +/// Kept separate from [`map_attempt_status`] because the same `status.code` means something +/// different here: `SUCCESS` on a capture is unambiguously `Charged` (there is no +/// authorised-only outcome to distinguish), and a failure is a *capture* failure — the +/// authorisation itself is untouched and can still be captured again or reversed, which +/// `CaptureFailed` conveys and the generic `Failure` does not. +fn map_capture_status(response: &IlixiumPaymentResponse) -> AttemptStatus { + match response.status.code { + IlixiumStatusCode::Success => AttemptStatus::Charged, + IlixiumStatusCode::Pending => AttemptStatus::CaptureInitiated, + IlixiumStatusCode::Cancelled => AttemptStatus::Voided, + IlixiumStatusCode::Declined | IlixiumStatusCode::Rejected | IlixiumStatusCode::Error => { + AttemptStatus::CaptureFailed + } + IlixiumStatusCode::Resubmission | IlixiumStatusCode::Unknown => { + AttemptStatus::CaptureInitiated + } + } +} + +/// Maps a `/direct/reversal` response onto a UCS attempt status. +/// +/// A sibling of [`map_capture_status`] rather than a reuse of it: the same `status.code` carries +/// a different meaning on a reversal. `SUCCESS` is `Voided` (the ring-fenced funds are released +/// back to the cardholder), and a failure is a *void* failure — the authorisation survives and +/// can still be captured, or reversed again once whatever blocked it clears — which is what +/// `VoidFailed` conveys and `CaptureFailed`/`Failure` do not. +fn map_void_status(response: &IlixiumPaymentResponse) -> AttemptStatus { + match response.status.code { + IlixiumStatusCode::Success => AttemptStatus::Voided, + IlixiumStatusCode::Cancelled => AttemptStatus::Voided, + IlixiumStatusCode::Pending => AttemptStatus::VoidInitiated, + IlixiumStatusCode::Declined | IlixiumStatusCode::Rejected | IlixiumStatusCode::Error => { + AttemptStatus::VoidFailed + } + IlixiumStatusCode::Resubmission | IlixiumStatusCode::Unknown => { + AttemptStatus::VoidInitiated + } + } +} + +/// Plain-language expansion of the failure codes `/direct/capture` can return. +/// +/// Ilixium's `status.message` is generic ("Validation failed", "Operation rejected"); the +/// actionable detail is the numeric code in `status.reasons.reason`, and these six are the ones +/// the capture endpoint documents. Note that 112 ("already captured") is still reported as a +/// failed capture rather than as `Charged`: it says a capture happened, not that *this* request +/// captured anything, and treating it as success would mask a duplicate-capture bug. +fn capture_failure_hint(code: &str) -> Option<&'static str> { + match code { + "103" => Some("the capture currency does not match the original transaction's currency"), + "104" => Some( + "no transaction matching transaction.merchantRef was found on this merchant account", + ), + "105" => { + Some("the original transaction was not successful, so there is nothing to capture") + } + "108" => Some( + "the original transaction was not of type AUTH — only an authorisation sent with \ + deferredCapture can be captured", + ), + "112" => Some("the transaction has already been captured"), + "117" => Some( + "another operation is already in progress on this transaction — Ilixium permits \ + exactly one at a time", + ), + _ => None, + } +} + +/// [`build_error_response`] with the documented capture codes spelled out in `reason`. Every +/// hint is prefixed with its own code, so nothing the generic builder would have reported is +/// lost. +fn build_capture_error_response( + response: &IlixiumPaymentResponse, + status: AttemptStatus, + http_code: u16, +) -> ErrorResponse { + let mut error = build_error_response(response, status, http_code); + let hints: Vec = response + .reason_codes() + .iter() + .filter_map(|code| capture_failure_hint(code).map(|hint| format!("{code}: {hint}"))) + .collect(); + if !hints.is_empty() { + error.reason = Some(hints.join("; ")); + } + error +} + +/// Plain-language expansion of the failure codes `/direct/reversal` can return. +/// +/// A sibling of [`capture_failure_hint`], not an extension of it: the two endpoints share only +/// 103/104/105/117, and the rest of each set is meaningless on the other flow. In particular +/// **142** — the partial-reversal rejection — has no capture counterpart at all (capture has no +/// "amount does not match original" code, because partial capture is permitted), and 108 +/// ("cannot CAPTURE") is capture-only. +fn reversal_failure_hint(code: &str) -> Option<&'static str> { + match code { + "103" => Some("the reversal currency does not match the original transaction's currency"), + "104" => Some( + "no transaction matching transaction.merchantRef was found on this merchant account", + ), + "105" => { + Some("the original transaction was not authorised, so there is nothing to reverse") + } + "106" => Some("the transaction has already been reversed"), + "109" => Some("the original transaction was not of a reversible type"), + "110" => Some( + "the transaction has already been settled — a refund is required instead of a \ + reversal", + ), + "112" => Some( + "the transaction has already been captured, and the reversal falls outside the \ + reversal-after-capture window configured for this merchant — a refund is required \ + instead", + ), + "117" => Some( + "another operation is already in progress on this transaction — Ilixium permits \ + exactly one at a time", + ), + "142" => Some( + "the reversal amount does not match the original transaction — Ilixium does not \ + support partial voids/reversals, so transaction.amount must equal the original \ + amount exactly", + ), + _ => None, + } +} + +/// [`build_error_response`] with the documented reversal codes spelled out in `reason`. Every +/// hint is prefixed with its own code, so nothing the generic builder would have reported is +/// lost. +fn build_void_error_response( + response: &IlixiumPaymentResponse, + status: AttemptStatus, + http_code: u16, +) -> ErrorResponse { + let mut error = build_error_response(response, status, http_code); + let hints: Vec = response + .reason_codes() + .iter() + .filter_map(|code| reversal_failure_hint(code).map(|hint| format!("{code}: {hint}"))) + .collect(); + if !hints.is_empty() { + error.reason = Some(hints.join("; ")); + } + error +} + +/// Maps a `/direct/refund` response onto a UCS **refund** status. +/// +/// A *fourth sibling* of [`map_attempt_status`] / [`map_capture_status`] / [`map_void_status`], +/// not a reuse of any of them, for two reasons. First, the target enum is different: +/// [`RefundStatus`], not `AttemptStatus` — a refund outcome says nothing about the payment +/// attempt, which stays `Charged` throughout. Second, the same `status.code` means a third thing +/// again: `SUCCESS` is a completed refund, and a failure here leaves the payment intact and +/// still refundable for a *different* amount (a 125 "exceeds original" rejection is retryable +/// with a smaller value), which `RefundStatus::Failure` conveys without implying anything about +/// the payment. +fn map_refund_status(response: &IlixiumPaymentResponse) -> RefundStatus { + match response.status.code { + IlixiumStatusCode::Success => RefundStatus::Success, + IlixiumStatusCode::Pending => RefundStatus::Pending, + IlixiumStatusCode::Cancelled + | IlixiumStatusCode::Declined + | IlixiumStatusCode::Rejected + | IlixiumStatusCode::Error => RefundStatus::Failure, + IlixiumStatusCode::Resubmission | IlixiumStatusCode::Unknown => RefundStatus::Pending, + } +} + +/// Plain-language expansion of the failure codes `/direct/refund` can return. +/// +/// A sibling of [`capture_failure_hint`] / [`reversal_failure_hint`], not an extension of +/// either: the refund set shares only 103/104/105/117 with them and diverges everywhere else. +/// 107/120/124/125 and the 204 account limit have no capture or reversal counterpart, while 108 +/// (capture-only) and 106/109/110/142 (reversal-only) are meaningless here. +/// +/// 111 is the one that most often surprises an integrator: an *uncaptured* authorisation cannot +/// be refunded at all — it must be reversed (Void) instead. +fn refund_failure_hint(code: &str) -> Option<&'static str> { + match code { + "103" => Some("the refund currency does not match the original transaction's currency"), + "104" => Some( + "no transaction matching transaction.merchantRef was found on this merchant account", + ), + "105" => Some("the original transaction was not successful, so there is nothing to refund"), + "107" => Some("the original transaction has already been refunded"), + "111" => Some( + "the original transaction has not been captured — capture it first, or reverse the \ + authorisation (Void) instead of refunding it", + ), + "117" => Some( + "another operation is already in progress on this transaction — Ilixium permits \ + exactly one at a time", + ), + "120" => Some( + "the original payment is not refundable through the Direct API — it used a payment \ + method the Direct API cannot refund", + ), + "121" => Some("the supplied payment method details are invalid"), + "124" => Some( + "the transaction has already been fully refunded — no more refunds can be made \ + against it", + ), + "125" => Some( + "the refund amount would take the total refunded above the original authorised \ + amount — refunds accumulate, so only the unrefunded remainder can still be refunded", + ), + "204" => Some("the account's transaction amount limit has been reached"), + _ => None, + } +} + +/// The refund counterpart of [`build_capture_error_response`] / [`build_void_error_response`]. +/// +/// Written out rather than layered on [`build_error_response`] because that builder stamps +/// `attempt_status: FlowStatus::Payment(..)`, and a refund failure must report +/// `FlowStatus::Refund(..)` — the payment attempt itself is untouched by a failed refund. +fn build_refund_error_response( + response: &IlixiumPaymentResponse, + status: RefundStatus, + http_code: u16, +) -> ErrorResponse { + let reason_codes = response.reason_codes(); + let attempt_message = response + .latest_attempt() + .and_then(|attempt| attempt.message.clone()); + let message = response + .status + .message + .clone() + .or(attempt_message) + .unwrap_or_else(|| common_utils::consts::NO_ERROR_MESSAGE.to_string()); + + let hints: Vec = reason_codes + .iter() + .filter_map(|code| refund_failure_hint(code).map(|hint| format!("{code}: {hint}"))) + .collect(); + + ErrorResponse { + status_code: http_code, + code: reason_codes + .first() + .cloned() + .unwrap_or_else(|| format!("{:?}", response.status.code).to_uppercase()), + message, + reason: if !hints.is_empty() { + Some(hints.join("; ")) + } else if reason_codes.is_empty() { + response.status.message.clone() + } else { + Some(reason_codes.join(", ")) + }, + attempt_status: Some(domain_types::router_data::FlowStatus::Refund(status)), + connector_transaction_id: response.gateway_ref(), + network_decline_code: None, + network_advice_code: None, + network_error_message: None, + typed_connector_response: None, + raw_connector_response: None, + raw_connector_request: None, + typed_connector_request: None, + } +} + +/// Resolves `RefundsResponseData::connector_refund_id`. +/// +/// **Ilixium mints no refund identifier.** `transaction.merchantRef` and +/// `transaction.gatewayRef` in a refund response both echo the *original payment* — the vendor's +/// published `/history/operations` example shows an `AUTH_CAP` and a `REFUND` sharing both values +/// — and `paymentResponse` has no `refundId` field. `/docs/direct/reconciliation` says so +/// outright: "The `merchantRef` field is designed to only identify the transaction, not the +/// operations within it." +/// +/// The resolution order is therefore: +/// +/// 1. **`status.operationRef`** — a ULID that "will uniquely identify each operation" and the +/// only refund-unique value the API defines. The same page warns it "will be available soon", +/// so it may simply be absent. +/// 2. **`paymentHistory.paymentAttempt[latest].operationRef`** — the same value, echoed +/// per attempt; checked in case the platform populates one place before the other. +/// 3. **The refund's own UCS reference** (`merchant_refund_id`, validated in +/// [`resolve_refund_reference`] when the request was built). Deterministic, unique per refund, +/// and honest about its origin. +/// +/// What is deliberately *not* used is the payment's `merchantRef` or `gatewayRef`: reusing either +/// would give every partial refund on one payment the same `connector_refund_id`. +/// +/// **Known limitation:** until `operationRef` goes live, N partial refunds against one payment are +/// distinguished only by the UCS-side reference. Ilixium itself cannot tell them apart — +/// `/history/operations` returns N `type: REFUND` entries differing only by `entryDate`, +/// `processedDate` and `amount` — so an id from step 3 cannot be resolved back through any Ilixium +/// endpoint. +fn refund_identifier( + response: &IlixiumPaymentResponse, + refund_reference: Option, + http_code: u16, +) -> Result> { + let operation_ref = response + .status + .operation_ref + .clone() + .or_else(|| { + response + .latest_attempt() + .and_then(|attempt| attempt.operation_ref.clone()) + }) + .filter(|value| !value.is_empty()); + + if let Some(operation_ref) = operation_ref { + return Ok(operation_ref); + } + + tracing::debug!( + "Ilixium returned no status.operationRef on a refund response; falling back to the UCS \ + refund reference as connector_refund_id. Multiple partial refunds on one payment are \ + not individually distinguishable at Ilixium until operationRef goes live." + ); + + refund_reference.ok_or_else(|| { + error_stack::report!(errors::ConnectorError::ResponseDeserializationFailed { + context: errors::ResponseTransformationErrorContext { + http_status_code: Some(http_code), + additional_context: Some( + "Ilixium's refund response carries no status.operationRef and UCS supplied \ + no merchant_refund_id, so this refund has no identifier: the response's \ + merchantRef and gatewayRef both echo the original payment and would collide \ + across partial refunds." + .to_string(), + ), + }, + }) + }) +} + +/// Builds the ACS auto-POST form. Ilixium never receives the return URL — the merchant owns +/// the `TermUrl` — so the connector assembles the form itself (tech spec UNDECIDED #5, +/// option (a)). The 1.x MPI field names `MD`/`PaReq` are retained by Ilixium even for 3DS2. +/// +/// `term_url` is supplied by the caller rather than read off the request, because the two flows +/// that can receive a 3DS challenge hold it under different names and types: `Authorize` has +/// `complete_authorize_url`/`router_return_url` (`Option`), `PreAuthenticate` has +/// `continue_redirection_url`/`router_return_url` (`Option`). +fn build_three_ds_redirect_form( + response: &IlixiumPaymentResponse, + acs_url: &str, + term_url: Option, + http_code: u16, +) -> Result> { + let deserialization_error = |detail: &str| { + error_stack::report!(errors::ConnectorError::ResponseDeserializationFailed { + context: errors::ResponseTransformationErrorContext { + http_status_code: Some(http_code), + additional_context: Some(detail.to_string()), + }, + }) + }; + + let card_response = response.card_response().ok_or_else(|| { + deserialization_error( + "Ilixium returned PENDING with an ACS URL but no cardResponse to read \ + threeDSecureMd / threeDSecurePaReq from", + ) + })?; + + let md = card_response.three_d_secure_md.as_ref().ok_or_else(|| { + deserialization_error("Ilixium 3DS PENDING response is missing threeDSecureMd") + })?; + let pa_req = card_response + .three_d_secure_pa_req + .as_ref() + .ok_or_else(|| { + deserialization_error("Ilixium 3DS PENDING response is missing threeDSecurePaReq") + })?; + + let term_url = term_url.ok_or_else(|| { + error_stack::report!(errors::ConnectorError::ResponseHandlingFailed { + context: errors::ResponseTransformationErrorContext { + http_status_code: Some(http_code), + additional_context: Some( + "Ilixium 3DS requires a TermUrl for the ACS form, but neither the \ + completion URL nor router_return_url was supplied" + .to_string(), + ), + }, + }) + })?; + + let mut form_fields = std::collections::HashMap::with_capacity(3); + form_fields.insert("MD".to_string(), md.peek().to_owned()); + form_fields.insert("PaReq".to_string(), pa_req.peek().to_owned()); + form_fields.insert("TermUrl".to_string(), term_url); + + Ok(RedirectForm::Form { + endpoint: acs_url.to_string(), + method: common_utils::request::Method::Post, + form_fields, + }) +} + +fn build_error_response( + response: &IlixiumPaymentResponse, + status: AttemptStatus, + http_code: u16, +) -> ErrorResponse { + let reason_codes = response.reason_codes(); + let attempt_message = response + .latest_attempt() + .and_then(|attempt| attempt.message.clone()); + let message = response + .status + .message + .clone() + .or(attempt_message) + .unwrap_or_else(|| common_utils::consts::NO_ERROR_MESSAGE.to_string()); + + ErrorResponse { + status_code: http_code, + code: reason_codes + .first() + .cloned() + .unwrap_or_else(|| format!("{:?}", response.status.code).to_uppercase()), + message, + reason: if reason_codes.is_empty() { + response.status.message.clone() + } else { + Some(reason_codes.join(", ")) + }, + attempt_status: Some(domain_types::router_data::FlowStatus::Payment(status)), + connector_transaction_id: response.gateway_ref(), + network_decline_code: None, + network_advice_code: None, + network_error_message: None, + typed_connector_response: None, + raw_connector_response: None, + raw_connector_request: None, + typed_connector_request: None, + } +} + +impl + TryFrom> + for RouterDataV2, PaymentsResponseData> +{ + type Error = error_stack::Report; + + fn try_from( + item: crate::types::ResponseRouterData, + ) -> Result { + let response = item.response; + let status = map_attempt_status(&response, item.router_data.request.is_auto_capture()); + + let redirection_data = match response.three_ds_acs_url() { + Some(acs_url) => { + let request = &item.router_data.request; + Some(Box::new(build_three_ds_redirect_form( + &response, + acs_url, + request + .complete_authorize_url + .clone() + .or_else(|| request.router_return_url.clone()), + item.http_code, + )?)) + } + None => None, + }; + + let payments_response = if status == AttemptStatus::Failure { + Err(build_error_response(&response, status, item.http_code)) + } else { + Ok(PaymentsResponseData::TransactionResponse { + resource_id: response + .gateway_ref() + .map(ResponseId::ConnectorTransactionId) + .unwrap_or(ResponseId::NoResponseId), + redirection_data, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + network_txn_link_id: None, + connector_response_reference_id: response.merchant_ref(), + incremental_authorization_allowed: None, + splits: None, + status_code: item.http_code, + }) + }; + + Ok(Self { + response: payments_response, + resource_common_data: PaymentFlowData { + status, + ..item.router_data.resource_common_data + }, + ..item.router_data + }) + } +} + +/// `POST /direct/auth` response on the **PreAuthenticate** leg. +/// +/// The same `paymentResponse` envelope and the same status mapping the Authorize leg uses — this +/// *is* the authorisation, so `map_attempt_status` applies unchanged. Only the response variant +/// differs. +/// +/// Note this leg is terminal in two of its three outcomes. Ilixium decides at response time +/// whether to challenge, so a `SUCCESS` here means the payment is already charged and there is no +/// second leg. HS suppresses the follow-up Authorize (its `should_continue` default is `false`) +/// and finalises the attempt from the status set below, so nothing further is required here to +/// avoid a duplicate `/direct/auth` — see response code 102, "Duplicate Merchant Ref". +impl + TryFrom> + for RouterDataV2< + PreAuthenticate, + PaymentFlowData, + PaymentsPreAuthenticateData, + PaymentsResponseData, + > +{ + type Error = error_stack::Report; + + fn try_from( + item: crate::types::ResponseRouterData, + ) -> Result { + let response = item.response; + let requested_auto_capture = item.router_data.request.is_auto_capture().unwrap_or(true); + let status = map_attempt_status(&response, requested_auto_capture); + + let redirection_data = match response.three_ds_acs_url() { + Some(acs_url) => { + let request = &item.router_data.request; + Some(Box::new(build_three_ds_redirect_form( + &response, + acs_url, + request + .continue_redirection_url + .as_ref() + .or(request.router_return_url.as_ref()) + .map(ToString::to_string), + item.http_code, + )?)) + } + None => None, + }; + + let payments_response = if status == AttemptStatus::Failure { + Err(build_error_response(&response, status, item.http_code)) + } else { + Ok(PaymentsResponseData::PreAuthenticateResponse { + resource_id: Some( + response + .gateway_ref() + .map(ResponseId::ConnectorTransactionId) + .unwrap_or(ResponseId::NoResponseId), + ), + authentication_data: None, + redirection_data, + connector_response_reference_id: response.merchant_ref(), + status_code: item.http_code, + }) + }; + + Ok(Self { + response: payments_response, + resource_common_data: PaymentFlowData { + status, + ..item.router_data.resource_common_data + }, + ..item.router_data + }) + } +} + +impl TryFrom> + for RouterDataV2 +{ + type Error = error_stack::Report; + + fn try_from( + item: crate::types::ResponseRouterData, + ) -> Result { + let response = item.response; + let status = map_void_status(&response); + + let payments_response = if status == AttemptStatus::VoidFailed { + Err(build_void_error_response(&response, status, item.http_code)) + } else { + Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId( + response.gateway_ref().unwrap_or_else(|| { + item.router_data.request.connector_transaction_id.clone() + }), + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + network_txn_link_id: None, + connector_response_reference_id: response.merchant_ref(), + incremental_authorization_allowed: None, + splits: None, + status_code: item.http_code, + }) + }; + + Ok(Self { + response: payments_response, + resource_common_data: PaymentFlowData { + status, + ..item.router_data.resource_common_data + }, + ..item.router_data + }) + } +} + +impl TryFrom> + for RouterDataV2 +{ + type Error = error_stack::Report; + + fn try_from( + item: crate::types::ResponseRouterData, + ) -> Result { + let response = item.response; + let status = map_capture_status(&response); + + let payments_response = if status == AttemptStatus::CaptureFailed { + Err(build_capture_error_response( + &response, + status, + item.http_code, + )) + } else { + Ok(PaymentsResponseData::TransactionResponse { + resource_id: response + .gateway_ref() + .map(ResponseId::ConnectorTransactionId) + .unwrap_or_else(|| item.router_data.request.connector_transaction_id.clone()), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + network_txn_link_id: None, + connector_response_reference_id: response.merchant_ref(), + incremental_authorization_allowed: None, + splits: None, + status_code: item.http_code, + }) + }; + + Ok(Self { + response: payments_response, + resource_common_data: PaymentFlowData { + status, + ..item.router_data.resource_common_data + }, + ..item.router_data + }) + } +} + +impl TryFrom> + for RouterDataV2 +{ + type Error = error_stack::Report; + + fn try_from( + item: crate::types::ResponseRouterData, + ) -> Result { + let response = item.response; + let status = map_refund_status(&response); + + let refunds_response = if status == RefundStatus::Failure { + Err(build_refund_error_response( + &response, + status, + item.http_code, + )) + } else { + Ok(RefundsResponseData { + connector_refund_id: refund_identifier( + &response, + refund_reference( + &item.router_data.request, + &item.router_data.resource_common_data, + ), + item.http_code, + )?, + refund_status: status, + status_code: item.http_code, + acquirer_reference_number: None, + }) + }; + + Ok(Self { + response: refunds_response, + resource_common_data: RefundFlowData { + status, + ..item.router_data.resource_common_data + }, + ..item.router_data + }) + } +} + +/// The maximum period `/history/operations` accepts, in hours. Exceeding it is a validation +/// rejection (`VA73` family), not a truncation. +const HISTORY_MAX_WINDOW_HOURS: i64 = 24; + +/// `connector_feature_data` keys through which a caller can anchor the history window on the +/// payment's own creation time instead of on "now". See [`resolve_history_window`]. +const HISTORY_PERIOD_START_KEYS: [&str; 2] = + ["ilixium_history_period_start", "history_period_start"]; + +/// Renders one period bound in the *only* shape `historyOperationsRequest` accepts: +/// `yyyy-MM-ddTHH:mm:ssZ`, with a literal `Z` and **no fractional seconds**. +/// +/// The instant is first shifted to UTC, because the literal `Z` in the pattern is an assertion +/// about the offset, not a formatting directive — rendering a `+05:30` wall clock under a `Z` +/// suffix would silently shift the window by the offset. +fn format_history_period( + instant: OffsetDateTime, +) -> Result> { + let format = + time::macros::format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]Z"); + instant + .to_offset(UtcOffset::UTC) + .format(&format) + .change_context(errors::IntegrationError::RequestEncodingFailed { + context: errors::IntegrationErrorContext { + suggested_action: None, + doc_url: Some("https://docs.ilixium.com/docs/direct/history".to_string()), + additional_context: Some( + "Failed to render a POST /history/operations period bound as \ + yyyy-MM-ddTHH:mm:ssZ." + .to_string(), + ), + }, + }) +} + +/// Reads the caller-supplied window anchor out of `PaymentsSyncData::connector_feature_data`. +/// +/// Accepted as an RFC 3339 instant (`2025-12-09T11:56:11Z`, or with an offset / fractional +/// seconds — this is *input* to the connector, not the wire format, so it is parsed leniently and +/// re-rendered by [`format_history_period`]). A value that is present but unparsable is a hard +/// error: silently falling back to "now" would answer a question the caller did not ask. +fn extract_history_period_start( + feature_data: Option<&common_utils::pii::SecretSerdeValue>, +) -> Result, error_stack::Report> { + let Some(feature_data) = feature_data else { + return Ok(None); + }; + let value = feature_data.clone().expose(); + let Some(raw) = HISTORY_PERIOD_START_KEYS + .iter() + .find_map(|key| value.get(*key).and_then(|found| found.as_str())) + else { + return Ok(None); + }; + + OffsetDateTime::parse(raw, &Rfc3339) + .map(Some) + .change_context(errors::IntegrationError::InvalidDataFormat { + field_name: "connector_feature_data.ilixium_history_period_start", + context: errors::IntegrationErrorContext { + suggested_action: Some( + "Supply the start of the /history/operations window as an RFC 3339 instant, \ + e.g. \"2025-12-09T11:56:11Z\". It should be at or just before the payment's \ + creation time; Ilixium reports at most 24 hours from that point." + .to_string(), + ), + doc_url: Some("https://docs.ilixium.com/docs/direct/history".to_string()), + additional_context: None, + }, + }) +} + +/// Picks the `[periodStartDate, periodEndDate]` pair for this sync. +/// +/// **Why it is not anchored on the payment automatically.** Neither `PaymentsSyncData` nor +/// `PaymentFlowData` carries a creation timestamp — there is no `created_at`, and the only +/// time-shaped value anywhere on the PSync `RouterDataV2` is whatever the caller puts in +/// `connector_feature_data`. So: +/// +/// * **Caller-anchored** — when `connector_feature_data.ilixium_history_period_start` is present, +/// the window is `[start, min(start + 24h, now)]`. This is the accurate mode: a caller that +/// knows when the payment was created can sync it at any age. +/// * **Default** — otherwise `[now - 24h, now]`, the widest window the API allows. A payment +/// whose operations all fall outside it simply will not appear in the report, which surfaces as +/// the explicit "not found in window" error built by [`build_history_not_found_error`] rather +/// than as a guessed status. +/// +/// The 24-hour cap is satisfied by construction — the window is *clamped*, never widened — so the +/// platform can never answer `VA73` because of a period this function chose. +fn resolve_history_window( + feature_data: Option<&common_utils::pii::SecretSerdeValue>, +) -> Result<(OffsetDateTime, OffsetDateTime), error_stack::Report> { + let now = OffsetDateTime::now_utc(); + let max_window = Duration::hours(HISTORY_MAX_WINDOW_HOURS); + + let overflow = |bound: &'static str| { + error_stack::report!(errors::IntegrationError::InvalidDataFormat { + field_name: "connector_feature_data.ilixium_history_period_start", + context: errors::IntegrationErrorContext { + suggested_action: Some(format!( + "The supplied history window start is so far from the representable range \ + that the {bound} could not be computed. Supply a real payment creation \ + time." + )), + doc_url: Some("https://docs.ilixium.com/docs/direct/history".to_string()), + additional_context: None, + }, + }) + }; + + match extract_history_period_start(feature_data)? { + Some(start) => { + if start >= now { + return Err(error_stack::report!( + errors::IntegrationError::InvalidDataFormat { + field_name: "connector_feature_data.ilixium_history_period_start", + context: errors::IntegrationErrorContext { + suggested_action: Some( + "The history window start must be in the past — Ilixium reports \ + operations that have already happened. Supply the payment's \ + creation time." + .to_string(), + ), + doc_url: Some( + "https://docs.ilixium.com/docs/direct/history".to_string() + ), + additional_context: None, + }, + } + )); + } + let end = start + .checked_add(max_window) + .ok_or_else(|| overflow("period end"))? + .min(now); + Ok((start, end)) + } + None => { + let start = now + .checked_sub(max_window) + .ok_or_else(|| overflow("period start"))?; + Ok((start, now)) + } + } +} + +/// `historyOperationsRequest.reportFormat`. Sent explicitly because the Direct API defaults to +/// XML, and this integration parses JSON. +#[derive(Debug, Clone, Copy, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum IlixiumReportFormat { + Json, +} + +/// `POST /history/operations` body. +/// +/// **There is deliberately no `version` field.** `historyOperationsRequest` is the one Ilixium +/// request schema that does not declare one, so sending [`ILIXIUM_MESSAGE_VERSION`] here would be +/// an unknown property on the message *and* would change the bytes the `X-MERCHANT-DIGEST` is +/// computed over. +/// +/// There is likewise no place to put the payment's identity: the schema's four properties are +/// exactly the two period bounds, `merchant` and `reportFormat`. Selecting one payment out of the +/// report is [`IlixiumHistoryResponse::latest_payment_operation`]'s job. +#[derive(Debug, Clone, Serialize)] +pub struct IlixiumHistoryRequest { + /// `yyyy-MM-ddTHH:mm:ssZ` — see [`format_history_period`]. + #[serde(rename = "periodStartDate")] + pub period_start_date: String, + /// `yyyy-MM-ddTHH:mm:ssZ`, at most 24 hours after `periodStartDate`. + #[serde(rename = "periodEndDate")] + pub period_end_date: String, + pub merchant: IlixiumMerchant, + #[serde(rename = "reportFormat")] + pub report_format: IlixiumReportFormat, +} + +impl + TryFrom< + IlixiumRouterData< + RouterDataV2, + T, + >, + > for IlixiumHistoryRequest +{ + type Error = error_stack::Report; + + fn try_from( + item: IlixiumRouterData< + RouterDataV2, + T, + >, + ) -> Result { + let router_data = &item.router_data; + let auth = IlixiumAuthType::try_from(&router_data.connector_config)?; + + derive_merchant_ref( + &router_data + .resource_common_data + .connector_request_reference_id, + )?; + + let (period_start, period_end) = + resolve_history_window(router_data.request.connector_feature_data.as_ref())?; + + Ok(Self { + period_start_date: format_history_period(period_start)?, + period_end_date: format_history_period(period_end)?, + merchant: IlixiumMerchant::from(&auth), + report_format: IlixiumReportFormat::Json, + }) + } +} + +/// `historyStatus.code` — a **different enum** from [`IlixiumStatusCode`], which is why this is a +/// sibling type rather than a reuse. +/// +/// It adds `EXCEPTION` and `VALIDATION_ERRORS` and drops `RESUBMISSION`; deserialising a history +/// envelope through `IlixiumStatusCode` would collapse both new values onto its `#[serde(other)]` +/// arm and lose the distinction between "the query was malformed" and "the operation failed". +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum IlixiumHistoryStatusCode { + Success, + Declined, + Rejected, + Error, + Cancelled, + Pending, + /// "An exception has occurred during the processing of the request … please contact support." + Exception, + /// "The request was not accepted because it failed validation checks." The `reasons` element + /// then carries the individual codes (`VA72` = `periodStartDate`, `VA73` = `periodEndDate`). + ValidationErrors, + /// Any code Ilixium adds that this integration does not yet know about. + #[serde(other)] + Unknown, +} + +/// `HistoryTransaction.transactionType` — again a **different enum** from the one every +/// `/direct/*` request and `paymentResponse` uses (`ECOMMERCE` / `MAIL_ORDER` / +/// `TELEPHONE_ORDER`). Reusing [`IlixiumTransactionType`] here would fail to match every value +/// the history report can return. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum IlixiumHistoryTransactionType { + CnpEcommerce, + CnpMailOrder, + CnpTelephoneOrder, + #[serde(other)] + Unknown, +} + +/// `historyStatus`, used at two levels with two different meanings: on the envelope it is the +/// outcome of the *query*, and on an `operation[]` entry it is the outcome of that *operation*. +/// +/// Kept separate from [`IlixiumStatus`] purely because of the `code` enum; `reasons` reuses +/// [`IlixiumReasons`], whose shape is identical. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct IlixiumHistoryStatus { + pub code: IlixiumHistoryStatusCode, + /// The History API does **not** apply ISO-8583 formatting to this field, so the wording + /// differs from the corresponding `paymentResponse` message. Never string-match across the + /// two. + pub message: Option, + pub reasons: Option, + /// `required` in `statusDetails`, yet omitted from the vendor's own success example. + pub timestamp: Option, + #[serde(rename = "operationRef")] + pub operation_ref: Option, +} + +impl IlixiumHistoryStatus { + pub fn reason_codes(&self) -> Vec { + self.reasons + .as_ref() + .map(IlixiumReasons::codes) + .unwrap_or_default() + } +} + +/// `HistoryTransaction` — a different schema from the `transactionDetails` echoed by +/// `paymentResponse`, and much thinner: there is no `paymentHistory` and no `cardResponse` +/// anywhere in a history entry, so PSync can recover **no** `authCode`, `iso8583code`, 3-D Secure +/// field or card token. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct IlixiumHistoryTransaction { + pub amount: Option, + pub currency: Option, + /// The only value that ties a report entry back to a UCS payment. + #[serde(rename = "merchantRef")] + pub merchant_ref: Option, + #[serde(rename = "parentRef")] + pub parent_ref: Option, + #[serde(rename = "gatewayRef")] + pub gateway_ref: Option, + #[serde(rename = "transactionType")] + pub transaction_type: Option, + #[serde(rename = "recurringMode")] + pub recurring_mode: Option, +} + +/// One `operation[]` entry. +/// +/// Every field is optional even where the schema marks it required: the vendor's own published +/// example omits `processedDate` on an entry that never completed processing, and one +/// unexpectedly-absent field on one unrelated entry must not make the whole report — and +/// therefore every payment's sync — undeserialisable. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct IlixiumHistoryOperation { + #[serde(rename = "entryDate")] + pub entry_date: Option, + #[serde(rename = "processedDate")] + pub processed_date: Option, + /// Same Operation Type enum as `paymentResponse.type` (this one Ilixium really does share). + #[serde(rename = "type")] + pub operation_type: Option, + #[serde(rename = "customerId")] + pub customer_id: Option, + pub transaction: Option, + pub status: Option, +} + +impl IlixiumHistoryOperation { + /// `entryDate` as an instant. The API returns millisecond-bearing RFC 3339 + /// (`2025-12-09T15:05:01.000Z`), which is *not* the shape the request accepts — see + /// [`format_history_period`]. + fn entry_instant(&self) -> Option { + self.entry_date + .as_deref() + .and_then(|raw| OffsetDateTime::parse(raw, &Rfc3339).ok()) + } + + fn merchant_ref(&self) -> Option<&str> { + self.transaction + .as_ref() + .and_then(|transaction| transaction.merchant_ref.as_deref()) + } + + fn gateway_ref(&self) -> Option { + self.transaction + .as_ref() + .and_then(|transaction| transaction.gateway_ref.clone()) + } + + /// `status.operationRef` — the ULID that "will uniquely identify each operation". The only + /// value that can tell two REFUND entries on one payment apart, and absent from both entries + /// of the vendor's own published example. See [`IlixiumHistoryResponse::match_refund_operation`]. + fn operation_ref(&self) -> Option<&str> { + self.status + .as_ref() + .and_then(|status| status.operation_ref.as_deref()) + .filter(|value| !value.is_empty()) + } + + /// `transaction.amount` parsed from its minor-unit digit string (`^[\d]{1,12}$`). + fn minor_amount(&self) -> Option { + self.transaction + .as_ref() + .and_then(|transaction| transaction.amount.as_deref()) + .and_then(|amount| amount.parse::().ok()) + } +} + +/// Operation types that describe the **payment's own** lifecycle, and are therefore the ones +/// PSync selects between. +/// +/// `REFUND` is excluded because it is RSync's subject, not PSync's — the vendor's own published +/// example has an `AUTH_CAP` and a `REFUND` sharing one `merchantRef`, so a filter that ignored +/// `type` would report a payment's status from its refund. `CREDIT` is a `/direct/credit` payout +/// and is not part of any payment. `PAYMENT` is the APM/hosted-UI surface this connector never +/// touches. +/// +/// `REVERSAL` **is** included even though it is not produced by Authorize or Capture: it is what +/// this connector's own Void flow records, and the tech spec's sync-mapping table maps +/// `REVERSAL` + `SUCCESS` to `Voided`. Omitting it would make PSync report a successfully voided +/// payment as `Authorized`. +fn is_payment_lifecycle_operation(operation_type: IlixiumOperationType) -> bool { + matches!( + operation_type, + IlixiumOperationType::Auth + | IlixiumOperationType::AuthCap + | IlixiumOperationType::Capture + | IlixiumOperationType::Reversal + | IlixiumOperationType::ThreeDsSecureComplete + ) +} + +/// `historyResponse`. +/// +/// A genuinely different envelope from [`IlixiumPaymentResponse`] — different `status` enum, +/// different transaction schema, an `operation[]` array instead of `paymentHistory`, and no +/// `version` or `type` at the top level — so this is a new type rather than an alias. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct IlixiumHistoryResponse { + /// The outcome of the **query**, not of any payment. A `SUCCESS` here says nothing at all + /// about the operations in `operation[]`. + pub status: IlixiumHistoryStatus, + /// Absent on some failures and explicitly `[]` on a validation rejection. + #[serde(default)] + pub operation: Vec, +} + +impl IlixiumHistoryResponse { + /// The latest payment-lifecycle operation recorded for `merchant_ref`, or `None` when the + /// payment does not appear in the queried window at all. + /// + /// **Latest, never first.** `/docs/direct/history` is explicit that a late outcome is + /// appended as a *new* entry and that "historical information is never modified", so the most + /// recent `entryDate` is the current truth. Entries whose `entryDate` is missing or + /// unparsable sort below every dated entry (`None < Some` for `Option`), and + /// ties are broken by document order so that a later duplicate still wins. + pub fn latest_payment_operation(&self, merchant_ref: &str) -> Option<&IlixiumHistoryOperation> { + self.operation + .iter() + .enumerate() + .filter(|(_, operation)| { + operation.merchant_ref() == Some(merchant_ref) + && operation + .operation_type + .is_some_and(is_payment_lifecycle_operation) + }) + .max_by(|(left_index, left), (right_index, right)| { + left.entry_instant() + .cmp(&right.entry_instant()) + .then_with(|| left_index.cmp(right_index)) + }) + .map(|(_, operation)| operation) + } +} + +/// Maps one history `operation[]` entry onto a UCS attempt status. +/// +/// A **sibling** of [`map_attempt_status`] / [`map_capture_status`] / [`map_void_status`], not an +/// extension of any of them, for three reasons: the `status.code` enum is +/// [`IlixiumHistoryStatusCode`] rather than [`IlixiumStatusCode`]; the operation type is read off +/// the entry rather than off the envelope, so one mapper has to cover authorisation, capture and +/// reversal outcomes at once; and there is no `cardResponse`, so the 3-D Secure branch that +/// [`map_attempt_status`] relies on cannot exist here. +fn map_history_sync_status( + operation: &IlixiumHistoryOperation, + status: &IlixiumHistoryStatus, + requested_auto_capture: bool, +) -> AttemptStatus { + let operation_type = operation.operation_type; + match status.code { + IlixiumHistoryStatusCode::Success => match operation_type { + Some(IlixiumOperationType::AuthCap) | Some(IlixiumOperationType::Capture) => { + AttemptStatus::Charged + } + Some(IlixiumOperationType::Auth) => AttemptStatus::Authorized, + Some(IlixiumOperationType::Reversal) => AttemptStatus::Voided, + _ => { + if requested_auto_capture { + AttemptStatus::Charged + } else { + AttemptStatus::Authorized + } + } + }, + IlixiumHistoryStatusCode::Pending => match operation_type { + Some(IlixiumOperationType::Capture) => AttemptStatus::CaptureInitiated, + Some(IlixiumOperationType::Reversal) => AttemptStatus::VoidInitiated, + _ => AttemptStatus::Pending, + }, + IlixiumHistoryStatusCode::Cancelled => AttemptStatus::Voided, + IlixiumHistoryStatusCode::Declined + | IlixiumHistoryStatusCode::Rejected + | IlixiumHistoryStatusCode::Error + | IlixiumHistoryStatusCode::Exception + | IlixiumHistoryStatusCode::ValidationErrors => match operation_type { + Some(IlixiumOperationType::Capture) => AttemptStatus::CaptureFailed, + Some(IlixiumOperationType::Reversal) => AttemptStatus::VoidFailed, + _ => AttemptStatus::Failure, + }, + IlixiumHistoryStatusCode::Unknown => AttemptStatus::Pending, + } +} + +/// Plain-language expansion of the validation codes `/history/operations` can return **about the +/// query itself**. +/// +/// A sibling of [`capture_failure_hint`] / [`reversal_failure_hint`]: this endpoint's failure +/// vocabulary is entirely disjoint from theirs, because nothing here is a payment outcome. +fn history_query_failure_hint(code: &str) -> Option<&'static str> { + match code { + "VA72" | "VB72" | "VC72" => Some( + "periodStartDate was missing or malformed — it must match yyyy-MM-ddTHH:mm:ssZ \ + exactly, with no fractional seconds and a literal Z", + ), + "VA73" | "VB73" | "VC73" => Some( + "periodEndDate was missing or malformed — it must match yyyy-MM-ddTHH:mm:ssZ \ + exactly, and the period it closes may span at most 24 hours", + ), + "VA74" | "VB74" | "VC74" => Some("reportFormat was missing or was not one of JSON / XML"), + _ => None, + } +} + +/// The error surfaced when the *query* failed — `VALIDATION_ERRORS`, `EXCEPTION`, or any other +/// non-`SUCCESS` envelope code. +/// +/// `attempt_status` deliberately echoes the payment's **existing** status: a report this +/// connector could not read says nothing whatsoever about the payment, and inventing a status +/// from a failed query is exactly the mistake this flow has to avoid. +fn build_history_query_error( + response: &IlixiumHistoryResponse, + current_status: AttemptStatus, + http_code: u16, +) -> ErrorResponse { + let reason_codes = response.status.reason_codes(); + let hints: Vec = reason_codes + .iter() + .filter_map(|code| history_query_failure_hint(code).map(|hint| format!("{code}: {hint}"))) + .collect(); + + ErrorResponse { + status_code: http_code, + code: reason_codes + .first() + .cloned() + .unwrap_or_else(|| format!("{:?}", response.status.code).to_uppercase()), + message: response + .status + .message + .clone() + .unwrap_or_else(|| common_utils::consts::NO_ERROR_MESSAGE.to_string()), + reason: Some(if hints.is_empty() { + format!( + "POST /history/operations answered {:?}; the payment's status could not be read \ + and is left unchanged.", + response.status.code + ) + } else { + hints.join("; ") + }), + attempt_status: Some(domain_types::router_data::FlowStatus::Payment( + current_status, + )), + connector_transaction_id: None, + network_decline_code: None, + network_advice_code: None, + network_error_message: None, + typed_connector_response: None, + raw_connector_response: None, + raw_connector_request: None, + typed_connector_request: None, + } +} + +/// The error surfaced when the query succeeded but the payment is **not in the report**. +/// +/// This is emphatically *not* a failed payment. Ilixium's report is windowed and capped at 24 +/// hours, so an absent `merchantRef` means one of: the payment is older than the queried window, +/// it is newer than `periodEndDate`, or the platform has not recorded an operation for it yet. +/// None of those is a payment outcome, so `attempt_status` echoes the status the sync started +/// with — `PaymentFlowData::status`, which the framework seeds to `Pending` for a sync — leaving +/// the caller's view of the payment exactly as it was, and the message spells out the one thing +/// the caller can actually do about it. +fn build_history_not_found_error( + merchant_ref: &str, + current_status: AttemptStatus, + http_code: u16, +) -> ErrorResponse { + ErrorResponse { + status_code: http_code, + code: "ILIXIUM_NOT_IN_HISTORY_WINDOW".to_string(), + message: format!( + "Ilixium's operation history contains no payment operation for \ + transaction.merchantRef {merchant_ref} in the queried period. The payment's status \ + is unchanged." + ), + reason: Some( + "POST /history/operations is the only query the Ilixium Direct API offers and it \ + reports a window of at most 24 hours, which this connector defaults to [now - 24h, \ + now]. A payment older than that cannot be synced against the default window: \ + re-issue the sync with connector_feature_data.ilixium_history_period_start set to \ + the payment's creation time (RFC 3339, e.g. \"2025-12-09T11:56:11Z\") and the \ + window will be anchored there instead." + .to_string(), + ), + attempt_status: Some(domain_types::router_data::FlowStatus::Payment( + current_status, + )), + connector_transaction_id: None, + network_decline_code: None, + network_advice_code: None, + network_error_message: None, + typed_connector_response: None, + raw_connector_response: None, + raw_connector_request: None, + typed_connector_request: None, + } +} + +/// The error surfaced when the matched operation itself failed. +/// +/// Unlike the two above, this one *is* a payment outcome, so `attempt_status` carries the mapped +/// failure status. History entries carry no ISO-8583 code and no `cardResponse`, so the reason +/// codes in `status.reasons.reason` are the only machine-readable detail there is. +fn build_history_operation_error( + status: &IlixiumHistoryStatus, + attempt_status: AttemptStatus, + gateway_ref: Option, + http_code: u16, +) -> ErrorResponse { + let reason_codes = status.reason_codes(); + ErrorResponse { + status_code: http_code, + code: reason_codes + .first() + .cloned() + .unwrap_or_else(|| format!("{:?}", status.code).to_uppercase()), + message: status + .message + .clone() + .unwrap_or_else(|| common_utils::consts::NO_ERROR_MESSAGE.to_string()), + reason: if reason_codes.is_empty() { + status.message.clone() + } else { + Some(reason_codes.join(", ")) + }, + attempt_status: Some(domain_types::router_data::FlowStatus::Payment( + attempt_status, + )), + connector_transaction_id: gateway_ref, + network_decline_code: None, + network_advice_code: None, + network_error_message: None, + typed_connector_response: None, + raw_connector_response: None, + raw_connector_request: None, + typed_connector_request: None, + } +} + +impl TryFrom> + for RouterDataV2 +{ + type Error = error_stack::Report; + + fn try_from( + item: crate::types::ResponseRouterData, + ) -> Result { + let response = item.response; + let current_status = item.router_data.resource_common_data.status; + + let merchant_ref = derive_merchant_ref( + &item + .router_data + .resource_common_data + .connector_request_reference_id, + ) + .change_context(errors::ConnectorError::ResponseHandlingFailed { + context: errors::ResponseTransformationErrorContext { + http_status_code: Some(item.http_code), + additional_context: Some( + "Could not derive the Ilixium transaction.merchantRef to match this payment \ + against POST /history/operations." + .to_string(), + ), + }, + })?; + + if response.status.code != IlixiumHistoryStatusCode::Success { + return Ok(Self { + response: Err(build_history_query_error( + &response, + current_status, + item.http_code, + )), + ..item.router_data + }); + } + + let Some(operation) = response.latest_payment_operation(&merchant_ref) else { + return Ok(Self { + response: Err(build_history_not_found_error( + &merchant_ref, + current_status, + item.http_code, + )), + ..item.router_data + }); + }; + + let Some(operation_status) = operation.status.as_ref() else { + return Ok(Self { + response: Err(build_history_not_found_error( + &merchant_ref, + current_status, + item.http_code, + )), + ..item.router_data + }); + }; + + let status = map_history_sync_status( + operation, + operation_status, + item.router_data.request.is_auto_capture(), + ); + let gateway_ref = operation.gateway_ref(); + let operation_merchant_ref = operation.merchant_ref().map(ToOwned::to_owned); + + let raw_connector_status = RawConnectorStatus { + code: Some(format!("{:?}", operation_status.code).to_uppercase()), + message: operation_status.message.clone(), + reason: { + let codes = operation_status.reason_codes(); + (!codes.is_empty()).then(|| codes.join(", ")) + }, + }; + + let payments_response = if matches!( + status, + AttemptStatus::Failure | AttemptStatus::CaptureFailed | AttemptStatus::VoidFailed + ) { + Err(build_history_operation_error( + operation_status, + status, + gateway_ref.clone(), + item.http_code, + )) + } else { + Ok(PaymentsResponseData::TransactionResponse { + resource_id: gateway_ref.clone().map_or_else( + || item.router_data.request.connector_transaction_id.clone(), + ResponseId::ConnectorTransactionId, + ), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + network_txn_link_id: None, + connector_response_reference_id: operation_merchant_ref, + incremental_authorization_allowed: None, + splits: None, + status_code: item.http_code, + }) + }; + + Ok(Self { + response: payments_response, + resource_common_data: PaymentFlowData { + status, + raw_connector_status: Some(raw_connector_status), + ..item.router_data.resource_common_data + }, + ..item.router_data + }) + } +} + +/// The RSync request body — **the same message PSync sends**, aliased under its own name. +/// +/// The alias is not cosmetic. `create_all_prerequisites!` mints one `Templating` marker +/// struct per named request/response ident, so naming [`IlixiumHistoryRequest`] on a second flow +/// would try to define `IlixiumHistoryRequestTemplating` twice. A distinct ident gives RSync its +/// own marker while keeping exactly one request type, one serialisation and one digest. +pub type IlixiumRefundHistoryRequest = IlixiumHistoryRequest; + +/// The RSync response envelope — **the same message PSync parses**, aliased for the same +/// `Templating` reason as [`IlixiumRefundHistoryRequest`]. +pub type IlixiumRefundHistoryResponse = IlixiumHistoryResponse; + +impl + TryFrom< + IlixiumRouterData< + RouterDataV2, + T, + >, + > for IlixiumRefundHistoryRequest +{ + type Error = error_stack::Report; + + fn try_from( + item: IlixiumRouterData< + RouterDataV2, + T, + >, + ) -> Result { + let router_data = &item.router_data; + let request = &router_data.request; + let auth = IlixiumAuthType::try_from(&router_data.connector_config)?; + + resolve_original_merchant_ref( + request.connector_order_id.as_deref(), + &request.connector_transaction_id, + )?; + + let (period_start, period_end) = + resolve_history_window(request.connector_feature_data.as_ref())?; + + Ok(Self { + period_start_date: format_history_period(period_start)?, + period_end_date: format_history_period(period_end)?, + merchant: IlixiumMerchant::from(&auth), + report_format: IlixiumReportFormat::Json, + }) + } +} + +/// Length of a ULID in its canonical Crockford-Base32 text form. +const OPERATION_REF_LEN: usize = 26; + +/// Whether a value looks like an Ilixium `operationRef`. +/// +/// `/docs/direct/reconciliation` describes `operationRef` as a ULID, which is 26 characters of +/// Crockford Base32. The test is deliberately a *shape* test rather than a strict alphabet check: +/// its only job is to separate a real `operationRef` from the fallback id +/// [`refund_identifier`] hands out when Ilixium omits one (a UCS `merchant_refund_id`, which is a +/// dashed/prefixed value of a quite different length). Being slightly permissive costs nothing — +/// a value that passes but matches no entry simply falls through to the heuristic below. +fn is_operation_ref_shaped(value: &str) -> bool { + value.len() == OPERATION_REF_LEN && value.chars().all(|c| c.is_ascii_alphanumeric()) +} + +/// How a `type: REFUND` history entry was selected for the refund being synced. +/// +/// The distinction is carried out of the matcher rather than collapsed inside it because the two +/// arms have genuinely different authority, and the caller logs accordingly. +enum RefundOperationMatch<'a> { + /// Selected by an exact `status.operationRef` match. Authoritative: `operationRef` is + /// documented to uniquely identify an operation. + Exact(&'a IlixiumHistoryOperation), + /// Selected by the documented heuristic because no `operationRef` was available to match on. + /// `candidates` is how many `type: REFUND` entries remained after narrowing — anything above + /// 1 means the choice was genuinely ambiguous. + Heuristic { + operation: &'a IlixiumHistoryOperation, + candidates: usize, + }, +} + +impl RefundOperationMatch<'_> { + fn operation(&self) -> &IlixiumHistoryOperation { + match self { + Self::Exact(operation) => operation, + Self::Heuristic { operation, .. } => operation, + } + } +} + +impl IlixiumHistoryResponse { + /// Selects the `type: REFUND` entry that corresponds to the refund being synced. + /// + /// **The candidate set** is every entry whose `transaction.merchantRef` is the *original + /// payment's* and whose `type` is exactly `REFUND`. `CREDIT` is excluded by construction: it + /// is a `/direct/credit` payout, not a refund of a payment, and treating one as a refund would + /// report money-out that no refund record ever asked for. Every other operation type belongs + /// to the payment's own lifecycle and is PSync's subject (see + /// [`Self::latest_payment_operation`]). + /// + /// **Selecting one refund out of the candidates is the hard part, and it cannot be done + /// reliably today.** A variant-A refund is bound to its payment by the payment's own + /// `merchantRef`, and its history entry echoes the payment's `merchantRef` *and* `gatewayRef` + /// — observable in the vendor's own published example, where an `AUTH_CAP` and a `REFUND` + /// share both values, with **no `operationRef` on either**. So N partial refunds against one + /// payment produce N entries differing only by `entryDate`, `processedDate` and `amount`. + /// + /// The ladder is therefore: + /// + /// 1. **Exact `operationRef` match** — used when `RefundSyncData::connector_refund_id` holds + /// an [`is_operation_ref_shaped`] value (i.e. `/direct/refund` returned a real + /// `operationRef` and [`refund_identifier`] passed it through). This is the only + /// authoritative match, and the only one that survives multiple partial refunds. + /// 2. If that id is `operationRef`-shaped and the report *does* carry `operationRef`s on the + /// candidates but none of them is ours, the refund is **not in this window** — `None` — and + /// is emphatically *not* silently swapped for some other refund of the same payment. + /// 3. **Heuristic**, used only when no `operationRef` is available on either side: narrow to + /// the candidates whose `transaction.amount` equals the amount being synced (skipped when + /// that would leave nothing, so a report that omits `amount` still matches), then take the + /// **latest by `entryDate`** — the same "late outcomes are appended, never edited" rule + /// PSync relies on — with ties broken by document order. + /// + /// **Known limitation (tech spec UNDECIDED §12).** Step 3 cannot distinguish two refunds of + /// the same amount on the same payment. Syncing refund #1 of three identical partial refunds + /// returns the *latest* one's status. The caller is told: the match arm is reported back so + /// the response mapping can log the ambiguity, and the number of candidates is carried with + /// it. This resolves itself the moment Ilixium's `operationRef` goes live, at which point + /// step 1 applies and steps 2–3 become unreachable. + fn match_refund_operation( + &self, + merchant_ref: &str, + connector_refund_id: &str, + minor_refund_amount: Option, + ) -> Option> { + let candidates: Vec<(usize, &IlixiumHistoryOperation)> = self + .operation + .iter() + .enumerate() + .filter(|(_, operation)| { + operation.merchant_ref() == Some(merchant_ref) + && operation.operation_type == Some(IlixiumOperationType::Refund) + }) + .collect(); + + if candidates.is_empty() { + return None; + } + + if is_operation_ref_shaped(connector_refund_id) { + if let Some((_, operation)) = candidates + .iter() + .find(|(_, operation)| operation.operation_ref() == Some(connector_refund_id)) + { + return Some(RefundOperationMatch::Exact(operation)); + } + + if candidates + .iter() + .any(|(_, operation)| operation.operation_ref().is_some()) + { + return None; + } + } + + let narrowed: Vec<(usize, &IlixiumHistoryOperation)> = match minor_refund_amount { + Some(amount) => { + let matching: Vec<(usize, &IlixiumHistoryOperation)> = candidates + .iter() + .filter(|(_, operation)| operation.minor_amount() == Some(amount)) + .copied() + .collect(); + if matching.is_empty() { + candidates + } else { + matching + } + } + None => candidates, + }; + + let candidate_count = narrowed.len(); + narrowed + .into_iter() + .max_by(|(left_index, left), (right_index, right)| { + left.entry_instant() + .cmp(&right.entry_instant()) + .then_with(|| left_index.cmp(right_index)) + }) + .map(|(_, operation)| RefundOperationMatch::Heuristic { + operation, + candidates: candidate_count, + }) + } +} + +/// Maps one `type: REFUND` history entry onto a UCS [`RefundStatus`]. +/// +/// A **fifth sibling** of [`map_attempt_status`] / [`map_capture_status`] / [`map_void_status`] / +/// [`map_refund_status`], not a reuse of any of them. The nearest neighbour, [`map_refund_status`], +/// is keyed on [`IlixiumStatusCode`]; this one is keyed on [`IlixiumHistoryStatusCode`], a +/// different enum that adds `EXCEPTION` and `VALIDATION_ERRORS` and drops `RESUBMISSION`. Feeding +/// a history entry through `map_refund_status` would not even type-check, and collapsing the two +/// enums to make it would lose the distinction between "the operation failed" and "the query was +/// malformed". +/// +/// The nearest neighbour in the *other* direction, [`map_history_sync_status`], shares this enum +/// but targets `AttemptStatus` and branches on the operation type; here the operation type is +/// already known to be `REFUND`, so only the status code matters. +fn map_history_refund_status(status: &IlixiumHistoryStatus) -> RefundStatus { + match status.code { + IlixiumHistoryStatusCode::Success => RefundStatus::Success, + IlixiumHistoryStatusCode::Pending => RefundStatus::Pending, + IlixiumHistoryStatusCode::Cancelled + | IlixiumHistoryStatusCode::Declined + | IlixiumHistoryStatusCode::Rejected + | IlixiumHistoryStatusCode::Error + | IlixiumHistoryStatusCode::Exception + | IlixiumHistoryStatusCode::ValidationErrors => RefundStatus::Failure, + IlixiumHistoryStatusCode::Unknown => RefundStatus::Pending, + } +} + +/// The error surfaced when the *query* failed — the refund-side counterpart of +/// [`build_history_query_error`]. +/// +/// Written out rather than layered on it because that builder stamps +/// `attempt_status: FlowStatus::Payment(..)`, and a refund sync must report +/// `FlowStatus::Refund(..)`. As there, the status echoed back is the refund's **existing** one: a +/// report this connector could not read says nothing whatsoever about the refund. +fn build_refund_history_query_error( + response: &IlixiumRefundHistoryResponse, + current_status: RefundStatus, + http_code: u16, +) -> ErrorResponse { + let reason_codes = response.status.reason_codes(); + let hints: Vec = reason_codes + .iter() + .filter_map(|code| history_query_failure_hint(code).map(|hint| format!("{code}: {hint}"))) + .collect(); + + ErrorResponse { + status_code: http_code, + code: reason_codes + .first() + .cloned() + .unwrap_or_else(|| format!("{:?}", response.status.code).to_uppercase()), + message: response + .status + .message + .clone() + .unwrap_or_else(|| common_utils::consts::NO_ERROR_MESSAGE.to_string()), + reason: Some(if hints.is_empty() { + format!( + "POST /history/operations answered {:?}; the refund's status could not be read \ + and is left unchanged.", + response.status.code + ) + } else { + hints.join("; ") + }), + attempt_status: Some(domain_types::router_data::FlowStatus::Refund( + current_status, + )), + connector_transaction_id: None, + network_decline_code: None, + network_advice_code: None, + network_error_message: None, + typed_connector_response: None, + raw_connector_response: None, + raw_connector_request: None, + typed_connector_request: None, + } +} + +/// The error surfaced when the query succeeded but no `type: REFUND` entry for this payment is in +/// the report — the refund-side counterpart of [`build_history_not_found_error`]. +/// +/// This is emphatically *not* a failed refund, so `attempt_status` echoes the status the sync +/// started with. Three things can put a refund here, and the message names all of them: the refund +/// is outside the queried `<= 24h` window, the platform has not recorded an operation for it yet, +/// or `operationRef` is live and none of the payment's refund entries is this one. +fn build_refund_not_found_error( + merchant_ref: &str, + current_status: RefundStatus, + http_code: u16, +) -> ErrorResponse { + ErrorResponse { + status_code: http_code, + code: "ILIXIUM_REFUND_NOT_IN_HISTORY_WINDOW".to_string(), + message: format!( + "Ilixium's operation history contains no REFUND operation for \ + transaction.merchantRef {merchant_ref} in the queried period. The refund's status is \ + unchanged." + ), + reason: Some( + "POST /history/operations is the only query the Ilixium Direct API offers — there is \ + no refund-status endpoint and no lookup by refund id — and it reports a window of at \ + most 24 hours, which this connector defaults to [now - 24h, now]. A refund older \ + than that cannot be synced against the default window: re-issue the sync with \ + connector_feature_data.ilixium_history_period_start set to at or just before the \ + refund's creation time (RFC 3339, e.g. \"2025-12-09T11:56:11Z\") and the window will \ + be anchored there instead. Note also that CREDIT operations are payouts, not \ + refunds, and are never matched." + .to_string(), + ), + attempt_status: Some(domain_types::router_data::FlowStatus::Refund( + current_status, + )), + connector_transaction_id: None, + network_decline_code: None, + network_advice_code: None, + network_error_message: None, + typed_connector_response: None, + raw_connector_response: None, + raw_connector_request: None, + typed_connector_request: None, + } +} + +/// The error surfaced when the matched REFUND entry itself failed — the refund-side counterpart of +/// [`build_history_operation_error`]. +/// +/// Unlike the two above, this one *is* an outcome, so `attempt_status` carries the mapped +/// [`RefundStatus`]. History entries carry no ISO-8583 code and no `cardResponse`, so +/// `status.reasons.reason` is the only machine-readable detail there is; it is expanded through +/// [`refund_failure_hint`], the same vocabulary `/direct/refund` uses, because a REFUND history +/// entry records the outcome of exactly that operation. +fn build_refund_history_operation_error( + status: &IlixiumHistoryStatus, + refund_status: RefundStatus, + gateway_ref: Option, + http_code: u16, +) -> ErrorResponse { + let reason_codes = status.reason_codes(); + let hints: Vec = reason_codes + .iter() + .filter_map(|code| refund_failure_hint(code).map(|hint| format!("{code}: {hint}"))) + .collect(); + + ErrorResponse { + status_code: http_code, + code: reason_codes + .first() + .cloned() + .unwrap_or_else(|| format!("{:?}", status.code).to_uppercase()), + message: status + .message + .clone() + .unwrap_or_else(|| common_utils::consts::NO_ERROR_MESSAGE.to_string()), + reason: if !hints.is_empty() { + Some(hints.join("; ")) + } else if reason_codes.is_empty() { + status.message.clone() + } else { + Some(reason_codes.join(", ")) + }, + attempt_status: Some(domain_types::router_data::FlowStatus::Refund(refund_status)), + connector_transaction_id: gateway_ref, + network_decline_code: None, + network_advice_code: None, + network_error_message: None, + typed_connector_response: None, + raw_connector_response: None, + raw_connector_request: None, + typed_connector_request: None, + } +} + +/// Resolves the `connector_refund_id` an RSync reports back. +/// +/// Prefers the matched entry's `status.operationRef` — if the platform has started populating it, +/// echoing it upgrades a UCS-side fallback id to the real Ilixium one — and otherwise echoes the +/// id the sync was issued with, which is still correct. Erroring is reachable only if a sync was +/// somehow issued with no refund id at all. +fn resolve_sync_refund_identifier( + operation: &IlixiumHistoryOperation, + connector_refund_id: &str, + http_code: u16, +) -> Result> { + if let Some(operation_ref) = operation.operation_ref() { + return Ok(operation_ref.to_owned()); + } + + if !connector_refund_id.is_empty() { + return Ok(connector_refund_id.to_owned()); + } + + Err(error_stack::report!( + errors::ConnectorError::ResponseHandlingFailed { + context: errors::ResponseTransformationErrorContext { + http_status_code: Some(http_code), + additional_context: Some( + "The matched Ilixium REFUND history entry carries no status.operationRef and \ + the sync was issued with no connector_refund_id, so this refund has no \ + identifier: the entry's merchantRef and gatewayRef both echo the original \ + payment and would collide across partial refunds." + .to_string(), + ), + }, + } + )) +} + +impl TryFrom> + for RouterDataV2 +{ + type Error = error_stack::Report; + + fn try_from( + item: crate::types::ResponseRouterData, + ) -> Result { + let response = item.response; + let request = &item.router_data.request; + let current_status = item.router_data.resource_common_data.status; + + let merchant_ref = resolve_original_merchant_ref( + request.connector_order_id.as_deref(), + &request.connector_transaction_id, + ) + .change_context(errors::ConnectorError::ResponseHandlingFailed { + context: errors::ResponseTransformationErrorContext { + http_status_code: Some(item.http_code), + additional_context: Some( + "Could not resolve the original payment's Ilixium transaction.merchantRef to \ + match this refund against POST /history/operations." + .to_string(), + ), + }, + })?; + + if response.status.code != IlixiumHistoryStatusCode::Success { + return Ok(Self { + response: Err(build_refund_history_query_error( + &response, + current_status, + item.http_code, + )), + ..item.router_data + }); + } + + let refund_amount = request + .refund_money + .as_ref() + .map(|money| money.amount.get_amount_as_i64()); + + let Some(matched) = response.match_refund_operation( + &merchant_ref, + &request.connector_refund_id, + refund_amount, + ) else { + return Ok(Self { + response: Err(build_refund_not_found_error( + &merchant_ref, + current_status, + item.http_code, + )), + ..item.router_data + }); + }; + + if let RefundOperationMatch::Heuristic { candidates, .. } = &matched { + if *candidates > 1 { + tracing::warn!( + connector = "ilixium", + merchant_ref = %merchant_ref, + candidates = *candidates, + "Ilixium's operation history holds several REFUND entries for one payment and \ + none carries a status.operationRef, so this refund was matched by the \ + documented heuristic (amount, then latest entryDate) rather than \ + identified. Partial refunds of equal amounts on one payment are not \ + individually distinguishable at Ilixium until operationRef goes live." + ); + } + } + + let operation = matched.operation(); + + let Some(operation_status) = operation.status.as_ref() else { + return Ok(Self { + response: Err(build_refund_not_found_error( + &merchant_ref, + current_status, + item.http_code, + )), + ..item.router_data + }); + }; + + let status = map_history_refund_status(operation_status); + + let raw_connector_status = RawConnectorStatus { + code: Some(format!("{:?}", operation_status.code).to_uppercase()), + message: operation_status.message.clone(), + reason: { + let codes = operation_status.reason_codes(); + (!codes.is_empty()).then(|| codes.join(", ")) + }, + }; + + let refunds_response = if status == RefundStatus::Failure { + Err(build_refund_history_operation_error( + operation_status, + status, + operation.gateway_ref(), + item.http_code, + )) + } else { + Ok(RefundsResponseData { + connector_refund_id: resolve_sync_refund_identifier( + operation, + &request.connector_refund_id, + item.http_code, + )?, + refund_status: status, + status_code: item.http_code, + acquirer_reference_number: None, + }) + }; + + Ok(Self { + response: refunds_response, + resource_common_data: RefundFlowData { + status, + raw_connector_status: Some(raw_connector_status), + ..item.router_data.resource_common_data + }, + ..item.router_data + }) + } +} diff --git a/crates/integrations/connector-integration/src/connectors/maya.rs b/crates/integrations/connector-integration/src/connectors/maya.rs index 759bebbfb0..3d357e9937 100644 --- a/crates/integrations/connector-integration/src/connectors/maya.rs +++ b/crates/integrations/connector-integration/src/connectors/maya.rs @@ -17,7 +17,7 @@ use domain_types::{ types::Connectors, }; use error_stack::ResultExt; -use hyperswitch_masking::{ExposeInterface, Maskable}; +use hyperswitch_masking::{ExposeInterface, Maskable, Secret}; use interfaces::{ api::ConnectorCommon, connector_integration_v2::ConnectorIntegrationV2, connector_types, decode::BodyDecoding, @@ -213,7 +213,9 @@ impl Conn network_advice_code: None, network_error_message: None, typed_connector_response: typed, - raw_connector_response: None, + raw_connector_response: Some(Secret::new( + String::from_utf8_lossy(&res.response).to_string(), + )), raw_connector_request: None, typed_connector_request: None, }) @@ -634,7 +636,7 @@ impl fn process_redirect_response( &self, _request: &RequestDetails, - _connector_feature_data: Option<&hyperswitch_masking::Secret>, + _connector_feature_data: Option<&Secret>, ) -> CustomResult { // Maya is a redirect-only connector. The redirect body carries no // meaningful payment state; final status is confirmed via PSync or diff --git a/crates/integrations/connector-integration/src/connectors/maya/transformers.rs b/crates/integrations/connector-integration/src/connectors/maya/transformers.rs index e330b6eb1b..b89fb28a1a 100644 --- a/crates/integrations/connector-integration/src/connectors/maya/transformers.rs +++ b/crates/integrations/connector-integration/src/connectors/maya/transformers.rs @@ -1,4 +1,7 @@ -use common_utils::{types::FloatMajorUnit, Method}; +use common_utils::{ + types::{AmountConvertor, FloatMajorUnit, StringMajorUnit, StringMajorUnitForConnector}, + Method, +}; use domain_types::{ connector_flow::{Authorize, PSync, RSync, Refund, Void}, connector_types::{ @@ -10,10 +13,11 @@ use domain_types::{ payment_method_data::{PaymentMethodData, PaymentMethodDataTypes, WalletData}, router_data::ConnectorSpecificConfig, router_data_v2::RouterDataV2, + router_request_types::{PaymentSynIntegrityObject, RefundIntegrityObject}, router_response_types::RedirectForm, }; use error_stack::ResultExt; -use hyperswitch_masking::{PeekInterface, Secret}; +use hyperswitch_masking::Secret; use serde::{Deserialize, Serialize}; use url::Url; @@ -86,8 +90,6 @@ pub struct MayaPaymentsRequest { pub total_amount: MayaTotalAmount, pub redirect_url: MayaRedirectUrl, pub request_reference_number: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub user_id: Option, } /// `totalAmount` object carried by most Maya request bodies (`value` + `currency`, @@ -251,6 +253,27 @@ pub struct MayaWebhookBody { pub can_void: Option, #[serde(default)] pub can_refund: Option, + /// Whether Maya has captured funds for this payment. Present on the + /// payment-inquiry response but not on every webhook event, so optional. + #[serde(default)] + pub can_capture: Option, + /// Whether the payment has been paid. Returned by the payment-inquiry + /// endpoint; absent on some webhook events, so optional. + #[serde(default)] + pub is_paid: Option, + /// Processed amount returned by Maya. The API reference documents this as a + /// JSON number, but Maya's live payment response actually serializes it as a + /// decimal string (e.g. `"100"`), so it is modeled as `StringMajorUnit` and + /// fed through the money framework into the PSync integrity check. + #[serde(default)] + pub amount: Option, + /// ISO 4217 currency echoed by Maya on the payment object. + #[serde(default)] + pub currency: Option, + #[serde(default)] + pub created_at: Option, + #[serde(default)] + pub updated_at: Option, #[serde(default)] pub metadata: Option, } @@ -446,12 +469,6 @@ impl TryFrom; fn try_from(item: ResponseRouterData) -> Result { + let raw_connector_response = serde_json::to_string(&item.response).ok().map(Secret::new); + let redirect_url = Url::parse(&item.response.redirect_url).change_context( ConnectorError::response_deserialization_failed_with_context( item.http_code, @@ -513,6 +531,7 @@ impl TryFrom> type Error = error_stack::Report; fn try_from(item: ResponseRouterData) -> Result { + let raw_connector_response = serde_json::to_string(&item.response).ok().map(Secret::new); + let connector_request_reference_id = item .router_data .resource_common_data @@ -534,6 +555,22 @@ impl TryFrom> let payment = item.response; + // Maya returns the processed amount/currency on the payment object. Run them + // through the money framework and hand them to the PSync integrity check, + // but only when both are present (Maya omits them for e.g. expired payments). + let integrity_object = match (payment.amount.clone(), payment.currency) { + (Some(amount), Some(currency)) => { + let amount = StringMajorUnitForConnector + .convert_back(amount, currency) + .change_context(crate::utils::response_handling_fail_for_connector( + item.http_code, + "maya", + ))?; + Some(PaymentSynIntegrityObject { amount, currency }) + } + _ => None, + }; + let settlement_status = maya_settlement_status(payment.can_void, payment.can_refund); let status = common_enums::AttemptStatus::from(payment.status.clone()); @@ -567,10 +604,15 @@ impl TryFrom> splits: None, status_code: item.http_code, }), + request: PaymentsSyncData { + integrity_object, + ..item.router_data.request + }, resource_common_data: PaymentFlowData { status, settlement_status, raw_connector_status: Some(raw_connector_status), + raw_connector_response, ..item.router_data.resource_common_data }, ..item.router_data @@ -584,6 +626,8 @@ impl TryFrom> type Error = error_stack::Report; fn try_from(item: ResponseRouterData) -> Result { + let raw_connector_response = serde_json::to_string(&item.response).ok().map(Secret::new); + let status = common_enums::AttemptStatus::from(item.response.status.clone()); let void_id = item.response.id; let payment_id = item.response.payment; @@ -608,6 +652,7 @@ impl TryFrom> message: None, reason: None, }), + raw_connector_response, ..item.router_data.resource_common_data }, ..item.router_data @@ -658,6 +703,14 @@ pub struct MayaRefundResponse { pub status: MayaRefundStatus, #[serde(default)] pub reason: Option, + /// Refunded amount echoed by Maya. Unlike the payment object's numeric + /// `amount`, Maya's refund response serializes it as a JSON string, so it is + /// modeled as `StringMajorUnit` and fed through the money framework. + #[serde(default)] + pub amount: Option, + /// ISO 4217 currency echoed by Maya on the refund response. + #[serde(default)] + pub currency: Option, #[serde(default)] pub request_reference_number: Option, #[serde(default)] @@ -726,6 +779,8 @@ impl TryFrom> type Error = error_stack::Report; fn try_from(item: ResponseRouterData) -> Result { + let raw_connector_response = serde_json::to_string(&item.response).ok().map(Secret::new); + let refund_status = common_enums::RefundStatus::from(item.response.status.clone()); let raw_connector_status = RawConnectorStatus { @@ -734,6 +789,24 @@ impl TryFrom> reason: None, }; + // Feed Maya's echoed refund amount/currency through the money framework into + // the refund integrity check, but only when Maya returns both. + let integrity_object = match (item.response.amount.clone(), item.response.currency) { + (Some(amount), Some(currency)) => { + let refund_amount = StringMajorUnitForConnector + .convert_back(amount, currency) + .change_context(crate::utils::response_handling_fail_for_connector( + item.http_code, + "maya", + ))?; + Some(RefundIntegrityObject { + refund_amount, + currency, + }) + } + _ => None, + }; + Ok(Self { response: Ok(RefundsResponseData { connector_refund_id: item.response.id, @@ -741,8 +814,13 @@ impl TryFrom> status_code: item.http_code, acquirer_reference_number: None, }), + request: RefundsData { + integrity_object, + ..item.router_data.request + }, resource_common_data: RefundFlowData { raw_connector_status: Some(raw_connector_status), + raw_connector_response, ..item.router_data.resource_common_data }, ..item.router_data @@ -758,6 +836,8 @@ impl TryFrom> fn try_from( item: ResponseRouterData, ) -> Result { + let raw_connector_response = serde_json::to_string(&item.response).ok().map(Secret::new); + let refund_status = common_enums::RefundStatus::from(item.response.status.clone()); let raw_connector_status = RawConnectorStatus { @@ -775,6 +855,7 @@ impl TryFrom> }), resource_common_data: RefundFlowData { raw_connector_status: Some(raw_connector_status), + raw_connector_response, ..item.router_data.resource_common_data }, ..item.router_data diff --git a/crates/integrations/connector-integration/src/connectors/moneris.rs b/crates/integrations/connector-integration/src/connectors/moneris.rs new file mode 100644 index 0000000000..5b6adfb699 --- /dev/null +++ b/crates/integrations/connector-integration/src/connectors/moneris.rs @@ -0,0 +1,727 @@ +pub mod transformers; + +use std::fmt::Debug; + +use common_enums::CurrencyUnit; +use common_utils::{errors::CustomResult, events, ext_traits::ByteSliceExt, types::MinorUnit}; +use domain_types::{ + connector_flow::{ + Authorize, Capture, PSync, RSync, Refund, RepeatPayment, ServerAuthenticationToken, Void, + }, + connector_types::{ + PaymentFlowData, PaymentVoidData, PaymentsAuthorizeData, PaymentsCaptureData, + PaymentsResponseData, PaymentsSyncData, RefundFlowData, RefundSyncData, RefundsData, + RefundsResponseData, RepeatPaymentData, ServerAuthenticationTokenRequestData, + ServerAuthenticationTokenResponseData, + }, + errors::{ConnectorError, IntegrationError, IntegrationErrorContext}, + merchant_authentication_flow_data::MerchantAuthenticationFlowData, + 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::{ExposeInterface, Mask, Maskable}; +use interfaces::{ + api::ConnectorCommon, connector_integration_v2::ConnectorIntegrationV2, connector_types, + decode::BodyDecoding, verification::SourceVerification, +}; +use serde::Serialize; +use transformers::{ + self as moneris, MonerisAuthRequest, MonerisAuthResponse, MonerisAuthorizeResponse, + MonerisCancelRequest, MonerisCaptureResponse, MonerisPaymentsCaptureRequest, + MonerisPaymentsRequest, MonerisPaymentsResponse as MonerisRepeatPaymentResponse, + MonerisPaymentsResponse as MonerisPaymentSyncResponse, + MonerisPaymentsResponse as MonerisPaymentVoidResponse, MonerisRefundRequest, + MonerisRefundResponse, MonerisRefundResponse as MonerisRefundSyncResponse, + MonerisRepeatPaymentRequest, +}; + +use super::macros; +use crate::{types::ResponseRouterData, utils, with_error_response_body}; + +pub(crate) mod headers { + pub(crate) const AUTHORIZATION: &str = "Authorization"; + pub(crate) const CONTENT_TYPE: &str = "Content-Type"; +} + +impl + connector_types::ConnectorServiceTrait for Moneris +{ +} +impl + connector_types::PaymentAuthorizeV2 for Moneris +{ +} +impl + connector_types::PaymentSyncV2 for Moneris +{ +} +impl + connector_types::PaymentCapture for Moneris +{ +} +impl + connector_types::RefundV2 for Moneris +{ +} +impl + connector_types::RefundSyncV2 for Moneris +{ +} +impl + connector_types::PaymentVoidV2 for Moneris +{ +} +impl + connector_types::RepeatPaymentV2 for Moneris +{ +} +impl + connector_types::ServerAuthentication for Moneris +{ +} +impl + connector_types::ValidationTrait for Moneris +{ + fn should_do_access_token(&self, _payment_method: Option) -> bool { + true + } +} +impl + connector_types::IncomingWebhook for Moneris +{ +} +impl + connector_types::VerifyRedirectResponse for Moneris +{ +} +impl SourceVerification + for Moneris +{ +} +impl BodyDecoding + for Moneris +{ +} + +impl ConnectorCommon + for Moneris +{ + fn id(&self) -> &'static str { + "moneris" + } + + 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.moneris.base_url.as_ref() + } + + fn build_error_response( + &self, + res: Response, + event_builder: Option<&mut events::Event>, + _connector_config: &ConnectorSpecificConfig, + ) -> CustomResult { + let response: moneris::MonerisErrorResponse = res + .response + .parse_struct("MonerisErrorResponse") + .change_context(utils::response_handling_fail_for_connector( + res.status_code, + "moneris", + ))?; + + with_error_response_body!(event_builder, response); + + let reason = match &response.errors { + Some(error_list) => error_list + .iter() + .map(|error| format!("{}: {}", error.parameter_name, error.reason_code)) + .collect::>() + .join(" & "), + None => response.title.clone(), + }; + + Ok(ErrorResponse { + status_code: res.status_code, + code: response.category, + message: response.title, + reason: Some(reason), + attempt_status: None, + connector_transaction_id: None, + network_advice_code: None, + network_decline_code: None, + network_error_message: None, + typed_connector_response: None, + raw_connector_response: None, + raw_connector_request: None, + typed_connector_request: None, + }) + } +} + +macros::create_all_prerequisites!( + connector_name: Moneris, + generic_type: T, + api: [ + ( + flow: ServerAuthenticationToken, + request_body: MonerisAuthRequest, + response_body: MonerisAuthResponse, + router_data: RouterDataV2, + ), + ( + flow: Authorize, + request_body: MonerisPaymentsRequest, + response_body: MonerisAuthorizeResponse, + router_data: RouterDataV2, PaymentsResponseData>, + ), + ( + flow: RepeatPayment, + request_body: MonerisRepeatPaymentRequest, + response_body: MonerisRepeatPaymentResponse, + router_data: RouterDataV2, PaymentsResponseData>, + ), + ( + flow: PSync, + response_body: MonerisPaymentSyncResponse, + router_data: RouterDataV2, + ), + ( + flow: Capture, + request_body: MonerisPaymentsCaptureRequest, + response_body: MonerisCaptureResponse, + router_data: RouterDataV2, + ), + ( + flow: Void, + request_body: MonerisCancelRequest, + response_body: MonerisPaymentVoidResponse, + router_data: RouterDataV2, + ), + ( + flow: Refund, + request_body: MonerisRefundRequest, + response_body: MonerisRefundResponse, + router_data: RouterDataV2, + ), + ( + flow: RSync, + response_body: MonerisRefundSyncResponse, + router_data: RouterDataV2, + ) + ], + amount_converters: [ + amount_converter: MinorUnit + ], + member_functions: { + pub fn build_headers( + &self, + access_token: &str, + connector_config: &ConnectorSpecificConfig, + ) -> CustomResult)>, IntegrationError> { + let auth = moneris::MonerisAuthType::try_from(connector_config)?; + let mut header = vec![ + ( + headers::CONTENT_TYPE.to_string(), + self.common_get_content_type().to_string().into(), + ), + ( + moneris::auth_headers::API_VERSION.to_string(), + moneris::auth_headers::API_VERSION_VALUE + .to_string() + .into(), + ), + ( + moneris::auth_headers::X_MERCHANT_ID.to_string(), + auth.merchant_id.expose().into_masked(), + ), + ]; + let auth_header = ( + headers::AUTHORIZATION.to_string(), + format!("Bearer {}", access_token).into_masked(), + ); + header.push(auth_header); + Ok(header) + } + + pub fn connector_base_url_payments<'a, F, Req, Res>( + &self, + req: &'a RouterDataV2, + ) -> &'a str { + &req.resource_common_data.connectors.moneris.base_url + } + pub fn connector_base_url_refunds<'a, F, Req, Res>( + &self, + req: &'a RouterDataV2, + ) -> &'a str { + &req.resource_common_data.connectors.moneris.base_url + } + pub fn connector_base_url_merchant_auth<'a, F, Req, Res>( + &self, + req: &'a RouterDataV2, + ) -> &'a str { + &req.resource_common_data.connectors.moneris.base_url + } + } +); + +macros::macro_connector_implementation!( + connector_default_implementations: [], + connector: Moneris, + curl_request: FormUrlEncoded(MonerisAuthRequest), + curl_response: MonerisAuthResponse, + flow_name: ServerAuthenticationToken, + resource_common_data: MerchantAuthenticationFlowData, + flow_request: ServerAuthenticationTokenRequestData, + flow_response: ServerAuthenticationTokenResponseData, + http_method: Post, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize], + other_functions: { + fn get_headers( + &self, + _req: &RouterDataV2, + ) -> CustomResult)>, IntegrationError> { + Ok(vec![( + headers::CONTENT_TYPE.to_string(), + "application/x-www-form-urlencoded".to_string().into(), + )]) + } + fn get_url( + &self, + req: &RouterDataV2, + ) -> CustomResult { + Ok(format!("{}/oauth2/token", self.connector_base_url_merchant_auth(req))) + } + fn get_error_response_v2( + &self, + res: Response, + event_builder: Option<&mut events::Event>, + _connector_config: &ConnectorSpecificConfig, + ) -> CustomResult { + // auth error have different structure than common error + let response: moneris::MonerisAuthErrorResponse = res + .response + .parse_struct("MonerisAuthErrorResponse") + .change_context(utils::response_handling_fail_for_connector( + res.status_code, + "moneris", + ))?; + with_error_response_body!(event_builder, response); + Ok(ErrorResponse { + status_code: res.status_code, + code: response.error.to_string(), + message: response.error.clone(), + reason: response.error_description, + attempt_status: None, + connector_transaction_id: None, + network_advice_code: None, + network_decline_code: None, + network_error_message: None, + typed_connector_response: None, + raw_connector_response: None, + raw_connector_request: None, + typed_connector_request: None, + }) + } + } +); + +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Moneris, + curl_request: Json(MonerisPaymentsRequest), + curl_response: MonerisAuthorizeResponse, + 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> { + let access_token = req.resource_common_data + .access_token + .clone() + .ok_or(IntegrationError::FailedToObtainAuthType { + context: IntegrationErrorContext { + suggested_action: Some( + "Ensure the OAuth access token is obtained via the \ + ServerAuthenticationToken flow and stored on the payment \ + resource_common_data before triggering Authorize." + .to_string(), + ), + doc_url: None, + additional_context: Some( + "Moneris Authorize requires an OAuth access token on \ + `resource_common_data.access_token`, but it was None." + .to_string(), + ), + }, + })?; + self.build_headers( + &access_token.access_token.expose(), + &req.connector_config, + ) + } + fn get_url( + &self, + req: &RouterDataV2, PaymentsResponseData>, + ) -> CustomResult { + Ok(format!("{}/payments", self.connector_base_url_payments(req))) + } + } +); + +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Moneris, + curl_request: Json(MonerisRepeatPaymentRequest), + curl_response: MonerisRepeatPaymentResponse, + flow_name: RepeatPayment, + resource_common_data: PaymentFlowData, + flow_request: RepeatPaymentData, + 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> { + let access_token = req.resource_common_data + .access_token + .clone() + .ok_or(IntegrationError::FailedToObtainAuthType { + context: IntegrationErrorContext { + suggested_action: Some( + "Ensure the OAuth access token is obtained via the \ + ServerAuthenticationToken flow and stored on the payment \ + resource_common_data before triggering repeat_payment." + .to_string(), + ), + doc_url: None, + additional_context: Some( + "Moneris repeat_payment requires an OAuth access token on \ + `resource_common_data.access_token`, but it was None." + .to_string(), + ), + }, + })?; + self.build_headers( + &access_token.access_token.expose(), + &req.connector_config, + ) + } + + fn get_url( + &self, + req: &RouterDataV2, PaymentsResponseData>, + ) -> CustomResult { + Ok(format!("{}/payments", self.connector_base_url_payments(req))) + } + } +); + +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Moneris, + curl_response: MonerisPaymentSyncResponse, + flow_name: PSync, + resource_common_data: PaymentFlowData, + flow_request: PaymentsSyncData, + flow_response: PaymentsResponseData, + http_method: Get, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize], + other_functions: { + fn get_headers( + &self, + req: &RouterDataV2, + ) -> CustomResult)>, IntegrationError> { + let access_token = req.resource_common_data + .access_token + .clone() + .ok_or(IntegrationError::FailedToObtainAuthType { + context: IntegrationErrorContext { + suggested_action: Some( + "Ensure the OAuth access token is obtained via the \ + ServerAuthenticationToken flow and stored on the payment \ + resource_common_data before triggering PSync." + .to_string(), + ), + doc_url: None, + additional_context: Some( + "Moneris PSync requires an OAuth access token on \ + `resource_common_data.access_token`, but it was None." + .to_string(), + ), + }, + })?; + self.build_headers( + &access_token.access_token.expose(), + &req.connector_config, + ) + } + fn get_url( + &self, + req: &RouterDataV2, + ) -> CustomResult { + let connector_payment_id = req.request.get_connector_transaction_id()?; + Ok(format!( + "{}/payments/{connector_payment_id}", + self.connector_base_url_payments(req) + )) + } + } +); + +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Moneris, + curl_request: Json(MonerisPaymentsCaptureRequest), + curl_response: MonerisCaptureResponse, + 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> { + let access_token = req.resource_common_data + .access_token + .clone() + .ok_or(IntegrationError::FailedToObtainAuthType { + context: IntegrationErrorContext { + suggested_action: Some( + "Ensure the OAuth access token is obtained via the \ + ServerAuthenticationToken flow and stored on the payment \ + resource_common_data before triggering capture." + .to_string(), + ), + doc_url: None, + additional_context: Some( + "Moneris capture requires an OAuth access token on \ + `resource_common_data.access_token`, but it was None." + .to_string(), + ), + }, + })?; + self.build_headers( + &access_token.access_token.expose(), + &req.connector_config, + ) + } + fn get_url( + &self, + req: &RouterDataV2, + ) -> CustomResult { + let connector_payment_id = req.request.get_connector_transaction_id()?; + Ok(format!( + "{}/payments/{connector_payment_id}/complete", + self.connector_base_url_payments(req) + )) + } + } +); + +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Moneris, + curl_request: Json(MonerisCancelRequest), + curl_response: MonerisPaymentVoidResponse, + 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> { + let access_token = req.resource_common_data + .access_token + .clone() + .ok_or(IntegrationError::FailedToObtainAuthType { + context: IntegrationErrorContext { + suggested_action: Some( + "Ensure the OAuth access token is obtained via the \ + ServerAuthenticationToken flow and stored on the payment \ + resource_common_data before triggering void." + .to_string(), + ), + doc_url: None, + additional_context: Some( + "Moneris void requires an OAuth access token on \ + `resource_common_data.access_token`, but it was None." + .to_string(), + ), + }, + })?; + self.build_headers( + &access_token.access_token.expose(), + &req.connector_config, + ) + } + fn get_url( + &self, + req: &RouterDataV2, + ) -> CustomResult { + let connector_payment_id = req.request.connector_transaction_id.clone(); + Ok(format!( + "{}/payments/{connector_payment_id}/cancel", + self.connector_base_url_payments(req) + )) + } + } +); + +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Moneris, + curl_request: Json(MonerisRefundRequest), + curl_response: MonerisRefundResponse, + 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> { + let access_token = req.resource_common_data + .access_token + .clone() + .ok_or(IntegrationError::FailedToObtainAuthType { + context: IntegrationErrorContext { + suggested_action: Some( + "Ensure the OAuth access token is obtained via the \ + ServerAuthenticationToken flow and stored on the payment \ + resource_common_data before triggering Refund." + .to_string(), + ), + doc_url: None, + additional_context: Some( + "Moneris refund requires an OAuth access token on \ + `resource_common_data.access_token`, but it was None." + .to_string(), + ), + }, + })?; + self.build_headers( + &access_token.access_token.expose(), + &req.connector_config, + ) + } + fn get_url( + &self, + req: &RouterDataV2, + ) -> CustomResult { + Ok(format!("{}/refunds", self.connector_base_url_refunds(req))) + } + } +); + +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Moneris, + curl_response: MonerisRefundSyncResponse, + flow_name: RSync, + resource_common_data: RefundFlowData, + flow_request: RefundSyncData, + flow_response: RefundsResponseData, + http_method: Get, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize], + other_functions: { + fn get_headers( + &self, + req: &RouterDataV2, + ) -> CustomResult)>, IntegrationError> { + let access_token = req.resource_common_data + .access_token + .clone() + .ok_or(IntegrationError::FailedToObtainAuthType { + context: IntegrationErrorContext { + suggested_action: Some( + "Ensure the OAuth access token is obtained via the \ + ServerAuthenticationToken flow and stored on the payment \ + resource_common_data before triggering RSync." + .to_string(), + ), + doc_url: None, + additional_context: Some( + "Moneris rsync requires an OAuth access token on \ + `resource_common_data.access_token`, but it was None." + .to_string(), + ), + }, + })?; + self.build_headers( + &access_token.access_token.expose(), + &req.connector_config, + ) + } + fn get_url( + &self, + req: &RouterDataV2, + ) -> CustomResult { + let refund_id = req.request.connector_refund_id.clone(); + Ok(format!("{}/refunds/{refund_id}", self.connector_base_url_refunds(req))) + } + } +); + +macros::macro_connector_flow_status_impls!( + connector: Moneris, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize], + not_implemented: [ + SetupMandate, + ], + not_supported: [ + ServerSessionAuthenticationToken, + VoidPostRefund, + IncrementalAuthorization, + Accept, + SubmitEvidence, + DefendDispute, + CreateOrder, + CreateConnectorCustomer, + GetConnectorCustomer, + PaymentMethodToken, + PreAuthenticate, + Authenticate, + PostAuthenticate, + ClientAuthenticationToken, + MandateRevoke, + VoidPC, + ], +); diff --git a/crates/integrations/connector-integration/src/connectors/moneris/transformers.rs b/crates/integrations/connector-integration/src/connectors/moneris/transformers.rs new file mode 100644 index 0000000000..d03259f75b --- /dev/null +++ b/crates/integrations/connector-integration/src/connectors/moneris/transformers.rs @@ -0,0 +1,865 @@ +use crate::types::ResponseRouterData; +use common_enums::RefundStatus; +use common_utils::types::MinorUnit; +use domain_types::{ + connector_flow::{ + Authorize, Capture, RSync, Refund, RepeatPayment, ServerAuthenticationToken, Void, + }, + connector_types::{ + MandateReference, PaymentFlowData, PaymentVoidData, PaymentsAuthorizeData, + PaymentsCaptureData, PaymentsResponseData, RefundFlowData, RefundSyncData, RefundsData, + RefundsResponseData, RepeatPaymentData, ResponseId, ServerAuthenticationTokenResponseData, + }, + errors::{ + ConnectorError, IntegrationError, IntegrationErrorContext, + ResponseTransformationErrorContext, + }, + merchant_authentication_flow_data::MerchantAuthenticationFlowData, + payment_method_data::{PaymentMethodData, PaymentMethodDataTypes, RawCardNumber}, + router_data::ConnectorSpecificConfig, + router_data_v2::RouterDataV2, +}; +use error_stack::{Report, ResultExt}; +use hyperswitch_masking::{PeekInterface, Secret}; +use serde::{Deserialize, Serialize}; + +use super::MonerisRouterData; + +pub const BASE64_ENGINE: base64::engine::GeneralPurpose = base64::engine::general_purpose::STANDARD; +const CLIENT_CREDENTIALS: &str = "client_credentials"; + +pub mod auth_headers { + pub const X_MERCHANT_ID: &str = "X-Merchant-Id"; + pub const API_VERSION: &str = "Api-Version"; + pub const API_VERSION_VALUE: &str = "2026-08-14"; +} + +pub struct MonerisAuthType { + pub(super) client_id: Secret, + pub(super) client_secret: Secret, + pub(super) merchant_id: Secret, +} + +impl TryFrom<&ConnectorSpecificConfig> for MonerisAuthType { + type Error = Report; + fn try_from(auth_type: &ConnectorSpecificConfig) -> Result { + match auth_type { + ConnectorSpecificConfig::Moneris { + client_secret, + merchant_id, + client_id, + .. + } => Ok(Self { + client_id: client_id.to_owned(), + client_secret: client_secret.to_owned(), + merchant_id: merchant_id.to_owned(), + }), + _ => Err(error_stack::report!( + IntegrationError::FailedToObtainAuthType { + context: IntegrationErrorContext { + suggested_action: Some( + "Configure this merchant account's Moneris connector with a \ + client_id, client_secret and merchant_id (ConnectorSpecificConfig::Moneris)." + .to_string(), + ), + doc_url: None, + additional_context: Some( + "The connector_config passed to MonerisAuthType::try_from was not \ + the ConnectorSpecificConfig::Moneris variant — either a different \ + connector's config was routed to Moneris, or Moneris's client_id/ \ + client_secret/merchant_id were never configured for this merchant account." + .to_string(), + ), + } + } + )), + } + } +} + +#[derive(Debug, Clone, Serialize, PartialEq)] +pub struct MonerisAuthRequest { + client_id: Secret, + client_secret: Secret, + grant_type: String, +} + +impl + TryFrom< + MonerisRouterData< + RouterDataV2< + ServerAuthenticationToken, + MerchantAuthenticationFlowData, + domain_types::connector_types::ServerAuthenticationTokenRequestData, + ServerAuthenticationTokenResponseData, + >, + T, + >, + > for MonerisAuthRequest +{ + type Error = Report; + fn try_from( + item: MonerisRouterData< + RouterDataV2< + ServerAuthenticationToken, + MerchantAuthenticationFlowData, + domain_types::connector_types::ServerAuthenticationTokenRequestData, + ServerAuthenticationTokenResponseData, + >, + T, + >, + ) -> Result { + let auth = MonerisAuthType::try_from(&item.router_data.connector_config)?; + Ok(Self { + client_id: auth.client_id.clone(), + client_secret: auth.client_secret.clone(), + grant_type: CLIENT_CREDENTIALS.to_string(), + }) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct MonerisAuthResponse { + access_token: Secret, + token_type: String, + expires_in: String, +} + +impl TryFrom> + for RouterDataV2 +{ + type Error = Report; + fn try_from(item: ResponseRouterData) -> Result { + Ok(Self { + response: Ok(ServerAuthenticationTokenResponseData { + access_token: item.response.access_token, + expires_in: Some(item.response.expires_in.parse::().change_context( + ConnectorError::ResponseDeserializationFailed { + context: ResponseTransformationErrorContext { + http_status_code: None, + additional_context: Some(format!( + "Failed to parse Moneris `expires_in` value as i64: {:?}", + item.response.expires_in + )), + }, + }, + )?), + token_type: Some(item.response.token_type), + }), + ..item.router_data + }) + } +} + +#[derive(Debug, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct MonerisPaymentsRequest< + T: PaymentMethodDataTypes + std::fmt::Debug + Sync + Send + 'static + Serialize, +> { + idempotency_key: String, + amount: Amount, + payment_method: PaymentMethod, + automatic_capture: bool, +} + +#[derive(Default, Debug, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Amount { + currency: common_enums::Currency, + amount: MinorUnit, +} + +#[derive(Debug, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +pub enum PaymentMethod< + T: PaymentMethodDataTypes + std::fmt::Debug + Sync + Send + 'static + Serialize, +> { + Card(PaymentMethodCard), + PaymentMethodId(PaymentMethodId), +} + +#[derive(Debug, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PaymentMethodCard< + T: PaymentMethodDataTypes + std::fmt::Debug + Sync + Send + 'static + Serialize, +> { + payment_method_source: PaymentMethodSource, + card: MonerisCard, + store_payment_method: StorePaymentMethod, +} + +#[derive(Debug, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PaymentMethodId { + payment_method_source: PaymentMethodSource, + payment_method_id: Secret, +} + +#[derive(Debug, Serialize, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PaymentMethodSource { + Card, + PaymentMethodId, +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct MonerisCard< + T: PaymentMethodDataTypes + std::fmt::Debug + Sync + Send + 'static + Serialize, +> { + card_number: RawCardNumber, + expiry_month: Secret, + expiry_year: Secret, + card_security_code: Secret, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum StorePaymentMethod { + DoNotStore, + CardholderInitiated, + MerchantInitiated, +} + +impl + TryFrom< + MonerisRouterData< + RouterDataV2< + Authorize, + PaymentFlowData, + PaymentsAuthorizeData, + PaymentsResponseData, + >, + T, + >, + > for MonerisPaymentsRequest +{ + type Error = Report; + fn try_from( + item: MonerisRouterData< + RouterDataV2< + Authorize, + PaymentFlowData, + PaymentsAuthorizeData, + PaymentsResponseData, + >, + T, + >, + ) -> Result { + match item.router_data.request.payment_method_data.clone() { + PaymentMethodData::Card(ref req_card) => { + if item.router_data.resource_common_data.is_three_ds() { + Err(IntegrationError::NotSupported { + message: "Card 3DS".to_string(), + connector: "Moneris", + context: IntegrationErrorContext { + suggested_action: Some( + "Disable 3DS for this payment or use a Moneris integration \ + that supports 3DS authentication." + .to_string(), + ), + doc_url: None, + additional_context: Some( + "Moneris Authorize was called with three_ds enabled on \ + `resource_common_data`, but this connector implementation \ + does not support 3DS-authenticated card transactions." + .to_string(), + ), + }, + })? + }; + let idempotency_key = format!( + "auth_{}", + item.router_data + .resource_common_data + .connector_request_reference_id + ); + let amount = Amount { + currency: item.router_data.request.currency, + amount: item.router_data.request.amount, + }; + let payment_method = PaymentMethod::Card(PaymentMethodCard { + payment_method_source: PaymentMethodSource::Card, + card: MonerisCard { + card_number: req_card.card_number.clone(), + expiry_month: Secret::new( + req_card + .card_exp_month + .peek() + .parse::() + .change_context(IntegrationError::InvalidDataFormat { + field_name: "card_exp_month", + context: IntegrationErrorContext { + suggested_action: Some( + "Pass card_exp_month as a 1- or 2-digit numeric \ + string (e.g. \"1\" or \"12\")." + .to_string(), + ), + doc_url: None, + additional_context: Some(format!( + "Failed to parse card expiry month as i64; \ + received value: {:?}.", + req_card.card_exp_month.peek() + )), + }, + })?, + ), + expiry_year: Secret::new( + req_card + .get_expiry_year_4_digit() + .peek() + .parse::() + .change_context(IntegrationError::InvalidDataFormat { + field_name: "card_exp_year", + context: IntegrationErrorContext { + suggested_action: Some( + "Pass card_exp_year as a 4-digit numeric string \ + (e.g. \"2030\")." + .to_string(), + ), + doc_url: None, + additional_context: Some(format!( + "Failed to parse card expiry year as i64; \ + received value: {:?}.", + req_card.get_expiry_year_4_digit().peek() + )), + }, + })?, + ), + card_security_code: req_card.card_cvc.clone(), + }, + store_payment_method: if item + .router_data + .request + .is_customer_initiated_mandate_payment() + { + StorePaymentMethod::CardholderInitiated + } else { + StorePaymentMethod::DoNotStore + }, + }); + let automatic_capture = item.router_data.request.is_auto_capture(); + + Ok(Self { + idempotency_key, + amount, + payment_method, + automatic_capture, + }) + } + _ => Err(IntegrationError::NotImplemented( + "Only card payments are currently implemented for Moneris".to_string(), + IntegrationErrorContext { + suggested_action: Some( + "Use a card payment method, the only method currently implemented \ + for Moneris authorize." + .to_string(), + ), + doc_url: None, + additional_context: Some(format!( + "Moneris authorize received a non-card payment method: {:?}.", + item.router_data.request.payment_method_data + )), + }, + ) + .into()), + } + } +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct MonerisAuthorizeResponse { + payment_status: MonerisPaymentStatus, + payment_id: String, + payment_method: MonerisPaymentMethodData, +} + +impl + TryFrom> + for RouterDataV2, PaymentsResponseData> +{ + type Error = Report; + fn try_from( + item: ResponseRouterData, + ) -> Result { + let mandate_reference = if item + .router_data + .request + .is_customer_initiated_mandate_payment() + { + Some(Box::new(MandateReference { + connector_mandate_id: Some( + item.response + .payment_method + .payment_method_id + .peek() + .to_string(), + ), + payment_method_id: None, + mandate_metadata: None, + connector_mandate_request_reference_id: None, + })) + } else { + None + }; + Ok(Self { + resource_common_data: PaymentFlowData { + status: common_enums::AttemptStatus::from(item.response.payment_status), + ..item.router_data.resource_common_data + }, + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(item.response.payment_id.clone()), + redirection_data: None, + mandate_reference, + connector_metadata: None, + network_txn_id: None, + network_txn_link_id: None, + connector_response_reference_id: Some(item.response.payment_id), + incremental_authorization_allowed: None, + splits: None, + status_code: item.http_code, + }), + ..item.router_data + }) + } +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct MonerisPaymentsResponse { + payment_status: MonerisPaymentStatus, + payment_id: String, + payment_method: MonerisPaymentMethodData, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum MonerisPaymentStatus { + Succeeded, + #[default] + Processing, + Canceled, + Declined, + DeclinedRetry, + Authorized, +} + +impl From for common_enums::AttemptStatus { + fn from(item: MonerisPaymentStatus) -> Self { + match item { + MonerisPaymentStatus::Succeeded => Self::Charged, + MonerisPaymentStatus::Authorized => Self::Authorized, + MonerisPaymentStatus::Canceled => Self::Voided, + MonerisPaymentStatus::Declined | MonerisPaymentStatus::DeclinedRetry => Self::Failure, + MonerisPaymentStatus::Processing => Self::Pending, + } + } +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct MonerisPaymentMethodData { + payment_method_id: Secret, +} + +impl TryFrom> + for RouterDataV2 +{ + type Error = Report; + fn try_from( + item: ResponseRouterData, + ) -> Result { + Ok(Self { + resource_common_data: PaymentFlowData { + status: common_enums::AttemptStatus::from(item.response.payment_status), + ..item.router_data.resource_common_data + }, + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(item.response.payment_id.clone()), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + network_txn_link_id: None, + connector_response_reference_id: Some(item.response.payment_id), + incremental_authorization_allowed: None, + splits: None, + status_code: item.http_code, + }), + ..item.router_data + }) + } +} + +// Moneris does not return a distinct status for partial capture — a successful completion +// always comes back as SUCCEEDED regardless of whether the captured amount is less than +// the authorized amount. This dedicated response type carries out the amount comparison +// that the generic MonerisPaymentsResponse impl cannot perform. +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct MonerisCaptureResponse { + payment_status: MonerisPaymentStatus, + payment_id: String, + payment_method: MonerisPaymentMethodData, +} + +impl TryFrom> + for RouterDataV2 +{ + type Error = Report; + fn try_from( + item: ResponseRouterData, + ) -> Result { + let status = match item.response.payment_status { + MonerisPaymentStatus::Succeeded => { + let captured = item.router_data.request.minor_amount_to_capture; + match item + .router_data + .resource_common_data + .minor_amount_capturable + { + Some(authorized) if captured < authorized => { + common_enums::AttemptStatus::PartialCharged + } + _ => common_enums::AttemptStatus::Charged, + } + } + other => common_enums::AttemptStatus::from(other), + }; + Ok(Self { + resource_common_data: PaymentFlowData { + status, + ..item.router_data.resource_common_data + }, + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(item.response.payment_id.clone()), + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + network_txn_link_id: None, + connector_response_reference_id: Some(item.response.payment_id), + incremental_authorization_allowed: None, + splits: None, + status_code: item.http_code, + }), + ..item.router_data + }) + } +} + +#[derive(Debug, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct MonerisRepeatPaymentRequest< + T: PaymentMethodDataTypes + std::fmt::Debug + Sync + Send + 'static + Serialize, +> { + idempotency_key: String, + amount: Amount, + payment_method: PaymentMethod, + automatic_capture: bool, +} + +impl + TryFrom< + MonerisRouterData< + RouterDataV2< + RepeatPayment, + PaymentFlowData, + RepeatPaymentData, + PaymentsResponseData, + >, + T, + >, + > for MonerisRepeatPaymentRequest +{ + type Error = Report; + fn try_from( + item: MonerisRouterData< + RouterDataV2< + RepeatPayment, + PaymentFlowData, + RepeatPaymentData, + PaymentsResponseData, + >, + T, + >, + ) -> Result { + match item.router_data.request.payment_method_data.clone() { + PaymentMethodData::MandatePayment => { + let idempotency_key = format!( + "repeat_{}", + item.router_data + .resource_common_data + .connector_request_reference_id + ); + let amount = Amount { + currency: item.router_data.request.currency, + amount: item.router_data.request.minor_amount, + }; + let automatic_capture = item.router_data.request.is_auto_capture(); + let payment_method = PaymentMethod::PaymentMethodId(PaymentMethodId { + payment_method_source: PaymentMethodSource::PaymentMethodId, + payment_method_id: item + .router_data + .request + .connector_mandate_id() + .ok_or(IntegrationError::MissingRequiredField { + field_name: "connector_mandate_id", + context: IntegrationErrorContext { + suggested_action: Some( + "Provide `connector_mandate_id` in the RepeatPayment \ + request — this is the mandate reference returned by \ + Moneris during the initial customer-initiated mandate \ + setup (Authorize with `setup_future_usage`)." + .to_string(), + ), + doc_url: None, + additional_context: Some( + "RepeatPayment calls Moneris's stored-credential \ + endpoint which requires a `payment_method_id` derived \ + from `connector_mandate_id`, but the field was None." + .to_string(), + ), + }, + })? + .into(), + }); + Ok(Self { + idempotency_key, + amount, + payment_method, + automatic_capture, + }) + } + _ => Err(IntegrationError::NotImplemented( + "Only mandate (stored-credential) payments are currently implemented for \ + Moneris repeat_payment" + .to_string(), + IntegrationErrorContext { + suggested_action: Some( + "Use `payment_method_data = MandatePayment` together with a valid \ + `connector_mandate_id`; Moneris repeat_payment is a merchant-initiated \ + transaction that charges a previously stored credential." + .to_string(), + ), + doc_url: None, + additional_context: Some(format!( + "Moneris repeat_payment received a non-mandate payment method \ + variant: {:?}.", + item.router_data.request.payment_method_data + )), + }, + ) + .into()), + } + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MonerisPaymentsCaptureRequest { + amount: Amount, + idempotency_key: String, +} + +impl + TryFrom< + MonerisRouterData< + RouterDataV2, + T, + >, + > for MonerisPaymentsCaptureRequest +{ + type Error = Report; + fn try_from( + item: MonerisRouterData< + RouterDataV2, + T, + >, + ) -> Result { + let amount = Amount { + currency: item.router_data.request.currency, + amount: item.router_data.request.minor_amount_to_capture, + }; + let idempotency_key = format!( + "capture_{}", + item.router_data + .resource_common_data + .connector_request_reference_id + ); + Ok(Self { + amount, + idempotency_key, + }) + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MonerisCancelRequest { + idempotency_key: String, + reason: Option, +} + +impl + TryFrom< + MonerisRouterData< + RouterDataV2, + T, + >, + > for MonerisCancelRequest +{ + type Error = Report; + fn try_from( + item: MonerisRouterData< + RouterDataV2, + T, + >, + ) -> Result { + let idempotency_key = format!( + "void_{}", + item.router_data + .resource_common_data + .connector_request_reference_id + ); + let reason = item.router_data.request.cancellation_reason.clone(); + Ok(Self { + idempotency_key, + reason, + }) + } +} + +#[derive(Default, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MonerisRefundRequest { + pub refund_amount: Amount, + pub idempotency_key: String, + pub reason: Option, + pub payment_id: String, +} + +impl + TryFrom< + MonerisRouterData< + RouterDataV2, + T, + >, + > for MonerisRefundRequest +{ + type Error = Report; + fn try_from( + item: MonerisRouterData< + RouterDataV2, + T, + >, + ) -> Result { + let refund_amount = Amount { + currency: item.router_data.request.currency, + amount: item.router_data.request.minor_refund_amount, + }; + let idempotency_key = format!( + "refund_{}", + item.router_data + .resource_common_data + .connector_request_reference_id + ); + let reason = item.router_data.request.reason.clone(); + let payment_id = item.router_data.request.connector_transaction_id.clone(); + Ok(Self { + refund_amount, + idempotency_key, + reason, + payment_id, + }) + } +} + +#[derive(Debug, Serialize, Default, Deserialize, Clone)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum MonerisRefundStatus { + Succeeded, + #[default] + Processing, + Declined, + DeclinedRetry, +} + +impl From for RefundStatus { + fn from(item: MonerisRefundStatus) -> Self { + match item { + MonerisRefundStatus::Succeeded => Self::Success, + MonerisRefundStatus::Declined | MonerisRefundStatus::DeclinedRetry => Self::Failure, + MonerisRefundStatus::Processing => Self::Pending, + } + } +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MonerisRefundResponse { + refund_id: String, + refund_status: MonerisRefundStatus, +} + +impl TryFrom> + for RouterDataV2 +{ + type Error = Report; + fn try_from( + item: ResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(RefundsResponseData { + connector_refund_id: item.response.refund_id.to_string(), + refund_status: RefundStatus::from(item.response.refund_status), + acquirer_reference_number: None, + status_code: item.http_code, + }), + ..item.router_data + }) + } +} + +impl TryFrom> + for RouterDataV2 +{ + type Error = Report; + fn try_from( + item: ResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(RefundsResponseData { + connector_refund_id: item.response.refund_id.to_string(), + refund_status: RefundStatus::from(item.response.refund_status), + acquirer_reference_number: None, + status_code: item.http_code, + }), + ..item.router_data + }) + } +} + +#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct MonerisErrorResponse { + pub status: u16, + pub category: String, + pub title: String, + pub errors: Option>, +} + +#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct MonerisError { + pub reason_code: String, + pub parameter_name: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct MonerisAuthErrorResponse { + pub error: String, + pub error_description: Option, +} diff --git a/crates/integrations/connector-integration/src/connectors/multisafepay/transformers.rs b/crates/integrations/connector-integration/src/connectors/multisafepay/transformers.rs index d3e7e46c05..26b5869666 100644 --- a/crates/integrations/connector-integration/src/connectors/multisafepay/transformers.rs +++ b/crates/integrations/connector-integration/src/connectors/multisafepay/transformers.rs @@ -152,7 +152,7 @@ fn get_order_type_from_payment_method( | BankRedirectData::OnlineBankingFpx { .. } | BankRedirectData::OnlineBankingThailand { .. } | BankRedirectData::LocalBankRedirect {} - | BankRedirectData::OpenBanking {} + | BankRedirectData::OpenBanking { .. } | BankRedirectData::Netbanking { .. } => Err(IntegrationError::NotImplemented( crate::utils::get_unimplemented_payment_method_error_message("multisafepay"), Default::default(), @@ -283,7 +283,7 @@ fn get_gateway_from_payment_method( | BankRedirectData::OnlineBankingFpx { .. } | BankRedirectData::OnlineBankingThailand { .. } | BankRedirectData::LocalBankRedirect {} - | BankRedirectData::OpenBanking {} + | BankRedirectData::OpenBanking { .. } | BankRedirectData::Netbanking { .. } => Err(IntegrationError::NotImplemented( crate::utils::get_unimplemented_payment_method_error_message("multisafepay"), Default::default(), diff --git a/crates/integrations/connector-integration/src/connectors/nexinets/transformers.rs b/crates/integrations/connector-integration/src/connectors/nexinets/transformers.rs index b843168823..37c7505588 100644 --- a/crates/integrations/connector-integration/src/connectors/nexinets/transformers.rs +++ b/crates/integrations/connector-integration/src/connectors/nexinets/transformers.rs @@ -751,7 +751,7 @@ fn get_payment_details_and_product< | BankRedirectData::OnlineBankingFpx { .. } | BankRedirectData::OnlineBankingThailand { .. } | BankRedirectData::LocalBankRedirect {} - | BankRedirectData::OpenBanking {} + | BankRedirectData::OpenBanking { .. } | BankRedirectData::Netbanking { .. } => Err(IntegrationError::NotImplemented( utils::get_unimplemented_payment_method_error_message("nexinets"), Default::default(), diff --git a/crates/integrations/connector-integration/src/connectors/paypal/transformers.rs b/crates/integrations/connector-integration/src/connectors/paypal/transformers.rs index ceb3556d7d..3406363089 100644 --- a/crates/integrations/connector-integration/src/connectors/paypal/transformers.rs +++ b/crates/integrations/connector-integration/src/connectors/paypal/transformers.rs @@ -1284,7 +1284,7 @@ fn get_payment_source< | BankRedirectData::OnlineBankingFpx { .. } | BankRedirectData::OnlineBankingThailand { .. } | BankRedirectData::LocalBankRedirect {} - | BankRedirectData::OpenBanking {} + | BankRedirectData::OpenBanking { .. } | BankRedirectData::Netbanking { .. } => Err(IntegrationError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Paypal"), Default::default(), diff --git a/crates/integrations/connector-integration/src/connectors/qwikcilver.rs b/crates/integrations/connector-integration/src/connectors/qwikcilver.rs index 47825baab1..cc1a04b522 100644 --- a/crates/integrations/connector-integration/src/connectors/qwikcilver.rs +++ b/crates/integrations/connector-integration/src/connectors/qwikcilver.rs @@ -12,12 +12,13 @@ use common_utils::{ }; use domain_types::{ connector_flow::{ - Authorize, CreatePaymentMethod, GetPaymentMethod, Recharge, Refund, - ServerAuthenticationToken, + Authorize, CreatePaymentMethod, GetPaymentMethod, PaymentMethodEligibility, Recharge, + Refund, ServerAuthenticationToken, }, connector_types::{ CreatePaymentMethodData, CreatePaymentMethodResponseData, GetPaymentMethodData, - GetPaymentMethodResponseData, PaymentFlowData, PaymentsAuthorizeData, PaymentsResponseData, + GetPaymentMethodResponseData, PaymentFlowData, PaymentMethodEligibilityData, + PaymentMethodEligibilityResponse, PaymentsAuthorizeData, PaymentsResponseData, RechargeRequestData, RechargeResponseData, RefundFlowData, RefundsData, RefundsResponseData, ServerAuthenticationTokenRequestData, ServerAuthenticationTokenResponseData, @@ -40,9 +41,10 @@ use serde::Serialize; use transformers::{ self as qwikcilver, QwikcilverAuthType, QwikcilverAuthorizeRequest, QwikcilverAuthorizeResponse, QwikcilverCancelRedeemBody, QwikcilverCancelRedeemResponse, - QwikcilverCreateWalletRequest, QwikcilverEmptyBody, QwikcilverErrorResponse, - QwikcilverGetWalletResponse, QwikcilverRechargeRequest, QwikcilverRechargeResponse, - QwikcilverRedeemRequest, QwikcilverRedeemResponse, QwikcilverWalletEnvelope, + QwikcilverCreateWalletRequest, QwikcilverEligibilityResponse, QwikcilverEmptyBody, + QwikcilverErrorResponse, QwikcilverGetWalletResponse, QwikcilverRechargeRequest, + QwikcilverRechargeResponse, QwikcilverRedeemRequest, QwikcilverRedeemResponse, + QwikcilverWalletEnvelope, }; use super::macros; @@ -79,6 +81,10 @@ impl connector_types::GetPaymentMethodV2 for Qwikcilver { } +impl + connector_types::PaymentMethodEligibilityV2 for Qwikcilver +{ +} impl connector_types::ServerAuthentication for Qwikcilver { @@ -155,6 +161,11 @@ macros::create_all_prerequisites!( request_body: QwikcilverEmptyBody, response_body: QwikcilverGetWalletResponse, router_data: RouterDataV2, + ), + ( + flow: PaymentMethodEligibility, + response_body: QwikcilverEligibilityResponse, + router_data: RouterDataV2, ) ], amount_converters: [ @@ -233,6 +244,44 @@ macros::create_all_prerequisites!( ) -> &'a str { &req.resource_common_data.connectors.qwikcilver.base_url } + + /// Shared wallet lookup used by both `GetPaymentMethod` and + /// `PaymentMethodEligibility`, which make the identical connector call. + /// Primary: wallet number → `/wallet/{wn}`. + /// Fallback: customer phone → `/wallet/customer?phonenumber={phone}` (Pine Labs's + /// documented by-external-id lookup; response envelope is identical to the + /// by-wallet-number variant). + pub fn wallet_lookup_url( + &self, + base: &str, + wallet_number: Option<&str>, + phone: Option<&hyperswitch_masking::Secret>, + ) -> CustomResult { + if let Some(wallet_number) = wallet_number { + return Ok(format!( + "{base}Qwikcilver/eGMS.RestApi/api/v2/wallet/{}", + urlencoding::encode(wallet_number), + )); + } + if let Some(phone) = phone { + return Ok(format!( + "{base}Qwikcilver/eGMS.RestApi/api/v2/wallet/customer?phonenumber={}", + urlencoding::encode(phone.peek()), + )); + } + Err(IntegrationError::MissingRequiredField { + field_name: "connector_payment_method_id | customer.phone_number", + context: qwikcilver::qc_err_ctx( + "Qwikcilver's wallet lookup accepts either the wallet number (preferred) or \ + the customer's phone number as a fallback. Neither was supplied, so \ + there's no way to identify which wallet to fetch.", + "Set `connector_payment_method_id` to the wallet number returned by a \ + prior Create, OR set `customer.phone_number` to look up by Pine Labs's \ + external wallet id (the customer's mobile).", + ), + } + .into()) + } } ); @@ -545,39 +594,14 @@ macros::macro_connector_implementation!( &self, req: &RouterDataV2, ) -> CustomResult { - // Primary: wallet number → `/wallet/{wn}`. - // Fallback: customer phone → `/wallet/customer?phonenumber={phone}` (Pine Labs's documented - // by-external-id lookup; response envelope is identical to the by-wallet-number variant). - let base = self.connector_base_url_payments(req); - if let Some(wallet_number) = req.request.connector_payment_method_id.as_deref() { - return Ok(format!( - "{base}Qwikcilver/eGMS.RestApi/api/v2/wallet/{}", - urlencoding::encode(wallet_number), - )); - } - if let Some(phone) = req - .request - .customer - .as_ref() - .and_then(|c| c.customer_phone_number.as_ref()) - { - return Ok(format!( - "{base}Qwikcilver/eGMS.RestApi/api/v2/wallet/customer?phonenumber={}", - urlencoding::encode(phone.peek()), - )); - } - Err(IntegrationError::MissingRequiredField { - field_name: "connector_payment_method_id | customer.phone_number", - context: qwikcilver::qc_err_ctx( - "Qwikcilver Get accepts either the wallet number (preferred) or the \ - customer's phone number as a fallback. Neither was supplied, so there's \ - no way to identify which wallet to fetch.", - "Set `connector_payment_method_id` to the wallet number returned by a \ - prior Create, OR set `customer.phone_number` to look up by Pine Labs's \ - external wallet id (the customer's mobile).", - ), - } - .into()) + self.wallet_lookup_url( + self.connector_base_url_payments(req), + req.request.connector_payment_method_id.as_deref(), + req.request + .customer + .as_ref() + .and_then(|c| c.customer_phone_number.as_ref()), + ) } fn get_headers( @@ -594,6 +618,48 @@ macros::macro_connector_implementation!( } ); +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Qwikcilver, + curl_response: QwikcilverEligibilityResponse, + flow_name: PaymentMethodEligibility, + resource_common_data: PaymentFlowData, + flow_request: PaymentMethodEligibilityData, + flow_response: PaymentMethodEligibilityResponse, + http_method: Get, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize], + other_functions: { + fn get_url( + &self, + req: &RouterDataV2, + ) -> CustomResult { + // Performs the exact same connector call as `GetPaymentMethod` — eligibility for + // a Qwikcilver wallet is determined from the same wallet lookup response. + self.wallet_lookup_url( + self.connector_base_url_payments(req), + req.request.connector_payment_method_id.as_deref(), + req.request + .customer + .as_ref() + .and_then(|c| c.customer_phone_number.as_ref()), + ) + } + + fn get_headers( + &self, + req: &RouterDataV2, + ) -> CustomResult)>, IntegrationError> { + let token = self.extract_access_token(req.resource_common_data.access_token.as_ref())?; + let date = qwikcilver::current_datetime_qwikcilver(); + let txn_id = qwikcilver::derive_transaction_id_from_reference( + &req.resource_common_data.connector_request_reference_id, + ); + self.build_authenticated_headers(req, &token, &date, txn_id) + } + } +); + macros::macro_connector_flow_status_impls!( connector: Qwikcilver, generic_type: T, diff --git a/crates/integrations/connector-integration/src/connectors/qwikcilver/transformers.rs b/crates/integrations/connector-integration/src/connectors/qwikcilver/transformers.rs index b0611b370b..6d5c03be65 100644 --- a/crates/integrations/connector-integration/src/connectors/qwikcilver/transformers.rs +++ b/crates/integrations/connector-integration/src/connectors/qwikcilver/transformers.rs @@ -2,12 +2,13 @@ use common_enums::{AttemptStatus, RechargeStatus, RefundStatus}; use common_utils::types::FloatMajorUnit; use domain_types::{ connector_flow::{ - Authorize, CreatePaymentMethod, GetPaymentMethod, Recharge, Refund, - ServerAuthenticationToken, + Authorize, CreatePaymentMethod, GetPaymentMethod, PaymentMethodEligibility, Recharge, + Refund, ServerAuthenticationToken, }, connector_types::{ CreatePaymentMethodData, CreatePaymentMethodResponseData, CustomerInfo, - GetPaymentMethodData, GetPaymentMethodResponseData, PaymentFlowData, PaymentsAuthorizeData, + GetPaymentMethodData, GetPaymentMethodResponseData, PaymentFlowData, + PaymentMethodEligibilityData, PaymentMethodEligibilityResponse, PaymentsAuthorizeData, PaymentsResponseData, RechargeRequestData, RechargeResponseData, RefundFlowData, RefundsData, RefundsResponseData, ResponseId, ServerAuthenticationTokenRequestData, ServerAuthenticationTokenResponseData, @@ -948,6 +949,14 @@ where #[serde(transparent)] pub struct QwikcilverGetWalletResponse(pub QwikcilverWalletEnvelope); +/// Distinct response newtype for `PaymentMethodEligibility`. Wraps the identical +/// `QwikcilverWalletEnvelope` payload `GetPaymentMethod` parses — macro-generated templating +/// types are keyed by response type name, so this flow needs its own type to avoid colliding +/// with `GetPaymentMethod`'s templating impl, even though it's the same connector call. +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(transparent)] +pub struct QwikcilverEligibilityResponse(pub QwikcilverWalletEnvelope); + #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "PascalCase")] pub struct QwikcilverWalletDetails { @@ -1204,6 +1213,73 @@ impl TryFrom> } } +/// Performs the exact same wallet lookup as `GetPaymentMethod` and derives eligibility from +/// the wallet's status: ACTIVE → Eligible, INACTIVE → Ineligible. The resolved wallet's +/// payment method details (balance, items, etc.) are returned alongside the eligibility +/// verdict in the same response. +impl TryFrom> + for RouterDataV2< + PaymentMethodEligibility, + PaymentFlowData, + PaymentMethodEligibilityData, + PaymentMethodEligibilityResponse, + > +{ + type Error = error_stack::Report; + + fn try_from( + item: ResponseRouterData, + ) -> Result { + let mut data = item.router_data; + let body = item.response.0; + data.resource_common_data.raw_connector_response = + serde_json::to_string(&body).ok().map(Secret::new); + data.response = match body.response_code { + QWIKCILVER_SUCCESS_CODE => { + let currency = data.request.amount.currency; + let (eligibility, payment_method_details) = + if let Some(wallet) = body.wallet.as_ref() { + let eligibility = match map_wallet_status(wallet.status.as_ref()) { + Some(common_enums::WalletStatus::Active) => { + common_enums::EligibilityStatus::Eligible + } + Some(common_enums::WalletStatus::Inactive) => { + common_enums::EligibilityStatus::Ineligible + } + Some(common_enums::WalletStatus::Unspecified) | None => { + common_enums::EligibilityStatus::Unknown + } + }; + ( + eligibility, + Some(wallet_details_to_payment_method_details( + wallet, + Some(currency), + )), + ) + } else { + (common_enums::EligibilityStatus::Unknown, None) + }; + Ok(PaymentMethodEligibilityResponse { + eligibility, + payment_method_details, + status_code: u32::from(item.http_code), + }) + } + _ => { + let txn_id = body.transaction_id.map(|t| t.to_string()); + Err(error_response_from_qc( + (&body).into(), + txn_id, + item.http_code, + None, + )) + } + }; + Ok(data) + } +} + #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "PascalCase")] pub struct QwikcilverErrorResponse { diff --git a/crates/integrations/connector-integration/src/connectors/razorpay/test.rs b/crates/integrations/connector-integration/src/connectors/razorpay/test.rs index ae6db43384..3484b64795 100644 --- a/crates/integrations/connector-integration/src/connectors/razorpay/test.rs +++ b/crates/integrations/connector-integration/src/connectors/razorpay/test.rs @@ -120,6 +120,7 @@ mod tests { merchant_request_id: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, typed_connector_response: None, }, @@ -328,6 +329,7 @@ mod tests { amount: None, l2_l3_data: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, merchant_request_id: None, typed_connector_response: None, @@ -488,6 +490,7 @@ mod tests { amount: None, l2_l3_data: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, merchant_request_id: None, typed_connector_response: None, @@ -671,6 +674,7 @@ mod tests { l2_l3_data: None, merchant_request_id: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, typed_connector_response: None, }, @@ -1054,6 +1058,7 @@ mod tests { minor_amount_capturable: None, l2_l3_data: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, merchant_request_id: None, typed_connector_response: None, @@ -1271,6 +1276,7 @@ mod tests { l2_l3_data: None, merchant_request_id: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, typed_connector_response: None, }, @@ -1488,6 +1494,7 @@ mod tests { order_details: None, l2_l3_data: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, merchant_request_id: None, typed_connector_response: None, @@ -1620,6 +1627,7 @@ mod tests { l2_l3_data: None, merchant_request_id: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, typed_connector_response: None, }, @@ -1753,6 +1761,7 @@ mod tests { order_details: None, l2_l3_data: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, merchant_request_id: None, typed_connector_response: None, @@ -1935,6 +1944,7 @@ mod tests { l2_l3_data: None, merchant_request_id: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, typed_connector_response: None, }, @@ -2077,6 +2087,7 @@ mod tests { order_details: None, l2_l3_data: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, merchant_request_id: None, typed_connector_response: None, @@ -2209,6 +2220,7 @@ mod tests { order_details: None, l2_l3_data: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, merchant_request_id: None, typed_connector_response: None, diff --git a/crates/integrations/connector-integration/src/connectors/stripe/transformers.rs b/crates/integrations/connector-integration/src/connectors/stripe/transformers.rs index 1429d2e761..78ed3ff32e 100644 --- a/crates/integrations/connector-integration/src/connectors/stripe/transformers.rs +++ b/crates/integrations/connector-integration/src/connectors/stripe/transformers.rs @@ -1231,7 +1231,7 @@ impl TryFrom<&BankRedirectData> for StripePaymentMethodType { | BankRedirectData::OpenBankingUk { .. } | BankRedirectData::Trustly { .. } | BankRedirectData::LocalBankRedirect {} - | BankRedirectData::OpenBanking {} + | BankRedirectData::OpenBanking { .. } | BankRedirectData::Netbanking { .. } => Err(IntegrationError::NotImplemented( get_unimplemented_payment_method_error_message("stripe"), Default::default(), @@ -1869,7 +1869,7 @@ impl | BankRedirectData::Sofort { .. } | BankRedirectData::Trustly { .. } | BankRedirectData::LocalBankRedirect {} - | BankRedirectData::OpenBanking {} + | BankRedirectData::OpenBanking { .. } | BankRedirectData::Netbanking { .. } => Err(IntegrationError::NotImplemented( get_unimplemented_payment_method_error_message("stripe"), Default::default(), diff --git a/crates/integrations/connector-integration/src/connectors/tamara/transformers.rs b/crates/integrations/connector-integration/src/connectors/tamara/transformers.rs index 7b47eb3903..3481f8c513 100644 --- a/crates/integrations/connector-integration/src/connectors/tamara/transformers.rs +++ b/crates/integrations/connector-integration/src/connectors/tamara/transformers.rs @@ -974,6 +974,7 @@ impl TryFrom> Ok(Self { response: Ok(PaymentMethodEligibilityResponse { eligibility, + payment_method_details: None, status_code: u32::from(item.http_code), }), ..item.router_data.clone() diff --git a/crates/integrations/connector-integration/src/connectors/truelayer/transformers.rs b/crates/integrations/connector-integration/src/connectors/truelayer/transformers.rs index 2d96ba23ff..e62af2e782 100644 --- a/crates/integrations/connector-integration/src/connectors/truelayer/transformers.rs +++ b/crates/integrations/connector-integration/src/connectors/truelayer/transformers.rs @@ -12,7 +12,9 @@ use domain_types::{ VerifyWebhookSourceFlowData, }, merchant_authentication_flow_data::MerchantAuthenticationFlowData, - payment_method_data::{BankRedirectData, PaymentMethodData, PaymentMethodDataTypes}, + payment_method_data::{ + BankRedirectData, DefaultPCIHolder, PaymentMethodData, PaymentMethodDataTypes, + }, router_data::{ConnectorSpecificConfig, ErrorResponse, FlowStatus}, router_data_v2::RouterDataV2, router_request_types::VerifyWebhookSourceRequestData, @@ -21,7 +23,7 @@ use domain_types::{ utils::is_payment_failure, }; use error_stack::ResultExt; -use hyperswitch_masking::Secret; +use hyperswitch_masking::{PeekInterface, Secret}; use openssl::{ bn::{BigNum, BigNumContext}, ec::{EcGroup, EcKey, EcPoint}, @@ -31,6 +33,7 @@ use openssl::{ pkey::Public, }; use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; use std::collections::HashMap; use crate::{connectors::truelayer::TruelayerRouterData, types::ResponseRouterData, utils}; @@ -41,6 +44,9 @@ const SCOPE: &str = "payments"; const SIG_BYTES_EXPECTED_LENGTH: usize = 132; const P521_COORDINATE_BYTE_LEN: usize = 66; const PREFIX: &str = "/api"; +const SCHEME_SELECTION_TYPE: &str = "instant_preferred"; +const PAYMENT_METHOD_TYPE: &str = "bank_transfer"; +const BENEFICIARY_TYPE: &str = "merchant_account"; pub struct TruelayerAuthType { pub(super) client_id: Secret, @@ -241,12 +247,47 @@ struct PaymentMethod { beneficiary: Beneficiary, } +#[skip_serializing_none] #[derive(Debug, Clone, Serialize, PartialEq)] struct ProviderSelection { + #[serde(rename = "type")] + _type: ProviderSelectionType, + provider_id: Option, + remitter: Option, + scheme_selection: Option, +} + +#[derive(Debug, Clone, Serialize, PartialEq)] +struct SchemeSelection { #[serde(rename = "type")] _type: String, } +#[skip_serializing_none] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +struct Remitter { + account_identifier: TruelayerAccountIdentifier, + account_holder_name: Option>, +} + +#[skip_serializing_none] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct TruelayerAccountIdentifier { + #[serde(rename = "type")] + identifier_type: TruelayerAccountIdentifierType, + sort_code: Option>, + account_number: Option>, + iban: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +enum ProviderSelectionType { + UserSelected, + Preselected, +} + +#[skip_serializing_none] #[derive(Debug, Clone, Serialize, PartialEq)] struct Beneficiary { #[serde(rename = "type")] @@ -342,7 +383,13 @@ impl, ) -> Result { match &item.router_data.request.payment_method_data { - PaymentMethodData::BankRedirect(BankRedirectData::OpenBanking { .. }) => { + PaymentMethodData::BankRedirect(BankRedirectData::OpenBanking { + account_number, + sort_code, + iban, + account_holder_name, + additional_details, + }) => { let currency = item.router_data.request.currency; let amount_in_minor = item.router_data.request.amount; @@ -357,13 +404,56 @@ impl, failure_stage: Option, payment_source: Option, + payment_method: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct TruelayerPaymentMethod { + provider_selection: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct TruelayerProviderSelection { + provider_id: Option, } impl TryFrom> @@ -618,12 +719,65 @@ impl TryFrom> ..item.router_data }) } else { + let account_holder_name = response + .payment_source + .as_ref() + .and_then(|s| s.account_holder_name.clone()); + + let mut sort_code: Option> = None; + let mut account_number: Option> = None; + let mut iban: Option> = None; + + if let Some(source) = response.payment_source.as_ref() { + for identifier in source.account_identifiers.iter().flatten() { + match identifier.identifier_type { + TruelayerAccountIdentifierType::SortCodeAccountNumber => { + sort_code = identifier.sort_code.clone(); + account_number = identifier.account_number.clone(); + } + TruelayerAccountIdentifierType::Iban => { + iban = identifier.iban.clone(); + } + TruelayerAccountIdentifierType::Unknown => {} + } + } + } + + let provider_id = response + .payment_method + .as_ref() + .and_then(|pm| pm.provider_selection.as_ref()) + .and_then(|ps| ps.provider_id.clone()); + + let has_returned_open_banking_details = provider_id.is_some() + && account_holder_name.is_some() + && ((account_number.is_some() && sort_code.is_some()) || iban.is_some()); + + let additional_details = provider_id + .map(|pid| Secret::new(serde_json::json!({ "provider_id": pid }))); + + let connector_returned_payment_method_details = + if has_returned_open_banking_details { + Some(PaymentMethodData::::BankRedirect( + BankRedirectData::OpenBanking { + account_number, + sort_code, + iban, + account_holder_name, + additional_details, + }, + )) + } else { + None + }; + Ok(Self { resource_common_data: PaymentFlowData { status, sender_payment_instrument_id: response .payment_source .and_then(|source| source.id), + connector_returned_payment_method_details, ..item.router_data.resource_common_data }, response: Ok(PaymentsResponseData::TransactionResponse { @@ -1038,9 +1192,21 @@ pub struct TruelayerWebhookBody { pub payment_source: Option, } +/// Discriminator for the type of account identifier provided in a payment source. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum TruelayerAccountIdentifierType { + SortCodeAccountNumber, + Iban, + #[serde(other)] + Unknown, +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct TruelayerPaymentSource { pub id: Option, + pub account_holder_name: Option>, + pub account_identifiers: Option>, } pub fn get_webhook_event( diff --git a/crates/integrations/connector-integration/src/connectors/trustpay/transformers.rs b/crates/integrations/connector-integration/src/connectors/trustpay/transformers.rs index 91cb2aaffb..01c1c4962b 100644 --- a/crates/integrations/connector-integration/src/connectors/trustpay/transformers.rs +++ b/crates/integrations/connector-integration/src/connectors/trustpay/transformers.rs @@ -191,7 +191,7 @@ impl TryFrom<&BankRedirectData> for TrustpayPaymentMethod { | BankRedirectData::OnlineBankingFpx { .. } | BankRedirectData::OnlineBankingThailand { .. } | BankRedirectData::LocalBankRedirect {} - | BankRedirectData::OpenBanking {} + | BankRedirectData::OpenBanking { .. } | BankRedirectData::Netbanking { .. } => Err(IntegrationError::NotImplemented( utils::get_unimplemented_payment_method_error_message("trustpay"), Default::default(), diff --git a/crates/integrations/connector-integration/src/connectors/volt/transformers.rs b/crates/integrations/connector-integration/src/connectors/volt/transformers.rs index bde9deac61..2aa1f05023 100644 --- a/crates/integrations/connector-integration/src/connectors/volt/transformers.rs +++ b/crates/integrations/connector-integration/src/connectors/volt/transformers.rs @@ -190,7 +190,7 @@ impl { + BankRedirectData::OpenBanking { .. } => { if matches!(currency, common_enums::Currency::GBP) { Ok(( PaymentSystem::OpenBankingUk, diff --git a/crates/integrations/connector-integration/src/connectors/worldpayxml/requests.rs b/crates/integrations/connector-integration/src/connectors/worldpayxml/requests.rs index 1534ed82bf..683fa5416e 100644 --- a/crates/integrations/connector-integration/src/connectors/worldpayxml/requests.rs +++ b/crates/integrations/connector-integration/src/connectors/worldpayxml/requests.rs @@ -72,7 +72,7 @@ pub struct WorldpayxmlOrder { #[derive(Debug, Serialize)] pub struct WorldpayxmlCreateToken { #[serde(rename = "@tokenScope")] - pub token_scope: String, + pub token_scope: WorldpayxmlTokenScope, #[serde(rename = "tokenEventReference")] pub token_event_reference: String, } @@ -122,6 +122,13 @@ pub struct WorldpayxmlStoredCredentials { pub scheme_transaction_identifier: Option>, } +/// Scope Worldpay issues a payment token under. Only shopper scope is used. +#[derive(Debug, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum WorldpayxmlTokenScope { + Shopper, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "UPPERCASE")] pub enum WorldpayxmlUsageType { @@ -164,7 +171,7 @@ pub enum WorldpayxmlPaymentMethod { #[derive(Debug, Serialize)] pub struct WorldpayxmlTokenData { #[serde(rename = "@tokenScope")] - pub token_scope: Secret, + pub token_scope: WorldpayxmlTokenScope, #[serde(rename = "paymentTokenID")] pub payment_token_id: Secret, } diff --git a/crates/integrations/connector-integration/src/connectors/worldpayxml/responses.rs b/crates/integrations/connector-integration/src/connectors/worldpayxml/responses.rs index a2a358dc2c..211e616d82 100644 --- a/crates/integrations/connector-integration/src/connectors/worldpayxml/responses.rs +++ b/crates/integrations/connector-integration/src/connectors/worldpayxml/responses.rs @@ -1,5 +1,6 @@ +use common_utils::StringMinorUnit; use hyperswitch_masking::Secret; -use serde::{Deserialize, Serialize}; +use serde::{de, Deserialize, Deserializer, Serialize}; #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename = "paymentService")] @@ -80,36 +81,50 @@ pub struct WorldpayxmlPayment { pub scheme_response: Option, } -#[derive(Clone, Debug, Deserialize, Serialize)] +/// `lastEvent`/`PaymentStatus` values Worldpay reports for an order. +/// +/// Worldpay keeps adding journey events, so any value that is not modelled here deserializes to +/// [`WorldpayxmlLastEvent::Unknown`] instead of failing the whole response. +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum WorldpayxmlLastEvent { Authorised, Refused, Cancelled, Captured, + Settled, + SentForAuthorisation, SentForRefund, + SentForFastRefund, Refunded, + RefundRequested, RefundFailed, + RefundedByMerchant, Expired, Error, + QueryRequired, + CancelReceived, + RefundReceived, PushRequested, PushPending, PushApproved, PushRefused, SettledByMerchant, + #[serde(other)] + Unknown, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct WorldpayxmlResultCode { #[serde(rename = "@description")] - pub description: String, + pub description: Option, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct WorldpayxmlBalance { #[serde(rename = "@accountType")] - pub account_type: String, - pub amount: WorldpayxmlAmountResponse, + pub account_type: Option, + pub amount: Option, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -121,7 +136,7 @@ pub struct WorldpayxmlSchemeResponse { #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct WorldpayxmlPaymentMethodDetail { - pub card: WorldpayxmlCardResponse, + pub card: Option, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -130,7 +145,7 @@ pub struct WorldpayxmlCardResponse { #[serde(rename = "@number")] pub number: Option, #[serde(rename = "@type")] - pub card_type: String, + pub card_type: Option, pub expiry_date: Option, } @@ -162,7 +177,7 @@ pub struct WorldpayxmlAmountResponse { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct WorldpayxmlAuthorisationId { #[serde(rename = "@id")] - pub id: String, + pub id: Option, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -310,20 +325,96 @@ pub struct WorldpayxmlCancelOrRefundReceived { pub order_code: String, } -// PSync response can be either XML (PaymentService) or JSON (Webhook format) -#[derive(Clone, Debug, Deserialize, Serialize)] +/// Carrier for a PSync/RSync body, which is normally the XML `` order-inquiry +/// envelope but can also be the form/JSON order-notification body. +/// +/// It deliberately does **not** derive `Deserialize` as an `untagged` enum. `untagged` forces +/// serde's `deserialize_any`, and the buffered value it produces loses XML sequence and text +/// semantics — `balance` buffers as a map instead of a sequence and `X` +/// buffers as `{"$text": "X"}`, which a unit-variant enum cannot be built from. The `Payment` +/// variant therefore never matched, and the all-optional webhook variant silently absorbed every +/// payload. Instead the concrete `` type is tried first and the success is wrapped +/// manually, falling back to the notification body — mirroring hyperswitch's handling. +#[derive(Clone, Debug, Serialize)] #[serde(untagged)] pub enum WorldpayxmlTransactionResponse { Payment(Box), Webhook(WorldpayxmlWebhookResponse), } -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] +/// Concrete (never `deserialize_any`) shape covering both bodies a sync call can return, so the +/// right [`WorldpayxmlTransactionResponse`] variant can be picked without buffering the payload. +/// +/// The notification half is PascalCase on the wire, so `rename_all` carries it. The +/// `` half is not — those three keep an explicit rename, and must keep it. +#[derive(Debug, Deserialize)] +#[serde(rename = "paymentService", rename_all = "PascalCase")] +struct WorldpayxmlSyncResponseBody { + // `` order-inquiry reply: XML attributes and a lowercase element, none of + // which follow the notification body's PascalCase convention. + #[serde(rename = "@version")] + version: Option, + #[serde(rename = "@merchantCode")] + merchant_code: Option, + #[serde(rename = "reply")] + reply: Option, + // Order-notification body. + payment_amount: Option, + payment_id: Option, + order_code: Option, + payment_status: Option, + return_code: Option, + return_message: Option, +} + +impl<'de> Deserialize<'de> for WorldpayxmlTransactionResponse { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let body = WorldpayxmlSyncResponseBody::deserialize(deserializer)?; + + // Try the concrete `` envelope first, exactly as the payment flows do. + if let Some(reply) = body.reply { + return Ok(Self::Payment(Box::new(WorldpayxmlAuthorizeResponse { + version: body.version.unwrap_or_default(), + merchant_code: body.merchant_code.unwrap_or_default(), + reply, + }))); + } + + // Otherwise fall back to the order-notification body. + match (body.order_code, body.payment_status) { + (Some(order_code), Some(payment_status)) => { + Ok(Self::Webhook(WorldpayxmlWebhookResponse { + payment_amount: body.payment_amount, + payment_id: body.payment_id, + order_code, + payment_status, + return_code: body.return_code, + return_message: body.return_message, + })) + } + _ => Err(de::Error::custom( + "worldpayxml: sync response body carried neither a paymentService reply nor an order notification", + )), + } + } +} + +/// Order-notification body Worldpay can deliver for an order. +/// +/// `order_code` and `payment_status` are mandatory: making them optional is what let this shape +/// match every payload and mask genuine parse failures. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "PascalCase")] pub struct WorldpayxmlWebhookResponse { - pub order_code: Option, - pub last_event: Option, - pub payment_status: Option, + pub payment_amount: Option, + pub payment_id: Option, + pub order_code: String, + pub payment_status: WorldpayxmlLastEvent, + pub return_code: Option, + pub return_message: Option, } // Type alias for RSync - reuses PSync response structure diff --git a/crates/integrations/connector-integration/src/connectors/worldpayxml/transformers.rs b/crates/integrations/connector-integration/src/connectors/worldpayxml/transformers.rs index e707c6119c..47cd1ca97e 100644 --- a/crates/integrations/connector-integration/src/connectors/worldpayxml/transformers.rs +++ b/crates/integrations/connector-integration/src/connectors/worldpayxml/transformers.rs @@ -35,10 +35,6 @@ use domain_types::payment_address::AddressDetails; const API_VERSION: &str = "1.4"; -/// Worldpay scopes the tokens it issues to a single shopper, which is the scope stored as the -/// connector mandate id. -const TOKEN_SCOPE_SHOPPER: &str = "shopper"; - /// `captureDelay` value that leaves the order uncaptured, for manual capture. const CAPTURE_DELAY_MANUAL: &str = "OFF"; /// `captureDelay` value that captures the order immediately, for automatic capture. @@ -264,14 +260,32 @@ fn get_worldpayxml_wallet_payment_method( } /// Number of decimal places Worldpay expects for the order currency. -fn get_worldpayxml_exponent(currency: common_enums::Currency) -> String { - if currency.is_three_decimal_currency() { - "3".to_string() - } else if currency.is_zero_decimal_currency() { - "0".to_string() - } else { - "2".to_string() - } +/// +/// Defers to the shared currency helper rather than re-deriving the exponent, so four-decimal +/// currencies are handled and an unrecognised currency is rejected instead of silently being +/// treated as two-decimal. +fn get_worldpayxml_exponent( + currency: common_enums::Currency, +) -> Result> { + currency + .number_of_digits_after_decimal_point() + .map(|digits| digits.to_string()) + .map_err(|_| { + IntegrationError::InvalidDataFormat { + field_name: "currency", + context: IntegrationErrorContext { + suggested_action: Some( + "Use an ISO 4217 currency Worldpay accepts (e.g. GBP, USD, EUR)." + .to_string(), + ), + doc_url: None, + additional_context: Some(format!( + "Currency {currency:?} has no known minor-unit exponent" + )), + }, + } + .into() + }) } /// The telephone number is carried on the billing contact rather than the address itself, so it is @@ -392,11 +406,19 @@ impl billing_address.as_ref(), )?, PaymentMethodData::Wallet(wallet_data) => { + // Fall back to the billing name when the request carries no customer name, so a + // wallet payment still sends a cardholder name to Worldpay. let customer_name = router_data .request .customer_name .clone() - .map(|name| crate::utils::normalize_cardholder_name(Secret::new(name))); + .map(Secret::new) + .or_else(|| { + router_data + .resource_common_data + .get_optional_billing_full_name() + }) + .map(crate::utils::normalize_cardholder_name); get_worldpayxml_wallet_payment_method(wallet_data, customer_name)? } @@ -428,7 +450,7 @@ impl // Ask Worldpay to issue a payment token on the customer-initiated transaction; without it // there is nothing for a later merchant-initiated payment to be charged against. let create_token = is_cit_mandate_payment.then(|| requests::WorldpayxmlCreateToken { - token_scope: TOKEN_SCOPE_SHOPPER.to_string(), + token_scope: requests::WorldpayxmlTokenScope::Shopper, token_event_reference: router_data .resource_common_data .connector_request_reference_id @@ -468,7 +490,7 @@ impl amount: requests::WorldpayxmlAmount { value: converted_amount, currency_code: router_data.request.currency, - exponent: get_worldpayxml_exponent(router_data.request.currency), + exponent: get_worldpayxml_exponent(router_data.request.currency)?, }, payment_details: requests::WorldpayxmlPaymentDetails { action: Some(if is_manual_capture { @@ -555,11 +577,19 @@ impl billing_address.as_ref(), )?, PaymentMethodData::Wallet(wallet_data) => { + // Fall back to the billing name when the request carries no customer name, so a + // wallet payment still sends a cardholder name to Worldpay. let customer_name = router_data .request .customer_name .clone() - .map(|name| crate::utils::normalize_cardholder_name(Secret::new(name))); + .map(Secret::new) + .or_else(|| { + router_data + .resource_common_data + .get_optional_billing_full_name() + }) + .map(crate::utils::normalize_cardholder_name); get_worldpayxml_wallet_payment_method(wallet_data, customer_name)? } @@ -606,7 +636,7 @@ impl amount: requests::WorldpayxmlAmount { value: converted_amount, currency_code: router_data.request.currency, - exponent: get_worldpayxml_exponent(router_data.request.currency), + exponent: get_worldpayxml_exponent(router_data.request.currency)?, }, payment_details: requests::WorldpayxmlPaymentDetails { action: Some(if is_manual_capture { @@ -648,7 +678,7 @@ impl }, billing_address, create_token: Some(requests::WorldpayxmlCreateToken { - token_scope: TOKEN_SCOPE_SHOPPER.to_string(), + token_scope: requests::WorldpayxmlTokenScope::Shopper, token_event_reference: router_data .resource_common_data .connector_request_reference_id @@ -740,7 +770,8 @@ impl }, )?; - let is_manual_capture = !router_data.request.is_auto_capture(); + let is_manual_capture = router_data.request.capture_method == Some(CaptureMethod::Manual) + || router_data.request.capture_method == Some(CaptureMethod::ManualMultiple); let authenticated_shopper_id = get_worldpayxml_authenticated_shopper_id(&router_data.resource_common_data, true)?; @@ -772,13 +803,13 @@ impl amount: requests::WorldpayxmlAmount { value: converted_amount, currency_code: router_data.request.currency, - exponent: get_worldpayxml_exponent(router_data.request.currency), + exponent: get_worldpayxml_exponent(router_data.request.currency)?, }, payment_details: requests::WorldpayxmlPaymentDetails { action: None, payment_method: requests::WorldpayxmlPaymentMethod::TokenSsl( requests::WorldpayxmlTokenData { - token_scope: Secret::new(TOKEN_SCOPE_SHOPPER.to_string()), + token_scope: requests::WorldpayxmlTokenScope::Shopper, payment_token_id: Secret::new(payment_token_id), }, ), @@ -857,7 +888,7 @@ impl amount: requests::WorldpayxmlAmount { value: converted_amount, currency_code: router_data.request.currency, - exponent: get_worldpayxml_exponent(router_data.request.currency), + exponent: get_worldpayxml_exponent(router_data.request.currency)?, }, }, }, @@ -941,7 +972,7 @@ impl amount: requests::WorldpayxmlAmount { value: converted_amount, currency_code: router_data.request.currency, - exponent: get_worldpayxml_exponent(router_data.request.currency), + exponent: get_worldpayxml_exponent(router_data.request.currency)?, }, }, }, @@ -1025,65 +1056,90 @@ impl } } -// Helper function to map lastEvent to AttemptStatus +/// Maps the `lastEvent` of a payment order onto an attempt status. +/// +/// Only the events that belong to an authorisation/capture journey are mapped. A refund- or +/// payout-family event on a payment order means the response does not describe the order we asked +/// about, so it is reported as a protocol violation instead of being flattened into `Failure`, +/// which would be indistinguishable from a genuine decline. fn map_worldpayxml_authorize_status( last_event: &WorldpayxmlLastEvent, is_auto_capture: bool, - capture_sync_status: Option<&AttemptStatus>, -) -> AttemptStatus { + previous_status: Option<&AttemptStatus>, + http_code: u16, +) -> Result { match last_event { WorldpayxmlLastEvent::Authorised => { if is_auto_capture { - AttemptStatus::Pending + // The order was submitted for automatic capture, so an authorisation already + // settles the attempt — there is no separate capture to wait for. + Ok(AttemptStatus::Charged) } else { // Check if we're in CaptureInitiated or VoidInitiated state - match capture_sync_status { + Ok(match previous_status { Some(AttemptStatus::CaptureInitiated) => AttemptStatus::CaptureInitiated, Some(AttemptStatus::VoidInitiated) => AttemptStatus::VoidInitiated, _ => AttemptStatus::Authorized, - } + }) } } - WorldpayxmlLastEvent::Refused => AttemptStatus::Failure, - WorldpayxmlLastEvent::Cancelled => AttemptStatus::Voided, - WorldpayxmlLastEvent::Captured => AttemptStatus::Charged, - WorldpayxmlLastEvent::SentForRefund => AttemptStatus::Pending, - WorldpayxmlLastEvent::Refunded => AttemptStatus::Charged, - WorldpayxmlLastEvent::RefundFailed => AttemptStatus::Failure, - WorldpayxmlLastEvent::Expired => AttemptStatus::Failure, - WorldpayxmlLastEvent::Error => AttemptStatus::Failure, - - WorldpayxmlLastEvent::PushRequested - | WorldpayxmlLastEvent::PushPending - | WorldpayxmlLastEvent::PushApproved - | WorldpayxmlLastEvent::PushRefused - | WorldpayxmlLastEvent::SettledByMerchant => AttemptStatus::Failure, + WorldpayxmlLastEvent::Refused => Ok(AttemptStatus::Failure), + // Worldpay expires an authorisation that outlives its auth window (a manual-capture order + // left uncaptured, say). The money can never be taken, so this is terminal — not a + // protocol violation to error on. + WorldpayxmlLastEvent::Expired => Ok(AttemptStatus::Failure), + WorldpayxmlLastEvent::Cancelled => Ok(AttemptStatus::Voided), + WorldpayxmlLastEvent::Captured + | WorldpayxmlLastEvent::Settled + | WorldpayxmlLastEvent::SettledByMerchant => Ok(AttemptStatus::Charged), + WorldpayxmlLastEvent::SentForAuthorisation => Ok(AttemptStatus::Authorizing), + WorldpayxmlLastEvent::Unknown => Ok(retain_previous_attempt_status(previous_status)), + _ => Err(crate::utils::unexpected_response_fail( + http_code, + "worldpayxml: lastEvent is not part of a payment authorisation lifecycle.", + )), } } /// Maps `lastEvent` for a mandate setup, where an authorisation already completes the flow — /// there is no capture to wait for on a (usually zero-amount) verification order. -impl From<&WorldpayxmlLastEvent> for AttemptStatus { - fn from(last_event: &WorldpayxmlLastEvent) -> Self { - match last_event { - WorldpayxmlLastEvent::Authorised - | WorldpayxmlLastEvent::Captured - | WorldpayxmlLastEvent::SettledByMerchant => Self::Charged, - WorldpayxmlLastEvent::Cancelled => Self::Voided, - WorldpayxmlLastEvent::Refused - | WorldpayxmlLastEvent::SentForRefund - | WorldpayxmlLastEvent::Refunded - | WorldpayxmlLastEvent::RefundFailed - | WorldpayxmlLastEvent::Expired - | WorldpayxmlLastEvent::Error - | WorldpayxmlLastEvent::PushRequested - | WorldpayxmlLastEvent::PushPending - | WorldpayxmlLastEvent::PushApproved - | WorldpayxmlLastEvent::PushRefused => Self::Failure, - } +/// +/// Events outside that journey are rejected for the same reason as +/// [`map_worldpayxml_authorize_status`]. +fn map_worldpayxml_setup_mandate_status( + last_event: &WorldpayxmlLastEvent, + previous_status: Option<&AttemptStatus>, + http_code: u16, +) -> Result { + match last_event { + WorldpayxmlLastEvent::Refused => Ok(AttemptStatus::Failure), + // See [`map_worldpayxml_authorize_status`]: an expired authorisation is terminal. + WorldpayxmlLastEvent::Expired => Ok(AttemptStatus::Failure), + WorldpayxmlLastEvent::Cancelled => Ok(AttemptStatus::Voided), + WorldpayxmlLastEvent::Authorised + | WorldpayxmlLastEvent::Captured + | WorldpayxmlLastEvent::Settled + | WorldpayxmlLastEvent::SettledByMerchant => Ok(AttemptStatus::Charged), + WorldpayxmlLastEvent::SentForAuthorisation => Ok(AttemptStatus::Authorizing), + WorldpayxmlLastEvent::Unknown => Ok(retain_previous_attempt_status(previous_status)), + _ => Err(crate::utils::unexpected_response_fail( + http_code, + "worldpayxml: lastEvent is not part of a mandate setup lifecycle.", + )), } } +/// Worldpay keeps introducing journey events. An unrecognised one says nothing about the order, so +/// hold the status we already had rather than guessing at a terminal state. +fn retain_previous_attempt_status(previous_status: Option<&AttemptStatus>) -> AttemptStatus { + let status = previous_status.copied().unwrap_or_default(); + tracing::warn!( + retained_status = ?status, + "worldpayxml: unknown lastEvent received; retaining previous attempt status" + ); + status +} + /// Builds the mandate reference Hyperswitch stores for later merchant-initiated payments. /// /// The Worldpay payment token is what the merchant-initiated payment is charged against, and the @@ -1124,45 +1180,46 @@ fn get_worldpayxml_mandate_reference( }) } -// Helper function to map lastEvent to RefundStatus -impl From<&WorldpayxmlLastEvent> for RefundStatus { - fn from(last_event: &WorldpayxmlLastEvent) -> Self { - match last_event { - WorldpayxmlLastEvent::Refunded => Self::Success, - WorldpayxmlLastEvent::SentForRefund => Self::Pending, - WorldpayxmlLastEvent::RefundFailed => Self::Failure, - WorldpayxmlLastEvent::Captured => Self::Pending, - _ => Self::Pending, // Default to pending for unknown statuses +/// Maps the `lastEvent` of a refund order onto a refund status. +/// +/// Only the events that belong to a refund journey are mapped. `CAPTURED`/`SETTLED` are included +/// because Worldpay keeps reporting the underlying capture until the refund itself moves, so they +/// mean "the refund has not progressed yet" rather than "the refund succeeded". Anything outside +/// that journey means the response does not describe the order we asked about, so it is reported as +/// a protocol violation instead of being flattened into `Pending`, which would leave the refund +/// polling forever. +fn map_worldpayxml_refund_status( + last_event: &WorldpayxmlLastEvent, + previous_status: RefundStatus, + http_code: u16, +) -> Result { + match last_event { + WorldpayxmlLastEvent::Refunded | WorldpayxmlLastEvent::RefundedByMerchant => { + Ok(RefundStatus::Success) + } + WorldpayxmlLastEvent::SentForRefund + | WorldpayxmlLastEvent::RefundRequested + | WorldpayxmlLastEvent::SentForFastRefund => Ok(RefundStatus::Pending), + WorldpayxmlLastEvent::RefundFailed => Ok(RefundStatus::Failure), + // An expired order can no longer be refunded, so the refund will never complete. + WorldpayxmlLastEvent::Expired => Ok(RefundStatus::Failure), + WorldpayxmlLastEvent::Captured | WorldpayxmlLastEvent::Settled => Ok(RefundStatus::Pending), + WorldpayxmlLastEvent::Unknown => { + // An unrecognised event says nothing about the refund, so hold the status we already + // had rather than reporting a terminal one. + tracing::warn!( + retained_status = ?previous_status, + "worldpayxml: unknown lastEvent received; retaining previous refund status" + ); + Ok(previous_status) } + _ => Err(crate::utils::unexpected_response_fail( + http_code, + "worldpayxml: lastEvent is not part of a refund lifecycle.", + )), } } -// Helper function to parse string last_event from webhook/JSON responses -fn parse_last_event( - event_str: &str, - http_code: u16, -) -> Result> { - // The event string comes off the wire, so it must be escaped rather than interpolated — - // a bare `"` or `\` in it would otherwise produce malformed JSON and drop the event. - let quoted = serde_json::to_string(event_str).map_err(|_| { - Report::new( - ConnectorError::response_deserialization_failed_with_context( - http_code, - Some("invalid last_event".to_string()), - ), - ) - })?; - - serde_json::from_str("ed).map_err(|_| { - Report::new( - ConnectorError::response_deserialization_failed_with_context( - http_code, - Some("invalid last_event".to_string()), - ), - ) - }) -} - // Response transformers - Authorize impl TryFrom> @@ -1238,16 +1295,49 @@ impl crate::utils::response_deserialization_fail(item.http_code, "worldpayxml: response body did not match the expected format; confirm API version and connector documentation."), )?; - // Determine if auto-capture - let is_auto_capture = router_data.request.capture_method != Some(CaptureMethod::Manual) - && router_data.request.capture_method != Some(CaptureMethod::ManualMultiple); - // Map status from lastEvent let status = map_worldpayxml_authorize_status( &payment.last_event, - is_auto_capture, + router_data.request.capture_method != Some(CaptureMethod::Manual) + && router_data.request.capture_method != Some(CaptureMethod::ManualMultiple), Some(&router_data.resource_common_data.status), - ); + item.http_code, + )?; + + // A refused authorization is the most common decline path, so surface the ISO 8583 return + // code the same way the SetupMandate and RepeatPayment transformers do instead of handing + // the merchant a bare Failure with no code or reason. + if domain_types::utils::is_payment_failure(status) { + let return_code = payment.iso8583_return_code.as_ref(); + return Ok(Self { + resource_common_data: PaymentFlowData { + status, + ..router_data.resource_common_data.clone() + }, + response: Err(ErrorResponse { + code: return_code.map_or_else( + || common_utils::consts::NO_ERROR_CODE.to_string(), + |code| code.code.clone(), + ), + message: return_code.map_or_else( + || common_utils::consts::NO_ERROR_MESSAGE.to_string(), + |code| code.description.clone(), + ), + reason: return_code.map(|code| code.description.clone()), + status_code: item.http_code, + attempt_status: Some(FlowStatus::Payment(status)), + connector_transaction_id: Some(order_status.order_code.clone()), + network_decline_code: None, + network_advice_code: None, + network_error_message: None, + typed_connector_response: None, + raw_connector_response: None, + raw_connector_request: None, + typed_connector_request: None, + }), + ..router_data.clone() + }); + } // Build success response let payments_response_data = PaymentsResponseData::TransactionResponse { @@ -1258,7 +1348,7 @@ impl network_txn_id: payment .authorisation_id .as_ref() - .map(|auth_id| auth_id.id.clone()), + .and_then(|auth_id| auth_id.id.clone()), network_txn_link_id: None, connector_response_reference_id: Some(order_status.order_code.clone()), incremental_authorization_allowed: None, @@ -1353,9 +1443,13 @@ impl crate::utils::response_deserialization_fail(item.http_code, "worldpayxml: response body did not match the expected format; confirm API version and connector documentation."), )?; - let status = AttemptStatus::from(&payment.last_event); + let status = map_worldpayxml_setup_mandate_status( + &payment.last_event, + Some(&router_data.resource_common_data.status), + item.http_code, + )?; - if status == AttemptStatus::Failure { + if domain_types::utils::is_payment_failure(status) { let return_code = payment.iso8583_return_code.as_ref(); return Ok(Self { resource_common_data: PaymentFlowData { @@ -1399,7 +1493,7 @@ impl network_txn_id: payment .authorisation_id .as_ref() - .map(|auth_id| auth_id.id.clone()), + .and_then(|auth_id| auth_id.id.clone()), network_txn_link_id: None, connector_response_reference_id: Some(order_status.order_code.clone()), incremental_authorization_allowed: None, @@ -1491,14 +1585,16 @@ impl let status = map_worldpayxml_authorize_status( &payment.last_event, - router_data.request.is_auto_capture(), + router_data.request.capture_method != Some(CaptureMethod::Manual) + && router_data.request.capture_method != Some(CaptureMethod::ManualMultiple), Some(&router_data.resource_common_data.status), - ); + item.http_code, + )?; // A refused merchant-initiated payment is the case that most needs its decline detail // (retry and dunning logic key off it), so surface the ISO 8583 return code the same // way the SetupMandate transformer does instead of a bare Failure status. - if status == AttemptStatus::Failure { + if domain_types::utils::is_payment_failure(status) { let return_code = payment.iso8583_return_code.as_ref(); return Ok(Self { resource_common_data: PaymentFlowData { @@ -1538,7 +1634,7 @@ impl network_txn_id: payment .authorisation_id .as_ref() - .map(|auth_id| auth_id.id.clone()), + .and_then(|auth_id| auth_id.id.clone()), network_txn_link_id: None, connector_response_reference_id: Some(order_status.order_code.clone()), incremental_authorization_allowed: None, @@ -1756,13 +1852,26 @@ impl TryFrom // Special handling: If error exists but payment is None, return current status (don't fail) if let Some(error) = &order_status.error { if order_status.payment.is_none() { - // Error exists but no payment data - return current status as Pending + // An inquiry-level error with no payment element says nothing about the + // order itself, so hold the status we already had. Overwriting it would + // walk a terminal attempt (a Charged one, say) back to Pending. let payments_response_data = PaymentsResponseData::TransactionResponse { resource_id: ResponseId::ConnectorTransactionId( order_status.order_code.clone(), ), redirection_data: None, - mandate_reference: None, + // A token can be present even when the inquiry itself errored, and it + // is the only place the mandate becomes observable, so surface it. + mandate_reference: order_status.token.as_ref().map(|token| { + Box::new(MandateReference { + connector_mandate_id: Some( + token.token_details.payment_token_id.peek().to_string(), + ), + payment_method_id: None, + mandate_metadata: None, + connector_mandate_request_reference_id: None, + }) + }), connector_metadata: None, network_txn_id: None, network_txn_link_id: None, @@ -1773,10 +1882,6 @@ impl TryFrom }; return Ok(Self { - resource_common_data: PaymentFlowData { - status: AttemptStatus::Pending, - ..router_data.resource_common_data.clone() - }, response: Ok(payments_response_data), ..router_data.clone() }); @@ -1814,17 +1919,49 @@ impl TryFrom "worldpayxml: response body did not match the expected format; confirm API version and connector documentation."), )?; - // Determine if auto-capture from request data - let is_auto_capture = router_data.request.capture_method - != Some(CaptureMethod::Manual) - && router_data.request.capture_method != Some(CaptureMethod::ManualMultiple); - // Map status from lastEvent - reuse the helper function let status = map_worldpayxml_authorize_status( &payment.last_event, - is_auto_capture, + router_data.request.capture_method != Some(CaptureMethod::Manual) + && router_data.request.capture_method + != Some(CaptureMethod::ManualMultiple), Some(&router_data.resource_common_data.status), - ); + item.http_code, + )?; + + // A sync that observes a refused order carries the same decline detail an + // Authorize reply does, so report it identically rather than as a bare Failure. + if domain_types::utils::is_payment_failure(status) { + let return_code = payment.iso8583_return_code.as_ref(); + return Ok(Self { + resource_common_data: PaymentFlowData { + status, + ..router_data.resource_common_data.clone() + }, + response: Err(ErrorResponse { + code: return_code.map_or_else( + || common_utils::consts::NO_ERROR_CODE.to_string(), + |code| code.code.clone(), + ), + message: return_code.map_or_else( + || common_utils::consts::NO_ERROR_MESSAGE.to_string(), + |code| code.description.clone(), + ), + reason: return_code.map(|code| code.description.clone()), + status_code: item.http_code, + attempt_status: Some(FlowStatus::Payment(status)), + connector_transaction_id: Some(order_status.order_code.clone()), + network_decline_code: None, + network_advice_code: None, + network_error_message: None, + typed_connector_response: None, + raw_connector_response: None, + raw_connector_request: None, + typed_connector_request: None, + }), + ..router_data.clone() + }); + } // Build success response let payments_response_data = PaymentsResponseData::TransactionResponse { @@ -1837,7 +1974,7 @@ impl TryFrom network_txn_id: payment .authorisation_id .as_ref() - .map(|auth_id| auth_id.id.clone()), + .and_then(|auth_id| auth_id.id.clone()), network_txn_link_id: None, connector_response_reference_id: Some(order_status.order_code.clone()), incremental_authorization_allowed: None, @@ -1855,36 +1992,18 @@ impl TryFrom }) } responses::WorldpayxmlTransactionResponse::Webhook(webhook_response) => { - // Process JSON webhook response - let order_code = webhook_response - .order_code - .clone() - .unwrap_or_else(|| "unknown".to_string()); + // Process order-notification body + let order_code = webhook_response.order_code.clone(); - let last_event_str = webhook_response - .last_event - .as_ref() - .or(webhook_response.payment_status.as_ref()) - .ok_or( - crate::utils::response_deserialization_fail( - item.http_code, - "worldpayxml: response body did not match the expected format; confirm API version and connector documentation."), - )?; - - // Parse string to enum - let last_event = parse_last_event(last_event_str, item.http_code)?; - - // Determine if auto-capture from request data - let is_auto_capture = router_data.request.capture_method - != Some(CaptureMethod::Manual) - && router_data.request.capture_method != Some(CaptureMethod::ManualMultiple); - - // Map status from lastEvent + // Map status from PaymentStatus let status = map_worldpayxml_authorize_status( - &last_event, - is_auto_capture, + &webhook_response.payment_status, + router_data.request.capture_method != Some(CaptureMethod::Manual) + && router_data.request.capture_method + != Some(CaptureMethod::ManualMultiple), Some(&router_data.resource_common_data.status), - ); + item.http_code, + )?; // Build success response let payments_response_data = PaymentsResponseData::TransactionResponse { @@ -2035,7 +2154,11 @@ impl TryFrom> )?; // Map status from lastEvent using refund status mapping - let refund_status = RefundStatus::from(&payment.last_event); + let refund_status = map_worldpayxml_refund_status( + &payment.last_event, + router_data.request.refund_status, + item.http_code, + )?; // Check if refund failed and extract error details from ISO8583ReturnCode if refund_status == RefundStatus::Failure { @@ -2075,27 +2198,15 @@ impl TryFrom> }) } responses::WorldpayxmlTransactionResponse::Webhook(webhook_response) => { - // Process JSON webhook response - let order_code = webhook_response - .order_code - .clone() - .unwrap_or_else(|| "unknown".to_string()); - - let last_event_str = webhook_response - .last_event - .as_ref() - .or(webhook_response.payment_status.as_ref()) - .ok_or( - crate::utils::response_deserialization_fail( - item.http_code, - "worldpayxml: response body did not match the expected format; confirm API version and connector documentation."), - )?; - - // Parse string to enum - let last_event = parse_last_event(last_event_str, item.http_code)?; - - // Map status from lastEvent using refund status mapping - let refund_status = RefundStatus::from(&last_event); + // Process order-notification body + let order_code = webhook_response.order_code.clone(); + + // Map status from PaymentStatus using refund status mapping + let refund_status = map_worldpayxml_refund_status( + &webhook_response.payment_status, + router_data.request.refund_status, + item.http_code, + )?; // Build success response let refunds_response_data = RefundsResponseData { diff --git a/crates/integrations/connector-integration/src/default_implementations.rs b/crates/integrations/connector-integration/src/default_implementations.rs index fb944d7f79..a1428419fe 100644 --- a/crates/integrations/connector-integration/src/default_implementations.rs +++ b/crates/integrations/connector-integration/src/default_implementations.rs @@ -291,6 +291,9 @@ default_impl_verify_webhook_source_v2!( Grabpay, Tesouro, Boost, + Citigate, + Moneris, + Ilixium, ], ); // PayPal has its own implementation in paypal.rs @@ -367,7 +370,9 @@ macro_rules! default_impl_recharge_v2 { } default_impl_recharge_v2!( + Citigate, Boost, + Ilixium, Tesouro, AbsaSanlam, Aci, @@ -418,6 +423,7 @@ default_impl_recharge_v2!( Loonio, Mifinity, Mollie, + Moneris, Multisafepay, Netcetera, Nexinets, @@ -592,7 +598,9 @@ 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, + Ilixium, Tesouro, AbsaSanlam, Aci, @@ -643,6 +651,7 @@ default_impl_create_payment_method_v2!( Loonio, Mifinity, Mollie, + Moneris, Multisafepay, Netcetera, Nexinets, @@ -695,7 +704,9 @@ default_impl_create_payment_method_v2!( ); default_impl_get_payment_method_v2!( + Citigate, Boost, + Ilixium, Tesouro, AbsaSanlam, Aci, @@ -746,6 +757,7 @@ default_impl_get_payment_method_v2!( Loonio, Mifinity, Mollie, + Moneris, Multisafepay, Netcetera, Nexinets, @@ -799,7 +811,9 @@ default_impl_get_payment_method_v2!( default_impl_payment_method_eligibility_v2!( not_supported: [ + Citigate, Boost, + Ilixium, Tesouro, Adyen, Authorizedotnet, @@ -855,6 +869,7 @@ default_impl_payment_method_eligibility_v2!( Loonio, Mifinity, Mollie, + Moneris, Multisafepay, Nexinets, Nexixpay, @@ -870,7 +885,6 @@ default_impl_payment_method_eligibility_v2!( PinelabsOnline, Placetopay, Powertranz, - Qwikcilver, Rapyd, Razorpay, RazorpayV2, @@ -964,7 +978,9 @@ macro_rules! default_impl_refresh_payment_method_v2 { } default_impl_refresh_payment_method_v2!( + Citigate, Boost, + Ilixium, AbsaSanlam, Aci, Adyen, @@ -1016,6 +1032,7 @@ default_impl_refresh_payment_method_v2!( Loonio, Mifinity, Mollie, + Moneris, Multisafepay, Netcetera, Nexinets, diff --git a/crates/integrations/connector-integration/src/payout_connectors.rs b/crates/integrations/connector-integration/src/payout_connectors.rs index dbf99f87e1..8e4a4a1736 100644 --- a/crates/integrations/connector-integration/src/payout_connectors.rs +++ b/crates/integrations/connector-integration/src/payout_connectors.rs @@ -10,6 +10,9 @@ pub use self::paypal::PaypalPayouts; pub mod deutschebank; pub use self::deutschebank::DeutschebankPayouts; +pub mod truelayer; +pub use self::truelayer::TruelayerPayouts; + pub mod worldpayxml; pub use self::worldpayxml::WorldpayxmlPayouts; @@ -18,3 +21,6 @@ pub use self::cybersource::CybersourcePayouts; pub mod santander; pub use self::santander::SantanderPayouts; + +pub mod trustly; +pub use self::trustly::TrustlyPayouts; diff --git a/crates/integrations/connector-integration/src/payout_connectors/truelayer.rs b/crates/integrations/connector-integration/src/payout_connectors/truelayer.rs new file mode 100644 index 0000000000..a5d0b0741b --- /dev/null +++ b/crates/integrations/connector-integration/src/payout_connectors/truelayer.rs @@ -0,0 +1,664 @@ +pub mod transformers; + +use std::collections::BTreeMap; + +use common_enums::CurrencyUnit; +use common_utils::{ + errors::CustomResult, + events, + ext_traits::ByteSliceExt, + request::{ConnectorRequestData, RequestContent}, +}; +use domain_types::{ + connector_flow::{ + PayoutCreate, PayoutCreateLink, PayoutCreateRecipient, PayoutEligibility, + PayoutEnrollDisburseAccount, PayoutGet, PayoutStage, PayoutTransfer, PayoutVoid, + ServerAuthenticationToken, + }, + connector_types::{ + ServerAuthenticationTokenRequestData, ServerAuthenticationTokenResponseData, + }, + errors::{ + ConnectorError, IntegrationError, IntegrationErrorContext, + ResponseTransformationErrorContext, + }, + merchant_authentication_flow_data::MerchantAuthenticationFlowData, + payouts::payouts_types::{ + PayoutCreateLinkRequest, PayoutCreateLinkResponse, PayoutCreateRecipientRequest, + PayoutCreateRecipientResponse, PayoutCreateRequest, PayoutCreateResponse, + PayoutEligibilityRequest, PayoutEligibilityResponse, PayoutEnrollDisburseAccountRequest, + PayoutEnrollDisburseAccountResponse, PayoutFlowData, PayoutGetRequest, PayoutGetResponse, + PayoutStageRequest, PayoutStageResponse, PayoutTransferRequest, PayoutTransferResponse, + PayoutVoidRequest, PayoutVoidResponse, + }, + router_data::{ConnectorSpecificConfig, ErrorResponse}, + router_data_v2::RouterDataV2, + router_response_types::Response, + types::Connectors, +}; +use error_stack::ResultExt; +use hyperswitch_masking::{ExposeInterface, Mask, Maskable}; +use interfaces::{ + api::ConnectorCommon, + connector_integration_v2::ConnectorIntegrationV2, + connector_types::{ + PayoutCreateLinkV2, PayoutCreateRecipientV2, PayoutCreateV2, PayoutEligibilityV2, + PayoutEnrollDisburseAccountV2, PayoutGetV2, PayoutServiceTrait, PayoutStageV2, + PayoutTransferV2, PayoutVoidV2, ServerAuthentication, + }, +}; + +use crate::{types::ResponseRouterData, with_error_response_body}; +use transformers as truelayer; +use transformers::{ + TruelayerAccessTokenErrorResponse, TruelayerErrorResponse, TruelayerPayoutRequest, + TruelayerPayoutResponse, TruelayerPayoutSyncType, TruelayerServerAuthenticationTokenRequest, + TruelayerServerAuthenticationTokenResponse, +}; + +const TL_SIGNATURE: &str = "Tl-Signature"; + +pub(crate) mod headers { + pub(crate) const CONTENT_TYPE: &str = "Content-Type"; + pub(crate) const AUTHORIZATION: &str = "Authorization"; + pub(crate) const IDEMPOTENCY_KEY: &str = "Idempotency-Key"; +} + +pub struct TruelayerPayouts; + +impl TruelayerPayouts { + pub const fn new() -> &'static Self { + &Self + } + + fn build_signed_headers( + &self, + req: &RouterDataV2< + PayoutTransfer, + PayoutFlowData, + PayoutTransferRequest, + PayoutTransferResponse, + >, + ) -> CustomResult)>, IntegrationError> { + let access_token = req.resource_common_data.get_access_token().map_err(|_| { + IntegrationError::FailedToObtainAuthType { + context: IntegrationErrorContext { + additional_context: Some( + "TrueLayer PayoutTransfer requires an OAuth access token".to_string(), + ), + ..Default::default() + }, + } + })?; + let metadata = truelayer::TruelayerPayoutMetadata::try_from(&req.connector_config)?; + let idempotency_key = uuid::Uuid::new_v4().to_string(); + let request_body = self + .get_request_body(req)? + .map(|body| body.content.get_inner_value().expose().clone()); + + let mut signed_headers = BTreeMap::new(); + signed_headers.insert( + headers::IDEMPOTENCY_KEY.to_string(), + idempotency_key.clone(), + ); + + let tl_signature = truelayer::generate_tl_signature( + common_utils::request::Method::Post.to_string(), + "/v3/payouts", + &signed_headers, + request_body.as_deref(), + metadata.private_key.expose().clone(), + metadata.kid.expose().as_str(), + )?; + + Ok(vec![ + ( + headers::AUTHORIZATION.to_string(), + format!("Bearer {access_token}").into_masked(), + ), + (TL_SIGNATURE.to_string(), tl_signature.into_masked()), + (headers::IDEMPOTENCY_KEY.to_string(), idempotency_key.into()), + ( + headers::CONTENT_TYPE.to_string(), + self.common_get_content_type().to_string().into(), + ), + ]) + } + + fn build_bearer_headers( + &self, + resource_common_data: &PayoutFlowData, + ) -> CustomResult)>, IntegrationError> { + let access_token = resource_common_data.get_access_token().map_err(|_| { + IntegrationError::FailedToObtainAuthType { + context: IntegrationErrorContext { + additional_context: Some( + "TrueLayer PayoutGet requires an OAuth access token".to_string(), + ), + ..Default::default() + }, + } + })?; + + Ok(vec![ + ( + headers::AUTHORIZATION.to_string(), + format!("Bearer {access_token}").into_masked(), + ), + ( + headers::CONTENT_TYPE.to_string(), + self.common_get_content_type().to_string().into(), + ), + ]) + } +} + +impl ConnectorCommon for TruelayerPayouts { + fn id(&self) -> &'static str { + "truelayer" + } + + fn get_currency_unit(&self) -> CurrencyUnit { + CurrencyUnit::Minor + } + + fn common_get_content_type(&self) -> &'static str { + "application/json; charset=UTF-8" + } + + fn base_url<'a>(&self, connectors: &'a Connectors) -> &'a str { + &connectors.truelayer.base_url + } + + fn build_error_response( + &self, + res: Response, + event_builder: Option<&mut events::Event>, + _connector_config: &ConnectorSpecificConfig, + ) -> CustomResult { + let response: TruelayerErrorResponse = res + .response + .parse_struct("TruelayerErrorResponse") + .change_context(ConnectorError::ResponseDeserializationFailed { + context: ResponseTransformationErrorContext { + http_status_code: Some(res.status_code), + additional_context: Some( + "TrueLayer payouts - failed to deserialize error response".to_string(), + ), + }, + })?; + + with_error_response_body!(event_builder, response); + + let typed_connector_response = crate::connectors::macros::serialize_typed_connector_payload( + &response, + "typed_connector_response", + ); + + Ok(ErrorResponse { + status_code: res.status_code, + code: response.title.clone(), + message: response + .errors + .clone() + .unwrap_or_else(|| serde_json::Value::String(response.title.clone())) + .to_string(), + reason: Some(response.detail.clone()), + attempt_status: None, + connector_transaction_id: Some(response.trace_id.clone()), + network_advice_code: None, + network_decline_code: None, + network_error_message: None, + typed_connector_response, + raw_connector_response: None, + raw_connector_request: None, + typed_connector_request: None, + }) + } +} + +impl PayoutServiceTrait for TruelayerPayouts {} +impl ServerAuthentication for TruelayerPayouts {} + +impl + ConnectorIntegrationV2< + ServerAuthenticationToken, + MerchantAuthenticationFlowData, + ServerAuthenticationTokenRequestData, + ServerAuthenticationTokenResponseData, + > for TruelayerPayouts +{ + fn get_http_method(&self) -> common_utils::request::Method { + common_utils::request::Method::Post + } + + fn get_content_type(&self) -> &'static str { + "application/x-www-form-urlencoded" + } + + fn get_url( + &self, + req: &RouterDataV2< + ServerAuthenticationToken, + MerchantAuthenticationFlowData, + ServerAuthenticationTokenRequestData, + ServerAuthenticationTokenResponseData, + >, + ) -> CustomResult { + let base_url = req + .resource_common_data + .connectors + .truelayer + .secondary_base_url + .as_ref() + .ok_or(IntegrationError::FailedToObtainIntegrationUrl { + context: IntegrationErrorContext { + additional_context: Some( + "TrueLayer payouts require secondary_base_url (the TrueLayer authentication server) to be configured for the connector." + .to_string(), + ), + ..Default::default() + }, + })?; + Ok(format!("{base_url}/connect/token")) + } + + fn get_headers( + &self, + _req: &RouterDataV2< + ServerAuthenticationToken, + MerchantAuthenticationFlowData, + ServerAuthenticationTokenRequestData, + ServerAuthenticationTokenResponseData, + >, + ) -> CustomResult)>, IntegrationError> { + Ok(vec![( + headers::CONTENT_TYPE.to_string(), + "application/x-www-form-urlencoded".to_string().into(), + )]) + } + + fn get_request_body( + &self, + req: &RouterDataV2< + ServerAuthenticationToken, + MerchantAuthenticationFlowData, + ServerAuthenticationTokenRequestData, + ServerAuthenticationTokenResponseData, + >, + ) -> CustomResult, IntegrationError> { + let connector_req = TruelayerServerAuthenticationTokenRequest::try_from(req)?; + let typed = events::MaskedSerdeValue::from_masked_optional( + &connector_req, + "typed_connector_request", + ); + Ok(Some(ConnectorRequestData::new( + RequestContent::FormUrlEncoded(Box::new(connector_req)), + typed, + ))) + } + + fn handle_response_v2( + &self, + data: &RouterDataV2< + ServerAuthenticationToken, + MerchantAuthenticationFlowData, + ServerAuthenticationTokenRequestData, + ServerAuthenticationTokenResponseData, + >, + event_builder: Option<&mut events::Event>, + res: Response, + ) -> CustomResult< + RouterDataV2< + ServerAuthenticationToken, + MerchantAuthenticationFlowData, + ServerAuthenticationTokenRequestData, + ServerAuthenticationTokenResponseData, + >, + ConnectorError, + > { + let response: TruelayerServerAuthenticationTokenResponse = res + .response + .parse_struct("TruelayerServerAuthenticationTokenResponse") + .change_context(ConnectorError::ResponseDeserializationFailed { + context: ResponseTransformationErrorContext { + http_status_code: Some(res.status_code), + additional_context: Some( + "TrueLayer access-token response deserialization failed".to_string(), + ), + }, + })?; + + event_builder.map(|event| event.set_connector_response(&response)); + + RouterDataV2::try_from(ResponseRouterData { + response, + router_data: data.clone(), + http_code: res.status_code, + }) + .change_context(ConnectorError::ResponseDeserializationFailed { + context: ResponseTransformationErrorContext { + http_status_code: Some(res.status_code), + additional_context: Some( + "TrueLayer access-token response mapping failed".to_string(), + ), + }, + }) + } + + fn get_error_response_v2( + &self, + res: Response, + event_builder: Option<&mut events::Event>, + _connector_config: &ConnectorSpecificConfig, + ) -> CustomResult { + let response: TruelayerAccessTokenErrorResponse = res + .response + .parse_struct("TruelayerAccessTokenErrorResponse") + .change_context(ConnectorError::ResponseDeserializationFailed { + context: ResponseTransformationErrorContext { + http_status_code: Some(res.status_code), + additional_context: Some( + "TrueLayer access-token error deserialization failed".to_string(), + ), + }, + })?; + + with_error_response_body!(event_builder, response); + + let typed_connector_response = crate::connectors::macros::serialize_typed_connector_payload( + &response, + "typed_connector_response", + ); + + Ok(ErrorResponse { + status_code: res.status_code, + code: response.error, + message: response + .error_description + .clone() + .unwrap_or_else(|| common_utils::consts::NO_ERROR_MESSAGE.to_string()), + reason: response.error_details.and_then(|details| details.reason), + attempt_status: None, + connector_transaction_id: None, + network_advice_code: None, + network_decline_code: None, + network_error_message: None, + typed_connector_response, + raw_connector_response: None, + raw_connector_request: None, + typed_connector_request: None, + }) + } +} + +impl PayoutTransferV2 for TruelayerPayouts {} + +impl + ConnectorIntegrationV2< + PayoutTransfer, + PayoutFlowData, + PayoutTransferRequest, + PayoutTransferResponse, + > for TruelayerPayouts +{ + fn get_http_method(&self) -> common_utils::request::Method { + common_utils::request::Method::Post + } + + fn get_url( + &self, + req: &RouterDataV2< + PayoutTransfer, + PayoutFlowData, + PayoutTransferRequest, + PayoutTransferResponse, + >, + ) -> CustomResult { + Ok(format!( + "{}/v3/payouts", + self.base_url(&req.resource_common_data.connectors) + )) + } + + fn get_headers( + &self, + req: &RouterDataV2< + PayoutTransfer, + PayoutFlowData, + PayoutTransferRequest, + PayoutTransferResponse, + >, + ) -> CustomResult)>, IntegrationError> { + self.build_signed_headers(req) + } + + fn get_request_body( + &self, + req: &RouterDataV2< + PayoutTransfer, + PayoutFlowData, + PayoutTransferRequest, + PayoutTransferResponse, + >, + ) -> CustomResult, IntegrationError> { + let connector_req = TruelayerPayoutRequest::try_from(req)?; + let typed = events::MaskedSerdeValue::from_masked_optional( + &connector_req, + "typed_connector_request", + ); + Ok(Some(ConnectorRequestData::new( + RequestContent::Json(Box::new(connector_req)), + typed, + ))) + } + + fn handle_response_v2( + &self, + data: &RouterDataV2< + PayoutTransfer, + PayoutFlowData, + PayoutTransferRequest, + PayoutTransferResponse, + >, + event_builder: Option<&mut events::Event>, + res: Response, + ) -> CustomResult< + RouterDataV2, + ConnectorError, + > { + let response: TruelayerPayoutResponse = res + .response + .parse_struct("TruelayerPayoutResponse") + .change_context(ConnectorError::ResponseDeserializationFailed { + context: ResponseTransformationErrorContext { + http_status_code: Some(res.status_code), + additional_context: Some( + "TrueLayer PayoutTransfer response deserialization failed".to_string(), + ), + }, + })?; + + event_builder.map(|event| event.set_connector_response(&response)); + + RouterDataV2::try_from(ResponseRouterData { + response, + router_data: data.clone(), + http_code: res.status_code, + }) + .change_context(ConnectorError::ResponseDeserializationFailed { + context: ResponseTransformationErrorContext { + http_status_code: Some(res.status_code), + additional_context: Some( + "TrueLayer PayoutTransfer response mapping failed".to_string(), + ), + }, + }) + } + + fn get_error_response_v2( + &self, + res: Response, + event_builder: Option<&mut events::Event>, + connector_config: &ConnectorSpecificConfig, + ) -> CustomResult { + self.build_error_response(res, event_builder, connector_config) + } +} + +impl PayoutGetV2 for TruelayerPayouts {} + +impl ConnectorIntegrationV2 + for TruelayerPayouts +{ + fn get_http_method(&self) -> common_utils::request::Method { + common_utils::request::Method::Get + } + + fn get_url( + &self, + req: &RouterDataV2, + ) -> CustomResult { + let connector_payout_id = req.request.connector_payout_id.as_ref().ok_or( + IntegrationError::MissingRequiredField { + field_name: "connector_payout_id", + context: IntegrationErrorContext { + additional_context: Some( + "TrueLayer payout sync requires the connector_payout_id returned by the transfer call." + .to_string(), + ), + ..Default::default() + }, + }, + )?; + + Ok(format!( + "{}/v3/payouts/{connector_payout_id}", + self.base_url(&req.resource_common_data.connectors) + )) + } + + fn get_headers( + &self, + req: &RouterDataV2, + ) -> CustomResult)>, IntegrationError> { + self.build_bearer_headers(&req.resource_common_data) + } + + fn handle_response_v2( + &self, + data: &RouterDataV2, + event_builder: Option<&mut events::Event>, + res: Response, + ) -> CustomResult< + RouterDataV2, + ConnectorError, + > { + let response: TruelayerPayoutSyncType = res + .response + .parse_struct("TruelayerPayoutSyncType") + .change_context(ConnectorError::ResponseDeserializationFailed { + context: ResponseTransformationErrorContext { + http_status_code: Some(res.status_code), + additional_context: Some( + "TrueLayer PayoutGet response deserialization failed".to_string(), + ), + }, + })?; + + event_builder.map(|event| event.set_connector_response(&response)); + + RouterDataV2::try_from(ResponseRouterData { + response, + router_data: data.clone(), + http_code: res.status_code, + }) + .change_context(ConnectorError::ResponseDeserializationFailed { + context: ResponseTransformationErrorContext { + http_status_code: Some(res.status_code), + additional_context: Some("TrueLayer PayoutGet response mapping failed".to_string()), + }, + }) + } + + fn get_error_response_v2( + &self, + res: Response, + event_builder: Option<&mut events::Event>, + connector_config: &ConnectorSpecificConfig, + ) -> CustomResult { + self.build_error_response(res, event_builder, connector_config) + } +} + +macro_rules! impl_unimplemented_payout_flow { + ($trait_name:ident, $flow:ty, $request:ty, $response:ty, $flow_name:literal) => { + impl $trait_name for TruelayerPayouts {} + + impl ConnectorIntegrationV2<$flow, PayoutFlowData, $request, $response> + for TruelayerPayouts + { + fn get_url( + &self, + _req: &RouterDataV2<$flow, PayoutFlowData, $request, $response>, + ) -> CustomResult { + Err(IntegrationError::connector_flow_not_implemented( + self.id(), + $flow_name, + Default::default(), + ) + .into()) + } + } + }; +} + +impl_unimplemented_payout_flow!( + PayoutCreateV2, + PayoutCreate, + PayoutCreateRequest, + PayoutCreateResponse, + "payout_create" +); +impl_unimplemented_payout_flow!( + PayoutVoidV2, + PayoutVoid, + PayoutVoidRequest, + PayoutVoidResponse, + "payout_void" +); +impl_unimplemented_payout_flow!( + PayoutStageV2, + PayoutStage, + PayoutStageRequest, + PayoutStageResponse, + "payout_stage" +); +impl_unimplemented_payout_flow!( + PayoutCreateLinkV2, + PayoutCreateLink, + PayoutCreateLinkRequest, + PayoutCreateLinkResponse, + "payout_create_link" +); +impl_unimplemented_payout_flow!( + PayoutCreateRecipientV2, + PayoutCreateRecipient, + PayoutCreateRecipientRequest, + PayoutCreateRecipientResponse, + "payout_create_recipient" +); +impl_unimplemented_payout_flow!( + PayoutEnrollDisburseAccountV2, + PayoutEnrollDisburseAccount, + PayoutEnrollDisburseAccountRequest, + PayoutEnrollDisburseAccountResponse, + "payout_enroll_disburse_account" +); +impl_unimplemented_payout_flow!( + PayoutEligibilityV2, + PayoutEligibility, + PayoutEligibilityRequest, + PayoutEligibilityResponse, + "payout_eligibility" +); diff --git a/crates/integrations/connector-integration/src/payout_connectors/truelayer/transformers.rs b/crates/integrations/connector-integration/src/payout_connectors/truelayer/transformers.rs new file mode 100644 index 0000000000..7c9593127b --- /dev/null +++ b/crates/integrations/connector-integration/src/payout_connectors/truelayer/transformers.rs @@ -0,0 +1,616 @@ +use std::collections::BTreeMap; + +use common_enums::{Currency, PayoutStatus}; +use common_utils::errors::CustomResult; +use common_utils::types::MinorUnit; +use domain_types::{ + connector_flow::{PayoutGet, PayoutTransfer, ServerAuthenticationToken}, + connector_types::{ + ServerAuthenticationTokenRequestData, ServerAuthenticationTokenResponseData, + }, + errors::{ConnectorError, IntegrationError, IntegrationErrorContext}, + merchant_authentication_flow_data::MerchantAuthenticationFlowData, + payouts::{ + payout_method_data::{Bank, PayoutMethodData}, + payouts_types::{ + PayoutFlowData, PayoutGetRequest, PayoutGetResponse, PayoutTransferRequest, + PayoutTransferResponse, + }, + }, + router_data::{ConnectorSpecificConfig, ErrorResponse}, + router_data_v2::RouterDataV2, +}; +use error_stack::ResultExt; +use hyperswitch_masking::Secret; +use josekit::jws::{JwsHeader, ES512}; +use serde::{Deserialize, Serialize}; + +use crate::types::ResponseRouterData; + +const GRANT_TYPE: &str = "client_credentials"; +const SCOPE: &str = "payments"; + +pub struct TruelayerAuthType { + client_id: Secret, + client_secret: Secret, + merchant_account_id: Option>, + private_key: Option>, + kid: Option>, +} + +impl TryFrom<&ConnectorSpecificConfig> for TruelayerAuthType { + type Error = error_stack::Report; + + fn try_from(config: &ConnectorSpecificConfig) -> Result { + match config { + ConnectorSpecificConfig::Truelayer { + client_id, + client_secret, + merchant_account_id, + private_key, + kid, + .. + } => Ok(Self { + client_id: client_id.clone(), + client_secret: client_secret.clone(), + merchant_account_id: merchant_account_id.clone(), + private_key: private_key.clone(), + kid: kid.clone(), + }), + _ => Err(IntegrationError::FailedToObtainAuthType { + context: IntegrationErrorContext { + additional_context: Some( + "TrueLayer payouts require ConnectorSpecificConfig::Truelayer with client_id and client_secret supplied via x-connector-config." + .to_string(), + ), + ..Default::default() + }, + } + .into()), + } + } +} + +pub struct TruelayerPayoutMetadata { + pub merchant_account_id: Secret, + pub private_key: Secret, + pub kid: Secret, +} + +impl TryFrom<&ConnectorSpecificConfig> for TruelayerPayoutMetadata { + type Error = error_stack::Report; + + fn try_from(config: &ConnectorSpecificConfig) -> Result { + let auth = TruelayerAuthType::try_from(config)?; + Ok(Self { + merchant_account_id: auth.merchant_account_id.ok_or( + IntegrationError::MissingRequiredField { + field_name: "merchant_account_id", + context: IntegrationErrorContext { + additional_context: Some( + "TrueLayer payouts require merchant_account_id in the connector metadata; it identifies the TrueLayer merchant account funding the transfer." + .to_string(), + ), + ..Default::default() + }, + }, + )?, + private_key: auth + .private_key + .ok_or(IntegrationError::MissingRequiredField { + field_name: "private_key", + context: IntegrationErrorContext { + additional_context: Some( + "TrueLayer payouts require private_key in the connector metadata: a base64-encoded EC private key PEM used for ES512 JWS request signing." + .to_string(), + ), + ..Default::default() + }, + })?, + kid: auth.kid.ok_or(IntegrationError::MissingRequiredField { + field_name: "kid", + context: IntegrationErrorContext { + additional_context: Some( + "TrueLayer payouts require kid in the connector metadata; it must match the signing key registered in the TrueLayer console." + .to_string(), + ), + ..Default::default() + }, + })?, + }) + } +} + +#[derive(Debug, Clone, Serialize, PartialEq)] +pub struct TruelayerServerAuthenticationTokenRequest { + grant_type: String, + client_id: Secret, + client_secret: Secret, + scope: String, +} + +impl + TryFrom< + &RouterDataV2< + ServerAuthenticationToken, + MerchantAuthenticationFlowData, + ServerAuthenticationTokenRequestData, + ServerAuthenticationTokenResponseData, + >, + > for TruelayerServerAuthenticationTokenRequest +{ + type Error = error_stack::Report; + + fn try_from( + req: &RouterDataV2< + ServerAuthenticationToken, + MerchantAuthenticationFlowData, + ServerAuthenticationTokenRequestData, + ServerAuthenticationTokenResponseData, + >, + ) -> Result { + let auth = TruelayerAuthType::try_from(&req.connector_config)?; + Ok(Self { + grant_type: GRANT_TYPE.to_string(), + client_id: auth.client_id, + client_secret: auth.client_secret, + scope: SCOPE.to_string(), + }) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct TruelayerServerAuthenticationTokenResponse { + access_token: Secret, + expires_in: i64, + token_type: Option, +} + +impl TryFrom> + for RouterDataV2< + ServerAuthenticationToken, + MerchantAuthenticationFlowData, + ServerAuthenticationTokenRequestData, + ServerAuthenticationTokenResponseData, + > +{ + type Error = error_stack::Report; + + fn try_from( + item: ResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(ServerAuthenticationTokenResponseData { + access_token: item.response.access_token, + expires_in: Some(item.response.expires_in), + token_type: item.response.token_type, + }), + ..item.router_data + }) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct TruelayerAccessTokenErrorResponse { + pub error: String, + pub error_description: Option, + pub error_details: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct TruelayerErrorDetails { + pub reason: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct TruelayerErrorResponse { + #[serde(rename = "type")] + pub _type: String, + pub title: String, + pub status: i32, + pub trace_id: String, + pub detail: String, + pub errors: Option, +} + +#[derive(Debug, Serialize, PartialEq)] +pub struct TruelayerPayoutRequest { + merchant_account_id: Secret, + amount_in_minor: MinorUnit, + currency: Currency, + #[serde(skip_serializing_if = "Option::is_none")] + metadata: Option, + beneficiary: TruelayerBeneficiary, +} + +#[derive(Debug, Serialize, PartialEq)] +pub struct TruelayerPayoutRequestMetadata { + reference_id: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct TruelayerBeneficiary { + #[serde(rename = "type")] + _type: TruelayerBeneficiaryType, + reference: String, + #[serde(skip_serializing_if = "Option::is_none")] + account_holder_name: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + account_identifier: Option, + #[serde(skip_serializing_if = "Option::is_none")] + payment_source_id: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + user_id: Option>, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum TruelayerBeneficiaryType { + ExternalAccount, + PaymentSource, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct TruelayerAccountIdentifier { + #[serde(rename = "type")] + _type: String, + iban: Secret, +} + +impl + TryFrom< + &RouterDataV2< + PayoutTransfer, + PayoutFlowData, + PayoutTransferRequest, + PayoutTransferResponse, + >, + > for TruelayerPayoutRequest +{ + type Error = error_stack::Report; + + fn try_from( + req: &RouterDataV2< + PayoutTransfer, + PayoutFlowData, + PayoutTransferRequest, + PayoutTransferResponse, + >, + ) -> Result { + let reference = normalize_connector_request_reference_id( + &req.resource_common_data.connector_request_reference_id, + ); + + let metadata = TruelayerPayoutMetadata::try_from(&req.connector_config)?; + let request_metadata = Some(TruelayerPayoutRequestMetadata { + reference_id: Some( + req.resource_common_data + .connector_request_reference_id + .clone(), + ), + }); + + let beneficiary = match req.request.payout_method_data.as_ref() { + Some(PayoutMethodData::Bank(Bank::OpenBanking(open_banking))) => TruelayerBeneficiary { + _type: TruelayerBeneficiaryType::ExternalAccount, + reference: reference.clone(), + account_holder_name: Some(open_banking.account_holder_name.clone()), + account_identifier: Some(TruelayerAccountIdentifier { + _type: "iban".to_string(), + iban: open_banking.iban.clone(), + }), + payment_source_id: None, + user_id: None, + }, + Some(PayoutMethodData::Passthrough(passthrough)) => TruelayerBeneficiary { + _type: TruelayerBeneficiaryType::PaymentSource, + reference: reference.clone(), + account_holder_name: None, + account_identifier: None, + payment_source_id: Some(passthrough.psp_token.clone()), + user_id: passthrough.psp_customer_id.clone(), + }, + Some(_) | None => { + return Err(IntegrationError::connector_feature_not_supported( + "truelayer", + "selected payout method", + Default::default(), + ) + .into()); + } + }; + + Ok(Self { + merchant_account_id: metadata.merchant_account_id, + amount_in_minor: req.request.amount, + currency: req.request.destination_currency, + metadata: request_metadata, + beneficiary, + }) + } +} + +fn normalize_connector_request_reference_id(reference_id: &str) -> String { + reference_id + .chars() + .map(|character| if character == '_' { '-' } else { character }) + .collect() +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct TruelayerPayoutResponse { + id: String, +} + +impl TryFrom> + for RouterDataV2 +{ + type Error = error_stack::Report; + + fn try_from( + item: ResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(PayoutTransferResponse { + merchant_payout_id: item.router_data.request.merchant_payout_id.clone(), + payout_status: PayoutStatus::Initiated, + connector_payout_id: Some(item.response.id), + status_code: item.http_code, + }), + ..item.router_data + }) + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(untagged)] +pub enum TruelayerPayoutSyncType { + Sync(TruelayerPayoutSyncResponse), + Webhook(TruelayerPayoutsWebhookBody), +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct TruelayerPayoutSyncResponse { + id: String, + merchant_account_id: Secret, + amount_in_minor: MinorUnit, + currency: Currency, + beneficiary: TruelayerBeneficiary, + scheme_id: Option, + status: TruelayerPayoutStatus, + failed_at: Option, + failure_reason: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +enum TruelayerPayoutStatus { + Pending, + AuthorizationRequired, + Failed, + Authorizing, + Authorized, + Executed, +} + +impl From for PayoutStatus { + fn from(status: TruelayerPayoutStatus) -> Self { + match status { + TruelayerPayoutStatus::Pending + | TruelayerPayoutStatus::AuthorizationRequired + | TruelayerPayoutStatus::Authorizing => Self::Pending, + TruelayerPayoutStatus::Authorized | TruelayerPayoutStatus::Executed => Self::Success, + TruelayerPayoutStatus::Failed => Self::Failure, + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum TruelayerPayoutsWebhookEvent { + PayoutExecuted, + PayoutFailed, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct TruelayerPayoutsWebhookBody { + #[serde(rename = "type")] + pub _type: TruelayerPayoutsWebhookEvent, + pub event_version: i32, + pub event_id: String, + pub payout_id: String, + pub executed_at: Option, + pub failed_at: Option, + pub failure_reason: Option, + pub scheme_id: Option, +} + +impl TryFrom> + for RouterDataV2 +{ + type Error = error_stack::Report; + + fn try_from( + item: ResponseRouterData, + ) -> Result { + match item.response { + TruelayerPayoutSyncType::Sync(sync_response) => { + let status = PayoutStatus::from(sync_response.status); + build_payout_get_router_data( + item.router_data, + item.http_code, + sync_response.id, + status, + sync_response.failure_reason, + None, + ) + } + TruelayerPayoutSyncType::Webhook(webhook_response) => { + let status = match webhook_response._type { + TruelayerPayoutsWebhookEvent::PayoutExecuted => PayoutStatus::Success, + TruelayerPayoutsWebhookEvent::PayoutFailed => PayoutStatus::Failure, + }; + build_payout_get_router_data( + item.router_data, + item.http_code, + webhook_response.payout_id, + status, + webhook_response.failure_reason, + Some(webhook_response.event_id), + ) + } + } + } +} + +fn build_payout_get_router_data( + router_data: RouterDataV2, + http_code: u16, + connector_payout_id: String, + status: PayoutStatus, + failure_reason: Option, + connector_response_reference_id: Option, +) -> Result< + RouterDataV2, + error_stack::Report, +> { + let response = if status == PayoutStatus::Failure { + let reason = + failure_reason.unwrap_or_else(|| common_utils::consts::NO_ERROR_MESSAGE.to_string()); + Err(ErrorResponse { + code: reason.clone(), + message: reason.clone(), + reason: Some(reason), + attempt_status: None, + connector_transaction_id: Some(connector_payout_id), + status_code: http_code, + network_advice_code: None, + network_decline_code: None, + network_error_message: None, + typed_connector_response: None, + raw_connector_response: None, + raw_connector_request: None, + typed_connector_request: None, + }) + } else { + Ok(PayoutGetResponse { + merchant_payout_id: router_data.request.merchant_payout_id.clone(), + payout_status: status, + connector_payout_id: Some(connector_payout_id), + status_code: http_code, + }) + }; + + let _ = connector_response_reference_id; + + Ok(RouterDataV2 { + response, + ..router_data + }) +} + +pub fn generate_tl_signature( + method: String, + path: &str, + headers: &BTreeMap, + body: Option<&str>, + private_key: String, + kid: &str, +) -> CustomResult { + let payload = build_payload(method, path, headers, body); + let pem = domain_types::utils::base64_decode(private_key).change_context( + IntegrationError::RequestEncodingFailed { + context: IntegrationErrorContext { + additional_context: Some( + "Failed to base64-decode the TrueLayer private_key from connector metadata; it must be the base64 encoding of an EC private key PEM." + .to_string(), + ), + ..Default::default() + }, + }, + )?; + + let signer = + ES512 + .signer_from_pem(&pem) + .change_context(IntegrationError::RequestEncodingFailed { + context: IntegrationErrorContext { + additional_context: Some( + "Failed to build the ES512 signer from the TrueLayer private key; the decoded value must be a valid EC private key PEM." + .to_string(), + ), + ..Default::default() + }, + })?; + + let tl_headers = headers.keys().cloned().collect::>().join(","); + + let mut header = JwsHeader::new(); + header.set_algorithm("ES512"); + header.set_key_id(kid); + header + .set_claim("tl_version", Some("2".into())) + .change_context(IntegrationError::RequestEncodingFailed { + context: IntegrationErrorContext { + additional_context: Some( + "Failed to set the tl_version claim on the TrueLayer JWS header.".to_string(), + ), + ..Default::default() + }, + })?; + header + .set_claim("tl_headers", Some(tl_headers.into())) + .change_context(IntegrationError::RequestEncodingFailed { + context: IntegrationErrorContext { + additional_context: Some( + "Failed to set the tl_headers claim on the TrueLayer JWS header.".to_string(), + ), + ..Default::default() + }, + })?; + + let jws = josekit::jws::serialize_compact(payload.as_bytes(), &header, &signer) + .change_context(IntegrationError::RequestEncodingFailed { + context: IntegrationErrorContext { + additional_context: Some( + "Failed to serialize the TrueLayer Tl-Signature JWS.".to_string(), + ), + ..Default::default() + }, + })?; + + let parts: Vec<&str> = jws.split('.').collect(); + + match (parts.first(), parts.get(2)) { + (Some(first), Some(third)) => Ok(format!("{first}..{third}")), + _ => Err(IntegrationError::RequestEncodingFailed { + context: IntegrationErrorContext { + additional_context: Some( + "The generated TrueLayer JWS did not contain the expected header and signature segments." + .to_string(), + ), + ..Default::default() + }, + } + .into()), + } +} + +fn build_payload( + method: String, + path: &str, + headers: &BTreeMap, + body: Option<&str>, +) -> String { + let mut payload = format!("{} {}\n", method.to_uppercase(), path.trim_end_matches('/')); + + for (key, value) in headers { + payload.push_str(&format!("{key}: {value}\n")); + } + + if let Some(body) = body { + payload.push_str(body); + } + + payload +} diff --git a/crates/integrations/connector-integration/src/payout_connectors/trustly.rs b/crates/integrations/connector-integration/src/payout_connectors/trustly.rs new file mode 100644 index 0000000000..305c2e2e23 --- /dev/null +++ b/crates/integrations/connector-integration/src/payout_connectors/trustly.rs @@ -0,0 +1,382 @@ +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::{ + PayoutCreateRecipient, PayoutEligibility, PayoutGet, PayoutTransfer, + ServerAuthenticationToken, + }, + connector_types::{ + ServerAuthenticationTokenRequestData, ServerAuthenticationTokenResponseData, + }, + errors::{ + ConnectorError, IntegrationError, IntegrationErrorContext, + ResponseTransformationErrorContext, + }, + merchant_authentication_flow_data::MerchantAuthenticationFlowData, + payment_method_data::PaymentMethodDataTypes, + payouts::payouts_types::{ + PayoutCreateRecipientRequest, PayoutCreateRecipientResponse, PayoutEligibilityRequest, + PayoutEligibilityResponse, PayoutFlowData, PayoutGetRequest, PayoutGetResponse, + PayoutTransferRequest, PayoutTransferResponse, + }, + 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::{ + PayoutCreateRecipientV2, PayoutEligibilityV2, PayoutGetV2, PayoutServiceTrait, + PayoutTransferV2, ServerAuthentication, + }, +}; +use serde::Serialize; + +use super::super::connectors::macros; +// Reuse the shared Trustly error-response type for parsing connector errors. +use crate::connectors::trustly::transformers::TrustlyErrorResponse; +use crate::types::ResponseRouterData; +use transformers::{ + AccountPayoutRequest, AccountPayoutResponse, RegisterAccountRequest, RegisterAccountResponse, + TrustlyPayoutSyncRequest, TrustlyPayoutSyncResponse, +}; + +pub(crate) mod headers { + pub(crate) const CONTENT_TYPE: &str = "Content-Type"; +} + +const CONTENT_TYPE_JSON: &str = "application/json; charset=UTF-8"; + +macros::create_all_prerequisites!( + connector_name: TrustlyPayouts, + generic_type: T, + api: [ + ( + flow: PayoutCreateRecipient, + request_body: RegisterAccountRequest, + response_body: RegisterAccountResponse, + router_data: RouterDataV2, + ), + ( + flow: PayoutTransfer, + request_body: AccountPayoutRequest, + response_body: AccountPayoutResponse, + router_data: RouterDataV2, + ), + ( + flow: PayoutGet, + request_body: TrustlyPayoutSyncRequest, + response_body: TrustlyPayoutSyncResponse, + router_data: RouterDataV2, + ) + ], + amount_converters: [], + member_functions: { + // Trustly authenticates each request in the JSON-RPC body (username / password + // / signature), so only the content-type header is required. + fn payout_headers(&self) -> Vec<(String, Maskable)> { + vec![( + headers::CONTENT_TYPE.to_string(), + CONTENT_TYPE_JSON.to_string().into(), + )] + } + } +); + +// ===== CONNECTOR COMMON ===== + +impl ConnectorCommon + for TrustlyPayouts +{ + fn id(&self) -> &'static str { + "trustly" + } + + fn get_currency_unit(&self) -> CurrencyUnit { + CurrencyUnit::Base + } + + fn common_get_content_type(&self) -> &'static str { + CONTENT_TYPE_JSON + } + + fn base_url<'a>(&self, connectors: &'a Connectors) -> &'a str { + connectors.trustly.base_url.as_ref() + } + + fn get_auth_header( + &self, + _auth_type: &ConnectorSpecificConfig, + ) -> CustomResult)>, IntegrationError> { + Ok(Vec::new()) + } + + fn build_error_response( + &self, + res: Response, + event_builder: Option<&mut events::Event>, + _connector_config: &ConnectorSpecificConfig, + ) -> CustomResult { + let response: TrustlyErrorResponse = res + .response + .parse_struct("TrustlyErrorResponse") + .change_context(ConnectorError::ResponseDeserializationFailed { + context: ResponseTransformationErrorContext { + http_status_code: Some(res.status_code), + additional_context: Some( + "Trustly payout - failed to deserialize error response".to_string(), + ), + }, + })?; + + event_builder.map(|i| i.set_connector_response(&response)); + + let typed = + macros::serialize_typed_connector_payload(&response, "typed_connector_response"); + Ok(ErrorResponse { + code: response.error.code.to_string(), + message: response.error.message.clone(), + reason: Some(response.error.message), + status_code: res.status_code, + attempt_status: None, + connector_transaction_id: Some(response.error.error.uuid), + network_advice_code: None, + network_decline_code: None, + network_error_message: None, + typed_connector_response: typed, + raw_connector_response: None, + raw_connector_request: None, + typed_connector_request: None, + }) + } +} + +// ===== SERVER AUTHENTICATION (not implemented) ===== + +impl ServerAuthentication + for TrustlyPayouts +{ +} + +impl + ConnectorIntegrationV2< + ServerAuthenticationToken, + MerchantAuthenticationFlowData, + ServerAuthenticationTokenRequestData, + ServerAuthenticationTokenResponseData, + > for TrustlyPayouts +{ + fn get_url( + &self, + _req: &RouterDataV2< + ServerAuthenticationToken, + MerchantAuthenticationFlowData, + ServerAuthenticationTokenRequestData, + ServerAuthenticationTokenResponseData, + >, + ) -> CustomResult { + Err(IntegrationError::connector_flow_not_implemented( + ConnectorCommon::id(self), + "server_authentication_token", + IntegrationErrorContext::default(), + ) + .into()) + } +} + +// ===== PAYOUT SERVICE TRAIT ===== + +impl PayoutServiceTrait + for TrustlyPayouts +{ +} + +impl PayoutCreateRecipientV2 + for TrustlyPayouts +{ +} + +impl PayoutTransferV2 + for TrustlyPayouts +{ +} + +impl PayoutGetV2 + for TrustlyPayouts +{ +} + +// ===== PAYOUT CREATE RECIPIENT (RegisterAccount) ===== + +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: TrustlyPayouts, + curl_request: Json(RegisterAccountRequest), + curl_response: RegisterAccountResponse, + flow_name: PayoutCreateRecipient, + resource_common_data: PayoutFlowData, + flow_request: PayoutCreateRecipientRequest, + flow_response: PayoutCreateRecipientResponse, + http_method: Post, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize], + other_functions: { + fn get_url( + &self, + req: &RouterDataV2< + PayoutCreateRecipient, + PayoutFlowData, + PayoutCreateRecipientRequest, + PayoutCreateRecipientResponse, + >, + ) -> CustomResult { + Ok(self + .base_url(&req.resource_common_data.connectors) + .to_string()) + } + + fn get_headers( + &self, + _req: &RouterDataV2< + PayoutCreateRecipient, + PayoutFlowData, + PayoutCreateRecipientRequest, + PayoutCreateRecipientResponse, + >, + ) -> CustomResult)>, IntegrationError> { + Ok(self.payout_headers()) + } + } +); + +// ===== PAYOUT TRANSFER (AccountPayout) ===== + +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: TrustlyPayouts, + curl_request: Json(AccountPayoutRequest), + curl_response: AccountPayoutResponse, + flow_name: PayoutTransfer, + resource_common_data: PayoutFlowData, + flow_request: PayoutTransferRequest, + flow_response: PayoutTransferResponse, + http_method: Post, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize], + other_functions: { + fn get_url( + &self, + req: &RouterDataV2< + PayoutTransfer, + PayoutFlowData, + PayoutTransferRequest, + PayoutTransferResponse, + >, + ) -> CustomResult { + Ok(self + .base_url(&req.resource_common_data.connectors) + .to_string()) + } + + fn get_headers( + &self, + _req: &RouterDataV2< + PayoutTransfer, + PayoutFlowData, + PayoutTransferRequest, + PayoutTransferResponse, + >, + ) -> CustomResult)>, IntegrationError> { + Ok(self.payout_headers()) + } + } +); + +// ===== PAYOUT GET (GetWithdrawals) ===== + +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: TrustlyPayouts, + curl_request: Json(TrustlyPayoutSyncRequest), + curl_response: TrustlyPayoutSyncResponse, + flow_name: PayoutGet, + resource_common_data: PayoutFlowData, + flow_request: PayoutGetRequest, + flow_response: PayoutGetResponse, + http_method: Post, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize], + other_functions: { + fn get_url( + &self, + req: &RouterDataV2, + ) -> CustomResult { + Ok(self + .base_url(&req.resource_common_data.connectors) + .to_string()) + } + + fn get_headers( + &self, + _req: &RouterDataV2, + ) -> CustomResult)>, IntegrationError> { + Ok(self.payout_headers()) + } + } +); + +// ===== PAYOUT STUB FLOWS (not supported by Trustly) ===== + +macros::macro_connector_payout_implementation!( + connector: TrustlyPayouts, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize], + payout_flows: [ + PayoutCreate, + PayoutVoid, + PayoutStage, + PayoutCreateLink, + PayoutEnrollDisburseAccount + ] +); + +// `PayoutEligibility` has no arm in `macro_connector_payout_implementation!`, +// so its stub is still written out by hand. +impl PayoutEligibilityV2 + for TrustlyPayouts +{ +} + +impl + ConnectorIntegrationV2< + PayoutEligibility, + PayoutFlowData, + PayoutEligibilityRequest, + PayoutEligibilityResponse, + > for TrustlyPayouts +{ + fn get_url( + &self, + _req: &RouterDataV2< + PayoutEligibility, + PayoutFlowData, + PayoutEligibilityRequest, + PayoutEligibilityResponse, + >, + ) -> CustomResult { + Err(IntegrationError::connector_flow_not_implemented( + ConnectorCommon::id(self), + "payout_eligibility", + IntegrationErrorContext::default(), + ) + .into()) + } +} diff --git a/crates/integrations/connector-integration/src/payout_connectors/trustly/transformers.rs b/crates/integrations/connector-integration/src/payout_connectors/trustly/transformers.rs new file mode 100644 index 0000000000..e1020f6cd1 --- /dev/null +++ b/crates/integrations/connector-integration/src/payout_connectors/trustly/transformers.rs @@ -0,0 +1,906 @@ +use super::TrustlyPayoutsRouterData; +use crate::types::ResponseRouterData; +use base64::{engine::general_purpose, Engine}; +use common_utils::types::{StringMajorUnit, StringMajorUnitForConnector}; +use domain_types::{ + connector_flow::{PayoutCreateRecipient, PayoutGet, PayoutTransfer}, + errors::{ + ConnectorError, IntegrationError, IntegrationErrorContext, + ResponseTransformationErrorContext, + }, + payment_method_data::PaymentMethodDataTypes, + payouts::{ + payout_method_data::{Bank, PayoutMethodData}, + payouts_types::{ + PayoutCreateRecipientRequest, PayoutCreateRecipientResponse, PayoutFlowData, + PayoutGetRequest, PayoutGetResponse, PayoutTransferRequest, PayoutTransferResponse, + }, + }, + router_data::ConnectorSpecificConfig, + router_data_v2::RouterDataV2, + utils as domain_utils, +}; +use error_stack::Report; +use hyperswitch_masking::{ExposeInterface, Secret}; +use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa, sign::Signer}; +use serde::{Deserialize, Serialize}; +use std::{collections::BTreeMap, fmt::Debug}; + +// The Trustly error-response and webhook payload types are shared with the payment +// connector implementation. +use crate::connectors::trustly::transformers::{ + TrustlyErrorResponse, TrustlyWebhookBody, TrustlyWebhookMethod, +}; + +const TRUSTLY_VERSION: &str = "1.1"; + +// ===== AUTH TYPE ===== + +#[derive(Debug)] +pub struct TrustlyAuthType { + pub username: Secret, + pub password: Secret, + pub private_key: Secret, +} + +impl TryFrom<&ConnectorSpecificConfig> for TrustlyAuthType { + type Error = Report; + fn try_from(auth_type: &ConnectorSpecificConfig) -> Result { + match auth_type { + ConnectorSpecificConfig::Trustly { + username, + password, + private_key, + .. + } => Ok(Self { + username: username.clone(), + password: password.clone(), + private_key: private_key.clone(), + }), + _ => Err(IntegrationError::FailedToObtainAuthType { + context: Default::default(), + } + .into()), + } + } +} + +// ===== SHARED TYPES ===== + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +enum TrustlyMethod { + RegisterAccount, + AccountPayout, + GetWithdrawals, +} + +impl TrustlyMethod { + fn as_str(&self) -> &'static str { + match self { + Self::RegisterAccount => "RegisterAccount", + Self::AccountPayout => "AccountPayout", + Self::GetWithdrawals => "GetWithdrawals", + } + } +} + +// ===== REQUEST SIGNING ===== +// +// Trustly signs every JSON-RPC method with RSA-SHA256 over `method + uuid` followed +// by a canonical serialization of the payload. The payment connector implements the +// same scheme for its own methods; it is duplicated here so the payout connector does +// not depend on the payment connector's internals. + +/// Signature prefix Trustly expects for RSA-SHA256. The payout methods are always +/// signed with SHA256, so it is fixed rather than negotiated. +const TRUSTLY_SIGNATURE_PREFIX: &str = "alg=RS256;"; + +/// Canonical serialization used as the signature plaintext: object keys sorted, +/// null values dropped, and every scalar concatenated without separators. +fn serialize_value(value: &serde_json::Value) -> String { + match value { + serde_json::Value::Object(map) => { + let sorted: BTreeMap<_, _> = map.iter().collect(); + sorted + .iter() + .filter(|(_, v)| !v.is_null()) + .map(|(k, v)| format!("{}{}", k, serialize_value(v))) + .collect() + } + serde_json::Value::Array(arr) => arr.iter().map(serialize_value).collect(), + serde_json::Value::String(s) => s.clone(), + serde_json::Value::Number(n) => n.to_string(), + serde_json::Value::Bool(b) => b.to_string(), + serde_json::Value::Null => String::new(), + } +} + +fn trustly_serialize(data: &T) -> String { + let value = serde_json::to_value(data).unwrap_or_default(); + serialize_value(&value) +} + +fn generate_trustly_signature( + method: &str, + uuid: &str, + data: &T, + private_key: &str, +) -> Result { + let encoding_failed = || IntegrationError::RequestEncodingFailed { + context: Default::default(), + }; + + let pem = + domain_utils::base64_decode(private_key.to_string()).map_err(|_| encoding_failed())?; + let rsa = Rsa::private_key_from_pem(&pem).map_err(|_| encoding_failed())?; + let private_key = PKey::from_rsa(rsa).map_err(|_| encoding_failed())?; + + let plaintext = format!("{}{}{}", method, uuid, trustly_serialize(data)); + + let mut signer = + Signer::new(MessageDigest::sha256(), &private_key).map_err(|_| encoding_failed())?; + signer + .update(plaintext.as_bytes()) + .map_err(|_| encoding_failed())?; + let signature = signer.sign_to_vec().map_err(|_| encoding_failed())?; + + Ok(format!( + "{TRUSTLY_SIGNATURE_PREFIX}{}", + general_purpose::STANDARD.encode(&signature) + )) +} + +fn unsupported_payout_method_error(flow: &str) -> Report { + IntegrationError::NotSupported { + message: "Payout method is not supported".to_string(), + connector: "Trustly", + context: IntegrationErrorContext { + additional_context: Some(format!( + "Trustly {flow} - only the Trustly bank transfer payout method is supported" + )), + suggested_action: Some("Use a Trustly bank transfer payout method".to_string()), + doc_url: None, + }, + } + .into() +} + +fn missing_field(field_name: &'static str, flow: &str) -> Report { + IntegrationError::MissingRequiredField { + field_name, + context: IntegrationErrorContext { + additional_context: Some(format!("Trustly {flow} - missing required field")), + suggested_action: None, + doc_url: None, + }, + } + .into() +} + +#[derive(Debug, Deserialize)] +pub struct TrustlyAccountId { + account_id: Secret, +} + +fn to_payout_connector_meta( + connector_meta: Option, +) -> Result> { + let json = connector_meta + .ok_or_else(|| missing_field("payout_connector_metadata", "Payout Transfer"))?; + serde_json::from_value(json).map_err(|_| { + IntegrationError::InvalidDataFormat { + field_name: "payout_connector_metadata", + context: IntegrationErrorContext { + additional_context: Some( + "Trustly Payout Transfer - failed to parse payout_connector_metadata" + .to_string(), + ), + suggested_action: None, + doc_url: None, + }, + } + .into() + }) +} + +// ===== PAYOUT CREATE RECIPIENT (RegisterAccount) ===== + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct RegisterAccountRequest { + method: TrustlyMethod, + params: RegisterAccountParams, + version: String, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "PascalCase")] +pub struct RegisterAccountParams { + data: RegisterAccountData, + signature: Secret, + #[serde(rename = "UUID")] + uuid: String, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "PascalCase")] +pub struct RegisterAccountData { + account_number: Secret, + bank_number: Secret, + clearing_house: String, + end_user_i_d: String, + firstname: Secret, + lastname: Secret, + username: Secret, + password: Secret, + #[serde(skip_serializing_if = "Option::is_none")] + attributes: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "PascalCase")] +pub struct RegisterAccountAttributes { + #[serde(skip_serializing_if = "Option::is_none")] + address_country: Option, + #[serde(skip_serializing_if = "Option::is_none")] + address_line1: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + address_line2: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + address_city: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + address_postal_code: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + mobile_phone: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + email: Option, +} + +/// Derive the payee's first and last name from the customer name or, failing that, +/// from the billing address. +fn get_recipient_names( + req: &PayoutCreateRecipientRequest, +) -> Result<(Secret, Secret), Report> { + let customer_name = req + .customer + .as_ref() + .and_then(|c| c.name.clone()) + .map(Secret::new); + + if let (Some(first), Some(last)) = domain_utils::split_full_name(customer_name) { + return Ok((first, last)); + } + + let billing_address = req.get_optional_billing_address(); + match ( + billing_address.and_then(|addr| addr.get_optional_first_name()), + billing_address.and_then(|addr| addr.get_optional_last_name()), + ) { + (Some(first), Some(last)) => Ok((first, last)), + _ => Err(missing_field( + "customer.name / billing first_name and last_name", + "Payout Create Recipient", + )), + } +} + +impl + TryFrom< + TrustlyPayoutsRouterData< + RouterDataV2< + PayoutCreateRecipient, + PayoutFlowData, + PayoutCreateRecipientRequest, + PayoutCreateRecipientResponse, + >, + T, + >, + > for RegisterAccountRequest +{ + type Error = Report; + + fn try_from( + item: TrustlyPayoutsRouterData< + RouterDataV2< + PayoutCreateRecipient, + PayoutFlowData, + PayoutCreateRecipientRequest, + PayoutCreateRecipientResponse, + >, + T, + >, + ) -> Result { + let item = &item.router_data; + let trustly_data = match item.request.payout_method_data.as_ref() { + Some(PayoutMethodData::Bank(Bank::Trustly(data))) => data, + _ => return Err(unsupported_payout_method_error("Payout Create Recipient")), + }; + + let (account_number, bank_number) = if let Some(iban) = trustly_data.iban.clone() { + (iban, Secret::new(String::new())) + } else { + ( + trustly_data.bank_account_number.clone().ok_or_else(|| { + missing_field("bank_account_number", "Payout Create Recipient") + })?, + trustly_data + .bank_number + .clone() + .ok_or_else(|| missing_field("bank_number", "Payout Create Recipient"))?, + ) + }; + + let (first_name, last_name) = get_recipient_names(&item.request)?; + + let end_user_id = item + .request + .customer + .as_ref() + .and_then(|c| c.merchant_customer_id.clone()) + .ok_or_else(|| { + missing_field("customer.merchant_customer_id", "Payout Create Recipient") + })?; + + let billing = item + .request + .address + .as_ref() + .and_then(|a| a.billing_address.as_ref()); + let customer_email = item.request.customer.as_ref().and_then(|c| c.email.clone()); + + let attributes = if billing.is_some() || customer_email.is_some() { + let billing_address = item.request.get_optional_billing_address(); + Some(RegisterAccountAttributes { + address_city: billing_address.and_then(|addr| addr.get_optional_city()), + address_country: billing_address.and_then(|addr| addr.get_optional_country()), + address_line1: billing_address.and_then(|addr| addr.get_optional_line1()), + address_line2: billing_address.and_then(|addr| addr.get_optional_line2()), + address_postal_code: billing_address.and_then(|addr| addr.get_optional_zip()), + email: billing.and_then(|b| b.get_email().ok()), + mobile_phone: billing.and_then(|b| b.get_phone_with_country_code().ok()), + }) + } else { + None + }; + + let auth = TrustlyAuthType::try_from(&item.connector_config)?; + + let register_account_data = RegisterAccountData { + account_number, + bank_number, + clearing_house: common_enums::Country::from_alpha2(trustly_data.bank_country_code) + .to_string() + .to_uppercase(), + end_user_i_d: end_user_id, + firstname: first_name, + lastname: last_name, + username: auth.username.clone(), + password: auth.password.clone(), + attributes, + }; + + let uuid = uuid::Uuid::new_v4().to_string(); + let signature = generate_trustly_signature( + TrustlyMethod::RegisterAccount.as_str(), + uuid.as_str(), + ®ister_account_data, + &auth.private_key.clone().expose(), + ) + .map_err(Report::new)?; + + Ok(Self { + method: TrustlyMethod::RegisterAccount, + params: RegisterAccountParams { + data: register_account_data, + signature: Secret::new(signature), + uuid, + }, + version: TRUSTLY_VERSION.to_string(), + }) + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(untagged)] +pub enum RegisterAccountResponse { + Success(RegisterAccountResponseSuccess), + Error(Box), +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct RegisterAccountResponseSuccess { + pub result: RegisterAccountResponseResult, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct RegisterAccountResponseResult { + data: RegisterAccountResponseResultData, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct RegisterAccountResponseResultData { + accountid: Secret, + clearinghouse: String, + bank: String, +} + +impl TryFrom> + for RouterDataV2< + PayoutCreateRecipient, + PayoutFlowData, + PayoutCreateRecipientRequest, + PayoutCreateRecipientResponse, + > +{ + type Error = Report; + + fn try_from( + item: ResponseRouterData, + ) -> Result { + match item.response { + RegisterAccountResponse::Success(response) => { + let account_id = response.result.data.accountid; + let payout_connector_metadata = Some(Secret::new(serde_json::json!({ + "account_id": account_id, + }))); + Ok(Self { + response: Ok(PayoutCreateRecipientResponse { + merchant_payout_id: item.router_data.request.merchant_payout_id.clone(), + payout_status: common_enums::PayoutStatus::RequiresCreation, + connector_payout_id: None, + status_code: item.http_code, + payout_connector_metadata, + }), + ..item.router_data + }) + } + RegisterAccountResponse::Error(error_response) => Ok(Self { + response: Err(build_error_from_response(&error_response, item.http_code)), + ..item.router_data + }), + } + } +} + +// ===== PAYOUT TRANSFER (AccountPayout) ===== + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct AccountPayoutRequest { + method: TrustlyMethod, + params: AccountPayoutParams, + version: String, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "PascalCase")] +pub struct AccountPayoutParams { + signature: Secret, + #[serde(rename = "UUID")] + uuid: String, + data: AccountPayoutData, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "PascalCase")] +pub struct AccountPayoutData { + account_i_d: Secret, + amount: StringMajorUnit, + #[serde(skip_serializing_if = "Option::is_none")] + attributes: Option, + currency: common_enums::Currency, + end_user_i_d: String, + message_i_d: String, + notification_u_r_l: String, + password: Secret, + username: Secret, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "PascalCase")] +pub struct AccountPayoutAttributes { + shopper_statement: String, +} + +impl + TryFrom< + TrustlyPayoutsRouterData< + RouterDataV2< + PayoutTransfer, + PayoutFlowData, + PayoutTransferRequest, + PayoutTransferResponse, + >, + T, + >, + > for AccountPayoutRequest +{ + type Error = Report; + + fn try_from( + item: TrustlyPayoutsRouterData< + RouterDataV2< + PayoutTransfer, + PayoutFlowData, + PayoutTransferRequest, + PayoutTransferResponse, + >, + T, + >, + ) -> Result { + let item = &item.router_data; + // The Trustly payout only makes sense for the Trustly bank transfer method. + match item.request.payout_method_data.as_ref() { + Some(PayoutMethodData::Bank(Bank::Trustly(_))) => {} + _ => return Err(unsupported_payout_method_error("Payout Transfer")), + } + + // The account id created by the RegisterAccount (recipient) step is + // carried over in payout_connector_metadata. + let metadata = item + .request + .payout_connector_metadata + .clone() + .map(|secret| secret.expose()); + let account_id: TrustlyAccountId = to_payout_connector_meta(metadata)?; + + let amount = domain_utils::convert_amount( + &StringMajorUnitForConnector, + item.request.amount, + item.request.source_currency, + )?; + + let notification_url = item + .request + .webhook_url + .clone() + .ok_or_else(|| missing_field("webhook_url", "Payout Transfer"))?; + + let shopper_statement = item + .resource_common_data + .description + .clone() + .ok_or_else(|| missing_field("description", "Payout Transfer"))?; + + let end_user_id = item + .request + .get_customer_id()? + .get_string_repr() + .to_string(); + + let auth = TrustlyAuthType::try_from(&item.connector_config)?; + + let account_payout_data = AccountPayoutData { + account_i_d: account_id.account_id, + amount, + attributes: Some(AccountPayoutAttributes { shopper_statement }), + currency: item.request.destination_currency, + end_user_i_d: end_user_id, + message_i_d: format!("payout_{}", item.resource_common_data.payout_id), + notification_u_r_l: notification_url, + password: auth.password.clone(), + username: auth.username.clone(), + }; + + let uuid = uuid::Uuid::new_v4().to_string(); + let signature = generate_trustly_signature( + TrustlyMethod::AccountPayout.as_str(), + uuid.as_str(), + &account_payout_data, + &auth.private_key.clone().expose(), + ) + .map_err(Report::new)?; + + Ok(Self { + method: TrustlyMethod::AccountPayout, + params: AccountPayoutParams { + data: account_payout_data, + signature: Secret::new(signature), + uuid, + }, + version: TRUSTLY_VERSION.to_string(), + }) + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +enum PayoutResult { + #[serde(rename = "0")] + Failed, + #[serde(rename = "1")] + Pending, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(untagged)] +pub enum AccountPayoutResponse { + Success(AccountPayoutResponseSuccess), + Error(Box), +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct AccountPayoutResponseSuccess { + version: String, + result: AccountPayoutResponseResult, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct AccountPayoutResponseResult { + data: AccountPayoutResponseData, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct AccountPayoutResponseData { + orderid: String, + result: PayoutResult, +} + +impl TryFrom> + for RouterDataV2 +{ + type Error = Report; + + fn try_from( + item: ResponseRouterData, + ) -> Result { + match item.response { + AccountPayoutResponse::Success(success_response) => { + let data = success_response.result.data; + let payout_status = match data.result { + PayoutResult::Failed => common_enums::PayoutStatus::Failure, + PayoutResult::Pending => common_enums::PayoutStatus::Initiated, + }; + Ok(Self { + response: Ok(PayoutTransferResponse { + merchant_payout_id: item.router_data.request.merchant_payout_id.clone(), + payout_status, + connector_payout_id: Some(data.orderid), + status_code: item.http_code, + }), + ..item.router_data + }) + } + AccountPayoutResponse::Error(error_response) => Ok(Self { + response: Err(build_error_from_response(&error_response, item.http_code)), + ..item.router_data + }), + } + } +} + +// ===== PAYOUT GET (GetWithdrawals) ===== + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct TrustlyPayoutSyncRequest { + method: TrustlyMethod, + params: PayoutSyncRequestParams, + version: String, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "PascalCase")] +pub struct PayoutSyncRequestParams { + #[serde(rename = "UUID")] + uuid: String, + data: PayoutSyncRequestData, + signature: Secret, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "PascalCase")] +pub struct PayoutSyncRequestData { + order_id: Secret, + password: Secret, + username: Secret, +} + +impl + TryFrom< + TrustlyPayoutsRouterData< + RouterDataV2, + T, + >, + > for TrustlyPayoutSyncRequest +{ + type Error = Report; + + fn try_from( + item: TrustlyPayoutsRouterData< + RouterDataV2, + T, + >, + ) -> Result { + let item = &item.router_data; + let auth = TrustlyAuthType::try_from(&item.connector_config)?; + + let order_id = item + .request + .connector_payout_id + .clone() + .ok_or_else(|| missing_field("connector_payout_id", "Payout Get"))?; + + let data = PayoutSyncRequestData { + order_id: Secret::new(order_id), + password: auth.password.clone(), + username: auth.username.clone(), + }; + + let uuid = uuid::Uuid::new_v4().to_string(); + let signature = generate_trustly_signature( + TrustlyMethod::GetWithdrawals.as_str(), + uuid.as_str(), + &data, + &auth.private_key.clone().expose(), + ) + .map_err(Report::new)?; + + Ok(Self { + method: TrustlyMethod::GetWithdrawals, + params: PayoutSyncRequestParams { + uuid, + data, + signature: Secret::new(signature), + }, + version: TRUSTLY_VERSION.to_string(), + }) + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(untagged)] +pub enum TrustlyPayoutSyncResponse { + Success(TrustlyPayoutSyncResponseSuccess), + Error(Box), + Webhook(Box), +} + +fn get_payout_status_from_webhook( + method: TrustlyWebhookMethod, +) -> Result> { + match method { + TrustlyWebhookMethod::Credit => Ok(common_enums::PayoutStatus::Reversed), + TrustlyWebhookMethod::Cancel => Ok(common_enums::PayoutStatus::Cancelled), + TrustlyWebhookMethod::PayoutFailed => Ok(common_enums::PayoutStatus::Failure), + TrustlyWebhookMethod::PayoutConfirmation => Ok(common_enums::PayoutStatus::Success), + _ => Err(ConnectorError::UnexpectedResponseError { + context: ResponseTransformationErrorContext { + http_status_code: None, + additional_context: Some( + "Trustly GetWithdrawals - unexpected webhook method in sync response" + .to_string(), + ), + }, + } + .into()), + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct TrustlyPayoutSyncResponseSuccess { + result: TrustlyPayoutSyncResponseResult, + version: String, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct TrustlyPayoutSyncResponseResult { + uuid: String, + method: String, + data: Vec, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct TrustlyPayoutSyncResponseData { + reference: String, + orderid: String, + transferstate: TrustlyPayoutStatus, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +enum TrustlyPayoutStatus { + Confirmed, + Executing, + Executed, + Pending, + Queued, + Preparing, + Prepared, + Bounced, + Error, + Failed, + Returned, +} + +impl From for common_enums::PayoutStatus { + fn from(item: TrustlyPayoutStatus) -> Self { + match item { + TrustlyPayoutStatus::Confirmed => Self::Success, + TrustlyPayoutStatus::Failed + | TrustlyPayoutStatus::Error + | TrustlyPayoutStatus::Bounced + | TrustlyPayoutStatus::Returned => Self::Failure, + TrustlyPayoutStatus::Executing | TrustlyPayoutStatus::Executed => Self::Pending, + TrustlyPayoutStatus::Pending + | TrustlyPayoutStatus::Queued + | TrustlyPayoutStatus::Preparing + | TrustlyPayoutStatus::Prepared => Self::Initiated, + } + } +} + +impl TryFrom> + for RouterDataV2 +{ + type Error = Report; + + fn try_from( + item: ResponseRouterData, + ) -> Result { + match item.response { + TrustlyPayoutSyncResponse::Success(response) => { + let (payout_status, connector_payout_id) = match response.result.data.first() { + Some(first) => ( + common_enums::PayoutStatus::from(first.transferstate.clone()), + Some(first.orderid.clone()), + ), + None => ( + common_enums::PayoutStatus::Pending, + item.router_data.request.connector_payout_id.clone(), + ), + }; + Ok(Self { + response: Ok(PayoutGetResponse { + merchant_payout_id: item.router_data.request.merchant_payout_id.clone(), + payout_status, + connector_payout_id, + status_code: item.http_code, + }), + ..item.router_data + }) + } + TrustlyPayoutSyncResponse::Error(error_response) => Ok(Self { + response: Err(build_error_from_response(&error_response, item.http_code)), + ..item.router_data + }), + TrustlyPayoutSyncResponse::Webhook(webhook_body) => { + let payout_status = get_payout_status_from_webhook(webhook_body.method.clone())?; + Ok(Self { + response: Ok(PayoutGetResponse { + merchant_payout_id: item.router_data.request.merchant_payout_id.clone(), + payout_status, + connector_payout_id: Some(webhook_body.params.data.orderid.clone()), + status_code: item.http_code, + }), + ..item.router_data + }) + } + } + } +} + +// ===== ERROR HANDLING ===== + +/// Build a domain `ErrorResponse` from a Trustly JSON-RPC error body embedded in +/// an otherwise `2xx`/untagged response. +fn build_error_from_response( + error_response: &TrustlyErrorResponse, + status_code: u16, +) -> domain_types::router_data::ErrorResponse { + let typed = crate::connectors::macros::serialize_typed_connector_payload( + error_response, + "typed_connector_response", + ); + domain_types::router_data::ErrorResponse { + code: error_response.error.code.to_string(), + message: error_response.error.message.clone(), + reason: Some(error_response.error.message.clone()), + status_code, + attempt_status: None, + connector_transaction_id: Some(error_response.error.error.uuid.clone()), + network_advice_code: None, + network_decline_code: None, + network_error_message: None, + typed_connector_response: typed, + raw_connector_response: None, + raw_connector_request: None, + typed_connector_request: None, + } +} diff --git a/crates/integrations/connector-integration/src/payout_connectors/worldpayxml/transformers.rs b/crates/integrations/connector-integration/src/payout_connectors/worldpayxml/transformers.rs index 872d0122ca..1510df0525 100644 --- a/crates/integrations/connector-integration/src/payout_connectors/worldpayxml/transformers.rs +++ b/crates/integrations/connector-integration/src/payout_connectors/worldpayxml/transformers.rs @@ -1,6 +1,6 @@ use domain_types::{ connector_flow::{PayoutGet, PayoutTransfer, PayoutVoid}, - errors::{ConnectorError, IntegrationError}, + errors::{ConnectorError, IntegrationError, IntegrationErrorContext}, payouts::payouts_types::{ PayoutFlowData, PayoutGetRequest, PayoutGetResponse, PayoutTransferRequest, PayoutTransferResponse, PayoutVoidRequest, PayoutVoidResponse, @@ -70,17 +70,34 @@ fn map_worldpayxml_payout_status( | WorldpayxmlLastEvent::PushRefused | WorldpayxmlLastEvent::Expired | WorldpayxmlLastEvent::Error => common_enums::PayoutStatus::Failure, + // Exhaustiveness only: the shared lastEvent enum gained variants for the payment flows. + // Every mapping above is unchanged from before those variants existed. + _ => common_enums::PayoutStatus::Pending, } } -fn worldpayxml_amount_exponent(currency: common_enums::Currency) -> String { - if currency.is_three_decimal_currency() { - "3".to_string() - } else if currency.is_zero_decimal_currency() { - "0".to_string() - } else { - "2".to_string() - } +fn worldpayxml_amount_exponent( + currency: common_enums::Currency, +) -> Result> { + currency + .number_of_digits_after_decimal_point() + .map(|digits| digits.to_string()) + .map_err(|_| { + IntegrationError::InvalidDataFormat { + field_name: "currency", + context: IntegrationErrorContext { + suggested_action: Some( + "Use an ISO 4217 currency Worldpay accepts (e.g. GBP, USD, EUR)." + .to_string(), + ), + doc_url: None, + additional_context: Some(format!( + "Currency {currency:?} has no known minor-unit exponent" + )), + }, + } + .into() + }) } // ----- PayoutTransfer (PoFulfill) request ----- @@ -188,7 +205,7 @@ impl amount: requests::WorldpayxmlAmount { value: converted_amount, currency_code: request.destination_currency, - exponent: worldpayxml_amount_exponent(request.destination_currency), + exponent: worldpayxml_amount_exponent(request.destination_currency)?, }, payment_details: requests::WorldpayxmlPayoutPaymentDetails { payment_method }, }, diff --git a/crates/integrations/connector-integration/src/types.rs b/crates/integrations/connector-integration/src/types.rs index b8cd3f9ae4..e355e47a37 100644 --- a/crates/integrations/connector-integration/src/types.rs +++ b/crates/integrations/connector-integration/src/types.rs @@ -103,6 +103,7 @@ impl Box::new(connectors::Barclaycard::new()), ConnectorEnum::Billwerk => Box::new(connectors::Billwerk::new()), ConnectorEnum::Payme => Box::new(connectors::Payme::new()), + ConnectorEnum::Moneris => Box::new(connectors::Moneris::new()), ConnectorEnum::Nuvei => Box::new(connectors::Nuvei::new()), ConnectorEnum::Airwallex => Box::new(connectors::Airwallex::new()), ConnectorEnum::Bambora => Box::new(connectors::Bambora::new()), @@ -142,6 +143,8 @@ 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()), + ConnectorEnum::Ilixium => Box::new(connectors::Ilixium::::new()), } } } @@ -230,6 +233,10 @@ impl PayoutConnectorData { Box::new(payout_connectors::CybersourcePayouts::new()) } PayoutConnectorEnum::Santander => Box::new(payout_connectors::SantanderPayouts::new()), + PayoutConnectorEnum::Truelayer => Box::new(payout_connectors::TruelayerPayouts::new()), + PayoutConnectorEnum::Trustly => Box::new(payout_connectors::TrustlyPayouts::< + domain_types::payment_method_data::DefaultPCIHolder, + >::new()), } } } diff --git a/crates/internal/composite-service/src/payment_methods.rs b/crates/internal/composite-service/src/payment_methods.rs index 0d15eb098a..9956132ae4 100644 --- a/crates/internal/composite-service/src/payment_methods.rs +++ b/crates/internal/composite-service/src/payment_methods.rs @@ -5,12 +5,14 @@ use grpc_api_types::payments::{ composite_payment_method_service_server::CompositePaymentMethodService, merchant_authentication_service_server::MerchantAuthenticationService, payment_method_service_server::PaymentMethodService, CompositePaymentMethodCreateRequest, - CompositePaymentMethodCreateResponse, CompositePaymentMethodGetRequest, + CompositePaymentMethodCreateResponse, CompositePaymentMethodEligibilityRequest, + CompositePaymentMethodEligibilityResponse, CompositePaymentMethodGetRequest, CompositePaymentMethodGetResponse, CompositePaymentMethodRechargeRequest, CompositePaymentMethodRechargeResponse, MerchantAuthenticationServiceCreateServerAuthenticationTokenResponse, - PaymentMethodServiceCreateRequest, PaymentMethodServiceGetRequest, - PaymentMethodServiceRechargeRequest, PaymentMethodServiceTokenizeRequest, + PaymentMethodServiceCreateRequest, PaymentMethodServiceEligibilityRequest, + PaymentMethodServiceGetRequest, PaymentMethodServiceRechargeRequest, + PaymentMethodServiceTokenizeRequest, }; use ucs_env::error::ResultExtGrpc; @@ -83,6 +85,26 @@ impl CompositeAccessTokenRequest for CompositePaymentMethodGetRequest { } } +impl CompositeAccessTokenRequest for CompositePaymentMethodEligibilityRequest { + fn payment_method(&self) -> Option { + None + } + + fn state(&self) -> Option<&grpc_api_types::payments::ConnectorState> { + self.state.as_ref() + } + + fn build_access_token_request( + &self, + connector: &ConnectorVariant, + ) -> grpc_api_types::payments::MerchantAuthenticationServiceCreateServerAuthenticationTokenRequest + { + grpc_api_types::payments::MerchantAuthenticationServiceCreateServerAuthenticationTokenRequest::foreign_from(( + self, connector, + )) + } +} + /// Composite Payment Method Service that combines payment-method operations /// with the access-token bootstrap. /// @@ -335,6 +357,38 @@ where tokenize_response, })) } + + async fn process_eligibility( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let (metadata, extensions, payload) = request.into_parts(); + let connector = connector_variant_from_composite_metadata(&metadata).map_err(|err| *err)?; + let access_token_response = self + .create_server_authentication_token(&connector, &payload, &metadata, &extensions) + .await?; + + let inner = PaymentMethodServiceEligibilityRequest::foreign_from(( + &payload, + access_token_response.as_ref(), + )); + let mut inner_request = tonic::Request::new(inner); + *inner_request.metadata_mut() = metadata; + *inner_request.extensions_mut() = extensions; + + let eligibility_response = self + .payment_method_service + .eligibility(inner_request) + .await? + .into_inner(); + + Ok(tonic::Response::new( + CompositePaymentMethodEligibilityResponse { + access_token_response, + eligibility_response: Some(eligibility_response), + }, + )) + } } #[tonic::async_trait] @@ -369,4 +423,13 @@ where ) -> Result, tonic::Status> { self.process_recharge(request).await } + + /// Check payment method eligibility (e.g. gift-card/wallet status, performing the same + /// connector call as `get`). Same bootstrap + forward pattern. + async fn eligibility( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + self.process_eligibility(request).await + } } diff --git a/crates/internal/composite-service/src/transformers.rs b/crates/internal/composite-service/src/transformers.rs index a6f0df1167..0950e0bb34 100644 --- a/crates/internal/composite-service/src/transformers.rs +++ b/crates/internal/composite-service/src/transformers.rs @@ -1,11 +1,12 @@ use domain_types::connector_types::{ConnectorEnum, ConnectorVariant}; use grpc_api_types::payments::{ CompositeAuthorizeRequest, CompositeCaptureRequest, CompositeGetRequest, - CompositePaymentMethodCreateRequest, CompositePaymentMethodGetRequest, - CompositePaymentMethodRechargeRequest, CompositePreAuthenticateRequest, - CompositeRefundGetRequest, CompositeRefundRequest, CompositeVerifyRedirectResponseRequest, - CompositeVoidRequest, ConnectorState, CustomerServiceCreateRequest, - CustomerServiceCreateResponse, CustomerServiceGetRequest, CustomerServiceGetResponse, + CompositePaymentMethodCreateRequest, CompositePaymentMethodEligibilityRequest, + CompositePaymentMethodGetRequest, CompositePaymentMethodRechargeRequest, + CompositePreAuthenticateRequest, CompositeRefundGetRequest, CompositeRefundRequest, + CompositeVerifyRedirectResponseRequest, CompositeVoidRequest, ConnectorState, + CustomerServiceCreateRequest, CustomerServiceCreateResponse, CustomerServiceGetRequest, + CustomerServiceGetResponse, MerchantAuthenticationServiceCreateServerAuthenticationTokenRequest, MerchantAuthenticationServiceCreateServerAuthenticationTokenResponse, MerchantAuthenticationServiceCreateServerSessionAuthenticationTokenRequest, @@ -16,9 +17,10 @@ use grpc_api_types::payments::{ PaymentMethodAuthenticationServicePostAuthenticateResponse, PaymentMethodAuthenticationServicePreAuthenticateRequest, PaymentMethodAuthenticationServicePreAuthenticateResponse, PaymentMethodServiceCreateRequest, - PaymentMethodServiceGetRequest, PaymentMethodServiceRechargeRequest, - PaymentMethodServiceTokenizeRequest, PaymentMethodServiceTokenizeResponse, - PaymentServiceAuthorizeRequest, PaymentServiceCaptureRequest, PaymentServiceCreateOrderRequest, + PaymentMethodServiceEligibilityRequest, PaymentMethodServiceGetRequest, + PaymentMethodServiceRechargeRequest, PaymentMethodServiceTokenizeRequest, + PaymentMethodServiceTokenizeResponse, PaymentServiceAuthorizeRequest, + PaymentServiceCaptureRequest, PaymentServiceCreateOrderRequest, PaymentServiceCreateOrderResponse, PaymentServiceGetRequest, PaymentServiceRefundRequest, PaymentServiceVerifyRedirectResponseResponse, PaymentServiceVoidRequest, RefundServiceGetRequest, @@ -953,6 +955,66 @@ impl } } +impl ForeignFrom<(&CompositePaymentMethodEligibilityRequest, &ConnectorVariant)> + for MerchantAuthenticationServiceCreateServerAuthenticationTokenRequest +{ + fn foreign_from( + (item, connector): (&CompositePaymentMethodEligibilityRequest, &ConnectorVariant), + ) -> Self { + Self { + merchant_access_token_id: item.merchant_access_token_id.clone(), + connector: grpc_connector_from_connector_variant(connector), + metadata: item.metadata.clone(), + connector_feature_data: item.connector_feature_data.clone(), + test_mode: item.test_mode, + merchant_request_id: item.merchant_request_id.clone(), + } + } +} + +impl + ForeignFrom<( + &CompositePaymentMethodEligibilityRequest, + Option<&MerchantAuthenticationServiceCreateServerAuthenticationTokenResponse>, + )> for PaymentMethodServiceEligibilityRequest +{ + fn foreign_from( + (item, access_token_response): ( + &CompositePaymentMethodEligibilityRequest, + Option<&MerchantAuthenticationServiceCreateServerAuthenticationTokenResponse>, + ), + ) -> Self { + let access_token_from_req = item + .state + .as_ref() + .and_then(|state| state.access_token.clone()); + let access_token = get_access_token(access_token_from_req, access_token_response); + let connector_customer_id = item + .state + .as_ref() + .and_then(|state| state.connector_customer_id.clone()); + let resolved_state = Some(ConnectorState { + access_token, + connector_customer_id, + }); + + Self { + amount: item.amount, + customer: item.customer.clone(), + address: item.address.clone(), + order_details: item.order_details.clone(), + country: item.country, + payment_method_type: item.payment_method_type, + description: item.description.clone(), + metadata: item.metadata.clone(), + connector_feature_data: item.connector_feature_data.clone(), + test_mode: item.test_mode, + connector_payment_method_id: item.connector_payment_method_id.clone(), + state: resolved_state, + } + } +} + impl ForeignFrom<&CompositePaymentMethodGetRequest> for PaymentMethodServiceTokenizeRequest { fn foreign_from(item: &CompositePaymentMethodGetRequest) -> Self { Self { diff --git a/crates/internal/composite-service/tests/composite_request_schema_check.rs b/crates/internal/composite-service/tests/composite_request_schema_check.rs index dc1cfc8d92..f1221b6efa 100644 --- a/crates/internal/composite-service/tests/composite_request_schema_check.rs +++ b/crates/internal/composite-service/tests/composite_request_schema_check.rs @@ -114,6 +114,16 @@ const COMPOSITE_FLOW_SPECS: &[CompositeFlowSpec] = &[ ignore_granular_only_fields: DEFAULT_IGNORE_GRANULAR_ONLY_FIELDS, ignore_composite_only_fields: DEFAULT_IGNORE_COMPOSITE_ONLY_FIELDS, }, + CompositeFlowSpec { + name: "payment_method_eligibility", + composite_request_message: "CompositePaymentMethodEligibilityRequest", + granular_request_messages: &[ + "MerchantAuthenticationServiceCreateServerAuthenticationTokenRequest", + "PaymentMethodServiceEligibilityRequest", + ], + ignore_granular_only_fields: DEFAULT_IGNORE_GRANULAR_ONLY_FIELDS, + ignore_composite_only_fields: DEFAULT_IGNORE_COMPOSITE_ONLY_FIELDS, + }, CompositeFlowSpec { name: "pre_authenticate", composite_request_message: "CompositePreAuthenticateRequest", diff --git a/crates/internal/field-probe/probe-config.toml b/crates/internal/field-probe/probe-config.toml index e124d53667..dacd0e2ddc 100644 --- a/crates/internal/field-probe/probe-config.toml +++ b/crates/internal/field-probe/probe-config.toml @@ -58,6 +58,9 @@ name = "trustpay" [[oauth_connectors]] name = "fiservcommercehub" +[[oauth_connectors]] +name = "moneris" + # Payment methods to probe for authorize flow # Each entry maps a PM name to whether it's enabled [payment_methods] diff --git a/crates/internal/field-probe/src/auth.rs b/crates/internal/field-probe/src/auth.rs index 641c45e5e6..c6657f3776 100644 --- a/crates/internal/field-probe/src/auth.rs +++ b/crates/internal/field-probe/src/auth.rs @@ -309,6 +309,12 @@ pub(crate) fn dummy_auth(connector: &ConnectorEnum) -> ConnectorSpecificConfig { base_url: None, secondary_base_url: None, }, + ConnectorEnum::Moneris => ConnectorSpecificConfig::Moneris { + client_secret: s(), + merchant_id: m(), + client_id: id(), + base_url: None, + }, ConnectorEnum::Nmi => ConnectorSpecificConfig::Nmi { api_key: k(), public_key: None, @@ -792,5 +798,16 @@ 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, + }, + ConnectorEnum::Ilixium => ConnectorSpecificConfig::Ilixium { + api_key: k(), + key1: k(), + api_secret: k(), + base_url: None, + }, } } diff --git a/crates/internal/integration-tests/src/connector_specs/citigate/specs.json b/crates/internal/integration-tests/src/connector_specs/citigate/specs.json new file mode 100644 index 0000000000..3fe703d6ee --- /dev/null +++ b/crates/internal/integration-tests/src/connector_specs/citigate/specs.json @@ -0,0 +1,11 @@ +{ + "connector": "citigate", + "supported_suites": [ + "PaymentService/Authorize", + "PaymentService/Get", + "PaymentService/Capture", + "PaymentService/Void", + "PaymentService/Refund", + "RefundService/Get" + ] +} diff --git a/crates/internal/integration-tests/src/connector_specs/ilixium/specs.json b/crates/internal/integration-tests/src/connector_specs/ilixium/specs.json new file mode 100644 index 0000000000..55ec8ff41f --- /dev/null +++ b/crates/internal/integration-tests/src/connector_specs/ilixium/specs.json @@ -0,0 +1,12 @@ +{ + "connector": "ilixium", + "supported_suites": [ + "PaymentService/Authorize", + "PaymentService/Capture", + "PaymentService/Get", + "PaymentService/Refund", + "RefundService/Get", + "PaymentService/Void", + "PaymentMethodAuthenticationService/PreAuthenticate" + ] +} diff --git a/crates/internal/integration-tests/src/connector_specs/moneris/specs.json b/crates/internal/integration-tests/src/connector_specs/moneris/specs.json new file mode 100644 index 0000000000..08c319a511 --- /dev/null +++ b/crates/internal/integration-tests/src/connector_specs/moneris/specs.json @@ -0,0 +1,13 @@ +{ + "connector": "moneris", + "supported_suites": [ + "MerchantAuthenticationService/CreateServerAuthenticationToken", + "PaymentService/Authorize", + "PaymentService/Capture", + "PaymentService/Get", + "PaymentService/Refund", + "PaymentService/Void", + "RecurringPaymentService/Charge", + "RefundService/Get" + ] +} diff --git a/crates/types-traits/domain_types/src/connector_types.rs b/crates/types-traits/domain_types/src/connector_types.rs index ea042f94f9..c5d43bd535 100644 --- a/crates/types-traits/domain_types/src/connector_types.rs +++ b/crates/types-traits/domain_types/src/connector_types.rs @@ -22,7 +22,8 @@ use crate::{ mandates::{CustomerAcceptance, MandateData}, payment_address::{self, Address, AddressDetails, PhoneDetails}, payment_method_data::{ - self, Card, CustomerDocumentDetails, PaymentMethodData, PaymentMethodDataTypes, + self, Card, CustomerDocumentDetails, DefaultPCIHolder, PaymentMethodData, + PaymentMethodDataTypes, }, router_data::{self, ConnectorResponseData}, router_request_types::{ @@ -120,6 +121,7 @@ pub enum ConnectorEnum { Barclaycard, Nexixpay, Mollie, + Moneris, Airwallex, Tsys, Bankofamerica, @@ -162,6 +164,8 @@ pub enum ConnectorEnum { Grabpay, Tesouro, Boost, + Citigate, + Ilixium, } // snake case for enum variants @@ -244,6 +248,8 @@ pub enum PayoutConnectorEnum { Worldpayxml, Cybersource, Santander, + Truelayer, + Trustly, } impl TryFrom for PayoutConnectorEnum { @@ -256,6 +262,8 @@ impl TryFrom for PayoutConnectorEnum { ConnectorEnum::Itaubank => Ok(Self::Itaubank), ConnectorEnum::Worldpayxml => Ok(Self::Worldpayxml), ConnectorEnum::Cybersource => Ok(Self::Cybersource), + ConnectorEnum::Truelayer => Ok(Self::Truelayer), + ConnectorEnum::Trustly => Ok(Self::Trustly), _ => Err(IntegrationError::InvalidDataFormat { field_name: "connector", context: IntegrationErrorContext::default(), @@ -297,6 +305,8 @@ impl ForeignTryFrom for PayoutConnectorEnum { AuthType::Worldpayxml(_) => Ok(Self::Worldpayxml), AuthType::Cybersource(_) => Ok(Self::Cybersource), AuthType::Santander(_) => Ok(Self::Santander), + AuthType::Truelayer(_) => Ok(Self::Truelayer), + AuthType::Trustly(_) => Ok(Self::Trustly), _ => Err(error_stack::Report::new( IntegrationError::InvalidDataFormat { field_name: "connector", @@ -494,6 +504,7 @@ impl ForeignTryFrom for ConnectorEnum { grpc_api_types::payments::Connector::Hyperpg => Ok(Self::Hyperpg), grpc_api_types::payments::Connector::Zift => Ok(Self::Zift), grpc_api_types::payments::Connector::Revolv3 => Ok(Self::Revolv3), + grpc_api_types::payments::Connector::Moneris => Ok(Self::Moneris), grpc_api_types::payments::Connector::Ppro => Ok(Self::Ppro), grpc_api_types::payments::Connector::Fiservcommercehub => Ok(Self::Fiservcommercehub), grpc_api_types::payments::Connector::Truelayer => Ok(Self::Truelayer), @@ -519,7 +530,9 @@ impl ForeignTryFrom for ConnectorEnum { grpc_api_types::payments::Connector::Glomopay => Ok(Self::Glomopay), grpc_api_types::payments::Connector::Givepayments => Ok(Self::Givepayments), grpc_api_types::payments::Connector::Boost => Ok(Self::Boost), + grpc_api_types::payments::Connector::Ilixium => Ok(Self::Ilixium), 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", @@ -818,6 +831,7 @@ pub struct PaymentFlowData { /// idempotency token on their wire envelope. pub merchant_request_id: Option, pub sender_payment_instrument_id: Option, + pub connector_returned_payment_method_details: Option>, /// Settlement phase reported by the connector. /// Lives on PaymentFlowData (not PaymentsSyncData) so other flows can /// populate it in the future if a connector starts reporting settlement state on authorize, capture, etc. @@ -1586,6 +1600,10 @@ pub struct PaymentMethodEligibilityData { pub amount: common_utils::types::Money, /// Customer details (phone, email, name, etc.) for eligibility check. pub customer: Option, + /// Connector-issued payment method ID (e.g. wallet number) being checked, + /// when known. Mirrors `GetPaymentMethodData`'s identifier so connectors + /// can reuse the same lookup as `GetPaymentMethod`. + pub connector_payment_method_id: Option, /// Market/country the eligibility check is for. BNPL eligibility is /// country-gated, so connectors operating per-market rely on this. /// (Billing/shipping address and order line items are carried on the @@ -1607,6 +1625,10 @@ pub struct PaymentMethodEligibilityData { #[derive(Debug, Clone)] pub struct PaymentMethodEligibilityResponse { pub eligibility: common_enums::EligibilityStatus, + /// Payment method details resolved as part of the eligibility check (e.g. + /// wallet/gift-card balance and items), when the connector call that + /// determines eligibility also returns them. + pub payment_method_details: Option, pub status_code: u32, } @@ -2226,6 +2248,14 @@ pub struct PaymentsPreAuthenticateData { pub mandate_reference: Option, /// Merchant transaction id, used to derive the FRM DDC sessionId (e.g. Kount). pub merchant_transaction_id: Option, + /// Merchant-supplied connector metadata, mirroring `PaymentsAuthorizeData::metadata`. + /// + /// The gRPC request has always carried this (`PaymentMethodAuthenticationService + /// PreAuthenticateRequest.metadata`) but it was previously dropped on the floor here, so a + /// connector whose PreAuthenticate leg sends a full authorisation could not reach + /// merchant-supplied fields that have no home in the UCS payment model — Ilixium's + /// schema-mandatory `customer.dateOfBirth`, for one. + pub metadata: Option, } impl PaymentsPreAuthenticateData { @@ -4163,7 +4193,7 @@ impl From> for PaymentMethodData Self::LocalBankRedirect } payment_method_data::BankRedirectData::Eft { .. } => Self::Eft, - payment_method_data::BankRedirectData::OpenBanking {} => Self::OpenBanking, + payment_method_data::BankRedirectData::OpenBanking { .. } => Self::OpenBanking, payment_method_data::BankRedirectData::Netbanking { .. } => Self::Netbanking, }, PaymentMethodData::BankDebit(bank_debit_data) => match bank_debit_data { @@ -5656,6 +5686,7 @@ impl ForeignTryFrom AuthType::Authorizedotnet(_) => Ok(Self::Payment(ConnectorEnum::Authorizedotnet)), AuthType::Ppro(_) => Ok(Self::Payment(ConnectorEnum::Ppro)), AuthType::PinelabsOnline(_) => Ok(Self::Payment(ConnectorEnum::PinelabsOnline)), + AuthType::Moneris(_) => Ok(Self::Payment(ConnectorEnum::Moneris)), AuthType::Easebuzz(_) => Ok(Self::Payment(ConnectorEnum::Easebuzz)), AuthType::Juspay(_) => Ok(Self::Payment(ConnectorEnum::Juspay)), AuthType::Glomopay(_) => Ok(Self::Payment(ConnectorEnum::Glomopay)), @@ -5667,6 +5698,8 @@ 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::Ilixium(_) => Ok(Self::Payment(ConnectorEnum::Ilixium)), 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/payment_address.rs b/crates/types-traits/domain_types/src/payment_address.rs index 6392564664..e3dd08dcf7 100644 --- a/crates/types-traits/domain_types/src/payment_address.rs +++ b/crates/types-traits/domain_types/src/payment_address.rs @@ -310,9 +310,15 @@ impl AddressDetails { ))) } + pub fn get_optional_line1(&self) -> Option> { + self.line1.clone() + } pub fn get_optional_line2(&self) -> Option> { self.line2.clone() } + pub fn get_optional_city(&self) -> Option> { + self.city.clone() + } pub fn get_optional_country(&self) -> Option { self.country } diff --git a/crates/types-traits/domain_types/src/payment_method_data.rs b/crates/types-traits/domain_types/src/payment_method_data.rs index c1b7449e25..ddd2cfdfc8 100644 --- a/crates/types-traits/domain_types/src/payment_method_data.rs +++ b/crates/types-traits/domain_types/src/payment_method_data.rs @@ -782,7 +782,13 @@ pub enum BankRedirectData { Eft { provider: String, }, - OpenBanking {}, + OpenBanking { + account_number: Option>, + sort_code: Option>, + iban: Option>, + account_holder_name: Option>, + additional_details: Option>, + }, Netbanking { issuer: common_enums::BankNames, }, diff --git a/crates/types-traits/domain_types/src/payouts/payout_method_data.rs b/crates/types-traits/domain_types/src/payouts/payout_method_data.rs index 80a6b96057..18975ac381 100644 --- a/crates/types-traits/domain_types/src/payouts/payout_method_data.rs +++ b/crates/types-traits/domain_types/src/payouts/payout_method_data.rs @@ -79,6 +79,8 @@ pub enum Bank { Pix(PixBankTransfer), PixKey(PixKeyBankTransfer), PixEmv(PixEmvBankTransfer), + OpenBanking(OpenBanking), + Trustly(TrustlyBankTransfer), } #[derive(Default, Eq, PartialEq, Clone, Debug)] @@ -178,6 +180,25 @@ pub struct PixEmvBankTransfer { pub emv: Secret, } +#[derive(Default, Eq, PartialEq, Clone, Debug)] +// Trustly bank transfer destination. The account can be identified either by an +// IBAN or by a bank_account_number + bank_number pair. +pub struct TrustlyBankTransfer { + /// International Bank Account Number (IBAN). When present, it is used as the + /// account number and no separate bank number is required. + pub iban: Option>, + + /// Bank account number, used when an IBAN is not available. + pub bank_account_number: Option>, + + /// Bank/clearing number identifying the destination bank. + pub bank_number: Option>, + + /// Bank country code. Maps to Trustly's `ClearingHouse` (the English country + /// name in upper case). + pub bank_country_code: common_enums::CountryAlpha2, +} + #[derive(Eq, PartialEq, Clone, Debug)] pub enum Wallet { ApplePayDecrypt(ApplePayDecrypt), @@ -197,6 +218,14 @@ pub struct Interac { pub email: Email, } +#[derive(Default, Eq, PartialEq, Clone, Debug)] +pub struct OpenBanking { + /// Account holder name + pub account_holder_name: Secret, + /// International Bank Account Number (iban) - used in many countries for identifying a bank along with it's customer. + pub iban: Secret, +} + #[derive(Default, Eq, PartialEq, Clone, Debug)] pub struct OpenBankingUk { /// Account holder name @@ -210,6 +239,9 @@ pub struct Passthrough { /// PSP token generated for the payout method pub psp_token: Secret, + /// PSP customer ID + pub psp_customer_id: Option>, + /// Payout method type of the token pub token_type: common_enums::PaymentMethodType, } diff --git a/crates/types-traits/domain_types/src/payouts/payouts_types.rs b/crates/types-traits/domain_types/src/payouts/payouts_types.rs index 55c7c77fff..329e730c26 100644 --- a/crates/types-traits/domain_types/src/payouts/payouts_types.rs +++ b/crates/types-traits/domain_types/src/payouts/payouts_types.rs @@ -140,6 +140,7 @@ pub struct PayoutTransferRequest { pub source_bank_data: Option, pub customer: Option, pub connector_eligibility_reference_id: Option, + pub payout_connector_metadata: Option, } impl PayoutTransferRequest { @@ -392,6 +393,19 @@ pub struct PayoutCreateRecipientRequest { pub source_currency: common_enums::Currency, pub payout_method_data: Option, pub recipient_type: common_enums::PayoutRecipientType, + pub customer: Option, + pub address: Option, +} + +impl PayoutCreateRecipientRequest { + /// Navigate to the billing `AddressDetails`; per-field accessors live on + /// [`crate::payment_address::AddressDetails`] and are reused from there. + pub fn get_optional_billing_address(&self) -> Option<&crate::payment_address::AddressDetails> { + self.address + .as_ref() + .and_then(|a| a.billing_address.as_ref()) + .and_then(|b| b.address.as_ref()) + } } #[derive(Debug, Clone)] @@ -400,6 +414,7 @@ pub struct PayoutCreateRecipientResponse { pub payout_status: common_enums::PayoutStatus, pub connector_payout_id: Option, pub status_code: u16, + pub payout_connector_metadata: Option, } #[derive(Debug, Clone)] diff --git a/crates/types-traits/domain_types/src/payouts/types.rs b/crates/types-traits/domain_types/src/payouts/types.rs index 9671179d89..b136136e68 100644 --- a/crates/types-traits/domain_types/src/payouts/types.rs +++ b/crates/types-traits/domain_types/src/payouts/types.rs @@ -562,6 +562,39 @@ impl ForeignTryFrom } } +impl ForeignTryFrom + for payouts::payout_method_data::TrustlyBankTransfer +{ + type Error = IntegrationError; + fn foreign_try_from( + trustly: grpc_api_types::payouts::TrustlyBankTransferPayout, + ) -> Result> { + let bank_country_code = { + let cc = grpc_api_types::payments::CountryAlpha2::try_from(trustly.bank_country_code) + .change_context(IntegrationError::InvalidDataFormat { + field_name: "bank_country_code", + context: IntegrationErrorContext { + additional_context: Some("Invalid bank country code".to_owned()), + ..Default::default() + }, + })?; + common_enums::CountryAlpha2::foreign_try_from(cc)? + }; + Ok(payouts::payout_method_data::TrustlyBankTransfer { + iban: trustly + .iban + .map(|i| ::hyperswitch_masking::Secret::new(i.peek().to_string())), + bank_account_number: trustly + .bank_account_number + .map(|n| ::hyperswitch_masking::Secret::new(n.peek().to_string())), + bank_number: trustly + .bank_number + .map(|n| ::hyperswitch_masking::Secret::new(n.peek().to_string())), + bank_country_code, + }) + } +} + impl ForeignTryFrom for payouts::payout_method_data::PixBankTransfer { @@ -853,6 +886,50 @@ impl ForeignTryFrom } } +impl ForeignTryFrom + for payouts::payout_method_data::OpenBanking +{ + type Error = IntegrationError; + fn foreign_try_from( + ob: grpc_api_types::payouts::OpenBankingPayout, + ) -> Result> { + Ok(payouts::payout_method_data::OpenBanking { + account_holder_name: ::hyperswitch_masking::Secret::new( + ob.account_holder_name + .ok_or_else(|| { + error_stack::report!(IntegrationError::MissingRequiredField { + field_name: "account_holder_name", + context: IntegrationErrorContext { + additional_context: Some( + "Account holder name is required for OpenBanking".to_owned() + ), + ..Default::default() + }, + }) + })? + .peek() + .to_string(), + ), + iban: ::hyperswitch_masking::Secret::new( + ob.iban + .ok_or_else(|| { + error_stack::report!(IntegrationError::MissingRequiredField { + field_name: "iban", + context: IntegrationErrorContext { + additional_context: Some( + "IBAN is required for OpenBanking".to_owned() + ), + ..Default::default() + }, + }) + })? + .peek() + .to_string(), + ), + }) + } +} + impl ForeignTryFrom for payouts::payout_method_data::OpenBankingUk { @@ -923,6 +1000,7 @@ impl ForeignTryFrom })?; Ok(payouts::payout_method_data::Passthrough { psp_token: ::hyperswitch_masking::Secret::new(pt.psp_token), + psp_customer_id: pt.psp_customer_id, token_type, }) } @@ -979,6 +1057,11 @@ impl ForeignTryFrom payouts::payout_method_data::PixEmvBankTransfer::foreign_try_from(pix_emv)?, ))) } + grpc_api_types::payouts::payout_method::PayoutMethodData::Trustly(trustly) => { + Ok(Self::Bank(payouts::payout_method_data::Bank::Trustly( + payouts::payout_method_data::TrustlyBankTransfer::foreign_try_from(trustly)?, + ))) + } grpc_api_types::payouts::payout_method::PayoutMethodData::ApplePayDecrypt( apple_pay_decrypt, ) => Ok(Self::Wallet( @@ -1010,6 +1093,11 @@ impl ForeignTryFrom payouts::payout_method_data::OpenBankingUk::foreign_try_from(open_banking_uk)?, ), )), + grpc_api_types::payouts::payout_method::PayoutMethodData::OpenBanking(open_banking) => { + Ok(Self::Bank(payouts::payout_method_data::Bank::OpenBanking( + payouts::payout_method_data::OpenBanking::foreign_try_from(open_banking)?, + ))) + } grpc_api_types::payouts::payout_method::PayoutMethodData::Passthrough(passthrough) => { Ok(Self::Passthrough( payouts::payout_method_data::Passthrough::foreign_try_from(passthrough)?, @@ -1134,34 +1222,7 @@ impl ForeignTryFrom let customer = value .customer - .map( - |customer| -> Result<_, error_stack::Report> { - let email = customer - .email - .map(|email_str| { - common_utils::pii::Email::try_from(email_str.expose()).map_err(|e| { - error_stack::Report::new(IntegrationError::InvalidDataFormat { - field_name: "email", - context: IntegrationErrorContext { - additional_context: Some("Invalid email".to_owned()), - ..Default::default() - }, - }) - .attach_printable(format!("{e:?}")) - }) - }) - .transpose()?; - - Ok(payouts::payouts_types::PayoutCustomer { - name: customer.name, - email, - merchant_customer_id: customer.id, - connector_customer_id: customer.connector_customer_id, - phone_number: customer.phone_number, - phone_country_code: customer.phone_country_code, - }) - }, - ) + .map(convert_payouts_customer_to_domain) .transpose()?; let address = value @@ -1187,6 +1248,15 @@ impl ForeignTryFrom customer, address, connector_eligibility_reference_id: value.connector_eligibility_reference_id, + payout_connector_metadata: value + .payout_connector_metadata + .map(|m| { + common_utils::pii::SecretSerdeValue::foreign_try_from(( + m, + "payout_connector_metadata", + )) + }) + .transpose()?, }) } } @@ -1212,6 +1282,35 @@ impl ForeignTryFrom } } +fn convert_payouts_customer_to_domain( + customer: grpc_api_types::payments::Customer, +) -> Result> { + let email = customer + .email + .map(|email_str| { + common_utils::pii::Email::try_from(email_str.expose()).map_err(|e| { + error_stack::Report::new(IntegrationError::InvalidDataFormat { + field_name: "customer.email", + context: IntegrationErrorContext { + additional_context: Some("Invalid email".to_owned()), + ..Default::default() + }, + }) + .attach_printable(format!("{e:?}")) + }) + }) + .transpose()?; + + Ok(payouts::payouts_types::PayoutCustomer { + name: customer.name, + email, + merchant_customer_id: customer.id, + connector_customer_id: customer.connector_customer_id, + phone_number: customer.phone_number, + phone_country_code: customer.phone_country_code, + }) +} + fn convert_payouts_address_to_domain( addr: grpc_api_types::payouts::Address, ) -> Result> { @@ -1457,6 +1556,16 @@ impl ForeignTryFrom Result<_, error_stack::Report> { + Ok(hyperswitch_masking::Secret::new( + serde_json::to_string(meta.peek()).change_context( + crate::errors::ConnectorError::ResponseDeserializationFailed { + context: Default::default(), + }, + )?, + )) + }, + ) + .transpose()?; Ok( grpc_api_types::payouts::PayoutServiceCreateRecipientResponse { merchant_payout_id: response.merchant_payout_id, @@ -2109,6 +2234,7 @@ pub fn generate_payout_create_recipient_response( connector_payout_id: response.connector_payout_id, error: None, status_code: u32::from(response.status_code), + connector_metadata, }, ) } @@ -2131,6 +2257,7 @@ pub fn generate_payout_create_recipient_response( issuer_details: None, }), status_code: u32::from(err.status_code), + connector_metadata: None, }, ), } @@ -2190,34 +2317,7 @@ impl ForeignTryFrom let customer = value .customer - .map( - |customer| -> Result<_, error_stack::Report> { - let email = customer - .email - .map(|email_str| { - common_utils::pii::Email::try_from(email_str.expose()).map_err(|e| { - error_stack::Report::new(IntegrationError::InvalidDataFormat { - field_name: "email", - context: IntegrationErrorContext { - additional_context: Some("Invalid email".to_owned()), - ..Default::default() - }, - }) - .attach_printable(format!("{e:?}")) - }) - }) - .transpose()?; - - Ok(payouts::payouts_types::PayoutCustomer { - name: customer.name, - email, - merchant_customer_id: customer.id, - connector_customer_id: customer.connector_customer_id, - phone_number: customer.phone_number, - phone_country_code: customer.phone_country_code, - }) - }, - ) + .map(convert_payouts_customer_to_domain) .transpose()?; let address = value diff --git a/crates/types-traits/domain_types/src/router_data.rs b/crates/types-traits/domain_types/src/router_data.rs index c1f7470711..9d5ee8e425 100644 --- a/crates/types-traits/domain_types/src/router_data.rs +++ b/crates/types-traits/domain_types/src/router_data.rs @@ -508,6 +508,12 @@ pub enum ConnectorSpecificConfig { base_url: Option, secondary_base_url: Option, }, + Moneris { + client_secret: Secret, + merchant_id: Secret, + client_id: Secret, + base_url: Option, + }, Nmi { api_key: Secret, public_key: Option>, @@ -966,6 +972,21 @@ pub enum ConnectorSpecificConfig { merchant_secret: Secret, base_url: Option, }, + Citigate { + api_key: Secret, + key1: Secret, + base_url: Option, + }, + /// Ilixium Direct API. + /// `api_key` = Digest Calculation Password (input to X-MERCHANT-DIGEST, never sent) + /// `key1` = MerchantId (request body `merchant.merchantId`) + /// `api_secret` = AccountId (request body `merchant.accountId`) + Ilixium { + api_key: Secret, + key1: Secret, + api_secret: Secret, + base_url: Option, + }, } impl ConnectorSpecificConfig { @@ -1142,6 +1163,11 @@ impl ConnectorSpecificConfig { merchant_id, client_secret }, + Moneris { + client_secret, + merchant_id, + client_id + }, Noon { api_key, business_identifier, @@ -1313,6 +1339,12 @@ impl ConnectorSpecificConfig { api_secret }, Boost { api_key }, + Citigate { api_key, key1 }, + Ilixium { + api_key, + key1, + api_secret + }, Imerchantsolutions { api_key }, Interpayments { api_key }, TwocTwopPaco { @@ -1610,6 +1642,11 @@ impl ConnectorSpecificConfig { merchant_id, client_secret }, + Moneris { + client_secret, + merchant_id, + client_id + }, Noon { api_key, business_identifier, @@ -1783,6 +1820,12 @@ impl ConnectorSpecificConfig { api_secret }, Boost { api_key }, + Citigate { api_key, key1 }, + Ilixium { + api_key, + key1, + api_secret + }, Imerchantsolutions { api_key }, Interpayments { api_key }, TwocTwopPaco { @@ -2016,6 +2059,12 @@ impl ForeignTryFrom for Conne api_key: multisafepay.api_key.ok_or_else(err)?, base_url: multisafepay.base_url, }), + AuthType::Moneris(moneris) => Ok(Self::Moneris { + client_secret: moneris.client_secret.ok_or_else(err)?, + merchant_id: moneris.merchant_id.ok_or_else(err)?, + client_id: moneris.client_id.ok_or_else(err)?, + base_url: moneris.base_url, + }), AuthType::Nexinets(nexinets) => Ok(Self::Nexinets { merchant_id: nexinets.merchant_id.ok_or_else(err)?, api_key: nexinets.api_key.ok_or_else(err)?, @@ -2405,6 +2454,17 @@ 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::Ilixium(ilixium) => Ok(Self::Ilixium { + api_key: ilixium.api_key.ok_or_else(err)?, + key1: ilixium.key1.ok_or_else(err)?, + api_secret: ilixium.api_secret.ok_or_else(err)?, + base_url: ilixium.base_url, + }), AuthType::Imerchantsolutions(imerchantsolutions) => Ok(Self::Imerchantsolutions { api_key: imerchantsolutions.api_key.ok_or_else(err)?, merchant_id: imerchantsolutions.merchant_id, @@ -3145,6 +3205,19 @@ impl ForeignTryFrom<(&ConnectorAuthType, &connector_types::ConnectorVariant)> }), _ => Err(err().into()), }, + ConnectorEnum::Moneris => match auth { + ConnectorAuthType::SignatureKey { + api_key, + key1, + api_secret, + } => Ok(Self::Moneris { + client_secret: api_key.clone(), + merchant_id: api_secret.clone(), + client_id: key1.clone(), + base_url: None, + }), + _ => Err(err().into()), + }, ConnectorEnum::Noon => match auth { ConnectorAuthType::SignatureKey { api_key, @@ -3605,6 +3678,19 @@ impl ForeignTryFrom<(&ConnectorAuthType, &connector_types::ConnectorVariant)> }), _ => Err(err().into()), }, + ConnectorEnum::Ilixium => match auth { + ConnectorAuthType::SignatureKey { + api_key, + key1, + api_secret, + } => Ok(Self::Ilixium { + api_key: api_key.clone(), + key1: key1.clone(), + api_secret: api_secret.clone(), + base_url: None, + }), + _ => Err(err().into()), + }, ConnectorEnum::Boost => match auth { ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self::Boost { client_id: api_key.clone(), @@ -3613,6 +3699,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(), @@ -3831,6 +3925,20 @@ impl ForeignTryFrom<(&ConnectorAuthType, &connector_types::ConnectorVariant)> _ => Err(err().into()), }, PayoutConnectorEnum::Santander => Err(err().into()), + PayoutConnectorEnum::Truelayer => Err(err().into()), + PayoutConnectorEnum::Trustly => match auth { + ConnectorAuthType::SignatureKey { + api_key, + key1, + api_secret, + } => Ok(Self::Trustly { + username: api_key.clone(), + password: key1.clone(), + private_key: api_secret.clone(), + base_url: None, + }), + _ => Err(err().into()), + }, }, } } diff --git a/crates/types-traits/domain_types/src/types.rs b/crates/types-traits/domain_types/src/types.rs index 914ecfbb47..4cfd6f5c39 100644 --- a/crates/types-traits/domain_types/src/types.rs +++ b/crates/types-traits/domain_types/src/types.rs @@ -427,7 +427,10 @@ pub struct Connectors { pub grabpay: ConnectorParams, pub tesouro: ConnectorParams, pub boost: ConnectorParams, + pub ilixium: ConnectorParams, pub santander: ConnectorParams, + pub citigate: ConnectorParams, + pub moneris: ConnectorParams, } #[derive(Clone, Deserialize, Serialize, Debug, Default, PartialEq, config_patch_derive::Patch)] @@ -696,6 +699,9 @@ impl Connectors { ConnectorEnum::Mollie => { patched.mollie.apply(params_patch); } + ConnectorEnum::Moneris => { + patched.moneris.apply(params_patch); + } ConnectorEnum::Multisafepay => { patched.multisafepay.apply(params_patch); } @@ -708,6 +714,9 @@ impl Connectors { ConnectorEnum::Payme => { patched.payme.apply(params_patch); } + ConnectorEnum::Tamara => { + patched.tamara.apply(params_patch); + } ConnectorEnum::Placetopay => { patched.placetopay.apply(params_patch); } @@ -785,6 +794,9 @@ impl Connectors { ConnectorEnum::Boost => { patched.boost.apply(params_patch); } + ConnectorEnum::Ilixium => { + patched.ilixium.apply(params_patch); + } ConnectorEnum::Maya => { patched.maya.apply(params_patch); } @@ -798,7 +810,7 @@ impl Connectors { context: IntegrationErrorContext { additional_context: Some(format!( "Connector '{}' is not supported for dynamic URL patching from superposition. \ - Supported connectors: stripe, adyen, paypal, braintree, checkout, cybersource, revolut, aci, bankofamerica, worldpay, rapyd, fiserv, nexinets, elavon, novalnet, trustpay, forte, bambora, bamboraapac, barclaycard, billwerk, bluesnap, calida, cashfree, celero, cryptopay, datatrans, finix, fiservcommercehub, fiservemea, globalpay, helcim, hipay, imerchantsolutions, jpmorgan, loonio, mifinity, mollie, multisafepay, nexixpay, payload, payme, placetopay, powertranz, revolv3, absa_sanlam, shift4, silverflow, stax, truelayer, trustly, trustpayments, tsys, wellsfargo, worldpayvantiv, worldpayxml, zift, gigadat, givepayments, boost", + Supported connectors: stripe, adyen, paypal, braintree, checkout, cybersource, revolut, aci, bankofamerica, worldpay, rapyd, fiserv, nexinets, elavon, novalnet, trustpay, forte, bambora, bamboraapac, barclaycard, billwerk, bluesnap, calida, cashfree, celero, cryptopay, datatrans, finix, fiservcommercehub, fiservemea, globalpay, helcim, hipay, imerchantsolutions, jpmorgan, loonio, mifinity, mollie, moneris, multisafepay, nexixpay, payload, payme, tamara, placetopay, powertranz, revolv3, absa_sanlam, shift4, silverflow, stax, truelayer, trustly, trustpayments, tsys, wellsfargo, worldpayvantiv, worldpayxml, zift, gigadat, givepayments, boost, ilixium", connector )), ..Default::default() @@ -829,6 +841,7 @@ impl Connectors { PayoutConnectorEnum::Worldpayxml => patched.worldpayxml.apply(params_patch), PayoutConnectorEnum::Cybersource => patched.cybersource.apply(params_patch), PayoutConnectorEnum::Santander => patched.santander.apply(params_patch), + PayoutConnectorEnum::Trustly => patched.trustly.apply(params_patch), // Deutschebank uses `ConnectorParamsWithCaBundle`, so patch the resolved // URLs while leaving its `server_ca_bundle` untouched. PayoutConnectorEnum::Deutschebank => { @@ -842,6 +855,7 @@ impl Connectors { server_ca_bundle: None, }) } + PayoutConnectorEnum::Truelayer => patched.truelayer.apply(params_patch), } Ok(patched) } @@ -1346,6 +1360,67 @@ impl ForeignFrom for grpc_api_types::payments::U } } +impl ForeignTryFrom> + for grpc_api_types::payments::PaymentMethod +{ + type Error = ConnectorError; + + fn foreign_try_from( + payment_method_data: PaymentMethodData, + ) -> Result> { + use grpc_api_types::payments::payment_method::PaymentMethod as ProtoPaymentMethod; + + let payment_method = match payment_method_data { + PaymentMethodData::BankRedirect(payment_method_data::BankRedirectData::OpenBanking { + account_number, + sort_code, + iban, + account_holder_name, + additional_details, + }) => { + let additional_details = additional_details + .map(|details| { + serde_json::to_string(details.peek()).map(Secret::new).change_context( + ConnectorError::ResponseHandlingFailed { + context: ResponseTransformationErrorContext { + additional_context: Some( + "Failed to serialize connector-returned Open Banking additional payment details" + .to_owned(), + ), + ..Default::default() + }, + }, + ) + }) + .transpose()?; + + ProtoPaymentMethod::OpenBanking(grpc_api_types::payments::OpenBanking { + iban, + account_number, + sort_code, + account_holder_name, + additional_details, + }) + } + _ => { + return Err(report!(ConnectorError::UnexpectedResponseError { + context: ResponseTransformationErrorContext { + additional_context: Some( + "connector_returned_payment_method_details cannot be represented as proto PaymentMethod yet" + .to_owned(), + ), + ..Default::default() + }, + })) + } + }; + + Ok(Self { + payment_method: Some(payment_method), + }) + } +} + impl ForeignFrom for grpc_api_types::payments::RawConnectorStatus { fn foreign_from(value: RawConnectorStatus) -> Self { Self { @@ -1899,9 +1974,27 @@ impl< .and_then(|c| CountryAlpha2::from_str(&c).ok()), }, )), - grpc_api_types::payments::payment_method::PaymentMethod::OpenBanking(_) => { + grpc_api_types::payments::payment_method::PaymentMethod::OpenBanking(open_banking) => { Ok(PaymentMethodData::BankRedirect( - payment_method_data::BankRedirectData::OpenBanking {}, + payment_method_data::BankRedirectData::OpenBanking { + account_number: open_banking.account_number, + sort_code: open_banking.sort_code, + iban: open_banking.iban, + account_holder_name: open_banking.account_holder_name, + additional_details: open_banking + .additional_details + .and_then(|details| { + serde_json::from_str(details.peek()) + .map(Secret::new) + .map_err(|error| { + tracing::warn!( + ?error, + "Failed to parse Open Banking additional_details; continuing without it" + ); + }) + .ok() + }), + }, )) } grpc_api_types::payments::payment_method::PaymentMethod::LocalBankRedirect(_) => { @@ -5385,6 +5478,7 @@ impl ForeignTryFrom<(PaymentServiceAuthorizeRequest, Connectors, &MaskedMetadata minor_amount_authorized: None, merchant_request_id: value.merchant_request_id.clone(), sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, }) } @@ -5488,6 +5582,7 @@ impl ForeignTryFrom<(AuthorizationRequest, Connectors, &MaskedMetadata)> for Pay merchant_request_id: value.merchant_request_id.clone(), l2_l3_data: l2_l3_data.map(Box::new), sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, }) } @@ -5582,6 +5677,7 @@ impl ForeignTryFrom<(SetupRecurringRequest, Connectors, &MaskedMetadata)> for Pa minor_amount_authorized: None, merchant_request_id: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, l2_l3_data: l2_l3_data.map(Box::new), settlement_status: None, }) @@ -5703,6 +5799,7 @@ impl merchant_request_id: None, l2_l3_data: l2_l3_data.map(Box::new), sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, }) } @@ -5800,6 +5897,7 @@ impl merchant_request_id: value.merchant_request_id.clone(), l2_l3_data: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, }) } @@ -5878,6 +5976,7 @@ impl ForeignTryFrom<(PaymentServiceVoidRequest, Connectors, &MaskedMetadata)> fo merchant_request_id: value.merchant_request_id.clone(), l2_l3_data: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, }) } @@ -5919,6 +6018,13 @@ impl let merchant_id_from_header = extract_merchant_id_from_metadata(metadata)?; + let access_token = value + .state + .as_ref() + .and_then(|state| state.access_token.as_ref()) + .map(ServerAuthenticationTokenResponseData::foreign_try_from) + .transpose()?; + Ok(Self { raw_connector_status: None, merchant_id: merchant_id_from_header, @@ -5945,7 +6051,7 @@ impl minor_amount_captured: None, minor_amount_capturable: None, amount: None, - access_token: None, + access_token, session_token: None, reference_id: None, connector_order_id: None, @@ -5967,6 +6073,7 @@ impl minor_amount_authorized: None, merchant_request_id: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, }) } @@ -6418,6 +6525,9 @@ pub fn generate_payment_method_eligibility_response( .into(), status_code: response.status_code, error_info: None, + payment_method_details: response + .payment_method_details + .map(grpc_api_types::payments::PaymentMethodDetails::foreign_from), raw_connector_request, typed_connector_request, raw_connector_response, @@ -6441,6 +6551,7 @@ pub fn generate_payment_method_eligibility_response( }), issuer_details: None, }), + payment_method_details: None, raw_connector_request, typed_connector_request, raw_connector_response, @@ -8275,6 +8386,22 @@ pub fn generate_payment_sync_response( connector_feature_data: convert_connector_metadata_to_secret_string( connector_metadata, ), + connector_returned_payment_method_details: router_data_v2 + .resource_common_data + .connector_returned_payment_method_details + .clone() + .and_then(|payment_method_data| { + grpc_api_types::payments::PaymentMethod::foreign_try_from( + payment_method_data, + ) + .map_err(|error| { + tracing::warn!( + ?error, + "Failed to convert connector_returned_payment_method_details; omitting it" + ); + }) + .ok() + }), }) } PaymentsResponseData::MultipleCaptureResponse { @@ -8386,6 +8513,7 @@ pub fn generate_payment_sync_response( |status| grpc_api_types::payments::SettlementStatus::from(status) as i32, ), connector_feature_data: None, + connector_returned_payment_method_details: None, }) } _ => Err(report!(ConnectorError::UnexpectedResponseError { @@ -8430,7 +8558,12 @@ pub fn generate_payment_sync_response( &e.connector_transaction_id, ), connector_reference_id: None, - merchant_transaction_id: None, + merchant_transaction_id: Some( + router_data_v2 + .resource_common_data + .connector_request_reference_id + .clone(), + ), mandate_reference: None, mandate_reference_details: None, status: status as i32, @@ -8478,6 +8611,7 @@ pub fn generate_payment_sync_response( splits: None, settlement_status: None, connector_feature_data: None, + connector_returned_payment_method_details: None, }) } } @@ -9450,6 +9584,7 @@ impl ForeignTryFrom for PaymentServiceGetResponse { splits: None, settlement_status: None, connector_feature_data: None, + connector_returned_payment_method_details: None, }) } } @@ -9504,6 +9639,7 @@ impl ForeignTryFrom for PaymentMethodEli Ok(Self { amount, customer, + connector_payment_method_id: value.connector_payment_method_id, country_code: country, payment_method_type, description: value.description, @@ -9877,6 +10013,7 @@ impl merchant_request_id: value.merchant_request_id.clone(), l2_l3_data: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, }) } @@ -9992,6 +10129,7 @@ impl merchant_request_id: None, l2_l3_data: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, }) } @@ -11073,6 +11211,7 @@ impl merchant_request_id: value.merchant_request_id.clone(), l2_l3_data: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, }) } @@ -11557,6 +11696,7 @@ impl merchant_request_id: None, l2_l3_data: l2_l3_data.map(Box::new), sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, }) } @@ -11658,6 +11798,7 @@ impl merchant_request_id: None, l2_l3_data: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, }) } @@ -12919,6 +13060,7 @@ impl merchant_request_id: None, l2_l3_data: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, }) } @@ -13695,6 +13837,7 @@ impl merchant_request_id: None, l2_l3_data: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, }) } @@ -13876,6 +14019,7 @@ impl merchant_request_id: value.merchant_request_id, l2_l3_data: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, }) } @@ -13991,6 +14135,7 @@ impl merchant_request_id: None, l2_l3_data: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, }) } @@ -14102,6 +14247,7 @@ impl merchant_request_id: None, l2_l3_data: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, }) } @@ -14400,6 +14546,7 @@ impl merchant_request_id: None, l2_l3_data: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, }) } @@ -14491,6 +14638,7 @@ impl l2_l3_data: None, sender_payment_instrument_id: None, settlement_status: None, + connector_returned_payment_method_details: None, }) } } @@ -16145,6 +16293,10 @@ impl< .transpose()?, mandate_reference: None, merchant_transaction_id: value.merchant_transaction_id, + metadata: value + .metadata + .map(|m| SecretSerdeValue::foreign_try_from((m, "metadata"))) + .transpose()?, }) } } @@ -16466,8 +16618,24 @@ impl connector_request_reference_id: extract_connector_request_reference_id( &value.merchant_order_id.clone(), ), - customer_id: None, - connector_customer: None, + customer_id: value + .customer + .clone() + .and_then(|customer| customer.connector_customer_id) + .map(|customer_id| CustomerId::try_from(Cow::from(customer_id))) + .transpose() + .change_context(IntegrationError::InvalidDataFormat { + field_name: "customer.connector_customer_id", + context: IntegrationErrorContext { + additional_context: Some("Failed to parse Customer Id".to_string()), + suggested_action: Some("Provide a valid connector customer ID".to_string()), + doc_url: None, + }, + })?, + connector_customer: value + .customer + .clone() + .and_then(|customer| customer.connector_customer_id), description: value.description, return_url: value.return_url.clone(), connector_feature_data: value @@ -16501,6 +16669,7 @@ impl merchant_request_id: None, l2_l3_data: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, }) } @@ -16614,6 +16783,7 @@ impl merchant_request_id: None, l2_l3_data: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, }) } @@ -16719,6 +16889,7 @@ impl merchant_request_id: None, l2_l3_data: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, }) } @@ -16802,6 +16973,7 @@ impl merchant_request_id: None, l2_l3_data: None, sender_payment_instrument_id: None, + connector_returned_payment_method_details: None, settlement_status: None, }) } diff --git a/crates/types-traits/grpc-api-types/proto/composite_payment.proto b/crates/types-traits/grpc-api-types/proto/composite_payment.proto index a6fde3b36f..a8785080a1 100644 --- a/crates/types-traits/grpc-api-types/proto/composite_payment.proto +++ b/crates/types-traits/grpc-api-types/proto/composite_payment.proto @@ -621,6 +621,34 @@ message CompositePaymentMethodRechargeResponse { optional PaymentMethodServiceRechargeResponse recharge_response = 2; // Recharge operation result } +// Request message for composite payment method eligibility flow. +message CompositePaymentMethodEligibilityRequest { + Money amount = 1; // Order amount and currency + optional Customer customer = 2; // Customer details (phone, email, name, etc.) + optional PaymentAddress address = 3; // Billing and shipping address + repeated OrderDetailsWithAmount order_details = 4; // Order line items + optional CountryAlpha2 country = 5; // Market/country the eligibility check is for + optional PaymentMethodType payment_method_type = 6; // The specific payment method eligibility is being checked for + optional string description = 7; // Description for connector-rendered eligibility messaging + optional SecretString connector_feature_data = 8; // Connector-specific feature data for the transaction + optional bool test_mode = 9; // Sandbox/test mode flag + optional string connector_payment_method_id = 10; // Connector-issued payment method ID (e.g. wallet number) being checked, when known + + // State Information (for access token) + optional ConnectorState state = 11; + + // Access Token Request fields + optional SecretString metadata = 12; + optional string merchant_access_token_id = 13; + optional string merchant_request_id = 14; // Per-request idempotency / correlation token forwarded to the bootstrap call +} + +// Response message for composite payment method eligibility flow. +message CompositePaymentMethodEligibilityResponse { + optional MerchantAuthenticationServiceCreateServerAuthenticationTokenResponse access_token_response = 1; + optional PaymentMethodServiceEligibilityResponse eligibility_response = 2; +} + // Request message for composite verify redirect response flow. message CompositeVerifyRedirectResponseRequest { // Identification diff --git a/crates/types-traits/grpc-api-types/proto/composite_services.proto b/crates/types-traits/grpc-api-types/proto/composite_services.proto index 1707744d87..d8ecb6dfae 100644 --- a/crates/types-traits/grpc-api-types/proto/composite_services.proto +++ b/crates/types-traits/grpc-api-types/proto/composite_services.proto @@ -57,6 +57,9 @@ service CompositePaymentMethodService { // Runs composite payment method recharge flow. rpc Recharge(CompositePaymentMethodRechargeRequest) returns (CompositePaymentMethodRechargeResponse); + + // Runs composite payment method eligibility flow with access token bootstrapping. + rpc Eligibility(CompositePaymentMethodEligibilityRequest) returns (CompositePaymentMethodEligibilityResponse); } // Service for composite fraud and risk management operations. diff --git a/crates/types-traits/grpc-api-types/proto/payment.proto b/crates/types-traits/grpc-api-types/proto/payment.proto index 63047b8a56..d51110dfb8 100644 --- a/crates/types-traits/grpc-api-types/proto/payment.proto +++ b/crates/types-traits/grpc-api-types/proto/payment.proto @@ -900,6 +900,8 @@ enum Connector { MAYA = 134; GRABPAY = 135; BOOST = 136; + CITIGATE = 137; + ILIXIUM = 138; } // Payment method types @@ -2821,6 +2823,7 @@ message PaymentServiceGetResponse { optional ConnectorSplitResponseData splits = 32; optional SecretString connector_feature_data = 33; + // Settlement phase reported by the connector. See SettlementStatus for the // per-value semantics. Absent when the connector doesn't distinguish phases. optional SettlementStatus settlement_status = 34; @@ -2834,6 +2837,10 @@ message PaymentServiceGetResponse { // Another reference for the connector's transaction, returned in the connector's response. // Distinct from connector_transaction_id — e.g. Stripe charge ID (ch_xxx), order codes, invoice numbers. optional string connector_reference_id = 37; + + // Payment method details received from the connector mid-payment, intended for + // saving for the returning-customer flow. + optional PaymentMethod connector_returned_payment_method_details = 40; } // Request message for voiding a payment @@ -5645,6 +5652,29 @@ message BoostConfig { optional string base_url = 50; } +message CitigateConfig { + SecretString api_key = 1; + SecretString key1 = 2; + optional string base_url = 50; +} + +message IlixiumConfig { + // Digest Calculation Password (never transmitted; input to X-MERCHANT-DIGEST) + SecretString api_key = 1; + // MerchantId - sent in the request body as merchant.merchantId + SecretString key1 = 2; + // AccountId - sent in the request body as merchant.accountId + SecretString api_secret = 3; + optional string base_url = 50; +} + +message MonerisConfig { + SecretString client_secret = 1; + SecretString merchant_id = 2; + SecretString client_id = 3; + 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; @@ -5961,6 +5991,12 @@ message ConnectorSpecificConfig { GrabpayConfig grabpay = 144; // BOOST = 136 BoostConfig boost = 145; + // CITIGATE = 137 + CitigateConfig citigate = 146; + // MONERIS = 48 + MonerisConfig moneris = 147; + // ILIXIUM = 138 + IlixiumConfig ilixium = 148; } } @@ -6231,6 +6267,15 @@ message PaymentMethodServiceEligibilityRequest { // Sandbox/test mode flag (true for test environment). optional bool test_mode = 10; + + // Connector-issued payment method ID (e.g. wallet number) being checked, + // when known. Mirrors PaymentMethodServiceGetRequest's identifier so + // connectors can reuse the same lookup as PaymentMethodService.Get. + optional string connector_payment_method_id = 11; + + // State Information (for access token), set by CompositePaymentMethodService.Eligibility + // after bootstrapping the connector's session token. + optional ConnectorState state = 12; } // Response from a pre-checkout eligibility check. @@ -6253,6 +6298,11 @@ message PaymentMethodServiceEligibilityResponse { // Response headers from the connector. map response_headers = 6; + + // Payment method details resolved as part of the eligibility check (e.g. + // wallet/gift-card balance and items), when the connector call that + // determines eligibility also returns them. + optional PaymentMethodDetails payment_method_details = 9; } // ============================================================================ diff --git a/crates/types-traits/grpc-api-types/proto/payment_methods.proto b/crates/types-traits/grpc-api-types/proto/payment_methods.proto index 8ea7225315..3d7f771e6f 100644 --- a/crates/types-traits/grpc-api-types/proto/payment_methods.proto +++ b/crates/types-traits/grpc-api-types/proto/payment_methods.proto @@ -985,7 +985,16 @@ message OpenBankingUK { // Open Banking - Open Banking implementation message OpenBanking { - // Fields will be added as needed for Open Banking integration + // International Bank Account Number + optional SecretString iban = 1; + // Bank account number + optional SecretString account_number = 2; + // Sort code for the bank account + optional SecretString sort_code = 3; + // Name of the account holder + optional SecretString account_holder_name = 4; + // Additional payment details as a JSON string + optional SecretString additional_details = 5; } // Open Banking Payment Initiation Service diff --git a/crates/types-traits/grpc-api-types/proto/payouts.proto b/crates/types-traits/grpc-api-types/proto/payouts.proto index 8653964fe8..d650f34510 100644 --- a/crates/types-traits/grpc-api-types/proto/payouts.proto +++ b/crates/types-traits/grpc-api-types/proto/payouts.proto @@ -155,6 +155,18 @@ message PixEmvBankTransferPayout { SecretString emv = 1; } +message TrustlyBankTransferPayout { + // The International Bank Account Number (IBAN). Provide either the IBAN or the + // bank_account_number/bank_number pair. + optional SecretString iban = 1; + // The bank account number (used when an IBAN is not available) + optional SecretString bank_account_number = 2; + // The bank/clearing number that identifies the destination bank + optional SecretString bank_number = 3; + // The ISO Alpha-2 country code of the bank (maps to Trustly's ClearingHouse) + CountryAlpha2 bank_country_code = 4; +} + message ApplePayDecrypt { // The device primary account number (DPAN) CardNumberType dpan = 1; @@ -194,15 +206,24 @@ message OpenBankingUkPayout { SecretString iban = 2; } +message OpenBankingPayout { + // The name of the account holder + SecretString account_holder_name = 1; + // The International Bank Account Number (IBAN) + SecretString iban = 2; +} + message Passthrough { // The token provided by the payment service provider string psp_token = 1; // The type of the tokenized payment method PaymentMethodType token_type = 2; + // The customer identifier held by the payment service provider for this token + optional SecretString psp_customer_id = 3; } // PayoutMethod represents a payout instrument used to process a payout -// It contains a oneof field with all possible payout method +// It contains a oneof field with all possible payout method message PayoutMethod { oneof payout_method_data { // CARD METHODS @@ -229,6 +250,8 @@ message PayoutMethod { // BANK TRANSFER METHODS PixKeyBankTransferPayout pix_key = 12; PixEmvBankTransferPayout pix_emv = 13; + OpenBankingPayout open_banking = 14; + TrustlyBankTransferPayout trustly = 15; } } @@ -331,6 +354,9 @@ message PayoutServiceTransferRequest { // Connector's reference for a prior eligibility check, when the processor // requires it to authorise the transfer. optional string connector_eligibility_reference_id = 16; + // Connector metadata carried over from a preceding flow (e.g. the recipient + // account id returned by create_recipient). + optional SecretString payout_connector_metadata = 17; } // Response message for transferring a payout @@ -515,6 +541,9 @@ message PayoutServiceCreateRecipientResponse { optional ErrorInfo error = 4; // HTTP status code from the connector uint32 status_code = 5; + // Connector metadata produced while creating the recipient (e.g. the + // registered account id), to be echoed back on the subsequent transfer. + optional SecretString connector_metadata = 6; } // Request message for enrolling disburse account diff --git a/crates/types-traits/ucs_interface_common/src/metadata.rs b/crates/types-traits/ucs_interface_common/src/metadata.rs index 8f4163c626..1bcff60b8b 100644 --- a/crates/types-traits/ucs_interface_common/src/metadata.rs +++ b/crates/types-traits/ucs_interface_common/src/metadata.rs @@ -28,6 +28,7 @@ pub struct MetadataPayload { pub tenant_id: String, pub request_id: String, pub merchant_id: String, + pub org_id: String, pub connector: connector_types::ConnectorVariant, pub lineage_ids: LineageIds<'static>, /// Typed connector integration config extracted from request metadata. @@ -56,6 +57,7 @@ pub fn get_metadata_payload( let (connector, connector_config) = connector_and_config_from_metadata(metadata)?; let merchant_id = merchant_id_from_metadata(metadata)?; + let org_id = org_id_from_metadata(metadata); let tenant_id = tenant_id_from_metadata(metadata)?; let request_id = request_id_from_metadata(metadata)?; let lineage_ids = extract_lineage_fields_from_metadata(metadata, &server_config.lineage); @@ -69,6 +71,7 @@ pub fn get_metadata_payload( tenant_id, request_id, merchant_id, + org_id, connector, lineage_ids, connector_config, @@ -263,6 +266,14 @@ pub fn merchant_id_from_metadata( )) } +pub fn org_id_from_metadata(metadata: &metadata::MetadataMap) -> String { + metadata + .get(consts::X_ORG_ID) + .and_then(|value| value.to_str().ok()) + .unwrap_or_default() + .to_string() +} + pub fn request_id_from_metadata( metadata: &metadata::MetadataMap, ) -> CustomResult { diff --git a/data/field_probe/authorizedotnet.json b/data/field_probe/authorizedotnet.json index 8f2b4b04bc..6a417ccde2 100644 --- a/data/field_probe/authorizedotnet.json +++ b/data/field_probe/authorizedotnet.json @@ -352,7 +352,7 @@ }, "OpenBanking": { "status": "not_supported", - "error": "Payment method BankRedirect(OpenBanking) is not supported by authorizedotnet" + "error": "Payment method BankRedirect(OpenBanking { account_number: None, sort_code: None, iban: None, account_holder_name: None, additional_details: None }) is not supported by authorizedotnet" }, "OpenBankingPis": { "status": "not_supported", diff --git a/data/field_probe/boost.json b/data/field_probe/boost.json index a607a40389..2b5177234f 100644 --- a/data/field_probe/boost.json +++ b/data/field_probe/boost.json @@ -294,7 +294,7 @@ }, "OpenBanking": { "status": "not_implemented", - "error": "This feature is not implemented: Boost only supports CardRedirect payment_method_data for Authorize (card-3DS-redirect flow); other payment method data types are out of scope for this connector implementation. Unsupported payment_method_data variant for Boost Authorize: BankRedirect(OpenBanking)" + "error": "This feature is not implemented: Boost only supports CardRedirect payment_method_data for Authorize (card-3DS-redirect flow); other payment method data types are out of scope for this connector implementation. Unsupported payment_method_data variant for Boost Authorize: BankRedirect(OpenBanking { account_number: None, sort_code: None, iban: None, account_holder_name: None, additional_details: None })" }, "OpenBankingPis": { "status": "not_supported", diff --git a/data/field_probe/citigate.json b/data/field_probe/citigate.json new file mode 100644 index 0000000000..00f1c35ea3 --- /dev/null +++ b/data/field_probe/citigate.json @@ -0,0 +1,807 @@ +{ + "connector": "citigate", + "flows": { + "authenticate": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: authenticate flow for citigate" + } + }, + "authorize": { + "Ach": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "AchBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Affirm": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Afterpay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Alfamart": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "AliPayRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "AmazonPayRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "ApplePay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "ApplePayDecrypted": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "ApplePayThirdPartySdk": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Bacs": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "BacsBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "BancontactCard": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "BcaBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Becs": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "BillDeskRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Bizum": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Blik": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Bluecode": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "BniVaBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Boleto": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "BriVaBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Card": { + "status": "supported", + "proto_request": { + "merchant_transaction_id": "probe_txn_001", + "amount": { + "minor_amount": 1000, + "currency": "USD" + }, + "payment_method": { + "card": { + "card_number": "4111111111111111", + "card_exp_month": "03", + "card_exp_year": "2030", + "card_cvc": "737", + "card_holder_name": "John Doe" + } + }, + "capture_method": "AUTOMATIC", + "address": { + "billing_address": { + "first_name": "John", + "last_name": "Doe", + "line1": "123 Main St", + "city": "Seattle", + "state": "WA", + "zip_code": "98101", + "country_alpha2_code": "US", + "email": "test@example.com", + "phone_number": "4155552671", + "phone_country_code": "+1" + } + }, + "auth_type": "NO_THREE_DS", + "return_url": "https://example.com/return", + "browser_info": { + "ip_address": "1.2.3.4" + } + }, + "sample": { + "url": "https://gw-test.cgate.tech/orion/interface/json.ashx", + "method": "Post", + "headers": { + "content-type": "application/json", + "via": "HyperSwitch" + }, + "body": "{\"PaymentTypeID\":\"1\",\"TransTypeID\":\"0\",\"MerchantName\":\"probe_key\",\"MerchantPassword\":\"probe_key\",\"MerchantRef\":\"probe_txn_001\",\"Currency\":\"USD\",\"Amount\":\"1000\",\"Brand\":\"VISA\",\"CardholderName\":\"John Doe\",\"CardNo\":\"4111111111111111\",\"ExpiryYear\":\"2030\",\"ExpiryMonth\":\"03\",\"CVV\":\"737\",\"Firstname\":\"John\",\"Surname\":\"Doe\",\"StreetLine1\":\"123 Main St\",\"City\":\"Seattle\",\"PostalCode\":\"98101\",\"StateProvince\":\"WA\",\"Country\":\"US\",\"Email\":\"test@example.com\",\"Telephone\":\"+14155552671\",\"UserIP\":\"1.2.3.4\",\"SuccessURL\":\"https://example.com/return\",\"FailURL\":\"https://example.com/return\"}" + } + }, + "CashappQr": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "CashfreeRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "CimbVaBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "ClassicReward": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Crypto": { + "status": "not_supported", + "error": "Invalid data format: payment_method. The provided payment method variant is empty or not supported by this flow" + }, + "Dana": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "DanamonVaBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "DuitNow": { + "status": "not_supported", + "error": "Invalid data format: payment_method. The provided payment method variant is empty or not supported by this flow" + }, + "EVoucher": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "EaseBuzzRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Efecty": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Eft": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Eps": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "FamilyMart": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "GCash": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Giropay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Givex": { + "status": "not_supported", + "error": "Invalid data format: payment_method. The provided payment method variant is empty or not supported by this flow" + }, + "GoPay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "GooglePay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "GooglePayDecrypted": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "GooglePayThirdPartySdk": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Ideal": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Indomaret": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "IndonesianBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "InstantBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "InstantBankTransferFinland": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "InstantBankTransferPoland": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Interac": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "KakaoPay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Klarna": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Lawson": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "LazyPayRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "LocalBankRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "LocalBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "MandiriVaBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "MbWay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Mifinity": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "MiniStop": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "MobilePayRedirect": { + "status": "not_supported", + "error": "Invalid data format: payment_method. The provided payment method variant is empty or not supported by this flow" + }, + "Momo": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "MultibancoBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Netbanking": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "OnlineBankingCzechRepublic": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "OnlineBankingFinland": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "OnlineBankingFpx": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "OnlineBankingPoland": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "OnlineBankingSlovakia": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "OnlineBankingThailand": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "OpenBanking": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "OpenBankingPis": { + "status": "not_supported", + "error": "Invalid data format: payment_method. The provided payment method variant is empty or not supported by this flow" + }, + "OpenBankingUk": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Oxxo": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "PagoEfectivo": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "PayEasy": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "PaySafeCard": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "PayURedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "PaypalRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "PaypalSdk": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Paysera": { + "status": "not_supported", + "error": "Invalid data format: payment_method. The provided payment method variant is empty or not supported by this flow" + }, + "Paze": { + "status": "not_supported", + "error": "Invalid data format: payment_method. The provided payment method variant is empty or not supported by this flow" + }, + "PermataBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "PhonePeRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Pix": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Przelewy24": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Pse": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "RedCompra": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "RedPagos": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "RevolutPay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "SamsungPay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Satispay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Seicomart": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Sepa": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "SepaBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "SepaGuaranteedDebit": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "SevenEleven": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Skrill": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Sofort": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Swish": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "TouchNGo": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Trustly": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Twint": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "UpiCollect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "UpiIntent": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "UpiQr": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Vipps": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "WeChatPayQr": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + }, + "Wero": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + } + }, + "capture": { + "default": { + "status": "supported", + "proto_request": { + "merchant_capture_id": "probe_capture_001", + "connector_transaction_id": "probe_connector_txn_001", + "amount_to_capture": { + "minor_amount": 1000, + "currency": "USD" + } + }, + "sample": { + "url": "https://gw-test.cgate.tech/orion/interface/json.ashx", + "method": "Post", + "headers": { + "content-type": "application/json", + "via": "HyperSwitch" + }, + "body": "{\"PaymentTypeID\":\"1\",\"TransTypeID\":\"3\",\"MerchantName\":\"probe_key\",\"MerchantPassword\":\"probe_key\",\"MerchantRef\":\"probe_capture_001\",\"TransactionID\":\"probe_connector_txn_001\"}" + } + } + }, + "create_client_authentication_token": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: client_authentication_token flow for citigate" + } + }, + "create_order": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: create_order flow for citigate" + } + }, + "create_server_authentication_token": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: server_authentication_token flow for citigate" + } + }, + "create_server_session_authentication_token": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: server_session_authentication_token flow for citigate" + } + }, + "customer_create": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: create_connector_customer flow for citigate" + } + }, + "customer_get": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: get_connector_customer flow for citigate" + } + }, + "dispute_accept": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: accept_dispute flow for citigate" + } + }, + "dispute_defend": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: defend_dispute flow for citigate" + } + }, + "dispute_get": { + "default": { + "status": "not_implemented" + } + }, + "dispute_submit_evidence": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: submit_evidence flow for citigate" + } + }, + "eligibility": { + "default": { + "status": "not_supported", + "error": "eligibility flow not supported by citigate connector" + } + }, + "get": { + "default": { + "status": "supported", + "proto_request": { + "merchant_transaction_id": "probe_merchant_txn_001", + "connector_transaction_id": "probe_connector_txn_001", + "amount": { + "minor_amount": 1000, + "currency": "USD" + } + }, + "sample": { + "url": "https://gw-test.cgate.tech/orion/interface/json.ashx", + "method": "Post", + "headers": { + "content-type": "application/json", + "via": "HyperSwitch" + }, + "body": "{\"PaymentTypeID\":\"1\",\"TransTypeID\":\"8\",\"MerchantName\":\"probe_key\",\"MerchantPassword\":\"probe_key\",\"MerchantRef\":\"probe_merchant_txn_001\"}" + } + } + }, + "handle_event": { + "default": { + "status": "not_implemented" + } + }, + "incremental_authorization": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: incremental_authorization flow for citigate" + } + }, + "parse_event": { + "default": { + "status": "not_implemented" + } + }, + "payment_method_eligibility": { + "default": { + "status": "not_implemented" + } + }, + "post_authenticate": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: post_authenticate flow for citigate" + } + }, + "pre_authenticate": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: pre_authenticate flow for citigate" + } + }, + "proxy_authorize": { + "default": { + "status": "supported", + "proto_request": { + "merchant_transaction_id": "probe_proxy_txn_001", + "amount": { + "minor_amount": 1000, + "currency": "USD" + }, + "card_proxy": { + "card_number": "4111111111111111", + "card_exp_month": "03", + "card_exp_year": "2030", + "card_cvc": "123", + "card_holder_name": "John Doe", + "card_network": "VISA" + }, + "address": { + "billing_address": { + "first_name": "John", + "last_name": "Doe", + "line1": "123 Main St", + "city": "Seattle", + "state": "WA", + "zip_code": "98101", + "country_alpha2_code": "US", + "email": "test@example.com", + "phone_number": "4155552671", + "phone_country_code": "+1" + } + }, + "capture_method": "AUTOMATIC", + "auth_type": "NO_THREE_DS", + "return_url": "https://example.com/return", + "browser_info": { + "ip_address": "1.2.3.4" + } + }, + "sample": { + "url": "https://gw-test.cgate.tech/orion/interface/json.ashx", + "method": "Post", + "headers": { + "content-type": "application/json", + "via": "HyperSwitch" + }, + "body": "{\"PaymentTypeID\":\"1\",\"TransTypeID\":\"0\",\"MerchantName\":\"probe_key\",\"MerchantPassword\":\"probe_key\",\"MerchantRef\":\"probe_proxy_txn_001\",\"Currency\":\"USD\",\"Amount\":\"1000\",\"Brand\":\"VISA\",\"CardholderName\":\"John Doe\",\"CardNo\":\"{{$card_number}}\",\"ExpiryYear\":\"2030\",\"ExpiryMonth\":\"03\",\"CVV\":\"{{$card_cvc}}\",\"Firstname\":\"John\",\"Surname\":\"Doe\",\"StreetLine1\":\"123 Main St\",\"City\":\"Seattle\",\"PostalCode\":\"98101\",\"StateProvince\":\"WA\",\"Country\":\"US\",\"Email\":\"test@example.com\",\"Telephone\":\"+14155552671\",\"UserIP\":\"1.2.3.4\",\"SuccessURL\":\"https://example.com/return\",\"FailURL\":\"https://example.com/return\"}" + } + } + }, + "proxy_setup_recurring": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: setup_mandate flow for citigate" + } + }, + "recurring_charge": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: repeat_payment flow for citigate" + } + }, + "recurring_revoke": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: mandate_revoke flow for citigate" + } + }, + "refresh": { + "default": { + "status": "not_implemented" + } + }, + "refund": { + "default": { + "status": "supported", + "proto_request": { + "merchant_refund_id": "probe_refund_001", + "connector_transaction_id": "probe_connector_txn_001", + "payment_amount": 1000, + "refund_amount": { + "minor_amount": 1000, + "currency": "USD" + }, + "reason": "customer_request" + }, + "sample": { + "url": "https://gw-test.cgate.tech/orion/interface/json.ashx", + "method": "Post", + "headers": { + "content-type": "application/json", + "via": "HyperSwitch" + }, + "body": "{\"PaymentTypeID\":\"1\",\"TransTypeID\":\"5\",\"MerchantName\":\"probe_key\",\"MerchantPassword\":\"probe_key\",\"MerchantRef\":\"probe_refund_001\",\"TransactionID\":\"probe_connector_txn_001\"}" + } + } + }, + "refund_get": { + "default": { + "status": "supported", + "proto_request": { + "merchant_refund_id": "probe_refund_001", + "connector_transaction_id": "probe_connector_txn_001", + "refund_id": "probe_refund_id_001" + }, + "sample": { + "url": "https://gw-test.cgate.tech/orion/interface/json.ashx", + "method": "Post", + "headers": { + "content-type": "application/json", + "via": "HyperSwitch" + }, + "body": "{\"PaymentTypeID\":\"1\",\"TransTypeID\":\"8\",\"MerchantName\":\"probe_key\",\"MerchantPassword\":\"probe_key\",\"MerchantRef\":\"probe_refund_001\"}" + } + } + }, + "reverse": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: void_post_capture flow for citigate" + } + }, + "setup_recurring": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: setup_mandate flow for citigate" + } + }, + "token_authorize": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are supported by citigate" + } + }, + "token_setup_recurring": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: setup_mandate flow for citigate" + } + }, + "tokenize": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: payment_method_token flow for citigate" + } + }, + "verify_redirect": { + "default": { + "status": "not_implemented" + } + }, + "void": { + "default": { + "status": "supported", + "proto_request": { + "merchant_void_id": "probe_void_001", + "connector_transaction_id": "probe_connector_txn_001" + }, + "sample": { + "url": "https://gw-test.cgate.tech/orion/interface/json.ashx", + "method": "Post", + "headers": { + "content-type": "application/json", + "via": "HyperSwitch" + }, + "body": "{\"PaymentTypeID\":\"1\",\"TransTypeID\":\"4\",\"MerchantName\":\"probe_key\",\"MerchantPassword\":\"probe_key\",\"MerchantRef\":\"probe_void_001\",\"TransactionID\":\"probe_connector_txn_001\"}" + } + } + } + } +} \ No newline at end of file diff --git a/data/field_probe/glomopay.json b/data/field_probe/glomopay.json index d31279ecf2..26ba68c66a 100644 --- a/data/field_probe/glomopay.json +++ b/data/field_probe/glomopay.json @@ -326,7 +326,7 @@ }, "OpenBanking": { "status": "not_implemented", - "error": "This feature is not implemented: Glomopay Authorize does not support payment method variant: BankRedirect(OpenBanking)" + "error": "This feature is not implemented: Glomopay Authorize does not support payment method variant: BankRedirect(OpenBanking { account_number: None, sort_code: None, iban: None, account_holder_name: None, additional_details: None })" }, "OpenBankingPis": { "status": "not_supported", diff --git a/data/field_probe/ilixium.json b/data/field_probe/ilixium.json new file mode 100644 index 0000000000..3f10e1b6c3 --- /dev/null +++ b/data/field_probe/ilixium.json @@ -0,0 +1,797 @@ +{ + "connector": "ilixium", + "flows": { + "authenticate": { + "default": { + "status": "not_supported", + "error": "authenticate flow not supported by ilixium connector" + } + }, + "authorize": { + "Ach": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankDebit(AchBankDebit { account_number: *** alloc::string::String ***, routing_number: *** alloc::string::String ***, card_holder_name: None, bank_account_holder_name: Some(*** alloc::string::String ***), bank_name: None, bank_type: None, bank_holder_type: None })" + }, + "AchBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankTransfer(AchBankTransfer)" + }, + "Affirm": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: PayLater(AffirmRedirect)" + }, + "Afterpay": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: PayLater(AfterpayClearpayRedirect)" + }, + "Alfamart": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Voucher(Alfamart(AlfamartVoucherData))" + }, + "AliPayRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(AliPayRedirect(AliPayRedirection))" + }, + "AmazonPayRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(AmazonPayRedirect(AmazonPayRedirectData))" + }, + "ApplePay": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(ApplePay(ApplePayWalletData { payment_data: Encrypted(\"eyJ2ZXJzaW9uIjoiRUNfdjEiLCJkYXRhIjoicHJvYmUiLCJzaWduYXR1cmUiOiJwcm9iZSJ9\"), payment_method: ApplepayPaymentMethod { display_name: \"Visa 1111\", network: \"Visa\", pm_type: \"debit\" }, transaction_identifier: \"probe_txn_id\" }))" + }, + "ApplePayDecrypted": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(ApplePay(ApplePayWalletData { payment_data: Decrypted(ApplePayDecryptedData { application_primary_account_number: CardNumber(411111**********), application_expiration_month: *** alloc::string::String ***, application_expiration_year: *** alloc::string::String ***, payment_data: ApplePayCryptogramData { online_payment_cryptogram: *** alloc::string::String ***, eci_indicator: Some(\"05\") } }), payment_method: ApplepayPaymentMethod { display_name: \"Visa 1111\", network: \"Visa\", pm_type: \"debit\" }, transaction_identifier: \"probe_txn_id\" }))" + }, + "ApplePayThirdPartySdk": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(ApplePayThirdPartySdk(ApplePayThirdPartySdkData { token: Some(*** alloc::string::String ***) }))" + }, + "Bacs": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankDebit(BacsBankDebit { account_number: *** alloc::string::String ***, sort_code: *** alloc::string::String ***, bank_account_holder_name: Some(*** alloc::string::String ***) })" + }, + "BacsBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankTransfer(BacsBankTransfer)" + }, + "BancontactCard": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankRedirect(BancontactCard { card_number: Some(CardNumber(411111**********)), card_exp_month: Some(*** alloc::string::String ***), card_exp_year: Some(*** alloc::string::String ***), card_holder_name: Some(*** alloc::string::String ***) })" + }, + "BcaBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankTransfer(BcaBankTransfer)" + }, + "Becs": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankDebit(BecsBankDebit { account_number: *** alloc::string::String ***, bsb_number: *** alloc::string::String ***, bank_account_holder_name: Some(*** alloc::string::String ***) })" + }, + "BillDeskRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(BillDeskRedirect(BillDeskRedirection))" + }, + "Bizum": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankRedirect(Bizum)" + }, + "Blik": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankRedirect(Blik { blik_code: Some(\"777124\") })" + }, + "Bluecode": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(BluecodeRedirect)" + }, + "BniVaBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankTransfer(BniVaBankTransfer)" + }, + "Boleto": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Voucher(Boleto(BoletoVoucherData { social_security_number: None, expiration_date: None }))" + }, + "BriVaBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankTransfer(BriVaBankTransfer)" + }, + "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", + "customer": { + "email": "test@example.com" + }, + "address": { + "billing_address": { + "first_name": "John", + "last_name": "Doe", + "country_alpha2_code": "US" + } + }, + "auth_type": "NO_THREE_DS", + "return_url": "https://example.com/return" + }, + "sample": { + "url": "https://prprocessing.ilixium.com/platform/ili/direct/auth", + "method": "Post", + "headers": { + "accept": "application/json", + "content-type": "application/json", + "via": "HyperSwitch", + "x-merchant-digest": "cHJvYmVfa2V5OnByb2JlX3NlY3JldA==" + }, + "body": "{\"version\":2,\"deferredCapture\":false,\"transaction\":{\"transactionType\":\"ECOMMERCE\",\"merchantRef\":\"probe_txn_001\",\"amount\":\"1000\",\"currency\":\"USD\"},\"paymentMethodType\":\"CARD\",\"merchant\":{\"merchantId\":\"probe_key\",\"accountId\":\"probe_key\"},\"card\":{\"cardNumber\":\"4111111111111111\",\"expiryDate\":\"032030\",\"securityCode\":\"737\"},\"customer\":{\"customerId\":\"probe_txn_001\",\"email\":\"test@example.com\",\"firstName\":\"John\",\"surname\":\"Doe\",\"address\":{\"country\":\"USA\"}}}" + } + }, + "CashappQr": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(CashappQr(CashappQr))" + }, + "CashfreeRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(CashfreeRedirect(CashfreeRedirection))" + }, + "CimbVaBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankTransfer(CimbVaBankTransfer)" + }, + "ClassicReward": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Reward" + }, + "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: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(DanaRedirect)" + }, + "DanamonVaBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankTransfer(DanamonVaBankTransfer)" + }, + "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: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Reward" + }, + "EaseBuzzRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(EaseBuzzRedirect(EaseBuzzRedirection))" + }, + "Efecty": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Voucher(Efecty)" + }, + "Eft": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankRedirect(Eft { provider: \"ozow\" })" + }, + "Eps": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankRedirect(Eps { bank_name: None, country: None })" + }, + "FamilyMart": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Voucher(FamilyMart(JCSVoucherData))" + }, + "GCash": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(GcashRedirect(GcashRedirection))" + }, + "Giropay": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankRedirect(Giropay { bank_account_bic: None, bank_account_iban: None, country: None })" + }, + "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: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(GoPayRedirect(GoPayRedirection))" + }, + "GooglePay": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(GooglePay(GooglePayWalletData { pm_type: \"CARD\", description: \"Visa 1111\", info: GooglePayPaymentMethodInfo { card_network: \"VISA\", card_details: \"1111\", assurance_details: None }, tokenization_data: Encrypted(GpayEncryptedTokenizationData { token_type: \"PAYMENT_GATEWAY\", token: \"{\\\"id\\\":\\\"tok_probe_gpay\\\",\\\"object\\\":\\\"token\\\",\\\"type\\\":\\\"card\\\"}\" }) }))" + }, + "GooglePayDecrypted": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(GooglePay(GooglePayWalletData { pm_type: \"CARD\", description: \"Visa 1111\", info: GooglePayPaymentMethodInfo { card_network: \"VISA\", card_details: \"1111\", assurance_details: None }, tokenization_data: Decrypted(GooglePayDecryptedData { card_exp_month: *** alloc::string::String ***, card_exp_year: *** alloc::string::String ***, application_primary_account_number: CardNumber(411111**********), cryptogram: Some(*** alloc::string::String ***), eci_indicator: Some(\"05\") }) }))" + }, + "GooglePayThirdPartySdk": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(GooglePayThirdPartySdk(GooglePayThirdPartySdkData { token: Some(*** alloc::string::String ***) }))" + }, + "Ideal": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankRedirect(Ideal { bank_name: None })" + }, + "Indomaret": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Voucher(Indomaret(IndomaretVoucherData))" + }, + "IndonesianBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankTransfer(IndonesianBankTransfer { bank_name: None })" + }, + "InstantBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankTransfer(InstantBankTransfer)" + }, + "InstantBankTransferFinland": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankTransfer(InstantBankTransferFinland)" + }, + "InstantBankTransferPoland": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankTransfer(InstantBankTransferPoland)" + }, + "Interac": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankRedirect(Interac { country: None, email: None })" + }, + "KakaoPay": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(KakaoPayRedirect(KakaoPayRedirection))" + }, + "Klarna": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: PayLater(KlarnaRedirect)" + }, + "Lawson": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Voucher(Lawson(JCSVoucherData))" + }, + "LazyPayRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(LazyPayRedirect(LazyPayRedirection))" + }, + "LocalBankRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankRedirect(LocalBankRedirect)" + }, + "LocalBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankTransfer(LocalBankTransfer { bank_code: None })" + }, + "MandiriVaBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankTransfer(MandiriVaBankTransfer)" + }, + "MbWay": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(MbWay(MbWayData))" + }, + "Mifinity": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(Mifinity(MifinityData { date_of_birth: *** time::date::Date ***, language_preference: Some(\"en\") }))" + }, + "MiniStop": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Voucher(MiniStop(JCSVoucherData))" + }, + "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: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(MomoRedirect(MomoRedirection))" + }, + "MultibancoBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankTransfer(MultibancoBankTransfer)" + }, + "Netbanking": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankRedirect(Netbanking { issuer: HdfcBank })" + }, + "OnlineBankingCzechRepublic": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankRedirect(OnlineBankingCzechRepublic { issuer: CeskaSporitelna })" + }, + "OnlineBankingFinland": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankRedirect(OnlineBankingFinland { email: Some(Email(****@example.com)) })" + }, + "OnlineBankingFpx": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankRedirect(OnlineBankingFpx { issuer: Maybank })" + }, + "OnlineBankingPoland": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankRedirect(OnlineBankingPoland { issuer: BankPekaoSa })" + }, + "OnlineBankingSlovakia": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankRedirect(OnlineBankingSlovakia { issuer: TatraPay })" + }, + "OnlineBankingThailand": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankRedirect(OnlineBankingThailand { issuer: BangkokBank })" + }, + "OpenBanking": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankRedirect(OpenBanking { account_number: None, sort_code: None, iban: None, account_holder_name: None, additional_details: None })" + }, + "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: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankRedirect(OpenBankingUk { issuer: None, country: None })" + }, + "Oxxo": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Voucher(Oxxo)" + }, + "PagoEfectivo": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Voucher(PagoEfectivo)" + }, + "PayEasy": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Voucher(PayEasy(JCSVoucherData))" + }, + "PaySafeCard": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: GiftCard(PaySafeCard)" + }, + "PayURedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(PayURedirect(PayURedirection))" + }, + "PaypalRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(PaypalRedirect(PaypalRedirection { email: Some(Email(****@example.com)) }))" + }, + "PaypalSdk": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(PaypalSdk(PayPalWalletData { token: \"probe_paypal_sdk_token\" }))" + }, + "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: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankTransfer(PermataBankTransfer)" + }, + "PhonePeRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(PhonePeRedirect(PhonePeRedirection))" + }, + "Pix": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankTransfer(Pix { pix_key: None, cpf: None, cnpj: None, source_bank_account_id: None, destination_bank_account_id: None, expiry_date: None })" + }, + "Przelewy24": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankRedirect(Przelewy24 { bank_name: None })" + }, + "Pse": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankTransfer(Pse)" + }, + "RedCompra": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Voucher(RedCompra)" + }, + "RedPagos": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Voucher(RedPagos)" + }, + "RevolutPay": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(RevolutPay(RevolutPayData))" + }, + "SamsungPay": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(SamsungPay(SamsungPayWalletData { payment_credential: SamsungPayWalletCredentials { method: Some(\"3DS\"), recurring_payment: Some(false), card_brand: Visa, dpan_last_four_digits: None, card_last_four_digits: \"1234\", token_data: SamsungPayTokenData { three_ds_type: Some(\"S\"), version: \"100\", data: *** alloc::string::String *** } } }))" + }, + "Satispay": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(Satispay(SatispayData))" + }, + "Seicomart": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Voucher(Seicomart(JCSVoucherData))" + }, + "Sepa": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankDebit(SepaBankDebit { iban: *** alloc::string::String ***, bank_account_holder_name: Some(*** alloc::string::String ***) })" + }, + "SepaBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankTransfer(SepaBankTransfer)" + }, + "SepaGuaranteedDebit": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankDebit(SepaGuaranteedBankDebit { iban: *** alloc::string::String ***, bank_account_holder_name: Some(*** alloc::string::String ***) })" + }, + "SevenEleven": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Voucher(SevenEleven(JCSVoucherData))" + }, + "Skrill": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(Skrill(SkrillData))" + }, + "Sofort": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankRedirect(Sofort { country: None, preferred_language: None })" + }, + "Swish": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(SwishQr(SwishQrData))" + }, + "TouchNGo": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(TouchNGoRedirect(TouchNGoRedirection))" + }, + "Trustly": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: BankRedirect(Trustly { country: None })" + }, + "Twint": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(TwintRedirect)" + }, + "UpiCollect": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Upi(UpiCollect(UpiCollectData { vpa_id: Some(****@upi), upi_source: None }))" + }, + "UpiIntent": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Upi(UpiIntent(UpiIntentData { upi_source: None, app_name: None }))" + }, + "UpiQr": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Upi(UpiQr(UpiQrData { upi_source: None }))" + }, + "Vipps": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(VippsRedirect)" + }, + "WeChatPayQr": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(WeChatPayQr(WeChatPayQr))" + }, + "Wero": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: Wallet(Wero(WeroData))" + } + }, + "capture": { + "default": { + "status": "supported", + "proto_request": { + "merchant_capture_id": "probe_capture_001", + "connector_transaction_id": "probe_connector_txn_001", + "amount_to_capture": { + "minor_amount": 1000, + "currency": "USD" + } + }, + "sample": { + "url": "https://prprocessing.ilixium.com/platform/ili/direct/capture", + "method": "Post", + "headers": { + "accept": "application/json", + "content-type": "application/json", + "via": "HyperSwitch", + "x-merchant-digest": "cHJvYmVfa2V5OnByb2JlX3NlY3JldA==" + }, + "body": "{\"version\":2,\"transaction\":{\"merchantRef\":\"probe_capture_001\",\"amount\":\"1000\",\"currency\":\"USD\"},\"merchant\":{\"merchantId\":\"probe_key\",\"accountId\":\"probe_key\"}}" + } + } + }, + "create_client_authentication_token": { + "default": { + "status": "not_supported", + "error": "client_authentication_token flow not supported by ilixium connector" + } + }, + "create_order": { + "default": { + "status": "not_supported", + "error": "create_order flow not supported by ilixium connector" + } + }, + "create_server_authentication_token": { + "default": { + "status": "not_supported", + "error": "server_authentication_token flow not supported by ilixium connector" + } + }, + "create_server_session_authentication_token": { + "default": { + "status": "not_supported", + "error": "server_session_authentication_token flow not supported by ilixium connector" + } + }, + "customer_create": { + "default": { + "status": "not_supported", + "error": "create_connector_customer flow not supported by ilixium connector" + } + }, + "customer_get": { + "default": { + "status": "not_supported", + "error": "get_connector_customer flow not supported by ilixium connector" + } + }, + "dispute_accept": { + "default": { + "status": "not_supported", + "error": "accept_dispute flow not supported by ilixium connector" + } + }, + "dispute_defend": { + "default": { + "status": "not_supported", + "error": "defend_dispute flow not supported by ilixium connector" + } + }, + "dispute_get": { + "default": { + "status": "not_implemented" + } + }, + "dispute_submit_evidence": { + "default": { + "status": "not_supported", + "error": "submit_evidence flow not supported by ilixium connector" + } + }, + "eligibility": { + "default": { + "status": "not_supported", + "error": "eligibility flow not supported by ilixium connector" + } + }, + "get": { + "default": { + "status": "supported", + "proto_request": { + "merchant_transaction_id": "probe_merchant_txn_001", + "connector_transaction_id": "probe_connector_txn_001", + "amount": { + "minor_amount": 1000, + "currency": "USD" + } + }, + "sample": { + "url": "https://prprocessing.ilixium.com/platform/ili/history/operations", + "method": "Post", + "headers": { + "accept": "application/json", + "content-type": "application/json", + "via": "HyperSwitch", + "x-merchant-digest": "cHJvYmVfa2V5OnByb2JlX3NlY3JldA==" + }, + "body": "{\"periodStartDate\":\"2020-01-01T00:00:00+00:00\",\"periodEndDate\":\"2020-01-01T00:00:00+00:00\",\"merchant\":{\"merchantId\":\"probe_key\",\"accountId\":\"probe_key\"},\"reportFormat\":\"JSON\"}" + } + } + }, + "handle_event": { + "default": { + "status": "not_implemented" + } + }, + "incremental_authorization": { + "default": { + "status": "not_supported", + "error": "incremental_authorization flow not supported by ilixium connector" + } + }, + "parse_event": { + "default": { + "status": "not_implemented" + } + }, + "payment_method_eligibility": { + "default": { + "status": "not_implemented" + } + }, + "post_authenticate": { + "default": { + "status": "not_supported", + "error": "post_authenticate flow not supported by ilixium connector" + } + }, + "pre_authenticate": { + "default": { + "status": "error", + "error": "Stuck on field: connector_request_reference_id — Missing required field: connector_request_reference_id" + } + }, + "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" + }, + "customer": { + "email": "test@example.com" + }, + "address": { + "billing_address": { + "first_name": "John", + "last_name": "Doe", + "country_alpha2_code": "US" + } + }, + "capture_method": "AUTOMATIC", + "auth_type": "NO_THREE_DS", + "return_url": "https://example.com/return" + }, + "sample": { + "url": "https://prprocessing.ilixium.com/platform/ili/direct/auth", + "method": "Post", + "headers": { + "accept": "application/json", + "content-type": "application/json", + "via": "HyperSwitch", + "x-merchant-digest": "cHJvYmVfa2V5OnByb2JlX3NlY3JldA==" + }, + "body": "{\"version\":2,\"deferredCapture\":false,\"transaction\":{\"transactionType\":\"ECOMMERCE\",\"merchantRef\":\"probe_proxy_txn_001\",\"amount\":\"1000\",\"currency\":\"USD\"},\"paymentMethodType\":\"CARD\",\"merchant\":{\"merchantId\":\"probe_key\",\"accountId\":\"probe_key\"},\"card\":{\"cardNumber\":\"{{$card_number}}\",\"expiryDate\":\"032030\",\"securityCode\":\"{{$card_cvc}}\"},\"customer\":{\"customerId\":\"probe_proxy_txn_001\",\"email\":\"test@example.com\",\"firstName\":\"John\",\"surname\":\"Doe\",\"address\":{\"country\":\"USA\"}}}" + } + } + }, + "proxy_setup_recurring": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: setup_mandate flow for ilixium" + } + }, + "recurring_charge": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: repeat_payment flow for ilixium" + } + }, + "recurring_revoke": { + "default": { + "status": "not_supported", + "error": "mandate_revoke flow not supported by ilixium connector" + } + }, + "refresh": { + "default": { + "status": "not_implemented" + } + }, + "refund": { + "default": { + "status": "supported", + "proto_request": { + "merchant_refund_id": "probe_refund_001", + "connector_transaction_id": "probe_connector_txn_001", + "payment_amount": 1000, + "refund_amount": { + "minor_amount": 1000, + "currency": "USD" + }, + "reason": "customer_request", + "connector_order_id": "connector_order_id" + }, + "sample": { + "url": "https://prprocessing.ilixium.com/platform/ili/direct/refund", + "method": "Post", + "headers": { + "accept": "application/json", + "content-type": "application/json", + "via": "HyperSwitch", + "x-merchant-digest": "cHJvYmVfa2V5OnByb2JlX3NlY3JldA==" + }, + "body": "{\"version\":2,\"transaction\":{\"merchantRef\":\"connector_order_id\",\"amount\":\"1000\",\"currency\":\"USD\"},\"merchant\":{\"merchantId\":\"probe_key\",\"accountId\":\"probe_key\"}}" + } + } + }, + "refund_get": { + "default": { + "status": "error", + "error": "Stuck on field: connector_order_id. RefundFlowData::connector_request_reference_id is derived from merchant_refund_id, so it identifies the refund rather than the payment, and connector_transaction_id holds Ilixium's gatewayRef — Missing required field: connector_order_id. RefundFlowData::connector_request_reference_id is derived from merchant_refund_id, so it identifies the refund rather than the payment, and connector_transaction_id holds Ilixium's gatewayRef (a dashed UUID) which is not a merchantRef and cannot be converted into one. Rather than send a reference that matches no transaction — response code 104, 'No Matching Transaction', or a history filter that selects nothing — the request is refused here." + } + }, + "reverse": { + "default": { + "status": "not_supported", + "error": "void_post_capture flow not supported by ilixium connector" + } + }, + "setup_recurring": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: setup_mandate flow for ilixium" + } + }, + "token_authorize": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: Ilixium supports only raw card payments on POST /direct/auth; wallets, APMs, bank transfers and stored-card tokens are out of scope for this connector implementation. Unsupported payment_method_data variant for Ilixium /direct/auth: PaymentMethodToken(PaymentMethodToken { token: *** alloc::string::String *** })" + } + }, + "token_setup_recurring": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: setup_mandate flow for ilixium" + } + }, + "tokenize": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: payment_method_token flow for ilixium" + } + }, + "verify_redirect": { + "default": { + "status": "not_implemented" + } + }, + "void": { + "default": { + "status": "supported", + "proto_request": { + "merchant_void_id": "probe_void_001", + "connector_transaction_id": "probe_connector_txn_001", + "amount": { + "minor_amount": 1000, + "currency": "USD" + } + }, + "sample": { + "url": "https://prprocessing.ilixium.com/platform/ili/direct/reversal", + "method": "Post", + "headers": { + "accept": "application/json", + "content-type": "application/json", + "via": "HyperSwitch", + "x-merchant-digest": "cHJvYmVfa2V5OnByb2JlX3NlY3JldA==" + }, + "body": "{\"version\":2,\"transaction\":{\"merchantRef\":\"probe_void_001\",\"amount\":\"1000\",\"currency\":\"USD\"},\"merchant\":{\"merchantId\":\"probe_key\",\"accountId\":\"probe_key\"}}" + } + } + } + } +} \ No newline at end of file diff --git a/data/field_probe/juspay.json b/data/field_probe/juspay.json index 6743a820d5..0f84f53d18 100644 --- a/data/field_probe/juspay.json +++ b/data/field_probe/juspay.json @@ -735,7 +735,7 @@ }, "OpenBanking": { "status": "not_implemented", - "error": "This feature is not implemented: Juspay does not support bank redirect variant: OpenBanking" + "error": "This feature is not implemented: Juspay does not support bank redirect variant: OpenBanking { account_number: None, sort_code: None, iban: None, account_holder_name: None, additional_details: None }" }, "OpenBankingPis": { "status": "not_supported", diff --git a/data/field_probe/moneris.json b/data/field_probe/moneris.json new file mode 100644 index 0000000000..b4dc4030af --- /dev/null +++ b/data/field_probe/moneris.json @@ -0,0 +1,858 @@ +{ + "connector": "moneris", + "flows": { + "authenticate": { + "default": { + "status": "not_supported", + "error": "authenticate flow not supported by moneris connector" + } + }, + "authorize": { + "Ach": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankDebit(AchBankDebit { account_number: *** alloc::string::String ***, routing_number: *** alloc::string::String ***, card_holder_name: None, bank_account_holder_name: Some(*** alloc::string::String ***), bank_name: None, bank_type: None, bank_holder_type: None })." + }, + "AchBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankTransfer(AchBankTransfer)." + }, + "Affirm": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: PayLater(AffirmRedirect)." + }, + "Afterpay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: PayLater(AfterpayClearpayRedirect)." + }, + "Alfamart": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Voucher(Alfamart(AlfamartVoucherData))." + }, + "AliPayRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(AliPayRedirect(AliPayRedirection))." + }, + "AmazonPayRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(AmazonPayRedirect(AmazonPayRedirectData))." + }, + "ApplePay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(ApplePay(ApplePayWalletData { payment_data: Encrypted(\"eyJ2ZXJzaW9uIjoiRUNfdjEiLCJkYXRhIjoicHJvYmUiLCJzaWduYXR1cmUiOiJwcm9iZSJ9\"), payment_method: ApplepayPaymentMethod { display_name: \"Visa 1111\", network: \"Visa\", pm_type: \"debit\" }, transaction_identifier: \"probe_txn_id\" }))." + }, + "ApplePayDecrypted": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(ApplePay(ApplePayWalletData { payment_data: Decrypted(ApplePayDecryptedData { application_primary_account_number: CardNumber(411111**********), application_expiration_month: *** alloc::string::String ***, application_expiration_year: *** alloc::string::String ***, payment_data: ApplePayCryptogramData { online_payment_cryptogram: *** alloc::string::String ***, eci_indicator: Some(\"05\") } }), payment_method: ApplepayPaymentMethod { display_name: \"Visa 1111\", network: \"Visa\", pm_type: \"debit\" }, transaction_identifier: \"probe_txn_id\" }))." + }, + "ApplePayThirdPartySdk": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(ApplePayThirdPartySdk(ApplePayThirdPartySdkData { token: Some(*** alloc::string::String ***) }))." + }, + "Bacs": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankDebit(BacsBankDebit { account_number: *** alloc::string::String ***, sort_code: *** alloc::string::String ***, bank_account_holder_name: Some(*** alloc::string::String ***) })." + }, + "BacsBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankTransfer(BacsBankTransfer)." + }, + "BancontactCard": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankRedirect(BancontactCard { card_number: Some(CardNumber(411111**********)), card_exp_month: Some(*** alloc::string::String ***), card_exp_year: Some(*** alloc::string::String ***), card_holder_name: Some(*** alloc::string::String ***) })." + }, + "BcaBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankTransfer(BcaBankTransfer)." + }, + "Becs": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankDebit(BecsBankDebit { account_number: *** alloc::string::String ***, bsb_number: *** alloc::string::String ***, bank_account_holder_name: Some(*** alloc::string::String ***) })." + }, + "BillDeskRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(BillDeskRedirect(BillDeskRedirection))." + }, + "Bizum": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankRedirect(Bizum)." + }, + "Blik": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankRedirect(Blik { blik_code: Some(\"777124\") })." + }, + "Bluecode": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(BluecodeRedirect)." + }, + "BniVaBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankTransfer(BniVaBankTransfer)." + }, + "Boleto": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Voucher(Boleto(BoletoVoucherData { social_security_number: None, expiration_date: None }))." + }, + "BriVaBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankTransfer(BriVaBankTransfer)." + }, + "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": {} + }, + "auth_type": "NO_THREE_DS", + "return_url": "https://example.com/return", + "state": { + "access_token": { + "token": "probe_access_token", + "expires_in_seconds": 3600, + "token_type": "Bearer" + } + } + }, + "sample": { + "url": "https://api.sb.moneris.io/payments", + "method": "Post", + "headers": { + "api-version": "2026-08-14", + "authorization": "Bearer probe_access_token", + "content-type": "application/json", + "via": "HyperSwitch", + "x-merchant-id": "probe_merchant" + }, + "body": "{\"idempotencyKey\":\"auth_probe_txn_001\",\"amount\":{\"currency\":\"USD\",\"amount\":1000},\"paymentMethod\":{\"paymentMethodSource\":\"CARD\",\"card\":{\"cardNumber\":\"4111111111111111\",\"expiryMonth\":3,\"expiryYear\":2030,\"cardSecurityCode\":\"737\"},\"storePaymentMethod\":\"DO_NOT_STORE\"},\"automaticCapture\":true}" + } + }, + "CashappQr": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(CashappQr(CashappQr))." + }, + "CashfreeRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(CashfreeRedirect(CashfreeRedirection))." + }, + "CimbVaBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankTransfer(CimbVaBankTransfer)." + }, + "ClassicReward": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Reward." + }, + "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 currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(DanaRedirect)." + }, + "DanamonVaBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankTransfer(DanamonVaBankTransfer)." + }, + "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 currently implemented for Moneris. Moneris authorize received a non-card payment method: Reward." + }, + "EaseBuzzRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(EaseBuzzRedirect(EaseBuzzRedirection))." + }, + "Efecty": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Voucher(Efecty)." + }, + "Eft": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankRedirect(Eft { provider: \"ozow\" })." + }, + "Eps": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankRedirect(Eps { bank_name: None, country: None })." + }, + "FamilyMart": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Voucher(FamilyMart(JCSVoucherData))." + }, + "GCash": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(GcashRedirect(GcashRedirection))." + }, + "Giropay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankRedirect(Giropay { bank_account_bic: None, bank_account_iban: None, country: None })." + }, + "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 currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(GoPayRedirect(GoPayRedirection))." + }, + "GooglePay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(GooglePay(GooglePayWalletData { pm_type: \"CARD\", description: \"Visa 1111\", info: GooglePayPaymentMethodInfo { card_network: \"VISA\", card_details: \"1111\", assurance_details: None }, tokenization_data: Encrypted(GpayEncryptedTokenizationData { token_type: \"PAYMENT_GATEWAY\", token: \"{\\\"id\\\":\\\"tok_probe_gpay\\\",\\\"object\\\":\\\"token\\\",\\\"type\\\":\\\"card\\\"}\" }) }))." + }, + "GooglePayDecrypted": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(GooglePay(GooglePayWalletData { pm_type: \"CARD\", description: \"Visa 1111\", info: GooglePayPaymentMethodInfo { card_network: \"VISA\", card_details: \"1111\", assurance_details: None }, tokenization_data: Decrypted(GooglePayDecryptedData { card_exp_month: *** alloc::string::String ***, card_exp_year: *** alloc::string::String ***, application_primary_account_number: CardNumber(411111**********), cryptogram: Some(*** alloc::string::String ***), eci_indicator: Some(\"05\") }) }))." + }, + "GooglePayThirdPartySdk": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(GooglePayThirdPartySdk(GooglePayThirdPartySdkData { token: Some(*** alloc::string::String ***) }))." + }, + "Ideal": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankRedirect(Ideal { bank_name: None })." + }, + "Indomaret": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Voucher(Indomaret(IndomaretVoucherData))." + }, + "IndonesianBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankTransfer(IndonesianBankTransfer { bank_name: None })." + }, + "InstantBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankTransfer(InstantBankTransfer)." + }, + "InstantBankTransferFinland": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankTransfer(InstantBankTransferFinland)." + }, + "InstantBankTransferPoland": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankTransfer(InstantBankTransferPoland)." + }, + "Interac": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankRedirect(Interac { country: None, email: None })." + }, + "KakaoPay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(KakaoPayRedirect(KakaoPayRedirection))." + }, + "Klarna": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: PayLater(KlarnaRedirect)." + }, + "Lawson": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Voucher(Lawson(JCSVoucherData))." + }, + "LazyPayRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(LazyPayRedirect(LazyPayRedirection))." + }, + "LocalBankRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankRedirect(LocalBankRedirect)." + }, + "LocalBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankTransfer(LocalBankTransfer { bank_code: None })." + }, + "MandiriVaBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankTransfer(MandiriVaBankTransfer)." + }, + "MbWay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(MbWay(MbWayData))." + }, + "Mifinity": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(Mifinity(MifinityData { date_of_birth: *** time::date::Date ***, language_preference: Some(\"en\") }))." + }, + "MiniStop": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Voucher(MiniStop(JCSVoucherData))." + }, + "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 currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(MomoRedirect(MomoRedirection))." + }, + "MultibancoBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankTransfer(MultibancoBankTransfer)." + }, + "Netbanking": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankRedirect(Netbanking { issuer: HdfcBank })." + }, + "OnlineBankingCzechRepublic": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankRedirect(OnlineBankingCzechRepublic { issuer: CeskaSporitelna })." + }, + "OnlineBankingFinland": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankRedirect(OnlineBankingFinland { email: Some(Email(****@example.com)) })." + }, + "OnlineBankingFpx": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankRedirect(OnlineBankingFpx { issuer: Maybank })." + }, + "OnlineBankingPoland": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankRedirect(OnlineBankingPoland { issuer: BankPekaoSa })." + }, + "OnlineBankingSlovakia": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankRedirect(OnlineBankingSlovakia { issuer: TatraPay })." + }, + "OnlineBankingThailand": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankRedirect(OnlineBankingThailand { issuer: BangkokBank })." + }, + "OpenBanking": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankRedirect(OpenBanking { account_number: None, sort_code: None, iban: None, account_holder_name: None, additional_details: None })." + }, + "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 currently implemented for Moneris. Moneris authorize received a non-card payment method: BankRedirect(OpenBankingUk { issuer: None, country: None })." + }, + "Oxxo": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Voucher(Oxxo)." + }, + "PagoEfectivo": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Voucher(PagoEfectivo)." + }, + "PayEasy": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Voucher(PayEasy(JCSVoucherData))." + }, + "PaySafeCard": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: GiftCard(PaySafeCard)." + }, + "PayURedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(PayURedirect(PayURedirection))." + }, + "PaypalRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(PaypalRedirect(PaypalRedirection { email: Some(Email(****@example.com)) }))." + }, + "PaypalSdk": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(PaypalSdk(PayPalWalletData { token: \"probe_paypal_sdk_token\" }))." + }, + "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 currently implemented for Moneris. Moneris authorize received a non-card payment method: BankTransfer(PermataBankTransfer)." + }, + "PhonePeRedirect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(PhonePeRedirect(PhonePeRedirection))." + }, + "Pix": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankTransfer(Pix { pix_key: None, cpf: None, cnpj: None, source_bank_account_id: None, destination_bank_account_id: None, expiry_date: None })." + }, + "Przelewy24": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankRedirect(Przelewy24 { bank_name: None })." + }, + "Pse": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankTransfer(Pse)." + }, + "RedCompra": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Voucher(RedCompra)." + }, + "RedPagos": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Voucher(RedPagos)." + }, + "RevolutPay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(RevolutPay(RevolutPayData))." + }, + "SamsungPay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(SamsungPay(SamsungPayWalletData { payment_credential: SamsungPayWalletCredentials { method: Some(\"3DS\"), recurring_payment: Some(false), card_brand: Visa, dpan_last_four_digits: None, card_last_four_digits: \"1234\", token_data: SamsungPayTokenData { three_ds_type: Some(\"S\"), version: \"100\", data: *** alloc::string::String *** } } }))." + }, + "Satispay": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(Satispay(SatispayData))." + }, + "Seicomart": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Voucher(Seicomart(JCSVoucherData))." + }, + "Sepa": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankDebit(SepaBankDebit { iban: *** alloc::string::String ***, bank_account_holder_name: Some(*** alloc::string::String ***) })." + }, + "SepaBankTransfer": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankTransfer(SepaBankTransfer)." + }, + "SepaGuaranteedDebit": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankDebit(SepaGuaranteedBankDebit { iban: *** alloc::string::String ***, bank_account_holder_name: Some(*** alloc::string::String ***) })." + }, + "SevenEleven": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Voucher(SevenEleven(JCSVoucherData))." + }, + "Skrill": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(Skrill(SkrillData))." + }, + "Sofort": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankRedirect(Sofort { country: None, preferred_language: None })." + }, + "Swish": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(SwishQr(SwishQrData))." + }, + "TouchNGo": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(TouchNGoRedirect(TouchNGoRedirection))." + }, + "Trustly": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: BankRedirect(Trustly { country: None })." + }, + "Twint": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(TwintRedirect)." + }, + "UpiCollect": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Upi(UpiCollect(UpiCollectData { vpa_id: Some(****@upi), upi_source: None }))." + }, + "UpiIntent": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Upi(UpiIntent(UpiIntentData { upi_source: None, app_name: None }))." + }, + "UpiQr": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Upi(UpiQr(UpiQrData { upi_source: None }))." + }, + "Vipps": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(VippsRedirect)." + }, + "WeChatPayQr": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(WeChatPayQr(WeChatPayQr))." + }, + "Wero": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: Wallet(Wero(WeroData))." + } + }, + "capture": { + "default": { + "status": "supported", + "proto_request": { + "merchant_capture_id": "probe_capture_001", + "connector_transaction_id": "probe_connector_txn_001", + "amount_to_capture": { + "minor_amount": 1000, + "currency": "USD" + }, + "state": { + "access_token": { + "token": "probe_access_token", + "expires_in_seconds": 3600, + "token_type": "Bearer" + } + } + }, + "sample": { + "url": "https://api.sb.moneris.io/payments/probe_connector_txn_001/complete", + "method": "Post", + "headers": { + "api-version": "2026-08-14", + "authorization": "Bearer probe_access_token", + "content-type": "application/json", + "via": "HyperSwitch", + "x-merchant-id": "probe_merchant" + }, + "body": "{\"amount\":{\"currency\":\"USD\",\"amount\":1000},\"idempotencyKey\":\"capture_probe_capture_001\"}" + } + } + }, + "create_client_authentication_token": { + "default": { + "status": "not_supported", + "error": "client_authentication_token flow not supported by moneris connector" + } + }, + "create_order": { + "default": { + "status": "not_supported", + "error": "create_order flow not supported by moneris connector" + } + }, + "create_server_authentication_token": { + "default": { + "status": "supported", + "proto_request": {}, + "sample": { + "url": "https://api.sb.moneris.io/oauth2/token", + "method": "Post", + "headers": { + "content-type": "application/x-www-form-urlencoded", + "via": "HyperSwitch" + }, + "body": "client_id=probe_id&client_secret=probe_secret&grant_type=client_credentials" + } + } + }, + "create_server_session_authentication_token": { + "default": { + "status": "not_supported", + "error": "server_session_authentication_token flow not supported by moneris connector" + } + }, + "customer_create": { + "default": { + "status": "not_supported", + "error": "create_connector_customer flow not supported by moneris connector" + } + }, + "customer_get": { + "default": { + "status": "not_supported", + "error": "get_connector_customer flow not supported by moneris connector" + } + }, + "dispute_accept": { + "default": { + "status": "not_supported", + "error": "accept_dispute flow not supported by moneris connector" + } + }, + "dispute_defend": { + "default": { + "status": "not_supported", + "error": "defend_dispute flow not supported by moneris connector" + } + }, + "dispute_get": { + "default": { + "status": "not_implemented" + } + }, + "dispute_submit_evidence": { + "default": { + "status": "not_supported", + "error": "submit_evidence flow not supported by moneris connector" + } + }, + "eligibility": { + "default": { + "status": "not_supported", + "error": "eligibility flow not supported by moneris connector" + } + }, + "get": { + "default": { + "status": "supported", + "proto_request": { + "merchant_transaction_id": "probe_merchant_txn_001", + "connector_transaction_id": "probe_connector_txn_001", + "amount": { + "minor_amount": 1000, + "currency": "USD" + }, + "state": { + "access_token": { + "token": "probe_access_token", + "expires_in_seconds": 3600, + "token_type": "Bearer" + } + } + }, + "sample": { + "url": "https://api.sb.moneris.io/payments/probe_connector_txn_001", + "method": "Get", + "headers": { + "api-version": "2026-08-14", + "authorization": "Bearer probe_access_token", + "content-type": "application/json", + "via": "HyperSwitch", + "x-merchant-id": "probe_merchant" + }, + "body": null + } + } + }, + "handle_event": { + "default": { + "status": "not_implemented" + } + }, + "incremental_authorization": { + "default": { + "status": "not_supported", + "error": "incremental_authorization flow not supported by moneris connector" + } + }, + "parse_event": { + "default": { + "status": "not_implemented" + } + }, + "payment_method_eligibility": { + "default": { + "status": "not_implemented" + } + }, + "post_authenticate": { + "default": { + "status": "not_supported", + "error": "post_authenticate flow not supported by moneris connector" + } + }, + "pre_authenticate": { + "default": { + "status": "not_supported", + "error": "pre_authenticate flow not supported by moneris connector" + } + }, + "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": {} + }, + "capture_method": "AUTOMATIC", + "auth_type": "NO_THREE_DS", + "return_url": "https://example.com/return", + "state": { + "access_token": { + "token": "probe_access_token", + "expires_in_seconds": 3600, + "token_type": "Bearer" + } + } + }, + "sample": { + "url": "https://api.sb.moneris.io/payments", + "method": "Post", + "headers": { + "api-version": "2026-08-14", + "authorization": "Bearer probe_access_token", + "content-type": "application/json", + "via": "HyperSwitch", + "x-merchant-id": "probe_merchant" + }, + "body": "{\"idempotencyKey\":\"auth_probe_proxy_txn_001\",\"amount\":{\"currency\":\"USD\",\"amount\":1000},\"paymentMethod\":{\"paymentMethodSource\":\"CARD\",\"card\":{\"cardNumber\":\"{{$card_number}}\",\"expiryMonth\":3,\"expiryYear\":2030,\"cardSecurityCode\":\"{{$card_cvc}}\"},\"storePaymentMethod\":\"DO_NOT_STORE\"},\"automaticCapture\":true}" + } + } + }, + "proxy_setup_recurring": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: setup_mandate flow for moneris" + } + }, + "recurring_charge": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: Only mandate (stored-credential) payments are currently implemented for Moneris repeat_payment. Moneris repeat_payment received a non-mandate payment method variant: PaymentMethodToken(PaymentMethodToken { token: *** alloc::string::String *** })." + } + }, + "recurring_revoke": { + "default": { + "status": "not_supported", + "error": "mandate_revoke flow not supported by moneris connector" + } + }, + "refresh": { + "default": { + "status": "not_implemented" + } + }, + "refund": { + "default": { + "status": "supported", + "proto_request": { + "merchant_refund_id": "probe_refund_001", + "connector_transaction_id": "probe_connector_txn_001", + "payment_amount": 1000, + "refund_amount": { + "minor_amount": 1000, + "currency": "USD" + }, + "reason": "customer_request", + "state": { + "access_token": { + "token": "probe_access_token", + "expires_in_seconds": 3600, + "token_type": "Bearer" + } + } + }, + "sample": { + "url": "https://api.sb.moneris.io/refunds", + "method": "Post", + "headers": { + "api-version": "2026-08-14", + "authorization": "Bearer probe_access_token", + "content-type": "application/json", + "via": "HyperSwitch", + "x-merchant-id": "probe_merchant" + }, + "body": "{\"refundAmount\":{\"currency\":\"USD\",\"amount\":1000},\"idempotencyKey\":\"refund_probe_refund_001\",\"reason\":\"customer_request\",\"paymentId\":\"probe_connector_txn_001\"}" + } + } + }, + "refund_get": { + "default": { + "status": "supported", + "proto_request": { + "merchant_refund_id": "probe_refund_001", + "connector_transaction_id": "probe_connector_txn_001", + "refund_id": "probe_refund_id_001", + "state": { + "access_token": { + "token": "probe_access_token", + "expires_in_seconds": 3600, + "token_type": "Bearer" + } + } + }, + "sample": { + "url": "https://api.sb.moneris.io/refunds/probe_refund_id_001", + "method": "Get", + "headers": { + "api-version": "2026-08-14", + "authorization": "Bearer probe_access_token", + "content-type": "application/json", + "via": "HyperSwitch", + "x-merchant-id": "probe_merchant" + }, + "body": null + } + } + }, + "reverse": { + "default": { + "status": "not_supported", + "error": "void_post_capture flow not supported by moneris connector" + } + }, + "setup_recurring": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: setup_mandate flow for moneris" + } + }, + "token_authorize": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: Only card payments are currently implemented for Moneris. Moneris authorize received a non-card payment method: PaymentMethodToken(PaymentMethodToken { token: *** alloc::string::String *** })." + } + }, + "token_setup_recurring": { + "default": { + "status": "not_implemented", + "error": "This feature is not implemented: setup_mandate flow for moneris" + } + }, + "tokenize": { + "default": { + "status": "not_supported", + "error": "payment_method_token flow not supported by moneris connector" + } + }, + "verify_redirect": { + "default": { + "status": "not_implemented" + } + }, + "void": { + "default": { + "status": "supported", + "proto_request": { + "merchant_void_id": "probe_void_001", + "connector_transaction_id": "probe_connector_txn_001", + "state": { + "access_token": { + "token": "probe_access_token", + "expires_in_seconds": 3600, + "token_type": "Bearer" + } + } + }, + "sample": { + "url": "https://api.sb.moneris.io/payments/probe_connector_txn_001/cancel", + "method": "Post", + "headers": { + "api-version": "2026-08-14", + "authorization": "Bearer probe_access_token", + "content-type": "application/json", + "via": "HyperSwitch", + "x-merchant-id": "probe_merchant" + }, + "body": "{\"idempotencyKey\":\"void_probe_void_001\",\"reason\":null}" + } + } + } + } +} \ No newline at end of file diff --git a/data/field_probe/nexixpay.json b/data/field_probe/nexixpay.json index f2ed30ebb0..1e5eb5df15 100644 --- a/data/field_probe/nexixpay.json +++ b/data/field_probe/nexixpay.json @@ -294,7 +294,7 @@ }, "OpenBanking": { "status": "not_implemented", - "error": "This feature is not implemented: Payment method BankRedirect(OpenBanking)" + "error": "This feature is not implemented: Payment method BankRedirect(OpenBanking { account_number: None, sort_code: None, iban: None, account_holder_name: None, additional_details: None })" }, "OpenBankingPis": { "status": "not_supported", diff --git a/data/field_probe/nuvei.json b/data/field_probe/nuvei.json index 394822b0b2..3fe118c359 100644 --- a/data/field_probe/nuvei.json +++ b/data/field_probe/nuvei.json @@ -511,7 +511,7 @@ }, "OpenBanking": { "status": "not_supported", - "error": "Bank redirect method OpenBanking not supported by Nuvei is not supported by nuvei" + "error": "Bank redirect method OpenBanking { account_number: None, sort_code: None, iban: None, account_holder_name: None, additional_details: None } not supported by Nuvei is not supported by nuvei" }, "OpenBankingPis": { "status": "not_supported", diff --git a/data/field_probe/qwikcilver.json b/data/field_probe/qwikcilver.json index 6d80591d10..77cc06f18a 100644 --- a/data/field_probe/qwikcilver.json +++ b/data/field_probe/qwikcilver.json @@ -526,8 +526,8 @@ }, "eligibility": { "default": { - "status": "not_supported", - "error": "eligibility flow not supported by qwikcilver connector" + "status": "error", + "error": "Stuck on field: connector_payment_method_id | customer.phone_number. Qwikcilver's wallet lookup accepts either the wallet number — Missing required field: connector_payment_method_id | customer.phone_number. Qwikcilver's wallet lookup accepts either the wallet number (preferred) or the customer's phone number as a fallback. Neither was supplied, so there's no way to identify which wallet to fetch." } }, "get": { diff --git a/data/field_probe/shift4.json b/data/field_probe/shift4.json index e5fae9a256..6c71c89458 100644 --- a/data/field_probe/shift4.json +++ b/data/field_probe/shift4.json @@ -377,7 +377,7 @@ }, "OpenBanking": { "status": "not_supported", - "error": "BankRedirect type OpenBanking is not supported by Shift4 is not supported by Shift4" + "error": "BankRedirect type OpenBanking { account_number: None, sort_code: None, iban: None, account_holder_name: None, additional_details: None } is not supported by Shift4 is not supported by Shift4" }, "OpenBankingPis": { "status": "not_supported", diff --git a/data/integration-source-links.json b/data/integration-source-links.json index 5fea39f78b..89d8929e95 100644 --- a/data/integration-source-links.json +++ b/data/integration-source-links.json @@ -116,5 +116,16 @@ "https://docs.tamara.co/docs/postman-collection", "https://docs.tamara.co/docs/testing-checklist", "https://docs.tamara.co/docs/online-go-live-testing-checklist" + ], + "Ilixium": [ + "https://docs.ilixium.com/docs/api/authorisation", + "https://docs.ilixium.com/docs/api/threedcomplete", + "https://docs.ilixium.com/docs/direct/3dsecure", + "https://docs.ilixium.com/docs/direct/digest", + "https://docs.ilixium.com/docs/direct/introduction", + "https://docs.ilixium.com/docs/direct/optional-address", + "https://docs.ilixium.com/docs/api/api", + "https://docs.ilixium.com/docs/response-codes/", + "https://docs.ilixium.com/docs/sandbox/" ] -} \ No newline at end of file +} diff --git a/docs-generated/all_connector.md b/docs-generated/all_connector.md index 902efc966d..fd2d24ad15 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 | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | @@ -69,6 +70,7 @@ Authorize a payment amount on a payment method. This reserves funds without capt | [Hyperpg](connectors/hyperpg.md) | ✓ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | x | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | | [Hyperswitch](connectors/hyperswitch.md) | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | | [Iatapay](connectors/iatapay.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 | +| [Ilixium](connectors/ilixium.md) | ✓ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | | [Imerchantsolutions](connectors/imerchantsolutions.md) | ✓ | ⚠ | ? | ✓ | ⚠ | ? | ✓ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | | [Itaubank](connectors/itaubank.md) | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | | [Jpmorgan](connectors/jpmorgan.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 | @@ -78,6 +80,7 @@ Authorize a payment amount on a payment method. This reserves funds without capt | [Maya](connectors/maya.md) | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | | [MiFinity](connectors/mifinity.md) | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ✓ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | | [Mollie](connectors/mollie.md) | ✓ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ✓ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | +| [Moneris](connectors/moneris.md) | ✓ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | | [Multisafepay](connectors/multisafepay.md) | ✓ | ⚠ | ⚠ | ⚠ | ⚠ | ✓ | ✓ | ⚠ | ⚠ | ⚠ | ⚠ | ✓ | ⚠ | ✓ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ✓ | ✓ | ✓ | ✓ | ✓ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ✓ | ⚠ | ⚠ | ⚠ | x | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | | [Netcetera](connectors/netcetera.md) | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | | [Nexinets](connectors/nexinets.md) | ✓ | ⚠ | ✓ | ? | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ✓ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ✓ | ✓ | ⚠ | ✓ | ✓ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | @@ -158,6 +161,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 | @@ -183,6 +187,7 @@ Consolidated view of Get, Void, Refund, Capture, Reverse, CreateOrder, and other | [Hyperpg](connectors/hyperpg.md) | ✓ | ⚠ | x | ⚠ | ⚠ | ✓ | x | ⚠ | ⚠ | ⚠ | ⚠ | ✓ | ⚠ | ⚠ | ⚠ | ✓ | x | ⚠ | ⚠ | ⚠ | x | x | x | x | x | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | | [Hyperswitch](connectors/hyperswitch.md) | ✓ | ⚠ | x | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ? | ⚠ | ⚠ | x | ⚠ | ⚠ | ⚠ | x | x | x | x | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | x | x | ✓ | ✓ | x | | [Iatapay](connectors/iatapay.md) | ✓ | x | x | x | ⚠ | ✓ | x | ⚠ | ⚠ | x | ⚠ | x | ⚠ | ⚠ | x | ✓ | x | x | x | ⚠ | x | x | x | x | x | ✓ | ⚠ | ⚠ | x | x | x | x | ⚠ | x | x | ⚠ | ⚠ | x | +| [Ilixium](connectors/ilixium.md) | ✓ | ✓ | x | ✓ | x | ✓ | x | ⚠ | ⚠ | ⚠ | ⚠ | ✓ | ⚠ | ⚠ | x | ? | x | x | x | ⚠ | x | x | x | x | x | x | x | x | ? | x | x | x | ⚠ | x | x | ⚠ | ⚠ | x | | [Imerchantsolutions](connectors/imerchantsolutions.md) | ✓ | ✓ | ⚠ | ✓ | ⚠ | ✓ | x | ⚠ | ⚠ | ⚠ | ⚠ | ✓ | ⚠ | ? | ⚠ | ✓ | x | x | x | ⚠ | x | x | x | x | x | ⚠ | x | x | x | x | x | x | ⚠ | x | x | ✓ | ✓ | x | | [Itaubank](connectors/itaubank.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 | | [Jpmorgan](connectors/jpmorgan.md) | ✓ | ✓ | ? | ✓ | ⚠ | ✓ | ⚠ | ⚠ | ✓ | ✓ | ⚠ | ✓ | ✓ | ✓ | ⚠ | ✓ | x | ⚠ | ⚠ | ⚠ | x | x | x | x | x | ✓ | ⚠ | ✓ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | @@ -192,6 +197,7 @@ Consolidated view of Get, Void, Refund, Capture, Reverse, CreateOrder, and other | [Maya](connectors/maya.md) | ✓ | ✓ | x | ⚠ | ⚠ | ✓ | ⚠ | ✓ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ✓ | x | ⚠ | ⚠ | ⚠ | x | x | x | x | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | x | x | ✓ | ✓ | x | | [MiFinity](connectors/mifinity.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 | | [Mollie](connectors/mollie.md) | ✓ | ✓ | ⚠ | ? | ⚠ | ✓ | x | ⚠ | ⚠ | ✓ | ⚠ | ✓ | ⚠ | ⚠ | ⚠ | ✓ | x | ⚠ | ⚠ | ? | x | x | x | x | x | ⚠ | ⚠ | ✓ | ⚠ | ⚠ | ⚠ | x | ⚠ | x | x | ⚠ | ⚠ | x | +| [Moneris](connectors/moneris.md) | ✓ | ✓ | x | ✓ | x | ✓ | x | ⚠ | ⚠ | ⚠ | ⚠ | ✓ | ⚠ | ⚠ | x | ✓ | x | x | x | x | x | x | x | x | x | ✓ | x | x | x | x | x | x | ⚠ | x | x | ⚠ | ⚠ | x | | [Multisafepay](connectors/multisafepay.md) | ✓ | ⚠ | ⚠ | ⚠ | ⚠ | ✓ | x | ⚠ | ⚠ | ⚠ | ⚠ | ✓ | ⚠ | ⚠ | ⚠ | ✓ | x | x | x | ⚠ | x | x | x | x | x | x | x | ✓ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | | [Netcetera](connectors/netcetera.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 | | [Nexinets](connectors/nexinets.md) | ✓ | ? | x | ? | ⚠ | ✓ | ⚠ | ⚠ | x | ⚠ | x | ✓ | x | ✓ | ⚠ | ? | x | ⚠ | ⚠ | ⚠ | x | x | x | x | x | x | x | ✓ | ⚠ | ⚠ | ⚠ | x | ⚠ | x | x | ⚠ | ⚠ | x | @@ -214,7 +220,7 @@ Consolidated view of Get, Void, Refund, Capture, Reverse, CreateOrder, and other | [PlacetoPay](connectors/placetopay.md) | ✓ | ✓ | ✓ | ✓ | x | ✓ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ✓ | ⚠ | ⚠ | ⚠ | ? | x | ⚠ | ⚠ | ⚠ | x | x | x | x | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | | [Powertranz](connectors/powertranz.md) | ✓ | ✓ | ⚠ | ✓ | x | ✓ | x | ⚠ | ✓ | x | x | ✓ | ✓ | x | ⚠ | ✓ | x | x | x | ⚠ | x | x | x | x | x | x | x | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | | [Ppro](connectors/ppro.md) | ✓ | ✓ | x | ✓ | ⚠ | ✓ | x | ✓ | x | ? | ? | x | x | ✓ | ⚠ | ✓ | x | x | x | ⚠ | x | x | x | x | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ✓ | ✓ | x | -| [Qwikcilver](connectors/qwikcilver.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 | +| [Qwikcilver](connectors/qwikcilver.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 | | [Rapyd](connectors/rapyd.md) | ✓ | ✓ | x | ✓ | ? | ✓ | ⚠ | ⚠ | ? | ✓ | ⚠ | ✓ | ⚠ | ✓ | ⚠ | ✓ | x | ⚠ | ⚠ | ⚠ | x | x | x | x | x | x | x | ✓ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | | [Razorpay](connectors/razorpay.md) | ✓ | x | x | ✓ | ✓ | ✓ | x | ⚠ | ⚠ | ? | ⚠ | ? | ⚠ | ⚠ | x | ✓ | x | ⚠ | ⚠ | ⚠ | x | x | x | x | x | ⚠ | ✓ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ✓ | ✓ | x | | [Razorpay V2](connectors/razorpayv2.md) | ✓ | x | x | ⚠ | ✓ | ✓ | x | ⚠ | ⚠ | ✓ | ⚠ | ✓ | ⚠ | ⚠ | ⚠ | ✓ | x | ⚠ | ⚠ | ⚠ | x | x | x | x | x | x | x | x | x | x | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | diff --git a/docs-generated/api-reference/services/payout-service/README.md b/docs-generated/api-reference/services/payout-service/README.md index 39484a19c1..c16b96e129 100644 --- a/docs-generated/api-reference/services/payout-service/README.md +++ b/docs-generated/api-reference/services/payout-service/README.md @@ -40,6 +40,7 @@ The service supports various payout flows including synchronous transfers, stage | [`CreateLink`](./create-link.md) | Generate a secure URL for the recipient to provide their own payout method details to claim funds. | Sending funds via email without knowing bank details | | [`CreateRecipient`](./create-recipient.md) | Register a new recipient entity (individual or business) with the payment processor. | Onboarding a new seller or contractor | | [`EnrollDisburseAccount`](./enroll-disburse-account.md) | Register and verify a destination account (like a bank account) to receive disbursements. | Linking a vendor's bank account for future payouts | +| [`Eligibility`](./eligibility.md) | Check whether a payout is eligible before initiating it (e.g. SEPA Verification of Payee). | Pre-validating a payee or bank account before transfer | ## Common Patterns diff --git a/docs-generated/api-reference/services/payout-service/create-link.md b/docs-generated/api-reference/services/payout-service/create-link.md index 9a5ac05659..147a0a78a2 100644 --- a/docs-generated/api-reference/services/payout-service/create-link.md +++ b/docs-generated/api-reference/services/payout-service/create-link.md @@ -35,6 +35,8 @@ Use this operation when you do not have the recipient's payout method details up | `webhook_url` | string | No | URL where payout status updates should be sent. | | `browser_info` | BrowserInformation | No | Information about the user's browser. | | `access_token` | SecretString | No | Access token for the connector, if required. | +| `description` | string | No | Description of the payout. | +| `source_bank_data` | SourceBankData | No | Details of the bank account from which the payout is funded. | ## Response Fields diff --git a/docs-generated/api-reference/services/payout-service/create.md b/docs-generated/api-reference/services/payout-service/create.md index cc21636d5e..ab3ad7f3f2 100644 --- a/docs-generated/api-reference/services/payout-service/create.md +++ b/docs-generated/api-reference/services/payout-service/create.md @@ -37,6 +37,7 @@ This operation is the first step in the payout lifecycle. Use this when you have | `browser_info` | BrowserInformation | No | Information about the user's browser, used for fraud prevention. | | `access_token` | SecretString | No | Access token for the connector, if required. | | `source_bank_data` | SourceBankData | No | Details of the bank account from which the payout is funded. | +| `description` | string | No | Description of the payout. | ## Response Fields diff --git a/docs-generated/api-reference/services/payout-service/eligibility.md b/docs-generated/api-reference/services/payout-service/eligibility.md new file mode 100644 index 0000000000..8237789265 --- /dev/null +++ b/docs-generated/api-reference/services/payout-service/eligibility.md @@ -0,0 +1,78 @@ +--- +title: Check Payout Eligibility +description: Check the eligibility of a payout before initiating it. +--- + +# Check Payout Eligibility + +## Overview + +The `Eligibility` RPC in the Payout Service checks whether a payout can be made to a given payout method before funds are committed. It is used for pre-verification flows such as SEPA Verification of Payee (VoP) and other payee-verification checks. + +## Purpose + +Use this operation to validate a payee or payout method before initiating a transfer, reducing the risk of failed or misdirected payouts. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Verify a payee before transfer | Call `Eligibility` with the payout method and amount, then inspect `payout_eligible`. | +| Pre-check a bank account | Call `Eligibility` with the bank details to confirm the account can receive funds. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchant_payout_id` | string | No | Your internal identifier for the eligibility check. | +| `connector_feature_data` | SecretString | No | Connector-specific metadata passed to the eligibility check. | +| `payout_method_data` | PayoutMethod | No | Specific details of the payout instrument being checked. | +| `amount` | Money | Yes | The amount to be paid out. | +| `connector_payout_id` | string | No | An existing payout identifier from the connector. | +| `destination_currency` | Currency | Yes | The currency in which the recipient will receive the payout. | +| `access_token` | SecretString | No | Access token for the connector, if required. | +| `address` | PayoutAddress | No | Address information associated with the payout. | +| `customer` | Customer | No | Details about the customer receiving the payout. | +| `source_bank_data` | SourceBankData | No | Details of the bank account from which the payout is funded. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchant_payout_id` | string | No | Your internal identifier for the eligibility check. | +| `payout_status` | PayoutEnums.PayoutStatus | No | The current status of the payout. | +| `connector_payout_id` | string | No | The unique payout ID assigned by the connector (set only when the payee is eligible). | +| `error` | ErrorInfo | No | Details of any error that occurred during the check. | +| `status_code` | uint32 | Yes | The HTTP status code returned from the connector. | +| `payout_eligible` | bool | No | Whether the payout is eligible. | +| `connector_metadata` | SecretString | No | Connector-specific details as a JSON object, surfaced to the merchant. | +| `connector_eligibility_reference_id` | string | No | Connector's reference for the eligibility check itself. Set for every verdict so the check remains traceable for reconciliation. | + +## Example + +```bash +grpcurl -H "x-connector: stripe" \ + -H "x-connector-config: {\"config\":{\"Stripe\":{\"api_key\":\"$STRIPE_API_KEY\"}}}" \ + -d '{ + "amount": {"minor_amount": 1000, "currency": "USD"}, + "destination_currency": "USD", + "payout_method_data": { + "ach": { + "bank_account_number": "000123456789", + "bank_routing_number": "110000000" + } + } + }' \ + localhost:8080 types.PayoutService/Eligibility +``` + +```json +{ + "payout_eligible": true, + "connector_eligibility_reference_id": "elig_1Hh1XYZ2eZvKYlo2C", + "status_code": 200 +} +``` + +## Next Steps + +- [Transfer Payout](./transfer.md) +- [Create Payout](./create.md) diff --git a/docs-generated/api-reference/services/payout-service/get.md b/docs-generated/api-reference/services/payout-service/get.md index af8e4fdaab..452c27ec13 100644 --- a/docs-generated/api-reference/services/payout-service/get.md +++ b/docs-generated/api-reference/services/payout-service/get.md @@ -24,6 +24,7 @@ Use this operation to poll for the status of a payout or to verify details befor | `merchant_payout_id` | string | No | Your internal identifier for the payout. | | `connector_payout_id` | string | No | The unique payout ID assigned by the connector. | | `access_token` | SecretString | No | Access token for the connector, if required. | +| `source_bank_data` | SourceBankData | No | Source (debtor) bank data. Some connectors (e.g. Deutsche Bank) require the debtor account details to perform a status enquiry. | ## Response Fields diff --git a/docs-generated/api-reference/services/payout-service/transfer.md b/docs-generated/api-reference/services/payout-service/transfer.md index c15c9508e3..f3702cbae7 100644 --- a/docs-generated/api-reference/services/payout-service/transfer.md +++ b/docs-generated/api-reference/services/payout-service/transfer.md @@ -35,6 +35,8 @@ Use this operation to move funds to the destination payout method. | `browser_info` | BrowserInformation | No | Information about the user's browser. | | `access_token` | SecretString | No | Access token for the connector, if required. | | `source_bank_data` | SourceBankData | No | Details of the bank account from which the payout is funded. | +| `description` | string | No | Description of the payout. | +| `connector_eligibility_reference_id` | string | No | Connector's reference for a prior eligibility check, when the processor requires it to authorise the transfer. | ## Response Fields diff --git a/docs-generated/connectors/citigate.md b/docs-generated/connectors/citigate.md new file mode 100644 index 0000000000..9687accf6e --- /dev/null +++ b/docs-generated/connectors/citigate.md @@ -0,0 +1,376 @@ +# Citigate + + + +## SDK Configuration + +Use this config for all flows in this connector. Replace `YOUR_API_KEY` with your actual credentials. + + + + + + + + + +
PythonJavaScriptKotlinRust
+ +
Python + +```python +from payments.generated import sdk_config_pb2, payment_pb2, payment_methods_pb2 + +config = sdk_config_pb2.ConnectorConfig( + options=sdk_config_pb2.SdkOptions(environment=sdk_config_pb2.Environment.SANDBOX), + connector_config=payment_pb2.ConnectorSpecificConfig( + citigate=payment_pb2.CitigateConfig( + api_key=payment_methods_pb2.SecretString(value="YOUR_API_KEY"), + key1=payment_methods_pb2.SecretString(value="YOUR_KEY1"), + base_url="YOUR_BASE_URL", + ), + ), +) + +``` + +
+ +
+ +
JavaScript + +```javascript +const { PaymentClient } = require('hyperswitch-prism'); +const { ConnectorConfig, Environment, Connector } = require('hyperswitch-prism').types; + +const config = ConnectorConfig.create({ + connector: Connector.CITIGATE, + environment: Environment.SANDBOX, + auth: { + citigate: { + apiKey: { value: 'YOUR_API_KEY' }, + key1: { value: 'YOUR_KEY1' }, + baseUrl: 'YOUR_BASE_URL', + } + }, +}); +``` + +
+ +
+ +
Kotlin + +```kotlin +val config = ConnectorConfig.newBuilder() + .setOptions(SdkOptions.newBuilder().setEnvironment(Environment.SANDBOX).build()) + .setConnectorConfig( + ConnectorSpecificConfig.newBuilder() + .setCitigate(CitigateConfig.newBuilder() + .setApiKey(SecretString.newBuilder().setValue("YOUR_API_KEY").build()) + .setKey1(SecretString.newBuilder().setValue("YOUR_KEY1").build()) + .setBaseUrl("YOUR_BASE_URL") + .build()) + .build() + ) + .build() +``` + +
+ +
+ +
Rust + +```rust +use grpc_api_types::payments::*; +use grpc_api_types::payments::connector_specific_config; + +let config = ConnectorConfig { + connector_config: Some(ConnectorSpecificConfig { + config: Some(connector_specific_config::Config::Citigate(CitigateConfig { + api_key: Some(hyperswitch_masking::Secret::new("YOUR_API_KEY".to_string())), // Authentication credential + key1: Some(hyperswitch_masking::Secret::new("YOUR_KEY1".to_string())), // Authentication credential + base_url: Some("https://sandbox.example.com".to_string()), // Base URL for API calls + ..Default::default() + })), + }), + options: Some(SdkOptions { + environment: Environment::Sandbox.into(), + }), +}; +``` + +
+ +
+ +## Integration Scenarios + +Complete, runnable examples for common integration patterns. Each example shows the full flow with status handling. Copy-paste into your app and replace placeholder values. + +### One-step Payment (Authorize + Capture) + +Simple payment that authorizes and captures in one call. Use for immediate charges. + +**Response status handling:** + +| Status | Recommended action | +|--------|-------------------| +| `AUTHORIZED` | Payment authorized and captured — funds will be settled automatically | +| `PENDING` | Payment processing — await webhook for final status before fulfilling | +| `FAILED` | Payment declined — surface error to customer, do not retry without new details | + +**Examples:** [Python](../../examples/citigate/citigate.py#L149) · [JavaScript](../../examples/citigate/citigate.js) · [Kotlin](../../examples/citigate/citigate.kt#L126) · [Rust](../../examples/citigate/citigate.rs#L185) + +### Card Payment (Authorize + Capture) + +Two-step card payment. First authorize, then capture. Use when you need to verify funds before finalizing. + +**Response status handling:** + +| Status | Recommended action | +|--------|-------------------| +| `AUTHORIZED` | Funds reserved — proceed to Capture to settle | +| `PENDING` | Awaiting async confirmation — wait for webhook before capturing | +| `FAILED` | Payment declined — surface error to customer, do not retry without new details | + +**Examples:** [Python](../../examples/citigate/citigate.py#L168) · [JavaScript](../../examples/citigate/citigate.js) · [Kotlin](../../examples/citigate/citigate.kt#L142) · [Rust](../../examples/citigate/citigate.rs#L201) + +### Refund + +Return funds to the customer for a completed payment. + +**Examples:** [Python](../../examples/citigate/citigate.py#L193) · [JavaScript](../../examples/citigate/citigate.js) · [Kotlin](../../examples/citigate/citigate.kt#L164) · [Rust](../../examples/citigate/citigate.rs#L224) + +### Void Payment + +Cancel an authorized but not-yet-captured payment. + +**Examples:** [Python](../../examples/citigate/citigate.py#L218) · [JavaScript](../../examples/citigate/citigate.js) · [Kotlin](../../examples/citigate/citigate.kt#L186) · [Rust](../../examples/citigate/citigate.rs#L247) + +### Get Payment Status + +Retrieve current payment status from the connector. + +**Examples:** [Python](../../examples/citigate/citigate.py#L240) · [JavaScript](../../examples/citigate/citigate.js) · [Kotlin](../../examples/citigate/citigate.kt#L205) · [Rust](../../examples/citigate/citigate.rs#L266) + +## API Reference + +| Flow (Service.RPC) | Category | gRPC Request Message | +|--------------------|----------|----------------------| +| [PaymentService.Authorize](#paymentserviceauthorize) | Payments | `PaymentServiceAuthorizeRequest` | +| [PaymentService.Capture](#paymentservicecapture) | Payments | `PaymentServiceCaptureRequest` | +| [PaymentService.Get](#paymentserviceget) | Payments | `PaymentServiceGetRequest` | +| [PaymentService.ProxyAuthorize](#paymentserviceproxyauthorize) | Payments | `PaymentServiceProxyAuthorizeRequest` | +| [PaymentService.Refund](#paymentservicerefund) | Payments | `PaymentServiceRefundRequest` | +| [RefundService.Get](#refundserviceget) | Refunds | `RefundServiceGetRequest` | +| [PaymentService.Void](#paymentservicevoid) | Payments | `PaymentServiceVoidRequest` | + +### Payments + +#### PaymentService.Authorize + +Authorize a payment amount on a payment method. This reserves funds without capturing them, essential for verifying availability before finalizing. + +| | Message | +|---|---------| +| **Request** | `PaymentServiceAuthorizeRequest` | +| **Response** | `PaymentServiceAuthorizeResponse` | + +**Supported payment method types:** + +| Payment Method | Supported | +|----------------|:---------:| +| Card | ✓ | +| Bancontact | ⚠ | +| Apple Pay | ⚠ | +| Apple Pay Dec | ⚠ | +| Apple Pay SDK | ⚠ | +| Google Pay | ⚠ | +| Google Pay Dec | ⚠ | +| Google Pay SDK | ⚠ | +| PayPal SDK | ⚠ | +| Amazon Pay | ⚠ | +| Cash App | ⚠ | +| PayPal | ⚠ | +| WeChat Pay | ⚠ | +| Alipay | ⚠ | +| Revolut Pay | ⚠ | +| MiFinity | ⚠ | +| Bluecode | ⚠ | +| Paze | x | +| Samsung Pay | ⚠ | +| MB Way | ⚠ | +| Satispay | ⚠ | +| Wero | ⚠ | +| GoPay | ⚠ | +| GCash | ⚠ | +| Momo | ⚠ | +| Dana | ⚠ | +| Kakao Pay | ⚠ | +| Touch 'n Go | ⚠ | +| Twint | ⚠ | +| Vipps | ⚠ | +| Swish | ⚠ | +| Affirm | ⚠ | +| Afterpay | ⚠ | +| Klarna | ⚠ | +| UPI Collect | ⚠ | +| UPI Intent | ⚠ | +| UPI QR | ⚠ | +| Thailand | ⚠ | +| Czech | ⚠ | +| Finland | ⚠ | +| FPX | ⚠ | +| Poland | ⚠ | +| Slovakia | ⚠ | +| UK | ⚠ | +| PIS | x | +| Generic | ⚠ | +| Local | ⚠ | +| iDEAL | ⚠ | +| Sofort | ⚠ | +| Trustly | ⚠ | +| Giropay | ⚠ | +| EPS | ⚠ | +| Przelewy24 | ⚠ | +| PSE | ⚠ | +| BLIK | ⚠ | +| Interac | ⚠ | +| Bizum | ⚠ | +| EFT | ⚠ | +| DuitNow | x | +| ACH | ⚠ | +| SEPA | ⚠ | +| BACS | ⚠ | +| Multibanco | ⚠ | +| Instant | ⚠ | +| Instant FI | ⚠ | +| Instant PL | ⚠ | +| Pix | ⚠ | +| Permata | ⚠ | +| BCA | ⚠ | +| BNI VA | ⚠ | +| BRI VA | ⚠ | +| CIMB VA | ⚠ | +| Danamon VA | ⚠ | +| Mandiri VA | ⚠ | +| Local | ⚠ | +| Indonesian | ⚠ | +| ACH | ⚠ | +| SEPA | ⚠ | +| BACS | ⚠ | +| BECS | ⚠ | +| SEPA Guaranteed | ⚠ | +| Crypto | x | +| Reward | ⚠ | +| Givex | x | +| PaySafeCard | ⚠ | +| E-Voucher | ⚠ | +| Boleto | ⚠ | +| Efecty | ⚠ | +| Pago Efectivo | ⚠ | +| Red Compra | ⚠ | +| Red Pagos | ⚠ | +| Alfamart | ⚠ | +| Indomaret | ⚠ | +| Oxxo | ⚠ | +| 7-Eleven | ⚠ | +| Lawson | ⚠ | +| Mini Stop | ⚠ | +| Family Mart | ⚠ | +| Seicomart | ⚠ | +| Pay Easy | ⚠ | + +**Payment method objects** — use these in the `payment_method` field of the Authorize request. + +##### Card (Raw PAN) + +```python +"payment_method": { + "card": { + "card_number": "4111111111111111", + "card_exp_month": "03", + "card_exp_year": "2030", + "card_cvc": "737", + "card_holder_name": "John Doe" + } +} +``` + +**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts#L271) · [Kotlin](../../examples/citigate/citigate.kt#L223) · [Rust](../../examples/citigate/citigate.rs) + +#### PaymentService.Capture + +Finalize an authorized payment by transferring funds. Captures the authorized amount to complete the transaction and move funds to your merchant account. + +| | Message | +|---|---------| +| **Request** | `PaymentServiceCaptureRequest` | +| **Response** | `PaymentServiceCaptureResponse` | + +**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts#L280) · [Kotlin](../../examples/citigate/citigate.kt#L235) · [Rust](../../examples/citigate/citigate.rs) + +#### PaymentService.Get + +Retrieve current payment status from the payment processor. Enables synchronization between your system and payment processors for accurate state tracking. + +| | Message | +|---|---------| +| **Request** | `PaymentServiceGetRequest` | +| **Response** | `PaymentServiceGetResponse` | + +**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts#L289) · [Kotlin](../../examples/citigate/citigate.kt#L245) · [Rust](../../examples/citigate/citigate.rs) + +#### PaymentService.ProxyAuthorize + +Authorize using vault-aliased card data. Proxy substitutes before connector. + +| | Message | +|---|---------| +| **Request** | `PaymentServiceProxyAuthorizeRequest` | +| **Response** | `PaymentServiceAuthorizeResponse` | + +**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts#L298) · [Kotlin](../../examples/citigate/citigate.kt#L253) · [Rust](../../examples/citigate/citigate.rs) + +#### PaymentService.Refund + +Process a partial or full refund for a captured payment. Returns funds to the customer when goods are returned or services are cancelled. + +| | Message | +|---|---------| +| **Request** | `PaymentServiceRefundRequest` | +| **Response** | `RefundResponse` | + +**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts#L307) · [Kotlin](../../examples/citigate/citigate.kt#L295) · [Rust](../../examples/citigate/citigate.rs) + +#### PaymentService.Void + +Cancel an authorized payment that has not been captured. Releases held funds back to the customer's payment method when a transaction cannot be completed. + +| | Message | +|---|---------| +| **Request** | `PaymentServiceVoidRequest` | +| **Response** | `PaymentServiceVoidResponse` | + +**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts) · [Kotlin](../../examples/citigate/citigate.kt#L317) · [Rust](../../examples/citigate/citigate.rs) + +### Refunds + +#### RefundService.Get + +Retrieve refund status from the payment processor. Tracks refund progress through processor settlement for accurate customer communication. + +| | Message | +|---|---------| +| **Request** | `RefundServiceGetRequest` | +| **Response** | `RefundResponse` | + +**Examples:** [Python](../../examples/citigate/citigate.py) · [TypeScript](../../examples/citigate/citigate.ts#L316) · [Kotlin](../../examples/citigate/citigate.kt#L305) · [Rust](../../examples/citigate/citigate.rs) diff --git a/docs-generated/connectors/ilixium.md b/docs-generated/connectors/ilixium.md new file mode 100644 index 0000000000..2b204cccfd --- /dev/null +++ b/docs-generated/connectors/ilixium.md @@ -0,0 +1,366 @@ +# Ilixium + + + +## 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( + ilixium=payment_pb2.IlixiumConfig( + api_key=payment_methods_pb2.SecretString(value="YOUR_API_KEY"), + key1=payment_methods_pb2.SecretString(value="YOUR_KEY1"), + api_secret=payment_methods_pb2.SecretString(value="YOUR_API_SECRET"), + 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.ILIXIUM, + environment: Environment.SANDBOX, + auth: { + ilixium: { + apiKey: { value: 'YOUR_API_KEY' }, + key1: { value: 'YOUR_KEY1' }, + apiSecret: { value: 'YOUR_API_SECRET' }, + baseUrl: 'YOUR_BASE_URL', + } + }, +}); +``` + +
+ +
+ +
Kotlin + +```kotlin +val config = ConnectorConfig.newBuilder() + .setOptions(SdkOptions.newBuilder().setEnvironment(Environment.SANDBOX).build()) + .setConnectorConfig( + ConnectorSpecificConfig.newBuilder() + .setIlixium(IlixiumConfig.newBuilder() + .setApiKey(SecretString.newBuilder().setValue("YOUR_API_KEY").build()) + .setKey1(SecretString.newBuilder().setValue("YOUR_KEY1").build()) + .setApiSecret(SecretString.newBuilder().setValue("YOUR_API_SECRET").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::Ilixium(IlixiumConfig { + 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 + api_secret: Some(hyperswitch_masking::Secret::new("YOUR_API_SECRET".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/ilixium/ilixium.py#L133) · [JavaScript](../../examples/ilixium/ilixium.js) · [Kotlin](../../examples/ilixium/ilixium.kt#L124) · [Rust](../../examples/ilixium/ilixium.rs#L168) + +### 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/ilixium/ilixium.py#L152) · [JavaScript](../../examples/ilixium/ilixium.js) · [Kotlin](../../examples/ilixium/ilixium.kt#L140) · [Rust](../../examples/ilixium/ilixium.rs#L184) + +### Refund + +Return funds to the customer for a completed payment. + +**Examples:** [Python](../../examples/ilixium/ilixium.py#L177) · [JavaScript](../../examples/ilixium/ilixium.js) · [Kotlin](../../examples/ilixium/ilixium.kt#L162) · [Rust](../../examples/ilixium/ilixium.rs#L207) + +### Void Payment + +Cancel an authorized but not-yet-captured payment. + +**Examples:** [Python](../../examples/ilixium/ilixium.py#L202) · [JavaScript](../../examples/ilixium/ilixium.js) · [Kotlin](../../examples/ilixium/ilixium.kt#L184) · [Rust](../../examples/ilixium/ilixium.rs#L230) + +### Get Payment Status + +Retrieve current payment status from the connector. + +**Examples:** [Python](../../examples/ilixium/ilixium.py#L224) · [JavaScript](../../examples/ilixium/ilixium.js) · [Kotlin](../../examples/ilixium/ilixium.kt#L203) · [Rust](../../examples/ilixium/ilixium.rs#L249) + +## 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` | +| [PaymentService.Void](#paymentservicevoid) | Payments | `PaymentServiceVoidRequest` | + +### Payments + +#### PaymentService.Authorize + +Authorize a payment amount on a payment method. This reserves funds without capturing them, essential for verifying availability before finalizing. + +| | Message | +|---|---------| +| **Request** | `PaymentServiceAuthorizeRequest` | +| **Response** | `PaymentServiceAuthorizeResponse` | + +**Supported payment method types:** + +| Payment Method | Supported | +|----------------|:---------:| +| Card | ✓ | +| Bancontact | ⚠ | +| Apple Pay | ⚠ | +| Apple Pay Dec | ⚠ | +| Apple Pay SDK | ⚠ | +| Google Pay | ⚠ | +| Google Pay Dec | ⚠ | +| Google Pay SDK | ⚠ | +| PayPal SDK | ⚠ | +| Amazon Pay | ⚠ | +| Cash App | ⚠ | +| PayPal | ⚠ | +| WeChat Pay | ⚠ | +| Alipay | ⚠ | +| Revolut Pay | ⚠ | +| MiFinity | ⚠ | +| Bluecode | ⚠ | +| Paze | x | +| Samsung Pay | ⚠ | +| MB Way | ⚠ | +| Satispay | ⚠ | +| Wero | ⚠ | +| GoPay | ⚠ | +| GCash | ⚠ | +| Momo | ⚠ | +| Dana | ⚠ | +| Kakao Pay | ⚠ | +| Touch 'n Go | ⚠ | +| Twint | ⚠ | +| Vipps | ⚠ | +| Swish | ⚠ | +| Affirm | ⚠ | +| Afterpay | ⚠ | +| Klarna | ⚠ | +| UPI Collect | ⚠ | +| UPI Intent | ⚠ | +| UPI QR | ⚠ | +| Thailand | ⚠ | +| Czech | ⚠ | +| Finland | ⚠ | +| FPX | ⚠ | +| Poland | ⚠ | +| Slovakia | ⚠ | +| UK | ⚠ | +| PIS | x | +| Generic | ⚠ | +| Local | ⚠ | +| iDEAL | ⚠ | +| Sofort | ⚠ | +| Trustly | ⚠ | +| Giropay | ⚠ | +| EPS | ⚠ | +| Przelewy24 | ⚠ | +| PSE | ⚠ | +| BLIK | ⚠ | +| Interac | ⚠ | +| Bizum | ⚠ | +| EFT | ⚠ | +| DuitNow | x | +| ACH | ⚠ | +| SEPA | ⚠ | +| BACS | ⚠ | +| Multibanco | ⚠ | +| Instant | ⚠ | +| Instant FI | ⚠ | +| Instant PL | ⚠ | +| Pix | ⚠ | +| Permata | ⚠ | +| BCA | ⚠ | +| BNI VA | ⚠ | +| BRI VA | ⚠ | +| CIMB VA | ⚠ | +| Danamon VA | ⚠ | +| Mandiri VA | ⚠ | +| Local | ⚠ | +| Indonesian | ⚠ | +| ACH | ⚠ | +| SEPA | ⚠ | +| BACS | ⚠ | +| BECS | ⚠ | +| SEPA Guaranteed | ⚠ | +| Crypto | x | +| Reward | ⚠ | +| Givex | x | +| PaySafeCard | ⚠ | +| E-Voucher | ⚠ | +| Boleto | ⚠ | +| Efecty | ⚠ | +| Pago Efectivo | ⚠ | +| Red Compra | ⚠ | +| Red Pagos | ⚠ | +| Alfamart | ⚠ | +| Indomaret | ⚠ | +| Oxxo | ⚠ | +| 7-Eleven | ⚠ | +| Lawson | ⚠ | +| Mini Stop | ⚠ | +| Family Mart | ⚠ | +| Seicomart | ⚠ | +| Pay Easy | ⚠ | + +**Payment method objects** — use these in the `payment_method` field of the Authorize request. + +##### Card (Raw PAN) + +```python +"payment_method": { + "card": { + "card_number": "4111111111111111", + "card_exp_month": "03", + "card_exp_year": "2030", + "card_cvc": "737", + "card_holder_name": "John Doe" + } +} +``` + +**Examples:** [Python](../../examples/ilixium/ilixium.py) · [TypeScript](../../examples/ilixium/ilixium.ts#L255) · [Kotlin](../../examples/ilixium/ilixium.kt#L221) · [Rust](../../examples/ilixium/ilixium.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/ilixium/ilixium.py) · [TypeScript](../../examples/ilixium/ilixium.ts#L264) · [Kotlin](../../examples/ilixium/ilixium.kt#L233) · [Rust](../../examples/ilixium/ilixium.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/ilixium/ilixium.py) · [TypeScript](../../examples/ilixium/ilixium.ts#L273) · [Kotlin](../../examples/ilixium/ilixium.kt#L243) · [Rust](../../examples/ilixium/ilixium.rs) + +#### PaymentService.ProxyAuthorize + +Authorize using vault-aliased card data. Proxy substitutes before connector. + +| | Message | +|---|---------| +| **Request** | `PaymentServiceProxyAuthorizeRequest` | +| **Response** | `PaymentServiceAuthorizeResponse` | + +**Examples:** [Python](../../examples/ilixium/ilixium.py) · [TypeScript](../../examples/ilixium/ilixium.ts#L282) · [Kotlin](../../examples/ilixium/ilixium.kt#L251) · [Rust](../../examples/ilixium/ilixium.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/ilixium/ilixium.py) · [TypeScript](../../examples/ilixium/ilixium.ts#L291) · [Kotlin](../../examples/ilixium/ilixium.kt#L286) · [Rust](../../examples/ilixium/ilixium.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/ilixium/ilixium.py) · [TypeScript](../../examples/ilixium/ilixium.ts) · [Kotlin](../../examples/ilixium/ilixium.kt#L296) · [Rust](../../examples/ilixium/ilixium.rs) diff --git a/docs-generated/connectors/moneris.md b/docs-generated/connectors/moneris.md new file mode 100644 index 0000000000..4369245886 --- /dev/null +++ b/docs-generated/connectors/moneris.md @@ -0,0 +1,394 @@ +# Moneris + + + +## 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( + moneris=payment_pb2.MonerisConfig( + client_secret=payment_methods_pb2.SecretString(value="YOUR_CLIENT_SECRET"), + merchant_id=payment_methods_pb2.SecretString(value="YOUR_MERCHANT_ID"), + client_id=payment_methods_pb2.SecretString(value="YOUR_CLIENT_ID"), + 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.MONERIS, + environment: Environment.SANDBOX, + auth: { + moneris: { + clientSecret: { value: 'YOUR_CLIENT_SECRET' }, + merchantId: { value: 'YOUR_MERCHANT_ID' }, + clientId: { value: 'YOUR_CLIENT_ID' }, + baseUrl: 'YOUR_BASE_URL', + } + }, +}); +``` + +
+ +
+ +
Kotlin + +```kotlin +val config = ConnectorConfig.newBuilder() + .setOptions(SdkOptions.newBuilder().setEnvironment(Environment.SANDBOX).build()) + .setConnectorConfig( + ConnectorSpecificConfig.newBuilder() + .setMoneris(MonerisConfig.newBuilder() + .setClientSecret(SecretString.newBuilder().setValue("YOUR_CLIENT_SECRET").build()) + .setMerchantId(SecretString.newBuilder().setValue("YOUR_MERCHANT_ID").build()) + .setClientId(SecretString.newBuilder().setValue("YOUR_CLIENT_ID").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::Moneris(MonerisConfig { + client_secret: Some(hyperswitch_masking::Secret::new("YOUR_CLIENT_SECRET".to_string())), // Authentication credential + merchant_id: Some(hyperswitch_masking::Secret::new("YOUR_MERCHANT_ID".to_string())), // Authentication credential + client_id: Some(hyperswitch_masking::Secret::new("YOUR_CLIENT_ID".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/moneris/moneris.py#L176) · [JavaScript](../../examples/moneris/moneris.js) · [Kotlin](../../examples/moneris/moneris.kt#L149) · [Rust](../../examples/moneris/moneris.rs#L221) + +### 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/moneris/moneris.py#L195) · [JavaScript](../../examples/moneris/moneris.js) · [Kotlin](../../examples/moneris/moneris.kt#L165) · [Rust](../../examples/moneris/moneris.rs#L237) + +### Refund + +Return funds to the customer for a completed payment. + +**Examples:** [Python](../../examples/moneris/moneris.py#L220) · [JavaScript](../../examples/moneris/moneris.js) · [Kotlin](../../examples/moneris/moneris.kt#L187) · [Rust](../../examples/moneris/moneris.rs#L260) + +### Void Payment + +Cancel an authorized but not-yet-captured payment. + +**Examples:** [Python](../../examples/moneris/moneris.py#L245) · [JavaScript](../../examples/moneris/moneris.js) · [Kotlin](../../examples/moneris/moneris.kt#L209) · [Rust](../../examples/moneris/moneris.rs#L283) + +### Get Payment Status + +Retrieve current payment status from the connector. + +**Examples:** [Python](../../examples/moneris/moneris.py#L267) · [JavaScript](../../examples/moneris/moneris.js) · [Kotlin](../../examples/moneris/moneris.kt#L228) · [Rust](../../examples/moneris/moneris.rs#L302) + +## API Reference + +| Flow (Service.RPC) | Category | gRPC Request Message | +|--------------------|----------|----------------------| +| [PaymentService.Authorize](#paymentserviceauthorize) | Payments | `PaymentServiceAuthorizeRequest` | +| [PaymentService.Capture](#paymentservicecapture) | Payments | `PaymentServiceCaptureRequest` | +| [MerchantAuthenticationService.CreateServerAuthenticationToken](#merchantauthenticationservicecreateserverauthenticationtoken) | Authentication | `MerchantAuthenticationServiceCreateServerAuthenticationTokenRequest` | +| [PaymentService.Get](#paymentserviceget) | Payments | `PaymentServiceGetRequest` | +| [PaymentService.ProxyAuthorize](#paymentserviceproxyauthorize) | Payments | `PaymentServiceProxyAuthorizeRequest` | +| [PaymentService.Refund](#paymentservicerefund) | Payments | `PaymentServiceRefundRequest` | +| [RefundService.Get](#refundserviceget) | Refunds | `RefundServiceGetRequest` | +| [PaymentService.Void](#paymentservicevoid) | Payments | `PaymentServiceVoidRequest` | + +### Payments + +#### PaymentService.Authorize + +Authorize a payment amount on a payment method. This reserves funds without capturing them, essential for verifying availability before finalizing. + +| | Message | +|---|---------| +| **Request** | `PaymentServiceAuthorizeRequest` | +| **Response** | `PaymentServiceAuthorizeResponse` | + +**Supported payment method types:** + +| Payment Method | Supported | +|----------------|:---------:| +| Card | ✓ | +| Bancontact | ⚠ | +| Apple Pay | ⚠ | +| Apple Pay Dec | ⚠ | +| Apple Pay SDK | ⚠ | +| Google Pay | ⚠ | +| Google Pay Dec | ⚠ | +| Google Pay SDK | ⚠ | +| PayPal SDK | ⚠ | +| Amazon Pay | ⚠ | +| Cash App | ⚠ | +| PayPal | ⚠ | +| WeChat Pay | ⚠ | +| Alipay | ⚠ | +| Revolut Pay | ⚠ | +| MiFinity | ⚠ | +| Bluecode | ⚠ | +| Paze | x | +| Samsung Pay | ⚠ | +| MB Way | ⚠ | +| Satispay | ⚠ | +| Wero | ⚠ | +| GoPay | ⚠ | +| GCash | ⚠ | +| Momo | ⚠ | +| Dana | ⚠ | +| Kakao Pay | ⚠ | +| Touch 'n Go | ⚠ | +| Twint | ⚠ | +| Vipps | ⚠ | +| Swish | ⚠ | +| Affirm | ⚠ | +| Afterpay | ⚠ | +| Klarna | ⚠ | +| UPI Collect | ⚠ | +| UPI Intent | ⚠ | +| UPI QR | ⚠ | +| Thailand | ⚠ | +| Czech | ⚠ | +| Finland | ⚠ | +| FPX | ⚠ | +| Poland | ⚠ | +| Slovakia | ⚠ | +| UK | ⚠ | +| PIS | x | +| Generic | ⚠ | +| Local | ⚠ | +| iDEAL | ⚠ | +| Sofort | ⚠ | +| Trustly | ⚠ | +| Giropay | ⚠ | +| EPS | ⚠ | +| Przelewy24 | ⚠ | +| PSE | ⚠ | +| BLIK | ⚠ | +| Interac | ⚠ | +| Bizum | ⚠ | +| EFT | ⚠ | +| DuitNow | x | +| ACH | ⚠ | +| SEPA | ⚠ | +| BACS | ⚠ | +| Multibanco | ⚠ | +| Instant | ⚠ | +| Instant FI | ⚠ | +| Instant PL | ⚠ | +| Pix | ⚠ | +| Permata | ⚠ | +| BCA | ⚠ | +| BNI VA | ⚠ | +| BRI VA | ⚠ | +| CIMB VA | ⚠ | +| Danamon VA | ⚠ | +| Mandiri VA | ⚠ | +| Local | ⚠ | +| Indonesian | ⚠ | +| ACH | ⚠ | +| SEPA | ⚠ | +| BACS | ⚠ | +| BECS | ⚠ | +| SEPA Guaranteed | ⚠ | +| Crypto | x | +| Reward | ⚠ | +| Givex | x | +| PaySafeCard | ⚠ | +| E-Voucher | ⚠ | +| Boleto | ⚠ | +| Efecty | ⚠ | +| Pago Efectivo | ⚠ | +| Red Compra | ⚠ | +| Red Pagos | ⚠ | +| Alfamart | ⚠ | +| Indomaret | ⚠ | +| Oxxo | ⚠ | +| 7-Eleven | ⚠ | +| Lawson | ⚠ | +| Mini Stop | ⚠ | +| Family Mart | ⚠ | +| Seicomart | ⚠ | +| Pay Easy | ⚠ | + +**Payment method objects** — use these in the `payment_method` field of the Authorize request. + +##### Card (Raw PAN) + +```python +"payment_method": { + "card": { + "card_number": "4111111111111111", + "card_exp_month": "03", + "card_exp_year": "2030", + "card_cvc": "737", + "card_holder_name": "John Doe" + } +} +``` + +**Examples:** [Python](../../examples/moneris/moneris.py) · [TypeScript](../../examples/moneris/moneris.ts#L300) · [Kotlin](../../examples/moneris/moneris.kt#L246) · [Rust](../../examples/moneris/moneris.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/moneris/moneris.py) · [TypeScript](../../examples/moneris/moneris.ts#L309) · [Kotlin](../../examples/moneris/moneris.kt#L258) · [Rust](../../examples/moneris/moneris.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/moneris/moneris.py) · [TypeScript](../../examples/moneris/moneris.ts#L327) · [Kotlin](../../examples/moneris/moneris.kt#L278) · [Rust](../../examples/moneris/moneris.rs) + +#### PaymentService.ProxyAuthorize + +Authorize using vault-aliased card data. Proxy substitutes before connector. + +| | Message | +|---|---------| +| **Request** | `PaymentServiceProxyAuthorizeRequest` | +| **Response** | `PaymentServiceAuthorizeResponse` | + +**Examples:** [Python](../../examples/moneris/moneris.py) · [TypeScript](../../examples/moneris/moneris.ts#L336) · [Kotlin](../../examples/moneris/moneris.kt#L286) · [Rust](../../examples/moneris/moneris.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/moneris/moneris.py) · [TypeScript](../../examples/moneris/moneris.ts#L345) · [Kotlin](../../examples/moneris/moneris.kt#L322) · [Rust](../../examples/moneris/moneris.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/moneris/moneris.py) · [TypeScript](../../examples/moneris/moneris.ts) · [Kotlin](../../examples/moneris/moneris.kt#L351) · [Rust](../../examples/moneris/moneris.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/moneris/moneris.py) · [TypeScript](../../examples/moneris/moneris.ts#L354) · [Kotlin](../../examples/moneris/moneris.kt#L332) · [Rust](../../examples/moneris/moneris.rs) + +### Authentication + +#### MerchantAuthenticationService.CreateServerAuthenticationToken + +Generate short-lived connector authentication token. Provides secure credentials for connector API access without storing secrets client-side. + +| | Message | +|---|---------| +| **Request** | `MerchantAuthenticationServiceCreateServerAuthenticationTokenRequest` | +| **Response** | `MerchantAuthenticationServiceCreateServerAuthenticationTokenResponse` | + +**Examples:** [Python](../../examples/moneris/moneris.py) · [TypeScript](../../examples/moneris/moneris.ts#L318) · [Kotlin](../../examples/moneris/moneris.kt#L268) · [Rust](../../examples/moneris/moneris.rs) diff --git a/docs-generated/llms.txt b/docs-generated/llms.txt index 5c794c7530..20d0d14faa 100644 --- a/docs-generated/llms.txt +++ b/docs-generated/llms.txt @@ -1,5 +1,5 @@ # Connector Service — LLM Navigation Index -# Connectors: 105 +# Connectors: 108 # # 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: 108 docs_root: docs-generated/connectors/ examples_root: examples/ all_connectors_matrix: docs-generated/all_connector.md @@ -190,6 +190,14 @@ payment_methods: Ach, ApplePayDecrypted, Card, GooglePayDecrypted flows: authorize, capture, get, proxy_authorize, proxy_setup_recurring, recurring_charge, refund, refund_get, setup_recurring, void examples_python: examples/checkout/checkout.py +## Citigate +connector_id: citigate +doc: docs/connectors/citigate.md +scenarios: checkout_autocapture, checkout_card, refund, void_payment, get_payment +payment_methods: Card +flows: authorize, capture, get, proxy_authorize, refund, refund_get, void +examples_python: examples/citigate/citigate.py + ## CryptoPay connector_id: cryptopay doc: docs/connectors/cryptopay.md @@ -390,6 +398,14 @@ payment_methods: Ideal, LocalBankRedirect, UpiCollect, UpiIntent, UpiQr flows: authorize, create_server_authentication_token, get, refund, refund_get examples_python: none +## Ilixium +connector_id: ilixium +doc: docs/connectors/ilixium.md +scenarios: checkout_autocapture, checkout_card, refund, void_payment, get_payment +payment_methods: Card +flows: authorize, capture, get, proxy_authorize, refund, void +examples_python: examples/ilixium/ilixium.py + ## Imerchantsolutions connector_id: imerchantsolutions doc: docs/connectors/imerchantsolutions.md @@ -462,6 +478,14 @@ payment_methods: Card, Klarna flows: authorize, create_client_authentication_token, get, proxy_authorize, refund, refund_get, token_authorize, void examples_python: examples/mollie/mollie.py +## Moneris +connector_id: moneris +doc: docs/connectors/moneris.md +scenarios: checkout_autocapture, checkout_card, refund, void_payment, get_payment +payment_methods: Card +flows: authorize, capture, create_server_authentication_token, get, proxy_authorize, refund, refund_get, void +examples_python: examples/moneris/moneris.py + ## Multisafepay connector_id: multisafepay doc: docs/connectors/multisafepay.md diff --git a/docs-generated/sdks/java/payout-service/README.md b/docs-generated/sdks/java/payout-service/README.md index f18ea54db8..9b4d04eaf6 100644 --- a/docs-generated/sdks/java/payout-service/README.md +++ b/docs-generated/sdks/java/payout-service/README.md @@ -36,6 +36,7 @@ The Payout Service enables you to send funds to recipients using the Java SDK. U | [`createLink`](./create-link.md) | Create link between recipient and payout. Associate payout with recipient. | Setting up recipient relationships | | [`createRecipient`](./create-recipient.md) | Create payout recipient. Store recipient bank/payment details. | First time paying a new recipient | | [`enrollDisburseAccount`](./enroll-disburse-account.md) | Enroll disburse account. Set up account for payouts. | Onboarding new payout accounts | +| [`eligibility`](./eligibility.md) | Check payout eligibility before initiating a transfer. | Pre-validating a payee or bank account | ## SDK Setup diff --git a/docs-generated/sdks/java/payout-service/create-link.md b/docs-generated/sdks/java/payout-service/create-link.md new file mode 100644 index 0000000000..dc9780110f --- /dev/null +++ b/docs-generated/sdks/java/payout-service/create-link.md @@ -0,0 +1,98 @@ +# createLink Method + + + +## Overview + +The `createLink` method generates a URL that can be sent to a recipient, allowing them to securely provide their own payout method details (like bank account information) to claim the funds. + +## Purpose + +Use this operation when you do not have the recipient's payout method details upfront. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Send funds to a user via email | Call `createLink` and share the generated URL with the user. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier for the payout. | +| `address` | PayoutAddress | Yes | Address information associated with the payout. | +| `connectorFeatureData` | SecretString | No | Connector-specific metadata or feature configurations. | +| `payoutMethodData` | PayoutMethod | No | Specific details of the payout instrument (optional). | +| `connectorQuoteId` | string | No | Pre-negotiated quote ID if applicable. | +| `connectorPayoutId` | string | No | An existing payout identifier from the connector. | +| `amount` | Money | Yes | The amount to be paid out. | +| `destinationCurrency` | Currency | Yes | The currency for the payout. | +| `customer` | Customer | No | Details about the customer. | +| `priority` | PayoutEnums.PayoutPriority | No | Priority of the payout. | +| `connectorPayoutMethodId` | string | No | The connector's unique ID for a stored payout method. | +| `webhookUrl` | string | No | URL where payout status updates should be sent. | +| `browserInfo` | BrowserInformation | No | Information about the user's browser. | +| `accessToken` | SecretString | No | Access token for the connector, if required. | +| `description` | string | No | Description of the payout. | +| `sourceBankData` | SourceBankData | No | Details of the bank account from which the payout is funded. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier passed to the payout processor. | +| `payoutStatus` | PayoutEnums.PayoutStatus | No | The current status of the payout link. | +| `connectorPayoutId` | string | No | The unique payout ID assigned by the connector. | +| `error` | ErrorInfo | No | Details of any error that occurred. | +| `statusCode` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```java +import com.hyperswitch.prism.PayoutClient; +import java.util.Map; +import java.util.HashMap; + +PayoutClient payoutClient = PayoutClient.builder() + .connector("stripe") + .apiKey("YOUR_API_KEY") + .environment("SANDBOX") + .build(); +``` + +### Request + +```java +Map request = new HashMap<>(); +request.put("amount", Map.of("minorAmount", 1000, "currency", "USD")); +request.put("destinationCurrency", "USD"); + +Response response = payoutClient.createLink(request); +``` + +### Response + +```java +{ + "payoutStatus": "REQUIRES_PAYOUT_METHOD_DATA", + "connectorPayoutId": "po_1Hh1XYZ2eZvKYlo2C", + "statusCode": 200 +} +``` + +## Next Steps + +- [Get Payout Status](./get.md) diff --git a/docs-generated/sdks/java/payout-service/create-recipient.md b/docs-generated/sdks/java/payout-service/create-recipient.md new file mode 100644 index 0000000000..7f7ea2f520 --- /dev/null +++ b/docs-generated/sdks/java/payout-service/create-recipient.md @@ -0,0 +1,89 @@ +# createRecipient Method + + + +## Overview + +The `createRecipient` method registers a new recipient entity (individual or business) with the payment processor. This is often required before funds can be transferred to them. + +## Purpose + +Use this operation to set up a vendor, contractor, or user in the processor's system. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Onboard a new seller | Call `createRecipient` with their details and `recipientType`. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier for the payout/recipient operation. | +| `address` | PayoutAddress | Yes | Address information associated with the recipient. | +| `payoutMethodData` | PayoutMethod | No | Specific details of the payout instrument for the recipient. | +| `amount` | Money | Yes | The amount to be paid out (if creating recipient inline with a payout). | +| `recipientType` | PayoutEnums.PayoutRecipientType | Yes | Type of entity (e.g., INDIVIDUAL, COMPANY). | +| `customer` | Customer | No | Details about the customer/recipient. | +| `accessToken` | SecretString | No | Access token for the connector, if required. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier passed to the payout processor. | +| `payoutStatus` | PayoutEnums.PayoutStatus | No | The status of the recipient creation. | +| `connectorPayoutId` | string | No | The unique identifier assigned to the recipient or payout. | +| `error` | ErrorInfo | No | Details of any error that occurred. | +| `statusCode` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```java +import com.hyperswitch.prism.PayoutClient; +import java.util.Map; +import java.util.HashMap; + +PayoutClient payoutClient = PayoutClient.builder() + .connector("stripe") + .apiKey("YOUR_API_KEY") + .environment("SANDBOX") + .build(); +``` + +### Request + +```java +Map request = new HashMap<>(); +request.put("recipientType", "INDIVIDUAL"); +request.put("amount", Map.of("minorAmount", 0, "currency", "USD")); + +Response response = payoutClient.createRecipient(request); +``` + +### Response + +```java +{ + "payoutStatus": "SUCCESS", + "connectorPayoutId": "acct_1Hh1XYZ2eZvKYlo2C", + "statusCode": 200 +} +``` + +## Next Steps + +- [Transfer Payout](./transfer.md) diff --git a/docs-generated/sdks/java/payout-service/create.md b/docs-generated/sdks/java/payout-service/create.md new file mode 100644 index 0000000000..a37e91b422 --- /dev/null +++ b/docs-generated/sdks/java/payout-service/create.md @@ -0,0 +1,101 @@ +# create Method + + + +## Overview + +The `create` method in the Payout Service is used to initiate a transfer of funds from your merchant account to a customer, vendor, or third-party entity. This is commonly used in marketplaces, gig-economy platforms, or any business model requiring external disbursements. + +## Purpose + +This operation is the first step in the payout lifecycle. Use this when you have sufficient funds and wish to send money to a registered payout method (like a bank account, card, or wallet). + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Send funds to a vendor | Call `create` with the vendor's bank account details and payout amount. | +| Refund a customer via an alternative method | Provide the customer's wallet or card details in the `payoutMethodData`. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier passed to the payout processor. | +| `address` | PayoutAddress | Yes | The shipping and billing address associated with the payout. | +| `connectorFeatureData` | SecretString | No | Connector-specific metadata or feature configurations. | +| `payoutMethodData` | PayoutMethod | No | Specific details of the payout instrument (e.g., Card, ACH, Pix). | +| `connectorQuoteId` | string | No | Pre-negotiated quote ID if applicable. | +| `connectorPayoutId` | string | No | An existing payout identifier from the connector, if any. | +| `amount` | Money | Yes | The amount to be paid out along with the source currency. | +| `destinationCurrency` | Currency | Yes | The currency in which the recipient will receive the payout. | +| `customer` | Customer | No | Details about the customer receiving the payout. | +| `priority` | PayoutEnums.PayoutPriority | No | Priority of the payout (e.g., INSTANT, WIRE). | +| `connectorPayoutMethodId` | string | No | The connector's unique ID for a stored payout method. | +| `webhookUrl` | string | No | URL where payout status updates should be sent. | +| `browserInfo` | BrowserInformation | No | Information about the user's browser, used for fraud prevention. | +| `accessToken` | SecretString | No | Access token for the connector, if required. | +| `sourceBankData` | SourceBankData | No | Details of the bank account from which the payout is funded. | +| `description` | string | No | Description of the payout. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier passed to the payout processor. | +| `payoutStatus` | PayoutEnums.PayoutStatus | No | The current status of the payout (e.g., PENDING, SUCCESS). | +| `connectorPayoutId` | string | No | The unique payout ID assigned by the connector. | +| `error` | ErrorInfo | No | Details of any error that occurred during creation. | +| `statusCode` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```java +import com.hyperswitch.prism.PayoutClient; +import java.util.Map; +import java.util.HashMap; + +PayoutClient payoutClient = PayoutClient.builder() + .connector("stripe") + .apiKey("YOUR_API_KEY") + .environment("SANDBOX") + .build(); +``` + +### Request + +```java +Map request = new HashMap<>(); +request.put("amount", Map.of("minorAmount", 1000, "currency", "USD")); +request.put("destinationCurrency", "USD"); +request.put("payoutMethodData", Map.of("card", Map.of("cardNumber", "4242424242424242", "cardExpMonth", "12", "cardExpYear", "2027"))); + +Response response = payoutClient.create(request); +``` + +### Response + +```java +{ + "payoutStatus": "PENDING", + "connectorPayoutId": "po_1Hh1XYZ2eZvKYlo2C", + "statusCode": 200 +} +``` + +## Next Steps + +- [Get Payout Status](./get.md) +- [Void Payout](./void.md) diff --git a/docs-generated/sdks/java/payout-service/eligibility.md b/docs-generated/sdks/java/payout-service/eligibility.md new file mode 100644 index 0000000000..19c61f5bc0 --- /dev/null +++ b/docs-generated/sdks/java/payout-service/eligibility.md @@ -0,0 +1,98 @@ +# Eligibility Method + + + +## Overview + +The `eligibility` method in the Payout Service checks whether a payout can be made to a given payout method before funds are committed. It is used for pre-verification flows such as SEPA Verification of Payee (VoP) and other payee-verification checks. + +## Purpose + +Use this operation to validate a payee or payout method before initiating a transfer, reducing the risk of failed or misdirected payouts. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Verify a payee before transfer | Call `eligibility` with the payout method and amount, then inspect `payoutEligible`. | +| Pre-check a bank account | Call `eligibility` with the bank details to confirm the account can receive funds. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier for the eligibility check. | +| `connectorFeatureData` | SecretString | No | Connector-specific metadata passed to the eligibility check. | +| `payoutMethodData` | PayoutMethod | No | Specific details of the payout instrument being checked. | +| `amount` | Money | Yes | The amount to be paid out. | +| `connectorPayoutId` | string | No | An existing payout identifier from the connector. | +| `destinationCurrency` | Currency | Yes | The currency in which the recipient will receive the payout. | +| `accessToken` | SecretString | No | Access token for the connector, if required. | +| `address` | PayoutAddress | No | Address information associated with the payout. | +| `customer` | Customer | No | Details about the customer receiving the payout. | +| `sourceBankData` | SourceBankData | No | Details of the bank account from which the payout is funded. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier for the eligibility check. | +| `payoutStatus` | PayoutEnums.PayoutStatus | No | The current status of the payout. | +| `connectorPayoutId` | string | No | The unique payout ID assigned by the connector (set only when the payee is eligible). | +| `error` | ErrorInfo | No | Details of any error that occurred during the check. | +| `statusCode` | uint32 | Yes | The HTTP status code returned from the connector. | +| `payoutEligible` | bool | No | Whether the payout is eligible. | +| `connectorMetadata` | SecretString | No | Connector-specific details as a JSON object, surfaced to the merchant. | +| `connectorEligibilityReferenceId` | string | No | Connector's reference for the eligibility check itself. Set for every verdict so the check remains traceable for reconciliation. | + +## Example + +### SDK Setup + +```java +import com.hyperswitch.prism.PayoutClient; +import java.util.Map; +import java.util.HashMap; + +PayoutClient payoutClient = PayoutClient.builder() + .connector("stripe") + .apiKey("YOUR_API_KEY") + .environment("SANDBOX") + .build(); +``` + +### Request + +```java +Map request = new HashMap<>(); +request.put("amount", Map.of("minorAmount", 1000, "currency", "USD")); +request.put("destinationCurrency", "USD"); +request.put("payoutMethodData", Map.of("ach", Map.of("bankAccountNumber", "000123456789", "bankRoutingNumber", "110000000"))); + +Response response = payoutClient.eligibility(request); +``` + +### Response + +```java +{ + "payoutEligible": true, + "connectorEligibilityReferenceId": "elig_1Hh1XYZ2eZvKYlo2C", + "statusCode": 200 +} +``` + +## Next Steps + +- [Transfer Payout](./transfer.md) +- [Create Payout](./create.md) diff --git a/docs-generated/sdks/java/payout-service/enroll-disburse-account.md b/docs-generated/sdks/java/payout-service/enroll-disburse-account.md new file mode 100644 index 0000000000..1c23b152bf --- /dev/null +++ b/docs-generated/sdks/java/payout-service/enroll-disburse-account.md @@ -0,0 +1,88 @@ +# enrollDisburseAccount Method + + + +## Overview + +The `enrollDisburseAccount` method registers and verifies a destination account (like a bank account) to receive disbursements from your platform. + +## Purpose + +Use this operation to securely enroll payout destinations. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Link a vendor's bank account | Call `enrollDisburseAccount` with their bank details. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier for the operation. | +| `address` | PayoutAddress | Yes | Address information associated with the account. | +| `payoutMethodData` | PayoutMethod | No | Specific details of the payout instrument being enrolled. | +| `amount` | Money | Yes | The amount to be paid out (if enrolling inline). | +| `customer` | Customer | No | Details about the customer/account holder. | +| `accessToken` | SecretString | No | Access token for the connector, if required. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier. | +| `payoutStatus` | PayoutEnums.PayoutStatus | No | The status of the enrollment. | +| `connectorPayoutId` | string | No | The unique identifier assigned by the connector. | +| `error` | ErrorInfo | No | Details of any error that occurred. | +| `statusCode` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```java +import com.hyperswitch.prism.PayoutClient; +import java.util.Map; +import java.util.HashMap; + +PayoutClient payoutClient = PayoutClient.builder() + .connector("stripe") + .apiKey("YOUR_API_KEY") + .environment("SANDBOX") + .build(); +``` + +### Request + +```java +Map request = new HashMap<>(); +request.put("amount", Map.of("minorAmount", 0, "currency", "USD")); +request.put("payoutMethodData", Map.of("ach", Map.of("bankAccountNumber", "000123456789", "bankRoutingNumber", "110000000"))); + +Response response = payoutClient.enrollDisburseAccount(request); +``` + +### Response + +```java +{ + "payoutStatus": "SUCCESS", + "connectorPayoutId": "ba_1Hh1XYZ2eZvKYlo2C", + "statusCode": 200 +} +``` + +## Next Steps + +- [Transfer Payout](./transfer.md) diff --git a/docs-generated/sdks/java/payout-service/get.md b/docs-generated/sdks/java/payout-service/get.md new file mode 100644 index 0000000000..9827602cfa --- /dev/null +++ b/docs-generated/sdks/java/payout-service/get.md @@ -0,0 +1,86 @@ +# get Method + + + +## Overview + +The `get` method allows you to check the current status of a payout. This is essential for syncing your internal systems with the payment processor's state, especially for asynchronous payout methods like bank transfers. + +## Purpose + +Use this operation to poll for the status of a payout or to verify details before taking further action. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Check if a payout succeeded | Call `get` with the `merchantPayoutId` or `connectorPayoutId`. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier for the payout. | +| `connectorPayoutId` | string | No | The unique payout ID assigned by the connector. | +| `accessToken` | SecretString | No | Access token for the connector, if required. | +| `sourceBankData` | SourceBankData | No | Source (debtor) bank data. Some connectors (e.g. Deutsche Bank) require the debtor account details to perform a status enquiry. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier passed to the payout processor. | +| `payoutStatus` | PayoutEnums.PayoutStatus | No | The current status of the payout. | +| `connectorPayoutId` | string | No | The unique payout ID assigned by the connector. | +| `error` | ErrorInfo | No | Details of any error that occurred. | +| `statusCode` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```java +import com.hyperswitch.prism.PayoutClient; +import java.util.Map; +import java.util.HashMap; + +PayoutClient payoutClient = PayoutClient.builder() + .connector("stripe") + .apiKey("YOUR_API_KEY") + .environment("SANDBOX") + .build(); +``` + +### Request + +```java +Map request = new HashMap<>(); +request.put("merchantPayoutId", "po_internal_12345"); + +Response response = payoutClient.get(request); +``` + +### Response + +```java +{ + "merchantPayoutId": "po_internal_12345", + "payoutStatus": "SUCCESS", + "connectorPayoutId": "po_1Hh1XYZ2eZvKYlo2C", + "statusCode": 200 +} +``` + +## Next Steps + +- [Void Payout](./void.md) diff --git a/docs-generated/sdks/java/payout-service/stage.md b/docs-generated/sdks/java/payout-service/stage.md new file mode 100644 index 0000000000..7c79310141 --- /dev/null +++ b/docs-generated/sdks/java/payout-service/stage.md @@ -0,0 +1,90 @@ +# stage Method + + + +## Overview + +The `stage` method allows you to prepare or stage a payout before it is executed. This can be used in workflows where payouts are reviewed or batched before final transfer. + +## Purpose + +Use this operation to put a payout into a staged state. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Prepare a payout for review | Call `stage` with the quote details and amount. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantQuoteId` | string | No | Your internal quote identifier. | +| `address` | PayoutAddress | Yes | Address information associated with the payout. | +| `amount` | Money | Yes | The amount to be staged. | +| `destinationCurrency` | Currency | Yes | The currency in which the payout will be received. | +| `customer` | Customer | No | Details about the customer. | +| `browserInfo` | BrowserInformation | No | Information about the user's browser. | +| `accessToken` | SecretString | No | Access token for the connector, if required. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier passed to the payout processor. | +| `payoutStatus` | PayoutEnums.PayoutStatus | No | The current status of the staged payout. | +| `connectorPayoutId` | string | No | The unique payout ID assigned by the connector. | +| `error` | ErrorInfo | No | Details of any error that occurred. | +| `statusCode` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```java +import com.hyperswitch.prism.PayoutClient; +import java.util.Map; +import java.util.HashMap; + +PayoutClient payoutClient = PayoutClient.builder() + .connector("stripe") + .apiKey("YOUR_API_KEY") + .environment("SANDBOX") + .build(); +``` + +### Request + +```java +Map request = new HashMap<>(); +request.put("merchantQuoteId", "quote_123"); +request.put("amount", Map.of("minorAmount", 1000, "currency", "USD")); +request.put("destinationCurrency", "USD"); + +Response response = payoutClient.stage(request); +``` + +### Response + +```java +{ + "payoutStatus": "PENDING", + "connectorPayoutId": "po_1Hh1XYZ2eZvKYlo2C", + "statusCode": 200 +} +``` + +## Next Steps + +- [Transfer Payout](./transfer.md) diff --git a/docs-generated/sdks/java/payout-service/transfer.md b/docs-generated/sdks/java/payout-service/transfer.md new file mode 100644 index 0000000000..957d56437a --- /dev/null +++ b/docs-generated/sdks/java/payout-service/transfer.md @@ -0,0 +1,99 @@ +# transfer Method + + + +## Overview + +The `transfer` method in the Payout Service is used to execute the actual fund transfer for a previously initiated payout, or to perform a combined create and transfer operation depending on the processor's flow. + +## Purpose + +Use this operation to move funds to the destination payout method. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Execute a payout transfer | Call `transfer` with the required payout details and amount. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier passed to the payout processor. | +| `address` | PayoutAddress | Yes | Address information associated with the payout. | +| `payoutMethodData` | PayoutMethod | No | Specific details of the payout instrument. | +| `connectorQuoteId` | string | No | Pre-negotiated quote ID if applicable. | +| `amount` | Money | Yes | The amount to be transferred. | +| `connectorPayoutId` | string | No | An existing payout identifier from the connector. | +| `destinationCurrency` | Currency | Yes | The currency in which the recipient will receive the payout. | +| `customer` | Customer | No | Details about the customer receiving the payout. | +| `priority` | PayoutEnums.PayoutPriority | No | Priority of the payout transfer. | +| `connectorPayoutMethodId` | string | No | The connector's unique ID for a stored payout method. | +| `webhookUrl` | string | No | URL where payout status updates should be sent. | +| `browserInfo` | BrowserInformation | No | Information about the user's browser. | +| `accessToken` | SecretString | No | Access token for the connector, if required. | +| `sourceBankData` | SourceBankData | No | Details of the bank account from which the payout is funded. | +| `description` | string | No | Description of the payout. | +| `connectorEligibilityReferenceId` | string | No | Connector's reference for a prior eligibility check, when the processor requires it to authorise the transfer. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier passed to the payout processor. | +| `payoutStatus` | PayoutEnums.PayoutStatus | No | The current status of the payout transfer. | +| `connectorPayoutId` | string | No | The unique payout ID assigned by the connector. | +| `error` | ErrorInfo | No | Details of any error that occurred during transfer. | +| `statusCode` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```java +import com.hyperswitch.prism.PayoutClient; +import java.util.Map; +import java.util.HashMap; + +PayoutClient payoutClient = PayoutClient.builder() + .connector("stripe") + .apiKey("YOUR_API_KEY") + .environment("SANDBOX") + .build(); +``` + +### Request + +```java +Map request = new HashMap<>(); +request.put("amount", Map.of("minorAmount", 1000, "currency", "USD")); +request.put("destinationCurrency", "USD"); +request.put("payoutMethodData", Map.of("card", Map.of("cardNumber", "4242424242424242", "cardExpMonth", "12", "cardExpYear", "2027"))); + +Response response = payoutClient.transfer(request); +``` + +### Response + +```java +{ + "payoutStatus": "SUCCESS", + "connectorPayoutId": "tr_1Hh1XYZ2eZvKYlo2C", + "statusCode": 200 +} +``` + +## Next Steps + +- [Get Payout Status](./get.md) diff --git a/docs-generated/sdks/java/payout-service/void.md b/docs-generated/sdks/java/payout-service/void.md new file mode 100644 index 0000000000..da5764e293 --- /dev/null +++ b/docs-generated/sdks/java/payout-service/void.md @@ -0,0 +1,86 @@ +# void Method + + + +## Overview + +The `void` method cancels a payout that has been initiated but not yet completed or transferred. This is used when a payout needs to be stopped before the funds leave the merchant's account. + +## Purpose + +Use this operation to halt a payout process. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Cancel a scheduled payout | Call `void` with the `merchantPayoutId` and required details. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier for the payout. | +| `address` | PayoutAddress | Yes | Address information associated with the payout. | +| `connectorFeatureData` | SecretString | No | Connector-specific metadata or feature configurations. | +| `connectorPayoutId` | string | No | The unique payout ID assigned by the connector. | +| `accessToken` | SecretString | No | Access token for the connector, if required. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier passed to the payout processor. | +| `payoutStatus` | PayoutEnums.PayoutStatus | No | The current status of the payout (e.g., CANCELLED). | +| `connectorPayoutId` | string | No | The unique payout ID assigned by the connector. | +| `error` | ErrorInfo | No | Details of any error that occurred. | +| `statusCode` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```java +import com.hyperswitch.prism.PayoutClient; +import java.util.Map; +import java.util.HashMap; + +PayoutClient payoutClient = PayoutClient.builder() + .connector("stripe") + .apiKey("YOUR_API_KEY") + .environment("SANDBOX") + .build(); +``` + +### Request + +```java +Map request = new HashMap<>(); +request.put("connectorPayoutId", "po_1Hh1XYZ2eZvKYlo2C"); + +Response response = payoutClient.void(request); +``` + +### Response + +```java +{ + "payoutStatus": "CANCELLED", + "connectorPayoutId": "po_1Hh1XYZ2eZvKYlo2C", + "statusCode": 200 +} +``` + +## Next Steps + +- [Get Payout Status](./get.md) diff --git a/docs-generated/sdks/node/payout-service/README.md b/docs-generated/sdks/node/payout-service/README.md index bf5aebdbf1..29b2d20c13 100644 --- a/docs-generated/sdks/node/payout-service/README.md +++ b/docs-generated/sdks/node/payout-service/README.md @@ -36,6 +36,7 @@ The Payout Service enables you to send funds to recipients using the Node.js SDK | [`createLink`](./create-link.md) | Create link between recipient and payout. Associate payout with recipient. | Setting up recipient relationships | | [`createRecipient`](./create-recipient.md) | Create payout recipient. Store recipient bank/payment details. | First time paying a new recipient | | [`enrollDisburseAccount`](./enroll-disburse-account.md) | Enroll disburse account. Set up account for payouts. | Onboarding new payout accounts | +| [`eligibility`](./eligibility.md) | Check payout eligibility before initiating a transfer. | Pre-validating a payee or bank account | ## SDK Setup diff --git a/docs-generated/sdks/node/payout-service/create-link.md b/docs-generated/sdks/node/payout-service/create-link.md new file mode 100644 index 0000000000..c4e52a1906 --- /dev/null +++ b/docs-generated/sdks/node/payout-service/create-link.md @@ -0,0 +1,100 @@ +# CreateLink Method + + + +## Overview + +The `createLink` method generates a URL that can be sent to a recipient, allowing them to securely provide their own payout method details (like bank account information) to claim the funds. + +## Purpose + +Use this operation when you do not have the recipient's payout method details upfront. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Send funds to a user via email | Call `createLink` and share the generated URL with the user. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier for the payout. | +| `address` | PayoutAddress | Yes | Address information associated with the payout. | +| `connectorFeatureData` | SecretString | No | Connector-specific metadata or feature configurations. | +| `payoutMethodData` | PayoutMethod | No | Specific details of the payout instrument (optional). | +| `connectorQuoteId` | string | No | Pre-negotiated quote ID if applicable. | +| `connectorPayoutId` | string | No | An existing payout identifier from the connector. | +| `amount` | Money | Yes | The amount to be paid out. | +| `destinationCurrency` | Currency | Yes | The currency for the payout. | +| `customer` | Customer | No | Details about the customer. | +| `priority` | PayoutEnums.PayoutPriority | No | Priority of the payout. | +| `connectorPayoutMethodId` | string | No | The connector's unique ID for a stored payout method. | +| `webhookUrl` | string | No | URL where payout status updates should be sent. | +| `browserInfo` | BrowserInformation | No | Information about the user's browser. | +| `accessToken` | SecretString | No | Access token for the connector, if required. | +| `description` | string | No | Description of the payout. | +| `sourceBankData` | SourceBankData | No | Details of the bank account from which the payout is funded. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier passed to the payout processor. | +| `payoutStatus` | PayoutEnums.PayoutStatus | No | The current status of the payout link. | +| `connectorPayoutId` | string | No | The unique payout ID assigned by the connector. | +| `error` | ErrorInfo | No | Details of any error that occurred. | +| `statusCode` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```javascript +const { PayoutClient } = require('hyperswitch-prism'); + +const payoutClient = new PayoutClient({ + connector: 'stripe', + apiKey: '[REDACTED_ENV_SECRET]', + environment: 'SANDBOX' +}); +``` + +### Request + +```javascript +const request = { + amount: { + minorAmount: 1000, + currency: "USD" + }, + destinationCurrency: "USD" +}; + +const response = await payoutClient.createLink(request); +``` + +### Response + +```javascript +{ + payoutStatus: "REQUIRES_PAYOUT_METHOD_DATA", + connectorPayoutId: "po_1Hh1XYZ2eZvKYlo2C", + statusCode: 200 +} +``` + +## Next Steps + +- [Get Payout Status](./get.md) diff --git a/docs-generated/sdks/node/payout-service/create-recipient.md b/docs-generated/sdks/node/payout-service/create-recipient.md new file mode 100644 index 0000000000..695224dade --- /dev/null +++ b/docs-generated/sdks/node/payout-service/create-recipient.md @@ -0,0 +1,91 @@ +# CreateRecipient Method + + + +## Overview + +The `createRecipient` method registers a new recipient entity (individual or business) with the payment processor. This is often required before funds can be transferred to them. + +## Purpose + +Use this operation to set up a vendor, contractor, or user in the processor's system. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Onboard a new seller | Call `createRecipient` with their details and `recipientType`. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier for the payout/recipient operation. | +| `address` | PayoutAddress | Yes | Address information associated with the recipient. | +| `payoutMethodData` | PayoutMethod | No | Specific details of the payout instrument for the recipient. | +| `amount` | Money | Yes | The amount to be paid out (if creating recipient inline with a payout). | +| `recipientType` | PayoutEnums.PayoutRecipientType | Yes | Type of entity (e.g., INDIVIDUAL, COMPANY). | +| `customer` | Customer | No | Details about the customer/recipient. | +| `accessToken` | SecretString | No | Access token for the connector, if required. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier passed to the payout processor. | +| `payoutStatus` | PayoutEnums.PayoutStatus | No | The status of the recipient creation. | +| `connectorPayoutId` | string | No | The unique identifier assigned to the recipient or payout. | +| `error` | ErrorInfo | No | Details of any error that occurred. | +| `statusCode` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```javascript +const { PayoutClient } = require('hyperswitch-prism'); + +const payoutClient = new PayoutClient({ + connector: 'stripe', + apiKey: '[REDACTED_ENV_SECRET]', + environment: 'SANDBOX' +}); +``` + +### Request + +```javascript +const request = { + recipientType: "INDIVIDUAL", + amount: { + minorAmount: 0, + currency: "USD" + } +}; + +const response = await payoutClient.createRecipient(request); +``` + +### Response + +```javascript +{ + payoutStatus: "SUCCESS", + connectorPayoutId: "acct_1Hh1XYZ2eZvKYlo2C", + statusCode: 200 +} +``` + +## Next Steps + +- [Transfer Payout](./transfer.md) diff --git a/docs-generated/sdks/node/payout-service/create.md b/docs-generated/sdks/node/payout-service/create.md new file mode 100644 index 0000000000..3103233c67 --- /dev/null +++ b/docs-generated/sdks/node/payout-service/create.md @@ -0,0 +1,109 @@ +# Create Method + + + +## Overview + +The `create` method in the Payout Service is used to initiate a transfer of funds from your merchant account to a customer, vendor, or third-party entity. This is commonly used in marketplaces, gig-economy platforms, or any business model requiring external disbursements. + +## Purpose + +This operation is the first step in the payout lifecycle. Use this when you have sufficient funds and wish to send money to a registered payout method (like a bank account, card, or wallet). + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Send funds to a vendor | Call `create` with the vendor's bank account details and payout amount. | +| Refund a customer via an alternative method | Provide the customer's wallet or card details in the `payoutMethodData`. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier passed to the payout processor. | +| `address` | PayoutAddress | Yes | The shipping and billing address associated with the payout. | +| `connectorFeatureData` | SecretString | No | Connector-specific metadata or feature configurations. | +| `payoutMethodData` | PayoutMethod | No | Specific details of the payout instrument (e.g., Card, ACH, Pix). | +| `connectorQuoteId` | string | No | Pre-negotiated quote ID if applicable. | +| `connectorPayoutId` | string | No | An existing payout identifier from the connector, if any. | +| `amount` | Money | Yes | The amount to be paid out along with the source currency. | +| `destinationCurrency` | Currency | Yes | The currency in which the recipient will receive the payout. | +| `customer` | Customer | No | Details about the customer receiving the payout. | +| `priority` | PayoutEnums.PayoutPriority | No | Priority of the payout (e.g., INSTANT, WIRE). | +| `connectorPayoutMethodId` | string | No | The connector's unique ID for a stored payout method. | +| `webhookUrl` | string | No | URL where payout status updates should be sent. | +| `browserInfo` | BrowserInformation | No | Information about the user's browser, used for fraud prevention. | +| `accessToken` | SecretString | No | Access token for the connector, if required. | +| `sourceBankData` | SourceBankData | No | Details of the bank account from which the payout is funded. | +| `description` | string | No | Description of the payout. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier passed to the payout processor. | +| `payoutStatus` | PayoutEnums.PayoutStatus | No | The current status of the payout (e.g., PENDING, SUCCESS). | +| `connectorPayoutId` | string | No | The unique payout ID assigned by the connector. | +| `error` | ErrorInfo | No | Details of any error that occurred during creation. | +| `statusCode` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```javascript +const { PayoutClient } = require('hyperswitch-prism'); + +const payoutClient = new PayoutClient({ + connector: 'stripe', + apiKey: '[REDACTED_ENV_SECRET]', + environment: 'SANDBOX' +}); +``` + +### Request + +```javascript +const request = { + amount: { + minorAmount: 1000, + currency: "USD" + }, + destinationCurrency: "USD", + payoutMethodData: { + card: { + cardNumber: "4242424242424242", + cardExpMonth: "12", + cardExpYear: "2027" + } + } +}; + +const response = await payoutClient.create(request); +``` + +### Response + +```javascript +{ + payoutStatus: "PENDING", + connectorPayoutId: "po_1Hh1XYZ2eZvKYlo2C", + statusCode: 200 +} +``` + +## Next Steps + +- [Get Payout Status](./get.md) +- [Void Payout](./void.md) diff --git a/docs-generated/sdks/node/payout-service/eligibility.md b/docs-generated/sdks/node/payout-service/eligibility.md new file mode 100644 index 0000000000..0d7bf2cb49 --- /dev/null +++ b/docs-generated/sdks/node/payout-service/eligibility.md @@ -0,0 +1,105 @@ +# Eligibility Method + + + +## Overview + +The `eligibility` method in the Payout Service checks whether a payout can be made to a given payout method before funds are committed. It is used for pre-verification flows such as SEPA Verification of Payee (VoP) and other payee-verification checks. + +## Purpose + +Use this operation to validate a payee or payout method before initiating a transfer, reducing the risk of failed or misdirected payouts. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Verify a payee before transfer | Call `eligibility` with the payout method and amount, then inspect `payoutEligible`. | +| Pre-check a bank account | Call `eligibility` with the bank details to confirm the account can receive funds. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier for the eligibility check. | +| `connectorFeatureData` | SecretString | No | Connector-specific metadata passed to the eligibility check. | +| `payoutMethodData` | PayoutMethod | No | Specific details of the payout instrument being checked. | +| `amount` | Money | Yes | The amount to be paid out. | +| `connectorPayoutId` | string | No | An existing payout identifier from the connector. | +| `destinationCurrency` | Currency | Yes | The currency in which the recipient will receive the payout. | +| `accessToken` | SecretString | No | Access token for the connector, if required. | +| `address` | PayoutAddress | No | Address information associated with the payout. | +| `customer` | Customer | No | Details about the customer receiving the payout. | +| `sourceBankData` | SourceBankData | No | Details of the bank account from which the payout is funded. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier for the eligibility check. | +| `payoutStatus` | PayoutEnums.PayoutStatus | No | The current status of the payout. | +| `connectorPayoutId` | string | No | The unique payout ID assigned by the connector (set only when the payee is eligible). | +| `error` | ErrorInfo | No | Details of any error that occurred during the check. | +| `statusCode` | uint32 | Yes | The HTTP status code returned from the connector. | +| `payoutEligible` | bool | No | Whether the payout is eligible. | +| `connectorMetadata` | SecretString | No | Connector-specific details as a JSON object, surfaced to the merchant. | +| `connectorEligibilityReferenceId` | string | No | Connector's reference for the eligibility check itself. Set for every verdict so the check remains traceable for reconciliation. | + +## Example + +### SDK Setup + +```javascript +const { PayoutClient } = require('hyperswitch-prism'); + +const payoutClient = new PayoutClient({ + connector: 'stripe', + apiKey: 'YOUR_API_KEY', + environment: 'SANDBOX' +}); +``` + +### Request + +```javascript +const request = { + amount: { + minorAmount: 1000, + currency: "USD" + }, + destinationCurrency: "USD", + payoutMethodData: { + ach: { + bankAccountNumber: "000123456789", + bankRoutingNumber: "110000000" + } + } +}; + +const response = await payoutClient.eligibility(request); +``` + +### Response + +```javascript +{ + payoutEligible: true, + connectorEligibilityReferenceId: "elig_1Hh1XYZ2eZvKYlo2C", + statusCode: 200 +} +``` + +## Next Steps + +- [Transfer Payout](./transfer.md) +- [Create Payout](./create.md) diff --git a/docs-generated/sdks/node/payout-service/enroll-disburse-account.md b/docs-generated/sdks/node/payout-service/enroll-disburse-account.md new file mode 100644 index 0000000000..d34e89b4d7 --- /dev/null +++ b/docs-generated/sdks/node/payout-service/enroll-disburse-account.md @@ -0,0 +1,95 @@ +# EnrollDisburseAccount Method + + + +## Overview + +The `enrollDisburseAccount` method registers and verifies a destination account (like a bank account) to receive disbursements from your platform. + +## Purpose + +Use this operation to securely enroll payout destinations. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Link a vendor's bank account | Call `enrollDisburseAccount` with their bank details. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier for the operation. | +| `address` | PayoutAddress | Yes | Address information associated with the account. | +| `payoutMethodData` | PayoutMethod | No | Specific details of the payout instrument being enrolled. | +| `amount` | Money | Yes | The amount to be paid out (if enrolling inline). | +| `customer` | Customer | No | Details about the customer/account holder. | +| `accessToken` | SecretString | No | Access token for the connector, if required. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier. | +| `payoutStatus` | PayoutEnums.PayoutStatus | No | The status of the enrollment. | +| `connectorPayoutId` | string | No | The unique identifier assigned by the connector. | +| `error` | ErrorInfo | No | Details of any error that occurred. | +| `statusCode` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```javascript +const { PayoutClient } = require('hyperswitch-prism'); + +const payoutClient = new PayoutClient({ + connector: 'stripe', + apiKey: '[REDACTED_ENV_SECRET]', + environment: 'SANDBOX' +}); +``` + +### Request + +```javascript +const request = { + amount: { + minorAmount: 0, + currency: "USD" + }, + payoutMethodData: { + ach: { + bankAccountNumber: "000123456789", + bankRoutingNumber: "110000000" + } + } +}; + +const response = await payoutClient.enrollDisburseAccount(request); +``` + +### Response + +```javascript +{ + payoutStatus: "SUCCESS", + connectorPayoutId: "ba_1Hh1XYZ2eZvKYlo2C", + statusCode: 200 +} +``` + +## Next Steps + +- [Transfer Payout](./transfer.md) diff --git a/docs-generated/sdks/node/payout-service/get.md b/docs-generated/sdks/node/payout-service/get.md new file mode 100644 index 0000000000..b0973587ca --- /dev/null +++ b/docs-generated/sdks/node/payout-service/get.md @@ -0,0 +1,85 @@ +# Get Method + + + +## Overview + +The `get` method allows you to check the current status of a payout. This is essential for syncing your internal systems with the payment processor's state, especially for asynchronous payout methods like bank transfers. + +## Purpose + +Use this operation to poll for the status of a payout or to verify details before taking further action. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Check if a payout succeeded | Call `get` with the `merchantPayoutId` or `connectorPayoutId`. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier for the payout. | +| `connectorPayoutId` | string | No | The unique payout ID assigned by the connector. | +| `accessToken` | SecretString | No | Access token for the connector, if required. | +| `sourceBankData` | SourceBankData | No | Source (debtor) bank data. Some connectors (e.g. Deutsche Bank) require the debtor account details to perform a status enquiry. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier passed to the payout processor. | +| `payoutStatus` | PayoutEnums.PayoutStatus | No | The current status of the payout. | +| `connectorPayoutId` | string | No | The unique payout ID assigned by the connector. | +| `error` | ErrorInfo | No | Details of any error that occurred. | +| `statusCode` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```javascript +const { PayoutClient } = require('hyperswitch-prism'); + +const payoutClient = new PayoutClient({ + connector: 'stripe', + apiKey: '[REDACTED_ENV_SECRET]', + environment: 'SANDBOX' +}); +``` + +### Request + +```javascript +const request = { + merchantPayoutId: "po_internal_12345" +}; + +const response = await payoutClient.get(request); +``` + +### Response + +```javascript +{ + merchantPayoutId: "po_internal_12345", + payoutStatus: "SUCCESS", + connectorPayoutId: "po_1Hh1XYZ2eZvKYlo2C", + statusCode: 200 +} +``` + +## Next Steps + +- [Void Payout](./void.md) diff --git a/docs-generated/sdks/node/payout-service/stage.md b/docs-generated/sdks/node/payout-service/stage.md new file mode 100644 index 0000000000..329103ea5d --- /dev/null +++ b/docs-generated/sdks/node/payout-service/stage.md @@ -0,0 +1,92 @@ +# Stage Method + + + +## Overview + +The `stage` method allows you to prepare or stage a payout before it is executed. This can be used in workflows where payouts are reviewed or batched before final transfer. + +## Purpose + +Use this operation to put a payout into a staged state. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Prepare a payout for review | Call `stage` with the quote details and amount. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantQuoteId` | string | No | Your internal quote identifier. | +| `address` | PayoutAddress | Yes | Address information associated with the payout. | +| `amount` | Money | Yes | The amount to be staged. | +| `destinationCurrency` | Currency | Yes | The currency in which the payout will be received. | +| `customer` | Customer | No | Details about the customer. | +| `browserInfo` | BrowserInformation | No | Information about the user's browser. | +| `accessToken` | SecretString | No | Access token for the connector, if required. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier passed to the payout processor. | +| `payoutStatus` | PayoutEnums.PayoutStatus | No | The current status of the staged payout. | +| `connectorPayoutId` | string | No | The unique payout ID assigned by the connector. | +| `error` | ErrorInfo | No | Details of any error that occurred. | +| `statusCode` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```javascript +const { PayoutClient } = require('hyperswitch-prism'); + +const payoutClient = new PayoutClient({ + connector: 'stripe', + apiKey: '[REDACTED_ENV_SECRET]', + environment: 'SANDBOX' +}); +``` + +### Request + +```javascript +const request = { + merchantQuoteId: "quote_123", + amount: { + minorAmount: 1000, + currency: "USD" + }, + destinationCurrency: "USD" +}; + +const response = await payoutClient.stage(request); +``` + +### Response + +```javascript +{ + payoutStatus: "PENDING", + connectorPayoutId: "po_1Hh1XYZ2eZvKYlo2C", + statusCode: 200 +} +``` + +## Next Steps + +- [Transfer Payout](./transfer.md) diff --git a/docs-generated/sdks/node/payout-service/transfer.md b/docs-generated/sdks/node/payout-service/transfer.md new file mode 100644 index 0000000000..a467cc4b3e --- /dev/null +++ b/docs-generated/sdks/node/payout-service/transfer.md @@ -0,0 +1,107 @@ +# Transfer Method + + + +## Overview + +The `transfer` method in the Payout Service is used to execute the actual fund transfer for a previously initiated payout, or to perform a combined create and transfer operation depending on the processor's flow. + +## Purpose + +Use this operation to move funds to the destination payout method. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Execute a payout transfer | Call `transfer` with the required payout details and amount. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier passed to the payout processor. | +| `address` | PayoutAddress | Yes | Address information associated with the payout. | +| `payoutMethodData` | PayoutMethod | No | Specific details of the payout instrument. | +| `connectorQuoteId` | string | No | Pre-negotiated quote ID if applicable. | +| `amount` | Money | Yes | The amount to be transferred. | +| `connectorPayoutId` | string | No | An existing payout identifier from the connector. | +| `destinationCurrency` | Currency | Yes | The currency in which the recipient will receive the payout. | +| `customer` | Customer | No | Details about the customer receiving the payout. | +| `priority` | PayoutEnums.PayoutPriority | No | Priority of the payout transfer. | +| `connectorPayoutMethodId` | string | No | The connector's unique ID for a stored payout method. | +| `webhookUrl` | string | No | URL where payout status updates should be sent. | +| `browserInfo` | BrowserInformation | No | Information about the user's browser. | +| `accessToken` | SecretString | No | Access token for the connector, if required. | +| `sourceBankData` | SourceBankData | No | Details of the bank account from which the payout is funded. | +| `description` | string | No | Description of the payout. | +| `connectorEligibilityReferenceId` | string | No | Connector's reference for a prior eligibility check, when the processor requires it to authorise the transfer. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier passed to the payout processor. | +| `payoutStatus` | PayoutEnums.PayoutStatus | No | The current status of the payout transfer. | +| `connectorPayoutId` | string | No | The unique payout ID assigned by the connector. | +| `error` | ErrorInfo | No | Details of any error that occurred during transfer. | +| `statusCode` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```javascript +const { PayoutClient } = require('hyperswitch-prism'); + +const payoutClient = new PayoutClient({ + connector: 'stripe', + apiKey: '[REDACTED_ENV_SECRET]', + environment: 'SANDBOX' +}); +``` + +### Request + +```javascript +const request = { + amount: { + minorAmount: 1000, + currency: "USD" + }, + destinationCurrency: "USD", + payoutMethodData: { + card: { + cardNumber: "4242424242424242", + cardExpMonth: "12", + cardExpYear: "2027" + } + } +}; + +const response = await payoutClient.transfer(request); +``` + +### Response + +```javascript +{ + payoutStatus: "SUCCESS", + connectorPayoutId: "tr_1Hh1XYZ2eZvKYlo2C", + statusCode: 200 +} +``` + +## Next Steps + +- [Get Payout Status](./get.md) diff --git a/docs-generated/sdks/node/payout-service/void.md b/docs-generated/sdks/node/payout-service/void.md new file mode 100644 index 0000000000..54608d69a4 --- /dev/null +++ b/docs-generated/sdks/node/payout-service/void.md @@ -0,0 +1,85 @@ +# Void Method + + + +## Overview + +The `void` method cancels a payout that has been initiated but not yet completed or transferred. This is used when a payout needs to be stopped before the funds leave the merchant's account. + +## Purpose + +Use this operation to halt a payout process. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Cancel a scheduled payout | Call `void` with the `merchantPayoutId` and required details. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier for the payout. | +| `address` | PayoutAddress | Yes | Address information associated with the payout. | +| `connectorFeatureData` | SecretString | No | Connector-specific metadata or feature configurations. | +| `connectorPayoutId` | string | No | The unique payout ID assigned by the connector. | +| `accessToken` | SecretString | No | Access token for the connector, if required. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier passed to the payout processor. | +| `payoutStatus` | PayoutEnums.PayoutStatus | No | The current status of the payout (e.g., CANCELLED). | +| `connectorPayoutId` | string | No | The unique payout ID assigned by the connector. | +| `error` | ErrorInfo | No | Details of any error that occurred. | +| `statusCode` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```javascript +const { PayoutClient } = require('hyperswitch-prism'); + +const payoutClient = new PayoutClient({ + connector: 'stripe', + apiKey: '[REDACTED_ENV_SECRET]', + environment: 'SANDBOX' +}); +``` + +### Request + +```javascript +const request = { + connectorPayoutId: "po_1Hh1XYZ2eZvKYlo2C" +}; + +const response = await payoutClient.void(request); +``` + +### Response + +```javascript +{ + payoutStatus: "CANCELLED", + connectorPayoutId: "po_1Hh1XYZ2eZvKYlo2C", + statusCode: 200 +} +``` + +## Next Steps + +- [Get Payout Status](./get.md) diff --git a/docs-generated/sdks/php/payout-service/README.md b/docs-generated/sdks/php/payout-service/README.md index 4243bfd523..9fe66ce94a 100644 --- a/docs-generated/sdks/php/payout-service/README.md +++ b/docs-generated/sdks/php/payout-service/README.md @@ -36,6 +36,7 @@ The Payout Service enables you to send funds to recipients using the PHP SDK. Us | [`createLink`](./create-link.md) | Create link between recipient and payout. Associate payout with recipient. | Setting up recipient relationships | | [`createRecipient`](./create-recipient.md) | Create payout recipient. Store recipient bank/payment details. | First time paying a new recipient | | [`enrollDisburseAccount`](./enroll-disburse-account.md) | Enroll disburse account. Set up account for payouts. | Onboarding new payout accounts | +| [`eligibility`](./eligibility.md) | Check payout eligibility before initiating a transfer. | Pre-validating a payee or bank account | ## SDK Setup diff --git a/docs-generated/sdks/php/payout-service/create-link.md b/docs-generated/sdks/php/payout-service/create-link.md new file mode 100644 index 0000000000..2edcdf3243 --- /dev/null +++ b/docs-generated/sdks/php/payout-service/create-link.md @@ -0,0 +1,100 @@ +# createLink Method + + + +## Overview + +The `createLink` method generates a URL that can be sent to a recipient, allowing them to securely provide their own payout method details (like bank account information) to claim the funds. + +## Purpose + +Use this operation when you do not have the recipient's payout method details upfront. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Send funds to a user via email | Call `createLink` and share the generated URL with the user. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier for the payout. | +| `address` | PayoutAddress | Yes | Address information associated with the payout. | +| `connectorFeatureData` | SecretString | No | Connector-specific metadata or feature configurations. | +| `payoutMethodData` | PayoutMethod | No | Specific details of the payout instrument (optional). | +| `connectorQuoteId` | string | No | Pre-negotiated quote ID if applicable. | +| `connectorPayoutId` | string | No | An existing payout identifier from the connector. | +| `amount` | Money | Yes | The amount to be paid out. | +| `destinationCurrency` | Currency | Yes | The currency for the payout. | +| `customer` | Customer | No | Details about the customer. | +| `priority` | PayoutEnums.PayoutPriority | No | Priority of the payout. | +| `connectorPayoutMethodId` | string | No | The connector's unique ID for a stored payout method. | +| `webhookUrl` | string | No | URL where payout status updates should be sent. | +| `browserInfo` | BrowserInformation | No | Information about the user's browser. | +| `accessToken` | SecretString | No | Access token for the connector, if required. | +| `description` | string | No | Description of the payout. | +| `sourceBankData` | SourceBankData | No | Details of the bank account from which the payout is funded. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier passed to the payout processor. | +| `payoutStatus` | PayoutEnums.PayoutStatus | No | The current status of the payout link. | +| `connectorPayoutId` | string | No | The unique payout ID assigned by the connector. | +| `error` | ErrorInfo | No | Details of any error that occurred. | +| `statusCode` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```php +use HyperswitchPrism\PayoutClient; + +$payoutClient = new PayoutClient([ + 'connector' => 'stripe', + 'apiKey' => 'YOUR_API_KEY', + 'environment' => 'SANDBOX' +]); +``` + +### Request + +```php +$request = [ + 'amount' => [ + 'minorAmount' => 1000, + 'currency' => 'USD' + ], + 'destinationCurrency' => 'USD' +]; + +$response = $payoutClient->createLink($request); +``` + +### Response + +```php +[ + 'payoutStatus' => 'REQUIRES_PAYOUT_METHOD_DATA', + 'connectorPayoutId' => 'po_1Hh1XYZ2eZvKYlo2C', + 'statusCode' => 200 +] +``` + +## Next Steps + +- [Get Payout Status](./get.md) diff --git a/docs-generated/sdks/php/payout-service/create-recipient.md b/docs-generated/sdks/php/payout-service/create-recipient.md new file mode 100644 index 0000000000..6f7eb438e0 --- /dev/null +++ b/docs-generated/sdks/php/payout-service/create-recipient.md @@ -0,0 +1,91 @@ +# createRecipient Method + + + +## Overview + +The `createRecipient` method registers a new recipient entity (individual or business) with the payment processor. This is often required before funds can be transferred to them. + +## Purpose + +Use this operation to set up a vendor, contractor, or user in the processor's system. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Onboard a new seller | Call `createRecipient` with their details and `recipientType`. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier for the payout/recipient operation. | +| `address` | PayoutAddress | Yes | Address information associated with the recipient. | +| `payoutMethodData` | PayoutMethod | No | Specific details of the payout instrument for the recipient. | +| `amount` | Money | Yes | The amount to be paid out (if creating recipient inline with a payout). | +| `recipientType` | PayoutEnums.PayoutRecipientType | Yes | Type of entity (e.g., INDIVIDUAL, COMPANY). | +| `customer` | Customer | No | Details about the customer/recipient. | +| `accessToken` | SecretString | No | Access token for the connector, if required. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier passed to the payout processor. | +| `payoutStatus` | PayoutEnums.PayoutStatus | No | The status of the recipient creation. | +| `connectorPayoutId` | string | No | The unique identifier assigned to the recipient or payout. | +| `error` | ErrorInfo | No | Details of any error that occurred. | +| `statusCode` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```php +use HyperswitchPrism\PayoutClient; + +$payoutClient = new PayoutClient([ + 'connector' => 'stripe', + 'apiKey' => 'YOUR_API_KEY', + 'environment' => 'SANDBOX' +]); +``` + +### Request + +```php +$request = [ + 'recipientType' => 'INDIVIDUAL', + 'amount' => [ + 'minorAmount' => 0, + 'currency' => 'USD' + ] +]; + +$response = $payoutClient->createRecipient($request); +``` + +### Response + +```php +[ + 'payoutStatus' => 'SUCCESS', + 'connectorPayoutId' => 'acct_1Hh1XYZ2eZvKYlo2C', + 'statusCode' => 200 +] +``` + +## Next Steps + +- [Transfer Payout](./transfer.md) diff --git a/docs-generated/sdks/php/payout-service/create.md b/docs-generated/sdks/php/payout-service/create.md new file mode 100644 index 0000000000..28b3e464e3 --- /dev/null +++ b/docs-generated/sdks/php/payout-service/create.md @@ -0,0 +1,109 @@ +# create Method + + + +## Overview + +The `create` method in the Payout Service is used to initiate a transfer of funds from your merchant account to a customer, vendor, or third-party entity. This is commonly used in marketplaces, gig-economy platforms, or any business model requiring external disbursements. + +## Purpose + +This operation is the first step in the payout lifecycle. Use this when you have sufficient funds and wish to send money to a registered payout method (like a bank account, card, or wallet). + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Send funds to a vendor | Call `create` with the vendor's bank account details and payout amount. | +| Refund a customer via an alternative method | Provide the customer's wallet or card details in the `payoutMethodData`. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier passed to the payout processor. | +| `address` | PayoutAddress | Yes | The shipping and billing address associated with the payout. | +| `connectorFeatureData` | SecretString | No | Connector-specific metadata or feature configurations. | +| `payoutMethodData` | PayoutMethod | No | Specific details of the payout instrument (e.g., Card, ACH, Pix). | +| `connectorQuoteId` | string | No | Pre-negotiated quote ID if applicable. | +| `connectorPayoutId` | string | No | An existing payout identifier from the connector, if any. | +| `amount` | Money | Yes | The amount to be paid out along with the source currency. | +| `destinationCurrency` | Currency | Yes | The currency in which the recipient will receive the payout. | +| `customer` | Customer | No | Details about the customer receiving the payout. | +| `priority` | PayoutEnums.PayoutPriority | No | Priority of the payout (e.g., INSTANT, WIRE). | +| `connectorPayoutMethodId` | string | No | The connector's unique ID for a stored payout method. | +| `webhookUrl` | string | No | URL where payout status updates should be sent. | +| `browserInfo` | BrowserInformation | No | Information about the user's browser, used for fraud prevention. | +| `accessToken` | SecretString | No | Access token for the connector, if required. | +| `sourceBankData` | SourceBankData | No | Details of the bank account from which the payout is funded. | +| `description` | string | No | Description of the payout. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier passed to the payout processor. | +| `payoutStatus` | PayoutEnums.PayoutStatus | No | The current status of the payout (e.g., PENDING, SUCCESS). | +| `connectorPayoutId` | string | No | The unique payout ID assigned by the connector. | +| `error` | ErrorInfo | No | Details of any error that occurred during creation. | +| `statusCode` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```php +use HyperswitchPrism\PayoutClient; + +$payoutClient = new PayoutClient([ + 'connector' => 'stripe', + 'apiKey' => 'YOUR_API_KEY', + 'environment' => 'SANDBOX' +]); +``` + +### Request + +```php +$request = [ + 'amount' => [ + 'minorAmount' => 1000, + 'currency' => 'USD' + ], + 'destinationCurrency' => 'USD', + 'payoutMethodData' => [ + 'card' => [ + 'cardNumber' => '4242424242424242', + 'cardExpMonth' => '12', + 'cardExpYear' => '2027' + ] + ] +]; + +$response = $payoutClient->create($request); +``` + +### Response + +```php +[ + 'payoutStatus' => 'PENDING', + 'connectorPayoutId' => 'po_1Hh1XYZ2eZvKYlo2C', + 'statusCode' => 200 +] +``` + +## Next Steps + +- [Get Payout Status](./get.md) +- [Void Payout](./void.md) diff --git a/docs-generated/sdks/php/payout-service/eligibility.md b/docs-generated/sdks/php/payout-service/eligibility.md new file mode 100644 index 0000000000..a8bbe59563 --- /dev/null +++ b/docs-generated/sdks/php/payout-service/eligibility.md @@ -0,0 +1,105 @@ +# Eligibility Method + + + +## Overview + +The `eligibility` method in the Payout Service checks whether a payout can be made to a given payout method before funds are committed. It is used for pre-verification flows such as SEPA Verification of Payee (VoP) and other payee-verification checks. + +## Purpose + +Use this operation to validate a payee or payout method before initiating a transfer, reducing the risk of failed or misdirected payouts. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Verify a payee before transfer | Call `eligibility` with the payout method and amount, then inspect `payoutEligible`. | +| Pre-check a bank account | Call `eligibility` with the bank details to confirm the account can receive funds. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier for the eligibility check. | +| `connectorFeatureData` | SecretString | No | Connector-specific metadata passed to the eligibility check. | +| `payoutMethodData` | PayoutMethod | No | Specific details of the payout instrument being checked. | +| `amount` | Money | Yes | The amount to be paid out. | +| `connectorPayoutId` | string | No | An existing payout identifier from the connector. | +| `destinationCurrency` | Currency | Yes | The currency in which the recipient will receive the payout. | +| `accessToken` | SecretString | No | Access token for the connector, if required. | +| `address` | PayoutAddress | No | Address information associated with the payout. | +| `customer` | Customer | No | Details about the customer receiving the payout. | +| `sourceBankData` | SourceBankData | No | Details of the bank account from which the payout is funded. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier for the eligibility check. | +| `payoutStatus` | PayoutEnums.PayoutStatus | No | The current status of the payout. | +| `connectorPayoutId` | string | No | The unique payout ID assigned by the connector (set only when the payee is eligible). | +| `error` | ErrorInfo | No | Details of any error that occurred during the check. | +| `statusCode` | uint32 | Yes | The HTTP status code returned from the connector. | +| `payoutEligible` | bool | No | Whether the payout is eligible. | +| `connectorMetadata` | SecretString | No | Connector-specific details as a JSON object, surfaced to the merchant. | +| `connectorEligibilityReferenceId` | string | No | Connector's reference for the eligibility check itself. Set for every verdict so the check remains traceable for reconciliation. | + +## Example + +### SDK Setup + +```php +use HyperswitchPrism\PayoutClient; + +$payoutClient = new PayoutClient([ + 'connector' => 'stripe', + 'apiKey' => 'YOUR_API_KEY', + 'environment' => 'SANDBOX' +]); +``` + +### Request + +```php +$request = [ + 'amount' => [ + 'minorAmount' => 1000, + 'currency' => 'USD' + ], + 'destinationCurrency' => 'USD', + 'payoutMethodData' => [ + 'ach' => [ + 'bankAccountNumber' => '000123456789', + 'bankRoutingNumber' => '110000000' + ] + ] +]; + +$response = $payoutClient->eligibility($request); +``` + +### Response + +```php +[ + 'payoutEligible' => true, + 'connectorEligibilityReferenceId' => 'elig_1Hh1XYZ2eZvKYlo2C', + 'statusCode' => 200 +] +``` + +## Next Steps + +- [Transfer Payout](./transfer.md) +- [Create Payout](./create.md) diff --git a/docs-generated/sdks/php/payout-service/enroll-disburse-account.md b/docs-generated/sdks/php/payout-service/enroll-disburse-account.md new file mode 100644 index 0000000000..f5839fc610 --- /dev/null +++ b/docs-generated/sdks/php/payout-service/enroll-disburse-account.md @@ -0,0 +1,95 @@ +# enrollDisburseAccount Method + + + +## Overview + +The `enrollDisburseAccount` method registers and verifies a destination account (like a bank account) to receive disbursements from your platform. + +## Purpose + +Use this operation to securely enroll payout destinations. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Link a vendor's bank account | Call `enrollDisburseAccount` with their bank details. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier for the operation. | +| `address` | PayoutAddress | Yes | Address information associated with the account. | +| `payoutMethodData` | PayoutMethod | No | Specific details of the payout instrument being enrolled. | +| `amount` | Money | Yes | The amount to be paid out (if enrolling inline). | +| `customer` | Customer | No | Details about the customer/account holder. | +| `accessToken` | SecretString | No | Access token for the connector, if required. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier. | +| `payoutStatus` | PayoutEnums.PayoutStatus | No | The status of the enrollment. | +| `connectorPayoutId` | string | No | The unique identifier assigned by the connector. | +| `error` | ErrorInfo | No | Details of any error that occurred. | +| `statusCode` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```php +use HyperswitchPrism\PayoutClient; + +$payoutClient = new PayoutClient([ + 'connector' => 'stripe', + 'apiKey' => 'YOUR_API_KEY', + 'environment' => 'SANDBOX' +]); +``` + +### Request + +```php +$request = [ + 'amount' => [ + 'minorAmount' => 0, + 'currency' => 'USD' + ], + 'payoutMethodData' => [ + 'ach' => [ + 'bankAccountNumber' => '000123456789', + 'bankRoutingNumber' => '110000000' + ] + ] +]; + +$response = $payoutClient->enrollDisburseAccount($request); +``` + +### Response + +```php +[ + 'payoutStatus' => 'SUCCESS', + 'connectorPayoutId' => 'ba_1Hh1XYZ2eZvKYlo2C', + 'statusCode' => 200 +] +``` + +## Next Steps + +- [Transfer Payout](./transfer.md) diff --git a/docs-generated/sdks/php/payout-service/get.md b/docs-generated/sdks/php/payout-service/get.md new file mode 100644 index 0000000000..afe407d34d --- /dev/null +++ b/docs-generated/sdks/php/payout-service/get.md @@ -0,0 +1,85 @@ +# get Method + + + +## Overview + +The `get` method allows you to check the current status of a payout. This is essential for syncing your internal systems with the payment processor's state, especially for asynchronous payout methods like bank transfers. + +## Purpose + +Use this operation to poll for the status of a payout or to verify details before taking further action. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Check if a payout succeeded | Call `get` with the `merchantPayoutId` or `connectorPayoutId`. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier for the payout. | +| `connectorPayoutId` | string | No | The unique payout ID assigned by the connector. | +| `accessToken` | SecretString | No | Access token for the connector, if required. | +| `sourceBankData` | SourceBankData | No | Source (debtor) bank data. Some connectors (e.g. Deutsche Bank) require the debtor account details to perform a status enquiry. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier passed to the payout processor. | +| `payoutStatus` | PayoutEnums.PayoutStatus | No | The current status of the payout. | +| `connectorPayoutId` | string | No | The unique payout ID assigned by the connector. | +| `error` | ErrorInfo | No | Details of any error that occurred. | +| `statusCode` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```php +use HyperswitchPrism\PayoutClient; + +$payoutClient = new PayoutClient([ + 'connector' => 'stripe', + 'apiKey' => 'YOUR_API_KEY', + 'environment' => 'SANDBOX' +]); +``` + +### Request + +```php +$request = [ + 'merchantPayoutId' => 'po_internal_12345' +]; + +$response = $payoutClient->get($request); +``` + +### Response + +```php +[ + 'merchantPayoutId' => 'po_internal_12345', + 'payoutStatus' => 'SUCCESS', + 'connectorPayoutId' => 'po_1Hh1XYZ2eZvKYlo2C', + 'statusCode' => 200 +] +``` + +## Next Steps + +- [Void Payout](./void.md) diff --git a/docs-generated/sdks/php/payout-service/stage.md b/docs-generated/sdks/php/payout-service/stage.md new file mode 100644 index 0000000000..9b1590458e --- /dev/null +++ b/docs-generated/sdks/php/payout-service/stage.md @@ -0,0 +1,92 @@ +# stage Method + + + +## Overview + +The `stage` method allows you to prepare or stage a payout before it is executed. This can be used in workflows where payouts are reviewed or batched before final transfer. + +## Purpose + +Use this operation to put a payout into a staged state. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Prepare a payout for review | Call `stage` with the quote details and amount. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantQuoteId` | string | No | Your internal quote identifier. | +| `address` | PayoutAddress | Yes | Address information associated with the payout. | +| `amount` | Money | Yes | The amount to be staged. | +| `destinationCurrency` | Currency | Yes | The currency in which the payout will be received. | +| `customer` | Customer | No | Details about the customer. | +| `browserInfo` | BrowserInformation | No | Information about the user's browser. | +| `accessToken` | SecretString | No | Access token for the connector, if required. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier passed to the payout processor. | +| `payoutStatus` | PayoutEnums.PayoutStatus | No | The current status of the staged payout. | +| `connectorPayoutId` | string | No | The unique payout ID assigned by the connector. | +| `error` | ErrorInfo | No | Details of any error that occurred. | +| `statusCode` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```php +use HyperswitchPrism\PayoutClient; + +$payoutClient = new PayoutClient([ + 'connector' => 'stripe', + 'apiKey' => 'YOUR_API_KEY', + 'environment' => 'SANDBOX' +]); +``` + +### Request + +```php +$request = [ + 'merchantQuoteId' => 'quote_123', + 'amount' => [ + 'minorAmount' => 1000, + 'currency' => 'USD' + ], + 'destinationCurrency' => 'USD' +]; + +$response = $payoutClient->stage($request); +``` + +### Response + +```php +[ + 'payoutStatus' => 'PENDING', + 'connectorPayoutId' => 'po_1Hh1XYZ2eZvKYlo2C', + 'statusCode' => 200 +] +``` + +## Next Steps + +- [Transfer Payout](./transfer.md) diff --git a/docs-generated/sdks/php/payout-service/transfer.md b/docs-generated/sdks/php/payout-service/transfer.md new file mode 100644 index 0000000000..b62a6e3ccf --- /dev/null +++ b/docs-generated/sdks/php/payout-service/transfer.md @@ -0,0 +1,107 @@ +# transfer Method + + + +## Overview + +The `transfer` method in the Payout Service is used to execute the actual fund transfer for a previously initiated payout, or to perform a combined create and transfer operation depending on the processor's flow. + +## Purpose + +Use this operation to move funds to the destination payout method. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Execute a payout transfer | Call `transfer` with the required payout details and amount. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier passed to the payout processor. | +| `address` | PayoutAddress | Yes | Address information associated with the payout. | +| `payoutMethodData` | PayoutMethod | No | Specific details of the payout instrument. | +| `connectorQuoteId` | string | No | Pre-negotiated quote ID if applicable. | +| `amount` | Money | Yes | The amount to be transferred. | +| `connectorPayoutId` | string | No | An existing payout identifier from the connector. | +| `destinationCurrency` | Currency | Yes | The currency in which the recipient will receive the payout. | +| `customer` | Customer | No | Details about the customer receiving the payout. | +| `priority` | PayoutEnums.PayoutPriority | No | Priority of the payout transfer. | +| `connectorPayoutMethodId` | string | No | The connector's unique ID for a stored payout method. | +| `webhookUrl` | string | No | URL where payout status updates should be sent. | +| `browserInfo` | BrowserInformation | No | Information about the user's browser. | +| `accessToken` | SecretString | No | Access token for the connector, if required. | +| `sourceBankData` | SourceBankData | No | Details of the bank account from which the payout is funded. | +| `description` | string | No | Description of the payout. | +| `connectorEligibilityReferenceId` | string | No | Connector's reference for a prior eligibility check, when the processor requires it to authorise the transfer. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier passed to the payout processor. | +| `payoutStatus` | PayoutEnums.PayoutStatus | No | The current status of the payout transfer. | +| `connectorPayoutId` | string | No | The unique payout ID assigned by the connector. | +| `error` | ErrorInfo | No | Details of any error that occurred during transfer. | +| `statusCode` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```php +use HyperswitchPrism\PayoutClient; + +$payoutClient = new PayoutClient([ + 'connector' => 'stripe', + 'apiKey' => 'YOUR_API_KEY', + 'environment' => 'SANDBOX' +]); +``` + +### Request + +```php +$request = [ + 'amount' => [ + 'minorAmount' => 1000, + 'currency' => 'USD' + ], + 'destinationCurrency' => 'USD', + 'payoutMethodData' => [ + 'card' => [ + 'cardNumber' => '4242424242424242', + 'cardExpMonth' => '12', + 'cardExpYear' => '2027' + ] + ] +]; + +$response = $payoutClient->transfer($request); +``` + +### Response + +```php +[ + 'payoutStatus' => 'SUCCESS', + 'connectorPayoutId' => 'tr_1Hh1XYZ2eZvKYlo2C', + 'statusCode' => 200 +] +``` + +## Next Steps + +- [Get Payout Status](./get.md) diff --git a/docs-generated/sdks/php/payout-service/void.md b/docs-generated/sdks/php/payout-service/void.md new file mode 100644 index 0000000000..bd577e1b87 --- /dev/null +++ b/docs-generated/sdks/php/payout-service/void.md @@ -0,0 +1,85 @@ +# void Method + + + +## Overview + +The `void` method cancels a payout that has been initiated but not yet completed or transferred. This is used when a payout needs to be stopped before the funds leave the merchant's account. + +## Purpose + +Use this operation to halt a payout process. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Cancel a scheduled payout | Call `void` with the `merchantPayoutId` and required details. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier for the payout. | +| `address` | PayoutAddress | Yes | Address information associated with the payout. | +| `connectorFeatureData` | SecretString | No | Connector-specific metadata or feature configurations. | +| `connectorPayoutId` | string | No | The unique payout ID assigned by the connector. | +| `accessToken` | SecretString | No | Access token for the connector, if required. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchantPayoutId` | string | No | Your internal identifier passed to the payout processor. | +| `payoutStatus` | PayoutEnums.PayoutStatus | No | The current status of the payout (e.g., CANCELLED). | +| `connectorPayoutId` | string | No | The unique payout ID assigned by the connector. | +| `error` | ErrorInfo | No | Details of any error that occurred. | +| `statusCode` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```php +use HyperswitchPrism\PayoutClient; + +$payoutClient = new PayoutClient([ + 'connector' => 'stripe', + 'apiKey' => 'YOUR_API_KEY', + 'environment' => 'SANDBOX' +]); +``` + +### Request + +```php +$request = [ + 'connectorPayoutId' => 'po_1Hh1XYZ2eZvKYlo2C' +]; + +$response = $payoutClient->void($request); +``` + +### Response + +```php +[ + 'payoutStatus' => 'CANCELLED', + 'connectorPayoutId' => 'po_1Hh1XYZ2eZvKYlo2C', + 'statusCode' => 200 +] +``` + +## Next Steps + +- [Get Payout Status](./get.md) diff --git a/docs-generated/sdks/python/payout-service/README.md b/docs-generated/sdks/python/payout-service/README.md index 6444f4df95..42b2773310 100644 --- a/docs-generated/sdks/python/payout-service/README.md +++ b/docs-generated/sdks/python/payout-service/README.md @@ -36,6 +36,7 @@ The Payout Service enables you to send funds to recipients using the Python SDK. | [`create_link`](./create-link.md) | Create link between recipient and payout. Associate payout with recipient. | Setting up recipient relationships | | [`create_recipient`](./create-recipient.md) | Create payout recipient. Store recipient bank/payment details. | First time paying a new recipient | | [`enroll_disburse_account`](./enroll-disburse-account.md) | Enroll disburse account. Set up account for payouts. | Onboarding new payout accounts | +| [`eligibility`](./eligibility.md) | Check payout eligibility before initiating a transfer. | Pre-validating a payee or bank account | ## SDK Setup diff --git a/docs-generated/sdks/python/payout-service/create-link.md b/docs-generated/sdks/python/payout-service/create-link.md new file mode 100644 index 0000000000..5793bd1837 --- /dev/null +++ b/docs-generated/sdks/python/payout-service/create-link.md @@ -0,0 +1,100 @@ +# create_link Method + + + +## Overview + +The `create_link` method generates a URL that can be sent to a recipient, allowing them to securely provide their own payout method details (like bank account information) to claim the funds. + +## Purpose + +Use this operation when you do not have the recipient's payout method details upfront. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Send funds to a user via email | Call `create_link` and share the generated URL with the user. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchant_payout_id` | string | No | Your internal identifier for the payout. | +| `address` | PayoutAddress | Yes | Address information associated with the payout. | +| `connector_feature_data` | SecretString | No | Connector-specific metadata or feature configurations. | +| `payout_method_data` | PayoutMethod | No | Specific details of the payout instrument (optional). | +| `connector_quote_id` | string | No | Pre-negotiated quote ID if applicable. | +| `connector_payout_id` | string | No | An existing payout identifier from the connector. | +| `amount` | Money | Yes | The amount to be paid out. | +| `destination_currency` | Currency | Yes | The currency for the payout. | +| `customer` | Customer | No | Details about the customer. | +| `priority` | PayoutEnums.PayoutPriority | No | Priority of the payout. | +| `connector_payout_method_id` | string | No | The connector's unique ID for a stored payout method. | +| `webhook_url` | string | No | URL where payout status updates should be sent. | +| `browser_info` | BrowserInformation | No | Information about the user's browser. | +| `access_token` | SecretString | No | Access token for the connector, if required. | +| `description` | string | No | Description of the payout. | +| `source_bank_data` | SourceBankData | No | Details of the bank account from which the payout is funded. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchant_payout_id` | string | No | Your internal identifier passed to the payout processor. | +| `payout_status` | PayoutEnums.PayoutStatus | No | The current status of the payout link. | +| `connector_payout_id` | string | No | The unique payout ID assigned by the connector. | +| `error` | ErrorInfo | No | Details of any error that occurred. | +| `status_code` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```python +from hyperswitch_prism import PayoutClient + +payout_client = PayoutClient( + connector='stripe', + api_key='[REDACTED_ENV_SECRET]', + environment='SANDBOX' +) +``` + +### Request + +```python +request = { + "amount": { + "minor_amount": 1000, + "currency": "USD" + }, + "destination_currency": "USD" +} + +response = await payout_client.create_link(request) +``` + +### Response + +```python +{ + "payout_status": "REQUIRES_PAYOUT_METHOD_DATA", + "connector_payout_id": "po_1Hh1XYZ2eZvKYlo2C", + "status_code": 200 +} +``` + +## Next Steps + +- [Get Payout Status](./get.md) diff --git a/docs-generated/sdks/python/payout-service/create-recipient.md b/docs-generated/sdks/python/payout-service/create-recipient.md new file mode 100644 index 0000000000..68912e457e --- /dev/null +++ b/docs-generated/sdks/python/payout-service/create-recipient.md @@ -0,0 +1,91 @@ +# create_recipient Method + + + +## Overview + +The `create_recipient` method registers a new recipient entity (individual or business) with the payment processor. This is often required before funds can be transferred to them. + +## Purpose + +Use this operation to set up a vendor, contractor, or user in the processor's system. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Onboard a new seller | Call `create_recipient` with their details and `recipient_type`. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchant_payout_id` | string | No | Your internal identifier for the payout/recipient operation. | +| `address` | PayoutAddress | Yes | Address information associated with the recipient. | +| `payout_method_data` | PayoutMethod | No | Specific details of the payout instrument for the recipient. | +| `amount` | Money | Yes | The amount to be paid out (if creating recipient inline with a payout). | +| `recipient_type` | PayoutEnums.PayoutRecipientType | Yes | Type of entity (e.g., INDIVIDUAL, COMPANY). | +| `customer` | Customer | No | Details about the customer/recipient. | +| `access_token` | SecretString | No | Access token for the connector, if required. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchant_payout_id` | string | No | Your internal identifier passed to the payout processor. | +| `payout_status` | PayoutEnums.PayoutStatus | No | The status of the recipient creation. | +| `connector_payout_id` | string | No | The unique identifier assigned to the recipient or payout. | +| `error` | ErrorInfo | No | Details of any error that occurred. | +| `status_code` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```python +from hyperswitch_prism import PayoutClient + +payout_client = PayoutClient( + connector='stripe', + api_key='[REDACTED_ENV_SECRET]', + environment='SANDBOX' +) +``` + +### Request + +```python +request = { + "recipient_type": "INDIVIDUAL", + "amount": { + "minor_amount": 0, + "currency": "USD" + } +} + +response = await payout_client.create_recipient(request) +``` + +### Response + +```python +{ + "payout_status": "SUCCESS", + "connector_payout_id": "acct_1Hh1XYZ2eZvKYlo2C", + "status_code": 200 +} +``` + +## Next Steps + +- [Transfer Payout](./transfer.md) diff --git a/docs-generated/sdks/python/payout-service/create.md b/docs-generated/sdks/python/payout-service/create.md new file mode 100644 index 0000000000..dd50decb5e --- /dev/null +++ b/docs-generated/sdks/python/payout-service/create.md @@ -0,0 +1,109 @@ +# create Method + + + +## Overview + +The `create` method in the Payout Service is used to initiate a transfer of funds from your merchant account to a customer, vendor, or third-party entity. This is commonly used in marketplaces, gig-economy platforms, or any business model requiring external disbursements. + +## Purpose + +This operation is the first step in the payout lifecycle. Use this when you have sufficient funds and wish to send money to a registered payout method (like a bank account, card, or wallet). + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Send funds to a vendor | Call `create` with the vendor's bank account details and payout amount. | +| Refund a customer via an alternative method | Provide the customer's wallet or card details in the `payout_method_data`. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchant_payout_id` | string | No | Your internal identifier passed to the payout processor. | +| `address` | PayoutAddress | Yes | The shipping and billing address associated with the payout. | +| `connector_feature_data` | SecretString | No | Connector-specific metadata or feature configurations. | +| `payout_method_data` | PayoutMethod | No | Specific details of the payout instrument (e.g., Card, ACH, Pix). | +| `connector_quote_id` | string | No | Pre-negotiated quote ID if applicable. | +| `connector_payout_id` | string | No | An existing payout identifier from the connector, if any. | +| `amount` | Money | Yes | The amount to be paid out along with the source currency. | +| `destination_currency` | Currency | Yes | The currency in which the recipient will receive the payout. | +| `customer` | Customer | No | Details about the customer receiving the payout. | +| `priority` | PayoutEnums.PayoutPriority | No | Priority of the payout (e.g., INSTANT, WIRE). | +| `connector_payout_method_id` | string | No | The connector's unique ID for a stored payout method. | +| `webhook_url` | string | No | URL where payout status updates should be sent. | +| `browser_info` | BrowserInformation | No | Information about the user's browser, used for fraud prevention. | +| `access_token` | SecretString | No | Access token for the connector, if required. | +| `source_bank_data` | SourceBankData | No | Details of the bank account from which the payout is funded. | +| `description` | string | No | Description of the payout. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchant_payout_id` | string | No | Your internal identifier passed to the payout processor. | +| `payout_status` | PayoutEnums.PayoutStatus | No | The current status of the payout (e.g., PENDING, SUCCESS). | +| `connector_payout_id` | string | No | The unique payout ID assigned by the connector. | +| `error` | ErrorInfo | No | Details of any error that occurred during creation. | +| `status_code` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```python +from hyperswitch_prism import PayoutClient + +payout_client = PayoutClient( + connector='stripe', + api_key='[REDACTED_ENV_SECRET]', + environment='SANDBOX' +) +``` + +### Request + +```python +request = { + "amount": { + "minor_amount": 1000, + "currency": "USD" + }, + "destination_currency": "USD", + "payout_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "12", + "card_exp_year": "2027" + } + } +} + +response = await payout_client.create(request) +``` + +### Response + +```python +{ + "payout_status": "PENDING", + "connector_payout_id": "po_1Hh1XYZ2eZvKYlo2C", + "status_code": 200 +} +``` + +## Next Steps + +- [Get Payout Status](./get.md) +- [Void Payout](./void.md) diff --git a/docs-generated/sdks/python/payout-service/eligibility.md b/docs-generated/sdks/python/payout-service/eligibility.md new file mode 100644 index 0000000000..c8fbe86076 --- /dev/null +++ b/docs-generated/sdks/python/payout-service/eligibility.md @@ -0,0 +1,105 @@ +# Eligibility Method + + + +## Overview + +The `eligibility` method in the Payout Service checks whether a payout can be made to a given payout method before funds are committed. It is used for pre-verification flows such as SEPA Verification of Payee (VoP) and other payee-verification checks. + +## Purpose + +Use this operation to validate a payee or payout method before initiating a transfer, reducing the risk of failed or misdirected payouts. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Verify a payee before transfer | Call `eligibility` with the payout method and amount, then inspect `payout_eligible`. | +| Pre-check a bank account | Call `eligibility` with the bank details to confirm the account can receive funds. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchant_payout_id` | string | No | Your internal identifier for the eligibility check. | +| `connector_feature_data` | SecretString | No | Connector-specific metadata passed to the eligibility check. | +| `payout_method_data` | PayoutMethod | No | Specific details of the payout instrument being checked. | +| `amount` | Money | Yes | The amount to be paid out. | +| `connector_payout_id` | string | No | An existing payout identifier from the connector. | +| `destination_currency` | Currency | Yes | The currency in which the recipient will receive the payout. | +| `access_token` | SecretString | No | Access token for the connector, if required. | +| `address` | PayoutAddress | No | Address information associated with the payout. | +| `customer` | Customer | No | Details about the customer receiving the payout. | +| `source_bank_data` | SourceBankData | No | Details of the bank account from which the payout is funded. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchant_payout_id` | string | No | Your internal identifier for the eligibility check. | +| `payout_status` | PayoutEnums.PayoutStatus | No | The current status of the payout. | +| `connector_payout_id` | string | No | The unique payout ID assigned by the connector (set only when the payee is eligible). | +| `error` | ErrorInfo | No | Details of any error that occurred during the check. | +| `status_code` | uint32 | Yes | The HTTP status code returned from the connector. | +| `payout_eligible` | bool | No | Whether the payout is eligible. | +| `connector_metadata` | SecretString | No | Connector-specific details as a JSON object, surfaced to the merchant. | +| `connector_eligibility_reference_id` | string | No | Connector's reference for the eligibility check itself. Set for every verdict so the check remains traceable for reconciliation. | + +## Example + +### SDK Setup + +```python +from hyperswitch_prism import PayoutClient + +payout_client = PayoutClient( + connector='stripe', + api_key='YOUR_API_KEY', + environment='SANDBOX' +) +``` + +### Request + +```python +request = { + "amount": { + "minor_amount": 1000, + "currency": "USD" + }, + "destination_currency": "USD", + "payout_method_data": { + "ach": { + "bank_account_number": "000123456789", + "bank_routing_number": "110000000" + } + } +} + +response = await payout_client.eligibility(request) +``` + +### Response + +```python +{ + "payout_eligible": True, + "connector_eligibility_reference_id": "elig_1Hh1XYZ2eZvKYlo2C", + "status_code": 200 +} +``` + +## Next Steps + +- [Transfer Payout](./transfer.md) +- [Create Payout](./create.md) diff --git a/docs-generated/sdks/python/payout-service/enroll-disburse-account.md b/docs-generated/sdks/python/payout-service/enroll-disburse-account.md new file mode 100644 index 0000000000..9f5cc3fc91 --- /dev/null +++ b/docs-generated/sdks/python/payout-service/enroll-disburse-account.md @@ -0,0 +1,95 @@ +# enroll_disburse_account Method + + + +## Overview + +The `enroll_disburse_account` method registers and verifies a destination account (like a bank account) to receive disbursements from your platform. + +## Purpose + +Use this operation to securely enroll payout destinations. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Link a vendor's bank account | Call `enroll_disburse_account` with their bank details. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchant_payout_id` | string | No | Your internal identifier for the operation. | +| `address` | PayoutAddress | Yes | Address information associated with the account. | +| `payout_method_data` | PayoutMethod | No | Specific details of the payout instrument being enrolled. | +| `amount` | Money | Yes | The amount to be paid out (if enrolling inline). | +| `customer` | Customer | No | Details about the customer/account holder. | +| `access_token` | SecretString | No | Access token for the connector, if required. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchant_payout_id` | string | No | Your internal identifier. | +| `payout_status` | PayoutEnums.PayoutStatus | No | The status of the enrollment. | +| `connector_payout_id` | string | No | The unique identifier assigned by the connector. | +| `error` | ErrorInfo | No | Details of any error that occurred. | +| `status_code` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```python +from hyperswitch_prism import PayoutClient + +payout_client = PayoutClient( + connector='stripe', + api_key='[REDACTED_ENV_SECRET]', + environment='SANDBOX' +) +``` + +### Request + +```python +request = { + "amount": { + "minor_amount": 0, + "currency": "USD" + }, + "payout_method_data": { + "ach": { + "bank_account_number": "000123456789", + "bank_routing_number": "110000000" + } + } +} + +response = await payout_client.enroll_disburse_account(request) +``` + +### Response + +```python +{ + "payout_status": "SUCCESS", + "connector_payout_id": "ba_1Hh1XYZ2eZvKYlo2C", + "status_code": 200 +} +``` + +## Next Steps + +- [Transfer Payout](./transfer.md) diff --git a/docs-generated/sdks/python/payout-service/get.md b/docs-generated/sdks/python/payout-service/get.md new file mode 100644 index 0000000000..2f4099b11c --- /dev/null +++ b/docs-generated/sdks/python/payout-service/get.md @@ -0,0 +1,85 @@ +# get Method + + + +## Overview + +The `get` method allows you to check the current status of a payout. This is essential for syncing your internal systems with the payment processor's state, especially for asynchronous payout methods like bank transfers. + +## Purpose + +Use this operation to poll for the status of a payout or to verify details before taking further action. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Check if a payout succeeded | Call `get` with the `merchant_payout_id` or `connector_payout_id`. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchant_payout_id` | string | No | Your internal identifier for the payout. | +| `connector_payout_id` | string | No | The unique payout ID assigned by the connector. | +| `access_token` | SecretString | No | Access token for the connector, if required. | +| `source_bank_data` | SourceBankData | No | Source (debtor) bank data. Some connectors (e.g. Deutsche Bank) require the debtor account details to perform a status enquiry. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchant_payout_id` | string | No | Your internal identifier passed to the payout processor. | +| `payout_status` | PayoutEnums.PayoutStatus | No | The current status of the payout. | +| `connector_payout_id` | string | No | The unique payout ID assigned by the connector. | +| `error` | ErrorInfo | No | Details of any error that occurred. | +| `status_code` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```python +from hyperswitch_prism import PayoutClient + +payout_client = PayoutClient( + connector='stripe', + api_key='[REDACTED_ENV_SECRET]', + environment='SANDBOX' +) +``` + +### Request + +```python +request = { + "merchant_payout_id": "po_internal_12345" +} + +response = await payout_client.get(request) +``` + +### Response + +```python +{ + "merchant_payout_id": "po_internal_12345", + "payout_status": "SUCCESS", + "connector_payout_id": "po_1Hh1XYZ2eZvKYlo2C", + "status_code": 200 +} +``` + +## Next Steps + +- [Void Payout](./void.md) diff --git a/docs-generated/sdks/python/payout-service/stage.md b/docs-generated/sdks/python/payout-service/stage.md new file mode 100644 index 0000000000..3a70ce93f4 --- /dev/null +++ b/docs-generated/sdks/python/payout-service/stage.md @@ -0,0 +1,92 @@ +# stage Method + + + +## Overview + +The `stage` method allows you to prepare or stage a payout before it is executed. This can be used in workflows where payouts are reviewed or batched before final transfer. + +## Purpose + +Use this operation to put a payout into a staged state. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Prepare a payout for review | Call `stage` with the quote details and amount. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchant_quote_id` | string | No | Your internal quote identifier. | +| `address` | PayoutAddress | Yes | Address information associated with the payout. | +| `amount` | Money | Yes | The amount to be staged. | +| `destination_currency` | Currency | Yes | The currency in which the payout will be received. | +| `customer` | Customer | No | Details about the customer. | +| `browser_info` | BrowserInformation | No | Information about the user's browser. | +| `access_token` | SecretString | No | Access token for the connector, if required. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchant_payout_id` | string | No | Your internal identifier passed to the payout processor. | +| `payout_status` | PayoutEnums.PayoutStatus | No | The current status of the staged payout. | +| `connector_payout_id` | string | No | The unique payout ID assigned by the connector. | +| `error` | ErrorInfo | No | Details of any error that occurred. | +| `status_code` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```python +from hyperswitch_prism import PayoutClient + +payout_client = PayoutClient( + connector='stripe', + api_key='[REDACTED_ENV_SECRET]', + environment='SANDBOX' +) +``` + +### Request + +```python +request = { + "merchant_quote_id": "quote_123", + "amount": { + "minor_amount": 1000, + "currency": "USD" + }, + "destination_currency": "USD" +} + +response = await payout_client.stage(request) +``` + +### Response + +```python +{ + "payout_status": "PENDING", + "connector_payout_id": "po_1Hh1XYZ2eZvKYlo2C", + "status_code": 200 +} +``` + +## Next Steps + +- [Transfer Payout](./transfer.md) diff --git a/docs-generated/sdks/python/payout-service/transfer.md b/docs-generated/sdks/python/payout-service/transfer.md new file mode 100644 index 0000000000..1651b1bd93 --- /dev/null +++ b/docs-generated/sdks/python/payout-service/transfer.md @@ -0,0 +1,107 @@ +# transfer Method + + + +## Overview + +The `transfer` method in the Payout Service is used to execute the actual fund transfer for a previously initiated payout, or to perform a combined create and transfer operation depending on the processor's flow. + +## Purpose + +Use this operation to move funds to the destination payout method. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Execute a payout transfer | Call `transfer` with the required payout details and amount. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchant_payout_id` | string | No | Your internal identifier passed to the payout processor. | +| `address` | PayoutAddress | Yes | Address information associated with the payout. | +| `payout_method_data` | PayoutMethod | No | Specific details of the payout instrument. | +| `connector_quote_id` | string | No | Pre-negotiated quote ID if applicable. | +| `amount` | Money | Yes | The amount to be transferred. | +| `connector_payout_id` | string | No | An existing payout identifier from the connector. | +| `destination_currency` | Currency | Yes | The currency in which the recipient will receive the payout. | +| `customer` | Customer | No | Details about the customer receiving the payout. | +| `priority` | PayoutEnums.PayoutPriority | No | Priority of the payout transfer. | +| `connector_payout_method_id` | string | No | The connector's unique ID for a stored payout method. | +| `webhook_url` | string | No | URL where payout status updates should be sent. | +| `browser_info` | BrowserInformation | No | Information about the user's browser. | +| `access_token` | SecretString | No | Access token for the connector, if required. | +| `source_bank_data` | SourceBankData | No | Details of the bank account from which the payout is funded. | +| `description` | string | No | Description of the payout. | +| `connector_eligibility_reference_id` | string | No | Connector's reference for a prior eligibility check, when the processor requires it to authorise the transfer. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchant_payout_id` | string | No | Your internal identifier passed to the payout processor. | +| `payout_status` | PayoutEnums.PayoutStatus | No | The current status of the payout transfer. | +| `connector_payout_id` | string | No | The unique payout ID assigned by the connector. | +| `error` | ErrorInfo | No | Details of any error that occurred during transfer. | +| `status_code` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```python +from hyperswitch_prism import PayoutClient + +payout_client = PayoutClient( + connector='stripe', + api_key='[REDACTED_ENV_SECRET]', + environment='SANDBOX' +) +``` + +### Request + +```python +request = { + "amount": { + "minor_amount": 1000, + "currency": "USD" + }, + "destination_currency": "USD", + "payout_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "12", + "card_exp_year": "2027" + } + } +} + +response = await payout_client.transfer(request) +``` + +### Response + +```python +{ + "payout_status": "SUCCESS", + "connector_payout_id": "tr_1Hh1XYZ2eZvKYlo2C", + "status_code": 200 +} +``` + +## Next Steps + +- [Get Payout Status](./get.md) diff --git a/docs-generated/sdks/python/payout-service/void.md b/docs-generated/sdks/python/payout-service/void.md new file mode 100644 index 0000000000..9140010fe3 --- /dev/null +++ b/docs-generated/sdks/python/payout-service/void.md @@ -0,0 +1,85 @@ +# void Method + + + +## Overview + +The `void` method cancels a payout that has been initiated but not yet completed or transferred. This is used when a payout needs to be stopped before the funds leave the merchant's account. + +## Purpose + +Use this operation to halt a payout process. + +| Scenario | Developer Implementation | +|----------|--------------------------| +| Cancel a scheduled payout | Call `void` with the `merchant_payout_id` and required details. | + +## Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchant_payout_id` | string | No | Your internal identifier for the payout. | +| `address` | PayoutAddress | Yes | Address information associated with the payout. | +| `connector_feature_data` | SecretString | No | Connector-specific metadata or feature configurations. | +| `connector_payout_id` | string | No | The unique payout ID assigned by the connector. | +| `access_token` | SecretString | No | Access token for the connector, if required. | + +## Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `merchant_payout_id` | string | No | Your internal identifier passed to the payout processor. | +| `payout_status` | PayoutEnums.PayoutStatus | No | The current status of the payout (e.g., CANCELLED). | +| `connector_payout_id` | string | No | The unique payout ID assigned by the connector. | +| `error` | ErrorInfo | No | Details of any error that occurred. | +| `status_code` | uint32 | Yes | The HTTP status code returned from the connector. | + +## Example + +### SDK Setup + +```python +from hyperswitch_prism import PayoutClient + +payout_client = PayoutClient( + connector='stripe', + api_key='[REDACTED_ENV_SECRET]', + environment='SANDBOX' +) +``` + +### Request + +```python +request = { + "connector_payout_id": "po_1Hh1XYZ2eZvKYlo2C" +} + +response = await payout_client.void(request) +``` + +### Response + +```python +{ + "payout_status": "CANCELLED", + "connector_payout_id": "po_1Hh1XYZ2eZvKYlo2C", + "status_code": 200 +} +``` + +## Next Steps + +- [Get Payout Status](./get.md) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index c18cadac3f..5010612ae0 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -72,6 +72,7 @@ - [Create Link](api-reference/services/payout-service/create-link.md) - [Create Recipient](api-reference/services/payout-service/create-recipient.md) - [Enroll Disburse Account](api-reference/services/payout-service/enroll-disburse-account.md) + - [Check Eligibility](api-reference/services/payout-service/eligibility.md) - [Domain Schema](api-reference/domain-schema/README.md) - [FAQs](FAQs.md) @@ -121,6 +122,7 @@ - [Create Link](sdks/java/payout-service/create-link.md) - [Create Recipient](sdks/java/payout-service/create-recipient.md) - [Enroll Disburse Account](sdks/java/payout-service/enroll-disburse-account.md) + - [Check Eligibility](sdks/java/payout-service/eligibility.md) - [Node.js SDK Overview](sdks/node/README.md) - [Payment Service Overview](sdks/node/payment-service/README.md) @@ -166,6 +168,7 @@ - [Create Link](sdks/node/payout-service/create-link.md) - [Create Recipient](sdks/node/payout-service/create-recipient.md) - [Enroll Disburse Account](sdks/node/payout-service/enroll-disburse-account.md) + - [Check Eligibility](sdks/node/payout-service/eligibility.md) - [Python SDK Overview](sdks/python/README.md) - [Payment Service Overview](sdks/python/payment-service/README.md) @@ -210,4 +213,5 @@ - [Stage](sdks/python/payout-service/stage.md) - [Create Link](sdks/python/payout-service/create-link.md) - [Create Recipient](sdks/python/payout-service/create-recipient.md) - - [Enroll Disburse Account](sdks/python/payout-service/enroll-disburse-account.md) \ No newline at end of file + - [Enroll Disburse Account](sdks/python/payout-service/enroll-disburse-account.md) + - [Check Eligibility](sdks/python/payout-service/eligibility.md) \ No newline at end of file diff --git a/examples/citigate/citigate.kt b/examples/citigate/citigate.kt new file mode 100644 index 0000000000..b8158a1b0d --- /dev/null +++ b/examples/citigate/citigate.kt @@ -0,0 +1,345 @@ +// This file is auto-generated. Do not edit manually. +// Replace YOUR_API_KEY and placeholder values with real data. +// Regenerate: python3 scripts/generate-connector-docs.py citigate +// +// Citigate — all scenarios and flows in one file. +// Run a scenario: ./gradlew run --args="citigate processCheckoutCard" + +package examples.citigate + +import types.Payment.* +import types.PaymentMethods.* +import payments.PaymentClient +import payments.RefundClient +import payments.AuthenticationType +import payments.CaptureMethod +import payments.CardNetwork +import payments.CountryAlpha2 +import payments.Currency +import payments.ConnectorConfig +import payments.SdkOptions +import payments.Environment +import payments.ConnectorSpecificConfig +import types.Payment.CitigateConfig +import payments.SecretString + +val SUPPORTED_FLOWS = listOf("authorize", "capture", "get", "proxy_authorize", "refund", "refund_get", "void") + +val _defaultConfig: ConnectorConfig = ConnectorConfig.newBuilder() + .setOptions(SdkOptions.newBuilder().setEnvironment(Environment.SANDBOX).build()) + .setConnectorConfig( + ConnectorSpecificConfig.newBuilder() + .setCitigate(CitigateConfig.newBuilder() + .setApiKey(SecretString.newBuilder().setValue("YOUR_API_KEY").build()) + .setKey1(SecretString.newBuilder().setValue("YOUR_KEY1").build()) + .setBaseUrl("YOUR_BASE_URL") + .build()) + .build() + ) + .build() + + + +private fun buildAuthorizeRequest(captureMethodStr: String): PaymentServiceAuthorizeRequest { + return PaymentServiceAuthorizeRequest.newBuilder().apply { + merchantTransactionId = "probe_txn_001" // Identification. + amountBuilder.apply { // The amount for the payment. + minorAmount = 1000L // Amount in minor units (e.g., 1000 = $10.00). + currency = Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + } + paymentMethodBuilder.apply { // Payment method to be used. + cardBuilder.apply { // Generic card payment. + cardNumberBuilder.value = "4111111111111111" // Card Identification. + cardExpMonthBuilder.value = "03" + cardExpYearBuilder.value = "2030" + cardCvcBuilder.value = "737" + cardHolderNameBuilder.value = "John Doe" // Cardholder Information. + } + } + captureMethod = CaptureMethod.valueOf(captureMethodStr) // Method for capturing the payment. + addressBuilder.apply { // Address Information. + billingAddressBuilder.apply { + firstNameBuilder.value = "John" // Personal Information. + lastNameBuilder.value = "Doe" + line1Builder.value = "123 Main St" // Address Details. + cityBuilder.value = "Seattle" + stateBuilder.value = "WA" + zipCodeBuilder.value = "98101" + countryAlpha2Code = CountryAlpha2.US + emailBuilder.value = "test@example.com" // Contact Information. + phoneNumberBuilder.value = "4155552671" + phoneCountryCode = "+1" + } + } + authType = AuthenticationType.NO_THREE_DS // Authentication Details. + returnUrl = "https://example.com/return" // URLs for Redirection and Webhooks. + browserInfoBuilder.apply { + ipAddress = "1.2.3.4" // Device Information. + } + }.build() +} + +private fun buildCaptureRequest(connectorTransactionIdStr: String): PaymentServiceCaptureRequest { + return PaymentServiceCaptureRequest.newBuilder().apply { + merchantCaptureId = "probe_capture_001" // Identification. + connectorTransactionId = connectorTransactionIdStr + amountToCaptureBuilder.apply { // Capture Details. + minorAmount = 1000L // Amount in minor units (e.g., 1000 = $10.00). + currency = Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + } + }.build() +} + +private fun buildGetRequest(connectorTransactionIdStr: String): PaymentServiceGetRequest { + return PaymentServiceGetRequest.newBuilder().apply { + merchantTransactionId = "probe_merchant_txn_001" // Identification. + connectorTransactionId = connectorTransactionIdStr + amountBuilder.apply { // Amount Information. + minorAmount = 1000L // Amount in minor units (e.g., 1000 = $10.00). + currency = Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + } + }.build() +} + +private fun buildRefundRequest(connectorTransactionIdStr: String): PaymentServiceRefundRequest { + return PaymentServiceRefundRequest.newBuilder().apply { + merchantRefundId = "probe_refund_001" // Identification. + connectorTransactionId = connectorTransactionIdStr + paymentAmount = 1000L // Amount Information. + refundAmountBuilder.apply { + minorAmount = 1000L // Amount in minor units (e.g., 1000 = $10.00). + currency = Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + } + reason = "customer_request" // Reason for the refund. + }.build() +} + +private fun buildVoidRequest(connectorTransactionIdStr: String): PaymentServiceVoidRequest { + return PaymentServiceVoidRequest.newBuilder().apply { + merchantVoidId = "probe_void_001" // Identification. + connectorTransactionId = connectorTransactionIdStr + }.build() +} + +// Scenario: One-step Payment (Authorize + Capture) +// Simple payment that authorizes and captures in one call. Use for immediate charges. +fun processCheckoutAutocapture(txnId: String, config: ConnectorConfig = _defaultConfig): Map { + val paymentClient = PaymentClient(config) + + // Step 1: Authorize — reserve funds on the payment method + val authorizeResponse = paymentClient.authorize(buildAuthorizeRequest("AUTOMATIC")) + + when (authorizeResponse.status.name) { + "FAILED" -> throw RuntimeException("Payment failed: ${authorizeResponse.error.unifiedDetails.message}") + "PENDING" -> return mapOf("status" to "PENDING") // await webhook before proceeding + } + + return mapOf("status" to authorizeResponse.status.name, "transactionId" to authorizeResponse.connectorTransactionId, "error" to authorizeResponse.error) +} + +// Scenario: Card Payment (Authorize + Capture) +// Two-step card payment. First authorize, then capture. Use when you need to verify funds before finalizing. +fun processCheckoutCard(txnId: String, config: ConnectorConfig = _defaultConfig): Map { + val paymentClient = PaymentClient(config) + + // Step 1: Authorize — reserve funds on the payment method + val authorizeResponse = paymentClient.authorize(buildAuthorizeRequest("MANUAL")) + + when (authorizeResponse.status.name) { + "FAILED" -> throw RuntimeException("Payment failed: ${authorizeResponse.error.unifiedDetails.message}") + "PENDING" -> return mapOf("status" to "PENDING") // await webhook before proceeding + } + + // Step 2: Capture — settle the reserved funds + val captureResponse = paymentClient.capture(buildCaptureRequest(authorizeResponse.connectorTransactionId ?: "")) + + if (captureResponse.status.name == "FAILED") + throw RuntimeException("Capture failed: ${captureResponse.error.unifiedDetails.message}") + + return mapOf("status" to captureResponse.status.name, "transactionId" to authorizeResponse.connectorTransactionId, "error" to authorizeResponse.error) +} + +// Scenario: Refund +// Return funds to the customer for a completed payment. +fun processRefund(txnId: String, config: ConnectorConfig = _defaultConfig): Map { + val paymentClient = PaymentClient(config) + + // Step 1: Authorize — reserve funds on the payment method + val authorizeResponse = paymentClient.authorize(buildAuthorizeRequest("AUTOMATIC")) + + when (authorizeResponse.status.name) { + "FAILED" -> throw RuntimeException("Payment failed: ${authorizeResponse.error.unifiedDetails.message}") + "PENDING" -> return mapOf("status" to "PENDING") // await webhook before proceeding + } + + // Step 2: Refund — return funds to the customer + val refundResponse = paymentClient.refund(buildRefundRequest(authorizeResponse.connectorTransactionId ?: "")) + + if (refundResponse.status.name == "FAILED") + throw RuntimeException("Refund failed: ${refundResponse.error.unifiedDetails.message}") + + return mapOf("status" to refundResponse.status.name, "error" to refundResponse.error) +} + +// Scenario: Void Payment +// Cancel an authorized but not-yet-captured payment. +fun processVoidPayment(txnId: String, config: ConnectorConfig = _defaultConfig): Map { + val paymentClient = PaymentClient(config) + + // Step 1: Authorize — reserve funds on the payment method + val authorizeResponse = paymentClient.authorize(buildAuthorizeRequest("MANUAL")) + + when (authorizeResponse.status.name) { + "FAILED" -> throw RuntimeException("Payment failed: ${authorizeResponse.error.unifiedDetails.message}") + "PENDING" -> return mapOf("status" to "PENDING") // await webhook before proceeding + } + + // Step 2: Void — release reserved funds (cancel authorization) + val voidResponse = paymentClient.void(buildVoidRequest(authorizeResponse.connectorTransactionId ?: "")) + + return mapOf("status" to voidResponse.status.name, "transactionId" to authorizeResponse.connectorTransactionId, "error" to voidResponse.error) +} + +// Scenario: Get Payment Status +// Retrieve current payment status from the connector. +fun processGetPayment(txnId: String, config: ConnectorConfig = _defaultConfig): Map { + val paymentClient = PaymentClient(config) + + // Step 1: Authorize — reserve funds on the payment method + val authorizeResponse = paymentClient.authorize(buildAuthorizeRequest("MANUAL")) + + when (authorizeResponse.status.name) { + "FAILED" -> throw RuntimeException("Payment failed: ${authorizeResponse.error.unifiedDetails.message}") + "PENDING" -> return mapOf("status" to "PENDING") // await webhook before proceeding + } + + // Step 2: Get — retrieve current payment status from the connector + val getResponse = paymentClient.get(buildGetRequest(authorizeResponse.connectorTransactionId ?: "")) + + return mapOf("status" to getResponse.status.name, "transactionId" to getResponse.connectorTransactionId, "error" to getResponse.error) +} + +// Flow: PaymentService.Authorize (Card) +fun authorize(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = PaymentClient(config) + val request = buildAuthorizeRequest("AUTOMATIC") + val response = client.authorize(request) + when (response.status.name) { + "FAILED" -> throw RuntimeException("Authorize failed: ${response.error.unifiedDetails.message}") + "PENDING" -> println("Pending — await webhook before proceeding") + else -> println("Authorized: ${response.connectorTransactionId}") + } +} + +// Flow: PaymentService.Capture +fun capture(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = PaymentClient(config) + val request = buildCaptureRequest("probe_connector_txn_001") + val response = client.capture(request) + if (response.status.name == "FAILED") + throw RuntimeException("Capture failed: ${response.error.unifiedDetails.message}") + println("Done: ${response.status.name}") +} + +// Flow: PaymentService.Get +fun get(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = PaymentClient(config) + val request = buildGetRequest("probe_connector_txn_001") + val response = client.get(request) + println("Status: ${response.status.name}") +} + +// Flow: PaymentService.ProxyAuthorize +fun proxyAuthorize(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = PaymentClient(config) + val request = PaymentServiceProxyAuthorizeRequest.newBuilder().apply { + merchantTransactionId = "probe_proxy_txn_001" + amountBuilder.apply { + minorAmount = 1000L // Amount in minor units (e.g., 1000 = $10.00). + currency = Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + } + cardProxyBuilder.apply { // Card proxy for vault-aliased payments (VGS, Basis Theory, Spreedly). Real card values are substituted by the proxy before reaching the connector. + cardNumberBuilder.value = "4111111111111111" // Card Identification. + cardExpMonthBuilder.value = "03" + cardExpYearBuilder.value = "2030" + cardCvcBuilder.value = "123" + cardHolderNameBuilder.value = "John Doe" // Cardholder Information. + cardNetwork = CardNetwork.VISA + } + addressBuilder.apply { + billingAddressBuilder.apply { + firstNameBuilder.value = "John" // Personal Information. + lastNameBuilder.value = "Doe" + line1Builder.value = "123 Main St" // Address Details. + cityBuilder.value = "Seattle" + stateBuilder.value = "WA" + zipCodeBuilder.value = "98101" + countryAlpha2Code = CountryAlpha2.US + emailBuilder.value = "test@example.com" // Contact Information. + phoneNumberBuilder.value = "4155552671" + phoneCountryCode = "+1" + } + } + captureMethod = CaptureMethod.AUTOMATIC + authType = AuthenticationType.NO_THREE_DS + returnUrl = "https://example.com/return" + browserInfoBuilder.apply { + ipAddress = "1.2.3.4" // Device Information. + } + }.build() + val response = client.proxy_authorize(request) + println("Status: ${response.status.name}") +} + +// Flow: PaymentService.Refund +fun refund(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = PaymentClient(config) + val request = buildRefundRequest("probe_connector_txn_001") + val response = client.refund(request) + if (response.status.name == "FAILED") + throw RuntimeException("Refund failed: ${response.error.unifiedDetails.message}") + println("Done: ${response.status.name}") +} + +// Flow: RefundService.Get +fun refundGet(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = RefundClient(config) + val request = RefundServiceGetRequest.newBuilder().apply { + merchantRefundId = "probe_refund_001" // Identification. + connectorTransactionId = "probe_connector_txn_001" + refundId = "probe_refund_id_001" // Deprecated. + }.build() + val response = client.refund_get(request) + println("Status: ${response.status.name}") +} + +// Flow: PaymentService.Void +fun void(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = PaymentClient(config) + val request = buildVoidRequest("probe_connector_txn_001") + val response = client.void(request) + if (response.status.name == "FAILED") + throw RuntimeException("Void failed: ${response.error.unifiedDetails.message}") + println("Done: ${response.status.name}") +} + + +fun main(args: Array) { + val txnId = "order_001" + val flow = args.firstOrNull() ?: "processCheckoutAutocapture" + when (flow) { + "processCheckoutAutocapture" -> processCheckoutAutocapture(txnId) + "processCheckoutCard" -> processCheckoutCard(txnId) + "processRefund" -> processRefund(txnId) + "processVoidPayment" -> processVoidPayment(txnId) + "processGetPayment" -> processGetPayment(txnId) + "authorize" -> authorize(txnId) + "capture" -> capture(txnId) + "get" -> get(txnId) + "proxyAuthorize" -> proxyAuthorize(txnId) + "refund" -> refund(txnId) + "refundGet" -> refundGet(txnId) + "void" -> void(txnId) + else -> System.err.println("Unknown flow: $flow. Available: processCheckoutAutocapture, processCheckoutCard, processRefund, processVoidPayment, processGetPayment, authorize, capture, get, proxyAuthorize, refund, refundGet, void") + } +} diff --git a/examples/citigate/citigate.py b/examples/citigate/citigate.py new file mode 100644 index 0000000000..fd81ec7976 --- /dev/null +++ b/examples/citigate/citigate.py @@ -0,0 +1,322 @@ +# This file is auto-generated. Do not edit manually. +# Replace YOUR_API_KEY and placeholder values with real data. +# Regenerate: python3 scripts/generate-connector-docs.py citigate +# +# Citigate — all integration scenarios and flows in one file. +# Run a scenario: python3 citigate.py checkout_card + +import asyncio +import sys +from payments import PaymentClient +from payments import RefundClient +from payments.generated import sdk_config_pb2, payment_pb2, payment_methods_pb2 + +SUPPORTED_FLOWS = ["authorize", "capture", "get", "proxy_authorize", "refund", "refund_get", "void"] + +_default_config = sdk_config_pb2.ConnectorConfig( + options=sdk_config_pb2.SdkOptions(environment=sdk_config_pb2.Environment.SANDBOX), + connector_config=payment_pb2.ConnectorSpecificConfig( + citigate=payment_pb2.CitigateConfig( + api_key=payment_methods_pb2.SecretString(value="YOUR_API_KEY"), + key1=payment_methods_pb2.SecretString(value="YOUR_KEY1"), + base_url="YOUR_BASE_URL", + ), + ), +) + + + + +def _build_authorize_request(capture_method: str): + return payment_pb2.PaymentServiceAuthorizeRequest( + merchant_transaction_id="probe_txn_001", # Identification. + amount=payment_pb2.Money( # The amount for the payment. + minor_amount=1000, # Amount in minor units (e.g., 1000 = $10.00). + currency=payment_pb2.Currency.Value("USD"), # ISO 4217 currency code (e.g., "USD", "EUR"). + ), + payment_method=payment_methods_pb2.PaymentMethod( # Payment method to be used. + card=payment_methods_pb2.CardDetails( + card_number=payment_methods_pb2.CardNumberType(value="4111111111111111"), # Card Identification. + card_exp_month=payment_methods_pb2.SecretString(value="03"), + card_exp_year=payment_methods_pb2.SecretString(value="2030"), + card_cvc=payment_methods_pb2.SecretString(value="737"), + card_holder_name=payment_methods_pb2.SecretString(value="John Doe"), # Cardholder Information. + ), + ), + capture_method=payment_pb2.CaptureMethod.Value(capture_method), # Method for capturing the payment. + address=payment_pb2.PaymentAddress( # Address Information. + billing_address=payment_pb2.Address( + first_name=payment_methods_pb2.SecretString(value="John"), # Personal Information. + last_name=payment_methods_pb2.SecretString(value="Doe"), + line1=payment_methods_pb2.SecretString(value="123 Main St"), # Address Details. + city=payment_methods_pb2.SecretString(value="Seattle"), + state=payment_methods_pb2.SecretString(value="WA"), + zip_code=payment_methods_pb2.SecretString(value="98101"), + country_alpha2_code=payment_methods_pb2.CountryAlpha2.Value("US"), + email=payment_methods_pb2.SecretString(value="test@example.com"), # Contact Information. + phone_number=payment_methods_pb2.SecretString(value="4155552671"), + phone_country_code="+1", + ), + ), + auth_type=payment_pb2.AuthenticationType.Value("NO_THREE_DS"), # Authentication Details. + return_url="https://example.com/return", # URLs for Redirection and Webhooks. + browser_info=payment_pb2.BrowserInformation( + ip_address="1.2.3.4", # Device Information. + ), + ) + +def _build_capture_request(connector_transaction_id: str): + return payment_pb2.PaymentServiceCaptureRequest( + merchant_capture_id="probe_capture_001", # Identification. + connector_transaction_id=connector_transaction_id, + amount_to_capture=payment_pb2.Money( # Capture Details. + minor_amount=1000, # Amount in minor units (e.g., 1000 = $10.00). + currency=payment_pb2.Currency.Value("USD"), # ISO 4217 currency code (e.g., "USD", "EUR"). + ), + ) + +def _build_get_request(connector_transaction_id: str): + return payment_pb2.PaymentServiceGetRequest( + merchant_transaction_id="probe_merchant_txn_001", # Identification. + connector_transaction_id=connector_transaction_id, + amount=payment_pb2.Money( # Amount Information. + minor_amount=1000, # Amount in minor units (e.g., 1000 = $10.00). + currency=payment_pb2.Currency.Value("USD"), # ISO 4217 currency code (e.g., "USD", "EUR"). + ), + ) + +def _build_proxy_authorize_request(): + return payment_pb2.PaymentServiceProxyAuthorizeRequest( + merchant_transaction_id="probe_proxy_txn_001", + amount=payment_pb2.Money( + minor_amount=1000, # Amount in minor units (e.g., 1000 = $10.00). + currency=payment_pb2.Currency.Value("USD"), # ISO 4217 currency code (e.g., "USD", "EUR"). + ), + card_proxy=payment_methods_pb2.ProxyCardDetails( # Card proxy for vault-aliased payments (VGS, Basis Theory, Spreedly). Real card values are substituted by the proxy before reaching the connector. + card_number=payment_methods_pb2.SecretString(value="4111111111111111"), # Card Identification. + card_exp_month=payment_methods_pb2.SecretString(value="03"), + card_exp_year=payment_methods_pb2.SecretString(value="2030"), + card_cvc=payment_methods_pb2.SecretString(value="123"), + card_holder_name=payment_methods_pb2.SecretString(value="John Doe"), # Cardholder Information. + card_network=payment_methods_pb2.CardNetwork.Value("VISA"), + ), + address=payment_pb2.PaymentAddress( + billing_address=payment_pb2.Address( + first_name=payment_methods_pb2.SecretString(value="John"), # Personal Information. + last_name=payment_methods_pb2.SecretString(value="Doe"), + line1=payment_methods_pb2.SecretString(value="123 Main St"), # Address Details. + city=payment_methods_pb2.SecretString(value="Seattle"), + state=payment_methods_pb2.SecretString(value="WA"), + zip_code=payment_methods_pb2.SecretString(value="98101"), + country_alpha2_code=payment_methods_pb2.CountryAlpha2.Value("US"), + email=payment_methods_pb2.SecretString(value="test@example.com"), # Contact Information. + phone_number=payment_methods_pb2.SecretString(value="4155552671"), + phone_country_code="+1", + ), + ), + capture_method=payment_pb2.CaptureMethod.Value("AUTOMATIC"), + auth_type=payment_pb2.AuthenticationType.Value("NO_THREE_DS"), + return_url="https://example.com/return", + browser_info=payment_pb2.BrowserInformation( + ip_address="1.2.3.4", # Device Information. + ), + ) + +def _build_refund_request(connector_transaction_id: str): + return payment_pb2.PaymentServiceRefundRequest( + merchant_refund_id="probe_refund_001", # Identification. + connector_transaction_id=connector_transaction_id, + payment_amount=1000, # Amount Information. + refund_amount=payment_pb2.Money( + minor_amount=1000, # Amount in minor units (e.g., 1000 = $10.00). + currency=payment_pb2.Currency.Value("USD"), # ISO 4217 currency code (e.g., "USD", "EUR"). + ), + reason="customer_request", # Reason for the refund. + ) + +def _build_refund_get_request(): + return payment_pb2.RefundServiceGetRequest( + merchant_refund_id="probe_refund_001", # Identification. + connector_transaction_id="probe_connector_txn_001", + refund_id="probe_refund_id_001", # Deprecated. + ) + +def _build_void_request(connector_transaction_id: str): + return payment_pb2.PaymentServiceVoidRequest( + merchant_void_id="probe_void_001", # Identification. + connector_transaction_id=connector_transaction_id, + ) +async def process_checkout_autocapture(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """One-step Payment (Authorize + Capture) + + Simple payment that authorizes and captures in one call. Use for immediate charges. + """ + payment_client = PaymentClient(config) + + # Step 1: Authorize — reserve funds on the payment method + authorize_response = await payment_client.authorize(_build_authorize_request("AUTOMATIC")) + + if authorize_response.status == "FAILED": + raise RuntimeError(f"Payment failed: {authorize_response.error}") + if authorize_response.status == "PENDING": + # Awaiting async confirmation — handle via webhook + return {"status": "pending", "transaction_id": authorize_response.connector_transaction_id} + + return {"status": getattr(authorize_response, "status", ""), "transaction_id": getattr(authorize_response, "connector_transaction_id", ""), "error": getattr(authorize_response, "error", None)} + + +async def process_checkout_card(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Card Payment (Authorize + Capture) + + Two-step card payment. First authorize, then capture. Use when you need to verify funds before finalizing. + """ + payment_client = PaymentClient(config) + + # Step 1: Authorize — reserve funds on the payment method + authorize_response = await payment_client.authorize(_build_authorize_request("MANUAL")) + + if authorize_response.status == "FAILED": + raise RuntimeError(f"Payment failed: {authorize_response.error}") + if authorize_response.status == "PENDING": + # Awaiting async confirmation — handle via webhook + return {"status": "pending", "transaction_id": authorize_response.connector_transaction_id} + + # Step 2: Capture — settle the reserved funds + capture_response = await payment_client.capture(_build_capture_request(authorize_response.connector_transaction_id)) + + if capture_response.status == "FAILED": + raise RuntimeError(f"Capture failed: {capture_response.error}") + + return {"status": getattr(capture_response, "status", ""), "transaction_id": getattr(authorize_response, "connector_transaction_id", ""), "error": getattr(capture_response, "error", None)} + + +async def process_refund(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Refund + + Return funds to the customer for a completed payment. + """ + payment_client = PaymentClient(config) + + # Step 1: Authorize — reserve funds on the payment method + authorize_response = await payment_client.authorize(_build_authorize_request("AUTOMATIC")) + + if authorize_response.status == "FAILED": + raise RuntimeError(f"Payment failed: {authorize_response.error}") + if authorize_response.status == "PENDING": + # Awaiting async confirmation — handle via webhook + return {"status": "pending", "transaction_id": authorize_response.connector_transaction_id} + + # Step 2: Refund — return funds to the customer + refund_response = await payment_client.refund(_build_refund_request(authorize_response.connector_transaction_id)) + + if refund_response.status == "FAILED": + raise RuntimeError(f"Refund failed: {refund_response.error}") + + return {"status": getattr(refund_response, "status", ""), "error": getattr(refund_response, "error", None)} + + +async def process_void_payment(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Void Payment + + Cancel an authorized but not-yet-captured payment. + """ + payment_client = PaymentClient(config) + + # Step 1: Authorize — reserve funds on the payment method + authorize_response = await payment_client.authorize(_build_authorize_request("MANUAL")) + + if authorize_response.status == "FAILED": + raise RuntimeError(f"Payment failed: {authorize_response.error}") + if authorize_response.status == "PENDING": + # Awaiting async confirmation — handle via webhook + return {"status": "pending", "transaction_id": authorize_response.connector_transaction_id} + + # Step 2: Void — release reserved funds (cancel authorization) + void_response = await payment_client.void(_build_void_request(authorize_response.connector_transaction_id)) + + return {"status": getattr(void_response, "status", ""), "transaction_id": getattr(authorize_response, "connector_transaction_id", ""), "error": getattr(void_response, "error", None)} + + +async def process_get_payment(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Get Payment Status + + Retrieve current payment status from the connector. + """ + payment_client = PaymentClient(config) + + # Step 1: Authorize — reserve funds on the payment method + authorize_response = await payment_client.authorize(_build_authorize_request("MANUAL")) + + if authorize_response.status == "FAILED": + raise RuntimeError(f"Payment failed: {authorize_response.error}") + if authorize_response.status == "PENDING": + # Awaiting async confirmation — handle via webhook + return {"status": "pending", "transaction_id": authorize_response.connector_transaction_id} + + # Step 2: Get — retrieve current payment status from the connector + get_response = await payment_client.get(_build_get_request(authorize_response.connector_transaction_id)) + + return {"status": getattr(get_response, "status", ""), "transaction_id": getattr(get_response, "connector_transaction_id", ""), "error": getattr(get_response, "error", None)} + + +async def process_authorize(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Flow: PaymentService.Authorize (Card)""" + payment_client = PaymentClient(config) + + authorize_response = await payment_client.authorize(_build_authorize_request("AUTOMATIC")) + + return {"status": authorize_response.status, "transaction_id": authorize_response.connector_transaction_id} + + +async def process_capture(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Flow: PaymentService.Capture""" + payment_client = PaymentClient(config) + + capture_response = await payment_client.capture(_build_capture_request("probe_connector_txn_001")) + + return {"status": capture_response.status} + + +async def process_get(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Flow: PaymentService.Get""" + payment_client = PaymentClient(config) + + get_response = await payment_client.get(_build_get_request("probe_connector_txn_001")) + + return {"status": get_response.status} + + +async def process_proxy_authorize(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Flow: PaymentService.ProxyAuthorize""" + payment_client = PaymentClient(config) + + proxy_response = await payment_client.proxy_authorize(_build_proxy_authorize_request()) + + return {"status": proxy_response.status} + + +async def process_refund_get(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Flow: RefundService.Get""" + refund_client = RefundClient(config) + + refund_response = await refund_client.refund_get(_build_refund_get_request()) + + return {"status": refund_response.status} + + +async def process_void(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Flow: PaymentService.Void""" + payment_client = PaymentClient(config) + + void_response = await payment_client.void(_build_void_request("probe_connector_txn_001")) + + return {"status": void_response.status} + +if __name__ == "__main__": + scenario = sys.argv[1] if len(sys.argv) > 1 else "checkout_autocapture" + fn = globals().get(f"process_{scenario}") + if not fn: + available = [k[8:] for k in globals() if k.startswith("process_")] + print(f"Unknown scenario: {scenario}. Available: {available}", file=sys.stderr) + sys.exit(1) + asyncio.run(fn("order_001")) diff --git a/examples/citigate/citigate.rs b/examples/citigate/citigate.rs new file mode 100644 index 0000000000..22751a8f2a --- /dev/null +++ b/examples/citigate/citigate.rs @@ -0,0 +1,514 @@ +// This file is auto-generated. Do not edit manually. +// Replace YOUR_API_KEY and placeholder values with real data. +// Regenerate: python3 scripts/generate-connector-docs.py citigate +// +// Citigate — all scenarios and flows in one file. +// Run a scenario: cargo run --example citigate -- process_checkout_card +use cards::CardNumber; +use grpc_api_types::payments::connector_specific_config; +use grpc_api_types::payments::payment_method; +use grpc_api_types::payments::*; +use hyperswitch_masking::Secret; +use hyperswitch_payments_client::ConnectorClient; +use std::collections::HashMap; +use std::str::FromStr; + +#[allow(dead_code)] +pub const SUPPORTED_FLOWS: &[&str] = &[ + "authorize", + "capture", + "get", + "proxy_authorize", + "refund", + "refund_get", + "void", +]; + +#[allow(dead_code)] +fn build_client() -> ConnectorClient { + // Configure the connector with authentication + let config = ConnectorConfig { + connector_config: Some(ConnectorSpecificConfig { + config: Some(connector_specific_config::Config::Citigate( + CitigateConfig { + api_key: Some(hyperswitch_masking::Secret::new("YOUR_API_KEY".to_string())), // Authentication credential + key1: Some(hyperswitch_masking::Secret::new("YOUR_KEY1".to_string())), // Authentication credential + base_url: Some("https://sandbox.example.com".to_string()), // Base URL for API calls + ..Default::default() + }, + )), + }), + options: Some(SdkOptions { + environment: Environment::Sandbox.into(), + }), + }; + ConnectorClient::new(config, None).unwrap() +} + +pub fn build_authorize_request(capture_method: &str) -> PaymentServiceAuthorizeRequest { + PaymentServiceAuthorizeRequest { + merchant_transaction_id: Some("probe_txn_001".to_string()), // Identification. + amount: Some(Money { + // The amount for the payment. + minor_amount: 1000, // Amount in minor units (e.g., 1000 = $10.00). + currency: Currency::Usd.into(), // ISO 4217 currency code (e.g., "USD", "EUR"). + }), + payment_method: Some(PaymentMethod { + // Payment method to be used. + payment_method: Some(payment_method::PaymentMethod::Card(CardDetails { + card_number: Some(CardNumber::from_str("4111111111111111").unwrap()), // Card Identification. + card_exp_month: Some(Secret::new("03".to_string())), + card_exp_year: Some(Secret::new("2030".to_string())), + card_cvc: Some(Secret::new("737".to_string())), + card_holder_name: Some(Secret::new("John Doe".to_string())), // Cardholder Information. + ..Default::default() + })), + ..Default::default() + }), + capture_method: Some( + CaptureMethod::from_str_name(capture_method) + .unwrap_or_default() + .into(), + ), // Method for capturing the payment. + address: Some(PaymentAddress { + // Address Information. + billing_address: Some(Address { + first_name: Some(Secret::new("John".to_string())), // Personal Information. + last_name: Some(Secret::new("Doe".to_string())), + line1: Some(Secret::new("123 Main St".to_string())), // Address Details. + city: Some(Secret::new("Seattle".to_string())), + state: Some(Secret::new("WA".to_string())), + zip_code: Some(Secret::new("98101".to_string())), + country_alpha2_code: Some(CountryAlpha2::Us.into()), + email: Some(Secret::new("test@example.com".to_string())), // Contact Information. + phone_number: Some(Secret::new("4155552671".to_string())), + phone_country_code: Some("+1".to_string()), + ..Default::default() + }), + ..Default::default() + }), + auth_type: AuthenticationType::NoThreeDs.into(), // Authentication Details. + return_url: Some("https://example.com/return".to_string()), // URLs for Redirection and Webhooks. + browser_info: Some(BrowserInformation { + ip_address: Some("1.2.3.4".to_string()), // Device Information. + ..Default::default() + }), + ..Default::default() + } +} + +pub fn build_capture_request(connector_transaction_id: &str) -> PaymentServiceCaptureRequest { + PaymentServiceCaptureRequest { + merchant_capture_id: Some("probe_capture_001".to_string()), // Identification. + connector_transaction_id: connector_transaction_id.to_string(), + amount_to_capture: Some(Money { + // Capture Details. + minor_amount: 1000, // Amount in minor units (e.g., 1000 = $10.00). + currency: Currency::Usd.into(), // ISO 4217 currency code (e.g., "USD", "EUR"). + }), + ..Default::default() + } +} + +pub fn build_get_request(connector_transaction_id: &str) -> PaymentServiceGetRequest { + PaymentServiceGetRequest { + merchant_transaction_id: Some("probe_merchant_txn_001".to_string()), // Identification. + connector_transaction_id: connector_transaction_id.to_string(), + amount: Some(Money { + // Amount Information. + minor_amount: 1000, // Amount in minor units (e.g., 1000 = $10.00). + currency: Currency::Usd.into(), // ISO 4217 currency code (e.g., "USD", "EUR"). + }), + ..Default::default() + } +} + +pub fn build_proxy_authorize_request() -> PaymentServiceProxyAuthorizeRequest { + PaymentServiceProxyAuthorizeRequest { + merchant_transaction_id: Some("probe_proxy_txn_001".to_string()), + amount: Some(Money { + minor_amount: 1000, // Amount in minor units (e.g., 1000 = $10.00). + currency: Currency::Usd.into(), // ISO 4217 currency code (e.g., "USD", "EUR"). + }), + card_proxy: Some(ProxyCardDetails { + // Card proxy for vault-aliased payments (VGS, Basis Theory, Spreedly). Real card values are substituted by the proxy before reaching the connector. + card_number: Some(Secret::new("4111111111111111".to_string())), // Card Identification. + card_exp_month: Some(Secret::new("03".to_string())), + card_exp_year: Some(Secret::new("2030".to_string())), + card_cvc: Some(Secret::new("123".to_string())), + card_holder_name: Some(Secret::new("John Doe".to_string())), // Cardholder Information. + card_network: Some(CardNetwork::Visa.into()), + ..Default::default() + }), + address: Some(PaymentAddress { + billing_address: Some(Address { + first_name: Some(Secret::new("John".to_string())), // Personal Information. + last_name: Some(Secret::new("Doe".to_string())), + line1: Some(Secret::new("123 Main St".to_string())), // Address Details. + city: Some(Secret::new("Seattle".to_string())), + state: Some(Secret::new("WA".to_string())), + zip_code: Some(Secret::new("98101".to_string())), + country_alpha2_code: Some(CountryAlpha2::Us.into()), + email: Some(Secret::new("test@example.com".to_string())), // Contact Information. + phone_number: Some(Secret::new("4155552671".to_string())), + phone_country_code: Some("+1".to_string()), + ..Default::default() + }), + ..Default::default() + }), + capture_method: Some(CaptureMethod::Automatic.into()), + auth_type: AuthenticationType::NoThreeDs.into(), + return_url: Some("https://example.com/return".to_string()), + browser_info: Some(BrowserInformation { + ip_address: Some("1.2.3.4".to_string()), // Device Information. + ..Default::default() + }), + ..Default::default() + } +} + +pub fn build_refund_request(connector_transaction_id: &str) -> PaymentServiceRefundRequest { + PaymentServiceRefundRequest { + merchant_refund_id: Some("probe_refund_001".to_string()), // Identification. + connector_transaction_id: connector_transaction_id.to_string(), + payment_amount: 1000, // Amount Information. + refund_amount: Some(Money { + minor_amount: 1000, // Amount in minor units (e.g., 1000 = $10.00). + currency: Currency::Usd.into(), // ISO 4217 currency code (e.g., "USD", "EUR"). + }), + reason: Some("customer_request".to_string()), // Reason for the refund. + ..Default::default() + } +} + +pub fn build_refund_get_request() -> RefundServiceGetRequest { + RefundServiceGetRequest { + merchant_refund_id: Some("probe_refund_001".to_string()), // Identification. + connector_transaction_id: "probe_connector_txn_001".to_string(), + refund_id: "probe_refund_id_001".to_string(), // Deprecated. + ..Default::default() + } +} + +pub fn build_void_request(connector_transaction_id: &str) -> PaymentServiceVoidRequest { + PaymentServiceVoidRequest { + merchant_void_id: Some("probe_void_001".to_string()), // Identification. + connector_transaction_id: connector_transaction_id.to_string(), + ..Default::default() + } +} + +// Scenario: One-step Payment (Authorize + Capture) +// Simple payment that authorizes and captures in one call. Use for immediate charges. +#[allow(dead_code)] +pub async fn process_checkout_autocapture( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + // Step 1: Authorize — reserve funds on the payment method + let authorize_response = client + .authorize(build_authorize_request("AUTOMATIC"), &HashMap::new(), None) + .await?; + + match authorize_response.status() { + PaymentStatus::Failure | PaymentStatus::AuthorizationFailed => { + return Err(format!("Payment failed: {:?}", authorize_response.error).into()) + } + PaymentStatus::Pending => return Ok("pending — awaiting webhook".to_string()), + _ => {} + } + + Ok(format!( + "Payment: {:?} — {}", + authorize_response.status(), + authorize_response + .connector_transaction_id + .as_deref() + .unwrap_or("") + )) +} + +// Scenario: Card Payment (Authorize + Capture) +// Two-step card payment. First authorize, then capture. Use when you need to verify funds before finalizing. +#[allow(dead_code)] +pub async fn process_checkout_card( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + // Step 1: Authorize — reserve funds on the payment method + let authorize_response = client + .authorize(build_authorize_request("MANUAL"), &HashMap::new(), None) + .await?; + + match authorize_response.status() { + PaymentStatus::Failure | PaymentStatus::AuthorizationFailed => { + return Err(format!("Payment failed: {:?}", authorize_response.error).into()) + } + PaymentStatus::Pending => return Ok("pending — awaiting webhook".to_string()), + _ => {} + } + + // Step 2: Capture — settle the reserved funds + let capture_response = client + .capture( + build_capture_request( + authorize_response + .connector_transaction_id + .as_deref() + .unwrap_or(""), + ), + &HashMap::new(), + None, + ) + .await?; + + if capture_response.status() == PaymentStatus::Failure { + return Err(format!("Capture failed: {:?}", capture_response.error).into()); + } + + Ok(format!( + "Payment completed: {}", + authorize_response + .connector_transaction_id + .as_deref() + .unwrap_or("") + )) +} + +// Scenario: Refund +// Return funds to the customer for a completed payment. +#[allow(dead_code)] +pub async fn process_refund( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + // Step 1: Authorize — reserve funds on the payment method + let authorize_response = client + .authorize(build_authorize_request("AUTOMATIC"), &HashMap::new(), None) + .await?; + + match authorize_response.status() { + PaymentStatus::Failure | PaymentStatus::AuthorizationFailed => { + return Err(format!("Payment failed: {:?}", authorize_response.error).into()) + } + PaymentStatus::Pending => return Ok("pending — awaiting webhook".to_string()), + _ => {} + } + + // Step 2: Refund — return funds to the customer + let refund_response = client + .refund( + build_refund_request( + authorize_response + .connector_transaction_id + .as_deref() + .unwrap_or(""), + ), + &HashMap::new(), + None, + ) + .await?; + + if refund_response.status() == RefundStatus::RefundFailure { + return Err(format!("Refund failed: {:?}", refund_response.error).into()); + } + + Ok(format!("Refunded: {:?}", refund_response.status())) +} + +// Scenario: Void Payment +// Cancel an authorized but not-yet-captured payment. +#[allow(dead_code)] +pub async fn process_void_payment( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + // Step 1: Authorize — reserve funds on the payment method + let authorize_response = client + .authorize(build_authorize_request("MANUAL"), &HashMap::new(), None) + .await?; + + match authorize_response.status() { + PaymentStatus::Failure | PaymentStatus::AuthorizationFailed => { + return Err(format!("Payment failed: {:?}", authorize_response.error).into()) + } + PaymentStatus::Pending => return Ok("pending — awaiting webhook".to_string()), + _ => {} + } + + // Step 2: Void — release reserved funds (cancel authorization) + let void_response = client + .void( + build_void_request( + authorize_response + .connector_transaction_id + .as_deref() + .unwrap_or(""), + ), + &HashMap::new(), + None, + ) + .await?; + + Ok(format!("Voided: {:?}", void_response.status())) +} + +// Scenario: Get Payment Status +// Retrieve current payment status from the connector. +#[allow(dead_code)] +pub async fn process_get_payment( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + // Step 1: Authorize — reserve funds on the payment method + let authorize_response = client + .authorize(build_authorize_request("MANUAL"), &HashMap::new(), None) + .await?; + + match authorize_response.status() { + PaymentStatus::Failure | PaymentStatus::AuthorizationFailed => { + return Err(format!("Payment failed: {:?}", authorize_response.error).into()) + } + PaymentStatus::Pending => return Ok("pending — awaiting webhook".to_string()), + _ => {} + } + + // Step 2: Get — retrieve current payment status from the connector + let get_response = client + .get( + build_get_request( + authorize_response + .connector_transaction_id + .as_deref() + .unwrap_or(""), + ), + &HashMap::new(), + None, + ) + .await?; + + Ok(format!("Status: {:?}", get_response.status())) +} + +// Flow: PaymentService.Authorize (Card) +#[allow(dead_code)] +pub async fn process_authorize( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + let response = client + .authorize(build_authorize_request("AUTOMATIC"), &HashMap::new(), None) + .await?; + match response.status() { + PaymentStatus::Failure | PaymentStatus::AuthorizationFailed => { + Err(format!("Authorize failed: {:?}", response.error).into()) + } + PaymentStatus::Pending => Ok("pending — await webhook".to_string()), + _ => Ok(format!( + "Authorized: {}", + response.connector_transaction_id.as_deref().unwrap_or("") + )), + } +} + +// Flow: PaymentService.Capture +#[allow(dead_code)] +pub async fn process_capture( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + let response = client + .capture( + build_capture_request("probe_connector_txn_001"), + &HashMap::new(), + None, + ) + .await?; + Ok(format!("status: {:?}", response.status())) +} + +// Flow: PaymentService.Get +#[allow(dead_code)] +pub async fn process_get( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + let response = client + .get( + build_get_request("probe_connector_txn_001"), + &HashMap::new(), + None, + ) + .await?; + Ok(format!("status: {:?}", response.status())) +} + +// Flow: PaymentService.ProxyAuthorize +#[allow(dead_code)] +pub async fn process_proxy_authorize( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + let response = client + .proxy_authorize(build_proxy_authorize_request(), &HashMap::new(), None) + .await?; + Ok(format!("status: {:?}", response.status())) +} + +// Flow: RefundService.Get +#[allow(dead_code)] +pub async fn process_refund_get( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + let response = client + .refund_get(build_refund_get_request(), &HashMap::new(), None) + .await?; + Ok(format!("status: {:?}", response.status())) +} + +// Flow: PaymentService.Void +#[allow(dead_code)] +pub async fn process_void( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + let response = client + .void( + build_void_request("probe_connector_txn_001"), + &HashMap::new(), + None, + ) + .await?; + Ok(format!("status: {:?}", response.status())) +} + +#[allow(dead_code)] +#[tokio::main] +async fn main() { + let client = build_client(); + let flow = std::env::args() + .nth(1) + .unwrap_or_else(|| "process_checkout_autocapture".to_string()); + let result: Result> = match flow.as_str() { + "process_checkout_autocapture" => process_checkout_autocapture(&client, "order_001").await, + "process_checkout_card" => process_checkout_card(&client, "order_001").await, + "process_refund" => process_refund(&client, "order_001").await, + "process_void_payment" => process_void_payment(&client, "order_001").await, + "process_get_payment" => process_get_payment(&client, "order_001").await, + "process_authorize" => process_authorize(&client, "txn_001").await, + "process_capture" => process_capture(&client, "txn_001").await, + "process_get" => process_get(&client, "txn_001").await, + "process_proxy_authorize" => process_proxy_authorize(&client, "txn_001").await, + "process_refund_get" => process_refund_get(&client, "txn_001").await, + "process_void" => process_void(&client, "txn_001").await, + _ => { + eprintln!("Unknown flow: {}. Available: process_checkout_autocapture, process_checkout_card, process_refund, process_void_payment, process_get_payment, process_authorize, process_capture, process_get, process_proxy_authorize, process_refund_get, process_void", flow); + return; + } + }; + match result { + Ok(msg) => println!("✓ {msg}"), + Err(e) => eprintln!("✗ {e}"), + } +} diff --git a/examples/citigate/citigate.ts b/examples/citigate/citigate.ts new file mode 100644 index 0000000000..3151497cf7 --- /dev/null +++ b/examples/citigate/citigate.ts @@ -0,0 +1,352 @@ +// This file is auto-generated. Do not edit manually. +// Replace YOUR_API_KEY and placeholder values with real data. +// Regenerate: python3 scripts/generate-connector-docs.py citigate +// +// Citigate — all integration scenarios and flows in one file. +// Run a scenario: npx tsx citigate.ts checkout_autocapture + +import { PaymentClient, RefundClient, types } from 'hyperswitch-prism'; +const { Environment, AuthenticationType, CaptureMethod, CardNetwork, CountryAlpha2, Currency } = types; +export const SUPPORTED_FLOWS = ["authorize", "capture", "get", "proxy_authorize", "refund", "refund_get", "void"]; + +const _defaultConfig: types.IConnectorConfig = { + options: { + environment: Environment.SANDBOX, + }, + connectorConfig: { + citigate: { + apiKey: { value: 'YOUR_API_KEY' }, + key1: { value: 'YOUR_KEY1' }, + baseUrl: 'YOUR_BASE_URL', + } + }, +}; + + +function _buildAuthorizeRequest(captureMethod: types.CaptureMethod): types.IPaymentServiceAuthorizeRequest { + return { + "merchantTransactionId": "probe_txn_001", // Identification. + "amount": { // The amount for the payment. + "minorAmount": 1000, // Amount in minor units (e.g., 1000 = $10.00). + "currency": Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + }, + "paymentMethod": { // Payment method to be used. + "card": { // Generic card payment. + "cardNumber": {"value": "4111111111111111"}, // Card Identification. + "cardExpMonth": {"value": "03"}, + "cardExpYear": {"value": "2030"}, + "cardCvc": {"value": "737"}, + "cardHolderName": {"value": "John Doe"} // Cardholder Information. + } + }, + "captureMethod": captureMethod, // Method for capturing the payment. + "address": { // Address Information. + "billingAddress": { + "firstName": {"value": "John"}, // Personal Information. + "lastName": {"value": "Doe"}, + "line1": {"value": "123 Main St"}, // Address Details. + "city": {"value": "Seattle"}, + "state": {"value": "WA"}, + "zipCode": {"value": "98101"}, + "countryAlpha2Code": CountryAlpha2.US, + "email": {"value": "test@example.com"}, // Contact Information. + "phoneNumber": {"value": "4155552671"}, + "phoneCountryCode": "+1" + } + }, + "authType": AuthenticationType.NO_THREE_DS, // Authentication Details. + "returnUrl": "https://example.com/return", // URLs for Redirection and Webhooks. + "browserInfo": { + "ipAddress": "1.2.3.4" // Device Information. + } + }; +} + +function _buildCaptureRequest(connectorTransactionId: string): types.IPaymentServiceCaptureRequest { + return { + "merchantCaptureId": "probe_capture_001", // Identification. + "connectorTransactionId": connectorTransactionId, + "amountToCapture": { // Capture Details. + "minorAmount": 1000, // Amount in minor units (e.g., 1000 = $10.00). + "currency": Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + } + }; +} + +function _buildGetRequest(connectorTransactionId: string): types.IPaymentServiceGetRequest { + return { + "merchantTransactionId": "probe_merchant_txn_001", // Identification. + "connectorTransactionId": connectorTransactionId, + "amount": { // Amount Information. + "minorAmount": 1000, // Amount in minor units (e.g., 1000 = $10.00). + "currency": Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + } + }; +} + +function _buildProxyAuthorizeRequest(): types.IPaymentServiceProxyAuthorizeRequest { + return { + "merchantTransactionId": "probe_proxy_txn_001", + "amount": { + "minorAmount": 1000, // Amount in minor units (e.g., 1000 = $10.00). + "currency": Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + }, + "cardProxy": { // Card proxy for vault-aliased payments (VGS, Basis Theory, Spreedly). Real card values are substituted by the proxy before reaching the connector. + "cardNumber": {"value": "4111111111111111"}, // Card Identification. + "cardExpMonth": {"value": "03"}, + "cardExpYear": {"value": "2030"}, + "cardCvc": {"value": "123"}, + "cardHolderName": {"value": "John Doe"}, // Cardholder Information. + "cardNetwork": CardNetwork.VISA + }, + "address": { + "billingAddress": { + "firstName": {"value": "John"}, // Personal Information. + "lastName": {"value": "Doe"}, + "line1": {"value": "123 Main St"}, // Address Details. + "city": {"value": "Seattle"}, + "state": {"value": "WA"}, + "zipCode": {"value": "98101"}, + "countryAlpha2Code": CountryAlpha2.US, + "email": {"value": "test@example.com"}, // Contact Information. + "phoneNumber": {"value": "4155552671"}, + "phoneCountryCode": "+1" + } + }, + "captureMethod": CaptureMethod.AUTOMATIC, + "authType": AuthenticationType.NO_THREE_DS, + "returnUrl": "https://example.com/return", + "browserInfo": { + "ipAddress": "1.2.3.4" // Device Information. + } + }; +} + +function _buildRefundRequest(connectorTransactionId: string): types.IPaymentServiceRefundRequest { + return { + "merchantRefundId": "probe_refund_001", // Identification. + "connectorTransactionId": connectorTransactionId, + "paymentAmount": 1000, // Amount Information. + "refundAmount": { + "minorAmount": 1000, // Amount in minor units (e.g., 1000 = $10.00). + "currency": Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + }, + "reason": "customer_request" // Reason for the refund. + }; +} + +function _buildRefundGetRequest(): types.IRefundServiceGetRequest { + return { + "merchantRefundId": "probe_refund_001", // Identification. + "connectorTransactionId": "probe_connector_txn_001", + "refundId": "probe_refund_id_001" // Deprecated. + }; +} + +function _buildVoidRequest(connectorTransactionId: string): types.IPaymentServiceVoidRequest { + return { + "merchantVoidId": "probe_void_001", // Identification. + "connectorTransactionId": connectorTransactionId + }; +} + + +// ANCHOR: scenario_functions +// One-step Payment (Authorize + Capture) +// Simple payment that authorizes and captures in one call. Use for immediate charges. +async function processCheckoutAutocapture(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + // Step 1: Authorize — reserve funds on the payment method + const authorizeResponse = await paymentClient.authorize(_buildAuthorizeRequest(CaptureMethod.AUTOMATIC)); + + if (authorizeResponse.status === types.PaymentStatus.FAILURE) { + throw new Error(`Payment failed: ${JSON.stringify(authorizeResponse.error)}`); + } + if (authorizeResponse.status === types.PaymentStatus.PENDING) { + // Awaiting async confirmation — handle via webhook + return { status: 'pending', connectorTransactionId: authorizeResponse.connectorTransactionId }; + } + + return { status: authorizeResponse.status, transactionId: authorizeResponse.connectorTransactionId!, error: authorizeResponse.error } as any; +} + +// Card Payment (Authorize + Capture) +// Two-step card payment. First authorize, then capture. Use when you need to verify funds before finalizing. +async function processCheckoutCard(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + // Step 1: Authorize — reserve funds on the payment method + const authorizeResponse = await paymentClient.authorize(_buildAuthorizeRequest(CaptureMethod.MANUAL)); + + if (authorizeResponse.status === types.PaymentStatus.FAILURE) { + throw new Error(`Payment failed: ${JSON.stringify(authorizeResponse.error)}`); + } + if (authorizeResponse.status === types.PaymentStatus.PENDING) { + // Awaiting async confirmation — handle via webhook + return { status: 'pending', connectorTransactionId: authorizeResponse.connectorTransactionId }; + } + + // Step 2: Capture — settle the reserved funds + const captureResponse = await paymentClient.capture(_buildCaptureRequest(authorizeResponse.connectorTransactionId!)); + + if (captureResponse.status === types.PaymentStatus.FAILURE) { + throw new Error(`Capture failed: ${JSON.stringify(captureResponse.error)}`); + } + + return { status: captureResponse.status, transactionId: authorizeResponse.connectorTransactionId!, error: authorizeResponse.error } as any; +} + +// Refund +// Return funds to the customer for a completed payment. +async function processRefund(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + // Step 1: Authorize — reserve funds on the payment method + const authorizeResponse = await paymentClient.authorize(_buildAuthorizeRequest(CaptureMethod.AUTOMATIC)); + + if (authorizeResponse.status === types.PaymentStatus.FAILURE) { + throw new Error(`Payment failed: ${JSON.stringify(authorizeResponse.error)}`); + } + if (authorizeResponse.status === types.PaymentStatus.PENDING) { + // Awaiting async confirmation — handle via webhook + return { status: 'pending', connectorTransactionId: authorizeResponse.connectorTransactionId }; + } + + // Step 2: Refund — return funds to the customer + const refundResponse = await paymentClient.refund(_buildRefundRequest(authorizeResponse.connectorTransactionId!)); + + if (refundResponse.status === types.RefundStatus.REFUND_FAILURE) { + throw new Error(`Refund failed: ${JSON.stringify(refundResponse.error)}`); + } + + return { status: refundResponse.status, error: refundResponse.error } as any; +} + +// Void Payment +// Cancel an authorized but not-yet-captured payment. +async function processVoidPayment(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + // Step 1: Authorize — reserve funds on the payment method + const authorizeResponse = await paymentClient.authorize(_buildAuthorizeRequest(CaptureMethod.MANUAL)); + + if (authorizeResponse.status === types.PaymentStatus.FAILURE) { + throw new Error(`Payment failed: ${JSON.stringify(authorizeResponse.error)}`); + } + if (authorizeResponse.status === types.PaymentStatus.PENDING) { + // Awaiting async confirmation — handle via webhook + return { status: 'pending', connectorTransactionId: authorizeResponse.connectorTransactionId }; + } + + // Step 2: Void — release reserved funds (cancel authorization) + const voidResponse = await paymentClient.void(_buildVoidRequest(authorizeResponse.connectorTransactionId!)); + + return { status: voidResponse.status, transactionId: authorizeResponse.connectorTransactionId!, error: voidResponse.error } as any; +} + +// Get Payment Status +// Retrieve current payment status from the connector. +async function processGetPayment(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + // Step 1: Authorize — reserve funds on the payment method + const authorizeResponse = await paymentClient.authorize(_buildAuthorizeRequest(CaptureMethod.MANUAL)); + + if (authorizeResponse.status === types.PaymentStatus.FAILURE) { + throw new Error(`Payment failed: ${JSON.stringify(authorizeResponse.error)}`); + } + if (authorizeResponse.status === types.PaymentStatus.PENDING) { + // Awaiting async confirmation — handle via webhook + return { status: 'pending', connectorTransactionId: authorizeResponse.connectorTransactionId }; + } + + // Step 2: Get — retrieve current payment status from the connector + const getResponse = await paymentClient.get(_buildGetRequest(authorizeResponse.connectorTransactionId!)); + + return { status: getResponse.status, transactionId: getResponse.connectorTransactionId!, error: getResponse.error } as any; +} + +// Flow: PaymentService.Authorize (Card) +async function authorize(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + const authorizeResponse = await paymentClient.authorize(_buildAuthorizeRequest(CaptureMethod.AUTOMATIC)); + + return authorizeResponse; +} + +// Flow: PaymentService.Capture +async function capture(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + const captureResponse = await paymentClient.capture(_buildCaptureRequest('probe_connector_txn_001')); + + return captureResponse; +} + +// Flow: PaymentService.Get +async function get(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + const getResponse = await paymentClient.get(_buildGetRequest('probe_connector_txn_001')); + + return getResponse; +} + +// Flow: PaymentService.ProxyAuthorize +async function proxyAuthorize(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + const proxyResponse = await paymentClient.proxyAuthorize(_buildProxyAuthorizeRequest()); + + return proxyResponse; +} + +// Flow: PaymentService.Refund +async function refund(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + const refundResponse = await paymentClient.refund(_buildRefundRequest('probe_connector_txn_001')); + + return refundResponse; +} + +// Flow: RefundService.Get +async function refundGet(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const refundClient = new RefundClient(config); + + const refundResponse = await refundClient.refundGet(_buildRefundGetRequest()); + + return refundResponse; +} + +// Flow: PaymentService.Void +async function voidPayment(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + const voidResponse = await paymentClient.void(_buildVoidRequest('probe_connector_txn_001')); + + return voidResponse; +} + + +// Export all process* functions for the smoke test +export { + processCheckoutAutocapture, processCheckoutCard, processRefund, processVoidPayment, processGetPayment, authorize, capture, get, proxyAuthorize, refund, refundGet, voidPayment, _buildAuthorizeRequest, _buildCaptureRequest, _buildGetRequest, _buildProxyAuthorizeRequest, _buildRefundRequest, _buildRefundGetRequest, _buildVoidRequest +}; + +// CLI runner +if (require.main === module) { + const scenario = process.argv[2] || 'checkout_autocapture'; + const key = 'process' + scenario.replace(/_([a-z])/g, (_, l) => l.toUpperCase()).replace(/^(.)/, c => c.toUpperCase()); + const fn = (globalThis as any)[key] || (exports as any)[key]; + if (!fn) { + const available = Object.keys(exports).map(k => + k.replace(/^process/, '').replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '') + ); + console.error(`Unknown scenario: ${scenario}. Available: ${available.join(', ')}`); + process.exit(1); + } + fn('order_001').catch(console.error); +} diff --git a/examples/ilixium/ilixium.kt b/examples/ilixium/ilixium.kt new file mode 100644 index 0000000000..b185f9255e --- /dev/null +++ b/examples/ilixium/ilixium.kt @@ -0,0 +1,323 @@ +// 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 ilixium +// +// Ilixium — all scenarios and flows in one file. +// Run a scenario: ./gradlew run --args="ilixium processCheckoutCard" + +package examples.ilixium + +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.IlixiumConfig +import payments.SecretString + +val SUPPORTED_FLOWS = listOf("authorize", "capture", "get", "proxy_authorize", "refund", "void") + +val _defaultConfig: ConnectorConfig = ConnectorConfig.newBuilder() + .setOptions(SdkOptions.newBuilder().setEnvironment(Environment.SANDBOX).build()) + .setConnectorConfig( + ConnectorSpecificConfig.newBuilder() + .setIlixium(IlixiumConfig.newBuilder() + .setApiKey(SecretString.newBuilder().setValue("YOUR_API_KEY").build()) + .setKey1(SecretString.newBuilder().setValue("YOUR_KEY1").build()) + .setApiSecret(SecretString.newBuilder().setValue("YOUR_API_SECRET").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. + customerBuilder.apply { // Customer Information. + emailBuilder.value = "test@example.com" // Customer's email address. + } + addressBuilder.apply { // Address Information. + billingAddressBuilder.apply { + firstNameBuilder.value = "John" // Personal Information. + lastNameBuilder.value = "Doe" + countryAlpha2Code = CountryAlpha2.US + } + } + authType = AuthenticationType.NO_THREE_DS // Authentication Details. + returnUrl = "https://example.com/return" // URLs for Redirection and Webhooks. + }.build() +} + +private fun buildCaptureRequest(connectorTransactionIdStr: String): PaymentServiceCaptureRequest { + return PaymentServiceCaptureRequest.newBuilder().apply { + merchantCaptureId = "probe_capture_001" // Identification. + connectorTransactionId = connectorTransactionIdStr + amountToCaptureBuilder.apply { // Capture Details. + minorAmount = 1000L // Amount in minor units (e.g., 1000 = $10.00). + currency = Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + } + }.build() +} + +private fun buildGetRequest(connectorTransactionIdStr: String): PaymentServiceGetRequest { + return PaymentServiceGetRequest.newBuilder().apply { + merchantTransactionId = "probe_merchant_txn_001" // Identification. + connectorTransactionId = connectorTransactionIdStr + amountBuilder.apply { // Amount Information. + minorAmount = 1000L // Amount in minor units (e.g., 1000 = $10.00). + currency = Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + } + }.build() +} + +private fun buildRefundRequest(connectorTransactionIdStr: String): PaymentServiceRefundRequest { + return PaymentServiceRefundRequest.newBuilder().apply { + merchantRefundId = "probe_refund_001" // Identification. + connectorTransactionId = connectorTransactionIdStr + paymentAmount = 1000L // Amount Information. + refundAmountBuilder.apply { + minorAmount = 1000L // Amount in minor units (e.g., 1000 = $10.00). + currency = Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + } + reason = "customer_request" // Reason for the refund. + connectorOrderId = "connector_order_id" // Connector-side identifier for the original payment that this refund targets. + }.build() +} + +private fun buildVoidRequest(connectorTransactionIdStr: String): PaymentServiceVoidRequest { + return PaymentServiceVoidRequest.newBuilder().apply { + merchantVoidId = "probe_void_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 { + val paymentClient = PaymentClient(config) + + // Step 1: Authorize — reserve funds on the payment method + val authorizeResponse = paymentClient.authorize(buildAuthorizeRequest("AUTOMATIC")) + + when (authorizeResponse.status.name) { + "FAILED" -> throw RuntimeException("Payment failed: ${authorizeResponse.error.unifiedDetails.message}") + "PENDING" -> return mapOf("status" to "PENDING") // await webhook before proceeding + } + + return mapOf("status" to authorizeResponse.status.name, "transactionId" to authorizeResponse.connectorTransactionId, "error" to authorizeResponse.error) +} + +// Scenario: Card Payment (Authorize + Capture) +// Two-step card payment. First authorize, then capture. Use when you need to verify funds before finalizing. +fun processCheckoutCard(txnId: String, config: ConnectorConfig = _defaultConfig): Map { + val paymentClient = PaymentClient(config) + + // Step 1: Authorize — reserve funds on the payment method + val authorizeResponse = paymentClient.authorize(buildAuthorizeRequest("MANUAL")) + + when (authorizeResponse.status.name) { + "FAILED" -> throw RuntimeException("Payment failed: ${authorizeResponse.error.unifiedDetails.message}") + "PENDING" -> return mapOf("status" to "PENDING") // await webhook before proceeding + } + + // Step 2: Capture — settle the reserved funds + val captureResponse = paymentClient.capture(buildCaptureRequest(authorizeResponse.connectorTransactionId ?: "")) + + if (captureResponse.status.name == "FAILED") + throw RuntimeException("Capture failed: ${captureResponse.error.unifiedDetails.message}") + + return mapOf("status" to captureResponse.status.name, "transactionId" to authorizeResponse.connectorTransactionId, "error" to authorizeResponse.error) +} + +// Scenario: Refund +// Return funds to the customer for a completed payment. +fun processRefund(txnId: String, config: ConnectorConfig = _defaultConfig): Map { + val paymentClient = PaymentClient(config) + + // Step 1: Authorize — reserve funds on the payment method + val authorizeResponse = paymentClient.authorize(buildAuthorizeRequest("AUTOMATIC")) + + when (authorizeResponse.status.name) { + "FAILED" -> throw RuntimeException("Payment failed: ${authorizeResponse.error.unifiedDetails.message}") + "PENDING" -> return mapOf("status" to "PENDING") // await webhook before proceeding + } + + // Step 2: Refund — return funds to the customer + val refundResponse = paymentClient.refund(buildRefundRequest(authorizeResponse.connectorTransactionId ?: "")) + + if (refundResponse.status.name == "FAILED") + throw RuntimeException("Refund failed: ${refundResponse.error.unifiedDetails.message}") + + return mapOf("status" to refundResponse.status.name, "error" to refundResponse.error) +} + +// Scenario: Void Payment +// Cancel an authorized but not-yet-captured payment. +fun processVoidPayment(txnId: String, config: ConnectorConfig = _defaultConfig): Map { + val paymentClient = PaymentClient(config) + + // Step 1: Authorize — reserve funds on the payment method + val authorizeResponse = paymentClient.authorize(buildAuthorizeRequest("MANUAL")) + + when (authorizeResponse.status.name) { + "FAILED" -> throw RuntimeException("Payment failed: ${authorizeResponse.error.unifiedDetails.message}") + "PENDING" -> return mapOf("status" to "PENDING") // await webhook before proceeding + } + + // Step 2: Void — release reserved funds (cancel authorization) + val voidResponse = paymentClient.void(buildVoidRequest(authorizeResponse.connectorTransactionId ?: "")) + + return mapOf("status" to voidResponse.status.name, "transactionId" to authorizeResponse.connectorTransactionId, "error" to voidResponse.error) +} + +// Scenario: Get Payment Status +// Retrieve current payment status from the connector. +fun processGetPayment(txnId: String, config: ConnectorConfig = _defaultConfig): Map { + val paymentClient = PaymentClient(config) + + // Step 1: Authorize — reserve funds on the payment method + val authorizeResponse = paymentClient.authorize(buildAuthorizeRequest("MANUAL")) + + when (authorizeResponse.status.name) { + "FAILED" -> throw RuntimeException("Payment failed: ${authorizeResponse.error.unifiedDetails.message}") + "PENDING" -> return mapOf("status" to "PENDING") // await webhook before proceeding + } + + // Step 2: Get — retrieve current payment status from the connector + val getResponse = paymentClient.get(buildGetRequest(authorizeResponse.connectorTransactionId ?: "")) + + return mapOf("status" to getResponse.status.name, "transactionId" to getResponse.connectorTransactionId, "error" to getResponse.error) +} + +// Flow: PaymentService.Authorize (Card) +fun authorize(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = PaymentClient(config) + val request = buildAuthorizeRequest("AUTOMATIC") + val response = client.authorize(request) + when (response.status.name) { + "FAILED" -> throw RuntimeException("Authorize failed: ${response.error.unifiedDetails.message}") + "PENDING" -> println("Pending — await webhook before proceeding") + else -> println("Authorized: ${response.connectorTransactionId}") + } +} + +// Flow: PaymentService.Capture +fun capture(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = PaymentClient(config) + val request = buildCaptureRequest("probe_connector_txn_001") + val response = client.capture(request) + if (response.status.name == "FAILED") + throw RuntimeException("Capture failed: ${response.error.unifiedDetails.message}") + println("Done: ${response.status.name}") +} + +// Flow: PaymentService.Get +fun get(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = PaymentClient(config) + val request = buildGetRequest("probe_connector_txn_001") + val response = client.get(request) + println("Status: ${response.status.name}") +} + +// Flow: PaymentService.ProxyAuthorize +fun proxyAuthorize(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = PaymentClient(config) + val request = PaymentServiceProxyAuthorizeRequest.newBuilder().apply { + merchantTransactionId = "probe_proxy_txn_001" + amountBuilder.apply { + minorAmount = 1000L // Amount in minor units (e.g., 1000 = $10.00). + currency = Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + } + cardProxyBuilder.apply { // Card proxy for vault-aliased payments (VGS, Basis Theory, Spreedly). Real card values are substituted by the proxy before reaching the connector. + cardNumberBuilder.value = "4111111111111111" // Card Identification. + cardExpMonthBuilder.value = "03" + cardExpYearBuilder.value = "2030" + cardCvcBuilder.value = "123" + cardHolderNameBuilder.value = "John Doe" // Cardholder Information. + cardNetwork = CardNetwork.VISA + } + customerBuilder.apply { + emailBuilder.value = "test@example.com" // Customer's email address. + } + addressBuilder.apply { + billingAddressBuilder.apply { + firstNameBuilder.value = "John" // Personal Information. + lastNameBuilder.value = "Doe" + countryAlpha2Code = CountryAlpha2.US + } + } + captureMethod = CaptureMethod.AUTOMATIC + authType = AuthenticationType.NO_THREE_DS + returnUrl = "https://example.com/return" + }.build() + val response = client.proxy_authorize(request) + println("Status: ${response.status.name}") +} + +// Flow: PaymentService.Refund +fun refund(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = PaymentClient(config) + val request = buildRefundRequest("probe_connector_txn_001") + val response = client.refund(request) + if (response.status.name == "FAILED") + throw RuntimeException("Refund failed: ${response.error.unifiedDetails.message}") + println("Done: ${response.status.name}") +} + +// Flow: PaymentService.Void +fun void(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = PaymentClient(config) + val request = buildVoidRequest("probe_connector_txn_001") + val response = client.void(request) + if (response.status.name == "FAILED") + throw RuntimeException("Void failed: ${response.error.unifiedDetails.message}") + println("Done: ${response.status.name}") +} + + +fun main(args: Array) { + val txnId = "order_001" + val flow = args.firstOrNull() ?: "processCheckoutAutocapture" + when (flow) { + "processCheckoutAutocapture" -> processCheckoutAutocapture(txnId) + "processCheckoutCard" -> processCheckoutCard(txnId) + "processRefund" -> processRefund(txnId) + "processVoidPayment" -> processVoidPayment(txnId) + "processGetPayment" -> processGetPayment(txnId) + "authorize" -> authorize(txnId) + "capture" -> capture(txnId) + "get" -> get(txnId) + "proxyAuthorize" -> proxyAuthorize(txnId) + "refund" -> refund(txnId) + "void" -> void(txnId) + else -> System.err.println("Unknown flow: $flow. Available: processCheckoutAutocapture, processCheckoutCard, processRefund, processVoidPayment, processGetPayment, authorize, capture, get, proxyAuthorize, refund, void") + } +} diff --git a/examples/ilixium/ilixium.py b/examples/ilixium/ilixium.py new file mode 100644 index 0000000000..e8536e3660 --- /dev/null +++ b/examples/ilixium/ilixium.py @@ -0,0 +1,297 @@ +# 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 ilixium +# +# Ilixium — all integration scenarios and flows in one file. +# Run a scenario: python3 ilixium.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", "capture", "get", "proxy_authorize", "refund", "void"] + +_default_config = sdk_config_pb2.ConnectorConfig( + options=sdk_config_pb2.SdkOptions(environment=sdk_config_pb2.Environment.SANDBOX), + connector_config=payment_pb2.ConnectorSpecificConfig( + ilixium=payment_pb2.IlixiumConfig( + api_key=payment_methods_pb2.SecretString(value="YOUR_API_KEY"), + key1=payment_methods_pb2.SecretString(value="YOUR_KEY1"), + api_secret=payment_methods_pb2.SecretString(value="YOUR_API_SECRET"), + 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. + customer=payment_pb2.Customer( # Customer Information. + email=payment_methods_pb2.SecretString(value="test@example.com"), # Customer's email address. + ), + 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"), + country_alpha2_code=payment_methods_pb2.CountryAlpha2.Value("US"), + ), + ), + auth_type=payment_pb2.AuthenticationType.Value("NO_THREE_DS"), # Authentication Details. + return_url="https://example.com/return", # URLs for Redirection and Webhooks. + ) + +def _build_capture_request(connector_transaction_id: str): + return payment_pb2.PaymentServiceCaptureRequest( + merchant_capture_id="probe_capture_001", # Identification. + connector_transaction_id=connector_transaction_id, + amount_to_capture=payment_pb2.Money( # Capture Details. + minor_amount=1000, # Amount in minor units (e.g., 1000 = $10.00). + currency=payment_pb2.Currency.Value("USD"), # ISO 4217 currency code (e.g., "USD", "EUR"). + ), + ) + +def _build_get_request(connector_transaction_id: str): + return payment_pb2.PaymentServiceGetRequest( + merchant_transaction_id="probe_merchant_txn_001", # Identification. + connector_transaction_id=connector_transaction_id, + amount=payment_pb2.Money( # Amount Information. + minor_amount=1000, # Amount in minor units (e.g., 1000 = $10.00). + currency=payment_pb2.Currency.Value("USD"), # ISO 4217 currency code (e.g., "USD", "EUR"). + ), + ) + +def _build_proxy_authorize_request(): + return payment_pb2.PaymentServiceProxyAuthorizeRequest( + merchant_transaction_id="probe_proxy_txn_001", + amount=payment_pb2.Money( + minor_amount=1000, # Amount in minor units (e.g., 1000 = $10.00). + currency=payment_pb2.Currency.Value("USD"), # ISO 4217 currency code (e.g., "USD", "EUR"). + ), + card_proxy=payment_methods_pb2.ProxyCardDetails( # Card proxy for vault-aliased payments (VGS, Basis Theory, Spreedly). Real card values are substituted by the proxy before reaching the connector. + card_number=payment_methods_pb2.SecretString(value="4111111111111111"), # Card Identification. + card_exp_month=payment_methods_pb2.SecretString(value="03"), + card_exp_year=payment_methods_pb2.SecretString(value="2030"), + card_cvc=payment_methods_pb2.SecretString(value="123"), + card_holder_name=payment_methods_pb2.SecretString(value="John Doe"), # Cardholder Information. + card_network=payment_methods_pb2.CardNetwork.Value("VISA"), + ), + customer=payment_pb2.Customer( + email=payment_methods_pb2.SecretString(value="test@example.com"), # Customer's email address. + ), + 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"), + country_alpha2_code=payment_methods_pb2.CountryAlpha2.Value("US"), + ), + ), + capture_method=payment_pb2.CaptureMethod.Value("AUTOMATIC"), + auth_type=payment_pb2.AuthenticationType.Value("NO_THREE_DS"), + return_url="https://example.com/return", + ) + +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. + connector_order_id="connector_order_id", # Connector-side identifier for the original payment that this refund targets. + ) + +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, + 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"). + ), + ) +async def process_checkout_autocapture(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """One-step Payment (Authorize + Capture) + + Simple payment that authorizes and captures in one call. Use for immediate charges. + """ + payment_client = PaymentClient(config) + + # Step 1: Authorize — reserve funds on the payment method + authorize_response = await payment_client.authorize(_build_authorize_request("AUTOMATIC")) + + if authorize_response.status == "FAILED": + raise RuntimeError(f"Payment failed: {authorize_response.error}") + if authorize_response.status == "PENDING": + # Awaiting async confirmation — handle via webhook + return {"status": "pending", "transaction_id": authorize_response.connector_transaction_id} + + return {"status": getattr(authorize_response, "status", ""), "transaction_id": getattr(authorize_response, "connector_transaction_id", ""), "error": getattr(authorize_response, "error", None)} + + +async def process_checkout_card(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Card Payment (Authorize + Capture) + + Two-step card payment. First authorize, then capture. Use when you need to verify funds before finalizing. + """ + payment_client = PaymentClient(config) + + # Step 1: Authorize — reserve funds on the payment method + authorize_response = await payment_client.authorize(_build_authorize_request("MANUAL")) + + if authorize_response.status == "FAILED": + raise RuntimeError(f"Payment failed: {authorize_response.error}") + if authorize_response.status == "PENDING": + # Awaiting async confirmation — handle via webhook + return {"status": "pending", "transaction_id": authorize_response.connector_transaction_id} + + # Step 2: Capture — settle the reserved funds + capture_response = await payment_client.capture(_build_capture_request(authorize_response.connector_transaction_id)) + + if capture_response.status == "FAILED": + raise RuntimeError(f"Capture failed: {capture_response.error}") + + return {"status": getattr(capture_response, "status", ""), "transaction_id": getattr(authorize_response, "connector_transaction_id", ""), "error": getattr(capture_response, "error", None)} + + +async def process_refund(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Refund + + Return funds to the customer for a completed payment. + """ + payment_client = PaymentClient(config) + + # Step 1: Authorize — reserve funds on the payment method + authorize_response = await payment_client.authorize(_build_authorize_request("AUTOMATIC")) + + if authorize_response.status == "FAILED": + raise RuntimeError(f"Payment failed: {authorize_response.error}") + if authorize_response.status == "PENDING": + # Awaiting async confirmation — handle via webhook + return {"status": "pending", "transaction_id": authorize_response.connector_transaction_id} + + # Step 2: Refund — return funds to the customer + refund_response = await payment_client.refund(_build_refund_request(authorize_response.connector_transaction_id)) + + if refund_response.status == "FAILED": + raise RuntimeError(f"Refund failed: {refund_response.error}") + + return {"status": getattr(refund_response, "status", ""), "error": getattr(refund_response, "error", None)} + + +async def process_void_payment(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Void Payment + + Cancel an authorized but not-yet-captured payment. + """ + payment_client = PaymentClient(config) + + # Step 1: Authorize — reserve funds on the payment method + authorize_response = await payment_client.authorize(_build_authorize_request("MANUAL")) + + if authorize_response.status == "FAILED": + raise RuntimeError(f"Payment failed: {authorize_response.error}") + if authorize_response.status == "PENDING": + # Awaiting async confirmation — handle via webhook + return {"status": "pending", "transaction_id": authorize_response.connector_transaction_id} + + # Step 2: Void — release reserved funds (cancel authorization) + void_response = await payment_client.void(_build_void_request(authorize_response.connector_transaction_id)) + + return {"status": getattr(void_response, "status", ""), "transaction_id": getattr(authorize_response, "connector_transaction_id", ""), "error": getattr(void_response, "error", None)} + + +async def process_get_payment(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Get Payment Status + + Retrieve current payment status from the connector. + """ + payment_client = PaymentClient(config) + + # Step 1: Authorize — reserve funds on the payment method + authorize_response = await payment_client.authorize(_build_authorize_request("MANUAL")) + + if authorize_response.status == "FAILED": + raise RuntimeError(f"Payment failed: {authorize_response.error}") + if authorize_response.status == "PENDING": + # Awaiting async confirmation — handle via webhook + return {"status": "pending", "transaction_id": authorize_response.connector_transaction_id} + + # Step 2: Get — retrieve current payment status from the connector + get_response = await payment_client.get(_build_get_request(authorize_response.connector_transaction_id)) + + return {"status": getattr(get_response, "status", ""), "transaction_id": getattr(get_response, "connector_transaction_id", ""), "error": getattr(get_response, "error", None)} + + +async def process_authorize(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Flow: PaymentService.Authorize (Card)""" + payment_client = PaymentClient(config) + + authorize_response = await payment_client.authorize(_build_authorize_request("AUTOMATIC")) + + return {"status": authorize_response.status, "transaction_id": authorize_response.connector_transaction_id} + + +async def process_capture(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Flow: PaymentService.Capture""" + payment_client = PaymentClient(config) + + capture_response = await payment_client.capture(_build_capture_request("probe_connector_txn_001")) + + return {"status": capture_response.status} + + +async def process_get(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Flow: PaymentService.Get""" + payment_client = PaymentClient(config) + + get_response = await payment_client.get(_build_get_request("probe_connector_txn_001")) + + return {"status": get_response.status} + + +async def process_proxy_authorize(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Flow: PaymentService.ProxyAuthorize""" + payment_client = PaymentClient(config) + + proxy_response = await payment_client.proxy_authorize(_build_proxy_authorize_request()) + + return {"status": proxy_response.status} + + +async def process_void(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Flow: PaymentService.Void""" + payment_client = PaymentClient(config) + + void_response = await payment_client.void(_build_void_request("probe_connector_txn_001")) + + return {"status": void_response.status} + +if __name__ == "__main__": + scenario = sys.argv[1] if len(sys.argv) > 1 else "checkout_autocapture" + fn = globals().get(f"process_{scenario}") + if not fn: + available = [k[8:] for k in globals() if k.startswith("process_")] + print(f"Unknown scenario: {scenario}. Available: {available}", file=sys.stderr) + sys.exit(1) + asyncio.run(fn("order_001")) diff --git a/examples/ilixium/ilixium.rs b/examples/ilixium/ilixium.rs new file mode 100644 index 0000000000..37c4f43878 --- /dev/null +++ b/examples/ilixium/ilixium.rs @@ -0,0 +1,485 @@ +// 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 ilixium +// +// Ilixium — all scenarios and flows in one file. +// Run a scenario: cargo run --example ilixium -- process_checkout_card +use cards::CardNumber; +use grpc_api_types::payments::connector_specific_config; +use grpc_api_types::payments::payment_method; +use grpc_api_types::payments::*; +use hyperswitch_masking::Secret; +use hyperswitch_payments_client::ConnectorClient; +use std::collections::HashMap; +use std::str::FromStr; + +#[allow(dead_code)] +pub const SUPPORTED_FLOWS: &[&str] = &[ + "authorize", + "capture", + "get", + "proxy_authorize", + "refund", + "void", +]; + +#[allow(dead_code)] +fn build_client() -> ConnectorClient { + // Configure the connector with authentication + let config = ConnectorConfig { + connector_config: Some(ConnectorSpecificConfig { + config: Some(connector_specific_config::Config::Ilixium(IlixiumConfig { + 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 + api_secret: Some(hyperswitch_masking::Secret::new( + "YOUR_API_SECRET".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. + customer: Some(Customer { + // Customer Information. + email: Some(Secret::new("test@example.com".to_string())), // Customer's email address. + ..Default::default() + }), + 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())), + country_alpha2_code: Some(CountryAlpha2::Us.into()), + ..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. + ..Default::default() + } +} + +pub fn build_capture_request(connector_transaction_id: &str) -> PaymentServiceCaptureRequest { + PaymentServiceCaptureRequest { + merchant_capture_id: Some("probe_capture_001".to_string()), // Identification. + connector_transaction_id: connector_transaction_id.to_string(), + amount_to_capture: Some(Money { + // Capture Details. + minor_amount: 1000, // Amount in minor units (e.g., 1000 = $10.00). + currency: Currency::Usd.into(), // ISO 4217 currency code (e.g., "USD", "EUR"). + }), + ..Default::default() + } +} + +pub fn build_get_request(connector_transaction_id: &str) -> PaymentServiceGetRequest { + PaymentServiceGetRequest { + merchant_transaction_id: Some("probe_merchant_txn_001".to_string()), // Identification. + connector_transaction_id: connector_transaction_id.to_string(), + amount: Some(Money { + // Amount Information. + minor_amount: 1000, // Amount in minor units (e.g., 1000 = $10.00). + currency: Currency::Usd.into(), // ISO 4217 currency code (e.g., "USD", "EUR"). + }), + ..Default::default() + } +} + +pub fn build_proxy_authorize_request() -> PaymentServiceProxyAuthorizeRequest { + PaymentServiceProxyAuthorizeRequest { + merchant_transaction_id: Some("probe_proxy_txn_001".to_string()), + amount: Some(Money { + minor_amount: 1000, // Amount in minor units (e.g., 1000 = $10.00). + currency: Currency::Usd.into(), // ISO 4217 currency code (e.g., "USD", "EUR"). + }), + card_proxy: Some(ProxyCardDetails { + // Card proxy for vault-aliased payments (VGS, Basis Theory, Spreedly). Real card values are substituted by the proxy before reaching the connector. + card_number: Some(Secret::new("4111111111111111".to_string())), // Card Identification. + card_exp_month: Some(Secret::new("03".to_string())), + card_exp_year: Some(Secret::new("2030".to_string())), + card_cvc: Some(Secret::new("123".to_string())), + card_holder_name: Some(Secret::new("John Doe".to_string())), // Cardholder Information. + card_network: Some(CardNetwork::Visa.into()), + ..Default::default() + }), + customer: Some(Customer { + email: Some(Secret::new("test@example.com".to_string())), // Customer's email address. + ..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())), + country_alpha2_code: Some(CountryAlpha2::Us.into()), + ..Default::default() + }), + ..Default::default() + }), + capture_method: Some(CaptureMethod::Automatic.into()), + auth_type: AuthenticationType::NoThreeDs.into(), + return_url: Some("https://example.com/return".to_string()), + ..Default::default() + } +} + +pub fn build_refund_request(connector_transaction_id: &str) -> PaymentServiceRefundRequest { + PaymentServiceRefundRequest { + merchant_refund_id: Some("probe_refund_001".to_string()), // Identification. + connector_transaction_id: connector_transaction_id.to_string(), + payment_amount: 1000, // Amount Information. + refund_amount: Some(Money { + minor_amount: 1000, // Amount in minor units (e.g., 1000 = $10.00). + currency: Currency::Usd.into(), // ISO 4217 currency code (e.g., "USD", "EUR"). + }), + reason: Some("customer_request".to_string()), // Reason for the refund. + connector_order_id: Some("connector_order_id".to_string()), // Connector-side identifier for the original payment that this refund targets. + ..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(), + 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() + } +} + +// Scenario: One-step Payment (Authorize + Capture) +// Simple payment that authorizes and captures in one call. Use for immediate charges. +#[allow(dead_code)] +pub async fn process_checkout_autocapture( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + // Step 1: Authorize — reserve funds on the payment method + let authorize_response = client + .authorize(build_authorize_request("AUTOMATIC"), &HashMap::new(), None) + .await?; + + match authorize_response.status() { + PaymentStatus::Failure | PaymentStatus::AuthorizationFailed => { + return Err(format!("Payment failed: {:?}", authorize_response.error).into()) + } + PaymentStatus::Pending => return Ok("pending — awaiting webhook".to_string()), + _ => {} + } + + Ok(format!( + "Payment: {:?} — {}", + authorize_response.status(), + authorize_response + .connector_transaction_id + .as_deref() + .unwrap_or("") + )) +} + +// Scenario: Card Payment (Authorize + Capture) +// Two-step card payment. First authorize, then capture. Use when you need to verify funds before finalizing. +#[allow(dead_code)] +pub async fn process_checkout_card( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + // Step 1: Authorize — reserve funds on the payment method + let authorize_response = client + .authorize(build_authorize_request("MANUAL"), &HashMap::new(), None) + .await?; + + match authorize_response.status() { + PaymentStatus::Failure | PaymentStatus::AuthorizationFailed => { + return Err(format!("Payment failed: {:?}", authorize_response.error).into()) + } + PaymentStatus::Pending => return Ok("pending — awaiting webhook".to_string()), + _ => {} + } + + // Step 2: Capture — settle the reserved funds + let capture_response = client + .capture( + build_capture_request( + authorize_response + .connector_transaction_id + .as_deref() + .unwrap_or(""), + ), + &HashMap::new(), + None, + ) + .await?; + + if capture_response.status() == PaymentStatus::Failure { + return Err(format!("Capture failed: {:?}", capture_response.error).into()); + } + + Ok(format!( + "Payment completed: {}", + authorize_response + .connector_transaction_id + .as_deref() + .unwrap_or("") + )) +} + +// Scenario: Refund +// Return funds to the customer for a completed payment. +#[allow(dead_code)] +pub async fn process_refund( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + // Step 1: Authorize — reserve funds on the payment method + let authorize_response = client + .authorize(build_authorize_request("AUTOMATIC"), &HashMap::new(), None) + .await?; + + match authorize_response.status() { + PaymentStatus::Failure | PaymentStatus::AuthorizationFailed => { + return Err(format!("Payment failed: {:?}", authorize_response.error).into()) + } + PaymentStatus::Pending => return Ok("pending — awaiting webhook".to_string()), + _ => {} + } + + // Step 2: Refund — return funds to the customer + let refund_response = client + .refund( + build_refund_request( + authorize_response + .connector_transaction_id + .as_deref() + .unwrap_or(""), + ), + &HashMap::new(), + None, + ) + .await?; + + if refund_response.status() == RefundStatus::RefundFailure { + return Err(format!("Refund failed: {:?}", refund_response.error).into()); + } + + Ok(format!("Refunded: {:?}", refund_response.status())) +} + +// Scenario: Void Payment +// Cancel an authorized but not-yet-captured payment. +#[allow(dead_code)] +pub async fn process_void_payment( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + // Step 1: Authorize — reserve funds on the payment method + let authorize_response = client + .authorize(build_authorize_request("MANUAL"), &HashMap::new(), None) + .await?; + + match authorize_response.status() { + PaymentStatus::Failure | PaymentStatus::AuthorizationFailed => { + return Err(format!("Payment failed: {:?}", authorize_response.error).into()) + } + PaymentStatus::Pending => return Ok("pending — awaiting webhook".to_string()), + _ => {} + } + + // Step 2: Void — release reserved funds (cancel authorization) + let void_response = client + .void( + build_void_request( + authorize_response + .connector_transaction_id + .as_deref() + .unwrap_or(""), + ), + &HashMap::new(), + None, + ) + .await?; + + Ok(format!("Voided: {:?}", void_response.status())) +} + +// Scenario: Get Payment Status +// Retrieve current payment status from the connector. +#[allow(dead_code)] +pub async fn process_get_payment( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + // Step 1: Authorize — reserve funds on the payment method + let authorize_response = client + .authorize(build_authorize_request("MANUAL"), &HashMap::new(), None) + .await?; + + match authorize_response.status() { + PaymentStatus::Failure | PaymentStatus::AuthorizationFailed => { + return Err(format!("Payment failed: {:?}", authorize_response.error).into()) + } + PaymentStatus::Pending => return Ok("pending — awaiting webhook".to_string()), + _ => {} + } + + // Step 2: Get — retrieve current payment status from the connector + let get_response = client + .get( + build_get_request( + authorize_response + .connector_transaction_id + .as_deref() + .unwrap_or(""), + ), + &HashMap::new(), + None, + ) + .await?; + + Ok(format!("Status: {:?}", get_response.status())) +} + +// Flow: PaymentService.Authorize (Card) +#[allow(dead_code)] +pub async fn process_authorize( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + let response = client + .authorize(build_authorize_request("AUTOMATIC"), &HashMap::new(), None) + .await?; + match response.status() { + PaymentStatus::Failure | PaymentStatus::AuthorizationFailed => { + Err(format!("Authorize failed: {:?}", response.error).into()) + } + PaymentStatus::Pending => Ok("pending — await webhook".to_string()), + _ => Ok(format!( + "Authorized: {}", + response.connector_transaction_id.as_deref().unwrap_or("") + )), + } +} + +// Flow: PaymentService.Capture +#[allow(dead_code)] +pub async fn process_capture( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + let response = client + .capture( + build_capture_request("probe_connector_txn_001"), + &HashMap::new(), + None, + ) + .await?; + Ok(format!("status: {:?}", response.status())) +} + +// Flow: PaymentService.Get +#[allow(dead_code)] +pub async fn process_get( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + let response = client + .get( + build_get_request("probe_connector_txn_001"), + &HashMap::new(), + None, + ) + .await?; + Ok(format!("status: {:?}", response.status())) +} + +// Flow: PaymentService.ProxyAuthorize +#[allow(dead_code)] +pub async fn process_proxy_authorize( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + let response = client + .proxy_authorize(build_proxy_authorize_request(), &HashMap::new(), None) + .await?; + Ok(format!("status: {:?}", response.status())) +} + +// Flow: PaymentService.Void +#[allow(dead_code)] +pub async fn process_void( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + let response = client + .void( + build_void_request("probe_connector_txn_001"), + &HashMap::new(), + None, + ) + .await?; + Ok(format!("status: {:?}", response.status())) +} + +#[allow(dead_code)] +#[tokio::main] +async fn main() { + let client = build_client(); + let flow = std::env::args() + .nth(1) + .unwrap_or_else(|| "process_checkout_autocapture".to_string()); + let result: Result> = match flow.as_str() { + "process_checkout_autocapture" => process_checkout_autocapture(&client, "order_001").await, + "process_checkout_card" => process_checkout_card(&client, "order_001").await, + "process_refund" => process_refund(&client, "order_001").await, + "process_void_payment" => process_void_payment(&client, "order_001").await, + "process_get_payment" => process_get_payment(&client, "order_001").await, + "process_authorize" => process_authorize(&client, "txn_001").await, + "process_capture" => process_capture(&client, "txn_001").await, + "process_get" => process_get(&client, "txn_001").await, + "process_proxy_authorize" => process_proxy_authorize(&client, "txn_001").await, + "process_void" => process_void(&client, "txn_001").await, + _ => { + eprintln!("Unknown flow: {}. Available: process_checkout_autocapture, process_checkout_card, process_refund, process_void_payment, process_get_payment, process_authorize, process_capture, process_get, process_proxy_authorize, process_void", flow); + return; + } + }; + match result { + Ok(msg) => println!("✓ {msg}"), + Err(e) => eprintln!("✗ {e}"), + } +} diff --git a/examples/ilixium/ilixium.ts b/examples/ilixium/ilixium.ts new file mode 100644 index 0000000000..a16dc50dce --- /dev/null +++ b/examples/ilixium/ilixium.ts @@ -0,0 +1,327 @@ +// 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 ilixium +// +// Ilixium — all integration scenarios and flows in one file. +// Run a scenario: npx tsx ilixium.ts checkout_autocapture + +import { PaymentClient, types } from 'hyperswitch-prism'; +const { Environment, AuthenticationType, CaptureMethod, CardNetwork, CountryAlpha2, Currency } = types; +export const SUPPORTED_FLOWS = ["authorize", "capture", "get", "proxy_authorize", "refund", "void"]; + +const _defaultConfig: types.IConnectorConfig = { + options: { + environment: Environment.SANDBOX, + }, + connectorConfig: { + ilixium: { + apiKey: { value: 'YOUR_API_KEY' }, + key1: { value: 'YOUR_KEY1' }, + apiSecret: { value: 'YOUR_API_SECRET' }, + 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. + "customer": { // Customer Information. + "email": {"value": "test@example.com"} // Customer's email address. + }, + "address": { // Address Information. + "billingAddress": { + "firstName": {"value": "John"}, // Personal Information. + "lastName": {"value": "Doe"}, + "countryAlpha2Code": CountryAlpha2.US + } + }, + "authType": AuthenticationType.NO_THREE_DS, // Authentication Details. + "returnUrl": "https://example.com/return" // URLs for Redirection and Webhooks. + }; +} + +function _buildCaptureRequest(connectorTransactionId: string): types.IPaymentServiceCaptureRequest { + return { + "merchantCaptureId": "probe_capture_001", // Identification. + "connectorTransactionId": connectorTransactionId, + "amountToCapture": { // Capture Details. + "minorAmount": 1000, // Amount in minor units (e.g., 1000 = $10.00). + "currency": Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + } + }; +} + +function _buildGetRequest(connectorTransactionId: string): types.IPaymentServiceGetRequest { + return { + "merchantTransactionId": "probe_merchant_txn_001", // Identification. + "connectorTransactionId": connectorTransactionId, + "amount": { // Amount Information. + "minorAmount": 1000, // Amount in minor units (e.g., 1000 = $10.00). + "currency": Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + } + }; +} + +function _buildProxyAuthorizeRequest(): types.IPaymentServiceProxyAuthorizeRequest { + return { + "merchantTransactionId": "probe_proxy_txn_001", + "amount": { + "minorAmount": 1000, // Amount in minor units (e.g., 1000 = $10.00). + "currency": Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + }, + "cardProxy": { // Card proxy for vault-aliased payments (VGS, Basis Theory, Spreedly). Real card values are substituted by the proxy before reaching the connector. + "cardNumber": {"value": "4111111111111111"}, // Card Identification. + "cardExpMonth": {"value": "03"}, + "cardExpYear": {"value": "2030"}, + "cardCvc": {"value": "123"}, + "cardHolderName": {"value": "John Doe"}, // Cardholder Information. + "cardNetwork": CardNetwork.VISA + }, + "customer": { + "email": {"value": "test@example.com"} // Customer's email address. + }, + "address": { + "billingAddress": { + "firstName": {"value": "John"}, // Personal Information. + "lastName": {"value": "Doe"}, + "countryAlpha2Code": CountryAlpha2.US + } + }, + "captureMethod": CaptureMethod.AUTOMATIC, + "authType": AuthenticationType.NO_THREE_DS, + "returnUrl": "https://example.com/return" + }; +} + +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. + "connectorOrderId": "connector_order_id" // Connector-side identifier for the original payment that this refund targets. + }; +} + +function _buildVoidRequest(connectorTransactionId: string): types.IPaymentServiceVoidRequest { + return { + "merchantVoidId": "probe_void_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"). + } + }; +} + + +// ANCHOR: scenario_functions +// One-step Payment (Authorize + Capture) +// Simple payment that authorizes and captures in one call. Use for immediate charges. +async function processCheckoutAutocapture(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + // Step 1: Authorize — reserve funds on the payment method + const authorizeResponse = await paymentClient.authorize(_buildAuthorizeRequest(CaptureMethod.AUTOMATIC)); + + if (authorizeResponse.status === types.PaymentStatus.FAILURE) { + throw new Error(`Payment failed: ${JSON.stringify(authorizeResponse.error)}`); + } + if (authorizeResponse.status === types.PaymentStatus.PENDING) { + // Awaiting async confirmation — handle via webhook + return { status: 'pending', connectorTransactionId: authorizeResponse.connectorTransactionId }; + } + + return { status: authorizeResponse.status, transactionId: authorizeResponse.connectorTransactionId!, error: authorizeResponse.error } as any; +} + +// Card Payment (Authorize + Capture) +// Two-step card payment. First authorize, then capture. Use when you need to verify funds before finalizing. +async function processCheckoutCard(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + // Step 1: Authorize — reserve funds on the payment method + const authorizeResponse = await paymentClient.authorize(_buildAuthorizeRequest(CaptureMethod.MANUAL)); + + if (authorizeResponse.status === types.PaymentStatus.FAILURE) { + throw new Error(`Payment failed: ${JSON.stringify(authorizeResponse.error)}`); + } + if (authorizeResponse.status === types.PaymentStatus.PENDING) { + // Awaiting async confirmation — handle via webhook + return { status: 'pending', connectorTransactionId: authorizeResponse.connectorTransactionId }; + } + + // Step 2: Capture — settle the reserved funds + const captureResponse = await paymentClient.capture(_buildCaptureRequest(authorizeResponse.connectorTransactionId!)); + + if (captureResponse.status === types.PaymentStatus.FAILURE) { + throw new Error(`Capture failed: ${JSON.stringify(captureResponse.error)}`); + } + + return { status: captureResponse.status, transactionId: authorizeResponse.connectorTransactionId!, error: authorizeResponse.error } as any; +} + +// Refund +// Return funds to the customer for a completed payment. +async function processRefund(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + // Step 1: Authorize — reserve funds on the payment method + const authorizeResponse = await paymentClient.authorize(_buildAuthorizeRequest(CaptureMethod.AUTOMATIC)); + + if (authorizeResponse.status === types.PaymentStatus.FAILURE) { + throw new Error(`Payment failed: ${JSON.stringify(authorizeResponse.error)}`); + } + if (authorizeResponse.status === types.PaymentStatus.PENDING) { + // Awaiting async confirmation — handle via webhook + return { status: 'pending', connectorTransactionId: authorizeResponse.connectorTransactionId }; + } + + // Step 2: Refund — return funds to the customer + const refundResponse = await paymentClient.refund(_buildRefundRequest(authorizeResponse.connectorTransactionId!)); + + if (refundResponse.status === types.RefundStatus.REFUND_FAILURE) { + throw new Error(`Refund failed: ${JSON.stringify(refundResponse.error)}`); + } + + return { status: refundResponse.status, error: refundResponse.error } as any; +} + +// Void Payment +// Cancel an authorized but not-yet-captured payment. +async function processVoidPayment(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + // Step 1: Authorize — reserve funds on the payment method + const authorizeResponse = await paymentClient.authorize(_buildAuthorizeRequest(CaptureMethod.MANUAL)); + + if (authorizeResponse.status === types.PaymentStatus.FAILURE) { + throw new Error(`Payment failed: ${JSON.stringify(authorizeResponse.error)}`); + } + if (authorizeResponse.status === types.PaymentStatus.PENDING) { + // Awaiting async confirmation — handle via webhook + return { status: 'pending', connectorTransactionId: authorizeResponse.connectorTransactionId }; + } + + // Step 2: Void — release reserved funds (cancel authorization) + const voidResponse = await paymentClient.void(_buildVoidRequest(authorizeResponse.connectorTransactionId!)); + + return { status: voidResponse.status, transactionId: authorizeResponse.connectorTransactionId!, error: voidResponse.error } as any; +} + +// Get Payment Status +// Retrieve current payment status from the connector. +async function processGetPayment(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + // Step 1: Authorize — reserve funds on the payment method + const authorizeResponse = await paymentClient.authorize(_buildAuthorizeRequest(CaptureMethod.MANUAL)); + + if (authorizeResponse.status === types.PaymentStatus.FAILURE) { + throw new Error(`Payment failed: ${JSON.stringify(authorizeResponse.error)}`); + } + if (authorizeResponse.status === types.PaymentStatus.PENDING) { + // Awaiting async confirmation — handle via webhook + return { status: 'pending', connectorTransactionId: authorizeResponse.connectorTransactionId }; + } + + // Step 2: Get — retrieve current payment status from the connector + const getResponse = await paymentClient.get(_buildGetRequest(authorizeResponse.connectorTransactionId!)); + + return { status: getResponse.status, transactionId: getResponse.connectorTransactionId!, error: getResponse.error } as any; +} + +// Flow: PaymentService.Authorize (Card) +async function authorize(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + const authorizeResponse = await paymentClient.authorize(_buildAuthorizeRequest(CaptureMethod.AUTOMATIC)); + + return authorizeResponse; +} + +// Flow: PaymentService.Capture +async function capture(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + const captureResponse = await paymentClient.capture(_buildCaptureRequest('probe_connector_txn_001')); + + return captureResponse; +} + +// Flow: PaymentService.Get +async function get(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + const getResponse = await paymentClient.get(_buildGetRequest('probe_connector_txn_001')); + + return getResponse; +} + +// Flow: PaymentService.ProxyAuthorize +async function proxyAuthorize(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + const proxyResponse = await paymentClient.proxyAuthorize(_buildProxyAuthorizeRequest()); + + return proxyResponse; +} + +// Flow: PaymentService.Refund +async function refund(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + const refundResponse = await paymentClient.refund(_buildRefundRequest('probe_connector_txn_001')); + + return refundResponse; +} + +// Flow: PaymentService.Void +async function voidPayment(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + const voidResponse = await paymentClient.void(_buildVoidRequest('probe_connector_txn_001')); + + return voidResponse; +} + + +// Export all process* functions for the smoke test +export { + processCheckoutAutocapture, processCheckoutCard, processRefund, processVoidPayment, processGetPayment, authorize, capture, get, proxyAuthorize, refund, voidPayment, _buildAuthorizeRequest, _buildCaptureRequest, _buildGetRequest, _buildProxyAuthorizeRequest, _buildRefundRequest, _buildVoidRequest +}; + +// CLI runner +if (require.main === module) { + const scenario = process.argv[2] || 'checkout_autocapture'; + const key = 'process' + scenario.replace(/_([a-z])/g, (_, l) => l.toUpperCase()).replace(/^(.)/, c => c.toUpperCase()); + const fn = (globalThis as any)[key] || (exports as any)[key]; + if (!fn) { + const available = Object.keys(exports).map(k => + k.replace(/^process/, '').replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '') + ); + console.error(`Unknown scenario: ${scenario}. Available: ${available.join(', ')}`); + process.exit(1); + } + fn('order_001').catch(console.error); +} diff --git a/examples/moneris/moneris.kt b/examples/moneris/moneris.kt new file mode 100644 index 0000000000..9f8ff39118 --- /dev/null +++ b/examples/moneris/moneris.kt @@ -0,0 +1,380 @@ +// 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 moneris +// +// Moneris — all scenarios and flows in one file. +// Run a scenario: ./gradlew run --args="moneris processCheckoutCard" + +package examples.moneris + +import types.Payment.* +import types.PaymentMethods.* +import payments.PaymentClient +import payments.MerchantAuthenticationClient +import payments.RefundClient +import payments.AuthenticationType +import payments.CaptureMethod +import payments.CardNetwork +import payments.Currency +import payments.ConnectorConfig +import payments.SdkOptions +import payments.Environment +import payments.ConnectorSpecificConfig +import types.Payment.MonerisConfig +import payments.SecretString + +val SUPPORTED_FLOWS = listOf("authorize", "capture", "create_server_authentication_token", "get", "proxy_authorize", "refund", "refund_get", "void") + +val _defaultConfig: ConnectorConfig = ConnectorConfig.newBuilder() + .setOptions(SdkOptions.newBuilder().setEnvironment(Environment.SANDBOX).build()) + .setConnectorConfig( + ConnectorSpecificConfig.newBuilder() + .setMoneris(MonerisConfig.newBuilder() + .setClientSecret(SecretString.newBuilder().setValue("YOUR_CLIENT_SECRET").build()) + .setMerchantId(SecretString.newBuilder().setValue("YOUR_MERCHANT_ID").build()) + .setClientId(SecretString.newBuilder().setValue("YOUR_CLIENT_ID").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 { + } + } + authType = AuthenticationType.NO_THREE_DS // Authentication Details. + returnUrl = "https://example.com/return" // URLs for Redirection and Webhooks. + stateBuilder.apply { // State Information. + accessTokenBuilder.apply { // Access token obtained from connector. + tokenBuilder.value = "probe_access_token" // The token string. + expiresInSeconds = 3600L // Expiration timestamp (seconds since epoch). + tokenType = "Bearer" // Token type (e.g., "Bearer", "Basic"). + } + } + }.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"). + } + stateBuilder.apply { // State Information. + accessTokenBuilder.apply { // Access token obtained from connector. + tokenBuilder.value = "probe_access_token" // The token string. + expiresInSeconds = 3600L // Expiration timestamp (seconds since epoch). + tokenType = "Bearer" // Token type (e.g., "Bearer", "Basic"). + } + } + }.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"). + } + stateBuilder.apply { // State Information. + accessTokenBuilder.apply { // Access token obtained from connector. + tokenBuilder.value = "probe_access_token" // The token string. + expiresInSeconds = 3600L // Expiration timestamp (seconds since epoch). + tokenType = "Bearer" // Token type (e.g., "Bearer", "Basic"). + } + } + }.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. + stateBuilder.apply { // State data for access token storage and. + accessTokenBuilder.apply { // Access token obtained from connector. + tokenBuilder.value = "probe_access_token" // The token string. + expiresInSeconds = 3600L // Expiration timestamp (seconds since epoch). + tokenType = "Bearer" // Token type (e.g., "Bearer", "Basic"). + } + } + }.build() +} + +private fun buildVoidRequest(connectorTransactionIdStr: String): PaymentServiceVoidRequest { + return PaymentServiceVoidRequest.newBuilder().apply { + merchantVoidId = "probe_void_001" // Identification. + connectorTransactionId = connectorTransactionIdStr + stateBuilder.apply { // State Information. + accessTokenBuilder.apply { // Access token obtained from connector. + tokenBuilder.value = "probe_access_token" // The token string. + expiresInSeconds = 3600L // Expiration timestamp (seconds since epoch). + tokenType = "Bearer" // Token type (e.g., "Bearer", "Basic"). + } + } + }.build() +} + +// Scenario: One-step Payment (Authorize + Capture) +// Simple payment that authorizes and captures in one call. Use for immediate charges. +fun processCheckoutAutocapture(txnId: String, config: ConnectorConfig = _defaultConfig): Map { + val paymentClient = PaymentClient(config) + + // Step 1: Authorize — reserve funds on the payment method + val authorizeResponse = paymentClient.authorize(buildAuthorizeRequest("AUTOMATIC")) + + when (authorizeResponse.status.name) { + "FAILED" -> throw RuntimeException("Payment failed: ${authorizeResponse.error.unifiedDetails.message}") + "PENDING" -> return mapOf("status" to "PENDING") // await webhook before proceeding + } + + return mapOf("status" to authorizeResponse.status.name, "transactionId" to authorizeResponse.connectorTransactionId, "error" to authorizeResponse.error) +} + +// Scenario: Card Payment (Authorize + Capture) +// Two-step card payment. First authorize, then capture. Use when you need to verify funds before finalizing. +fun processCheckoutCard(txnId: String, config: ConnectorConfig = _defaultConfig): Map { + val paymentClient = PaymentClient(config) + + // Step 1: Authorize — reserve funds on the payment method + val authorizeResponse = paymentClient.authorize(buildAuthorizeRequest("MANUAL")) + + when (authorizeResponse.status.name) { + "FAILED" -> throw RuntimeException("Payment failed: ${authorizeResponse.error.unifiedDetails.message}") + "PENDING" -> return mapOf("status" to "PENDING") // await webhook before proceeding + } + + // Step 2: Capture — settle the reserved funds + val captureResponse = paymentClient.capture(buildCaptureRequest(authorizeResponse.connectorTransactionId ?: "")) + + if (captureResponse.status.name == "FAILED") + throw RuntimeException("Capture failed: ${captureResponse.error.unifiedDetails.message}") + + return mapOf("status" to captureResponse.status.name, "transactionId" to authorizeResponse.connectorTransactionId, "error" to authorizeResponse.error) +} + +// Scenario: Refund +// Return funds to the customer for a completed payment. +fun processRefund(txnId: String, config: ConnectorConfig = _defaultConfig): Map { + val paymentClient = PaymentClient(config) + + // Step 1: Authorize — reserve funds on the payment method + val authorizeResponse = paymentClient.authorize(buildAuthorizeRequest("AUTOMATIC")) + + when (authorizeResponse.status.name) { + "FAILED" -> throw RuntimeException("Payment failed: ${authorizeResponse.error.unifiedDetails.message}") + "PENDING" -> return mapOf("status" to "PENDING") // await webhook before proceeding + } + + // Step 2: Refund — return funds to the customer + val refundResponse = paymentClient.refund(buildRefundRequest(authorizeResponse.connectorTransactionId ?: "")) + + if (refundResponse.status.name == "FAILED") + throw RuntimeException("Refund failed: ${refundResponse.error.unifiedDetails.message}") + + return mapOf("status" to refundResponse.status.name, "error" to refundResponse.error) +} + +// Scenario: Void Payment +// Cancel an authorized but not-yet-captured payment. +fun processVoidPayment(txnId: String, config: ConnectorConfig = _defaultConfig): Map { + val paymentClient = PaymentClient(config) + + // Step 1: Authorize — reserve funds on the payment method + val authorizeResponse = paymentClient.authorize(buildAuthorizeRequest("MANUAL")) + + when (authorizeResponse.status.name) { + "FAILED" -> throw RuntimeException("Payment failed: ${authorizeResponse.error.unifiedDetails.message}") + "PENDING" -> return mapOf("status" to "PENDING") // await webhook before proceeding + } + + // Step 2: Void — release reserved funds (cancel authorization) + val voidResponse = paymentClient.void(buildVoidRequest(authorizeResponse.connectorTransactionId ?: "")) + + return mapOf("status" to voidResponse.status.name, "transactionId" to authorizeResponse.connectorTransactionId, "error" to voidResponse.error) +} + +// Scenario: Get Payment Status +// Retrieve current payment status from the connector. +fun processGetPayment(txnId: String, config: ConnectorConfig = _defaultConfig): Map { + val paymentClient = PaymentClient(config) + + // Step 1: Authorize — reserve funds on the payment method + val authorizeResponse = paymentClient.authorize(buildAuthorizeRequest("MANUAL")) + + when (authorizeResponse.status.name) { + "FAILED" -> throw RuntimeException("Payment failed: ${authorizeResponse.error.unifiedDetails.message}") + "PENDING" -> return mapOf("status" to "PENDING") // await webhook before proceeding + } + + // Step 2: Get — retrieve current payment status from the connector + val getResponse = paymentClient.get(buildGetRequest(authorizeResponse.connectorTransactionId ?: "")) + + return mapOf("status" to getResponse.status.name, "transactionId" to getResponse.connectorTransactionId, "error" to getResponse.error) +} + +// Flow: PaymentService.Authorize (Card) +fun authorize(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = PaymentClient(config) + val request = buildAuthorizeRequest("AUTOMATIC") + val response = client.authorize(request) + when (response.status.name) { + "FAILED" -> throw RuntimeException("Authorize failed: ${response.error.unifiedDetails.message}") + "PENDING" -> println("Pending — await webhook before proceeding") + else -> println("Authorized: ${response.connectorTransactionId}") + } +} + +// Flow: PaymentService.Capture +fun capture(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = PaymentClient(config) + val request = buildCaptureRequest("probe_connector_txn_001") + val response = client.capture(request) + if (response.status.name == "FAILED") + throw RuntimeException("Capture failed: ${response.error.unifiedDetails.message}") + println("Done: ${response.status.name}") +} + +// Flow: MerchantAuthenticationService.CreateServerAuthenticationToken +fun createServerAuthenticationToken(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = MerchantAuthenticationClient(config) + val request = MerchantAuthenticationServiceCreateServerAuthenticationTokenRequest.newBuilder().apply { + + }.build() + val response = client.create_server_authentication_token(request) + println("StatusCode: ${response.statusCode}") +} + +// Flow: PaymentService.Get +fun get(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = PaymentClient(config) + val request = buildGetRequest("probe_connector_txn_001") + val response = client.get(request) + println("Status: ${response.status.name}") +} + +// Flow: PaymentService.ProxyAuthorize +fun proxyAuthorize(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = PaymentClient(config) + val request = PaymentServiceProxyAuthorizeRequest.newBuilder().apply { + merchantTransactionId = "probe_proxy_txn_001" + amountBuilder.apply { + minorAmount = 1000L // Amount in minor units (e.g., 1000 = $10.00). + currency = Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + } + cardProxyBuilder.apply { // Card proxy for vault-aliased payments (VGS, Basis Theory, Spreedly). Real card values are substituted by the proxy before reaching the connector. + cardNumberBuilder.value = "4111111111111111" // Card Identification. + cardExpMonthBuilder.value = "03" + cardExpYearBuilder.value = "2030" + cardCvcBuilder.value = "123" + cardHolderNameBuilder.value = "John Doe" // Cardholder Information. + cardNetwork = CardNetwork.VISA + } + addressBuilder.apply { + billingAddressBuilder.apply { + } + } + captureMethod = CaptureMethod.AUTOMATIC + authType = AuthenticationType.NO_THREE_DS + returnUrl = "https://example.com/return" + stateBuilder.apply { + accessTokenBuilder.apply { // Access token obtained from connector. + tokenBuilder.value = "probe_access_token" // The token string. + expiresInSeconds = 3600L // Expiration timestamp (seconds since epoch). + tokenType = "Bearer" // Token type (e.g., "Bearer", "Basic"). + } + } + }.build() + val response = client.proxy_authorize(request) + println("Status: ${response.status.name}") +} + +// Flow: PaymentService.Refund +fun refund(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = PaymentClient(config) + val request = buildRefundRequest("probe_connector_txn_001") + val response = client.refund(request) + if (response.status.name == "FAILED") + throw RuntimeException("Refund failed: ${response.error.unifiedDetails.message}") + println("Done: ${response.status.name}") +} + +// Flow: RefundService.Get +fun refundGet(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = RefundClient(config) + val request = RefundServiceGetRequest.newBuilder().apply { + merchantRefundId = "probe_refund_001" // Identification. + connectorTransactionId = "probe_connector_txn_001" + refundId = "probe_refund_id_001" // Deprecated. + stateBuilder.apply { // State Information. + accessTokenBuilder.apply { // Access token obtained from connector. + tokenBuilder.value = "probe_access_token" // The token string. + expiresInSeconds = 3600L // Expiration timestamp (seconds since epoch). + tokenType = "Bearer" // Token type (e.g., "Bearer", "Basic"). + } + } + }.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) + "createServerAuthenticationToken" -> createServerAuthenticationToken(txnId) + "get" -> get(txnId) + "proxyAuthorize" -> proxyAuthorize(txnId) + "refund" -> refund(txnId) + "refundGet" -> refundGet(txnId) + "void" -> void(txnId) + else -> System.err.println("Unknown flow: $flow. Available: processCheckoutAutocapture, processCheckoutCard, processRefund, processVoidPayment, processGetPayment, authorize, capture, createServerAuthenticationToken, get, proxyAuthorize, refund, refundGet, void") + } +} diff --git a/examples/moneris/moneris.py b/examples/moneris/moneris.py new file mode 100644 index 0000000000..eddb6be13f --- /dev/null +++ b/examples/moneris/moneris.py @@ -0,0 +1,358 @@ +# 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 moneris +# +# Moneris — all integration scenarios and flows in one file. +# Run a scenario: python3 moneris.py checkout_card + +import asyncio +import sys +from payments import PaymentClient +from payments import MerchantAuthenticationClient +from payments import RefundClient +from payments.generated import sdk_config_pb2, payment_pb2, payment_methods_pb2 + +SUPPORTED_FLOWS = ["authorize", "capture", "create_server_authentication_token", "get", "proxy_authorize", "refund", "refund_get", "void"] + +_default_config = sdk_config_pb2.ConnectorConfig( + options=sdk_config_pb2.SdkOptions(environment=sdk_config_pb2.Environment.SANDBOX), + connector_config=payment_pb2.ConnectorSpecificConfig( + moneris=payment_pb2.MonerisConfig( + client_secret=payment_methods_pb2.SecretString(value="YOUR_CLIENT_SECRET"), + merchant_id=payment_methods_pb2.SecretString(value="YOUR_MERCHANT_ID"), + client_id=payment_methods_pb2.SecretString(value="YOUR_CLIENT_ID"), + 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(), + ), + auth_type=payment_pb2.AuthenticationType.Value("NO_THREE_DS"), # Authentication Details. + return_url="https://example.com/return", # URLs for Redirection and Webhooks. + state=payment_pb2.ConnectorState( # State Information. + access_token=payment_pb2.AccessToken( # Access token obtained from connector. + token=payment_methods_pb2.SecretString(value="probe_access_token"), # The token string. + expires_in_seconds=3600, # Expiration timestamp (seconds since epoch). + token_type="Bearer", # Token type (e.g., "Bearer", "Basic"). + ), + ), + ) + +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"). + ), + state=payment_pb2.ConnectorState( # State Information. + access_token=payment_pb2.AccessToken( # Access token obtained from connector. + token=payment_methods_pb2.SecretString(value="probe_access_token"), # The token string. + expires_in_seconds=3600, # Expiration timestamp (seconds since epoch). + token_type="Bearer", # Token type (e.g., "Bearer", "Basic"). + ), + ), + ) + +def _build_create_server_authentication_token_request(): + return payment_pb2.MerchantAuthenticationServiceCreateServerAuthenticationTokenRequest( + ) + +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"). + ), + state=payment_pb2.ConnectorState( # State Information. + access_token=payment_pb2.AccessToken( # Access token obtained from connector. + token=payment_methods_pb2.SecretString(value="probe_access_token"), # The token string. + expires_in_seconds=3600, # Expiration timestamp (seconds since epoch). + token_type="Bearer", # Token type (e.g., "Bearer", "Basic"). + ), + ), + ) + +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(), + ), + capture_method=payment_pb2.CaptureMethod.Value("AUTOMATIC"), + auth_type=payment_pb2.AuthenticationType.Value("NO_THREE_DS"), + return_url="https://example.com/return", + state=payment_pb2.ConnectorState( + access_token=payment_pb2.AccessToken( # Access token obtained from connector. + token=payment_methods_pb2.SecretString(value="probe_access_token"), # The token string. + expires_in_seconds=3600, # Expiration timestamp (seconds since epoch). + token_type="Bearer", # Token type (e.g., "Bearer", "Basic"). + ), + ), + ) + +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. + state=payment_pb2.ConnectorState( # State data for access token storage and. + access_token=payment_pb2.AccessToken( # Access token obtained from connector. + token=payment_methods_pb2.SecretString(value="probe_access_token"), # The token string. + expires_in_seconds=3600, # Expiration timestamp (seconds since epoch). + token_type="Bearer", # Token type (e.g., "Bearer", "Basic"). + ), + ), + ) + +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. + state=payment_pb2.ConnectorState( # State Information. + access_token=payment_pb2.AccessToken( # Access token obtained from connector. + token=payment_methods_pb2.SecretString(value="probe_access_token"), # The token string. + expires_in_seconds=3600, # Expiration timestamp (seconds since epoch). + token_type="Bearer", # Token type (e.g., "Bearer", "Basic"). + ), + ), + ) + +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, + state=payment_pb2.ConnectorState( # State Information. + access_token=payment_pb2.AccessToken( # Access token obtained from connector. + token=payment_methods_pb2.SecretString(value="probe_access_token"), # The token string. + expires_in_seconds=3600, # Expiration timestamp (seconds since epoch). + token_type="Bearer", # Token type (e.g., "Bearer", "Basic"). + ), + ), + ) +async def process_checkout_autocapture(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """One-step Payment (Authorize + Capture) + + Simple payment that authorizes and captures in one call. Use for immediate charges. + """ + payment_client = PaymentClient(config) + + # Step 1: Authorize — reserve funds on the payment method + authorize_response = await payment_client.authorize(_build_authorize_request("AUTOMATIC")) + + if authorize_response.status == "FAILED": + raise RuntimeError(f"Payment failed: {authorize_response.error}") + if authorize_response.status == "PENDING": + # Awaiting async confirmation — handle via webhook + return {"status": "pending", "transaction_id": authorize_response.connector_transaction_id} + + return {"status": getattr(authorize_response, "status", ""), "transaction_id": getattr(authorize_response, "connector_transaction_id", ""), "error": getattr(authorize_response, "error", None)} + + +async def process_checkout_card(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Card Payment (Authorize + Capture) + + Two-step card payment. First authorize, then capture. Use when you need to verify funds before finalizing. + """ + payment_client = PaymentClient(config) + + # Step 1: Authorize — reserve funds on the payment method + authorize_response = await payment_client.authorize(_build_authorize_request("MANUAL")) + + if authorize_response.status == "FAILED": + raise RuntimeError(f"Payment failed: {authorize_response.error}") + if authorize_response.status == "PENDING": + # Awaiting async confirmation — handle via webhook + return {"status": "pending", "transaction_id": authorize_response.connector_transaction_id} + + # Step 2: Capture — settle the reserved funds + capture_response = await payment_client.capture(_build_capture_request(authorize_response.connector_transaction_id)) + + if capture_response.status == "FAILED": + raise RuntimeError(f"Capture failed: {capture_response.error}") + + return {"status": getattr(capture_response, "status", ""), "transaction_id": getattr(authorize_response, "connector_transaction_id", ""), "error": getattr(capture_response, "error", None)} + + +async def process_refund(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Refund + + Return funds to the customer for a completed payment. + """ + payment_client = PaymentClient(config) + + # Step 1: Authorize — reserve funds on the payment method + authorize_response = await payment_client.authorize(_build_authorize_request("AUTOMATIC")) + + if authorize_response.status == "FAILED": + raise RuntimeError(f"Payment failed: {authorize_response.error}") + if authorize_response.status == "PENDING": + # Awaiting async confirmation — handle via webhook + return {"status": "pending", "transaction_id": authorize_response.connector_transaction_id} + + # Step 2: Refund — return funds to the customer + refund_response = await payment_client.refund(_build_refund_request(authorize_response.connector_transaction_id)) + + if refund_response.status == "FAILED": + raise RuntimeError(f"Refund failed: {refund_response.error}") + + return {"status": getattr(refund_response, "status", ""), "error": getattr(refund_response, "error", None)} + + +async def process_void_payment(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Void Payment + + Cancel an authorized but not-yet-captured payment. + """ + payment_client = PaymentClient(config) + + # Step 1: Authorize — reserve funds on the payment method + authorize_response = await payment_client.authorize(_build_authorize_request("MANUAL")) + + if authorize_response.status == "FAILED": + raise RuntimeError(f"Payment failed: {authorize_response.error}") + if authorize_response.status == "PENDING": + # Awaiting async confirmation — handle via webhook + return {"status": "pending", "transaction_id": authorize_response.connector_transaction_id} + + # Step 2: Void — release reserved funds (cancel authorization) + void_response = await payment_client.void(_build_void_request(authorize_response.connector_transaction_id)) + + return {"status": getattr(void_response, "status", ""), "transaction_id": getattr(authorize_response, "connector_transaction_id", ""), "error": getattr(void_response, "error", None)} + + +async def process_get_payment(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Get Payment Status + + Retrieve current payment status from the connector. + """ + payment_client = PaymentClient(config) + + # Step 1: Authorize — reserve funds on the payment method + authorize_response = await payment_client.authorize(_build_authorize_request("MANUAL")) + + if authorize_response.status == "FAILED": + raise RuntimeError(f"Payment failed: {authorize_response.error}") + if authorize_response.status == "PENDING": + # Awaiting async confirmation — handle via webhook + return {"status": "pending", "transaction_id": authorize_response.connector_transaction_id} + + # Step 2: Get — retrieve current payment status from the connector + get_response = await payment_client.get(_build_get_request(authorize_response.connector_transaction_id)) + + return {"status": getattr(get_response, "status", ""), "transaction_id": getattr(get_response, "connector_transaction_id", ""), "error": getattr(get_response, "error", None)} + + +async def process_authorize(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Flow: PaymentService.Authorize (Card)""" + payment_client = PaymentClient(config) + + authorize_response = await payment_client.authorize(_build_authorize_request("AUTOMATIC")) + + return {"status": authorize_response.status, "transaction_id": authorize_response.connector_transaction_id} + + +async def process_capture(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Flow: PaymentService.Capture""" + payment_client = PaymentClient(config) + + capture_response = await payment_client.capture(_build_capture_request("probe_connector_txn_001")) + + return {"status": capture_response.status} + + +async def process_create_server_authentication_token(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Flow: MerchantAuthenticationService.CreateServerAuthenticationToken""" + merchantauthentication_client = MerchantAuthenticationClient(config) + + create_response = await merchantauthentication_client.create_server_authentication_token(_build_create_server_authentication_token_request()) + + return {"status": create_response.status} + + +async def process_get(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Flow: PaymentService.Get""" + payment_client = PaymentClient(config) + + get_response = await payment_client.get(_build_get_request("probe_connector_txn_001")) + + return {"status": get_response.status} + + +async def process_proxy_authorize(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Flow: PaymentService.ProxyAuthorize""" + payment_client = PaymentClient(config) + + proxy_response = await payment_client.proxy_authorize(_build_proxy_authorize_request()) + + return {"status": proxy_response.status} + + +async def process_refund_get(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Flow: RefundService.Get""" + refund_client = RefundClient(config) + + refund_response = await refund_client.refund_get(_build_refund_get_request()) + + return {"status": refund_response.status} + + +async def process_void(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Flow: PaymentService.Void""" + payment_client = PaymentClient(config) + + void_response = await payment_client.void(_build_void_request("probe_connector_txn_001")) + + return {"status": void_response.status} + +if __name__ == "__main__": + scenario = sys.argv[1] if len(sys.argv) > 1 else "checkout_autocapture" + fn = globals().get(f"process_{scenario}") + if not fn: + available = [k[8:] for k in globals() if k.startswith("process_")] + print(f"Unknown scenario: {scenario}. Available: {available}", file=sys.stderr) + sys.exit(1) + asyncio.run(fn("order_001")) diff --git a/examples/moneris/moneris.rs b/examples/moneris/moneris.rs new file mode 100644 index 0000000000..539c4c9806 --- /dev/null +++ b/examples/moneris/moneris.rs @@ -0,0 +1,587 @@ +// 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 moneris +// +// Moneris — all scenarios and flows in one file. +// Run a scenario: cargo run --example moneris -- process_checkout_card +use cards::CardNumber; +use grpc_api_types::payments::connector_specific_config; +use grpc_api_types::payments::payment_method; +use grpc_api_types::payments::*; +use hyperswitch_masking::Secret; +use hyperswitch_payments_client::ConnectorClient; +use std::collections::HashMap; +use std::str::FromStr; + +#[allow(dead_code)] +pub const SUPPORTED_FLOWS: &[&str] = &[ + "authorize", + "capture", + "create_server_authentication_token", + "get", + "proxy_authorize", + "refund", + "refund_get", + "void", +]; + +#[allow(dead_code)] +fn build_client() -> ConnectorClient { + // Configure the connector with authentication + let config = ConnectorConfig { + connector_config: Some(ConnectorSpecificConfig { + config: Some(connector_specific_config::Config::Moneris(MonerisConfig { + client_secret: Some(hyperswitch_masking::Secret::new( + "YOUR_CLIENT_SECRET".to_string(), + )), // Authentication credential + merchant_id: Some(hyperswitch_masking::Secret::new( + "YOUR_MERCHANT_ID".to_string(), + )), // Authentication credential + client_id: Some(hyperswitch_masking::Secret::new( + "YOUR_CLIENT_ID".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 { + ..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. + state: Some(ConnectorState { + // State Information. + access_token: Some(AccessToken { + // Access token obtained from connector. + token: Some(Secret::new("probe_access_token".to_string())), // The token string. + expires_in_seconds: Some(3600), // Expiration timestamp (seconds since epoch). + token_type: Some("Bearer".to_string()), // Token type (e.g., "Bearer", "Basic"). + }), + ..Default::default() + }), + ..Default::default() + } +} + +pub fn build_capture_request(connector_transaction_id: &str) -> PaymentServiceCaptureRequest { + PaymentServiceCaptureRequest { + merchant_capture_id: Some("probe_capture_001".to_string()), // Identification. + connector_transaction_id: connector_transaction_id.to_string(), + amount_to_capture: Some(Money { + // Capture Details. + minor_amount: 1000, // Amount in minor units (e.g., 1000 = $10.00). + currency: Currency::Usd.into(), // ISO 4217 currency code (e.g., "USD", "EUR"). + }), + state: Some(ConnectorState { + // State Information. + access_token: Some(AccessToken { + // Access token obtained from connector. + token: Some(Secret::new("probe_access_token".to_string())), // The token string. + expires_in_seconds: Some(3600), // Expiration timestamp (seconds since epoch). + token_type: Some("Bearer".to_string()), // Token type (e.g., "Bearer", "Basic"). + }), + ..Default::default() + }), + ..Default::default() + } +} + +pub fn build_create_server_authentication_token_request( +) -> MerchantAuthenticationServiceCreateServerAuthenticationTokenRequest { + MerchantAuthenticationServiceCreateServerAuthenticationTokenRequest { + ..Default::default() + } +} + +pub fn build_get_request(connector_transaction_id: &str) -> PaymentServiceGetRequest { + PaymentServiceGetRequest { + merchant_transaction_id: Some("probe_merchant_txn_001".to_string()), // Identification. + connector_transaction_id: connector_transaction_id.to_string(), + amount: Some(Money { + // Amount Information. + minor_amount: 1000, // Amount in minor units (e.g., 1000 = $10.00). + currency: Currency::Usd.into(), // ISO 4217 currency code (e.g., "USD", "EUR"). + }), + state: Some(ConnectorState { + // State Information. + access_token: Some(AccessToken { + // Access token obtained from connector. + token: Some(Secret::new("probe_access_token".to_string())), // The token string. + expires_in_seconds: Some(3600), // Expiration timestamp (seconds since epoch). + token_type: Some("Bearer".to_string()), // Token type (e.g., "Bearer", "Basic"). + }), + ..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 { + ..Default::default() + }), + ..Default::default() + }), + capture_method: Some(CaptureMethod::Automatic.into()), + auth_type: AuthenticationType::NoThreeDs.into(), + return_url: Some("https://example.com/return".to_string()), + state: Some(ConnectorState { + access_token: Some(AccessToken { + // Access token obtained from connector. + token: Some(Secret::new("probe_access_token".to_string())), // The token string. + expires_in_seconds: Some(3600), // Expiration timestamp (seconds since epoch). + token_type: Some("Bearer".to_string()), // Token type (e.g., "Bearer", "Basic"). + }), + ..Default::default() + }), + ..Default::default() + } +} + +pub fn build_refund_request(connector_transaction_id: &str) -> PaymentServiceRefundRequest { + PaymentServiceRefundRequest { + merchant_refund_id: Some("probe_refund_001".to_string()), // Identification. + connector_transaction_id: connector_transaction_id.to_string(), + payment_amount: 1000, // Amount Information. + refund_amount: Some(Money { + minor_amount: 1000, // Amount in minor units (e.g., 1000 = $10.00). + currency: Currency::Usd.into(), // ISO 4217 currency code (e.g., "USD", "EUR"). + }), + reason: Some("customer_request".to_string()), // Reason for the refund. + state: Some(ConnectorState { + // State data for access token storage and. + access_token: Some(AccessToken { + // Access token obtained from connector. + token: Some(Secret::new("probe_access_token".to_string())), // The token string. + expires_in_seconds: Some(3600), // Expiration timestamp (seconds since epoch). + token_type: Some("Bearer".to_string()), // Token type (e.g., "Bearer", "Basic"). + }), + ..Default::default() + }), + ..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. + state: Some(ConnectorState { + // State Information. + access_token: Some(AccessToken { + // Access token obtained from connector. + token: Some(Secret::new("probe_access_token".to_string())), // The token string. + expires_in_seconds: Some(3600), // Expiration timestamp (seconds since epoch). + token_type: Some("Bearer".to_string()), // Token type (e.g., "Bearer", "Basic"). + }), + ..Default::default() + }), + ..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(), + state: Some(ConnectorState { + // State Information. + access_token: Some(AccessToken { + // Access token obtained from connector. + token: Some(Secret::new("probe_access_token".to_string())), // The token string. + expires_in_seconds: Some(3600), // Expiration timestamp (seconds since epoch). + token_type: Some("Bearer".to_string()), // Token type (e.g., "Bearer", "Basic"). + }), + ..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("") + )) +} + +// Scenario: Card Payment (Authorize + Capture) +// Two-step card payment. First authorize, then capture. Use when you need to verify funds before finalizing. +#[allow(dead_code)] +pub async fn process_checkout_card( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + // Step 1: Authorize — reserve funds on the payment method + let authorize_response = client + .authorize(build_authorize_request("MANUAL"), &HashMap::new(), None) + .await?; + + match authorize_response.status() { + PaymentStatus::Failure | PaymentStatus::AuthorizationFailed => { + return Err(format!("Payment failed: {:?}", authorize_response.error).into()) + } + PaymentStatus::Pending => return Ok("pending — awaiting webhook".to_string()), + _ => {} + } + + // Step 2: Capture — settle the reserved funds + let capture_response = client + .capture( + build_capture_request( + authorize_response + .connector_transaction_id + .as_deref() + .unwrap_or(""), + ), + &HashMap::new(), + None, + ) + .await?; + + if capture_response.status() == PaymentStatus::Failure { + return Err(format!("Capture failed: {:?}", capture_response.error).into()); + } + + Ok(format!( + "Payment completed: {}", + authorize_response + .connector_transaction_id + .as_deref() + .unwrap_or("") + )) +} + +// Scenario: Refund +// Return funds to the customer for a completed payment. +#[allow(dead_code)] +pub async fn process_refund( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + // Step 1: Authorize — reserve funds on the payment method + let authorize_response = client + .authorize(build_authorize_request("AUTOMATIC"), &HashMap::new(), None) + .await?; + + match authorize_response.status() { + PaymentStatus::Failure | PaymentStatus::AuthorizationFailed => { + return Err(format!("Payment failed: {:?}", authorize_response.error).into()) + } + PaymentStatus::Pending => return Ok("pending — awaiting webhook".to_string()), + _ => {} + } + + // Step 2: Refund — return funds to the customer + let refund_response = client + .refund( + build_refund_request( + authorize_response + .connector_transaction_id + .as_deref() + .unwrap_or(""), + ), + &HashMap::new(), + None, + ) + .await?; + + if refund_response.status() == RefundStatus::RefundFailure { + return Err(format!("Refund failed: {:?}", refund_response.error).into()); + } + + Ok(format!("Refunded: {:?}", refund_response.status())) +} + +// Scenario: Void Payment +// Cancel an authorized but not-yet-captured payment. +#[allow(dead_code)] +pub async fn process_void_payment( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + // Step 1: Authorize — reserve funds on the payment method + let authorize_response = client + .authorize(build_authorize_request("MANUAL"), &HashMap::new(), None) + .await?; + + match authorize_response.status() { + PaymentStatus::Failure | PaymentStatus::AuthorizationFailed => { + return Err(format!("Payment failed: {:?}", authorize_response.error).into()) + } + PaymentStatus::Pending => return Ok("pending — awaiting webhook".to_string()), + _ => {} + } + + // Step 2: Void — release reserved funds (cancel authorization) + let void_response = client + .void( + build_void_request( + authorize_response + .connector_transaction_id + .as_deref() + .unwrap_or(""), + ), + &HashMap::new(), + None, + ) + .await?; + + Ok(format!("Voided: {:?}", void_response.status())) +} + +// Scenario: Get Payment Status +// Retrieve current payment status from the connector. +#[allow(dead_code)] +pub async fn process_get_payment( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + // Step 1: Authorize — reserve funds on the payment method + let authorize_response = client + .authorize(build_authorize_request("MANUAL"), &HashMap::new(), None) + .await?; + + match authorize_response.status() { + PaymentStatus::Failure | PaymentStatus::AuthorizationFailed => { + return Err(format!("Payment failed: {:?}", authorize_response.error).into()) + } + PaymentStatus::Pending => return Ok("pending — awaiting webhook".to_string()), + _ => {} + } + + // Step 2: Get — retrieve current payment status from the connector + let get_response = client + .get( + build_get_request( + authorize_response + .connector_transaction_id + .as_deref() + .unwrap_or(""), + ), + &HashMap::new(), + None, + ) + .await?; + + Ok(format!("Status: {:?}", get_response.status())) +} + +// Flow: PaymentService.Authorize (Card) +#[allow(dead_code)] +pub async fn process_authorize( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + let response = client + .authorize(build_authorize_request("AUTOMATIC"), &HashMap::new(), None) + .await?; + match response.status() { + PaymentStatus::Failure | PaymentStatus::AuthorizationFailed => { + Err(format!("Authorize failed: {:?}", response.error).into()) + } + PaymentStatus::Pending => Ok("pending — await webhook".to_string()), + _ => Ok(format!( + "Authorized: {}", + response.connector_transaction_id.as_deref().unwrap_or("") + )), + } +} + +// Flow: PaymentService.Capture +#[allow(dead_code)] +pub async fn process_capture( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + let response = client + .capture( + build_capture_request("probe_connector_txn_001"), + &HashMap::new(), + None, + ) + .await?; + Ok(format!("status: {:?}", response.status())) +} + +// Flow: MerchantAuthenticationService.CreateServerAuthenticationToken +#[allow(dead_code)] +pub async fn process_create_server_authentication_token( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + let response = client + .create_server_authentication_token( + build_create_server_authentication_token_request(), + &HashMap::new(), + None, + ) + .await?; + Ok(format!("status: {:?}", response.status())) +} + +// Flow: PaymentService.Get +#[allow(dead_code)] +pub async fn process_get( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + let response = client + .get( + build_get_request("probe_connector_txn_001"), + &HashMap::new(), + None, + ) + .await?; + Ok(format!("status: {:?}", response.status())) +} + +// Flow: PaymentService.ProxyAuthorize +#[allow(dead_code)] +pub async fn process_proxy_authorize( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + let response = client + .proxy_authorize(build_proxy_authorize_request(), &HashMap::new(), None) + .await?; + Ok(format!("status: {:?}", response.status())) +} + +// Flow: RefundService.Get +#[allow(dead_code)] +pub async fn process_refund_get( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + let response = client + .refund_get(build_refund_get_request(), &HashMap::new(), None) + .await?; + Ok(format!("status: {:?}", response.status())) +} + +// Flow: PaymentService.Void +#[allow(dead_code)] +pub async fn process_void( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + let response = client + .void( + build_void_request("probe_connector_txn_001"), + &HashMap::new(), + None, + ) + .await?; + Ok(format!("status: {:?}", response.status())) +} + +#[allow(dead_code)] +#[tokio::main] +async fn main() { + let client = build_client(); + let flow = std::env::args() + .nth(1) + .unwrap_or_else(|| "process_checkout_autocapture".to_string()); + let result: Result> = match flow.as_str() { + "process_checkout_autocapture" => process_checkout_autocapture(&client, "order_001").await, + "process_checkout_card" => process_checkout_card(&client, "order_001").await, + "process_refund" => process_refund(&client, "order_001").await, + "process_void_payment" => process_void_payment(&client, "order_001").await, + "process_get_payment" => process_get_payment(&client, "order_001").await, + "process_authorize" => process_authorize(&client, "txn_001").await, + "process_capture" => process_capture(&client, "txn_001").await, + "process_create_server_authentication_token" => { + process_create_server_authentication_token(&client, "txn_001").await + } + "process_get" => process_get(&client, "txn_001").await, + "process_proxy_authorize" => process_proxy_authorize(&client, "txn_001").await, + "process_refund_get" => process_refund_get(&client, "txn_001").await, + "process_void" => process_void(&client, "txn_001").await, + _ => { + eprintln!("Unknown flow: {}. Available: process_checkout_autocapture, process_checkout_card, process_refund, process_void_payment, process_get_payment, process_authorize, process_capture, process_create_server_authentication_token, process_get, process_proxy_authorize, process_refund_get, process_void", flow); + return; + } + }; + match result { + Ok(msg) => println!("✓ {msg}"), + Err(e) => eprintln!("✗ {e}"), + } +} diff --git a/examples/moneris/moneris.ts b/examples/moneris/moneris.ts new file mode 100644 index 0000000000..747ac87090 --- /dev/null +++ b/examples/moneris/moneris.ts @@ -0,0 +1,390 @@ +// 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 moneris +// +// Moneris — all integration scenarios and flows in one file. +// Run a scenario: npx tsx moneris.ts checkout_autocapture + +import { PaymentClient, MerchantAuthenticationClient, RefundClient, types } from 'hyperswitch-prism'; +const { Environment, AuthenticationType, CaptureMethod, CardNetwork, Currency } = types; +export const SUPPORTED_FLOWS = ["authorize", "capture", "create_server_authentication_token", "get", "proxy_authorize", "refund", "refund_get", "void"]; + +const _defaultConfig: types.IConnectorConfig = { + options: { + environment: Environment.SANDBOX, + }, + connectorConfig: { + moneris: { + clientSecret: { value: 'YOUR_CLIENT_SECRET' }, + merchantId: { value: 'YOUR_MERCHANT_ID' }, + clientId: { value: 'YOUR_CLIENT_ID' }, + 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": { + } + }, + "authType": AuthenticationType.NO_THREE_DS, // Authentication Details. + "returnUrl": "https://example.com/return", // URLs for Redirection and Webhooks. + "state": { // State Information. + "accessToken": { // Access token obtained from connector. + "token": {"value": "probe_access_token"}, // The token string. + "expiresInSeconds": 3600, // Expiration timestamp (seconds since epoch). + "tokenType": "Bearer" // Token type (e.g., "Bearer", "Basic"). + } + } + }; +} + +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"). + }, + "state": { // State Information. + "accessToken": { // Access token obtained from connector. + "token": {"value": "probe_access_token"}, // The token string. + "expiresInSeconds": 3600, // Expiration timestamp (seconds since epoch). + "tokenType": "Bearer" // Token type (e.g., "Bearer", "Basic"). + } + } + }; +} + +function _buildCreateServerAuthenticationTokenRequest(): types.IMerchantAuthenticationServiceCreateServerAuthenticationTokenRequest { + return { + }; +} + +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"). + }, + "state": { // State Information. + "accessToken": { // Access token obtained from connector. + "token": {"value": "probe_access_token"}, // The token string. + "expiresInSeconds": 3600, // Expiration timestamp (seconds since epoch). + "tokenType": "Bearer" // Token type (e.g., "Bearer", "Basic"). + } + } + }; +} + +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": { + } + }, + "captureMethod": CaptureMethod.AUTOMATIC, + "authType": AuthenticationType.NO_THREE_DS, + "returnUrl": "https://example.com/return", + "state": { + "accessToken": { // Access token obtained from connector. + "token": {"value": "probe_access_token"}, // The token string. + "expiresInSeconds": 3600, // Expiration timestamp (seconds since epoch). + "tokenType": "Bearer" // Token type (e.g., "Bearer", "Basic"). + } + } + }; +} + +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. + "state": { // State data for access token storage and. + "accessToken": { // Access token obtained from connector. + "token": {"value": "probe_access_token"}, // The token string. + "expiresInSeconds": 3600, // Expiration timestamp (seconds since epoch). + "tokenType": "Bearer" // Token type (e.g., "Bearer", "Basic"). + } + } + }; +} + +function _buildRefundGetRequest(): types.IRefundServiceGetRequest { + return { + "merchantRefundId": "probe_refund_001", // Identification. + "connectorTransactionId": "probe_connector_txn_001", + "refundId": "probe_refund_id_001", // Deprecated. + "state": { // State Information. + "accessToken": { // Access token obtained from connector. + "token": {"value": "probe_access_token"}, // The token string. + "expiresInSeconds": 3600, // Expiration timestamp (seconds since epoch). + "tokenType": "Bearer" // Token type (e.g., "Bearer", "Basic"). + } + } + }; +} + +function _buildVoidRequest(connectorTransactionId: string): types.IPaymentServiceVoidRequest { + return { + "merchantVoidId": "probe_void_001", // Identification. + "connectorTransactionId": connectorTransactionId, + "state": { // State Information. + "accessToken": { // Access token obtained from connector. + "token": {"value": "probe_access_token"}, // The token string. + "expiresInSeconds": 3600, // Expiration timestamp (seconds since epoch). + "tokenType": "Bearer" // Token type (e.g., "Bearer", "Basic"). + } + } + }; +} + + +// ANCHOR: scenario_functions +// One-step Payment (Authorize + Capture) +// Simple payment that authorizes and captures in one call. Use for immediate charges. +async function processCheckoutAutocapture(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + // Step 1: Authorize — reserve funds on the payment method + const authorizeResponse = await paymentClient.authorize(_buildAuthorizeRequest(CaptureMethod.AUTOMATIC)); + + if (authorizeResponse.status === types.PaymentStatus.FAILURE) { + throw new Error(`Payment failed: ${JSON.stringify(authorizeResponse.error)}`); + } + if (authorizeResponse.status === types.PaymentStatus.PENDING) { + // Awaiting async confirmation — handle via webhook + return { status: 'pending', connectorTransactionId: authorizeResponse.connectorTransactionId }; + } + + return { status: authorizeResponse.status, transactionId: authorizeResponse.connectorTransactionId!, error: authorizeResponse.error } as any; +} + +// Card Payment (Authorize + Capture) +// Two-step card payment. First authorize, then capture. Use when you need to verify funds before finalizing. +async function processCheckoutCard(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + // Step 1: Authorize — reserve funds on the payment method + const authorizeResponse = await paymentClient.authorize(_buildAuthorizeRequest(CaptureMethod.MANUAL)); + + if (authorizeResponse.status === types.PaymentStatus.FAILURE) { + throw new Error(`Payment failed: ${JSON.stringify(authorizeResponse.error)}`); + } + if (authorizeResponse.status === types.PaymentStatus.PENDING) { + // Awaiting async confirmation — handle via webhook + return { status: 'pending', connectorTransactionId: authorizeResponse.connectorTransactionId }; + } + + // Step 2: Capture — settle the reserved funds + const captureResponse = await paymentClient.capture(_buildCaptureRequest(authorizeResponse.connectorTransactionId!)); + + if (captureResponse.status === types.PaymentStatus.FAILURE) { + throw new Error(`Capture failed: ${JSON.stringify(captureResponse.error)}`); + } + + return { status: captureResponse.status, transactionId: authorizeResponse.connectorTransactionId!, error: authorizeResponse.error } as any; +} + +// Refund +// Return funds to the customer for a completed payment. +async function processRefund(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + // Step 1: Authorize — reserve funds on the payment method + const authorizeResponse = await paymentClient.authorize(_buildAuthorizeRequest(CaptureMethod.AUTOMATIC)); + + if (authorizeResponse.status === types.PaymentStatus.FAILURE) { + throw new Error(`Payment failed: ${JSON.stringify(authorizeResponse.error)}`); + } + if (authorizeResponse.status === types.PaymentStatus.PENDING) { + // Awaiting async confirmation — handle via webhook + return { status: 'pending', connectorTransactionId: authorizeResponse.connectorTransactionId }; + } + + // Step 2: Refund — return funds to the customer + const refundResponse = await paymentClient.refund(_buildRefundRequest(authorizeResponse.connectorTransactionId!)); + + if (refundResponse.status === types.RefundStatus.REFUND_FAILURE) { + throw new Error(`Refund failed: ${JSON.stringify(refundResponse.error)}`); + } + + return { status: refundResponse.status, error: refundResponse.error } as any; +} + +// Void Payment +// Cancel an authorized but not-yet-captured payment. +async function processVoidPayment(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + // Step 1: Authorize — reserve funds on the payment method + const authorizeResponse = await paymentClient.authorize(_buildAuthorizeRequest(CaptureMethod.MANUAL)); + + if (authorizeResponse.status === types.PaymentStatus.FAILURE) { + throw new Error(`Payment failed: ${JSON.stringify(authorizeResponse.error)}`); + } + if (authorizeResponse.status === types.PaymentStatus.PENDING) { + // Awaiting async confirmation — handle via webhook + return { status: 'pending', connectorTransactionId: authorizeResponse.connectorTransactionId }; + } + + // Step 2: Void — release reserved funds (cancel authorization) + const voidResponse = await paymentClient.void(_buildVoidRequest(authorizeResponse.connectorTransactionId!)); + + return { status: voidResponse.status, transactionId: authorizeResponse.connectorTransactionId!, error: voidResponse.error } as any; +} + +// Get Payment Status +// Retrieve current payment status from the connector. +async function processGetPayment(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + // Step 1: Authorize — reserve funds on the payment method + const authorizeResponse = await paymentClient.authorize(_buildAuthorizeRequest(CaptureMethod.MANUAL)); + + if (authorizeResponse.status === types.PaymentStatus.FAILURE) { + throw new Error(`Payment failed: ${JSON.stringify(authorizeResponse.error)}`); + } + if (authorizeResponse.status === types.PaymentStatus.PENDING) { + // Awaiting async confirmation — handle via webhook + return { status: 'pending', connectorTransactionId: authorizeResponse.connectorTransactionId }; + } + + // Step 2: Get — retrieve current payment status from the connector + const getResponse = await paymentClient.get(_buildGetRequest(authorizeResponse.connectorTransactionId!)); + + return { status: getResponse.status, transactionId: getResponse.connectorTransactionId!, error: getResponse.error } as any; +} + +// Flow: PaymentService.Authorize (Card) +async function authorize(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + const authorizeResponse = await paymentClient.authorize(_buildAuthorizeRequest(CaptureMethod.AUTOMATIC)); + + return authorizeResponse; +} + +// Flow: PaymentService.Capture +async function capture(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + const captureResponse = await paymentClient.capture(_buildCaptureRequest('probe_connector_txn_001')); + + return captureResponse; +} + +// Flow: MerchantAuthenticationService.CreateServerAuthenticationToken +async function createServerAuthenticationToken(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const merchantAuthenticationClient = new MerchantAuthenticationClient(config); + + const createResponse = await merchantAuthenticationClient.createServerAuthenticationToken(_buildCreateServerAuthenticationTokenRequest()); + + return createResponse; +} + +// Flow: PaymentService.Get +async function get(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + const getResponse = await paymentClient.get(_buildGetRequest('probe_connector_txn_001')); + + return getResponse; +} + +// Flow: PaymentService.ProxyAuthorize +async function proxyAuthorize(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + const proxyResponse = await paymentClient.proxyAuthorize(_buildProxyAuthorizeRequest()); + + return proxyResponse; +} + +// Flow: PaymentService.Refund +async function refund(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + const refundResponse = await paymentClient.refund(_buildRefundRequest('probe_connector_txn_001')); + + return refundResponse; +} + +// Flow: RefundService.Get +async function refundGet(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const refundClient = new RefundClient(config); + + const refundResponse = await refundClient.refundGet(_buildRefundGetRequest()); + + return refundResponse; +} + +// Flow: PaymentService.Void +async function voidPayment(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + const voidResponse = await paymentClient.void(_buildVoidRequest('probe_connector_txn_001')); + + return voidResponse; +} + + +// Export all process* functions for the smoke test +export { + processCheckoutAutocapture, processCheckoutCard, processRefund, processVoidPayment, processGetPayment, authorize, capture, createServerAuthenticationToken, get, proxyAuthorize, refund, refundGet, voidPayment, _buildAuthorizeRequest, _buildCaptureRequest, _buildCreateServerAuthenticationTokenRequest, _buildGetRequest, _buildProxyAuthorizeRequest, _buildRefundRequest, _buildRefundGetRequest, _buildVoidRequest +}; + +// CLI runner +if (require.main === module) { + const scenario = process.argv[2] || 'checkout_autocapture'; + const key = 'process' + scenario.replace(/_([a-z])/g, (_, l) => l.toUpperCase()).replace(/^(.)/, c => c.toUpperCase()); + const fn = (globalThis as any)[key] || (exports as any)[key]; + if (!fn) { + const available = Object.keys(exports).map(k => + k.replace(/^process/, '').replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '') + ); + console.error(`Unknown scenario: ${scenario}. Available: ${available.join(', ')}`); + process.exit(1); + } + fn('order_001').catch(console.error); +} diff --git a/sdk/javascript/src/payments/_generated_grpc_client.ts b/sdk/javascript/src/payments/_generated_grpc_client.ts index f8289741e5..8f57b3e4b9 100644 --- a/sdk/javascript/src/payments/_generated_grpc_client.ts +++ b/sdk/javascript/src/payments/_generated_grpc_client.ts @@ -97,6 +97,7 @@ const _SECRET_STRING_FIELDS: Record = { UpiCollect: ["vpaId"], PixPayment: ["pixKey", "cpf", "cnpj"], OnlineBankingFinland: ["email"], + OpenBanking: ["iban", "accountNumber", "sortCode", "accountHolderName", "additionalDetails"], Giropay: ["bankAccountBic", "bankAccountIban"], Interac: ["email"], BancontactCard: ["cardNumber", "cardExpMonth", "cardExpYear", "cardHolderName"], @@ -323,6 +324,9 @@ const _SECRET_STRING_FIELDS: Record = { GrabpayConfig: ["partnerId", "partnerSecret", "clientId", "clientSecret", "merchantId"], TesouroConfig: ["apiKey", "key1", "apiSecret"], BoostConfig: ["clientId", "merchantSecret"], + CitigateConfig: ["apiKey", "key1"], + IlixiumConfig: ["apiKey", "key1", "apiSecret"], + MonerisConfig: ["clientSecret", "merchantId", "clientId"], PaymentServiceTokenAuthorizeRequest: ["connectorToken", "metadata", "connectorFeatureData"], PaymentServiceTokenSetupRecurringRequest: ["connectorToken", "metadata", "connectorFeatureData"], PaymentServiceProxyAuthorizeRequest: ["metadata", "connectorFeatureData"], @@ -342,18 +346,22 @@ const _SECRET_STRING_FIELDS: Record = { PixBankTransferPayout: ["bankAccountNumber", "taxId", "ispb", "accountHolderName"], PixKeyBankTransferPayout: ["pixKey"], PixEmvBankTransferPayout: ["emv"], + TrustlyBankTransferPayout: ["iban", "bankAccountNumber", "bankNumber"], ApplePayDecrypt: ["dpan", "expiryMonth", "expiryYear", "cardHolderName"], Paypal: ["email", "telephoneNumber", "paypalId"], Venmo: ["telephoneNumber"], InteracPayout: ["email"], OpenBankingUkPayout: ["accountHolderName", "iban"], + OpenBankingPayout: ["accountHolderName", "iban"], + Passthrough: ["pspCustomerId"], PayoutServiceCreateRequest: ["connectorFeatureData", "accessToken"], - PayoutServiceTransferRequest: ["accessToken"], + PayoutServiceTransferRequest: ["accessToken", "payoutConnectorMetadata"], PayoutServiceStageRequest: ["accessToken"], PayoutServiceGetRequest: ["accessToken"], PayoutServiceVoidRequest: ["connectorFeatureData", "accessToken"], PayoutServiceCreateLinkRequest: ["connectorFeatureData", "accessToken"], PayoutServiceCreateRecipientRequest: ["accessToken"], + PayoutServiceCreateRecipientResponse: ["connectorMetadata"], PayoutServiceEnrollDisburseAccountRequest: ["accessToken"], PayoutMethodEligibilityRequest: ["connectorFeatureData", "accessToken"], PayoutMethodEligibilityResponse: ["connectorMetadata"], @@ -433,7 +441,7 @@ const _MSG_FIELD_TYPES: Record> = { PartnerMerchantIdentifierDetails: { "partnerDetails": "PartnerApplicationDetails", "merchantDetails": "MerchantApplicationDetails" }, PaymentServiceAuthorizeResponse: { "error": "ErrorInfo", "responseHeaders": "ResponseHeadersEntry", "redirectionData": "RedirectForm", "state": "ConnectorState", "authorizedMoney": "Money", "mandateReference": "MandateReference", "connectorResponse": "ConnectorResponseData", "splits": "ConnectorSplitResponseData", "mandateReferenceDetails": "MandateReferenceDetails", "rawConnectorStatus": "RawConnectorStatus" }, PaymentServiceGetRequest: { "amount": "Money", "state": "ConnectorState", "splitPayments": "SplitPaymentsDetails", "mandateReference": "ConnectorMandateReferenceId" }, - PaymentServiceGetResponse: { "error": "ErrorInfo", "responseHeaders": "ResponseHeadersEntry", "mandateReference": "MandateReference", "amount": "Money", "connectorResponse": "ConnectorResponseData", "state": "ConnectorState", "redirectionData": "RedirectForm", "paymentMethodUpdate": "PaymentMethodUpdate", "splits": "ConnectorSplitResponseData", "mandateReferenceDetails": "MandateReferenceDetails", "rawConnectorStatus": "RawConnectorStatus" }, + PaymentServiceGetResponse: { "error": "ErrorInfo", "responseHeaders": "ResponseHeadersEntry", "mandateReference": "MandateReference", "amount": "Money", "connectorResponse": "ConnectorResponseData", "state": "ConnectorState", "redirectionData": "RedirectForm", "paymentMethodUpdate": "PaymentMethodUpdate", "splits": "ConnectorSplitResponseData", "mandateReferenceDetails": "MandateReferenceDetails", "rawConnectorStatus": "RawConnectorStatus", "connectorReturnedPaymentMethodDetails": "PaymentMethod" }, PaymentServiceVoidRequest: { "browserInfo": "BrowserInformation", "amount": "Money", "state": "ConnectorState", "splitPayments": "SplitPaymentsDetails" }, PaymentServiceVoidResponse: { "error": "ErrorInfo", "responseHeaders": "ResponseHeadersEntry", "state": "ConnectorState", "mandateReference": "MandateReference", "splits": "ConnectorSplitResponseData", "mandateReferenceDetails": "MandateReferenceDetails", "rawConnectorStatus": "RawConnectorStatus" }, PaymentServiceReverseRequest: { "browserInfo": "BrowserInformation" }, @@ -514,7 +522,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", "moneris": "MonerisConfig", "ilixium": "IlixiumConfig" }, 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" }, @@ -524,8 +532,8 @@ const _MSG_FIELD_TYPES: Record> = { ChargeRefundsOptions: { "destination": "DestinationChargeRefund", "direct": "DirectChargeRefund" }, AdyenSplitData: { "splitItems": "AdyenSplitItem" }, SplitPaymentsDetails: { "stripeSplitPayment": "StripeSplitPaymentData", "adyenSplitPayment": "AdyenSplitData" }, - PaymentMethodServiceEligibilityRequest: { "amount": "Money", "customer": "Customer", "address": "PaymentAddress", "orderDetails": "OrderDetailsWithAmount" }, - PaymentMethodServiceEligibilityResponse: { "errorInfo": "ErrorInfo", "responseHeaders": "ResponseHeadersEntry" }, + PaymentMethodServiceEligibilityRequest: { "amount": "Money", "customer": "Customer", "address": "PaymentAddress", "orderDetails": "OrderDetailsWithAmount", "state": "ConnectorState" }, + PaymentMethodServiceEligibilityResponse: { "errorInfo": "ErrorInfo", "responseHeaders": "ResponseHeadersEntry", "paymentMethodDetails": "PaymentMethodDetails" }, ConnectorSplitResponseData: { "stripeSplitResponse": "StripeSplitResponseData", "adyenSplitResponse": "AdyenSplitData" }, FrmServicePreRiskCheckRequest: { "amount": "Money", "customerInfo": "Customer", "paymentMethod": "PaymentMethod", "browserInfo": "BrowserInformation", "orderDetails": "OrderDetailsWithAmount", "address": "PaymentAddress", "state": "ConnectorState", "mandateDetails": "MandateAmountData", "merchantDetails": "MerchantDetails" }, FrmServicePreRiskCheckResponse: { "error": "ErrorInfo", "responseHeaders": "ResponseHeadersEntry" }, @@ -536,7 +544,7 @@ const _MSG_FIELD_TYPES: Record> = { CompositeFrmPreRiskCheckResponse: { "preRiskCheckResponse": "FrmServicePreRiskCheckResponse", "accessTokenResponse": "MerchantAuthenticationServiceCreateServerAuthenticationTokenResponse" }, CompositeFrmPostRiskCheckResponse: { "postRiskCheckResponse": "FrmServicePostRiskCheckResponse", "accessTokenResponse": "MerchantAuthenticationServiceCreateServerAuthenticationTokenResponse" }, PayoutAddress: { "shippingAddress": "Address", "billingAddress": "Address" }, - PayoutMethod: { "card": "CardPayout", "ach": "AchBankTransferPayout", "bacs": "BacsBankTransferPayout", "sepa": "SepaBankTransferPayout", "pix": "PixBankTransferPayout", "applePayDecrypt": "ApplePayDecrypt", "paypal": "Paypal", "venmo": "Venmo", "interac": "InteracPayout", "openBankingUk": "OpenBankingUkPayout", "passthrough": "Passthrough", "pixKey": "PixKeyBankTransferPayout", "pixEmv": "PixEmvBankTransferPayout" }, + PayoutMethod: { "card": "CardPayout", "ach": "AchBankTransferPayout", "bacs": "BacsBankTransferPayout", "sepa": "SepaBankTransferPayout", "pix": "PixBankTransferPayout", "applePayDecrypt": "ApplePayDecrypt", "paypal": "Paypal", "venmo": "Venmo", "interac": "InteracPayout", "openBankingUk": "OpenBankingUkPayout", "passthrough": "Passthrough", "pixKey": "PixKeyBankTransferPayout", "pixEmv": "PixEmvBankTransferPayout", "openBanking": "OpenBankingPayout", "trustly": "TrustlyBankTransferPayout" }, SourceBankData: { "ach": "AchBankTransferPayout", "bacs": "BacsBankTransferPayout", "sepa": "SepaBankTransferPayout", "pix": "PixBankTransferPayout", "pixKey": "PixKeyBankTransferPayout", "pixEmv": "PixEmvBankTransferPayout" }, PayoutServiceCreateRequest: { "address": "PayoutAddress", "payoutMethodData": "PayoutMethod", "amount": "Money", "customer": "Customer", "browserInfo": "BrowserInformation", "sourceBankData": "SourceBankData" }, PayoutServiceCreateResponse: { "error": "ErrorInfo" },