use chrono::NaiveDate;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::framework::AccountingFramework;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomerContract {
pub contract_id: Uuid,
pub customer_id: String,
pub customer_name: String,
pub company_code: String,
pub inception_date: NaiveDate,
pub end_date: Option<NaiveDate>,
#[serde(with = "rust_decimal::serde::str")]
pub transaction_price: Decimal,
pub currency: String,
pub status: ContractStatus,
pub performance_obligations: Vec<PerformanceObligation>,
pub variable_consideration: Vec<VariableConsideration>,
pub has_significant_financing: bool,
#[serde(default, with = "rust_decimal::serde::str_option")]
pub financing_rate: Option<Decimal>,
pub framework: AccountingFramework,
pub modifications: Vec<ContractModification>,
pub sales_order_id: Option<Uuid>,
#[serde(default)]
pub journal_entry_ids: Vec<Uuid>,
}
impl CustomerContract {
pub fn new(
customer_id: impl Into<String>,
customer_name: impl Into<String>,
company_code: impl Into<String>,
inception_date: NaiveDate,
transaction_price: Decimal,
currency: impl Into<String>,
framework: AccountingFramework,
) -> Self {
Self {
contract_id: Uuid::now_v7(),
customer_id: customer_id.into(),
customer_name: customer_name.into(),
company_code: company_code.into(),
inception_date,
end_date: None,
transaction_price,
currency: currency.into(),
status: ContractStatus::Active,
performance_obligations: Vec::new(),
variable_consideration: Vec::new(),
has_significant_financing: false,
financing_rate: None,
framework,
modifications: Vec::new(),
sales_order_id: None,
journal_entry_ids: Vec::new(),
}
}
pub fn add_performance_obligation(&mut self, obligation: PerformanceObligation) {
self.performance_obligations.push(obligation);
}
pub fn add_variable_consideration(&mut self, vc: VariableConsideration) {
self.variable_consideration.push(vc);
}
pub fn total_allocated_price(&self) -> Decimal {
self.performance_obligations
.iter()
.map(|po| po.allocated_price)
.sum()
}
pub fn total_revenue_recognized(&self) -> Decimal {
self.performance_obligations
.iter()
.map(|po| po.revenue_recognized)
.sum()
}
pub fn total_deferred_revenue(&self) -> Decimal {
self.performance_obligations
.iter()
.map(|po| po.deferred_revenue)
.sum()
}
pub fn is_fully_satisfied(&self) -> bool {
self.performance_obligations
.iter()
.all(PerformanceObligation::is_satisfied)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ContractStatus {
Pending,
#[default]
Active,
Modified,
Complete,
Terminated,
Disputed,
}
impl std::fmt::Display for ContractStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Pending => write!(f, "Pending"),
Self::Active => write!(f, "Active"),
Self::Modified => write!(f, "Modified"),
Self::Complete => write!(f, "Complete"),
Self::Terminated => write!(f, "Terminated"),
Self::Disputed => write!(f, "Disputed"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceObligation {
pub obligation_id: Uuid,
pub contract_id: Uuid,
pub sequence: u32,
pub description: String,
pub obligation_type: ObligationType,
pub satisfaction_pattern: SatisfactionPattern,
pub progress_method: Option<ProgressMethod>,
#[serde(with = "rust_decimal::serde::str")]
pub standalone_selling_price: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub allocated_price: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub progress_percent: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub revenue_recognized: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub deferred_revenue: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub contract_asset: Decimal,
pub satisfaction_date: Option<NaiveDate>,
pub expected_satisfaction_date: Option<NaiveDate>,
pub material_right: Option<MaterialRight>,
}
impl PerformanceObligation {
pub fn new(
contract_id: Uuid,
sequence: u32,
description: impl Into<String>,
obligation_type: ObligationType,
satisfaction_pattern: SatisfactionPattern,
standalone_selling_price: Decimal,
) -> Self {
Self {
obligation_id: Uuid::now_v7(),
contract_id,
sequence,
description: description.into(),
obligation_type,
satisfaction_pattern,
progress_method: match satisfaction_pattern {
SatisfactionPattern::OverTime => Some(ProgressMethod::default()),
SatisfactionPattern::PointInTime => None,
},
standalone_selling_price,
allocated_price: Decimal::ZERO,
progress_percent: Decimal::ZERO,
revenue_recognized: Decimal::ZERO,
deferred_revenue: Decimal::ZERO,
contract_asset: Decimal::ZERO,
satisfaction_date: None,
expected_satisfaction_date: None,
material_right: None,
}
}
pub fn is_satisfied(&self) -> bool {
self.satisfaction_date.is_some() || self.progress_percent >= Decimal::from(100)
}
pub fn update_progress(&mut self, new_progress: Decimal, as_of_date: NaiveDate) {
let old_revenue = self.revenue_recognized;
self.progress_percent = new_progress.min(Decimal::from(100));
let target_revenue = self.allocated_price * self.progress_percent / Decimal::from(100);
self.revenue_recognized = target_revenue;
self.deferred_revenue = self.allocated_price - self.revenue_recognized;
if self.progress_percent >= Decimal::from(100) && self.satisfaction_date.is_none() {
self.satisfaction_date = Some(as_of_date);
}
let _ = old_revenue; }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ObligationType {
#[default]
Good,
Service,
License,
Series,
ServiceTypeWarranty,
MaterialRight,
}
impl std::fmt::Display for ObligationType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Good => write!(f, "Good"),
Self::Service => write!(f, "Service"),
Self::License => write!(f, "License"),
Self::Series => write!(f, "Series"),
Self::ServiceTypeWarranty => write!(f, "Service-Type Warranty"),
Self::MaterialRight => write!(f, "Material Right"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum SatisfactionPattern {
#[default]
PointInTime,
OverTime,
}
impl std::fmt::Display for SatisfactionPattern {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::PointInTime => write!(f, "Point in Time"),
Self::OverTime => write!(f, "Over Time"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ProgressMethod {
#[default]
Output,
Input,
StraightLine,
}
impl std::fmt::Display for ProgressMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Output => write!(f, "Output Method"),
Self::Input => write!(f, "Input Method"),
Self::StraightLine => write!(f, "Straight-Line"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VariableConsideration {
pub vc_id: Uuid,
pub contract_id: Uuid,
pub vc_type: VariableConsiderationType,
#[serde(with = "rust_decimal::serde::str")]
pub estimated_amount: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub constrained_amount: Decimal,
pub estimation_method: EstimationMethod,
#[serde(with = "rust_decimal::serde::str")]
pub probability: Decimal,
pub description: String,
pub resolution_date: Option<NaiveDate>,
#[serde(default, with = "rust_decimal::serde::str_option")]
pub actual_amount: Option<Decimal>,
}
impl VariableConsideration {
pub fn new(
contract_id: Uuid,
vc_type: VariableConsiderationType,
estimated_amount: Decimal,
description: impl Into<String>,
) -> Self {
Self {
vc_id: Uuid::now_v7(),
contract_id,
vc_type,
estimated_amount,
constrained_amount: estimated_amount,
estimation_method: EstimationMethod::ExpectedValue,
probability: Decimal::from(80),
description: description.into(),
resolution_date: None,
actual_amount: None,
}
}
pub fn apply_constraint(&mut self, constraint_threshold: Decimal) {
self.constrained_amount = self.estimated_amount * constraint_threshold;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VariableConsiderationType {
Discount,
Rebate,
RightOfReturn,
IncentiveBonus,
Penalty,
PriceConcession,
Royalty,
ContingentPayment,
}
impl std::fmt::Display for VariableConsiderationType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Discount => write!(f, "Discount"),
Self::Rebate => write!(f, "Rebate"),
Self::RightOfReturn => write!(f, "Right of Return"),
Self::IncentiveBonus => write!(f, "Incentive Bonus"),
Self::Penalty => write!(f, "Penalty"),
Self::PriceConcession => write!(f, "Price Concession"),
Self::Royalty => write!(f, "Royalty"),
Self::ContingentPayment => write!(f, "Contingent Payment"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum EstimationMethod {
#[default]
ExpectedValue,
MostLikelyAmount,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MaterialRight {
pub right_type: MaterialRightType,
#[serde(with = "rust_decimal::serde::str")]
pub standalone_selling_price: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub exercise_probability: Decimal,
pub expiration_date: Option<NaiveDate>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MaterialRightType {
RenewalOption,
LoyaltyPoints,
FutureDiscount,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContractModification {
pub modification_id: Uuid,
pub modification_date: NaiveDate,
pub treatment: ModificationTreatment,
#[serde(with = "rust_decimal::serde::str")]
pub price_change: Decimal,
pub description: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ModificationTreatment {
SeparateContract,
TerminateAndCreate,
CumulativeCatchUp,
Prospective,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RevenueRecognitionEntry {
pub contract_id: Uuid,
pub obligation_id: Uuid,
pub period_date: NaiveDate,
#[serde(with = "rust_decimal::serde::str")]
pub revenue_amount: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub cumulative_revenue: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub deferred_revenue_balance: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub contract_asset_balance: Decimal,
#[serde(with = "rust_decimal::serde::str")]
pub progress_percent: Decimal,
pub journal_entry_id: Option<Uuid>,
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_contract_creation() {
let contract = CustomerContract::new(
"CUST001",
"Acme Corp",
"1000",
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
dec!(100000),
"USD",
AccountingFramework::UsGaap,
);
assert_eq!(contract.customer_id, "CUST001");
assert_eq!(contract.transaction_price, dec!(100000));
assert_eq!(contract.status, ContractStatus::Active);
assert!(contract.performance_obligations.is_empty());
}
#[test]
fn test_performance_obligation() {
let contract_id = Uuid::now_v7();
let mut po = PerformanceObligation::new(
contract_id,
1,
"Software License",
ObligationType::License,
SatisfactionPattern::PointInTime,
dec!(50000),
);
po.allocated_price = dec!(50000);
po.update_progress(dec!(100), NaiveDate::from_ymd_opt(2024, 3, 31).unwrap());
assert!(po.is_satisfied());
assert_eq!(po.revenue_recognized, dec!(50000));
assert_eq!(po.deferred_revenue, dec!(0));
}
#[test]
fn test_over_time_recognition() {
let contract_id = Uuid::now_v7();
let mut po = PerformanceObligation::new(
contract_id,
1,
"Consulting Services",
ObligationType::Service,
SatisfactionPattern::OverTime,
dec!(120000),
);
po.allocated_price = dec!(120000);
po.update_progress(dec!(25), NaiveDate::from_ymd_opt(2024, 1, 31).unwrap());
assert_eq!(po.revenue_recognized, dec!(30000));
assert_eq!(po.deferred_revenue, dec!(90000));
po.update_progress(dec!(50), NaiveDate::from_ymd_opt(2024, 2, 29).unwrap());
assert_eq!(po.revenue_recognized, dec!(60000));
assert_eq!(po.deferred_revenue, dec!(60000));
}
#[test]
fn test_variable_consideration_constraint() {
let contract_id = Uuid::now_v7();
let mut vc = VariableConsideration::new(
contract_id,
VariableConsiderationType::Rebate,
dec!(10000),
"Volume rebate",
);
vc.apply_constraint(dec!(0.80));
assert_eq!(vc.constrained_amount, dec!(8000));
}
}