Summary
transform_de_output_for_router silently drops every merchant connector account after the first when a merchant holds several MCAs of the same connector, losing both routing targets and fallbacks.
Detail
The function builds the router-facing connector list from a Decision Engine /routing/evaluate response: evaluated_output first, then the remaining connectors from output as fallbacks, deduplicated. The dedup set was keyed on the connector name alone:
if seen.insert(eval_conn.connector) { ... } // merchant_connector_id not part of the key
RoutableConnectorChoice carries both connector and merchant_connector_id, but only connector went into the set.
Failure case
Any rule that spans two MCAs of one connector — e.g. a volume split across two paypal MCAs. DE returns the sampled winner in evaluated_output and both arms in output; the second paypal MCA is then discarded, so the router loses its fallback and the list length disagrees with the legacy euclid result.
Found while investigating a persistent shadow-mode diff (is_equal_length=false, DE one connector vs HS two) on a sandbox profile with exactly that shape — see #13565.
Fix
Key the dedup set on the full (connector, merchant_connector_id) pair. The second loop builds the RoutableConnectorChoice before keying off it, which also removes a duplicated RoutableConnectors::from_str — the TryFrom<ConnectorInfo> impl already performs that parse with equivalent error logging.
Behaviour note: entries with merchant_connector_id: None no longer collapse into a same-named entry with Some(id). Both lists are serialized from the same ConnectorInfo objects in the same rule, so this is not expected to trigger in practice.
Why it matters
Static routing via the Decision Engine must preserve MCA-level granularity; collapsing by connector name loses both the routing target and its fallbacks for multi-MCA merchants — and generates permanent, unfixable shadow diffs that would trip the kill switch on an otherwise healthy profile.
PRs
Summary
transform_de_output_for_routersilently drops every merchant connector account after the first when a merchant holds several MCAs of the same connector, losing both routing targets and fallbacks.Detail
The function builds the router-facing connector list from a Decision Engine
/routing/evaluateresponse:evaluated_outputfirst, then the remaining connectors fromoutputas fallbacks, deduplicated. The dedup set was keyed on the connector name alone:RoutableConnectorChoicecarries bothconnectorandmerchant_connector_id, but onlyconnectorwent into the set.Failure case
Any rule that spans two MCAs of one connector — e.g. a volume split across two paypal MCAs. DE returns the sampled winner in
evaluated_outputand both arms inoutput; the second paypal MCA is then discarded, so the router loses its fallback and the list length disagrees with the legacy euclid result.Found while investigating a persistent shadow-mode diff (
is_equal_length=false, DE one connector vs HS two) on a sandbox profile with exactly that shape — see #13565.Fix
Key the dedup set on the full
(connector, merchant_connector_id)pair. The second loop builds theRoutableConnectorChoicebefore keying off it, which also removes a duplicatedRoutableConnectors::from_str— theTryFrom<ConnectorInfo>impl already performs that parse with equivalent error logging.Behaviour note: entries with
merchant_connector_id: Noneno longer collapse into a same-named entry withSome(id). Both lists are serialized from the sameConnectorInfoobjects in the same rule, so this is not expected to trigger in practice.Why it matters
Static routing via the Decision Engine must preserve MCA-level granularity; collapsing by connector name loses both the routing target and its fallbacks for multi-MCA merchants — and generates permanent, unfixable shadow diffs that would trip the kill switch on an otherwise healthy profile.
PRs