Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -558,21 +558,11 @@ macros::macro_connector_implementation!(
req: &RouterDataV2<RepeatPayment, PaymentFlowData, RepeatPaymentData<T>, PaymentsResponseData>,
) -> CustomResult<String, IntegrationError> {
// 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(),
Expand Down Expand Up @@ -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": "<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,
Expand All @@ -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))
Expand Down
4 changes: 4 additions & 0 deletions crates/internal/composite-service/src/transformers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
}
}
Expand Down
12 changes: 10 additions & 2 deletions crates/types-traits/domain_types/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
12 changes: 12 additions & 0 deletions crates/types-traits/grpc-api-types/proto/payment.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion data/field_probe/airwallex.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion sdk/javascript/src/payments/_generated_grpc_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ const _MSG_FIELD_TYPES: Record<string, Record<string, string>> = {
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" },
Expand Down
Loading