diff --git a/crates/integrations/connector-integration/src/connectors/checkout.rs b/crates/integrations/connector-integration/src/connectors/checkout.rs index a537a2207b..dc069ce752 100644 --- a/crates/integrations/connector-integration/src/connectors/checkout.rs +++ b/crates/integrations/connector-integration/src/connectors/checkout.rs @@ -4,11 +4,15 @@ use std::fmt::Debug; use common_utils::{consts, errors::CustomResult, events, ext_traits::ByteSliceExt}; use domain_types::{ - connector_flow::{Authorize, Capture, PSync, RSync, Refund, RepeatPayment, SetupMandate, Void}, + connector_flow::{ + Authorize, Capture, PSync, PaymentMethodToken, RSync, Refund, RepeatPayment, SetupMandate, + Void, + }, connector_types::{ - PaymentFlowData, PaymentVoidData, PaymentsAuthorizeData, PaymentsCaptureData, - PaymentsResponseData, PaymentsSyncData, RefundFlowData, RefundSyncData, RefundsData, - RefundsResponseData, RepeatPaymentData, ResponseId, SetupMandateRequestData, + PaymentFlowData, PaymentMethodTokenResponse, PaymentMethodTokenizationData, + PaymentVoidData, PaymentsAuthorizeData, PaymentsCaptureData, PaymentsResponseData, + PaymentsSyncData, RefundFlowData, RefundSyncData, RefundsData, RefundsResponseData, + RepeatPaymentData, ResponseId, SetupMandateRequestData, }, payment_method_data::PaymentMethodDataTypes, router_data::{ConnectorSpecificConfig, ErrorResponse}, @@ -25,11 +29,11 @@ use interfaces::{ }; use serde::Serialize; use transformers::{ - CheckoutErrorResponse, PaymentCaptureRequest, PaymentCaptureResponse, PaymentVoidRequest, - PaymentVoidResponse, PaymentsRequest, PaymentsRequest as SetupMandateRequest, - PaymentsRequest as RepeatPaymentRequest, PaymentsResponse, PaymentsResponse as PSyncResponse, - PaymentsResponse as SetupMandateResponse, PaymentsResponse as RepeatPaymentResponse, - RSyncResponse, RefundRequest, RefundResponse, + CheckoutErrorResponse, CheckoutTokenRequest, CheckoutTokenResponse, PaymentCaptureRequest, + PaymentCaptureResponse, PaymentVoidRequest, PaymentVoidResponse, PaymentsRequest, + PaymentsRequest as SetupMandateRequest, PaymentsRequest as RepeatPaymentRequest, + PaymentsResponse, PaymentsResponse as PSyncResponse, PaymentsResponse as SetupMandateResponse, + PaymentsResponse as RepeatPaymentResponse, RSyncResponse, RefundRequest, RefundResponse, }; use super::macros; @@ -112,6 +116,10 @@ impl connector_types::RepeatPaymentV2 for Checkout { } +impl + connector_types::PaymentTokenV2 for Checkout +{ +} macros::create_all_prerequisites!( connector_name: Checkout, generic_type: T, @@ -161,6 +169,12 @@ macros::create_all_prerequisites!( flow: RSync, response_body: RSyncResponse, router_data: RouterDataV2, + ), + ( + flow: PaymentMethodToken, + request_body: CheckoutTokenRequest, + response_body: CheckoutTokenResponse, + router_data: RouterDataV2, PaymentMethodTokenResponse>, ) ], amount_converters: [], @@ -171,13 +185,38 @@ macros::create_all_prerequisites!( ) -> CustomResult)>, IntegrationError> { let mut header = vec![( headers::CONTENT_TYPE.to_string(), - "application/json".to_string().into(), + self.common_get_content_type().to_string().into(), )]; let mut auth_header = self.get_auth_header(&req.connector_config)?; header.append(&mut auth_header); Ok(header) } + /// Headers for Checkout's `POST /tokens`. + /// + /// Tokenization is the one Checkout endpoint that is authenticated with the account's + /// **public** key, which — counter-intuitively — lives in `api_key`, while the secret + /// key every other endpoint uses lives in `api_secret`. See the note on + /// [`transformers::CheckoutAuthType`]. Verified against the sandbox: `Bearer sk_...` + /// on `/tokens` returns 403, and `Bearer pk_...` on `/payments` returns 401. This is + /// why the flow cannot reuse [`Self::build_headers`]. + pub fn build_tokenization_headers( + &self, + req: &RouterDataV2, + ) -> CustomResult)>, IntegrationError> { + let auth = transformers::CheckoutAuthType::try_from(&req.connector_config)?; + Ok(vec![ + ( + headers::CONTENT_TYPE.to_string(), + self.common_get_content_type().to_string().into(), + ), + ( + headers::AUTHORIZATION.to_string(), + format!("Bearer {}", auth.api_key.peek()).into_masked(), + ), + ]) + } + pub fn connector_base_url_payments<'a, F, Req, Res>( &self, req: &'a RouterDataV2, @@ -214,6 +253,9 @@ impl Conn context: Default::default(), }, )?; + // `api_secret` holds the **secret** key (`sk_...`) — used by every endpoint except + // `POST /tokens`, which takes the public key from `api_key`. See the note on + // [`transformers::CheckoutAuthType`]; the naming is inherited, not descriptive. Ok(vec![( headers::AUTHORIZATION.to_string(), format!("Bearer {}", auth.api_secret.peek()).into_masked(), @@ -536,6 +578,37 @@ macros::macro_connector_implementation!( } ); +// Connector-decryption head: hand Checkout the raw Apple Pay / Google Pay payload and get a +// single-use `tok_...` back. The Authorize / SetupMandate tail then spends that token as +// `source.type = "token"` (see `build_wallet_token_source` in transformers.rs). +macros::macro_connector_implementation!( + connector_default_implementations: [get_content_type, get_error_response_v2], + connector: Checkout, + curl_request: Json(CheckoutTokenRequest), + curl_response: CheckoutTokenResponse, + flow_name: PaymentMethodToken, + resource_common_data: PaymentFlowData, + flow_request: PaymentMethodTokenizationData, + flow_response: PaymentMethodTokenResponse, + http_method: Post, + generic_type: T, + [PaymentMethodDataTypes + Debug + Sync + Send + 'static + Serialize], + other_functions: { + fn get_headers( + &self, + req: &RouterDataV2, PaymentMethodTokenResponse>, + ) -> CustomResult)>, IntegrationError> { + self.build_tokenization_headers(req) + } + fn get_url( + &self, + req: &RouterDataV2, PaymentMethodTokenResponse>, + ) -> CustomResult { + Ok(format!("{}tokens", self.connector_base_url_payments(req))) + } + } +); + impl ConnectorErrorTypeMapping for Checkout { @@ -677,7 +750,6 @@ macros::macro_connector_flow_status_impls!( SubmitEvidence, DefendDispute, CreateOrder, - PaymentMethodToken, PreAuthenticate, Authenticate, PostAuthenticate, diff --git a/crates/integrations/connector-integration/src/connectors/checkout/transformers.rs b/crates/integrations/connector-integration/src/connectors/checkout/transformers.rs index 23fc86fc2f..8d3cf3358c 100644 --- a/crates/integrations/connector-integration/src/connectors/checkout/transformers.rs +++ b/crates/integrations/connector-integration/src/connectors/checkout/transformers.rs @@ -5,12 +5,13 @@ use common_utils::{ types::MinorUnit, }; use domain_types::{ - connector_flow::{Authorize, Capture, RepeatPayment, SetupMandate, Void}, + connector_flow::{Authorize, Capture, PaymentMethodToken, RepeatPayment, SetupMandate, Void}, connector_types::{ MandateReference, MandateReferenceId, PartnerMerchantIdentifierDetails, PaymentFlowData, - PaymentVoidData, PaymentsAuthorizeData, PaymentsCaptureData, PaymentsResponseData, - PaymentsSyncData, RefundFlowData, RefundSyncData, RefundsData, RefundsResponseData, - RepeatPaymentData, ResponseId, SetupMandateRequestData, + PaymentMethodTokenResponse, PaymentMethodTokenizationData, PaymentVoidData, + PaymentsAuthorizeData, PaymentsCaptureData, PaymentsResponseData, PaymentsSyncData, + RefundFlowData, RefundSyncData, RefundsData, RefundsResponseData, RepeatPaymentData, + ResponseId, SetupMandateRequestData, }, errors::{ConnectorError, IntegrationError, IntegrationErrorContext}, payment_method_data::{ @@ -82,6 +83,16 @@ pub struct WalletSource { /// Constants for ACH payment type const ACH_PAYMENT_TYPE: &str = "ach"; const ACH_COUNTRY_US: &str = "US"; +/// Source `type` Checkout expects for a wallet token that arrives already decrypted into a +/// network token. +const NETWORK_TOKEN_TYPE: &str = "network_token"; +/// Documentation for the connector-decryption path, surfaced on the errors that ask for a token. +const CHECKOUT_TOKENS_DOC_URL: &str = + "https://api-reference.checkout.com/tag/Tokens/#operation/requestAToken"; +const APPLE_PAY_TOKEN_TYPE: &str = "applepay"; +const GOOGLE_PAY_TOKEN_TYPE: &str = "googlepay"; +/// Google Pay does not surface an ECI of its own, and Checkout requires one on network tokens. +const GOOGLE_PAY_DEFAULT_ECI: &str = "06"; /// Checkout.com ACH account holder type (mapped from common_enums::BankHolderType) #[derive(Debug, Serialize)] @@ -246,9 +257,24 @@ pub enum CheckoutPaymentType { Recurring, } +/// Checkout credentials. +/// +/// NOTE — the two key fields are the opposite way round from what the names suggest, and this +/// is deliberate. Checkout issues two keys per account, mapped onto the auth fields as: +/// +/// * `api_key` = the **public** key (`pk_...`). Used *only* by `POST /tokens`, the wallet +/// tokenization exchange. That endpoint rejects the secret key with `403`. +/// * `api_secret` = the **secret** key (`sk_...`). Used by every other endpoint (`/payments`, +/// captures, voids, refunds, syncs). `/payments` rejects the public key with `401`. +/// +/// Do not "fix" this by swapping them or by introducing a separate public-key field: the +/// asymmetry is Checkout's, the field names are the connector auth type's, and the two are matched here on +/// purpose so a merchant's existing Checkout credentials work unchanged. pub struct CheckoutAuthType { + /// Public key (`pk_...`) — see the note on [`CheckoutAuthType`]. Tokenization only. pub api_key: Secret, pub processing_channel_id: Secret, + /// Secret key (`sk_...`) — see the note on [`CheckoutAuthType`]. Everything except tokenization. pub api_secret: Secret, } @@ -425,6 +451,263 @@ fn split_account_holder_name( } } +/// Error for a wallet payload that reaches Authorize while still encrypted. +/// +/// Checkout decrypts wallet payloads at its end, but only behind a separate `POST /tokens` call +/// that is authenticated with the account's *public* key and yields a single-use `tok_...`. A +/// `POST /payments` request cannot carry the raw wallet payload, so the exchange has to happen +/// before Authorize is invoked; the resulting token is then handed back on +/// `payment_method.token` and consumed as `source.type = "token"`. +/// +/// UCS now performs that exchange itself — see the `PaymentMethodToken` flow +/// ([`CheckoutTokenRequest`]) exposed as `PaymentMethodService/Tokenize`. So this is no longer +/// "Checkout cannot do this"; it is "this payload is at the wrong step of a two-call sequence". +/// The arm is deliberately kept rather than tokenizing inline from Authorize: Checkout's tokens +/// are single-use and expire 15 minutes after issue, so minting one inside Authorize would hide a +/// second network call (with different credentials, and its own failure modes) behind a flow the +/// caller believes is one request, and would silently double-charge nothing but double-spend the +/// token on any Authorize retry. Keeping the two calls explicit also lets the caller reuse a token +/// across Authorize and SetupMandate, which is what the tail of this path already supports. +fn encrypted_wallet_needs_token(wallet_name: &str) -> error_stack::Report { + error_stack::report!(IntegrationError::NotSupported { + message: format!("{wallet_name} payload that is still encrypted"), + connector: "checkout", + context: IntegrationErrorContext { + suggested_action: Some( + "Call PaymentMethodService/Tokenize on this connector first — it performs \ + Checkout's POST /tokens exchange (authenticated with the account public key) — \ + then send the returned `tok_...` on `payment_method.token`, or supply the wallet \ + already decrypted as a network token" + .to_owned(), + ), + doc_url: Some(CHECKOUT_TOKENS_DOC_URL.to_owned()), + additional_context: None, + }, + }) +} + +/// Builds the Checkout payment source for a Checkout-issued reference token (`tok_...`). +/// +/// This is the tail of the connector-decryption path: the wallet payload was handed to Checkout's +/// `POST /tokens` endpoint, Checkout decrypted it and returned a single-use token, and the payment +/// itself just references that token, i.e. `PaymentMethodToken::Token` -> +/// `PaymentSource::Wallets { source_type: Token }` mapping. +fn build_wallet_token_source< + T: PaymentMethodDataTypes + std::fmt::Debug + Sync + Send + 'static + Serialize, +>( + token: Secret, + billing_address: Option, +) -> PaymentSource { + PaymentSource::Wallets(WalletSource { + source_type: CheckoutSourceTypes::Token, + token, + billing_address, + }) +} + +/// Builds the Checkout payment source for a wallet whose token has already been decrypted. +/// +/// Checkout accepts a decrypted wallet in the network-token shape (PAN + cryptogram). A wallet that +/// is still encrypted has to go through Checkout's `POST /tokens` exchange first — see +/// [`encrypted_wallet_needs_token`] and [`build_wallet_token_source`]. Shared by the Authorize and +/// SetupMandate flows so a zero-amount mandate setup accepts the same wallets as a regular payment. +fn build_predecrypted_wallet_source< + T: PaymentMethodDataTypes + std::fmt::Debug + Sync + Send + 'static + Serialize, +>( + wallet_data: &WalletData, + billing_address: Option, +) -> Result, error_stack::Report> { + match wallet_data { + WalletData::GooglePay(google_pay_data) => match &google_pay_data.tokenization_data { + domain_types::payment_method_data::GpayTokenizationData::Decrypted( + google_pay_decrypted_data, + ) => { + let expiry_month = google_pay_decrypted_data + .get_expiry_month() + .change_context(IntegrationError::InvalidDataFormat { + field_name: "google_pay_decrypted_data.card_exp_month", + context: Default::default(), + })?; + + let expiry_year = google_pay_decrypted_data + .get_four_digit_expiry_year() + .change_context(IntegrationError::InvalidDataFormat { + field_name: "google_pay_decrypted_data.card_exp_year", + context: Default::default(), + })?; + + Ok(PaymentSource::GooglePayPredecrypt(Box::new( + GooglePayPredecrypt { + _type: NETWORK_TOKEN_TYPE.to_string(), + token: google_pay_decrypted_data + .application_primary_account_number + .clone(), + token_type: GOOGLE_PAY_TOKEN_TYPE.to_string(), + expiry_month, + expiry_year, + eci: GOOGLE_PAY_DEFAULT_ECI.to_string(), + cryptogram: google_pay_decrypted_data.cryptogram.clone(), + billing_address, + }, + ))) + } + domain_types::payment_method_data::GpayTokenizationData::Encrypted(_) => { + Err(encrypted_wallet_needs_token("Google Pay")) + } + }, + WalletData::ApplePay(apple_pay_data) => match apple_pay_data + .payment_data + .get_decrypted_apple_pay_payment_data_optional() + { + Some(apple_pay_decrypt_data) => Ok(PaymentSource::ApplePayPredecrypt(Box::new( + ApplePayPredecrypt { + token: apple_pay_decrypt_data + .application_primary_account_number + .clone(), + decrypt_type: NETWORK_TOKEN_TYPE.to_string(), + token_type: APPLE_PAY_TOKEN_TYPE.to_string(), + expiry_month: apple_pay_decrypt_data.get_expiry_month(), + expiry_year: apple_pay_decrypt_data.get_four_digit_expiry_year(), + eci: apple_pay_decrypt_data.payment_data.eci_indicator.clone(), + cryptogram: apple_pay_decrypt_data + .payment_data + .online_payment_cryptogram + .clone(), + billing_address, + }, + ))), + None => Err(encrypted_wallet_needs_token("Apple Pay")), + }, + _ => Err(IntegrationError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("checkout"), + Default::default(), + ) + .into()), + } +} + +/// Request body for Checkout's `POST /tokens` — the connector-decryption head. +/// +/// Checkout wants the *raw* wallet payload exactly as the wallet SDK produced it, wrapped in a +/// discriminator: `{"type": "applepay" | "googlepay", "token_data": { .. }}`. The response is a +/// single-use `tok_...` that `POST /payments` then consumes as `source.type = "token"` (see +/// [`build_wallet_token_source`]). +#[derive(Debug, Serialize)] +#[serde(rename_all = "lowercase")] +#[serde(tag = "type", content = "token_data")] +pub enum CheckoutTokenRequest { + Googlepay(CheckoutGooglePayData), + Applepay(Box), +} + +/// Google Pay `PaymentData.paymentMethodData.tokenizationData.token`, parsed out of the opaque +/// JSON string the Google Pay SDK hands over. +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CheckoutGooglePayData { + protocol_version: Secret, + signature: Secret, + signed_message: Secret, +} + +/// Apple Pay `PKPaymentToken.paymentData`, parsed out of the base64 blob the Apple Pay SDK +/// hands over. +#[derive(Debug, Serialize, Deserialize)] +pub struct CheckoutApplePayData { + version: Secret, + data: Secret, + signature: Secret, + header: CheckoutApplePayHeader, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CheckoutApplePayHeader { + ephemeral_public_key: Secret, + public_key_hash: Secret, + transaction_id: Secret, +} + +/// Response of `POST /tokens`. Checkout echoes the request `type` and expiry alongside the token; +/// only the token is load-bearing for the payment that follows. +#[derive(Debug, Deserialize, Serialize)] +pub struct CheckoutTokenResponse { + token: Secret, +} + +impl + TryFrom< + CheckoutRouterData< + RouterDataV2< + PaymentMethodToken, + PaymentFlowData, + PaymentMethodTokenizationData, + PaymentMethodTokenResponse, + >, + T, + >, + > for CheckoutTokenRequest +{ + type Error = error_stack::Report; + fn try_from( + item: CheckoutRouterData< + RouterDataV2< + PaymentMethodToken, + PaymentFlowData, + PaymentMethodTokenizationData, + PaymentMethodTokenResponse, + >, + T, + >, + ) -> Result { + match &item.router_data.request.payment_method_data { + PaymentMethodData::Wallet(wallet_data) => match wallet_data { + WalletData::GooglePay(_) => Ok(Self::Googlepay( + wallet_data.get_wallet_token_as_json("Google Pay".to_string())?, + )), + WalletData::ApplePay(_) => Ok(Self::Applepay(Box::new( + wallet_data.get_wallet_token_as_json("Apple Pay".to_string())?, + ))), + _ => Err(IntegrationError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("checkout"), + Default::default(), + ) + .into()), + }, + _ => Err(IntegrationError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("checkout"), + Default::default(), + ) + .into()), + } + } +} + +impl TryFrom> + for RouterDataV2< + PaymentMethodToken, + PaymentFlowData, + PaymentMethodTokenizationData, + PaymentMethodTokenResponse, + > +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(PaymentMethodTokenResponse { + token: item.response.token.expose(), + // Checkout's `tok_...` is single-use and expires after 15 minutes, so it is + // not a durable payment method identifier. + connector_payment_method_id: None, + status_code: item.http_code, + }), + ..item.router_data + }) + } +} + fn build_metadata( metadata: Option>, partner_merchant_identifier_details: Option<&PartnerMerchantIdentifierDetails>, @@ -534,176 +817,98 @@ impl { - let (first_name, last_name) = split_account_holder_name(ccard.card_holder_name); + let (source_var, previous_payment_id, merchant_initiated, store_for_future_use) = + match item.router_data.request.payment_method_data.clone() { + PaymentMethodData::Card(ccard) => { + let (first_name, last_name) = split_account_holder_name(ccard.card_holder_name); + + let payment_source = PaymentSource::Card(CardSource { + source_type: CheckoutSourceTypes::Card, + number: ccard.card_number.clone(), + expiry_month: ccard.card_exp_month.clone(), + expiry_year: ccard.card_exp_year.clone(), + cvv: Some(ccard.card_cvc), + billing_address: billing_details, + account_holder: Some(CheckoutAccountHolderDetails { + first_name, + last_name, + }), + }); + Ok((payment_source, None, Some(false), store_for_future_use)) + } + PaymentMethodData::Wallet(wallet_data) => { + let p_source = build_predecrypted_wallet_source(&wallet_data, billing_details)?; + Ok((p_source, None, Some(false), store_for_future_use)) + } + // Connector-decryption path: the wallet payload was already exchanged for a + // Checkout token (`tok_...`) via `POST /tokens`, so the payment only references + // it. The token is single-use and expires 15 minutes after it was issued. + PaymentMethodData::PaymentMethodToken(token_data) => { + let payment_source = + build_wallet_token_source(token_data.token, billing_details); + Ok((payment_source, None, Some(false), store_for_future_use)) + } + PaymentMethodData::BankDebit(BankDebitData::AchBankDebit { + account_number, + routing_number, + bank_account_holder_name, + card_holder_name, + bank_holder_type, + bank_type, + .. + }) => { + // Get account holder name from bank_account_holder_name, card_holder_name, or billing details + let holder_name = bank_account_holder_name.or(card_holder_name).or_else(|| { + item.router_data + .resource_common_data + .get_billing_full_name() + .ok() + }); - let payment_source = PaymentSource::Card(CardSource { - source_type: CheckoutSourceTypes::Card, - number: ccard.card_number.clone(), - expiry_month: ccard.card_exp_month.clone(), - expiry_year: ccard.card_exp_year.clone(), - cvv: Some(ccard.card_cvc), - billing_address: billing_details, - account_holder: Some(CheckoutAccountHolderDetails { - first_name, - last_name, - }), - }); - Ok((payment_source, None, Some(false), store_for_future_use)) - } - PaymentMethodData::Wallet(wallet_data) => match wallet_data { - WalletData::GooglePay(google_pay_data) => { - match &google_pay_data.tokenization_data { - domain_types::payment_method_data::GpayTokenizationData::Decrypted( - google_pay_decrypted_data, - ) => { - let token = google_pay_decrypted_data - .application_primary_account_number - .clone(); - - let expiry_month = google_pay_decrypted_data - .get_expiry_month() - .change_context(IntegrationError::InvalidDataFormat { - field_name: "google_pay_decrypted_data.card_exp_month", - context: Default::default(), - })?; - - let expiry_year = google_pay_decrypted_data - .get_four_digit_expiry_year() - .change_context(IntegrationError::InvalidDataFormat { - field_name: "google_pay_decrypted_data.card_exp_year", - context: Default::default(), - })?; - - let cryptogram = google_pay_decrypted_data.cryptogram.clone(); - - let p_source = - PaymentSource::GooglePayPredecrypt(Box::new(GooglePayPredecrypt { - _type: "network_token".to_string(), - token, - token_type: "googlepay".to_string(), - expiry_month, - expiry_year, - eci: "06".to_string(), - cryptogram, - billing_address: billing_details, - })); - - Ok((p_source, None, Some(false), store_for_future_use)) - } - domain_types::payment_method_data::GpayTokenizationData::Encrypted(_) => { - Err(IntegrationError::MissingRequiredField { - field_name: "google_pay_decrypted_data", - context: Default::default(), + // Map bank_holder_type to Checkout's expected format + let holder_type: CheckoutAchHolderType = bank_holder_type + .map(Into::into) + .unwrap_or(CheckoutAchHolderType::Individual); + + // Only include account_holder when a name is available to avoid + // sending null first_name/last_name which causes ACH validation errors + let account_holder = match holder_name { + Some(name) => { + let (first_name, last_name) = split_account_holder_name(Some(name)); + Some(AchAccountHolder { + holder_type, + first_name, + last_name, }) } - } - } - WalletData::ApplePay(apple_pay_data) => { - match apple_pay_data - .payment_data - .get_decrypted_apple_pay_payment_data_optional() - { - Some(apple_pay_decrypt_data) => { - let exp_month = apple_pay_decrypt_data.get_expiry_month(); - let expiry_year_4_digit = - apple_pay_decrypt_data.get_four_digit_expiry_year(); - let p_source = - PaymentSource::ApplePayPredecrypt(Box::new(ApplePayPredecrypt { - token: apple_pay_decrypt_data - .application_primary_account_number - .clone(), - decrypt_type: "network_token".to_string(), - token_type: "applepay".to_string(), - expiry_month: exp_month, - expiry_year: expiry_year_4_digit, - eci: apple_pay_decrypt_data.payment_data.eci_indicator.clone(), - cryptogram: apple_pay_decrypt_data - .payment_data - .online_payment_cryptogram - .clone(), - billing_address: billing_details, - })); - Ok((p_source, None, Some(false), store_for_future_use)) - } - None => Err(IntegrationError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("checkout"), - Default::default(), - )), - } + None => None, + }; + + let account_type = CheckoutBankType::try_from( + bank_type.unwrap_or(common_enums::BankType::Savings), + )?; + + let payment_source = PaymentSource::AchBankDebit(AchBankDebitSource { + source_type: ACH_PAYMENT_TYPE.to_string(), + account_type, + country: ACH_COUNTRY_US.to_string(), + account_number: account_number.clone(), + routing_number: routing_number.clone(), + account_holder, + }); + // For ACH bank debit, we typically want to store for future use if it's a mandate payment + let store_for_future = if item.router_data.request.is_mandate_payment() { + Some(true) + } else { + store_for_future_use + }; + Ok((payment_source, None, Some(false), store_for_future)) } _ => Err(IntegrationError::NotImplemented( utils::get_unimplemented_payment_method_error_message("checkout"), Default::default(), )), - }, - PaymentMethodData::BankDebit(BankDebitData::AchBankDebit { - account_number, - routing_number, - bank_account_holder_name, - card_holder_name, - bank_holder_type, - bank_type, - .. - }) => { - // Get account holder name from bank_account_holder_name, card_holder_name, or billing details - let holder_name = bank_account_holder_name.or(card_holder_name).or_else(|| { - item.router_data - .resource_common_data - .get_billing_full_name() - .ok() - }); - - // Map bank_holder_type to Checkout's expected format - let holder_type: CheckoutAchHolderType = bank_holder_type - .map(Into::into) - .unwrap_or(CheckoutAchHolderType::Individual); - - // Only include account_holder when a name is available to avoid - // sending null first_name/last_name which causes ACH validation errors - let account_holder = match holder_name { - Some(name) => { - let (first_name, last_name) = split_account_holder_name(Some(name)); - Some(AchAccountHolder { - holder_type, - first_name, - last_name, - }) - } - None => None, - }; - - let account_type = CheckoutBankType::try_from( - bank_type.unwrap_or(common_enums::BankType::Savings), - )?; - - let payment_source = PaymentSource::AchBankDebit(AchBankDebitSource { - source_type: ACH_PAYMENT_TYPE.to_string(), - account_type, - country: ACH_COUNTRY_US.to_string(), - account_number: account_number.clone(), - routing_number: routing_number.clone(), - account_holder, - }); - // For ACH bank debit, we typically want to store for future use if it's a mandate payment - let store_for_future = if item.router_data.request.is_mandate_payment() { - Some(true) - } else { - store_for_future_use - }; - Ok((payment_source, None, Some(false), store_for_future)) - } - _ => Err(IntegrationError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("checkout"), - Default::default(), - )), - }?; + }?; let authentication_data = item.router_data.request.authentication_data.as_ref(); @@ -1325,6 +1530,19 @@ impl { + let payment_source = + build_predecrypted_wallet_source(&wallet_data, billing_details)?; + Ok((payment_source, None, Some(false), payment_type, Some(true))) + } + // Connector-decryption path: same as Authorize, the wallet payload was already + // exchanged for a Checkout token, so the mandate setup only references it. + PaymentMethodData::PaymentMethodToken(token_data) => { + let payment_source = build_wallet_token_source(token_data.token, billing_details); + Ok((payment_source, None, Some(false), payment_type, Some(true))) + } _ => Err(IntegrationError::NotImplemented( utils::get_unimplemented_payment_method_error_message("checkout"), Default::default(), diff --git a/crates/internal/integration-tests/src/connector_specs/checkout/specs.json b/crates/internal/integration-tests/src/connector_specs/checkout/specs.json index 5cd75f2a84..b645c51ec4 100644 --- a/crates/internal/integration-tests/src/connector_specs/checkout/specs.json +++ b/crates/internal/integration-tests/src/connector_specs/checkout/specs.json @@ -8,6 +8,7 @@ "PaymentService/Refund", "RefundService/Get", "PaymentService/SetupRecurring", - "PaymentService/Void" + "PaymentService/Void", + "PaymentMethodService/Tokenize" ] } diff --git a/data/field_probe/checkout.json b/data/field_probe/checkout.json index 959cfb26f4..ad5179ccf5 100644 --- a/data/field_probe/checkout.json +++ b/data/field_probe/checkout.json @@ -66,8 +66,8 @@ "error": "This feature is not implemented: Selected payment method through checkout" }, "ApplePay": { - "status": "not_implemented", - "error": "This feature is not implemented: Selected payment method through checkout" + "status": "not_supported", + "error": "Apple Pay payload that is still encrypted is not supported by checkout" }, "ApplePayDecrypted": { "status": "supported", @@ -276,8 +276,8 @@ "error": "This feature is not implemented: Selected payment method through checkout" }, "GooglePay": { - "status": "error", - "error": "Stuck on field: google_pay_decrypted_data — Missing required field: google_pay_decrypted_data" + "status": "not_supported", + "error": "Google Pay payload that is still encrypted is not supported by checkout" }, "GooglePayDecrypted": { "status": "supported", @@ -956,20 +956,83 @@ }, "token_authorize": { "default": { - "status": "not_implemented", - "error": "This feature is not implemented: Selected payment method through checkout" + "status": "supported", + "proto_request": { + "merchant_transaction_id": "probe_tokenized_txn_001", + "amount": { + "minor_amount": 1000, + "currency": "USD" + }, + "connector_token": "pm_1AbcXyzStripeTestToken", + "address": { + "billing_address": {} + }, + "capture_method": "AUTOMATIC", + "return_url": "https://example.com/return" + }, + "sample": { + "url": "https://api.sandbox.checkout.com/payments", + "method": "Post", + "headers": { + "authorization": "Bearer probe_secret", + "content-type": "application/json", + "via": "HyperSwitch" + }, + "body": "{\"source\":{\"type\":\"token\",\"token\":\"pm_1AbcXyzStripeTestToken\",\"billing_address\":{}},\"amount\":1000,\"currency\":\"USD\",\"processing_channel_id\":\"probe_id\",\"3ds\":{\"enabled\":false,\"force_3ds\":false,\"eci\":null,\"cryptogram\":null,\"xid\":null,\"version\":null,\"challenge_indicator\":\"no_preference\"},\"success_url\":\"https://example.com/return?status=success\",\"failure_url\":\"https://example.com/return?status=failure\",\"capture\":true,\"reference\":\"probe_tokenized_txn_001\",\"payment_type\":\"Regular\",\"merchant_initiated\":false}" + } } }, "token_setup_recurring": { "default": { - "status": "not_implemented", - "error": "This feature is not implemented: Selected payment method through checkout" + "status": "supported", + "proto_request": { + "merchant_recurring_payment_id": "probe_tokenized_mandate_001", + "amount": { + "minor_amount": 0, + "currency": "USD" + }, + "connector_token": "pm_1AbcXyzStripeTestToken", + "address": { + "billing_address": {} + }, + "customer_acceptance": { + "acceptance_type": "ONLINE", + "accepted_at": 0, + "online_mandate_details": { + "ip_address": "127.0.0.1", + "user_agent": "Mozilla/5.0" + } + }, + "setup_mandate_details": { + "mandate_type": { + "multi_use": { + "amount": 0, + "currency": "USD", + "amount_money": { + "minor_amount": 0, + "currency": "USD" + } + } + } + }, + "setup_future_usage": "OFF_SESSION" + }, + "sample": { + "url": "https://api.sandbox.checkout.com/payments", + "method": "Post", + "headers": { + "authorization": "Bearer probe_secret", + "content-type": "application/json", + "via": "HyperSwitch" + }, + "body": "{\"source\":{\"type\":\"token\",\"token\":\"pm_1AbcXyzStripeTestToken\",\"billing_address\":{}},\"amount\":0,\"currency\":\"USD\",\"processing_channel_id\":\"probe_id\",\"3ds\":{\"enabled\":false,\"force_3ds\":false,\"eci\":null,\"cryptogram\":null,\"xid\":null,\"version\":null,\"challenge_indicator\":\"no_preference\"},\"success_url\":null,\"failure_url\":null,\"capture\":true,\"reference\":\"probe_tokenized_mandate_001\",\"payment_type\":\"Unscheduled\",\"merchant_initiated\":false,\"store_for_future_use\":true}" + } } }, "tokenize": { "default": { "status": "not_implemented", - "error": "This feature is not implemented: payment_method_token flow for checkout" + "error": "This feature is not implemented: Selected payment method through checkout" } }, "verify_redirect": { diff --git a/data/integration-source-links.json b/data/integration-source-links.json index 5fea39f78b..4f2da3849e 100644 --- a/data/integration-source-links.json +++ b/data/integration-source-links.json @@ -116,5 +116,24 @@ "https://docs.tamara.co/docs/postman-collection", "https://docs.tamara.co/docs/testing-checklist", "https://docs.tamara.co/docs/online-go-live-testing-checklist" + ], + "Checkout": [ + "https://www.checkout.com/docs/payments/add-payment-methods/apple-pay/unified-payments-api", + "https://www.checkout.com/docs/payments/add-payment-methods/google-pay/unified-payments-api", + "https://api-reference.checkout.com/", + "https://api-reference.checkout.com/v1/swagger.json", + "https://www.checkout.com/docs/payments/accept-payments/accept-a-payment-using-the-payments-api", + "https://www.checkout.com/docs/developer-resources/api/api-endpoints", + "https://www.checkout.com/docs/developer-resources/api/manage-api-keys/api-keys", + "https://www.checkout.com/docs/developer-resources/api/idempotency", + "https://www.checkout.com/docs/developer-resources/event-notifications/receive-webhooks", + "https://www.checkout.com/docs/developer-resources/event-notifications/receive-webhooks/configure-your-webhook-server", + "https://www.checkout.com/docs/developer-resources/event-notifications/event-types", + "https://www.checkout.com/docs/developer-resources/codes/api-response-codes", + "https://www.checkout.com/docs/developer-resources/codes/error-codes", + "https://www.checkout.com/docs/developer-resources/codes/eci-values", + "https://www.checkout.com/docs/payments/store-and-manage-credentials/store-credentials/network-tokens", + "https://www.checkout.com/docs/developer-resources/testing/test-cards", + "https://www.checkout.com/docs/developer-resources/testing/payments-testing" ] -} \ No newline at end of file +} diff --git a/docs-generated/all_connector.md b/docs-generated/all_connector.md index 22c10cb027..d5ff17de95 100644 --- a/docs-generated/all_connector.md +++ b/docs-generated/all_connector.md @@ -43,7 +43,7 @@ Authorize a payment amount on a payment method. This reserves funds without capt | [Cashfree](connectors/cashfree.md) | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | x | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | x | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | x | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | x | ? | x | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | | [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 | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | +| [Checkout.com](connectors/checkout.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 | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | 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 | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | @@ -157,7 +157,7 @@ Consolidated view of Get, Void, Refund, Capture, Reverse, CreateOrder, and other | [Cashfree](connectors/cashfree.md) | ✓ | ✓ | x | ✓ | ✓ | ✓ | x | ⚠ | ⚠ | ? | ⚠ | ? | ⚠ | ⚠ | ⚠ | ✓ | x | ⚠ | ⚠ | ⚠ | x | x | x | x | x | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | | [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 | +| [Checkout.com](connectors/checkout.md) | ✓ | ✓ | x | ✓ | ⚠ | ✓ | ⚠ | ⚠ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | x | ✓ | x | ⚠ | ⚠ | ⚠ | x | x | x | x | x | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | | [CryptoPay](connectors/cryptopay.md) | ✓ | x | x | x | x | ⚠ | x | ⚠ | ⚠ | x | ⚠ | x | ⚠ | ⚠ | ⚠ | ⚠ | x | ⚠ | ⚠ | x | x | x | x | x | x | x | x | x | x | x | x | x | ⚠ | x | x | ✓ | ✓ | x | | [CyberSource](connectors/cybersource.md) | ✓ | ✓ | ✓ | ✓ | ⚠ | ✓ | ✓ | ⚠ | ? | ✓ | ? | ✓ | ? | ✓ | ✓ | ✓ | x | ⚠ | ⚠ | ⚠ | x | x | x | x | x | ⚠ | ⚠ | ? | ✓ | ✓ | ✓ | x | ⚠ | x | x | ⚠ | ⚠ | x | | [Datatrans](connectors/datatrans.md) | ✓ | ✓ | ✓ | ✓ | ⚠ | ✓ | ⚠ | ⚠ | ✓ | ✓ | ⚠ | ✓ | ✓ | ? | ⚠ | ✓ | x | ⚠ | ⚠ | ⚠ | x | x | x | x | x | ⚠ | ⚠ | ✓ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | ⚠ | x | diff --git a/docs-generated/connectors/checkout.md b/docs-generated/connectors/checkout.md index 52e5f0c464..80afef8722 100644 --- a/docs-generated/connectors/checkout.md +++ b/docs-generated/connectors/checkout.md @@ -131,7 +131,7 @@ Simple payment that authorizes and captures in one call. Use for immediate charg | `PENDING` | Payment processing — await webhook for final status before fulfilling | | `FAILED` | Payment declined — surface error to customer, do not retry without new details | -**Examples:** [Python](../../examples/checkout/checkout.py#L201) · [JavaScript](../../examples/checkout/checkout.js) · [Kotlin](../../examples/checkout/checkout.kt#L117) · [Rust](../../examples/checkout/checkout.rs#L253) +**Examples:** [Python](../../examples/checkout/checkout.py#L250) · [JavaScript](../../examples/checkout/checkout.js) · [Kotlin](../../examples/checkout/checkout.kt#L117) · [Rust](../../examples/checkout/checkout.rs#L315) ### Card Payment (Authorize + Capture) @@ -145,25 +145,25 @@ Two-step card payment. First authorize, then capture. Use when you need to verif | `PENDING` | Awaiting async confirmation — wait for webhook before capturing | | `FAILED` | Payment declined — surface error to customer, do not retry without new details | -**Examples:** [Python](../../examples/checkout/checkout.py#L220) · [JavaScript](../../examples/checkout/checkout.js) · [Kotlin](../../examples/checkout/checkout.kt#L133) · [Rust](../../examples/checkout/checkout.rs#L269) +**Examples:** [Python](../../examples/checkout/checkout.py#L269) · [JavaScript](../../examples/checkout/checkout.js) · [Kotlin](../../examples/checkout/checkout.kt#L133) · [Rust](../../examples/checkout/checkout.rs#L331) ### Refund Return funds to the customer for a completed payment. -**Examples:** [Python](../../examples/checkout/checkout.py#L245) · [JavaScript](../../examples/checkout/checkout.js) · [Kotlin](../../examples/checkout/checkout.kt#L155) · [Rust](../../examples/checkout/checkout.rs#L292) +**Examples:** [Python](../../examples/checkout/checkout.py#L294) · [JavaScript](../../examples/checkout/checkout.js) · [Kotlin](../../examples/checkout/checkout.kt#L155) · [Rust](../../examples/checkout/checkout.rs#L354) ### Void Payment Cancel an authorized but not-yet-captured payment. -**Examples:** [Python](../../examples/checkout/checkout.py#L270) · [JavaScript](../../examples/checkout/checkout.js) · [Kotlin](../../examples/checkout/checkout.kt#L177) · [Rust](../../examples/checkout/checkout.rs#L315) +**Examples:** [Python](../../examples/checkout/checkout.py#L319) · [JavaScript](../../examples/checkout/checkout.js) · [Kotlin](../../examples/checkout/checkout.kt#L177) · [Rust](../../examples/checkout/checkout.rs#L377) ### Get Payment Status Retrieve current payment status from the connector. -**Examples:** [Python](../../examples/checkout/checkout.py#L292) · [JavaScript](../../examples/checkout/checkout.js) · [Kotlin](../../examples/checkout/checkout.kt#L196) · [Rust](../../examples/checkout/checkout.rs#L334) +**Examples:** [Python](../../examples/checkout/checkout.py#L341) · [JavaScript](../../examples/checkout/checkout.js) · [Kotlin](../../examples/checkout/checkout.kt#L196) · [Rust](../../examples/checkout/checkout.rs#L396) ## API Reference @@ -178,6 +178,8 @@ Retrieve current payment status from the connector. | [PaymentService.Refund](#paymentservicerefund) | Payments | `PaymentServiceRefundRequest` | | [RefundService.Get](#refundserviceget) | Refunds | `RefundServiceGetRequest` | | [PaymentService.SetupRecurring](#paymentservicesetuprecurring) | Payments | `PaymentServiceSetupRecurringRequest` | +| [PaymentService.TokenAuthorize](#paymentservicetokenauthorize) | Payments | `PaymentServiceTokenAuthorizeRequest` | +| [PaymentService.TokenSetupRecurring](#paymentservicetokensetuprecurring) | Payments | `PaymentServiceTokenSetupRecurringRequest` | | [PaymentService.Void](#paymentservicevoid) | Payments | `PaymentServiceVoidRequest` | ### Payments @@ -197,10 +199,10 @@ Authorize a payment amount on a payment method. This reserves funds without capt |----------------|:---------:| | Card | ✓ | | Bancontact | ⚠ | -| Apple Pay | ⚠ | +| Apple Pay | x | | Apple Pay Dec | ✓ | | Apple Pay SDK | ⚠ | -| Google Pay | ? | +| Google Pay | x | | Google Pay Dec | ✓ | | Google Pay SDK | ⚠ | | PayPal SDK | ⚠ | @@ -324,7 +326,7 @@ Authorize a payment amount on a payment method. This reserves funds without capt } ``` -**Examples:** [Python](../../examples/checkout/checkout.py) · [TypeScript](../../examples/checkout/checkout.ts#L326) · [Kotlin](../../examples/checkout/checkout.kt#L214) · [Rust](../../examples/checkout/checkout.rs) +**Examples:** [Python](../../examples/checkout/checkout.py) · [TypeScript](../../examples/checkout/checkout.ts#L379) · [Kotlin](../../examples/checkout/checkout.kt#L214) · [Rust](../../examples/checkout/checkout.rs) #### PaymentService.Capture @@ -335,7 +337,7 @@ Finalize an authorized payment by transferring funds. Captures the authorized am | **Request** | `PaymentServiceCaptureRequest` | | **Response** | `PaymentServiceCaptureResponse` | -**Examples:** [Python](../../examples/checkout/checkout.py) · [TypeScript](../../examples/checkout/checkout.ts#L335) · [Kotlin](../../examples/checkout/checkout.kt#L226) · [Rust](../../examples/checkout/checkout.rs) +**Examples:** [Python](../../examples/checkout/checkout.py) · [TypeScript](../../examples/checkout/checkout.ts#L388) · [Kotlin](../../examples/checkout/checkout.kt#L226) · [Rust](../../examples/checkout/checkout.rs) #### PaymentService.Get @@ -346,7 +348,7 @@ Retrieve current payment status from the payment processor. Enables synchronizat | **Request** | `PaymentServiceGetRequest` | | **Response** | `PaymentServiceGetResponse` | -**Examples:** [Python](../../examples/checkout/checkout.py) · [TypeScript](../../examples/checkout/checkout.ts#L344) · [Kotlin](../../examples/checkout/checkout.kt#L236) · [Rust](../../examples/checkout/checkout.rs) +**Examples:** [Python](../../examples/checkout/checkout.py) · [TypeScript](../../examples/checkout/checkout.ts#L397) · [Kotlin](../../examples/checkout/checkout.kt#L236) · [Rust](../../examples/checkout/checkout.rs) #### PaymentService.ProxyAuthorize @@ -357,7 +359,7 @@ Authorize using vault-aliased card data. Proxy substitutes before connector. | **Request** | `PaymentServiceProxyAuthorizeRequest` | | **Response** | `PaymentServiceAuthorizeResponse` | -**Examples:** [Python](../../examples/checkout/checkout.py) · [TypeScript](../../examples/checkout/checkout.ts#L353) · [Kotlin](../../examples/checkout/checkout.kt#L244) · [Rust](../../examples/checkout/checkout.rs) +**Examples:** [Python](../../examples/checkout/checkout.py) · [TypeScript](../../examples/checkout/checkout.ts#L406) · [Kotlin](../../examples/checkout/checkout.kt#L244) · [Rust](../../examples/checkout/checkout.rs) #### PaymentService.ProxySetupRecurring @@ -368,7 +370,7 @@ Setup recurring mandate using vault-aliased card data. | **Request** | `PaymentServiceProxySetupRecurringRequest` | | **Response** | `PaymentServiceSetupRecurringResponse` | -**Examples:** [Python](../../examples/checkout/checkout.py) · [TypeScript](../../examples/checkout/checkout.ts#L362) · [Kotlin](../../examples/checkout/checkout.kt#L273) · [Rust](../../examples/checkout/checkout.rs) +**Examples:** [Python](../../examples/checkout/checkout.py) · [TypeScript](../../examples/checkout/checkout.ts#L415) · [Kotlin](../../examples/checkout/checkout.kt#L273) · [Rust](../../examples/checkout/checkout.rs) #### PaymentService.Refund @@ -379,7 +381,7 @@ Process a partial or full refund for a captured payment. Returns funds to the cu | **Request** | `PaymentServiceRefundRequest` | | **Response** | `RefundResponse` | -**Examples:** [Python](../../examples/checkout/checkout.py) · [TypeScript](../../examples/checkout/checkout.ts#L380) · [Kotlin](../../examples/checkout/checkout.kt#L336) · [Rust](../../examples/checkout/checkout.rs) +**Examples:** [Python](../../examples/checkout/checkout.py) · [TypeScript](../../examples/checkout/checkout.ts#L433) · [Kotlin](../../examples/checkout/checkout.kt#L336) · [Rust](../../examples/checkout/checkout.rs) #### PaymentService.SetupRecurring @@ -390,7 +392,29 @@ Configure a payment method for recurring billing. Sets up the mandate and paymen | **Request** | `PaymentServiceSetupRecurringRequest` | | **Response** | `PaymentServiceSetupRecurringResponse` | -**Examples:** [Python](../../examples/checkout/checkout.py) · [TypeScript](../../examples/checkout/checkout.ts#L398) · [Kotlin](../../examples/checkout/checkout.kt#L358) · [Rust](../../examples/checkout/checkout.rs) +**Examples:** [Python](../../examples/checkout/checkout.py) · [TypeScript](../../examples/checkout/checkout.ts#L451) · [Kotlin](../../examples/checkout/checkout.kt#L358) · [Rust](../../examples/checkout/checkout.rs) + +#### PaymentService.TokenAuthorize + +Authorize using a connector-issued payment method token. + +| | Message | +|---|---------| +| **Request** | `PaymentServiceTokenAuthorizeRequest` | +| **Response** | `PaymentServiceAuthorizeResponse` | + +**Examples:** [Python](../../examples/checkout/checkout.py) · [TypeScript](../../examples/checkout/checkout.ts#L460) · [Kotlin](../../examples/checkout/checkout.kt#L397) · [Rust](../../examples/checkout/checkout.rs) + +#### PaymentService.TokenSetupRecurring + +Setup a recurring mandate using a connector token. + +| | Message | +|---|---------| +| **Request** | `PaymentServiceTokenSetupRecurringRequest` | +| **Response** | `PaymentServiceSetupRecurringResponse` | + +**Examples:** [Python](../../examples/checkout/checkout.py) · [TypeScript](../../examples/checkout/checkout.ts#L469) · [Kotlin](../../examples/checkout/checkout.kt#L418) · [Rust](../../examples/checkout/checkout.rs) #### PaymentService.Void @@ -401,7 +425,7 @@ Cancel an authorized payment that has not been captured. Releases held funds bac | **Request** | `PaymentServiceVoidRequest` | | **Response** | `PaymentServiceVoidResponse` | -**Examples:** [Python](../../examples/checkout/checkout.py) · [TypeScript](../../examples/checkout/checkout.ts) · [Kotlin](../../examples/checkout/checkout.kt#L397) · [Rust](../../examples/checkout/checkout.rs) +**Examples:** [Python](../../examples/checkout/checkout.py) · [TypeScript](../../examples/checkout/checkout.ts) · [Kotlin](../../examples/checkout/checkout.kt#L458) · [Rust](../../examples/checkout/checkout.rs) ### Refunds @@ -414,7 +438,7 @@ Retrieve refund status from the payment processor. Tracks refund progress throug | **Request** | `RefundServiceGetRequest` | | **Response** | `RefundResponse` | -**Examples:** [Python](../../examples/checkout/checkout.py) · [TypeScript](../../examples/checkout/checkout.ts#L389) · [Kotlin](../../examples/checkout/checkout.kt#L346) · [Rust](../../examples/checkout/checkout.rs) +**Examples:** [Python](../../examples/checkout/checkout.py) · [TypeScript](../../examples/checkout/checkout.ts#L442) · [Kotlin](../../examples/checkout/checkout.kt#L346) · [Rust](../../examples/checkout/checkout.rs) ### Mandates @@ -427,4 +451,4 @@ Charge using an existing stored recurring payment instruction. Processes repeat | **Request** | `RecurringPaymentServiceChargeRequest` | | **Response** | `RecurringPaymentServiceChargeResponse` | -**Examples:** [Python](../../examples/checkout/checkout.py) · [TypeScript](../../examples/checkout/checkout.ts#L371) · [Kotlin](../../examples/checkout/checkout.kt#L305) · [Rust](../../examples/checkout/checkout.rs) +**Examples:** [Python](../../examples/checkout/checkout.py) · [TypeScript](../../examples/checkout/checkout.ts#L424) · [Kotlin](../../examples/checkout/checkout.kt#L305) · [Rust](../../examples/checkout/checkout.rs) diff --git a/docs-generated/llms.txt b/docs-generated/llms.txt index 8a88682a0b..9731afabca 100644 --- a/docs-generated/llms.txt +++ b/docs-generated/llms.txt @@ -187,7 +187,7 @@ connector_id: checkout doc: docs/connectors/checkout.md scenarios: checkout_autocapture, checkout_card, refund, void_payment, get_payment payment_methods: Ach, ApplePayDecrypted, Card, GooglePayDecrypted -flows: authorize, capture, get, proxy_authorize, proxy_setup_recurring, recurring_charge, refund, refund_get, setup_recurring, void +flows: authorize, capture, get, proxy_authorize, proxy_setup_recurring, recurring_charge, refund, refund_get, setup_recurring, token_authorize, token_setup_recurring, void examples_python: examples/checkout/checkout.py ## CryptoPay diff --git a/examples/checkout/checkout.kt b/examples/checkout/checkout.kt index ea63b76580..3e18bfc347 100644 --- a/examples/checkout/checkout.kt +++ b/examples/checkout/checkout.kt @@ -26,7 +26,7 @@ import payments.ConnectorSpecificConfig import types.Payment.CheckoutConfig import payments.SecretString -val SUPPORTED_FLOWS = listOf("authorize", "capture", "get", "proxy_authorize", "proxy_setup_recurring", "recurring_charge", "refund", "refund_get", "setup_recurring", "void") +val SUPPORTED_FLOWS = listOf("authorize", "capture", "get", "proxy_authorize", "proxy_setup_recurring", "recurring_charge", "refund", "refund_get", "setup_recurring", "token_authorize", "token_setup_recurring", "void") val _defaultConfig: ConnectorConfig = ConnectorConfig.newBuilder() .setOptions(SdkOptions.newBuilder().setEnvironment(Environment.SANDBOX).build()) @@ -393,6 +393,67 @@ fun setupRecurring(txnId: String, config: ConnectorConfig = _defaultConfig) { } } +// Flow: PaymentService.TokenAuthorize +fun tokenAuthorize(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = PaymentClient(config) + val request = PaymentServiceTokenAuthorizeRequest.newBuilder().apply { + merchantTransactionId = "probe_tokenized_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"). + } + connectorTokenBuilder.value = "pm_1AbcXyzStripeTestToken" // Connector-issued token. Replaces PaymentMethod entirely. Examples: Stripe pm_xxx, Adyen recurringDetailReference, Braintree nonce. + addressBuilder.apply { + billingAddressBuilder.apply { + } + } + captureMethod = CaptureMethod.AUTOMATIC + returnUrl = "https://example.com/return" + }.build() + val response = client.token_authorize(request) + println("Status: ${response.status.name}") +} + +// Flow: PaymentService.TokenSetupRecurring +fun tokenSetupRecurring(txnId: String, config: ConnectorConfig = _defaultConfig) { + val client = PaymentClient(config) + val request = PaymentServiceTokenSetupRecurringRequest.newBuilder().apply { + merchantRecurringPaymentId = "probe_tokenized_mandate_001" + amountBuilder.apply { + minorAmount = 0L // Amount in minor units (e.g., 1000 = $10.00). + currency = Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + } + connectorTokenBuilder.value = "pm_1AbcXyzStripeTestToken" + addressBuilder.apply { + billingAddressBuilder.apply { + } + } + customerAcceptanceBuilder.apply { + acceptanceType = AcceptanceType.ONLINE // Type of acceptance (e.g., online, offline). + acceptedAt = 0L // Timestamp when the acceptance was made (Unix timestamp, seconds since epoch). + onlineMandateDetailsBuilder.apply { // Details if the acceptance was an online mandate. + ipAddress = "127.0.0.1" // IP address from which the mandate was accepted. + userAgent = "Mozilla/5.0" // User agent string of the browser used for mandate acceptance. + } + } + setupMandateDetailsBuilder.apply { + mandateTypeBuilder.apply { // Type of mandate (single_use or multi_use) with amount details. + multiUseBuilder.apply { // Multi use mandate with amount details (for recurring payments). + amount = 0L // Use amount_money instead (will be removed in a future release). + currency = Currency.USD // Use amount_money.currency instead (will be removed in a future release). + amountMoneyBuilder.apply { // Amount in Money type. + minorAmount = 0L // Amount in minor units (e.g., 1000 = $10.00). + currency = Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + } + } + } + } + setupFutureUsage = FutureUsage.OFF_SESSION + }.build() + val response = client.token_setup_recurring(request) + println("Status: ${response.status.name}") +} + // Flow: PaymentService.Void fun void(txnId: String, config: ConnectorConfig = _defaultConfig) { val client = PaymentClient(config) @@ -422,7 +483,9 @@ fun main(args: Array) { "refund" -> refund(txnId) "refundGet" -> refundGet(txnId) "setupRecurring" -> setupRecurring(txnId) + "tokenAuthorize" -> tokenAuthorize(txnId) + "tokenSetupRecurring" -> tokenSetupRecurring(txnId) "void" -> void(txnId) - else -> System.err.println("Unknown flow: $flow. Available: processCheckoutAutocapture, processCheckoutCard, processRefund, processVoidPayment, processGetPayment, authorize, capture, get, proxyAuthorize, proxySetupRecurring, recurringCharge, refund, refundGet, setupRecurring, void") + else -> System.err.println("Unknown flow: $flow. Available: processCheckoutAutocapture, processCheckoutCard, processRefund, processVoidPayment, processGetPayment, authorize, capture, get, proxyAuthorize, proxySetupRecurring, recurringCharge, refund, refundGet, setupRecurring, tokenAuthorize, tokenSetupRecurring, void") } } diff --git a/examples/checkout/checkout.py b/examples/checkout/checkout.py index 7c6fc5f8a4..b64f40bc6b 100644 --- a/examples/checkout/checkout.py +++ b/examples/checkout/checkout.py @@ -12,7 +12,7 @@ from payments import RefundClient from payments.generated import sdk_config_pb2, payment_pb2, payment_methods_pb2 -SUPPORTED_FLOWS = ["authorize", "capture", "get", "proxy_authorize", "proxy_setup_recurring", "recurring_charge", "refund", "refund_get", "setup_recurring", "void"] +SUPPORTED_FLOWS = ["authorize", "capture", "get", "proxy_authorize", "proxy_setup_recurring", "recurring_charge", "refund", "refund_get", "setup_recurring", "token_authorize", "token_setup_recurring", "void"] _default_config = sdk_config_pb2.ConnectorConfig( options=sdk_config_pb2.SdkOptions(environment=sdk_config_pb2.Environment.SANDBOX), @@ -193,6 +193,55 @@ def _build_setup_recurring_request(): ), ) +def _build_token_authorize_request(): + return payment_pb2.PaymentServiceTokenAuthorizeRequest( + merchant_transaction_id="probe_tokenized_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"). + ), + connector_token=payment_methods_pb2.SecretString(value="pm_1AbcXyzStripeTestToken"), # Connector-issued token. Replaces PaymentMethod entirely. Examples: Stripe pm_xxx, Adyen recurringDetailReference, Braintree nonce. + address=payment_pb2.PaymentAddress( + billing_address=payment_pb2.Address(), + ), + capture_method=payment_pb2.CaptureMethod.Value("AUTOMATIC"), + return_url="https://example.com/return", + ) + +def _build_token_setup_recurring_request(): + return payment_pb2.PaymentServiceTokenSetupRecurringRequest( + merchant_recurring_payment_id="probe_tokenized_mandate_001", + amount=payment_pb2.Money( + minor_amount=0, # Amount in minor units (e.g., 1000 = $10.00). + currency=payment_pb2.Currency.Value("USD"), # ISO 4217 currency code (e.g., "USD", "EUR"). + ), + connector_token=payment_methods_pb2.SecretString(value="pm_1AbcXyzStripeTestToken"), + address=payment_pb2.PaymentAddress( + billing_address=payment_pb2.Address(), + ), + customer_acceptance=payment_pb2.CustomerAcceptance( + acceptance_type=payment_pb2.AcceptanceType.Value("ONLINE"), # Type of acceptance (e.g., online, offline). + accepted_at=0, # Timestamp when the acceptance was made (Unix timestamp, seconds since epoch). + online_mandate_details=payment_pb2.OnlineMandate( # Details if the acceptance was an online mandate. + ip_address="127.0.0.1", # IP address from which the mandate was accepted. + user_agent="Mozilla/5.0", # User agent string of the browser used for mandate acceptance. + ), + ), + setup_mandate_details=payment_pb2.SetupMandateDetails( + mandate_type=payment_pb2.MandateType( # Type of mandate (single_use or multi_use) with amount details. + multi_use=payment_pb2.MandateAmountData( + amount=0, # Use amount_money instead (will be removed in a future release). + currency=payment_pb2.Currency.Value("USD"), # Use amount_money.currency instead (will be removed in a future release). + amount_money=payment_pb2.Money( # Amount in Money type. + minor_amount=0, # Amount in minor units (e.g., 1000 = $10.00). + currency=payment_pb2.Currency.Value("USD"), # ISO 4217 currency code (e.g., "USD", "EUR"). + ), + ), + ), + ), + setup_future_usage=payment_pb2.FutureUsage.Value("OFF_SESSION"), + ) + def _build_void_request(connector_transaction_id: str): return payment_pb2.PaymentServiceVoidRequest( merchant_void_id="probe_void_001", # Identification. @@ -383,6 +432,24 @@ async def process_setup_recurring(merchant_transaction_id: str, config: sdk_conf return {"status": setup_response.status, "mandate_id": setup_response.connector_recurring_payment_id} +async def process_token_authorize(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Flow: PaymentService.TokenAuthorize""" + payment_client = PaymentClient(config) + + token_response = await payment_client.token_authorize(_build_token_authorize_request()) + + return {"status": token_response.status} + + +async def process_token_setup_recurring(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): + """Flow: PaymentService.TokenSetupRecurring""" + payment_client = PaymentClient(config) + + token_response = await payment_client.token_setup_recurring(_build_token_setup_recurring_request()) + + return {"status": token_response.status} + + async def process_void(merchant_transaction_id: str, config: sdk_config_pb2.ConnectorConfig = _default_config): """Flow: PaymentService.Void""" payment_client = PaymentClient(config) diff --git a/examples/checkout/checkout.rs b/examples/checkout/checkout.rs index 3db71a0ca7..e99f5ca5c8 100644 --- a/examples/checkout/checkout.rs +++ b/examples/checkout/checkout.rs @@ -24,6 +24,8 @@ pub const SUPPORTED_FLOWS: &[&str] = &[ "refund", "refund_get", "setup_recurring", + "token_authorize", + "token_setup_recurring", "void", ]; @@ -274,6 +276,71 @@ pub fn build_setup_recurring_request() -> PaymentServiceSetupRecurringRequest { } } +pub fn build_token_authorize_request() -> PaymentServiceTokenAuthorizeRequest { + PaymentServiceTokenAuthorizeRequest { + merchant_transaction_id: Some("probe_tokenized_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"). + }), + connector_token: Some(Secret::new("pm_1AbcXyzStripeTestToken".to_string())), // Connector-issued token. Replaces PaymentMethod entirely. Examples: Stripe pm_xxx, Adyen recurringDetailReference, Braintree nonce. + address: Some(PaymentAddress { + billing_address: Some(Address { + ..Default::default() + }), + ..Default::default() + }), + capture_method: Some(CaptureMethod::Automatic.into()), + return_url: Some("https://example.com/return".to_string()), + ..Default::default() + } +} + +pub fn build_token_setup_recurring_request() -> PaymentServiceTokenSetupRecurringRequest { + PaymentServiceTokenSetupRecurringRequest { + merchant_recurring_payment_id: "probe_tokenized_mandate_001".to_string(), + amount: Some(Money { + minor_amount: 0, // Amount in minor units (e.g., 1000 = $10.00). + currency: Currency::Usd.into(), // ISO 4217 currency code (e.g., "USD", "EUR"). + }), + connector_token: Some(Secret::new("pm_1AbcXyzStripeTestToken".to_string())), + address: Some(PaymentAddress { + billing_address: Some(Address { + ..Default::default() + }), + ..Default::default() + }), + customer_acceptance: Some(CustomerAcceptance { + acceptance_type: AcceptanceType::Online.into(), // Type of acceptance (e.g., online, offline). + accepted_at: 0, // Timestamp when the acceptance was made (Unix timestamp, seconds since epoch). + online_mandate_details: Some(OnlineMandate { + // Details if the acceptance was an online mandate. + ip_address: Some("127.0.0.1".to_string()), // IP address from which the mandate was accepted. + user_agent: "Mozilla/5.0".to_string(), // User agent string of the browser used for mandate acceptance. + }), + }), + setup_mandate_details: Some(SetupMandateDetails { + mandate_type: Some(MandateType { + // Type of mandate (single_use or multi_use) with amount details. + mandate_type: Some(mandate_type::MandateType::MultiUse(MandateAmountData { + amount: 0, // Use amount_money instead (will be removed in a future release). + currency: Currency::Usd.into(), // Use amount_money.currency instead (will be removed in a future release). + amount_money: Some(Money { + // Amount in Money type. + minor_amount: 0, // Amount in minor units (e.g., 1000 = $10.00). + currency: Currency::Usd.into(), // ISO 4217 currency code (e.g., "USD", "EUR"). + }), + ..Default::default() + })), + ..Default::default() + }), + ..Default::default() + }), + setup_future_usage: Some(FutureUsage::OffSession.into()), + ..Default::default() + } +} + pub fn build_void_request(connector_transaction_id: &str) -> PaymentServiceVoidRequest { PaymentServiceVoidRequest { merchant_void_id: Some("probe_void_001".to_string()), // Identification. @@ -596,6 +663,30 @@ pub async fn process_setup_recurring( )) } +// Flow: PaymentService.TokenAuthorize +#[allow(dead_code)] +pub async fn process_token_authorize( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + let response = client + .token_authorize(build_token_authorize_request(), &HashMap::new(), None) + .await?; + Ok(format!("status: {:?}", response.status())) +} + +// Flow: PaymentService.TokenSetupRecurring +#[allow(dead_code)] +pub async fn process_token_setup_recurring( + client: &ConnectorClient, + _merchant_transaction_id: &str, +) -> Result> { + let response = client + .token_setup_recurring(build_token_setup_recurring_request(), &HashMap::new(), None) + .await?; + Ok(format!("status: {:?}", response.status())) +} + // Flow: PaymentService.Void #[allow(dead_code)] pub async fn process_void( @@ -633,9 +724,11 @@ async fn main() { "process_recurring_charge" => process_recurring_charge(&client, "txn_001").await, "process_refund_get" => process_refund_get(&client, "txn_001").await, "process_setup_recurring" => process_setup_recurring(&client, "txn_001").await, + "process_token_authorize" => process_token_authorize(&client, "txn_001").await, + "process_token_setup_recurring" => process_token_setup_recurring(&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_proxy_setup_recurring, process_recurring_charge, process_refund_get, process_setup_recurring, process_void", flow); + eprintln!("Unknown flow: {}. Available: process_checkout_autocapture, process_checkout_card, process_refund, process_void_payment, process_get_payment, process_authorize, process_capture, process_get, process_proxy_authorize, process_proxy_setup_recurring, process_recurring_charge, process_refund_get, process_setup_recurring, process_token_authorize, process_token_setup_recurring, process_void", flow); return; } }; diff --git a/examples/checkout/checkout.ts b/examples/checkout/checkout.ts index 5f89edc75b..6f93b109df 100644 --- a/examples/checkout/checkout.ts +++ b/examples/checkout/checkout.ts @@ -7,7 +7,7 @@ import { PaymentClient, RecurringPaymentClient, RefundClient, types } from 'hyperswitch-prism'; const { Environment, AcceptanceType, AuthenticationType, CaptureMethod, CardNetwork, Currency, FutureUsage, PaymentMethodType } = types; -export const SUPPORTED_FLOWS = ["authorize", "capture", "get", "proxy_authorize", "proxy_setup_recurring", "recurring_charge", "refund", "refund_get", "setup_recurring", "void"]; +export const SUPPORTED_FLOWS = ["authorize", "capture", "get", "proxy_authorize", "proxy_setup_recurring", "recurring_charge", "refund", "refund_get", "setup_recurring", "token_authorize", "token_setup_recurring", "void"]; const _defaultConfig: types.IConnectorConfig = { options: { @@ -198,6 +198,59 @@ function _buildSetupRecurringRequest(): types.IPaymentServiceSetupRecurringReque }; } +function _buildTokenAuthorizeRequest(): types.IPaymentServiceTokenAuthorizeRequest { + return { + "merchantTransactionId": "probe_tokenized_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"). + }, + "connectorToken": {"value": "pm_1AbcXyzStripeTestToken"}, // Connector-issued token. Replaces PaymentMethod entirely. Examples: Stripe pm_xxx, Adyen recurringDetailReference, Braintree nonce. + "address": { + "billingAddress": { + } + }, + "captureMethod": CaptureMethod.AUTOMATIC, + "returnUrl": "https://example.com/return" + }; +} + +function _buildTokenSetupRecurringRequest(): types.IPaymentServiceTokenSetupRecurringRequest { + return { + "merchantRecurringPaymentId": "probe_tokenized_mandate_001", + "amount": { + "minorAmount": 0, // Amount in minor units (e.g., 1000 = $10.00). + "currency": Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + }, + "connectorToken": {"value": "pm_1AbcXyzStripeTestToken"}, + "address": { + "billingAddress": { + } + }, + "customerAcceptance": { + "acceptanceType": AcceptanceType.ONLINE, // Type of acceptance (e.g., online, offline). + "acceptedAt": 0, // Timestamp when the acceptance was made (Unix timestamp, seconds since epoch). + "onlineMandateDetails": { // Details if the acceptance was an online mandate. + "ipAddress": "127.0.0.1", // IP address from which the mandate was accepted. + "userAgent": "Mozilla/5.0" // User agent string of the browser used for mandate acceptance. + } + }, + "setupMandateDetails": { + "mandateType": { // Type of mandate (single_use or multi_use) with amount details. + "multiUse": { // Multi use mandate with amount details (for recurring payments). + "amount": 0, // Use amount_money instead (will be removed in a future release). + "currency": Currency.USD, // Use amount_money.currency instead (will be removed in a future release). + "amountMoney": { // Amount in Money type. + "minorAmount": 0, // Amount in minor units (e.g., 1000 = $10.00). + "currency": Currency.USD // ISO 4217 currency code (e.g., "USD", "EUR"). + } + } + } + }, + "setupFutureUsage": FutureUsage.OFF_SESSION + }; +} + function _buildVoidRequest(connectorTransactionId: string): types.IPaymentServiceVoidRequest { return { "merchantVoidId": "probe_void_001", // Identification. @@ -403,6 +456,24 @@ async function setupRecurring(merchantTransactionId: string, config: types.IConn return setupResponse; } +// Flow: PaymentService.TokenAuthorize +async function tokenAuthorize(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + const tokenResponse = await paymentClient.tokenAuthorize(_buildTokenAuthorizeRequest()); + + return tokenResponse; +} + +// Flow: PaymentService.TokenSetupRecurring +async function tokenSetupRecurring(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { + const paymentClient = new PaymentClient(config); + + const tokenResponse = await paymentClient.tokenSetupRecurring(_buildTokenSetupRecurringRequest()); + + return tokenResponse; +} + // Flow: PaymentService.Void async function voidPayment(merchantTransactionId: string, config: types.IConnectorConfig = _defaultConfig) { const paymentClient = new PaymentClient(config); @@ -415,7 +486,7 @@ async function voidPayment(merchantTransactionId: string, config: types.IConnect // Export all process* functions for the smoke test export { - processCheckoutAutocapture, processCheckoutCard, processRefund, processVoidPayment, processGetPayment, authorize, capture, get, proxyAuthorize, proxySetupRecurring, recurringCharge, refund, refundGet, setupRecurring, voidPayment, _buildAuthorizeRequest, _buildCaptureRequest, _buildGetRequest, _buildProxyAuthorizeRequest, _buildProxySetupRecurringRequest, _buildRecurringChargeRequest, _buildRefundRequest, _buildRefundGetRequest, _buildSetupRecurringRequest, _buildVoidRequest + processCheckoutAutocapture, processCheckoutCard, processRefund, processVoidPayment, processGetPayment, authorize, capture, get, proxyAuthorize, proxySetupRecurring, recurringCharge, refund, refundGet, setupRecurring, tokenAuthorize, tokenSetupRecurring, voidPayment, _buildAuthorizeRequest, _buildCaptureRequest, _buildGetRequest, _buildProxyAuthorizeRequest, _buildProxySetupRecurringRequest, _buildRecurringChargeRequest, _buildRefundRequest, _buildRefundGetRequest, _buildSetupRecurringRequest, _buildTokenAuthorizeRequest, _buildTokenSetupRecurringRequest, _buildVoidRequest }; // CLI runner