grid_tariffs/
feed_in_revenue.rs

1use chrono::{DateTime, TimeZone};
2use serde::Serialize;
3
4use crate::{Cost, CostPeriods, CostPeriodsSimple, Country, Language, Money, currency::Currency};
5
6/// Feed-in revenue, per kWh (usually from solar production)
7/// A Swedish concept for "thanking" micro producers (<=43,5 kW) for reducing losses in the grid
8#[derive(Debug, Clone, Copy, Serialize)]
9#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
10pub enum FeedInRevenue {
11    Simple(Cost),
12    /// Not yet checked
13    Unverified,
14    /// Could not be located on their website or elsewhere
15    Unlisted,
16    /// Varies by the current spot price
17    SpotPriceVariable {
18        base_cost: Money,
19        spot_price_multiplier: f64,
20        /// If this is approximated from actual data, or if it's based on documented pricing
21        approximated: bool,
22    },
23    Periods(CostPeriods),
24}
25
26impl FeedInRevenue {
27    pub const fn is_unverified(&self) -> bool {
28        matches!(self, Self::Unverified)
29    }
30
31    pub(crate) const fn new_periods(periods: CostPeriods) -> Self {
32        Self::Periods(periods)
33    }
34
35    pub(crate) const fn fixed_subunit(subunit: f64) -> Self {
36        Self::Simple(Cost::fixed_subunit(subunit))
37    }
38
39    pub(crate) const fn fixed_subunit_remove_vat(subunit: f64, country: Country) -> Self {
40        Self::Simple(Cost::fixed_subunit(subunit).remove_vat(country))
41    }
42
43    pub const fn spot_price_variable(
44        base_cost_subunit: f64,
45        spot_price_multiplier: f64,
46        approximated: bool,
47    ) -> Self {
48        if spot_price_multiplier < 0.0 || spot_price_multiplier > 1.0 {
49            panic!("`spot_price_multiplier` too low/high");
50        }
51        Self::SpotPriceVariable {
52            base_cost: Money::new_subunit(base_cost_subunit),
53            spot_price_multiplier,
54            approximated,
55        }
56    }
57
58    pub fn simplified(
59        &self,
60        fuse_size: u16,
61        yearly_consumption: u32,
62        language: Language,
63    ) -> FeedInRevenueSimplified {
64        FeedInRevenueSimplified::new(self, fuse_size, yearly_consumption, language)
65    }
66
67    pub fn kwh_revenue<Tz: TimeZone>(
68        &self,
69        timestamp: DateTime<Tz>,
70        spotprice: Money,
71        fuse_size: u16,
72        yearly_consumption: u32,
73    ) -> Money
74    where
75        DateTime<Tz>: Copy,
76    {
77        match *self {
78            FeedInRevenue::Unverified | FeedInRevenue::Unlisted => Money::ZERO,
79            FeedInRevenue::Simple(cost) => cost
80                .cost_for(fuse_size, yearly_consumption)
81                .unwrap_or_default(),
82            FeedInRevenue::SpotPriceVariable {
83                base_cost,
84                spot_price_multiplier,
85                approximated: _,
86            } => base_cost + (spotprice * spot_price_multiplier),
87            FeedInRevenue::Periods(cost_periods) => cost_periods
88                .matching_periods(timestamp)
89                .into_iter()
90                .flat_map(|period| period.cost().cost_for(fuse_size, yearly_consumption))
91                .sum(),
92        }
93    }
94}
95
96/// Feed-in revenue, per kWh (usually from solar production)
97/// Like FeedInRevenue, but with costs being simple Money objects
98#[derive(Debug, Clone, Serialize)]
99#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
100pub enum FeedInRevenueSimplified {
101    Simple(Option<Money>),
102    /// Not yet checked
103    Unverified,
104    /// Could not be located on their website or elsewhere
105    Unlisted,
106    /// Varies by the current spot price
107    SpotPriceVariable {
108        base_cost: Money,
109        spot_price_multiplier: f64,
110        /// If this is approximated from actual data, or if it's based on documented pricing
111        approximated: bool,
112        info: String,
113    },
114    Periods(CostPeriodsSimple),
115}
116
117impl FeedInRevenueSimplified {
118    fn new(
119        revenue: &FeedInRevenue,
120        fuse_size: u16,
121        yearly_consumption: u32,
122        language: Language,
123    ) -> Self {
124        match *revenue {
125            FeedInRevenue::Unlisted => Self::Unlisted,
126            FeedInRevenue::Unverified => Self::Unverified,
127            FeedInRevenue::Simple(cost) => {
128                Self::Simple(cost.cost_for(fuse_size, yearly_consumption))
129            }
130            FeedInRevenue::SpotPriceVariable {
131                base_cost,
132                spot_price_multiplier,
133                approximated,
134            } => Self::SpotPriceVariable {
135                base_cost,
136                spot_price_multiplier,
137                approximated,
138                info: Default::default(),
139            },
140            FeedInRevenue::Periods(periods) => Self::Periods(CostPeriodsSimple::new(
141                periods,
142                fuse_size,
143                yearly_consumption,
144                language,
145            )),
146        }
147        .add_info(language)
148    }
149
150    fn add_info(self, language: Language) -> Self {
151        match self {
152            FeedInRevenueSimplified::SpotPriceVariable {
153                base_cost,
154                spot_price_multiplier,
155                approximated,
156                info: _,
157            } => {
158                let percentage = spot_price_multiplier * 100.;
159                let mut info = match language {
160                    Language::En => format!(
161                        "The grid operator bases its feed-in revenue on a fixed part of {} and {:.2}% of the current spot price.",
162                        base_cost.display(Currency::SEK).subunit_display(),
163                        percentage
164                    ),
165                    Language::Sv => format!(
166                        "Nätbolaget baserar sin nätnytta på en fast del om {} samt {:.2}% av spotpriset.",
167                        base_cost.display(Currency::SEK).subunit_display(),
168                        percentage
169                    ),
170                };
171                if approximated {
172                    info.push_str(match language {
173                        Language::En => " The base fee and percentage are estimated, as the grid operator doesn't list them on their website.",
174                        Language::Sv => " Basavgift och procentsats är uppskattade, eftersom nätbolaget inte skriver ut exakt vad de är på sin webbplats.",
175                    })
176                }
177                FeedInRevenueSimplified::SpotPriceVariable {
178                    base_cost,
179                    spot_price_multiplier,
180                    approximated,
181                    info,
182                }
183            }
184            _ => self,
185        }
186    }
187}