1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
//! 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,
}