Salta al contenuto principale

Scope 1, 2 e 3: Data Modeling per Software ESG Reporting

Il GHG Protocol (Greenhouse Gas Protocol) è diventato il framework universale de facto per misurare e rendicontare le emissioni di gas serra aziendali.

Cosa ne pensi di questo articolo?

Scope 1, 2 e 3: Data Modeling per Software ESG Reporting
Green SoftwareFoto:panumas nikhomkhaiPexels

In Breve

Il GHG Protocol (Greenhouse Gas Protocol) è diventato il framework universale de facto per misurare e rendicontare le emissioni di gas serra aziendali.

Scope 1, 2 e 3: Data Modeling per Software ESG Reporting

Il GHG Protocol (Greenhouse Gas Protocol) è diventato il framework universale de facto per misurare e rendicontare le emissioni di gas serra aziendali. Sviluppato dal World Resources Institute (WRI) e dal World Business Council for Sustainable Development (WBCSD), definisce tre "scope" di emissioni che coprono l'intera catena del valore di un'organizzazione.

Con l'entrata in vigore della CSRD (Corporate Sustainability Reporting Directive) dell'Unione Europea e dello standard ESRS E1, migliaia di aziende - incluse le società software e tech - sono ora obbligate a raccogliere, calcolare e pubblicare i propri dati di emissioni secondo metodologie rigorose. A partire dal 2024 le grandi imprese con oltre 500 dipendenti hanno dovuto avviare la raccolta dati, con il primo obbligo di rendicontazione nel 2025; le aziende con oltre 250 dipendenti seguiranno a partire dal 2026.

Ma perché i developer devono capire Scope 1, 2 e 3? Perché costruire un sistema ESG affidabile non è solo un problema di sustainability manager: è una sfida di data engineering. Richiede un modello dati coerente, pipeline di calcolo accurate, API per la raccolta delle attività, e report audit-ready. Chi progetta e implementa questi sistemi determina la qualità - e quindi la credibilità - dell'intera rendicontazione ESG.

In questo articolo costruiremo da zero un sistema di data modeling per ESG reporting, dalla definizione degli scope alle tabelle del database, dai calcoli Python agli endpoint FastAPI, fino a un caso studio completo per una PMI software con 200 dipendenti.

Cosa Imparerai

  • La struttura del GHG Protocol: Scope 1, 2 e 3 con definizioni precise
  • Modelli entity-relationship per emissioni dirette, indirette e della catena del valore
  • Location-based vs market-based per Scope 2: differenze e impatto sul dato
  • Le 15 categorie Scope 3 e quali sono rilevanti per le aziende software
  • Schema completo SQLAlchemy: Organization, Facility, EmissionSource, EmissionFactor, ActivityData
  • Calcoli Python con activity-based estimation e spend-based estimation
  • Aggregazione, confronto Year-over-Year e allineamento con target SBTi
  • API REST con FastAPI per submit attività, calcolo emissioni e generazione report
  • Unit testing per calcoli GHG con pytest e validazione fattori di emissione
  • Caso studio end-to-end: PMI software 200 dipendenti, Scope 1+2+3 completo

Serie Green Software - 10 Articoli

# Titolo Stato
1 Principi Green Software Engineering e SCI Pubblicato
2 CodeCarbon: Misurare le Emissioni del Codice Python Pubblicato
3 Carbon Aware SDK: Workload Shifting e Time Shifting Pubblicato
4 Climatiq API: Emission Factors e Carbon Calculation Pubblicato
5 Scope 1, 2 e 3: Data Modeling per ESG Reporting Articolo corrente
6 ESG & CSRD: Obblighi Europei per le Aziende Tech Prossimamente
7 Sustainable Software Patterns: Architetture a Basso Impatto Prossimamente
8 GreenOps: Cloud Optimization per la Sostenibilità Prossimamente
9 Scope 3 Pipeline: Automatizzare la Value Chain Prossimamente
10 AI Carbon Footprint: LLM, Training e Inferenza Prossimamente

Il GHG Protocol: Framework Universale per le Emissioni

Il GHG Protocol Corporate Standard, pubblicato nel 2001 e aggiornato nel 2004, è il fondamento di quasi tutti i framework di rendicontazione climatica: CDP, TCFD, CSRD/ESRS E1, SBTi e molti altri vi fanno riferimento diretto. La sua forza sta nella chiarezza concettuale: tutte le emissioni di un'organizzazione vengono classificate in tre categorie mutuamente esclusive ma complessivamente esaustive.

Il framework utilizza il concetto di organizational boundary (confine organizzativo) per determinare quali emissioni includere nel calcolo. Esistono due approcci: l'equity share approach (basato sulla quota di partecipazione finanziaria) e il control approach (basato sul controllo operativo o finanziario). La maggior parte delle aziende adotta il controllo operativo come criterio primario.

I Tre Scope: Panoramica

Scope Definizione Esempi tipici Obbligatorietà ESRS E1
Scope 1 Emissioni dirette da fonti di proprietà o sotto controllo dell'organizzazione Gas naturale caldaie, flotta aziendale, generatori diesel, processi industriali Obbligatorio
Scope 2 Emissioni indirette dall'acquisto di energia (elettricità, vapore, caldo, freddo) Elettricità uffici, raffreddamento data center, teleriscaldamento Obbligatorio (entrambi i metodi)
Scope 3 Tutte le altre emissioni indirette nella catena del valore (upstream e downstream) Viaggi business, pendolarismo, acquisto beni/servizi, uso prodotti venduti Obbligatorio (categorie materiali)

Scope 1: Modello Dati per le Emissioni Dirette

Le emissioni Scope 1 derivano da sorgenti fisiche direttamente controllate dall'azienda. Per le aziende software, queste sono tipicamente ridotte ma non trascurabili: comprendono il riscaldamento degli uffici (gas naturale), eventuali generatori di backup, e la flotta aziendale (auto aziendali o furgoni per consegne hardware).

Il GHG Protocol identifica quattro categorie di emissioni Scope 1:

  • Combustione stazionaria: Caldaie, riscaldatori, generatori che bruciano combustibili fossili
  • Combustione mobile: Veicoli aziendali alimentati a benzina, diesel o LPG
  • Emissioni di processo: Reazioni chimiche o biologiche (raramente rilevanti per il software)
  • Emissioni fuggitive: Perdite di refrigeranti (R-410A, R-134a) da impianti HVAC

La formula base per il calcolo Scope 1 è:

Emissioni (tCO₂e) = Dato attività × Fattore di emissione × GWP

Dove:

  • Dato attività: quantità di combustibile consumato (litri, m³, kWh)
  • Fattore di emissione: kg CO₂ per unità di combustibile (fonte: IPCC, DEFRA, IEA)
  • GWP: Global Warming Potential (CO₂=1, CH₄=28, N₂O=265, HFC varia)

L'entity-relationship diagram per Scope 1 prevede le seguenti entità principali:

-- Entity-Relationship: Scope 1 Emissioni Dirette

ORGANIZATION (1) ---< FACILITY (>1)
   |id, name, tax_id         |id, org_id, name, type, country, area_m2

FACILITY (1) ---< EMISSION_SOURCE (>1)
                       |id, facility_id, source_type, description
                       |  source_type: STATIONARY | MOBILE | FUGITIVE | PROCESS

EMISSION_SOURCE (1) ---< ACTIVITY_DATA (>1)
                              |id, source_id, period_start, period_end
                              |quantity, unit, data_quality, evidence_url

ACTIVITY_DATA (N) ---> EMISSION_FACTOR (1)
                              |id, gas, fuel_type, unit_from, unit_to
                              |factor_value, gwp_ar5, source, year, region

ACTIVITY_DATA (1) ---< GHG_CALCULATION (1)
                              |id, activity_id, scope, co2_kg, ch4_kg
                              |n2o_kg, hfc_kg, co2e_total_kg, method
                              |calculated_at, calculated_by

