use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SamplingMethodology {
MonetaryUnitSampling,
RandomSelection,
SystematicSelection,
HaphazardSelection,
}
impl std::fmt::Display for SamplingMethodology {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::MonetaryUnitSampling => "Monetary Unit Sampling (MUS)",
Self::RandomSelection => "Random Selection",
Self::SystematicSelection => "Systematic Selection",
Self::HaphazardSelection => "Haphazard Selection",
};
write!(f, "{s}")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum KeyItemReason {
AboveTolerableError,
UnusualNature,
HighRisk,
ManagementOverride,
}
impl std::fmt::Display for KeyItemReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::AboveTolerableError => "Amount above tolerable error",
Self::UnusualNature => "Unusual nature (related party / unusual counterparty)",
Self::HighRisk => "High-risk area (significant risk per ISA 315.28)",
Self::ManagementOverride => "Management override — manual JE to automated account",
};
write!(f, "{s}")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyItem {
pub item_id: String,
#[serde(with = "crate::serde_decimal")]
pub amount: Decimal,
pub reason: KeyItemReason,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SelectionType {
KeyItem,
Representative,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SampledItem {
pub item_id: String,
pub sampling_plan_id: String,
#[serde(with = "crate::serde_decimal")]
pub amount: Decimal,
pub selection_type: SelectionType,
pub tested: bool,
pub misstatement_found: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub misstatement_amount: Option<Decimal>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SamplingPlan {
pub id: String,
pub entity_code: String,
pub account_area: String,
pub assertion: String,
pub methodology: SamplingMethodology,
pub population_size: usize,
#[serde(with = "crate::serde_decimal")]
pub population_value: Decimal,
pub key_items: Vec<KeyItem>,
#[serde(with = "crate::serde_decimal")]
pub key_items_value: Decimal,
#[serde(with = "crate::serde_decimal")]
pub remaining_population_value: Decimal,
pub sample_size: usize,
#[serde(with = "crate::serde_decimal")]
pub sampling_interval: Decimal,
pub cra_level: String,
#[serde(with = "crate::serde_decimal")]
pub tolerable_error: Decimal,
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn key_item_reason_display() {
assert_eq!(
KeyItemReason::AboveTolerableError.to_string(),
"Amount above tolerable error"
);
}
#[test]
fn sampling_methodology_display() {
assert_eq!(
SamplingMethodology::MonetaryUnitSampling.to_string(),
"Monetary Unit Sampling (MUS)"
);
assert_eq!(
SamplingMethodology::SystematicSelection.to_string(),
"Systematic Selection"
);
}
#[test]
fn sampling_plan_structure() {
let plan = SamplingPlan {
id: "SP-C001-TRADE_RECEIVABLES-Existence".into(),
entity_code: "C001".into(),
account_area: "Trade Receivables".into(),
assertion: "Existence".into(),
methodology: SamplingMethodology::MonetaryUnitSampling,
population_size: 500,
population_value: dec!(1_000_000),
key_items: vec![KeyItem {
item_id: "JE-001".into(),
amount: dec!(50_000),
reason: KeyItemReason::AboveTolerableError,
}],
key_items_value: dec!(50_000),
remaining_population_value: dec!(950_000),
sample_size: 25,
sampling_interval: dec!(38_000),
cra_level: "Moderate".into(),
tolerable_error: dec!(32_500),
};
assert_eq!(
plan.population_value - plan.key_items_value,
plan.remaining_population_value
);
assert_eq!(plan.key_items.len(), 1);
assert_eq!(plan.key_items[0].reason, KeyItemReason::AboveTolerableError);
}
}