#![allow(clippy::unwrap_used)]
use std::collections::HashSet;
use chrono::NaiveDate;
use datasynth_config::schema::{ManufacturingCostingConfig, ProductionOrderConfig, RoutingConfig};
use datasynth_generators::{
BomGenerator, InventoryMovementGenerator, MaterialGenerator, ProductionOrderGenerator,
QualityInspectionGenerator,
};
use rust_decimal::Decimal;
fn date(y: i32, m: u32, d: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(y, m, d).unwrap()
}
#[test]
fn test_manufacturing_pipeline_coherence() {
let seed = 42u64;
let company_code = "C001";
let start_date = date(2025, 1, 1);
let end_date = date(2025, 3, 31);
let currency = "USD";
let mut mat_gen = MaterialGenerator::new(seed);
let material_pool = mat_gen.generate_material_pool(10, company_code, start_date);
assert_eq!(
material_pool.materials.len(),
10,
"Should generate 10 materials"
);
let material_id_set: HashSet<&str> = material_pool
.materials
.iter()
.map(|m| m.material_id.as_str())
.collect();
let material_tuples: Vec<(String, String)> = material_pool
.materials
.iter()
.map(|m| (m.material_id.clone(), m.description.clone()))
.collect();
let mut prod_gen = ProductionOrderGenerator::new(seed + 1);
let prod_config = ProductionOrderConfig {
orders_per_month: 5,
..Default::default()
};
let costing = ManufacturingCostingConfig::default();
let routing = RoutingConfig::default();
let orders = prod_gen.generate(
company_code,
&material_tuples,
start_date,
end_date,
&prod_config,
&costing,
&routing,
);
assert_eq!(orders.len(), 15, "Should generate 15 production orders");
let order_id_set: HashSet<&str> = orders.iter().map(|o| o.order_id.as_str()).collect();
for order in &orders {
assert_eq!(order.company_code, company_code);
assert!(
material_id_set.contains(order.material_id.as_str()),
"Production order references unknown material: {}",
order.material_id
);
assert!(
order.planned_quantity > Decimal::ZERO,
"Planned quantity should be positive"
);
assert!(
order.planned_cost > Decimal::ZERO,
"Planned cost should be positive"
);
assert!(
!order.operations.is_empty(),
"Production order should have routing operations"
);
assert!(order.labor_hours > 0.0, "Labor hours should be positive");
assert!(
order.machine_hours > 0.0,
"Machine hours should be positive"
);
}
let mut qi_gen = QualityInspectionGenerator::new(seed + 2);
let order_tuples: Vec<(String, String, String)> = orders
.iter()
.map(|o| {
(
o.order_id.clone(),
o.material_id.clone(),
o.material_description.clone(),
)
})
.collect();
let inspections = qi_gen.generate(company_code, &order_tuples, date(2025, 3, 31));
assert_eq!(
inspections.len(),
orders.len(),
"Should generate one inspection per production order"
);
for insp in &inspections {
assert_eq!(insp.company_code, company_code);
assert!(
order_id_set.contains(insp.reference_id.as_str()),
"Quality inspection references unknown production order: {}",
insp.reference_id
);
assert_eq!(
insp.reference_type, "production_order",
"Reference type should be production_order"
);
assert!(
material_id_set.contains(insp.material_id.as_str()),
"Quality inspection references unknown material: {}",
insp.material_id
);
assert!(insp.lot_size > Decimal::ZERO, "Lot size should be positive");
assert!(
insp.sample_size > Decimal::ZERO,
"Sample size should be positive"
);
assert!(
insp.sample_size <= insp.lot_size,
"Sample size ({}) should not exceed lot size ({})",
insp.sample_size,
insp.lot_size
);
assert!(
!insp.characteristics.is_empty(),
"Inspection should have characteristics"
);
assert!(
insp.characteristics.len() >= 2 && insp.characteristics.len() <= 5,
"Should have 2-5 characteristics, got {}",
insp.characteristics.len()
);
for c in &insp.characteristics {
assert!(
c.lower_limit < c.target_value,
"Lower limit should be below target"
);
assert!(
c.upper_limit > c.target_value,
"Upper limit should be above target"
);
let within = c.actual_value >= c.lower_limit && c.actual_value <= c.upper_limit;
assert_eq!(
c.passed, within,
"Passed flag inconsistent with limits for characteristic '{}'",
c.name
);
}
}
let mut bom_gen = BomGenerator::new(seed + 3);
let bom_components = bom_gen.generate(company_code, &material_tuples);
assert!(
!bom_components.is_empty(),
"Should generate BOM components for 10 materials"
);
for comp in &bom_components {
assert!(
material_id_set.contains(comp.component_material_id.as_str()),
"BOM component references unknown material: {}",
comp.component_material_id
);
assert!(
comp.quantity > Decimal::ZERO,
"BOM component quantity should be positive"
);
assert!(comp.id.is_some(), "BOM component should have an ID");
assert!(
comp.parent_material.is_some(),
"BOM component should have a parent material"
);
let parent = comp.parent_material.as_ref().unwrap();
assert!(
material_id_set.contains(parent.as_str()),
"BOM parent material is unknown: {}",
parent
);
assert_ne!(
comp.component_material_id, *parent,
"BOM component should not be its own parent"
);
}
let mut inv_gen = InventoryMovementGenerator::new(seed + 4);
let movements = inv_gen.generate(
company_code,
&material_tuples,
start_date,
end_date,
3, currency,
);
assert!(!movements.is_empty(), "Should generate inventory movements");
for mv in &movements {
assert_eq!(mv.entity_code, company_code);
assert_eq!(mv.currency, currency);
assert!(
material_id_set.contains(mv.material_code.as_str()),
"Inventory movement references unknown material: {}",
mv.material_code
);
assert!(
mv.quantity > Decimal::ZERO,
"Movement quantity should be positive"
);
assert!(
mv.value > Decimal::ZERO,
"Movement value should be positive"
);
assert!(
mv.movement_date >= start_date && mv.movement_date <= end_date,
"Movement date {} outside expected range [{}, {}]",
mv.movement_date,
start_date,
end_date
);
assert!(
!mv.reference_doc.is_empty(),
"Movement should have a reference document"
);
}
println!(
"MFG pipeline coherence OK: {} materials -> {} orders, {} inspections, {} BOM components, {} movements",
material_pool.materials.len(),
orders.len(),
inspections.len(),
bom_components.len(),
movements.len()
);
}
#[test]
fn test_manufacturing_pipeline_deterministic() {
let seed = 55u64;
let company_code = "C001";
let start = date(2025, 1, 1);
let end = date(2025, 1, 31);
let generate = |s: u64| {
let mut mat_gen = MaterialGenerator::new(s);
let pool = mat_gen.generate_material_pool(5, company_code, start);
let material_tuples: Vec<(String, String)> = pool
.materials
.iter()
.map(|m| (m.material_id.clone(), m.description.clone()))
.collect();
let mut prod_gen = ProductionOrderGenerator::new(s + 1);
let config = ProductionOrderConfig {
orders_per_month: 3,
..Default::default()
};
let costing = ManufacturingCostingConfig::default();
let routing = RoutingConfig::default();
let orders = prod_gen.generate(
company_code,
&material_tuples,
start,
end,
&config,
&costing,
&routing,
);
let mut bom_gen = BomGenerator::new(s + 2);
let bom = bom_gen.generate(company_code, &material_tuples);
let mut inv_gen = InventoryMovementGenerator::new(s + 3);
let movements = inv_gen.generate(company_code, &material_tuples, start, end, 2, "USD");
(
pool.materials.len(),
orders.len(),
orders.first().map(|o| o.order_id.clone()),
bom.len(),
movements.len(),
)
};
let run1 = generate(seed);
let run2 = generate(seed);
assert_eq!(
run1, run2,
"Deterministic runs should produce identical results"
);
}
#[test]
fn test_production_orders_with_empty_materials() {
let mut gen = ProductionOrderGenerator::new(42);
let empty: Vec<(String, String)> = vec![];
let config = ProductionOrderConfig::default();
let costing = ManufacturingCostingConfig::default();
let routing = RoutingConfig::default();
let orders = gen.generate(
"C001",
&empty,
date(2025, 1, 1),
date(2025, 1, 31),
&config,
&costing,
&routing,
);
assert!(
orders.is_empty(),
"Should return empty orders for empty material list"
);
}
#[test]
fn test_bom_with_few_materials() {
let mut gen = BomGenerator::new(42);
let materials = vec![
("MAT-001".to_string(), "Widget".to_string()),
("MAT-002".to_string(), "Gadget".to_string()),
];
let bom = gen.generate("C001", &materials);
assert!(
bom.is_empty(),
"BOM generator should return empty for < 3 materials"
);
}