1use chrono::{Datelike, NaiveDate};
11use datasynth_core::accounts::{
12 cash_accounts::OPERATING_CASH, control_accounts::FIXED_ASSETS, intangible_accounts::*,
13};
14use datasynth_core::models::{
15 business_combination::{
16 AcquisitionConsideration, AcquisitionFvAdjustment, AcquisitionPpa, BusinessCombination,
17 },
18 journal_entry::{JournalEntry, JournalEntryHeader, JournalEntryLine, TransactionSource},
19};
20use datasynth_core::utils::seeded_rng;
21use datasynth_core::uuid_factory::{DeterministicUuidFactory, GeneratorType};
22use rand::prelude::*;
23use rand_chacha::ChaCha8Rng;
24use rand_distr::LogNormal;
25use rust_decimal::Decimal;
26use rust_decimal_macros::dec;
27
28const ACQUIREE_NAMES: &[&str] = &[
34 "Apex Innovations Ltd",
35 "BlueCrest Technologies Inc",
36 "Cascade Manufacturing Co",
37 "Deltron Systems GmbH",
38 "Elevate Software Corp",
39 "FusionTech Solutions",
40 "GlobalEdge Partners",
41 "Harbinger Analytics Inc",
42 "IronBridge Industries",
43 "Jetstream Logistics Ltd",
44 "Keystone Digital GmbH",
45 "Lighthouse Pharma Corp",
46 "Meridian Energy Solutions",
47 "NovaTrend Consulting",
48 "Oceanic Data Systems",
49 "Pinnacle Biotech AG",
50 "Quickstep Retail Group",
51 "Redwood Semiconductor",
52 "Silverline Communications",
53 "TrueVision AI Corp",
54];
55
56#[derive(Debug, Default)]
62pub struct BusinessCombinationSnapshot {
63 pub combinations: Vec<BusinessCombination>,
65 pub journal_entries: Vec<JournalEntry>,
67}
68
69pub struct BusinessCombinationGenerator {
76 rng: ChaCha8Rng,
77 uuid_factory: DeterministicUuidFactory,
78}
79
80impl BusinessCombinationGenerator {
81 pub fn new(seed: u64) -> Self {
83 Self {
84 rng: seeded_rng(seed, 0),
85 uuid_factory: DeterministicUuidFactory::new(seed, GeneratorType::BusinessCombination),
86 }
87 }
88
89 pub fn generate(
99 &mut self,
100 company_code: &str,
101 currency: &str,
102 start_date: NaiveDate,
103 end_date: NaiveDate,
104 acquisition_count: usize,
105 framework: &str,
106 ) -> BusinessCombinationSnapshot {
107 if acquisition_count == 0 {
108 return BusinessCombinationSnapshot::default();
109 }
110
111 let count = acquisition_count.min(5);
112 let mut snapshot = BusinessCombinationSnapshot::default();
113
114 for i in 0..count {
115 let combination =
116 self.generate_one(company_code, currency, start_date, end_date, i, framework);
117
118 let day1_jes = self.generate_day1_journal_entries(company_code, currency, &combination);
120 snapshot.journal_entries.extend(day1_jes);
121
122 let amort_jes = self.generate_amortization_journal_entries(
124 company_code,
125 currency,
126 &combination,
127 start_date,
128 end_date,
129 );
130 snapshot.journal_entries.extend(amort_jes);
131
132 snapshot.combinations.push(combination);
133 }
134
135 snapshot
136 }
137
138 fn generate_one(
143 &mut self,
144 company_code: &str,
145 currency: &str,
146 start_date: NaiveDate,
147 end_date: NaiveDate,
148 index: usize,
149 framework: &str,
150 ) -> BusinessCombination {
151 let id = format!(
152 "BC-{}-{:04}",
153 company_code,
154 self.rng.random_range(1u32..=9999u32)
155 );
156
157 let acquiree_name = ACQUIREE_NAMES[index % ACQUIREE_NAMES.len()].to_string();
158
159 let acquisition_date = self.random_date_in_period(start_date, end_date);
161
162 let total_consideration = self.sample_consideration_amount();
164 let consideration = self.build_consideration(total_consideration);
165
166 let ppa = self.build_ppa(total_consideration, currency);
168
169 let raw_goodwill = total_consideration - ppa.net_identifiable_assets_fv;
171 let goodwill = if raw_goodwill > Decimal::ZERO {
172 raw_goodwill
173 } else {
174 Decimal::ZERO
176 };
177
178 BusinessCombination {
179 id,
180 acquirer_entity: company_code.to_string(),
181 acquiree_name,
182 acquiree_entity_code: None,
187 acquisition_date,
188 consideration,
189 purchase_price_allocation: ppa,
190 goodwill,
191 nci_measurement_method:
197 datasynth_core::models::intercompany::NciMeasurementMethod::Proportionate,
198 acquisition_date_nci_fair_value: None,
199 framework: framework.to_string(),
200 }
201 }
202
203 fn sample_consideration_amount(&mut self) -> Decimal {
205 let mu = 16.1_f64;
207 let sigma = 1.0_f64;
208 let log_normal = LogNormal::new(mu, sigma).expect("valid log-normal params");
209 let raw: f64 = log_normal.sample(&mut self.rng);
210 let clamped = raw.clamp(1_000_000.0, 50_000_000.0);
212 let rounded = (clamped / 1_000.0).round() * 1_000.0;
214 Decimal::from_f64_retain(rounded).unwrap_or(Decimal::from(10_000_000u64))
215 }
216
217 fn build_consideration(&mut self, total: Decimal) -> AcquisitionConsideration {
219 let cash_pct = self.rng.random_range(0.60_f64..=0.90_f64);
220 let cash_pct_dec = Decimal::from_f64_retain(cash_pct).unwrap_or(dec!(0.75));
221 let cash = (total * cash_pct_dec).round_dp(2);
222
223 let remainder = total - cash;
224
225 let contingent = if self.rng.random_bool(0.40) {
227 let contingent_pct = self.rng.random_range(0.30_f64..=0.60_f64);
228 let contingent_pct_dec = Decimal::from_f64_retain(contingent_pct).unwrap_or(dec!(0.40));
229 let c = (remainder * contingent_pct_dec).round_dp(2);
230 Some(c)
231 } else {
232 None
233 };
234
235 let shares_issued_value = if remainder > Decimal::ZERO {
236 let shares = remainder - contingent.unwrap_or(Decimal::ZERO);
237 if shares > Decimal::ZERO {
238 Some(shares.round_dp(2))
239 } else {
240 None
241 }
242 } else {
243 None
244 };
245
246 AcquisitionConsideration {
247 cash,
248 shares_issued_value,
249 contingent_consideration: contingent,
250 total,
251 }
252 }
253
254 fn build_ppa(&mut self, total_consideration: Decimal, _currency: &str) -> AcquisitionPpa {
256 let mut assets: Vec<AcquisitionFvAdjustment> = Vec::new();
257 let mut liabilities: Vec<AcquisitionFvAdjustment> = Vec::new();
258
259 let ppe_book = self.pct_of(total_consideration, 0.25_f64, 0.45_f64);
261 let ppe_stepup_pct = self.rng.random_range(0.10_f64..=0.25_f64);
262 let ppe_fv = self.apply_step_up(ppe_book, ppe_stepup_pct);
263 assets.push(AcquisitionFvAdjustment {
264 asset_or_liability: "Property, Plant & Equipment".to_string(),
265 book_value: ppe_book,
266 fair_value: ppe_fv,
267 step_up: ppe_fv - ppe_book,
268 useful_life_years: None, });
270
271 let cr_fv = self.pct_of(total_consideration, 0.15_f64, 0.25_f64);
273 let cr_life = self.rng.random_range(10u32..=15u32);
274 assets.push(AcquisitionFvAdjustment {
275 asset_or_liability: "Customer Relationships".to_string(),
276 book_value: Decimal::ZERO,
277 fair_value: cr_fv,
278 step_up: cr_fv,
279 useful_life_years: Some(cr_life),
280 });
281
282 let tn_fv = self.pct_of(total_consideration, 0.05_f64, 0.10_f64);
284 let tn_life = self.rng.random_range(15u32..=20u32);
285 assets.push(AcquisitionFvAdjustment {
286 asset_or_liability: "Trade Name".to_string(),
287 book_value: Decimal::ZERO,
288 fair_value: tn_fv,
289 step_up: tn_fv,
290 useful_life_years: Some(tn_life),
291 });
292
293 let tech_fv = self.pct_of(total_consideration, 0.05_f64, 0.15_f64);
295 let tech_life = self.rng.random_range(5u32..=8u32);
296 assets.push(AcquisitionFvAdjustment {
297 asset_or_liability: "Developed Technology".to_string(),
298 book_value: Decimal::ZERO,
299 fair_value: tech_fv,
300 step_up: tech_fv,
301 useful_life_years: Some(tech_life),
302 });
303
304 let inv_book = self.pct_of(total_consideration, 0.10_f64, 0.20_f64);
306 let inv_stepup_pct = self.rng.random_range(0.03_f64..=0.08_f64);
307 let inv_fv = self.apply_step_up(inv_book, inv_stepup_pct);
308 assets.push(AcquisitionFvAdjustment {
309 asset_or_liability: "Inventory".to_string(),
310 book_value: inv_book,
311 fair_value: inv_fv,
312 step_up: inv_fv - inv_book,
313 useful_life_years: None,
314 });
315
316 if self.rng.random_bool(0.70) {
318 let ar_book = self.pct_of(total_consideration, 0.05_f64, 0.15_f64);
319 assets.push(AcquisitionFvAdjustment {
320 asset_or_liability: "Accounts Receivable".to_string(),
321 book_value: ar_book,
322 fair_value: ar_book, step_up: Decimal::ZERO,
324 useful_life_years: None,
325 });
326 }
327
328 let ap_book = self.pct_of(total_consideration, 0.08_f64, 0.18_f64);
331 liabilities.push(AcquisitionFvAdjustment {
332 asset_or_liability: "Accounts Payable".to_string(),
333 book_value: ap_book,
334 fair_value: ap_book,
335 step_up: Decimal::ZERO,
336 useful_life_years: None,
337 });
338
339 if self.rng.random_bool(0.70) {
341 let debt_book = self.pct_of(total_consideration, 0.10_f64, 0.25_f64);
342 let debt_fv_adj = self.rng.random_range(-0.05_f64..=0.05_f64);
344 let debt_fv = self.apply_step_up(debt_book, debt_fv_adj);
345 liabilities.push(AcquisitionFvAdjustment {
346 asset_or_liability: "Long-term Debt".to_string(),
347 book_value: debt_book,
348 fair_value: debt_fv,
349 step_up: debt_fv - debt_book,
350 useful_life_years: None,
351 });
352 }
353
354 if self.rng.random_bool(0.40) {
356 let def_rev = self.pct_of(total_consideration, 0.02_f64, 0.06_f64);
357 liabilities.push(AcquisitionFvAdjustment {
358 asset_or_liability: "Deferred Revenue".to_string(),
359 book_value: def_rev,
360 fair_value: def_rev,
361 step_up: Decimal::ZERO,
362 useful_life_years: None,
363 });
364 }
365
366 let total_asset_fv: Decimal = assets.iter().map(|a| a.fair_value).sum();
368 let total_liability_fv: Decimal = liabilities.iter().map(|l| l.fair_value).sum();
369 let net_identifiable_assets_fv = total_asset_fv - total_liability_fv;
370
371 AcquisitionPpa {
372 identifiable_assets: assets,
373 identifiable_liabilities: liabilities,
374 net_identifiable_assets_fv,
375 }
376 }
377
378 fn generate_day1_journal_entries(
384 &mut self,
385 company_code: &str,
386 currency: &str,
387 bc: &BusinessCombination,
388 ) -> Vec<JournalEntry> {
389 let doc_id = self.uuid_factory.next();
390 let mut header = JournalEntryHeader::with_deterministic_id(
391 company_code.to_string(),
392 bc.acquisition_date,
393 doc_id,
394 );
395 header.document_type = "BC".to_string();
396 header.currency = currency.to_string();
397 header.source = TransactionSource::Manual;
398 header.header_text = Some(format!("Acquisition of {} – Day 1 PPA", bc.acquiree_name));
399 header.reference = Some(bc.id.clone());
400
401 let mut je = JournalEntry::new(header);
402 let mut line_num: u32 = 1;
403
404 for adj in &bc.purchase_price_allocation.identifiable_assets {
406 if adj.fair_value > Decimal::ZERO {
407 let account = asset_gl_account(&adj.asset_or_liability);
408 let mut line = JournalEntryLine::debit(doc_id, line_num, account, adj.fair_value);
409 line.line_text = Some(format!("Acquired asset: {}", adj.asset_or_liability));
410 je.add_line(line);
411 line_num += 1;
412 }
413 }
414
415 if bc.goodwill > Decimal::ZERO {
417 let mut line =
418 JournalEntryLine::debit(doc_id, line_num, GOODWILL.to_string(), bc.goodwill);
419 line.line_text = Some(format!("Goodwill – acquisition of {}", bc.acquiree_name));
420 je.add_line(line);
421 line_num += 1;
422 }
423
424 for adj in &bc.purchase_price_allocation.identifiable_liabilities {
426 if adj.fair_value > Decimal::ZERO {
427 let account = liability_gl_account(&adj.asset_or_liability);
428 let mut line = JournalEntryLine::credit(doc_id, line_num, account, adj.fair_value);
429 line.line_text = Some(format!("Assumed liability: {}", adj.asset_or_liability));
430 je.add_line(line);
431 line_num += 1;
432 }
433 }
434
435 if bc.consideration.cash > Decimal::ZERO {
437 let mut line = JournalEntryLine::credit(
438 doc_id,
439 line_num,
440 OPERATING_CASH.to_string(),
441 bc.consideration.cash,
442 );
443 line.line_text = Some("Cash paid – business combination".to_string());
444 je.add_line(line);
445 line_num += 1;
446 }
447
448 if let Some(shares_val) = bc.consideration.shares_issued_value {
450 if shares_val > Decimal::ZERO {
451 let mut line =
452 JournalEntryLine::credit(doc_id, line_num, "3100".to_string(), shares_val);
453 line.line_text = Some("Shares issued – business combination".to_string());
454 je.add_line(line);
455 line_num += 1;
456 }
457 }
458
459 if let Some(contingent) = bc.consideration.contingent_consideration {
461 if contingent > Decimal::ZERO {
462 let mut line =
463 JournalEntryLine::credit(doc_id, line_num, "2800".to_string(), contingent);
464 line.line_text = Some("Contingent consideration liability".to_string());
465 je.add_line(line);
466 line_num += 1;
467 }
468 }
469
470 let raw_goodwill =
472 bc.consideration.total - bc.purchase_price_allocation.net_identifiable_assets_fv;
473 if raw_goodwill < Decimal::ZERO {
474 let gain = (-raw_goodwill).round_dp(2);
475 let mut line =
476 JournalEntryLine::credit(doc_id, line_num, BARGAIN_PURCHASE_GAIN.to_string(), gain);
477 line.line_text = Some("Bargain purchase gain".to_string());
478 je.add_line(line);
479 }
480
481 vec![je]
482 }
483
484 fn generate_amortization_journal_entries(
488 &mut self,
489 company_code: &str,
490 currency: &str,
491 bc: &BusinessCombination,
492 start_date: NaiveDate,
493 end_date: NaiveDate,
494 ) -> Vec<JournalEntry> {
495 let mut jes = Vec::new();
496
497 let intangibles: Vec<(&AcquisitionFvAdjustment, u32)> = bc
499 .purchase_price_allocation
500 .identifiable_assets
501 .iter()
502 .filter_map(|adj| adj.useful_life_years.map(|life| (adj, life)))
503 .filter(|(adj, _)| adj.fair_value > Decimal::ZERO)
504 .collect();
505
506 if intangibles.is_empty() {
507 return jes;
508 }
509
510 let mut period_dates: Vec<NaiveDate> = Vec::new();
512 let acq_date = bc.acquisition_date;
513 let mut current =
514 NaiveDate::from_ymd_opt(start_date.year(), start_date.month(), 1).unwrap_or(start_date);
515
516 loop {
517 let month_end = last_day_of_month(current.year(), current.month());
519 if month_end > end_date {
520 break;
521 }
522 if month_end > acq_date {
524 period_dates.push(month_end);
525 }
526 let next_month = current.month() % 12 + 1;
528 let next_year = if current.month() == 12 {
529 current.year() + 1
530 } else {
531 current.year()
532 };
533 match NaiveDate::from_ymd_opt(next_year, next_month, 1) {
534 Some(d) => current = d,
535 None => break,
536 }
537 }
538
539 for period_end in period_dates {
540 let doc_id = self.uuid_factory.next();
541 let mut header = JournalEntryHeader::with_deterministic_id(
542 company_code.to_string(),
543 period_end,
544 doc_id,
545 );
546 header.document_type = "AM".to_string();
547 header.currency = currency.to_string();
548 header.source = TransactionSource::Automated;
549 header.header_text = Some(format!(
550 "Amortization – acquired intangibles ({})",
551 bc.acquiree_name
552 ));
553 header.reference = Some(bc.id.clone());
554
555 let mut je = JournalEntry::new(header);
556 let mut line_num: u32 = 1;
557 for (adj, life_years) in &intangibles {
558 let months = Decimal::from(*life_years) * Decimal::from(12u32);
560 let monthly_amort = (adj.fair_value / months).round_dp(2);
561
562 if monthly_amort == Decimal::ZERO {
563 continue;
564 }
565
566 let amort_account = intangible_amort_account(&adj.asset_or_liability);
567
568 let mut dr_line = JournalEntryLine::debit(
570 doc_id,
571 line_num,
572 AMORTIZATION_EXPENSE.to_string(),
573 monthly_amort,
574 );
575 dr_line.line_text = Some(format!("Amortization – {}", adj.asset_or_liability));
576 je.add_line(dr_line);
577 line_num += 1;
578
579 let mut cr_line =
581 JournalEntryLine::credit(doc_id, line_num, amort_account, monthly_amort);
582 cr_line.line_text =
583 Some(format!("Accum. amortization – {}", adj.asset_or_liability));
584 je.add_line(cr_line);
585 line_num += 1;
586 }
587
588 if !je.lines.is_empty() {
590 jes.push(je);
591 }
592 }
593
594 jes
595 }
596
597 fn pct_of(&mut self, base: Decimal, pct_min: f64, pct_max: f64) -> Decimal {
603 let pct = self.rng.random_range(pct_min..=pct_max);
604 let pct_dec = Decimal::from_f64_retain(pct)
605 .unwrap_or(Decimal::from_f64_retain(pct_min).unwrap_or(Decimal::ONE));
606 (base * pct_dec).round_dp(2)
607 }
608
609 fn apply_step_up(&mut self, book_value: Decimal, step_up_pct: f64) -> Decimal {
611 let pct_dec = Decimal::from_f64_retain(step_up_pct).unwrap_or(Decimal::ZERO);
612 (book_value * (Decimal::ONE + pct_dec)).round_dp(2)
613 }
614
615 fn random_date_in_period(&mut self, start: NaiveDate, end: NaiveDate) -> NaiveDate {
618 let total_days = (end - start).num_days();
619 if total_days <= 0 {
620 return start;
621 }
622 let usable_days = (total_days * 3 / 4).max(1);
624 let offset = self.rng.random_range(0i64..usable_days);
625 start + chrono::Duration::days(offset)
626 }
627}
628
629fn asset_gl_account(description: &str) -> String {
635 match description {
636 "Property, Plant & Equipment" => FIXED_ASSETS.to_string(),
637 "Customer Relationships" => CUSTOMER_RELATIONSHIPS.to_string(),
638 "Trade Name" => TRADE_NAME.to_string(),
639 "Developed Technology" => TECHNOLOGY.to_string(),
640 "Inventory" => "1200".to_string(),
641 "Accounts Receivable" => "1100".to_string(),
642 _ => "1890".to_string(), }
644}
645
646fn liability_gl_account(description: &str) -> String {
648 match description {
649 "Accounts Payable" => "2000".to_string(),
650 "Long-term Debt" => "2600".to_string(),
651 "Deferred Revenue" => "2300".to_string(),
652 _ => "2890".to_string(), }
654}
655
656fn intangible_amort_account(description: &str) -> String {
658 let _ = description;
661 ACCUMULATED_AMORTIZATION.to_string()
662}
663
664fn last_day_of_month(year: i32, month: u32) -> NaiveDate {
666 let next_month = month % 12 + 1;
667 let next_year = if month == 12 { year + 1 } else { year };
668 NaiveDate::from_ymd_opt(next_year, next_month, 1)
669 .and_then(|d| d.pred_opt())
670 .unwrap_or_else(|| {
671 NaiveDate::from_ymd_opt(year, month, 28)
673 .unwrap_or(NaiveDate::from_ymd_opt(year, 1, 28).unwrap_or(NaiveDate::MIN))
674 })
675}
676
677#[cfg(test)]
682#[allow(clippy::unwrap_used)]
683mod tests {
684 use super::*;
685
686 fn make_gen() -> BusinessCombinationGenerator {
687 BusinessCombinationGenerator::new(42)
688 }
689
690 fn make_dates() -> (NaiveDate, NaiveDate) {
691 let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
692 let end = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
693 (start, end)
694 }
695
696 #[test]
697 fn test_basic_generation() {
698 let mut gen = make_gen();
699 let (start, end) = make_dates();
700 let snap = gen.generate("C001", "USD", start, end, 2, "IFRS");
701
702 assert_eq!(snap.combinations.len(), 2);
703 assert!(!snap.journal_entries.is_empty());
704 }
705
706 #[test]
707 fn test_goodwill_equals_consideration_minus_net_assets() {
708 let mut gen = make_gen();
709 let (start, end) = make_dates();
710 let snap = gen.generate("C001", "USD", start, end, 3, "US_GAAP");
711
712 for bc in &snap.combinations {
713 let raw_goodwill =
714 bc.consideration.total - bc.purchase_price_allocation.net_identifiable_assets_fv;
715 if raw_goodwill >= Decimal::ZERO {
716 assert_eq!(bc.goodwill, raw_goodwill, "Goodwill mismatch for {}", bc.id);
717 } else {
718 assert_eq!(
720 bc.goodwill,
721 Decimal::ZERO,
722 "Bargain purchase goodwill should be zero for {}",
723 bc.id
724 );
725 }
726 }
727 }
728
729 #[test]
730 fn test_at_least_4_identifiable_assets() {
731 let mut gen = make_gen();
732 let (start, end) = make_dates();
733 let snap = gen.generate("C001", "USD", start, end, 3, "IFRS");
734
735 for bc in &snap.combinations {
736 assert!(
737 bc.purchase_price_allocation.identifiable_assets.len() >= 4,
738 "PPA should have at least 4 assets, got {} for {}",
739 bc.purchase_price_allocation.identifiable_assets.len(),
740 bc.id
741 );
742 }
743 }
744
745 #[test]
746 fn test_day1_jes_balanced() {
747 let mut gen = make_gen();
748 let (start, end) = make_dates();
749 let snap = gen.generate("C001", "USD", start, end, 2, "IFRS");
750
751 let day1_jes: Vec<_> = snap
753 .journal_entries
754 .iter()
755 .filter(|je| je.header.document_type == "BC")
756 .collect();
757
758 assert!(!day1_jes.is_empty(), "Should have Day 1 JEs");
759
760 for je in &day1_jes {
761 let total_debits: Decimal = je.lines.iter().map(|l| l.debit_amount).sum();
762 let total_credits: Decimal = je.lines.iter().map(|l| l.credit_amount).sum();
763 assert_eq!(
764 total_debits, total_credits,
765 "Day 1 JE {} is unbalanced: debits={}, credits={}",
766 je.header.document_id, total_debits, total_credits
767 );
768 }
769 }
770
771 #[test]
772 fn test_amortization_jes_balanced() {
773 let mut gen = make_gen();
774 let (start, end) = make_dates();
775 let snap = gen.generate("C001", "USD", start, end, 2, "IFRS");
776
777 let amort_jes: Vec<_> = snap
778 .journal_entries
779 .iter()
780 .filter(|je| je.header.document_type == "AM")
781 .collect();
782
783 assert!(!amort_jes.is_empty(), "Should have amortization JEs");
784
785 for je in &amort_jes {
786 let total_debits: Decimal = je.lines.iter().map(|l| l.debit_amount).sum();
787 let total_credits: Decimal = je.lines.iter().map(|l| l.credit_amount).sum();
788 assert_eq!(
789 total_debits, total_credits,
790 "Amortization JE {} is unbalanced: debits={}, credits={}",
791 je.header.document_id, total_debits, total_credits
792 );
793 }
794 }
795
796 #[test]
797 fn test_ppa_fair_values_positive_for_assets() {
798 let mut gen = make_gen();
799 let (start, end) = make_dates();
800 let snap = gen.generate("C001", "USD", start, end, 2, "US_GAAP");
801
802 for bc in &snap.combinations {
803 for adj in &bc.purchase_price_allocation.identifiable_assets {
804 assert!(
805 adj.fair_value > Decimal::ZERO,
806 "Asset {} should have positive fair value for {}",
807 adj.asset_or_liability,
808 bc.id
809 );
810 }
811 }
812 }
813
814 #[test]
815 fn test_consideration_total_correct() {
816 let mut gen = make_gen();
817 let (start, end) = make_dates();
818 let snap = gen.generate("C001", "USD", start, end, 3, "IFRS");
819
820 for bc in &snap.combinations {
821 let c = &bc.consideration;
822 let computed_total = c.cash
823 + c.shares_issued_value.unwrap_or(Decimal::ZERO)
824 + c.contingent_consideration.unwrap_or(Decimal::ZERO);
825 assert_eq!(
826 computed_total, c.total,
827 "Consideration components don't add up for {}",
828 bc.id
829 );
830 }
831 }
832
833 #[test]
834 fn test_deterministic_output() {
835 let (start, end) = make_dates();
836 let mut gen1 = BusinessCombinationGenerator::new(99);
837 let mut gen2 = BusinessCombinationGenerator::new(99);
838
839 let snap1 = gen1.generate("C001", "USD", start, end, 2, "IFRS");
840 let snap2 = gen2.generate("C001", "USD", start, end, 2, "IFRS");
841
842 assert_eq!(snap1.combinations.len(), snap2.combinations.len());
843 for (a, b) in snap1.combinations.iter().zip(snap2.combinations.iter()) {
844 assert_eq!(a.id, b.id);
845 assert_eq!(a.goodwill, b.goodwill);
846 assert_eq!(a.consideration.total, b.consideration.total);
847 }
848 assert_eq!(snap1.journal_entries.len(), snap2.journal_entries.len());
849 }
850
851 #[test]
852 fn test_zero_count_returns_empty() {
853 let mut gen = make_gen();
854 let (start, end) = make_dates();
855 let snap = gen.generate("C001", "USD", start, end, 0, "IFRS");
856 assert!(snap.combinations.is_empty());
857 assert!(snap.journal_entries.is_empty());
858 }
859}