1use chrono::NaiveDate;
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Default)]
14pub struct DriftContext {
15 pub economic_cycle_factor: f64,
17 pub is_recession: bool,
19 pub inflation_rate: f64,
21 pub market_sentiment: MarketSentiment,
23 pub period: u32,
25 pub total_periods: u32,
27}
28
29impl DriftContext {
30 pub fn neutral() -> Self {
32 Self {
33 economic_cycle_factor: 1.0,
34 is_recession: false,
35 inflation_rate: 0.02,
36 market_sentiment: MarketSentiment::Neutral,
37 period: 0,
38 total_periods: 12,
39 }
40 }
41
42 pub fn years_elapsed(&self) -> f64 {
44 self.period as f64 / 12.0
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
50#[serde(rename_all = "snake_case")]
51pub enum MarketSentiment {
52 VeryPessimistic,
54 Pessimistic,
56 #[default]
58 Neutral,
59 Optimistic,
61 VeryOptimistic,
63}
64
65impl MarketSentiment {
66 pub fn factor(&self) -> f64 {
68 match self {
69 Self::VeryPessimistic => 0.6,
70 Self::Pessimistic => 0.8,
71 Self::Neutral => 1.0,
72 Self::Optimistic => 1.2,
73 Self::VeryOptimistic => 1.4,
74 }
75 }
76}
77
78pub trait BehavioralDrift {
80 fn behavioral_state_at(&self, date: NaiveDate, context: &DriftContext) -> BehavioralState;
82
83 fn evolve(&mut self, current_date: NaiveDate, context: &DriftContext);
85}
86
87#[derive(Debug, Clone, Default)]
89pub struct BehavioralState {
90 pub payment_days_delta: f64,
92 pub order_factor: f64,
94 pub error_factor: f64,
96 pub processing_time_factor: f64,
98 pub quality_factor: f64,
100 pub price_sensitivity: f64,
102}
103
104impl BehavioralState {
105 pub fn neutral() -> Self {
107 Self {
108 payment_days_delta: 0.0,
109 order_factor: 1.0,
110 error_factor: 1.0,
111 processing_time_factor: 1.0,
112 quality_factor: 1.0,
113 price_sensitivity: 1.0,
114 }
115 }
116}
117
118#[derive(Debug, Clone, Default, Serialize, Deserialize)]
124pub struct VendorBehavioralDrift {
125 #[serde(default)]
127 pub payment_terms_drift: PaymentTermsDrift,
128 #[serde(default)]
130 pub quality_drift: VendorQualityDrift,
131 #[serde(default)]
133 pub pricing_drift: PricingBehaviorDrift,
134}
135
136impl VendorBehavioralDrift {
137 pub fn state_at(&self, context: &DriftContext) -> BehavioralState {
139 let years = context.years_elapsed();
140
141 let payment_days = self.payment_terms_drift.extension_rate_per_year
143 * years
144 * (1.0
145 + self.payment_terms_drift.economic_sensitivity
146 * (context.economic_cycle_factor - 1.0));
147
148 let quality_factor = if years < 1.0 {
150 1.0 + self.quality_drift.new_vendor_improvement_rate * years
152 } else {
153 1.0 + self.quality_drift.new_vendor_improvement_rate
155 - self.quality_drift.complacency_decline_rate * (years - 1.0)
156 };
157
158 let price_sensitivity =
160 1.0 + self.pricing_drift.inflation_pass_through * context.inflation_rate * years;
161
162 BehavioralState {
163 payment_days_delta: payment_days,
164 quality_factor: quality_factor.clamp(0.7, 1.3),
165 price_sensitivity,
166 ..BehavioralState::neutral()
167 }
168 }
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct PaymentTermsDrift {
174 #[serde(default = "default_extension_rate")]
176 pub extension_rate_per_year: f64,
177 #[serde(default = "default_economic_sensitivity")]
179 pub economic_sensitivity: f64,
180}
181
182fn default_extension_rate() -> f64 {
183 2.5
184}
185
186fn default_economic_sensitivity() -> f64 {
187 1.0
188}
189
190impl Default for PaymentTermsDrift {
191 fn default() -> Self {
192 Self {
193 extension_rate_per_year: 2.5,
194 economic_sensitivity: 1.0,
195 }
196 }
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct VendorQualityDrift {
202 #[serde(default = "default_improvement_rate")]
204 pub new_vendor_improvement_rate: f64,
205 #[serde(default = "default_decline_rate")]
207 pub complacency_decline_rate: f64,
208}
209
210fn default_improvement_rate() -> f64 {
211 0.02
212}
213
214fn default_decline_rate() -> f64 {
215 0.01
216}
217
218impl Default for VendorQualityDrift {
219 fn default() -> Self {
220 Self {
221 new_vendor_improvement_rate: 0.02,
222 complacency_decline_rate: 0.01,
223 }
224 }
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct PricingBehaviorDrift {
230 #[serde(default = "default_pass_through")]
232 pub inflation_pass_through: f64,
233 #[serde(default = "default_volatility")]
235 pub price_volatility: f64,
236}
237
238fn default_pass_through() -> f64 {
239 0.80
240}
241
242fn default_volatility() -> f64 {
243 0.10
244}
245
246impl Default for PricingBehaviorDrift {
247 fn default() -> Self {
248 Self {
249 inflation_pass_through: 0.80,
250 price_volatility: 0.10,
251 }
252 }
253}
254
255#[derive(Debug, Clone, Default, Serialize, Deserialize)]
261pub struct CustomerBehavioralDrift {
262 #[serde(default)]
264 pub payment_drift: CustomerPaymentDrift,
265 #[serde(default)]
267 pub order_drift: OrderPatternDrift,
268}
269
270impl CustomerBehavioralDrift {
271 pub fn state_at(&self, context: &DriftContext) -> BehavioralState {
273 let payment_days = if context.is_recession || context.economic_cycle_factor < 0.9 {
275 let severity = 1.0 - context.economic_cycle_factor;
276 self.payment_drift.downturn_days_extension.0 as f64
277 + (self.payment_drift.downturn_days_extension.1 as f64
278 - self.payment_drift.downturn_days_extension.0 as f64)
279 * severity
280 } else {
281 0.0
282 };
283
284 let years = context.years_elapsed();
286 let order_factor = 1.0 + self.order_drift.digital_shift_rate * years;
287
288 BehavioralState {
289 payment_days_delta: payment_days,
290 order_factor,
291 ..BehavioralState::neutral()
292 }
293 }
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize)]
298pub struct CustomerPaymentDrift {
299 #[serde(default = "default_downturn_extension")]
301 pub downturn_days_extension: (u32, u32),
302 #[serde(default = "default_bad_debt_increase")]
304 pub downturn_bad_debt_increase: f64,
305}
306
307fn default_downturn_extension() -> (u32, u32) {
308 (5, 15)
309}
310
311fn default_bad_debt_increase() -> f64 {
312 0.02
313}
314
315impl Default for CustomerPaymentDrift {
316 fn default() -> Self {
317 Self {
318 downturn_days_extension: (5, 15),
319 downturn_bad_debt_increase: 0.02,
320 }
321 }
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct OrderPatternDrift {
327 #[serde(default = "default_digital_shift")]
329 pub digital_shift_rate: f64,
330 #[serde(default = "default_consolidation")]
332 pub order_consolidation_rate: f64,
333}
334
335fn default_digital_shift() -> f64 {
336 0.05
337}
338
339fn default_consolidation() -> f64 {
340 0.02
341}
342
343impl Default for OrderPatternDrift {
344 fn default() -> Self {
345 Self {
346 digital_shift_rate: 0.05,
347 order_consolidation_rate: 0.02,
348 }
349 }
350}
351
352#[derive(Debug, Clone, Default, Serialize, Deserialize)]
358pub struct EmployeeBehavioralDrift {
359 #[serde(default)]
361 pub approval_drift: ApprovalPatternDrift,
362 #[serde(default)]
364 pub error_drift: ErrorPatternDrift,
365}
366
367impl EmployeeBehavioralDrift {
368 pub fn state_at(&self, context: &DriftContext, is_period_end: bool) -> BehavioralState {
370 let years = context.years_elapsed();
371
372 let eom_factor = if is_period_end {
374 1.0 + self.approval_drift.eom_intensity_increase_per_year * years
375 } else {
376 1.0
377 };
378
379 let months = context.period as f64;
382 let error_factor = if months < self.error_drift.learning_curve_months as f64 {
383 let progress = months / self.error_drift.learning_curve_months as f64;
385 1.0 + self.error_drift.new_employee_error_rate * (1.0 - progress)
386 } else {
387 let fatigue_years = (months - self.error_drift.learning_curve_months as f64) / 12.0;
389 1.0 + self.error_drift.fatigue_error_increase * fatigue_years
390 };
391
392 BehavioralState {
393 processing_time_factor: eom_factor,
394 error_factor: error_factor.clamp(0.5, 2.0),
395 ..BehavioralState::neutral()
396 }
397 }
398}
399
400#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct ApprovalPatternDrift {
403 #[serde(default = "default_eom_intensity")]
405 pub eom_intensity_increase_per_year: f64,
406 #[serde(default = "default_rubber_stamp")]
408 pub rubber_stamp_volume_threshold: u32,
409}
410
411fn default_eom_intensity() -> f64 {
412 0.05
413}
414
415fn default_rubber_stamp() -> u32 {
416 50
417}
418
419impl Default for ApprovalPatternDrift {
420 fn default() -> Self {
421 Self {
422 eom_intensity_increase_per_year: 0.05,
423 rubber_stamp_volume_threshold: 50,
424 }
425 }
426}
427
428#[derive(Debug, Clone, Serialize, Deserialize)]
430pub struct ErrorPatternDrift {
431 #[serde(default = "default_new_error_rate")]
433 pub new_employee_error_rate: f64,
434 #[serde(default = "default_learning_months")]
436 pub learning_curve_months: u32,
437 #[serde(default = "default_fatigue_increase")]
439 pub fatigue_error_increase: f64,
440}
441
442fn default_new_error_rate() -> f64 {
443 0.08
444}
445
446fn default_learning_months() -> u32 {
447 6
448}
449
450fn default_fatigue_increase() -> f64 {
451 0.01
452}
453
454impl Default for ErrorPatternDrift {
455 fn default() -> Self {
456 Self {
457 new_employee_error_rate: 0.08,
458 learning_curve_months: 6,
459 fatigue_error_increase: 0.01,
460 }
461 }
462}
463
464#[derive(Debug, Clone, Default, Serialize, Deserialize)]
470pub struct CollectiveBehavioralDrift {
471 #[serde(default)]
473 pub year_end_intensity: YearEndIntensityDrift,
474 #[serde(default)]
476 pub automation_adoption: AutomationAdoptionDrift,
477 #[serde(default)]
479 pub remote_work_impact: RemoteWorkDrift,
480}
481
482impl CollectiveBehavioralDrift {
483 pub fn state_at(&self, context: &DriftContext, month: u32) -> CollectiveState {
485 let years = context.years_elapsed();
486
487 let is_year_end = month == 11 || month == 0; let year_end_factor = if is_year_end {
490 1.0 + self.year_end_intensity.intensity_increase_per_year * years
491 } else {
492 1.0
493 };
494
495 let automation_rate = if self.automation_adoption.s_curve_enabled {
497 let midpoint_years = self.automation_adoption.adoption_midpoint_months as f64 / 12.0;
498 let steepness = self.automation_adoption.steepness;
499 1.0 / (1.0 + (-steepness * (years - midpoint_years)).exp())
500 } else {
501 0.0
502 };
503
504 let posting_time_variance = if self.remote_work_impact.enabled {
506 1.0 - self.remote_work_impact.posting_time_flattening * years.min(2.0)
507 } else {
508 1.0
509 };
510
511 CollectiveState {
512 year_end_intensity_factor: year_end_factor,
513 automation_rate,
514 posting_time_variance_factor: posting_time_variance.max(0.5),
515 }
516 }
517}
518
519#[derive(Debug, Clone, Default)]
521pub struct CollectiveState {
522 pub year_end_intensity_factor: f64,
524 pub automation_rate: f64,
526 pub posting_time_variance_factor: f64,
528}
529
530#[derive(Debug, Clone, Serialize, Deserialize)]
532pub struct YearEndIntensityDrift {
533 #[serde(default = "default_intensity_increase")]
535 pub intensity_increase_per_year: f64,
536}
537
538fn default_intensity_increase() -> f64 {
539 0.05
540}
541
542impl Default for YearEndIntensityDrift {
543 fn default() -> Self {
544 Self {
545 intensity_increase_per_year: 0.05,
546 }
547 }
548}
549
550#[derive(Debug, Clone, Serialize, Deserialize)]
552pub struct AutomationAdoptionDrift {
553 #[serde(default)]
555 pub s_curve_enabled: bool,
556 #[serde(default = "default_midpoint")]
558 pub adoption_midpoint_months: u32,
559 #[serde(default = "default_steepness")]
561 pub steepness: f64,
562}
563
564fn default_midpoint() -> u32 {
565 24
566}
567
568fn default_steepness() -> f64 {
569 0.15
570}
571
572impl Default for AutomationAdoptionDrift {
573 fn default() -> Self {
574 Self {
575 s_curve_enabled: false,
576 adoption_midpoint_months: 24,
577 steepness: 0.15,
578 }
579 }
580}
581
582#[derive(Debug, Clone, Serialize, Deserialize)]
584pub struct RemoteWorkDrift {
585 #[serde(default)]
587 pub enabled: bool,
588 #[serde(default = "default_flattening")]
590 pub posting_time_flattening: f64,
591}
592
593fn default_flattening() -> f64 {
594 0.3
595}
596
597impl Default for RemoteWorkDrift {
598 fn default() -> Self {
599 Self {
600 enabled: false,
601 posting_time_flattening: 0.3,
602 }
603 }
604}
605
606#[derive(Debug, Clone, Serialize, Deserialize, Default)]
612pub struct BehavioralDriftConfig {
613 #[serde(default)]
615 pub enabled: bool,
616 #[serde(default)]
618 pub vendor_behavior: VendorBehavioralDrift,
619 #[serde(default)]
621 pub customer_behavior: CustomerBehavioralDrift,
622 #[serde(default)]
624 pub employee_behavior: EmployeeBehavioralDrift,
625 #[serde(default)]
627 pub collective: CollectiveBehavioralDrift,
628}
629
630impl BehavioralDriftConfig {
631 pub fn compute_effects(
633 &self,
634 context: &DriftContext,
635 month: u32,
636 is_period_end: bool,
637 ) -> BehavioralEffects {
638 if !self.enabled {
639 return BehavioralEffects::neutral();
640 }
641
642 BehavioralEffects {
643 vendor: self.vendor_behavior.state_at(context),
644 customer: self.customer_behavior.state_at(context),
645 employee: self.employee_behavior.state_at(context, is_period_end),
646 collective: self.collective.state_at(context, month),
647 }
648 }
649}
650
651#[derive(Debug, Clone, Default)]
653pub struct BehavioralEffects {
654 pub vendor: BehavioralState,
656 pub customer: BehavioralState,
658 pub employee: BehavioralState,
660 pub collective: CollectiveState,
662}
663
664impl BehavioralEffects {
665 pub fn neutral() -> Self {
667 Self {
668 vendor: BehavioralState::neutral(),
669 customer: BehavioralState::neutral(),
670 employee: BehavioralState::neutral(),
671 collective: CollectiveState::default(),
672 }
673 }
674}
675
676#[cfg(test)]
677mod tests {
678 use super::*;
679
680 #[test]
681 fn test_vendor_behavioral_drift() {
682 let drift = VendorBehavioralDrift::default();
683 let context = DriftContext {
684 period: 24, total_periods: 36,
686 ..DriftContext::neutral()
687 };
688
689 let state = drift.state_at(&context);
690 assert!(state.payment_days_delta > 0.0);
692 assert!(state.quality_factor < 1.02);
694 }
695
696 #[test]
697 fn test_customer_downturn_drift() {
698 let drift = CustomerBehavioralDrift::default();
699 let context = DriftContext {
700 is_recession: true,
701 economic_cycle_factor: 0.8,
702 ..DriftContext::neutral()
703 };
704
705 let state = drift.state_at(&context);
706 assert!(state.payment_days_delta > 0.0);
708 }
709
710 #[test]
711 fn test_employee_learning_curve() {
712 let drift = EmployeeBehavioralDrift::default();
713
714 let context_new = DriftContext {
716 period: 1,
717 ..DriftContext::neutral()
718 };
719 let state_new = drift.state_at(&context_new, false);
720 assert!(state_new.error_factor > 1.0); let context_exp = DriftContext {
724 period: 12,
725 ..DriftContext::neutral()
726 };
727 let state_exp = drift.state_at(&context_exp, false);
728 assert!(state_exp.error_factor < state_new.error_factor); }
730
731 #[test]
732 fn test_automation_s_curve() {
733 let drift = CollectiveBehavioralDrift {
734 automation_adoption: AutomationAdoptionDrift {
735 s_curve_enabled: true,
736 adoption_midpoint_months: 24,
737 steepness: 0.15,
738 },
739 ..Default::default()
740 };
741
742 let context_early = DriftContext {
744 period: 6,
745 ..DriftContext::neutral()
746 };
747 let state_early = drift.state_at(&context_early, 6);
748
749 let context_mid = DriftContext {
751 period: 24,
752 ..DriftContext::neutral()
753 };
754 let state_mid = drift.state_at(&context_mid, 0);
755
756 let context_late = DriftContext {
758 period: 48,
759 ..DriftContext::neutral()
760 };
761 let state_late = drift.state_at(&context_late, 0);
762
763 assert!(state_early.automation_rate < 0.5);
765 assert!((state_mid.automation_rate - 0.5).abs() < 0.2);
766 assert!(state_late.automation_rate > 0.5);
767 }
768}