# Dashboard v2 — Prisma / DB Delta

**Contract:** `specs/contracts/dashboard-v2.openapi.yaml`  
**Owner:** Agent F (`feat/core-dashboard-v2` in `mpgis-farmer-platform`)  
**Fixture source:** `Agnet/lib/fixtures/dashboard-mockup-personas.ts` (copy field values verbatim into seed)

---

## 1. Summary

Dashboard v2 enriches `GET /farmer/dashboard` with Identity, Finance, and Inputs module fields. This document defines the **database changes** required in `packages/database/prisma/schema.prisma` and the mapping to API response fields.

**Sync rule:** Seed values for the three demo personas (`usr-demo-islomjon-001`, `usr-demo-agrobank-002`, `usr-demo-burkina-003`) must match `dashboard-mockup-personas.ts` field-for-field.

---

## 2. Existing models — column additions

### 2.1 `FarmerProfile`

| Column | Type | Nullable | API field | Notes |
|--------|------|----------|-----------|-------|
| `publicFarmerId` | `String` | yes | `identity.farmerId` | Unique, format `AGID-FMR-*` |
| `displayName` | `String` | yes | `identity.farmName` | Farm / display name |

```prisma
model FarmerProfile {
  // existing fields...
  publicFarmerId String? @unique
  displayName    String?

  @@index([publicFarmerId])
}
```

### 2.2 `KycRecord` (new model or extend onboarding step persist)

KYC data currently lives in `OnboardingSession.stepData.KYC`. Agent F should either:

- **Option A (preferred):** Add normalized `KycRecord` table linked to `User`
- **Option B:** Read from `stepData` JSON at dashboard enrich time

| Column | Type | API field | Notes |
|--------|------|-----------|-------|
| `photoUrl` | `String?` | `identity.ekyc.photoUrl` | Never return raw PINFL |
| `dob` | `DateTime?` | `identity.ekyc.dob` | ISO date in response |
| `pinflMasked` | `String?` | `identity.ekyc.aadhaarMasked` | Masked only |
| `status` | `KycStatus` | `identity.kycStatus` | Existing enum |

```prisma
model KycRecord {
  id        String    @id @default(cuid())
  userId    String    @unique
  photoUrl  String?
  dob       DateTime?
  pinflMasked String?
  status    KycStatus @default(pending)
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([userId])
}
```

### 2.3 `LandParcel` (existing)

| Column | Type | API field | Notes |
|--------|------|-----------|-------|
| `geometry` | PostGIS `geometry(Polygon,4326)` | `identity.gis.polygonGeoJson` | Serialize to GeoJSON on read |
| `areaHa` | `Float?` | `identity.gis.areaHa` | Already exists |
| — | computed centroid | `identity.gis.lat`, `identity.gis.lng` | `ST_Centroid(geometry)` |

No schema change required for geometry (already `Unsupported("geometry(Polygon, 4326)")`). Add mapper util in core-api.

### 2.4 `Document` (existing)

Extend dashboard mapper to filter by `documentType` and map:

| DB | API |
|----|-----|
| `documentType` + `s3Key` / display name | `identity.landEvidence[].name` |
| file extension | `identity.landEvidence[].type` (`pdf` \| `doc`) |
| `qualityScore` or verification flag | `identity.landEvidence[].verified` |

Consider adding `verificationStatus` enum column if not inferrable from OCR.

### 2.5 `FinanceCase` (existing)

| Column | Type | API field | Notes |
|--------|------|-----------|-------|
| `slaDeadline` | `DateTime?` | `finance.sla.dueAt` | **Already exists** as `slaDeadline` |
| `routedScheme` | `String?` | `finance.routedInstitution.name` | Map via `InstitutionProfile` |
| — | new JSON or relation | `finance.statusHistory` | For `stages[]` |

Rename mapping: `slaDeadline` → API `sla.dueAt`; compute `remainingHours` / `totalHours` in mapper.

---

## 3. New models

### 3.1 `YieldHistory`

```prisma
model YieldHistory {
  id                  String   @id @default(cuid())
  onboardingSessionId String
  landParcelId        String?
  year                Int
  yieldTHa            Float
  crop                String
  createdAt           DateTime @default(now())
  updatedAt           DateTime @updatedAt

  onboardingSession OnboardingSession @relation(fields: [onboardingSessionId], references: [id], onDelete: Cascade)
  landParcel        LandParcel?       @relation(fields: [landParcelId], references: [id], onDelete: SetNull)

  @@index([onboardingSessionId])
  @@index([landParcelId])
  @@index([year])
}
```

### 3.2 `EligibilityRuleResult` (or read from `ScoringRun.rulesResult` JSON)

If not already normalized:

```prisma
model EligibilityRuleResult {
  id                  String   @id @default(cuid())
  scoringRunId        String
  ruleKey             String
  label               String
  passed              Boolean
  createdAt           DateTime @default(now())
  updatedAt           DateTime @updatedAt

  scoringRun ScoringRun @relation(fields: [scoringRunId], references: [id], onDelete: Cascade)

  @@index([scoringRunId])
  @@unique([scoringRunId, ruleKey])
}
```

