Skip to content

Reporting Concepts: Boundaries, Baselines, and Consolidation

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


Purpose

Define the fundamental reporting concepts that govern how ESG data is scoped, aggregated, and compared over time. These concepts are critical for GRI compliance, year-over-year comparability, and accurate stakeholder communication.


Scope

In Scope: - Organizational boundary definitions (GRI 2-2) - Consolidation approaches for data aggregation - Operational and facility/site boundary management - Time boundary and reporting period management - Baseline establishment and recalculation rules - Comparability and restatement triggers

Out of Scope: - Value chain (Scope 3) boundary setting (vNext) - Materiality assessment processes (v1 uses pre-configured topics) - Target-setting methodologies (vNext)


Key Decisions & Assumptions

Decision Rationale Alternatives Considered
Support all 3 GRI boundary approaches Different clients use different approaches; flexibility required Hard-code to operational control only
Site-level data collection Aligns with operational reality; enables facility-level reporting Business unit-level only (less granular)
Baseline recalculation on 30% threshold Follows GHG Protocol guidance; industry best practice Fixed baseline (less accurate over time)
Fiscal year agnostic Clients have different fiscal calendars Force calendar year alignment

1. Organizational Boundaries (GRI 2-2)

Overview

The organizational boundary defines which legal entities and operations are included in an ESG report. GRI 2-2 requires organizations to describe their approach to determining this boundary.

Three Approaches

1.1 Financial Control Approach

Definition: Include entities over which the organization has the ability to direct financial and operating policies to gain economic benefits.

When to Use: - Aligns with financial reporting consolidation (IFRS/GAAP) - Common for publicly traded companies - Provides consistency between ESG and financial reports

Example: ParentCo owns 60% of SubCo and controls the board. SubCo is included at 100% of its metrics (not just 60%).

Platform Implementation:

// Organisation entity (Quarkus Panache)
@Entity
@Table(name = "organisations")
data class Organisation(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,

    @Column(nullable = false)
    val tenantId: Long,

    @Column(nullable = false)
    val name: String,

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    val consolidationApproach: ConsolidationApproach = ConsolidationApproach.OPERATIONAL_CONTROL,

    @Column(nullable = false, length = 5)
    val fiscalYearEnd: String // e.g., "12-31" for calendar year
)

enum class ConsolidationApproach {
    FINANCIAL_CONTROL,
    OPERATIONAL_CONTROL,
    EQUITY_SHARE
}

1.2 Operational Control Approach

Definition: Include entities over which the organization has full authority to introduce and implement operating policies.

When to Use: - Organization operates assets but may not own them (e.g., leased facilities) - Common for companies with significant leased operations - Preferred for GHG accounting (GHG Protocol Corporate Standard)

Example: RetailCo leases all its stores. It has operational control (sets policies, manages operations) but no ownership. All stores are included at 100%.

Platform Implementation: - Same as financial control in database structure - Difference is in which entities are marked as included_in_reporting = true

1.3 Equity Share Approach

Definition: Include entities based on percentage of ownership/equity share. Metrics are scaled proportionally.

When to Use: - Joint ventures where control is shared - Organizations want to report "their share" of impacts - Less common than control approaches

Example: InfraCo owns 40% of a joint venture power plant. Report 40% of the plant's emissions, energy, water usage.

Platform Implementation:

// Business unit entity with equity share
@Entity
@Table(name = "business_units")
data class BusinessUnit(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,

    @Column(nullable = false)
    val organisationId: Long,

    @Column(nullable = false)
    val name: String,

    @Column(precision = 5, scale = 2)
    val equitySharePercentage: BigDecimal?, // e.g., 40.00 for 40%

    @Column(nullable = false)
    val includedInReporting: Boolean = true,

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "organisation_id", insertable = false, updatable = false)
    val organisation: Organisation? = null
) {
    // Scope metric values by equity share
    fun getScaledMetricValue(rawValue: BigDecimal): BigDecimal {
        return if (organisation?.consolidationApproach == ConsolidationApproach.EQUITY_SHARE) {
            rawValue * (equitySharePercentage ?: BigDecimal.ZERO) / BigDecimal(100)
        } else {
            rawValue // 100% for control approaches
        }
    }
}