Scope 2: Location-Based vs Market-Based

Lo Scope 2 è particolarmente complesso per le aziende tech perché l'energia elettrica consumata dagli uffici e dai data center è spesso la principale fonte di emissioni indirette. Il GHG Protocol Scope 2 Guidance (2015) ha introdotto l'obbligo di rendicontare con entrambi i metodi: location-based e market-based.

Location-Based vs Market-Based: Differenze Chiave

Aspetto Location-Based Market-Based
Definizione Intensità media della rete elettrica locale dove avviene il consumo Emissioni dei generatori da cui si acquista contrattualmente energia
Fattore emissione Grid average emission factor (gCO₂/kWh) per paese/regione Supplier-specific factor oppure residual mix factor
Strumenti contrattuali Non applicabili GO (Garanzie d'Origine), REC (Renewable Energy Certificates), PPA
Esempio Italia 2024 ~310 gCO₂/kWh (dato AIB residual mix) 0 gCO₂/kWh se si acquistano GO 100% rinnovabili verificate
Proposta GHG Protocol 2025 Nuova gerarchia fattori con precisione oraria Richiesta hourly matching per rinnovabili
Uso in ESRS E1 Disclosure obbligatoria (E1-6) Disclosure obbligatoria (E1-6)

Per modellare correttamente lo Scope 2 nel database, è necessario gestire i certificati energetici (GO/REC) come entità separate che si "consumano" contro il consumo elettrico dell'azienda, abbassando il market-based factor verso zero:

-- Modello Scope 2: Energia e Certificati

ELECTRICITY_CONSUMPTION
  id              UUID PRIMARY KEY
  facility_id     UUID REFERENCES FACILITY
  period_start    DATE
  period_end      DATE
  kwh_consumed    DECIMAL(15,3)
  meter_id        VARCHAR(50)
  -- Location-based
  grid_region     VARCHAR(20)  -- e.g. 'IT', 'DE-WEST', 'FR'
  grid_ef_g_kwh   DECIMAL(8,3) -- gCO2e/kWh from IEA/AIB
  lb_co2e_kg      DECIMAL(15,3) GENERATED ALWAYS AS
                  (kwh_consumed * grid_ef_g_kwh / 1000)
  -- Market-based
  supplier_ef_g_kwh DECIMAL(8,3) DEFAULT NULL
  residual_mix_ef   DECIMAL(8,3) DEFAULT NULL
  mb_co2e_kg      DECIMAL(15,3)  -- calcolato dopo allocazione GO/REC

RENEWABLE_CERTIFICATE
  id              UUID PRIMARY KEY
  cert_type       VARCHAR(10)  -- 'GO' | 'REC' | 'I-REC'
  cert_id         VARCHAR(100) UNIQUE
  technology      VARCHAR(30)  -- 'WIND' | 'SOLAR' | 'HYDRO'
  country         VARCHAR(2)
  production_date DATE
  kwh_value       DECIMAL(15,3)
  issuer          VARCHAR(100)
  status          VARCHAR(20)  -- 'ACTIVE' | 'CANCELLED' | 'EXPIRED'

CERTIFICATE_ALLOCATION
  id              UUID PRIMARY KEY
  cert_id         UUID REFERENCES RENEWABLE_CERTIFICATE
  consumption_id  UUID REFERENCES ELECTRICITY_CONSUMPTION
  kwh_allocated   DECIMAL(15,3)
  allocation_date DATE
  -- Vincolo: kwh_allocated <= RENEWABLE_CERTIFICATE.kwh_value

Scope 3: Le 15 Categorie della Value Chain

Lo Scope 3 è tipicamente la fonte principale di emissioni per le aziende software: può rappresentare oltre il 70-80% del totale. Il GHG Protocol Technical Guidance for Calculating Scope 3 Emissions definisce 15 categorie, suddivise in upstream (legate all'acquisto di beni e servizi) e downstream (legate all'uso dei prodotti venduti).

Le 15 Categorie Scope 3 per Aziende Software

# Categoria Tipo Rilevanza Software Metodo Calcolo
1 Purchased Goods & Services Upstream Alta (SaaS, cloud, hardware) Spend-based o supplier-specific
2 Capital Goods Upstream Media (laptop, server, arredi) Spend-based o physical unit
3 Fuel & Energy Related Activities Upstream Media (T&D losses, upstream fuel) Activity-based
4 Upstream Transportation & Distribution Upstream Bassa (hardware delivery) Spend-based o distance-based
5 Waste Generated in Operations Upstream Bassa Waste-type specific
6 Business Travel Upstream Alta (voli, hotel, treni) Distance-based o spend-based
7 Employee Commuting Upstream Alta (pendolarismo, remote work) Survey + distance-based
8 Upstream Leased Assets Upstream Media (uffici in affitto) Asset-specific
9 Downstream Transportation Downstream Bassa Distance-based
10 Processing of Sold Products Downstream Non applicabile (software) Activity-based
11 Use of Sold Products Downstream Alta (energia per eseguire il software) Lifetime energy use
12 End-of-Life Treatment Downstream Bassa (dispositivi utente) Waste-type specific
13 Downstream Leased Assets Downstream Bassa Asset-specific
14 Franchises Downstream Non applicabile N/A
15 Investments Downstream Media (portafoglio VC/startup) Portfolio-based

Per le aziende software, le categorie più rilevanti sono tipicamente: Cat. 1 (cloud computing acquistato), Cat. 6 (business travel), Cat. 7 (employee commuting) e Cat. 11 (energia consumata dagli utenti per eseguire il software). Quest'ultima è spesso sottovalutata ma per SaaS enterprise con milioni di utenti può essere enorme.

Schema SQLAlchemy: Modello Completo

Costruiamo ora uno schema database completo in SQLAlchemy 2.0 che supporta tutti e tre gli scope. Il design segue i principi di alta coesione e basso accoppiamento, con tabelle separate per ogni concetto dominio e relazioni chiare.

# models/base.py
from sqlalchemy import Column, String, DateTime, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import DeclarativeBase
import uuid

class Base(DeclarativeBase):
    pass

class TimestampMixin:
    """Mixin per created_at e updated_at automatici."""
    created_at = Column(DateTime(timezone=True), server_default=func.now())
    updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# models/organization.py
from sqlalchemy import Column, String, Integer, Boolean, ForeignKey
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
import uuid
from .base import Base, TimestampMixin

class Organization(Base, TimestampMixin):
    """Azienda che produce il report ESG."""
    __tablename__ = "organizations"

    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    name = Column(String(200), nullable=False)
    legal_name = Column(String(200))
    tax_id = Column(String(50))          # Partita IVA o VAT number
    country = Column(String(2), nullable=False)  # ISO 3166-1 alpha-2
    industry_code = Column(String(10))   # NACE Rev.2
    employee_count = Column(Integer)
    fiscal_year_end = Column(String(5), default="12-31")  # MM-DD
    reporting_currency = Column(String(3), default="EUR")
    boundary_approach = Column(String(20), default="OPERATIONAL_CONTROL")
    # OPERATIONAL_CONTROL | FINANCIAL_CONTROL | EQUITY_SHARE
    active = Column(Boolean, default=True)

    facilities = relationship("Facility", back_populates="organization")
    reporting_periods = relationship("ReportingPeriod", back_populates="organization")


class Facility(Base, TimestampMixin):
    """Sede fisica (ufficio, data center, magazzino)."""
    __tablename__ = "facilities"

    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False)
    name = Column(String(200), nullable=False)
    facility_type = Column(String(30))
    # OFFICE | DATA_CENTER | WAREHOUSE | LAB | REMOTE

    address = Column(String(500))
    city = Column(String(100))
    country = Column(String(2), nullable=False)
    grid_region = Column(String(30))   # Per fattore emissione location-based
    area_sqm = Column(Integer)         # Per calcoli per m2
    employee_fte = Column(Integer)     # Full-time equivalent

    organization = relationship("Organization", back_populates="facilities")
    emission_sources = relationship("EmissionSource", back_populates="facility")