Maps to `identity.compliance[]`.

### 3.3 `InstitutionMatch`

```prisma
model InstitutionMatch {
  id            String   @id @default(cuid())
  financeCaseId String
  institutionId String
  matchScore    Float
  status        String   // routed | view
  createdAt     DateTime @default(now())
  updatedAt     DateTime @updatedAt

  financeCase FinanceCase @relation(fields: [financeCaseId], references: [id], onDelete: Cascade)

  @@index([financeCaseId])
}
```

Maps to `finance.bankMatches[]`.

### 3.4 `InputVoucher`

```prisma
model InputVoucher {
  id                  String   @id @default(cuid())
  onboardingSessionId String
  code                String   @unique
  amount              Float
  currency            String   @default("UZS")
  status              String   // active | redeemed | expired
  paymentRoute        Json     // VoucherPaymentRoute[]
  createdAt           DateTime @default(now())
  updatedAt           DateTime @updatedAt

  onboardingSession OnboardingSession @relation(fields: [onboardingSessionId], references: [id], onDelete: Cascade)

  @@index([onboardingSessionId])
}
```

### 3.5 `DeliveryShipment`

```prisma
model DeliveryShipment {
  id                  String   @id @default(cuid())
  onboardingSessionId String
  trackingId          String   @unique
  status              String   // pending | in_transit | delivered
  eta                 DateTime?
  steps               Json     // DeliveryStep[]
  createdAt           DateTime @default(now())
  updatedAt           DateTime @updatedAt

  onboardingSession OnboardingSession @relation(fields: [onboardingSessionId], references: [id], onDelete: Cascade)

  @@index([onboardingSessionId])
}
```

### 3.6 `Supplier` + `ProductCatalog` (inputs module)

Minimal v1 for dashboard enrich:

```prisma
model Supplier {
  id        String   @id @default(cuid())
  name      String
  logoUrl   String?
  rating    Float
  countryCode String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([countryCode])
}

model ProductCatalog {
  id          String   @id @default(cuid())
  category    String
  name        String
  unitPrice   Float
  currency    String   @default("UZS")
  imageUrl    String?
  countryCode String
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

  @@index([category])
  @@index([countryCode])
}
```

`CatalogAggregate` can be a **view** or computed in the dashboard mapper from `ProductCatalog` + order tables.

---

## 4. Scoring normalization

| DB | API | Transform |
|----|-----|-----------|
| `ScoringRun.totalScore` (0–1000) | `finance.eligibilityScore` | `Math.round(totalScore / 10)` clamped 0–100 |
| `ScoringRun.decision` | top-level `eligibility` | existing |
| `ScoringAudit.cbuClass` | `finance.cbuClass` | existing |

---

## 5. Seed plan (`seed/dashboard-demo.ts`)

Create three users matching fixture IDs:

| Persona | `userId` | Phone | Module emphasis |
|---------|----------|-------|-----------------|
| islomjon | `usr-demo-islomjon-001` | `+998901110001` | Identity — GIS + eKYC |
| agrobank-case | `usr-demo-agrobank-002` | `+998901110002` | Finance — 78/100, Agrobank |
| burkina-inputs | `usr-demo-burkina-003` | `+998901110003` | Inputs — catalog + voucher |

```typescript
// Pseudocode — implement in Agent F branch
import { DASHBOARD_MOCKUP_PERSONAS } from "../../../Agnet/lib/fixtures/dashboard-mockup-personas";

await seedDashboardPersona("islomjon", DASHBOARD_MOCKUP_PERSONAS.islomjon);
await seedDashboardPersona("agrobankCase", DASHBOARD_MOCKUP_PERSONAS.agrobankCase);
await seedDashboardPersona("burkinaInputs", DASHBOARD_MOCKUP_PERSONAS.burkinaInputs);
```

**CI contract test:** Compare `GET /farmer/dashboard` response for seed users ↔ `dashboard-mockup-personas.ts` (deep equal on module blocks).

---

## 6. PII / security

- Never return raw PINFL, full phone, or unmasked national ID in dashboard response.
- `identity.ekyc.aadhaarMasked` and `identity.ekyc.mobileMasked` only.
- `photoUrl` may be a signed URL or static demo path in dev.

---

## 7. Migration order

1. Add nullable columns to `FarmerProfile` (`publicFarmerId`, `displayName`)
2. Create `KycRecord`, `YieldHistory`, `InstitutionMatch`, `InputVoucher`, `DeliveryShipment`
3. Create `Supplier`, `ProductCatalog` (or defer inputs to mock-only if F is split)
4. Run `prisma migrate deploy` + `db:seed`
5. Implement core-api dashboard enrich mapper
6. Agnet `test:contract` dashboard-v2 tests go green against live core-api

---

*G0 deliverable — no Prisma migration in this PR. Agent F implements per this delta.*
