use super::funejson::Value; use super::dates_days_between::days_between; use super::finance_proration::prorate; use super::money_amount::{money, money_from_value, money_to_value, Money}; /// A buyer's completion statement. Each outgoing is split at the end of the /// completion day (the seller owns the property until then) by /// finance.proration, so the two shares always add back to the charge. /// /// # Panics /// Panics on mixed currencies, a negative price, charge or fee, a deposit /// outside 0..=price, a malformed date, or a period that ends before it starts. pub fn completion_statement( price: &Money, deposit_paid: &Money, completion_date: &str, apportionments: &[PeriodCharge], fees: &[FeeLine], ) -> CompletionStatement { let currency = price.currency.as_str(); let same = |m: &Money, what: &str| { if m.currency != currency { panic!("currency mismatch: {} is {}, the price is {}", what, m.currency, currency); } }; same(deposit_paid, "depositPaid"); if price.minor < 0 { panic!("price must not be negative, received {}", price.minor); } if deposit_paid.minor < 0 || deposit_paid.minor > price.minor { panic!("depositPaid must be between 0 and the price, received {}", deposit_paid.minor); } let mut balance = price.minor - deposit_paid.minor; let mut lines: Vec = Vec::new(); for charge in apportionments { same(&charge.amount, &format!("\"{}\"", charge.label)); if charge.amount.minor < 0 { panic!("\"{}\" must not be negative, received {}", charge.label, charge.amount.minor); } let total_days = days_between(&charge.period_start, &charge.period_end) + 1; if total_days < 1 { panic!( "\"{}\": periodEnd {} is before periodStart {}", charge.label, charge.period_end, charge.period_start ); } // The seller owns the property to the end of the completion day. let up_to_completion = days_between(&charge.period_start, completion_date) + 1; let seller_days = total_days.min(up_to_completion.max(0)); let split = prorate(&charge.amount, total_days, seller_days); let adjustment = if charge.paid_by_seller { split.unused.minor } else { -split.used.minor }; balance += adjustment; lines.push(ApportionmentLine { label: charge.label.clone(), seller_days, buyer_days: total_days - seller_days, adjustment: money(adjustment, currency), }); } let mut fees_total = 0i64; let mut fee_lines: Vec = Vec::new(); for fee in fees { same(&fee.amount, &format!("\"{}\"", fee.label)); if fee.amount.minor < 0 { panic!("\"{}\" must not be negative, received {}", fee.label, fee.amount.minor); } fees_total += fee.amount.minor; fee_lines.push(FeeLine { label: fee.label.clone(), amount: money(fee.amount.minor, currency) }); } CompletionStatement { price: money(price.minor, currency), deposit_paid: money(deposit_paid.minor, currency), apportionments: lines, balance_to_seller: money(balance, currency), fees: fee_lines, fees_total: money(fees_total, currency), total_due: money(balance + fees_total, currency), } } pub fn period_charge_from_value(v: &Value) -> PeriodCharge { PeriodCharge { label: v.get("label").as_str().to_string(), amount: money_from_value(v.get("amount")), period_start: v.get("periodStart").as_str().to_string(), period_end: v.get("periodEnd").as_str().to_string(), paid_by_seller: v.get("paidBySeller").as_bool(), } } pub fn fee_line_from_value(v: &Value) -> FeeLine { FeeLine { label: v.get("label").as_str().to_string(), amount: money_from_value(v.get("amount")) } } fn fee_line_to_value(fee: &FeeLine) -> Value { Value::obj(vec![("label", Value::str(&fee.label)), ("amount", money_to_value(&fee.amount))]) } pub fn completion_statement_to_value(s: &CompletionStatement) -> Value { Value::obj(vec![ ("price", money_to_value(&s.price)), ("depositPaid", money_to_value(&s.deposit_paid)), ( "apportionments", Value::Arr( s.apportionments .iter() .map(|l| { Value::obj(vec![ ("label", Value::str(&l.label)), ("sellerDays", Value::Int(l.seller_days)), ("buyerDays", Value::Int(l.buyer_days)), ("adjustment", money_to_value(&l.adjustment)), ]) }) .collect(), ), ), ("balanceToSeller", money_to_value(&s.balance_to_seller)), ("fees", Value::Arr(s.fees.iter().map(fee_line_to_value).collect())), ("feesTotal", money_to_value(&s.fees_total)), ("totalDue", money_to_value(&s.total_due)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let charges: Vec = args[3].as_arr().iter().map(period_charge_from_value).collect(); let fees: Vec = args[4].as_arr().iter().map(fee_line_from_value).collect(); completion_statement_to_value(&completion_statement( &money_from_value(&args[0]), &money_from_value(&args[1]), args[2].as_str(), &charges, &fees, )) }