Skip to content

Admin API

Status: Final Version: 1.0 Last Updated: 2026-01-11


Table of Contents

  1. Purpose
  2. Authentication & Authorization
  3. Role-Based Access Control (RBAC)
  4. Submission Management
  5. Reporting Period Management
  6. Metric Catalog Management
  7. Report Generation
  8. Audit Log
  9. Task Queue & Notifications
  10. Pagination & Filtering
  11. Error Handling
  12. Implementation Guide
  13. Security Considerations
  14. Performance Guidelines

Purpose

Define REST endpoints for web dashboard users (Reviewers, Approvers, Admins) to manage configurations, review submissions, approve data, and generate reports.

The Admin API enables: - Reviewers: Validate and review field-submitted data - Approvers: Approve or reject reviewed submissions - Admins: Configure metrics, manage users, generate reports - All Roles: View audit logs and track changes


Authentication & Authorization

All Admin API endpoints require: 1. Valid JWT Token in Authorization: Bearer <token> header 2. Active User Account with appropriate role 3. Tenant Context enforced via tenant_id in JWT

See API Overview for authentication details.

Example Request:

GET /api/v1/admin/submissions HTTP/1.1
Host: api.esg-platform.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json


Role-Based Access Control (RBAC)

The Admin API enforces role-based permissions on all endpoints.

Role Hierarchy

Role Permissions Can Access
Admin Full system access All endpoints
Approver Approve submissions, lock periods, generate reports Submissions (R/W), Periods (R/W), Reports (R/W), Audit (R)
Reviewer Review submissions, add comments Submissions (R/W), Periods (R), Reports (R), Audit (R)
Collector Submit field data Collector API only (no Admin API access)

RBAC Enforcement

Each endpoint documents required permissions in format: Permission: {role}.{resource}.{action}

Example: - admin.submissions.approve - Requires Approver or Admin role - admin.metrics.create - Requires Admin role only - admin.submissions.view - Requires Reviewer, Approver, or Admin role

Error Response (403 Forbidden):

{
  "error": {
    "code": "AUTH_INSUFFICIENT_PERMISSIONS",
    "message": "User does not have permission to perform this action",
    "details": {
      "required_permission": "admin.submissions.approve",
      "user_role": "REVIEWER",
      "allowed_roles": ["APPROVER", "ADMIN"]
    },
    "request_id": "req_xyz789",
    "timestamp": "2026-01-11T14:30:00Z"
  }
}


Submission Management

Administrative endpoints for reviewing, approving, and managing metric submissions.

GET /api/v1/admin/submissions

Fetch submissions for review with advanced filtering and pagination.

Permission: admin.submissions.view (Reviewer, Approver, Admin)

Query Parameters:

Parameter Type Required Description Example
filter[state] string No Filter by submission state validated, processed, approved, rejected
filter[reporting_period_id] uuid No Filter by reporting period 550e8400-e29b-41d4-a716-446655440000
filter[site_id] uuid No Filter by site 650e8400-e29b-41d4-a716-446655440001
filter[metric_id] string No Filter by metric GRI_302_1_ELECTRICITY
filter[assigned_to] uuid No Filter by assigned reviewer 750e8400-e29b-41d4-a716-446655440002
filter[submitted_from] datetime No Filter submissions after date 2026-01-01T00:00:00Z
filter[submitted_to] datetime No Filter submissions before date 2026-01-31T23:59:59Z
filter[has_validation_warnings] boolean No Filter by validation warning presence true, false
search string No Full-text search in comments electricity
sort string No Sort field (prefix - for descending) -created_at, value, state
page integer No Page number (1-indexed) 1
pageSize integer No Items per page (max 100) 50

Response (200 OK):

{
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440010",
      "submissionUuid": "550e8400-e29b-41d4-a716-446655440011",
      "metric": {
        "metricId": "GRI_302_1_ELECTRICITY",
        "name": "Electricity Consumption",
        "unit": "MWh",
        "category": "ENERGY"
      },
      "site": {
        "id": "650e8400-e29b-41d4-a716-446655440001",
        "name": "Factory A - Shanghai",
        "country": "CN"
      },
      "reportingPeriod": {
        "id": "750e8400-e29b-41d4-a716-446655440003",
        "name": "Q1 2026",
        "startDate": "2026-01-01",
        "endDate": "2026-03-31"
      },
      "value": 1250.50,
      "unit": "MWh",
      "state": "VALIDATED",
      "validationResults": {
        "passed": true,
        "warnings": [
          {
            "type": "ANOMALY_DETECTION",
            "severity": "LOW",
            "message": "Value 15% higher than Q4 2025 average"
          }
        ],
        "errors": []
      },
      "evidence": [
        {
          "id": "850e8400-e29b-41d4-a716-446655440004",
          "filename": "utility_bill_jan2026.pdf",
          "fileSize": 245632,
          "contentType": "application/pdf",
          "uploadedAt": "2026-01-05T10:15:00Z"
        }
      ],
      "assignedTo": {
        "id": "950e8400-e29b-41d4-a716-446655440005",
        "name": "Jane Reviewer",
        "email": "jane.reviewer@example.com"
      },
      "submittedBy": {
        "id": "a50e8400-e29b-41d4-a716-446655440006",
        "name": "John Collector",
        "email": "john.collector@example.com"
      },
      "submittedAt": "2026-01-05T10:30:00Z",
      "reviewedAt": null,
      "approvedAt": null,
      "metadata": {
        "dataSource": "MANUAL_ENTRY",
        "collectorApp": "ESG Mobile v2.1.0"
      },
      "createdAt": "2026-01-05T10:30:00Z",
      "updatedAt": "2026-01-05T14:22:00Z"
    }
  ],
  "meta": {
    "page": 1,
    "pageSize": 50,
    "total": 328,
    "totalPages": 7
  },
  "links": {
    "first": "/api/v1/admin/submissions?page=1&pageSize=50",
    "prev": null,
    "next": "/api/v1/admin/submissions?page=2&pageSize=50",
    "last": "/api/v1/admin/submissions?page=7&pageSize=50"
  }
}

Error Responses: - 401 Unauthorized - Invalid or expired JWT token - 403 Forbidden - User lacks admin.submissions.view permission - 422 Unprocessable Entity - Invalid filter parameters - 429 Too Many Requests - Rate limit exceeded (500 req/hour)

Example Request:

curl -X GET "https://api.esg-platform.example.com/api/v1/admin/submissions?filter[state]=validated&filter[has_validation_warnings]=true&sort=-submitted_at&page=1&pageSize=20" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -H "Content-Type: application/json"


GET /api/v1/admin/submissions/{id}

Fetch detailed submission information including full audit trail.

Permission: admin.submissions.view (Reviewer, Approver, Admin)

Path Parameters: - id (uuid, required) - Submission ID

Response (200 OK):

{
  "id": "550e8400-e29b-41d4-a716-446655440010",
  "submissionUuid": "550e8400-e29b-41d4-a716-446655440011",
  "metric": {
    "metricId": "GRI_302_1_ELECTRICITY",
    "name": "Electricity Consumption",
    "unit": "MWh",
    "category": "ENERGY",
    "validationRules": [
      {"type": "DOMAIN", "rule": "min", "value": 0},
      {"type": "DOMAIN", "rule": "max", "value": 1000000}
    ]
  },
  "site": {
    "id": "650e8400-e29b-41d4-a716-446655440001",
    "name": "Factory A - Shanghai",
    "country": "CN",
    "organizationalBoundary": "OPERATIONAL_CONTROL"
  },
  "reportingPeriod": {
    "id": "750e8400-e29b-41d4-a716-446655440003",
    "name": "Q1 2026",
    "startDate": "2026-01-01",
    "endDate": "2026-03-31",
    "state": "OPEN"
  },
  "value": 1250.50,
  "unit": "MWh",
  "state": "VALIDATED",
  "validationResults": {
    "passed": true,
    "validatedAt": "2026-01-05T10:30:05Z",
    "warnings": [
      {
        "type": "ANOMALY_DETECTION",
        "severity": "LOW",
        "message": "Value 15% higher than Q4 2025 average (1087.45 MWh)",
        "context": {
          "previousValue": 1087.45,
          "percentageChange": 15.0
        }
      }
    ],
    "errors": []
  },
  "evidence": [
    {
      "id": "850e8400-e29b-41d4-a716-446655440004",
      "filename": "utility_bill_jan2026.pdf",
      "fileSize": 245632,
      "contentType": "application/pdf",
      "uploadedAt": "2026-01-05T10:15:00Z",
      "uploadedBy": {
        "id": "a50e8400-e29b-41d4-a716-446655440006",
        "name": "John Collector"
      },
      "downloadUrl": "/api/v1/collector/evidence/850e8400-e29b-41d4-a716-446655440004"
    }
  ],
  "assignedTo": {
    "id": "950e8400-e29b-41d4-a716-446655440005",
    "name": "Jane Reviewer",
    "email": "jane.reviewer@example.com",
    "role": "REVIEWER"
  },
  "reviewComments": [
    {
      "id": "b50e8400-e29b-41d4-a716-446655440007",
      "author": {
        "id": "950e8400-e29b-41d4-a716-446655440005",
        "name": "Jane Reviewer"
      },
      "comment": "Checked against utility bill - value confirmed",
      "createdAt": "2026-01-05T14:22:00Z"
    }
  ],
  "auditTrail": [
    {
      "action": "CREATED",
      "actor": {
        "id": "a50e8400-e29b-41d4-a716-446655440006",
        "name": "John Collector"
      },
      "timestamp": "2026-01-05T10:30:00Z",
      "metadata": {"source": "mobile_app"}
    },
    {
      "action": "VALIDATED",
      "actor": {"id": "system", "name": "Validation Engine"},
      "timestamp": "2026-01-05T10:30:05Z",
      "metadata": {"validationType": "AUTOMATED"}
    },
    {
      "action": "ASSIGNED",
      "actor": {"id": "system", "name": "Task Router"},
      "assignedTo": "950e8400-e29b-41d4-a716-446655440005",
      "timestamp": "2026-01-05T10:30:10Z"
    },
    {
      "action": "COMMENT_ADDED",
      "actor": {
        "id": "950e8400-e29b-41d4-a716-446655440005",
        "name": "Jane Reviewer"
      },
      "timestamp": "2026-01-05T14:22:00Z"
    }
  ],
  "submittedBy": {
    "id": "a50e8400-e29b-41d4-a716-446655440006",
    "name": "John Collector",
    "email": "john.collector@example.com",
    "role": "COLLECTOR"
  },
  "submittedAt": "2026-01-05T10:30:00Z",
  "reviewedAt": null,
  "approvedAt": null,
  "metadata": {
    "dataSource": "MANUAL_ENTRY",
    "collectorApp": "ESG Mobile v2.1.0",
    "location": {"lat": 31.2304, "lng": 121.4737}
  },
  "createdAt": "2026-01-05T10:30:00Z",
  "updatedAt": "2026-01-05T14:22:00Z"
}

