use chrono::NaiveDate;
use datasynth_core::models::{
InspectionCharacteristic, InspectionResult, InspectionType, QualityInspection,
};
use datasynth_core::utils::seeded_rng;
use datasynth_core::uuid_factory::{DeterministicUuidFactory, GeneratorType};
use rand::prelude::*;
use rand_chacha::ChaCha8Rng;
use rust_decimal::Decimal;
use smallvec::SmallVec;
use tracing::debug;
const CHARACTERISTIC_NAMES: &[&str] = &[
"Dimension A",
"Weight",
"Surface Finish",
"Tensile Strength",
"Hardness",
"Thickness",
"Diameter",
"Flatness",
"Concentricity",
"Color Consistency",
];
const DISPOSITIONS_ACCEPTED: &[&str] = &["use_as_is", "stock"];
const DISPOSITIONS_CONDITIONAL: &[&str] = &["use_as_is", "rework", "downgrade"];
const DISPOSITIONS_REJECTED: &[&str] = &["return_to_vendor", "scrap", "rework"];
pub struct QualityInspectionGenerator {
rng: ChaCha8Rng,
uuid_factory: DeterministicUuidFactory,
}
impl QualityInspectionGenerator {
pub fn new(seed: u64) -> Self {
Self {
rng: seeded_rng(seed, 0),
uuid_factory: DeterministicUuidFactory::new(seed, GeneratorType::QualityInspection),
}
}
pub fn generate(
&mut self,
company_code: &str,
production_orders: &[(String, String, String)],
inspection_date: NaiveDate,
) -> Vec<QualityInspection> {
debug!(company_code, order_count = production_orders.len(), %inspection_date, "Generating quality inspections");
production_orders
.iter()
.map(|(order_id, material_id, material_desc)| {
self.generate_one(
company_code,
order_id,
material_id,
material_desc,
inspection_date,
)
})
.collect()
}
fn generate_one(
&mut self,
company_code: &str,
order_id: &str,
material_id: &str,
material_description: &str,
inspection_date: NaiveDate,
) -> QualityInspection {
let inspection_id = self.uuid_factory.next().to_string();
let inspection_type = self.pick_inspection_type();
let lot_size_f64: f64 = self.rng.random_range(50.0..=1000.0);
let lot_size = Decimal::from_f64_retain(lot_size_f64.round()).unwrap_or(Decimal::from(100));
let sample_pct: f64 = self.rng.random_range(0.10..=0.30);
let sample_size_f64 = (lot_size_f64 * sample_pct).round().max(1.0);
let sample_size = Decimal::from_f64_retain(sample_size_f64).unwrap_or(Decimal::from(10));
let num_characteristics: usize = self.rng.random_range(2..=5);
let characteristics = self.generate_characteristics(num_characteristics);
let defect_count = characteristics.iter().filter(|c| !c.passed).count() as u32;
let defect_rate = if sample_size_f64 > 0.0 {
defect_count as f64 / sample_size_f64
} else {
0.0
};
let result = self.pick_result();
let inspector_id = Some(format!("QC-{:02}", self.rng.random_range(1..=20)));
let disposition = match result {
InspectionResult::Accepted => DISPOSITIONS_ACCEPTED
.choose(&mut self.rng)
.map(std::string::ToString::to_string),
InspectionResult::Conditionally => DISPOSITIONS_CONDITIONAL
.choose(&mut self.rng)
.map(std::string::ToString::to_string),
InspectionResult::Rejected => DISPOSITIONS_REJECTED
.choose(&mut self.rng)
.map(std::string::ToString::to_string),
InspectionResult::Pending => None,
};
let notes = match result {
InspectionResult::Rejected => Some(format!(
"{defect_count} defects found in {num_characteristics} characteristics. Material held for disposition."
)),
InspectionResult::Conditionally => Some(format!(
"Minor deviations noted. {defect_count} characteristic(s) marginally out of spec."
)),
_ => None,
};
QualityInspection {
inspection_id,
company_code: company_code.to_string(),
reference_type: "production_order".to_string(),
reference_id: order_id.to_string(),
material_id: material_id.to_string(),
material_description: material_description.to_string(),
inspection_type,
inspection_date,
inspector_id,
lot_size,
sample_size,
defect_count,
defect_rate,
result,
characteristics,
disposition,
notes,
}
}
fn pick_inspection_type(&mut self) -> InspectionType {
let roll: f64 = self.rng.random();
if roll < 0.40 {
InspectionType::Final
} else if roll < 0.65 {
InspectionType::InProcess
} else if roll < 0.85 {
InspectionType::Incoming
} else if roll < 0.95 {
InspectionType::Random
} else {
InspectionType::Periodic
}
}
fn pick_result(&mut self) -> InspectionResult {
let roll: f64 = self.rng.random();
if roll < 0.80 {
InspectionResult::Accepted
} else if roll < 0.90 {
InspectionResult::Conditionally
} else if roll < 0.97 {
InspectionResult::Rejected
} else {
InspectionResult::Pending
}
}
fn generate_characteristics(
&mut self,
count: usize,
) -> SmallVec<[InspectionCharacteristic; 4]> {
let mut indices: Vec<usize> = (0..CHARACTERISTIC_NAMES.len()).collect();
indices.shuffle(&mut self.rng);
let selected_count = count.min(indices.len());
indices[..selected_count]
.iter()
.map(|&idx| {
let name = CHARACTERISTIC_NAMES[idx].to_string();
let target_value: f64 = self.rng.random_range(10.0..=100.0);
let tolerance_pct: f64 = self.rng.random_range(0.05..=0.15);
let lower_limit = target_value * (1.0 - tolerance_pct);
let upper_limit = target_value * (1.0 + tolerance_pct);
let actual_factor: f64 = self.rng.random_range(0.95..=1.05);
let actual_value = target_value * actual_factor;
let passed = actual_value >= lower_limit && actual_value <= upper_limit;
InspectionCharacteristic {
name,
target_value,
actual_value,
lower_limit,
upper_limit,
passed,
}
})
.collect()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
fn sample_orders() -> Vec<(String, String, String)> {
vec![
(
"PO-001".to_string(),
"MAT-001".to_string(),
"Widget Alpha".to_string(),
),
(
"PO-002".to_string(),
"MAT-002".to_string(),
"Widget Beta".to_string(),
),
(
"PO-003".to_string(),
"MAT-003".to_string(),
"Widget Gamma".to_string(),
),
]
}
#[test]
fn test_basic_generation() {
let mut gen = QualityInspectionGenerator::new(42);
let orders = sample_orders();
let date = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
let inspections = gen.generate("C001", &orders, date);
assert_eq!(inspections.len(), orders.len());
for insp in &inspections {
assert_eq!(insp.company_code, "C001");
assert_eq!(insp.inspection_date, date);
assert!(!insp.inspection_id.is_empty());
assert_eq!(insp.reference_type, "production_order");
assert!(insp.lot_size > Decimal::ZERO);
assert!(insp.sample_size > Decimal::ZERO);
assert!(insp.sample_size <= insp.lot_size);
assert!(!insp.characteristics.is_empty());
assert!(insp.characteristics.len() >= 2 && insp.characteristics.len() <= 5);
assert!(insp.inspector_id.is_some());
}
}
#[test]
fn test_deterministic() {
let orders = sample_orders();
let date = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
let mut gen1 = QualityInspectionGenerator::new(12345);
let insp1 = gen1.generate("C001", &orders, date);
let mut gen2 = QualityInspectionGenerator::new(12345);
let insp2 = gen2.generate("C001", &orders, date);
assert_eq!(insp1.len(), insp2.len());
for (i1, i2) in insp1.iter().zip(insp2.iter()) {
assert_eq!(i1.inspection_id, i2.inspection_id);
assert_eq!(i1.lot_size, i2.lot_size);
assert_eq!(i1.sample_size, i2.sample_size);
assert_eq!(i1.defect_count, i2.defect_count);
assert_eq!(i1.characteristics.len(), i2.characteristics.len());
}
}
#[test]
fn test_characteristics_limits() {
let mut gen = QualityInspectionGenerator::new(99);
let orders = sample_orders();
let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
let inspections = gen.generate("C001", &orders, date);
for insp in &inspections {
for char in &insp.characteristics {
assert!(
char.lower_limit < char.target_value,
"Lower limit {} should be below target {}",
char.lower_limit,
char.target_value,
);
assert!(
char.upper_limit > char.target_value,
"Upper limit {} should be above target {}",
char.upper_limit,
char.target_value,
);
let within_limits =
char.actual_value >= char.lower_limit && char.actual_value <= char.upper_limit;
assert_eq!(
char.passed, within_limits,
"Passed flag ({}) inconsistent with limits for {}",
char.passed, char.name,
);
}
}
}
}