1use std::collections::HashSet;
28
29use datasynth_core::models::audit::risk_assessment_cra::{
30 AuditAssertion, CombinedRiskAssessment, CraLevel,
31};
32use datasynth_core::models::audit::sampling_plan::{
33 KeyItem, KeyItemReason, SampledItem, SamplingMethodology, SamplingPlan, SelectionType,
34};
35use datasynth_core::models::journal_entry::{JournalEntry, JournalEntryLine};
36use datasynth_core::utils::seeded_rng;
37use rand::RngExt;
38use rand_chacha::ChaCha8Rng;
39use rust_decimal::Decimal;
40use rust_decimal_macros::dec;
41use tracing::info;
42
43fn methodology_for_assertion(assertion: AuditAssertion, cra: CraLevel) -> SamplingMethodology {
53 use AuditAssertion::*;
54 if cra == CraLevel::Minimal {
55 return SamplingMethodology::HaphazardSelection;
56 }
57 match assertion {
58 Existence | ValuationAndAllocation | RightsAndObligations | CompletenessBalance => {
60 SamplingMethodology::MonetaryUnitSampling
61 }
62 PresentationAndDisclosure => SamplingMethodology::RandomSelection,
64 Occurrence | Completeness | Accuracy | Cutoff | Classification => {
66 SamplingMethodology::SystematicSelection
67 }
68 }
69}
70
71fn sample_size_for_cra(rng: &mut ChaCha8Rng, cra: CraLevel) -> usize {
73 match cra {
74 CraLevel::Minimal => 0,
75 CraLevel::Low => rng.random_range(10usize..=15),
76 CraLevel::Moderate => rng.random_range(20usize..=30),
77 CraLevel::High => rng.random_range(40usize..=60),
78 }
79}
80
81fn misstatement_rate(cra: CraLevel) -> f64 {
83 match cra {
84 CraLevel::Minimal => 0.02,
85 CraLevel::Low => 0.04,
86 CraLevel::Moderate => 0.08,
87 CraLevel::High => 0.15,
88 }
89}
90
91fn account_area_to_prefixes(account_area: &str) -> Vec<&'static str> {
93 let lower = account_area.to_lowercase();
94 if lower.contains("revenue") || lower.contains("sales") {
95 vec!["4"]
96 } else if lower.contains("receivable") {
97 vec!["11"]
98 } else if lower.contains("payable") {
99 vec!["20"]
100 } else if lower.contains("inventory") || lower.contains("stock") {
101 vec!["12", "13"]
102 } else if lower.contains("cash") || lower.contains("bank") {
103 vec!["10"]
104 } else if lower.contains("fixed asset") || lower.contains("ppe") || lower.contains("property") {
105 vec!["14", "15", "16"]
106 } else if lower.contains("equity") || lower.contains("capital") {
107 vec!["3"]
108 } else if lower.contains("expense") || lower.contains("cost") {
109 vec!["5", "6"]
110 } else if lower.contains("debt") || lower.contains("loan") || lower.contains("borrow") {
111 vec!["23", "24"]
112 } else if lower.contains("tax") {
113 vec!["17", "25"]
114 } else if lower.contains("provision") {
115 vec!["26"]
116 } else if lower.contains("intangible") || lower.contains("goodwill") {
117 vec!["19"]
118 } else if lower.contains("interest") {
119 vec!["71"]
120 } else if lower.contains("other income") || lower.contains("other expense") {
121 vec!["7"]
122 } else if lower.contains("depreciation") || lower.contains("amortization") {
123 vec!["60"]
124 } else if lower.contains("salary") || lower.contains("wages") || lower.contains("payroll") {
125 vec!["61"]
126 } else if lower.contains("rent") || lower.contains("lease") {
127 vec!["63"]
128 } else {
129 vec![] }
131}
132
133fn filter_je_lines_for_area<'a>(
136 entries: &'a [JournalEntry],
137 account_area: &str,
138) -> Vec<(&'a JournalEntry, &'a JournalEntryLine, Decimal)> {
139 let prefixes = account_area_to_prefixes(account_area);
140 let mut results = Vec::new();
141
142 for je in entries {
143 for line in &je.lines {
144 let matches = if prefixes.is_empty() {
145 true
146 } else {
147 prefixes.iter().any(|p| line.account_code.starts_with(p))
148 };
149 if matches {
150 let amount = (line.debit_amount - line.credit_amount).abs();
151 if amount > Decimal::ZERO {
152 results.push((je, line, amount));
153 }
154 }
155 }
156 }
157 results
158}
159
160#[derive(Debug, Clone)]
166pub struct SamplingPlanGeneratorConfig {
167 pub key_item_fraction: f64,
170 pub min_population_size: usize,
172 pub max_population_size: usize,
174 pub base_population_value: Decimal,
176}
177
178impl Default for SamplingPlanGeneratorConfig {
179 fn default() -> Self {
180 Self {
181 key_item_fraction: 0.05, min_population_size: 100,
183 max_population_size: 2_000,
184 base_population_value: dec!(5_000_000),
185 }
186 }
187}
188
189pub struct SamplingPlanGenerator {
195 rng: ChaCha8Rng,
196 config: SamplingPlanGeneratorConfig,
197}
198
199impl SamplingPlanGenerator {
200 pub fn new(seed: u64) -> Self {
202 Self {
203 rng: seeded_rng(seed, 0x530), config: SamplingPlanGeneratorConfig::default(),
205 }
206 }
207
208 pub fn with_config(seed: u64, config: SamplingPlanGeneratorConfig) -> Self {
210 Self {
211 rng: seeded_rng(seed, 0x530),
212 config,
213 }
214 }
215
216 pub fn generate_for_cras(
225 &mut self,
226 cras: &[CombinedRiskAssessment],
227 tolerable_error: Option<Decimal>,
228 ) -> (Vec<SamplingPlan>, Vec<SampledItem>) {
229 info!("Generating sampling plans for {} CRAs", cras.len());
230 let mut plans: Vec<SamplingPlan> = Vec::new();
231 let mut all_items: Vec<SampledItem> = Vec::new();
232
233 for cra in cras {
234 if cra.combined_risk < CraLevel::Moderate {
236 continue;
237 }
238
239 let te =
240 tolerable_error.unwrap_or_else(|| self.config.base_population_value * dec!(0.05));
241
242 let (plan, items) = self.generate_plan(cra, te);
243 all_items.extend(items);
244 plans.push(plan);
245 }
246
247 info!(
248 "Generated {} sampling plans with {} sampled items",
249 plans.len(),
250 all_items.len()
251 );
252 (plans, all_items)
253 }
254
255 fn generate_plan(
257 &mut self,
258 cra: &CombinedRiskAssessment,
259 tolerable_error: Decimal,
260 ) -> (SamplingPlan, Vec<SampledItem>) {
261 let methodology = methodology_for_assertion(cra.assertion, cra.combined_risk);
262 let rep_sample_size = sample_size_for_cra(&mut self.rng, cra.combined_risk);
263
264 let pop_size = self
266 .rng
267 .random_range(self.config.min_population_size..=self.config.max_population_size);
268 let pop_value = self.synthetic_population_value(pop_size);
269
270 let key_items = self.generate_key_items(pop_size, pop_value, tolerable_error, cra);
272 let key_items_value: Decimal = key_items.iter().map(|k| k.amount).sum();
273 let remaining_value = (pop_value - key_items_value).max(Decimal::ZERO);
274
275 let sampling_interval = if rep_sample_size > 0 && remaining_value > Decimal::ZERO {
277 remaining_value / Decimal::from(rep_sample_size as i64)
278 } else {
279 Decimal::ZERO
280 };
281
282 let plan_id = format!(
283 "SP-{}-{}-{}",
284 cra.entity_code,
285 cra.account_area.replace(' ', "_").to_uppercase(),
286 format!("{:?}", cra.assertion).to_uppercase(),
287 );
288
289 let plan = SamplingPlan {
290 id: plan_id.clone(),
291 entity_code: cra.entity_code.clone(),
292 account_area: cra.account_area.clone(),
293 assertion: format!("{}", cra.assertion),
294 methodology,
295 population_size: pop_size,
296 population_value: pop_value,
297 key_items: key_items.clone(),
298 key_items_value,
299 remaining_population_value: remaining_value,
300 sample_size: rep_sample_size,
301 sampling_interval,
302 cra_level: cra.combined_risk.to_string(),
303 tolerable_error,
304 };
305
306 let mut sampled_items: Vec<SampledItem> = Vec::new();
308 let misstatement_p = misstatement_rate(cra.combined_risk);
309
310 for ki in &key_items {
312 let misstatement_found: bool = self.rng.random::<f64>() < misstatement_p;
313 let misstatement_amount = if misstatement_found {
314 let pct = Decimal::try_from(self.rng.random_range(0.01_f64..=0.15_f64))
315 .unwrap_or(dec!(0.05));
316 Some((ki.amount * pct).round_dp(2))
317 } else {
318 None
319 };
320
321 sampled_items.push(SampledItem {
322 item_id: ki.item_id.clone(),
323 sampling_plan_id: plan_id.clone(),
324 amount: ki.amount,
325 selection_type: SelectionType::KeyItem,
326 key_item_reason: Some(ki.reason),
327 tested: true,
328 misstatement_found,
329 misstatement_amount,
330 });
331 }
332
333 if rep_sample_size > 0 && remaining_value > Decimal::ZERO {
335 let avg_remaining_item_value =
336 remaining_value / Decimal::from((pop_size - key_items.len()).max(1) as i64);
337
338 for i in 0..rep_sample_size {
339 let item_id = format!("{plan_id}-REP-{i:04}");
340 let jitter_pct = Decimal::try_from(self.rng.random_range(0.5_f64..=2.0_f64))
342 .unwrap_or(Decimal::ONE);
343 let amount = (avg_remaining_item_value * jitter_pct)
344 .round_dp(2)
345 .max(dec!(1));
346
347 let misstatement_found: bool = self.rng.random::<f64>() < misstatement_p;
348 let misstatement_amount = if misstatement_found {
349 let pct = Decimal::try_from(self.rng.random_range(0.01_f64..=0.30_f64))
350 .unwrap_or(dec!(0.05));
351 Some((amount * pct).round_dp(2))
352 } else {
353 None
354 };
355
356 sampled_items.push(SampledItem {
357 item_id,
358 sampling_plan_id: plan_id.clone(),
359 amount,
360 selection_type: SelectionType::Representative,
361 key_item_reason: None,
362 tested: true,
363 misstatement_found,
364 misstatement_amount,
365 });
366 }
367 }
368
369 (plan, sampled_items)
370 }
371
372 fn synthetic_population_value(&mut self, pop_size: usize) -> Decimal {
374 let avg_item = self.rng.random_range(500_i64..=50_000);
376 let raw = Decimal::from(pop_size as i64) * Decimal::from(avg_item);
377 ((raw / dec!(1000)).round() * dec!(1000)).max(dec!(10_000))
379 }
380
381 fn generate_key_items(
387 &mut self,
388 pop_size: usize,
389 pop_value: Decimal,
390 tolerable_error: Decimal,
391 cra: &CombinedRiskAssessment,
392 ) -> Vec<KeyItem> {
393 let fraction = match cra.combined_risk {
394 CraLevel::High => self.config.key_item_fraction * 2.0,
395 _ => self.config.key_item_fraction,
396 };
397 let n_key_items = ((pop_size as f64 * fraction) as usize).clamp(1, 20);
398
399 let avg_key_value = pop_value
401 * Decimal::try_from(self.config.key_item_fraction * 3.0).unwrap_or(dec!(0.15))
402 / Decimal::from(n_key_items as i64);
403 let key_item_min = tolerable_error * dec!(1.01); let key_item_max = (avg_key_value * dec!(2)).max(key_item_min * dec!(2)); let mut items = Vec::with_capacity(n_key_items);
407 for i in 0..n_key_items {
408 let amount_f = self.rng.random_range(
409 key_item_min.to_string().parse::<f64>().unwrap_or(10_000.0)
410 ..=key_item_max.to_string().parse::<f64>().unwrap_or(500_000.0),
411 );
412 let amount = Decimal::try_from(amount_f)
413 .unwrap_or(key_item_min)
414 .round_dp(2)
415 .max(key_item_min);
416
417 let reason = self.pick_key_item_reason(cra, i);
418
419 items.push(KeyItem {
420 item_id: format!(
421 "{}-{}-KEY-{i:03}",
422 cra.entity_code,
423 cra.account_area.replace(' ', "_").to_uppercase()
424 ),
425 amount,
426 reason,
427 });
428 }
429
430 let key_total: Decimal = items.iter().map(|k| k.amount).sum();
434 if key_total > pop_value {
435 let scale = (pop_value * dec!(0.8)) / key_total;
436 for item in &mut items {
437 item.amount = (item.amount * scale).round_dp(2);
438 }
439 }
440
441 items
442 }
443
444 fn pick_key_item_reason(
446 &mut self,
447 cra: &CombinedRiskAssessment,
448 index: usize,
449 ) -> KeyItemReason {
450 if index == 0 {
452 return KeyItemReason::AboveTolerableError;
453 }
454 if cra.significant_risk {
456 let roll: f64 = self.rng.random();
457 if roll < 0.40 {
458 return KeyItemReason::ManagementOverride;
459 }
460 if roll < 0.70 {
461 return KeyItemReason::HighRisk;
462 }
463 }
464 let roll: f64 = self.rng.random();
465 if roll < 0.60 {
466 KeyItemReason::AboveTolerableError
467 } else if roll < 0.80 {
468 KeyItemReason::UnusualNature
469 } else {
470 KeyItemReason::HighRisk
471 }
472 }
473
474 pub fn generate_for_cras_with_population(
484 &mut self,
485 cras: &[CombinedRiskAssessment],
486 tolerable_error: Option<Decimal>,
487 journal_entries: &[JournalEntry],
488 ) -> (Vec<SamplingPlan>, Vec<SampledItem>) {
489 info!(
490 "Generating JE-aware sampling plans for {} CRAs against {} journal entries",
491 cras.len(),
492 journal_entries.len()
493 );
494 let mut plans: Vec<SamplingPlan> = Vec::new();
495 let mut all_items: Vec<SampledItem> = Vec::new();
496
497 for cra in cras {
498 if cra.combined_risk < CraLevel::Moderate {
500 continue;
501 }
502
503 let te =
504 tolerable_error.unwrap_or_else(|| self.config.base_population_value * dec!(0.05));
505
506 let matching_lines = filter_je_lines_for_area(journal_entries, &cra.account_area);
507
508 let (plan, items) = if matching_lines.is_empty() {
509 self.generate_plan(cra, te)
511 } else {
512 self.generate_plan_from_population(cra, te, &matching_lines)
513 };
514
515 all_items.extend(items);
516 plans.push(plan);
517 }
518
519 info!(
520 "Generated {} JE-aware sampling plans with {} sampled items",
521 plans.len(),
522 all_items.len()
523 );
524 (plans, all_items)
525 }
526
527 fn generate_plan_from_population(
529 &mut self,
530 cra: &CombinedRiskAssessment,
531 tolerable_error: Decimal,
532 matching_lines: &[(&JournalEntry, &JournalEntryLine, Decimal)],
533 ) -> (SamplingPlan, Vec<SampledItem>) {
534 let methodology = methodology_for_assertion(cra.assertion, cra.combined_risk);
535 let rep_sample_size = sample_size_for_cra(&mut self.rng, cra.combined_risk);
536
537 let population_size = matching_lines.len();
539 let population_value: Decimal = matching_lines.iter().map(|(_, _, amt)| *amt).sum();
540
541 let mut sorted_lines: Vec<_> = matching_lines.to_vec();
543 sorted_lines.sort_by_key(|b| std::cmp::Reverse(b.2));
544
545 let mut key_items: Vec<KeyItem> = Vec::new();
547 let mut seen_ids: HashSet<String> = HashSet::new();
548
549 for (idx, (je, _line, amount)) in sorted_lines.iter().enumerate() {
550 if *amount <= tolerable_error {
551 break;
552 }
553 if key_items.len() >= 20 {
554 break;
555 }
556 let je_id = je.header.document_id.to_string();
557 if seen_ids.contains(&je_id) {
559 continue;
560 }
561 seen_ids.insert(je_id.clone());
562 let reason = self.pick_key_item_reason(cra, idx);
563 key_items.push(KeyItem {
564 item_id: je_id,
565 amount: *amount,
566 reason,
567 });
568 }
569
570 let key_items_value: Decimal = key_items.iter().map(|k| k.amount).sum();
571 let remaining_value = (population_value - key_items_value).max(Decimal::ZERO);
572
573 let remaining: Vec<_> = sorted_lines
575 .iter()
576 .filter(|(je, _, _)| !seen_ids.contains(&je.header.document_id.to_string()))
577 .collect();
578 let actual_rep_size = rep_sample_size.min(remaining.len());
579 let step = remaining.len().checked_div(actual_rep_size).unwrap_or(0);
580 let start = if step > 0 {
581 self.rng.random_range(0..step)
582 } else {
583 0
584 };
585
586 let sampling_interval = if actual_rep_size > 0 && remaining_value > Decimal::ZERO {
588 remaining_value / Decimal::from(actual_rep_size as i64)
589 } else {
590 Decimal::ZERO
591 };
592
593 let plan_id = format!(
594 "SP-{}-{}-{}",
595 cra.entity_code,
596 cra.account_area.replace(' ', "_").to_uppercase(),
597 format!("{:?}", cra.assertion).to_uppercase(),
598 );
599
600 let plan = SamplingPlan {
601 id: plan_id.clone(),
602 entity_code: cra.entity_code.clone(),
603 account_area: cra.account_area.clone(),
604 assertion: format!("{}", cra.assertion),
605 methodology,
606 population_size,
607 population_value,
608 key_items: key_items.clone(),
609 key_items_value,
610 remaining_population_value: remaining_value,
611 sample_size: actual_rep_size,
612 sampling_interval,
613 cra_level: cra.combined_risk.to_string(),
614 tolerable_error,
615 };
616
617 let mut sampled_items: Vec<SampledItem> = Vec::new();
619 let misstatement_p = misstatement_rate(cra.combined_risk);
620
621 for ki in &key_items {
623 let misstatement_found: bool = self.rng.random::<f64>() < misstatement_p;
624 let misstatement_amount = if misstatement_found {
625 let pct = Decimal::try_from(self.rng.random_range(0.01_f64..=0.15_f64))
626 .unwrap_or(dec!(0.05));
627 Some((ki.amount * pct).round_dp(2))
628 } else {
629 None
630 };
631
632 sampled_items.push(SampledItem {
633 item_id: ki.item_id.clone(),
634 sampling_plan_id: plan_id.clone(),
635 amount: ki.amount,
636 selection_type: SelectionType::KeyItem,
637 key_item_reason: Some(ki.reason),
638 tested: true,
639 misstatement_found,
640 misstatement_amount,
641 });
642 }
643
644 if actual_rep_size > 0 && step > 0 {
646 let mut rep_seen: HashSet<String> = HashSet::new();
647 for i in 0..actual_rep_size {
648 let idx = (start + i * step) % remaining.len();
649 let (je, _line, amount) = remaining[idx];
650 let je_id = je.header.document_id.to_string();
651
652 if rep_seen.contains(&je_id) {
654 continue;
655 }
656 rep_seen.insert(je_id.clone());
657
658 let misstatement_found: bool = self.rng.random::<f64>() < misstatement_p;
659 let misstatement_amount = if misstatement_found {
660 let pct = Decimal::try_from(self.rng.random_range(0.01_f64..=0.30_f64))
661 .unwrap_or(dec!(0.05));
662 Some((amount * pct).round_dp(2))
663 } else {
664 None
665 };
666
667 sampled_items.push(SampledItem {
668 item_id: je_id,
669 sampling_plan_id: plan_id.clone(),
670 amount: *amount,
671 selection_type: SelectionType::Representative,
672 key_item_reason: None,
673 tested: true,
674 misstatement_found,
675 misstatement_amount,
676 });
677 }
678 }
679
680 (plan, sampled_items)
681 }
682}
683
684#[cfg(test)]
689mod tests {
690 use super::*;
691 use datasynth_core::models::audit::risk_assessment_cra::RiskRating;
692 use rust_decimal_macros::dec;
693
694 fn make_cra(
695 account_area: &str,
696 assertion: AuditAssertion,
697 ir: RiskRating,
698 cr: RiskRating,
699 ) -> CombinedRiskAssessment {
700 CombinedRiskAssessment::new("C001", account_area, assertion, ir, cr, false, vec![])
701 }
702
703 #[test]
704 fn moderate_cra_generates_plan() {
705 let cra = make_cra(
706 "Trade Receivables",
707 AuditAssertion::Existence,
708 RiskRating::Medium,
709 RiskRating::Medium,
710 );
711 assert_eq!(cra.combined_risk, CraLevel::Moderate);
712
713 let mut gen = SamplingPlanGenerator::new(42);
714 let (plans, items) = gen.generate_for_cras(&[cra], Some(dec!(32_500)));
715
716 assert_eq!(
717 plans.len(),
718 1,
719 "Should generate exactly one plan for Moderate CRA"
720 );
721 let plan = &plans[0];
722 assert!(!items.is_empty(), "Should generate sampled items");
723 assert!(
724 plan.sample_size >= 20 && plan.sample_size <= 30,
725 "Moderate CRA sample size 20–30"
726 );
727 }
728
729 #[test]
730 fn low_cra_skipped() {
731 let cra = make_cra(
732 "Cash",
733 AuditAssertion::Existence,
734 RiskRating::Low,
735 RiskRating::Low,
736 );
737 assert_eq!(cra.combined_risk, CraLevel::Minimal);
738
739 let mut gen = SamplingPlanGenerator::new(42);
740 let (plans, _items) = gen.generate_for_cras(&[cra], Some(dec!(32_500)));
741
742 assert!(
743 plans.is_empty(),
744 "Minimal CRA should produce no sampling plan"
745 );
746 }
747
748 #[test]
749 fn high_cra_large_sample() {
750 let cra = make_cra(
751 "Revenue",
752 AuditAssertion::Occurrence,
753 RiskRating::High,
754 RiskRating::High,
755 );
756 assert_eq!(cra.combined_risk, CraLevel::High);
757
758 let mut gen = SamplingPlanGenerator::new(99);
759 let (plans, _) = gen.generate_for_cras(&[cra], Some(dec!(32_500)));
760
761 assert_eq!(plans.len(), 1);
762 let plan = &plans[0];
763 assert!(
764 plan.sample_size >= 40,
765 "High CRA sample size should be 40–60"
766 );
767 }
768
769 #[test]
770 fn key_items_all_above_tolerable_error() {
771 let cra = make_cra(
772 "Provisions",
773 AuditAssertion::ValuationAndAllocation,
774 RiskRating::High,
775 RiskRating::Medium,
776 );
777
778 let mut gen = SamplingPlanGenerator::new(7);
779 let te = dec!(32_500);
780 let (plans, _) = gen.generate_for_cras(&[cra], Some(te));
781
782 assert!(!plans.is_empty());
783 let plan = &plans[0];
784 for ki in &plan.key_items {
785 assert!(
786 ki.amount >= te,
787 "Key item amount {} must be >= tolerable error {}",
788 ki.amount,
789 te
790 );
791 }
792 }
793
794 #[test]
795 fn sampling_interval_formula() {
796 let cra = make_cra(
797 "Inventory",
798 AuditAssertion::Existence,
799 RiskRating::High,
800 RiskRating::Medium,
801 );
802
803 let mut gen = SamplingPlanGenerator::new(13);
804 let te = dec!(32_500);
805 let (plans, _) = gen.generate_for_cras(&[cra], Some(te));
806
807 assert!(!plans.is_empty());
808 let plan = &plans[0];
809 if plan.sample_size > 0 && plan.remaining_population_value > Decimal::ZERO {
810 let expected_interval =
811 plan.remaining_population_value / Decimal::from(plan.sample_size as i64);
812 let diff = (plan.sampling_interval - expected_interval).abs();
814 assert!(
815 diff < dec!(0.01),
816 "Interval {} ≠ remaining/sample_size {}",
817 plan.sampling_interval,
818 expected_interval
819 );
820 }
821 }
822
823 #[test]
824 fn balance_assertion_uses_mus() {
825 let cra = make_cra(
826 "Trade Receivables",
827 AuditAssertion::Existence,
828 RiskRating::Medium,
829 RiskRating::Medium,
830 );
831 let methodology = methodology_for_assertion(cra.assertion, CraLevel::Moderate);
832 assert_eq!(methodology, SamplingMethodology::MonetaryUnitSampling);
833 }
834
835 #[test]
836 fn transaction_assertion_uses_systematic() {
837 let methodology = methodology_for_assertion(AuditAssertion::Occurrence, CraLevel::Moderate);
838 assert_eq!(methodology, SamplingMethodology::SystematicSelection);
839 }
840
841 #[test]
842 fn all_sampled_items_have_plan_id() {
843 let cras = vec![
844 make_cra(
845 "Revenue",
846 AuditAssertion::Occurrence,
847 RiskRating::High,
848 RiskRating::Medium,
849 ),
850 make_cra(
851 "Inventory",
852 AuditAssertion::Existence,
853 RiskRating::High,
854 RiskRating::Low,
855 ),
856 ];
857
858 let mut gen = SamplingPlanGenerator::new(55);
859 let te = dec!(32_500);
860 let (plans, items) = gen.generate_for_cras(&cras, Some(te));
861
862 assert!(!plans.is_empty());
863 assert!(!items.is_empty());
864 assert!(
866 items.iter().all(|i| i.tested),
867 "All items should be marked tested"
868 );
869 }
870}