Error Responses: - 404 Not Found - Submission not found or not accessible to this tenant - 403 Forbidden - User lacks permission to view this submission


POST /api/v1/admin/submissions/{id}/approve

Approve a validated submission, marking it as approved for reporting.

Permission: admin.submissions.approve (Approver, Admin only)

Path Parameters: - id (uuid, required) - Submission ID

Request Body:

{
  "comment": "Data verified against utility bills. Jan 2026 electricity consumption aligns with invoice #INV-2026-0145.",
  "metadata": {
    "invoiceNumber": "INV-2026-0145",
    "verificationMethod": "INVOICE_MATCH"
  }
}

Response (200 OK):

{
  "id": "550e8400-e29b-41d4-a716-446655440010",
  "state": "APPROVED",
  "approvedBy": {
    "id": "c50e8400-e29b-41d4-a716-446655440008",
    "name": "Jane Approver",
    "email": "jane.approver@example.com"
  },
  "approvedAt": "2026-01-10T14:00:00Z",
  "approvalComment": "Data verified against utility bills. Jan 2026 electricity consumption aligns with invoice #INV-2026-0145.",
  "updatedAt": "2026-01-10T14:00:00Z"
}

Error Responses: - 400 Bad Request - Submission not in approvable state (must be VALIDATED or PROCESSED)

{
  "error": {
    "code": "STATE_INVALID_TRANSITION",
    "message": "Cannot approve submission in current state",
    "details": {
      "currentState": "DRAFT",
      "requiredStates": ["VALIDATED", "PROCESSED"]
    },
    "request_id": "req_xyz789",
    "timestamp": "2026-01-10T14:00:00Z"
  }
}
- 403 Forbidden - User lacks admin.submissions.approve permission - 404 Not Found - Submission not found - 409 Conflict - Submission already approved by another approver

State Transition: VALIDATEDAPPROVED or PROCESSEDAPPROVED

Side Effects: - Creates audit log entry - Triggers notification to submitter - Updates reporting period completion percentage - Unlocks dependent calculations (if any)


POST /api/v1/admin/submissions/{id}/reject

Reject a submission with detailed feedback for the collector to address.

Permission: admin.submissions.reject (Reviewer, Approver, Admin)

Path Parameters: - id (uuid, required) - Submission ID

Request Body:

{
  "reason": "Value appears significantly higher than historical average. Please verify meter reading and unit conversion.",
  "requiredCorrections": [
    "Verify electricity meter reading from January 2026",
    "Confirm unit is MWh (not kWh)",
    "Attach clear photo of meter display",
    "Cross-check against utility invoice"
  ],
  "severity": "MAJOR",
  "metadata": {
    "historicalAverage": 1087.45,
    "submittedValue": 1250.50,
    "percentageDeviation": 15.0
  }
}

Response (200 OK):

{
  "id": "550e8400-e29b-41d4-a716-446655440010",
  "state": "REJECTED",
  "rejectedBy": {
    "id": "950e8400-e29b-41d4-a716-446655440005",
    "name": "Jane Reviewer",
    "email": "jane.reviewer@example.com"
  },
  "rejectedAt": "2026-01-10T14:30:00Z",
  "reviewerFeedback": {
    "reason": "Value appears significantly higher than historical average. Please verify meter reading and unit conversion.",
    "requiredCorrections": [
      "Verify electricity meter reading from January 2026",
      "Confirm unit is MWh (not kWh)",
      "Attach clear photo of meter display",
      "Cross-check against utility invoice"
    ],
    "severity": "MAJOR"
  },
  "updatedAt": "2026-01-10T14:30:00Z"
}

Error Responses: - 400 Bad Request - Submission not in rejectable state - 403 Forbidden - User lacks permission - 404 Not Found - Submission not found - 422 Unprocessable Entity - Missing required rejection reason

State Transition: VALIDATEDREJECTED or PROCESSEDREJECTED

Side Effects: - Creates audit log entry - Sends notification to submitter with feedback - Unlocks submission for editing (submitter can update and resubmit) - Decrements reporting period completion percentage


POST /api/v1/admin/submissions/{id}/comments

Add a review comment to a submission without changing its state.

Permission: admin.submissions.comment (Reviewer, Approver, Admin)

Path Parameters: - id (uuid, required) - Submission ID

Request Body:

{
  "comment": "Waiting for clarification from site manager on meter replacement in mid-January. May explain higher reading.",
  "visibility": "INTERNAL"
}

Response (201 Created):

{
  "id": "d50e8400-e29b-41d4-a716-446655440009",
  "submissionId": "550e8400-e29b-41d4-a716-446655440010",
  "author": {
    "id": "950e8400-e29b-41d4-a716-446655440005",
    "name": "Jane Reviewer"
  },
  "comment": "Waiting for clarification from site manager on meter replacement in mid-January. May explain higher reading.",
  "visibility": "INTERNAL",
  "createdAt": "2026-01-10T15:00:00Z"
}

Visibility Options: - INTERNAL - Visible only to reviewers and approvers - PUBLIC - Visible to submitter as well


POST /api/v1/admin/submissions/bulk-assign

Bulk assign submissions to reviewers for efficient workload distribution.

Permission: admin.submissions.assign (Approver, Admin)

Request Body:

{
  "submissionIds": [
    "550e8400-e29b-41d4-a716-446655440010",
    "550e8400-e29b-41d4-a716-446655440011",
    "550e8400-e29b-41d4-a716-446655440012"
  ],
  "assignToUserId": "950e8400-e29b-41d4-a716-446655440005",
  "priority": "HIGH",
  "dueDate": "2026-01-15T23:59:59Z"
}

Response (200 OK):

{
  "assignedCount": 3,
  "failedAssignments": [],
  "assignedTo": {
    "id": "950e8400-e29b-41d4-a716-446655440005",
    "name": "Jane Reviewer",
    "currentWorkload": 12
  }
}


Reporting Period Management

Endpoints for managing reporting periods (quarters, fiscal years) and their lifecycles.

GET /api/v1/admin/reporting-periods

List all reporting periods with completion statistics.

Permission: admin.periods.view (Reviewer, Approver, Admin)

Query Parameters: - filter[state] (string) - Filter by state: open, locked, archived - filter[year] (integer) - Filter by year: 2026 - sort (string) - Sort by: start_date, -end_date, name

Response (200 OK):

