Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c7c2d65
feat(core): Add network_transaction_link_id field plumbing for Master…
ayush22667 May 12, 2026
5da7e46
ayush22667 May 12, 2026
64c5e41
feat(schema): introduce network_transaction_link_id field in relevant…
ayush22667 May 12, 2026
bd10edc
docs(openapi): re-generate OpenAPI specification
hyperswitch-bot[bot] May 12, 2026
4fc8411
feat: update Mastercard TLID documentation for clarity across models
ayush22667 May 12, 2026
9a8e385
feat(schema): reintroduce network_transaction_link_id field in Paymen…
ayush22667 May 12, 2026
bd2f8dd
docs(openapi): re-generate OpenAPI specification
hyperswitch-bot[bot] May 12, 2026
9891839
feat(schema): add network_transaction_link_id support in payment meth…
ayush22667 May 12, 2026
ca85ac4
Merge branch 'feat/add-tlid-support' of https://github.com/juspay/hyp…
ayush22667 May 12, 2026
dff4d7b
Merge branch 'main' into feat/add-tlid-support
ayush22667 May 13, 2026
d7e3fef
feat(schema): remove NULL constraint from network_transaction_link_id…
ayush22667 May 13, 2026
145e062
Merge branch 'feat/add-tlid-support' of https://github.com/juspay/hyp…
ayush22667 May 13, 2026
5593c1a
Merge branch 'main' into feat/add-tlid-support
ayush22667 May 13, 2026
be53fbc
feat(core): Wire TLID through Adyen payment flows and API response
ayush22667 May 13, 2026
6db2b65
docs(openapi): re-generate OpenAPI specification
hyperswitch-bot[bot] May 13, 2026
d38bde5
feat(tests): Add network_transaction_link_id to payment creation test…
ayush22667 May 14, 2026
1208df3
Merge branch 'feat/add-tlid-support-for-adyen' of https://github.com/…
ayush22667 May 14, 2026
69ba95a
Merge branch 'feat/add-tlid-support' into feat/add-tlid-support-for-a…
ayush22667 May 14, 2026
c6d484b
Merge branch 'main' into feat/add-tlid-support
ayush22667 May 14, 2026
c637059
Merge branch 'feat/add-tlid-support' into feat/add-tlid-support-for-a…
ayush22667 May 14, 2026
de31d20
Merge branch 'main' into feat/add-tlid-support-for-adyen
ayush22667 May 16, 2026
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
15 changes: 15 additions & 0 deletions api-reference/v1/openapi_spec_v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -29700,6 +29700,11 @@
"description": "This is the transaction id generated by the network",
"nullable": true
},
"network_transaction_link_id": {
"type": "string",
"description": "The Mastercard Transaction Link Identifier (TLID) stored on the saved payment method.",
"nullable": true
},
"is_eligible_for_mit_payment": {
"type": "boolean",
"description": "This indicates whether a payment method is eligible for performing a mit transaction"
Expand Down Expand Up @@ -31831,6 +31836,11 @@
"description": "The network transaction ID is a unique identifier for the transaction as recognized by the payment network (e.g., Visa, Mastercard), this ID can be used to reference it for future transactions or recurring payments.\nRefer `payment_method_tokenization_details` for detailed view of payment method tokenization",
"nullable": true
},
"network_transaction_link_id": {
"type": "string",
"description": "The Mastercard Transaction Link Identifier (TLID) for this payment. Returned on CITs that set up\nstored credentials. External-vault merchants should persist this and echo it back on subsequent\nMIT requests. Mandatory for Mastercard recurring/MIT (no static fallback).",
"nullable": true
},
"payment_method_status": {
"allOf": [
{
Expand Down Expand Up @@ -33508,6 +33518,11 @@
"description": "The network transaction ID is a unique identifier for the transaction as recognized by the payment network (e.g., Visa, Mastercard), this ID can be used to reference it for future transactions or recurring payments.\nRefer `payment_method_tokenization_details` for detailed view of payment method tokenization",
"nullable": true
},
"network_transaction_link_id": {
"type": "string",
"description": "The Mastercard Transaction Link Identifier (TLID) for this payment. Returned on CITs that set up\nstored credentials. External-vault merchants should persist this and echo it back on subsequent\nMIT requests. Mandatory for Mastercard recurring/MIT (no static fallback).",
"nullable": true
},
"payment_method_status": {
"allOf": [
{
Expand Down
8 changes: 8 additions & 0 deletions crates/api_models/src/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7677,6 +7677,12 @@ pub struct PaymentsResponse {
#[smithy(value_type = "Option<String>")]
pub network_transaction_id: Option<String>,

/// The Mastercard Transaction Link Identifier (TLID) for this payment. Returned on CITs that set up
/// stored credentials. External-vault merchants should persist this and echo it back on subsequent
/// MIT requests. Mandatory for Mastercard recurring/MIT (no static fallback).
#[smithy(value_type = "Option<String>")]
pub network_transaction_link_id: Option<String>,

/// Payment Method Status, refers to the status of the payment method used for this payment.
/// Refer `payment_method_tokenization_details` for detailed view of payment method tokenization
#[schema(value_type = Option<PaymentMethodStatus>)]
Expand Down Expand Up @@ -7839,6 +7845,8 @@ pub struct PaymentMethodTokenizationDetails {
pub network_tokenization: bool,
/// This is the transaction id generated by the network
pub network_transaction_id: Option<String>,
/// The Mastercard Transaction Link Identifier (TLID) stored on the saved payment method.
pub network_transaction_link_id: Option<String>,
/// This indicates whether a payment method is eligible for performing a mit transaction
pub is_eligible_for_mit_payment: bool,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2173,6 +2173,22 @@ fn get_additional_data(
}
};

let transaction_link_id = item.request.mandate_id.as_ref().and_then(|mandate_ids| {
mandate_ids
.mandate_reference_id
.as_ref()
.and_then(|mandate_ref_id| match mandate_ref_id {
payments::MandateReferenceId::NetworkMandateId(ref_data) => {
ref_data.transaction_link_id.clone()
}
payments::MandateReferenceId::NetworkTokenWithNTI(ref_data) => {
ref_data.transaction_link_id.clone()
}
payments::MandateReferenceId::ConnectorMandateId(_)
| payments::MandateReferenceId::CardWithLimitedData => None,
})
});

Ok(Some(AdditionalData {
authorisation_type,
manual_capture,
Expand All @@ -2189,6 +2205,7 @@ fn get_additional_data(
}),
paymentdatasource,
capture_delay_hours,
transaction_link_id,
..AdditionalData::default()
}))
}
Expand Down Expand Up @@ -3133,6 +3150,7 @@ impl
get_recurring_processing_model(item.router_data)?;
let browser_info = None;
let additional_data = get_additional_data(item.router_data)?;

let return_url = item.router_data.request.get_router_return_url()?;
let payment_method_type = item.router_data.request.payment_method_type;
let testing_data = item
Expand Down Expand Up @@ -4550,7 +4568,10 @@ pub fn get_adyen_response(
.map(|network_tx_id| network_tx_id.clone().expose())
});

let network_txn_link_id = None; // TODO(TLID-PR2): extract from response.additional_data.transaction_link_id
let network_txn_link_id = response
.additional_data
.as_ref()
.and_then(|additional_data| additional_data.transaction_link_id.clone());

let charges = match &response.splits {
Some(split_items) => Some(construct_charge_response(response.store, split_items)),
Expand Down
88 changes: 65 additions & 23 deletions crates/router/src/core/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10732,14 +10732,11 @@ where
network_token_data.into(),
))
}
Some(ActionType::CardWithNetworkTransactionId(network_transaction_id)) => {
Some(ActionType::CardWithNetworkTransactionId(card_with_nti_ref)) => {
logger::info!("using card with network_transaction_id for MIT flow");

Some(payments_api::MandateReferenceId::NetworkMandateId(
payments_api::NetworkMandateIdRef {
network_transaction_id,
transaction_link_id: None,
},
card_with_nti_ref.into(),
))
}
Some(ActionType::ConnectorMandate(connector_mandate_details)) => {
Expand Down Expand Up @@ -10875,12 +10872,14 @@ where
.as_ref()
.ok_or(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to fetch the network transaction id")?;
let transaction_link_id = payment_method_info.network_transaction_link_id.clone();

let mandate_reference_id = Some(payments_api::MandateReferenceId::NetworkMandateId(
payments_api::NetworkMandateIdRef {
Comment thread
ayush22667 marked this conversation as resolved.
CardWithNTIRef {
network_transaction_id: network_transaction_id.to_string(),
transaction_link_id: None,
},
transaction_link_id,
}
.into(),
));

connector_choice = Some((connector_data, mandate_reference_id.clone()));
Expand Down Expand Up @@ -10930,6 +10929,7 @@ pub struct NetworkTokenExpiry {
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, Eq, PartialEq)]
pub struct NTWithNTIRef {
pub network_transaction_id: String,
pub transaction_link_id: Option<String>,
pub token_exp_month: Option<Secret<String>>,
pub token_exp_year: Option<Secret<String>>,
}
Expand All @@ -10938,18 +10938,33 @@ impl From<NTWithNTIRef> for payments_api::NetworkTokenWithNTIRef {
fn from(network_token_data: NTWithNTIRef) -> Self {
Self {
network_transaction_id: network_token_data.network_transaction_id,
transaction_link_id: None,
transaction_link_id: network_token_data.transaction_link_id,
token_exp_month: network_token_data.token_exp_month,
token_exp_year: network_token_data.token_exp_year,
}
}
}

#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, Eq, PartialEq)]
pub struct CardWithNTIRef {
pub network_transaction_id: String,
pub transaction_link_id: Option<String>,
}

impl From<CardWithNTIRef> for payments_api::NetworkMandateIdRef {
fn from(card_data: CardWithNTIRef) -> Self {
Self {
network_transaction_id: card_data.network_transaction_id,
transaction_link_id: card_data.transaction_link_id,
}
}
}

// This represents the recurring details of a connector which will be used for retries
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub enum ActionType {
NetworkTokenWithNetworkTransactionId(NTWithNTIRef),
CardWithNetworkTransactionId(String), // Network Transaction Id
CardWithNetworkTransactionId(CardWithNTIRef),
Comment thread
ayush22667 marked this conversation as resolved.
#[cfg(feature = "v1")]
ConnectorMandate(hyperswitch_domain_models::mandates::PaymentsMandateReference),
}
Expand Down Expand Up @@ -11005,7 +11020,7 @@ impl ActionTypesBuilder {
payment_method_data: Option<&domain::PaymentMethodData>,
) -> Self {
match is_network_token_with_ntid_flow {
IsNtWithNtiFlow::NtWithNtiSupported(network_transaction_id)
IsNtWithNtiFlow::NtWithNtiSupported(card_with_nti_ref)
if is_nt_with_ntid_supported_connector =>
{
match payment_method_data {
Expand All @@ -11027,7 +11042,12 @@ impl ActionTypesBuilder {
.token_exp_year
.clone(),
),
network_transaction_id,
network_transaction_id: card_with_nti_ref
.network_transaction_id
.clone(),
transaction_link_id: card_with_nti_ref
.transaction_link_id
.clone(),
},
));
}
Expand All @@ -11047,7 +11067,12 @@ impl ActionTypesBuilder {
ActionType::NetworkTokenWithNetworkTransactionId(NTWithNTIRef {
token_exp_month,
token_exp_year,
network_transaction_id,
network_transaction_id: card_with_nti_ref
.network_transaction_id
.clone(),
transaction_link_id: card_with_nti_ref
.transaction_link_id
.clone(),
})
},
),
Expand All @@ -11066,12 +11091,20 @@ impl ActionTypesBuilder {
payment_method_info: &domain::PaymentMethod,
) -> Self {
if is_card_with_ntid_flow {
self.action_types.extend(
payment_method_info
.network_transaction_id
.as_ref()
.map(|ntid| ActionType::CardWithNetworkTransactionId(ntid.clone())),
);
self.action_types
.extend(
payment_method_info
.network_transaction_id
.as_ref()
.map(|ntid| {
ActionType::CardWithNetworkTransactionId(CardWithNTIRef {
network_transaction_id: ntid.clone(),
transaction_link_id: payment_method_info
.network_transaction_link_id
.clone(),
})
}),
);
}
self
}
Expand Down Expand Up @@ -11170,8 +11203,8 @@ pub fn is_network_transaction_id_flow(

#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, Eq, PartialEq)]
pub enum IsNtWithNtiFlow {
NtWithNtiSupported(String), //Network token with Network transaction id supported flow
NTWithNTINotSupported, //Network token with Network transaction id not supported
NtWithNtiSupported(CardWithNTIRef), //Network token with Network transaction id + TLID supported flow
NTWithNTINotSupported, //Network token with Network transaction id not supported
}

pub fn is_network_token_with_network_transaction_id_flow(
Expand All @@ -11186,6 +11219,7 @@ pub fn is_network_token_with_network_transaction_id_flow(
is_network_tokenization_enabled,
payment_method_info.get_payment_method_type(),
payment_method_info.network_transaction_id.clone(),
payment_method_info.network_transaction_link_id.clone(),
payment_method_info.network_token_locker_id.is_some(),
payment_method_info
.network_token_requestor_reference_id
Expand All @@ -11198,21 +11232,29 @@ pub fn is_network_token_with_network_transaction_id_flow(
true,
Some(storage_enums::PaymentMethod::Card),
Some(network_transaction_id),
transaction_link_id,
true,
true,
_,
_,
) => IsNtWithNtiFlow::NtWithNtiSupported(network_transaction_id),
) => IsNtWithNtiFlow::NtWithNtiSupported(CardWithNTIRef {
network_transaction_id,
transaction_link_id,
}),
(
Some(true),
true,
Some(storage_enums::PaymentMethod::Card),
Some(network_transaction_id),
transaction_link_id,
_,
_,
true,
Some(domain::PaymentMethodData::CardWithNetworkTokenDetails(_)),
) => IsNtWithNtiFlow::NtWithNtiSupported(network_transaction_id),
) => IsNtWithNtiFlow::NtWithNtiSupported(CardWithNTIRef {
network_transaction_id,
transaction_link_id,
}),
_ => IsNtWithNtiFlow::NTWithNTINotSupported,
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,10 @@ impl<F: Clone + Sync> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for
.as_ref()
.map(|surcharge_details| surcharge_details.tax_on_surcharge_amount);
let network_transaction_id = payment_data.payment_attempt.network_transaction_id.clone();
let network_transaction_link_id = payment_data
.payment_attempt
.network_transaction_link_id
.clone();
payment_data.payment_attempt = state
.store
.update_payment_attempt_with_attempt_id(
Expand All @@ -930,7 +934,7 @@ impl<F: Clone + Sync> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for
payment_method_billing_address_id,
updated_by: storage_scheme.to_string(),
network_transaction_id,
network_transaction_link_id: None,
network_transaction_link_id,
net_amount:
hyperswitch_domain_models::payments::payment_attempt::NetAmount::new(
payment_data.amount.into(),
Expand Down
3 changes: 3 additions & 0 deletions crates/router/src/core/payments/transformers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4140,6 +4140,7 @@ where
browser_info: payment_attempt.browser_info,
payment_method_id: payment_attempt.payment_method_id,
network_transaction_id: payment_attempt.network_transaction_id,
network_transaction_link_id: payment_attempt.network_transaction_link_id,
payment_method_status: payment_data
.get_payment_method_info()
.map(|info| info.status),
Expand Down Expand Up @@ -4512,6 +4513,7 @@ impl ForeignFrom<(storage::PaymentIntent, storage::PaymentAttempt)> for api::Pay
is_iframe_redirection_enabled:pi.is_iframe_redirection_enabled,
payment_channel: pi.payment_channel,
network_transaction_id: None,
network_transaction_link_id: None,
Comment thread
ayush22667 marked this conversation as resolved.
Comment on lines 4515 to +4516
enable_partial_authorization: pi.enable_partial_authorization,
enable_overcapture: pi.enable_overcapture,
is_overcapture_enabled: pa.is_overcapture_enabled,
Expand Down Expand Up @@ -4547,6 +4549,7 @@ impl ForeignTryFrom<&domain::PaymentMethod> for api_payments::PaymentMethodToken
psp_tokenization,
network_tokenization,
network_transaction_id: payment_method.network_transaction_id.clone(),
network_transaction_link_id: payment_method.network_transaction_link_id.clone(),
is_eligible_for_mit_payment: psp_tokenization || is_network_transaction_id_present,
})
}
Expand Down
1 change: 1 addition & 0 deletions crates/router/src/db/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1527,6 +1527,7 @@ mod tests {
whole_connector_response: None,
payment_channel: None,
network_transaction_id: None,
network_transaction_link_id: None,
enable_partial_authorization: None,
is_overcapture_enabled: None,
enable_overcapture: None,
Expand Down
2 changes: 2 additions & 0 deletions crates/router/tests/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@ async fn payments_create_core() {
whole_connector_response: None,
payment_channel: None,
network_transaction_id: None,
network_transaction_link_id: None,
enable_partial_authorization: None,
is_overcapture_enabled: None,
enable_overcapture: None,
Expand Down Expand Up @@ -788,6 +789,7 @@ async fn payments_create_core_adyen_no_redirect() {
installment_data: None,
state_metadata: None,
connector_response_metadata: None,
network_transaction_link_id: None,
},
vec![],
));
Expand Down
2 changes: 2 additions & 0 deletions crates/router/tests/payments2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ async fn payments_create_core() {
whole_connector_response: None,
payment_channel: None,
network_transaction_id: None,
network_transaction_link_id: None,
enable_partial_authorization: None,
is_overcapture_enabled: None,
enable_overcapture: None,
Expand Down Expand Up @@ -557,6 +558,7 @@ async fn payments_create_core_adyen_no_redirect() {
installment_data: None,
state_metadata: None,
connector_response_metadata: None,
network_transaction_link_id: None,
},
vec![],
));
Expand Down
Loading