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 acquisition_date,
183 consideration,
184 purchase_price_allocation: ppa,
185 goodwill,
186 framework: framework.to_string(),
187 }
188 }
189
190 fn sample_consideration_amount(&mut self) -> Decimal {
192 let mu = 16.1_f64;
194 let sigma = 1.0_f64;
195 let log_normal = LogNormal::new(mu, sigma).expect("valid log-normal params");
196 let raw: f64 = log_normal.sample(&mut self.rng);
197 let clamped = raw.clamp(1_000_000.0, 50_000_000.0);
199 let rounded = (clamped / 1_000.0).round() * 1_000.0;
201 Decimal::from_f64_retain(rounded).unwrap_or(Decimal::from(10_000_000u64))
202 }
203
204 fn build_consideration(&mut self, total: Decimal) -> AcquisitionConsideration {
206 let cash_pct = self.rng.random_range(0.60_f64..=0.90_f64);
207 let cash_pct_dec = Decimal::from_f64_retain(cash_pct).unwrap_or(dec!(0.75));
208 let cash = (total * cash_pct_dec).round_dp(2);
209
210 let remainder = total - cash;
211
212 let contingent = if self.rng.random_bool(0.40) {
214 let contingent_pct = self.rng.random_range(0.30_f64..=0.60_f64);
215 let contingent_pct_dec = Decimal::from_f64_retain(contingent_pct).unwrap_or(dec!(0.40));
216 let c = (remainder * contingent_pct_dec).round_dp(2);
217 Some(c)
218 } else {
219 None
220 };
221
222 let shares_issued_value = if remainder > Decimal::ZERO {
223 let shares = remainder - contingent.unwrap_or(Decimal::ZERO);
224 if shares > Decimal::ZERO {
225 Some(shares.round_dp(2))
226 } else {
227 None
228 }
229 } else {
230 None
231 };
232
233 AcquisitionConsideration {
234 cash,
235 shares_issued_value,
236 contingent_consideration: contingent,
237 total,
238 }
239 }
240
241 fn build_ppa(&mut self, total_consideration: Decimal, _currency: &str) -> AcquisitionPpa {
243 let mut assets: Vec<AcquisitionFvAdjustment> = Vec::new();
244 let mut liabilities: Vec<AcquisitionFvAdjustment> = Vec::new();
245
246 let ppe_book = self.pct_of(total_consideration, 0.25_f64, 0.45_f64);
248 let ppe_stepup_pct = self.rng.random_range(0.10_f64..=0.25_f64);
249 let ppe_fv = self.apply_step_up(ppe_book, ppe_stepup_pct);
250 assets.push(AcquisitionFvAdjustment {
251 asset_or_liability: "Property, Plant & Equipment".to_string(),
252 book_value: ppe_book,
253 fair_value: ppe_fv,
254 step_up: ppe_fv - ppe_book,
255 useful_life_years: None, });
257
258 let cr_fv = self.pct_of(total_consideration, 0.15_f64, 0.25_f64);
260 let cr_life = self.rng.random_range(10u32..=15u32);
261 assets.push(AcquisitionFvAdjustment {
262 asset_or_liability: "Customer Relationships".to_string(),
263 book_value: Decimal::ZERO,
264 fair_value: cr_fv,
265 step_up: cr_fv,
266 useful_life_years: Some(cr_life),
267 });
268
269 let tn_fv = self.pct_of(total_consideration, 0.05_f64, 0.10_f64);
271 let tn_life = self.rng.random_range(15u32..=20u32);
272 assets.push(AcquisitionFvAdjustment {
273 asset_or_liability: "Trade Name".to_string(),
274 book_value: Decimal::ZERO,
275 fair_value: tn_fv,
276 step_up: tn_fv,
277 useful_life_years: Some(tn_life),
278 });
279
280 let tech_fv = self.pct_of(total_consideration, 0.05_f64, 0.15_f64);
282 let tech_life = self.rng.random_range(5u32..=8u32);
283 assets.push(AcquisitionFvAdjustment {
284 asset_or_liability: "Developed Technology".to_string(),
285 book_value: Decimal::ZERO,
286 fair_value: tech_fv,
287 step_up: tech_fv,
288 useful_life_years: Some(tech_life),
289 });
290
291 let inv_book = self.pct_of(total_consideration, 0.10_f64, 0.20_f64);
293 let inv_stepup_pct = self.rng.random_range(0.03_f64..=0.08_f64);
294 let inv_fv = self.apply_step_up(inv_book, inv_stepup_pct);
295 assets.push(AcquisitionFvAdjustment {
296 asset_or_liability: "Inventory".to_string(),
297 book_value: inv_book,
298 fair_value: inv_fv,
299 step_up: inv_fv - inv_book,
300 useful_life_years: None,
301 });
302
303 if self.rng.random_bool(0.70) {
305 let ar_book = self.pct_of(total_consideration, 0.05_f64, 0.15_f64);
306 assets.push(AcquisitionFvAdjustment {
307 asset_or_liability: "Accounts Receivable".to_string(),
308 book_value: ar_book,
309 fair_value: ar_book, step_up: Decimal::ZERO,
311 useful_life_years: None,
312 });
313 }
314
315 let ap_book = self.pct_of(total_consideration, 0.08_f64, 0.18_f64);
318 liabilities.push(AcquisitionFvAdjustment {
319 asset_or_liability: "Accounts Payable".to_string(),
320 book_value: ap_book,
321 fair_value: ap_book,
322 step_up: Decimal::ZERO,
323 useful_life_years: None,
324 });
325
326 if self.rng.random_bool(0.70) {
328 let debt_book = self.pct_of(total_consideration, 0.10_f64, 0.25_f64);
329 let debt_fv_adj = self.rng.random_range(-0.05_f64..=0.05_f64);
331 let debt_fv = self.apply_step_up(debt_book, debt_fv_adj);
332 liabilities.push(AcquisitionFvAdjustment {
333 asset_or_liability: "Long-term Debt".to_string(),
334 book_value: debt_book,
335 fair_value: debt_fv,
336 step_up: debt_fv - debt_book,
337 useful_life_years: None,
338 });
339 }
340
341 if self.rng.random_bool(0.40) {
343 let def_rev = self.pct_of(total_consideration, 0.02_f64, 0.06_f64);
344 liabilities.push(AcquisitionFvAdjustment {
345 asset_or_liability: "Deferred Revenue".to_string(),
346 book_value: def_rev,
347 fair_value: def_rev,
348 step_up: Decimal::ZERO,
349 useful_life_years: None,
350 });
351 }
352
353 let total_asset_fv: Decimal = assets.iter().map(|a| a.fair_value).sum();
355 let total_liability_fv: Decimal = liabilities.iter().map(|l| l.fair_value).sum();
356 let net_identifiable_assets_fv = total_asset_fv - total_liability_fv;
357
358 AcquisitionPpa {
359 identifiable_assets: assets,
360 identifiable_liabilities: liabilities,
361 net_identifiable_assets_fv,
362 }
363 }
364
365 fn generate_day1_journal_entries(
371 &mut self,
372 company_code: &str,
373 currency: &str,
374 bc: &BusinessCombination,
375 ) -> Vec<JournalEntry> {
376 let doc_id = self.uuid_factory.next();
377 let mut header = JournalEntryHeader::with_deterministic_id(
378 company_code.to_string(),
379 bc.acquisition_date,
380 doc_id,
381 );
382 header.document_type = "BC".to_string();
383 header.currency = currency.to_string();
384 header.source = TransactionSource::Manual;
385 header.header_text = Some(format!("Acquisition of {} – Day 1 PPA", bc.acquiree_name));
386 header.reference = Some(bc.id.clone());
387
388 let mut je = JournalEntry::new(header);
389 let mut line_num: u32 = 1;
390
391 for adj in &bc.purchase_price_allocation.identifiable_assets {
393 if adj.fair_value > Decimal::ZERO {
394 let account = asset_gl_account(&adj.asset_or_liability);
395 let mut line = JournalEntryLine::debit(doc_id, line_num, account, adj.fair_value);
396 line.line_text = Some(format!("Acquired asset: {}", adj.asset_or_liability));
397 je.add_line(line);
398 line_num += 1;
399 }
400 }
401
402 if bc.goodwill > Decimal::ZERO {
404 let mut line =
405 JournalEntryLine::debit(doc_id, line_num, GOODWILL.to_string(), bc.goodwill);
406 line.line_text = Some(format!("Goodwill – acquisition of {}", bc.acquiree_name));
407 je.add_line(line);
408 line_num += 1;
409 }
410
411 for adj in &bc.purchase_price_allocation.identifiable_liabilities {
413 if adj.fair_value > Decimal::ZERO {
414 let account = liability_gl_account(&adj.asset_or_liability);
415 let mut line = JournalEntryLine::credit(doc_id, line_num, account, adj.fair_value);
416 line.line_text = Some(format!("Assumed liability: {}", adj.asset_or_liability));
417 je.add_line(line);
418 line_num += 1;
419 }
420 }
421
422 if bc.consideration.cash > Decimal::ZERO {
424 let mut line = JournalEntryLine::credit(
425 doc_id,
426 line_num,
427 OPERATING_CASH.to_string(),
428 bc.consideration.cash,
429 );
430 line.line_text = Some("Cash paid – business combination".to_string());
431 je.add_line(line);
432 line_num += 1;
433 }
434
435 if let Some(shares_val) = bc.consideration.shares_issued_value {
437 if shares_val > Decimal::ZERO {
438 let mut line =
439 JournalEntryLine::credit(doc_id, line_num, "3100".to_string(), shares_val);
440 line.line_text = Some("Shares issued – business combination".to_string());
441 je.add_line(line);
442 line_num += 1;
443 }
444 }
445
446 if let Some(contingent) = bc.consideration.contingent_consideration {
448 if contingent > Decimal::ZERO {
449 let mut line =
450 JournalEntryLine::credit(doc_id, line_num, "2800".to_string(), contingent);
451 line.line_text = Some("Contingent consideration liability".to_string());
452 je.add_line(line);
453 line_num += 1;
454 }
455 }
456
457 let raw_goodwill =
459 bc.consideration.total - bc.purchase_price_allocation.net_identifiable_assets_fv;
460 if raw_goodwill < Decimal::ZERO {
461 let gain = (-raw_goodwill).round_dp(2);
462 let mut line =
463 JournalEntryLine::credit(doc_id, line_num, BARGAIN_PURCHASE_GAIN.to_string(), gain);
464 line.line_text = Some("Bargain purchase gain".to_string());
465 je.add_line(line);
466 }
467
468 vec![je]
469 }
470
471 fn generate_amortization_journal_entries(
475 &mut self,
476 company_code: &str,
477 currency: &str,
478 bc: &BusinessCombination,
479 start_date: NaiveDate,
480 end_date: NaiveDate,
481 ) -> Vec<JournalEntry> {
482 let mut jes = Vec::new();
483
484 let intangibles: Vec<(&AcquisitionFvAdjustment, u32)> = bc
486 .purchase_price_allocation
487 .identifiable_assets
488 .iter()
489 .filter_map(|adj| adj.useful_life_years.map(|life| (adj, life)))
490 .filter(|(adj, _)| adj.fair_value > Decimal::ZERO)
491 .collect();
492
493 if intangibles.is_empty() {
494 return jes;
495 }
496
497 let mut period_dates: Vec<NaiveDate> = Vec::new();
499 let acq_date = bc.acquisition_date;
500 let mut current =
501 NaiveDate::from_ymd_opt(start_date.year(), start_date.month(), 1).unwrap_or(start_date);
502
503 loop {
504 let month_end = last_day_of_month(current.year(), current.month());
506 if month_end > end_date {
507 break;
508 }
509 if month_end > acq_date {
511 period_dates.push(month_end);
512 }
513 let next_month = current.month() % 12 + 1;
515 let next_year = if current.month() == 12 {
516 current.year() + 1
517 } else {
518 current.year()
519 };
520 match NaiveDate::from_ymd_opt(next_year, next_month, 1) {
521 Some(d) => current = d,
522 None => break,
523 }
524 }
525
526 for period_end in period_dates {
527 let doc_id = self.uuid_factory.next();
528 let mut header = JournalEntryHeader::with_deterministic_id(
529 company_code.to_string(),
530 period_end,
531 doc_id,
532 );
533 header.document_type = "AM".to_string();
534 header.currency = currency.to_string();
535 header.source = TransactionSource::Automated;
536 header.header_text = Some(format!(
537 "Amortization – acquired intangibles ({})",
538 bc.acquiree_name
539 ));
540 header.reference = Some(bc.id.clone());
541
542 let mut je = JournalEntry::new(header);
543 let mut line_num: u32 = 1;
544 for (adj, life_years) in &intangibles {
545 let months = Decimal::from(*life_years) * Decimal::from(12u32);
547 let monthly_amort = (adj.fair_value / months).round_dp(2);
548
549 if monthly_amort == Decimal::ZERO {
550 continue;
551 }
552
553 let amort_account = intangible_amort_account(&adj.asset_or_liability);
554
555 let mut dr_line = JournalEntryLine::debit(
557 doc_id,
558 line_num,
559 AMORTIZATION_EXPENSE.to_string(),
560 monthly_amort,
561 );
562 dr_line.line_text = Some(format!("Amortization – {}", adj.asset_or_liability));
563 je.add_line(dr_line);
564 line_num += 1;
565
566 let mut cr_line =
568 JournalEntryLine::credit(doc_id, line_num, amort_account, monthly_amort);
569 cr_line.line_text =
570 Some(format!("Accum. amortization – {}", adj.asset_or_liability));
571 je.add_line(cr_line);
572 line_num += 1;
573 }
574
575 if !je.lines.is_empty() {
577 jes.push(je);
578 }
579 }
580
581 jes
582 }
583
584 fn pct_of(&mut self, base: Decimal, pct_min: f64, pct_max: f64) -> Decimal {
590 let pct = self.rng.random_range(pct_min..=pct_max);
591 let pct_dec = Decimal::from_f64_retain(pct)
592 .unwrap_or(Decimal::from_f64_retain(pct_min).unwrap_or(Decimal::ONE));
593 (base * pct_dec).round_dp(2)
594 }
595
596 fn apply_step_up(&mut self, book_value: Decimal, step_up_pct: f64) -> Decimal {
598 let pct_dec = Decimal::from_f64_retain(step_up_pct).unwrap_or(Decimal::ZERO);
599 (book_value * (Decimal::ONE + pct_dec)).round_dp(2)
600 }
601
602 fn random_date_in_period(&mut self, start: NaiveDate, end: NaiveDate) -> NaiveDate {
605 let total_days = (end - start).num_days();
606 if total_days <= 0 {
607 return start;
608 }
609 let usable_days = (total_days * 3 / 4).max(1);
611 let offset = self.rng.random_range(0i64..usable_days);
612 start + chrono::Duration::days(offset)
613 }
614}
615
616fn asset_gl_account(description: &str) -> String {
622 match description {
623 "Property, Plant & Equipment" => FIXED_ASSETS.to_string(),
624 "Customer Relationships" => CUSTOMER_RELATIONSHIPS.to_string(),
625 "Trade Name" => TRADE_NAME.to_string(),
626 "Developed Technology" => TECHNOLOGY.to_string(),
627 "Inventory" => "1200".to_string(),
628 "Accounts Receivable" => "1100".to_string(),
629 _ => "1890".to_string(), }
631}
632
633fn liability_gl_account(description: &str) -> String {
635 match description {
636 "Accounts Payable" => "2000".to_string(),
637 "Long-term Debt" => "2600".to_string(),
638 "Deferred Revenue" => "2300".to_string(),
639 _ => "2890".to_string(), }
641}
642
643fn intangible_amort_account(description: &str) -> String {
645 let _ = description;
648 ACCUMULATED_AMORTIZATION.to_string()
649}
650
651fn last_day_of_month(year: i32, month: u32) -> NaiveDate {
653 let next_month = month % 12 + 1;
654 let next_year = if month == 12 { year + 1 } else { year };
655 NaiveDate::from_ymd_opt(next_year, next_month, 1)
656 .and_then(|d| d.pred_opt())
657 .unwrap_or_else(|| {
658 NaiveDate::from_ymd_opt(year, month, 28)
660 .unwrap_or(NaiveDate::from_ymd_opt(year, 1, 28).unwrap_or(NaiveDate::MIN))
661 })
662}
663
664#[cfg(test)]
669#[allow(clippy::unwrap_used)]
670mod tests {
671 use super::*;
672
673 fn make_gen() -> BusinessCombinationGenerator {
674 BusinessCombinationGenerator::new(42)
675 }
676
677 fn make_dates() -> (NaiveDate, NaiveDate) {
678 let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
679 let end = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
680 (start, end)
681 }
682
683 #[test]
684 fn test_basic_generation() {
685 let mut gen = make_gen();
686 let (start, end) = make_dates();
687 let snap = gen.generate("C001", "USD", start, end, 2, "IFRS");
688
689 assert_eq!(snap.combinations.len(), 2);
690 assert!(!snap.journal_entries.is_empty());
691 }
692
693 #[test]
694 fn test_goodwill_equals_consideration_minus_net_assets() {
695 let mut gen = make_gen();
696 let (start, end) = make_dates();
697 let snap = gen.generate("C001", "USD", start, end, 3, "US_GAAP");
698
699 for bc in &snap.combinations {
700 let raw_goodwill =
701 bc.consideration.total - bc.purchase_price_allocation.net_identifiable_assets_fv;
702 if raw_goodwill >= Decimal::ZERO {
703 assert_eq!(bc.goodwill, raw_goodwill, "Goodwill mismatch for {}", bc.id);
704 } else {
705 assert_eq!(
707 bc.goodwill,
708 Decimal::ZERO,
709 "Bargain purchase goodwill should be zero for {}",
710 bc.id
711 );
712 }
713 }
714 }
715
716 #[test]
717 fn test_at_least_4_identifiable_assets() {
718 let mut gen = make_gen();
719 let (start, end) = make_dates();
720 let snap = gen.generate("C001", "USD", start, end, 3, "IFRS");
721
722 for bc in &snap.combinations {
723 assert!(
724 bc.purchase_price_allocation.identifiable_assets.len() >= 4,
725 "PPA should have at least 4 assets, got {} for {}",
726 bc.purchase_price_allocation.identifiable_assets.len(),
727 bc.id
728 );
729 }
730 }
731
732 #[test]
733 fn test_day1_jes_balanced() {
734 let mut gen = make_gen();
735 let (start, end) = make_dates();
736 let snap = gen.generate("C001", "USD", start, end, 2, "IFRS");
737
738 let day1_jes: Vec<_> = snap
740 .journal_entries
741 .iter()
742 .filter(|je| je.header.document_type == "BC")
743 .collect();
744
745 assert!(!day1_jes.is_empty(), "Should have Day 1 JEs");
746
747 for je in &day1_jes {
748 let total_debits: Decimal = je.lines.iter().map(|l| l.debit_amount).sum();
749 let total_credits: Decimal = je.lines.iter().map(|l| l.credit_amount).sum();
750 assert_eq!(
751 total_debits, total_credits,
752 "Day 1 JE {} is unbalanced: debits={}, credits={}",
753 je.header.document_id, total_debits, total_credits
754 );
755 }
756 }
757
758 #[test]
759 fn test_amortization_jes_balanced() {
760 let mut gen = make_gen();
761 let (start, end) = make_dates();
762 let snap = gen.generate("C001", "USD", start, end, 2, "IFRS");
763
764 let amort_jes: Vec<_> = snap
765 .journal_entries
766 .iter()
767 .filter(|je| je.header.document_type == "AM")
768 .collect();
769
770 assert!(!amort_jes.is_empty(), "Should have amortization JEs");
771
772 for je in &amort_jes {
773 let total_debits: Decimal = je.lines.iter().map(|l| l.debit_amount).sum();
774 let total_credits: Decimal = je.lines.iter().map(|l| l.credit_amount).sum();
775 assert_eq!(
776 total_debits, total_credits,
777 "Amortization JE {} is unbalanced: debits={}, credits={}",
778 je.header.document_id, total_debits, total_credits
779 );
780 }
781 }
782
783 #[test]
784 fn test_ppa_fair_values_positive_for_assets() {
785 let mut gen = make_gen();
786 let (start, end) = make_dates();
787 let snap = gen.generate("C001", "USD", start, end, 2, "US_GAAP");
788
789 for bc in &snap.combinations {
790 for adj in &bc.purchase_price_allocation.identifiable_assets {
791 assert!(
792 adj.fair_value > Decimal::ZERO,
793 "Asset {} should have positive fair value for {}",
794 adj.asset_or_liability,
795 bc.id
796 );
797 }
798 }
799 }
800
801 #[test]
802 fn test_consideration_total_correct() {
803 let mut gen = make_gen();
804 let (start, end) = make_dates();
805 let snap = gen.generate("C001", "USD", start, end, 3, "IFRS");
806
807 for bc in &snap.combinations {
808 let c = &bc.consideration;
809 let computed_total = c.cash
810 + c.shares_issued_value.unwrap_or(Decimal::ZERO)
811 + c.contingent_consideration.unwrap_or(Decimal::ZERO);
812 assert_eq!(
813 computed_total, c.total,
814 "Consideration components don't add up for {}",
815 bc.id
816 );
817 }
818 }
819
820 #[test]
821 fn test_deterministic_output() {
822 let (start, end) = make_dates();
823 let mut gen1 = BusinessCombinationGenerator::new(99);
824 let mut gen2 = BusinessCombinationGenerator::new(99);
825
826 let snap1 = gen1.generate("C001", "USD", start, end, 2, "IFRS");
827 let snap2 = gen2.generate("C001", "USD", start, end, 2, "IFRS");
828
829 assert_eq!(snap1.combinations.len(), snap2.combinations.len());
830 for (a, b) in snap1.combinations.iter().zip(snap2.combinations.iter()) {
831 assert_eq!(a.id, b.id);
832 assert_eq!(a.goodwill, b.goodwill);
833 assert_eq!(a.consideration.total, b.consideration.total);
834 }
835 assert_eq!(snap1.journal_entries.len(), snap2.journal_entries.len());
836 }
837
838 #[test]
839 fn test_zero_count_returns_empty() {
840 let mut gen = make_gen();
841 let (start, end) = make_dates();
842 let snap = gen.generate("C001", "USD", start, end, 0, "IFRS");
843 assert!(snap.combinations.is_empty());
844 assert!(snap.journal_entries.is_empty());
845 }
846}