1use std::slice::Iter;
2
3use chrono::{DateTime, Datelike};
4use itertools::Itertools;
5use serde::Serialize;
6
7use crate::{
8 Country, Language, LoadType, Money, Timezone, helpers,
9 hours::Hours,
10 months::{Month, Months},
11};
12
13#[derive(Debug, Clone, Copy, Serialize)]
15#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
16pub enum Cost {
17 None,
18 Unverified,
20 Fixed(Money),
21 Fuses(&'static [(u16, Money)]),
22 FusesYearlyConsumption(&'static [(u16, Option<u32>, Money)]),
24 FuseRange(&'static [(u16, u16, Money)]),
25}
26
27impl Cost {
28 pub const fn fuses(values: &'static [(u16, Money)]) -> Self {
29 Self::Fuses(values)
30 }
31
32 pub const fn fuse_range(ranges: &'static [(u16, u16, Money)]) -> Self {
33 Self::FuseRange(ranges)
34 }
35
36 pub const fn fuses_with_yearly_consumption(
37 values: &'static [(u16, Option<u32>, Money)],
38 ) -> Self {
39 Self::FusesYearlyConsumption(values)
40 }
41
42 pub const fn fixed(int: i64, fract: u8) -> Self {
43 Self::Fixed(Money::new(int, fract))
44 }
45
46 pub const fn fixed_yearly(int: i64, fract: u8) -> Self {
47 Self::Fixed(Money::new(int, fract).divide_by(12))
48 }
49
50 pub const fn fixed_subunit(subunit: f64) -> Self {
51 Self::Fixed(Money::new_subunit(subunit))
52 }
53
54 pub const fn is_unverified(&self) -> bool {
55 matches!(self, Self::Unverified)
56 }
57
58 pub const fn divide_by(&self, by: i64) -> Self {
59 match self {
60 Self::None => Self::None,
61 Self::Unverified => Self::Unverified,
62 Self::Fixed(money) => Self::Fixed(money.divide_by(by)),
63 Self::Fuses(_) => panic!(".divide_by() is unsupported on Cost::Fuses"),
64 Self::FusesYearlyConsumption(_) => {
65 panic!(".divide_by() is unsupported on Cost::FuseRangeYearlyConsumption")
66 }
67 Self::FuseRange(_) => panic!(".divide_by() is unsupported on Cost::FuseRange"),
68 }
69 }
70
71 pub const fn cost_for(&self, fuse_size: u16, yearly_consumption: u32) -> Option<Money> {
72 match *self {
73 Cost::None => None,
74 Cost::Unverified => None,
75 Cost::Fixed(money) => Some(money),
76 Cost::Fuses(values) => {
77 let mut i = 0;
78 while i < values.len() {
79 let (fsize, money) = values[i];
80 if fuse_size == fsize {
81 return Some(money);
82 }
83 i += 1;
84 }
85 None
86 }
87 Cost::FusesYearlyConsumption(values) => {
88 let mut i = 0;
89 while i < values.len() {
90 let (fsize, max_consumption, money) = values[i];
91 if fsize == fuse_size {
92 if let Some(max_consumption) = max_consumption {
93 if yearly_consumption <= max_consumption {
94 return Some(money);
95 }
96 } else {
97 return Some(money);
98 }
99 }
100 i += 1;
101 }
102 None
103 }
104 Cost::FuseRange(ranges) => {
105 let mut i = 0;
106 while i < ranges.len() {
107 let (min, max, money) = ranges[i];
108 if fuse_size >= min && fuse_size <= max {
109 return Some(money);
110 }
111 i += 1;
112 }
113 None
114 }
115 }
116 }
117
118 pub const fn add_vat(&self, country: Country) -> Cost {
119 match self {
120 Cost::None => Cost::None,
121 Cost::Unverified => Cost::Unverified,
122 Cost::Fixed(money) => Cost::Fixed(money.add_vat(country)),
123 Cost::Fuses(_) => todo!(),
124 Cost::FusesYearlyConsumption(_) => todo!(),
125 Cost::FuseRange(_) => todo!(),
126 }
127 }
128
129 pub const fn remove_vat(&self, country: Country) -> Cost {
130 match self {
131 Cost::None => Cost::None,
132 Cost::Unverified => Cost::Unverified,
133 Cost::Fixed(money) => Cost::Fixed(money.remove_vat(country)),
134 Cost::Fuses(_) => todo!(),
135 Cost::FusesYearlyConsumption(_) => todo!(),
136 Cost::FuseRange(_) => todo!(),
137 }
138 }
139
140 pub fn is_yearly_consumption_based(&self, fuse_size: u16) -> bool {
141 match self {
142 Cost::FusesYearlyConsumption(items) => items
143 .iter()
144 .filter(|(fsize, _, _)| *fsize == fuse_size)
145 .any(|(_, yearly_consumption, _)| yearly_consumption.is_some()),
146 _ => false,
147 }
148 }
149}
150
151#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
153#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
154pub enum CostPeriodMatching {
155 First,
157 All,
159}
160
161#[derive(Debug, Clone, Copy, Serialize)]
162#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
163pub struct CostPeriods {
164 match_method: CostPeriodMatching,
165 periods: &'static [CostPeriod],
166}
167
168impl CostPeriods {
169 pub const fn new_first(periods: &'static [CostPeriod]) -> Self {
171 Self {
172 match_method: CostPeriodMatching::First,
173 periods,
174 }
175 }
176
177 pub const fn new_all(periods: &'static [CostPeriod]) -> Self {
179 Self {
180 match_method: CostPeriodMatching::All,
181 periods,
182 }
183 }
184
185 pub const fn match_method(&self) -> CostPeriodMatching {
186 self.match_method
187 }
188
189 pub fn iter(&self) -> Iter<'_, CostPeriod> {
190 self.periods.iter()
191 }
192
193 pub(crate) fn is_yearly_consumption_based(&self, fuse_size: u16) -> bool {
194 self.periods
195 .iter()
196 .any(|cp| cp.is_yearly_consumption_based(fuse_size))
197 }
198
199 pub fn matching_periods<Tz: chrono::TimeZone>(
200 &self,
201 timestamp: DateTime<Tz>,
202 ) -> Vec<&CostPeriod>
203 where
204 DateTime<Tz>: Copy,
205 {
206 let mut ret = vec![];
207 for period in self.periods {
208 if period.matches(timestamp) {
209 ret.push(period);
210 if self.match_method == CostPeriodMatching::First {
211 break;
212 }
213 }
214 }
215 ret
216 }
217}
218
219#[derive(Debug, Clone, Serialize)]
221#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
222pub struct CostPeriodsSimple {
223 periods: Vec<CostPeriodSimple>,
224}
225
226impl CostPeriodsSimple {
227 pub(crate) fn new(
228 periods: CostPeriods,
229 fuse_size: u16,
230 yearly_consumption: u32,
231 language: Language,
232 ) -> Self {
233 Self {
234 periods: periods
235 .periods
236 .iter()
237 .flat_map(|period| {
238 CostPeriodSimple::new(period, fuse_size, yearly_consumption, language)
239 })
240 .collect(),
241 }
242 }
243}
244
245#[derive(Debug, Clone, Serialize)]
246#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
247pub struct CostPeriod {
248 cost: Cost,
249 load: LoadType,
250 #[serde(serialize_with = "helpers::skip_nones")]
251 include: [Option<Include>; 2],
252 #[serde(serialize_with = "helpers::skip_nones")]
253 exclude: [Option<Exclude>; 2],
254}
255
256#[derive(Debug, Clone, Serialize)]
258#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
259pub(crate) struct CostPeriodSimple {
260 cost: Money,
261 load: LoadType,
262 include: Vec<Include>,
263 exclude: Vec<Exclude>,
264 info: String,
265}
266
267impl CostPeriodSimple {
268 fn new(
269 period: &CostPeriod,
270 fuse_size: u16,
271 yearly_consumption: u32,
272 language: Language,
273 ) -> Option<Self> {
274 let cost = period.cost().cost_for(fuse_size, yearly_consumption)?;
275 Some(
276 Self {
277 cost,
278 load: period.load,
279 include: period.include.into_iter().flatten().collect(),
280 exclude: period.exclude.into_iter().flatten().collect(),
281 info: Default::default(),
282 }
283 .add_info(language),
284 )
285 }
286
287 fn add_info(mut self, language: Language) -> Self {
288 let mut infos = self
289 .include
290 .iter()
291 .map(|i| i.translate(language))
292 .chain(self.exclude.iter().map(|e| e.translate(language).into()))
293 .collect_vec();
294 for (idx, info) in infos.iter_mut().enumerate() {
295 if let Some(first_char) = info.chars().next() {
296 *info = (if idx == 0 {
297 first_char.to_uppercase().to_string()
298 } else {
299 first_char.to_lowercase().to_string()
300 }) + &info[first_char.len_utf8()..];
301 }
302 }
303 self.info = infos.join(", ");
304 self
305 }
306}
307
308impl CostPeriod {
309 pub const fn builder() -> CostPeriodBuilder {
310 CostPeriodBuilder::new()
311 }
312
313 pub const fn cost(&self) -> Cost {
314 self.cost
315 }
316
317 pub const fn load(&self) -> LoadType {
318 self.load
319 }
320
321 pub fn matches<Tz: chrono::TimeZone>(&self, timestamp: DateTime<Tz>) -> bool
322 where
323 DateTime<Tz>: Copy,
324 {
325 for include in self.include_period_types() {
326 if !include.matches(timestamp) {
327 return false;
328 }
329 }
330
331 for exclude in self.exclude_period_types() {
332 if exclude.matches(timestamp) {
333 return false;
334 }
335 }
336 true
337 }
338
339 fn include_period_types(&self) -> Vec<Include> {
340 self.include.iter().flatten().copied().collect()
341 }
342
343 fn exclude_period_types(&self) -> Vec<Exclude> {
344 self.exclude.iter().flatten().copied().collect()
345 }
346
347 fn is_yearly_consumption_based(&self, fuse_size: u16) -> bool {
348 self.cost.is_yearly_consumption_based(fuse_size)
349 }
350}
351
352#[derive(Clone)]
353pub struct CostPeriodBuilder {
354 timezone: Option<Timezone>,
355 cost: Cost,
356 load: Option<LoadType>,
357 include: [Option<Include>; 2],
358 exclude: [Option<Exclude>; 2],
359}
360
361impl Default for CostPeriodBuilder {
362 fn default() -> Self {
363 Self::new()
364 }
365}
366
367impl CostPeriodBuilder {
368 pub const fn new() -> Self {
369 let builder = Self {
370 timezone: None,
371 cost: Cost::None,
372 load: None,
373 include: [None; 2],
374 exclude: [None; 2],
375 };
376 builder.timezone(Timezone::Stockholm)
378 }
379
380 pub const fn build(self) -> CostPeriod {
381 CostPeriod {
382 cost: self.cost,
383 load: self.load.expect("`load` must be specified"),
384 include: self.include,
385 exclude: self.exclude,
386 }
387 }
388
389 pub const fn cost(mut self, cost: Cost) -> Self {
390 self.cost = cost;
391 self
392 }
393
394 pub const fn load(mut self, load: LoadType) -> Self {
395 self.load = Some(load);
396 self
397 }
398
399 pub const fn fixed_cost(mut self, int: i64, fract: u8) -> Self {
400 self.cost = Cost::fixed(int, fract);
401 self
402 }
403
404 pub const fn fixed_cost_subunit(mut self, subunit: f64) -> Self {
405 self.cost = Cost::fixed_subunit(subunit);
406 self
407 }
408
409 pub const fn timezone(mut self, timezone: Timezone) -> Self {
410 self.timezone = Some(timezone);
411 self
412 }
413
414 const fn get_timezone(&self) -> Timezone {
415 self.timezone.expect("`timezone` must be specified")
416 }
417
418 pub const fn include(mut self, period_type: Include) -> Self {
419 let mut i = 0;
420 while i < self.include.len() {
421 if self.include[i].is_some() {
422 i += 1;
423 } else {
424 self.include[i] = Some(period_type);
425 return self;
426 }
427 }
428 panic!("Too many includes");
429 }
430
431 pub const fn months(self, from: Month, to: Month) -> Self {
432 let timezone = self.get_timezone();
433 self.include(Include::Months(Months::new(from, to, timezone)))
434 }
435
436 pub const fn month(self, month: Month) -> Self {
437 self.months(month, month)
438 }
439
440 pub const fn hours(self, from: u8, to_inclusive: u8) -> Self {
441 let timezone = self.get_timezone();
442 self.include(Include::Hours(Hours::new(from, to_inclusive, timezone)))
443 }
444
445 const fn exclude(mut self, period_type: Exclude) -> Self {
446 let mut i = 0;
447 while i < self.exclude.len() {
448 if self.exclude[i].is_some() {
449 i += 1;
450 } else {
451 self.exclude[i] = Some(period_type);
452 return self;
453 }
454 }
455 panic!("Too many excludes");
456 }
457
458 pub const fn exclude_public_holidays(self, country: Country) -> Self {
459 let tz = self.get_timezone();
460 self.exclude(Exclude::PublicHolidays(country, tz))
461 }
462
463 pub const fn exclude_weekends(self) -> Self {
464 let tz = self.get_timezone();
465 self.exclude(Exclude::Weekends(tz))
466 }
467}
468
469#[derive(Debug, Clone, Copy, Serialize)]
470#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
471pub enum Include {
472 Months(Months),
473 Hours(Hours),
474}
475
476impl Include {
477 fn translate(&self, language: Language) -> String {
478 match self {
479 Include::Months(months) => months.translate(language),
480 Include::Hours(hours) => hours.translate(language),
481 }
482 }
483
484 fn matches<Tz: chrono::TimeZone>(&self, timestamp: DateTime<Tz>) -> bool {
485 match self {
486 Include::Months(months) => months.matches(timestamp),
487 Include::Hours(hours) => hours.matches(timestamp),
488 }
489 }
490}
491
492#[derive(Debug, Clone, Copy, Serialize)]
493#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
494pub enum Exclude {
495 Weekends(Timezone),
496 #[serde(rename = "Holidays")]
497 PublicHolidays(Country, Timezone),
498}
499
500impl Exclude {
501 pub(crate) fn translate(&self, language: Language) -> &'static str {
502 match language {
503 Language::En => match self {
504 Exclude::Weekends(_) => "Not weekends",
505 Exclude::PublicHolidays(country, _) => match country {
506 Country::SE => "Not swedish holidays",
507 },
508 },
509 Language::Sv => match self {
510 Exclude::Weekends(_) => "Ej helger",
511 Exclude::PublicHolidays(country, _) => match country {
512 Country::SE => "Ej helgdagar",
513 },
514 },
515 }
516 }
517
518 fn matches<Tz: chrono::TimeZone>(&self, timestamp: DateTime<Tz>) -> bool {
519 let tz_timestamp = timestamp.with_timezone(&self.tz());
520 match self {
521 Exclude::Weekends(_) => (6..=7).contains(&tz_timestamp.weekday().number_from_monday()),
522 Exclude::PublicHolidays(country, _) => {
523 country.is_public_holiday(tz_timestamp.date_naive())
524 }
525 }
526 }
527
528 const fn tz(&self) -> chrono_tz::Tz {
529 match self {
530 Exclude::Weekends(timezone) => timezone.to_tz(),
531 Exclude::PublicHolidays(_, timezone) => timezone.to_tz(),
532 }
533 }
534}
535
536#[cfg(test)]
537mod tests {
538
539 use super::*;
540 use crate::money::Money;
541 use crate::months::Month::*;
542 use crate::{Stockholm, Utc};
543
544 #[test]
545 fn cost_for_none() {
546 const NONE_COST: Cost = Cost::None;
547 assert_eq!(NONE_COST.cost_for(16, 0), None);
548 assert_eq!(NONE_COST.cost_for(25, 5000), None);
549 }
550
551 #[test]
552 fn cost_for_unverified() {
553 const UNVERIFIED_COST: Cost = Cost::Unverified;
554 assert_eq!(UNVERIFIED_COST.cost_for(16, 0), None);
555 assert_eq!(UNVERIFIED_COST.cost_for(25, 5000), None);
556 }
557
558 #[test]
559 fn cost_for_fixed() {
560 const FIXED_COST: Cost = Cost::Fixed(Money::new(100, 50));
561 assert_eq!(FIXED_COST.cost_for(16, 0), Some(Money::new(100, 50)));
563 assert_eq!(FIXED_COST.cost_for(25, 5000), Some(Money::new(100, 50)));
564 assert_eq!(FIXED_COST.cost_for(63, 10000), Some(Money::new(100, 50)));
565 }
566
567 #[test]
568 fn cost_for_fuses_exact_match() {
569 const FUSES_COST: Cost = Cost::fuses(&[
570 (16, Money::new(50, 0)),
571 (25, Money::new(75, 0)),
572 (35, Money::new(100, 0)),
573 (50, Money::new(150, 0)),
574 ]);
575
576 assert_eq!(FUSES_COST.cost_for(16, 0), Some(Money::new(50, 0)));
578 assert_eq!(FUSES_COST.cost_for(25, 0), Some(Money::new(75, 0)));
579 assert_eq!(FUSES_COST.cost_for(35, 0), Some(Money::new(100, 0)));
580 assert_eq!(FUSES_COST.cost_for(50, 0), Some(Money::new(150, 0)));
581
582 assert_eq!(FUSES_COST.cost_for(25, 500000), Some(Money::new(75, 0)));
584 }
585
586 #[test]
587 fn cost_for_fuses_no_match() {
588 const FUSES_COST: Cost = Cost::fuses(&[(16, Money::new(50, 0)), (25, Money::new(75, 0))]);
589
590 assert_eq!(FUSES_COST.cost_for(20, 0), None);
592 assert_eq!(FUSES_COST.cost_for(63, 0), None);
593 }
594
595 #[test]
596 fn cost_for_fuses_yearly_consumption_with_limit() {
597 const FUSES_WITH_CONSUMPTION: Cost = Cost::fuses_with_yearly_consumption(&[
598 (16, Some(5000), Money::new(50, 0)),
599 (16, None, Money::new(75, 0)),
600 (25, Some(10000), Money::new(100, 0)),
601 (25, None, Money::new(125, 0)),
602 ]);
603
604 assert_eq!(
606 FUSES_WITH_CONSUMPTION.cost_for(16, 3000),
607 Some(Money::new(50, 0))
608 );
609
610 assert_eq!(
612 FUSES_WITH_CONSUMPTION.cost_for(16, 5000),
613 Some(Money::new(50, 0))
614 );
615
616 assert_eq!(
618 FUSES_WITH_CONSUMPTION.cost_for(16, 6000),
619 Some(Money::new(75, 0))
620 );
621
622 assert_eq!(
624 FUSES_WITH_CONSUMPTION.cost_for(16, 20000),
625 Some(Money::new(75, 0))
626 );
627
628 assert_eq!(
630 FUSES_WITH_CONSUMPTION.cost_for(25, 10000),
631 Some(Money::new(100, 0))
632 );
633
634 assert_eq!(
636 FUSES_WITH_CONSUMPTION.cost_for(25, 15000),
637 Some(Money::new(125, 0))
638 );
639
640 assert_eq!(
642 FUSES_WITH_CONSUMPTION.cost_for(25, 5000),
643 Some(Money::new(100, 0))
644 );
645 }
646
647 #[test]
648 fn cost_for_fuses_yearly_consumption_no_limit() {
649 const FUSES_NO_LIMIT: Cost = Cost::fuses_with_yearly_consumption(&[
650 (16, None, Money::new(50, 0)),
651 (25, None, Money::new(75, 0)),
652 ]);
653
654 assert_eq!(FUSES_NO_LIMIT.cost_for(16, 0), Some(Money::new(50, 0)));
656 assert_eq!(FUSES_NO_LIMIT.cost_for(16, 1000), Some(Money::new(50, 0)));
657 assert_eq!(FUSES_NO_LIMIT.cost_for(16, 50000), Some(Money::new(50, 0)));
658 assert_eq!(FUSES_NO_LIMIT.cost_for(25, 100000), Some(Money::new(75, 0)));
659 }
660
661 #[test]
662 fn cost_for_fuses_yearly_consumption_no_fuse_match() {
663 const FUSES_WITH_CONSUMPTION: Cost = Cost::fuses_with_yearly_consumption(&[
664 (16, Some(5000), Money::new(50, 0)),
665 (25, Some(10000), Money::new(100, 0)),
666 ]);
667
668 assert_eq!(FUSES_WITH_CONSUMPTION.cost_for(35, 5000), None);
670 assert_eq!(FUSES_WITH_CONSUMPTION.cost_for(50, 10000), None);
671 }
672
673 #[test]
674 fn cost_for_fuses_yearly_consumption_max_limit_no_fallback() {
675 const FUSES_ONLY_LIMITS: Cost = Cost::fuses_with_yearly_consumption(&[
676 (16, Some(5000), Money::new(50, 0)),
677 (25, Some(10000), Money::new(100, 0)),
678 ]);
679
680 assert_eq!(FUSES_ONLY_LIMITS.cost_for(16, 0), Some(Money::new(50, 0)));
682 assert_eq!(
683 FUSES_ONLY_LIMITS.cost_for(16, 3000),
684 Some(Money::new(50, 0))
685 );
686 assert_eq!(
687 FUSES_ONLY_LIMITS.cost_for(16, 4999),
688 Some(Money::new(50, 0))
689 );
690 assert_eq!(
691 FUSES_ONLY_LIMITS.cost_for(16, 5000),
692 Some(Money::new(50, 0))
693 );
694 assert_eq!(
695 FUSES_ONLY_LIMITS.cost_for(25, 9999),
696 Some(Money::new(100, 0))
697 );
698 assert_eq!(
699 FUSES_ONLY_LIMITS.cost_for(25, 10000),
700 Some(Money::new(100, 0))
701 );
702
703 assert_eq!(FUSES_ONLY_LIMITS.cost_for(16, 5001), None);
705 assert_eq!(FUSES_ONLY_LIMITS.cost_for(16, 10000), None);
706 assert_eq!(FUSES_ONLY_LIMITS.cost_for(25, 10001), None);
707 assert_eq!(FUSES_ONLY_LIMITS.cost_for(25, 20000), None);
708 }
709
710 #[test]
711 fn cost_for_fuse_range_within_range() {
712 const FUSE_BASED: Cost = Cost::fuse_range(&[
713 (16, 35, Money::new(54, 0)),
714 (35, u16::MAX, Money::new(108, 50)),
715 ]);
716
717 assert_eq!(FUSE_BASED.cost_for(10, 0), None);
719 assert_eq!(FUSE_BASED.cost_for(15, 0), None);
720
721 assert_eq!(FUSE_BASED.cost_for(16, 0), Some(Money::new(54, 0)));
723 assert_eq!(FUSE_BASED.cost_for(25, 0), Some(Money::new(54, 0)));
724 assert_eq!(FUSE_BASED.cost_for(35, 0), Some(Money::new(54, 0)));
725
726 assert_eq!(FUSE_BASED.cost_for(36, 0), Some(Money::new(108, 50)));
728 assert_eq!(FUSE_BASED.cost_for(50, 0), Some(Money::new(108, 50)));
729 assert_eq!(FUSE_BASED.cost_for(200, 0), Some(Money::new(108, 50)));
730 assert_eq!(FUSE_BASED.cost_for(u16::MAX, 0), Some(Money::new(108, 50)));
731 }
732
733 #[test]
734 fn cost_for_fuse_range_multiple_ranges() {
735 const MULTI_RANGE: Cost = Cost::fuse_range(&[
736 (1, 15, Money::new(20, 0)),
737 (16, 35, Money::new(50, 0)),
738 (36, 63, Money::new(100, 0)),
739 (64, u16::MAX, Money::new(200, 0)),
740 ]);
741
742 assert_eq!(MULTI_RANGE.cost_for(10, 0), Some(Money::new(20, 0)));
744 assert_eq!(MULTI_RANGE.cost_for(25, 0), Some(Money::new(50, 0)));
745 assert_eq!(MULTI_RANGE.cost_for(50, 0), Some(Money::new(100, 0)));
746 assert_eq!(MULTI_RANGE.cost_for(100, 0), Some(Money::new(200, 0)));
747
748 assert_eq!(MULTI_RANGE.cost_for(25, 10000), Some(Money::new(50, 0)));
750 }
751
752 #[test]
753 fn include_matches_hours() {
754 let include = Include::Hours(Hours::new(6, 22, Stockholm));
755 let timestamp_match = Stockholm.dt(2025, 1, 15, 14, 0, 0);
756 let timestamp_no_match = Stockholm.dt(2025, 1, 15, 23, 0, 0);
757
758 assert!(include.matches(timestamp_match));
759 assert!(!include.matches(timestamp_no_match));
760 }
761
762 #[test]
763 fn include_matches_months() {
764 let include = Include::Months(Months::new(November, March, Stockholm));
765 let timestamp_match = Stockholm.dt(2025, 1, 15, 12, 0, 0);
766 let timestamp_no_match = Stockholm.dt(2025, 7, 15, 12, 0, 0);
767
768 assert!(include.matches(timestamp_match));
769 assert!(!include.matches(timestamp_no_match));
770 }
771
772 #[test]
773 fn exclude_matches_weekends_saturday() {
774 let exclude = Exclude::Weekends(Stockholm);
775 let timestamp = Stockholm.dt(2025, 1, 4, 12, 0, 0);
777 assert!(exclude.matches(timestamp));
778 }
779
780 #[test]
781 fn exclude_matches_weekends_sunday() {
782 let exclude = Exclude::Weekends(Stockholm);
783 let timestamp = Stockholm.dt(2025, 1, 5, 12, 0, 0);
785 assert!(exclude.matches(timestamp));
786 }
787
788 #[test]
789 fn exclude_does_not_match_weekday() {
790 let exclude = Exclude::Weekends(Stockholm);
791 let timestamp = Stockholm.dt(2025, 1, 6, 12, 0, 0);
793 assert!(!exclude.matches(timestamp));
794 }
795
796 #[test]
797 fn exclude_matches_swedish_new_year() {
798 let exclude = Exclude::PublicHolidays(Country::SE, Stockholm);
799 let timestamp = Stockholm.dt(2025, 1, 1, 12, 0, 0);
801 assert!(exclude.matches(timestamp));
802 }
803
804 #[test]
805 fn exclude_does_not_match_non_holiday() {
806 let exclude = Exclude::PublicHolidays(Country::SE, Stockholm);
807 let timestamp = Stockholm.dt(2025, 1, 2, 12, 0, 0);
809 assert!(!exclude.matches(timestamp));
810 }
811
812 #[test]
813 fn cost_period_matches_with_single_include() {
814 let period = CostPeriod::builder()
815 .load(LoadType::High)
816 .fixed_cost(10, 0)
817 .hours(6, 22)
818 .build();
819
820 let timestamp_match = Stockholm.dt(2025, 1, 15, 14, 0, 0);
821 let timestamp_no_match = Stockholm.dt(2025, 1, 15, 23, 0, 0);
822
823 assert!(period.matches(timestamp_match));
824 assert!(!period.matches(timestamp_no_match));
825 }
826
827 #[test]
828 fn cost_period_matches_with_multiple_includes() {
829 let period = CostPeriod::builder()
830 .load(LoadType::High)
831 .fixed_cost(10, 0)
832 .hours(6, 22)
833 .months(November, March)
834 .build();
835
836 let timestamp_match = Stockholm.dt(2025, 1, 15, 14, 0, 0);
838 let timestamp_wrong_hours = Stockholm.dt(2025, 1, 15, 23, 0, 0);
840 let timestamp_wrong_months = Stockholm.dt(2025, 7, 15, 14, 0, 0);
842
843 assert!(period.matches(timestamp_match));
844 assert!(!period.matches(timestamp_wrong_hours));
845 assert!(!period.matches(timestamp_wrong_months));
846 }
847
848 #[test]
849 fn cost_period_matches_with_exclude_weekends() {
850 let period = CostPeriod::builder()
851 .load(LoadType::High)
852 .fixed_cost(10, 0)
853 .hours(6, 22)
854 .exclude_weekends()
855 .build();
856
857 println!("Excludes: {:?}", period.exclude_period_types());
858 println!("Includes: {:?}", period.include_period_types());
859
860 let timestamp_weekday = Stockholm.dt(2025, 1, 6, 14, 0, 0);
862 let timestamp_saturday = Stockholm.dt(2025, 1, 4, 14, 0, 0);
864
865 assert!(period.matches(timestamp_weekday));
866 assert!(!period.matches(timestamp_saturday));
867 }
868
869 #[test]
870 fn cost_period_matches_with_exclude_holidays() {
871 let period = CostPeriod::builder()
872 .load(LoadType::High)
873 .fixed_cost(10, 0)
874 .hours(6, 22)
875 .exclude_public_holidays(Country::SE)
876 .build();
877
878 let timestamp_regular = Stockholm.dt(2025, 1, 2, 14, 0, 0);
880 let timestamp_holiday = Stockholm.dt(2025, 1, 1, 14, 0, 0);
882
883 assert!(period.matches(timestamp_regular));
884 assert!(!period.matches(timestamp_holiday));
885 }
886
887 #[test]
888 fn cost_period_matches_complex_scenario() {
889 let period = CostPeriod::builder()
891 .load(LoadType::High)
892 .fixed_cost(10, 0)
893 .months(November, March)
894 .hours(6, 22)
895 .exclude_weekends()
896 .exclude_public_holidays(Country::SE)
897 .build();
898
899 let timestamp_match = Stockholm.dt(2025, 1, 15, 14, 0, 0);
901
902 let timestamp_wrong_hours = Stockholm.dt(2025, 1, 15, 23, 0, 0);
904
905 let timestamp_weekend = Stockholm.dt(2025, 1, 4, 14, 0, 0);
907
908 let timestamp_holiday = Stockholm.dt(2025, 1, 1, 14, 0, 0);
910
911 let timestamp_summer = Stockholm.dt(2025, 7, 15, 14, 0, 0);
913
914 assert!(period.matches(timestamp_match));
915 assert!(!period.matches(timestamp_wrong_hours));
916 assert!(!period.matches(timestamp_weekend));
917 assert!(!period.matches(timestamp_holiday));
918 assert!(!period.matches(timestamp_summer));
919 }
920
921 #[test]
922 fn cost_period_matches_base_load() {
923 let period = CostPeriod::builder()
925 .load(LoadType::Base)
926 .fixed_cost(5, 0)
927 .build();
928
929 let timestamp1 = Stockholm.dt(2025, 1, 1, 0, 0, 0);
931 let timestamp2 = Stockholm.dt(2025, 7, 15, 23, 59, 59);
932 let timestamp3 = Stockholm.dt(2025, 1, 4, 12, 0, 0);
933
934 assert!(period.matches(timestamp1));
935 assert!(period.matches(timestamp2));
936 assert!(period.matches(timestamp3));
937 }
938
939 #[test]
940 fn include_matches_hours_wraparound() {
941 let include = Include::Hours(Hours::new(22, 5, Stockholm));
943
944 let timestamp_evening = Stockholm.dt(2025, 1, 15, 22, 0, 0);
946 assert!(include.matches(timestamp_evening));
947
948 let timestamp_midnight = Stockholm.dt(2025, 1, 15, 0, 0, 0);
950 assert!(include.matches(timestamp_midnight));
951
952 let timestamp_morning = Stockholm.dt(2025, 1, 15, 5, 30, 0);
954 assert!(include.matches(timestamp_morning));
955
956 let timestamp_day = Stockholm.dt(2025, 1, 15, 14, 0, 0);
958 assert!(!include.matches(timestamp_day));
959
960 let timestamp_after = Stockholm.dt(2025, 1, 15, 6, 0, 0);
962 assert!(!include.matches(timestamp_after));
963
964 let timestamp_before = Stockholm.dt(2025, 1, 15, 21, 59, 59);
966 assert!(!include.matches(timestamp_before));
967 }
968
969 #[test]
970 fn include_matches_months_wraparound() {
971 let include = Include::Months(Months::new(November, March, Stockholm));
973
974 let timestamp_nov = Stockholm.dt(2025, 11, 15, 12, 0, 0);
976 assert!(include.matches(timestamp_nov));
977
978 let timestamp_dec = Stockholm.dt(2025, 12, 15, 12, 0, 0);
980 assert!(include.matches(timestamp_dec));
981
982 let timestamp_jan = Stockholm.dt(2025, 1, 15, 12, 0, 0);
984 assert!(include.matches(timestamp_jan));
985
986 let timestamp_mar = Stockholm.dt(2025, 3, 15, 12, 0, 0);
988 assert!(include.matches(timestamp_mar));
989
990 let timestamp_jul = Stockholm.dt(2025, 7, 15, 12, 0, 0);
992 assert!(!include.matches(timestamp_jul));
993
994 let timestamp_oct = Stockholm.dt(2025, 10, 31, 23, 59, 59);
996 assert!(!include.matches(timestamp_oct));
997
998 let timestamp_apr = Stockholm.dt(2025, 4, 1, 0, 0, 0);
1000 assert!(!include.matches(timestamp_apr));
1001 }
1002
1003 #[test]
1004 fn cost_period_matches_hours_wraparound() {
1005 let period = CostPeriod::builder()
1007 .load(LoadType::Low)
1008 .fixed_cost(5, 0)
1009 .hours(22, 5)
1010 .build();
1011
1012 let timestamp_match_evening = Stockholm.dt(2025, 1, 15, 23, 0, 0);
1013 let timestamp_match_morning = Stockholm.dt(2025, 1, 15, 3, 0, 0);
1014 let timestamp_no_match = Stockholm.dt(2025, 1, 15, 14, 0, 0);
1015
1016 assert!(period.matches(timestamp_match_evening));
1017 assert!(period.matches(timestamp_match_morning));
1018 assert!(!period.matches(timestamp_no_match));
1019 }
1020
1021 #[test]
1022 fn cost_period_matches_with_both_excludes() {
1023 let period = CostPeriod::builder()
1024 .load(LoadType::High)
1025 .fixed_cost(10, 0)
1026 .hours(6, 22)
1027 .exclude_weekends()
1028 .exclude_public_holidays(Country::SE)
1029 .build();
1030
1031 let weekday = Stockholm.dt(2025, 1, 2, 14, 0, 0);
1033 assert!(period.matches(weekday));
1034
1035 let saturday = Stockholm.dt(2025, 1, 4, 14, 0, 0);
1037 assert!(!period.matches(saturday));
1038
1039 let holiday = Stockholm.dt(2025, 1, 1, 14, 0, 0);
1041 assert!(!period.matches(holiday));
1042
1043 let wrong_hours = Stockholm.dt(2025, 1, 2, 23, 0, 0);
1045 assert!(!period.matches(wrong_hours));
1046 }
1047
1048 #[test]
1049 fn exclude_matches_friday_is_not_weekend() {
1050 let exclude = Exclude::Weekends(Stockholm);
1051 let friday = Stockholm.dt(2025, 1, 3, 12, 0, 0);
1053 assert!(!exclude.matches(friday));
1054 }
1055
1056 #[test]
1057 fn exclude_matches_monday_is_not_weekend() {
1058 let exclude = Exclude::Weekends(Stockholm);
1059 let monday = Stockholm.dt(2025, 1, 6, 12, 0, 0);
1061 assert!(!exclude.matches(monday));
1062 }
1063
1064 #[test]
1065 fn exclude_matches_holiday_midsummer() {
1066 let exclude = Exclude::PublicHolidays(Country::SE, Stockholm);
1067 let midsummer = Stockholm.dt(2025, 6, 21, 12, 0, 0);
1069 assert!(exclude.matches(midsummer));
1070 }
1071
1072 #[test]
1073 fn cost_period_matches_month_and_hours() {
1074 let period = CostPeriod::builder()
1076 .load(LoadType::Low)
1077 .fixed_cost(5, 0)
1078 .month(June)
1079 .hours(22, 5)
1080 .build();
1081
1082 let match_june_night = Stockholm.dt(2025, 6, 15, 23, 0, 0);
1084 assert!(period.matches(match_june_night));
1085
1086 let june_day = Stockholm.dt(2025, 6, 15, 14, 0, 0);
1088 assert!(!period.matches(june_day));
1089
1090 let july_night = Stockholm.dt(2025, 7, 15, 23, 0, 0);
1092 assert!(!period.matches(july_night));
1093 }
1094
1095 #[test]
1096 fn cost_period_matches_months_and_hours_with_exclude() {
1097 let period = CostPeriod::builder()
1099 .load(LoadType::High)
1100 .fixed_cost(15, 0)
1101 .months(November, March)
1102 .hours(6, 22)
1103 .exclude_weekends()
1104 .exclude_public_holidays(Country::SE)
1105 .build();
1106
1107 let perfect = Stockholm.dt(2025, 1, 15, 10, 0, 0);
1109 assert!(period.matches(perfect));
1110
1111 let first_hour = Stockholm.dt(2025, 1, 15, 6, 0, 0);
1113 assert!(period.matches(first_hour));
1114
1115 let last_hour = Stockholm.dt(2025, 1, 15, 22, 59, 59);
1117 assert!(period.matches(last_hour));
1118
1119 let too_early = Stockholm.dt(2025, 1, 15, 5, 59, 59);
1121 assert!(!period.matches(too_early));
1122
1123 let too_late = Stockholm.dt(2025, 1, 15, 23, 0, 0);
1125 assert!(!period.matches(too_late));
1126
1127 let summer = Stockholm.dt(2025, 7, 15, 10, 0, 0);
1129 assert!(!period.matches(summer));
1130
1131 let weekend = Stockholm.dt(2025, 1, 4, 10, 0, 0);
1133 assert!(!period.matches(weekend));
1134 }
1135
1136 #[test]
1137 fn cost_period_matches_base_with_restrictions() {
1138 let period = CostPeriod::builder()
1140 .load(LoadType::Base)
1141 .fixed_cost(3, 0)
1142 .hours(0, 5)
1143 .build();
1144
1145 let match_night = Stockholm.dt(2025, 1, 15, 3, 0, 0);
1147 assert!(period.matches(match_night));
1148
1149 let no_match_day = Stockholm.dt(2025, 1, 15, 14, 0, 0);
1151 assert!(!period.matches(no_match_day));
1152 }
1153
1154 #[test]
1155 fn cost_period_matches_single_month() {
1156 let period = CostPeriod::builder()
1157 .load(LoadType::High)
1158 .fixed_cost(10, 0)
1159 .month(December)
1160 .build();
1161
1162 let dec_first = Stockholm.dt(2025, 12, 1, 0, 0, 0);
1164 assert!(period.matches(dec_first));
1165
1166 let dec_last = Stockholm.dt(2025, 12, 31, 23, 59, 59);
1168 assert!(period.matches(dec_last));
1169
1170 let nov = Stockholm.dt(2025, 11, 30, 12, 0, 0);
1172 assert!(!period.matches(nov));
1173
1174 let jan = Stockholm.dt(2025, 1, 1, 12, 0, 0);
1176 assert!(!period.matches(jan));
1177 }
1178
1179 #[test]
1180 fn cost_period_matches_all_hours() {
1181 let period = CostPeriod::builder()
1183 .load(LoadType::Low)
1184 .fixed_cost(5, 0)
1185 .hours(0, 23)
1186 .build();
1187
1188 let midnight = Stockholm.dt(2025, 1, 15, 0, 0, 0);
1189 let noon = Stockholm.dt(2025, 1, 15, 12, 0, 0);
1190 let almost_midnight = Stockholm.dt(2025, 1, 15, 23, 59, 59);
1191
1192 assert!(period.matches(midnight));
1193 assert!(period.matches(noon));
1194 assert!(period.matches(almost_midnight));
1195 }
1196
1197 #[test]
1198 fn cost_period_matches_edge_of_month_range() {
1199 let period = CostPeriod::builder()
1201 .load(LoadType::Low)
1202 .fixed_cost(5, 0)
1203 .months(May, September)
1204 .build();
1205
1206 let may_start = Stockholm.dt(2025, 5, 1, 0, 0, 0);
1208 assert!(period.matches(may_start));
1209
1210 let april_end = Stockholm.dt(2025, 4, 30, 23, 59, 59);
1212 assert!(!period.matches(april_end));
1213
1214 let sept_end = Stockholm.dt(2025, 9, 30, 23, 59, 59);
1216 assert!(period.matches(sept_end));
1217
1218 let oct_start = Stockholm.dt(2025, 10, 1, 0, 0, 0);
1220 assert!(!period.matches(oct_start));
1221 }
1222
1223 #[test]
1224 fn include_matches_month_boundary() {
1225 let include = Include::Months(Months::new(February, February, Stockholm));
1227
1228 let feb_start = Stockholm.dt(2025, 2, 1, 0, 0, 0);
1230 assert!(include.matches(feb_start));
1231
1232 let feb_end = Stockholm.dt(2025, 2, 28, 23, 59, 59);
1234 assert!(include.matches(feb_end));
1235
1236 let jan_end = Stockholm.dt(2025, 1, 31, 23, 59, 59);
1238 assert!(!include.matches(jan_end));
1239
1240 let mar_start = Stockholm.dt(2025, 3, 1, 0, 0, 0);
1242 assert!(!include.matches(mar_start));
1243 }
1244
1245 #[test]
1246 fn include_matches_hours_exact_boundaries() {
1247 let include = Include::Hours(Hours::new(6, 22, Stockholm));
1248
1249 let start = Stockholm.dt(2025, 1, 15, 6, 0, 0);
1251 assert!(include.matches(start));
1252
1253 let end = Stockholm.dt(2025, 1, 15, 22, 59, 59);
1255 assert!(include.matches(end));
1256
1257 let before = Stockholm.dt(2025, 1, 15, 5, 59, 59);
1259 assert!(!include.matches(before));
1260
1261 let after = Stockholm.dt(2025, 1, 15, 23, 0, 0);
1263 assert!(!include.matches(after));
1264 }
1265
1266 #[test]
1267 fn exclude_matches_weekends_with_utc_timestamps() {
1268 let exclude = Exclude::Weekends(Stockholm);
1269
1270 let saturday_utc = Utc.dt(2025, 1, 4, 11, 0, 0);
1273 assert!(exclude.matches(saturday_utc));
1274
1275 let sunday_utc = Utc.dt(2025, 1, 5, 11, 0, 0);
1278 assert!(exclude.matches(sunday_utc));
1279
1280 let monday_utc = Utc.dt(2025, 1, 6, 11, 0, 0);
1283 assert!(!exclude.matches(monday_utc));
1284 }
1285
1286 #[test]
1287 fn exclude_matches_weekends_timezone_boundary() {
1288 let exclude = Exclude::Weekends(Stockholm);
1289
1290 let friday_utc_saturday_stockholm = Utc.dt(2025, 1, 3, 23, 0, 0);
1294 assert!(
1295 exclude.matches(friday_utc_saturday_stockholm),
1296 "Should match because it's Saturday in Stockholm timezone"
1297 );
1298
1299 let sunday_utc_monday_stockholm = Utc.dt(2025, 1, 5, 23, 0, 0);
1303 assert!(
1304 !exclude.matches(sunday_utc_monday_stockholm),
1305 "Should not match because it's Monday in Stockholm timezone"
1306 );
1307
1308 let sunday_late_utc = Utc.dt(2025, 1, 5, 22, 59, 0);
1311 assert!(
1312 exclude.matches(sunday_late_utc),
1313 "Should match because it's still Sunday in Stockholm timezone"
1314 );
1315 }
1316
1317 #[test]
1318 fn exclude_matches_holidays_with_utc_timestamps() {
1319 let exclude = Exclude::PublicHolidays(Country::SE, Stockholm);
1320
1321 let new_year_utc = Utc.dt(2025, 1, 1, 11, 0, 0);
1324 assert!(exclude.matches(new_year_utc));
1325
1326 let regular_day_utc = Utc.dt(2025, 1, 2, 11, 0, 0);
1329 assert!(!exclude.matches(regular_day_utc));
1330 }
1331
1332 #[test]
1333 fn exclude_matches_holidays_timezone_boundary() {
1334 let exclude = Exclude::PublicHolidays(Country::SE, Stockholm);
1335
1336 let dec31_utc_jan1_stockholm = Utc.dt(2024, 12, 31, 23, 0, 0);
1340 assert!(
1341 exclude.matches(dec31_utc_jan1_stockholm),
1342 "Should match because it's New Year's Day in Stockholm timezone"
1343 );
1344
1345 let jan1_utc_jan2_stockholm = Utc.dt(2025, 1, 1, 23, 0, 0);
1349 assert!(
1350 !exclude.matches(jan1_utc_jan2_stockholm),
1351 "Should not match because it's January 2 in Stockholm timezone"
1352 );
1353 }
1354
1355 #[test]
1356 fn exclude_matches_weekends_summer_timezone() {
1357 let exclude = Exclude::Weekends(Stockholm);
1358
1359 let saturday_summer_utc = Utc.dt(2025, 6, 7, 10, 0, 0);
1362 assert!(exclude.matches(saturday_summer_utc));
1363
1364 let friday_utc_saturday_stockholm_summer = Utc.dt(2025, 6, 6, 22, 0, 0);
1367 assert!(
1368 exclude.matches(friday_utc_saturday_stockholm_summer),
1369 "Should match because it's Saturday in Stockholm timezone (CEST)"
1370 );
1371 }
1372}