{
  "data": [
    {
      "id": "750e8400-e29b-41d4-a716-446655440003",
      "name": "Q1 2026",
      "description": "First Quarter 2026 Reporting Period",
      "startDate": "2026-01-01",
      "endDate": "2026-03-31",
      "state": "OPEN",
      "completionPercentage": 68.5,
      "submissionsCount": {
        "total": 500,
        "draft": 25,
        "validated": 98,
        "processed": 50,
        "approved": 327,
        "rejected": 0
      },
      "mandatoryMetricsCount": {
        "required": 120,
        "submitted": 98,
        "approved": 82
      },
      "slaDaysRemaining": 15,
      "lockedAt": null,
      "lockedBy": null,
      "contentHash": null,
      "createdAt": "2025-12-01T00:00:00Z",
      "updatedAt": "2026-01-10T16:00:00Z"
    },
    {
      "id": "850e8400-e29b-41d4-a716-446655440012",
      "name": "Q4 2025",
      "description": "Fourth Quarter 2025 Reporting Period",
      "startDate": "2025-10-01",
      "endDate": "2025-12-31",
      "state": "LOCKED",
      "completionPercentage": 100.0,
      "submissionsCount": {
        "total": 480,
        "approved": 480,
        "draft": 0,
        "validated": 0,
        "processed": 0,
        "rejected": 0
      },
      "mandatoryMetricsCount": {
        "required": 120,
        "submitted": 120,
        "approved": 120
      },
      "slaDaysRemaining": 0,
      "lockedAt": "2025-12-15T16:00:00Z",
      "lockedBy": {
        "id": "c50e8400-e29b-41d4-a716-446655440008",
        "name": "Jane Approver"
      },
      "contentHash": "sha256:a3c4f8b912e5d7f6c3b2a1e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c9",
      "createdAt": "2025-09-01T00:00:00Z",
      "updatedAt": "2025-12-15T16:00:00Z"
    }
  ],
  "meta": {
    "page": 1,
    "pageSize": 50,
    "total": 8,
    "totalPages": 1
  }
}


GET /api/v1/admin/reporting-periods/{id}

Get detailed information about a specific reporting period.

Permission: admin.periods.view (Reviewer, Approver, Admin)

Response (200 OK):

{
  "id": "750e8400-e29b-41d4-a716-446655440003",
  "name": "Q1 2026",
  "description": "First Quarter 2026 Reporting Period",
  "startDate": "2026-01-01",
  "endDate": "2026-03-31",
  "state": "OPEN",
  "completionPercentage": 68.5,
  "submissionsCount": {
    "total": 500,
    "draft": 25,
    "validated": 98,
    "processed": 50,
    "approved": 327,
    "rejected": 0
  },
  "mandatoryMetricsCount": {
    "required": 120,
    "submitted": 98,
    "approved": 82,
    "missingMetrics": [
      "GRI_305_1_SCOPE1_CO2",
      "GRI_306_3_WASTE_GENERATED"
    ]
  },
  "siteCoverage": {
    "totalSites": 15,
    "sitesReporting": 12,
    "sitesComplete": 8,
    "sitesMissing": [
      {"id": "site_003", "name": "Factory C - Mumbai"},
      {"id": "site_007", "name": "Warehouse G - Dubai"}
    ]
  },
  "slaDaysRemaining": 15,
  "lockedAt": null,
  "lockedBy": null,
  "contentHash": null,
  "createdAt": "2025-12-01T00:00:00Z",
  "updatedAt": "2026-01-10T16:00:00Z"
}


POST /api/v1/admin/reporting-periods

Create a new reporting period.

Permission: admin.periods.create (Admin only)

Request Body:

{
  "name": "Q2 2026",
  "description": "Second Quarter 2026 Reporting Period",
  "startDate": "2026-04-01",
  "endDate": "2026-06-30",
  "reportingDeadline": "2026-07-15",
  "mandatoryMetrics": [
    "GRI_302_1_ELECTRICITY",
    "GRI_305_1_SCOPE1_CO2",
    "GRI_306_3_WASTE_GENERATED"
  ]
}

Response (201 Created):

{
  "id": "950e8400-e29b-41d4-a716-446655440013",
  "name": "Q2 2026",
  "description": "Second Quarter 2026 Reporting Period",
  "startDate": "2026-04-01",
  "endDate": "2026-06-30",
  "state": "OPEN",
  "completionPercentage": 0.0,
  "createdAt": "2026-01-10T16:30:00Z"
}


POST /api/v1/admin/reporting-periods/{id}/lock

Lock a reporting period to prevent further modifications. This creates an immutable snapshot for compliance.

Permission: admin.periods.lock (Approver, Admin)

Path Parameters: - id (uuid, required) - Reporting period ID

Request Body:

{
  "justification": "All Q4 2025 data reviewed and approved for FY2025 annual sustainability report. Ready for external audit.",
  "reviewedBy": [
    "950e8400-e29b-41d4-a716-446655440005",
    "c50e8400-e29b-41d4-a716-446655440008"
  ]
}

Response (200 OK):

{
  "id": "850e8400-e29b-41d4-a716-446655440012",
  "name": "Q4 2025",
  "state": "LOCKED",
  "lockedAt": "2026-01-10T16:45:00Z",
  "lockedBy": {
    "id": "c50e8400-e29b-41d4-a716-446655440008",
    "name": "Jane Approver",
    "email": "jane.approver@example.com"
  },
  "lockJustification": "All Q4 2025 data reviewed and approved for FY2025 annual sustainability report. Ready for external audit.",
  "contentHash": "sha256:a3c4f8b912e5d7f6c3b2a1e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c9",
  "updatedAt": "2026-01-10T16:45:00Z"
}

Prerequisites: - All mandatory metrics must be submitted - No submissions in DRAFT or VALIDATED state (must be APPROVED or REJECTED) - At least 95% of submissions must be APPROVED

Error Responses: - 400 Bad Request - Prerequisites not met

{
  "error": {
    "code": "STATE_PREREQUISITES_NOT_MET",
    "message": "Cannot lock period: prerequisites not met",
    "details": {
      "unreviewedSubmissions": 12,
      "missingMandatoryMetrics": ["GRI_305_1_SCOPE1_CO2"],
      "approvalRate": 92.5,
      "minimumApprovalRate": 95.0
    },
    "request_id": "req_xyz789",
    "timestamp": "2026-01-10T16:45:00Z"
  }
}

State Transition: OPENLOCKED

Side Effects: - Generates SHA-256 content hash of all approved submissions - Prevents any further modifications to submissions in this period - Triggers compliance report generation - Sends notifications to stakeholders - Creates immutable audit log entry


POST /api/v1/admin/reporting-periods/{id}/unlock

Unlock a locked period for restatement (requires strong justification).

Permission: admin.periods.unlock (Admin only)

Request Body:

{
  "justification": "Material error discovered in GRI 305-1 calculation methodology. Restatement required per GRI Standards.",
  "approvedBy": "CFO Jane Smith",
  "restatementReason": "CALCULATION_ERROR",
  "affectedMetrics": ["GRI_305_1_SCOPE1_CO2"]
}

Response (200 OK):

{
  "id": "850e8400-e29b-41d4-a716-446655440012",
  "name": "Q4 2025",
  "state": "OPEN",
  "version": 2,
  "previousContentHash": "sha256:a3c4f8b912e5d7f6c3b2a1e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c9",
  "restatement": {
    "reason": "CALCULATION_ERROR",
    "justification": "Material error discovered in GRI 305-1 calculation methodology. Restatement required per GRI Standards.",
    "approvedBy": "CFO Jane Smith",
    "unlockedAt": "2026-01-10T17:00:00Z",
    "unlockedBy": {
      "id": "admin_001",
      "name": "System Admin"
    }
  }
}

State Transition: LOCKEDOPEN (version incremented)



Metric Catalog Management

Administrative endpoints for managing metric templates and custom KPIs (Admin role only).

GET /api/v1/admin/metrics

List all metric templates with validation rules and framework mappings.

Permission: admin.metrics.view (All roles can view; Admin can modify)

Query Parameters: - filter[category] (string) - Filter by category: ENERGY, EMISSIONS, WATER, WASTE, SOCIAL, GOVERNANCE - filter[is_mandatory] (boolean) - Filter mandatory metrics - filter[framework] (string) - Filter by framework: GRI, SASB, TCFD, CUSTOM - search (string) - Search metric name or ID

Response (200 OK):

{
  "data": [
    {
      "metricId": "GRI_302_1_ELECTRICITY",
      "name": "Electricity Consumption",
      "description": "Total electricity consumed by the organization from all sources",
      "unit": "MWh",
      "dataType": "numeric",
      "category": "ENERGY",
      "isMandatory": true,
      "isCustomKpi": false,
      "dimensionality": "site",
      "framework": {
        "name": "GRI",
        "version": "2021",
        "disclosure": {
          "code": "302-1",
          "name": "Energy consumption within the organization"
        }
      },
      "validationRules": [
        {"type": "DOMAIN", "rule": "min", "value": 0, "message": "Value cannot be negative"},
        {"type": "DOMAIN", "rule": "max", "value": 1000000, "message": "Value exceeds reasonable maximum"},
        {"type": "REQUIRED_EVIDENCE", "minimumFiles": 1, "acceptedTypes": ["pdf", "jpg", "png"]}
      ],
      "createdAt": "2025-01-01T00:00:00Z",
      "updatedAt": "2025-06-15T10:00:00Z"
    }
  ],
  "meta": {
    "page": 1,
    "pageSize": 50,
    "total": 145,
    "totalPages": 3
  }
}


POST /api/v1/admin/metrics

Create a custom KPI metric for organization-specific tracking.

Permission: admin.metrics.create (Admin only)

