openapi: 3.1.0
info:
  title: AGNET Uzbekistan BFF API
  version: 1.0.0
  description: |
    Next.js Backend-for-Frontend (BFF) layer for the AGNET farmer platform.

    This API is consumed by the browser via `lib/api-client.ts` with `credentials: same-origin`.
    All authenticated routes require the httpOnly cookie `agnet_token` set by `POST /auth/verify`.

    ## Architecture

    ```
    Browser → /api/* (this spec) → CORE_API_URL (NestJS core-api :4000) → PostgreSQL/PostGIS
    ```

    When `CORE_API_USE_MOCK=true`, routes fall back to in-memory handlers in `lib/dev-api-mock.ts`.

    ## Related specifications

    - Core API: `../mpgis-farmer-platform/packages/api-contracts/openapi/core-api.v1.yaml`
    - GIS service: `gis.v1.yaml`
    - Scoring service: `scoring.v1.yaml`
    - Documents service: `documents.v1.yaml`
    - Rules engine: `rules.v1.yaml`

  contact:
    name: AGNET Platform Team

servers:
  - url: https://agnet.global/api
    description: Production
  - url: http://localhost:3000/api
    description: Local development

tags:
  - name: Auth
    description: OTP authentication and session management
  - name: Onboarding
    description: 13-step farmer onboarding wizard
  - name: Documents
    description: Document upload during onboarding
  - name: Farmer
    description: Post-onboarding farmer dashboard data
  - name: Reference
    description: Public reference/lookup data

