1use chrono::{Datelike, NaiveDate};
7use serde::{Deserialize, Serialize};
8
9use crate::models::IndustrySector;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct SeasonalEvent {
14 pub name: String,
16 pub start_month: u8,
18 pub start_day: u8,
20 pub end_month: u8,
22 pub end_day: u8,
24 pub multiplier: f64,
26 pub priority: u8,
28}
29
30impl SeasonalEvent {
31 pub fn new(
33 name: impl Into<String>,
34 start_month: u8,
35 start_day: u8,
36 end_month: u8,
37 end_day: u8,
38 multiplier: f64,
39 ) -> Self {
40 Self {
41 name: name.into(),
42 start_month,
43 start_day,
44 end_month,
45 end_day,
46 multiplier,
47 priority: 0,
48 }
49 }
50
51 pub fn with_priority(mut self, priority: u8) -> Self {
53 self.priority = priority;
54 self
55 }
56
57 pub fn is_active(&self, date: NaiveDate) -> bool {
59 let month = date.month() as u8;
60 let day = date.day() as u8;
61
62 if self.start_month > self.end_month {
64 if month > self.start_month || month < self.end_month {
66 return true;
67 }
68 if month == self.start_month && day >= self.start_day {
69 return true;
70 }
71 if month == self.end_month && day <= self.end_day {
72 return true;
73 }
74 return false;
75 }
76
77 if month < self.start_month || month > self.end_month {
79 return false;
80 }
81
82 if month == self.start_month && day < self.start_day {
83 return false;
84 }
85
86 if month == self.end_month && day > self.end_day {
87 return false;
88 }
89
90 true
91 }
92}
93
94#[derive(Debug, Clone)]
96pub struct IndustrySeasonality {
97 pub industry: IndustrySector,
99 pub events: Vec<SeasonalEvent>,
101}
102
103impl IndustrySeasonality {
104 pub fn new(industry: IndustrySector) -> Self {
106 Self {
107 industry,
108 events: Vec::new(),
109 }
110 }
111
112 pub fn for_industry(industry: IndustrySector) -> Self {
114 match industry {
115 IndustrySector::Retail => Self::retail(),
116 IndustrySector::Manufacturing => Self::manufacturing(),
117 IndustrySector::FinancialServices => Self::financial_services(),
118 IndustrySector::Healthcare => Self::healthcare(),
119 IndustrySector::Technology => Self::technology(),
120 IndustrySector::ProfessionalServices => Self::professional_services(),
121 IndustrySector::Energy => Self::energy(),
122 IndustrySector::Transportation => Self::transportation(),
123 IndustrySector::RealEstate => Self::real_estate(),
124 IndustrySector::Telecommunications => Self::telecommunications(),
125 }
126 }
127
128 pub fn get_multiplier(&self, date: NaiveDate) -> f64 {
133 let active_events: Vec<&SeasonalEvent> =
134 self.events.iter().filter(|e| e.is_active(date)).collect();
135
136 if active_events.is_empty() {
137 return 1.0;
138 }
139
140 active_events
142 .into_iter()
143 .max_by_key(|e| e.priority)
144 .map(|e| e.multiplier)
145 .unwrap_or(1.0)
146 }
147
148 pub fn add_event(&mut self, event: SeasonalEvent) {
150 self.events.push(event);
151 }
152
153 fn retail() -> Self {
155 let mut s = Self::new(IndustrySector::Retail);
156
157 s.add_event(
159 SeasonalEvent::new("Black Friday/Cyber Monday", 11, 20, 11, 30, 8.0).with_priority(10),
160 );
161
162 s.add_event(SeasonalEvent::new("Christmas Rush", 12, 15, 12, 24, 6.0).with_priority(9));
164
165 s.add_event(SeasonalEvent::new("Post-Holiday Returns", 1, 1, 1, 15, 3.0).with_priority(7));
167
168 s.add_event(SeasonalEvent::new("Back-to-School", 8, 1, 8, 31, 2.0).with_priority(5));
170
171 s.add_event(SeasonalEvent::new("Valentine's Day", 2, 7, 2, 14, 1.8).with_priority(4));
173
174 s.add_event(SeasonalEvent::new("Easter Season", 3, 20, 4, 15, 1.5).with_priority(3));
176
177 s.add_event(SeasonalEvent::new("Summer Slowdown", 6, 1, 7, 31, 0.7).with_priority(2));
179
180 s
181 }
182
183 fn manufacturing() -> Self {
185 let mut s = Self::new(IndustrySector::Manufacturing);
186
187 s.add_event(SeasonalEvent::new("Year-End Close", 12, 20, 12, 31, 4.0).with_priority(10));
189
190 s.add_event(
192 SeasonalEvent::new("Q4 Inventory Buildup", 10, 1, 11, 30, 2.0).with_priority(6),
193 );
194
195 s.add_event(SeasonalEvent::new("Model Year Transition", 9, 1, 9, 30, 1.5).with_priority(5));
197
198 s.add_event(
200 SeasonalEvent::new("Spring Production Ramp", 3, 1, 4, 30, 1.3).with_priority(3),
201 );
202
203 s.add_event(SeasonalEvent::new("Summer Shutdown", 7, 1, 7, 31, 0.6).with_priority(4));
205
206 s.add_event(SeasonalEvent::new("Holiday Shutdown", 12, 24, 12, 26, 0.2).with_priority(11));
208
209 s
210 }
211
212 fn financial_services() -> Self {
214 let mut s = Self::new(IndustrySector::FinancialServices);
215
216 s.add_event(SeasonalEvent::new("Year-End", 12, 15, 12, 31, 8.0).with_priority(10));
218
219 s.add_event(SeasonalEvent::new("Q1 Close", 3, 26, 3, 31, 5.0).with_priority(9));
221
222 s.add_event(SeasonalEvent::new("Q2 Close", 6, 25, 6, 30, 5.0).with_priority(9));
224
225 s.add_event(SeasonalEvent::new("Q3 Close", 9, 25, 9, 30, 5.0).with_priority(9));
227
228 s.add_event(SeasonalEvent::new("Tax Deadline", 4, 10, 4, 20, 3.0).with_priority(7));
230
231 s.add_event(SeasonalEvent::new("Audit Season", 1, 15, 2, 28, 2.5).with_priority(6));
233
234 s.add_event(SeasonalEvent::new("Regulatory Filing", 2, 1, 2, 28, 2.0).with_priority(5));
236
237 s
238 }
239
240 fn healthcare() -> Self {
242 let mut s = Self::new(IndustrySector::Healthcare);
243
244 s.add_event(SeasonalEvent::new("Year-End", 12, 15, 12, 31, 3.0).with_priority(10));
246
247 s.add_event(SeasonalEvent::new("Insurance Enrollment", 1, 1, 1, 31, 2.0).with_priority(8));
249
250 s.add_event(SeasonalEvent::new("Flu Season", 10, 1, 10, 31, 1.5).with_priority(4));
252 s.add_event(SeasonalEvent::new("Flu Season Extended", 11, 1, 2, 28, 1.5).with_priority(4));
253
254 s.add_event(SeasonalEvent::new("Open Enrollment", 11, 1, 11, 30, 1.8).with_priority(6));
256
257 s.add_event(SeasonalEvent::new("Summer Slowdown", 6, 15, 8, 15, 0.8).with_priority(3));
259
260 s
261 }
262
263 fn technology() -> Self {
265 let mut s = Self::new(IndustrySector::Technology);
266
267 s.add_event(
269 SeasonalEvent::new("Q4 Enterprise Deals", 12, 1, 12, 31, 4.0).with_priority(10),
270 );
271
272 s.add_event(SeasonalEvent::new("Holiday Sales", 11, 15, 11, 30, 2.0).with_priority(8));
274
275 s.add_event(SeasonalEvent::new("Back-to-School", 8, 1, 9, 15, 1.5).with_priority(5));
277
278 s.add_event(SeasonalEvent::new("Spring Launches", 3, 1, 3, 31, 1.8).with_priority(6));
280 s.add_event(SeasonalEvent::new("Fall Launches", 9, 1, 9, 30, 1.8).with_priority(6));
281
282 s.add_event(SeasonalEvent::new("Summer Slowdown", 7, 1, 8, 15, 0.7).with_priority(3));
284
285 s
286 }
287
288 fn professional_services() -> Self {
290 let mut s = Self::new(IndustrySector::ProfessionalServices);
291
292 s.add_event(SeasonalEvent::new("Year-End", 12, 10, 12, 31, 3.0).with_priority(10));
294
295 s.add_event(SeasonalEvent::new("Tax Season", 2, 1, 4, 15, 2.5).with_priority(8));
297
298 s.add_event(SeasonalEvent::new("Budget Season", 10, 1, 11, 30, 1.8).with_priority(6));
300
301 s.add_event(SeasonalEvent::new("Summer Slowdown", 7, 1, 8, 31, 0.75).with_priority(3));
303
304 s.add_event(SeasonalEvent::new("Holiday Period", 12, 23, 12, 26, 0.3).with_priority(11));
306
307 s
308 }
309
310 fn energy() -> Self {
312 let mut s = Self::new(IndustrySector::Energy);
313
314 s.add_event(
316 SeasonalEvent::new("Winter Heating Season", 11, 1, 2, 28, 1.8).with_priority(6),
317 );
318
319 s.add_event(SeasonalEvent::new("Summer Cooling Season", 6, 1, 8, 31, 1.5).with_priority(5));
321
322 s.add_event(SeasonalEvent::new("Year-End", 12, 15, 12, 31, 3.0).with_priority(10));
324
325 s.add_event(SeasonalEvent::new("Spring Shoulder", 3, 15, 5, 15, 0.8).with_priority(3));
327 s.add_event(SeasonalEvent::new("Fall Shoulder", 9, 15, 10, 15, 0.8).with_priority(3));
328
329 s
330 }
331
332 fn transportation() -> Self {
334 let mut s = Self::new(IndustrySector::Transportation);
335
336 s.add_event(SeasonalEvent::new("Holiday Shipping", 11, 15, 12, 24, 4.0).with_priority(10));
338
339 s.add_event(SeasonalEvent::new("Back-to-School", 8, 1, 8, 31, 1.5).with_priority(5));
341
342 s.add_event(SeasonalEvent::new("Summer Travel", 6, 15, 8, 15, 1.3).with_priority(4));
344
345 s.add_event(SeasonalEvent::new("January Slowdown", 1, 5, 1, 31, 0.7).with_priority(3));
347
348 s
349 }
350
351 fn real_estate() -> Self {
353 let mut s = Self::new(IndustrySector::RealEstate);
354
355 s.add_event(SeasonalEvent::new("Spring Buying Season", 3, 1, 6, 30, 2.0).with_priority(6));
357
358 s.add_event(SeasonalEvent::new("Year-End Closings", 12, 1, 12, 31, 2.5).with_priority(8));
360
361 s.add_event(SeasonalEvent::new("Summer Moving", 6, 1, 8, 31, 1.8).with_priority(5));
363
364 s.add_event(SeasonalEvent::new("Winter Slowdown", 1, 1, 2, 28, 0.6).with_priority(3));
366
367 s
368 }
369
370 fn telecommunications() -> Self {
372 let mut s = Self::new(IndustrySector::Telecommunications);
373
374 s.add_event(
376 SeasonalEvent::new("Holiday Activations", 11, 15, 12, 31, 2.0).with_priority(8),
377 );
378
379 s.add_event(SeasonalEvent::new("Back-to-School", 8, 1, 9, 15, 1.5).with_priority(5));
381
382 s.add_event(SeasonalEvent::new("Year-End Billing", 12, 15, 12, 31, 1.8).with_priority(7));
384
385 s.add_event(SeasonalEvent::new("Q1 Slowdown", 1, 15, 2, 28, 0.8).with_priority(3));
387
388 s
389 }
390}
391
392#[derive(Debug, Clone, Serialize, Deserialize)]
394pub struct CustomSeasonalEventConfig {
395 pub name: String,
397 pub start_month: u8,
399 pub start_day: u8,
401 pub end_month: u8,
403 pub end_day: u8,
405 pub multiplier: f64,
407 #[serde(default = "default_priority")]
409 pub priority: u8,
410}
411
412fn default_priority() -> u8 {
413 5
414}
415
416impl From<CustomSeasonalEventConfig> for SeasonalEvent {
417 fn from(config: CustomSeasonalEventConfig) -> Self {
418 SeasonalEvent::new(
419 config.name,
420 config.start_month,
421 config.start_day,
422 config.end_month,
423 config.end_day,
424 config.multiplier,
425 )
426 .with_priority(config.priority)
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433
434 #[test]
435 fn test_seasonal_event_active() {
436 let event = SeasonalEvent::new("Test Event", 11, 20, 11, 30, 2.0);
437
438 assert!(event.is_active(NaiveDate::from_ymd_opt(2024, 11, 20).unwrap()));
440 assert!(event.is_active(NaiveDate::from_ymd_opt(2024, 11, 25).unwrap()));
441 assert!(event.is_active(NaiveDate::from_ymd_opt(2024, 11, 30).unwrap()));
442
443 assert!(!event.is_active(NaiveDate::from_ymd_opt(2024, 11, 19).unwrap()));
445 assert!(!event.is_active(NaiveDate::from_ymd_opt(2024, 12, 1).unwrap()));
446 assert!(!event.is_active(NaiveDate::from_ymd_opt(2024, 10, 25).unwrap()));
447 }
448
449 #[test]
450 fn test_year_spanning_event() {
451 let event = SeasonalEvent::new("Holiday Period", 12, 20, 1, 5, 0.3);
452
453 assert!(event.is_active(NaiveDate::from_ymd_opt(2024, 12, 20).unwrap()));
455 assert!(event.is_active(NaiveDate::from_ymd_opt(2024, 12, 31).unwrap()));
456
457 assert!(event.is_active(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()));
459 assert!(event.is_active(NaiveDate::from_ymd_opt(2024, 1, 5).unwrap()));
460
461 assert!(!event.is_active(NaiveDate::from_ymd_opt(2024, 12, 19).unwrap()));
463 assert!(!event.is_active(NaiveDate::from_ymd_opt(2024, 1, 6).unwrap()));
464 assert!(!event.is_active(NaiveDate::from_ymd_opt(2024, 6, 15).unwrap()));
465 }
466
467 #[test]
468 fn test_retail_seasonality() {
469 let seasonality = IndustrySeasonality::for_industry(IndustrySector::Retail);
470
471 let black_friday = NaiveDate::from_ymd_opt(2024, 11, 25).unwrap();
473 assert!((seasonality.get_multiplier(black_friday) - 8.0).abs() < 0.01);
474
475 let regular_day = NaiveDate::from_ymd_opt(2024, 5, 15).unwrap();
477 assert!((seasonality.get_multiplier(regular_day) - 1.0).abs() < 0.01);
478
479 let summer = NaiveDate::from_ymd_opt(2024, 7, 15).unwrap();
481 assert!((seasonality.get_multiplier(summer) - 0.7).abs() < 0.01);
482 }
483
484 #[test]
485 fn test_financial_services_seasonality() {
486 let seasonality = IndustrySeasonality::for_industry(IndustrySector::FinancialServices);
487
488 let year_end = NaiveDate::from_ymd_opt(2024, 12, 20).unwrap();
490 assert!((seasonality.get_multiplier(year_end) - 8.0).abs() < 0.01);
491
492 let q1_end = NaiveDate::from_ymd_opt(2024, 3, 28).unwrap();
494 assert!((seasonality.get_multiplier(q1_end) - 5.0).abs() < 0.01);
495 }
496
497 #[test]
498 fn test_priority_handling() {
499 let mut s = IndustrySeasonality::new(IndustrySector::Retail);
500
501 s.add_event(SeasonalEvent::new("Low Priority", 12, 1, 12, 31, 2.0).with_priority(1));
503 s.add_event(SeasonalEvent::new("High Priority", 12, 15, 12, 25, 5.0).with_priority(10));
504
505 let overlap = NaiveDate::from_ymd_opt(2024, 12, 20).unwrap();
507 assert!((s.get_multiplier(overlap) - 5.0).abs() < 0.01);
508
509 let low_only = NaiveDate::from_ymd_opt(2024, 12, 5).unwrap();
511 assert!((s.get_multiplier(low_only) - 2.0).abs() < 0.01);
512 }
513
514 #[test]
515 fn test_all_industries_have_events() {
516 let industries = [
517 IndustrySector::Retail,
518 IndustrySector::Manufacturing,
519 IndustrySector::FinancialServices,
520 IndustrySector::Healthcare,
521 IndustrySector::Technology,
522 IndustrySector::ProfessionalServices,
523 IndustrySector::Energy,
524 IndustrySector::Transportation,
525 IndustrySector::RealEstate,
526 IndustrySector::Telecommunications,
527 ];
528
529 for industry in industries {
530 let s = IndustrySeasonality::for_industry(industry);
531 assert!(
532 !s.events.is_empty(),
533 "Industry {:?} should have seasonal events",
534 industry
535 );
536 }
537 }
538}