Skip to main content

datasynth_core/models/
evidence_anchor.rs

1//! Evidence-anchor models — the ISA-505 external-corroboration layer (Phase 2).
2//!
3//! An [`EvidenceAnchor`] records, per material GL account, whether the account's activity is
4//! corroborated by evidence *exogenous* to the ledger (external confirmation / bank reconciliation /
5//! third-party record). A material account with no corroboration is a **dangling node** — the
6//! ISA-505 existence/occurrence lead.
7//!
8//! This is the corroboration-coverage counterpart to [`crate::models::ExternalExpectation`] (which is
9//! aggregate *deviation*, ISA 520). The two close different perfect-crime variants: a mimetic fraud
10//! that inflates an aggregate deviates from its expectation; a mimetic fraud that nets out or relocates
11//! its booking leaves the aggregate intact but routes through a **fabricated counterparty that cannot
12//! confirm** — caught only here. To evade this arm too, the adversary must forge the external evidence
13//! (the expensive, fragile "perfect audit crime" of `prop:counter`), modelled by the small evade rate.
14//! Because the engine knows the generating process, each record carries the ground truth (whether the
15//! account was touched by fraud), so the dangling-node detector can be scored against it.
16
17use rust_decimal::Decimal;
18use serde::{Deserialize, Serialize};
19
20use crate::models::chart_of_accounts::AccountType;
21
22/// How an account's activity is corroborated by evidence exogenous to the ledger.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
24#[serde(rename_all = "snake_case")]
25pub enum CorroborationMethod {
26    /// No external corroboration obtained (a dangling node if the account is material).
27    #[default]
28    None,
29    /// External confirmation (ISA 505) from the counterparty / bank.
30    Confirmation,
31    /// Reconciliation to a third-party bank statement.
32    BankReconciliation,
33    /// Other third-party record (registry, contract, customs, etc.).
34    ThirdPartyRecord,
35}
36
37/// An ISA-505 external-corroboration record for a single GL account over a fiscal year.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct EvidenceAnchor {
40    /// Unique anchor identifier.
41    pub anchor_id: String,
42    /// Company the account belongs to.
43    pub company_code: String,
44    /// GL account number (the corroboration unit; its associated counterparty is the confirmation recipient).
45    pub account_code: String,
46    /// GL account description.
47    pub account_description: String,
48    /// GL account type.
49    pub account_type: AccountType,
50    /// Fiscal year the anchor applies to.
51    pub fiscal_year: i32,
52    /// Total posting activity for the account (legitimate + any fraud).
53    #[serde(with = "crate::serde_decimal")]
54    pub total_activity: Decimal,
55    /// Number of journal entries touching the account.
56    pub transaction_count: u32,
57    /// Whether the account is material (its activity share meets the threshold).
58    pub is_material: bool,
59    /// Whether the account's activity is corroborated by exogenous evidence.
60    pub corroborated: bool,
61    /// How it was corroborated (`None` if not).
62    pub corroboration_method: CorroborationMethod,
63    /// Whether this is a dangling node: material with no external corroboration (the ISA-505 lead).
64    pub is_dangling: bool,
65    /// Ground truth: posting activity attributable to fraud journal entries.
66    #[serde(with = "crate::serde_decimal")]
67    pub fraud_activity: Decimal,
68    /// Ground truth: whether the account was touched by any fraud entry.
69    pub is_fraud_linked: bool,
70}