1use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Default)]
13pub struct DriftContext {
14 pub economic_cycle_factor: f64,
16 pub is_recession: bool,
18 pub inflation_rate: f64,
20 pub market_sentiment: MarketSentiment,
22 pub period: u32,
24 pub total_periods: u32,
26}
27
28impl DriftContext {
29 pub fn neutral() -> Self {
31 Self {
32 economic_cycle_factor: 1.0,
33 is_recession: false,
34 inflation_rate: 0.02,
35 market_sentiment: MarketSentiment::Neutral,
36 period: 0,
37 total_periods: 12,
38 }
39 }
40
41 pub fn years_elapsed(&self) -> f64 {
43 self.period as f64 / 12.0
44 }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
49#[serde(rename_all = "snake_case")]
50pub enum MarketSentiment {
51 VeryPessimistic,
53 Pessimistic,
55 #[default]
57 Neutral,
58 Optimistic,
60 VeryOptimistic,
62}
63
64impl MarketSentiment {
65 pub fn factor(&self) -> f64 {
67 match self {
68 Self::VeryPessimistic => 0.6,
69 Self::Pessimistic => 0.8,
70 Self::Neutral => 1.0,
71 Self::Optimistic => 1.2,
72 Self::VeryOptimistic => 1.4,
73 }
74 }
75}
76
77#[derive(Debug, Clone, Default)]
79pub struct BehavioralState {
80 pub payment_days_delta: f64,
82 pub order_factor: f64,
84 pub error_factor: f64,
86 pub processing_time_factor: f64,
88 pub quality_factor: f64,
90 pub price_sensitivity: f64,
92}
93
94impl BehavioralState {
95 pub fn neutral() -> Self {
97 Self {
98 payment_days_delta: 0.0,
99 order_factor: 1.0,
100 error_factor: 1.0,
101 processing_time_factor: 1.0,
102 quality_factor: 1.0,
103 price_sensitivity: 1.0,
104 }
105 }
106}
107
108#[derive(Debug, Clone, Default, Serialize, Deserialize)]
114pub struct VendorBehavioralDrift {
115 #[serde(default)]
117 pub payment_terms_drift: PaymentTermsDrift,
118 #[serde(default)]
120 pub quality_drift: VendorQualityDrift,
121 #[serde(default)]
123 pub pricing_drift: PricingBehaviorDrift,
124}
125
126impl VendorBehavioralDrift {
127 pub fn state_at(&self, context: &DriftContext) -> BehavioralState {
129 let years = context.years_elapsed();
130
131 let payment_days = self.payment_terms_drift.extension_rate_per_year
133 * years
134 * (1.0
135 + self.payment_terms_drift.economic_sensitivity
136 * (context.economic_cycle_factor - 1.0));
137
138 let quality_factor = if years < 1.0 {
140 1.0 + self.quality_drift.new_vendor_improvement_rate * years
142 } else {
143 1.0 + self.quality_drift.new_vendor_improvement_rate
145 - self.quality_drift.complacency_decline_rate * (years - 1.0)
146 };
147
148 let price_sensitivity =
150 1.0 + self.pricing_drift.inflation_pass_through * context.inflation_rate * years;
151
152 BehavioralState {
153 payment_days_delta: payment_days,
154 quality_factor: quality_factor.clamp(0.7, 1.3),
155 price_sensitivity,
156 ..BehavioralState::neutral()
157 }
158 }
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct PaymentTermsDrift {
164 #[serde(default = "default_extension_rate")]
166 pub extension_rate_per_year: f64,
167 #[serde(default = "default_economic_sensitivity")]
169 pub economic_sensitivity: f64,
170}
171
172fn default_extension_rate() -> f64 {
173 2.5
174}
175
176fn default_economic_sensitivity() -> f64 {
177 1.0
178}
179
180impl Default for PaymentTermsDrift {
181 fn default() -> Self {
182 Self {
183 extension_rate_per_year: 2.5,
184 economic_sensitivity: 1.0,
185 }
186 }
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct VendorQualityDrift {
192 #[serde(default = "default_improvement_rate")]
194 pub new_vendor_improvement_rate: f64,
195 #[serde(default = "default_decline_rate")]
197 pub complacency_decline_rate: f64,
198}
199
200fn default_improvement_rate() -> f64 {
201 0.02
202}
203
204fn default_decline_rate() -> f64 {
205 0.01
206}
207
208impl Default for VendorQualityDrift {
209 fn default() -> Self {
210 Self {
211 new_vendor_improvement_rate: 0.02,
212 complacency_decline_rate: 0.01,
213 }
214 }
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct PricingBehaviorDrift {
220 #[serde(default = "default_pass_through")]
222 pub inflation_pass_through: f64,
223 #[serde(default = "default_volatility")]
225 pub price_volatility: f64,
226}
227
228fn default_pass_through() -> f64 {
229 0.80
230}
231
232fn default_volatility() -> f64 {
233 0.10
234}
235
236impl Default for PricingBehaviorDrift {
237 fn default() -> Self {
238 Self {
239 inflation_pass_through: 0.80,
240 price_volatility: 0.10,
241 }
242 }
243}
244
245#[derive(Debug, Clone, Default, Serialize, Deserialize)]
251pub struct CustomerBehavioralDrift {
252 #[serde(default)]
254 pub payment_drift: CustomerPaymentDrift,
255 #[serde(default)]
257 pub order_drift: OrderPatternDrift,
258}
259
260impl CustomerBehavioralDrift {
261 pub fn state_at(&self, context: &DriftContext) -> BehavioralState {
263 let payment_days = if context.is_recession || context.economic_cycle_factor < 0.9 {
265 let severity = 1.0 - context.economic_cycle_factor;
266 self.payment_drift.downturn_days_extension.0 as f64
267 + (self.payment_drift.downturn_days_extension.1 as f64
268 - self.payment_drift.downturn_days_extension.0 as f64)
269 * severity
270 } else {
271 0.0
272 };
273
274 let years = context.years_elapsed();
276 let order_factor = 1.0 + self.order_drift.digital_shift_rate * years;
277
278 BehavioralState {
279 payment_days_delta: payment_days,
280 order_factor,
281 ..BehavioralState::neutral()
282 }
283 }
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct CustomerPaymentDrift {
289 #[serde(default = "default_downturn_extension")]
291 pub downturn_days_extension: (u32, u32),
292 #[serde(default = "default_bad_debt_increase")]
294 pub downturn_bad_debt_increase: f64,
295}
296
297fn default_downturn_extension() -> (u32, u32) {
298 (5, 15)
299}
300
301fn default_bad_debt_increase() -> f64 {
302 0.02
303}
304
305impl Default for CustomerPaymentDrift {
306 fn default() -> Self {
307 Self {
308 downturn_days_extension: (5, 15),
309 downturn_bad_debt_increase: 0.02,
310 }
311 }
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct OrderPatternDrift {
317 #[serde(default = "default_digital_shift")]
319 pub digital_shift_rate: f64,
320 #[serde(default = "default_consolidation")]
322 pub order_consolidation_rate: f64,
323}
324
325fn default_digital_shift() -> f64 {
326 0.05
327}
328
329fn default_consolidation() -> f64 {
330 0.02
331}
332
333impl Default for OrderPatternDrift {
334 fn default() -> Self {
335 Self {
336 digital_shift_rate: 0.05,
337 order_consolidation_rate: 0.02,
338 }
339 }
340}
341
342#[derive(Debug, Clone, Default, Serialize, Deserialize)]
348pub struct EmployeeBehavioralDrift {
349 #[serde(default)]
351 pub approval_drift: ApprovalPatternDrift,
352 #[serde(default)]
354 pub error_drift: ErrorPatternDrift,
355}
356
357impl EmployeeBehavioralDrift {
358 pub fn state_at(&self, context: &DriftContext, is_period_end: bool) -> BehavioralState {
360 let years = context.years_elapsed();
361
362 let eom_factor = if is_period_end {
364 1.0 + self.approval_drift.eom_intensity_increase_per_year * years
365 } else {
366 1.0
367 };
368
369 let months = context.period as f64;
372 let error_factor = if months < self.error_drift.learning_curve_months as f64 {
373 let progress = months / self.error_drift.learning_curve_months as f64;
375 1.0 + self.error_drift.new_employee_error_rate * (1.0 - progress)
376 } else {
377 let fatigue_years = (months - self.error_drift.learning_curve_months as f64) / 12.0;
379 1.0 + self.error_drift.fatigue_error_increase * fatigue_years
380 };
381
382 BehavioralState {
383 processing_time_factor: eom_factor,
384 error_factor: error_factor.clamp(0.5, 2.0),
385 ..BehavioralState::neutral()
386 }
387 }
388}
389
390#[derive(Debug, Clone, Serialize, Deserialize)]
392pub struct ApprovalPatternDrift {
393 #[serde(default = "default_eom_intensity")]
395 pub eom_intensity_increase_per_year: f64,
396 #[serde(default = "default_rubber_stamp")]
398 pub rubber_stamp_volume_threshold: u32,
399}
400
401fn default_eom_intensity() -> f64 {
402 0.05
403}
404
405fn default_rubber_stamp() -> u32 {
406 50
407}
408
409impl Default for ApprovalPatternDrift {
410 fn default() -> Self {
411 Self {
412 eom_intensity_increase_per_year: 0.05,
413 rubber_stamp_volume_threshold: 50,
414 }
415 }
416}
417
418#[derive(Debug, Clone, Serialize, Deserialize)]
420pub struct ErrorPatternDrift {
421 #[serde(default = "default_new_error_rate")]
423 pub new_employee_error_rate: f64,
424 #[serde(default = "default_learning_months")]
426 pub learning_curve_months: u32,
427 #[serde(default = "default_fatigue_increase")]
429 pub fatigue_error_increase: f64,
430}
431
432fn default_new_error_rate() -> f64 {
433 0.08
434}
435
436fn default_learning_months() -> u32 {
437 6
438}
439
440fn default_fatigue_increase() -> f64 {
441 0.01
442}
443
444impl Default for ErrorPatternDrift {
445 fn default() -> Self {
446 Self {
447 new_employee_error_rate: 0.08,
448 learning_curve_months: 6,
449 fatigue_error_increase: 0.01,
450 }
451 }
452}
453
454#[derive(Debug, Clone, Default, Serialize, Deserialize)]
460pub struct CollectiveBehavioralDrift {
461 #[serde(default)]
463 pub year_end_intensity: YearEndIntensityDrift,
464 #[serde(default)]
466 pub automation_adoption: AutomationAdoptionDrift,
467 #[serde(default)]
469 pub remote_work_impact: RemoteWorkDrift,
470}
471
472impl CollectiveBehavioralDrift {
473 pub fn state_at(&self, context: &DriftContext, month: u32) -> CollectiveState {
475 let years = context.years_elapsed();
476
477 let is_year_end = month == 11 || month == 0; let year_end_factor = if is_year_end {
480 1.0 + self.year_end_intensity.intensity_increase_per_year * years
481 } else {
482 1.0
483 };
484
485 let automation_rate = if self.automation_adoption.s_curve_enabled {
487 let midpoint_years = self.automation_adoption.adoption_midpoint_months as f64 / 12.0;
488 let steepness = self.automation_adoption.steepness;
489 1.0 / (1.0 + (-steepness * (years - midpoint_years)).exp())
490 } else {
491 0.0
492 };
493
494 let posting_time_variance = if self.remote_work_impact.enabled {
496 1.0 - self.remote_work_impact.posting_time_flattening * years.min(2.0)
497 } else {
498 1.0
499 };
500
501 CollectiveState {
502 year_end_intensity_factor: year_end_factor,
503 automation_rate,
504 posting_time_variance_factor: posting_time_variance.max(0.5),
505 }
506 }
507}
508
509#[derive(Debug, Clone, Default)]
511pub struct CollectiveState {
512 pub year_end_intensity_factor: f64,
514 pub automation_rate: f64,
516 pub posting_time_variance_factor: f64,
518}
519
520#[derive(Debug, Clone, Serialize, Deserialize)]
522pub struct YearEndIntensityDrift {
523 #[serde(default = "default_intensity_increase")]
525 pub intensity_increase_per_year: f64,
526}
527
528fn default_intensity_increase() -> f64 {
529 0.05
530}
531
532impl Default for YearEndIntensityDrift {
533 fn default() -> Self {
534 Self {
535 intensity_increase_per_year: 0.05,
536 }
537 }
538}
539
540#[derive(Debug, Clone, Serialize, Deserialize)]
542pub struct AutomationAdoptionDrift {
543 #[serde(default)]
545 pub s_curve_enabled: bool,
546 #[serde(default = "default_midpoint")]
548 pub adoption_midpoint_months: u32,
549 #[serde(default = "default_steepness")]
551 pub steepness: f64,
552}
553
554fn default_midpoint() -> u32 {
555 24
556}
557
558fn default_steepness() -> f64 {
559 0.15
560}
561
562impl Default for AutomationAdoptionDrift {
563 fn default() -> Self {
564 Self {
565 s_curve_enabled: false,
566 adoption_midpoint_months: 24,
567 steepness: 0.15,
568 }
569 }
570}
571
572#[derive(Debug, Clone, Serialize, Deserialize)]
574pub struct RemoteWorkDrift {
575 #[serde(default)]
577 pub enabled: bool,
578 #[serde(default = "default_flattening")]
580 pub posting_time_flattening: f64,
581}
582
583fn default_flattening() -> f64 {
584 0.3
585}
586
587impl Default for RemoteWorkDrift {
588 fn default() -> Self {
589 Self {
590 enabled: false,
591 posting_time_flattening: 0.3,
592 }
593 }
594}
595
596#[derive(Debug, Clone, Serialize, Deserialize, Default)]
602pub struct BehavioralDriftConfig {
603 #[serde(default)]
605 pub enabled: bool,
606 #[serde(default)]
608 pub vendor_behavior: VendorBehavioralDrift,
609 #[serde(default)]
611 pub customer_behavior: CustomerBehavioralDrift,
612 #[serde(default)]
614 pub employee_behavior: EmployeeBehavioralDrift,
615 #[serde(default)]
617 pub collective: CollectiveBehavioralDrift,
618}
619
620impl BehavioralDriftConfig {
621 pub fn compute_effects(
623 &self,
624 context: &DriftContext,
625 month: u32,
626 is_period_end: bool,
627 ) -> BehavioralEffects {
628 if !self.enabled {
629 return BehavioralEffects::neutral();
630 }
631
632 BehavioralEffects {
633 vendor: self.vendor_behavior.state_at(context),
634 customer: self.customer_behavior.state_at(context),
635 employee: self.employee_behavior.state_at(context, is_period_end),
636 collective: self.collective.state_at(context, month),
637 }
638 }
639}
640
641#[derive(Debug, Clone, Default)]
643pub struct BehavioralEffects {
644 pub vendor: BehavioralState,
646 pub customer: BehavioralState,
648 pub employee: BehavioralState,
650 pub collective: CollectiveState,
652}
653
654impl BehavioralEffects {
655 pub fn neutral() -> Self {
657 Self {
658 vendor: BehavioralState::neutral(),
659 customer: BehavioralState::neutral(),
660 employee: BehavioralState::neutral(),
661 collective: CollectiveState::default(),
662 }
663 }
664}
665
666#[cfg(test)]
667#[allow(clippy::unwrap_used)]
668mod tests {
669 use super::*;
670
671 #[test]
672 fn test_vendor_behavioral_drift() {
673 let drift = VendorBehavioralDrift::default();
674 let context = DriftContext {
675 period: 24, total_periods: 36,
677 ..DriftContext::neutral()
678 };
679
680 let state = drift.state_at(&context);
681 assert!(state.payment_days_delta > 0.0);
683 assert!(state.quality_factor < 1.02);
685 }
686
687 #[test]
688 fn test_customer_downturn_drift() {
689 let drift = CustomerBehavioralDrift::default();
690 let context = DriftContext {
691 is_recession: true,
692 economic_cycle_factor: 0.8,
693 ..DriftContext::neutral()
694 };
695
696 let state = drift.state_at(&context);
697 assert!(state.payment_days_delta > 0.0);
699 }
700
701 #[test]
702 fn test_employee_learning_curve() {
703 let drift = EmployeeBehavioralDrift::default();
704
705 let context_new = DriftContext {
707 period: 1,
708 ..DriftContext::neutral()
709 };
710 let state_new = drift.state_at(&context_new, false);
711 assert!(state_new.error_factor > 1.0); let context_exp = DriftContext {
715 period: 12,
716 ..DriftContext::neutral()
717 };
718 let state_exp = drift.state_at(&context_exp, false);
719 assert!(state_exp.error_factor < state_new.error_factor); }
721
722 #[test]
723 fn test_automation_s_curve() {
724 let drift = CollectiveBehavioralDrift {
725 automation_adoption: AutomationAdoptionDrift {
726 s_curve_enabled: true,
727 adoption_midpoint_months: 24,
728 steepness: 0.15,
729 },
730 ..Default::default()
731 };
732
733 let context_early = DriftContext {
735 period: 6,
736 ..DriftContext::neutral()
737 };
738 let state_early = drift.state_at(&context_early, 6);
739
740 let context_mid = DriftContext {
742 period: 24,
743 ..DriftContext::neutral()
744 };
745 let state_mid = drift.state_at(&context_mid, 0);
746
747 let context_late = DriftContext {
749 period: 48,
750 ..DriftContext::neutral()
751 };
752 let state_late = drift.state_at(&context_late, 0);
753
754 assert!(state_early.automation_rate < 0.5);
756 assert!((state_mid.automation_rate - 0.5).abs() < 0.2);
757 assert!(state_late.automation_rate > 0.5);
758 }
759}