# models/emission_source.py
from sqlalchemy import Column, String, Enum, ForeignKey
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
import enum, uuid
from .base import Base, TimestampMixin

class SourceType(str, enum.Enum):
    # Scope 1
    STATIONARY_COMBUSTION = "STATIONARY_COMBUSTION"
    MOBILE_COMBUSTION = "MOBILE_COMBUSTION"
    FUGITIVE = "FUGITIVE"
    PROCESS = "PROCESS"
    # Scope 2
    ELECTRICITY = "ELECTRICITY"
    STEAM = "STEAM"
    HEATING = "HEATING"
    COOLING = "COOLING"
    # Scope 3 (le 15 categorie)
    S3_PURCHASED_GOODS = "S3_PURCHASED_GOODS"       # Cat. 1
    S3_CAPITAL_GOODS = "S3_CAPITAL_GOODS"           # Cat. 2
    S3_FUEL_ENERGY = "S3_FUEL_ENERGY"               # Cat. 3
    S3_UPSTREAM_TRANSPORT = "S3_UPSTREAM_TRANSPORT" # Cat. 4
    S3_WASTE = "S3_WASTE"                           # Cat. 5
    S3_BUSINESS_TRAVEL = "S3_BUSINESS_TRAVEL"       # Cat. 6
    S3_EMPLOYEE_COMMUTE = "S3_EMPLOYEE_COMMUTE"     # Cat. 7
    S3_UPSTREAM_LEASED = "S3_UPSTREAM_LEASED"       # Cat. 8
    S3_DOWNSTREAM_TRANSPORT = "S3_DOWNSTREAM_TRANSPORT" # Cat. 9
    S3_USE_OF_PRODUCTS = "S3_USE_OF_PRODUCTS"       # Cat. 11
    S3_END_OF_LIFE = "S3_END_OF_LIFE"               # Cat. 12
    S3_INVESTMENTS = "S3_INVESTMENTS"               # Cat. 15

class EmissionSource(Base, TimestampMixin):
    """Sorgente specifica di emissione (es. 'Caldaia edificio A')."""
    __tablename__ = "emission_sources"

    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    facility_id = Column(UUID(as_uuid=True), ForeignKey("facilities.id"), nullable=True)
    organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False)
    source_type = Column(String(40), nullable=False)  # SourceType enum value
    scope = Column(Integer, nullable=False)            # 1, 2, o 3
    scope3_category = Column(Integer, nullable=True)   # 1-15 se scope=3
    description = Column(String(500))
    unit_of_measure = Column(String(20))  # 'kWh', 'litri', 'km', 'EUR', 'kg'
    active = Column(Boolean, default=True)

    facility = relationship("Facility", back_populates="emission_sources")
    activity_records = relationship("ActivityRecord", back_populates="source")
# models/emission_factor.py
from sqlalchemy import Column, String, Numeric, Integer, ForeignKey
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
import uuid
from .base import Base, TimestampMixin

class EmissionFactor(Base, TimestampMixin):
    """
    Fattore di emissione da fonti ufficiali.
    Converte un'attività (es. kWh) in kg CO2e.
    """
    __tablename__ = "emission_factors"

    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    name = Column(String(200), nullable=False)
    category = Column(String(50))        # 'electricity' | 'fuel' | 'transport' | ...
    fuel_type = Column(String(50))       # 'natural_gas' | 'diesel' | 'petrol' | ...
    technology = Column(String(50))      # 'gas_boiler' | 'bev' | ...

    # Unità
    activity_unit = Column(String(20), nullable=False)   # 'kWh', 'litre', 'km', ...
    emission_unit = Column(String(20), default="kg_co2e") # Output sempre in kg CO2e

    # Valori per singolo gas (kg/unità)
    co2_factor = Column(Numeric(18, 8))
    ch4_factor = Column(Numeric(18, 8))
    n2o_factor = Column(Numeric(18, 8))
    hfc_factor = Column(Numeric(18, 8))

    # GWP (AR5 100-year, IPCC 2014)
    gwp_co2 = Column(Numeric(6, 2), default=1.0)
    gwp_ch4 = Column(Numeric(6, 2), default=28.0)
    gwp_n2o = Column(Numeric(6, 2), default=265.0)

    # Metadati fonte
    source = Column(String(100))    # 'DEFRA_2024' | 'IEA_2024' | 'IPCC_AR6'
    year = Column(Integer)
    region = Column(String(10))     # 'IT' | 'EU' | 'UK' | 'GLOBAL'
    version = Column(String(20))

    activity_records = relationship("ActivityRecord", back_populates="emission_factor")
# models/activity_record.py
from sqlalchemy import Column, String, Numeric, Date, ForeignKey, Text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
import uuid
from .base import Base, TimestampMixin

class ActivityRecord(Base, TimestampMixin):
    """
    Dato di attività misurato o stimato per una fonte di emissione.
    Es: 'Ufficio Milano ha consumato 15.432 kWh a novembre 2024'.
    """
    __tablename__ = "activity_records"

    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    source_id = Column(UUID(as_uuid=True), ForeignKey("emission_sources.id"), nullable=False)
    factor_id = Column(UUID(as_uuid=True), ForeignKey("emission_factors.id"), nullable=False)
    reporting_period_id = Column(UUID(as_uuid=True), ForeignKey("reporting_periods.id"))

    # Periodo
    period_start = Column(Date, nullable=False)
    period_end = Column(Date, nullable=False)

    # Quantità
    quantity = Column(Numeric(18, 4), nullable=False)
    unit = Column(String(20), nullable=False)

    # Qualità del dato
    data_quality = Column(String(20), default="MEASURED")
    # MEASURED | ESTIMATED | CALCULATED | MODELED
    uncertainty_pct = Column(Numeric(5, 2))  # Es: 10.00 = +-10%
    evidence_url = Column(Text)  # Link a fattura, report, foglio elettrico

    # Note
    notes = Column(Text)
    reviewer = Column(String(100))

    source = relationship("EmissionSource", back_populates="activity_records")
    emission_factor = relationship("EmissionFactor", back_populates="activity_records")
    calculation = relationship("GHGCalculation", back_populates="activity_record", uselist=False)


class GHGCalculation(Base, TimestampMixin):
    """
    Risultato del calcolo GHG per un ActivityRecord.
    Separato per permettere ricalcoli senza perdere il dato sorgente.
    """
    __tablename__ = "ghg_calculations"

    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    activity_record_id = Column(UUID(as_uuid=True),
        ForeignKey("activity_records.id"), nullable=False, unique=True)

    # Emissioni per gas (kg)
    co2_kg = Column(Numeric(18, 4), default=0)
    ch4_kg = Column(Numeric(18, 4), default=0)
    n2o_kg = Column(Numeric(18, 4), default=0)
    hfc_kg = Column(Numeric(18, 4), default=0)

    # Totale in CO2e (kg e tonnellate)
    co2e_kg = Column(Numeric(18, 4), nullable=False)
    co2e_tonnes = Column(Numeric(18, 6), nullable=False)

    # Metodo di calcolo
    calculation_method = Column(String(50))
    # ACTIVITY_BASED | SPEND_BASED | AVERAGE_DATA | HYBRID

    # Scope 2 specifico
    is_location_based = Column(Boolean)
    is_market_based = Column(Boolean)

    calculated_at = Column(DateTime(timezone=True), server_default=func.now())
    calculator_version = Column(String(20), default="1.0.0")

    activity_record = relationship("ActivityRecord", back_populates="calculation")

Calcoli GHG in Python: Activity-Based e Spend-Based

