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
- Constant Perimeter:
- Report on same organizational boundary as prior year
-
If boundary changes, restate prior year (pro forma)
-
Consistent Methodology:
- Use same calculation methods and emission factors
-
If methodology changes, restate prior year(s) or disclose non-comparability
-
Baseline Adjustments:
- Recalculate baseline when > 30% change
- 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:
- Organisation → BusinessUnit → Site (hierarchy)
- ReportingPeriod → MetricSubmission (data collection)
- ReportingPeriod → Restatement (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
- Related: ESG Domain Glossary - Term definitions
- Related: Backend Domain Models - Entity relationships
- Related: Complete Data Model - Migrations and schemas
- Related: Locking & Restatements - Restatement workflows
- Dependencies: Metric Catalog - Aggregation method definitions
Change Log
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2026-01-03 | Senior Product Architect | Initial version covering boundaries, consolidation, baselines |