Request Body:

{
  "metricId": "CUSTOM_WATER_RECYCLING",
  "name": "Water Recycling Rate",
  "description": "Percentage of water recycled and reused in manufacturing processes",
  "unit": "%",
  "dataType": "numeric",
  "category": "WATER",
  "isCustomKpi": true,
  "dimensionality": "site",
  "validationRules": [
    {"type": "DOMAIN", "rule": "min", "value": 0},
    {"type": "DOMAIN", "rule": "max", "value": 100},
    {"type": "REQUIRED_EVIDENCE", "minimumFiles": 1}
  ],
  "calculationMethod": "(recycled_water_volume / total_water_intake) * 100"
}

Response (201 Created):

{
  "metricId": "CUSTOM_WATER_RECYCLING",
  "name": "Water Recycling Rate",
  "unit": "%",
  "category": "WATER",
  "isCustomKpi": true,
  "createdAt": "2026-01-10T17:30:00Z"
}


Report Generation

Asynchronous report generation endpoints with job tracking.

POST /api/v1/admin/reports/generate

Queue a report generation job for a locked reporting period.

Permission: admin.reports.generate (Approver, Admin)

Request Body:

{
  "reportingPeriodId": "850e8400-e29b-41d4-a716-446655440012",
  "reportType": "gri_sustainability_report",
  "format": "pdf",
  "includeSections": ["environmental", "social", "governance"],
  "options": {
    "includeAuditTrail": true,
    "includeEvidence": false,
    "language": "en",
    "template": "corporate_standard"
  }
}

Report Types: - gri_sustainability_report - Full GRI sustainability report - carbon_footprint_report - Carbon emissions summary - data_quality_report - Validation and completeness metrics - custom_kpi_dashboard - Custom KPI summary - human_capital_report - Human capital metrics (GRI 405-1, GRI 401, Employee Age) - environment_report - Environmental metrics (G.1-G.10: Production, Materials, Energy, Water, Air Emissions, Waste, TSF, Rehabilitation, Incidents)

Response (202 Accepted):

{
  "jobId": "a50e8400-e29b-41d4-a716-446655440020",
  "status": "QUEUED",
  "estimatedCompletion": "2026-01-10T18:00:00Z",
  "statusUrl": "/api/v1/admin/reports/jobs/a50e8400-e29b-41d4-a716-446655440020",
  "queuePosition": 3
}

Error Responses: - 400 Bad Request - Period not locked or report prerequisites not met - 403 Forbidden - User lacks permission - 429 Too Many Requests - Rate limit: 10 reports/hour per tenant


GET /api/v1/admin/reports/jobs/{jobId}

Check status of a report generation job.

Permission: admin.reports.view (Approver, Admin)

Response (200 OK) - In Progress:

{
  "jobId": "a50e8400-e29b-41d4-a716-446655440020",
  "status": "PROCESSING",
  "progress": 45,
  "currentStep": "Generating PDF from template",
  "estimatedCompletion": "2026-01-10T18:00:00Z",
  "createdAt": "2026-01-10T17:45:00Z"
}

Response (200 OK) - Completed:

{
  "jobId": "a50e8400-e29b-41d4-a716-446655440020",
  "status": "COMPLETED",
  "progress": 100,
  "downloadUrl": "/api/v1/admin/reports/download/gri_report_q4_2025.pdf",
  "fileSize": 2457600,
  "expiresAt": "2026-01-17T17:45:00Z",
  "completedAt": "2026-01-10T17:58:00Z"
}

Job Statuses: - QUEUED - Waiting in queue - PROCESSING - Currently generating - COMPLETED - Ready for download - FAILED - Generation failed (see error field)


GET /api/v1/admin/reports/download/{filename}

Download a generated report file.

Permission: admin.reports.download (Approver, Admin)

Response (200 OK): - Content-Type: application/pdf or application/vnd.openxmlformats-officedocument.spreadsheetml.sheet - Content-Disposition: attachment; filename="gri_report_q4_2025.pdf" - Binary file content

Error Responses: - 404 Not Found - File not found or expired - 410 Gone - File expired (7-day retention)


Human Capital Report Generation

Generate human capital reports covering GRI 405-1 (Employee Demographics), GRI 401 (Employment Type and Turnover), and Employee Age distribution.

POST /api/v1/admin/reports/human-capital/generate

Queue a human capital report generation job.

Permission: admin.reports.generate (Approver, Admin)

Request Body:

{
  "reportingPeriodId": "850e8400-e29b-41d4-a716-446655440012",
  "format": "xlsx",
  "filters": {
    "organisationId": "org_uuid",
    "siteIds": ["site_uuid_1", "site_uuid_2"],
    "employmentLevels": ["EXECUTIVE", "SALARIED_STAFF_NON_NEC", "WAGED_STAFF_NEC"],
    "employmentTypes": ["PERMANENT", "FIXED_TERM"],
    "genders": ["MALE", "FEMALE"],
    "ageGroups": ["UNDER_30", "AGED_30_50", "OVER_50"]
  },
  "options": {
    "includeDemographics": true,
    "includeEmploymentType": true,
    "includeAgeDistribution": true,
    "aggregationLevel": "site",
    "language": "en"
  }
}

Request Parameters:

Field Type Required Description
reportingPeriodId uuid Yes Reporting period (fiscal year) for the report
format string Yes Export format: csv or xlsx
filters object No Optional filters to scope the report
filters.organisationId uuid Yes* Organisation ID (required unless multi-org tenant)
filters.siteIds array[uuid] No Specific sites to include (omit for all sites)
filters.employmentLevels array[string] No Filter E.1 by employment level
filters.employmentTypes array[string] No Filter E.2 by employment type
filters.genders array[string] No Filter E.2 recruitment by gender
filters.ageGroups array[string] No Filter E.3 by age group
options object No Report generation options
options.includeDemographics boolean No Include E.1 (GRI 405-1) section (default: true)
options.includeEmploymentType boolean No Include E.2 (GRI 401) section (default: true)
options.includeAgeDistribution boolean No Include E.3 (Employee Age) section (default: true)
options.aggregationLevel string No site or organisation (default: organisation)
options.language string No Report language: en (default), es, fr

Response (202 Accepted):

{
  "jobId": "b60e8400-e29b-41d4-a716-446655440030",
  "status": "QUEUED",
  "estimatedCompletion": "2026-01-17T22:10:00Z",
  "statusUrl": "/api/v1/admin/reports/jobs/b60e8400-e29b-41d4-a716-446655440030",
  "queuePosition": 1,
  "reportType": "human_capital_report",
  "format": "xlsx"
}

Error Responses: - 400 Bad Request - Invalid filters or missing required parameters

{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Invalid report generation request",
    "details": {
      "filters.employmentLevels": "Invalid employment level: MANAGER. Allowed values: EXECUTIVE, SALARIED_STAFF_NON_NEC, WAGED_STAFF_NEC"
    },
    "request_id": "req_xyz789",
    "timestamp": "2026-01-17T22:00:00Z"
  }
}
- 403 Forbidden - User lacks permission - 404 Not Found - Reporting period not found - 429 Too Many Requests - Rate limit: 10 reports/hour per tenant


Export Format Specifications

CSV Export

File Naming Convention:

{Organisation}_{ReportType}_{FiscalYear}_{ExportDate}.csv

Example: Eureka_Gold_Human_Capital_FY2025_2026-01-17.csv

Structure: Single CSV file with all sections concatenated, using section headers to separate E.1, E.2, and E.3.

E.1 Columns (Employee Demographics):

Employment_Level,Quarter,Male,Female,From_Local_Community,Percent_Male,Percent_Female,Percent_From_Local_Community,Total_Employees,Reporting_Period,Organisation_ID,Site_ID,Collection_Date

E.2 Columns (Employment Type and Turnover):

Metric_Type,Employment_Type_Or_Gender,Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec,Aggregate_Value,Aggregate_Type,Reporting_Period,Organisation_ID,Site_ID

E.3 Columns (Employee Age):

Age_Group,Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec,Annual_Total,Reporting_Period,Organisation_ID,Site_ID

XLSX Export

File Naming Convention:

{Organisation}_{ReportType}_{FiscalYear}_{ExportDate}.xlsx

Example: Eureka_Gold_Human_Capital_FY2025_2026-01-17.xlsx

Sheet Structure:

Sheet # Sheet Name Content Rows
1 E.1 Demographics GRI 405-1 quarterly employee demographics by employment level 1 header + N data rows (3 employment levels × 4 quarters)
2 E.2 Employment & Turnover GRI 401 monthly employment type, recruitment, departures, casual workers 4 sub-tables with headers
3 E.3 Age Distribution Monthly employee age group distribution 1 header + 3 age groups + 1 total row
4 Reference Data Employment level definitions, age group ranges, data collection notes Metadata

Sheet 1 - E.1 Demographics:

| Employment Level | Quarter | Male | Female | From Local Community | % Male | % Female | % From Local Community | Total Employees | Site | Collection Date |

Sheet 2 - E.2 Employment & Turnover:

