use chrono::{Datelike, NaiveDate};
use serde::{Deserialize, Serialize};
use crate::models::IndustrySector;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SeasonalEvent {
pub name: String,
pub start_month: u8,
pub start_day: u8,
pub end_month: u8,
pub end_day: u8,
pub multiplier: f64,
pub priority: u8,
}
impl SeasonalEvent {
pub fn new(
name: impl Into<String>,
start_month: u8,
start_day: u8,
end_month: u8,
end_day: u8,
multiplier: f64,
) -> Self {
Self {
name: name.into(),
start_month,
start_day,
end_month,
end_day,
multiplier,
priority: 0,
}
}
pub fn with_priority(mut self, priority: u8) -> Self {
self.priority = priority;
self
}
pub fn is_active(&self, date: NaiveDate) -> bool {
let month = date.month() as u8;
let day = date.day() as u8;
if self.start_month > self.end_month {
if month > self.start_month || month < self.end_month {
return true;
}
if month == self.start_month && day >= self.start_day {
return true;
}
if month == self.end_month && day <= self.end_day {
return true;
}
return false;
}
if month < self.start_month || month > self.end_month {
return false;
}
if month == self.start_month && day < self.start_day {
return false;
}
if month == self.end_month && day > self.end_day {
return false;
}
true
}
}
#[derive(Debug, Clone)]
pub struct IndustrySeasonality {
pub industry: IndustrySector,
pub events: Vec<SeasonalEvent>,
}
impl IndustrySeasonality {
pub fn new(industry: IndustrySector) -> Self {
Self {
industry,
events: Vec::new(),
}
}
pub fn for_industry(industry: IndustrySector) -> Self {
match industry {
IndustrySector::Retail => Self::retail(),
IndustrySector::Manufacturing => Self::manufacturing(),
IndustrySector::FinancialServices => Self::financial_services(),
IndustrySector::Healthcare => Self::healthcare(),
IndustrySector::Technology => Self::technology(),
IndustrySector::ProfessionalServices => Self::professional_services(),
IndustrySector::Energy => Self::energy(),
IndustrySector::Transportation => Self::transportation(),
IndustrySector::RealEstate => Self::real_estate(),
IndustrySector::Telecommunications => Self::telecommunications(),
}
}
pub fn get_multiplier(&self, date: NaiveDate) -> f64 {
let active_events: Vec<&SeasonalEvent> =
self.events.iter().filter(|e| e.is_active(date)).collect();
if active_events.is_empty() {
return 1.0;
}
active_events
.into_iter()
.max_by_key(|e| e.priority)
.map(|e| e.multiplier)
.unwrap_or(1.0)
}
pub fn add_event(&mut self, event: SeasonalEvent) {
self.events.push(event);
}
fn retail() -> Self {
let mut s = Self::new(IndustrySector::Retail);
s.add_event(
SeasonalEvent::new("Black Friday/Cyber Monday", 11, 20, 11, 30, 8.0).with_priority(10),
);
s.add_event(SeasonalEvent::new("Christmas Rush", 12, 15, 12, 24, 6.0).with_priority(9));
s.add_event(SeasonalEvent::new("Post-Holiday Returns", 1, 1, 1, 15, 3.0).with_priority(7));
s.add_event(SeasonalEvent::new("Back-to-School", 8, 1, 8, 31, 2.0).with_priority(5));
s.add_event(SeasonalEvent::new("Valentine's Day", 2, 7, 2, 14, 1.8).with_priority(4));
s.add_event(SeasonalEvent::new("Easter Season", 3, 20, 4, 15, 1.5).with_priority(3));
s.add_event(SeasonalEvent::new("Summer Slowdown", 6, 1, 7, 31, 0.7).with_priority(2));
s
}
fn manufacturing() -> Self {
let mut s = Self::new(IndustrySector::Manufacturing);
s.add_event(SeasonalEvent::new("Year-End Close", 12, 20, 12, 31, 4.0).with_priority(10));
s.add_event(
SeasonalEvent::new("Q4 Inventory Buildup", 10, 1, 11, 30, 2.0).with_priority(6),
);
s.add_event(SeasonalEvent::new("Model Year Transition", 9, 1, 9, 30, 1.5).with_priority(5));
s.add_event(
SeasonalEvent::new("Spring Production Ramp", 3, 1, 4, 30, 1.3).with_priority(3),
);
s.add_event(SeasonalEvent::new("Summer Shutdown", 7, 1, 7, 31, 0.6).with_priority(4));
s.add_event(SeasonalEvent::new("Holiday Shutdown", 12, 24, 12, 26, 0.2).with_priority(11));
s
}
fn financial_services() -> Self {
let mut s = Self::new(IndustrySector::FinancialServices);
s.add_event(SeasonalEvent::new("Year-End", 12, 15, 12, 31, 8.0).with_priority(10));
s.add_event(SeasonalEvent::new("Q1 Close", 3, 26, 3, 31, 5.0).with_priority(9));
s.add_event(SeasonalEvent::new("Q2 Close", 6, 25, 6, 30, 5.0).with_priority(9));
s.add_event(SeasonalEvent::new("Q3 Close", 9, 25, 9, 30, 5.0).with_priority(9));
s.add_event(SeasonalEvent::new("Tax Deadline", 4, 10, 4, 20, 3.0).with_priority(7));
s.add_event(SeasonalEvent::new("Audit Season", 1, 15, 2, 28, 2.5).with_priority(6));
s.add_event(SeasonalEvent::new("Regulatory Filing", 2, 1, 2, 28, 2.0).with_priority(5));
s
}
fn healthcare() -> Self {
let mut s = Self::new(IndustrySector::Healthcare);
s.add_event(SeasonalEvent::new("Year-End", 12, 15, 12, 31, 3.0).with_priority(10));
s.add_event(SeasonalEvent::new("Insurance Enrollment", 1, 1, 1, 31, 2.0).with_priority(8));
s.add_event(SeasonalEvent::new("Flu Season", 10, 1, 10, 31, 1.5).with_priority(4));
s.add_event(SeasonalEvent::new("Flu Season Extended", 11, 1, 2, 28, 1.5).with_priority(4));
s.add_event(SeasonalEvent::new("Open Enrollment", 11, 1, 11, 30, 1.8).with_priority(6));
s.add_event(SeasonalEvent::new("Summer Slowdown", 6, 15, 8, 15, 0.8).with_priority(3));
s
}
fn technology() -> Self {
let mut s = Self::new(IndustrySector::Technology);
s.add_event(
SeasonalEvent::new("Q4 Enterprise Deals", 12, 1, 12, 31, 4.0).with_priority(10),
);
s.add_event(SeasonalEvent::new("Holiday Sales", 11, 15, 11, 30, 2.0).with_priority(8));
s.add_event(SeasonalEvent::new("Back-to-School", 8, 1, 9, 15, 1.5).with_priority(5));
s.add_event(SeasonalEvent::new("Spring Launches", 3, 1, 3, 31, 1.8).with_priority(6));
s.add_event(SeasonalEvent::new("Fall Launches", 9, 1, 9, 30, 1.8).with_priority(6));
s.add_event(SeasonalEvent::new("Summer Slowdown", 7, 1, 8, 15, 0.7).with_priority(3));
s
}
fn professional_services() -> Self {
let mut s = Self::new(IndustrySector::ProfessionalServices);
s.add_event(SeasonalEvent::new("Year-End", 12, 10, 12, 31, 3.0).with_priority(10));
s.add_event(SeasonalEvent::new("Tax Season", 2, 1, 4, 15, 2.5).with_priority(8));
s.add_event(SeasonalEvent::new("Budget Season", 10, 1, 11, 30, 1.8).with_priority(6));
s.add_event(SeasonalEvent::new("Summer Slowdown", 7, 1, 8, 31, 0.75).with_priority(3));
s.add_event(SeasonalEvent::new("Holiday Period", 12, 23, 12, 26, 0.3).with_priority(11));
s
}
fn energy() -> Self {
let mut s = Self::new(IndustrySector::Energy);
s.add_event(
SeasonalEvent::new("Winter Heating Season", 11, 1, 2, 28, 1.8).with_priority(6),
);
s.add_event(SeasonalEvent::new("Summer Cooling Season", 6, 1, 8, 31, 1.5).with_priority(5));
s.add_event(SeasonalEvent::new("Year-End", 12, 15, 12, 31, 3.0).with_priority(10));
s.add_event(SeasonalEvent::new("Spring Shoulder", 3, 15, 5, 15, 0.8).with_priority(3));
s.add_event(SeasonalEvent::new("Fall Shoulder", 9, 15, 10, 15, 0.8).with_priority(3));
s
}
fn transportation() -> Self {
let mut s = Self::new(IndustrySector::Transportation);
s.add_event(SeasonalEvent::new("Holiday Shipping", 11, 15, 12, 24, 4.0).with_priority(10));
s.add_event(SeasonalEvent::new("Back-to-School", 8, 1, 8, 31, 1.5).with_priority(5));
s.add_event(SeasonalEvent::new("Summer Travel", 6, 15, 8, 15, 1.3).with_priority(4));
s.add_event(SeasonalEvent::new("January Slowdown", 1, 5, 1, 31, 0.7).with_priority(3));
s
}
fn real_estate() -> Self {
let mut s = Self::new(IndustrySector::RealEstate);
s.add_event(SeasonalEvent::new("Spring Buying Season", 3, 1, 6, 30, 2.0).with_priority(6));
s.add_event(SeasonalEvent::new("Year-End Closings", 12, 1, 12, 31, 2.5).with_priority(8));
s.add_event(SeasonalEvent::new("Summer Moving", 6, 1, 8, 31, 1.8).with_priority(5));
s.add_event(SeasonalEvent::new("Winter Slowdown", 1, 1, 2, 28, 0.6).with_priority(3));
s
}
fn telecommunications() -> Self {
let mut s = Self::new(IndustrySector::Telecommunications);
s.add_event(
SeasonalEvent::new("Holiday Activations", 11, 15, 12, 31, 2.0).with_priority(8),
);
s.add_event(SeasonalEvent::new("Back-to-School", 8, 1, 9, 15, 1.5).with_priority(5));
s.add_event(SeasonalEvent::new("Year-End Billing", 12, 15, 12, 31, 1.8).with_priority(7));
s.add_event(SeasonalEvent::new("Q1 Slowdown", 1, 15, 2, 28, 0.8).with_priority(3));
s
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomSeasonalEventConfig {
pub name: String,
pub start_month: u8,
pub start_day: u8,
pub end_month: u8,
pub end_day: u8,
pub multiplier: f64,
#[serde(default = "default_priority")]
pub priority: u8,
}
fn default_priority() -> u8 {
5
}
impl From<CustomSeasonalEventConfig> for SeasonalEvent {
fn from(config: CustomSeasonalEventConfig) -> Self {
SeasonalEvent::new(
config.name,
config.start_month,
config.start_day,
config.end_month,
config.end_day,
config.multiplier,
)
.with_priority(config.priority)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_seasonal_event_active() {
let event = SeasonalEvent::new("Test Event", 11, 20, 11, 30, 2.0);
assert!(event.is_active(NaiveDate::from_ymd_opt(2024, 11, 20).unwrap()));
assert!(event.is_active(NaiveDate::from_ymd_opt(2024, 11, 25).unwrap()));
assert!(event.is_active(NaiveDate::from_ymd_opt(2024, 11, 30).unwrap()));
assert!(!event.is_active(NaiveDate::from_ymd_opt(2024, 11, 19).unwrap()));
assert!(!event.is_active(NaiveDate::from_ymd_opt(2024, 12, 1).unwrap()));
assert!(!event.is_active(NaiveDate::from_ymd_opt(2024, 10, 25).unwrap()));
}
#[test]
fn test_year_spanning_event() {
let event = SeasonalEvent::new("Holiday Period", 12, 20, 1, 5, 0.3);
assert!(event.is_active(NaiveDate::from_ymd_opt(2024, 12, 20).unwrap()));
assert!(event.is_active(NaiveDate::from_ymd_opt(2024, 12, 31).unwrap()));
assert!(event.is_active(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()));
assert!(event.is_active(NaiveDate::from_ymd_opt(2024, 1, 5).unwrap()));
assert!(!event.is_active(NaiveDate::from_ymd_opt(2024, 12, 19).unwrap()));
assert!(!event.is_active(NaiveDate::from_ymd_opt(2024, 1, 6).unwrap()));
assert!(!event.is_active(NaiveDate::from_ymd_opt(2024, 6, 15).unwrap()));
}
#[test]
fn test_retail_seasonality() {
let seasonality = IndustrySeasonality::for_industry(IndustrySector::Retail);
let black_friday = NaiveDate::from_ymd_opt(2024, 11, 25).unwrap();
assert!((seasonality.get_multiplier(black_friday) - 8.0).abs() < 0.01);
let regular_day = NaiveDate::from_ymd_opt(2024, 5, 15).unwrap();
assert!((seasonality.get_multiplier(regular_day) - 1.0).abs() < 0.01);
let summer = NaiveDate::from_ymd_opt(2024, 7, 15).unwrap();
assert!((seasonality.get_multiplier(summer) - 0.7).abs() < 0.01);
}
#[test]
fn test_financial_services_seasonality() {
let seasonality = IndustrySeasonality::for_industry(IndustrySector::FinancialServices);
let year_end = NaiveDate::from_ymd_opt(2024, 12, 20).unwrap();
assert!((seasonality.get_multiplier(year_end) - 8.0).abs() < 0.01);
let q1_end = NaiveDate::from_ymd_opt(2024, 3, 28).unwrap();
assert!((seasonality.get_multiplier(q1_end) - 5.0).abs() < 0.01);
}
#[test]
fn test_priority_handling() {
let mut s = IndustrySeasonality::new(IndustrySector::Retail);
s.add_event(SeasonalEvent::new("Low Priority", 12, 1, 12, 31, 2.0).with_priority(1));
s.add_event(SeasonalEvent::new("High Priority", 12, 15, 12, 25, 5.0).with_priority(10));
let overlap = NaiveDate::from_ymd_opt(2024, 12, 20).unwrap();
assert!((s.get_multiplier(overlap) - 5.0).abs() < 0.01);
let low_only = NaiveDate::from_ymd_opt(2024, 12, 5).unwrap();
assert!((s.get_multiplier(low_only) - 2.0).abs() < 0.01);
}
#[test]
fn test_all_industries_have_events() {
let industries = [
IndustrySector::Retail,
IndustrySector::Manufacturing,
IndustrySector::FinancialServices,
IndustrySector::Healthcare,
IndustrySector::Technology,
IndustrySector::ProfessionalServices,
IndustrySector::Energy,
IndustrySector::Transportation,
IndustrySector::RealEstate,
IndustrySector::Telecommunications,
];
for industry in industries {
let s = IndustrySeasonality::for_industry(industry);
assert!(
!s.events.is_empty(),
"Industry {:?} should have seasonal events",
industry
);
}
}
}