Platform Configuration

Database Schema: - organisations.consolidation_approach: ENUM ('financial_control', 'operational_control', 'equity_share') - business_units.equity_share_percentage: DECIMAL(5,2) - only used if equity_share approach - business_units.included_in_reporting: BOOLEAN - whether entity is within boundary

Validation Rules: - If consolidation_approach = equity_share, every business unit must have equity_share_percentage set - If control approaches, equity_share_percentage must be NULL or 100.00 - included_in_reporting must be explicitly set (no default assumptions)

Acceptance Criteria: - [ ] Admin can configure consolidation approach at organization level - [ ] Business units and sites inherit approach but can override equity share - [ ] Reporting aggregation queries apply approach correctly - [ ] Boundary description exported in GRI 2-2 disclosure


2. Facility/Site Boundaries

Definition

Facility boundary defines the physical or operational scope of a site (which buildings, processes, or activities are included).

Common Scenarios

Scenario Boundary Decision Platform Handling
Single factory Entire premises included Single sites record
Campus with multiple buildings Include all buildings under org control Single site with building metadata in site.metadata JSON
Shared industrial park Only org's buildings/operations Site boundary description in site.boundary_description
Leased office in multi-tenant building Only org's leased floors Site marked as partial_facility = true, include floor area in metadata
Mobile operations (vehicles) Not a fixed site vNext: mobile asset tracking

Platform Implementation

Database Schema:

-- sites table additions
ALTER TABLE sites ADD COLUMN boundary_description TEXT;
ALTER TABLE sites ADD COLUMN partial_facility BOOLEAN DEFAULT false;
ALTER TABLE sites ADD COLUMN site_area_sqm DECIMAL(12,2);
ALTER TABLE sites ADD COLUMN metadata JSONB; -- { "buildings": [...], "floors": [...], "exclusions": [...] }

Hibernate ORM Entity (Quarkus):

@Entity
@Table(name = "sites")
data class Site(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,

    @Column(columnDefinition = "TEXT")
    val boundaryDescription: String? = null,

    @Column(nullable = false)
    val partialFacility: Boolean = false,

    @Column(precision = 12, scale = 2)
    val siteAreaSqm: BigDecimal? = null,

    @Type(JsonBinaryType::class)
    @Column(columnDefinition = "jsonb")
    val metadata: Map<String, Any>? = null
) {
    // Check if site boundary is clearly defined
    fun hasClearBoundary(): Boolean {
        return !boundaryDescription.isNullOrBlank() &&
               (!partialFacility || metadata?.get("included_areas") != null)
    }
}

Acceptance Criteria: - [ ] Sites can store boundary descriptions (text field) - [ ] Partial facility sites flagged and require area/scope metadata - [ ] Validation warns if boundary description missing for new sites - [ ] Export includes boundary description in site-level reporting


3. Time Boundaries

Reporting Period Definition

A reporting period is a defined time span for ESG data collection and reporting.

Common Types: - Calendar Year: Jan 1 - Dec 31 - Fiscal Year: Aligns with financial reporting (e.g., Apr 1 - Mar 31) - Quarterly: For interim reporting - Custom: Ad-hoc periods for specific projects

Platform Implementation

Database Schema:

@Entity
@Table(name = "reporting_periods")
data class ReportingPeriod(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,

    @Column(nullable = false)
    val tenantId: Long,

    @Column(nullable = false)
    val organisationId: Long,

    @Column(nullable = false)
    val name: String, // e.g., "FY2025", "Q1 2026"

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    val periodType: PeriodType, // ENUM: ANNUAL, QUARTERLY, CUSTOM

    @Column(nullable = false)
    val startDate: LocalDate,

    @Column(nullable = false)
    val endDate: LocalDate,

    @Column(nullable = false)
    val fiscalYear: Int, // e.g., 2025

    @Column(nullable = false)
    val isBaseline: Boolean = false,

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    val state: PeriodState, // ENUM: OPEN, IN_REVIEW, APPROVED, LOCKED

    val lockedAt: Instant? = null,

    @Column(length = 64)
    val contentHash: String? = null // SHA-256 of all submissions when locked
)

