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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
//! NCI opening-balance ingestion + writer — Task 7.2.
//!
//! Reads prior-period closing NCI balances from
//! `{prior_period_dir}/consolidated/nci_rollforward.json` (the file
//! emitted by Task 9.1's aggregate driver via [`write_nci_rollforward`])
//! and returns them as the current period's opening balances, mirroring
//! the IFRS 10 / ASC 810 rollforward identity:
//!
//! ```text
//! opening_nci(period N) := closing_nci(period N-1)
//! ```
//!
//! # Standards reference
//!
//! - **IFRS 10.B94 / IFRS 12.12** — the NCI carrying amount must be
//! rolled forward period-on-period; the prior-period closing balance
//! *is* the current-period opening balance.
//! - **ASC 810-10-45** — equivalent US GAAP requirement.
//!
//! # File-not-found semantics
//!
//! Missing `consolidated/nci_rollforward.json` is **not** an error — it
//! just means the engagement is in its first period. We log a
//! `tracing::warn!` (so the audit log captures the choice) and return
//! an empty map. Callers iterating over their entities default every
//! opening balance to zero.
//!
//! # Corruption / duplicates
//!
//! - Malformed JSON surfaces as [`GroupError::Serde`] (the standard
//! `From<serde_json::Error>` conversion).
//! - Multiple records for the same `entity_code` surfaces as
//! [`GroupError::Aggregate`] — silent overwriting would mask a
//! regression in the rollforward writer.
use BTreeMap;
use fs;
use ;
use Decimal;
use crate;
use NciRollforward;
/// Subdirectory within the group output root where the consolidated
/// NCI rollforward sits. Mirrors the spec §"Aggregate phase outputs"
/// layout used by other consolidated artefacts (e.g.
/// `cta_rollforward.json`).
pub const CONSOLIDATED_SUBDIR: &str = "consolidated";
/// File name for the on-disk NCI rollforward array, per spec §"Aggregate
/// phase outputs".
pub const NCI_ROLLFORWARD_FILENAME: &str = "nci_rollforward.json";
// ── Public API ────────────────────────────────────────────────────────────────
/// Read opening NCI balances from a prior period's aggregate output.
///
/// Walks `{prior_period_dir}/consolidated/nci_rollforward.json` (the
/// file emitted by Task 9.1's aggregate driver via
/// [`write_nci_rollforward`]), extracts each entity's `closing_nci`,
/// and returns the `(entity_code, closing_nci)` pairs as the current
/// period's opening balances.
///
/// `Ok(BTreeMap::new())` is returned (with a `tracing::warn!` log) when
/// the file is missing — the spec says "missing file → default to 0
/// per entity, with a warning". Callers receiving an empty map default
/// every entity's opening NCI to zero.
///
/// # Errors
///
/// - [`GroupError::Serde`] if the file exists but cannot be parsed as a
/// JSON array of [`NciRollforward`].
/// - [`GroupError::Aggregate`] if the file contains two or more
/// records for the same `entity_code` — silent overwriting would
/// mask a regression in the writer.
/// - [`GroupError::Io`] for any I/O failure other than file-not-found
/// (which is converted to an empty map, not an error).
/// Write an array of [`NciRollforward`] records to
/// `{out_dir}/consolidated/nci_rollforward.json`.
///
/// Creates the `consolidated/` subdirectory if it doesn't already
/// exist. Output is pretty-printed JSON with a trailing newline so
/// the file is human-readable when opened in an editor. Returns the
/// absolute path of the written file so callers logging "wrote
/// nci_rollforward.json to …" don't have to re-derive it.
///
/// # Errors
///
/// - [`GroupError::Io`] if the subdirectory creation or file write
/// fails.
/// - [`GroupError::Serde`] if the rollforward array fails to serialise
/// (should be impossible — every field is `Serialize`-friendly).