use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use crate::error::CorpFinanceError;
use crate::CorpFinanceResult;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CbamGood {
pub product: String,
pub quantity_tonnes: Decimal,
pub embedded_emissions: Decimal,
pub origin_country: String,
pub origin_carbon_price: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CbamInput {
pub imported_goods: Vec<CbamGood>,
pub eu_ets_price: Decimal,
pub eu_free_allocation_pct: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CbamGoodResult {
pub product: String,
pub total_emissions: Decimal,
pub gross_cbam_cost: Decimal,
pub origin_credit: Decimal,
pub free_allocation_credit: Decimal,
pub net_cbam_cost: Decimal,
pub effective_carbon_price: Decimal,
pub price_differential: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CbamOutput {
pub total_embedded_emissions: Decimal,
pub goods_results: Vec<CbamGoodResult>,
pub total_gross_cost: Decimal,
pub total_net_cost: Decimal,
pub total_origin_credits: Decimal,
pub certificates_required: Decimal,
pub average_effective_price: Decimal,
}
pub fn calculate_cbam(input: &CbamInput) -> CorpFinanceResult<CbamOutput> {
if input.imported_goods.is_empty() {
return Err(CorpFinanceError::InsufficientData(
"At least one imported good is required".into(),
));
}
if input.eu_ets_price < Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: "eu_ets_price".into(),
reason: "EU ETS price cannot be negative".into(),
});
}
if input.eu_free_allocation_pct < Decimal::ZERO || input.eu_free_allocation_pct > Decimal::ONE {
return Err(CorpFinanceError::InvalidInput {
field: "eu_free_allocation_pct".into(),
reason: "Free allocation percentage must be between 0 and 1".into(),
});
}
for (i, good) in input.imported_goods.iter().enumerate() {
if good.quantity_tonnes < Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: format!("imported_goods[{}].quantity_tonnes", i),
reason: "Quantity cannot be negative".into(),
});
}
if good.embedded_emissions < Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: format!("imported_goods[{}].embedded_emissions", i),
reason: "Embedded emissions cannot be negative".into(),
});
}
if good.origin_carbon_price < Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: format!("imported_goods[{}].origin_carbon_price", i),
reason: "Origin carbon price cannot be negative".into(),
});
}
}
let mut goods_results = Vec::with_capacity(input.imported_goods.len());
let mut total_embedded_emissions = Decimal::ZERO;
let mut total_gross_cost = Decimal::ZERO;
let mut total_net_cost = Decimal::ZERO;
let mut total_origin_credits = Decimal::ZERO;
for good in &input.imported_goods {
let total_emissions = good.quantity_tonnes * good.embedded_emissions;
let gross_cbam_cost = total_emissions * input.eu_ets_price;
let origin_credit = total_emissions * good.origin_carbon_price;
let free_allocation_credit = gross_cbam_cost * input.eu_free_allocation_pct;
let net_raw = gross_cbam_cost - origin_credit - free_allocation_credit;
let net_cbam_cost = if net_raw > Decimal::ZERO {
net_raw
} else {
Decimal::ZERO
};
let effective_carbon_price = if total_emissions > Decimal::ZERO {
net_cbam_cost / total_emissions
} else {
Decimal::ZERO
};
let price_differential = input.eu_ets_price - good.origin_carbon_price;
total_embedded_emissions += total_emissions;
total_gross_cost += gross_cbam_cost;
total_net_cost += net_cbam_cost;
total_origin_credits += origin_credit;
goods_results.push(CbamGoodResult {
product: good.product.clone(),
total_emissions,
gross_cbam_cost,
origin_credit,
free_allocation_credit,
net_cbam_cost,
effective_carbon_price,
price_differential,
});
}
let certificates_required = if input.eu_ets_price > Decimal::ZERO {
total_net_cost / input.eu_ets_price
} else {
Decimal::ZERO
};
let average_effective_price = if total_embedded_emissions > Decimal::ZERO {
total_net_cost / total_embedded_emissions
} else {
Decimal::ZERO
};
Ok(CbamOutput {
total_embedded_emissions,
goods_results,
total_gross_cost,
total_net_cost,
total_origin_credits,
certificates_required,
average_effective_price,
})
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
fn steel_good() -> CbamGood {
CbamGood {
product: "Steel".into(),
quantity_tonnes: dec!(1000),
embedded_emissions: dec!(2.0),
origin_country: "China".into(),
origin_carbon_price: dec!(10),
}
}
fn cement_good() -> CbamGood {
CbamGood {
product: "Cement".into(),
quantity_tonnes: dec!(500),
embedded_emissions: dec!(0.8),
origin_country: "Turkey".into(),
origin_carbon_price: dec!(5),
}
}
fn base_input() -> CbamInput {
CbamInput {
imported_goods: vec![steel_good()],
eu_ets_price: dec!(80),
eu_free_allocation_pct: dec!(0.10),
}
}
#[test]
fn test_single_product_total_emissions() {
let input = base_input();
let out = calculate_cbam(&input).unwrap();
assert_eq!(out.total_embedded_emissions, dec!(2000));
}
#[test]
fn test_single_product_gross_cost() {
let input = base_input();
let out = calculate_cbam(&input).unwrap();
assert_eq!(out.goods_results[0].gross_cbam_cost, dec!(160000));
}
#[test]
fn test_single_product_origin_credit() {
let input = base_input();
let out = calculate_cbam(&input).unwrap();
assert_eq!(out.goods_results[0].origin_credit, dec!(20000));
}
#[test]
fn test_single_product_free_allocation_credit() {
let input = base_input();
let out = calculate_cbam(&input).unwrap();
assert_eq!(out.goods_results[0].free_allocation_credit, dec!(16000));
}
#[test]
fn test_single_product_net_cost() {
let input = base_input();
let out = calculate_cbam(&input).unwrap();
assert_eq!(out.goods_results[0].net_cbam_cost, dec!(124000));
}
#[test]
fn test_effective_carbon_price() {
let input = base_input();
let out = calculate_cbam(&input).unwrap();
assert_eq!(out.goods_results[0].effective_carbon_price, dec!(62));
}
#[test]
fn test_price_differential() {
let input = base_input();
let out = calculate_cbam(&input).unwrap();
assert_eq!(out.goods_results[0].price_differential, dec!(70));
}
#[test]
fn test_certificates_required() {
let input = base_input();
let out = calculate_cbam(&input).unwrap();
assert_eq!(out.certificates_required, dec!(1550));
}
#[test]
fn test_multiple_products() {
let mut input = base_input();
input.imported_goods.push(cement_good());
let out = calculate_cbam(&input).unwrap();
assert_eq!(out.total_embedded_emissions, dec!(2400));
assert_eq!(out.goods_results.len(), 2);
}
#[test]
fn test_multiple_products_total_gross() {
let mut input = base_input();
input.imported_goods.push(cement_good());
let out = calculate_cbam(&input).unwrap();
assert_eq!(out.total_gross_cost, dec!(192000));
}
#[test]
fn test_high_origin_price_zero_net() {
let mut input = base_input();
input.imported_goods[0].origin_carbon_price = dec!(100);
let out = calculate_cbam(&input).unwrap();
assert_eq!(out.goods_results[0].net_cbam_cost, Decimal::ZERO);
}
#[test]
fn test_zero_origin_price_full_cbam() {
let mut input = base_input();
input.imported_goods[0].origin_carbon_price = Decimal::ZERO;
let out = calculate_cbam(&input).unwrap();
assert_eq!(out.goods_results[0].net_cbam_cost, dec!(144000));
}
#[test]
fn test_zero_free_allocation() {
let mut input = base_input();
input.eu_free_allocation_pct = Decimal::ZERO;
let out = calculate_cbam(&input).unwrap();
assert_eq!(out.goods_results[0].net_cbam_cost, dec!(140000));
}
#[test]
fn test_full_free_allocation() {
let mut input = base_input();
input.eu_free_allocation_pct = Decimal::ONE;
let out = calculate_cbam(&input).unwrap();
assert_eq!(out.goods_results[0].net_cbam_cost, Decimal::ZERO);
}
#[test]
fn test_empty_goods_rejected() {
let input = CbamInput {
imported_goods: vec![],
eu_ets_price: dec!(80),
eu_free_allocation_pct: dec!(0.10),
};
let result = calculate_cbam(&input);
assert!(result.is_err());
}
#[test]
fn test_negative_ets_price_rejected() {
let mut input = base_input();
input.eu_ets_price = dec!(-10);
let result = calculate_cbam(&input);
assert!(result.is_err());
}
#[test]
fn test_free_allocation_out_of_range_rejected() {
let mut input = base_input();
input.eu_free_allocation_pct = dec!(1.5);
let result = calculate_cbam(&input);
assert!(result.is_err());
}
#[test]
fn test_negative_quantity_rejected() {
let mut input = base_input();
input.imported_goods[0].quantity_tonnes = dec!(-100);
let result = calculate_cbam(&input);
assert!(result.is_err());
}
#[test]
fn test_negative_embedded_emissions_rejected() {
let mut input = base_input();
input.imported_goods[0].embedded_emissions = dec!(-1);
let result = calculate_cbam(&input);
assert!(result.is_err());
}
#[test]
fn test_negative_origin_price_rejected() {
let mut input = base_input();
input.imported_goods[0].origin_carbon_price = dec!(-5);
let result = calculate_cbam(&input);
assert!(result.is_err());
}
#[test]
fn test_average_effective_price() {
let input = base_input();
let out = calculate_cbam(&input).unwrap();
assert_eq!(out.average_effective_price, dec!(62));
}
#[test]
fn test_mixed_origins() {
let mut input = base_input();
input.imported_goods.push(CbamGood {
product: "Aluminium".into(),
quantity_tonnes: dec!(200),
embedded_emissions: dec!(5.0),
origin_country: "Norway".into(),
origin_carbon_price: dec!(75),
});
let out = calculate_cbam(&input).unwrap();
assert_eq!(out.goods_results[1].net_cbam_cost, Decimal::ZERO);
assert_eq!(out.total_net_cost, dec!(124000));
}
#[test]
fn test_zero_eu_ets_price() {
let mut input = base_input();
input.eu_ets_price = Decimal::ZERO;
let out = calculate_cbam(&input).unwrap();
assert_eq!(out.total_gross_cost, Decimal::ZERO);
assert_eq!(out.certificates_required, Decimal::ZERO);
}
}