juspay / juspay/decision-engine

feat(feedback): only update gateway scores for valid PSP-side errors, not client/merchant errors

Open
#237 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
128
Forks
36
Avg merge
1d 15h
Merged PRs (30d)
34

Description

## Context

Gateway scoring in the decision engine currently runs whenever `update-gateway-score` is called with a failed `TxnStatus`. The scoring-type decision lives in `get_gateway_scoring_type` in [`src/feedback/gateway_scoring_service.rs`](src/feedback/gateway_scoring_service.rs) (around L293) and is effectively:

```rust
if is_success {
GatewayScoringType::Reward
} else if is_failure {
GatewayScoringType::PenaliseSrv3
} else if time_difference < threshold {
GatewayScoringType::Penalise
} else {
GatewayScoringType::PenaliseSrv3
}
```

There is no inspection of *why* the transaction failed. Every failure — whether the gateway was down, the customer entered a wrong CVV, the merchant sent an invalid request, or the card issuer declined for insufficient funds — penalises the gateway's SR / elimination / latency scores identically.

This is incorrect: scoring should reflect PSP health, and client-side / merchant-side / issuer-side failures are not signals that the PSP is unhealthy. Penalising on these errors causes:

- Healthy gateways to be demoted in rankings after a spike of merchant-side errors (e.g. a buggy integration sending malformed requests).
- Routing to oscillate away from a perfectly good gateway during a customer-behavior spike (e.g. a marketing push causing lots of declined cards).
- Scoring noise that makes SR-based routing less predictable.

## Motivation

Gateway scores should be a trustworthy signal of gateway health. A gateway must only be penalised when it was genuinely at fault.

## Proposal

This builds on the Gateway Status Mapping (GSM) work (see the related issue adding a GSM table). With GSM in place, each `(gateway, error_code, error_message)` tuple has a canonical classification. We should use that classification to filter scoring updates.

1. **Classify errors** into at least these buckets in the GSM (or via a new `error_category` column):
- `PSP_FAULT` — connector / gateway failure, timeout, 5xx, acquirer down. **Score update: yes.**
- `ISSUER_DECLINE` — card declined, insufficient funds, do-not-honor. **Score update: no (or separate signal).**
- `CLIENT_ERROR` — bad CVV, wrong OTP, user cancelled, expired card. **Score update: no.**
- `MERCHANT_ERROR` — invalid request, bad signature, misconfigured MID. **Score update: no.**
- `UNKNOWN` — no mapping found. **Score update: configurable default (recommend: yes, but log loudly so mappings can be added).**

2. **Plumb `error_code` / `error_message` through the feedback path** from `UpdateScorePayload` into `get_gateway_scoring_type` and `update_gateway_score` in `src/feedback/gateway_scoring_service.rs`.

3. **Short-circuit scoring** in `check_and_update_gateway_score_` when the classified category is not `PSP_FAULT` (and not `UNKNOWN` treated as PSP). Return a success response so callers still get their retry decision, but skip the Redis score mutations.

4. **Metrics**: add a counter broken down by category so operators can see how many feedback calls were filtered out and why.

5. **Config knob**: a merchant-level (and global-default) setting to choose strict mode (only `PSP_FAULT` updates scores) vs lenient mode (current behavior, for backwards compatibility during rollout).

## Acceptance Criteria

- [ ] `get_gateway_scoring_type` (or a new wrapper) takes the error classification into account and returns a no-op variant when the failure is not the PSP's fault.
- [ ] Gateway SR / elimination / latency scores are unchanged when a failure is classified as `CLIENT_ERROR`, `MERCHANT_ERROR`, or `ISSUER_DECLINE`.
- [ ] `PSP_FAULT` failures continue to penalise the gateway exactly as today.
- [ ] A new Prometheus counter exposes filtered-vs-applied scoring updates by category.
- [ ] A merchant-level config flag can opt-in/out of strict filtering, with the default chosen for a safe rollout.
- [ ] Unit tests cover each category → expected scoring action.
- [ ] Docs updated: `updateGatewayScore.mdx` explains that classification is driven by the GSM table and that unmapped codes behave per the default policy.

## Dependencies

- Builds on the GSM table work — this issue should land after (or alongside) the GSM issue so that error classification has a canonical source.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.