openapi: 3.0.3
info:
  title: ONON.PRO API
  version: 1.1.0
  description: Catalog, calculations, search, demand analytics and unit conversion for the ONON.PRO platform.
servers:
  - url: /api/v1
paths:
  /categories:
    get:
      summary: List localized categories
      parameters:
        - $ref: '#/components/parameters/Locale'
      responses:
        '200':
          description: Category list
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Category' }
  /categories/{categoryKey}:
    get:
      summary: Get a category
      parameters:
        - name: categoryKey
          in: path
          required: true
          schema: { type: string }
        - $ref: '#/components/parameters/Locale'
      responses:
        '200': { description: Category, content: { application/json: { schema: { $ref: '#/components/schemas/Category' } } } }
        '404': { $ref: '#/components/responses/NotFound' }
  /calculators:
    get:
      summary: List calculators
      parameters:
        - $ref: '#/components/parameters/Locale'
        - { name: category, in: query, schema: { type: string } }
        - { name: query, in: query, schema: { type: string, maxLength: 200 } }
        - { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
        - { name: pageSize, in: query, schema: { type: integer, minimum: 1, maximum: 50, default: 12 } }
      responses:
        '200': { description: Paginated calculators, content: { application/json: { schema: { $ref: '#/components/schemas/CalculatorPage' } } } }
  /calculators/{calculatorKey}:
    get:
      summary: Get a localized calculator definition
      parameters:
        - $ref: '#/components/parameters/CalculatorKey'
        - $ref: '#/components/parameters/Locale'
      responses:
        '200': { description: Calculator definition, content: { application/json: { schema: { $ref: '#/components/schemas/CalculatorResource' } } } }
        '404': { $ref: '#/components/responses/NotFound' }
  /calculators/by-on/{onCode}:
    get:
      summary: Resolve a calculator by ON code
      parameters:
        - { name: onCode, in: path, required: true, schema: { type: string, pattern: '^[NOno]+$' } }
        - $ref: '#/components/parameters/Locale'
      responses:
        '200': { description: Calculator definition, content: { application/json: { schema: { $ref: '#/components/schemas/CalculatorResource' } } } }
        '404': { $ref: '#/components/responses/NotFound' }
  /calculators/{calculatorKey}/calculate:
    post:
      summary: Run a backend calculation
      parameters:
        - $ref: '#/components/parameters/CalculatorKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CalculateRequest' }
      responses:
        '200': { description: Calculation result, content: { application/json: { schema: { $ref: '#/components/schemas/CalculateResponse' } } } }
        '422': { $ref: '#/components/responses/InvalidInput' }
  /units/convert:
    post:
      summary: Convert compatible units
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [value, from, to]
              properties:
                value: { type: number }
                from: { type: string }
                to: { type: string }
      responses:
        '200':
          description: Converted value
          content:
            application/json:
              schema:
                type: object
                properties: { value: { type: number }, from: { type: string }, to: { type: string } }
        '422': { $ref: '#/components/responses/InvalidInput' }
  /units/quantities:
    get:
      summary: List quantities and units
      responses:
        '200':
          description: Quantity list
          content:
            application/json:
              schema: { type: array, items: { $ref: '#/components/schemas/Quantity' } }
  /units/quantities/{quantityKey}:
    get:
      summary: Get one quantity
      parameters:
        - { name: quantityKey, in: path, required: true, schema: { type: string } }
      responses:
        '200': { description: Quantity, content: { application/json: { schema: { $ref: '#/components/schemas/Quantity' } } } }
        '404': { $ref: '#/components/responses/NotFound' }
  /search:
    get:
      summary: Full-text calculator search
      parameters:
        - { name: q, in: query, required: true, schema: { type: string, maxLength: 200 } }
        - $ref: '#/components/parameters/Locale'
      responses:
        '200':
          description: Ranked search results
          content:
            application/json:
              schema:
                type: object
                properties:
                  query: { type: string }
                  searchId: { type: string, format: uuid }
                  clusterId: { type: string, format: uuid }
                  items: { type: array, items: { $ref: '#/components/schemas/SearchResult' } }
  /analytics/searches:
    post:
      summary: Record a client-rendered internal search impression
      description: Stores only a redacted query and a one-way anonymous session hash.
      parameters:
        - $ref: '#/components/parameters/AnalyticsSession'
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/SearchImpression' } } }
      responses:
        '201': { description: Search receipt, content: { application/json: { schema: { $ref: '#/components/schemas/SearchReceipt' } } } }
        '429': { description: Analytics rate limit exceeded }
  /analytics/searches/{searchId}/click:
    post:
      summary: Record a click on a ranked search result
      parameters:
        - $ref: '#/components/parameters/SearchID'
        - $ref: '#/components/parameters/AnalyticsSession'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [position, resultKey]
              properties:
                position: { type: integer, minimum: 1, maximum: 100 }
                resultKey: { type: string, maxLength: 160 }
                sessionId: { type: string, writeOnly: true }
      responses:
        '204': { description: Recorded }
  /analytics/searches/{searchId}/outcome:
    post:
      summary: Record no-click, reformulation, exit or quick-return behavior
      parameters:
        - $ref: '#/components/parameters/SearchID'
        - $ref: '#/components/parameters/AnalyticsSession'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [outcome]
              properties:
                outcome: { type: string, enum: [no_click, reformulated, exit, quick_return] }
                sessionId: { type: string, writeOnly: true }
      responses:
        '204': { description: Recorded }
  /analytics/feature-requests:
    post:
      summary: Submit a voluntary calculator request
      parameters:
        - $ref: '#/components/parameters/AnalyticsSession'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text, locale]
              properties:
                text: { type: string, minLength: 3, maxLength: 500 }
                locale: { type: string }
                searchId: { type: string, format: uuid }
                sessionId: { type: string, writeOnly: true }
      responses:
        '201': { description: Suggestion recorded }
  /admin/calculators:
    get:
      summary: List and export every published calculator definition
      security: [{ AdminApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/Locale'
        - { name: download, in: query, schema: { type: boolean }, description: Adds an attachment filename header when set to true }
      responses:
        '200':
          description: Complete localized calculator catalog
          content: { application/json: { schema: { $ref: '#/components/schemas/AdminCalculatorExport' } } }
        '401': { description: Invalid administrator key }
  /admin/search-demand/{view}:
    get:
      summary: Read a Search Demand Analytics report
      security: [{ AdminApiKey: [] }]
      parameters:
        - name: view
          in: path
          required: true
          schema: { type: string, enum: [overview, zero-results, low-relevance, growing, clusters] }
      responses:
        '200': { description: Metrics, clusters and Opportunity Scores }
        '401': { description: Invalid administrator key }
  /admin/search-demand/drafts:
    get:
      summary: List calculator drafts created from demand clusters
      security: [{ AdminApiKey: [] }]
      responses: { '200': { description: Draft list }, '401': { description: Invalid administrator key } }
  /admin/search-demand/suggestions:
    get:
      summary: List voluntary calculator suggestions
      security: [{ AdminApiKey: [] }]
      responses: { '200': { description: Suggestion list }, '401': { description: Invalid administrator key } }
  /admin/search-demand/closed:
    get:
      summary: List closed gaps with live before/after metrics
      security: [{ AdminApiKey: [] }]
      responses: { '200': { description: Closed gap history }, '401': { description: Invalid administrator key } }
  /admin/search-demand/clusters/{clusterId}/draft:
    post:
      summary: Create a calculator draft from a search cluster
      security: [{ AdminApiKey: [] }]
      parameters:
        - { name: clusterId, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, properties: { category: { type: string } } } } }
      responses:
        '201': { description: Draft created }
  /admin/search-demand/drafts/{draftId}/publish:
    post:
      summary: Link a published calculator to its demand cluster and start before/after measurement
      security: [{ AdminApiKey: [] }]
      parameters:
        - { name: draftId, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [calculatorKey]
              properties: { calculatorKey: { type: string } }
      responses:
        '200': { description: Initial before/after comparison }
  /admin/search-demand/clusters/{clusterId}/merge:
    post:
      summary: Merge a source query cluster into a target cluster
      security: [{ AdminApiKey: [] }]
      parameters:
        - { name: clusterId, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [sourceClusterId]
              properties: { sourceClusterId: { type: string, format: uuid } }
      responses:
        '204': { description: Clusters merged }
  /fishing/forecast:
    get:
      summary: Build an hourly heuristic fishing activity forecast
      description: Uses location, date and forecast weather. The score is an aid, not a guarantee of a catch.
      parameters:
        - { name: latitude, in: query, required: true, schema: { type: number, minimum: -90, maximum: 90 } }
        - { name: longitude, in: query, required: true, schema: { type: number, minimum: -180, maximum: 180 } }
        - { name: date, in: query, required: true, schema: { type: string, format: date } }
        - $ref: '#/components/parameters/Locale'
      responses:
        '200':
          description: Localized forecast and hourly activity scores
          content: { application/json: { schema: { $ref: '#/components/schemas/FishingForecast' } } }
        '422': { $ref: '#/components/responses/InvalidInput' }
components:
  parameters:
    Locale:
      name: locale
      in: query
      schema: { type: string, enum: [en, zh, hi, es, fr, ar, bn, pt, ru, id], default: en }
    CalculatorKey:
      name: calculatorKey
      in: path
      required: true
      schema: { type: string }
    SearchID:
      name: searchId
      in: path
      required: true
      schema: { type: string, format: uuid }
    AnalyticsSession:
      name: X-Analytics-Session
      in: header
      required: false
      description: Ephemeral anonymous tab identifier; persisted only as a one-way hash.
      schema: { type: string, maxLength: 200 }
  securitySchemes:
    AdminApiKey:
      type: apiKey
      in: header
      name: X-Admin-Key
  responses:
    NotFound:
      description: Resource not found
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
    InvalidInput:
      description: Invalid input
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } }
  schemas:
    Category:
      type: object
      required: [key, onCode, title, description, sortOrder]
      properties:
        key: { type: string }
        onCode: { type: string }
        title: { type: string }
        description: { type: string }
        sortOrder: { type: integer }
        translationFallback: { type: boolean }
    CalculatorSummary:
      type: object
      properties:
        key: { type: string }
        onCode: { type: string }
        category: { type: string }
        title: { type: string }
        description: { type: string }
        url: { type: string }
        featured: { type: boolean }
    CalculatorPage:
      type: object
      properties:
        items: { type: array, items: { $ref: '#/components/schemas/CalculatorSummary' } }
        page: { type: integer }
        pageSize: { type: integer }
        total: { type: integer }
    CalculatorResource:
      type: object
      additionalProperties: true
      required: [key, category, version, engineType, inputs, outputs, formulas, translation]
      properties:
        key: { type: string }
        category: { type: string }
        version: { type: integer }
        engineType: { type: string, enum: [expression, custom] }
        onCode: { type: string }
        inputs: { type: array, items: { type: object } }
        outputs: { type: array, items: { type: object } }
        formulas: { type: array, items: { type: object } }
        translation: { type: object }
        translationFallback: { type: boolean }
    AdminCalculatorExport:
      type: object
      required: [schemaVersion, generatedAt, locale, total, calculators]
      properties:
        schemaVersion: { type: integer, enum: [1] }
        generatedAt: { type: string, format: date-time }
        locale: { type: string }
        total: { type: integer }
        calculators:
          type: array
          items:
            allOf:
              - $ref: '#/components/schemas/CalculatorResource'
              - type: object
                required: [url, featured]
                properties:
                  url: { type: string }
                  featured: { type: boolean }
    InputValue:
      type: object
      required: [value]
      properties:
        value: { nullable: true }
        unit: { type: string }
    CalculateRequest:
      type: object
      required: [locale, inputs]
      properties:
        locale: { type: string, enum: [en, zh, hi, es, fr, ar, bn, pt, ru, id] }
        inputs:
          type: object
          additionalProperties: { $ref: '#/components/schemas/InputValue' }
        requestedOutputUnits:
          type: object
          additionalProperties: { type: string }
        significantDigits: { type: integer, enum: [2, 4, 6, 8, 10] }
    CalculateResponse:
      type: object
      properties:
        calculator: { type: string }
        definitionVersion: { type: integer }
        normalizedInputs: { type: object }
        results: { type: object }
        steps: { type: array, items: { type: object } }
        warnings: { type: array, items: { type: object } }
        chart: { type: object }
    Unit:
      type: object
      properties: { id: { type: string }, symbol: { type: string }, quantity: { type: string } }
    Quantity:
      type: object
      properties:
        key: { type: string }
        siUnit: { type: string }
        units: { type: array, items: { $ref: '#/components/schemas/Unit' } }
    SearchResult:
      type: object
      properties:
        type: { type: string }
        key: { type: string }
        title: { type: string }
        description: { type: string }
        category: { type: string }
        url: { type: string }
        score: { type: number }
    SearchReceipt:
      type: object
      properties:
        searchId: { type: string, format: uuid }
        clusterId: { type: string, format: uuid }
    SearchImpression:
      type: object
      required: [query, locale, results]
      properties:
        query: { type: string, maxLength: 200 }
        locale: { type: string }
        sessionId: { type: string, writeOnly: true }
        results:
          type: array
          maxItems: 100
          items:
            type: object
            required: [key, score]
            properties:
              key: { type: string }
              score: { type: number, minimum: 0, maximum: 1 }
    FishingForecast:
      type: object
      required: [date, latitude, longitude, timezone, score, level, sunrise, sunset, bestWindows, hours, disclaimer, provider]
      properties:
        date: { type: string, format: date }
        latitude: { type: number }
        longitude: { type: number }
        timezone: { type: string }
        score: { type: integer, minimum: 0, maximum: 100 }
        level: { type: string }
        sunrise: { type: string }
        sunset: { type: string }
        peakHour: { type: string }
        bestWindows: { type: array, items: { $ref: '#/components/schemas/FishingWindow' } }
        hours: { type: array, items: { $ref: '#/components/schemas/FishingHour' } }
        disclaimer: { type: string }
        provider: { type: string }
        providerUrl: { type: string, format: uri }
        generatedAt: { type: string, format: date-time }
    FishingWindow:
      type: object
      properties:
        start: { type: string }
        end: { type: string }
        score: { type: integer, minimum: 0, maximum: 100 }
        level: { type: string }
    FishingHour:
      type: object
      properties:
        time: { type: string }
        score: { type: integer, minimum: 0, maximum: 100 }
        level: { type: string }
        temperature: { type: number }
        windSpeed: { type: number }
        precipitation: { type: number }
        pressure: { type: number }
        cloudCover: { type: number }
        weatherCode: { type: integer }
        reasons: { type: array, items: { type: string } }
    ErrorResponse:
      type: object
      properties:
        error:
          type: object
          required: [code, message, requestId]
          properties:
            code: { type: string }
            message: { type: string }
            fields: { type: object }
            requestId: { type: string }
