grid_tariffs/
transfer_fee.rs

1use chrono::{DateTime, TimeZone};
2use serde::Serialize;
3
4use crate::{Cost, CostPeriods, CostPeriodsSimple, Language, Money, currency::Currency};
5
6#[derive(Debug, Clone, Copy, Serialize)]
7#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
8pub enum TransferFee {
9    /// Fee does not change except possibly by fuse size
10    Simple(Cost),
11    /// Transfer fee that varies by the current spot price
12    /// We have currently only observed that Växjö Energi uses this variant
13    SpotPriceVariable {
14        base_cost: Money,
15        spot_price_multiplier: f64,
16        approximated: bool,
17    },
18    Periods(CostPeriods),
19}
20
21impl TransferFee {
22    pub const fn new_periods(periods: CostPeriods) -> Self {
23        Self::Periods(periods)
24    }
25
26    pub const fn simple_cost(&self) -> Option<Cost> {
27        match self {
28            Self::Simple(cost) => Some(*cost),
29            _ => None,
30        }
31    }
32
33    pub const fn none() -> Self {
34        Self::Simple(Cost::None)
35    }
36
37    pub const fn spot_price_variable(
38        base_cost_subunit: f64,
39        spot_price_multiplier: f64,
40        approximated: bool,
41    ) -> Self {
42        if spot_price_multiplier < 0.0 || spot_price_multiplier > 1.0 {
43            panic!("`spot_price_multiplier` too low/high");
44        }
45        Self::SpotPriceVariable {
46            base_cost: Money::new_subunit(base_cost_subunit),
47            spot_price_multiplier,
48            approximated,
49        }
50    }
51
52    pub fn simplified(
53        &self,
54        fuse_size: u16,
55        yearly_consumption: u32,
56        language: Language,
57    ) -> TransferFeeSimplified {
58        TransferFeeSimplified::new(self, fuse_size, yearly_consumption, language)
59    }
60
61    pub(crate) const fn fixed(int: i64, fract: u8) -> Self {
62        Self::Simple(Cost::fixed(int, fract))
63    }
64
65    pub(crate) const fn fixed_subunit(subunit: f64) -> Self {
66        Self::Simple(Cost::fixed_subunit(subunit))
67    }
68
69    pub fn kwh_cost<Tz: TimeZone>(
70        &self,
71        timestamp: DateTime<Tz>,
72        spotprice: Money,
73        fuse_size: u16,
74        yearly_consumption: u32,
75    ) -> Money
76    where
77        DateTime<Tz>: Copy,
78    {
79        match *self {
80            TransferFee::Simple(cost) => cost
81                .cost_for(fuse_size, yearly_consumption)
82                .unwrap_or_default(),
83            TransferFee::SpotPriceVariable {
84                base_cost,
85                spot_price_multiplier,
86                approximated: _,
87            } => base_cost + (spotprice * spot_price_multiplier),
88            TransferFee::Periods(cost_periods) => cost_periods
89                .matching_periods(timestamp)
90                .into_iter()
91                .flat_map(|p| p.cost().cost_for(fuse_size, yearly_consumption))
92                .sum(),
93        }
94    }
95
96    pub(crate) fn is_yearly_consumption_based(&self, fuse_size: u16) -> bool {
97        match self {
98            TransferFee::SpotPriceVariable { .. } => false,
99            TransferFee::Simple(cost) => cost.is_yearly_consumption_based(fuse_size),
100            TransferFee::Periods(periods) => periods.is_yearly_consumption_based(fuse_size),
101        }
102    }
103
104    /// Use when the operator states that they use spot price variable pricing, but don't specify the actual multipliers
105    pub(crate) const fn spot_price_variable_placeholder() -> TransferFee {
106        Self::spot_price_variable(2.0, 0.06, true)
107    }
108}
109
110/// Like TransferFee, but with costs being simple Money objects
111#[derive(Debug, Clone, Serialize)]
112#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
113pub enum TransferFeeSimplified {
114    /// Price was not listed on their website
115    Unlisted,
116    /// Fee does not change except possibly by fuse size
117    Simple(Money),
118    /// Transfer fee that varies by the current spot price
119    /// We have currently only observed that Växjö Energi uses this variant
120    SpotPriceVariable {
121        base_cost: Money,
122        spot_price_multiplier: f64,
123        approximated: bool,
124        info: String,
125    },
126    Periods(CostPeriodsSimple),
127}
128
129impl TransferFeeSimplified {
130    fn new(fee: &TransferFee, fuse_size: u16, yearly_consumption: u32, language: Language) -> Self {
131        match *fee {
132            TransferFee::Simple(cost) => TransferFeeSimplified::Simple(
133                cost.cost_for(fuse_size, yearly_consumption)
134                    .unwrap_or(Money::ZERO),
135            ),
136            TransferFee::SpotPriceVariable {
137                base_cost,
138                spot_price_multiplier,
139                approximated,
140            } => TransferFeeSimplified::SpotPriceVariable {
141                base_cost,
142                spot_price_multiplier,
143                approximated,
144                info: Default::default(),
145            },
146            TransferFee::Periods(periods) => TransferFeeSimplified::Periods(
147                CostPeriodsSimple::new(periods, fuse_size, yearly_consumption, language),
148            ),
149        }
150        .add_info(language)
151    }
152
153    fn add_info(self, language: Language) -> Self {
154        match self {
155            TransferFeeSimplified::Unlisted => self,
156            TransferFeeSimplified::Simple(_) => self,
157            TransferFeeSimplified::SpotPriceVariable {
158                base_cost,
159                spot_price_multiplier,
160                approximated,
161                info: _,
162            } => {
163                let percentage = spot_price_multiplier * 100.;
164                let mut info = match language {
165                    Language::En => format!(
166                        "The grid operator bases its transfer fee on a fixed part of {} and {:.2}% of the current spot price.",
167                        base_cost.display(Currency::SEK).subunit_display(),
168                        percentage
169                    ),
170                    Language::Sv => format!(
171                        "Nätbolaget baserar sin överföringsavgift på en fast del om {} samt {:.2}% av spotpriset.",
172                        base_cost.display(Currency::SEK).subunit_display(),
173                        percentage
174                    ),
175                };
176                if approximated {
177                    info.push_str(match language {
178                        Language::En => " The base fee and percentage are estimated, as the grid operator doesn't list them on their website.",
179                        Language::Sv => " Basavgift och procentsats är uppskattade, eftersom nätbolaget inte skriver ut exakt vad de är på sin webbplats.",
180                    })
181                }
182                TransferFeeSimplified::SpotPriceVariable {
183                    base_cost,
184                    spot_price_multiplier,
185                    approximated,
186                    info,
187                }
188            }
189            TransferFeeSimplified::Periods(_) => self,
190        }
191    }
192}