enum class PeriodType { ANNUAL, QUARTERLY, CUSTOM }
enum class PeriodState { OPEN, IN_REVIEW, APPROVED, LOCKED }

Cutoff Date Handling

Principle: Data must be from activities within the reporting period. Late submissions are allowed if they reflect in-period activities.

Implementation: - metric_submissions.activity_date must fall within reporting period start/end - metric_submissions.submitted_at can be after period end (late submission) - Validation rule: activity_date >= period.start_date AND activity_date <= period.end_date

Example: - Reporting Period: Jan 1 - Dec 31, 2025 - Activity: Energy bill for December 2025 - Submission Date: Feb 15, 2026 (late but acceptable) - Validation: ✅ PASS (activity_date = Dec 2025 within period)

Retrospective Adjustments

If data for a locked period needs correction: 1. Requires restatement process (see Locking & Restatements) 2. Unlock period (Admin role with justification) 3. Submit corrected data (new version) 4. Re-lock with updated content hash 5. Audit log records restatement trigger and actor


4. Baselines

Definition

A baseline is a reference point (historical year or average of years) against which progress is measured.

Example: "We reduced GHG emissions by 25% compared to our 2020 baseline."

Baseline Selection

GHG Protocol Guidance: - Choose a year with reliable data - Typically 3-10 years before current reporting - Should reflect "normal" operations (not anomalous year)

Platform Approach (v1): - Admin selects one reporting period as baseline - reporting_periods.is_baseline = true - Only one baseline per organization per metric category (Environmental, Social, Governance)

Baseline Recalculation Rules

Trigger: Significant structural changes to the organization

GHG Protocol Thresholds (Platform Default): - Acquisitions, divestments, mergers, or outsourcing that change baseline emissions by > 30% - Methodology changes, data quality improvements, or errors that change baseline by > 30% - Changes in calculation methods or emission factors

Recalculation Approach: - Retrospective Recalculation: Adjust baseline as if change had occurred in baseline year - Example: Acquired a factory in 2025. Recalculate 2020 baseline to include factory's 2020 emissions for comparability.

Platform Implementation

Service Layer:

@ApplicationScoped
class BaselineManagementService(
    private val reportingPeriodRepository: ReportingPeriodRepository
) {
    @Inject
    lateinit var baselineEstablishedEvent: Event<BaselineEstablished>

    @Transactional
    fun setAsBaseline(period: ReportingPeriod) {
        // Only one baseline per tenant/org
        reportingPeriodRepository
            .findByTenantIdAndOrganisationId(period.tenantId, period.organisationId)
            .forEach { it.copy(isBaseline = false) }
            .also { periods -> periods.forEach { reportingPeriodRepository.persist(it) } }

        val updated = period.copy(isBaseline = true)
        reportingPeriodRepository.persist(updated)

        baselineEstablishedEvent.fire(BaselineEstablished(updated))
    }

    // Check if recalculation needed
    fun needsRecalculation(changePercentage: Double): Boolean {
        return abs(changePercentage) > 30.0 // 30% threshold
    }
}

Audit Trail:

// When baseline recalculated (Quarkus Security + Panache)
@Inject
lateinit var securityIdentity: SecurityIdentity

@Inject
lateinit var objectMapper: ObjectMapper

val auditLog = AuditLog(
    tenantId = period.tenantId,
    actorUserId = securityIdentity.principal.name.toLong(),
    action = "baseline.recalculated",
    entityType = "ReportingPeriod",
    entityId = period.id.toString(),
    beforeState = objectMapper.writeValueAsString(oldMetricValues),
    afterState = objectMapper.writeValueAsString(newMetricValues),
    justification = "Acquired FactoryCo (35% increase in emissions); retrospective recalculation applied",
    timestamp = Instant.now()
)
auditLogRepository.persist(auditLog)

