datasynth_group/standalone.rs
1//! Standalone single-process generation — Task 9.2.
2//!
3//! [`generate_standalone`] runs the full v5.0 pipeline — manifest +
4//! shards + aggregate — in one call, without spawning a subprocess
5//! per phase. It is the in-process equivalent of:
6//!
7//! ```text
8//! datasynth-data group manifest --config group.yaml --out manifest.json
9//! datasynth-data group shard --manifest manifest.json --shard $SHARD_ID --out ./out
10//! datasynth-data group aggregate --manifest manifest.json --shards-dir ./out --out ./out
11//! ```
12//!
13//! ...with manifest persistence at `out_dir/manifest.json` for
14//! debuggability and for parity with the multi-step CLI flow.
15//!
16//! # Memory caveat — orchestrator runs are heavy
17//!
18//! `run_shard` drives [`datasynth_runtime::EnhancedOrchestrator::generate`]
19//! end-to-end for every entity in the shard. Each Mini-Acme entity
20//! peaks at **~17 GiB RSS** for ~15 minutes; running all five entities
21//! sequentially takes 60–90 minutes on a single host.
22//!
23//! When [`StandaloneOptions::parallel_shards = true`] (the default),
24//! the driver uses [`rayon`] to schedule shards concurrently. Peak
25//! RSS scales linearly with the number of shards in flight — N shards
26//! × 17 GiB ≈ 17·N GiB. This is fine on the XXL Azure VM (256 GiB)
27//! but will OOM a 32 GiB workstation in seconds. The associated
28//! `tests/standalone_e2e.rs` is `#[ignore]`d for exactly this reason —
29//! mirror the pattern in [`crate::shard::runner::run_shard`]
30//! integration tests.
31//!
32//! For the determinism harness (`tests/property/determinism.rs`)
33//! callers should pass `parallel_shards: false` so two runs over the
34//! same input produce byte-identical archives without the rayon
35//! scheduler's non-deterministic interleaving in flight (writes to
36//! disk are still deterministic per-shard because the runner's per-
37//! entity output writer is sync, but the scheduler may flush in a
38//! different order).
39//!
40//! # File layout
41//!
42//! After a successful run:
43//!
44//! ```text
45//! {out_dir}/
46//! ├── manifest.json
47//! ├── entities/
48//! │ ├── ENTITY_A/ ← per-shard runner output (verbatim)
49//! │ ├── ENTITY_B/
50//! │ └── ...
51//! ├── shard_summary.json ← from each run_shard call (overwrites
52//! │ per-shard; the last shard's summary
53//! │ persists. Each ShardSummary is also
54//! │ captured in StandaloneSummary.shard_summaries)
55//! ├── consolidated/ ← from run_aggregate (Chunk 6/7/8 outputs)
56//! └── ic_eliminations/ ← from run_aggregate (coverage report)
57//! ```
58//!
59//! Note: `shard_summary.json` is not race-free across shards — the
60//! runner writes it per-shard at `{out_dir}/shard_summary.json`, which
61//! means the last shard wins. This is a known v5.0 limitation; the
62//! per-shard summaries are reliably available in
63//! [`StandaloneSummary::shard_summaries`].
64
65use std::fs;
66use std::path::{Path, PathBuf};
67
68use rayon::prelude::*;
69use serde::{Deserialize, Serialize};
70
71use crate::aggregate::driver::{run_aggregate, AggregateOptions, AggregateSummary};
72use crate::config::GroupConfig;
73use crate::errors::{GroupError, GroupResult};
74use crate::manifest::builder::build_manifest;
75use crate::shard::runner::{run_shard_with_opening_balances, ShardSummary};
76
77// ── Public types ──────────────────────────────────────────────────────────────
78
79/// Knobs the caller can supply to tune [`generate_standalone`]
80/// behaviour.
81///
82/// All fields default to "sensible production": no prior period,
83/// fail-fast on missing shards, parallel shard execution. Override
84/// `parallel_shards = false` for determinism harnesses.
85#[derive(Debug, Clone)]
86pub struct StandaloneOptions {
87 /// Forwarded verbatim to
88 /// [`crate::aggregate::driver::AggregateOptions::prior_period_aggregate`].
89 pub prior_period_aggregate: Option<PathBuf>,
90 /// Forwarded verbatim to
91 /// [`crate::aggregate::driver::AggregateOptions::tolerate_missing_shards`].
92 pub tolerate_missing_shards: bool,
93 /// When `true`, run shards in parallel via [`rayon`]. Defaults to
94 /// `true`. Set to `false` for determinism harnesses (sequential
95 /// shard execution removes the scheduler's interleaving from the
96 /// output trace).
97 pub parallel_shards: bool,
98 /// Forwarded verbatim to
99 /// [`crate::aggregate::driver::AggregateOptions::cgu_test_inputs`]
100 /// — the per-period CGU goodwill impairment test inputs. Empty
101 /// by default; an engagement that wants annual IAS 36 § 10
102 /// impairment testing supplies one entry per CGU under test.
103 pub cgu_test_inputs: Vec<crate::aggregate::cgu_impairment::CguTestInputs>,
104 /// **v5.5.2** — Forwarded verbatim to
105 /// [`crate::aggregate::driver::AggregateOptions::cpi_series_by_currency`].
106 /// Per-currency CPI series for IAS 29 § 12 indexed restatement.
107 /// Empty by default — single-period engagements without
108 /// hyperinflationary subsidiaries see no behaviour change. When
109 /// the chain runner forwards a non-empty map, every period applies
110 /// the same series; vary by period at the library level if the
111 /// engagement requires per-period CPI overrides.
112 pub cpi_series_by_currency: std::collections::BTreeMap<
113 String,
114 datasynth_core::models::hyperinflation::GeneralPriceIndex,
115 >,
116 /// **v5.3** — Per-entity opening-balance carryover from a prior
117 /// period. When non-empty, the shard runner pre-populates each
118 /// matching entity's `ShardContext.opening_balances` so the
119 /// orchestrator's Phase 3b uses these BS positions instead of
120 /// generating fresh openings. Empty by default — single-period
121 /// engagements see no behaviour change.
122 ///
123 /// **v5.31 C2 (#157)** — auto-populated by
124 /// [`generate_standalone_chain`] from the prior period's
125 /// `entities/{code}/period_close/trial_balances.json` via
126 /// [`crate::aggregate::opening_balance::read_prior_period_closing_tbs`]
127 /// followed by
128 /// [`datasynth_generators::balance::project_closing_to_opening`].
129 /// The projection (vs the legacy `extract_opening_balances`) is
130 /// what absorbs prior-period net income into Retained Earnings —
131 /// the orchestrator emits `Adjusted` TBs (not `PostClosing`), so
132 /// dropping P&L without absorbing it would silently lose the
133 /// period's earnings on the chain hand-off.
134 ///
135 /// Callers using `generate_standalone` directly can populate this
136 /// manually when they want to drive multi-period continuity
137 /// without the chain helper.
138 pub entity_opening_balances: std::collections::BTreeMap<
139 String,
140 Vec<datasynth_core::models::balance::EntityOpeningBalance>,
141 >,
142 /// **v5.31 C2 (#157)** — accounting framework used to project the
143 /// prior period's closing TB onto next-period opening balances.
144 /// Selects which account code holds Retained Earnings (US GAAP
145 /// `"3200"`, SKR03/04 `"2970"`, IFRS varies). Only consulted by
146 /// [`generate_standalone_chain`] when computing carryover; the
147 /// per-period generation itself reads framework off the config.
148 /// Defaults to `"us_gaap"`. Must match how the prior period was
149 /// generated — mismatches mean net income lands in the wrong
150 /// account.
151 pub closing_to_opening_framework: String,
152}
153
154impl Default for StandaloneOptions {
155 fn default() -> Self {
156 Self {
157 prior_period_aggregate: None,
158 tolerate_missing_shards: false,
159 parallel_shards: true,
160 cgu_test_inputs: Vec::new(),
161 entity_opening_balances: std::collections::BTreeMap::new(),
162 cpi_series_by_currency: std::collections::BTreeMap::new(),
163 closing_to_opening_framework: "us_gaap".to_string(),
164 }
165 }
166}
167
168/// Top-level result returned by [`generate_standalone`].
169///
170/// `shard_summaries` is one entry per shard in
171/// `manifest.shard_plan.shards`, in the order the manifest declares
172/// them (rayon scheduling does not affect the result ordering — we
173/// sort post-collect).
174#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
175pub struct StandaloneSummary {
176 /// Path to the persisted manifest at `{out_dir}/manifest.json`.
177 pub manifest_path: PathBuf,
178 /// Per-shard summaries from `run_shard`, one per shard in the
179 /// manifest's [`crate::manifest::shard_plan::ShardPlan`].
180 pub shard_summaries: Vec<ShardSummary>,
181 /// Aggregate-phase summary from [`run_aggregate`].
182 pub aggregate: AggregateSummary,
183}
184
185// ── Public API ────────────────────────────────────────────────────────────────
186
187/// Drive the full v5.0 pipeline (manifest → shards → aggregate) in one
188/// call.
189///
190/// 1. Build the manifest from `cfg`.
191/// 2. Persist it to `{out_dir}/manifest.json`.
192/// 3. Run every shard in `manifest.shard_plan.shards` (parallel or
193/// sequential per `opts.parallel_shards`), writing per-entity
194/// archives under `{out_dir}/entities/{code}/`.
195/// 4. Run the aggregate-phase driver on `out_dir` (which now contains
196/// every shard's output) and emit consolidated FS artefacts.
197/// 5. Return a [`StandaloneSummary`] linking the manifest, every
198/// shard's summary, and the aggregate summary.
199///
200/// # Errors
201///
202/// - [`GroupError::Manifest`] / [`GroupError::Config`] propagated from
203/// [`build_manifest`].
204/// - [`GroupError::Shard`] propagated from any `run_shard` failure
205/// (orchestrator construction, generation, or per-entity output).
206/// - [`GroupError::Aggregate`] / [`GroupError::Io`] /
207/// [`GroupError::Serde`] propagated from [`run_aggregate`].
208/// - [`GroupError::Io`] if the manifest cannot be persisted.
209pub fn generate_standalone(
210 cfg: &GroupConfig,
211 out_dir: &Path,
212 opts: &StandaloneOptions,
213) -> GroupResult<StandaloneSummary> {
214 // ── 1. Manifest ─────────────────────────────────────────────────
215 let manifest = build_manifest(cfg)?;
216
217 // ── 2. Persist manifest at out_dir/manifest.json ────────────────
218 fs::create_dir_all(out_dir).map_err(GroupError::Io)?;
219 let manifest_path = out_dir.join("manifest.json");
220 let mut manifest_json = serde_json::to_string_pretty(&manifest)?;
221 manifest_json.push('\n');
222 fs::write(&manifest_path, manifest_json).map_err(GroupError::Io)?;
223
224 // ── 3. Run every shard ──────────────────────────────────────────
225 //
226 // Capture the shard ids in declaration order from
227 // `shard_plan.shards`, then dispatch via rayon (parallel) or a
228 // plain map (sequential). Both code paths materialise into a
229 // `Vec<ShardSummary>` in declaration order so callers receive a
230 // deterministic ordering regardless of scheduler interleaving.
231 let shard_ids: Vec<String> = manifest
232 .shard_plan
233 .shards
234 .iter()
235 .map(|s| s.shard_id.clone())
236 .collect();
237
238 let shard_summaries: Vec<ShardSummary> = if opts.parallel_shards {
239 // rayon's par_iter preserves the input order in the collected
240 // Vec, so the result is still declaration-ordered.
241 shard_ids
242 .par_iter()
243 .map(|sid| {
244 run_shard_with_opening_balances(
245 &manifest,
246 sid,
247 out_dir,
248 &opts.entity_opening_balances,
249 )
250 })
251 .collect::<GroupResult<Vec<_>>>()?
252 } else {
253 let mut out: Vec<ShardSummary> = Vec::with_capacity(shard_ids.len());
254 for sid in &shard_ids {
255 out.push(run_shard_with_opening_balances(
256 &manifest,
257 sid,
258 out_dir,
259 &opts.entity_opening_balances,
260 )?);
261 }
262 out
263 };
264
265 // ── 4. Aggregate phase ──────────────────────────────────────────
266 //
267 // The runner writes every per-entity archive under
268 // `{out_dir}/entities/{code}/`, so `shards_dir == out_dir` for the
269 // aggregate driver.
270 let agg_opts = AggregateOptions {
271 prior_period_aggregate: opts.prior_period_aggregate.clone(),
272 tolerate_missing_shards: opts.tolerate_missing_shards,
273 cgu_test_inputs: opts.cgu_test_inputs.clone(),
274 cpi_series_by_currency: opts.cpi_series_by_currency.clone(),
275 };
276 let aggregate = run_aggregate(&manifest, out_dir, out_dir, &agg_opts)?;
277
278 Ok(StandaloneSummary {
279 manifest_path,
280 shard_summaries,
281 aggregate,
282 })
283}
284
285// ── Multi-period chain helper (v5.3) ──────────────────────────────────────────
286
287/// One period in a multi-period engagement chain. Carries the
288/// period-specific config + the output subdirectory the chain
289/// runner uses to scope this period's archive.
290///
291/// `out_subdir` is interpreted relative to the chain's `base_out_dir`
292/// (the second parameter of [`generate_standalone_chain`]) — typically
293/// something like `"2024_Q1"` or `"period_001"`. Subdirs must be
294/// unique within a chain or [`generate_standalone_chain`] returns
295/// [`GroupError::Config`].
296#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct PeriodChainSpec {
298 /// Period config (start_date / length / fiscal_year_end) for this
299 /// period. Replaces [`GroupConfig::period`] when the chain runner
300 /// invokes the per-period [`generate_standalone`] call.
301 pub period: crate::config::PeriodConfig,
302 /// Output subdirectory under `base_out_dir`. Must be unique
303 /// within the chain.
304 pub out_subdir: String,
305}
306
307/// Drive the full v5.0 pipeline once per period, chaining the
308/// aggregate-phase prior-period plumbing automatically.
309///
310/// For each period in `periods`:
311///
312/// 1. Clone `base_cfg`, override its `.period` with the spec's
313/// period config. All other fields (ownership, fx, audit, tax,
314/// cgu, output) flow through unchanged.
315/// 2. Compute `out_dir = base_out_dir.join(spec.out_subdir)`.
316/// 3. For period 0, use `opts` as-is — caller's
317/// `prior_period_aggregate` is preserved (lets engagements seed
318/// from an externally produced archive).
319/// 4. For periods 1..N, override `opts.prior_period_aggregate` to
320/// point to the previous period's `out_dir` so opening NCI / CTA /
321/// equity-method values flow forward automatically.
322/// 5. Invoke [`generate_standalone`] and append the [`StandaloneSummary`]
323/// to the result vector.
324///
325/// On per-period failure the chain stops; outputs already written for
326/// prior periods are preserved on disk.
327///
328/// # Companion to [`crate::aggregate::run_aggregate_chain`]
329///
330/// `run_aggregate_chain` (PR #150) handles the same prior-period
331/// stitching at the aggregate-only layer (i.e. when shard archives
332/// already exist on disk). This helper extends the pattern to the
333/// full pipeline — it generates the data per period **and** chains
334/// the aggregate plumbing.
335///
336/// # Errors
337///
338/// - [`GroupError::Config`] if `periods` is empty.
339/// - [`GroupError::Config`] if any two `out_subdir` values collide.
340/// - Any error from the underlying [`generate_standalone`] call.
341///
342/// # Caveat — orchestrator opening balances
343///
344/// **v5.3** — This helper now threads **both** the aggregate-phase
345/// prior-period plumbing (opening NCI / CTA / equity-method carrying
346/// values) **and** the orchestrator-side opening-balance carryover
347/// (entity-level opening TB = prior period's closing TB). Between
348/// periods N and N+1:
349///
350/// 1. After period N's `generate_standalone` returns, walk
351/// `period_N_out_dir/entities/{code}/period_close/trial_balances.json`
352/// via [`crate::aggregate::opening_balance::read_prior_period_closing_tbs`]
353/// for every entity in the manifest.
354/// 2. Project each closing TB onto its BS positions via
355/// [`crate::aggregate::opening_balance::extract_opening_balances`].
356/// 3. Convert the group-side `OpeningBalance` records into core-side
357/// `EntityOpeningBalance` records (the runtime-friendly carrier
358/// `ShardContext.opening_balances` consumes).
359/// 4. Populate period-(N+1)'s `StandaloneOptions.entity_opening_balances`
360/// with the per-entity carry-forwards. The shard runner installs
361/// these into each entity's `ShardContext`, and the orchestrator's
362/// Phase 3b uses them in place of the industry-mix opening
363/// generator.
364///
365/// On the first period (idx == 0) and when the entity has no prior
366/// closing TB on disk (e.g. it was a generated entity that wasn't
367/// persisted), the carryover is silently skipped — that entity's
368/// orchestrator falls through to fresh generator output.
369pub fn generate_standalone_chain(
370 base_cfg: &GroupConfig,
371 periods: Vec<PeriodChainSpec>,
372 base_out_dir: &Path,
373 opts: &StandaloneOptions,
374) -> GroupResult<Vec<StandaloneSummary>> {
375 if periods.is_empty() {
376 return Err(GroupError::Config(
377 "generate_standalone_chain: periods must be non-empty".to_string(),
378 ));
379 }
380
381 // Reject duplicate out_subdir values up front — running two
382 // periods into the same directory would produce a corrupt archive
383 // (the second period's outputs would silently overwrite the first).
384 let mut seen: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
385 for spec in &periods {
386 if !seen.insert(spec.out_subdir.as_str()) {
387 return Err(GroupError::Config(format!(
388 "generate_standalone_chain: duplicate out_subdir `{}` — every \
389 period must have a unique output directory",
390 spec.out_subdir,
391 )));
392 }
393 }
394
395 let mut summaries: Vec<StandaloneSummary> = Vec::with_capacity(periods.len());
396 let mut prior_out: Option<PathBuf> = None;
397
398 for (idx, spec) in periods.into_iter().enumerate() {
399 let mut cfg = base_cfg.clone();
400 cfg.period = spec.period;
401
402 let out_dir = base_out_dir.join(&spec.out_subdir);
403
404 let mut period_opts = opts.clone();
405 if idx > 0 {
406 period_opts.prior_period_aggregate = prior_out.clone();
407 // **v5.3** — auto-thread the orchestrator-side opening-
408 // balance carryover from the prior period. Walk the
409 // manifest to know which entity codes to ask for, then
410 // load + project each entity's closing TB into runtime-
411 // friendly EntityOpeningBalance records. Entities whose
412 // closing TB doesn't exist on disk are silently skipped
413 // (best-effort — the orchestrator's industry-mix
414 // generator handles them).
415 if let Some(prior) = &prior_out {
416 let entity_codes: Vec<String> = base_cfg
417 .ownership
418 .entities
419 .iter()
420 .map(|e| e.code.clone())
421 .collect();
422 let closing_tbs = crate::aggregate::opening_balance::read_prior_period_closing_tbs(
423 prior,
424 &entity_codes,
425 )?;
426 // **v5.31 C2 (#157)** — project each closing TB onto its
427 // opening positions via `project_closing_to_opening`,
428 // which absorbs the period's net income into Retained
429 // Earnings. The legacy `extract_opening_balances` used
430 // to live here, but it silently dropped P&L lines
431 // without absorbing them — correct only when the
432 // closing TB is already PostClosing (zero P&L). The
433 // orchestrator emits `Adjusted` TBs (still showing
434 // period P&L), so the legacy path lost net income on
435 // the chain hand-off.
436 let mut openings_by_entity: std::collections::BTreeMap<
437 String,
438 Vec<datasynth_core::models::balance::EntityOpeningBalance>,
439 > = std::collections::BTreeMap::new();
440 for (code, tb) in &closing_tbs {
441 let core_openings = datasynth_generators::balance::project_closing_to_opening(
442 tb,
443 &opts.closing_to_opening_framework,
444 );
445 if !core_openings.is_empty() {
446 openings_by_entity.insert(code.clone(), core_openings);
447 }
448 }
449 period_opts.entity_opening_balances = openings_by_entity;
450 }
451 }
452
453 let summary = generate_standalone(&cfg, &out_dir, &period_opts)?;
454 prior_out = Some(out_dir);
455 summaries.push(summary);
456 }
457
458 Ok(summaries)
459}
460
461// ── Tests ──────────────────────────────────────────────────────────────────────
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466 use chrono::NaiveDate;
467 use std::path::PathBuf;
468
469 #[test]
470 fn period_chain_spec_field_shape() {
471 // Pin the public API shape — refactors that hide or rename
472 // fields will break the build.
473 let spec = PeriodChainSpec {
474 period: crate::config::PeriodConfig {
475 start_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
476 length: crate::config::PeriodLength::Quarterly,
477 fiscal_year_end: None,
478 },
479 out_subdir: "2024_Q1".to_string(),
480 };
481 assert_eq!(spec.out_subdir, "2024_Q1");
482 assert_eq!(spec.period.length, crate::config::PeriodLength::Quarterly);
483 }
484
485 #[test]
486 fn empty_periods_rejected() {
487 let cfg = sample_min_config();
488 let err = generate_standalone_chain(
489 &cfg,
490 Vec::new(),
491 &PathBuf::from("/tmp/never"),
492 &StandaloneOptions::default(),
493 )
494 .unwrap_err();
495 assert!(format!("{err}").contains("must be non-empty"));
496 }
497
498 #[test]
499 fn duplicate_out_subdir_rejected() {
500 let cfg = sample_min_config();
501 let p1 = PeriodChainSpec {
502 period: crate::config::PeriodConfig {
503 start_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
504 length: crate::config::PeriodLength::Quarterly,
505 fiscal_year_end: None,
506 },
507 out_subdir: "DUP".to_string(),
508 };
509 let p2 = PeriodChainSpec {
510 period: crate::config::PeriodConfig {
511 start_date: NaiveDate::from_ymd_opt(2024, 4, 1).unwrap(),
512 length: crate::config::PeriodLength::Quarterly,
513 fiscal_year_end: None,
514 },
515 out_subdir: "DUP".to_string(),
516 };
517 let err = generate_standalone_chain(
518 &cfg,
519 vec![p1, p2],
520 &PathBuf::from("/tmp/never"),
521 &StandaloneOptions::default(),
522 )
523 .unwrap_err();
524 assert!(format!("{err}").contains("duplicate out_subdir"));
525 }
526
527 #[test]
528 fn standalone_options_default_has_empty_entity_opening_balances() {
529 // v5.3 carryover field defaults to empty — single-period
530 // engagements see no behaviour change.
531 let opts = StandaloneOptions::default();
532 assert!(opts.entity_opening_balances.is_empty());
533 }
534
535 #[test]
536 fn standalone_options_can_carry_per_entity_openings() {
537 use datasynth_core::models::balance::{AccountType, EntityOpeningBalance};
538 use rust_decimal::Decimal;
539 let mut opts = StandaloneOptions::default();
540 opts.entity_opening_balances.insert(
541 "SUB".to_string(),
542 vec![EntityOpeningBalance {
543 account_code: "1000".to_string(),
544 account_type: AccountType::Asset,
545 debit: Decimal::from(50_000),
546 credit: Decimal::ZERO,
547 }],
548 );
549 assert_eq!(opts.entity_opening_balances.len(), 1);
550 assert_eq!(opts.entity_opening_balances["SUB"][0].account_code, "1000");
551 }
552
553 /// Helper — minimum-viable GroupConfig for shape-only tests.
554 /// Doesn't reach the orchestrator (early-validation tests bail
555 /// before generate_standalone is called).
556 fn sample_min_config() -> GroupConfig {
557 GroupConfig {
558 id: "TEST".to_string(),
559 name: None,
560 presentation_currency: "CHF".to_string(),
561 period: crate::config::PeriodConfig {
562 start_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
563 length: crate::config::PeriodLength::Quarterly,
564 fiscal_year_end: None,
565 },
566 seed: 42,
567 defaults: serde_yaml::Value::Null,
568 scoping_profiles: Default::default(),
569 ownership: crate::config::OwnershipConfig {
570 parent_entity_code: "P".to_string(),
571 entities: Vec::new(),
572 generated: Vec::new(),
573 entities_from: None,
574 },
575 intercompany: Default::default(),
576 fx: crate::config::FxConfig {
577 base_currency: "CHF".to_string(),
578 rate_source: Default::default(),
579 rates: Default::default(),
580 policy: crate::config::FxPolicyConfig {
581 balance_sheet: crate::config::FxRateBasis::Closing,
582 income_statement: crate::config::FxRateBasis::Average,
583 equity: crate::config::FxRateBasis::Historical,
584 },
585 },
586 audit: Default::default(),
587 tax: Default::default(),
588 cgu: Default::default(),
589 output: Default::default(),
590 fleet: None,
591 }
592 }
593}