BackARTICLE // FIXING A MULTI-LEV…

Fixing a Multi-Level Incentive System (L1–L4) Without Breaking the Workflow

How I debugged and stabilized a hierarchical incentive system with approval, hold, and reporting logic across multiple roles.

This started with a system that looked correct on paper:

A multi-level incentive flow with 4 roles (L1 → L4), approvals, and reporting.

But once I started testing real scenarios, everything began to break in subtle ways.


The original goal

I wanted a system where:

  • Sales (L1) creates incentives
  • Team Lead (L2) reviews
  • Manager (L3) reviews
  • Owner (L4) finalizes

And each level should:

  • Only see incentives when the previous level is approved
  • Be blocked if a lower level puts it on hold
  • Resume correctly when reopened

This sounds straightforward.

It wasn’t.


Where things went wrong

The main issue was this:

Database state ≠ UI state

Examples:

  • L2 could see incentives even if L1 hadn’t approved
  • Held incentives didn’t propagate correctly upward
  • Higher levels still saw items they shouldn’t act on
  • Reports showed everything, including irrelevant states

Everything worked individually, but not together.


The core realization

I stopped relying on raw database fields like:

  • status
  • holdReason
  • heldBy

And introduced a derived concept:

Effective State


Effective State

Instead of trusting the current incentive row, I computed:

  • effective status
  • effective hold reason
  • effective blocker

Based on lower-level incentives in the same sale

Example idea:

function computeEffectiveState(inc) {
  const lowerHeld = checkLowerLevelHold(inc)

  if (lowerHeld) {
    return {
      effectiveStatus: "ON_HOLD",
      effectiveHeldBy: lowerHeld.heldBy
    }
  }

  return {
    effectiveStatus: inc.status
  }
}

This changed everything.

Now the UI reflects workflow reality, not just DB values.


Visibility logic (the real rules)

Each level follows an upward dependency:

L1 → always visible

L2 → only if L1 is CLAIMABLE or ON_HOLD

L3 → only if L2 is ready

L4 → only if L3 is ready

Additionally:

If any lower level is ON_HOLD, all upper levels:

cannot act

see it as blocked

This creates a controlled flow instead of chaos.


Reports needed different rules

Reports are not workflow.

They are financial output.

So I restricted reports to:

CLAIMABLE

CLAIM_REQUESTED

PAID

And always scoped them to:

beneficiaryUserId = currentUser

That removed a lot of noise instantly.


Frontend bugs that surfaced

Filters didn’t update the table

The API was returning correct data.

But the UI didn’t change.

Cause:

React Query key didn’t include filters

Fix:

queryKey: ["reports", queryParams]


Select input didn’t update

Using:

defaultValue

Instead of:

value

So UI didn’t reflect URL changes.


CSV export broke numbers

₹5,000 became 5

Cause:

CSV comma parsing

Fix:

"₹5,000"

Escaping values solved it.


UI improvements

I also fixed a structural issue:

I was reusing one filter component everywhere.

But:

Incentives page → full lifecycle

Review page → pending + hold

Reports → only financial states

So I made filters configurable instead of duplicating them.


Small but useful addition

Added per-row export in reports:

Export one incentive as CSV

Keep bulk export for full dataset

This made reports more practical.


Tech stack

Next.js (App Router)

React Query

Nodejs (Express.js)

Prisma

PostgreSQL

The challenge was not tooling.

It was correctly modeling workflow behavior.


What this taught me

Database status is not enough

Always derive UI state when hierarchy exists

Query keys must include filters

CSV is fragile unless escaped

Reusable components should be configurable, not rigid


Closing

This wasn’t about adding more code.

It was about making the system behave like the real process it represents.

Once the logic matched reality, everything else became simpler.

ioNihal

Designed and Developed withemoticon

ioNihal © 2026.

  • Home
  • Skills
  • Projects
  • Experience
  • About
  • Contact