///|
fn item_kind(value : Json, path : String) -> String raise PricingError {
guard value is Object(o) && o.contains("kind") else {
invalid(
"kind", path, "expected an object with explicit kind metered or package",
)
return ""
}
text_value(o["kind"], path + ".kind")
}
///|
pub fn encode_bill_input(
items : ArrayView[BillItem],
) -> String raise PricingError {
bounded_json(bill_input_json(items))
}
///|
fn bill_input_json(items : ArrayView[BillItem]) -> Json raise PricingError {
ignore(check_bill(items))
let values : Array[Json] = items.map(item => {
match item {
Metered(c) =>
{
"kind": "metered",
"plan": plan_json(c.plan),
"usage": usage_json(c.usage),
"minimum": c.minimum,
"zero_usage": zero_usage_name(c.zero_usage),
}
Packaged(p, u) =>
{
"kind": "package",
"plan": package_plan_json(p),
"usage": usage_json(u),
}
}
})
let json : Json = { "schema": "jieheng.bill-input.v1", "items": values }
json
}
///|
pub fn decode_bill_input(text : String) -> Array[BillItem] raise PricingError {
bill_input_from_json(parse_document(text))
}
///|
fn bill_input_from_json(value : Json) -> Array[BillItem] raise PricingError {
let root = fields(value, ["schema", "items"], "bill")
expect_value(root, "schema", "jieheng.bill-input.v1", "bill")
let values = bounded_items(root["items"], 64, "bill.items")
let items : Array[BillItem] = []
for i, value in values {
let path = "bill.items[\{i}]"
match item_kind(value, path) {
"metered" => {
let r = fields(
value,
["kind", "plan", "usage", "minimum", "zero_usage"],
path,
)
items.push(
Metered({
plan: plan_from_json(r["plan"]),
usage: usage_from_json(r["usage"]),
minimum: text_value(r["minimum"], path + ".minimum"),
zero_usage: zero_usage_from_name(
text_value(r["zero_usage"], path + ".zero_usage"),
),
}),
)
}
"package" => {
let r = fields(value, ["kind", "plan", "usage"], path)
items.push(
Packaged(
package_plan_from_json(r["plan"]),
usage_from_json(r["usage"]),
),
)
}
_ =>
invalid(
"kind",
path + ".kind",
"only metered and package items are supported",
)
}
}
ignore(check_bill(items[:]))
items
}
///|
pub fn encode_bill(bill : Bill) -> String raise PricingError {
bounded_json(bill_json(bill))
}
///|
fn bill_json(bill : Bill) -> Json raise PricingError {
if bill.items.length() == 0 || bill.items.length() > 64 {
invalid("size", "bill.items", "expected 1 to 64 items")
}
let items : Array[Json] = bill.items.map(item => {
match item {
MeteredCharge(c) => { "kind": "metered", "evidence": charge_json(c) }
PackageCharge(c) => { "kind": "package", "evidence": packages_json(c) }
}
})
let t = bill.totals
let json : Json = {
"schema": "jieheng.bill.v1",
"customer": bill.customer,
"period": bill.period,
"currency": bill.currency,
"items": items,
"totals": {
"usage_total": t.usage_total,
"top_up": t.top_up,
"total": t.total,
"scale": t.scale,
"rounding": rounding_name(t.rounding),
"minor_units": t.minor_units,
"amount": t.amount,
"delta": t.delta,
},
}
json
}
///|
/// Strict structural decode; verify_bill requires the complete trusted manifest.
pub fn decode_bill(text : String) -> Bill raise PricingError {
bill_from_json(parse_document(text))
}
///|
fn bill_from_json(value : Json) -> Bill raise PricingError {
let r = fields(
value,
["schema", "customer", "period", "currency", "items", "totals"],
"bill",
)
expect_value(r, "schema", "jieheng.bill.v1", "bill")
let values = bounded_items(r["items"], 64, "bill.items")
let items : Array[BillEvidence] = []
for i, value in values {
let path = "bill.items[\{i}]"
let v = fields(value, ["kind", "evidence"], path)
match item_kind(value, path) {
"metered" => items.push(MeteredCharge(charge_from_json(v["evidence"])))
"package" => items.push(PackageCharge(packages_from_json(v["evidence"])))
_ =>
invalid(
"kind",
path + ".kind",
"only metered and package evidence is supported",
)
}
}
let t = fields(
r["totals"],
[
"usage_total", "top_up", "total", "scale", "rounding", "minor_units", "amount",
"delta",
],
"bill.totals",
)
{
customer: text_value(r["customer"], "bill.customer"),
period: text_value(r["period"], "bill.period"),
currency: text_value(r["currency"], "bill.currency"),
items,
totals: {
usage_total: text_value(t["usage_total"], "bill.usage_total"),
top_up: text_value(t["top_up"], "bill.top_up"),
total: text_value(t["total"], "bill.total"),
scale: small_integer(t["scale"], 18, "bill.scale"),
rounding: rounding_from_name(text_value(t["rounding"], "bill.rounding")),
minor_units: text_value(t["minor_units"], "bill.minor_units"),
amount: text_value(t["amount"], "bill.amount"),
delta: text_value(t["delta"], "bill.delta"),
},
}
}