Skip to main content

datasynth_group/
config.rs

1//! `group:` YAML configuration types (spec §3).
2
3use chrono::NaiveDate;
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use std::collections::BTreeMap;
7
8/// Top-level group engagement config.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct GroupConfig {
11    pub id: String,
12    #[serde(default)]
13    pub name: Option<String>,
14    pub presentation_currency: String,
15    pub period: PeriodConfig,
16    pub seed: u64,
17
18    #[serde(default)]
19    pub defaults: serde_yaml::Value, // inherited into each entity; opaque at this layer
20
21    #[serde(default)]
22    pub scoping_profiles: BTreeMap<String, serde_yaml::Value>,
23
24    pub ownership: OwnershipConfig,
25
26    #[serde(default)]
27    pub intercompany: IntercompanyConfig,
28
29    pub fx: FxConfig,
30
31    #[serde(default)]
32    pub audit: AuditEngagementConfig,
33
34    #[serde(default)]
35    pub tax: TaxGroupConfig,
36
37    /// **v5.2** — IAS 36 § 10 cash-generating-unit (CGU) plan: defines
38    /// the CGUs the engagement tests for goodwill impairment + the
39    /// goodwill amounts allocated to each CGU at acquisition date.
40    /// Empty by default; engagements without CGU allocations skip the
41    /// annual impairment test entirely (no `consolidated/cgu_impairment_tests.json`
42    /// is emitted).
43    #[serde(default)]
44    pub cgu: CguConfig,
45
46    #[serde(default)]
47    pub output: OutputLayoutConfig,
48
49    #[serde(default)]
50    pub fleet: Option<FleetConfig>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct PeriodConfig {
55    pub start_date: NaiveDate,
56    pub length: PeriodLength,
57    #[serde(default)]
58    pub fiscal_year_end: Option<NaiveDate>,
59}
60
61#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
62#[serde(rename_all = "lowercase")]
63pub enum PeriodLength {
64    Monthly,
65    Quarterly,
66    SemiAnnual,
67    Annual,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct OwnershipConfig {
72    pub parent_entity_code: String,
73    #[serde(default)]
74    pub entities: Vec<EntityConfig>,
75    #[serde(default)]
76    pub generated: Vec<GeneratedEntityBlock>,
77    #[serde(default)]
78    pub entities_from: Option<std::path::PathBuf>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct EntityConfig {
83    pub code: String,
84    #[serde(default)]
85    pub name: Option<String>,
86    pub country: String,
87    pub functional_currency: String,
88    pub scoping_profile: String,
89    pub consolidation_method: ConsolidationMethod,
90    #[serde(default)]
91    pub ownership_percent: Option<Decimal>,
92    #[serde(default)]
93    pub parent_code: Option<String>,
94    #[serde(default)]
95    pub acquisition_date: Option<NaiveDate>,
96    #[serde(default)]
97    pub accounting_framework: Option<String>,
98    #[serde(default)]
99    pub industry: Option<String>,
100    #[serde(default)]
101    pub rows: Option<u64>,
102    /// **v5.2** — IFRS 3 § 41-42 / IFRS 10 § 23 / IFRS 10.B97
103    /// ownership-change events that affected this entity during the
104    /// reporting period.  Each entry describes one mid-period
105    /// transition: control gained (new acquisition or increase from
106    /// associate), control increased / decreased within consolidation
107    /// (equity-transaction treatment per IFRS 10.23), or control lost
108    /// (deconsolidation per IFRS 10.B97).  Empty by default; the
109    /// shard runner emits `intercompany/ownership_change_events.json`
110    /// per entity only when this list is non-empty so v5.0–v5.1
111    /// archives stay byte-identical.  The aggregate-phase rollforward
112    /// wiring (consuming these events to drive proper IFRS 3 / IFRS
113    /// 10 NCI treatment) is a follow-up PR.
114    #[serde(default)]
115    pub ownership_changes: Vec<OwnershipChangeEntry>,
116    /// **v5.2** — IAS 29 hyperinflationary status of this entity's
117    /// functional currency.  Defaults to `NotHyperinflationary`,
118    /// preserving v5.0–v5.1 behaviour byte-for-byte.  When set to
119    /// `Hyperinflationary`, the aggregate phase will (in a follow-up
120    /// PR) apply IAS 29 § 12 restatement to non-monetary items
121    /// before IAS 21 closing-rate translation per IAS 21 § 42(b).
122    #[serde(default)]
123    pub hyperinflation_status: datasynth_core::models::HyperinflationStatus,
124    #[serde(default, flatten)]
125    pub overrides: BTreeMap<String, serde_yaml::Value>, // generic per-entity overrides
126}
127
128#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
129#[serde(rename_all = "snake_case")]
130pub enum ConsolidationMethod {
131    Parent,
132    Full,
133    EquityMethod,
134    Proportional,
135    FairValue,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct GeneratedEntityBlock {
140    pub count: u32,
141    pub code_prefix: String,
142    #[serde(default)]
143    pub country: Vec<String>,
144    #[serde(default)]
145    pub functional_currency: Option<String>,
146    pub scoping_profile: String,
147    pub consolidation_method: ConsolidationMethod,
148    #[serde(default)]
149    pub ownership_percent_range: Option<[Decimal; 2]>,
150    #[serde(default)]
151    pub parent_code: Option<String>,
152    #[serde(default)]
153    pub accounting_framework: Option<String>,
154    #[serde(default)]
155    pub industry: Option<String>,
156}
157
158#[derive(Debug, Clone, Default, Serialize, Deserialize)]
159pub struct IntercompanyConfig {
160    #[serde(default)]
161    pub relationships: Vec<IcRelationshipConfig>,
162    #[serde(default)]
163    pub matching: IcMatchingConfig,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
167#[serde(untagged)]
168pub enum IcRelationshipConfig {
169    Explicit(IcRelationshipExplicit),
170    Pattern(IcRelationshipPattern),
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
174#[serde(deny_unknown_fields)]
175pub struct IcRelationshipExplicit {
176    pub seller: String,
177    pub buyer: String,
178    pub types: Vec<IcTransactionType>,
179    pub annual_volume: Decimal,
180    #[serde(default)]
181    pub transfer_pricing: Option<TransferPricingMethod>,
182    #[serde(default)]
183    pub markup_percent: Option<Decimal>,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
187#[serde(deny_unknown_fields)]
188pub struct IcRelationshipPattern {
189    pub pattern: IcPattern,
190    pub types: Vec<IcTransactionType>,
191    pub per_pair_volume: Decimal,
192    #[serde(default)]
193    pub transfer_pricing: Option<TransferPricingMethod>,
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct IcPattern {
198    #[serde(default)]
199    pub seller_scoping_profile: Option<String>,
200    #[serde(default)]
201    pub buyer_scoping_profile: Option<String>,
202    #[serde(default)]
203    pub seller: Option<String>,
204    #[serde(default)]
205    pub buyer: Option<String>,
206}
207
208#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
209#[serde(rename_all = "snake_case")]
210pub enum IcTransactionType {
211    GoodsSale,
212    ServiceProvided,
213    ManagementFee,
214    Royalty,
215    CostSharing,
216    LoanInterest,
217    Dividend,
218    ExpenseRecharge,
219}
220
221#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
222#[serde(rename_all = "snake_case")]
223pub enum TransferPricingMethod {
224    CostPlus,
225    ComparableUncontrolled,
226    ResalePrice,
227    TransactionalNetMargin,
228    ProfitSplit,
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
232pub struct IcMatchingConfig {
233    #[serde(default = "default_strategy")]
234    pub strategy: IcMatchingStrategy,
235    #[serde(default = "default_coverage_target")]
236    pub coverage_target: f64,
237    /// **v5.3** — fuzzy-matching amount-drift tolerance, expressed as
238    /// a percentage of the larger of the seller / buyer side amounts.
239    /// Used **only** when `strategy == EmergentFuzzy`.  When the
240    /// observed drift exceeds this tolerance, the matcher rejects the
241    /// pair with [`crate::aggregate::ic_matcher::UnmatchedReason::AmountDriftAboveTolerance`]
242    /// instead of silently treating it as matched.
243    ///
244    /// Default `0.0` means "exact match required" — the v5.0–v5.2
245    /// `ManifestDriven` contract.  A typical fuzzy-mode value is
246    /// `0.005` (50 bps) which absorbs FX-rounding drift but flags
247    /// genuine reconciliation breaks.
248    #[serde(default = "default_tolerance_percent")]
249    pub tolerance_percent: rust_decimal::Decimal,
250}
251
252fn default_strategy() -> IcMatchingStrategy {
253    IcMatchingStrategy::ManifestDriven
254}
255fn default_coverage_target() -> f64 {
256    0.98
257}
258fn default_tolerance_percent() -> rust_decimal::Decimal {
259    rust_decimal::Decimal::ZERO
260}
261
262impl Default for IcMatchingConfig {
263    fn default() -> Self {
264        Self {
265            strategy: default_strategy(),
266            coverage_target: default_coverage_target(),
267            tolerance_percent: default_tolerance_percent(),
268        }
269    }
270}
271
272#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
273#[serde(rename_all = "snake_case")]
274pub enum IcMatchingStrategy {
275    ManifestDriven,
276    EmergentFuzzy,
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct FxConfig {
281    pub base_currency: String,
282    #[serde(default)]
283    pub rate_source: FxRateSource,
284    #[serde(default)]
285    pub rates: BTreeMap<String, BTreeMap<NaiveDate, Decimal>>,
286    pub policy: FxPolicyConfig,
287}
288
289#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
290#[serde(rename_all = "snake_case")]
291pub enum FxRateSource {
292    #[default]
293    Inline,
294    UserSupplied,
295    HistoricalSeries,
296}
297
298#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
299pub struct FxPolicyConfig {
300    pub balance_sheet: FxRateBasis,
301    pub income_statement: FxRateBasis,
302    pub equity: FxRateBasis,
303}
304
305#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
306#[serde(rename_all = "snake_case")]
307pub enum FxRateBasis {
308    #[default]
309    Closing,
310    Average,
311    Historical,
312}
313
314#[derive(Debug, Clone, Default, Serialize, Deserialize)]
315pub struct AuditEngagementConfig {
316    #[serde(default)]
317    pub engagement_id: Option<String>,
318    #[serde(default)]
319    pub lead_auditor: Option<String>,
320    #[serde(default)]
321    pub framework: Option<String>, // isa | pcaob | dual
322    #[serde(default)]
323    pub fsm_blueprint: Option<String>,
324    #[serde(default)]
325    pub group_materiality: Option<GroupMaterialityConfig>,
326    #[serde(default)]
327    pub component_scope_thresholds: Option<ComponentScopeThresholds>,
328    #[serde(default)]
329    pub generate_kams: bool,
330    #[serde(default)]
331    pub generate_group_opinion: bool,
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct GroupMaterialityConfig {
336    pub basis: MaterialityBasis,
337    pub percent: Decimal,
338}
339
340#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
341#[serde(rename_all = "snake_case")]
342pub enum MaterialityBasis {
343    Revenue,
344    Assets,
345    PretaxIncome,
346    Equity,
347}
348
349#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct ComponentScopeThresholds {
351    pub full_scope: Decimal,
352    pub specific_scope: Decimal,
353}
354
355#[derive(Debug, Clone, Default, Serialize, Deserialize)]
356pub struct TaxGroupConfig {
357    #[serde(default)]
358    pub pillar_two: Option<PillarTwoConfig>,
359    #[serde(default)]
360    pub cbc_report: Option<CbcReportConfig>,
361    #[serde(default)]
362    pub transfer_pricing: Option<TpConfig>,
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize)]
366pub struct PillarTwoConfig {
367    pub enabled: bool,
368    #[serde(default)]
369    pub jurisdictions: Vec<String>,
370}
371
372#[derive(Debug, Clone, Serialize, Deserialize)]
373pub struct CbcReportConfig {
374    pub enabled: bool,
375    #[serde(default)]
376    pub reporting_jurisdiction: Option<String>,
377}
378
379#[derive(Debug, Clone, Serialize, Deserialize)]
380pub struct TpConfig {
381    #[serde(default)]
382    pub master_file: bool,
383    #[serde(default)]
384    pub local_files_for: Vec<String>,
385}
386
387#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
388pub struct OutputLayoutConfig {
389    #[serde(default = "default_layout")]
390    pub layout: OutputLayout,
391    #[serde(default = "default_true")]
392    pub shared_masters_at_root: bool,
393    #[serde(default)]
394    pub compression: Option<OutputCompression>,
395}
396
397fn default_layout() -> OutputLayout {
398    OutputLayout::PerEntitySubtree
399}
400fn default_true() -> bool {
401    true
402}
403
404#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
405#[serde(rename_all = "snake_case")]
406pub enum OutputLayout {
407    #[default]
408    PerEntitySubtree,
409    Flat,
410}
411
412#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
413#[serde(rename_all = "lowercase")]
414pub enum OutputCompression {
415    Json,
416    Csv,
417    Parquet,
418}
419
420#[derive(Debug, Clone, Default, Serialize, Deserialize)]
421pub struct FleetConfig {
422    #[serde(default)]
423    pub dispatcher: Option<String>, // v5.3: in_process | subprocess | remote
424    #[serde(default)]
425    pub max_concurrent_shards: Option<u32>,
426    #[serde(default)]
427    pub per_shard_timeout_seconds: Option<u64>,
428}
429
430/// **v5.2** — IAS 36 § 10 cash-generating-unit (CGU) plan.
431///
432/// Engagement-static CGU configuration: defines the CGUs the engagement
433/// will test for annual goodwill impairment + the goodwill amounts
434/// allocated to each CGU at the original acquisition date (IAS 36 § 80).
435/// Per-period test inputs (fair value less costs of disposal, value in
436/// use) live elsewhere — outside the manifest layer this PR ships.
437///
438/// Empty by default: an engagement without configured CGUs simply
439/// skips the impairment test phase and emits no
440/// `consolidated/cgu_impairment_tests.json` artefact.  This preserves
441/// backwards compatibility byte-for-byte for v5.0–v5.1 archives.
442#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
443pub struct CguConfig {
444    /// CGU definitions.  IDs must be unique within the engagement;
445    /// each CGU's `member_entity_codes` must reference entities that
446    /// exist in `ownership.entities`.  Validation happens in
447    /// [`crate::manifest::cgu_plan::build_cgu_plan`].
448    #[serde(default)]
449    pub cgus: Vec<CguDefinitionEntry>,
450
451    /// Goodwill allocations (one per (CGU, business combination) pair).
452    /// Amounts are non-negative; bargain purchases produce no
453    /// goodwill and therefore no allocation row.  Each entry's
454    /// `cgu_id` must reference an entry in `cgus`; the
455    /// `business_combination_id` is loosely validated (BC files live
456    /// per-shard so cross-validation is deferred to the aggregate
457    /// phase).
458    #[serde(default)]
459    pub goodwill_allocations: Vec<CguGoodwillAllocationEntry>,
460}
461
462/// One CGU definition entry.  Mirrors
463/// `datasynth_core::models::cgu::CashGeneratingUnit` so the manifest
464/// builder can lift it directly into the manifest plan.
465#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
466pub struct CguDefinitionEntry {
467    /// Stable CGU identifier — used by goodwill allocations and per-
468    /// period impairment-test inputs to refer to this CGU across
469    /// periods.
470    pub cgu_id: String,
471    /// Human-readable name (e.g. `"EMEA Consumer Products"`).
472    pub name: String,
473    /// Entity codes whose cash flows aggregate to form this CGU.  May
474    /// span multiple legal entities (cross-entity CGU) or be a sub-
475    /// division of a single entity (in which case it has one member).
476    /// Must be non-empty: the manifest builder rejects empty member
477    /// lists.
478    #[serde(default)]
479    pub member_entity_codes: Vec<String>,
480    /// Optional reportable-segment attribution for IFRS 8 / ASC 280
481    /// disclosure linkage.  Multiple CGUs can map to the same segment.
482    #[serde(default, skip_serializing_if = "Option::is_none")]
483    pub segment_code: Option<String>,
484}
485
486/// One goodwill-allocation entry — links a business combination's
487/// goodwill amount to one CGU at the acquisition date.
488#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
489pub struct CguGoodwillAllocationEntry {
490    /// CGU receiving the allocation (must reference a `cgu_id` in
491    /// [`CguConfig::cgus`]).
492    pub cgu_id: String,
493    /// Source business-combination identifier.  Loosely validated at
494    /// the manifest layer (BC files live per-shard); cross-validation
495    /// of the BC's existence happens during aggregate-phase
496    /// impairment-test wiring.
497    pub business_combination_id: String,
498    /// Allocated goodwill amount in the group presentation currency.
499    /// Always non-negative; manifest builder rejects negatives.
500    pub goodwill_amount: Decimal,
501    /// Acquisition date the allocation took effect.
502    pub allocation_date: NaiveDate,
503}
504
505/// **v5.2** — IFRS 3 § 41-42 / IFRS 10 § 23 / IFRS 10.B97 mid-period
506/// ownership-change event declared on an [`EntityConfig`].
507///
508/// `entity_code` and `parent_entity_code` are NOT carried here — the
509/// manifest builder fills them from the host [`EntityConfig::code`]
510/// and [`EntityConfig::parent_code`] respectively when lifting this
511/// entry into a `datasynth_core::models::OwnershipChangeEvent`.
512/// The host entity must therefore have `parent_code` set.
513#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
514pub struct OwnershipChangeEntry {
515    /// What kind of ownership change occurred — drives IFRS 3 / IFRS 10
516    /// accounting treatment.
517    pub event_type: datasynth_core::models::intercompany::OwnershipChangeType,
518    /// Date the change took effect (must lie within the manifest
519    /// period — validated at manifest build).
520    pub effective_date: NaiveDate,
521    /// Parent's ownership percent immediately before the event,
522    /// in `[0, 1]`.
523    pub ownership_percent_before: Decimal,
524    /// Parent's ownership percent immediately after the event,
525    /// in `[0, 1]`.
526    pub ownership_percent_after: Decimal,
527    /// Carrying amount of the previously-held interest in the
528    /// investor's books (IFRS 3.42 input for `ControlGained`).
529    /// Only meaningful for `ControlGained` / `ControlLost`; ignored
530    /// for the equity-transaction variants.
531    #[serde(default)]
532    pub previously_held_interest_carrying: Option<Decimal>,
533    /// Acquisition-date fair value of the previously-held interest
534    /// (IFRS 3.42 / IFRS 10.B97 re-measurement input).
535    #[serde(default)]
536    pub previously_held_interest_fair_value: Option<Decimal>,
537    /// Cash / share consideration paid (positive on `ControlGained` /
538    /// `ControlIncreased`) or received (negative on `ControlDecreased` /
539    /// `ControlLost`).  Sign convention: positive = outflow from
540    /// parent.
541    pub consideration_paid_or_received: Decimal,
542    /// IFRS 3 § 19 acquisition-date NCI fair value when this event
543    /// triggers a new consolidation (`ControlGained` only — must be
544    /// `Some(fv)` when method is `FullGoodwill`).
545    #[serde(default)]
546    pub acquisition_date_nci_fair_value: Option<Decimal>,
547    /// Method used to measure the new NCI (only relevant for
548    /// `ControlGained`).
549    #[serde(default)]
550    pub nci_measurement_method: datasynth_core::models::intercompany::NciMeasurementMethod,
551    /// Group presentation currency the amounts are denominated in.
552    pub currency: String,
553}