datasynth_eval/coherence/
inventory_cogs.rs1use rust_decimal::Decimal;
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone)]
16pub struct InventoryCOGSData {
17 pub opening_fg: Decimal,
19 pub production_completions: Decimal,
21 pub cogs: Decimal,
23 pub scrap: Decimal,
25 pub closing_fg: Decimal,
27
28 pub opening_wip: Decimal,
30 pub material_issues: Decimal,
32 pub labor_absorbed: Decimal,
34 pub overhead_applied: Decimal,
36 pub completions_out_of_wip: Decimal,
38 pub wip_scrap: Decimal,
40 pub closing_wip: Decimal,
42
43 pub total_variance: Decimal,
45 pub sum_of_component_variances: Decimal,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct InventoryCOGSEvaluation {
52 pub fg_reconciled: bool,
54 pub fg_imbalance: Decimal,
56 pub fg_expected_closing: Decimal,
58
59 pub wip_reconciled: bool,
61 pub wip_imbalance: Decimal,
63 pub wip_expected_closing: Decimal,
65
66 pub variances_reconciled: bool,
68 pub variance_difference: Decimal,
70
71 pub passes: bool,
73 pub failures: Vec<String>,
75}
76
77pub struct InventoryCOGSEvaluator {
79 tolerance: Decimal,
80}
81
82impl InventoryCOGSEvaluator {
83 pub fn new(tolerance: Decimal) -> Self {
85 Self { tolerance }
86 }
87
88 pub fn evaluate(&self, data: &InventoryCOGSData) -> InventoryCOGSEvaluation {
90 let mut failures = Vec::new();
91
92 let fg_expected_closing =
95 data.opening_fg + data.production_completions - data.cogs - data.scrap;
96 let fg_imbalance = (fg_expected_closing - data.closing_fg).abs();
97 let fg_reconciled = fg_imbalance <= self.tolerance;
98 if !fg_reconciled {
99 failures.push(format!(
100 "FG reconciliation failed: expected closing FG {} but got {} (imbalance {})",
101 fg_expected_closing, data.closing_fg, fg_imbalance
102 ));
103 }
104
105 let wip_expected_closing =
108 data.opening_wip + data.material_issues + data.labor_absorbed + data.overhead_applied
109 - data.completions_out_of_wip
110 - data.wip_scrap;
111 let wip_imbalance = (wip_expected_closing - data.closing_wip).abs();
112 let wip_reconciled = wip_imbalance <= self.tolerance;
113 if !wip_reconciled {
114 failures.push(format!(
115 "WIP reconciliation failed: expected closing WIP {} but got {} (imbalance {})",
116 wip_expected_closing, data.closing_wip, wip_imbalance
117 ));
118 }
119
120 let variance_difference = (data.total_variance - data.sum_of_component_variances).abs();
123 let variances_reconciled = variance_difference <= self.tolerance;
124 if !variances_reconciled {
125 failures.push(format!(
126 "Variance reconciliation failed: total variance {} != component sum {} (difference {})",
127 data.total_variance, data.sum_of_component_variances, variance_difference
128 ));
129 }
130
131 let passes = failures.is_empty();
132
133 InventoryCOGSEvaluation {
134 fg_reconciled,
135 fg_imbalance,
136 fg_expected_closing,
137 wip_reconciled,
138 wip_imbalance,
139 wip_expected_closing,
140 variances_reconciled,
141 variance_difference,
142 passes,
143 failures,
144 }
145 }
146}
147
148impl Default for InventoryCOGSEvaluator {
149 fn default() -> Self {
150 Self::new(Decimal::new(1, 2)) }
152}
153
154#[derive(Debug, Clone)]
158pub struct ICEliminationData {
159 pub matched_pair_count: usize,
161 pub elimination_entry_count: usize,
163 pub total_ic_amount: Decimal,
165 pub total_elimination_amount: Decimal,
167 pub pairs_with_eliminations: usize,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct ICEliminationEvaluation {
174 pub all_pairs_eliminated: bool,
176 pub coverage_rate: f64,
178 pub amount_reconciled: bool,
180 pub amount_difference: Decimal,
182 pub passes: bool,
184 pub failures: Vec<String>,
186}
187
188pub struct ICEliminationEvaluator {
190 tolerance: Decimal,
191}
192
193impl ICEliminationEvaluator {
194 pub fn new(tolerance: Decimal) -> Self {
196 Self { tolerance }
197 }
198
199 pub fn evaluate(&self, data: &ICEliminationData) -> ICEliminationEvaluation {
201 let mut failures = Vec::new();
202
203 let coverage_rate = if data.matched_pair_count == 0 {
205 1.0
206 } else {
207 data.pairs_with_eliminations as f64 / data.matched_pair_count as f64
208 };
209 let all_pairs_eliminated = data.pairs_with_eliminations >= data.matched_pair_count;
210 if !all_pairs_eliminated {
211 failures.push(format!(
212 "IC elimination incomplete: {}/{} pairs have eliminations (coverage {:.1}%)",
213 data.pairs_with_eliminations,
214 data.matched_pair_count,
215 coverage_rate * 100.0
216 ));
217 }
218
219 let amount_difference = (data.total_ic_amount - data.total_elimination_amount).abs();
221 let amount_reconciled = amount_difference <= self.tolerance;
222 if !amount_reconciled {
223 failures.push(format!(
224 "IC elimination amount mismatch: IC amount {} vs elimination amount {} (difference {})",
225 data.total_ic_amount, data.total_elimination_amount, amount_difference
226 ));
227 }
228
229 let passes = failures.is_empty();
230
231 ICEliminationEvaluation {
232 all_pairs_eliminated,
233 coverage_rate,
234 amount_reconciled,
235 amount_difference,
236 passes,
237 failures,
238 }
239 }
240}
241
242impl Default for ICEliminationEvaluator {
243 fn default() -> Self {
244 Self::new(Decimal::new(1, 2)) }
246}
247
248#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn test_cogs_all_balanced() {
256 let data = InventoryCOGSData {
257 opening_fg: Decimal::new(100_000, 0),
258 production_completions: Decimal::new(500_000, 0),
259 cogs: Decimal::new(450_000, 0),
260 scrap: Decimal::new(10_000, 0),
261 closing_fg: Decimal::new(140_000, 0),
262 opening_wip: Decimal::new(50_000, 0),
263 material_issues: Decimal::new(300_000, 0),
264 labor_absorbed: Decimal::new(150_000, 0),
265 overhead_applied: Decimal::new(100_000, 0),
266 completions_out_of_wip: Decimal::new(500_000, 0),
267 wip_scrap: Decimal::new(5_000, 0),
268 closing_wip: Decimal::new(95_000, 0),
269 total_variance: Decimal::new(15_000, 0),
270 sum_of_component_variances: Decimal::new(15_000, 0),
271 };
272
273 let eval = InventoryCOGSEvaluator::new(Decimal::new(1, 0));
274 let result = eval.evaluate(&data);
275 assert!(result.passes);
276 assert!(result.fg_reconciled);
277 assert!(result.wip_reconciled);
278 assert!(result.variances_reconciled);
279 }
280
281 #[test]
282 fn test_fg_imbalanced() {
283 let data = InventoryCOGSData {
284 opening_fg: Decimal::new(100_000, 0),
285 production_completions: Decimal::new(500_000, 0),
286 cogs: Decimal::new(450_000, 0),
287 scrap: Decimal::new(10_000, 0),
288 closing_fg: Decimal::new(999_000, 0), opening_wip: Decimal::ZERO,
290 material_issues: Decimal::ZERO,
291 labor_absorbed: Decimal::ZERO,
292 overhead_applied: Decimal::ZERO,
293 completions_out_of_wip: Decimal::ZERO,
294 wip_scrap: Decimal::ZERO,
295 closing_wip: Decimal::ZERO,
296 total_variance: Decimal::ZERO,
297 sum_of_component_variances: Decimal::ZERO,
298 };
299
300 let eval = InventoryCOGSEvaluator::new(Decimal::new(1, 0));
301 let result = eval.evaluate(&data);
302 assert!(!result.fg_reconciled);
303 assert!(!result.passes);
304 assert!(!result.failures.is_empty());
305 }
306
307 #[test]
308 fn test_ic_elimination_complete() {
309 let data = ICEliminationData {
310 matched_pair_count: 10,
311 elimination_entry_count: 10,
312 total_ic_amount: Decimal::new(1_000_000, 0),
313 total_elimination_amount: Decimal::new(1_000_000, 0),
314 pairs_with_eliminations: 10,
315 };
316
317 let eval = ICEliminationEvaluator::new(Decimal::new(1, 0));
318 let result = eval.evaluate(&data);
319 assert!(result.passes);
320 assert!(result.all_pairs_eliminated);
321 assert!((result.coverage_rate - 1.0).abs() < f64::EPSILON);
322 }
323
324 #[test]
325 fn test_ic_elimination_incomplete() {
326 let data = ICEliminationData {
327 matched_pair_count: 10,
328 elimination_entry_count: 7,
329 total_ic_amount: Decimal::new(1_000_000, 0),
330 total_elimination_amount: Decimal::new(700_000, 0),
331 pairs_with_eliminations: 7,
332 };
333
334 let eval = ICEliminationEvaluator::new(Decimal::new(1, 0));
335 let result = eval.evaluate(&data);
336 assert!(!result.passes);
337 assert!(!result.all_pairs_eliminated);
338 assert_eq!(result.coverage_rate, 0.7);
339 }
340}