datasynth-core 5.34.0

Core domain models, traits, and distributions for synthetic enterprise data generation
Documentation
//! Evidence-anchor models — the ISA-505 external-corroboration layer (Phase 2).
//!
//! An [`EvidenceAnchor`] records, per material GL account, whether the account's activity is
//! corroborated by evidence *exogenous* to the ledger (external confirmation / bank reconciliation /
//! third-party record). A material account with no corroboration is a **dangling node** — the
//! ISA-505 existence/occurrence lead.
//!
//! This is the corroboration-coverage counterpart to [`crate::models::ExternalExpectation`] (which is
//! aggregate *deviation*, ISA 520). The two close different perfect-crime variants: a mimetic fraud
//! that inflates an aggregate deviates from its expectation; a mimetic fraud that nets out or relocates
//! its booking leaves the aggregate intact but routes through a **fabricated counterparty that cannot
//! confirm** — caught only here. To evade this arm too, the adversary must forge the external evidence
//! (the expensive, fragile "perfect audit crime" of `prop:counter`), modelled by the small evade rate.
//! Because the engine knows the generating process, each record carries the ground truth (whether the
//! account was touched by fraud), so the dangling-node detector can be scored against it.

use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};

use crate::models::chart_of_accounts::AccountType;

/// How an account's activity is corroborated by evidence exogenous to the ledger.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum CorroborationMethod {
    /// No external corroboration obtained (a dangling node if the account is material).
    #[default]
    None,
    /// External confirmation (ISA 505) from the counterparty / bank.
    Confirmation,
    /// Reconciliation to a third-party bank statement.
    BankReconciliation,
    /// Other third-party record (registry, contract, customs, etc.).
    ThirdPartyRecord,
}

/// An ISA-505 external-corroboration record for a single GL account over a fiscal year.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvidenceAnchor {
    /// Unique anchor identifier.
    pub anchor_id: String,
    /// Company the account belongs to.
    pub company_code: String,
    /// GL account number (the corroboration unit; its associated counterparty is the confirmation recipient).
    pub account_code: String,
    /// GL account description.
    pub account_description: String,
    /// GL account type.
    pub account_type: AccountType,
    /// Fiscal year the anchor applies to.
    pub fiscal_year: i32,
    /// Total posting activity for the account (legitimate + any fraud).
    #[serde(with = "crate::serde_decimal")]
    pub total_activity: Decimal,
    /// Number of journal entries touching the account.
    pub transaction_count: u32,
    /// Whether the account is material (its activity share meets the threshold).
    pub is_material: bool,
    /// Whether the account's activity is corroborated by exogenous evidence.
    pub corroborated: bool,
    /// How it was corroborated (`None` if not).
    pub corroboration_method: CorroborationMethod,
    /// Whether this is a dangling node: material with no external corroboration (the ISA-505 lead).
    pub is_dangling: bool,
    /// Ground truth: posting activity attributable to fraud journal entries.
    #[serde(with = "crate::serde_decimal")]
    pub fraud_activity: Decimal,
    /// Ground truth: whether the account was touched by any fraud entry.
    pub is_fraud_linked: bool,
}