Four sub-tables separated by blank rows:

  1. Permanent vs. Fixed Term Employment

    | Employment Type | Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec | Average |
    

  2. New Recruitments (Permanent Staff by Gender)

    | Gender | Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec | Average |
    

  3. Departures (Total Permanent Staff)

    | Metric | Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec | Total |
    

  4. Casual Workers

    | Metric | Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec | Total |
    

Sheet 3 - E.3 Age Distribution:

| Age Group | Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec | Annual Total |

Sheet 4 - Reference Data:

Employment Levels:
- EXECUTIVE: Senior management and executives
- SALARIED_STAFF_NON_NEC: Salaried employees not covered by union agreements
- WAGED_STAFF_NEC: Hourly/waged employees covered by union agreements (NEC)

Age Groups:
- UNDER_30: Employees under 30 years old
- AGED_30_50: Employees aged 30-50 years
- OVER_50: Employees over 50 years old

Collection Frequency:
- E.1: Quarterly (end of quarter snapshot)
- E.2: Monthly (end of month values)
- E.3: Monthly (end of month snapshot)

Notes:
- Percentages in E.1 are calculated as (Count / Total) × 100
- Averages in E.2 are calculated as (Sum of monthly values) / (Number of months with data)
- Totals in E.2 (Departures, Casual Workers) represent annual sum
- Annual Total in E.3 represents unique employee counts (not simple sum of monthly totals)


Filter Behavior

Required Filters: - reportingPeriodId: Fiscal year for the report

Optional Filters:

  1. organisationId: Scope to specific organisation (required for multi-org tenants)
  2. siteIds: Array of site UUIDs
  3. Omit: Include all sites
  4. Provide: Include only specified sites
  5. Applies to: E.1 (site-level demographics for Salaried/Waged), E.2, E.3
  6. employmentLevels: Filter E.1 demographics
  7. Values: EXECUTIVE, SALARIED_STAFF_NON_NEC, WAGED_STAFF_NEC
  8. Example: ["EXECUTIVE"] returns only executive demographics
  9. employmentTypes: Filter E.2 employment type table
  10. Values: PERMANENT, FIXED_TERM
  11. Example: ["PERMANENT"] returns only permanent employee data
  12. genders: Filter E.2 recruitment table
  13. Values: MALE, FEMALE
  14. Example: ["FEMALE"] returns only female recruitment data
  15. ageGroups: Filter E.3 age distribution
  16. Values: UNDER_30, AGED_30_50, OVER_50
  17. Example: ["UNDER_30", "AGED_30_50"] excludes over-50 employees

Filter Application Logic: - Filters are applied with AND logic within a category (e.g., siteIds) - Multiple filters across categories use AND logic (e.g., siteIds AND employmentLevels) - Empty array means "no filter" (include all values) - Null/undefined filter field means "no filter" (include all values)

Example Filtered Request:

{
  "reportingPeriodId": "period_uuid",
  "format": "xlsx",
  "filters": {
    "siteIds": ["site_001", "site_002"],
    "employmentLevels": ["WAGED_STAFF_NEC"]
  }
}
Result: Report includes only waged staff (NEC) data from site_001 and site_002.


Aggregation Levels

Organisation-level Aggregation: - Default behavior - Aggregates all site-level data to organisation totals - E.1: Sum of site headcounts by employment level - E.2: Sum of site employment type, recruitment, departures, casual workers - E.3: Sum of site age group headcounts

Site-level Aggregation: - Set options.aggregationLevel = "site" - Returns separate rows/sheets for each site - E.1: One row per employment level per quarter per site - E.2: One table per site - E.3: One table per site - Useful for multi-site organisations tracking site-specific HR metrics

XLSX Sheet Structure (Site-level): - Sheet 1: E.1 Demographics (Site A) - Sheet 2: E.1 Demographics (Site B) - Sheet 3: E.2 Employment & Turnover (Site A) - Sheet 4: E.2 Employment & Turnover (Site B) - Sheet 5: E.3 Age Distribution (Site A) - Sheet 6: E.3 Age Distribution (Site B) - Sheet 7: Reference Data


Data Validation and Quality Checks

Before report generation, the API validates:

  1. Data Completeness:
  2. E.1: All 4 quarters have data (or missing quarters are future quarters)
  3. E.2: All 12 months have data (or missing months are future months)
  4. E.3: All 12 months have data (or missing months are future months)
  5. Warning if completeness < 80% for current fiscal year

  6. Data Consistency:

  7. E.1: Male + Female = Total for each row
  8. E.1: Percentages sum to 100% (Male% + Female% = 100%)
  9. E.1: From Local Community ≤ Total
  10. E.3: Under 30 + Aged 30-50 + Over 50 = Monthly Total
  11. Error if validation fails

  12. PII Protection:

  13. Minimum 5 employees per reported category
  14. Warning if any category has < 5 employees (suggest aggregation or suppression)
  15. No individual employee identifiers in export

  16. Calculation Accuracy:

  17. E.1: Percentages rounded to 2 decimal places
  18. E.2: Averages rounded to nearest integer
  19. E.2: Totals match sum of monthly values

Validation Response (if failed):

{
  "error": {
    "code": "DATA_QUALITY_VALIDATION_FAILED",
    "message": "Report data failed quality checks",
    "details": {
      "completeness": {
        "e1": 100.0,
        "e2": 75.0,
        "e3": 83.3
      },
      "validation_errors": [
        {
          "section": "E.1",
          "row": "Executive, Q1 2025",
          "issue": "Male + Female (7) does not equal Total (8)",
          "severity": "ERROR"
        },
        {
          "section": "E.2",
          "issue": "Missing data for October, November, December",
          "severity": "WARNING"
        }
      ],
      "pii_warnings": [
        {
          "section": "E.1",
          "category": "Executive, From Local Community",
          "count": 2,
          "recommendation": "Suppress or aggregate to prevent identification"
        }
      ]
    },
    "request_id": "req_xyz789",
    "timestamp": "2026-01-17T22:05:00Z"
  }
}


Performance Considerations

Estimated Generation Times: - Small organisation (1-3 sites, <1000 employees): 10-30 seconds - Medium organisation (4-10 sites, 1000-5000 employees): 30-90 seconds - Large organisation (10+ sites, 5000+ employees): 90-300 seconds

Resource Usage: - Memory: ~50MB per report generation job - Database queries: ~20-50 queries (depends on filters and aggregation level) - File size: CSV ~50KB-500KB, XLSX ~100KB-2MB

Optimization Tips: - Use site-level filters to reduce data volume - Generate reports during off-peak hours for large organisations - Cache intermediate aggregations for frequently requested reports


Environment Report Generation

Generate environment reports covering G.1-G.10 sections (Production, Materials Consumption, Energy Usage, Water Management, Air Emissions, Waste, TSF Management, Rehabilitation, Environmental Incidents).

POST /api/v1/admin/reports/environment/generate

Queue an environment report generation job.

Permission: admin.reports.generate (Approver, Admin)

Request Body:

{
  "reportingPeriodId": "850e8400-e29b-41d4-a716-446655440012",
  "format": "xlsx",
  "filters": {
    "organisationId": "org_uuid",
    "siteIds": ["site_uuid_1", "site_uuid_2"]
  },
  "options": {
    "includeProduction": true,
    "includeMaterials": true,
    "includeEnergy": true,
    "includeWater": true,
    "includeAirEmissions": true,
    "includeWaste": true,
    "includeTSF": true,
    "includeRehabilitation": true,
    "includeIncidents": true,
    "aggregationLevel": "site",
    "language": "en"
  }
}

Request Parameters:

Field Type Required Description
reportingPeriodId uuid Yes Reporting period (fiscal year) for the report
format string Yes Export format: csv or xlsx
filters object No Optional filters to scope the report
filters.organisationId uuid Yes* Organisation ID (required unless single-org tenant)
filters.siteIds array[uuid] No Specific sites to include (omit for all sites)
options object No Report generation options
options.includeProduction boolean No Include G.1 (Production) section (default: true)
options.includeMaterials boolean No Include G.2 (Materials Consumption) section (default: true)
options.includeEnergy boolean No Include G.3 (Energy Usage) section (default: true)
options.includeWater boolean No Include G.4 (Water Management) section (default: true)
options.includeAirEmissions boolean No Include G.5 (Air Emissions) section (default: true)
options.includeWaste boolean No Include G.6-G.7 (Waste) sections (default: true)
options.includeTSF boolean No Include G.8 (TSF Management) section (default: true)
options.includeRehabilitation boolean No Include G.9 (Rehabilitation) section (default: true)
options.includeIncidents boolean No Include G.10 (Environmental Incidents) section (default: true)
options.aggregationLevel string No site or organisation (default: organisation)
options.language string No Report language: en (default), es, fr

Response (202 Accepted):

{
  "jobId": "c70e8400-e29b-41d4-a716-446655440040",
  "status": "QUEUED",
  "estimatedCompletion": "2026-01-18T03:10:00Z",
  "statusUrl": "/api/v1/admin/reports/jobs/c70e8400-e29b-41d4-a716-446655440040",
  "queuePosition": 1,
  "reportType": "environment_report",
  "format": "xlsx"
}

