diff --git a/crates/integrations/connector-integration/src/connectors/airwallex.rs b/crates/integrations/connector-integration/src/connectors/airwallex.rs index c02cb32a7e..39d44685e5 100644 --- a/crates/integrations/connector-integration/src/connectors/airwallex.rs +++ b/crates/integrations/connector-integration/src/connectors/airwallex.rs @@ -27,7 +27,7 @@ use domain_types::{ types::Connectors, }; use error_stack::ResultExt; -use hyperswitch_masking::{ExposeInterface, Maskable, PeekInterface}; +use hyperswitch_masking::{ExposeInterface, Maskable}; use interfaces::{ api::ConnectorCommon, connector_integration_v2::ConnectorIntegrationV2, connector_types, decode::BodyDecoding, verification::SourceVerification, @@ -558,21 +558,11 @@ macros::macro_connector_implementation!( req: &RouterDataV2, PaymentsResponseData>, ) -> CustomResult { // Airwallex MIT requires a fresh PaymentIntent; caller must run CreateOrder - // first and pass connector_order_id via PaymentFlowData.connector_order_id, - // or (fallback) via connector_feature_data JSON: {"connector_order_id":"..."}. - // The fallback is required because RecurringPaymentServiceChargeRequest (proto) - // has no top-level `order_id` field, and its ForeignTryFrom for PaymentFlowData - // leaves `connector_order_id = None`. + // first and pass connector_order_id via PaymentFlowData.connector_order_id. let order_id = req .resource_common_data .connector_order_id .clone() - .or_else(|| { - req.resource_common_data - .connector_feature_data - .as_ref() - .and_then(|v| v.peek().get("connector_order_id").and_then(|id| id.as_str().map(|s| s.to_string()))) - }) .ok_or(IntegrationError::MissingRequiredField { field_name: "connector_order_id", context: Default::default(), @@ -660,11 +650,9 @@ macros::macro_connector_implementation!( // ===== CONNECTOR CUSTOMER CONNECTOR INTEGRATIONS ===== // Create Connector Customer — POST /api/v1/pa/customers/create. // -// Airwallex requires a Bearer access token (from ServerAuthenticationToken). -// CustomerServiceCreateRequest has no `state` field in the proto, so the access -// token is passed manually via `connector_feature_data` as JSON of the shape -// `{"access_token": ""}`. This mirrors the existing RepeatPayment fallback -// pattern for `connector_order_id`. +// Airwallex requires a Bearer access token (from ServerAuthenticationToken), +// supplied by the caller on CustomerServiceCreateRequest.state and surfaced as +// PaymentFlowData.access_token. macros::macro_connector_implementation!( connector_default_implementations: [get_content_type, get_error_response_v2], connector: Airwallex, @@ -685,18 +673,7 @@ macros::macro_connector_implementation!( let access_token = req .resource_common_data .get_access_token() - .ok() - .or_else(|| { - req.resource_common_data - .connector_feature_data - .as_ref() - .and_then(|v| { - v.peek() - .get("access_token") - .and_then(|tok| tok.as_str().map(|s| s.to_string())) - }) - }) - .ok_or(IntegrationError::FailedToObtainAuthType { + .change_context(IntegrationError::FailedToObtainAuthType { context: Default::default(), })?; Ok(self.build_headers(&access_token)) diff --git a/crates/internal/composite-service/src/transformers.rs b/crates/internal/composite-service/src/transformers.rs index 7db32c2cd9..de3bb52bb3 100644 --- a/crates/internal/composite-service/src/transformers.rs +++ b/crates/internal/composite-service/src/transformers.rs @@ -203,6 +203,10 @@ impl ForeignFrom<&CompositeAuthorizeRequest> for CustomerServiceCreateRequest { connector_feature_data: item.connector_feature_data.clone(), test_mode: item.test_mode, split_payments: item.split_payments.clone(), + // Connectors whose customer API authenticates separately from the + // merchant credentials (e.g. Airwallex Bearer tokens) read the token + // off this. Mirrors the CustomerServiceGetRequest impl below. + state: item.state.clone(), } } } diff --git a/crates/types-traits/domain_types/src/types.rs b/crates/types-traits/domain_types/src/types.rs index 6bf1cd7a75..1d15ebf7d3 100644 --- a/crates/types-traits/domain_types/src/types.rs +++ b/crates/types-traits/domain_types/src/types.rs @@ -5239,7 +5239,7 @@ impl access_token, session_token: None, reference_id: None, - connector_order_id: None, + connector_order_id: value.connector_order_id, preprocessing_id: None, connector_api_version: None, test_mode: value.test_mode, @@ -13650,6 +13650,14 @@ impl .map(|m| ForeignTryFrom::foreign_try_from((m, "merchant account metadata"))) .transpose()?; + // Extract access_token from state field + 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, @@ -13669,7 +13677,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, diff --git a/crates/types-traits/grpc-api-types/proto/payment.proto b/crates/types-traits/grpc-api-types/proto/payment.proto index 13f6434b0d..6fe28642e8 100644 --- a/crates/types-traits/grpc-api-types/proto/payment.proto +++ b/crates/types-traits/grpc-api-types/proto/payment.proto @@ -3573,6 +3573,13 @@ message RecurringPaymentServiceChargeRequest { // Channel through which the repeat payment was initiated. optional PaymentChannel payment_channel = 38; + + // Identifier of the order/intent created on the connector for this charge. + // Connectors whose MIT leg confirms a pre-created order (e.g. Airwallex + // confirming a PaymentIntent) require the caller to run CreateOrder first + // and pass the resulting id here. Mirrors + // PaymentServiceAuthorizeRequest.connector_order_id. + optional string connector_order_id = 39; } // Response message for repeat payment operation @@ -4546,6 +4553,11 @@ message CustomerServiceCreateRequest { // Split payment configuration (platform fees, transfers, etc.) optional SplitPaymentsDetails split_payments = 9; + + // Access token / connector session state for connectors whose customer API + // is authenticated separately from the merchant credentials (e.g. Airwallex + // Bearer tokens). Mirrors the `state` field on the payment service requests. + optional ConnectorState state = 10; } // Response message for creating a customer diff --git a/data/field_probe/airwallex.json b/data/field_probe/airwallex.json index fda8cc89c0..48f4f81c2d 100644 --- a/data/field_probe/airwallex.json +++ b/data/field_probe/airwallex.json @@ -995,7 +995,7 @@ "recurring_charge": { "default": { "status": "error", - "error": "Stuck on field: connector_order_id — Missing required field: connector_order_id" + "error": "Stuck on field: payment_method_id — Missing required field: payment_method_id" } }, "recurring_revoke": { diff --git a/sdk/javascript/src/payments/_generated_grpc_client.ts b/sdk/javascript/src/payments/_generated_grpc_client.ts index 1f85fda32c..b081405191 100644 --- a/sdk/javascript/src/payments/_generated_grpc_client.ts +++ b/sdk/javascript/src/payments/_generated_grpc_client.ts @@ -484,7 +484,7 @@ const _MSG_FIELD_TYPES: Record> = { BankAccount: { "balance": "Money", "availableBalance": "Money", "ach": "BankAccountDetailsAch", "bacs": "BankAccountDetailsBacs", "sepa": "BankAccountDetailsSepa" }, PaymentMethodServiceRechargeRequest: { "amount": "Money", "state": "ConnectorState" }, PaymentMethodServiceRechargeResponse: { "paymentMethodDetails": "PaymentMethodDetails", "error": "ErrorInfo", "responseHeaders": "ResponseHeadersEntry" }, - CustomerServiceCreateRequest: { "address": "PaymentAddress", "splitPayments": "SplitPaymentsDetails" }, + CustomerServiceCreateRequest: { "address": "PaymentAddress", "splitPayments": "SplitPaymentsDetails", "state": "ConnectorState" }, CustomerServiceCreateResponse: { "error": "ErrorInfo", "responseHeaders": "ResponseHeadersEntry" }, CustomerServiceGetResponse: { "customer": "Customer", "error": "ErrorInfo", "responseHeaders": "ResponseHeadersEntry" }, CustomerServiceUpdateRequest: { "address": "PaymentAddress" },