use chrono::{Duration, NaiveDate};
use datasynth_core::models::IcPairId;
use rust_decimal::prelude::ToPrimitive;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use crate::config::IcTransactionType;
use crate::manifest::builder::GroupManifest;
use crate::manifest::seeds::derive_ic_pair_id;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum IcRole {
Seller,
Buyer,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IcPairPlan {
pub pair_id: IcPairId,
pub ic_relationship_id: String,
pub role: IcRole,
pub partner_entity: String,
pub transaction_type: IcTransactionType,
pub amount: Decimal,
pub date: NaiveDate,
pub index: u64,
}
pub fn derive_ic_pair_plans(manifest: &GroupManifest, entity_code: &str) -> Vec<IcPairPlan> {
let mut out: Vec<IcPairPlan> = Vec::new();
for rel in &manifest.ic_relationships {
let is_seller = rel.seller == entity_code;
let is_buyer = rel.buyer == entity_code;
if !is_seller && !is_buyer {
continue;
}
let Some(tx_type) = rel.types.first().copied() else {
continue;
};
let avg = avg_amount(tx_type);
if avg.is_zero() {
continue;
}
let n = match compute_pair_count(rel.annual_volume, avg) {
Some(n) => n.max(1),
None => continue,
};
let role = if is_seller {
IcRole::Seller
} else {
IcRole::Buyer
};
let partner_entity = if is_seller {
rel.buyer.clone()
} else {
rel.seller.clone()
};
for i in 0..n {
let pair_id_bytes = derive_ic_pair_id(manifest.group_seed, &rel.id, i);
out.push(IcPairPlan {
pair_id: IcPairId::from_bytes(pair_id_bytes),
ic_relationship_id: rel.id.clone(),
role,
partner_entity: partner_entity.clone(),
transaction_type: tx_type,
amount: avg,
date: spread_date(manifest.period.start, manifest.period.end, i, n),
index: i,
});
}
}
out
}
pub fn avg_amount(t: IcTransactionType) -> Decimal {
match t {
IcTransactionType::GoodsSale => Decimal::from(50_000),
IcTransactionType::ServiceProvided => Decimal::from(30_000),
IcTransactionType::ManagementFee => Decimal::from(25_000),
IcTransactionType::Royalty => Decimal::from(100_000),
IcTransactionType::CostSharing => Decimal::from(30_000),
IcTransactionType::LoanInterest => Decimal::from(10_000),
IcTransactionType::Dividend => Decimal::from(75_000),
IcTransactionType::ExpenseRecharge => Decimal::from(20_000),
}
}
fn compute_pair_count(annual_volume: Decimal, avg: Decimal) -> Option<u64> {
if avg.is_zero() {
return None;
}
let raw = annual_volume / avg;
let rounded = raw.round();
rounded.to_u64()
}
fn spread_date(start: NaiveDate, end: NaiveDate, i: u64, n: u64) -> NaiveDate {
let period_days = (end - start).num_days().max(0) as u64;
if n <= 1 || period_days == 0 {
return start;
}
let offset = period_days.saturating_mul(i) / (n - 1);
start + Duration::days(offset as i64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_avg_amount_per_transaction_type() {
assert_eq!(
avg_amount(IcTransactionType::GoodsSale),
Decimal::from(50_000)
);
assert_eq!(
avg_amount(IcTransactionType::ServiceProvided),
Decimal::from(30_000)
);
assert_eq!(
avg_amount(IcTransactionType::ManagementFee),
Decimal::from(25_000)
);
assert_eq!(
avg_amount(IcTransactionType::Royalty),
Decimal::from(100_000)
);
assert_eq!(
avg_amount(IcTransactionType::CostSharing),
Decimal::from(30_000)
);
assert_eq!(
avg_amount(IcTransactionType::LoanInterest),
Decimal::from(10_000)
);
assert_eq!(
avg_amount(IcTransactionType::Dividend),
Decimal::from(75_000)
);
assert_eq!(
avg_amount(IcTransactionType::ExpenseRecharge),
Decimal::from(20_000)
);
}
#[test]
fn test_spread_date_first_and_last_land_on_bounds() {
let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
let end = NaiveDate::from_ymd_opt(2024, 3, 31).unwrap();
let n = 5u64;
assert_eq!(spread_date(start, end, 0, n), start);
assert_eq!(spread_date(start, end, n - 1, n), end);
}
#[test]
fn test_spread_date_n_one_returns_start() {
let start = NaiveDate::from_ymd_opt(2024, 5, 15).unwrap();
let end = NaiveDate::from_ymd_opt(2024, 5, 31).unwrap();
assert_eq!(spread_date(start, end, 0, 1), start);
}
#[test]
fn test_spread_date_zero_length_period_returns_start() {
let start = NaiveDate::from_ymd_opt(2024, 5, 15).unwrap();
assert_eq!(spread_date(start, start, 0, 10), start);
assert_eq!(spread_date(start, start, 5, 10), start);
assert_eq!(spread_date(start, start, 9, 10), start);
}
#[test]
fn test_spread_date_monotonic() {
let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
let end = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
let n = 20u64;
let mut prev = spread_date(start, end, 0, n);
for i in 1..n {
let here = spread_date(start, end, i, n);
assert!(
here >= prev,
"dates regressed at i={}: {} < {}",
i,
here,
prev
);
prev = here;
}
}
#[test]
fn test_compute_pair_count_rounds() {
assert_eq!(
compute_pair_count(Decimal::from(100_000), Decimal::from(30_000)),
Some(3)
);
assert_eq!(
compute_pair_count(Decimal::from(200_000), Decimal::from(30_000)),
Some(7)
);
assert_eq!(
compute_pair_count(Decimal::from(150_000), Decimal::from(50_000)),
Some(3)
);
assert_eq!(
compute_pair_count(Decimal::from(10_000), Decimal::from(50_000)),
Some(0)
);
}
#[test]
fn test_compute_pair_count_rejects_zero_avg() {
assert_eq!(
compute_pair_count(Decimal::from(100_000), Decimal::ZERO),
None
);
}
}