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/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/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/default_implementations.rs b/crates/integrations/connector-integration/src/default_implementations.rs index fb944d7f79..1ec5d2edb8 100644 --- a/crates/integrations/connector-integration/src/default_implementations.rs +++ b/crates/integrations/connector-integration/src/default_implementations.rs @@ -870,7 +870,6 @@ default_impl_payment_method_eligibility_v2!( PinelabsOnline, Placetopay, Powertranz, - Qwikcilver, Rapyd, Razorpay, RazorpayV2, 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/types-traits/domain_types/src/connector_types.rs b/crates/types-traits/domain_types/src/connector_types.rs index ea042f94f9..665f6e2c71 100644 --- a/crates/types-traits/domain_types/src/connector_types.rs +++ b/crates/types-traits/domain_types/src/connector_types.rs @@ -1586,6 +1586,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 +1611,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, } diff --git a/crates/types-traits/domain_types/src/types.rs b/crates/types-traits/domain_types/src/types.rs index 914ecfbb47..fb294b7c54 100644 --- a/crates/types-traits/domain_types/src/types.rs +++ b/crates/types-traits/domain_types/src/types.rs @@ -5919,6 +5919,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 +5952,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, @@ -6418,6 +6425,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 +6451,7 @@ pub fn generate_payment_method_eligibility_response( }), issuer_details: None, }), + payment_method_details: None, raw_connector_request, typed_connector_request, raw_connector_response, @@ -9504,6 +9515,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, 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..23a51b39f8 100644 --- a/crates/types-traits/grpc-api-types/proto/payment.proto +++ b/crates/types-traits/grpc-api-types/proto/payment.proto @@ -6231,6 +6231,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 +6262,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/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/docs-generated/all_connector.md b/docs-generated/all_connector.md index 22c10cb027..f1a8515510 100644 --- a/docs-generated/all_connector.md +++ b/docs-generated/all_connector.md @@ -214,7 +214,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/sdk/javascript/src/payments/_generated_grpc_client.ts b/sdk/javascript/src/payments/_generated_grpc_client.ts index f8289741e5..f8204a9dfc 100644 --- a/sdk/javascript/src/payments/_generated_grpc_client.ts +++ b/sdk/javascript/src/payments/_generated_grpc_client.ts @@ -524,8 +524,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" },