# AGNET × Agri-Access Integration — Project Specifications & Plan

**Repository:** `agnet-uzbekistan-platform` (Next.js BFF + farmer UI)  
**Upstream scoring source:** [`clchinkc/agri-access`](https://github.com/clchinkc/agri-access) (Flask Python, Prithvi-EO, SHAP, Basel III, Gemini)  
**Version:** 0.1.0 (integration draft)  
**Last updated:** 2026-06-26  
**Related backend:** `../mpgis-farmer-platform` (NestJS core-api, PostgreSQL/PostGIS, Prisma)  
**Parent plan:** [`PROJECT_PLAN.md`](./PROJECT_PLAN.md) — Phase 4 (GIS & Scoring)  
**Wizard field spec:** [`WIZARD_FIELD_SPEC.md`](./WIZARD_FIELD_SPEC.md)

---

## Executive Summary

This document specifies the integration of **Agri-Access** — an AI-powered agricultural credit scoring platform (Prithvi satellite ML, SHAP explainability, Basel III risk parameters, Gemini farmer explanations) — into the **AGNET** Uzbekistan farmer financial inclusion stack. Agri-Access was built for Indonesian banking (SLIK scale, IDR, KUR rates); this plan localizes it for Uzbekistan (CBU credit bureau equivalents, UZS, Central Asian crop/climate profiles) and wires it into AGNET's existing onboarding wizard (Steps 12–13), dashboard finance module, and NestJS scoring-service orchestration layer.

**Integration strategy:** Treat Agri-Access as a **Python scoring worker microservice** (`agri-access-worker`) invoked asynchronously by `scoring-service` in `mpgis-farmer-platform`. The Next.js BFF continues to poll `GET /api/onboarding/{id}/status`; no direct browser-to-Python calls. Agri-Access Flask endpoints become internal-only behind the NestJS adapter.

**OpenAPI specifications (extended by this plan):**

| Spec | Path | Scope |
|------|------|-------|
| BFF API (this repo) | `specs/openapi/bff-api.v1.yaml` | Browser-facing `/api/*` routes (unchanged surface; enriched status payload) |
| Core API | `../mpgis-farmer-platform/packages/api-contracts/openapi/core-api.v1.yaml` | Auth, onboarding, parcels, cases |
| GIS | `gis.v1.yaml` | Parcel geometry, NDVI enrichment |
| Scoring | `scoring.v1.yaml` | ML/rules fusion scoring — **extend with Agri-Access adapter contract** |
| Documents | `documents.v1.yaml` | Presigned upload, OCR extraction |
| Rules | `rules.v1.yaml` | Eligibility rules engine |
| **Agri-Access adapter (new)** | `specs/openapi/agri-access-adapter.v1.yaml` | Internal worker request/response schema |

**Key deliverables:**

| # | Deliverable | Phase |
|---|-------------|-------|
| 1 | Forked & localized `agri-access-worker` (Python) | Phase 4A |
| 2 | NestJS `AgriAccessScoringAdapter` in scoring-service | Phase 4A |
| 3 | OpenAPI extension for scoring-service ↔ worker | Phase 4A |
| 4 | Enhanced `StepScoring.tsx` / `StepResult.tsx` (SHAP, Basel III, Gemini explain) | Phase 4A |
| 5 | Finance dashboard module enrichment (risk rating, IFRS 9 stage) | Phase 4A |
| 6 | Security remediation (remove hardcoded API keys) | Phase 4A P0 |
| 7 | Uzbekistan CBU/credit bureau localization | Phase 4A P0 |

---

## System Architecture

```mermaid
flowchart TB
    subgraph Browser
        UI[Next.js Pages]
        Step12[StepScoring.tsx]
        Step13[StepResult.tsx]
        FinDash[FinanceModule.tsx]
        Client[lib/api-client.ts]
    end

    subgraph Agnet_BFF["Agnet BFF (this repo)"]
        API["/api/* routes"]
        Mock[dev-api-mock.ts]
        MW[middleware.ts]
    end

    subgraph Backend["mpgis-farmer-platform"]
        Core[core-api :4000]
        GIS[gis-service]
        Score[scoring-service]
        Adapter[AgriAccessScoringAdapter]
        DB[(PostgreSQL + PostGIS)]
    end

    subgraph Worker["agri-access-worker (Python)"]
        Flask[basel_iii_api.py]
        Prithvi[prithvi_extractor.py]
        ML[banking_credit_model.py]
        SHAP[shap-visualization.js]
        Gemini[Gemini explain API]
    end

    subgraph External
        NASA[NASA GIBS / Prithvi]
        Weather[OpenWeather / UZ climate model]
        Vault[Secrets vault]
    end

    UI --> Client
    Step12 --> Client
    Step13 --> Client
    FinDash --> Client
    Client --> API
    MW --> UI
    API -->|CORE_API_URL| Core
    API -->|CORE_API_USE_MOCK| Mock
    Core --> DB
    Core --> GIS
    Core --> Score
    Score --> Adapter
    Adapter -->|POST /internal/score| Flask
    Flask --> Prithvi
    Flask --> ML
    Flask --> SHAP
    Flask --> Gemini
    Prithvi --> NASA
    Flask --> Weather
    Adapter --> Vault
    Flask --> Vault
    GIS -->|NDVI GeoJSON| Score
```

**Orchestration flow:**

1. Farmer completes Step 11 (REVIEW) → `POST /api/onboarding/{id}/submit`
2. `core-api` enqueues scoring job → `scoring-service`
3. `scoring-service` aggregates `stepData` + GIS enrichment → maps to Agri-Access request
4. `agri-access-worker` runs Prithvi + ML + Basel III + SHAP (3–30 s)
5. Adapter normalizes response → AGNET `OnboardingStatusResponse` (score 0–1000, reasonCodes, eligibility)
6. `StepScoring.tsx` polls until `status === completed`; persists to `stepData.SCORING`
7. `StepResult.tsx` + `FinanceModule.tsx` display enriched outcome

---

## Completed Actions (Pre-Integration Baseline)

### AGNET Platform (already shipped — Phase 0–2)

| # | Item | Status | Evidence |
|---|------|--------|----------|
| 1 | 13-step onboarding wizard with SCORING + RESULT steps | ✅ Done | `components/onboarding/steps/StepScoring.tsx`, `StepResult.tsx` |
| 2 | Scoring status polling (`GET /api/onboarding/{id}/status`) | ✅ Done | `app/api/onboarding/[...path]/route.ts` |
| 3 | Reason code plain-language mapping (uz/ru) | ✅ Done | `lib/scoring-reasons.ts` |
| 4 | Finance dashboard module shell | ✅ Done | `components/farmer/modules/FinanceModule.tsx` |
| 5 | OpenAPI step schemas (`StepScoringData`, `StepResultData`) | ✅ Done | `specs/openapi/components/onboarding-steps.yaml` |
| 6 | Dev mock scoring (score 720, eligible) | ✅ Done | `lib/dev-api-mock.ts` |
| 7 | Phase 4 planned in parent PROJECT_PLAN | ✅ Done | M4.1–M4.6 milestones defined |

### Agri-Access Upstream (reference implementation — not yet integrated)

| # | Item | Status | Evidence (upstream repo) |
|---|------|--------|--------------------------|
| 8 | Flask API server with CORS | ✅ Available | `basel_iii_api.py` |
| 9 | Prithvi-EO-2.0-300M satellite feature extraction | ✅ Available | `prithvi_extractor.py` |
| 10 | Random Forest + XGBoost credit model with SHAP | ✅ Available | `banking_credit_model.py` |
| 11 | Basel III PD/LGD/EAD/ECL calculations | ✅ Available | `basel_iii_api.py` → `/api/analyze` |
| 12 | Gemini farmer explanations (ID/en) | ✅ Available | `/api/farmer/explain` |
| 13 | SHAP visualization endpoint | ✅ Available | `/api/shap-visualization/{output_type}` |
| 14 | Model health check | ✅ Available | `/api/model-status` |
| 15 | Demo UI (credit-score-arc.js, shap-visualization.js) | ✅ Available | Static frontend in upstream repo |

### Integration Gaps (must close in Phase 4A)

| # | Gap | Impact |
|---|-----|--------|
| G-1 | Hardcoded `GEMINI_API_KEY` and OpenWeather fallback key in upstream | **Critical security** — keys exposed in public GitHub |
| G-2 | Indonesian SLIK 1–5 scale vs AGNET 0–1000 score | Score normalization required |
| G-3 | IDR currency vs UZS | Currency conversion + locale formatting |
| G-4 | Indonesia crop/climate heuristics vs Uzbekistan | Model retraining or rule overlay |
| G-5 | No async job contract | scoring-service needs queue + idempotency |
| G-6 | Browser cannot call Python directly | Adapter + internal network only |
| G-7 | SHAP/Gemini/Basel III not surfaced in AGNET UI | StepScoring/Result/Finance need enrichment |

---

## Agri-Access → AGNET Field Mapping

Adapter layer (`AgriAccessScoringAdapter`) maps onboarding `stepData` to Agri-Access `/api/analyze` request body and normalizes response to AGNET schemas.

### Request mapping (AGNET `stepData` → Agri-Access input)

| Agri-Access field | Type | AGNET source | stepData path | Transform / notes |
|-------------------|------|--------------|---------------|-------------------|
| `farmerName` | string | FARM_PROFILE | `stepData.FARM_PROFILE.fullName` | Direct |
| `latitude` | float | GIS_MAPPING | `stepData.GIS_MAPPING.coordinates.lat` | Primary parcel; fallback agent_pending centroid |
| `longitude` | float | GIS_MAPPING | `stepData.GIS_MAPPING.coordinates.lng` | Primary parcel |
| `farmSize` | float (ha) | LAND_SKETCH | `stepData.LAND_SKETCH.parcels[primary].areaHa` | Sum if multi-parcel primary not set |
| `primaryCrop` | string | CROP_PLAN | `stepData.CROP_PLAN.crops[0].crop` | Map UZ crop ref → Agri-Access enum (`cotton`, `wheat`, `rice`, …) |
| `loanAmount` | float | FINANCE_REQUEST | `stepData.FINANCE_REQUEST.amountUzs` | UZS → pass as numeric; worker uses UZ locale |
| `loanTerm` | int (months) | FINANCE_REQUEST | `stepData.FINANCE_REQUEST.termMonths` | Direct |
| `loanPurpose` | string | FINANCE_REQUEST | `stepData.FINANCE_REQUEST.purpose` | Map finance_purpose ref → snake_case |
| `collateralType` | string | FARM_ASSETS | `stepData.FARM_ASSETS.assets[0].assetType` | Default `land` if none |
| *(extended)* `monthlyIncome` | float | FINANCE_REQUEST | `stepData.FINANCE_REQUEST.monthlyIncomeUzs` | DTI feature — not in upstream; add to adapter |
| *(extended)* `existingLoans` | float | FINANCE_REQUEST | `stepData.FINANCE_REQUEST.existingLoansUzs` | DTI feature |
| *(extended)* `repaymentHistory` | string | FINANCE_REQUEST | `stepData.FINANCE_REQUEST.repaymentHistory` | Map enum → numeric weight |
| *(extended)* `yearsFarming` | int | FARM_PROFILE | `stepData.FARM_PROFILE.yearsFarming` | Replaces mock `15.0` in upstream |
| *(extended)* `farmerAge` | int | FARM_PROFILE | computed from `dateOfBirth` | Replaces mock `35.0` |
| *(extended)* `ndviSnapshot` | object | GIS service | enrichment payload | Optional pre-computed NDVI from gis-service |
| *(extended)* `countryCode` | string | ACCOUNT | `stepData.ACCOUNT.countryCode` | Always `UZ`; drives locale rules |
| *(extended)* `consentCreditBureau` | boolean | CONSENT | `stepData.CONSENT.creditBureau` | Gate CBU pull (future) |

### Response mapping (Agri-Access output → AGNET status / stepData)

| Agri-Access response path | AGNET target | stepData / API field | Transform |
|---------------------------|--------------|----------------------|-----------|
| `basel_iii_results.credit_score` (300–850) | score | `OnboardingStatusResponse.score` | Linear map to **0–1000**: `round((score - 300) / 550 * 1000)` |
| `basel_iii_results.credit_score` | decision | `eligibilityOutcome` | ≥650 → `eligible`; 550–649 → `conditional`; <550 → `not_eligible` (tunable) |
| `shap_explanations.credit_score.features[]` | reasonCodes | `reasonCodes[]` | Map feature names → `RC_*` codes; set `direction`, `contribution` |
| `basel_iii_results.probability_of_default` | *(dashboard)* | `finance.riskPd` | Store in FinanceCase metadata |
| `basel_iii_results.risk_rating` | *(dashboard)* | `finance.riskRating` | A–E or localized |
| `basel_iii_results.ifrs9_stage` | *(dashboard)* | `finance.ifrs9Stage` | Stage 1/2/3 |
| `satellite_analysis` | SCORING enrichment | `stepData.SCORING.satelliteSummary` | New optional field |
| `weather_analysis` | SCORING enrichment | `stepData.SCORING.weatherSummary` | New optional field |
| `/api/farmer/explain` response | SCORING/RESULT | `stepData.SCORING.farmerExplanation` | uz/ru text from Gemini |
| `slik_score` (1–5) | *(internal)* | — | **Replace with `cbuClass`** (1–5 CBU collectibility) for UZ |
| `browseImages[]` | SCORING UI | `stepData.SCORING.satelliteImages` | URLs proxied via BFF image proxy |
| N/A (core-api) | RESULT | `financeCaseId`, `financeCaseStatus` | Created by scoring-service after score |
| N/A (rules engine) | RESULT | `conditionalRequirements[]` | Merged from rules + low-score conditions |

### SHAP feature → Reason code mapping (initial)

| Agri-Access SHAP feature | AGNET `ReasonCode.code` | Direction |
|--------------------------|-------------------------|-----------|
| `ndvi`, `vegetation_health` | `RC_NDVI_ANOMALY` | positive if above median |
| `predicted_yield`, `historical_yield` | `RC_YIELD_FORECAST` | positive |
| `rainfall_patterns`, `drought_index` | `RC_DROUGHT_INDEX` | negative if high risk |
| `farm_size`, `land_area` | `RC_LAND_AREA` | context-dependent |
| `loan_amount`, `dti_ratio` | `RC_DTI_PROXY` | negative if high |
| `repayment_history` | `RC_REPAYMENT_HISTORY` | positive |
| `document_quality` | `RC_DOC_QUALITY` | positive |

---

## To-Do List (Prioritized Backlog)

### P0 — Production Blockers (Integration)

| ID | Task | Owner | Depends On | Acceptance Criteria |
|----|------|-------|------------|---------------------|
| AA-001 | Fork `agri-access` into `mpgis-farmer-platform/services/agri-access-worker` | Backend | — | Submodule or vendored copy; MIT license preserved; CI builds Docker image |
| AA-002 | **Remove hardcoded secrets** — `GEMINI_API_KEY`, OpenWeather fallback key | Backend + DevOps | AA-001 | No API keys in source; load from env/vault; upstream keys rotated/revoked |
| AA-003 | Containerize Python worker (multi-stage, non-root) | DevOps | AA-001 | `docker build` succeeds; `/api/model-status` returns `ready` in <120s |
| AA-004 | Implement `AgriAccessScoringAdapter` in scoring-service | Backend | AA-003 | Unit tests for field mapping; mock worker responses |
| AA-005 | Extend `scoring.v1.yaml` with adapter contract | Backend | AA-004 | `POST /internal/score`, `ScoringJobRequest`, `ScoringJobResult` documented |
| AA-006 | Wire submit → async scoring job → status poll | Backend | AA-004 | E2E: submit onboarding → status transitions `scoring` → `completed` with real score |
| AA-007 | Uzbekistan score normalization (300–850 → 0–1000) + eligibility thresholds | Backend | AA-004 | Documented thresholds; matches `StepResult.tsx` `/1000` display |
| AA-008 | Replace SLIK with **CBU credit class** (1–5) in worker | Backend | AA-001 | `cbuClass` in response; SLIK references removed from UZ build |
| AA-009 | Uzbekistan crop enum mapping (cotton, wheat, rice, horticulture) | Backend | AA-004 | All `CROP_PLAN` reference values map without fallback to `rice` |
| AA-010 | Internal network isolation — worker not public | DevOps | AA-003 | Worker reachable only from scoring-service VPC/k8s namespace |
| AA-011 | Secrets vault integration (`GEMINI_API_KEY`, `OPENWEATHER_API_KEY`, worker auth token) | DevOps | AA-002, T-006 | Vault paths documented; no secrets in k8s manifests |

### P1 — Quality & UI Integration

| ID | Task | Owner | Depends On | Acceptance Criteria |
|----|------|-------|------------|---------------------|
| AA-012 | Enrich `StepScoring.tsx` — real phase signals from status payload | Frontend | AA-006 | Phases `geo`/`documents`/`scoring` driven by backend `scoringPhase` field |
| AA-013 | Enrich `StepResult.tsx` — Gemini farmer explanation block (uz/ru) | Frontend | AA-006, AA-020 | Explanation text shown; language matches `FARM_PROFILE.primaryLanguage` |
| AA-014 | SHAP top factors UI in StepScoring + StepResult | Frontend | AA-006 | Reuse reasonCodes; optional bar chart from `shap-visualization` data |
| AA-015 | Extend `FinanceModule.tsx` — Basel III summary (PD%, risk rating, IFRS 9) | Frontend | AA-006 | Cards for risk metrics when `finance.riskPd` present |
| AA-016 | BFF image proxy for satellite thumbnails | Frontend + BFF | AA-006 | `GET /api/scoring/satellite-image?url=…` avoids CORS; signed URLs |
| AA-017 | Extend `lib/scoring-reasons.ts` with Agri-Access SHAP features | Frontend | AA-006 | All mapped `RC_*` codes have uz/ru labels |
| AA-018 | OpenAPI: extend `StepScoringData` + `OnboardingStatusResponse` | Frontend + Backend | AA-005 | `satelliteSummary`, `farmerExplanation`, `scoringPhase` in spec |
| AA-019 | Contract tests: adapter ↔ worker | QA | AA-005 | Pact or schema validation on `/internal/score` |
| AA-020 | Gemini prompt localization (uz/ru, not id/en) | Backend | AA-008 | `/api/farmer/explain` accepts `language: uz \| ru \| en` |
| AA-021 | Uzbekistan climate fallback model (replace Indonesian) | Backend | AA-001 | `get_uzbekistan_climate_data()` for lat 37–46, lon 56–73 |
| AA-022 | Dev mock: optional `AGRI_ACCESS_USE_MOCK` for local stack | Backend | AA-004 | `dev:stack` runs without GPU; mock returns in 2s |

### P2 — Feature Enhancements

| ID | Task | Owner | Depends On | Acceptance Criteria |
|----|------|-------|------------|---------------------|
| AA-023 | Port `credit-score-arc.js` gauge to React component | Frontend | AA-007 | Arc gauge on StepResult; accessible; matches design system |
| AA-024 | Port `shap-visualization.js` to React (recharts/d3) | Frontend | AA-014 | Interactive SHAP beeswarm or bar in modal |
| AA-025 | CBU credit bureau API integration (when available) | Backend | AA-008, W-007 | Real bureau pull replaces synthetic repayment weight |
| AA-026 | Multi-parcel scoring (weighted by area) | Backend | W-016 | Score aggregates per-parcel Prithvi features |
| AA-027 | Officer/admin SHAP deep-dive view | Backend | T-018 | Full SHAP payload in admin portal |
| AA-028 | Model retraining pipeline with UZ labeled data | ML | AA-009 | Retrained model version tagged; A/B against rules baseline |
| AA-029 | Scoring audit log (immutable) | Backend | AA-006 | `ScoringAudit` record: inputs hash, model version, outputs |
| AA-030 | Dashboard finance: satellite imagery carousel | Frontend | AA-016 | Thumbnails in FinanceModule when available |

### P3 — Scale & Operations

| ID | Task | Owner | Depends On | Acceptance Criteria |
|----|------|-------|------------|---------------------|
| AA-031 | GPU node pool for Prithvi inference (optional) | DevOps | AA-003 | HPA on worker; CPU fallback documented |
| AA-032 | Scoring SLA monitoring (p95 < 45s) | DevOps | AA-006 | Datadog/Prometheus alerts on job duration |
| AA-033 | Worker horizontal scaling + job queue (Redis/BullMQ) | Backend | AA-006 | 10 concurrent jobs; no duplicate scoring |
| AA-034 | Model version pinning + rollback | DevOps | AA-028 | `MODEL_VERSION` env; rollback in <5 min |
| AA-035 | Rate limit Gemini explain calls | Backend | AA-020 | Max 1 explain per onboarding session |
| AA-036 | Disaster fallback to rules-only scoring | Backend | AA-006 | Worker down → rules engine score; flag `scoringSource: rules_fallback` |

---

## Detailed Project Plan

### Phase 4 — GIS & Scoring Integration ⏳ PLANNED (parent)

**Duration:** Weeks 13–16  
**Goal:** Real geospatial enrichment and ML scoring.  
**Status:** Milestones M4.1–M4.6 defined in [`PROJECT_PLAN.md`](./PROJECT_PLAN.md); Agri-Access integration deferred to Phase 4A.

| Milestone | Deliverables | Tasks | Status |
|-----------|--------------|-------|--------|
| M4.1 | Interactive GIS map | T-013 | ⏳ Planned |
| M4.2 | NDVI/SATAI enrichment pipeline | Backend gis-service | ⏳ Planned |
| M4.3 | Reason code transparency | W-012 | ✅ Partial (labels exist) |
| M4.4 | Institution case routing | FinanceCase adapters | ⏳ Planned |
| M4.5 | Wizard P1 enrichment | W-016–W-027 | ⏳ Planned |
| M4.6 | Multi-parcel + polygon GIS | W-016, W-017, T-013 | ⏳ Planned |

**Phase 4 exit criteria (unchanged):** GIS map live; NDVI enrichment; scoring returns real reason codes; institution routing creates FinanceCase.

---

### Phase 4A — Agri-Access Scoring Worker Integration ⏳ PLANNED

**Duration:** Weeks 13–18 (overlaps Phase 4; extends scoring track)  
**Goal:** Production-grade ML scoring via localized Agri-Access worker, NestJS adapter, and enriched farmer UI.

| Milestone | Deliverables | Tasks | Status |
|-----------|--------------|-------|--------|
| M4A.1 | Security remediation & worker fork | AA-001, AA-002, AA-011 | ⏳ Not started |
| M4A.2 | Dockerized agri-access-worker | AA-003, AA-010 | ⏳ Not started |
| M4A.3 | scoring-service adapter + OpenAPI | AA-004, AA-005, AA-019 | ⏳ Not started |
| M4A.4 | Uzbekistan localization (CBU, crops, climate, uz/ru Gemini) | AA-008, AA-009, AA-020, AA-021 | ⏳ Not started |
| M4A.5 | Async scoring pipeline E2E | AA-006, AA-007, AA-022 | ⏳ Not started |
| M4A.6 | StepScoring + StepResult UI enrichment | AA-012, AA-013, AA-014, AA-017, AA-018 | ⏳ Not started |
| M4A.7 | Finance dashboard Basel III display | AA-015, AA-030 | ⏳ Not started |
| M4A.8 | Observability + fallback | AA-032, AA-033, AA-036 | ⏳ Not started |

**Dependencies:**

- Phase 4 M4.2 (NDVI enrichment) — soft dependency; worker can run without pre-computed NDVI
- W-006 (UZS finance fields) — **hard dependency** for DTI accuracy
- W-007 (credit bureau consent) — hard dependency before AA-025
- T-006 (secrets vault) — hard dependency for AA-002, AA-011

**Exit criteria:**

- No hardcoded API keys in any deployed artifact
- Submit → score → result E2E passes with real worker (or documented mock for dev)
- `StepScoring.tsx` shows live phase progression from backend
- `StepResult.tsx` displays score /1000, top 3 SHAP reasons, Gemini explanation in farmer language
- `FinanceModule.tsx` shows PD% and risk rating when case exists
- `scoring.v1.yaml` + `agri-access-adapter.v1.yaml` published and CI-validated

**Estimated effort — Phase 4A:** ~35 dev-days (backend 18d, DevOps 6d, frontend 8d, QA 3d)

---

### Phase 5 — Localization & PWA ⏳ PLANNED (unchanged)

Agri-Access Gemini explanations and SHAP labels must align with Phase 5 i18n (W-015, W-026). Phase 4A delivers uz/ru for scoring-specific strings early; Phase 5 generalizes framework.

---

## OpenAPI Extension Notes (scoring-service adapter)

Create `specs/openapi/agri-access-adapter.v1.yaml` and extend `scoring.v1.yaml` in backend repo.

### New endpoints (internal — scoring-service only)

```yaml
# scoring.v1.yaml additions
POST /internal/score:
  summary: Submit scoring job to Agri-Access worker
  requestBody:
    $ref: '#/components/schemas/AgriAccessScoreRequest'
  responses:
    '202':
      description: Job accepted
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ScoringJobAccepted'
    '503':
      description: Worker unavailable — trigger rules fallback (AA-036)

GET /internal/score/{jobId}:
  summary: Poll worker job status (used by scoring-service, not BFF)
  responses:
    '200':
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/AgriAccessScoreResult'
```

### `AgriAccessScoreRequest` (canonical)

```yaml
AgriAccessScoreRequest:
  type: object
  required:
    - onboardingSessionId
    - countryCode
    - farmer
    - parcel
    - finance
    - consent
  properties:
    onboardingSessionId:
      type: string
      format: uuid
    countryCode:
      type: string
      enum: [UZ]
    farmer:
      type: object
      properties:
        fullName: { type: string }
        dateOfBirth: { type: string, format: date }
        yearsFarming: { type: integer }
        primaryLanguage: { type: string, enum: [uz, ru, en] }
    parcel:
      type: object
      properties:
        latitude: { type: number }
        longitude: { type: number }
        areaHa: { type: number }
        primaryCrop: { type: string }
        ndviSnapshot: { type: object, additionalProperties: true }
    finance:
      type: object
      properties:
        amountUzs: { type: number }
        termMonths: { type: integer }
        purpose: { type: string }
        monthlyIncomeUzs: { type: number }
        existingLoansUzs: { type: number }
        repaymentHistory: { type: string }
    collateral:
      type: object
      properties:
        assetType: { type: string }
    consent:
      type: object
      properties:
        creditBureau: { type: boolean }
        gisSatellite: { type: boolean }
    idempotencyKey:
      type: string
      description: onboardingSessionId + submit timestamp hash
```

### `AgriAccessScoreResult` → BFF `OnboardingStatusResponse` mapping

Extend existing BFF schema (`bff-api.v1.yaml` → `OnboardingStatusResponse`):

```yaml
OnboardingStatusResponse:
  properties:
    status:
      type: string
      enum: [submitted, scoring, completed, failed]
    scoringPhase:
      type: string
      enum: [geo, documents, scoring, finalize]
      description: Drives StepScoring phase UI (AA-012)
    score:
      type: integer
      minimum: 0
      maximum: 1000
    eligibilityOutcome:
      $ref: './components/enums.yaml#/components/schemas/EligibilityOutcome'
    reasonCodes:
      type: array
      items:
        $ref: './components/onboarding-steps.yaml#/components/schemas/ReasonCode'
    financeCaseId:
      type: string
    financeCaseStatus:
      $ref: './components/enums.yaml#/components/schemas/FinanceCaseStatus'
    conditionalRequirements:
      type: array
      items:
        $ref: './components/onboarding-steps.yaml#/components/schemas/ConditionalRequirement'
    scoringMeta:
      type: object
      properties:
        modelVersion: { type: string }
        scoringSource: { type: string, enum: [agri_access, rules_fallback] }
        cbuClass: { type: integer, minimum: 1, maximum: 5 }
        baselPd: { type: number }
        riskRating: { type: string }
        ifrs9Stage: { type: integer }
        farmerExplanation:
          type: object
          properties:
            uz: { type: string }
            ru: { type: string }
            en: { type: string }
        satelliteSummary: { type: string }
        processingDurationMs: { type: integer }
```

### Worker auth

All `agri-access-worker` calls include `Authorization: Bearer ${AGRI_ACCESS_WORKER_TOKEN}` — validated by Flask middleware added in AA-001. Token issued by scoring-service from vault; rotated quarterly.

---

## Microservice Deployment Architecture

| Component | Runtime | Port | Scaling | Notes |
|-----------|---------|------|---------|-------|
| `core-api` | NestJS | 4000 | HPA 2–8 | Orchestrates submit; owns OnboardingSession |
| `scoring-service` | NestJS | 4001 | HPA 2–4 | Adapter, job queue, status aggregation |
| `gis-service` | NestJS | 4002 | HPA 2–4 | NDVI enrichment pre-score |
| `agri-access-worker` | Python/Flask | 5001 | HPA 1–4 (GPU optional) | **Internal only**; Prithvi + ML + SHAP |
| Redis | — | 6379 | Cluster | BullMQ scoring job queue (AA-033) |

**Deployment topology (production):**

```
Internet → ALB → agnet BFF (Next.js)
                → core-api (public)
core-api → scoring-service (private subnet)
scoring-service → agri-access-worker (private subnet, no public IP)
scoring-service → gis-service (private)
agri-access-worker → NASA GIBS, OpenWeather (egress)
agri-access-worker → Gemini API (egress, key from vault)
```

**Local dev (`npm run dev:stack` extension):**

```bash
# Add to scripts/start-dev.sh
docker compose up agri-access-worker -d   # port 5001, mock Prithvi if no GPU
export SCORING_SERVICE_URL=http://localhost:4001
export AGRI_ACCESS_WORKER_URL=http://localhost:5001
export AGRI_ACCESS_USE_MOCK=true          # AA-022: skip GPU inference locally
```

**NestJS vs Python boundary:**

| Concern | NestJS (scoring-service) | Python (agri-access-worker) |
|---------|--------------------------|----------------------------|
| Session / auth | ✅ | ❌ |
| Field mapping / validation | ✅ | ❌ |
| Job queue / retry | ✅ | ❌ |
| Prithvi inference | ❌ | ✅ |
| ML ensemble + SHAP | ❌ | ✅ |
| Basel III math | ❌ | ✅ |
| Gemini prompts | ❌ | ✅ |
| FinanceCase creation | ✅ | ❌ |
| Rules engine fallback | ✅ | ❌ |

---

## UI Integration Points

### `StepScoring.tsx` (Step 12 — SCORING)

| Area | Current behavior | Phase 4A change | Task |
|------|------------------|-----------------|------|
| Phase progression | Client-side timer (`attempts % 3`) | Backend-driven `scoringPhase` from status poll | AA-012 |
| Poll endpoint | `GET /api/onboarding/{id}/status` | Same; enriched payload | AA-006 |
| Top reasons | `reasonCodes.slice(0, 3)` on complete | Same; populated from real SHAP | AA-014 |
| Error handling | Sets `scoringError: true` after MAX_POLLS | Distinguish `failed` vs timeout; retry CTA | AA-012 |
| Satellite phase label | "SATAI satellite analysis" | "Sun'iy yo'ldosh tahlili (Prithvi)" i18n | AA-017 |
| New: explanation preview | — | Collapsible Gemini summary when complete | AA-013 |
| New: satellite thumb | — | Optional 1×2 image strip from `satelliteImages` | AA-016 |

### `StepResult.tsx` (Step 13 — RESULT)

| Area | Current behavior | Phase 4A change | Task |
|------|------------------|-----------------|------|
| Score display | `/1000` from status | Real normalized score from worker | AA-007 |
| Reason codes | Up to 4 with `getReasonLabel` | SHAP-mapped codes + contributions | AA-014 |
| Finance case | `financeCaseId`, `financeCaseStatus` | Same; created post-score | AA-006 |
| Conditional reqs | Amber panel | Merge rules + CBU conditions | AA-006 |
| New: farmer explanation | — | Gemini block in uz/ru below score | AA-013 |
| New: credit gauge | — | Optional arc component (AA-023) | P2 |
| New: Basel summary | — | PD%, risk rating chips (officer-facing tooltip) | AA-015 |

### `FinanceModule.tsx` (Dashboard — `/farmers/dashboard/finance`)

| Area | Current behavior | Phase 4A change | Task |
|------|------------------|-----------------|------|
| Case progress | Static stages array | Add "Credit scoring complete" stage from `scoringMeta` | AA-015 |
| Info cards | Purpose, amount, income, repayment | Add PD%, CBU class, risk rating | AA-015 |
| Empty state | No finance request message | Unchanged | — |
| New: satellite carousel | — | Thumbnail strip when images in case metadata | AA-030 |
| New: explanation link | — | "Why this score?" expands Gemini text | AA-013 |

---

## Uzbekistan Localization Tasks

| ID | Indonesian (Agri-Access) | Uzbekistan (AGNET) | Implementation |
|----|---------------------------|---------------------|----------------|
| LOC-01 | SLIK 1–5 collectibility | **CBU / Kredit byurosi class 1–5** | Rename `slik_score` → `cbuClass`; update labels in worker |
| LOC-02 | SLIK descriptions (Lancar, Macet…) | UZ: *Silliq, Nazorat ostida, Shubhali…* RU: Cyrillic equivalents | `banking_credit_model.py` locale dict |
| LOC-03 | KUR interest rates | Uzbekistan Mikroqarz / bank pilot rates (configurable) | `CountryProfile.scoringRates` in backend |
| LOC-04 | IDR formatting (`format_currency_idr`) | UZS formatting (`format_currency_uzs`) | Worker + BFF locale |
| LOC-05 | Indonesian crop types (rice, palm oil) | cotton, wheat, rice, horticulture, silkworm | Reference seed + AA-009 mapping |
| LOC-06 | Java/Sumatra NPL regional factors | Fergana, Tashkent, Karakalpakstan regional weights | AA-021 climate + regional tables |
| LOC-07 | Gemini prompts (id/en) | uz/ru/en farmer explanations | AA-020 |
| LOC-08 | OpenWeather Indonesian fallback | Uzbekistan climate model (lat 37–46) | AA-021 |
| LOC-09 | Demo farmers (West Java, Sumatra) | Demo parcels (Fergana, Samarkand, Khorezm) | Seed data for QA |
| LOC-10 | Credit bureau: SLIK API (future) | **CBU / Credit Register** integration stub | AA-025; consent-gated |

---

## Security Remediation Tasks

| ID | Finding (upstream agri-access) | Severity | Remediation | Task |
|----|-------------------------------|----------|-------------|------|
| SEC-01 | Hardcoded `GEMINI_API_KEY` in `basel_iii_api.py` L61 | **Critical** | Remove; load `os.environ['GEMINI_API_KEY']`; rotate exposed key immediately | AA-002 |
| SEC-02 | Hardcoded OpenWeather fallback `'fc7cbfed…'` L123 | **High** | Remove default; fail gracefully to UZ climate model | AA-002 |
| SEC-03 | CORS `origins=["*"]` on Flask | **Medium** | Restrict to scoring-service internal origin | AA-010 |
| SEC-04 | No auth on `/api/analyze` | **High** | Bearer token middleware | AA-001 |
| SEC-05 | SHAP/Gemini payloads may contain PII | **Medium** | Redact in logs; encrypt at rest in ScoringAudit | AA-029 |
| SEC-06 | Satellite image proxy open redirect | **Medium** | Allowlist NASA/GIBS domains only in BFF proxy | AA-016 |
| SEC-07 | Worker runs as root in default Python image | **Medium** | Non-root user in Dockerfile | AA-003 |
| SEC-08 | Gemini explain rate abuse | **Low** | 1 request per session; cache by input hash | AA-035 |

---

## API Surface Summary

### External (unchanged BFF — farmer-facing)

```
POST   /api/onboarding/{id}/submit          → triggers scoring job
GET    /api/onboarding/{id}/status          → poll enriched status (scoringPhase, score, reasonCodes, scoringMeta)
GET    /api/farmer/dashboard                → finance module includes riskPd, cbuClass when scored
GET    /api/scoring/satellite-image?url=…   → NEW (AA-016) proxied thumbnails
```

### Internal (scoring-service ↔ agri-access-worker)

```
GET    /api/model-status                    → worker health (used by k8s probes)
POST   /api/analyze                         → main scoring (mapped from AgriAccessScoreRequest)
POST   /api/farmer/explain                  → Gemini explanation (uz/ru/en)
POST   /api/shap-visualization/{type}       → SHAP chart data for admin/deep-dive
GET    /api/proxy-image                     → DEPRECATED in favor of BFF proxy (AA-016)
```

### Upstream Agri-Access (reference — pre-integration)

```
POST   /api/analyze                         → basel_iii_api.py (Indonesia demo)
GET    /api/model-status
POST   /api/farmer/explain
POST   /api/shap-visualization/{output_type}
GET    /api/test
```

---

## Environment Variables

| Variable | Required | Default | Component | Purpose |
|----------|----------|---------|-----------|---------|
| `AGRI_ACCESS_WORKER_URL` | Yes (prod) | `http://localhost:5001` | scoring-service | Worker base URL (internal) |
| `AGRI_ACCESS_WORKER_TOKEN` | Yes (prod) | — | scoring-service + worker | Mutual auth bearer token |
| `AGRI_ACCESS_USE_MOCK` | No | `false` | scoring-service | Skip worker; return mock score (dev) |
| `GEMINI_API_KEY` | Yes (prod) | — | agri-access-worker | Gemini explanations — **vault only** |
| `OPENWEATHER_API_KEY` | No | — | agri-access-worker | Weather features; omit → UZ climate fallback |
| `PRITHVI_MODEL_CACHE_DIR` | No | `/models/prithvi` | agri-access-worker | Foundation model weights cache |
| `SCORING_SCORE_MIN` | No | `300` | agri-access-worker | Raw score floor (maps to 0) |
| `SCORING_SCORE_MAX` | No | `850` | agri-access-worker | Raw score ceiling (maps to 1000) |
| `SCORING_ELIGIBLE_MIN` | No | `650` | scoring-service | Raw score threshold → eligible |
| `SCORING_CONDITIONAL_MIN` | No | `550` | scoring-service | Raw score threshold → conditional |
| `REDIS_URL` | Yes (prod) | — | scoring-service | BullMQ job queue (AA-033) |
| `CORE_API_URL` | Yes | `http://localhost:4000` | Agnet BFF | Unchanged |
| `SCORING_SERVICE_URL` | Yes | `http://localhost:4001` | core-api | Scoring orchestration |

---

## Verification Checklist

Use this checklist to validate Agri-Access integration against the running system:

### Security (P0 — must pass before any deploy)

- [ ] `grep -r "AIzaSy" services/agri-access-worker/` returns no matches
- [ ] `grep -r "fc7cbfed" services/agri-access-worker/` returns no matches
- [ ] Worker pod has no public ingress; only scoring-service SA can reach port 5001
- [ ] `GEMINI_API_KEY` loaded from vault; not in k8s ConfigMap
- [ ] Exposed upstream Gemini key rotated in Google Cloud Console

### Worker health

- [ ] `GET /api/model-status` returns `{ "model_ready": true }` within 120s of container start
- [ ] `POST /api/analyze` with valid bearer token returns `success: true` in <45s (p95)
- [ ] `POST /api/analyze` without token returns `401`

### Adapter & pipeline

- [ ] Submit onboarding → `status` transitions `submitted` → `scoring` → `completed`
- [ ] `OnboardingStatusResponse.score` is integer 0–1000
- [ ] `reasonCodes` length ≥ 1 with valid `RC_*` codes after real score
- [ ] `scoringMeta.modelVersion` matches worker `MODEL_VERSION`
- [ ] `scoringMeta.scoringSource` is `agri_access` (not `rules_fallback`) when worker healthy
- [ ] Idempotent re-submit does not create duplicate scoring jobs

### Field mapping

- [ ] Fergana demo parcel (lat 40.38, lon 71.78) returns score without crop fallback warning
- [ ] `amountUzs` correctly passed; response EAD formatted in UZS not IDR
- [ ] `yearsFarming` from FARM_PROFILE used (not mock 15)
- [ ] `cbuClass` present (1–5); no `slik_score` in API responses

### UI — StepScoring.tsx

- [ ] Phase indicators advance when `scoringPhase` changes (not only on timer)
- [ ] Top 3 reason codes displayed with uz labels after completion
- [ ] Timeout after MAX_POLLS shows actionable error (not silent failure)
- [ ] `scoringComplete: true` persisted to `stepData.SCORING` on success

### UI — StepResult.tsx

- [ ] Score displayed as `/1000` matches normalized worker output
- [ ] Gemini `farmerExplanation` shown in farmer's `primaryLanguage`
- [ ] Conditional requirements panel appears when `eligibilityOutcome === conditional`
- [ ] `financeCaseId` displayed when present

### UI — FinanceModule.tsx

- [ ] Dashboard finance shows PD% and risk rating after scored onboarding
- [ ] Application progress includes completed scoring stage
- [ ] Empty state unchanged for farmers without finance request

### Localization

- [ ] Gemini explanation available in uz and ru
- [ ] Reason codes have uz/ru labels in `lib/scoring-reasons.ts`
- [ ] No Indonesian SLIK strings visible in farmer UI

### Fallback & ops

- [ ] Worker stopped → submit still completes with `scoringSource: rules_fallback`
- [ ] `npm run test:e2e:wizard` passes with `AGRI_ACCESS_USE_MOCK=true`
- [ ] OpenAPI validation passes for `agri-access-adapter.v1.yaml` in CI

---

## Risk Register

| Risk | Impact | Likelihood | Mitigation |
|------|--------|------------|------------|
| Exposed Gemini API key abused before rotation | High | High | AA-002 immediately; revoke key; audit usage |
| Prithvi inference too slow for UX (>60s) | High | Medium | AA-033 queue; AA-012 honest progress; pre-warm model; GPU nodes (AA-031) |
| Indonesia-trained model inaccurate for UZ cotton/wheat | High | High | AA-009 crop mapping; AA-028 retraining; rules fallback (AA-036) |
| Worker single point of failure | High | Medium | AA-036 rules fallback; AA-033 horizontal scaling |
| SHAP/Gemini outputs hallucinate farmer advice | Medium | Medium | AA-020 templated prompts; human review for conditional cases |
| CBU bureau API unavailable at launch | Medium | High | AA-025 deferred; synthetic repayment weight from FINANCE_REQUEST |
| Multi-parcel farmers scored on primary only | Medium | Medium | AA-026 multi-parcel weighting (P2) |
| BFF spec drift from enriched status payload | Medium | High | AA-018 OpenAPI first; AA-019 contract tests |
| GPU cost overrun | Medium | Low | CPU fallback documented; AA-031 optional GPU pool |
| PII in scoring audit logs | High | Low | SEC-05 redaction; AA-029 immutable audit with retention policy |
| Farmer distrust of "black box" AI score | High | Medium | AA-013 Gemini explain; AA-014 SHAP factors; W-012 plain language |

---

## Next Immediate Actions (Sprint 1 — Phase 4A Kickoff)

1. **AA-002** — Remove hardcoded secrets from forked worker; rotate Gemini + OpenWeather keys (0.5 day) — **do first**
2. **AA-001** — Fork agri-access into backend monorepo; add Bearer auth middleware (1 day)
3. **AA-003** — Dockerfile + docker-compose service; `/api/model-status` healthy (1 day)
4. **AA-005** — Draft `agri-access-adapter.v1.yaml` + extend `scoring.v1.yaml` (1 day)
5. **AA-004** — Implement `AgriAccessScoringAdapter` with field mapping table above (3 days)
6. **AA-008** — CBU class localization; remove SLIK from UZ build (1 day)
7. **AA-022** — Dev mock flag for local stack without GPU (0.5 day)

**Sprint 2 (Pipeline + UI):** AA-006, AA-007, AA-009, AA-012, AA-013, AA-017, AA-018, AA-021

**Sprint 3 (Dashboard + hardening):** AA-014, AA-015, AA-016, AA-019, AA-010, AA-011, AA-032, AA-036

---

*This document extends [`PROJECT_PLAN.md`](./PROJECT_PLAN.md) Phase 4 with Phase 4A Agri-Access integration. Update task statuses as work completes. Parent plan remains authoritative for non-scoring workstreams.*