Acceptance Criteria: - [ ] Admin can designate one reporting period as baseline - [ ] Baseline recalculation workflow requires justification and approval - [ ] Recalculated baselines trigger restatement process - [ ] Audit log captures baseline changes with before/after values - [ ] Reports clearly label restated baselines


5. Consolidation & Aggregation

Consolidation Definition

Consolidation is the process of combining data from multiple sites, business units, or entities into organizational-level totals.

Metric Types & Aggregation Rules

Metric Type Aggregation Method Example
Additive SUM Total GHG emissions = sum of all site emissions
Averaged WEIGHTED AVERAGE Average training hours per employee = sum(hours) / sum(employees)
Counted SUM Total recordable incidents = sum of site incidents
Intensive CALCULATED Energy intensity = sum(energy) / sum(revenue)
Non-Aggregable N/A (report separately) % renewable energy (varies by site, not meaningful to average)

Platform Implementation

Metric Definition Schema (Hibernate ORM with JSONB):

@Entity
@Table(name = "metric_definitions")
data class MetricDefinition(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,

    @Column(nullable = false, unique = true)
    val metricId: String, // e.g., "GRI_302_1_ELECTRICITY"

    @Column(nullable = false)
    val name: String,

    @Column(nullable = false)
    val unit: String,

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    val aggregationMethod: AggregationMethod,

    @Type(JsonBinaryType::class)
    @Column(columnDefinition = "jsonb")
    val aggregationFormula: Map<String, Any>? = null, // JSON formula for 'calculated' type

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    val dimensionality: Dimensionality
)

enum class AggregationMethod { SUM, WEIGHTED_AVERAGE, COUNT, CALCULATED, NONE }
enum class Dimensionality { SITE, BUSINESS_UNIT, ORGANISATION }

Aggregation Service (Quarkus CDI):

@ApplicationScoped
class MetricAggregationService {
    @Inject
    lateinit var metricSubmissionRepository: MetricSubmissionRepository

    fun aggregateToOrganisation(period: ReportingPeriod, metric: MetricDefinition): Any {
        val submissions = metricSubmissionRepository
            .findByReportingPeriodIdAndMetricDefinitionIdAndState(
                period.id,
                metric.id,
                SubmissionState.APPROVED
            )

        return when (metric.aggregationMethod) {
            AggregationMethod.SUM -> {
                submissions.sumOf { it.processedData["value"] as BigDecimal }
            }

            AggregationMethod.WEIGHTED_AVERAGE -> {
                val totalValue = submissions.sumOf { it.processedData["value"] as BigDecimal }
                val totalWeight = submissions.sumOf { it.processedData["weight"] as BigDecimal }
                if (totalWeight > BigDecimal.ZERO) totalValue / totalWeight else BigDecimal.ZERO
            }

            AggregationMethod.CALCULATED -> {
                // Execute formula (e.g., "SUM(energy) / SUM(revenue)")
                evaluateFormula(metric.aggregationFormula!!, submissions)
            }

            AggregationMethod.NONE -> {
                // Return map of individual site values
                submissions.associate { it.site.name to it.processedData["value"] }
            }

            else -> throw IllegalArgumentException("Unknown aggregation method: ${metric.aggregationMethod}")
        }
    }

    private fun evaluateFormula(formula: Map<String, Any>, submissions: List<MetricSubmission>): BigDecimal {
        // Formula evaluation logic
        TODO("Implement formula parser and evaluator")
    }
}

Equity Share Application

For organizations using equity share approach:

fun aggregateWithEquityShare(period: ReportingPeriod, metric: MetricDefinition): BigDecimal {
    val submissions = metricSubmissionRepository
        .findByReportingPeriodIdAndMetricDefinitionIdAndStateWithSiteAndBusinessUnit(
            period.id,
            metric.id,
            SubmissionState.APPROVED
        )

    return submissions.sumOf { submission ->
        val rawValue = submission.processedData["value"] as BigDecimal
        val equityShare = submission.site.businessUnit?.equitySharePercentage
            ?: BigDecimal(100)
        rawValue * equityShare / BigDecimal(100)
    }
}

Log-Based Aggregation

Some metrics are not submitted as single scalar values but are derived from transactional logs (e.g., Community Investment Activities, Stakeholder Engagements, OHS Incidents).

