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}