Salta al contenuto principale

Architettura Software Carbon Accounting: Piattaforme ESG

Progettare piattaforme di carbon accounting per reporting ESG: data model GHG Protocol, calcoli Scope 1-3 e automazione report.

Cosa ne pensi di questo articolo?

Architettura Software Carbon Accounting: Piattaforme ESG
EnergyTechFoto:Danny MenesesPexels

In Breve

Progettare piattaforme di carbon accounting per reporting ESG: data model GHG Protocol, calcoli Scope 1-3 e automazione report.

Architettura Software Carbon Accounting: Piattaforme ESG

Nel 2025, la rendicontazione delle emissioni di carbonio non e più una scelta volontaria per le grandi aziende europee: e un obbligo di legge. La Corporate Sustainability Reporting Directive (CSRD), entrata in vigore con il D.Lgs. 125/2024 in Italia, ha trasformato radicalmente il panorama del reporting ESG, portando la disciplina dal dominio della comunicazione corporate a quello della compliance regolamentare, con le stesse implicazioni operative e tecniche di un sistema contabile finanziario.

Il mercato globale del software di carbon accounting ha raggiunto 1.8 miliardi di dollari nel 2025 e si prevede superi i 6 miliardi entro il 2030, con un CAGR del 27.4%. Non si tratta semplicemente di comprare una piattaforma SaaS: progettare un sistema di carbon accounting robusto richiede competenze che spaziano dalla chimica dell'atmosfera all'ingegneria del software, dalla statistica al diritto comunitario. I team engineering che si trovano a costruire queste piattaforme devono comprendere il GHG Protocol, gli European Sustainability Reporting Standards (ESRS), i database di fattori di emissione e l'architettura di sistemi distribuiti capaci di raccogliere, calcolare e certificare dati da centinaia di fonti diverse.

Questo articolo e una guida tecnica completa: dalla modellazione del dominio con le entità del GHG Protocol, all'implementazione di un motore di calcolo in Python con FastAPI, fino all'automazione dei report CSRD/CDP/GRI e all'integrazione con la Climatiq API per i fattori di emissione real-time. Se stai costruendo una piattaforma ESG interna, valutando soluzioni enterprise o semplicemente vuoi capire come funzionano queste architetture sotto il cofano, sei nel posto giusto.

Cosa Imparerai in Questo Articolo

  • Il GHG Protocol: Scope 1, 2 e 3 e le 15 categorie di emissioni indirette della supply chain
  • Come modellare il dominio dati di un sistema carbon accounting (Organization, Facility, EmissionSource, Activity)
  • I principali database di emission factors: DEFRA, EPA, ecoinvent, Climatiq API
  • Architettura a microservizi: Data Collection, Calculation Engine, Reporting, Audit Trail
  • Implementazione Python con FastAPI e pandas per il calcolo delle emissioni
  • Automazione Scope 3: integrazione con ERP SAP, dati procurement, travel data
  • Generazione automatica report CSRD/ESRS, CDP e GRI Standards
  • Audit trail e data lineage: tracciabilita completa dei calcoli
  • Confronto piattaforme: Persefoni, Watershed, Sphera, Plan A
  • Case study: azienda manifatturiera italiana con reporting CSRD 2025
  • Normativa aggiornata: CSRD Omnibus 2025, EU Taxonomy, D.Lgs. 125/2024

Posizione nella Serie EnergyTech

#ArticoloStato
1MQTT e InfluxDB: Time Series per Dati EnergeticiPubblicato
2IEC 61850: Protocollo Standard per Reti Elettriche IntelligentiPubblicato
3DERMS: Gestione Distribuita delle Risorse EnergetichePubblicato
4Building Management System: Ottimizzazione Energetica AIPubblicato
5Renewable Energy Forecasting: ML per Fotovoltaico ed EolicoPubblicato
6EV Load Balancing: Smart Charging e Vehicle-to-GridPubblicato
7Blockchain P2P Energy Trading: Mercati Energetici DecentralizzatiPubblicato
8Sei qui - Architettura Software Carbon Accounting: Piattaforme ESGCorrente
9Digital Twin Energetico: Simulazione e OttimizzazioneProssimo
10OCPP e Infrastruttura EV: Standard e ImplementazioneIn arrivo

Il Contesto Normativo: CSRD, ESRS e D.Lgs. 125/2024

Comprendere la normativa non e un prerequisito burocratico: e la base su cui si progetta l'intera architettura del sistema. Ogni requisito tecnico, ogni campo del database, ogni endpoint dell'API deriva da un obbligo di disclosure specifico.

CSRD e le Onde di Applicazione

La Corporate Sustainability Reporting Directive ha definito tre ondate di applicazione per le aziende europee. La prima ondata (esercizio 2024, report pubblicati nel 2025) ha coinvolto le aziende già soggette alla precedente NFRD: società quotate, banche e assicurazioni con più di 500 dipendenti. La seconda e terza ondata, originariamente previste per il 2025 e 2026, sono state posticipate di due anni dalla Stop-the-Clock Directive pubblicata nella Gazzetta Ufficiale UE il 16 aprile 2025.

Il cambiamento più significativo e arrivato a dicembre 2025 con l'approvazione del pacchetto Omnibus I: la soglia di applicazione obbligatoria e stata alzata a 1.000 dipendenti e 450 milioni di euro di fatturato, riducendo di circa l'80% il numero di aziende soggette alla CSRD obbligatoria. Gli standard ESRS sono in fase di revisione con una riduzione del 61% dei datapoint obbligatori (da circa 1.100 a circa 430), con adozione prevista nella prima meta del 2026 e applicazione dal 2027.

Timeline CSRD Aggiornata (post-Omnibus 2025)

OndataSoggettiPrimo ReportStato
Wave 1 Aziende già NFRD (>500 dip., quotate/banche/assicurazioni) Report 2025 (FY 2024) In corso
Wave 2 Grandi aziende >1.000 dip. e >450M EUR fatturato Report 2027 (FY 2026) - posticipato Posticipato
Wave 3 PMI quotate su mercati regolamentati EU Report 2028 (FY 2027) - posticipato Posticipato
ESRS rev. Tutti i soggetti CSRD, standard semplificati Dal FY 2027 In consultazione

D.Lgs. 125/2024: Il Recepimento Italiano

L'Italia ha recepito la CSRD con il Decreto Legislativo 125 del 6 settembre 2024, entrato in vigore il 25 settembre 2024. Il decreto ha abrogato il precedente D.Lgs. 254/2016 che trasponeva la NFRD. Le principali novità per le aziende italiane includono: l'obbligo di assurance limitata (limited assurance) sul report di sostenibilità da parte di un revisore abilitato, l'integrazione del report di sostenibilità nella relazione sulla gestione, e la pubblicazione nella sezione dedicata del Registro delle Imprese.

EU Taxonomy: Il Collegamento

Il carbon accounting si interseca direttamente con la EU Taxonomy Regulation (Reg. UE 852/2020), che classifica le attivita economiche come "sostenibili" in base a sei obiettivi ambientali. Le aziende CSRD devono rendicontare i KPI di allineamento alla Taxonomy (quote di fatturato, CapEx, OpEx "allineati" e "eleggibili"). Con le semplificazioni del 2025, le aziende con meno di 1.000 dipendenti sono esentate dalla Taxonomy reporting, e le restanti possono limitarsi alle attivita che rappresentano almeno il 10% di fatturato, CapEx o OpEx.

GHG Protocol: Il Framework di Riferimento

Il Greenhouse Gas Protocol e lo standard contabile per le emissioni di gas serra più diffuso al mondo, sviluppato dal World Resources Institute (WRI) e dal World Business Council for Sustainable Development (WBCSD). Praticamente tutti i framework di reporting (CSRD/ESRS, CDP, GRI, ISO 14064) fanno riferimento o si basano sul GHG Protocol.

I Tre Scope: Definizioni Precise

La distinzione in tre "ambiti" (scope) permette di attribuire le emissioni in modo chiaro, evitando duplicazioni nel conteggio tra diversi attori della filiera.

Scope 1: Emissioni Dirette

Emissioni provenienti da fonti di proprietà o sotto il controllo dell'organizzazione. Includono: combustione di combustibili fossili in caldaie, forni, veicoli aziendali di proprietà; emissioni di processo (es. CO2 da reazioni chimiche, CH4 da allevamenti); emissioni fuggitive (perdite di refrigeranti, fughe di gas da impianti). I gas GHG rilevanti sono i sette del Protocollo di Kyoto: CO2, CH4, N2O, HFC, PFC, SF6, NF3, tutti convertiti in CO2 equivalente (CO2e) usando i Global Warming Potential (GWP) dell'IPCC.

Scope 2: Emissioni Indirette da Energia Acquistata

Emissioni associate alla generazione di energia elettrica, calore, vapore o raffreddamento acquistati e consumati dall'organizzazione. Il GHG Protocol Scope 2 Guidance (2015) richiede la rendicontazione con due metodi distinti: il metodo location-based (usa il fattore di emissione della rete elettrica nella localita di consumo, es. il fattore medio della rete italiana) e il metodo market-based (usa i fattori degli strumenti di mercato come i Contratti per Differenza, i Certificati di Energia Rinnovabile/GO, i Power Purchase Agreement). Entrambi devono essere rendicontati.

Scope 3: Altre Emissioni Indirette

