Skip to content

feat(external-3ds): Netcetera external 3DS authentication over VGS external vault (v1)#13143

Open
ShankarSinghC wants to merge 29 commits into
mainfrom
feat/external-3ds-vgs
Open

feat(external-3ds): Netcetera external 3DS authentication over VGS external vault (v1)#13143
ShankarSinghC wants to merge 29 commits into
mainfrom
feat/external-3ds-vgs

Conversation

@ShankarSinghC

@ShankarSinghC ShankarSinghC commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Type of Change

  • New feature

Description

Adds External 3DS (Netcetera) authentication for cards held in an external VGS vault (v1 payments only). The card lives in the VGS vault; Hyperswitch holds only VGS aliases — never the PAN.

The 3DS authentication legs (versioning / AReq / RReq) run through the Unified Connector Service against the Netcetera authentication connector. The VGS alias is carried in the gRPC card_proxy field (never a Luhn-validated card field), and the real card fields are substituted by the VGS proxy on the outbound leg.

Flow

  1. confirm → UCS pre-authenticate (card_proxy alias; netcetera MCA metadata forwarded as connector_feature_data).
  2. Frictionless: ARes CAVV stashed on the authentication record → resume → authorize.
  3. Challenge: ThreeDsInvoke next-action → SDK /3ds/authentication (AReq) → results (RRes) webhook vaults the CAVV → SDK re-confirm → authorize.
  4. pull=false authorizes via the RRes webhook; the SDK's post-challenge three_ds_authorize_url only reports status. Frictionless authorizes via the confirm/resume.

CVC is modelled as optional on the vault-token path (the CAVV replaces it on a 3DS-authenticated authorize), consistent with the saved-card CardToken model.

Notes

How did you test it?

Validated end-to-end on a local stack (VGS vault + Netcetera prev + Checkout): frictionless and challenge paths both charged end-to-end; save-card + repeat-customer verified. Re-verification of the CVC-optional change in progress.

test curls in the linked issue

ShankarSinghC and others added 6 commits July 2, 2026 18:55
…ternal vault (v1)

Adds external-3DS (Netcetera) authentication for payments whose card lives in a
VGS external vault (Hyperswitch holds only VGS aliases, never the PAN). The card
proxy (VGS alias) rides the UCS injector, which reveals the PAN and presents the
Netcetera mTLS client cert on the VGS outbound route.

Flow (v1 external-vault proxy): confirm -> pre-auth (versioning/PReq) ->
ThreeDsInvoke -> /3ds/authentication (AReq) -> frictionless (ARes CAVV) or
challenge (acs_url/creq) -> re-confirm resume -> post-authenticate -> authorize.

Frictionless path is validated end-to-end (charged): the ARes CAVV/ECI/xid reach
the PSP authorize via the external-vault proxy. Authorize router_data is rebuilt
after the resume sets payment_data.authentication so the 3DS proof is present.

Challenge path scaffolding is in place (trans_status C -> acs_url/creq, RReq via
the VGS mTLS proxy). Webhook-driven completion for pull_mechanism=false merchants
is the remaining work (Stage 2).

- consts: UCS_AUTH_NO_KEY for the credential-less Netcetera UCS route
- unified_connector_service/connector_config: omit x-connector-config for
  Netcetera (mTLS on VGS route, merchant fields via connector_feature_data)
- payments: external-vault pre-auth/AReq/resume orchestration + authorize rebuild
- authorize_flow / ucs transformers: post-authenticate carries card_proxy so the
  RReq routes through the VGS proxy
- payment_confirm_external_vault_proxy: allow RequiresCustomerAction resume;
  persist confirm browser_info for the AReq browser channel

Excludes local-only config/development.toml (test env + vault keys).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…k (Stage 2)

Wires the completed-challenge (pull_mechanism=false) path for external-3DS over
VGS external vault to "charged". After the cardholder finishes the ACS challenge,
Netcetera pushes the results (RRes) to the AReq-provided notification URL; this
lets our own webhook drive the auth record to Success and the SDK re-confirm
authorize with the resulting CAVV.

Three targeted fixes (validated end-to-end: challenge -> RRes webhook -> auth
Success + CAVV vaulted -> re-confirm -> checkout charged):