paths:
  /auth/otp:
    post:
      operationId: requestOtp
      tags: [Auth]
      summary: Request OTP for phone authentication
      description: Sends a one-time password to the farmer's phone number.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OtpRequest"
      responses:
        "200":
          description: OTP dispatched (BFF normalizes upstream 202 to 200)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OtpSentResponse"
        "400":
          $ref: "./components/errors.yaml#/components/responses/BadRequest"
        "503":
          $ref: "./components/errors.yaml#/components/responses/ServiceUnavailable"

  /auth/verify:
    post:
      operationId: verifyOtp
      tags: [Auth]
      summary: Verify OTP and establish session
      description: |
        Validates the OTP code. On success, sets httpOnly `agnet_token` cookie (7-day max-age)
        and returns the user object. The `accessToken` is **not** exposed to the client.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OtpVerifyRequest"
      responses:
        "200":
          description: Authentication successful; session cookie set
          headers:
            Set-Cookie:
              description: httpOnly JWT in `agnet_token`
              schema:
                type: string
                example: agnet_token=eyJ...; Path=/; HttpOnly; SameSite=Lax; Max-Age=604800
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VerifyResponse"
        "401":
          $ref: "./components/errors.yaml#/components/responses/Unauthorized"
        "503":
          $ref: "./components/errors.yaml#/components/responses/ServiceUnavailable"

  /auth/me:
    get:
      operationId: getCurrentUser
      tags: [Auth]
      summary: Get authenticated user and active onboarding session
      description: Returns user profile, KYC status, and any in-progress onboarding session.
      security:
        - cookieAuth: []
      responses:
        "200":
          description: Current user
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserMeResponse"
        "401":
          $ref: "./components/errors.yaml#/components/responses/Unauthorized"
        "503":
          $ref: "./components/errors.yaml#/components/responses/ServiceUnavailable"

  /auth/logout:
    post:
      operationId: logout
      tags: [Auth]
      summary: Clear session cookie
      description: Local BFF route only; clears `agnet_token` without upstream call.
      security: []
      responses:
        "200":
          description: Logged out
          content:
            application/json:
              schema:
                type: object
                required: [success]
                properties:
                  success:
                    type: boolean
                    example: true

  /onboarding:
    post:
      operationId: createOrResumeOnboarding
      tags: [Onboarding]
      summary: Create or resume onboarding session
      description: |
        Creates a new session or resumes an existing `in_progress` session for the authenticated user.
        Pass `?forceNew=true` to always create a new session.
      security:
        - cookieAuth: []
      parameters:
        - name: forceNew
          in: query
          required: false
          schema:
            type: boolean
            default: false
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateOnboardingRequest"
      responses:
        "200":
          description: Onboarding session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OnboardingSession"
        "401":
          $ref: "./components/errors.yaml#/components/responses/Unauthorized"
        "503":
          $ref: "./components/errors.yaml#/components/responses/ServiceUnavailable"

  /onboarding/{id}:
    get:
      operationId: getOnboardingSession
      tags: [Onboarding]
      summary: Get onboarding session by ID
      security:
        - cookieAuth: []
      parameters:
        - $ref: "#/components/parameters/OnboardingId"
      responses:
        "200":
          description: Onboarding session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OnboardingSession"
        "400":
          $ref: "./components/errors.yaml#/components/responses/BadRequest"
        "401":
          $ref: "./components/errors.yaml#/components/responses/Unauthorized"
        "404":
          $ref: "./components/errors.yaml#/components/responses/NotFound"
        "503":
          $ref: "./components/errors.yaml#/components/responses/ServiceUnavailable"

    delete:
      operationId: deleteOnboardingSession
      tags: [Onboarding]
      summary: Cancel and delete draft onboarding session
      security:
        - cookieAuth: []
      parameters:
        - $ref: "#/components/parameters/OnboardingId"
      responses:
        "204":
          description: Session deleted
        "400":
          $ref: "./components/errors.yaml#/components/responses/BadRequest"
        "401":
          $ref: "./components/errors.yaml#/components/responses/Unauthorized"
        "404":
          $ref: "./components/errors.yaml#/components/responses/NotFound"
        "503":
          $ref: "./components/errors.yaml#/components/responses/ServiceUnavailable"

  /onboarding/{id}/steps/{step}:
    patch:
      operationId: updateOnboardingStep
      tags: [Onboarding]
      summary: Save step data and optionally mark step complete
      description: |
        Merges `data` into `session.stepData[step]`. When `markComplete` is true (default),
        advances `currentStepKey` to the next step in the 13-step flow.
      security:
        - cookieAuth: []
      parameters:
        - $ref: "#/components/parameters/OnboardingId"
        - $ref: "#/components/parameters/OnboardingStep"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateStepRequest"
      responses:
        "200":
          description: Updated onboarding session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OnboardingSession"
        "400":
          $ref: "./components/errors.yaml#/components/responses/BadRequest"
        "401":
          $ref: "./components/errors.yaml#/components/responses/Unauthorized"
        "404":
          $ref: "./components/errors.yaml#/components/responses/NotFound"
        "503":
          $ref: "./components/errors.yaml#/components/responses/ServiceUnavailable"

  /onboarding/{id}/submit:
    post:
      operationId: submitOnboarding
      tags: [Onboarding]
      summary: Submit application for scoring
      description: Transitions session status to `submitted` and triggers scoring pipeline.
      security:
        - cookieAuth: []
      parameters:
        - $ref: "#/components/parameters/OnboardingId"
      responses:
        "200":
          description: Submitted session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OnboardingSession"
        "401":
          $ref: "./components/errors.yaml#/components/responses/Unauthorized"
        "404":
          $ref: "./components/errors.yaml#/components/responses/NotFound"
        "503":
          $ref: "./components/errors.yaml#/components/responses/ServiceUnavailable"

  /scoring/satellite-image:
    get:
      operationId: getSatelliteImage
      tags: [Scoring]
      summary: Proxy satellite thumbnail (NASA GIBS allowlist)
      description: |
        Proxies satellite imagery URLs to avoid CORS. Only NASA GIBS and
        earthdata domains are permitted (AA-016).
      security:
        - cookieAuth: []
      parameters:
        - name: url
          in: query
          required: true
          schema:
            type: string
            format: uri
      responses:
        "200":
          description: Image bytes
          content:
            image/png:
              schema:
                type: string
                format: binary
            image/jpeg:
              schema:
                type: string
                format: binary
        "400":
          description: URL not on allowlist
        "401":
          $ref: "./components/errors.yaml#/components/responses/Unauthorized"

  /onboarding/{id}/status:
    get:
      operationId: getOnboardingStatus
      tags: [Onboarding]
      summary: Poll scoring status and eligibility outcome
      description: Used by StepScoring to poll until scoring completes.
      security:
        - cookieAuth: []
      parameters:
        - $ref: "#/components/parameters/OnboardingId"
      responses:
        "200":
          description: Scoring status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OnboardingStatusResponse"
        "401":
          $ref: "./components/errors.yaml#/components/responses/Unauthorized"
        "404":
          $ref: "./components/errors.yaml#/components/responses/NotFound"
        "503":
          $ref: "./components/errors.yaml#/components/responses/ServiceUnavailable"

  /documents/upload:
    post:
      operationId: uploadDocument
      tags: [Documents]
      summary: Upload onboarding document
      description: |
        Multipart upload proxied to `POST /onboarding/{sessionId}/documents` on core-api.
        Max file size 10 MB. Falls back to stub upload when upstream returns 404.
      security:
        - cookieAuth: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [sessionId, docType, file]
              properties:
                sessionId:
                  type: string
                docType:
                  $ref: "./components/enums.yaml#/components/schemas/DocumentType"
                file:
                  type: string
                  format: binary
      responses:
        "200":
          description: Document uploaded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentUploadResponse"
        "400":
          $ref: "./components/errors.yaml#/components/responses/BadRequest"
        "401":
          $ref: "./components/errors.yaml#/components/responses/Unauthorized"
        "404":
          $ref: "./components/errors.yaml#/components/responses/NotFound"
        "413":
          $ref: "./components/errors.yaml#/components/responses/PayloadTooLarge"
        "503":
          $ref: "./components/errors.yaml#/components/responses/ServiceUnavailable"

  /farmer/dashboard:
    get:
      operationId: getFarmerDashboard
      tags: [Farmer]
      summary: Get aggregated farmer dashboard data
      description: |
        Returns finance, trade, identity, and inputs modules derived from the farmer's
        submitted onboarding session. Modules may be null if no submitted session exists.
      security:
        - cookieAuth: []
      responses:
        "200":
          description: Dashboard payload
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FarmerDashboardResponse"
        "401":
          $ref: "./components/errors.yaml#/components/responses/Unauthorized"
        "503":
          $ref: "./components/errors.yaml#/components/responses/ServiceUnavailable"

  /reference/options:
    get:
      operationId: getReferenceOptions
      tags: [Reference]
      summary: Get reference lookup options
      description: |
        Public endpoint for dropdowns in onboarding steps. No authentication required.
      security: []
      parameters:
        - name: countryCode
          in: query
          required: false
          schema:
            $ref: "./components/enums.yaml#/components/schemas/CountryCode"
            default: UZ
        - name: type
          in: query
          required: false
          schema:
            $ref: "./components/enums.yaml#/components/schemas/ReferenceOptionType"
        - name: institutionId
          in: query
          required: false
          schema:
            type: string
          description: Required when type=institution_checklist
      responses:
        "200":
          description: Reference options
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/ReferenceOption"
        "503":
          $ref: "./components/errors.yaml#/components/responses/ServiceUnavailable"