Le emissioni della catena del valore, divise in 15 categorie upstream e downstream. Sono tipicamente le più significative (in media 70-80% dell'impronta totale) e le più difficili da misurare. Le ESRS richiedono la rendicontazione delle categorie Scope 3 "materiali" identificate attraverso la Double Materiality Assessment.

Le 15 Categorie Scope 3

#CategoriaTipoTipica per
1Purchased goods and servicesUpstreamTutti i settori
2Capital goodsUpstreamManifatturiero, edilizia
3Fuel and energy related activitiesUpstreamTutti i settori
4Upstream transportation and distributionUpstreamRetail, manifatturiero
5Waste generated in operationsUpstreamManifatturiero, alimentare
6Business travelUpstreamServizi, tech
7Employee commutingUpstreamTutti i settori
8Upstream leased assetsUpstreamReal estate, retail
9Downstream transportationDownstreamManifatturiero, FMCG
10Processing of sold productsDownstreamMaterie prime, chimico
11Use of sold productsDownstreamAutomotive, elettronica
12End-of-life treatmentDownstreamPackaging, beni durevoli
13Downstream leased assetsDownstreamReal estate
14FranchisesDownstreamFood & beverage, retail
15InvestmentsDownstreamBanche, fondi di investimento

Data Model: Modellare il Dominio del Carbon Accounting

Il cuore di ogni piattaforma di carbon accounting e un data model robusto che rifletta fedelmente i concetti del GHG Protocol. Vediamo le entità principali con i loro attributi e relazioni.

Entità Principali

Un sistema completo prevede almeno sei entità core: Organization, Facility, EmissionSource, EmissionFactor, Activity e Calculation. Ecco il modello in Python con SQLAlchemy:

# models/core.py - Data model GHG Protocol completo
from sqlalchemy import Column, String, Float, Enum, DateTime, ForeignKey, JSON, Integer
from sqlalchemy.orm import relationship, DeclarativeBase
from datetime import datetime
from enum import Enum as PyEnum
import uuid

class Base(DeclarativeBase):
    pass

class ScopeType(PyEnum):
    SCOPE_1 = "scope_1"
    SCOPE_2_LOCATION = "scope_2_location"
    SCOPE_2_MARKET = "scope_2_market"
    SCOPE_3 = "scope_3"

class Scope3Category(PyEnum):
    CAT_1_PURCHASED_GOODS = "cat_1"
    CAT_2_CAPITAL_GOODS = "cat_2"
    CAT_3_FUEL_ENERGY = "cat_3"
    CAT_4_UPSTREAM_TRANSPORT = "cat_4"
    CAT_5_WASTE = "cat_5"
    CAT_6_BUSINESS_TRAVEL = "cat_6"
    CAT_7_EMPLOYEE_COMMUTING = "cat_7"
    CAT_8_UPSTREAM_LEASED = "cat_8"
    CAT_9_DOWNSTREAM_TRANSPORT = "cat_9"
    CAT_10_PROCESSING = "cat_10"
    CAT_11_USE_OF_PRODUCTS = "cat_11"
    CAT_12_END_OF_LIFE = "cat_12"
    CAT_13_DOWNSTREAM_LEASED = "cat_13"
    CAT_14_FRANCHISES = "cat_14"
    CAT_15_INVESTMENTS = "cat_15"

class Organization(Base):
    __tablename__ = "organizations"

    id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
    name = Column(String(255), nullable=False)
    legal_entity_id = Column(String(100))  # LEI o P.IVA
    country_code = Column(String(3), nullable=False)  # ISO 3166-1 alpha-3
    nace_code = Column(String(10))  # Codice settore NACE Rev.2
    reporting_year = Column(Integer, nullable=False)
    consolidation_approach = Column(String(50))  # equity_share, financial_control, operational_control
    base_year = Column(Integer)  # anno di riferimento per i target
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

    facilities = relationship("Facility", back_populates="organization")
    calculations = relationship("Calculation", back_populates="organization")

class Facility(Base):
    __tablename__ = "facilities"

    id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
    organization_id = Column(String, ForeignKey("organizations.id"), nullable=False)
    name = Column(String(255), nullable=False)
    facility_type = Column(String(50))  # manufacturing_plant, office, warehouse, data_center
    address = Column(String(500))
    country_code = Column(String(3), nullable=False)
    latitude = Column(Float)
    longitude = Column(Float)
    grid_region = Column(String(50))  # es. "IT_NORD", "DE_TENNET" per Scope 2
    floor_area_sqm = Column(Float)
    is_owned = Column(String(10))  # owned, leased, operated
    operational_start = Column(DateTime)

    organization = relationship("Organization", back_populates="facilities")
    emission_sources = relationship("EmissionSource", back_populates="facility")

class EmissionSource(Base):
    """Fonte di emissione specifica: caldaia, veicolo, processo, acquisto energia"""
    __tablename__ = "emission_sources"

    id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
    facility_id = Column(String, ForeignKey("facilities.id"), nullable=False)
    name = Column(String(255), nullable=False)
    source_type = Column(String(100))  # natural_gas_boiler, diesel_generator, company_car, electricity
    scope = Column(Enum(ScopeType), nullable=False)
    scope_3_category = Column(Enum(Scope3Category))  # solo per Scope 3
    fuel_type = Column(String(50))  # natural_gas, diesel, petrol, LPG
    unit_of_measure = Column(String(20))  # kWh, liters, kg, km, tonne
    description = Column(String(1000))
    is_active = Column(String(10), default="true")

    facility = relationship("Facility", back_populates="emission_sources")
    activities = relationship("Activity", back_populates="emission_source")

class EmissionFactor(Base):
    """Fattore di emissione da database certificato"""
    __tablename__ = "emission_factors"

    id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
    source_database = Column(String(50), nullable=False)  # DEFRA, EPA, ecoinvent, Climatiq, IPCC
    activity_id = Column(String(200))  # ID specifico del database sorgente
    name = Column(String(500), nullable=False)
    category = Column(String(100))
    region = Column(String(50))  # country/region code
    year = Column(Integer, nullable=False)
    unit_type = Column(String(50))  # kgCO2e/kWh, kgCO2e/liter, kgCO2e/km, kgCO2e/tonne
    co2e_factor = Column(Float, nullable=False)  # valore principale in kgCO2e
    co2_factor = Column(Float)   # CO2 separato
    ch4_factor = Column(Float)   # CH4 separato
    n2o_factor = Column(Float)   # N2O separato
    gwp_version = Column(String(20), default="AR6")  # AR5, AR6
    lca_activity = Column(String(50))  # upstream, combustion, downstream
    source_url = Column(String(500))
    last_updated = Column(DateTime)

class Activity(Base):
    """Registrazione di attivita con consumo misurato"""
    __tablename__ = "activities"

    id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
    emission_source_id = Column(String, ForeignKey("emission_sources.id"), nullable=False)
    emission_factor_id = Column(String, ForeignKey("emission_factors.id"))
    period_start = Column(DateTime, nullable=False)
    period_end = Column(DateTime, nullable=False)
    quantity = Column(Float, nullable=False)
    unit = Column(String(20), nullable=False)
    data_quality = Column(String(20), default="measured")  # measured, estimated, calculated, supplier
    data_source = Column(String(200))  # nome sistema sorgente: SAP, utility_bill, travel_tool
    raw_data = Column(JSON)  # dati originali non elaborati
    notes = Column(String(1000))
    created_by = Column(String(100))
    created_at = Column(DateTime, default=datetime.utcnow)

    emission_source = relationship("EmissionSource", back_populates="activities")
    emission_factor = relationship("EmissionFactor")
    calculation = relationship("Calculation", back_populates="activity", uselist=False)

class Calculation(Base):
    """Risultato del calcolo emissioni, immutabile una volta creato"""
    __tablename__ = "calculations"

    id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
    organization_id = Column(String, ForeignKey("organizations.id"), nullable=False)
    activity_id = Column(String, ForeignKey("activities.id"), nullable=False)
    scope = Column(Enum(ScopeType), nullable=False)
    scope_3_category = Column(Enum(Scope3Category))
    co2e_tonnes = Column(Float, nullable=False)  # risultato in tonnellate CO2e
    co2_tonnes = Column(Float)
    ch4_tonnes = Column(Float)
    n2o_tonnes = Column(Float)
    calculation_method = Column(String(50))  # spend_based, activity_based, hybrid, supplier_specific
    emission_factor_value = Column(Float)   # snapshot del fattore usato
    emission_factor_unit = Column(String(50))
    emission_factor_source = Column(String(100))
    calculation_formula = Column(String(500))  # formula usata, per audit
    calculated_at = Column(DateTime, default=datetime.utcnow)
    calculated_by = Column(String(100))  # user o sistema automatico
    version = Column(Integer, default=1)  # versioning per ricertificazione
    is_verified = Column(String(10), default="false")

    organization = relationship("Organization", back_populates="calculations")
    activity = relationship("Activity", back_populates="calculation")

Database di Emission Factors: DEFRA, EPA, ecoinvent, Climatiq

I fattori di emissione sono il cuore matematico del carbon accounting: convertono una quantità di attivita (litri di gasolio, kWh consumati, km percorsi, euro spesi) in tonnellate di CO2e. La qualità e l'aggiornamento dei fattori determina direttamente la qualità del report.

I Principali Database

DatabaseGestoreCoperturaAggiornamentoAccesso
DEFRA/BEIS UK Govt (DESNZ) UK + internazionale, tutti gli scope Annuale (luglio) Gratuito (Excel)
EPA GHG Hub US EPA USA, mobilita, energia, Scope 3 Annuale (gennaio) Gratuito (Excel)
ecoinvent ecoinvent Association Globale, LCA completo, 18.000+ dataset Semestrale Licenza a pagamento
IPCC EF Database IPCC Globale, inventari nazionali Con ogni Assessment Report Gratuito
Climatiq API Climatiq Multi-source, 50.000+ fattori Continuo (real-time) API (freemium)
AIB Residual Mix Association of Issuing Bodies Europa, Scope 2 market-based Annuale Gratuito
IEA Electricity International Energy Agency Globale, fattori rete elettrica Annuale Parzialmente gratuito

Integrazione Climatiq API

Climatiq fornisce un'API REST con più di 50.000 fattori di emissione verificati, copertura ISO 14067 e GHG Protocol, ed e una delle soluzioni più usate per integrare calcoli di emissioni in modo programmatico. L'API supporta Scope 1, 2 e 3 con metodi activity-based e spend-based.

# services/climatiq_client.py - Integrazione Climatiq API
import httpx
import os
from dataclasses import dataclass
from typing import Optional
from functools import lru_cache

CLIMATIQ_BASE_URL = "https://api.climatiq.io"

@dataclass
class EmissionEstimate:
    co2e: float
    co2e_unit: str
    co2e_calculation_method: str
    co2e_calculation_origin: str
    emission_factor_name: str
    emission_factor_id: str
    source: str
    year: int
    region: str

@dataclass
class ActivityData:
    activity_id: str
    data: dict  # { "energy": { "value": 1000, "energy_unit": "kWh" } }
    region: Optional[str] = None
    year: Optional[int] = None

class ClimatiqClient:
    def __init__(self, api_key: Optional[str] = None):
        self.api_key = api_key or os.environ.get("CLIMATIQ_API_KEY")
        if not self.api_key:
            raise ValueError("CLIMATIQ_API_KEY not set")
        self.client = httpx.AsyncClient(
            base_url=CLIMATIQ_BASE_URL,
            headers={
                "Authorization": f"Bearer {self.api_key}",
                "Content-Type": "application/json"
            },
            timeout=30.0
        )

    async def estimate_emission(self, activity: ActivityData) -> EmissionEstimate:
        """Calcola emissioni per una singola attivita"""
        payload = {
            "emission_factor": {
                "activity_id": activity.activity_id,
            },
            **activity.data
        }
        if activity.region:
            payload["emission_factor"]["region"] = activity.region
        if activity.year:
            payload["emission_factor"]["year"] = activity.year

        response = await self.client.post("/estimate", json=payload)
        response.raise_for_status()
        result = response.json()

        return EmissionEstimate(
            co2e=result["co2e"],
            co2e_unit=result["co2e_unit"],
            co2e_calculation_method=result["co2e_calculation_method"],
            co2e_calculation_origin=result.get("co2e_calculation_origin", ""),
            emission_factor_name=result["emission_factor"]["name"],
            emission_factor_id=result["emission_factor"]["activity_id"],
            source=result["emission_factor"]["source"],
            year=result["emission_factor"]["year"],
            region=result["emission_factor"].get("region", "")
        )

    async def estimate_batch(self, activities: list[ActivityData]) -> list[EmissionEstimate]:
        """Calcolo batch fino a 100 attivita"""
        payload = {
            "requests": [
                {
                    "emission_factor": {"activity_id": a.activity_id},
                    **a.data
                }
                for a in activities
            ]
        }
        response = await self.client.post("/batch", json=payload)
        response.raise_for_status()
        results = response.json()["results"]
        return [
            EmissionEstimate(
                co2e=r["co2e"],
                co2e_unit=r["co2e_unit"],
                co2e_calculation_method=r["co2e_calculation_method"],
                co2e_calculation_origin=r.get("co2e_calculation_origin", ""),
                emission_factor_name=r["emission_factor"]["name"],
                emission_factor_id=r["emission_factor"]["activity_id"],
                source=r["emission_factor"]["source"],
                year=r["emission_factor"]["year"],
                region=r["emission_factor"].get("region", "")
            )
            for r in results
        ]

    async def search_emission_factors(
        self,
        query: str,
        region: Optional[str] = None,
        year: Optional[int] = None,
        source: Optional[str] = None
    ) -> list[dict]:
        """Ricerca fattori di emissione nel database Climatiq"""
        params = {"query": query, "page": 1, "page_size": 20}
        if region:
            params["region"] = region
        if year:
            params["year"] = year
        if source:
            params["source"] = source

        response = await self.client.get("/search", params=params)
        response.raise_for_status()
        return response.json()["results"]

    async def __aenter__(self):
        return self

    async def __aexit__(self, *args):
        await self.client.aclose()


# Esempio utilizzo - calcolo Scope 2 location-based per impianto italiano
async def calculate_scope2_italy(kwh_consumed: float) -> float:
    """
    Calcola emissioni Scope 2 location-based per consumo elettrico in Italia.
    Fattore IEA 2024 per l'Italia: ~0.233 kgCO2e/kWh
    """
    async with ClimatiqClient() as client:
        activity = ActivityData(
            activity_id="electricity-supply_grid-source_supplier_mix",
            data={
                "energy": {
                    "value": kwh_consumed,
                    "energy_unit": "kWh"
                }
            },
            region="IT",  # Italia
            year=2024
        )
        estimate = await client.estimate_emission(activity)
        # Converti kgCO2e in tonnellate CO2e
        return estimate.co2e / 1000 if estimate.co2e_unit == "kg" else estimate.co2e

Architettura a Microservizi per Carbon Accounting

Una piattaforma enterprise di carbon accounting deve gestire flussi di dati eterogenei (bollette energetiche, dati ERP, note spese, dati fornitori), calcoli complessi e auditabili, e la generazione di report in formati diversi. L'architettura a microservizi e la scelta naturale per questi requisiti.

I Quattro Servizi Core

1. Data Collection Service

Responsabile dell'acquisizione dati da fonti diverse: API ERP (SAP, Oracle), utility bills tramite OCR/parser, strumenti di travel management (Concur, TravelPerk), dati procurement, upload manuali CSV, API fornitori. Espone endpoint per l'ingestion e gestisce la normalizzazione delle unita di misura.

2. Calculation Engine

Il cuore computazionale: riceve attivita normalizzate, seleziona i fattori di emissione appropriati (locali o via Climatiq API), applica le formule GHG Protocol, produce risultati immutabili con formula di calcolo esplicita per l'audit. Supporta ricalcoli storici quando i fattori vengono aggiornati.

3. Reporting Service

Genera report nei formati richiesti dai framework: CSRD/ESRS (con tagging XBRL), CDP questionnaire (formato JSON/XML), GRI Standards disclosures, report interni per dashboard. Gestisce il versioning dei report e la firma digitale per l'assurance.

4. Audit Trail Service

Mantiene un log immutabile di ogni operazione: chi ha inserito i dati, quale fattore e stato usato, quando e stato calcolato, chi ha approvato. Supporta il data lineage completo: dal dato sorgente al numero nel report finale. Essenziale per l'assurance da parte dei revisori.

Diagramma Architetturale


┌─────────────────────────────────────────────────────────────────┐
│                    CARBON ACCOUNTING PLATFORM                    │
├─────────────────────────────────────────────────────────────────┤
│  FRONTEND                                                        │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────────┐ │
│  │  Dashboard  │  │ Data Input  │  │   Report Generator      │ │
│  │  (Angular)  │  │   Wizard    │  │   (CSRD/CDP/GRI)        │ │
│  └──────┬──────┘  └──────┬──────┘  └─────────────┬───────────┘ │
├─────────┼────────────────┼─────────────────────────┼────────────┤
│  API GATEWAY (FastAPI/Kong)                                      │
│         │                │                          │            │
├─────────┼────────────────┼─────────────────────────┼────────────┤
│  MICROSERVICES                                                   │
│  ┌──────▼──────┐  ┌──────▼──────┐  ┌──────────────▼─────────┐ │
│  │   Data      │  │ Calculation │  │    Reporting           │ │
│  │ Collection  │→ │   Engine    │→ │    Service             │ │
│  │  Service    │  │  (Python)   │  │    (PDF/XBRL/JSON)     │ │
│  └──────┬──────┘  └──────┬──────┘  └────────────────────────┘ │
│         │                │                                       │
│  ┌──────▼──────┐  ┌──────▼──────┐  ┌────────────────────────┐ │
│  │    SAP      │  │  Climatiq   │  │   Audit Trail          │ │
│  │ Connector   │  │  API Client │  │   Service              │ │
│  └─────────────┘  └─────────────┘  └────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│  DATA LAYER                                                      │
│  ┌──────────────┐  ┌─────────────┐  ┌────────────────────────┐ │
│  │  PostgreSQL  │  │  TimeSeries │  │    Object Storage      │ │
│  │  (Core data) │  │ (InfluxDB)  │  │  (S3 - documents)      │ │
│  └──────────────┘  └─────────────┘  └────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

INTEGRAZIONI ESTERNE:
- SAP S/4HANA (procurement, energy data)
- Oracle NetSuite (financials per spend-based)
- TravelPerk/Concur (travel data - Cat.6)
- Climatiq API (emission factors)
- AIB Registry (GO certificates - Scope 2 market-based)
- Utility providers API (bollette energia)
      

Calculation Engine: Implementazione Python con FastAPI

Il motore di calcolo e il componente più critico. Deve essere preciso, auditabile, versionato e in grado di gestire migliaia di calcoli in parallelo. Ecco un'implementazione completa con FastAPI.

# calculation_engine/main.py - FastAPI Calculation Engine
from fastapi import FastAPI, HTTPException, BackgroundTasks, Depends
from pydantic import BaseModel, Field, validator
from typing import Optional, List
from datetime import datetime
from enum import Enum
import pandas as pd
import uuid
import asyncio
from decimal import Decimal, ROUND_HALF_UP

app = FastAPI(
    title="Carbon Calculation Engine",
    description="GHG Protocol-compliant emission calculation service",
    version="2.1.0"
)

# ── Pydantic Models ──────────────────────────────────────────────

class ScopeEnum(str, Enum):
    scope_1 = "scope_1"
    scope_2_location = "scope_2_location"
    scope_2_market = "scope_2_market"
    scope_3 = "scope_3"

class CalculationMethod(str, Enum):
    activity_based = "activity_based"    # quantità * fattore emissione
    spend_based = "spend_based"          # spesa in EUR * fattore intensità
    average_data = "average_data"        # media su periodi
    supplier_specific = "supplier_specific"  # dati diretti da fornitori

class ActivityInput(BaseModel):
    activity_id: str = Field(..., description="ID univoco attivita sorgente")
    emission_source_id: str
    scope: ScopeEnum
    scope_3_category: Optional[str] = None
    period_start: datetime
    period_end: datetime
    quantity: float = Field(..., gt=0, description="Quantità attivita")
    unit: str = Field(..., description="Unita di misura (kWh, liter, km, tonne, EUR)")
    emission_factor_id: Optional[str] = None  # se None, il motore cerca automaticamente
    calculation_method: CalculationMethod = CalculationMethod.activity_based
    region: Optional[str] = None
    data_quality: str = "measured"

    @validator("quantity")
    def quantity_must_be_positive(cls, v):
        if v <= 0:
            raise ValueError("La quantità deve essere positiva")
        return v

class CalculationResult(BaseModel):
    calculation_id: str
    activity_id: str
    scope: ScopeEnum
    co2e_tonnes: float
    co2_tonnes: Optional[float]
    ch4_tonnes: Optional[float]
    n2o_tonnes: Optional[float]
    emission_factor_value: float
    emission_factor_unit: str
    emission_factor_source: str
    emission_factor_year: int
    calculation_formula: str
    calculation_method: str
    uncertainty_percentage: Optional[float]
    calculated_at: datetime
    audit_reference: str

class BatchCalculationRequest(BaseModel):
    organization_id: str
    reporting_year: int
    activities: List[ActivityInput] = Field(..., max_items=500)

class ScopeAggregation(BaseModel):
    scope_1_tonnes: float
    scope_2_location_tonnes: float
    scope_2_market_tonnes: float
    scope_3_tonnes: float
    scope_3_by_category: dict
    total_location_based: float
    total_market_based: float
    reporting_year: int
    organization_id: str

# ── Calculation Logic ────────────────────────────────────────────

class EmissionCalculator:
    """
    Implementazione GHG Protocol Corporate Standard.
    Formula base: Emissioni (kgCO2e) = Attivita x Fattore di Emissione
    """

    # Fattori GWP AR6 (IPCC Sixth Assessment Report, 2021)
    GWP_AR6 = {
        "CO2": 1.0,
        "CH4": 27.9,    # CH4 fossil
        "CH4_bio": 27.9,
        "N2O": 273.0,
        "SF6": 25200.0,
        "NF3": 17400.0,
        "HFC134a": 1526.0,
        "HFC32": 771.0,
    }

    # Fattori di incertezza per qualità dato (DEFRA methodology)
    UNCERTAINTY_BY_DATA_QUALITY = {
        "measured": 5.0,      # dati misurati diretti
        "calculated": 10.0,   # calcolati da misure
        "estimated": 20.0,    # stime con dati proxy
        "spend_based": 35.0,  # spend-based (meno preciso)
        "default": 25.0,
    }

    def calculate_activity_based(
        self,
        quantity: float,
        unit: str,
        emission_factor: dict,
        gwp_version: str = "AR6"
    ) -> dict:
        """
        Calcolo activity-based:
        CO2e (kg) = Quantità (unita) x EF (kgCO2e/unita)
        """
        ef_value = emission_factor["co2e_factor"]
        ef_unit = emission_factor["unit_type"]

        # Verifica compatibilità unita
        if not self._units_compatible(unit, ef_unit):
            raise ValueError(
                f"Unita incompatibili: attivita in {unit}, "
                f"fattore in {ef_unit}"
            )

        co2e_kg = quantity * ef_value

        # Calcola componenti separate se disponibili
        co2_kg = quantity * emission_factor.get("co2_factor", 0)
        ch4_kg = quantity * emission_factor.get("ch4_factor", 0)
        n2o_kg = quantity * emission_factor.get("n2o_factor", 0)

        return {
            "co2e_tonnes": round(co2e_kg / 1000, 6),
            "co2_tonnes": round(co2_kg / 1000, 6) if co2_kg else None,
            "ch4_tonnes": round(ch4_kg / 1000, 6) if ch4_kg else None,
            "n2o_tonnes": round(n2o_kg / 1000, 6) if n2o_kg else None,
            "formula": (
                f"{quantity} {unit} x {ef_value} kgCO2e/{unit} "
                f"= {co2e_kg:.4f} kgCO2e "
                f"= {co2e_kg/1000:.6f} tCO2e"
            )
        }

    def calculate_spend_based(
        self,
        spend_eur: float,
        emission_intensity: float,  # kgCO2e/EUR
        currency: str = "EUR",
        exchange_rate: float = 1.0
    ) -> dict:
        """
        Calcolo spend-based (Scope 3 Cat.1 quando non si hanno dati attivita):
        CO2e (kg) = Spesa (EUR) x Intensità (kgCO2e/EUR)
        """
        spend_normalized = spend_eur * exchange_rate
        co2e_kg = spend_normalized * emission_intensity

        return {
            "co2e_tonnes": round(co2e_kg / 1000, 6),
            "co2_tonnes": None,
            "ch4_tonnes": None,
            "n2o_tonnes": None,
            "formula": (
                f"{spend_normalized:.2f} EUR x {emission_intensity} kgCO2e/EUR "
                f"= {co2e_kg:.4f} kgCO2e"
            )
        }

    def _units_compatible(self, activity_unit: str, ef_unit: str) -> bool:
        """Verifica compatibilità unita di misura"""
        # Normalizza: il fattore e tipicamente espresso come kgCO2e/[unita_attivita]
        ef_denominator = ef_unit.split("/")[-1].strip().lower() if "/" in ef_unit else ef_unit
        return activity_unit.lower() == ef_denominator

    def aggregate_by_scope(
        self,
        calculations: List[CalculationResult]
    ) -> ScopeAggregation:
        """Aggrega i calcoli per scope - usa pandas per performance"""
        df = pd.DataFrame([c.dict() for c in calculations])

        scope_1 = df[df["scope"] == "scope_1"]["co2e_tonnes"].sum()
        scope_2_loc = df[df["scope"] == "scope_2_location"]["co2e_tonnes"].sum()
        scope_2_mkt = df[df["scope"] == "scope_2_market"]["co2e_tonnes"].sum()
        scope_3_df = df[df["scope"] == "scope_3"]
        scope_3_total = scope_3_df["co2e_tonnes"].sum()

        # Aggregazione Scope 3 per categoria
        scope_3_by_cat = {}
        if not scope_3_df.empty and "scope_3_category" in scope_3_df.columns:
            scope_3_by_cat = (
                scope_3_df.groupby("scope_3_category")["co2e_tonnes"]
                .sum()
                .to_dict()
            )

        return ScopeAggregation(
            scope_1_tonnes=round(scope_1, 3),
            scope_2_location_tonnes=round(scope_2_loc, 3),
            scope_2_market_tonnes=round(scope_2_mkt, 3),
            scope_3_tonnes=round(scope_3_total, 3),
            scope_3_by_category=scope_3_by_cat,
            total_location_based=round(scope_1 + scope_2_loc + scope_3_total, 3),
            total_market_based=round(scope_1 + scope_2_mkt + scope_3_total, 3),
            reporting_year=2024,
            organization_id=""
        )

calculator = EmissionCalculator()

# ── API Endpoints ────────────────────────────────────────────────

@app.post("/v1/calculate/single", response_model=CalculationResult)
async def calculate_single(activity: ActivityInput):
    """Calcola emissioni per una singola attivita"""
    # In produzione: cerca emission factor dal DB o Climatiq
    # Qui mock per illustrazione
    mock_ef = {
        "co2e_factor": 0.233,  # kgCO2e/kWh - rete italiana 2024
        "co2_factor": 0.228,
        "ch4_factor": 0.002,
        "n2o_factor": 0.001,
        "unit_type": "kgCO2e/kWh",
        "source": "IEA 2024",
        "year": 2024
    }

    result = calculator.calculate_activity_based(
        quantity=activity.quantity,
        unit=activity.unit,
        emission_factor=mock_ef
    )

    calculation_id = str(uuid.uuid4())
    uncertainty = calculator.UNCERTAINTY_BY_DATA_QUALITY.get(
        activity.data_quality, 25.0
    )

    return CalculationResult(
        calculation_id=calculation_id,
        activity_id=activity.activity_id,
        scope=activity.scope,
        co2e_tonnes=result["co2e_tonnes"],
        co2_tonnes=result["co2_tonnes"],
        ch4_tonnes=result["ch4_tonnes"],
        n2o_tonnes=result["n2o_tonnes"],
        emission_factor_value=mock_ef["co2e_factor"],
        emission_factor_unit=mock_ef["unit_type"],
        emission_factor_source=mock_ef["source"],
        emission_factor_year=mock_ef["year"],
        calculation_formula=result["formula"],
        calculation_method=activity.calculation_method.value,
        uncertainty_percentage=uncertainty,
        calculated_at=datetime.utcnow(),
        audit_reference=f"CALC-{calculation_id[:8].upper()}"
    )

@app.post("/v1/calculate/batch", response_model=List[CalculationResult])
async def calculate_batch(request: BatchCalculationRequest):
    """Calcola emissioni per un batch di attivita (max 500)"""
    tasks = [calculate_single(activity) for activity in request.activities]
    results = await asyncio.gather(*tasks, return_exceptions=True)

    successful = [r for r in results if isinstance(r, CalculationResult)]
    failed = [r for r in results if isinstance(r, Exception)]

    if failed:
        # Log errori ma non blocca il batch
        pass

    return successful

@app.get("/v1/organizations/{org_id}/summary", response_model=ScopeAggregation)
async def get_emissions_summary(org_id: str, year: int = 2024):
    """Riepilogo emissioni per scope per un'organizzazione"""
    # In produzione: query dal DB
    pass

Automazione Scope 3: ERP, Procurement e Travel

Lo Scope 3 e la sfida più grande del carbon accounting: dati distribuiti tra decine di sistemi, fornitori con diversi livelli di maturita, metodologie di calcolo diverse per categoria. L'automazione e l'unico modo per renderlo sostenibile operativamente.

Integrazione SAP per Scope 3 Categoria 1 (Purchased Goods)

# integrations/sap_connector.py - Estrazione dati procurement da SAP
import httpx
from dataclasses import dataclass
from typing import List, Optional
from datetime import date
import pandas as pd

@dataclass
class ProcurementRecord:
    purchase_order_id: str
    vendor_id: str
    vendor_name: str
    vendor_country: str
    material_code: str
    material_description: str
    quantity: float
    unit: str
    amount_eur: float
    nace_code: Optional[str]  # classificazione settore fornitore
    delivery_date: date

class SAPConnector:
    """
    Connettore SAP S/4HANA via OData API.
    Estrae dati procurement per Scope 3 Cat.1 e Cat.4.
    """

    def __init__(self, base_url: str, client_id: str, client_secret: str):
        self.base_url = base_url
        self.client_id = client_id
        self.client_secret = client_secret
        self._token: Optional[str] = None

    async def authenticate(self):
        """OAuth 2.0 client credentials per SAP"""
        async with httpx.AsyncClient() as client:
            response = await client.post(
                f"{self.base_url}/oauth/token",
                data={
                    "grant_type": "client_credentials",
                    "client_id": self.client_id,
                    "client_secret": self.client_secret,
                }
            )
            response.raise_for_status()
            self._token = response.json()["access_token"]

    async def get_purchase_orders(
        self,
        year: int,
        cost_center: Optional[str] = None
    ) -> List[ProcurementRecord]:
        """
        Recupera ordini di acquisto dal modulo MM di SAP.
        Endpoint OData: /sap/opu/odata/sap/MM_PUR_PO_MANAGE_SRV/
        """
        if not self._token:
            await self.authenticate()

        params = {
            "$filter": f"PostingDate ge datetime'{year}-01-01T00:00:00' and "
                       f"PostingDate le datetime'{year}-12-31T23:59:59'",
            "$select": "PurchaseOrder,Supplier,SupplierName,OrderedQuantity,"
                       "PurchaseOrderQuantityUnit,NetPriceAmount,Currency",
            "$format": "json",
            "$top": 5000
        }

        async with httpx.AsyncClient() as client:
            response = await client.get(
                f"{self.base_url}/sap/opu/odata/sap/MM_PUR_PO_MANAGE_SRV/"
                "A_PurchaseOrder",
                headers={"Authorization": f"Bearer {self._token}"},
                params=params
            )
            response.raise_for_status()
            data = response.json()["d"]["results"]

        return [
            ProcurementRecord(
                purchase_order_id=item["PurchaseOrder"],
                vendor_id=item["Supplier"],
                vendor_name=item["SupplierName"],
                vendor_country="IT",  # da arricchire con master data
                material_code=item.get("Material", ""),
                material_description=item.get("MaterialName", ""),
                quantity=float(item["OrderedQuantity"]),
                unit=item["PurchaseOrderQuantityUnit"],
                amount_eur=float(item["NetPriceAmount"]),
                nace_code=None,  # da mappare con codifica NACE
                delivery_date=date.fromisoformat(item.get("ScheduleLine", f"{year}-12-31")[:10])
            )
            for item in data
        ]

    def calculate_scope3_cat1_spend_based(
        self,
        records: List[ProcurementRecord],
        emission_intensities: dict  # { nace_code: kgCO2e_per_EUR }
    ) -> pd.DataFrame:
        """
        Calcolo Scope 3 Cat.1 con metodo spend-based.
        Da usare quando non si hanno dati attivita dai fornitori.
        Fattori da EXIOBASE o WIOD (World Input-Output Database).
        """
        df = pd.DataFrame([vars(r) for r in records])

        # Mappa NACE -> intensità emissioni
        default_intensity = emission_intensities.get("DEFAULT", 0.35)  # kgCO2e/EUR
        df["emission_intensity"] = df["nace_code"].map(emission_intensities).fillna(default_intensity)
        df["co2e_kg"] = df["amount_eur"] * df["emission_intensity"]
        df["co2e_tonnes"] = df["co2e_kg"] / 1000

        # Aggregazione per fornitore
        summary = df.groupby(["vendor_id", "vendor_name"]).agg(
            total_spend_eur=("amount_eur", "sum"),
            total_co2e_tonnes=("co2e_tonnes", "sum"),
            transaction_count=("purchase_order_id", "count")
        ).reset_index()

        summary["avg_intensity"] = summary["total_co2e_tonnes"] * 1000 / summary["total_spend_eur"]
        return summary.sort_values("total_co2e_tonnes", ascending=False)


# integrations/travel_connector.py - Dati viaggi per Scope 3 Cat.6
@dataclass
class TravelRecord:
    employee_id: str
    travel_date: date
    origin_iata: str      # codice aeroporto IATA
    destination_iata: str
    transport_mode: str   # air, rail, car, ferry
    distance_km: float
    travel_class: str     # economy, business, first
    booking_amount_eur: float

class TravelDataProcessor:
    """Processa dati di business travel per Scope 3 Cat.6"""

    # Fattori emissione voli (kgCO2e/pkm) - DEFRA 2024
    DEFRA_FLIGHT_FACTORS = {
        ("short_haul", "economy"): 0.151,
        ("short_haul", "business"): 0.227,
        ("medium_haul", "economy"): 0.131,
        ("medium_haul", "business"): 0.262,
        ("long_haul", "economy"): 0.195,
        ("long_haul", "business"): 0.585,
        ("long_haul", "first"): 0.780,
    }

    # Fattori treno (kgCO2e/pkm) - media europea
    RAIL_FACTORS = {
        "IT": 0.004,  # Trenitalia (alta % rinnovabili)
        "DE": 0.006,
        "FR": 0.002,
        "DEFAULT": 0.041,
    }

    def classify_flight(self, distance_km: float) -> str:
        if distance_km < 1500:
            return "short_haul"
        elif distance_km < 4000:
            return "medium_haul"
        else:
            return "long_haul"

    def calculate_flight_emissions(self, record: TravelRecord) -> float:
        """
        Calcolo emissioni volo con Radiative Forcing Index (RFI = 1.9)
        per effetti non-CO2 ad alta quota (metodo DEFRA con uplift factor)
        """
        haul = self.classify_flight(record.distance_km)
        travel_class = record.travel_class.lower() if record.travel_class else "economy"
        key = (haul, travel_class)

        base_factor = self.DEFRA_FLIGHT_FACTORS.get(
            key,
            self.DEFRA_FLIGHT_FACTORS[(haul, "economy")]
        )

        # Applica Radiative Forcing (RFI) per effetti non-CO2 in alta quota
        rfi_factor = 1.9
        co2e_kg = record.distance_km * base_factor * rfi_factor

        return co2e_kg / 1000  # in tonnellate

    def process_travel_data(self, records: List[TravelRecord]) -> dict:
        """Elabora tutti i dati travel e restituisce riepilogo per Cat.6"""
        results = []
        for record in records:
            if record.transport_mode == "air":
                co2e = self.calculate_flight_emissions(record)
                method = "DEFRA 2024 with RFI=1.9"
            elif record.transport_mode == "rail":
                country = record.origin_iata[:2]  # approssimazione
                factor = self.RAIL_FACTORS.get(country, self.RAIL_FACTORS["DEFAULT"])
                co2e = record.distance_km * factor / 1000
                method = f"Rail factor {country}"
            else:
                co2e = record.distance_km * 0.171 / 1000  # car average
                method = "DEFRA car average"

            results.append({
                "employee_id": record.employee_id,
                "travel_date": record.travel_date,
                "transport_mode": record.transport_mode,
                "distance_km": record.distance_km,
                "co2e_tonnes": co2e,
                "method": method
            })

        df = pd.DataFrame(results)
        return {
            "total_co2e_tonnes": df["co2e_tonnes"].sum(),
            "by_mode": df.groupby("transport_mode")["co2e_tonnes"].sum().to_dict(),
            "by_month": df.groupby(df["travel_date"].apply(lambda x: x.month))["co2e_tonnes"].sum().to_dict(),
            "total_km": df["distance_km"].sum(),
            "record_count": len(df)
        }

Reporting Automatico: CSRD/ESRS, CDP e GRI

Generare i report regolamentari e spesso il processo più time-consuming del carbon accounting. L'automazione riduce drasticamente il tempo da settimane a ore, aumenta la consistenza e garantisce che ogni cifra sia tracciabile alla sorgente.

Struttura Report CSRD/ESRS E1 (Climate Change)

Lo standard ESRS E1 dedicato al clima richiede disclosure su: governance del clima, strategia e scenario analysis, gestione dei rischi, metriche e target. Le metriche di emissioni sono definite in ESRS E1-6 e richiedono dati per tutti e tre gli scope.

# reporting/csrd_generator.py - Generazione report CSRD/ESRS E1
from dataclasses import dataclass, asdict
from typing import List, Optional, Dict
from datetime import datetime
import json

@dataclass
class ESRS_E1_6_Disclosure:
    """
    ESRS E1-6: Gross Scopes 1, 2 and 3 greenhouse gas emissions
    Disclosure requirements per ESRS E1 (climate change)
    """
    organization_name: str
    legal_entity_identifier: str  # LEI
    reporting_period: str  # es. "FY2024"
    reporting_standard: str = "ESRS E1 - Climate Change"

    # Scope 1 - Emissioni dirette
    scope_1_total_gross_tco2e: float = 0.0
    scope_1_breakdown_by_ghg: Dict[str, float] = None  # { "CO2": x, "CH4": y ... }
    scope_1_breakdown_by_source: Dict[str, float] = None

    # Scope 2 - Emissioni indirette energia
    scope_2_location_based_tco2e: float = 0.0
    scope_2_market_based_tco2e: float = 0.0
    scope_2_purchased_electricity_kwh: float = 0.0
    scope_2_renewable_electricity_percentage: float = 0.0

    # Scope 3 - Emissioni catena del valore
    scope_3_total_tco2e: float = 0.0
    scope_3_upstream_tco2e: float = 0.0
    scope_3_downstream_tco2e: float = 0.0
    scope_3_by_category: Dict[str, float] = None

    # Intensità emissioni
    revenue_intensity_tco2e_per_meur: Optional[float] = None  # tCO2e/M EUR
    employee_intensity_tco2e_per_fte: Optional[float] = None

    # Confronto anno precedente e target
    base_year: int = 2020
    scope_1_2_reduction_vs_base: Optional[float] = None  # percentuale
    sbti_target: Optional[str] = None  # es. "1.5°C aligned, -42% by 2030"

    # Metodologia
    ghg_accounting_standard: str = "GHG Protocol Corporate Standard"
    emission_factor_sources: List[str] = None
    data_quality_notes: str = ""
    assurance_level: str = "limited_assurance"
    assurance_provider: str = ""

    def __post_init__(self):
        if self.scope_3_by_category is None:
            self.scope_3_by_category = {}
        if self.scope_1_breakdown_by_ghg is None:
            self.scope_1_breakdown_by_ghg = {}
        if self.emission_factor_sources is None:
            self.emission_factor_sources = []

    @property
    def total_ghg_location_based(self) -> float:
        return (self.scope_1_total_gross_tco2e +
                self.scope_2_location_based_tco2e +
                self.scope_3_total_tco2e)

    @property
    def total_ghg_market_based(self) -> float:
        return (self.scope_1_total_gross_tco2e +
                self.scope_2_market_based_tco2e +
                self.scope_3_total_tco2e)

class CSRDReportGenerator:
    def generate_esrs_e1_json(self, disclosure: ESRS_E1_6_Disclosure) -> str:
        """Genera disclosure ESRS E1-6 in formato JSON strutturato"""
        report = {
            "metadata": {
                "standard": disclosure.reporting_standard,
                "generated_at": datetime.utcnow().isoformat(),
                "reporting_period": disclosure.reporting_period,
                "organization": disclosure.organization_name,
                "lei": disclosure.legal_entity_identifier,
            },
            "ESRS_E1-6": {
                "gross_scope_1_tco2e": disclosure.scope_1_total_gross_tco2e,
                "scope_1_breakdown_by_ghg": disclosure.scope_1_breakdown_by_ghg,
                "scope_2_location_based_tco2e": disclosure.scope_2_location_based_tco2e,
                "scope_2_market_based_tco2e": disclosure.scope_2_market_based_tco2e,
                "scope_2_purchased_electricity_kwh": disclosure.scope_2_purchased_electricity_kwh,
                "scope_2_renewable_pct": disclosure.scope_2_renewable_electricity_percentage,
                "scope_3_total_tco2e": disclosure.scope_3_total_tco2e,
                "scope_3_by_category": disclosure.scope_3_by_category,
                "total_ghg_location_based_tco2e": disclosure.total_ghg_location_based,
                "total_ghg_market_based_tco2e": disclosure.total_ghg_market_based,
                "ghg_intensity_revenue": disclosure.revenue_intensity_tco2e_per_meur,
                "ghg_intensity_employee": disclosure.employee_intensity_tco2e_per_fte,
            },
            "methodology": {
                "accounting_standard": disclosure.ghg_accounting_standard,
                "emission_factor_sources": disclosure.emission_factor_sources,
                "base_year": disclosure.base_year,
                "consolidation_approach": "operational_control",
                "data_quality": disclosure.data_quality_notes,
            },
            "assurance": {
                "level": disclosure.assurance_level,
                "provider": disclosure.assurance_provider,
            },
            "targets": {
                "sbti_commitment": disclosure.sbti_target,
                "scope_1_2_reduction_vs_base": disclosure.scope_1_2_reduction_vs_base,
            }
        }
        return json.dumps(report, indent=2, ensure_ascii=False)

    def generate_cdp_questionnaire_c6(self, disclosure: ESRS_E1_6_Disclosure) -> dict:
        """
        Genera risposte per CDP questionnaire - sezione C6 (Emissions Data).
        CDP condivide molti requisiti con CSRD, il che riduce il double reporting.
        """
        return {
            "C6.1": {
                "question": "Provide total gross global Scope 1 emissions in metric tons CO2e",
                "response": disclosure.scope_1_total_gross_tco2e,
                "unit": "metric tons CO2e"
            },
            "C6.2": {
                "question": "Describe Scope 1 emissions by constituent gases",
                "response": disclosure.scope_1_breakdown_by_ghg
            },
            "C6.3": {
                "question": "Provide total gross global Scope 2 emissions",
                "response": {
                    "location_based": disclosure.scope_2_location_based_tco2e,
                    "market_based": disclosure.scope_2_market_based_tco2e,
                }
            },
            "C6.5": {
                "question": "Account for Scope 3 emissions",
                "response": {
                    "total": disclosure.scope_3_total_tco2e,
                    "categories": disclosure.scope_3_by_category
                }
            },
            "C6.10": {
                "question": "Describe Scope 1 and 2 GHG emissions by location",
                "note": "See facility-level breakdown in supporting data"
            }
        }

Audit Trail e Data Lineage

Con la CSRD che richiede limited assurance da un revisore abilitato, l'audit trail non e un requisito opzionale: e la spina dorsale della piattaforma. Ogni numero del report deve essere tracciabile fino alla sorgente originale con la formula di calcolo esplicita.

# audit/audit_trail.py - Sistema di audit immutabile
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Dict, Optional
import hashlib
import json
import uuid

@dataclass(frozen=True)  # immutabile
class AuditEvent:
    """
    Evento di audit immutabile. Non si modifica mai, solo si aggiungono nuovi eventi.
    Il hash SHA256 garantisce l'integrita della catena.
    """
    event_id: str
    event_type: str  # DATA_INGESTED, FACTOR_SELECTED, CALCULATION_PERFORMED, REPORT_GENERATED, APPROVED
    entity_type: str  # Activity, Calculation, EmissionFactor, Report
    entity_id: str
    actor_id: str    # user_id o service_name per azioni automatiche
    actor_type: str  # human, system
    timestamp: str   # ISO 8601 UTC
    data_before: Optional[str]  # JSON snapshot before change
    data_after: Optional[str]   # JSON snapshot after change
    metadata: str    # JSON { formula, ef_source, notes ... }
    previous_hash: str  # hash dell'evento precedente (blockchain-like)
    event_hash: str     # SHA256 di tutti i campi

    @classmethod
    def create(
        cls,
        event_type: str,
        entity_type: str,
        entity_id: str,
        actor_id: str,
        actor_type: str = "human",
        data_before: Optional[Dict] = None,
        data_after: Optional[Dict] = None,
        metadata: Optional[Dict] = None,
        previous_hash: str = "GENESIS"
    ) -> "AuditEvent":
        event_id = str(uuid.uuid4())
        timestamp = datetime.utcnow().isoformat() + "Z"

        data_before_str = json.dumps(data_before, sort_keys=True) if data_before else None
        data_after_str = json.dumps(data_after, sort_keys=True) if data_after else None
        metadata_str = json.dumps(metadata or {}, sort_keys=True)

        # Calcola hash dell'evento per garanzia di integrita
        hash_content = "|".join([
            event_id, event_type, entity_type, entity_id,
            actor_id, timestamp, data_after_str or "", previous_hash
        ])
        event_hash = hashlib.sha256(hash_content.encode()).hexdigest()

        return cls(
            event_id=event_id,
            event_type=event_type,
            entity_type=entity_type,
            entity_id=entity_id,
            actor_id=actor_id,
            actor_type=actor_type,
            timestamp=timestamp,
            data_before=data_before_str,
            data_after=data_after_str,
            metadata=metadata_str,
            previous_hash=previous_hash,
            event_hash=event_hash
        )

class DataLineageTracker:
    """
    Traccia il percorso completo di un dato dalla sorgente al report.
    Permette ai revisori di verificare ogni numero del report finale.
    """

    def __init__(self, db_session):
        self.db = db_session

    def trace_calculation(self, calculation_id: str) -> dict:
        """
        Ricostruisce il lineage completo di un calcolo:
        Report -> Calculation -> Activity -> EmissionFactor -> RawData -> Source System
        """
        # In produzione: query al DB per tutti gli eventi correlati
        lineage = {
            "calculation_id": calculation_id,
            "trace": [
                {
                    "step": 1,
                    "description": "Raw data ingested from SAP S/4HANA",
                    "system": "SAP_CONNECTOR",
                    "timestamp": "2025-01-15T08:30:00Z",
                    "data_summary": "Natural gas consumption: 5,420 m3, Jan 2024",
                    "audit_event_id": "AE-001"
                },
                {
                    "step": 2,
                    "description": "Unit conversion applied: m3 -> kWh",
                    "formula": "5,420 m3 x 10.55 kWh/m3 = 57,181 kWh",
                    "timestamp": "2025-01-15T08:31:00Z",
                    "audit_event_id": "AE-002"
                },
                {
                    "step": 3,
                    "description": "Emission factor selected from DEFRA 2024",
                    "emission_factor": "Natural gas - Gross CV: 0.18306 kgCO2e/kWh",
                    "factor_id": "DEFRA_2024_NG_GROSS",
                    "timestamp": "2025-01-15T08:31:05Z",
                    "audit_event_id": "AE-003"
                },
                {
                    "step": 4,
                    "description": "Emission calculation performed",
                    "formula": "57,181 kWh x 0.18306 kgCO2e/kWh = 10,467 kgCO2e = 10.467 tCO2e",
                    "result_tco2e": 10.467,
                    "scope": "Scope 1",
                    "timestamp": "2025-01-15T08:31:06Z",
                    "audit_event_id": "AE-004"
                },
                {
                    "step": 5,
                    "description": "Calculation included in FY2024 CSRD Report - ESRS E1-6",
                    "report_id": "RPT-CSRD-2024-001",
                    "approved_by": "CFO - Mario Rossi",
                    "timestamp": "2025-03-10T14:00:00Z",
                    "audit_event_id": "AE-089"
                }
            ],
            "data_quality_flag": "HIGH",
            "uncertainty_pct": 5.0,
            "verification_status": "verified_by_auditor"
        }
        return lineage

Confronto Piattaforme Enterprise: Build vs Buy

Prima di costruire una piattaforma custom, e essenziale valutare le soluzioni enterprise disponibili. Il mercato del carbon accounting software e molto maturo e le piattaforme leader coprono la maggior parte dei casi d'uso standard.

Piattaforma Punto di Forza Settore Target Prezzo Indicativo CSRD Ready
Persefoni Investor-grade reporting, XBRL tagging, SEC-focused Finance, Corporate $50K-500K/anno Si
Watershed Speed to report, Scope 3 tooling avanzato Tech, Enterprise $100K-1M/anno Si
Sphera LCA integrato, compliance industriale, risk management Manifattura, Energia, Chimico Su richiesta Si
Plan A UX semplice, rapida implementazione PMI, Medie imprese $10K-100K/anno Parziale
IBM Envizi 40.000+ emission factors, integrazione ERP Enterprise, Utility Su richiesta Si
Custom Build Flessibilità totale, integrazione nativa sistemi interni Grandi aziende con requisiti specifici $500K-5M sviluppo Dipende

Quando Costruire Custom vs Acquistare

Acquista una piattaforma se: hai requisiti standard, vuoi andare live in 3-6 mesi, non hai team engineering dedicato alla sostenibilità, il volume di dati e gestibile.

Costruisci custom se: hai processi produttivi molto specifici con dati non gestibili da piattaforme standard, vuoi integrare nativamente con sistemi legacy, hai requisiti di data residency non compatibili con SaaS, o il volume di dati e dell'ordine di milioni di record/anno (costi SaaS diventano proibitivi).

Case Study: PMI Manifatturiera Italiana con CSRD 2025

Un'azienda manifatturiera italiana del settore meccanico di precisione con 1.200 dipendenti, 350 milioni di euro di fatturato e stabilimenti a Torino, Milano e Brescia, si trova nella Wave 1 della CSRD e deve presentare il primo report per l'esercizio 2024 entro giugno 2025.

Inventario Emissioni FY2024

# case_study/inventory_2024.py - Esempio inventario GHG realistico
# Dati rappresentativi per PMI manifatturiera italiana

INVENTORY_2024 = {
    "organization": "MeccanicaPrecisione SpA",
    "reporting_year": 2024,
    "boundary": "Operational Control",
    "currency": "EUR",

    "scope_1": {
        "natural_gas_combustion": {
            "consumption_m3": 485_000,         # caldaie industriali
            "consumption_kwh": 5_116_750,       # conversione: 1 m3 NG = 10.55 kWh
            "emission_factor_kgco2e_kwh": 0.18306,  # DEFRA 2024
            "co2e_tonnes": 937.0,
            "source": "DEFRA 2024 - Natural Gas Gross CV"
        },
        "diesel_mobile": {
            "consumption_liters": 42_000,       # carrelli elevatori, mezzi operativi
            "emission_factor_kgco2e_liter": 2.56,   # DEFRA 2024
            "co2e_tonnes": 107.5,
            "source": "DEFRA 2024 - Diesel"
        },
        "fugitive_refrigerants": {
            "substance": "R-410A",
            "kg_recharged": 45,
            "gwp_ar6": 2088,
            "co2e_tonnes": 94.0,
            "source": "IPCC AR6 GWP100"
        },
        "total_co2e_tonnes": 1138.5
    },

    "scope_2": {
        "purchased_electricity": {
            "consumption_kwh": 8_250_000,
            "location_based": {
                "emission_factor": 0.233,       # IEA Italy 2024 kgCO2e/kWh
                "co2e_tonnes": 1922.3,
                "source": "IEA 2024 Italy Grid"
            },
            "market_based": {
                "go_certificates_kwh": 4_125_000,   # 50% rinnovabili con GO
                "residual_mix_factor": 0.395,        # AIB Italy residual mix 2024
                "co2e_tonnes": 1629.4,               # solo su 50% non coperto da GO
                "source": "AIB Italy Residual Mix 2024"
            }
        }
    },

    "scope_3": {
        "cat_1_purchased_goods": {
            "total_spend_eur": 145_000_000,
            "method": "spend_based + supplier_specific (top 20 suppliers)",
            "co2e_tonnes": 28_450.0,
            "data_quality": "mix: 35% supplier-specific, 65% spend-based"
        },
        "cat_4_upstream_transport": {
            "tonne_km": 3_200_000,
            "emission_factor": 0.089,  # kgCO2e/tonne-km road freight DEFRA
            "co2e_tonnes": 284.8,
        },
        "cat_5_waste": {
            "waste_tonnes": 1_850,
            "co2e_tonnes": 185.0,
            "method": "waste-type specific factors"
        },
        "cat_6_business_travel": {
            "total_km_air": 1_250_000,
            "total_km_rail": 320_000,
            "co2e_tonnes": 312.5,
            "source": "DEFRA 2024 with RFI=1.9"
        },
        "cat_7_employee_commuting": {
            "employees": 1_200,
            "avg_km_per_day": 22,
            "working_days": 220,
            "mode_split": {"car_solo": 0.65, "car_sharing": 0.05,
                           "public_transport": 0.25, "cycling": 0.05},
            "co2e_tonnes": 892.5
        },
        "cat_11_use_of_products": {
            "units_sold": 45_000,
            "avg_energy_use_kwh_per_unit_per_year": 850,
            "product_lifetime_years": 15,
            "co2e_tonnes_per_year": 2_250.0,
            "note": "Calcolato per anno di uso, non lifetime"
        },
        "total_co2e_tonnes": 32_374.8
    },

    "summary": {
        "scope_1_tco2e": 1_138.5,
        "scope_2_location_tco2e": 1_922.3,
        "scope_2_market_tco2e": 1_629.4,
        "scope_3_tco2e": 32_374.8,
        "total_location_based_tco2e": 35_435.6,
        "total_market_based_tco2e": 35_142.7,
        "intensity_tco2e_per_meur_revenue": 101.3,
        "intensity_tco2e_per_fte": 29.5,
        "scope_3_percentage_of_total": 91.4,  # tipico nel manifatturiero
    }
}

Osservazioni sul Case Study

Il risultato più significativo e che lo Scope 3 rappresenta il 91.4% delle emissioni totali, con la Categoria 1 (materie prime e componenti) che da sola vale l'80% dello Scope 3. Questo e tipico del settore manifatturiero e spiega perchè la CSRD richiede la rendicontazione della supply chain: senza Scope 3, il carbon accounting coprirebbe meno del 10% dell'impatto reale.

La differenza tra Scope 2 location-based (1.922 tCO2e) e market-based (1.629 tCO2e) e dovuta all'acquisto di certificati di energia rinnovabile (Garanzie di Origine - GO) per il 50% del consumo elettrico. Il residual mix italiano del 2024 (0.395 kgCO2e/kWh) e più alto del fattore medio della rete (0.233 kgCO2e/kWh): questo e controintuitivo ma corretto metodologicamente, perchè il residual mix esclude le energie già certificate con GO e quindi "contiene" una quota più alta di fonti ad alta intensità.

Testing: Validazione dei Calcoli

I calcoli di emissioni devono essere testati con la stessa rigorosita del codice finanziario. Un errore di un fattore di conversione può portare a errori nell'ordine di grandezza delle centinaia di tonnellate di CO2e.

# tests/test_calculation_engine.py
import pytest
from decimal import Decimal
from calculation_engine.main import EmissionCalculator

@pytest.fixture
def calculator():
    return EmissionCalculator()

class TestScope1Calculations:

    def test_natural_gas_combustion_scope1(self, calculator):
        """
        Test calcolo gas naturale con fattore DEFRA 2024.
        Valore atteso: 5000 kWh x 0.18306 kgCO2e/kWh = 915.3 kgCO2e = 0.9153 tCO2e
        """
        emission_factor = {
            "co2e_factor": 0.18306,
            "co2_factor": 0.18207,
            "ch4_factor": 0.000291,
            "n2o_factor": 0.000087,
            "unit_type": "kgCO2e/kWh",
            "source": "DEFRA 2024",
            "year": 2024
        }

        result = calculator.calculate_activity_based(
            quantity=5000,
            unit="kWh",
            emission_factor=emission_factor
        )

        assert abs(result["co2e_tonnes"] - 0.9153) < 0.001, (
            f"Scope 1 gas naturale: atteso 0.9153, ottenuto {result['co2e_tonnes']}"
        )
        assert result["co2_tonnes"] is not None
        assert "formula" in result

    def test_diesel_combustion_scope1(self, calculator):
        """
        Test calcolo gasolio.
        Fattore DEFRA 2024: 2.56179 kgCO2e/liter
        1000 litri -> 2.56179 kgCO2e -> 0.00256 tCO2e (arrotondato a 6 decimali)
        """
        emission_factor = {
            "co2e_factor": 2.56179,
            "co2_factor": 2.51476,
            "ch4_factor": 0.00179,
            "n2o_factor": 0.00524,
            "unit_type": "kgCO2e/liter",
            "source": "DEFRA 2024",
            "year": 2024
        }

        result = calculator.calculate_activity_based(
            quantity=1000,
            unit="liter",
            emission_factor=emission_factor
        )

        expected = 2561.79 / 1000
        assert abs(result["co2e_tonnes"] - expected) < 0.001

    def test_unit_incompatibility_raises_error(self, calculator):
        """Test che unita incompatibili generino errore"""
        emission_factor = {
            "co2e_factor": 0.18306,
            "unit_type": "kgCO2e/kWh",
        }

        with pytest.raises(ValueError, match="incompatibili"):
            calculator.calculate_activity_based(
                quantity=1000,
                unit="liter",  # incompatibile con kWh
                emission_factor=emission_factor
            )

class TestScope2Calculations:

    def test_scope2_location_based_italy_2024(self, calculator):
        """
        Fattore rete elettrica italiana IEA 2024: 0.233 kgCO2e/kWh
        100.000 kWh -> 23.300 kgCO2e -> 23.3 tCO2e
        """
        emission_factor = {
            "co2e_factor": 0.233,
            "unit_type": "kgCO2e/kWh",
            "source": "IEA 2024 Italy",
            "year": 2024
        }

        result = calculator.calculate_activity_based(
            quantity=100_000,
            unit="kWh",
            emission_factor=emission_factor
        )

        assert abs(result["co2e_tonnes"] - 23.3) < 0.01

    def test_scope2_market_based_with_go_certificates(self, calculator):
        """
        Con GO certificates per energia rinnovabile: fattore = 0 per la quota coperta.
        Residual mix IT 2024: 0.395 kgCO2e/kWh per la quota non coperta.
        50% rinnovabili (GO): 50.000 kWh x 0 = 0
        50% residual mix: 50.000 kWh x 0.395 = 19.750 kgCO2e = 19.75 tCO2e
        """
        # Calcolo solo sulla quota non coperta da GO
        emission_factor_residual = {
            "co2e_factor": 0.395,
            "unit_type": "kgCO2e/kWh",
            "source": "AIB Italy Residual Mix 2024",
            "year": 2024
        }
        non_go_kwh = 50_000  # 50% non coperto da GO

        result = calculator.calculate_activity_based(
            quantity=non_go_kwh,
            unit="kWh",
            emission_factor=emission_factor_residual
        )

        assert abs(result["co2e_tonnes"] - 19.75) < 0.01

class TestScope3Calculations:

    def test_business_travel_air_short_haul_economy(self):
        """Test emissioni volo breve raggio con RFI"""
        from integrations.travel_connector import TravelDataProcessor, TravelRecord
        from datetime import date

        processor = TravelDataProcessor()
        record = TravelRecord(
            employee_id="EMP001",
            travel_date=date(2024, 3, 15),
            origin_iata="LIN",
            destination_iata="FCO",
            transport_mode="air",
            distance_km=490,
            travel_class="economy",
            booking_amount_eur=180
        )

        # 490 km x 0.151 kgCO2e/pkm x 1.9 RFI = 140.531 kgCO2e = 0.14053 tCO2e
        co2e = processor.calculate_flight_emissions(record)
        expected = 490 * 0.151 * 1.9 / 1000
        assert abs(co2e - expected) < 0.001

    def test_spend_based_with_nace_intensity(self, calculator):
        """Test calcolo spend-based per Category 1"""
        result = calculator.calculate_spend_based(
            spend_eur=100_000,
            emission_intensity=0.35  # kgCO2e/EUR - manifattura metalli
        )

        # 100.000 EUR x 0.35 kgCO2e/EUR = 35.000 kgCO2e = 35.0 tCO2e
        assert abs(result["co2e_tonnes"] - 35.0) < 0.01

Science Based Targets (SBTi) e Dashboard

Il Science Based Targets initiative (SBTi) ha definito criteri precisi per target di riduzione delle emissioni allineati con l'Accordo di Parigi. La corporate standard SBTi richiede: riduzione Scope 1+2 del 42% entro il 2030 (baseline 2020) per uno scenario 1.5°C, e copertura Scope 3 se rappresentano più del 40% delle emissioni totali (quasi sempre vera nel manifatturiero).

Il dashboard della piattaforma deve mostrare non solo le emissioni correnti, ma il percorso verso il target: la curva di riduzione richiesta da SBTi, le emissioni effettive anno per anno, e la proiezione basata sulle iniziative di riduzione pianificate (PPA per energia rinnovabile, elettrificazione fleet, efficienza processi).

# dashboard/sbti_tracker.py - Tracking verso target SBTi
from dataclasses import dataclass
from typing import List, Dict
import pandas as pd
import numpy as np

@dataclass
class SBTiTarget:
    organization_id: str
    base_year: int
    base_year_scope_1_2_tco2e: float
    base_year_scope_3_tco2e: float
    target_year: int = 2030
    scope_12_reduction_pct: float = 42.0   # % riduzione vs base year
    scope_3_reduction_pct: float = 25.0    # % riduzione vs base year
    scenario: str = "1.5C"

    @property
    def scope_12_target_tco2e(self) -> float:
        return self.base_year_scope_1_2_tco2e * (1 - self.scope_12_reduction_pct / 100)

    @property
    def annual_reduction_rate(self) -> float:
        """Tasso di riduzione annuale lineare necessario"""
        years = self.target_year - self.base_year
        return self.scope_12_reduction_pct / years

def build_sbti_trajectory(target: SBTiTarget, actuals: Dict[int, float]) -> pd.DataFrame:
    """
    Costruisce tabella confronto tra traiettoria SBTi e emissioni effettive.
    actuals: { anno: tCO2e Scope1+2 effettivo }
    """
    years = range(target.base_year, target.target_year + 1)
    trajectory = []

    for year in years:
        years_elapsed = year - target.base_year
        total_years = target.target_year - target.base_year

        # Riduzione lineare richiesta da SBTi
        required_reduction_pct = (years_elapsed / total_years) * target.scope_12_reduction_pct
        sbti_budget = target.base_year_scope_1_2_tco2e * (1 - required_reduction_pct / 100)

        actual = actuals.get(year)
        on_track = actual <= sbti_budget if actual is not None else None

        trajectory.append({
            "year": year,
            "sbti_budget_tco2e": round(sbti_budget, 1),
            "actual_tco2e": actual,
            "gap_tco2e": round(actual - sbti_budget, 1) if actual else None,
            "on_track": on_track
        })

    df = pd.DataFrame(trajectory)
    return df

# Utilizzo per MeccanicaPrecisione SpA
target = SBTiTarget(
    organization_id="meccanica-precisione-spa",
    base_year=2020,
    base_year_scope_1_2_tco2e=3_850.0,  # baseline 2020
    base_year_scope_3_tco2e=35_000.0,
)

actuals_scope_12 = {
    2020: 3850.0,
    2021: 3720.0,
    2022: 3650.0,
    2023: 3320.0,
    2024: 3060.8,  # scope_1 + scope_2_market dal nostro inventory
}

trajectory = build_sbti_trajectory(target, actuals_scope_12)
print(trajectory.to_string(index=False))

# Output:
#  year  sbti_budget_tco2e  actual_tco2e  gap_tco2e  on_track
#  2020             3850.0        3850.0        0.0      True
#  2021             3608.6        3720.0      111.4     False
#  2022             3367.2        3650.0      282.8     False
#  2023             3125.8        3320.0      194.2     False
#  2024             2884.4        3060.8      176.4     False
# -> L'azienda e fuori traiettoria: serve accelerare le iniziative di riduzione

Best Practices e Anti-Pattern

Best Practices

  • Immutabilita dei calcoli: Una volta calcolata e salvata, una emissione non va mai modificata. Se cambiano i fattori, si crea una nuova versione del calcolo. L'audit trail deve mostrare entrambe le versioni.
  • Rendiconta entrambi i metodi Scope 2: Location-based E market-based sono entrambi obbligatori secondo ESRS e GHG Protocol. Non reportare solo il market-based perchè e più basso.
  • Documenta la materialita Scope 3: Non devi calcolare tutte le 15 categorie Scope 3. Ma devi dimostrare con una materiality analysis perchè hai incluso/escluso ciascuna categoria. Questo e un requisito esplicito ESRS.
  • Versioning dei fattori di emissione: I fattori cambiano ogni anno. Mantieni uno snapshot del fattore usato per ogni calcolo. Non aggiornare retroattivamente senza produrre un ricalcolo esplicito con nuova versione.
  • Data quality flags: Classifica ogni dato per qualità (misurato, calcolato, stimato, spend-based). Le ESRS richiedono di dichiarare la qualità dei dati Scope 3. Dati di qualità inferiore possono essere usati ma devono essere dichiarati.
  • Boundary documentation: Documenta esplicitamente le entità incluse nel confine di rendicontazione e quelle escluse, con la giustificazione (threshold <5%, dati non disponibili, ecc.).

Anti-Pattern da Evitare

  • Usare solo lo Scope 2 market-based: Il GHG Protocol richiede entrambi. Reportare solo il market-based (tipicamente più basso grazie a GO/PPA) senza il location-based e metodologicamente errato e potenziale greenwashing.
  • Emission factors non aggiornati: Usare fattori vecchi di 5+ anni introduce errori significativi, specialmente per la rete elettrica che cambia ogni anno con la crescita delle rinnovabili.
  • Duplicare le emissioni nel consolidamento: Se una controllata calcola le sue emissioni e la capogruppo le include nel consolidato, occorre usare il metodo equity share o operational control in modo consistente.
  • Ignorare l'incertezza: Tutti i calcoli di emissioni hanno un'incertezza, specialmente lo Scope 3. Non presentare numeri senza indicare il range di confidenza e il metodo usato.
  • Report senza assurance: Con la CSRD, i report senza limited assurance non soddisfano i requisiti normativi per le Wave 1. Pianifica l'assurance dal primo anno, non come afterthought.

Conclusioni e Roadmap Implementativa

Progettare una piattaforma di carbon accounting enterprise-grade e un progetto complesso che richiede competenze multidisciplinari. Ma la struttura concettuale e chiara: il GHG Protocol fornisce il framework contabile, i database di emission factors (DEFRA, EPA, Climatiq) forniscono i coefficienti, l'architettura a microservizi garantisce scalabilità e manutenibilità, e l'audit trail immutabile garantisce la credibilita per la CSRD.

Per una PMI che deve avvicinarsi alla CSRD nel 2025-2026, la raccomandazione pratica e di iniziare con una piattaforma SaaS matura (Persefoni, Plan A, o Watershed) per i primi due anni, raccogliere dati reali, capire i propri gap di dati e solo allora valutare se costruire una soluzione custom o restare su SaaS. Il 90% delle organizzazioni non ha bisogno di costruire da zero.

Per i team engineering che invece lavorano su piattaforme ESG come prodotto, i concetti illustrati in questo articolo - data model GHG Protocol, calculation engine auditabile, integrazione con API di emission factors e generazione report regolamentari - sono le fondamenta su cui costruire. Il mercato del carbon accounting software crescera del 27% annuo fino al 2030: c'è spazio per soluzioni specializzate, specialmente per settori industriali con requisiti di process data non coperti dalle piattaforme generaliste.

Roadmap Implementativa Consigliata

FaseDurataObiettivoOutput
Fase 1 1-2 mesi Double Materiality Assessment + boundary Lista categorie Scope 3 materiali, confine reporting
Fase 2 2-3 mesi Data collection Scope 1 e 2 Pipeline automatizzata con bollette, SAP, SCADA
Fase 3 3-4 mesi Scope 3 categorie materiali (spend-based) Calcoli Scope 3 per cat. 1, 4, 6, 7, 11
Fase 4 1-2 mesi Report CSRD/ESRS E1 e audit Report pronto per limited assurance
Fase 5 Continuo Miglioramento qualità dati e supplier engagement Riduzione spend-based, aumento supplier-specific Scope 3

Risorse Essenziali

  • GHG Protocol Corporate Standard: ghgprotocol.org - lo standard di riferimento, gratuito e scaricabile
  • DEFRA Emission Factors 2024: gov.uk/government/publications/ greenhouse-gas-reporting-conversion-factors-2024
  • Climatiq API: climatiq.io/docs - documentazione completa e quickstart gratuito
  • ESRS E1 Standard: efrag.org - European Financial Reporting Advisory Group, standard ESRS definitivi
  • SBTi Corporate Manual: sciencebasedtargets.org - guida alla definizione di target scientificamente fondati
  • ecoinvent Database: ecoinvent.org - database LCA per Scope 3 con dati processo dettagliati

Prossimi Articoli della Serie EnergyTech

Il prossimo articolo esplorera i Digital Twin Energetici: come creare repliche virtuali di impianti industriali per simulare scenari di riduzione delle emissioni prima di implementarli nel mondo fisico. Una tecnologia sempre più centrale nelle strategie di decarbonizzazione delle grandi industrie.

Per approfondire le tecnologie di AI applicate al business dei dati, consulta anche la serie Data Warehouse, AI e Trasformazione Digitale, in particolare gli articoli su Data Governance e Data Quality per AI Affidabile e su MLOps per Business, che trattano temi direttamente applicabili alla gestione dei modelli di calcolo delle emissioni in produzione.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti