Skip to content

feat(routing): record sticky-routing outcomes from the feedback path - #420

Open
prajjwalkumar17 wants to merge 2 commits into
mainfrom
feat/sticky-routing-feedback
Open

prajjwalkumar17 wants to merge 2 commits into
mainfrom
feat/sticky-routing-feedback

Conversation

@prajjwalkumar17

@prajjwalkumar17 prajjwalkumar17 commented Sep 4, 2026

Copy link
Copy Markdown
Member

Phase 2 of 3 for sticky routing (#393). Merge order: #419 → this → #412.

Important

This branch necessarily contains phase 1's commit (the code builds on it), so "Files changed" shows both. Review only this phase's commit: 5c542ea (Commits tab → last commit) — phase 1 is reviewed in #419. After #419 merges, this branch gets rebased so the diff collapses to the one commit.

Closes #415.

What this adds

POST /update-gateway-score gains three optional, backward-compatible fields — customerId, paymentMethod, paymentMethodType. Payload wins; the decide-time gateway_scoring_data_{payment_id} snapshot (which now also stashes customerId from the order) is the fallback, so existing callers change nothing and only need the fields when success webhooks can outlive the snapshot's 30-minute TTL.

  • Two-pass hook in check_and_update_gateway_score_: payload-only pass before the snapshot fetch (a CHARGED webhook hours late still counts — same placement rationale as the A/B outcome emit), snapshot pass after.
  • Both directions, narrow sets (merchant feedback is the source of truth): CHARGED/AUTHORIZED/PARTIAL_CHARGED increment the habit; failure statuses (AUTHENTICATION_FAILED/AUTHORIZATION_FAILED/JUSPAY_DECLINED/FAILURE) decrement it, floored at zero. Wider lifecycle statuses (VOIDED, AUTO_REFUNDED, …) touch neither. Failures never create a key or field, so footprint stays tied to successful payments. Retries are correct by construction: fail-on-A subtracts from A only if A had a habit; the succeeding attempt's connector gains.
  • No dedupe, by design: update-gateway-score is the source of truth — every event counts, N times if sent N times. This matches the engine's default SR scoring behavior (its feedback locks are opt-in per merchant) and keeps the hot path free of a per-payment lock key.
  • Bug fix en route: the GSM healthy-failure early-return now fires on failures only — previously a success carrying errorInfo (e.g. echoed from a 3DS step-up) skipped both the SR reward and the snapshot fetch.
  • Gated per merchant by the sticky_routing_enabled FeatureConf; sticky write failures are logged, never surfaced. openapi + api-refs updated.

🤖 Generated with Claude Code

@prajjwalkumar17
prajjwalkumar17 changed the base branch from feat/sticky-routing-storage to main September 4, 2026 09:15
@prajjwalkumar17
prajjwalkumar17 force-pushed the feat/sticky-routing-feedback branch from 9c50f17 to 7bec489 Compare September 6, 2026 08:48
@prajjwalkumar17
prajjwalkumar17 force-pushed the feat/sticky-routing-feedback branch from 7bec489 to dc9f338 Compare September 8, 2026 10:51
@prajjwalkumar17 prajjwalkumar17 changed the title feat(routing): record sticky-routing successes from the feedback path feat(routing): record sticky-routing outcomes from the feedback path Sep 8, 2026
@prajjwalkumar17
prajjwalkumar17 force-pushed the feat/sticky-routing-feedback branch from dc9f338 to 23f953b Compare September 9, 2026 08:45
@prajjwalkumar17 prajjwalkumar17 self-assigned this Sep 9, 2026
@prajjwalkumar17
prajjwalkumar17 marked this pull request as ready for review September 9, 2026 10:31
@prajjwalkumar17
prajjwalkumar17 force-pushed the feat/sticky-routing-feedback branch from 23f953b to 5c542ea Compare September 15, 2026 07:52
"enforceDynamicRoutingFailure": null
"enforceDynamicRoutingFailure": null,
"customerId": "cust_123",
"paymentMethod": "INTERAC",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

we already have the paymentMethod and paymentMethodType in the decide_gateway call right? why are we getting it again?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The decide-time values survive only in the gateway_scoring_data_{payment_id} Redis snapshot, which lives 30 minutes — DE has no transactions table, so nothing else remembers a payment's pm/pmt. Within that window the feedback fields are indeed redundant and can be omitted (the snapshot fallback covers it). They exist for feedback that arrives later — CHARGED webhooks routinely land hours after the decide — where without them the success can't be attributed to a customer × pm:pmt × connector habit at all. Same reason customerId is on the payload: payload wins, snapshot is the fallback, and only late-webhook callers need to send them.

prajjwalkumar17 and others added 2 commits September 15, 2026 15:37
…r scores

Storage layer only; feedback writes and the decide-side read come in the
stacked follow-ups.

- One Redis hash per (merchant, customer): fields pm:pmt:connector hold a
  NET count — successes increment, gateway failures decrement (floored at
  zero; merchant feedback is the source of truth in both directions).
  Failures never create a key or field, so key-creation stays tied to
  successful payments and the footprint stays bounded. Only positive
  counts qualify as pin candidates. Read is a single HGETALL returning
  counts sorted per exact combo; deliberately no cross-combo fallback
  (transaction-level fallbacks already cover a missing combo).
- Eviction safety: every key volatile with a sliding TTL re-armed only on
  success; per-customer combo cap with lowest-count pruning that drains
  full excess; per-merchant two-window admission budget so new-key growth
  is bounded up front. Defaults overridable via service config.
- Sticky on/off is a merchant-level feature flag (sticky_routing_enabled
  FeatureConf, dashboard plumbing in a follow-up) — deliberately NOT part
  of the euclid rule store, so toggling never touches the algorithm
  lifecycle.
- New wrapper commands: hincrby_with_expire, hincrby, hgetall_map,
  hdel_field.

Measured: 2-combo customer = 136B, 30-combo = 952B, listpack-encoded
(Redis 7.2.7). Refs #393.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Write side of sticky routing; the decide-side read comes next in the
stack. Merchant feedback is the source of truth in both directions:
successes (CHARGED/AUTHORIZED/PARTIAL_CHARGED) increment the habit,
failure statuses (AUTHENTICATION_FAILED/AUTHORIZATION_FAILED/
JUSPAY_DECLINED/FAILURE) decrement it, floored at zero. Wider lifecycle
statuses (VOIDED, AUTO_REFUNDED, ...) touch neither direction. Failures
never create a key or field.

- update-gateway-score gains optional customerId/paymentMethod/
  paymentMethodType (backward compatible). Payload wins; the decide-time
  GatewayScoringData snapshot (now also carrying customerId) is the
  fallback, so callers need the new fields only when webhooks can
  outlive the snapshot's 30-min TTL.
- Two-pass hook in check_and_update_gateway_score_: payload-only pass
  before the snapshot fetch (survives snapshot expiry, same rationale as
  the ab-test emit), snapshot pass after.
- Deliberately NO dedupe: every feedback event counts, N times if sent
  N times — matching the engine's default SR scoring behavior (its
  feedback locks are opt-in per merchant), and avoiding a lock key per
  payment on the hot path.
- GSM healthy-failure early return is now failure-only: a success with
  errorInfo previously skipped both the SR reward and any snapshot-based
  sticky write.
- Sticky key pm/pmt are case-folded at the key boundary: the snapshot
  stores them uppercased while payloads arrive verbatim, and the two
  write sources must land on one hash field. Connector stays verbatim
  for the eligible-list comparison at decide time.
- openapi + api-ref updated.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sticky routing: record successes on update-gateway-score

2 participants