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 {
462 if contingent > Decimal::ZERO {
463 let mut line = JournalEntryLine::credit(
464 doc_id,
465 line_num,
466 datasynth_core::accounts::liability_accounts::NET_PENSION_LIABILITY.to_string(),
467 contingent,
468 );
469 line.line_text = Some("Contingent consideration liability".to_string());
470 je.add_line(line);
471 line_num += 1;
472 }
473 }
474
475 let raw_goodwill =
477 bc.consideration.total - bc.purchase_price_allocation.net_identifiable_assets_fv;
478 if raw_goodwill < Decimal::ZERO {
479 let gain = (-raw_goodwill).round_dp(2);
480 let mut line =
481 JournalEntryLine::credit(doc_id, line_num, BARGAIN_PURCHASE_GAIN.to_string(), gain);
482 line.line_text = Some("Bargain purchase gain".to_string());
483 je.add_line(line);
484 }
485
486 vec![je]
487 }
488
489 fn generate_amortization_journal_entries(
493 &mut self,
494 company_code: &str,
495 currency: &str,
496 bc: &BusinessCombination,
497 start_date: NaiveDate,
498 end_date: NaiveDate,
499 ) -> Vec<JournalEntry> {
500 let mut jes = Vec::new();
501
502 let intangibles: Vec<(&AcquisitionFvAdjustment, u32)> = bc
504 .purchase_price_allocation
505 .identifiable_assets
506 .iter()
507 .filter_map(|adj| adj.useful_life_years.map(|life| (adj, life)))
508 .filter(|(adj, _)| adj.fair_value > Decimal::ZERO)
509 .collect();
510
511 if intangibles.is_empty() {
512 return jes;
513 }
514
515 let mut period_dates: Vec<NaiveDate> = Vec::new();
517 let acq_date = bc.acquisition_date;
518 let mut current =
519 NaiveDate::from_ymd_opt(start_date.year(), start_date.month(), 1).unwrap_or(start_date);
520
521 loop {
522 let month_end = last_day_of_month(current.year(), current.month());
524 if month_end > end_date {
525 break;
526 }
527 if month_end > acq_date {
529 period_dates.push(month_end);
530 }
531 let next_month = current.month() % 12 + 1;
533 let next_year = if current.month() == 12 {
534 current.year() + 1
535 } else {
536 current.year()
537 };
538 match NaiveDate::from_ymd_opt(next_year, next_month, 1) {
539 Some(d) => current = d,
540 None => break,
541 }
542 }
543
544 for period_end in period_dates {
545 let doc_id = self.uuid_factory.next();
546 let mut header = JournalEntryHeader::with_deterministic_id(
547 company_code.to_string(),
548 period_end,
549 doc_id,
550 );
551 header.document_type = "AM".to_string();
552 header.currency = currency.to_string();
553 header.source = TransactionSource::Automated;
554 header.header_text = Some(format!(
555 "Amortization – acquired intangibles ({})",
556 bc.acquiree_name
557 ));
558 header.reference = Some(bc.id.clone());
559
560 let mut je = JournalEntry::new(header);
561 let mut line_num: u32 = 1;
562 for (adj, life_years) in &intangibles {
563 let months = Decimal::from(*life_years) * Decimal::from(12u32);
565 let monthly_amort = (adj.fair_value / months).round_dp(2);
566
567 if monthly_amort == Decimal::ZERO {
568 continue;
569 }
570
571 let amort_account = intangible_amort_account(&adj.asset_or_liability);
572
573 let mut dr_line = JournalEntryLine::debit(
575 doc_id,
576 line_num,
577 AMORTIZATION_EXPENSE.to_string(),
578 monthly_amort,
579 );
580 dr_line.line_text = Some(format!("Amortization – {}", adj.asset_or_liability));
581 je.add_line(dr_line);
582 line_num += 1;
583
584 let mut cr_line =
586 JournalEntryLine::credit(doc_id, line_num, amort_account, monthly_amort);
587 cr_line.line_text =
588 Some(format!("Accum. amortization – {}", adj.asset_or_liability));
589 je.add_line(cr_line);
590 line_num += 1;
591 }
592
593 if !je.lines.is_empty() {
595 jes.push(je);
596 }
597 }
598
599 jes
600 }
601
602 fn pct_of(&mut self, base: Decimal, pct_min: f64, pct_max: f64) -> Decimal {
608 let pct = self.rng.random_range(pct_min..=pct_max);
609 let pct_dec = Decimal::from_f64_retain(pct)
610 .unwrap_or(Decimal::from_f64_retain(pct_min).unwrap_or(Decimal::ONE));
611 (base * pct_dec).round_dp(2)
612 }
613
614 fn apply_step_up(&mut self, book_value: Decimal, step_up_pct: f64) -> Decimal {
616 let pct_dec = Decimal::from_f64_retain(step_up_pct).unwrap_or(Decimal::ZERO);
617 (book_value * (Decimal::ONE + pct_dec)).round_dp(2)
618 }
619
620 fn random_date_in_period(&mut self, start: NaiveDate, end: NaiveDate) -> NaiveDate {
623 let total_days = (end - start).num_days();
624 if total_days <= 0 {
625 return start;
626 }
627 let usable_days = (total_days * 3 / 4).max(1);
629 let offset = self.rng.random_range(0i64..usable_days);
630 start + chrono::Duration::days(offset)
631 }
632}
633
634fn asset_gl_account(description: &str) -> String {
640 match description {
641 "Property, Plant & Equipment" => FIXED_ASSETS.to_string(),
642 "Customer Relationships" => CUSTOMER_RELATIONSHIPS.to_string(),
643 "Trade Name" => TRADE_NAME.to_string(),
644 "Developed Technology" => TECHNOLOGY.to_string(),
645 "Inventory" => "1200".to_string(),
646 "Accounts Receivable" => "1100".to_string(),
647 _ => "1890".to_string(), }
649}
650
651fn liability_gl_account(description: &str) -> String {
653 match description {
654 "Accounts Payable" => "2000".to_string(),
655 "Long-term Debt" => "2600".to_string(),
656 "Deferred Revenue" => "2300".to_string(),
657 _ => "2890".to_string(), }
659}
660
661fn intangible_amort_account(description: &str) -> String {
663 let _ = description;
666 ACCUMULATED_AMORTIZATION.to_string()
667}
668
669fn last_day_of_month(year: i32, month: u32) -> NaiveDate {
671 let next_month = month % 12 + 1;
672 let next_year = if month == 12 { year + 1 } else { year };
673 NaiveDate::from_ymd_opt(next_year, next_month, 1)
674 .and_then(|d| d.pred_opt())
675 .unwrap_or_else(|| {
676 NaiveDate::from_ymd_opt(year, month, 28)
678 .unwrap_or(NaiveDate::from_ymd_opt(year, 1, 28).unwrap_or(NaiveDate::MIN))
679 })
680}
681
682#[cfg(test)]
687mod tests {
688 use super::*;
689
690 fn make_gen() -> BusinessCombinationGenerator {
691 BusinessCombinationGenerator::new(42)
692 }
693
694 fn make_dates() -> (NaiveDate, NaiveDate) {
695 let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
696 let end = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
697 (start, end)
698 }
699
700 #[test]
701 fn test_basic_generation() {
702 let mut gen = make_gen();
703 let (start, end) = make_dates();
704 let snap = gen.generate("C001", "USD", start, end, 2, "IFRS");
705
706 assert_eq!(snap.combinations.len(), 2);
707 assert!(!snap.journal_entries.is_empty());
708 }
709
710 #[test]
711 fn test_goodwill_equals_consideration_minus_net_assets() {
712 let mut gen = make_gen();
713 let (start, end) = make_dates();
714 let snap = gen.generate("C001", "USD", start, end, 3, "US_GAAP");
715
716 for bc in &snap.combinations {
717 let raw_goodwill =
718 bc.consideration.total - bc.purchase_price_allocation.net_identifiable_assets_fv;
719 if raw_goodwill >= Decimal::ZERO {
720 assert_eq!(bc.goodwill, raw_goodwill, "Goodwill mismatch for {}", bc.id);
721 } else {
722 assert_eq!(
724 bc.goodwill,
725 Decimal::ZERO,
726 "Bargain purchase goodwill should be zero for {}",
727 bc.id
728 );
729 }
730 }
731 }
732
733 #[test]
734 fn test_at_least_4_identifiable_assets() {
735 let mut gen = make_gen();
736 let (start, end) = make_dates();
737 let snap = gen.generate("C001", "USD", start, end, 3, "IFRS");
738
739 for bc in &snap.combinations {
740 assert!(
741 bc.purchase_price_allocation.identifiable_assets.len() >= 4,
742 "PPA should have at least 4 assets, got {} for {}",
743 bc.purchase_price_allocation.identifiable_assets.len(),
744 bc.id
745 );
746 }
747 }
748
749 #[test]
750 fn test_day1_jes_balanced() {
751 let mut gen = make_gen();
752 let (start, end) = make_dates();
753 let snap = gen.generate("C001", "USD", start, end, 2, "IFRS");
754
755 let day1_jes: Vec<_> = snap
757 .journal_entries
758 .iter()
759 .filter(|je| je.header.document_type == "BC")
760 .collect();
761
762 assert!(!day1_jes.is_empty(), "Should have Day 1 JEs");
763
764 for je in &day1_jes {
765 let total_debits: Decimal = je.lines.iter().map(|l| l.debit_amount).sum();
766 let total_credits: Decimal = je.lines.iter().map(|l| l.credit_amount).sum();
767 assert_eq!(
768 total_debits, total_credits,
769 "Day 1 JE {} is unbalanced: debits={}, credits={}",
770 je.header.document_id, total_debits, total_credits
771 );
772 }
773 }
774
775 #[test]
776 fn test_amortization_jes_balanced() {
777 let mut gen = make_gen();
778 let (start, end) = make_dates();
779 let snap = gen.generate("C001", "USD", start, end, 2, "IFRS");
780
781 let amort_jes: Vec<_> = snap
782 .journal_entries
783 .iter()
784 .filter(|je| je.header.document_type == "AM")
785 .collect();
786
787 assert!(!amort_jes.is_empty(), "Should have amortization JEs");
788
789 for je in &amort_jes {
790 let total_debits: Decimal = je.lines.iter().map(|l| l.debit_amount).sum();
791 let total_credits: Decimal = je.lines.iter().map(|l| l.credit_amount).sum();
792 assert_eq!(
793 total_debits, total_credits,
794 "Amortization JE {} is unbalanced: debits={}, credits={}",
795 je.header.document_id, total_debits, total_credits
796 );
797 }
798 }
799
800 #[test]
801 fn test_ppa_fair_values_positive_for_assets() {
802 let mut gen = make_gen();
803 let (start, end) = make_dates();
804 let snap = gen.generate("C001", "USD", start, end, 2, "US_GAAP");
805
806 for bc in &snap.combinations {
807 for adj in &bc.purchase_price_allocation.identifiable_assets {
808 assert!(
809 adj.fair_value > Decimal::ZERO,
810 "Asset {} should have positive fair value for {}",
811 adj.asset_or_liability,
812 bc.id
813 );
814 }
815 }
816 }
817
818 #[test]
819 fn test_consideration_total_correct() {
820 let mut gen = make_gen();
821 let (start, end) = make_dates();
822 let snap = gen.generate("C001", "USD", start, end, 3, "IFRS");
823
824 for bc in &snap.combinations {
825 let c = &bc.consideration;
826 let computed_total = c.cash
827 + c.shares_issued_value.unwrap_or(Decimal::ZERO)
828 + c.contingent_consideration.unwrap_or(Decimal::ZERO);
829 assert_eq!(
830 computed_total, c.total,
831 "Consideration components don't add up for {}",
832 bc.id
833 );
834 }
835 }
836
837 #[test]
838 fn test_deterministic_output() {
839 let (start, end) = make_dates();
840 let mut gen1 = BusinessCombinationGenerator::new(99);
841 let mut gen2 = BusinessCombinationGenerator::new(99);
842
843 let snap1 = gen1.generate("C001", "USD", start, end, 2, "IFRS");
844 let snap2 = gen2.generate("C001", "USD", start, end, 2, "IFRS");
845
846 assert_eq!(snap1.combinations.len(), snap2.combinations.len());
847 for (a, b) in snap1.combinations.iter().zip(snap2.combinations.iter()) {
848 assert_eq!(a.id, b.id);
849 assert_eq!(a.goodwill, b.goodwill);
850 assert_eq!(a.consideration.total, b.consideration.total);
851 }
852 assert_eq!(snap1.journal_entries.len(), snap2.journal_entries.len());
853 }
854
855 #[test]
856 fn test_zero_count_returns_empty() {
857 let mut gen = make_gen();
858 let (start, end) = make_dates();
859 let snap = gen.generate("C001", "USD", start, end, 0, "IFRS");
860 assert!(snap.combinations.is_empty());
861 assert!(snap.journal_entries.is_empty());
862 }
863}