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