{
  "openapi": "3.0.3",
  "info": {
    "title": "GCX FSHS Field Reports Integration API",
    "version": "1.0.0",
    "description": "Read-only field reports and evidence. Generate a key with field_reports.read in Admin > API Management. Limit: 60 requests per minute per key. Traverse every cursor page, upsert by id and fingerprint, and overlap incremental checkpoints by at least one minute. No deletion tombstones are emitted."
  },
  "servers": [{ "url": "https://monitor.gcxapps.net/api/backend/integration/field-reports" }],
  "externalDocs": { "description": "Integration guide", "url": "https://monitor.gcxapps.net/docs/field-reports-api.md" },
  "security": [{ "ApiKey": [] }],
  "paths": {
    "/summary": {
      "get": {
        "operationId": "getFieldReportSummary",
        "summary": "Report counts and latest update times",
        "responses": {
          "200": { "description": "Summary", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Summary" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/deliveries": {
      "get": {
        "operationId": "listFieldDeliveries",
        "summary": "Delivery records, quantities, quality checks and evidence",
        "parameters": [
          { "$ref": "#/components/parameters/Limit" }, { "$ref": "#/components/parameters/Cursor" },
          { "$ref": "#/components/parameters/Order" }, { "$ref": "#/components/parameters/UpdatedSince" },
          { "$ref": "#/components/parameters/UpdatedUntil" }, { "$ref": "#/components/parameters/DateFrom" },
          { "$ref": "#/components/parameters/DateTo" }, { "$ref": "#/components/parameters/UserId" },
          { "$ref": "#/components/parameters/Region" }, { "$ref": "#/components/parameters/Status" },
          { "name": "plan_period_id", "in": "query", "description": "Local academic-year and term ID", "schema": { "type": "integer", "minimum": 1 } },
          { "name": "school_id", "in": "query", "schema": { "type": "integer", "minimum": 1 } },
          { "name": "commodity_id", "in": "query", "schema": { "type": "integer", "minimum": 1 } },
          { "name": "supplier_id", "in": "query", "schema": { "type": "integer", "minimum": 1 } }
        ],
        "responses": {
          "200": { "description": "Delivery page", "content": { "application/json": { "schema": { "allOf": [
            { "$ref": "#/components/schemas/FeedPage" },
            { "type": "object", "properties": { "feed": { "type": "string", "enum": ["deliveries"] }, "items": { "type": "array", "items": { "$ref": "#/components/schemas/Delivery" } } } }
          ] } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" },
          "422": { "$ref": "#/components/responses/InvalidQuery" }, "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/anonymous-reports": {
      "get": {
        "operationId": "listAnonymousFieldReports",
        "summary": "See Something, Say Something reports without reporter identity",
        "parameters": [
          { "$ref": "#/components/parameters/Limit" }, { "$ref": "#/components/parameters/Cursor" },
          { "$ref": "#/components/parameters/Order" }, { "$ref": "#/components/parameters/UpdatedSince" },
          { "$ref": "#/components/parameters/UpdatedUntil" }, { "$ref": "#/components/parameters/DateFrom" },
          { "$ref": "#/components/parameters/DateTo" }, { "$ref": "#/components/parameters/Region" }
        ],
        "responses": {
          "200": { "description": "Anonymous report page", "content": { "application/json": { "schema": { "allOf": [
            { "$ref": "#/components/schemas/FeedPage" },
            { "type": "object", "properties": { "feed": { "type": "string", "enum": ["anonymous-reports"] }, "items": { "type": "array", "items": { "$ref": "#/components/schemas/AnonymousReport" } } } }
          ] } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" },
          "422": { "$ref": "#/components/responses/InvalidQuery" }, "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/issue-reports": {
      "get": {
        "operationId": "listFieldIssueReports",
        "summary": "Authenticated application issue reports and optional screenshots",
        "parameters": [
          { "$ref": "#/components/parameters/Limit" }, { "$ref": "#/components/parameters/Cursor" },
          { "$ref": "#/components/parameters/Order" }, { "$ref": "#/components/parameters/UpdatedSince" },
          { "$ref": "#/components/parameters/UpdatedUntil" }, { "$ref": "#/components/parameters/DateFrom" },
          { "$ref": "#/components/parameters/DateTo" }, { "$ref": "#/components/parameters/UserId" },
          { "$ref": "#/components/parameters/Status" }
        ],
        "responses": {
          "200": { "description": "Issue report page", "content": { "application/json": { "schema": { "allOf": [
            { "$ref": "#/components/schemas/FeedPage" },
            { "type": "object", "properties": { "feed": { "type": "string", "enum": ["issue-reports"] }, "items": { "type": "array", "items": { "$ref": "#/components/schemas/IssueReport" } } } }
          ] } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" },
          "422": { "$ref": "#/components/responses/InvalidQuery" }, "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/media/{type}/{id}": {
      "get": {
        "operationId": "downloadFieldReportEvidence",
        "summary": "Authenticated evidence download",
        "description": "Resolve returned media URLs against https://monitor.gcxapps.net and send the same X-API-Key header. id is the report source_id, or the delivery source_id for delivery evidence. Responses use Cache-Control: private, no-store.",
        "parameters": [
          { "name": "type", "in": "path", "required": true, "schema": { "type": "string", "enum": ["anonymous", "issue", "delivery-photo", "delivery-voice"] } },
          { "name": "id", "in": "path", "required": true, "schema": { "type": "integer", "minimum": 1 } }
        ],
        "responses": {
          "200": { "description": "Evidence bytes, with detected Content-Type", "content": { "application/octet-stream": { "schema": { "type": "string", "format": "binary" } }, "image/*": { "schema": { "type": "string", "format": "binary" } }, "audio/*": { "schema": { "type": "string", "format": "binary" } }, "video/*": { "schema": { "type": "string", "format": "binary" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": { "ApiKey": { "type": "apiKey", "in": "header", "name": "X-API-Key", "description": "Admin-generated key with field_reports.read scope. Raw key is displayed once; never embed it in a browser or mobile app." } },
    "parameters": {
      "Limit": { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 100 } },
      "Cursor": { "name": "cursor", "in": "query", "description": "Opaque next_cursor from the previous page. Keep all filters and order unchanged.", "schema": { "type": "string" } },
      "Order": { "name": "order", "in": "query", "schema": { "type": "string", "enum": ["asc", "desc"], "default": "asc" } },
      "UpdatedSince": { "name": "updated_since", "in": "query", "description": "Inclusive timestamp with timezone", "schema": { "type": "string", "format": "date-time" } },
      "UpdatedUntil": { "name": "updated_until", "in": "query", "description": "Inclusive timestamp with timezone, not earlier than updated_since", "schema": { "type": "string", "format": "date-time" } },
      "DateFrom": { "name": "date_from", "in": "query", "description": "Inclusive delivery date, or report receipt date", "schema": { "type": "string", "format": "date" } },
      "DateTo": { "name": "date_to", "in": "query", "description": "Inclusive date through 23:59:59, not earlier than date_from", "schema": { "type": "string", "format": "date" } },
      "UserId": { "name": "user_id", "in": "query", "schema": { "type": "integer", "minimum": 1 } },
      "Region": { "name": "region", "in": "query", "description": "Exact region name", "schema": { "type": "string" } },
      "Status": { "name": "status", "in": "query", "description": "Exact delivery review_status or issue status", "schema": { "type": "string" } }
    },
    "responses": {
      "Unauthorized": { "description": "Missing, invalid or revoked API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Forbidden": { "description": "Key lacks field_reports.read", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "InvalidQuery": { "description": "Invalid cursor, unsupported filter, date, timestamp, range or limit", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "NotFound": { "description": "Evidence is absent or unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "RateLimited": { "description": "60 requests per minute exceeded. Retry with exponential backoff and jitter.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "ServerError": { "description": "Server error. Retry with exponential backoff and jitter.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
    },
    "schemas": {
      "Error": { "type": "object", "properties": { "error": { "oneOf": [{ "type": "string" }, { "type": "object", "properties": { "code": { "type": "string" }, "message": { "type": "string" }, "details": { "nullable": true } } }] } } },
      "FeedPage": { "type": "object", "required": ["feed", "generated_at", "filters", "has_more", "next_cursor", "items"], "properties": {
        "feed": { "type": "string" }, "generated_at": { "type": "string", "format": "date-time" },
        "filters": { "oneOf": [{ "type": "object", "additionalProperties": { "type": "string" } }, { "type": "array", "maxItems": 0, "items": { "type": "string" } }], "description": "Applied filters; empty PHP maps are serialized as []" },
        "has_more": { "type": "boolean" }, "next_cursor": { "type": "string", "nullable": true }, "items": { "type": "array", "items": { "type": "object" } }
      } },
      "Record": { "type": "object", "required": ["id", "record_type", "source_id", "client_submission_id", "created_at", "updated_at", "fingerprint"], "properties": {
        "id": { "type": "string", "description": "Stable prefixed integration ID; use as upsert key" },
        "record_type": { "type": "string" }, "source_id": { "type": "integer" },
        "client_submission_id": { "type": "string", "nullable": true }, "created_at": { "type": "string", "format": "date-time", "nullable": true },
        "updated_at": { "type": "string", "format": "date-time", "nullable": true }, "fingerprint": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
      } },
      "Officer": { "type": "object", "required": ["id", "name"], "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "role": { "type": "string" } } },
      "Media": { "type": "object", "nullable": true, "required": ["type", "url"], "properties": { "type": { "type": "string", "enum": ["image", "audio", "video"] }, "url": { "type": "string", "description": "Authenticated relative URL, resolved against the app origin" } } },
      "Delivery": { "allOf": [{ "$ref": "#/components/schemas/Record" }, { "type": "object", "properties": {
        "record_type": { "type": "string", "enum": ["delivery"] },
        "plan_period": { "type": "object", "properties": { "id": { "type": "integer" }, "label": { "type": "string" }, "academic_year": { "type": "string" }, "term": { "type": "string" } } },
        "school": { "type": "object", "properties": { "id": { "type": "integer" }, "code": { "type": "string" }, "name": { "type": "string" }, "region": { "type": "string", "nullable": true }, "district": { "type": "string", "nullable": true } } },
        "commodity": { "type": "object", "properties": { "id": { "type": "integer" }, "code": { "type": "string" }, "name": { "type": "string" }, "unit": { "type": "string" } } },
        "supplier": { "type": "object", "nullable": true, "properties": { "id": { "type": "integer" }, "code": { "type": "string", "nullable": true }, "name": { "type": "string", "nullable": true } } },
        "field_officer": { "$ref": "#/components/schemas/Officer" }, "delivery_date": { "type": "string", "format": "date" },
        "expected_quantity_before": { "type": "string", "description": "Decimal quantity in the commodity contract unit" },
        "actual_quantity": { "type": "string" }, "remaining_quantity_after": { "type": "string" }, "base_unit_total": { "type": "string", "nullable": true },
        "units": { "type": "array", "items": { "$ref": "#/components/schemas/DeliveryUnit" } },
        "quality": { "type": "object", "properties": { "status": { "type": "string", "enum": ["NO_ISSUES", "ISSUES_REPORTED", "NOT_RECORDED"] }, "has_issues": { "type": "boolean" }, "issues": { "type": "array", "items": { "type": "string" } } } },
        "review_status": { "type": "string", "nullable": true }, "is_flagged": { "type": "boolean" }, "flag_reason": { "type": "string", "nullable": true },
        "notes": { "type": "string", "nullable": true }, "voice_transcript": { "type": "string", "nullable": true }, "ocr_text": { "type": "string", "nullable": true },
        "photo_meta": { "type": "object", "nullable": true, "additionalProperties": true }, "evidence": { "type": "array", "items": { "$ref": "#/components/schemas/Media" } }
      } }] },
      "DeliveryUnit": { "type": "object", "properties": {
        "unit_name": { "type": "string" }, "unit_type": { "type": "string", "nullable": true }, "unit_size": { "type": "string" },
        "unit_measure": { "type": "string" }, "quantity": { "type": "string" }, "base_unit_total": { "type": "string" }, "notes": { "type": "string", "nullable": true }
      } },
      "AnonymousReport": { "allOf": [{ "$ref": "#/components/schemas/Record" }, { "type": "object", "description": "No user ID or reporter name is exposed. Legacy records may have location despite location_consent=false.", "properties": {
        "record_type": { "type": "string", "enum": ["anonymous_report"] }, "description": { "type": "string" }, "location_consent": { "type": "boolean" },
        "location": { "type": "object", "nullable": true, "properties": {
          "latitude": { "type": "number", "minimum": -90, "maximum": 90 }, "longitude": { "type": "number", "minimum": -180, "maximum": 180 },
          "accuracy_metres": { "type": "number", "nullable": true }, "region": { "type": "string", "nullable": true }, "address": { "type": "string", "nullable": true }
        } },
        "media": { "$ref": "#/components/schemas/Media" }, "captured_at": { "type": "string", "format": "date-time", "nullable": true }
      } }] },
      "IssueReport": { "allOf": [{ "$ref": "#/components/schemas/Record" }, { "type": "object", "properties": {
        "record_type": { "type": "string", "enum": ["issue_report"] }, "field_officer": { "$ref": "#/components/schemas/Officer" },
        "page_url": { "type": "string" }, "description": { "type": "string" }, "status": { "type": "string", "enum": ["OPEN", "IN_PROGRESS", "RESOLVED", "CLOSED"] }, "media": { "$ref": "#/components/schemas/Media" }
      } }] },
      "Summary": { "type": "object", "required": ["generated_at", "feeds"], "properties": {
        "generated_at": { "type": "string", "format": "date-time" }, "feeds": { "type": "object", "required": ["deliveries", "anonymous-reports", "issue-reports"], "properties": {
          "deliveries": { "$ref": "#/components/schemas/FeedSummary" }, "anonymous-reports": { "$ref": "#/components/schemas/FeedSummary" }, "issue-reports": { "$ref": "#/components/schemas/FeedSummary" }
        } }
      } },
      "FeedSummary": { "type": "object", "required": ["total", "latest_updated_at"], "properties": { "total": { "type": "integer" }, "latest_updated_at": { "type": "string", "format": "date-time", "nullable": true } } }
    }
  }
}
