insightfy-bloom-ms-ans / KPI_JWT_UPDATE.md
MukeshKapoor25's picture
feat(kpi): Implement comprehensive KPI security and documentation update
bbae70d

KPI API Update - JWT-based merchant_id

Change Summary

Date: 2025-11-09
Type: Security Enhancement
Impact: Breaking Change (Request Format)


What Changed

The KPI endpoints now automatically extract merchant_id from the JWT token instead of requiring it in the request body. This is more secure and follows REST API best practices.

Before (Old - Insecure)

POST /api/v1/kpi/stats
{
  "merchant_id": "IN-NATUR-CHEANN-7D2B-O9BP1",  // ❌ User could specify any merchant
  "period_window": "mtd"
}

After (New - Secure)

POST /api/v1/kpi/stats
{
  "period_window": "mtd"  // βœ… merchant_id from JWT token
}

Why This Change?

Security Issues with Old Approach

  1. Authorization Bypass: Users could potentially request data for other merchants
  2. Validation Overhead: Required server-side validation to match JWT merchant_id
  3. Redundancy: merchant_id already in JWT token
  4. Error Prone: Mismatch between JWT and request body caused 403 errors

Benefits of New Approach

  1. βœ… More Secure: Impossible to request other merchant's data
  2. βœ… Simpler: No need to include merchant_id in request
  3. βœ… Cleaner: Follows JWT authentication best practices
  4. βœ… Consistent: Matches pattern used in other services

Updated Endpoints

1. POST /api/v1/kpi/stats

Old Request:

curl -X POST http://localhost:9100/api/v1/kpi/stats \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "merchant_id": "IN-NATUR-CHEANN-7D2B-O9BP1",
    "period_window": "mtd"
  }'

New Request:

curl -X POST http://localhost:9100/api/v1/kpi/stats \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "period_window": "mtd"
  }'

2. POST /api/v1/kpi/stats/individual

Old Request:

curl -X POST http://localhost:9100/api/v1/kpi/stats/individual \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "merchant_id": "IN-NATUR-CHEANN-7D2B-O9BP1",
    "period_window": "mtd"
  }'

New Request:

curl -X POST http://localhost:9100/api/v1/kpi/stats/individual \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "period_window": "mtd"
  }'

Request Schema

KPIStatsRequest (Updated)

class KPIStatsRequest(BaseModel):
    """Request schema for merchant KPI stats."""
    period_window: PeriodWindow = Field(
        PeriodWindow.MTD, 
        description="Period window (mtd, qtd, ytd)"
    )

Fields:

  • period_window (required): One of mtd, qtd, ytd

Removed:

  • merchant_id - Now extracted from JWT token

Implementation Details

Router Changes

@router.post("/stats", response_model=KPIStatsResponse)
async def get_merchant_kpi_stats(
    kpi_stats_request: KPIStatsRequest = Body(...),
    current_user: dict = Depends(require_view_analytics_permission),
    correlation_id: str = Depends(get_request_id)
):
    # Get merchant_id from JWT token
    merchant_id = current_user["merchant_id"]  # βœ… Secure
    
    # Get KPI stats
    kpi_stats = await KPIService.get_merchant_kpi_stats(
        merchant_id=merchant_id,
        period_window=kpi_stats_request.period_window.value
    )

Key Changes:

  1. Removed merchant_id from request body
  2. Extract merchant_id from current_user (JWT payload)
  3. Removed validation logic (no longer needed)

Migration Guide

For API Consumers

Step 1: Update Request Body

Remove merchant_id from all KPI API requests.

Before:

const response = await fetch('/api/v1/kpi/stats', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    merchant_id: 'IN-NATUR-CHEANN-7D2B-O9BP1',  // ❌ Remove this
    period_window: 'mtd'
  })
});

After:

const response = await fetch('/api/v1/kpi/stats', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    period_window: 'mtd'  // βœ… Only this needed
  })
});

Step 2: Ensure JWT Token is Valid

Make sure your JWT token contains the merchant_id claim:

