///|
/// Candidate minus baseline for one identical metric snapshot. Indices preserve
/// both original bill orders; the change list follows the baseline order.
pub(all) struct BillChange {
metric : String
unit : String
baseline_index : Int32
candidate_index : Int32
usage_delta : String
top_up_delta : String
exact_delta : String
settled_delta : String
} derive(Debug, Eq)
///|
pub extend BillChange with Debug::{to_repr}
///|
pub extend BillChange with Eq::{equal, not_equal}
///|
pub(all) struct BillComparison {
baseline : Bill
candidate : Bill
changes : Array[BillChange]
exact_delta : String
settled_delta : String
} derive(Debug, Eq)
///|
pub extend BillComparison with Debug::{to_repr}
///|
pub extend BillComparison with Eq::{equal, not_equal}
///|
fn bill_item_unit(item : BillItem) -> String {
match item {
Metered(c) => c.plan.unit
Packaged(p, _) => p.unit
}
}
///|
fn align_bills(
before : ArrayView[BillItem],
after : ArrayView[BillItem],
) -> Array[Int32] raise PricingError {
let (bu, bc) = check_bill(before)
let (au, ac) = check_bill(after)
if bu.customer != au.customer ||
bu.period != au.period ||
bc != ac ||
before.length() != after.length() {
invalid(
"identity", "bill_comparison", "both manifests must share customer, period, currency and the complete metric set",
)
}
let indices : Map[String, Int32] = Map([])
for i, item in after {
indices[bill_identity(item).0.metric] = i
}
let aligned : Array[Int32] = []
for i, item in before {
let (usage, _) = bill_identity(item)
guard indices.get(usage.metric) is Some(j) else {
invalid(
"coverage",
"bill_comparison",
"candidate is missing metric '\{usage.metric}'",
)
return []
}
if usage != bill_identity(after[j]).0 ||
bill_item_unit(item) != bill_item_unit(after[j]) {
invalid(
"usage",
"bill_comparison.items[\{i}]",
"each metric must retain exactly the same usage and unit; no implicit conversion",
)
}
aligned.push(j)
}
aligned
}
///|
fn bill_item_amounts(e : BillEvidence) -> (String, String, String, String) {
match e {
MeteredCharge(c) =>
(c.calculation.total, c.top_up, c.total, c.settlement.amount)
PackageCharge(c) => (c.total, "0", c.total, c.settlement.amount)
}
}
///|
fn amount_difference(
before : String,
after : String,
) -> String raise PricingError {
format_atoms(
amount_atoms(after, "candidate.amount") -
amount_atoms(before, "baseline.amount"),
)
}
///|
fn bill_changes(
before : ArrayView[BillItem],
alignment : ArrayView[Int32],
baseline : Bill,
candidate : Bill,
) -> Array[BillChange] raise PricingError {
let rows : Array[BillChange] = []
for i, item in before {
let j = alignment[i]
let (br, bf, bt, bs) = bill_item_amounts(baseline.items[i])
let (ar, af, at, ass) = bill_item_amounts(candidate.items[j])
rows.push({
metric: bill_identity(item).0.metric,
unit: bill_item_unit(item),
baseline_index: i,
candidate_index: j,
usage_delta: amount_difference(br, ar),
top_up_delta: amount_difference(bf, af),
exact_delta: amount_difference(bt, at),
settled_delta: amount_difference(bs, ass),
})
}
rows
}
///|
/// Compare identical metric usages across full mixed contracts, aligning by
/// metric rather than position. Pricing models and minimum policies may change.
///
/// ```mbt check
/// test {
/// let p = @jieheng.compile_package(
/// id="sms",
/// version="v1",
/// metric="messages",
/// unit="message",
/// currency="USD",
/// package_size="1000",
/// package_price="0.85",
/// )
/// let items : Array[@jieheng.BillItem] = [
/// Packaged(p, {
/// customer: "acme",
/// metric: "messages",
/// period: "October",
/// quantity: "2501",
/// }),
/// ]
/// let c = @jieheng.compare_bills(items[:], items[:], scale=2, rounding=HalfEven)
/// inspect(c.exact_delta, content="0")
/// }
/// ```
pub fn compare_bills(
before : ArrayView[BillItem],
after : ArrayView[BillItem],
scale~ : Int32,
rounding~ : Rounding,
) -> BillComparison raise PricingError {
let alignment = align_bills(before, after)
let baseline = rate_bill(before, scale~, rounding~)
let candidate = rate_bill(after, scale~, rounding~)
let report = {
baseline,
candidate,
changes: bill_changes(before, alignment[:], baseline, candidate),
exact_delta: amount_difference(
baseline.totals.total,
candidate.totals.total,
),
settled_delta: amount_difference(
baseline.totals.amount,
candidate.totals.amount,
),
}
verify_bill_comparison(before, after, report, scale~, rounding~)
report
}
///|
/// Verify both complete bills against trusted manifests, then check alignment
/// and every signed difference. Does not invoke compare_bills or rate_bill.
pub fn verify_bill_comparison(
before : ArrayView[BillItem],
after : ArrayView[BillItem],
report : BillComparison,
scale~ : Int32,
rounding~ : Rounding,
) -> Unit raise PricingError {
let alignment = align_bills(before, after)
verify_bill(before, report.baseline, scale~, rounding~) catch {
Invalid(code~, path~, message~) =>
raise Invalid(code~, path="bill_comparison.baseline." + path, message~)
}
verify_bill(after, report.candidate, scale~, rounding~) catch {
Invalid(code~, path~, message~) =>
raise Invalid(code~, path="bill_comparison.candidate." + path, message~)
}
if report.changes !=
bill_changes(before, alignment[:], report.baseline, report.candidate) ||
report.exact_delta !=
amount_difference(
report.baseline.totals.total,
report.candidate.totals.total,
) ||
report.settled_delta !=
amount_difference(
report.baseline.totals.amount,
report.candidate.totals.amount,
) {
invalid(
"delta", "bill_comparison.changes", "preserve every aligned metric and its usage/top-up/exact/settled differences; totals must conserve verified item amounts",
)
}
}