Some checks are pending
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Waiting to run
Every OpenPay crate now passes clippy with -D warnings, and the openpay entry of the workspace build sets clippy: true so it stays there. Most of the findings were mechanical (clippy's own fixes, write! for push_str(&format!), let-else); where a lint is wrong for the code it is allowed at the narrowest scope with its reason: code tables keep one arm per code, units_cents amounts keep their grouping, op-fraud-graph's index casts rest on a bound upsert_entity now enforces (it used to hand every vertex past 2^32 the same id). Two findings were bugs. op-payouts capped FedNow at USD 1,000,000, the limit before November 2025; the network maximum is USD 10,000,000 and a test now sends five million. op-crypto-extra's EIP-7702 low-s bound was N - 2^255, not N/2: it refused valid low-s signatures in a band just below N/2. It is N/2 now, with tests at both edges. Co-Authored-By: Claude Opus 5.5 <[email protected]>
102 lines
3.7 KiB
Rust
102 lines
3.7 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.
|
|
|
|
#![allow(clippy::inconsistent_digit_grouping, reason = "amounts in minor units are written units_cents: 12_000_00 is 12 000.00")]
|
|
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
|
|
);
|
|
}
|