1use super::mixture::{LogNormalComponent, LogNormalMixtureConfig};
7use super::pareto::ParetoConfig;
8use super::weibull::WeibullConfig;
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
13#[serde(rename_all = "snake_case")]
14pub enum IndustryType {
15 Retail,
17 #[default]
19 Manufacturing,
20 FinancialServices,
22 Healthcare,
24 Technology,
26 Wholesale,
28 ProfessionalServices,
30 Construction,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct IndustryAmountProfile {
37 pub industry: IndustryType,
39 pub sales_amounts: LogNormalMixtureConfig,
41 pub purchase_amounts: LogNormalMixtureConfig,
43 pub payroll_amounts: LogNormalMixtureConfig,
45 pub capex_amounts: ParetoConfig,
47 pub days_to_payment: WeibullConfig,
49 pub seasonality: [f64; 12],
51 pub line_item_range: (u8, u8),
53 pub avg_daily_transactions: u32,
55}
56
57impl Default for IndustryAmountProfile {
58 fn default() -> Self {
59 Self::manufacturing()
60 }
61}
62
63impl IndustryAmountProfile {
64 pub fn retail() -> Self {
72 Self {
73 industry: IndustryType::Retail,
74 sales_amounts: LogNormalMixtureConfig {
75 components: vec![
76 LogNormalComponent::with_label(0.60, 3.5, 1.0, "pos_small"),
78 LogNormalComponent::with_label(0.25, 4.5, 0.8, "medium"),
80 LogNormalComponent::with_label(0.10, 6.0, 1.2, "large"),
82 LogNormalComponent::with_label(0.05, 7.5, 0.9, "luxury"),
84 ],
85 min_value: 0.01,
86 max_value: Some(50_000.0),
87 decimal_places: 2,
88 },
89 purchase_amounts: LogNormalMixtureConfig {
90 components: vec![
91 LogNormalComponent::with_label(0.70, 7.0, 1.5, "inventory"),
93 LogNormalComponent::with_label(0.25, 9.0, 1.0, "bulk"),
95 LogNormalComponent::with_label(0.05, 10.0, 0.8, "seasonal"),
97 ],
98 min_value: 100.0,
99 max_value: Some(1_000_000.0),
100 decimal_places: 2,
101 },
102 payroll_amounts: LogNormalMixtureConfig {
103 components: vec![
104 LogNormalComponent::with_label(0.60, 6.5, 0.6, "hourly"),
106 LogNormalComponent::with_label(0.35, 7.5, 0.5, "salary"),
108 LogNormalComponent::with_label(0.05, 8.5, 0.4, "management"),
110 ],
111 min_value: 200.0,
112 max_value: Some(50_000.0),
113 decimal_places: 2,
114 },
115 capex_amounts: ParetoConfig {
116 alpha: 2.0,
117 x_min: 5_000.0,
118 max_value: Some(500_000.0),
119 decimal_places: 2,
120 },
121 days_to_payment: WeibullConfig::days_to_payment(),
122 seasonality: [
123 0.75, 0.70, 0.85, 0.90, 0.95, 0.90, 0.85, 0.90, 0.95, 1.10, 1.40, 1.75, ],
136 line_item_range: (1, 50),
137 avg_daily_transactions: 500,
138 }
139 }
140
141 pub fn manufacturing() -> Self {
149 Self {
150 industry: IndustryType::Manufacturing,
151 sales_amounts: LogNormalMixtureConfig {
152 components: vec![
153 LogNormalComponent::with_label(0.50, 8.0, 1.5, "standard"),
155 LogNormalComponent::with_label(0.35, 10.0, 1.0, "large"),
157 LogNormalComponent::with_label(0.15, 12.0, 0.8, "enterprise"),
159 ],
160 min_value: 500.0,
161 max_value: Some(10_000_000.0),
162 decimal_places: 2,
163 },
164 purchase_amounts: LogNormalMixtureConfig {
165 components: vec![
166 LogNormalComponent::with_label(0.55, 8.5, 1.5, "raw_materials"),
168 LogNormalComponent::with_label(0.30, 7.5, 1.2, "components"),
170 LogNormalComponent::with_label(0.15, 10.0, 1.0, "equipment"),
172 ],
173 min_value: 100.0,
174 max_value: Some(5_000_000.0),
175 decimal_places: 2,
176 },
177 payroll_amounts: LogNormalMixtureConfig {
178 components: vec![
179 LogNormalComponent::with_label(0.50, 7.5, 0.5, "production"),
181 LogNormalComponent::with_label(0.30, 8.0, 0.4, "technical"),
183 LogNormalComponent::with_label(0.15, 9.0, 0.5, "management"),
185 LogNormalComponent::with_label(0.05, 10.0, 0.4, "executive"),
187 ],
188 min_value: 1000.0,
189 max_value: Some(100_000.0),
190 decimal_places: 2,
191 },
192 capex_amounts: ParetoConfig {
193 alpha: 1.5, x_min: 25_000.0,
195 max_value: Some(10_000_000.0),
196 decimal_places: 2,
197 },
198 days_to_payment: WeibullConfig {
199 shape: 2.0,
200 scale: 45.0, min_value: 5.0,
202 max_value: Some(90.0),
203 round_to_integer: true,
204 },
205 seasonality: [
206 0.90, 0.95, 1.00, 1.05, 1.00, 0.95, 0.85, 0.90, 1.05, 1.10, 1.05, 0.85, ],
219 line_item_range: (2, 25),
220 avg_daily_transactions: 50,
221 }
222 }
223
224 pub fn financial_services() -> Self {
232 Self {
233 industry: IndustryType::FinancialServices,
234 sales_amounts: LogNormalMixtureConfig {
235 components: vec![
236 LogNormalComponent::with_label(0.40, 6.0, 1.5, "ach_small"),
238 LogNormalComponent::with_label(0.30, 9.0, 1.5, "medium"),
240 LogNormalComponent::with_label(0.20, 12.0, 2.0, "wire_large"),
242 LogNormalComponent::with_label(0.10, 15.0, 1.5, "institutional"),
244 ],
245 min_value: 1.0,
246 max_value: Some(100_000_000.0),
247 decimal_places: 2,
248 },
249 purchase_amounts: LogNormalMixtureConfig {
250 components: vec![
251 LogNormalComponent::with_label(0.40, 7.0, 1.0, "software"),
253 LogNormalComponent::with_label(0.35, 9.0, 1.2, "professional"),
255 LogNormalComponent::with_label(0.25, 11.0, 1.0, "infrastructure"),
257 ],
258 min_value: 500.0,
259 max_value: Some(10_000_000.0),
260 decimal_places: 2,
261 },
262 payroll_amounts: LogNormalMixtureConfig {
263 components: vec![
264 LogNormalComponent::with_label(0.30, 8.0, 0.5, "operations"),
266 LogNormalComponent::with_label(0.35, 9.0, 0.4, "analyst"),
268 LogNormalComponent::with_label(0.25, 10.0, 0.4, "senior"),
270 LogNormalComponent::with_label(0.10, 11.5, 0.5, "executive"),
272 ],
273 min_value: 2000.0,
274 max_value: Some(500_000.0),
275 decimal_places: 2,
276 },
277 capex_amounts: ParetoConfig {
278 alpha: 1.8,
279 x_min: 50_000.0,
280 max_value: Some(50_000_000.0),
281 decimal_places: 2,
282 },
283 days_to_payment: WeibullConfig {
284 shape: 3.0, scale: 10.0, min_value: 1.0,
287 max_value: Some(30.0),
288 round_to_integer: true,
289 },
290 seasonality: [
291 1.05, 0.95, 1.15, 1.00, 0.95, 1.15, 0.90, 0.90, 1.15, 1.00, 0.95, 1.25, ],
304 line_item_range: (1, 10),
305 avg_daily_transactions: 1000,
306 }
307 }
308
309 pub fn healthcare() -> Self {
317 Self {
318 industry: IndustryType::Healthcare,
319 sales_amounts: LogNormalMixtureConfig {
320 components: vec![
321 LogNormalComponent::with_label(0.40, 4.0, 1.0, "copay"),
323 LogNormalComponent::with_label(0.35, 7.0, 1.5, "procedures"),
325 LogNormalComponent::with_label(0.20, 9.0, 1.2, "specialist"),
327 LogNormalComponent::with_label(0.05, 11.0, 1.0, "major"),
329 ],
330 min_value: 10.0,
331 max_value: Some(1_000_000.0),
332 decimal_places: 2,
333 },
334 purchase_amounts: LogNormalMixtureConfig {
335 components: vec![
336 LogNormalComponent::with_label(0.45, 6.0, 1.2, "consumables"),
338 LogNormalComponent::with_label(0.35, 8.0, 1.5, "pharma"),
340 LogNormalComponent::with_label(0.20, 10.0, 1.0, "equipment"),
342 ],
343 min_value: 50.0,
344 max_value: Some(5_000_000.0),
345 decimal_places: 2,
346 },
347 payroll_amounts: LogNormalMixtureConfig {
348 components: vec![
349 LogNormalComponent::with_label(0.35, 7.5, 0.5, "support"),
351 LogNormalComponent::with_label(0.35, 8.5, 0.4, "clinical"),
353 LogNormalComponent::with_label(0.25, 10.0, 0.5, "physician"),
355 LogNormalComponent::with_label(0.05, 11.0, 0.4, "specialist"),
357 ],
358 min_value: 1500.0,
359 max_value: Some(200_000.0),
360 decimal_places: 2,
361 },
362 capex_amounts: ParetoConfig {
363 alpha: 1.6,
364 x_min: 10_000.0,
365 max_value: Some(20_000_000.0),
366 decimal_places: 2,
367 },
368 days_to_payment: WeibullConfig {
369 shape: 1.5, scale: 60.0, min_value: 10.0,
372 max_value: Some(180.0),
373 round_to_integer: true,
374 },
375 seasonality: [
376 1.15, 1.10, 1.00, 0.95, 0.90, 0.90, 0.85, 0.90, 0.95, 1.00, 1.05, 1.10, ],
389 line_item_range: (1, 30),
390 avg_daily_transactions: 200,
391 }
392 }
393
394 pub fn technology() -> Self {
402 Self {
403 industry: IndustryType::Technology,
404 sales_amounts: LogNormalMixtureConfig {
405 components: vec![
406 LogNormalComponent::with_label(0.50, 5.5, 1.0, "smb"),
408 LogNormalComponent::with_label(0.30, 8.0, 1.0, "midmarket"),
410 LogNormalComponent::with_label(0.15, 10.5, 1.2, "enterprise"),
412 LogNormalComponent::with_label(0.05, 13.0, 0.8, "strategic"),
414 ],
415 min_value: 10.0,
416 max_value: Some(10_000_000.0),
417 decimal_places: 2,
418 },
419 purchase_amounts: LogNormalMixtureConfig {
420 components: vec![
421 LogNormalComponent::with_label(0.40, 6.0, 1.0, "saas"),
423 LogNormalComponent::with_label(0.35, 8.5, 1.5, "cloud"),
425 LogNormalComponent::with_label(0.15, 7.5, 1.0, "hardware"),
427 LogNormalComponent::with_label(0.10, 9.0, 1.0, "contractors"),
429 ],
430 min_value: 50.0,
431 max_value: Some(5_000_000.0),
432 decimal_places: 2,
433 },
434 payroll_amounts: LogNormalMixtureConfig {
435 components: vec![
436 LogNormalComponent::with_label(0.25, 8.5, 0.4, "junior"),
438 LogNormalComponent::with_label(0.40, 9.2, 0.3, "mid"),
440 LogNormalComponent::with_label(0.25, 10.0, 0.3, "senior"),
442 LogNormalComponent::with_label(0.10, 11.0, 0.4, "leadership"),
444 ],
445 min_value: 3000.0,
446 max_value: Some(300_000.0),
447 decimal_places: 2,
448 },
449 capex_amounts: ParetoConfig {
450 alpha: 2.2, x_min: 10_000.0,
452 max_value: Some(2_000_000.0),
453 decimal_places: 2,
454 },
455 days_to_payment: WeibullConfig {
456 shape: 2.5, scale: 15.0, min_value: 0.0,
459 max_value: Some(45.0),
460 round_to_integer: true,
461 },
462 seasonality: [
463 0.95, 0.95, 1.00, 1.00, 1.00, 1.00, 0.95, 0.95, 1.05, 1.05, 1.00, 1.05, ],
476 line_item_range: (1, 15),
477 avg_daily_transactions: 100,
478 }
479 }
480
481 pub fn for_industry(industry: IndustryType) -> Self {
483 match industry {
484 IndustryType::Retail => Self::retail(),
485 IndustryType::Manufacturing => Self::manufacturing(),
486 IndustryType::FinancialServices => Self::financial_services(),
487 IndustryType::Healthcare => Self::healthcare(),
488 IndustryType::Technology => Self::technology(),
489 IndustryType::Wholesale => Self::manufacturing(), IndustryType::ProfessionalServices => Self::technology(), IndustryType::Construction => Self::manufacturing(), }
493 }
494
495 pub fn seasonality_multiplier(&self, month: u8) -> f64 {
497 self.seasonality[(month % 12) as usize]
498 }
499}
500
501#[cfg(test)]
502#[allow(clippy::unwrap_used)]
503mod tests {
504 use super::*;
505
506 #[test]
507 fn test_retail_profile() {
508 let profile = IndustryAmountProfile::retail();
509 assert_eq!(profile.industry, IndustryType::Retail);
510 assert!(profile.sales_amounts.validate().is_ok());
511 assert!(profile.purchase_amounts.validate().is_ok());
512 }
513
514 #[test]
515 fn test_manufacturing_profile() {
516 let profile = IndustryAmountProfile::manufacturing();
517 assert_eq!(profile.industry, IndustryType::Manufacturing);
518 assert!(profile.sales_amounts.validate().is_ok());
519 }
520
521 #[test]
522 fn test_financial_services_profile() {
523 let profile = IndustryAmountProfile::financial_services();
524 assert_eq!(profile.industry, IndustryType::FinancialServices);
525 assert!(profile.sales_amounts.validate().is_ok());
526 }
527
528 #[test]
529 fn test_healthcare_profile() {
530 let profile = IndustryAmountProfile::healthcare();
531 assert_eq!(profile.industry, IndustryType::Healthcare);
532 assert!(profile.sales_amounts.validate().is_ok());
533 }
534
535 #[test]
536 fn test_technology_profile() {
537 let profile = IndustryAmountProfile::technology();
538 assert_eq!(profile.industry, IndustryType::Technology);
539 assert!(profile.sales_amounts.validate().is_ok());
540 }
541
542 #[test]
543 fn test_seasonality() {
544 let retail = IndustryAmountProfile::retail();
545
546 assert_eq!(retail.seasonality_multiplier(11), 1.75);
548
549 assert_eq!(retail.seasonality_multiplier(1), 0.70);
551
552 for month in 0..12 {
554 let factor = retail.seasonality_multiplier(month);
555 assert!(factor > 0.5 && factor < 2.0);
556 }
557 }
558
559 #[test]
560 fn test_for_industry() {
561 let retail = IndustryAmountProfile::for_industry(IndustryType::Retail);
562 assert_eq!(retail.industry, IndustryType::Retail);
563
564 let tech = IndustryAmountProfile::for_industry(IndustryType::Technology);
565 assert_eq!(tech.industry, IndustryType::Technology);
566 }
567
568 #[test]
569 fn test_component_weights_sum() {
570 let profiles = [
571 IndustryAmountProfile::retail(),
572 IndustryAmountProfile::manufacturing(),
573 IndustryAmountProfile::financial_services(),
574 IndustryAmountProfile::healthcare(),
575 IndustryAmountProfile::technology(),
576 ];
577
578 for profile in &profiles {
579 let sales_sum: f64 = profile
580 .sales_amounts
581 .components
582 .iter()
583 .map(|c| c.weight)
584 .sum();
585 assert!(
586 (sales_sum - 1.0).abs() < 0.01,
587 "Sales weights should sum to 1.0"
588 );
589
590 let purchase_sum: f64 = profile
591 .purchase_amounts
592 .components
593 .iter()
594 .map(|c| c.weight)
595 .sum();
596 assert!(
597 (purchase_sum - 1.0).abs() < 0.01,
598 "Purchase weights should sum to 1.0"
599 );
600
601 let payroll_sum: f64 = profile
602 .payroll_amounts
603 .components
604 .iter()
605 .map(|c| c.weight)
606 .sum();
607 assert!(
608 (payroll_sum - 1.0).abs() < 0.01,
609 "Payroll weights should sum to 1.0"
610 );
611 }
612 }
613}