{
  "sub": "bloom",
  "merchant_id": "IN-NATUR-CHEANN-7D2B-O9BP1",
  "associate_id": "AST011",
  "role_id": "admin",
  "branch_id": "hq",
  "exp": 1762701984
}

Step 3: Test

# Test with updated request
curl -X POST http://localhost:9100/api/v1/kpi/stats \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"period_window": "mtd"}'

Error Handling

Common Errors

1. Missing JWT Token

{
  "detail": "Not authenticated"
}

Solution: Include valid JWT token in Authorization header

2. Invalid JWT Token

{
  "detail": "Could not validate credentials"
}

Solution: Ensure JWT token is valid and not expired

3. Missing Permission

{
  "detail": "Forbidden"
}

Solution: Ensure user has VIEW_ANALYTICS permission

4. No Data Found

{
  "success": false,
  "message": "No KPI stats found for merchant X with period mtd"
}

Solution: Ensure merchant_kpi_stats collection has data for this merchant


Testing

Updated Test Files

  1. test_kpi_stats_request.json
{
  "period_window": "mtd"
}
  1. test_kpi_stats_endpoints.sh All test requests updated to remove merchant_id

Run Tests

# Shell tests
./test_kpi_stats_endpoints.sh http://localhost:9100 YOUR_JWT_TOKEN

# Python tests (if updated)
python test_kpi_stats.py

Backward Compatibility

⚠️ Breaking Change: This is a breaking change. Old requests with merchant_id in the body will still work but the field will be ignored.

Recommendation: Update all API consumers immediately to use the new format.


Security Benefits

Before (Vulnerable)

User sends: merchant_id = "MERCHANT_A"
JWT contains: merchant_id = "MERCHANT_B"
Server validates: ❌ Mismatch β†’ 403 Forbidden

After (Secure)

User sends: (no merchant_id)
JWT contains: merchant_id = "MERCHANT_B"
Server uses: βœ… JWT merchant_id β†’ Secure

Result: Impossible to request data for other merchants


Documentation Updates

Files Updated

  1. βœ… app/schemas/kpi_schema.py - Removed merchant_id field
  2. βœ… app/routers/kpi_router.py - Extract merchant_id from JWT
  3. βœ… test_kpi_stats_request.json - Updated example
  4. βœ… test_kpi_stats_endpoints.sh - Updated all tests
  5. βœ… KPI_JWT_UPDATE.md - This document

Example: Complete Request

curl -X 'POST' \
  'http://127.0.0.1:9100/api/v1/kpi/stats' \
  -H 'accept: application/json' \
  -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJibG9vbSIsIm1lcmNoYW50X2lkIjoiSU4tTkFUVVItQ0hFQU5OLTdEMkItTzlCUDEiLCJhc3NvY2lhdGVfaWQiOiJBU1QwMTEiLCJyb2xlX2lkIjoiYWRtaW4iLCJicmFuY2hfaWQiOiJocSIsImV4cCI6MTc2MjcwMTk4NH0.-A47U82dA4LOkoWEKIqCL7Fyv1CeA2bXbL_KZffewO0' \
  -H 'Content-Type: application/json' \
  -d '{
  "period_window": "mtd"
}'

Expected Response:

{
  "success": true,
  "message": "Merchant KPI stats retrieved successfully",
  "data": {
    "merchant_id": "IN-NATUR-CHEANN-7D2B-O9BP1",
    "period_window": "mtd",
    "generated_at": "2025-11-09T00:00:00Z",
    "kpis": {
      "total_revenue": {
        "title": "Total Revenue",
        "value": 1234567.89,
        "unit": "INR",
        "delta": 0.08,
        "source": "postgres"
      }
      // ... more KPIs
    },
    "charts": {
      // ... chart data
    }
  }
}

Summary

βœ… More Secure: merchant_id from JWT token only
βœ… Simpler API: One less field in request
βœ… Better UX: No more 403 errors from mismatch
βœ… Best Practice: Follows JWT authentication pattern

Action Required: Update all API consumers to remove merchant_id from request body.


Updated: 2025-11-09
Version: 2.1
Status: βœ… Production Ready