Con il modello dati definito, possiamo implementare i calcoli. Il GHG Protocol riconosce quattro metodi di calcolo in ordine decrescente di accuratezza:

  • Supplier-specific method: Dati diretti dal fornitore (più accurato)
  • Hybrid method: Combinazione di dati primari e secondari
  • Average-data method: Fattori di emissione medi per unità fisica
  • Spend-based method: Fattori per unità di spesa in EUR/USD (meno accurato)
# calculators/ghg_calculator.py
from decimal import Decimal
from dataclasses import dataclass
from typing import Optional
from models import ActivityRecord, EmissionFactor, GHGCalculation

@dataclass
class CalculationResult:
    co2_kg: Decimal
    ch4_kg: Decimal
    n2o_kg: Decimal
    hfc_kg: Decimal
    co2e_kg: Decimal
    co2e_tonnes: Decimal
    method: str


def activity_based_calculation(
    quantity: Decimal,
    factor: EmissionFactor
) -> CalculationResult:
    """
    Calcolo activity-based: quantità * fattore emissione.
    Formula GHG Protocol: E = AD * EF * GWP
    """
    co2_kg = Decimal(str(quantity)) * Decimal(str(factor.co2_factor or 0))
    ch4_kg = Decimal(str(quantity)) * Decimal(str(factor.ch4_factor or 0))
    n2o_kg = Decimal(str(quantity)) * Decimal(str(factor.n2o_factor or 0))
    hfc_kg = Decimal(str(quantity)) * Decimal(str(factor.hfc_factor or 0))

    # Converti in CO2e usando GWP AR5
    co2e_kg = (
        co2_kg * Decimal(str(factor.gwp_co2))
        + ch4_kg * Decimal(str(factor.gwp_ch4))
        + n2o_kg * Decimal(str(factor.gwp_n2o))
        + hfc_kg * Decimal("1300")  # GWP medio HFC-134a
    )

    return CalculationResult(
        co2_kg=co2_kg,
        ch4_kg=ch4_kg,
        n2o_kg=n2o_kg,
        hfc_kg=hfc_kg,
        co2e_kg=co2e_kg,
        co2e_tonnes=co2e_kg / Decimal("1000"),
        method="ACTIVITY_BASED"
    )


def spend_based_calculation(
    spend_eur: Decimal,
    spend_factor_kg_per_eur: Decimal
) -> CalculationResult:
    """
    Calcolo spend-based per Cat. 1 Scope 3 quando non si hanno
    dati fisici. Usa EXIOBASE o fattori DEFRA per settore NACE.
    Meno accurato ma utile come stima iniziale.
    """
    co2e_kg = spend_eur * spend_factor_kg_per_eur

    return CalculationResult(
        co2_kg=co2e_kg,  # Semplificazione: tutto attribuito a CO2
        ch4_kg=Decimal("0"),
        n2o_kg=Decimal("0"),
        hfc_kg=Decimal("0"),
        co2e_kg=co2e_kg,
        co2e_tonnes=co2e_kg / Decimal("1000"),
        method="SPEND_BASED"
    )


def business_travel_calculation(
    distance_km: Decimal,
    transport_mode: str,
    cabin_class: Optional[str] = None
) -> CalculationResult:
    """
    Calcolo Cat. 6 (Business Travel) con fattori DEFRA 2024.
    Fattori in kgCO2e per km per passeggero.
    """
    # Fattori DEFRA 2024 (kgCO2e/km/passeggero)
    FACTORS = {
        "SHORT_HAUL_ECONOMY": Decimal("0.15342"),  # Volo <1000km economy
        "SHORT_HAUL_BUSINESS": Decimal("0.23013"),
        "LONG_HAUL_ECONOMY": Decimal("0.19085"),   # Volo >1000km economy
        "LONG_HAUL_BUSINESS": Decimal("0.57256"),  # Business = 3x economy
        "LONG_HAUL_FIRST": Decimal("0.76340"),
        "RAIL_NATIONAL": Decimal("0.03549"),       # Treno nazionale UK
        "RAIL_EUROSTAR": Decimal("0.00416"),       # Eurostar molto efficiente
        "CAR_AVERAGE": Decimal("0.17100"),         # Auto media
        "CAR_ELECTRIC": Decimal("0.05280"),
        "HOTEL_NIGHT": Decimal("20.600"),          # Per notte, non per km
    }

    key = transport_mode
    if cabin_class and f"{transport_mode}_{cabin_class}" in FACTORS:
        key = f"{transport_mode}_{cabin_class}"

    factor = FACTORS.get(key, Decimal("0"))
    co2e_kg = distance_km * factor

    return CalculationResult(
        co2_kg=co2e_kg,
        ch4_kg=Decimal("0"),
        n2o_kg=Decimal("0"),
        hfc_kg=Decimal("0"),
        co2e_kg=co2e_kg,
        co2e_tonnes=co2e_kg / Decimal("1000"),
        method="ACTIVITY_BASED"
    )
# calculators/scope2_calculator.py
from decimal import Decimal
from dataclasses import dataclass
from typing import Optional

@dataclass
class Scope2Result:
    location_based_co2e_kg: Decimal
    market_based_co2e_kg: Decimal
    renewable_kwh_matched: Decimal
    grid_ef_g_kwh: Decimal
    market_ef_g_kwh: Decimal


def scope2_dual_calculation(
    kwh_consumed: Decimal,
    grid_region: str,
    go_certificates_kwh: Decimal = Decimal("0"),
    supplier_ef_g_kwh: Optional[Decimal] = None
) -> Scope2Result:
    """
    Calcolo Scope 2 con doppio metodo obbligatorio (GHG Protocol S2 Guidance).
    location_based: usa grid average emission factor (IEA/AIB)
    market_based: usa supplier factor o residual mix, dedotti GO/REC
    """
    # Grid emission factors per regione (gCO2e/kWh) - IEA 2024
    GRID_FACTORS = {
        "IT": Decimal("310.0"),   # Italia: mix gas + rinnovabili
        "DE": Decimal("364.0"),   # Germania: ancora carbone
        "FR": Decimal("52.0"),    # Francia: nucleare dominante
        "ES": Decimal("198.0"),
        "NL": Decimal("289.0"),
        "EU_AVERAGE": Decimal("231.0"),
        "UK": Decimal("209.0"),
        "US_AVERAGE": Decimal("386.0"),
    }

    grid_ef = GRID_FACTORS.get(grid_region, GRID_FACTORS["EU_AVERAGE"])

    # Location-based (sempre con grid average)
    lb_co2e_kg = kwh_consumed * grid_ef / Decimal("1000")

    # Market-based:
    # 1. Kwh coperti da GO/REC = 0 emissioni
    # 2. Kwh residui: usa supplier factor o residual mix
    covered_by_go = min(go_certificates_kwh, kwh_consumed)
    residual_kwh = kwh_consumed - covered_by_go

    # Residual mix factor (tipicamente > grid average perché
    # le energie rinnovabili sono "estratte" dal grid average)
    # Fonte: AIB European Residual Mixes 2023
    RESIDUAL_MIX = {
        "IT": Decimal("414.0"),   # Residual mix IT più alto del grid
        "DE": Decimal("542.0"),
        "FR": Decimal("73.0"),
        "EU_AVERAGE": Decimal("395.0"),
    }

    market_ef = supplier_ef_g_kwh or RESIDUAL_MIX.get(grid_region, Decimal("400"))
    mb_co2e_kg = residual_kwh * market_ef / Decimal("1000")

    return Scope2Result(
        location_based_co2e_kg=lb_co2e_kg,
        market_based_co2e_kg=mb_co2e_kg,
        renewable_kwh_matched=covered_by_go,
        grid_ef_g_kwh=grid_ef,
        market_ef_g_kwh=market_ef
    )