Error Responses: - 400 Bad Request - Invalid filters or missing required parameters - 403 Forbidden - User lacks permission - 404 Not Found - Reporting period not found - 429 Too Many Requests - Rate limit: 10 reports/hour per tenant


Export Format Specifications

CSV Export

File Naming Convention:

{Organisation}_Environment_{FiscalYear}_{ExportDate}.csv

Example: Eureka_Gold_Environment_FY2025_2026-01-18.csv

Structure: Single CSV file with all sections concatenated, using section headers to separate G.1-G.10.

XLSX Export

File Naming Convention:

{Organisation}_Environment_{FiscalYear}_{ExportDate}.xlsx

Example: Eureka_Gold_Environment_FY2025_2026-01-18.xlsx

Sheet Structure:

Sheet # Sheet Name Content Rows
1 G.1 Production Monthly crushed ore, milled ore, gold produced with totals/averages 1 header + 12 months + 2 summary rows
2 G.2 Materials Monthly consumables (activated carbon, cyanide, etc.) with per-tonne intensity 6 material types × 12 months + summary
3 G.3 Energy Monthly electricity (grid/generator), fuel consumption, consumption rates Multiple tables
4 G.4 Water Monthly abstraction/recycled/consumed + quarterly quality monitoring Multiple tables
5 G.5 Air Emissions Quarterly pollutant measurements by monitoring area with quality bands 4 pollutants × N areas × 4 quarters
6 G.6 Non-Mineralised Waste Monthly waste by type with disposal methods and specific waste per tonne N waste types × 12 months
7 G.7 Mineralised Waste Monthly waste rock and tailings 1 header + 12 months
8 G.8 TSF Management Monthly slurry density, freeboard, surface area, rate of rise 1 header + 12 months
9 G.9 Rehabilitation Quarterly rehabilitation activities log Variable (activity records)
10 G.10 Incidents Quarterly environmental incidents log Variable (incident records)
11 Reference Data Metric definitions, units, quality band definitions Metadata

Filter Behavior

Required Filters: - reportingPeriodId: Fiscal year for the report

Optional Filters:

  1. organisationId: Scope to specific organisation (required for multi-org tenants)
  2. siteIds: Array of site UUIDs
  3. Omit: Include all sites
  4. Provide: Include only specified sites
  5. Applies to: All G.1-G.10 sections

Filter Application Logic: - Filters are applied with AND logic within a category - Multiple filters across categories use AND logic - Empty array means "no filter" (include all values) - Null/undefined filter field means "no filter" (include all values)

Example Filtered Request:

{
  "reportingPeriodId": "period_uuid",
  "format": "xlsx",
  "filters": {
    "siteIds": ["site_001"]
  },
  "options": {
    "includeIncidents": false
  }
}
Result: Report includes all environmental data from site_001, excluding G.10 Environmental Incidents section.


Aggregation Levels

Organisation-level Aggregation: - Default behavior - Aggregates all site-level data to organisation totals - G.1-G.8: Sum of site values by month/quarter - G.9-G.10: Concatenated activity and incident logs from all sites

Site-level Aggregation: - Set options.aggregationLevel = "site" - Returns separate sheets for each site - Useful for multi-site organisations tracking site-specific environmental metrics

XLSX Sheet Structure (Site-level): - Sheets 1-10 repeated for each site with site name in sheet title - Final sheet: Reference Data


Data Validation and Quality Checks

Before report generation, the API validates:

  1. Data Completeness:
  2. G.1-G.3, G.6-G.8: All 12 months have data (or missing months are future months)
  3. G.4.4, G.5: All 4 quarters have data (or missing quarters are future quarters)
  4. G.9-G.10: Quarterly logs exist for completed quarters
  5. Warning if completeness < 80% for current fiscal year

  6. Data Consistency:

  7. G.1: Milled ore ≤ Crushed ore
  8. G.3: Generator fuel consumption aligns with generator electricity output
  9. G.4: Water balance consistency (abstracted + recycled ≈ consumed + losses)
  10. G.7-G.8: TSF tailings received aligns with tailings generated
  11. Error if validation fails

  12. Calculation Accuracy:

  13. G.2, G.3, G.4, G.6: Per-tonne intensity metrics calculated correctly
  14. G.3: Generator consumption rate (L/kWh) = fuel consumed / electricity generated
  15. Precision: 3 decimal places for rates, 2 decimal places for percentages

  16. Confidentiality:

  17. G.10 incidents classified as CONFIDENTIAL
  18. G.9 rehabilitation costs may be COMMERCIAL_IN_CONFIDENCE
  19. Ensure proper access control when exporting

Validation Response (if failed):

{
  "error": {
    "code": "DATA_QUALITY_VALIDATION_FAILED",
    "message": "Report data failed quality checks",
    "details": {
      "completeness": {
        "g1": 100.0,
        "g2": 91.7,
        "g3": 100.0,
        "g4": 75.0,
        "g5": 100.0,
        "g6": 83.3,
        "g7": 100.0,
        "g8": 100.0,
        "g9": 100.0,
        "g10": 100.0
      },
      "validation_errors": [
        {
          "section": "G.4",
          "issue": "Missing water quality data for Q2 and Q3",
          "severity": "WARNING"
        },
        {
          "section": "G.7-G.8",
          "issue": "TSF tailings received (125,340t) does not match tailings generated (127,890t)",
          "severity": "ERROR"
        }
      ]
    },
    "request_id": "req_xyz789",
    "timestamp": "2026-01-18T03:00:00Z"
  }
}


Performance Considerations

Estimated Generation Times: - Small site (1 site, monthly data): 10-30 seconds - Medium organisation (3-5 sites): 30-90 seconds - Large organisation (10+ sites): 90-300 seconds

Resource Usage: - Memory: ~50MB per report generation job - Database queries: ~30-70 queries (depends on filters and aggregation level) - File size: CSV ~100KB-1MB, XLSX ~200KB-3MB

Optimization Tips: - Use site-level filters to reduce data volume - Generate reports during off-peak hours for large organisations - Cache intermediate aggregations for frequently requested reports


Audit Log

Immutable audit trail of all administrative actions.

GET /api/v1/admin/audit-logs

Query audit log entries with filtering and search.

Permission: admin.audit.view (All roles)

Query Parameters: - filter[entity_type] (string) - Filter by entity: MetricSubmission, ReportingPeriod, Metric, User - filter[action] (string) - Filter by action: created, approved, rejected, locked, unlocked - filter[actor_user_id] (uuid) - Filter by user who performed action - filter[entity_id] (uuid) - Filter by specific entity - filter[date_from] (datetime) - Filter from date - filter[date_to] (datetime) - Filter to date - sort (string) - Sort by: -created_at (default), action, entity_type

Response (200 OK):

{
  "data": [
    {
      "id": "b50e8400-e29b-41d4-a716-446655440021",
      "actor": {
        "id": "c50e8400-e29b-41d4-a716-446655440008",
        "name": "Jane Approver",
        "email": "jane.approver@example.com",
        "role": "APPROVER"
      },
      "action": "submission.approved",
      "entityType": "MetricSubmission",
      "entityId": "550e8400-e29b-41d4-a716-446655440010",
      "entitySummary": {
        "metricId": "GRI_302_1_ELECTRICITY",
        "value": 1250.50,
        "site": "Factory A - Shanghai"
      },
      "beforeState": {
        "state": "PROCESSED",
        "approvedAt": null
      },
      "afterState": {
        "state": "APPROVED",
        "approvedAt": "2026-01-10T14:00:00Z"
      },
      "justification": "Data verified against utility bills. Jan 2026 electricity consumption aligns with invoice #INV-2026-0145.",
      "metadata": {
        "ipAddress": "203.0.113.42",
        "userAgent": "Mozilla/5.0...",
        "requestId": "req_xyz789"
      },
      "createdAt": "2026-01-10T14:00:00Z"
    },
    {
      "id": "c50e8400-e29b-41d4-a716-446655440022",
      "actor": {
        "id": "c50e8400-e29b-41d4-a716-446655440008",
        "name": "Jane Approver"
      },
      "action": "period.locked",
      "entityType": "ReportingPeriod",
      "entityId": "850e8400-e29b-41d4-a716-446655440012",
      "entitySummary": {
        "name": "Q4 2025",
        "submissionCount": 480
      },
      "beforeState": {
        "state": "OPEN",
        "contentHash": null
      },
      "afterState": {
        "state": "LOCKED",
        "contentHash": "sha256:a3c4f8b912e5d7f6c3b2a1e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c9"
      },
      "justification": "All Q4 2025 data reviewed and approved for FY2025 annual sustainability report.",
      "createdAt": "2026-01-10T16:45:00Z"
    }
  ],
  "meta": {
    "page": 1,
    "pageSize": 50,
    "total": 15432,
    "totalPages": 309
  }
}

Audit Log Properties: - Immutable: Cannot be modified or deleted once created - Tamper-proof: Each entry includes cryptographic hash chain - Retention: 7 years minimum for compliance - Performance: Indexed on entity_id, actor_user_id, created_at


Task Queue & Notifications

GET /api/v1/admin/tasks/my-queue

Get submissions assigned to current user for review.

Permission: admin.tasks.view (Reviewer, Approver, Admin)

Query Parameters: - filter[priority] (string) - Filter by priority: HIGH, MEDIUM, LOW - filter[due_soon] (boolean) - Filter tasks due within 48 hours - sort (string) - Sort by: due_date, -priority, -created_at

Response (200 OK):

{
  "data": [
    {
      "submissionId": "550e8400-e29b-41d4-a716-446655440010",
      "metric": {"metricId": "GRI_302_1_ELECTRICITY", "name": "Electricity Consumption"},
      "site": {"id": "site_001", "name": "Factory A - Shanghai"},
      "value": 1250.50,
      "state": "VALIDATED",
      "priority": "HIGH",
      "dueDate": "2026-01-12T23:59:59Z",
      "assignedAt": "2026-01-10T10:00:00Z",
      "ageInHours": 8,
      "hasValidationWarnings": true
    }
  ],
  "summary": {
    "totalTasks": 12,
    "dueSoon": 3,
    "overdue": 0,
    "avgAgeInHours": 24
  }
}


Pagination & Filtering

All list endpoints support consistent pagination and filtering.

Pagination

Parameters: - page (integer, default: 1) - Page number (1-indexed) - pageSize (integer, default: 50, max: 100) - Items per page

Response Meta:

{
  "meta": {
    "page": 2,
    "pageSize": 50,
    "total": 328,
    "totalPages": 7
  },
  "links": {
    "first": "/api/v1/admin/submissions?page=1&pageSize=50",
    "prev": "/api/v1/admin/submissions?page=1&pageSize=50",
    "next": "/api/v1/admin/submissions?page=3&pageSize=50",
    "last": "/api/v1/admin/submissions?page=7&pageSize=50"
  }
}

Filtering

Syntax: - Simple: filter[field]=value - Array: filter[field]=value1,value2 - Comparison: filter[field_gt]=100, filter[field_lt]=1000 - Date range: filter[submitted_from]=2026-01-01&filter[submitted_to]=2026-01-31

Example:

GET /api/v1/admin/submissions?filter[state]=validated,processed&filter[has_validation_warnings]=true&sort=-submitted_at

Sorting

Syntax: - Ascending: sort=field_name - Descending: sort=-field_name - Multiple: sort=-priority,created_at


Error Handling

All Admin API endpoints follow the standard error handling conventions documented in Error Handling.

Common Error Codes: - AUTH_INSUFFICIENT_PERMISSIONS - User role lacks required permission - STATE_INVALID_TRANSITION - Invalid state transition attempted - STATE_PREREQUISITES_NOT_MET - Action prerequisites not satisfied - RESOURCE_NOT_FOUND - Submission or period not found - VALIDATION_FAILED - Request validation failed - RATE_LIMIT_EXCEEDED - Too many requests

Example Error Response:

{
  "error": {
    "code": "AUTH_INSUFFICIENT_PERMISSIONS",
    "message": "User does not have permission to perform this action",
    "details": {
      "required_permission": "admin.submissions.approve",
      "user_role": "REVIEWER",
      "allowed_roles": ["APPROVER", "ADMIN"]
    },
    "request_id": "req_xyz789",
    "timestamp": "2026-01-11T14:30:00Z"
  }
}


Implementation Guide

Quarkus JAX-RS Resource Examples (Kotlin)

import jakarta.annotation.security.RolesAllowed
import jakarta.inject.Inject
import jakarta.ws.rs.*
import jakarta.ws.rs.core.MediaType
import jakarta.ws.rs.core.Response
import io.quarkus.security.identity.SecurityIdentity
import java.util.UUID

@Path("/api/v1/admin/submissions")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
class AdminSubmissionResource @Inject constructor(
    private val submissionService: SubmissionService,
    private val auditLogService: AuditLogService,
    private val securityIdentity: SecurityIdentity
) {

    @GET
    @RolesAllowed("REVIEWER", "APPROVER", "ADMIN")
    fun listSubmissions(
        @QueryParam("filter") filter: Map<String, String>?,
        @QueryParam("page") @DefaultValue("1") page: Int,
        @QueryParam("pageSize") @DefaultValue("50") pageSize: Int,
        @QueryParam("sort") sort: String?
    ): PagedResponse<SubmissionDto> {
        val user = securityIdentity.principal as UserPrincipal
        val tenantId = user.tenantId

        val submissions = submissionService.findAll(
            tenantId = tenantId,
            filters = filter ?: emptyMap(),
            page = page - 1,
            pageSize = pageSize.coerceAtMost(100),
            sort = sort
        )

        return submissions
    }

    @GET
    @Path("/{id}")
    @RolesAllowed("REVIEWER", "APPROVER", "ADMIN")
    fun getSubmission(@PathParam("id") id: UUID): SubmissionDetailDto {
        val user = securityIdentity.principal as UserPrincipal
        val tenantId = user.tenantId

        val submission = submissionService.findById(id, tenantId)
            ?: throw NotFoundException("Submission not found")

        return submission
    }

    @POST
    @Path("/{id}/approve")
    @RolesAllowed("APPROVER", "ADMIN")
    fun approveSubmission(
        @PathParam("id") id: UUID,
        request: ApproveSubmissionRequest
    ): SubmissionDto {
        val user = securityIdentity.principal as UserPrincipal

        val submission = submissionService.approve(
            submissionId = id,
            tenantId = user.tenantId,
            approvedBy = user.userId,
            comment = request.comment,
            metadata = request.metadata
        )

        auditLogService.log(
            actor = user,
            action = "submission.approved",
            entityType = "MetricSubmission",
            entityId = id,
            beforeState = mapOf("state" to "VALIDATED"),
            afterState = mapOf("state" to "APPROVED"),
            justification = request.comment
        )

        return submission
    }

    @POST
    @Path("/{id}/reject")
    @RolesAllowed("REVIEWER", "APPROVER", "ADMIN")
    fun rejectSubmission(
        @PathParam("id") id: UUID,
        request: RejectSubmissionRequest
    ): SubmissionDto {
        val user = securityIdentity.principal as UserPrincipal

        if (request.reason.isBlank()) {
            throw ValidationException("Rejection reason is required")
        }

        val submission = submissionService.reject(
            submissionId = id,
            tenantId = user.tenantId,
            rejectedBy = user.userId,
            reason = request.reason,
            requiredCorrections = request.requiredCorrections,
            severity = request.severity
        )

        return submission
    }

    @POST
    @Path("/bulk-assign")
    @RolesAllowed("APPROVER", "ADMIN")
    fun bulkAssign(request: BulkAssignRequest): BulkAssignResponse {
        val user = securityIdentity.principal as UserPrincipal
        val tenantId = user.tenantId

        val result = submissionService.bulkAssign(
            submissionIds = request.submissionIds,
            assignToUserId = request.assignToUserId,
            tenantId = tenantId,
            priority = request.priority,
            dueDate = request.dueDate
        )

        return result
    }
}

@Path("/api/v1/admin/reporting-periods")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
class AdminReportingPeriodResource @Inject constructor(
    private val periodService: ReportingPeriodService,
    private val auditLogService: AuditLogService,
    private val securityIdentity: SecurityIdentity
) {

    @POST
    @Path("/{id}/lock")
    @RolesAllowed("APPROVER", "ADMIN")
    fun lockPeriod(
        @PathParam("id") id: UUID,
        request: LockPeriodRequest
    ): ReportingPeriodDto {
        val user = securityIdentity.principal as UserPrincipal

        // Validate prerequisites
        val period = periodService.findById(id, user.tenantId)
            ?: throw NotFoundException("Reporting period not found")

        if (period.hasUnreviewedSubmissions()) {
            throw StatePrerequisiteException(
                "Cannot lock period: ${period.unreviewedSubmissionsCount} submissions still need review",
                details = mapOf(
                    "unreviewedSubmissions" to period.unreviewedSubmissionsCount,
                    "minimumApprovalRate" to 95.0,
                    "currentApprovalRate" to period.approvalRate
                )
            )
        }

        // Generate content hash
        val contentHash = periodService.generateContentHash(id, user.tenantId)

        // Lock period
        val lockedPeriod = periodService.lock(
            periodId = id,
            tenantId = user.tenantId,
            lockedBy = user.userId,
            justification = request.justification,
            contentHash = contentHash
        )

        auditLogService.log(
            actor = user,
            action = "period.locked",
            entityType = "ReportingPeriod",
            entityId = id,
            beforeState = mapOf("state" to "OPEN"),
            afterState = mapOf("state" to "LOCKED", "contentHash" to contentHash),
            justification = request.justification
        )

        return lockedPeriod
    }
}

Service Layer Example

import jakarta.enterprise.context.ApplicationScoped
import jakarta.enterprise.event.Event
import jakarta.inject.Inject
import jakarta.transaction.Transactional
import java.time.Instant
import java.util.UUID

@ApplicationScoped
class SubmissionService @Inject constructor(
    private val submissionRepository: SubmissionRepository,
    private val notificationService: NotificationService,
    private val submissionApprovedEvent: Event<SubmissionApprovedEvent>
) {

    @Transactional
    fun approve(
        submissionId: UUID,
        tenantId: UUID,
        approvedBy: UUID,
        comment: String?,
        metadata: Map<String, Any>?
    ): SubmissionDto {
        val submission = submissionRepository.findByIdAndTenantId(submissionId, tenantId)
            ?: throw NotFoundException("Submission not found")

        // Validate state transition
        if (submission.state !in listOf(State.VALIDATED, State.PROCESSED)) {
            throw StateTransitionException(
                "Cannot approve submission in state ${submission.state}",
                currentState = submission.state,
                requiredStates = listOf(State.VALIDATED, State.PROCESSED)
            )
        }

        // Update submission
        submission.state = State.APPROVED
        submission.approvedBy = approvedBy
        submission.approvedAt = Instant.now()
        submission.approvalComment = comment
        submission.metadata = (submission.metadata ?: emptyMap()) + (metadata ?: emptyMap())

        val saved = submissionRepository.persist(submission)

        // Send notification to submitter
        notificationService.notifySubmissionApproved(saved)

        // Fire CDI event for async processing
        submissionApprovedEvent.fire(SubmissionApprovedEvent(saved))

        return saved.toDto()
    }
}

RBAC Enforcement with Quarkus Security

import jakarta.annotation.Priority
import jakarta.inject.Inject
import jakarta.ws.rs.Priorities
import jakarta.ws.rs.container.ContainerRequestContext
import jakarta.ws.rs.container.ContainerRequestFilter
import jakarta.ws.rs.ext.Provider
import io.quarkus.security.identity.SecurityIdentity

// Tenant isolation is enforced via ContainerRequestFilter
@Provider
@Priority(Priorities.AUTHORIZATION + 1)
class TenantIsolationFilter @Inject constructor(
    private val securityIdentity: SecurityIdentity
) : ContainerRequestFilter {

    override fun filter(requestContext: ContainerRequestContext) {
        // Only process authenticated requests to admin endpoints
        if (requestContext.uriInfo.path.startsWith("/api/v1/admin/")) {
            if (securityIdentity.isAnonymous) {
                return
            }

            if (securityIdentity.principal is UserPrincipal) {
                val tenantId = (securityIdentity.principal as UserPrincipal).tenantId
                TenantContext.setTenantId(tenantId)
            }
        }
    }
}

// Clean up tenant context after request (using ContainerResponseFilter)
@Provider
@Priority(Priorities.AUTHORIZATION + 2)
class TenantContextCleanupFilter : ContainerResponseFilter {

    override fun filter(
        requestContext: ContainerRequestContext,
        responseContext: ContainerResponseContext
    ) {
        TenantContext.clear()
    }
}

// RBAC is enforced using @RolesAllowed at the resource method level
// Example:
// @GET
// @Path("/{id}")
// @RolesAllowed("REVIEWER", "APPROVER", "ADMIN")
// fun getSubmission(@PathParam("id") id: UUID): SubmissionDetailDto { ... }

Security Considerations

1. Tenant Isolation

CRITICAL: All database queries MUST include tenant_id filter to prevent cross-tenant data access.

// Always include tenant_id in WHERE clause
@Query("SELECT s FROM Submission s WHERE s.tenantId = :tenantId AND s.id = :id")
fun findByIdAndTenantId(id: UUID, tenantId: UUID): Submission?

// Use JPA @Where annotation for automatic tenant filtering
@Entity
@Where(clause = "tenant_id = current_tenant_id()")
class MetricSubmission {
    // ...
}

2. Role-Based Access Control

Enforcement Points: - Controller: @RolesAllowed annotations on endpoints - Service: Business logic validates user permissions - Database: Row-level security policies

Permission Matrix:

Action Admin Approver Reviewer Collector
View submissions Own only
Approve submissions
Reject submissions
Lock periods
Unlock periods
Create metrics
Generate reports

3. Audit Logging

Requirements: - Log ALL state-changing operations - Include before/after state snapshots - Store actor IP address and user agent - Immutable (append-only, no updates/deletes) - 7-year retention for compliance

4. Content Hash for Period Locking

Generate SHA-256 hash of all approved submissions to ensure data integrity:

fun generateContentHash(periodId: UUID, tenantId: UUID): String {
    val submissions = submissionRepository.findApprovedByPeriod(periodId, tenantId)
        .sortedBy { it.id } // Deterministic ordering

    val content = submissions.joinToString("\n") {
        "${it.id}|${it.metricId}|${it.value}|${it.approvedAt}"
    }

    return MessageDigest.getInstance("SHA-256")
        .digest(content.toByteArray())
        .joinToString("") { "%02x".format(it) }
        .let { "sha256:$it" }
}

5. Input Validation

  • Validate all request bodies with JSR-380 annotations
  • Sanitize user inputs to prevent injection attacks
  • Validate UUIDs are properly formatted
  • Check enum values are in allowed set
  • Validate date ranges are logical

6. Rate Limiting

Implement at API gateway level: - 500 requests/hour per user+tenant for admin endpoints - 10 reports/hour per tenant for report generation - Use Redis-backed sliding window algorithm


Performance Guidelines

1. Database Indexing

Required Indexes:

-- Submissions table
CREATE INDEX idx_submissions_tenant_state ON metric_submissions(tenant_id, state);
CREATE INDEX idx_submissions_tenant_period ON metric_submissions(tenant_id, reporting_period_id);
CREATE INDEX idx_submissions_tenant_created ON metric_submissions(tenant_id, created_at DESC);
CREATE INDEX idx_submissions_assigned_to ON metric_submissions(assigned_to_user_id, state) WHERE assigned_to_user_id IS NOT NULL;

-- Audit log table
CREATE INDEX idx_audit_logs_tenant_created ON audit_logs(tenant_id, created_at DESC);
CREATE INDEX idx_audit_logs_entity ON audit_logs(entity_type, entity_id);
CREATE INDEX idx_audit_logs_actor ON audit_logs(actor_user_id, created_at DESC);

2. Pagination

  • Default page size: 50
  • Maximum page size: 100
  • Use offset pagination for simplicity
  • Consider cursor-based pagination for large datasets (>10K records)

3. Caching Strategy

Redis Caching: - Metric templates (rarely change): 24-hour TTL - Reporting period metadata: 1-hour TTL - User permissions: 15-minute TTL - Audit log queries: No caching (always fresh)

import io.quarkus.cache.CacheResult
import io.quarkus.cache.CacheKey

@CacheResult(cacheName = "metrics")
fun getMetrics(@CacheKey tenantId: UUID): List<MetricDto> {
    return metricRepository.findByTenantId(tenantId).map { it.toDto() }
}

4. Async Processing

Queue long-running operations: - Report generation → Background job with Quarkus Scheduler or Quartz extension - Notification sending → Async with @RunOnVirtualThread or Mutiny Uni - Audit log writes → CDI async event observer with @ObservesAsync

5. Query Optimization

  • Use JPA fetch joins to avoid N+1 queries
  • Select only required columns for list views
  • Use database views for complex aggregations
  • Monitor slow queries with Hibernate statistics

6. Performance SLAs

Target Response Times: - GET list endpoints: p95 < 500ms - GET single resource: p95 < 200ms - POST approval/reject: p95 < 1000ms - Report generation: Complete within 5 minutes


Testing Checklist

Unit Tests

  • RBAC enforcement for each endpoint
  • State transition validation
  • Tenant isolation in queries
  • Input validation and sanitization
  • Error handling for invalid states

Integration Tests

  • End-to-end submission approval workflow
  • Period locking with prerequisites check
  • Bulk assignment distributes load fairly
  • Report generation job completes successfully
  • Audit logs created for all state changes

Security Tests

  • User cannot access other tenant's data
  • Reviewer cannot approve (only Approver/Admin can)
  • Cannot approve submission in DRAFT state
  • Cannot lock period with unreviewed submissions
  • Rate limiting enforced correctly

Performance Tests

  • List 10,000 submissions in <500ms
  • Approve submission in <1000ms
  • Lock period in <2000ms
  • Generate report for 1000 submissions in <5min
  • Concurrent approvals don't cause race conditions

Cross-References


Change Log

Version Date Author Changes
1.0 2026-01-03 Senior Product Architect Initial Admin API specification
1.1 2026-01-11 Ralph Agent Comprehensive expansion with RBAC, pagination, Kotlin examples
1.2 2026-01-17 Ralph Agent Added Human Capital Report Generation with CSV/XLSX export specs, filters, and aggregation levels
1.3 2026-01-18 Ralph Agent Added Environment Report Generation (G.1-G.10) with CSV/XLSX export specs, validation rules, and aggregation levels