Characteristics: - Source: Dedicated log tables (e.g., community_investment_activities) instead of metric_submissions - Granularity: Transaction-level (per activity, per incident) - Aggregation: Calculated at reporting time (SUM of amounts, COUNT of rows)

Platform Implementation: - Metric definition contains source_type = 'log_aggregation' and log_entity name - Aggregation service routes these to specialized query handlers instead of generic scalar aggregation

Example: - Metric: GRI_203_1_COMMUNITY_INVESTMENT_TOTAL - Source: community_investment_activities table - Logic: SELECT SUM(actual_amount) FROM community_investment_activities WHERE period_id = ?

Acceptance Criteria: - [ ] Aggregation service handles all metric types (sum, average, calculated, none) - [ ] Equity share approach correctly scales values before aggregation - [ ] Aggregation service supports log-based metrics (e.g., sum from activity logs) - [ ] Only 'approved' state submissions included in aggregation - [ ] Aggregation results cached and invalidated on submission changes - [ ] Reports display aggregation method used for each metric


6. Comparability & Consistency

Year-over-Year Comparability

Principle: Reported data should be comparable across years to track trends.

Threats to Comparability: - Changes in organizational boundary (acquisitions, divestments) - Changes in calculation methodology - Changes in data sources or quality - Changes in operational scope (new sites, closed sites)

Ensuring Comparability

  1. Constant Perimeter:
  2. Report on same organizational boundary as prior year
  3. If boundary changes, restate prior year (pro forma)

  4. Consistent Methodology:

  5. Use same calculation methods and emission factors
  6. If methodology changes, restate prior year(s) or disclose non-comparability

  7. Baseline Adjustments:

  8. Recalculate baseline when > 30% change
  9. Clearly label restated figures

Platform Implementation

Restatement Tracking:

// ReportingPeriod entity includes relationship
@Entity
@Table(name = "reporting_periods")
data class ReportingPeriod(
    // ... other fields ...

    @OneToMany(mappedBy = "reportingPeriod", cascade = [CascadeType.ALL])
    val restatements: List<Restatement> = emptyList()
)

@Entity
@Table(name = "restatements")
data class Restatement(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,

    @Column(nullable = false)
    val reportingPeriodId: Long,

    @Column(nullable = false)
    val restatementDate: LocalDate,

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    val trigger: RestatementTrigger,

    @Column(columnDefinition = "TEXT", nullable = false)
    val description: String,

    @Column(precision = 5, scale = 2, nullable = false)
    val impactPercentage: BigDecimal, // e.g., 12.5 for 12.5% change

    @Type(JsonBinaryType::class)
    @Column(columnDefinition = "jsonb", nullable = false)
    val beforeValues: Map<String, Any>,

    @Type(JsonBinaryType::class)
    @Column(columnDefinition = "jsonb", nullable = false)
    val afterValues: Map<String, Any>,

    @Column(nullable = false)
    val approvedByUserId: Long,

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "reporting_period_id", insertable = false, updatable = false)
    val reportingPeriod: ReportingPeriod? = null
)

enum class RestatementTrigger {
    ACQUISITION, DIVESTMENT, METHODOLOGY_CHANGE, ERROR_CORRECTION
}

Reporting Disclosure: - GRI 2-4 requires disclosure of restatements - Platform auto-generates restatement table for reports - Includes trigger, magnitude, affected metrics

Acceptance Criteria: - [ ] Restatements tracked in dedicated table with full audit trail - [ ] Reports auto-generate GRI 2-4 restatement disclosure - [ ] Restated periods clearly labeled in UI and exports - [ ] Comparability warnings shown when methodology differs year-over-year


7. Operational Boundary (GHG Protocol Context)

Definition

For GHG emissions specifically, operational boundary determines which emissions sources are included (Scope 1, 2, 3).

Out of Scope for v1: Scope 3 (value chain emissions)

In Scope for v1: Scope 1 & 2 mapping to GRI 305-1 and 305-2