# calculators/scope3_commute.py
from decimal import Decimal
from dataclasses import dataclass
from typing import Dict

@dataclass
class CommuteData:
    """Dati raccolti tramite survey annuale dipendenti."""
    employee_count: int
    avg_working_days: int          # Es: 220
    remote_work_days_pct: Decimal  # Es: 0.40 = 40% remote
    transport_split: Dict[str, Decimal]
    # {'car_solo': 0.45, 'car_pool': 0.10, 'public_transit': 0.30,
    #  'bicycle': 0.10, 'walk': 0.05}
    avg_commute_km_oneway: Decimal  # km sola andata


def employee_commute_calculation(data: CommuteData) -> Decimal:
    """
    Cat. 7 Scope 3: Pendolarismo dipendenti.
    Formula: n_dipendenti * giorni_ufficio * km_totali * EF * split_modale
    """
    # Fattori emissione (kgCO2e/km/passeggero) - DEFRA 2024
    MODE_FACTORS = {
        "car_solo": Decimal("0.171"),      # Auto singola
        "car_pool": Decimal("0.0855"),     # Carpooling 2 persone
        "public_transit": Decimal("0.089"), # Mix metropolitana/bus
        "bicycle": Decimal("0.0"),
        "walk": Decimal("0.0"),
        "electric_car": Decimal("0.053"),
        "motorcycle": Decimal("0.114"),
    }

    # Giorni effettivi in ufficio
    office_days_pct = Decimal("1") - data.remote_work_days_pct
    annual_office_days = Decimal(str(data.avg_working_days)) * office_days_pct
    km_per_day = data.avg_commute_km_oneway * Decimal("2")  # A/R

    total_co2e_kg = Decimal("0")

    for mode, share in data.transport_split.items():
        ef = MODE_FACTORS.get(mode, Decimal("0.1"))
        mode_co2e = (
            Decimal(str(data.employee_count))
            * annual_office_days
            * km_per_day
            * share
            * ef
        )
        total_co2e_kg += mode_co2e

    return total_co2e_kg

Aggregazione Annuale e Confronto Year-over-Year

Una volta calcolate le emissioni per ogni ActivityRecord, è necessario aggregarle per periodo di rendicontazione, confrontarle con l'anno precedente e verificare l'allineamento con i target SBTi.

# services/reporting_service.py
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from decimal import Decimal
from typing import Dict
from models import GHGCalculation, ActivityRecord, EmissionSource, ReportingPeriod

@dataclass
class AnnualReport:
    organization_id: str
    year: int
    scope1_co2e_tonnes: Decimal
    scope2_lb_co2e_tonnes: Decimal   # Location-based
    scope2_mb_co2e_tonnes: Decimal   # Market-based
    scope3_co2e_tonnes: Decimal
    scope3_by_category: Dict[int, Decimal]
    total_co2e_tonnes: Decimal       # S1 + S2(MB) + S3
    intensity_per_employee: Decimal  # tCO2e / FTE
    intensity_per_revenue: Decimal   # tCO2e / M EUR


def generate_annual_report(
    session: Session,
    organization_id: str,
    year: int,
    employee_fte: int,
    revenue_million_eur: Decimal
) -> AnnualReport:
    """Aggrega tutti i calcoli GHG per anno fiscale."""

    # Query aggregata per scope
    stmt = (
        select(
            EmissionSource.scope,
            EmissionSource.scope3_category,
            GHGCalculation.is_location_based,
            func.sum(GHGCalculation.co2e_tonnes).label("total_tonnes")
        )
        .join(ActivityRecord, GHGCalculation.activity_record_id == ActivityRecord.id)
        .join(EmissionSource, ActivityRecord.source_id == EmissionSource.id)
        .where(
            EmissionSource.organization_id == organization_id,
            func.extract("year", ActivityRecord.period_start) == year
        )
        .group_by(
            EmissionSource.scope,
            EmissionSource.scope3_category,
            GHGCalculation.is_location_based
        )
    )

    rows = session.execute(stmt).all()

    scope1 = Decimal("0")
    scope2_lb = Decimal("0")
    scope2_mb = Decimal("0")
    scope3_by_cat: Dict[int, Decimal] = {}

    for row in rows:
        tonnes = Decimal(str(row.total_tonnes or 0))
        if row.scope == 1:
            scope1 += tonnes
        elif row.scope == 2:
            if row.is_location_based:
                scope2_lb += tonnes
            else:
                scope2_mb += tonnes
        elif row.scope == 3:
            cat = row.scope3_category or 0
            scope3_by_cat[cat] = scope3_by_cat.get(cat, Decimal("0")) + tonnes

    scope3_total = sum(scope3_by_cat.values())
    total = scope1 + scope2_mb + scope3_total

    return AnnualReport(
        organization_id=organization_id,
        year=year,
        scope1_co2e_tonnes=scope1,
        scope2_lb_co2e_tonnes=scope2_lb,
        scope2_mb_co2e_tonnes=scope2_mb,
        scope3_co2e_tonnes=scope3_total,
        scope3_by_category=scope3_by_cat,
        total_co2e_tonnes=total,
        intensity_per_employee=total / Decimal(str(employee_fte)) if employee_fte else Decimal("0"),
        intensity_per_revenue=total / revenue_million_eur if revenue_million_eur else Decimal("0")
    )


def check_sbti_alignment(
    report: AnnualReport,
    baseline_year: int,
    baseline_emissions: Decimal,
    target_year: int = 2030,
    reduction_target_pct: Decimal = Decimal("0.50")  # SBTi: -50% by 2030
) -> Dict:
    """
    Verifica allineamento con target SBTi.
    Near-term: -50% entro 2030 (1.5°C pathway).
    Net-zero: -90-95% entro 2050.
    """
    current_year_progress = report.year - baseline_year
    total_years = target_year - baseline_year
    linear_target = baseline_emissions * (
        1 - reduction_target_pct * Decimal(str(current_year_progress)) / Decimal(str(total_years))
    )

    actual_reduction_pct = (baseline_emissions - report.total_co2e_tonnes) / baseline_emissions * 100

    return {
        "baseline_emissions_tonnes": float(baseline_emissions),
        "current_emissions_tonnes": float(report.total_co2e_tonnes),
        "linear_target_tonnes": float(linear_target),
        "actual_reduction_pct": float(actual_reduction_pct),
        "on_track": report.total_co2e_tonnes <= linear_target,
        "gap_tonnes": float(report.total_co2e_tonnes - linear_target),
        "required_annual_reduction_pct": float(
            reduction_target_pct / Decimal(str(total_years)) * 100
        )
    }

API REST per ESG: FastAPI Endpoints

Un sistema ESG enterprise ha bisogno di API robuste per permettere ai vari team di inviare dati di attività, triggherare calcoli e accedere ai report. FastAPI è la scelta ideale per la sua type safety, la documentazione automatica e le performance.

# api/routers/activities.py
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from pydantic import BaseModel, validator
from typing import Optional
from datetime import date
from decimal import Decimal
import uuid

router = APIRouter(prefix="/api/v1", tags=["activities"])

class ActivitySubmitRequest(BaseModel):
    source_id: uuid.UUID
    period_start: date
    period_end: date
    quantity: Decimal
    unit: str
    data_quality: str = "MEASURED"
    evidence_url: Optional[str] = None
    notes: Optional[str] = None

    @validator("data_quality")
    def validate_quality(cls, v: str) -> str:
        allowed = ["MEASURED", "ESTIMATED", "CALCULATED", "MODELED"]
        if v not in allowed:
            raise ValueError(f"data_quality deve essere uno di: {allowed}")
        return v

    @validator("period_end")
    def validate_dates(cls, v: date, values: dict) -> date:
        if "period_start" in values and v < values["period_start"]:
            raise ValueError("period_end deve essere >= period_start")
        return v

