open-standards/openpay/crates/op-revrec/tests/saas_setup_fee.rs
Transaction Science 781c331288 ingest batch 5 (salvage): 2 crates from 9-agent dispatch (session-limit hit mid-batch)
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]>
2026-05-24 09:28:38 -04:00

101 lines
3.6 KiB
Rust

//! Multi-element arrangement: SaaS subscription ($120/year, 12-month
//! straight-line) + one-time setup fee ($300, recognized at go-live).
//!
//! The combined contract is priced at $400 (a $20 discount off the
//! $420 list). Under ASC 606-10-32-31 the discount allocates by
//! relative SSP, NOT by line.
use chrono::NaiveDate;
use op_core::{Currency, Money};
use op_revrec::{
Contract, ContractId, InMemoryLedger, DeferredRevenueLedger, ObligationId,
PerformanceObligation, Presentation, RecognitionPattern, TransactionPrice,
allocate_transaction_price, generate,
};
fn ymd(y: i32, m: u32, d: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(y, m, d).expect("valid date")
}
fn usd(n: i64) -> Money {
Money::from_minor(n, Currency::USD)
}
#[tokio::test]
async fn saas_plus_setup_fee_full_lifecycle() {
let saas = PerformanceObligation {
id: ObligationId::new("saas-2026"),
standalone_selling_price: usd(120_00),
pattern: RecognitionPattern::StraightLine {
start: ymd(2026, 1, 1),
end: ymd(2026, 12, 31),
},
presentation: Presentation::Gross,
};
let setup = PerformanceObligation {
id: ObligationId::new("setup-fee"),
standalone_selling_price: usd(300_00),
pattern: RecognitionPattern::PointInTime { date: ymd(2026, 1, 15) },
presentation: Presentation::Gross,
};
let contract = Contract {
id: ContractId::new(),
customer_ref: "customer-1".into(),
effective_date: ymd(2026, 1, 1),
obligations: vec![saas.clone(), setup.clone()],
transaction_price: TransactionPrice::fixed(usd(400_00)),
};
// Step 4: allocate. SSPs: saas 12000, setup 30000 -> total 42000.
// saas alloc = 40000 * 12000 / 42000 = 11428.57 -> 11428
// setup alloc = plug = 40000 - 11428 = 28572
let alloc = allocate_transaction_price(&contract).expect("alloc");
let map: std::collections::HashMap<_, _> = alloc.into_iter().collect();
let saas_alloc = map[&ObligationId::new("saas-2026")];
let setup_alloc = map[&ObligationId::new("setup-fee")];
assert_eq!(saas_alloc + setup_alloc, 40_000);
// Step 5: schedules.
let saas_sched = generate(&saas, saas_alloc, Currency::USD).expect("saas sched");
let setup_sched = generate(&setup, setup_alloc, Currency::USD).expect("setup sched");
assert_eq!(saas_sched.entries.len(), 12);
assert_eq!(setup_sched.entries.len(), 1);
assert_eq!(saas_sched.total_minor(), saas_alloc);
assert_eq!(setup_sched.total_minor(), setup_alloc);
// Open deferral at contract inception and walk the schedule.
let ledger = InMemoryLedger::new();
ledger
.open_deferral(
contract.id,
None,
usd(400_00),
contract.effective_date,
"contract inception",
)
.await
.expect("open");
// Post setup recognition (the one entry on 2026-01-15).
ledger
.post_recognition(contract.id, &setup_sched.entries[0], "go-live")
.await
.expect("setup rec");
// Post the first 3 months of SaaS.
for e in saas_sched.entries.iter().take(3) {
ledger
.post_recognition(contract.id, e, "saas monthly")
.await
.expect("monthly rec");
}
let bal = ledger.balances(contract.id).await.expect("bal");
let three_months_saas: i64 = saas_sched.entries.iter().take(3).map(|e| e.amount_minor).sum();
assert_eq!(bal.recognized_minor, setup_alloc + three_months_saas);
assert_eq!(
bal.deferred_minor,
40_000 - setup_alloc - three_months_saas
);
}