Scope 1 (Direct Emissions)

  • Company-owned/controlled sources
  • Stationary combustion (boilers, furnaces)
  • Mobile combustion (company vehicles)
  • Process emissions (chemical reactions)
  • Fugitive emissions (refrigerants, leaks)

Scope 2 (Indirect Emissions)

  • Purchased electricity
  • Purchased heat/steam/cooling

Platform Implementation:

// Metric definitions tagged with GHG scope
@Entity
@Table(name = "metric_definitions")
data class MetricDefinition(
    // ... other fields ...

    @Type(JsonBinaryType::class)
    @Column(columnDefinition = "jsonb")
    val metadata: Map<String, Any>? = null
)

// Repository query method (Panache)
@ApplicationScoped
class MetricDefinitionRepository : PanacheRepository<MetricDefinition> {
    fun findByGhgScope(scope: String): List<MetricDefinition> {
        return list("SELECT m FROM MetricDefinition m WHERE jsonb_extract_path_text(m.metadata, 'ghg_scope') = ?1", scope)
    }
}

// Tagging in seed data (Quarkus Startup event)
@ApplicationScoped
class DataSeeder {
    @Inject
    lateinit var metricDefinitionRepository: MetricDefinitionRepository

    fun onStart(@Observes event: StartupEvent) {
        val metricDefinition = MetricDefinition(
            metricId = "GRI_305_1_SCOPE1_STATIONARY",
            name = "Scope 1 - Stationary Combustion",
            unit = "tonnes CO2e",
            aggregationMethod = AggregationMethod.SUM,
            dimensionality = Dimensionality.SITE,
            metadata = mapOf(
                "gri_disclosure" to "305-1",
                "ghg_scope" to "scope_1",
                "emission_category" to "stationary_combustion"
            )
        )
        metricDefinitionRepository.persist(metricDefinition)
    }
}

Acceptance Criteria: - [ ] Metrics tagged with GHG scope (1, 2, or N/A) - [ ] Reports can filter and aggregate by GHG scope - [ ] Scope 1 and 2 emissions clearly separated in disclosures


Data Model Notes

Entities Affected

Core Entities: - organisations: Stores consolidation approach, fiscal year - business_units: Stores equity share percentage, inclusion flag - sites: Stores boundary description, partial facility flag, area - reporting_periods: Stores baseline flag, period boundaries, locked state - restatements: Tracks baseline/data restatements

Relationships: - OrganisationBusinessUnitSite (hierarchy) - ReportingPeriodMetricSubmission (data collection) - ReportingPeriodRestatement (change tracking)


Quarkus Implementation Notes

Models (Hibernate ORM Entities)

Organisation: - Fields: consolidation_approach, fiscal_year_end - Methods: getIncludedBusinessUnits(), applyEquityShare() - Managed via Panache repository pattern

Site: - Fields: boundary_description, partial_facility, site_area_sqm, metadata - Methods: hasClearBoundary(), getIncludedAreas() - JSONB support via Hibernate @Type annotation

ReportingPeriod: - Fields: is_baseline, start_date, end_date, fiscal_year - State management: OPEN, IN_REVIEW, APPROVED, LOCKED - Cryptographic hash computed when locked

MetricDefinition: - Fields: aggregation_method, aggregation_formula, dimensionality - Metadata JSONB for GHG scope tagging - Aggregation executed via service layer

Flyway Migrations

-- V1__add_reporting_concepts_to_organisations.sql
-- Add to organisations table
ALTER TABLE organisations
ADD COLUMN consolidation_approach VARCHAR(50) NOT NULL DEFAULT 'OPERATIONAL_CONTROL'
    CHECK (consolidation_approach IN ('FINANCIAL_CONTROL', 'OPERATIONAL_CONTROL', 'EQUITY_SHARE')),
ADD COLUMN fiscal_year_end VARCHAR(5) NOT NULL; -- e.g., '12-31'

-- V2__add_reporting_concepts_to_business_units.sql
-- Add to business_units table
ALTER TABLE business_units
ADD COLUMN equity_share_percentage DECIMAL(5, 2) NULL,
ADD COLUMN included_in_reporting BOOLEAN NOT NULL DEFAULT true;

-- V3__add_reporting_concepts_to_sites.sql
-- Add to sites table
ALTER TABLE sites
ADD COLUMN boundary_description TEXT NULL,
ADD COLUMN partial_facility BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN site_area_sqm DECIMAL(12, 2) NULL,
ADD COLUMN metadata JSONB NULL;

-- V4__create_restatements_table.sql
-- Create restatements table
CREATE TYPE restatement_trigger AS ENUM ('ACQUISITION', 'DIVESTMENT', 'METHODOLOGY_CHANGE', 'ERROR_CORRECTION');

CREATE TABLE restatements (
    id BIGSERIAL PRIMARY KEY,
    tenant_id BIGINT NOT NULL REFERENCES tenants(id),
    reporting_period_id BIGINT NOT NULL REFERENCES reporting_periods(id),
    restatement_date DATE NOT NULL,
    trigger restatement_trigger NOT NULL,
    description TEXT NOT NULL,
    impact_percentage DECIMAL(5, 2) NOT NULL,
    before_values JSONB NOT NULL,
    after_values JSONB NOT NULL,
    approved_by_user_id BIGINT NOT NULL REFERENCES users(id),
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_restatements_tenant_period ON restatements(tenant_id, reporting_period_id);

Services (CDI Application-Scoped Beans)

MetricAggregationService: - Methods: aggregateToOrganisation(), aggregateWithEquityShare(), evaluateFormula() - Uses @ApplicationScoped for singleton lifecycle - Injected Panache repositories for data access

BaselineManagementService: - Methods: setBaseline(), recalculateBaseline(), checkRecalculationThreshold() - Uses @Transactional (jakarta.transaction) for transaction management - Fires CDI events for baseline changes

Async Processing

Async Patterns: - recalculateBaseline(): Triggered by CDI @ObservesAsync event when acquisition/divestment recorded - aggregateOrganisationMetrics(): Background job using @Scheduled (Quarkus Scheduler) or @RunOnVirtualThread - Virtual threads enable millions of concurrent tasks with minimal memory overhead

Configuration:

# Quarkus Scheduler configuration
quarkus.scheduler.enabled=true
quarkus.scheduler.metrics.enabled=true

# Virtual thread configuration
quarkus.virtual-threads.enabled=true

CDI Events

Event Types: - BaselineEstablished: CDI event fired when reporting period set as baseline - BaselineRecalculated: CDI event fired when baseline values updated - RestatementCreated: CDI event fired when historical data corrected

Event Observers:

@ApplicationScoped
class ReportingEventHandler {
    fun onBaselineEstablished(@Observes event: BaselineEstablished) {
        // Synchronous processing
        logger.info("Baseline established for period: ${event.period.name}")
    }

    fun onBaselineRecalculated(@ObservesAsync event: BaselineRecalculated) {
        // Asynchronous processing
        recalculateAggregations(event.period)
    }
}


Acceptance Criteria

Done When: - [ ] All three consolidation approaches (financial, operational, equity share) configurable at org level - [ ] Equity share percentage applied correctly in aggregation queries - [ ] Site boundary descriptions required for new sites (validation) - [ ] Reporting periods support baseline designation (only one per org) - [ ] Baseline recalculation workflow implemented with 30% threshold check - [ ] Restatements tracked in dedicated table with full audit trail - [ ] Aggregation service handles sum, average, calculated, and non-aggregable metrics - [ ] GHG Scope 1 and Scope 2 metrics correctly tagged and aggregable - [ ] Reports display consolidation approach and boundary descriptions (GRI 2-2 compliance) - [ ] Year-over-year comparability warnings shown when methodology changes

Not Done (vNext): - [ ] Scope 3 value chain emissions boundary (vNext) - [ ] Dynamic materiality assessment (v1 uses pre-configured topics) - [ ] Multi-baseline support (e.g., separate baselines for E, S, G) - [ ] Automated formula builder for calculated metrics


Cross-References


Change Log

Version Date Author Changes
1.0 2026-01-03 Senior Product Architect Initial version covering boundaries, consolidation, baselines