class ActivitySubmitResponse(BaseModel):
    activity_id: uuid.UUID
    co2e_tonnes: Decimal
    calculation_method: str
    message: str


@router.post("/activities", response_model=ActivitySubmitResponse, status_code=201)
async def submit_activity(
    req: ActivitySubmitRequest,
    session: Session = Depends(get_db),
    current_org: Organization = Depends(get_current_org)
) -> ActivitySubmitResponse:
    """
    Invia un dato di attività e calcola le emissioni GHG.
    Validazione automatica: unità coerente con la sorgente.
    """
    source = session.get(EmissionSource, req.source_id)
    if not source or source.organization_id != current_org.id:
        raise HTTPException(status_code=404, detail="Sorgente emissione non trovata")

    # Trova il fattore di emissione più recente per questa sorgente
    factor = get_best_emission_factor(session, source, req.unit, req.period_start.year)
    if not factor:
        raise HTTPException(
            status_code=422,
            detail=f"Nessun fattore emissione trovato per {source.source_type} in {req.unit}"
        )

    # Calcola emissioni
    result = activity_based_calculation(req.quantity, factor)

    # Salva ActivityRecord e GHGCalculation
    record = ActivityRecord(
        source_id=req.source_id,
        factor_id=factor.id,
        period_start=req.period_start,
        period_end=req.period_end,
        quantity=req.quantity,
        unit=req.unit,
        data_quality=req.data_quality,
        evidence_url=req.evidence_url,
        notes=req.notes
    )
    session.add(record)
    session.flush()  # Get the ID

    calc = GHGCalculation(
        activity_record_id=record.id,
        co2_kg=result.co2_kg,
        ch4_kg=result.ch4_kg,
        n2o_kg=result.n2o_kg,
        co2e_kg=result.co2e_kg,
        co2e_tonnes=result.co2e_tonnes,
        calculation_method=result.method
    )
    session.add(calc)
    session.commit()

    return ActivitySubmitResponse(
        activity_id=record.id,
        co2e_tonnes=result.co2e_tonnes,
        calculation_method=result.method,
        message=f"Emissioni calcolate: {result.co2e_tonnes:.4f} tCO2e"
    )
# api/routers/reports.py
from fastapi import APIRouter, Query
from pydantic import BaseModel
from typing import Dict
from decimal import Decimal

router = APIRouter(prefix="/api/v1", tags=["reports"])

class ESGReportResponse(BaseModel):
    organization_id: str
    year: int
    scope1_co2e_tonnes: float
    scope2_location_based_tonnes: float
    scope2_market_based_tonnes: float
    scope3_co2e_tonnes: float
    scope3_breakdown: Dict[int, float]
    total_co2e_tonnes: float
    intensity_per_employee: float
    intensity_per_revenue_m_eur: float
    sbti_alignment: Dict

class YoYComparisonResponse(BaseModel):
    year_current: int
    year_previous: int
    total_change_pct: float
    scope1_change_pct: float
    scope2_change_pct: float
    scope3_change_pct: float
    top_increasing_categories: list
    top_decreasing_categories: list


@router.get("/reports/annual/{year}", response_model=ESGReportResponse)
async def get_annual_report(
    year: int,
    session: Session = Depends(get_db),
    current_org: Organization = Depends(get_current_org)
) -> ESGReportResponse:
    """
    Genera report ESG annuale con tutte le metriche GHG Protocol.
    Include verifica allineamento SBTi.
    """
    employee_fte = get_employee_fte(session, current_org.id, year)
    revenue = get_annual_revenue(session, current_org.id, year)

    report = generate_annual_report(
        session, str(current_org.id), year, employee_fte, revenue
    )

    baseline = get_baseline_emissions(session, str(current_org.id))
    sbti = check_sbti_alignment(report, baseline["year"], baseline["tonnes"])

    return ESGReportResponse(
        organization_id=str(current_org.id),
        year=year,
        scope1_co2e_tonnes=float(report.scope1_co2e_tonnes),
        scope2_location_based_tonnes=float(report.scope2_lb_co2e_tonnes),
        scope2_market_based_tonnes=float(report.scope2_mb_co2e_tonnes),
        scope3_co2e_tonnes=float(report.scope3_co2e_tonnes),
        scope3_breakdown={k: float(v) for k, v in report.scope3_by_category.items()},
        total_co2e_tonnes=float(report.total_co2e_tonnes),
        intensity_per_employee=float(report.intensity_per_employee),
        intensity_per_revenue_m_eur=float(report.intensity_per_revenue),
        sbti_alignment=sbti
    )


@router.get("/reports/yoy/{year}", response_model=YoYComparisonResponse)
async def year_over_year(year: int, ...) -> YoYComparisonResponse:
    """Confronto Year-over-Year con identificazione trend per categoria."""
    curr = generate_annual_report(session, org_id, year, fte, rev)
    prev = generate_annual_report(session, org_id, year - 1, fte_prev, rev_prev)

    def pct_change(curr_val: Decimal, prev_val: Decimal) -> float:
        if prev_val == 0:
            return 0.0
        return float((curr_val - prev_val) / prev_val * 100)

    return YoYComparisonResponse(
        year_current=year,
        year_previous=year - 1,
        total_change_pct=pct_change(curr.total_co2e_tonnes, prev.total_co2e_tonnes),
        scope1_change_pct=pct_change(curr.scope1_co2e_tonnes, prev.scope1_co2e_tonnes),
        scope2_change_pct=pct_change(curr.scope2_mb_co2e_tonnes, prev.scope2_mb_co2e_tonnes),
        scope3_change_pct=pct_change(curr.scope3_co2e_tonnes, prev.scope3_co2e_tonnes),
        top_increasing_categories=get_top_changes(curr, prev, ascending=False),
        top_decreasing_categories=get_top_changes(curr, prev, ascending=True)
    )

Testing: Unit Test per Calcoli GHG

I calcoli GHG devono essere testati rigorosamente: errori nei fattori di emissione o nelle formule portano a rendicontazioni ESG errate, con potenziali implicazioni legali (greenwashing) e sanzioni sotto la CSRD. Il testing non è opzionale.

# tests/test_ghg_calculator.py
import pytest
from decimal import Decimal
from calculators.ghg_calculator import (
    activity_based_calculation,
    spend_based_calculation,
    business_travel_calculation
)
from calculators.scope2_calculator import scope2_dual_calculation
from calculators.scope3_commute import employee_commute_calculation, CommuteData
from models import EmissionFactor


@pytest.fixture
def natural_gas_factor() -> EmissionFactor:
    """Fattore gas naturale DEFRA 2024: 2.04264 kgCO2e/m3."""
    factor = EmissionFactor()
    factor.co2_factor = Decimal("1.88900")   # kgCO2/m3
    factor.ch4_factor = Decimal("0.00011")   # kgCH4/m3
    factor.n2o_factor = Decimal("0.00003")   # kgN2O/m3
    factor.hfc_factor = Decimal("0")
    factor.gwp_co2 = Decimal("1.0")
    factor.gwp_ch4 = Decimal("28.0")
    factor.gwp_n2o = Decimal("265.0")
    return factor


class TestActivityBasedCalculation:
    def test_natural_gas_calculation(self, natural_gas_factor):
        """Test: 1000 m3 gas naturale = ~2.04 tCO2e."""
        result = activity_based_calculation(Decimal("1000"), natural_gas_factor)

        assert result.method == "ACTIVITY_BASED"
        # CO2: 1000 * 1.889 = 1889 kg
        assert abs(result.co2_kg - Decimal("1889.00")) < Decimal("0.01")
        # CH4: 1000 * 0.00011 * 28 = 3.08 kgCO2e
        expected_co2e = (
            Decimal("1889.00") * Decimal("1.0")  # CO2
            + Decimal("0.11") * Decimal("28.0")   # CH4
            + Decimal("0.03") * Decimal("265.0")  # N2O
        )
        assert abs(result.co2e_kg - expected_co2e) < Decimal("1.0")
        assert result.co2e_tonnes == result.co2e_kg / Decimal("1000")

    def test_zero_consumption(self, natural_gas_factor):
        """Zero consumo = zero emissioni."""
        result = activity_based_calculation(Decimal("0"), natural_gas_factor)
        assert result.co2e_kg == Decimal("0")

    def test_precision_decimal(self, natural_gas_factor):
        """Verifica che i calcoli usino Decimal per evitare floating point errors."""
        result = activity_based_calculation(Decimal("333.333"), natural_gas_factor)
        # Non deve avere floating point drift
        assert isinstance(result.co2e_kg, Decimal)