- payments.rs (pre-auth): the incoming netcetera results webhook is keyed by
  `connector_authentication_id` (get_webhook_object_reference_id ->
  ConnectorAuthenticationId(threeDSServerTransID)), but netcetera reports its id
  on the versioning response as `threeds_server_transaction_id` (no distinct
  `transaction_id`), so the record persisted `connector_authentication_id = ""`
  and the webhook lookup missed. Fall back to the threeDSServerTransID.

- webhooks/incoming.rs (external_authentication_incoming_webhook_flow): for an
  external-vault profile, skip the native auto-authorize. The native path drives
  `payments::PaymentConfirm`, which needs a card/payment_method; for external
  vault the PAN lives behind a VGS alias re-fetched only inside the
  external-vault-proxy confirm op (a minimal PaymentConfirm errors IR_04). The
  auth record + vaulted CAVV are already persisted, so completion happens on the
  SDK re-confirm.

- payments.rs (post-authenticate resume): resolve the cavv from connector_metadata
  (frictionless ARes stash) / cavv column, then fall back to the temp-locker vault
  (get_tokenized_data). A completed challenge delivers the cavv via the RRes
  webhook, which vaults it keyed by authentication_id rather than writing
  connector_metadata; the vault fallback lets that path resolve.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…low parity

Makes external-vault external-3DS match normal-payments completion for
pull_mechanism=false, replacing the earlier SDK-re-confirm bridge.

- webhooks/incoming.rs: on the pull=false + Challenge + ARes results webhook, the
  external-vault branch now DRIVES the authorize itself via
  external_vault_proxy_for_payments_core(PaymentExternalVaultProxyConfirm) with a
  synthetic VaultCardTokenData request keyed by the stored payment_token (re-fetches
  the VGS alias, empty CVC, authorizes with the vaulted CAVV). No SDK call / no
  re-confirm required. Validated end-to-end: RRes webhook alone -> checkout charged.

- payments.rs (PaymentAuthenticateCompleteAuthorize::call_payment_flow, the
  three_ds_authorize_url redirect flow): resolve the real auth-connector (netcetera)
  MCA for external vault instead of the placeholder unified_authentication_service
  (which has no MCA -> HE_02), mirroring the /3ds/authentication prologue; and never
  take the standard-PaymentConfirm authorize branch for external vault (it has no VGS
  proxy). The SDK's post-challenge three_ds_authorize_url call now reports status
  (PSync) — succeeded after the webhook lands — matching normal pull=false+Challenge,
  instead of erroring. The Succeeded status guard + per-payment API lock prevent any
  double authorize, and for external vault the standard path structurally cannot
  charge a VGS-alias card.

Known edge: an SDK three_ds_authorize_url call that arrives STRICTLY BEFORE the RRes
webhook errors IR_04 (the external-vault attempt has no payment_method persisted
until authorize), where normal payments return pending; the webhook still completes
the payment. Fix (persist payment_method on the challenge-halt) tracked separately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…horize_url

Closes the external-vault-specific divergence where an SDK three_ds_authorize_url
call that lands BEFORE the RRes webhook errored, instead of returning pending like
normal payments (validated: [A] before-webhook -> requires_customer_action, [B]
webhook -> charged, [C] after-webhook -> succeeded).

Two causes, both from the proxy confirm's HaltForSdkChallenge early return skipping
the ConfirmUpdate that a normal confirm runs:

- payment_attempt AuthenticationUpdate: carry payment_method / payment_method_type
  (domain + diesel variants + mappings). The challenge-halt persist
  (persist_external_vault_pre_auth_challenge...) now sets them from payment_data, so
  the attempt has payment_method like a normal confirm. Without it a PSync in the
  redirect flow errored IR_04 "Missing required param: payment_method". None for the
  other AuthenticationUpdate callers leaves the columns unchanged (AsChangeset skips
  None).

- PaymentAuthenticateCompleteAuthorize redirect PSync: for external vault report the
  local status only (force_sync = false). A force_sync on a still-challenge-pending
  payment (no connector transaction yet) called the PSP sync with nothing to sync and
  failed "Something went wrong", stranding the payment. The RRes webhook owns the
  authorize; the SDK's call only needs the status.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The external-vault confirm vaults the card and the `{{$card_exp_year}}` injector
template later resolves to the stored year. A 4-digit year (as the web SDK / VGS
Collect supplies) then produces a 6-character YYYYMM `cardExpiryDate`, which fails
the 3DS AReq validation ("cardExpiryDate: string has wrong length. Expected 4 but
got 6"). Normalize to the last two digits before vaulting so the template resolves
to YY; vault template tokens ({{...}}) are left untouched.

Surfaced by an end-to-end run through the Hyperswitch web SDK (curl tests used a
2-digit year and so never hit it).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ig, status gate

- ExternalVaultCard.card_cvc -> Option<Secret<String>>: model CVC as optional
  (present on the SDK-driven confirm, absent on a CAVV-authenticated re-authorize)
  instead of forwarding empty-string placeholders; the connector now omits the
  field. Aligns with the CardToken optional-CVC model. Updates every construction
  and read site (domain struct, From impls, UCS->gRPC transformer, the op, and the
  webhook/post-auth callers).
- unified_connector_service/connector_config: add a Netcetera variant to
  ConnectorSpecificConfig plus a match arm, removing the special-case early return
  in build_connector_config_header (UCS ignores it via the x-auth: no-key shortcut).
- payment_confirm_external_vault_proxy: gate RequiresCustomerAction by payment
  source (Webhook / ExternalAuthenticator, plus Review), mirroring the standard
  confirm operation.
- webhooks/incoming: document the external-vault setup-mandate gap as a TODO.
- clean up two unnecessary-qualification lints.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ShankarSinghC ShankarSinghC requested review from a team as code owners July 2, 2026 13:32
hyperswitch-bot Bot and others added 10 commits July 2, 2026 13:33
…completion

- Adapt the external-vault auth-record fetch/update to main's renamed
  processor-aware functions (find/update_authentication_by_processor_merchant_id_
  authentication_id) — the cherry-picked feature code still called the old names,
  which main removed (this is why CI was failing to build).
- Pass processor.get_account().get_id() (a MerchantId) to
  build_unified_connector_service_auth_metadata, matching its current signature.
- Add the frictionless external-vault /authorize completion in call_payment_flow:
  the SDK's three_ds_authorize_url drives external_vault_proxy_for_payments_core so
  a frictionless payment charges instead of hanging at RequiresCustomerAction
  (challenge still completes via the pull=false RRes webhook).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove ~510 lines of oversized explanatory comments across the external-3ds
files, consolidate a masking import, and drop a redundant unit-test module.
No functional change — the card_exp_year YY-normalization logic is preserved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…fields like normal payments

Make the Netcetera-over-VGS external-vault 3DS flow work end-to-end through the real
web SDK (one-time + save-card, frictionless + challenge), and stop carrying per-payment
/ per-connector values on the netcetera MCA — infer them the way inline payments do.

- create_payment_method (external-vault proxy confirm): also create the proxy-card payment
  method (and thus mint the re-fetchable alias payment_token) when external 3DS is requested,
  not only when customer_acceptance is present. A one-time (no-save-card) payment now carries a
  payment_token for the multi-leg 3DS flow. Fixes the SDK's /3ds/authentication HE_02
  ("connector name unified_authentication_service does not exist") on one-time payments.

- call_payment_flow (/authorize/{connector}, three_ds_authorize_url): for external-vault, drive
  the external-vault-proxy authorize on frictionless (was PSync -> stuck at
  requires_customer_action), and fall through to the status-only PSync on challenge that the
  pull=false RRes webhook already completed (was re-confirm -> IR_16 "status succeeded").

- Inject the AReq's per-payment/connector values into the netcetera connector_feature_data so
  they are derived from the payment / PSP metadata rather than the netcetera MCA:
    * force_3ds_challenge              -> payment intent, else business-profile default
    * results_response_notification_url -> create_webhook_url (system-constructed pull=false RRes target)
    * notification_url                 -> create_authorize_url (three_ds_authorize_url)
    * acquirer_bin/merchant_id/country_code -> resolved from the PSP (checkout) MCA metadata

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…vault challenge

On an external-vault 3DS auth failure (RRes transStatus=N, or the cardholder cancelling the
challenge), fail_external_vault_post_authenticate marked only the authentication record Failed and
returned a PaymentAuthenticationFailed error, so the payment stayed stuck at RequiresCustomerAction
(webhook 400, SDK polling forever, "requires_customer_action" shown to the shopper).

Now it also transitions the payment to a terminal Failed state (attempt -> Failure with
EXTERNAL_AUTHENTICATION_FAILURE, intent -> Failed) and returns a new HaltWithAuthenticationFailure
decision so the failed payment_data flows to the response builder instead of erroring. This mirrors
how normal payments end an auth failure (status_handler_for_authentication_results): the webhook
returns 200, poll-completion + the outgoing merchant webhook fire, and the SDK renders "payment
failed" and stops polling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… plaintext

The external-vault frictionless AReq leg stashed the raw cavv as plaintext in the authentication
record's `connector_metadata` (`external_vault_frictionless_cavv`), while the challenge path already
vaults the cavv (encrypted, keyed by authentication_id) via the results webhook. Vault the
frictionless cavv the same way and set `connector_metadata: None`; the post-authenticate resume
already falls back to the vault, so drop its `connector_metadata` read. The cavv is now encrypted at
rest on both paths, with nothing plaintext in `connector_metadata` or the `cavv` column.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…VGS external vault

Support the external-3DS-over-VGS-external-vault flow (v1) for platform/
connected merchants, where the vault, customer, and proxy PM live on the
provider (platform) while the payment executes on the processor (connected):

- Resolve provider-owned resources under the provider: customer + vault MCA
  lookups, and the v2 provider profile (resolve_provider_profile). Add
  Platform::get_provider_as_processor.
- Carry payment_method_type on the external-vault-proxy confirm (frictionless
  /authorize + challenge webhook) so it does not rely on the resolved PM
  surfacing it for a connected PM (IR_04).
- Fetch the authentication record under the processor merchant in the
  post-authentication and PSync paths (stored under the connected merchant,
  not the provider the intent/profile resolves to).
- Gate proxy-card PM creation on modular-service eligibility, matching the
  token fetch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ure_data_from_auth_mca

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ShankarSinghC ShankarSinghC self-assigned this Jul 3, 2026
@ShankarSinghC ShankarSinghC added A-payment-methods Area: Payment Methods A-core Area: Core flows labels Jul 3, 2026
ShankarSinghC and others added 2 commits July 3, 2026 17:47
…ayment_status)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment on lines +219 to +224
pub fn get_provider_as_processor(&self) -> Processor {
Processor::new(
self.provider.get_account().clone(),
self.provider.get_key_store().clone(),
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this required?

force_3ds_challenge: Option<bool>,
// System-constructed AReq `notification_url` (browser CRes return = `three_ds_authorize_url`),
// injected into connector_feature_data so it is derived from the payment rather than MCA metadata.
notification_url: Option<String>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you convert into url:Url

Comment on lines +315 to +317
if payment_attempt.browser_info.is_none() {
payment_attempt.browser_info = request.browser_info.clone();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make it functional

.payment_intent
.request_external_three_ds_authentication
== Some(true);
if payment_data.customer_acceptance.is_none() && !is_external_three_ds_requested {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we storing card for all external vault use cases

Comment thread crates/router/src/core/payments.rs Outdated
}

#[cfg(feature = "v1")]
fn build_external_vault_pre_auth_authentication_store<F, D>(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we already have a create_authentication function can we reuse the existing function and pass extra fields as optional params

// Resolve the cavv from the `cavv` column, then the temp-locker vault (primary source — both
// the frictionless AReq leg and the completed challenge vault it encrypted by authentication_id).
let mut frictionless_cavv = authentication.cavv.clone();
if frictionless_cavv.is_none() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we have a log describing the retrieval tokenise data action

Comment thread crates/router/src/core/payments.rs Outdated
let response = if is_pull_mechanism_enabled
// For a challenge with pull=false the RRes webhook drives the authorize, so this call is
// status-only; frictionless (or pull=true) completes here.
let authorize_completes_on_this_call = is_pull_mechanism_enabled

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we have a better name for this variable

merchant_id: req.merchant_id,
param: req.param,
force_sync: req.force_sync,
force_sync: !is_external_vault_payment && req.force_sync,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need to check !is_external_vault_payment if is_external_vault_payment is true this will anyway not enter this else block

Comment thread crates/router/src/core/payments.rs Outdated

let processor = platform.get_processor();
let key_store = processor.get_key_store();
let key_manager_state: common_utils::types::keymanager::KeyManagerState = state.into();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this type caste is not required right

Comment thread crates/router/src/core/payments.rs Outdated
Comment on lines +13811 to +13819
crate::core::payment_methods::transformers::fetch_payment_method_from_modular_service(
state,
platform,
profile_id,
&payment_method_id,
None,
false,
)
.await

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we shouldn't be saving permanently in modular Auth service

)?,
));

let psp_connector_name = payment_attempt

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we fetching psp_connector_name in authentication we should be fetching authentication connector if required anywhere

let ucs_authentication_data =
hyperswitch_domain_models::router_request_types::UcsAuthenticationData {
eci: authentication.eci.clone(),
cavv: authentication.cavv.clone().map(Secret::new),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we shouldn't be storing cavv in authentication table

Comment on lines 14327 to +14339
@@ -12712,16 +14332,52 @@ pub async fn payment_external_authentication<F: Clone + Sync>(
id: profile_id.get_string_repr().to_owned(),
})?;

let payment_method_details = helpers::get_payment_method_details_from_payment_token(
&state,
&payment_attempt,
&payment_intent,
&platform,
storage_scheme,
)
.await?
.ok_or(errors::ApiErrorResponse::InternalServerError)
.attach_printable("missing payment_method_details")?;
let is_external_vault_payment = business_profile
.external_vault_details
.is_external_vault_enabled()
&& payment_attempt.payment_token.is_some();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

external_vault_details will be configured at provider level not processor level

Comment thread crates/router/src/core/payments.rs Outdated
Comment on lines +14345 to +14377
let merchant_connector_account = if is_external_vault_payment {
resolve_external_vault_authentication_connector(
&state,
platform.get_processor(),
&business_profile,
)
.await?
.1
} else {
helpers::get_merchant_connector_account(
&state,
platform.get_processor(),
None,
profile_id,
authentication_connector.as_str(),
None,
)
.await?
};

let payment_method_details = if is_external_vault_payment {
None
} else {
Some(
helpers::get_payment_method_details_from_payment_token(
&state,
&payment_attempt,
&payment_intent,
&platform,
storage_scheme,
)
.await?
.ok_or(errors::ApiErrorResponse::InternalServerError)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we put this evaluation into a single conditional statement

Comment on lines +14548 to +14552
let payment_method_details = payment_method_details
.ok_or(errors::ApiErrorResponse::InternalServerError)
.attach_printable(
"payment_method_details missing for the legacy authentication path",
)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we want to raise error here

Comment on lines +15943 to +15952
// Set the orchestrated 3DS authentication record (carries CAVV/ECI). Used by the
// external-vault proxy UCS pre-authenticate (frictionless) path. No-op on the v2 data
// types, which do not yet carry an `authentication` field.
fn set_authentication(
&mut self,
_authentication: Option<
hyperswitch_domain_models::router_request_types::authentication::AuthenticationStore,
>,
) {
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should set authentication carrying CAVV

self.external_vault_pmd = external_vault_pmd;
}

fn set_authentication(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Comment on lines +1770 to +1795
let mut metadata_value = metadata.expose();
if let Some(obj) = metadata_value.as_object_mut() {
if let Some(force) = force_3ds_challenge {
obj.insert(
"force_3ds_challenge".to_string(),
serde_json::Value::Bool(force),
);
}
if let Some(url) = results_response_notification_url {
obj.insert(
"results_response_notification_url".to_string(),
serde_json::Value::String(url),
);
}
if let Some(url) = notification_url {
obj.insert(
"notification_url".to_string(),
serde_json::Value::String(url),
);
}
if let Some(serde_json::Value::Object(acquirer)) = acquirer_metadata {
for (key, value) in acquirer {
obj.insert(key, value);
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will we be taking this refactor later?? of adding all these fields as a part of Authentication request rather than sending in metadata


/// Header value indicating that no credentials are required (e.g. external-3DS
/// over VGS where mTLS is handled on the outbound proxy route, not by UCS).
pub const UCS_AUTH_NO_KEY: &str = "no-key";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should not be NO-KEY it will be certficate Auth

ShankarSinghC and others added 6 commits July 3, 2026 23:59
… proxy

Restructure the external-vault-proxy 3DS entry point to the same shape the inline
confirm uses (call_external_three_ds_authentication_if_eligible), so review is 1:1:

- get_payment_external_authentication_flow_during_confirm_for_external_proxy returns
  the enum PaymentExternalAuthenticationFlowForExternalProxy {PreAuthenticationFlow |
  PostAuthenticationFlow{authentication_id}} (None = continue to authorize).
- call_external_three_ds_authentication_if_eligible_for_external_proxy dispatches on it
  to perform_pre_authentication_for_external_proxy / perform_post_authentication_for_external_proxy
  (the latter renamed from resume_external_vault_post_authenticate_v1).

Was one fn (call_unified_connector_service_pre_authenticate_if_eligible_for_external_vault_proxy_v1)
that fused the pre-auth body with an early-return post-auth branch. Drops the unused
merchant_connector_account param. Behavior-preserving — frictionless charge re-verified.
Also trims verbose comments in the same file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… update_trackers

- No-customer-acceptance external 3DS over the VGS proxy now stashes the vault
  alias in the temp locker instead of persisting a modular customer PM;
  fetch_payment_method resolves the TemporaryGeneric token on re-confirm.
- Route the pre-auth Halt (challenge / auth-failure) through the operation's
  update_trackers for parity with the inline flow: persist_/fail_ only mutate
  payment_data in-memory; decide_ucs sets the attempt Pending so the challenge
  resume authorizes correctly.
- Consolidate operation_core's two connector arms into one shared body
  (Retryable behavior preserved).
- perform_post honors is_pull_mechanism_enabled (skip the UCS pull when pull is
  disabled or the auth is terminal), matching inline perform_post_authentication.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…h-connector UCS gate

- read external_vault_details (VGS config) from the provider profile via
  resolve_provider_profile at the config-read sites (payment gates, vault-MCA
  fetch, incoming webhook); the modular PM-storage profile stays the processor's
- build the authenticate router_data with the resolved auth connector so
  should_call_unified_connector_service evaluates the connector actually called (Netcetera)
- mechanical: drop redundant KeyManagerState annotations, single payment_method_details
  require on the legacy path, rename authorize_completes_in_this_call, functional
  browser_info fallback, notification_url as a typed common_utils::types::Url

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread crates/router/src/core/payments.rs Outdated
.get_required_value("payment_method_id")
.attach_printable("could not resolve a payment_method_id from the payment_token")?;
let pm_with_raw =
crate::core::payment_methods::transformers::fetch_payment_method_from_modular_service(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can we use absolute imports

Comment on lines +3379 to +3383
{
let mut authorize_attempt = payment_data.get_payment_attempt().clone();
authorize_attempt.status = common_enums::AttemptStatus::Pending;
payment_data.set_payment_attempt(authorize_attempt);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we take this decision based on authentication status

Comment thread crates/router/src/core/payments.rs Outdated
)
});

if !is_frictionless_success {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pre_authentication should not be able to forse authentication whether the card is frictionless or not

Some(payment_method_id) => {
let parent_payment_method_token =
common_utils::generate_id(consts::ID_LENGTH, "token");
let token_data = storage::PaymentTokenData::PermanentCard(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this PermanentCard

Comment thread crates/router/src/core/payments.rs Outdated
Comment on lines +6271 to +6275
let should_authorize_via_redirect =
!is_external_vault_payment && authorization_completes_in_this_call;
let response = if is_external_vault_payment && authorization_completes_in_this_call {
let payment_token = payment_attempt
.payment_token

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you simplify this logic, looks like we can reuse variables here

ShankarSinghC and others added 4 commits July 5, 2026 01:44
…idy external-vault 3DS

payments.rs:
- remove unreachable `is_frictionless_success` short-circuit and the now-unused
  `build_external_vault_pre_auth_authentication_store` (Netcetera /3ds/versioning
  always returns cavv=None, so pre-auth always halts for the SDK 3DS flow)
- rename `persist_external_vault_pre_auth_challenge_and_prepare_sdk_invoke`
  -> `persist_external_vault_pre_auth_and_prepare_sdk_invoke` (runs for every
  external-vault 3DS, not only the challenge path)
- guard the `AuthenticationPending -> Pending` transition before authorize
- use the `pm_transformers` alias instead of a fully-qualified path

payment_confirm_external_vault_proxy.rs:
- preserve the attempt status on reject/confirm updates instead of hardcoding it
- rename `halt_intent_status` -> `intent_status_optional`

connector_config.rs: use `Self::Netcetera`

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… retrieve, pre-auth webhook_url

- payment_confirm_external_vault_proxy.rs: source customer_acceptance from
  payment_data on the confirm update so the connector mandate is stored after
  external 3DS.
- payment_methods.rs: return the vault alias for external-vault payment methods
  that have no locker_id instead of attempting a raw-card vault fetch, fixing
  the MIT/repeat-payment 500 (missing connector_vault_id).
- payments.rs: thread the merchant webhook_url into the external-vault
  pre-authenticate router data.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-core Area: Core flows A-payment-methods Area: Payment Methods

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants