OpenPay (1 crate)
op-revrec ASC 606 / IFRS 15 revenue recognition (5-step model,
multi-element allocation, deferred revenue ledger,
variable consideration with constraint, principal vs agent,
contract modifications, multi-currency translation)
27 tests across 6 FASB worked examples + SaaS lifecycle.
EOC (1 crate)
eoc-carbon Carbon intensity → joules → gCO2e accounting.
Electricity Maps, WattTime, CO2 Signal adapters; IEA
baseline fallback; PUE tables (AWS/GCP/Azure); region-aware
scheduling + demand shifting; SCI-for-AI bridge into eoc-eval.
21 tests passing.
Seven other crates (op-dispute-evidence, op-ewallets-asia, smart-byte-zk,
smart-byte-atproto, smart-byte-edv, eoc-agent, eoc-rag) were scoped in this
batch but the parallel agents hit session-limit before completing. They are
deferred to a follow-up batch.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
178 lines
6.4 KiB
Rust
178 lines
6.4 KiB
Rust
//! Step 3 helper: variable consideration with the constraint of
|
|
//! ASC 606-10-32-11 ("highly probable that a significant reversal will
|
|
//! not occur").
|
|
//!
|
|
//! Variable consideration covers discounts, rebates, refund liabilities,
|
|
//! performance bonuses, contingent royalties, and any other amount that
|
|
//! is not fixed at contract inception. The standard requires the entity
|
|
//! to estimate the amount AND constrain it to the portion for which a
|
|
//! material reversal is unlikely.
|
|
//!
|
|
//! We model both estimation methods the standard recognises:
|
|
//!
|
|
//! - **Expected value** (ASC 606-10-32-8(a)) — probability-weighted sum
|
|
//! over the distribution of possible outcomes. Best when the entity
|
|
//! has many similar contracts.
|
|
//! - **Most likely amount** (ASC 606-10-32-8(b)) — the single most likely
|
|
//! outcome. Best for binary outcomes (e.g. milestone bonus paid or not).
|
|
//!
|
|
//! The constraint is applied as an explicit ceiling — the auditor and
|
|
//! the entity agree on the amount that is "highly probable" of not
|
|
//! reversing, and that figure caps the recognized variable amount.
|
|
|
|
use rust_decimal::Decimal;
|
|
use rust_decimal::prelude::ToPrimitive;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// One outcome and its probability for the expected-value method.
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct Outcome {
|
|
/// The outcome amount in minor units (may be negative for a
|
|
/// refund-liability or discount).
|
|
pub amount_minor: i64,
|
|
/// Probability of this outcome, 0..=1.
|
|
pub probability: Decimal,
|
|
}
|
|
|
|
/// How the variable amount is estimated.
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum EstimationMethod {
|
|
/// Probability-weighted sum over the outcome distribution.
|
|
ExpectedValue {
|
|
/// Discrete distribution; probabilities must sum to ~1.0.
|
|
outcomes: Vec<Outcome>,
|
|
},
|
|
/// The single most likely outcome.
|
|
MostLikelyAmount {
|
|
/// The amount in minor units of the contract currency.
|
|
amount_minor: i64,
|
|
},
|
|
}
|
|
|
|
/// One variable-consideration component of a contract's transaction
|
|
/// price.
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct VariableConsideration {
|
|
/// Human-readable label (`"performance-bonus"`, `"volume-rebate"`).
|
|
pub label: String,
|
|
/// How the entity estimated the amount.
|
|
pub method: EstimationMethod,
|
|
/// The constraint ceiling in minor units — the largest amount the
|
|
/// entity may recognize without breaching the "highly probable / no
|
|
/// material reversal" test of ASC 606-10-32-11. Set to the
|
|
/// unconstrained estimate to remove the constraint.
|
|
pub constraint_ceiling_minor: i64,
|
|
}
|
|
|
|
impl VariableConsideration {
|
|
/// Build an expected-value component with an explicit constraint.
|
|
#[must_use]
|
|
pub fn expected_value(
|
|
label: impl Into<String>,
|
|
outcomes: Vec<Outcome>,
|
|
constraint_ceiling_minor: i64,
|
|
) -> Self {
|
|
Self {
|
|
label: label.into(),
|
|
method: EstimationMethod::ExpectedValue { outcomes },
|
|
constraint_ceiling_minor,
|
|
}
|
|
}
|
|
|
|
/// Build a most-likely-amount component with an explicit constraint.
|
|
#[must_use]
|
|
pub fn most_likely(
|
|
label: impl Into<String>,
|
|
amount_minor: i64,
|
|
constraint_ceiling_minor: i64,
|
|
) -> Self {
|
|
Self {
|
|
label: label.into(),
|
|
method: EstimationMethod::MostLikelyAmount { amount_minor },
|
|
constraint_ceiling_minor,
|
|
}
|
|
}
|
|
|
|
/// The unconstrained estimate, in minor units. Negative for
|
|
/// outcomes that reduce revenue (discounts, refunds).
|
|
#[must_use]
|
|
pub fn estimate_minor(&self) -> i64 {
|
|
match &self.method {
|
|
EstimationMethod::MostLikelyAmount { amount_minor } => *amount_minor,
|
|
EstimationMethod::ExpectedValue { outcomes } => {
|
|
// Probability-weighted sum. We compute in Decimal then
|
|
// round to integer minor units (round-half-up).
|
|
let mut acc = Decimal::ZERO;
|
|
for o in outcomes {
|
|
let m = Decimal::from(o.amount_minor);
|
|
acc += m * o.probability;
|
|
}
|
|
// Round half-up; saturate on i64 overflow.
|
|
acc.round_dp(0).to_i64().unwrap_or(i64::MAX)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Apply the constraint: the smaller of `|estimate|` and the
|
|
/// ceiling, preserving sign. Returns the amount safe to recognize.
|
|
#[must_use]
|
|
pub fn constrained_amount_minor(&self) -> i64 {
|
|
let est = self.estimate_minor();
|
|
let sign = est.signum();
|
|
// Take absolute value safely (i64::MIN.abs() would overflow;
|
|
// saturate to i64::MAX to keep the constraint comparison sound).
|
|
let abs = est.checked_abs().unwrap_or(i64::MAX);
|
|
let capped = abs.min(self.constraint_ceiling_minor.unsigned_abs() as i64);
|
|
sign.checked_mul(capped).unwrap_or(0)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use rust_decimal_macros_polyfill::dec;
|
|
|
|
// Local dec! polyfill since rust_decimal_macros isn't a workspace dep.
|
|
mod rust_decimal_macros_polyfill {
|
|
macro_rules! dec {
|
|
($lit:literal) => {{
|
|
let s = stringify!($lit);
|
|
<rust_decimal::Decimal as core::str::FromStr>::from_str(s)
|
|
.unwrap_or(rust_decimal::Decimal::ZERO)
|
|
}};
|
|
}
|
|
pub(crate) use dec;
|
|
}
|
|
|
|
#[test]
|
|
fn most_likely_constrained_below_ceiling_returns_estimate() {
|
|
let v = VariableConsideration::most_likely("bonus", 1_000, 5_000);
|
|
assert_eq!(v.constrained_amount_minor(), 1_000);
|
|
}
|
|
|
|
#[test]
|
|
fn most_likely_constrained_above_ceiling_returns_ceiling() {
|
|
let v = VariableConsideration::most_likely("bonus", 10_000, 5_000);
|
|
assert_eq!(v.constrained_amount_minor(), 5_000);
|
|
}
|
|
|
|
#[test]
|
|
fn expected_value_weighted_sum() {
|
|
let v = VariableConsideration::expected_value(
|
|
"rebate",
|
|
vec![
|
|
Outcome { amount_minor: 0, probability: dec!(0.5) },
|
|
Outcome { amount_minor: 1_000, probability: dec!(0.5) },
|
|
],
|
|
1_000,
|
|
);
|
|
assert_eq!(v.estimate_minor(), 500);
|
|
assert_eq!(v.constrained_amount_minor(), 500);
|
|
}
|
|
|
|
#[test]
|
|
fn negative_outcome_preserves_sign() {
|
|
let v = VariableConsideration::most_likely("discount", -2_000, 1_000);
|
|
assert_eq!(v.constrained_amount_minor(), -1_000);
|
|
}
|
|
}
|