class TestScope2DualMethod:
    def test_italy_location_based(self):
        """Test location-based Italia: 10.000 kWh * 310 gCO2e/kWh = 3100 kg."""
        result = scope2_dual_calculation(
            kwh_consumed=Decimal("10000"),
            grid_region="IT"
        )
        assert abs(result.location_based_co2e_kg - Decimal("3100.0")) < Decimal("1.0")

    def test_full_go_coverage_zeroes_market_based(self):
        """Con GO = consumo totale, market-based deve essere 0."""
        result = scope2_dual_calculation(
            kwh_consumed=Decimal("10000"),
            grid_region="IT",
            go_certificates_kwh=Decimal("10000")
        )
        assert result.market_based_co2e_kg == Decimal("0")
        assert result.renewable_kwh_matched == Decimal("10000")

    def test_partial_go_coverage(self):
        """Con GO parziali, market-based usa residual mix sul rimanente."""
        result = scope2_dual_calculation(
            kwh_consumed=Decimal("10000"),
            grid_region="IT",
            go_certificates_kwh=Decimal("6000")
        )
        # 4000 kWh residui * 414 gCO2e/kWh (residual mix IT) / 1000
        expected = Decimal("4000") * Decimal("414") / Decimal("1000")
        assert abs(result.market_based_co2e_kg - expected) < Decimal("0.01")


class TestEmployeeCommute:
    def test_standard_commute(self):
        """Test pendolarismo con mix modale realistico."""
        data = CommuteData(
            employee_count=200,
            avg_working_days=220,
            remote_work_days_pct=Decimal("0.40"),
            transport_split={
                "car_solo": Decimal("0.45"),
                "public_transit": Decimal("0.40"),
                "bicycle": Decimal("0.10"),
                "walk": Decimal("0.05")
            },
            avg_commute_km_oneway=Decimal("15")
        )
        result = employee_commute_calculation(data)
        assert result > Decimal("0")
        # Ordine di grandezza: ~50-150 tCO2e per 200 dipendenti
        assert Decimal("40000") < result < Decimal("200000")  # in kg

    def test_full_remote_work_zero(self):
        """100% remote work = 0 emissioni pendolarismo."""
        data = CommuteData(
            employee_count=200,
            avg_working_days=220,
            remote_work_days_pct=Decimal("1.0"),
            transport_split={"car_solo": Decimal("1.0")},
            avg_commute_km_oneway=Decimal("20")
        )
        result = employee_commute_calculation(data)
        assert result == Decimal("0")

Attenzione: Fattori di Emissione e Versioning

I fattori di emissione vengono aggiornati ogni anno (DEFRA, IEA, EPA). Un sistema ESG corretto deve:

  • Versionare i fattori nel database con anno e sorgente espliciti
  • Usare il fattore dell'anno di rendicontazione (non il più recente)
  • Permettere ricalcoli storici se i fattori vengono corretti
  • Mantenere l'audit trail: quale fattore era attivo al momento del calcolo
  • Non aggiornare mai un GHGCalculation in-place: creare sempre una nuova versione

Caso Studio: PMI Software con 200 Dipendenti

Vediamo un esempio concreto end-to-end. TechFlow S.r.l. è una PMI software italiana con 200 dipendenti, sede principale a Milano (affittata), un ufficio secondario a Roma, nessun data center proprio (tutto su AWS eu-west-1 e Azure westeurope).

Profilo Aziendale TechFlow S.r.l.

Parametro Valore
Dipendenti 200 FTE (70% Milano, 20% Roma, 10% full-remote)
Fatturato EUR 15 milioni
Uffici 2 sedi in affitto (totale 2.400 m²)
Infrastruttura IT 100% cloud (AWS + Azure), zero server fisici
Flotta 3 auto aziendali diesel, 1 furgone diesel
Business travel ~120 voli/anno, 40 notti hotel, 200 tratte ferroviarie
# Esempio calcolo completo TechFlow S.r.l. - Anno 2024

from decimal import Decimal
from calculators.ghg_calculator import activity_based_calculation, business_travel_calculation
from calculators.scope2_calculator import scope2_dual_calculation
from calculators.scope3_commute import employee_commute_calculation, CommuteData

# ---- SCOPE 1 ----
# Gas naturale: riscaldamento uffici (caldaie condominiali, incluse nel canone)
# TechFlow ha controllo operativo: include le caldaie
scope1_gas_heating = activity_based_calculation(
    Decimal("12500"),  # m3 gas naturale annui (entrambe le sedi)
    NATURAL_GAS_FACTOR_IT_2024
)
# Risultato atteso: ~25.5 tCO2e

# Flotta aziendale: 4 veicoli diesel
scope1_fleet_km = activity_based_calculation(
    Decimal("85000"),  # km totali flotta annui
    DIESEL_MOBILE_FACTOR_2024  # 0.16844 kgCO2e/km
)
# Risultato atteso: ~14.3 tCO2e

# Refrigeranti HVAC: perdita stimata 2% del carico R-410A
scope1_refrigerants = activity_based_calculation(
    Decimal("1.5"),    # kg R-410A persi
    R410A_FACTOR       # GWP=2088 kgCO2e/kg
)
# Risultato atteso: ~3.1 tCO2e

scope1_total = (
    scope1_gas_heating.co2e_tonnes
    + scope1_fleet_km.co2e_tonnes
    + scope1_refrigerants.co2e_tonnes
)
print(f"Scope 1 totale: {scope1_total:.2f} tCO2e")
# Output: Scope 1 totale: 42.90 tCO2e

# ---- SCOPE 2 ----
# Elettricità Milano: 320.000 kWh, GO acquistate: 200.000 kWh
scope2_milano = scope2_dual_calculation(
    kwh_consumed=Decimal("320000"),
    grid_region="IT",
    go_certificates_kwh=Decimal("200000")
)

# Elettricità Roma: 95.000 kWh, nessuna GO
scope2_roma = scope2_dual_calculation(
    kwh_consumed=Decimal("95000"),
    grid_region="IT",
    go_certificates_kwh=Decimal("0")
)

scope2_lb_total = scope2_milano.location_based_co2e_kg + scope2_roma.location_based_co2e_kg
scope2_mb_total = scope2_milano.market_based_co2e_kg + scope2_roma.market_based_co2e_kg

print(f"Scope 2 Location-Based: {scope2_lb_total/1000:.2f} tCO2e")
# Output: Scope 2 Location-Based: 128.45 tCO2e
print(f"Scope 2 Market-Based: {scope2_mb_total/1000:.2f} tCO2e")
# Output: Scope 2 Market-Based: 55.12 tCO2e (ridotto grazie alle GO)

# ---- SCOPE 3 ----

# Cat. 1: Cloud computing (AWS+Azure) - spend-based
# Spesa totale cloud: EUR 480.000/anno
# Fattore EXIOBASE settore "Computer Services" IT: ~0.062 kgCO2e/EUR
scope3_cat1_cloud = spend_based_calculation(
    spend_eur=Decimal("480000"),
    spend_factor_kg_per_eur=Decimal("0.062")
)
# Risultato atteso: ~29.8 tCO2e
# NOTA: Provider-specific è più accurato se disponibile
# AWS Carbon Footprint: ~18 tCO2e (più basso per energia rinnovabile AWS)