components:
  securitySchemes:
    cookieAuth:
      type: apiKey
      in: cookie
      name: agnet_token
      description: JWT issued by core-api, set as httpOnly cookie on OTP verify

  parameters:
    OnboardingId:
      name: id
      in: path
      required: true
      schema:
        type: string
      example: session_abc123

    OnboardingStep:
      name: step
      in: path
      required: true
      schema:
        $ref: "./components/enums.yaml#/components/schemas/OnboardingStep"

  schemas:
    OtpRequest:
      type: object
      required: [phone]
      properties:
        phone:
          type: string
          description: E.164 or local format; normalized server-side
          example: "+998901234567"
        countryCode:
          $ref: "./components/enums.yaml#/components/schemas/CountryCode"

    OtpVerifyRequest:
      allOf:
        - $ref: "#/components/schemas/OtpRequest"
        - type: object
          required: [code]
          properties:
            code:
              type: string
              minLength: 4
              maxLength: 6
              example: "1234"

    OtpSentResponse:
      type: object
      required: [sent]
      properties:
        sent:
          type: boolean
          example: true

    VerifyResponse:
      type: object
      required: [user]
      properties:
        user:
          $ref: "#/components/schemas/User"

    User:
      type: object
      required: [id, phone, countryCode, kycStatus]
      properties:
        id:
          type: string
        phone:
          type: string
        countryCode:
          type: string
        kycStatus:
          $ref: "./components/enums.yaml#/components/schemas/KycStatus"

    UserMeResponse:
      allOf:
        - $ref: "#/components/schemas/User"
        - type: object
          properties:
            farmerProfile:
              nullable: true
              type: object
              additionalProperties: true
            activeSession:
              nullable: true
              allOf:
                - $ref: "#/components/schemas/OnboardingSession"

    CreateOnboardingRequest:
      type: object
      properties:
        countryCode:
          $ref: "./components/enums.yaml#/components/schemas/CountryCode"
        channel:
          type: string
          enum: [farmer]
          default: farmer

    OnboardingSession:
      type: object
      required:
        - id
        - userId
        - countryCode
        - channel
        - currentStep
        - currentStepKey
        - status
        - stepData
        - completedSteps
      properties:
        id:
          type: string
        userId:
          type: string
        countryCode:
          type: string
        channel:
          type: string
        currentStep:
          type: integer
          minimum: 1
          maximum: 13
        currentStepKey:
          $ref: "./components/enums.yaml#/components/schemas/OnboardingStep"
        status:
          $ref: "./components/enums.yaml#/components/schemas/OnboardingStatus"
        stepData:
          $ref: "./components/onboarding-steps.yaml#/components/schemas/OnboardingStepData"
        completedSteps:
          type: array
          items:
            $ref: "./components/enums.yaml#/components/schemas/OnboardingStep"
        stepRecords:
          type: array
          items:
            type: object
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    UpdateStepRequest:
      type: object
      required: [data]
      properties:
        data:
          type: object
          description: Step-specific payload; see OnboardingStepData schemas
          additionalProperties: true
        markComplete:
          type: boolean
          default: true

    OnboardingStatusResponse:
      type: object
      required: [onboardingId, currentStep, completedSteps, status]
      properties:
        onboardingId:
          type: string
        currentStep:
          $ref: "./components/enums.yaml#/components/schemas/OnboardingStep"
        completedSteps:
          type: array
          items:
            $ref: "./components/enums.yaml#/components/schemas/OnboardingStep"
        status:
          $ref: "./components/enums.yaml#/components/schemas/OnboardingStatus"
        scoringPhase:
          type: string
          enum: [geo, documents, scoring, finalize]
          description: Drives StepScoring phase UI (AA-012)
        eligibilityOutcome:
          nullable: true
          $ref: "./components/enums.yaml#/components/schemas/EligibilityOutcome"
        score:
          nullable: true
          type: integer
          minimum: 0
          maximum: 1000
        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:
          $ref: "#/components/schemas/ScoringMeta"
        satelliteImages:
          type: array
          items:
            type: string
            format: uri

    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

    DocumentUploadResponse:
      type: object
      required: [documentId, sessionId, docType, fileName, status]
      properties:
        documentId:
          type: string
        sessionId:
          type: string
        docType:
          type: string
        fileName:
          type: string
        status:
          type: string
          enum: [uploaded, processing, failed]
        uploadedAt:
          type: string
          format: date-time

    ReferenceOption:
      type: object
      required: [value, label]
      properties:
        value:
          type: string
        label:
          type: string
        metadata:
          type: object
          additionalProperties: true

    FarmerDashboardResponse:
      type: object
      required: [userId, phone, applicationStatus]
      properties:
        userId:
          type: string
        phone:
          type: string
        sessionId:
          type: string
        applicationStatus:
          $ref: "./components/enums.yaml#/components/schemas/ApplicationStatus"
        score:
          nullable: true
          type: integer
        eligibility:
          nullable: true
          $ref: "./components/enums.yaml#/components/schemas/EligibilityOutcome"
        reasonCodes:
          type: array
          items:
            $ref: "./components/onboarding-steps.yaml#/components/schemas/ReasonCode"
        finance:
          nullable: true
          $ref: "#/components/schemas/FinanceModule"
        trade:
          nullable: true
          $ref: "#/components/schemas/TradeModule"
        identity:
          nullable: true
          $ref: "#/components/schemas/IdentityModule"
        inputs:
          nullable: true
          $ref: "#/components/schemas/InputsModule"

    FinanceModule:
      type: object
      properties:
        purpose:
          type: string
        amount:
          type: string
        existingLoans:
          type: string
        monthlyIncome:
          type: string
        repaymentHistory:
          type: string
        caseId:
          type: string
          example: AGN-FIN-XXXXXX
        status:
          $ref: "./components/enums.yaml#/components/schemas/FinanceCaseStatus"
        riskPd:
          type: number
          description: Basel III probability of default (0-1)
        riskRating:
          type: string
          description: Risk rating A-E
        ifrs9Stage:
          type: integer
          minimum: 1
          maximum: 3
        cbuClass:
          type: integer
          minimum: 1
          maximum: 5
        farmerExplanation:
          type: object
          properties:
            uz:
              type: string
            ru:
              type: string
            en:
              type: string
        satelliteImages:
          type: array
          items:
            type: string
            format: uri
        stages:
          type: array
          items:
            type: object
            properties:
              label:
                type: string
              completed:
                type: boolean

    TradeModule:
      type: object
      properties:
        crops:
          type: array
          items:
            $ref: "./components/onboarding-steps.yaml#/components/schemas/CropEntry"
        buyerMatches:
          type: array
          items:
            type: object
            properties:
              buyerName:
                type: string
              commodity:
                type: string
              priceRange:
                type: string
        exportDocsComplete:
          type: boolean

    IdentityModule:
      type: object
      properties:
        fullName:
          type: string
        district:
          type: string
        gender:
          type: string
        kycStatus:
          type: string
        farmSizeHa:
          type: string
        landOwnership:
          type: string
        hasGisMapping:
          type: boolean
        documentCompleteness:
          type: integer
          minimum: 0
          maximum: 100

    InputsModule:
      type: object
      properties:
        assets:
          type: array
          items:
            $ref: "./components/onboarding-steps.yaml#/components/schemas/AssetEntry"
        recommendedInputs:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
              category:
                type: string
              relevance:
                type: string