# Cat. 2: Capital goods - laptop/monitor (life cycle amortized)
# 40 laptop nuovi a EUR 1.200 cad, ammortizzati su 4 anni
scope3_cat2_laptops = spend_based_calculation(
    spend_eur=Decimal("40") * Decimal("1200") / Decimal("4"),
    spend_factor_kg_per_eur=Decimal("0.35")   # Electronic equipment
)
# Risultato atteso: ~4.2 tCO2e

# Cat. 6: Business travel
scope3_cat6_flights = business_travel_calculation(
    distance_km=Decimal("95000"),     # Totale km voli
    transport_mode="SHORT_HAUL_ECONOMY"  # Mix short/medium haul
)
scope3_cat6_rail = business_travel_calculation(
    distance_km=Decimal("18000"),     # Totale km treno
    transport_mode="RAIL_NATIONAL"
)
scope3_cat6_hotel = business_travel_calculation(
    distance_km=Decimal("40"),        # 40 notti hotel
    transport_mode="HOTEL_NIGHT"
)

scope3_cat6_total = (
    scope3_cat6_flights.co2e_tonnes
    + scope3_cat6_rail.co2e_tonnes
    + scope3_cat6_hotel.co2e_tonnes
)
# Risultato atteso: ~16.3 tCO2e

# Cat. 7: Employee commuting
commute_data = CommuteData(
    employee_count=180,   # Esclude 20 full-remote
    avg_working_days=220,
    remote_work_days_pct=Decimal("0.35"),  # Media 35% smart working
    transport_split={
        "car_solo": Decimal("0.38"),
        "public_transit": Decimal("0.45"),
        "bicycle": Decimal("0.08"),
        "walk": Decimal("0.06"),
        "car_pool": Decimal("0.03")
    },
    avg_commute_km_oneway=Decimal("12")
)
scope3_cat7_kg = employee_commute_calculation(commute_data)
# Risultato atteso: ~68.400 kg = 68.4 tCO2e

# ---- RIEPILOGO FINALE ----
scope3_total = (
    scope3_cat1_cloud.co2e_tonnes
    + scope3_cat2_laptops.co2e_tonnes
    + scope3_cat6_total
    + scope3_cat7_kg / Decimal("1000")
    # + Cat. 11 Use of sold products (SaaS): stimato ~15 tCO2e
    # + Cat. 3 T&D losses: ~8.2 tCO2e
)

grand_total = scope1_total + scope2_mb_total / Decimal("1000") + scope3_total

print("\n=== TECHFLOW S.R.L. - GHG INVENTORY 2024 ===")
print(f"Scope 1:  {float(scope1_total):8.1f} tCO2e  ( {float(scope1_total/grand_total)*100:4.1f}%)")
print(f"Scope 2:  {float(scope2_mb_total/1000):8.1f} tCO2e  ( {float(scope2_mb_total/1000/grand_total)*100:4.1f}%)")
print(f"Scope 3:  {float(scope3_total):8.1f} tCO2e  ( {float(scope3_total/grand_total)*100:4.1f}%)")
print(f"TOTALE:   {float(grand_total):8.1f} tCO2e")
print(f"Intensità: {float(grand_total/200):5.2f} tCO2e/dipendente")

# === TECHFLOW S.R.L. - GHG INVENTORY 2024 ===
# Scope 1:    42.9 tCO2e  ( 18.2%)
# Scope 2:    55.1 tCO2e  ( 23.4%)
# Scope 3:   137.8 tCO2e  ( 58.4%)
# TOTALE:    235.8 tCO2e
# Intensità:  1.18 tCO2e/dipendente

Benchmark: Intensità Carbon per Aziende Software (2024)

Dimensione tCO₂e/dipendente % Scope 3 Principale voce S3
Startup (<50 dip.) 0.8 - 1.2 65-75% Business travel
PMI software (50-500) 1.0 - 1.8 55-70% Pendolarismo + cloud
Enterprise tech (500+) 1.5 - 3.0 70-85% Cloud + use of products
Big Tech (hyperscaler) 3.0 - 8.0 75-90% Supply chain hardware + data centers
TechFlow (caso studio) 1.18 58% Pendolarismo

Opportunità di Riduzione per TechFlow

Dall'analisi del GHG Inventory 2024, emergono le leve di riduzione più efficaci:

  • Smart working esteso al 50%: Riduzione Cat. 7 di ~35% (-24 tCO2e). Il pendolarismo è la voce singola più grande.
  • GO al 100% per entrambe le sedi: Scope 2 market-based scende a ~0 tCO2e (-55 tCO2e). Costo stimato GO: EUR 3.000-5.000/anno.
  • Politica travel carbon budget: Sostituzione voli brevi (<3h) con treno. Riduzione Cat. 6 di ~40% (-6.5 tCO2e).
  • AWS Carbon Footprint Tool: Passare da stima spend-based a dato provider riduce Cat. 1 cloud di ~40% (-12 tCO2e) e migliora la qualità del dato.

Roadmap Decarbonizzazione TechFlow - Target SBTi 1.5°C

Anno Target (tCO₂e) Riduzione vs 2024 Azioni principali
2024 (baseline) 235.8 - Prima rendicontazione CSRD
2025 211.0 -10.5% GO 100%, smart working 50%
2027 165.0 -30% Flotta elettrica, travel policy
2030 118.0 -50% (SBTi near-term) Fornitori cloud green, remote-first
2050 <24 -90% (SBTi net-zero) Residuo neutralizzato, no compensazioni

Conclusioni e Next Steps

Il data modeling per ESG reporting non è un semplice esercizio di compliance: è la base su cui costruire una strategia di decarbonizzazione credibile e verificabile. Abbiamo visto come strutturare un sistema che copre l'intera catena del valore di un'azienda software, dalla caldaia del riscaldamento (Scope 1) al viaggio in aereo del commerciale (Scope 3 Cat. 6) fino all'energia consumata dagli utenti per eseguire il prodotto (Scope 3 Cat. 11).

I punti chiave da portare con sé:

  • Scope 3 domina: Per le aziende software, il 55-75% delle emissioni è Scope 3. Non modellarlo correttamente significa avere un'immagine distorta della propria impronta carbonica.
  • Dual-reporting Scope 2: ESRS E1 richiede entrambi i metodi (location-based e market-based). Il market-based incentiva l'acquisto di energia rinnovabile certificata.
  • Qualità del dato è tutto: La gerarchia activity-based > spend-based deve guidare le scelte di data collection. Iniziate con stime spend-based e miglioratele anno dopo anno.
  • Immutabilità delle calculation: Mai aggiornare un GHGCalculation in place. Versionate i fattori e mantenete l'audit trail completo per la revisione dei revisori ESG.
  • SBTi come nord star: Il target Science Based (-50% al 2030, -90% al 2050) deve guidare le priorità di riduzione, non il minimo compliance CSRD.

Prossimi Articoli della Serie

Nel prossimo articolo della serie, "ESG & CSRD: Obblighi Europei per le Aziende Tech", approfondiamo il quadro normativo completo: chi è soggetto alla CSRD, le scadenze per le PMI, il concetto di double materiality assessment, e come il sistema di data modeling costruito in questo articolo si traduce nelle disclosure ESRS E1 richieste all'Autorità.

Consulta anche gli articoli correlati:

  • Serie MLOps: Come integrare il carbon tracking nelle pipeline ML (metriche di sostenibilità nel model registry MLflow)
  • Serie AI Engineering: Carbon footprint dei sistemi RAG e LLM in produzione
  • Articolo 9 (questa serie): Scope 3 Pipeline - Automatizzare la raccolta dati dalla value chain con connettori API per fornitori

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