crates/monetize-server/src/services/purchase.rs
//! `Purchase.Quote` prices an order without moving money; `Purchase.Start`
//! runs the transaction; `Purchase.Status` reads the ledger. The resume after a
//! vendor callback lives in [`crate::callback`]; both go through
//! [`resume_reference`] so there is one path from "open attempt" to "verdict".
use std::sync::Arc;
use monetize::ledger::Closed;
use monetize::{Order, Outcome, Purchase as PurchaseReq, Quote, TxError};
use monetize_pay::Market;
use monetize_product::TenantId;
use monetize_proto::purchase_server::Purchase;
use monetize_proto::{self as pb, PurchaseState};
use tonic::{Request, Response, Status};
use super::{blocking, ledger_err, product_or_not_found};
use crate::auth::Caller;
use crate::convert::{fact_to_pb, money_to_pb, resource_to_pb, row_to_pb};
use crate::state::AppState;
pub struct PurchaseService(pub Arc<AppState>);
/// What one run of the transaction came to, in wire terms.
pub struct Verdict {
pub reference: String,
pub state: PurchaseState,
pub reason: String,
pub fact: Option<monetize_product::EntitlementFact>,
}
/// Run the transaction for `purchase` and REPORT a Paid outcome as a state change
/// (the core pushes the fact itself, so this is where the row is written). The
/// door for `Purchase.Start`, where a caller is asking to BUY something.
pub fn run_and_report(
s: &AppState,
product: monetize_product::Served<'_>,
vendor: monetize_pay::Vendor<'_>,
purchase: &PurchaseReq,
) -> Result<Verdict, Status> {
let reference = purchase.reference();
let before = s
.ledger
.entitlement(product.id(), &purchase.tenant.0)
.map_err(ledger_err)?
.map(|f| f.state);
// A16: the checkout returns to the site the tenant buys on, whichever instance takes the money.
let site = s
.registry
.estate_of_tenant(product.id(), &purchase.tenant.0)
.and_then(|e| s.registry.site_of_estate(&e));
let back = crate::returns::ReturningTo::of(vendor, site.as_deref());
let result = s.transaction(product, back.vendor()).buy(purchase);
report_verdict(s, product, before, verdict_of(&reference, result)?)
}
/// A Paid verdict becomes a state-change row. One writer for `Start` and for a
/// resume, so the console and the testmatrix see the same row whichever door the
/// verdict came through.
fn report_verdict(
s: &AppState,
product: monetize_product::Served<'_>,
before: Option<monetize_product::State>,
v: Verdict,
) -> Result<Verdict, Status> {
if let (PurchaseState::Paid, Some(fact)) = (v.state, &v.fact) {
s.report_change(crate::state::StateChange {
product: product.id().into(),
tenant: fact.tenant.0.clone(),
from: before,
to: fact.state,
source: fact.source.clone(),
at_unix_ms: s.now(),
pushed: true,
});
// An "Add users" order: the room is the tenant's now, so the users it
// was bought for are granted — whichever door reported the payment.
grant_paid_users(s, product, &v.reference)?;
}
Ok(v)
}
// ── "Add users" (owner, 2026-10-04) ─────────────────────────────────────────
/// **The users a PAID order bought room for, granted** — once: the product
/// answers a retried order with its first grant, and the ledger keeps one
/// [`monetize::Step::UsersGranted`] row per reference. A product that cannot
/// be reached leaves no row, and the next door that looks (a `Status` read,
/// the poller) tries again; the room is the tenant's either way.
///
/// **A row that cannot be read or written is an error** (`Unavailable`), never
/// a log line: the next read asks the product again, which answers the first
/// grant `unchanged`, and an unchanged answer is never invited a second time.
///
/// **A grant the product REFUSES is refused for good** (a name taken while the
/// customer paid): the payment is refunded through the vendor, the room the
/// order raised is given back, both are journaled, and the buyer is told —
/// once ([`refund_users`]).
///
/// **The ledger names no member** (G13, 5.4): the order's row holds the ask
/// without its name and address; they wait beside it ([`crate::named_users`])
/// and are forgotten once the grant's row stands. A named order whose name is
/// gone (a restore) is refused like a name taken: refunded, nobody added.
pub fn grant_paid_users(
s: &AppState,
product: monetize_product::Served<'_>,
reference: &str,
) -> Result<Option<pb::UsersGrant>, Status> {
let Some(attempt) = s.ledger.attempt(reference).map_err(ledger_err)? else {
return Ok(None);
};
let Some(bare) = attempt.purchase.users.clone() else {
return Ok(None);
};
if !matches!(attempt.closed, Some(Closed::Pushed { .. })) {
return Ok(None);
}
if let Some(granted) = users_granted_row(s, reference).map_err(ledger_err)? {
forget_names(s, reference);
return Ok(Some(granted));
}
let ask = match s.named_users.get(reference) {
Some(named) => monetize_product::UsersAsk {
name: named.name,
email: named.email,
..bare.clone()
},
None => bare.clone(),
};
if refund_of(s, reference).is_some() {
return Ok(refund_users(s, product, reference, &attempt, &ask, ""));
}
if !bare.principal_id.is_empty() && ask.name.is_empty() {
let why = "the name it was to add is no longer held here (monetize was restored while the order was open)";
eprintln!("monetize-server: ALERT users.refunded {reference}: paid, and {why}: refunded");
return Ok(refund_users(s, product, reference, &attempt, &ask, why));
}
let bought = sold_disk_bytes(product, &attempt.purchase.delta);
match product.add_users(&attempt.purchase.tenant, &ask, reference) {
Ok(answer) => {
// The product's retry of a grant made before (its row was not
// written then) is never invited again.
let invited = if answer.unchanged {
ATTEMPT_INVITED.to_owned()
} else {
invite_users(s, product, &attempt.purchase.tenant.0, &ask, &answer).0
};
let step = monetize::Step::UsersGranted {
reference: reference.to_owned(),
product: product.id().to_owned(),
tenant: attempt.purchase.tenant.0.clone(),
users: ask.without_names(),
principal_id: ask.principal_id.clone(),
seats: answer.seats,
room_bytes: answer.room_bytes,
from_unused_bytes: answer.need_bytes.saturating_sub(bought),
bought_bytes: bought,
invited: invited.clone(),
};
if let Err(e) = s.ledger.append(s.now(), step) {
let why = format!("{reference}: the users were granted, and the ledger row could not be written: {e}; written on the next read");
skidbladnir::functional_status("monetize-server/add-users", "grant", false, &why);
return Err(Status::unavailable(why));
}
forget_names(s, reference);
let row = users_granted_row(s, reference)
.map_err(ledger_err)?
.map(|g| pb::UsersGrant {
principal: answer.principal.clone(),
..g
});
Ok(Some(row.unwrap_or(pb::UsersGrant {
principal: answer.principal,
seats: answer.seats,
seats_open: answer.seats_open,
room_bytes: answer.room_bytes,
need_bytes: answer.need_bytes,
from_unused_bytes: answer.need_bytes.saturating_sub(bought),
bought_bytes: bought,
granted: true,
invited,
..Default::default()
})))
}
Err(monetize_product::ProductError::Refused(why)) => {
Ok(refund_users(s, product, reference, &attempt, &ask, &why))
}
Err(e) => {
skidbladnir::functional_status(
"monetize-server/add-users",
"grant",
false,
&format!("{reference}: paid, and the users were NOT granted yet: {e}"),
);
eprintln!("monetize-server: users {reference}: paid, and the product did not grant the users yet ({e}); retried on the next read");
Ok(None)
}
}
}
/// The names a paid order waited with, forgotten: its grant's row (or its
/// refund) stands. One that cannot be rewritten is said, and forgotten on the
/// next read.
fn forget_names(s: &AppState, reference: &str) {
if let Err(e) = s.named_users.forget(reference) {
eprintln!("monetize-server: users {reference}: the names beside the ledger could not be forgotten ({e}); again on the next read");
}
}
/// What a grant's row says of its invite when the product answered a retry:
/// the invite went out with the first answer, whose row was not written.
const ATTEMPT_INVITED: &str = "invited when first granted";
/// Who journals this server's own refunds.
const REFUNDED_BY: &str = "monetize";
/// `(refunded in full, the operator's reason)` for an Add-users order this
/// server refunded itself, or `None`.
fn refund_of(s: &AppState, reference: &str) -> Option<(bool, String)> {
let rows = s.ledger.rows(reference).ok()?;
let asked = rows.iter().rev().find_map(|row| match &row.step {
monetize::Step::RefundRequested { by, reason, .. } if by == REFUNDED_BY => {
Some(reason.clone())
}
_ => None,
})?;
let done = rows
.iter()
.any(|row| matches!(&row.step, monetize::Step::Reversed { kind: monetize_pay::ReversalKind::Refunded, by, .. } if by == REFUNDED_BY));
Some((done, asked))
}
/// What the buyer reads about a refunded Add-users order. `why` is the refusal as the
/// product filed it (both gunnar plugins: `gunnar <code>: <message>`): only gunnar's
/// `AlreadyExists` is a name taken — any other NO (a clan, a namespace gone) is said plainly,
/// never as a name taken (wiring tracer B 2026-10-08).
fn refunded_words(ask: &monetize_product::UsersAsk, why: &str) -> String {
let taken = why.contains("AlreadyExists");
match ask.name.as_str() {
// A named user whose name is no longer held here (forgotten once told).
"" if !ask.principal_id.is_empty() && taken => {
"The user could not be added after the payment (the name was taken while you paid), so the payment was refunded in full and nothing was kept.".to_owned()
}
"" if !ask.principal_id.is_empty() => {
"The user could not be added after the payment, so the payment was refunded in full and nothing was kept.".to_owned()
}
"" => format!(
"The {} seat(s) could not be added after the payment, so the payment was refunded in full and nothing was kept.",
ask.count.max(1)
),
name if taken => format!(
"{name} could not be added after the payment (the name was taken while you paid), so the payment was refunded in full and nothing was kept."
),
name => format!("{name} could not be added after the payment, so the payment was refunded in full and nothing was kept."),
}
}
/// **Refund a paid Add-users order whose users can never be granted**: the
/// vendor refund (journaled `RefundRequested` → `Reversed`, by `monetize`,
/// idempotent on the vendor's key), the room the order raised taken back off
/// the tenant's fact ([`take_back_room`], retried on every read until its
/// row stands), and the buyer told — the mail once, when the room is back. A
/// vendor that cannot be reached leaves the request written and is asked
/// again on the next read.
fn refund_users(
s: &AppState,
product: monetize_product::Served<'_>,
reference: &str,
attempt: &monetize::ledger::Attempt,
ask: &monetize_product::UsersAsk,
why: &str,
) -> Option<pb::UsersGrant> {
let bought = sold_disk_bytes(product, &attempt.purchase.delta);
// The refusal: this call's, or the one journaled when the refund was first asked for.
let refusal = match refund_of(s, reference) {
Some((_, asked)) if why.is_empty() => asked,
_ => why.to_owned(),
};
let words = refunded_words(ask, &refusal);
let refunded = |reason: String| pb::UsersGrant {
room_bytes: ask.room_bytes,
need_bytes: ask.room_bytes.saturating_mul(u64::from(ask.count.max(1))),
bought_bytes: bought,
refunded: true,
reason,
..Default::default()
};
let reason = match refund_of(s, reference) {
Some((true, asked)) => asked,
Some((false, asked)) => {
refund_by_vendor(s, reference, attempt, &asked)?;
asked
}
None => {
let reason = format!("Add users could not be granted after the capture: {why}");
refund_by_vendor(s, reference, attempt, &reason)?;
reason
}
};
// The room this order raised goes back with the money: its own state,
// retried on every read until its row stands; the buyer is told then.
match take_back_room(s, product, reference, attempt, bought) {
Ok(false) => {}
Ok(true) => {
let tenant = &attempt.purchase.tenant.0;
let subject = format!(
"{}: the users you paid for could not be added — refunded",
crate::nonpay::shown(product.id())
);
let body = format!("{words}\n\nOrder: {reference}\n");
let (sent, line) =
crate::nonpay::send(s, product.id(), tenant, "users.refunded", (subject, body));
skidbladnir::functional_status(
"monetize-server/add-users",
"refund",
true,
&format!(
"{reference}: refunded ({reason}); the buyer {}",
if sent { "was told" } else { "was NOT told" }
),
);
eprintln!("monetize-server: users {reference}: REFUNDED — {reason}; the buyer: {line}");
forget_names(s, reference);
}
Err(e) => {
skidbladnir::functional_status(
"monetize-server/add-users",
"refund",
false,
&format!("{reference}: refunded, and the room is NOT taken back yet: {e}"),
);
eprintln!("monetize-server: users {reference}: refunded, and the room could not be taken back ({e}); retried on the next read");
return None;
}
}
Some(refunded(words))
}
/// The vendor refund of an Add-users order, by this server; `None` when it did
/// not land (asked again on the next read, or left for an operator).
fn refund_by_vendor(
s: &AppState,
reference: &str,
attempt: &monetize::ledger::Attempt,
reason: &str,
) -> Option<()> {
match crate::reversal::refund_order_by(
s,
&attempt.handle.vendor,
&attempt.handle.id,
0,
reason,
REFUNDED_BY,
) {
Ok(_) => Some(()),
Err(crate::signup::RefundRefusal::Unavailable(e)) => {
eprintln!("monetize-server: users {reference}: the refund is REQUESTED and the vendor did not answer ({e}); asked again on the next read");
None
}
Err(e) => {
skidbladnir::functional_status("monetize-server/add-users", "refund", false, &format!("{reference}: paid, never grantable, and the refund was REFUSED: {e:?} — an operator must give the money back"));
eprintln!("monetize-server: users {reference}: paid, never grantable, and the refund was refused ({e:?}); an operator must act");
None
}
}
}
/// **Take back the room a refunded order raised, once**: the fact loses
/// `bought` on the disk meter (source `refund:<reference>`), then the
/// [`monetize::Step::RoomTakenBack`] row is written. `Ok(true)` = written by
/// this call; `Ok(false)` = it already stood. A crash between the two finds the
/// fact already carrying the refund's source and only writes the row.
fn take_back_room(
s: &AppState,
product: monetize_product::Served<'_>,
reference: &str,
attempt: &monetize::ledger::Attempt,
bought: u64,
) -> Result<bool, String> {
let _order = crate::reversal::OrderLock::take(reference);
let rows = s.ledger.rows(reference).map_err(|e| e.to_string())?;
if rows
.iter()
.any(|row| matches!(row.step, monetize::Step::RoomTakenBack { .. }))
{
return Ok(false);
}
let tenant = &attempt.purchase.tenant.0;
let meter = disk_meter(product).unwrap_or_default();
let mut taken = 0;
if let (false, true, Some(held)) = (
meter.is_empty(),
bought > 0,
s.ledger
.entitlement(product.id(), tenant)
.map_err(|e| e.to_string())?,
) {
taken = bought;
let source = format!("refund:{reference}");
if held.source != source {
let mut fact = held.clone();
fact.source = source;
fact.signature = Vec::new();
if let Some(cap) = fact.caps.get_mut(meter) {
*cap = cap.saturating_sub(bought);
}
let fact = s.signer_for(product.id()).sign_fact(fact);
if let Err(e) = s.apply_fact(product, &fact).map_err(|e| e.to_string())? {
eprintln!("monetize-server: users {reference}: the room is taken back and the product has not heard yet ({e}); the push is retried");
}
}
}
let step = monetize::Step::RoomTakenBack {
reference: reference.to_owned(),
product: product.id().to_owned(),
tenant: tenant.clone(),
meter: meter.to_owned(),
bytes: taken,
};
s.ledger.append(s.now(), step).map_err(|e| e.to_string())?;
Ok(true)
}
/// **The new named user's invite, delivered**: the link the console redeems
/// (`<origin>/login?invite=<secret>&user=<id>`), mailed to the address the
/// admin typed — through the deployment's one customer-notice path. Answers
/// `(the mail line, the link)`; both empty when there is nobody to invite or
/// nowhere to send it. The secret is in the link and nowhere else: never on
/// a ledger row.
fn invite_users(
s: &AppState,
product: monetize_product::Served<'_>,
tenant: &str,
ask: &monetize_product::UsersAsk,
answer: &monetize_product::UsersAnswer,
) -> (String, String) {
if answer.principal.is_empty()
|| answer.invite_secret.is_empty()
|| ask.invite_origin.is_empty()
{
return (String::new(), String::new());
}
let link = format!(
"{}/login?invite={}&user={}",
ask.invite_origin.trim_end_matches('/'),
answer.invite_secret,
answer.principal
);
let to = ask.email.trim();
if to.is_empty() {
return (String::new(), link);
}
let notice = monetize_pay::CustomerNotice {
product: product.id().to_owned(),
tenant: tenant.to_owned(),
to: to.to_owned(),
kind: "users.invite".to_owned(),
subject: format!("{}: {tenant} invited you", product.id()),
body: format!(
"{tenant} added you to {} as {}. Register your passkey with this link — it works once, for seven days:\n\n{link}\n",
product.id(),
answer.principal
),
};
let line = match s.signup_sink.notify(¬ice) {
Ok(_) => format!("mailed to {}", monetize_mail::masked(to)),
Err(e) => format!("NOT mailed to {}: {e}", monetize_mail::masked(to)),
};
(line, link)
}
/// The `UsersGranted` row under `reference`, on the wire.
fn users_granted_row(
s: &AppState,
reference: &str,
) -> Result<Option<pb::UsersGrant>, monetize::ledger::LedgerError> {
Ok(s.ledger
.rows(reference)?
.into_iter()
.find_map(|row| match row.step {
// No name: the row holds the member's id, the name is the estate's.
monetize::Step::UsersGranted {
seats,
room_bytes,
from_unused_bytes,
bought_bytes,
users,
invited,
..
} => Some(pb::UsersGrant {
invited,
principal: String::new(),
seats,
seats_open: 0,
room_bytes,
need_bytes: room_bytes.saturating_mul(u64::from(users.count.max(1))),
from_unused_bytes,
bought_bytes,
granted: true,
..Default::default()
}),
_ => None,
}))
}
/// The unit disk room is sold in: the price list's `disk_gib_month`.
const SOLD_IN: u64 = 1 << 30;
/// What an order SOLD on the product's disk meter (its `DiskBytes` delta).
fn sold_disk_bytes(
product: monetize_product::Served<'_>,
delta: &std::collections::BTreeMap<String, u64>,
) -> u64 {
delta
.iter()
.filter(|(m, _)| {
product.meters().iter().any(|meter| {
meter.name == m.as_str()
&& meter.backing == Some(monetize_product::Backing::DiskBytes)
})
})
.map(|(_, v)| *v)
.sum()
}
/// The product's disk meter: what room is sold on.
fn disk_meter(product: monetize_product::Served<'_>) -> Option<&'static str> {
product
.meters()
.iter()
.find(|m| m.backing == Some(monetize_product::Backing::DiskBytes))
.map(|m| m.name)
}
/// **The reference of users granted with nothing bought**:
/// `<product>/<tenant>/<YYYY-MM-DD>+users:<count>x<room>[:<name>]`, the
/// grant's own — deterministic, so a retry is the same grant — and spelled
/// like every order's (`<product>/<tenant>/…`), so the tenant's scope reads
/// it as theirs.
fn users_reference(
product: &str,
tenant: &str,
at_unix_ms: u64,
ask: &monetize_product::UsersAsk,
) -> String {
format!(
"{product}/{tenant}/{}+users:{}",
monetize::embed::civil::iso_date(at_unix_ms),
ask.fingerprint()
)
}
/// **The refusal of a short "Add users" on a server that does not sell
/// room**: what is unused, what was asked, and how much is missing.
fn users_short(
tenant: &str,
ask: &monetize_product::UsersAsk,
fit: &monetize_product::UsersAnswer,
why: &str,
) -> Status {
let size = monetize::transaction::words::size;
Status::failed_precondition(format!(
"{tenant} has {} of room unused; {} user(s) × {} need {}: {} short. {why} Add room to the account, or ask for \
fewer users or less room per user. {}",
size(fit.unused_bytes.unwrap_or(0)),
ask.count.max(1),
size(fit.room_bytes),
size(fit.need_bytes),
size(fit.shortfall_bytes),
monetize::transaction::words::NOTHING_CHARGED,
))
}
fn product_status(e: monetize_product::ProductError) -> Status {
match e {
monetize_product::ProductError::Refused(why) => Status::failed_precondition(why),
monetize_product::ProductError::Unavailable(why) => Status::unavailable(why),
}
}
/// **`Purchase.Start` with `users`** — "Add users" (owner, 2026-10-04). ONE
/// call for the console: the product is asked whether the users fit in the
/// room the account holds and has not stored or promised (and refuses a name
/// it could never create, before any money moves); if they fit they are
/// granted at once and nothing is bought; if not, EXACTLY the shortfall is
/// sold through the same quote, the same transaction and the same single
/// checkout every order gets — or, where this server sells no room, refused
/// naming what is missing. The users are granted when the order is paid
/// ([`grant_paid_users`]); a checkout that expires releases the attempt and
/// grants nothing.
fn start_users(
s: &AppState,
product: monetize_product::Served<'_>,
r: &pb::StartPurchaseRequest,
users: &pb::UsersOrder,
market: Market,
caller: Option<&Caller>,
site: Option<&str>,
) -> Result<pb::StartPurchaseResponse, Status> {
if !r.caps.is_empty() || !r.bundle.is_empty() || r.renew {
return Err(Status::invalid_argument(
"an \"Add users\" order names `users` and nothing else: no caps, no bundle, no renewal — the room it needs is \
decided here, from what the account already holds",
));
}
if r.tenant.is_empty() {
return Err(Status::invalid_argument("tenant is required"));
}
let ask = monetize_product::UsersAsk {
count: users.count.max(1),
room_bytes: users.room_bytes,
name: users.name.trim().to_owned(),
email: users.email.trim().to_owned(),
invite_origin: users.invite_origin.trim().to_owned(),
principal_id: String::new(),
};
// G13: the ledger knows the member by this id, never by the name.
let ask = monetize_product::UsersAsk {
principal_id: monetize::ledger::principal_id(product.id(), &r.tenant, &ask.name),
..ask
};
// FIRST: the ticket covers exactly this number of users at this room.
check_actor(
s,
product,
&r.tenant,
monetize_embed::PURPOSE_ORDER,
&ask.ticket_caps(),
Asker {
site,
ticket: &r.actor_ticket,
caller,
},
)?;
let tenant = TenantId(r.tenant.clone());
let starts = if r.starts_unix_ms == 0 {
s.now()
} else {
r.starts_unix_ms
};
// An account with NO fact is uncapped at gunnar and fits every ask: at a selling shop it is
// given the shop's account fact first (2026-10-08, the free-room leak), so the ask below
// is answered against the room it really holds.
crate::poller::account_holds_a_fact(s, product, &r.tenant).map_err(|why| {
Status::unavailable(format!(
"{}'s account fact could not be given, so nothing is sold now: {why}",
r.tenant
))
})?;
let fit = product.users_fit(&tenant, &ask).map_err(product_status)?;
let grant_of =
|answer: &monetize_product::UsersAnswer, bought: u64, granted: bool| pb::UsersGrant {
principal: answer.principal.clone(),
seats: answer.seats,
seats_open: answer.seats_open,
room_bytes: answer.room_bytes,
need_bytes: answer.need_bytes,
from_unused_bytes: answer.need_bytes.saturating_sub(bought),
bought_bytes: bought,
granted,
..Default::default()
};
if fit.shortfall_bytes == 0 {
// ★ The unused room covers it: granted NOW, nothing bought, no vendor.
// The account's seats before this grant are part of its name: a retry
// of THIS grant is the same reference (and the product answers the
// first one), while the admin asking for the same number again is a
// new grant — not silently the old one.
let reference = format!(
"{}@{}",
users_reference(product.id(), &r.tenant, starts, &ask),
fit.seats
);
let answer = product
.add_users(&tenant, &ask, &reference)
.map_err(product_status)?;
// The named user's invite: the link on THIS answer (the admin is on
// the page) and, given an address, in the user's mail.
let (invited, invite_url) = if answer.unchanged {
(String::new(), String::new())
} else {
invite_users(s, product, &r.tenant, &ask, &answer)
};
let step = monetize::Step::UsersGranted {
reference: reference.clone(),
product: product.id().to_owned(),
tenant: r.tenant.clone(),
users: ask.without_names(),
principal_id: ask.principal_id.clone(),
seats: answer.seats,
room_bytes: answer.room_bytes,
from_unused_bytes: answer.need_bytes,
bought_bytes: 0,
invited: invited.clone(),
};
s.ledger.append(s.now(), step).map_err(ledger_err)?;
return Ok(pb::StartPurchaseResponse {
reference,
state: PurchaseState::Paid as i32,
words: monetize::transaction::progress::USERS_FROM_UNUSED.to_owned(),
users: Some(pb::UsersGrant {
invite_url,
invited,
..grant_of(&answer, 0, true)
}),
..Default::default()
});
}
// ★ Short: sell EXACTLY what the unused room cannot cover, as an order for
// more room — the bundle path's arithmetic, on the product's disk meter.
let meter = disk_meter(product).ok_or_else(|| {
Status::failed_precondition(format!(
"{} sells no room: it declares no disk meter",
product.id()
))
})?;
let current = s
.ledger
.entitlement(product.id(), &r.tenant)
.map_err(ledger_err)?
.map(|f| f.caps)
.unwrap_or_default();
// In the unit room is SOLD in (a GiB-month): the price rounds a part GiB
// up, so the customer is given every GiB they pay for.
let buy = fit.shortfall_bytes.div_ceil(SOLD_IN) * SOLD_IN;
let caps = target_of(¤t, &[(meter.to_owned(), buy)].into_iter().collect());
let payer = if r.payer.is_empty() {
s.signups.email_of(product.id(), &r.tenant).ok().flatten()
} else {
Some(r.payer.clone())
};
let quote = quote_order(s, product, &r.tenant, caps, market, r.starts_unix_ms, payer)
.map_err(quote_status)?;
if let Err(refusal) = "e.serve {
eprintln!(
"monetize-server: users for {}/{} refused: {refusal}",
product.id(),
r.tenant
);
return Err(refused_status(refusal));
}
let mut purchase = quote.purchase.clone();
purchase.users = Some(ask.without_names());
// The open, free path sells nothing: room it would hand out for nothing is
// not room the account has. Refused, naming what is missing.
if purchase.is_unpriced() {
return Err(users_short(
&r.tenant,
&ask,
&fit,
"This server does not sell room.",
));
}
let Some(vendor) = s.registry.vendor_for_tenant(
&purchase.price,
market,
product.id(),
&purchase.tenant.0,
&r.vendor,
) else {
return Err(users_short(
&r.tenant,
&ask,
&fit,
"No payment can be taken here for the rest.",
));
};
let reference = purchase.reference();
// The names wait BESIDE the ledger until the grant (G13); before any money moves.
if !ask.name.is_empty() || !ask.email.is_empty() {
let named = crate::named_users::Named {
name: ask.name.clone(),
email: ask.email.clone(),
};
s.named_users.keep(&reference, named).map_err(|e| {
Status::unavailable(format!(
"the order's names could not be kept, so nothing is sold now: {e}"
))
})?;
}
let v = run_and_report(s, product, vendor, &purchase)?;
let handle = s
.ledger
.attempt(&reference)
.map_err(ledger_err)?
.map(|a| a.handle);
let redirect_url = match (&handle, v.state) {
(Some(h), PurchaseState::Pending) => redirect_url(s, vendor, h, &reference),
_ => String::new(),
};
let bought = sold_disk_bytes(product, &purchase.delta);
let users = match v.state {
PurchaseState::Paid => grant_paid_users(s, product, &reference)?
.unwrap_or_else(|| grant_of(&fit, bought, false)),
_ => grant_of(&fit, bought, false),
};
let (handle_vendor, handle_id) = handle.map(|h| (h.vendor, h.id)).unwrap_or_default();
Ok(pb::StartPurchaseResponse {
reference: v.reference,
state: v.state as i32,
handle_vendor,
handle_id,
redirect_url,
reason: v.reason,
fact: v.fact.as_ref().map(fact_to_pb),
words: match v.state {
PurchaseState::Paid => monetize::transaction::progress::QUEUED.to_owned(),
_ => String::new(),
},
users: Some(users),
})
}
/// **Where the room a paid order bought stands**, for `Status`: done. The room is the
/// tenant's at once; an order past the room is queued at the estate that grows its own set
/// (a signed `grow_room` order, [`crate::grow::queue_past_room`]), not in a queue here.
pub(crate) fn room_progress(_s: &AppState, _product: &str, _reference: &str) -> pb::RoomProgress {
use monetize::transaction::progress;
pb::RoomProgress {
state: "done".to_owned(),
words: progress::DONE.to_owned(),
}
}
pub fn verdict_of(reference: &str, result: Result<Outcome, TxError>) -> Result<Verdict, Status> {
let (state, reason, fact) = match result {
Ok(Outcome::Paid { fact, .. }) => (PurchaseState::Paid, String::new(), Some(fact)),
Ok(Outcome::Declined { reason, .. }) => (PurchaseState::Declined, reason, None),
Ok(Outcome::Released { reason, .. }) => (PurchaseState::Released, reason, None),
// **Not folded into `Released`.** The engine refused to claim a release
// it could not verify, and a door that quietly upgraded the verdict
// would undo that one call short of the operator who has to act on it.
// The uuids travel in `reason`, because an orphan nobody can name is an
// orphan nobody will go and free.
Ok(Outcome::ReleasedWithOrphans {
reason, orphans, ..
}) => (
PurchaseState::ReleasedWithOrphans,
format!(
"{reason} — the release is NOT proven: {}",
orphans.detail.join("; ")
),
None,
),
Err(TxError::Stalled { cause, .. }) => (PurchaseState::Pending, cause, None),
Err(TxError::ReserveUnavailable(e)) => {
return Err(Status::unavailable(format!("vendor: {e}")))
}
Err(e @ TxError::WrongProduct { .. }) => {
return Err(Status::invalid_argument(e.to_string()))
}
// An order refused before anything was spent. `failed_precondition`
// rather than `invalid_argument`: the request was well formed and the
// WORLD refused it — the tenant would be shrinking, the product cannot
// deliver the caps, nobody can take the money. The console shows the
// reason verbatim, because "the product cannot absorb this" is the
// operator's answer to the customer.
Err(TxError::Refused(r)) => return Err(Status::failed_precondition(r.to_string())),
Err(TxError::Ledger(e)) => return Err(ledger_err(e)),
// **Neither can arise from a PURCHASE**, and they are named here rather
// than swept into a wildcard so that stays a fact somebody has to
// re-examine. `Product` and `Cloud` belong to `super_purge`, which runs
// no vendor and produces no `Outcome`; a purchase asks the product only
// `can_absorb`, whose refusal is a `Refused` above. Answering
// `internal` says exactly that: if one of these ever reaches here, the
// transaction has changed shape and this arm is the wrong answer.
Err(e @ (TxError::Product(_) | TxError::Cloud(_))) => {
return Err(Status::internal(format!(
"a purchase produced a teardown error, which it has no path to: {e}"
)))
}
};
Ok(Verdict {
reference: reference.into(),
state,
reason,
fact,
})
}
/// How often a `Purchase.Status` read of an OPEN attempt asks the vendor
/// (see `status`): a console following an order polls every few seconds, and
/// every ask is a vendor API call.
pub const ASK_VENDOR_EVERY: std::time::Duration = std::time::Duration::from_secs(10);
/// Whether a Status read of `reference` should ask the vendor now; records the
/// ask. Bounded: entries older than an hour are dropped when the map grows.
fn ask_vendor_due(reference: &str) -> bool {
static ASKED: std::sync::LazyLock<
std::sync::Mutex<std::collections::HashMap<String, std::time::Instant>>,
> = std::sync::LazyLock::new(Default::default);
let now = std::time::Instant::now();
let mut asked = ASKED.lock().unwrap_or_else(|p| p.into_inner());
if asked.len() > 1024 {
asked.retain(|_, at| now.duration_since(*at) < std::time::Duration::from_secs(3600));
}
match asked.get(reference) {
Some(at) if now.duration_since(*at) < ASK_VENDOR_EVERY => false,
_ => {
asked.insert(reference.to_owned(), now);
true
}
}
}
/// **Resume ONE open attempt from the ledger** — the vendor named in its handle,
/// the product named in its reference, and
/// [`monetize::Transaction::reconcile_reference`] on the rows. `None` when the
/// reference is unknown or already closed.
///
/// It used to call `Transaction::run` again with the purchase read back out of
/// the attempt, and that is subtly the wrong verb. `run` is "buy this": it will
/// RESERVE AGAIN if the attempt turns out to be closed, and it goes straight to
/// `resume` — which for an attempt with no `Captured` row calls `pay.capture`
/// without ever asking the vendor what it thinks. So a reservation the vendor
/// had already captured, because the callback never arrived, was captured a
/// second time: idempotent at some vendors, a second charge at others, and
/// monetize does not choose which vendor it is talking to.
///
/// `reconcile_reference` asks `pay.status` first and records what the vendor says,
/// and it cannot start a new attempt at all — no open attempt, no action. It is
/// the same code the scheduled sweep runs, narrowed to one row, so a webhook and
/// the poller finish an attempt identically.
pub fn resume_reference(s: &AppState, reference: &str) -> Result<Option<Verdict>, Status> {
// **One resume per reference at a time** (stinkies money-webhook-504-in-growth):
// the webhook's grant, a vendor's redelivery, the poller's tick and a
// watching `Purchase.Status` all land here. Before the flight, a redelivery
// that arrived while the first grant was still pushing resumed the SAME
// open attempt and pushed the entitlement a second time (and could write a
// close for an attempt the first had closed). The loser touches nothing and
// answers PENDING — the holder is finishing it.
let Some(_flight) = s.resuming.fly(reference) else {
return Ok(Some(Verdict {
reference: reference.to_owned(),
state: PurchaseState::Pending,
reason: IN_FLIGHT.to_owned(),
fact: None,
}));
};
let Some(attempt) = s.ledger.attempt(reference).map_err(ledger_err)? else {
return Ok(None);
};
if !attempt.is_open() {
return Ok(None);
}
let product_id = reference.split('/').next().unwrap_or_default();
let product = product_or_not_found(s, product_id)?;
let vendor = s
.registry
.find_vendor(&attempt.handle.vendor)
.ok_or_else(|| {
Status::failed_precondition(format!(
"{reference} was reserved at {:?}, which is not linked into this server",
attempt.handle.vendor
))
})?;
let before = s
.ledger
.entitlement(product.id(), &attempt.purchase.tenant.0)
.map_err(ledger_err)?
.map(|f| f.state);
// `Ok(None)` here means the attempt closed between the two reads — the poller
// and a webhook race by design. Nothing happened, and that is not an error.
let outcome = match s
.transaction(product, vendor)
.reconcile_reference(reference)
{
Ok(None) => return Ok(None),
Ok(Some(o)) => Ok(o),
Err(e) => Err(e),
};
report_verdict(s, product, before, verdict_of(reference, outcome)?).map(Some)
}
/// The reason a [`resume_reference`] answers PENDING when another caller is
/// finishing the same reference right now.
pub const IN_FLIGHT: &str = "another caller is finishing this purchase right now";
/// **Where to send the customer.** The vendor's own answer first, the configured
/// template second, empty if neither has one.
///
/// The order is the whole point. `checkout_url` in monetize.toml is a template
/// over the handle and the reference, which is right for a vendor with ONE fixed
/// checkout page and cannot express a vendor that mints a URL per order — Klarna
/// returns one from create-order, a Stripe intent carries one in `next_action`,
/// and both contain a session id the vendor invented that no template of ours
/// could produce. So the vendor is asked first and is believed; the template is
/// what is left for the vendors it fits.
///
/// One writer for `Start` and for `Status` (one implementation): an operator reopening a
/// pending attempt must be offered the same link the sale did, not a second
/// opinion about it.
fn redirect_url(
s: &AppState,
vendor: monetize_pay::Vendor<'_>,
handle: &monetize_pay::ChargeHandle,
reference: &str,
) -> String {
if let Some(url) = vendor.page_url(handle) {
return url;
}
s.checkout_urls
.read()
.unwrap_or_else(|e| e.into_inner())
.get(&handle.vendor)
.map(|t| {
t.replace("{handle}", &handle.id)
.replace("{reference}", reference)
})
.unwrap_or_default()
}
/// The market a request names, or the deployment's default when it names none.
fn market_of(s: &AppState, wire: i32) -> Market {
market_of_wire(wire).unwrap_or_else(|| {
s.terms
.read()
.unwrap_or_else(|e| e.into_inner())
.default_market
})
}
/// Pure: the market a wire value names — `None` when it names none (or one this
/// build does not know), and the default applies. A DK order is a Danish order.
fn market_of_wire(wire: i32) -> Option<Market> {
match pb::Market::try_from(wire) {
Ok(pb::Market::Se) => Some(Market::Se),
Ok(pb::Market::Fi) => Some(Market::Fi),
Ok(pb::Market::Dk) => Some(Market::Dk),
_ => None,
}
}
/// **Quote an order under the deployment's terms — the one writer for `Quote`
/// and `Start`**, so what the console shows before the confirm button is what
/// `Start` runs. Nothing is reserved; the product's control plane is asked
/// once (`can_absorb`, and `servable` only for a product-scope cap).
/// **What is wrong with the REQUEST**, as opposed to what the world refuses.
/// Split out of [`quote_order`] because the two have different answers: a cap
/// naming a meter that does not exist is `invalid_argument` whoever asks, while
/// a product that cannot absorb the order is a quote with `can_serve = false`
/// that the shop window still shows.
fn check_order_args(
product: monetize_product::Served<'_>,
tenant: &str,
caps: &std::collections::BTreeMap<String, u64>,
) -> Result<(), Status> {
if tenant.is_empty() {
return Err(Status::invalid_argument("tenant is required"));
}
if let Some(cap) = caps
.keys()
.find(|k| !product.meters().iter().any(|m| m.name == k.as_str()))
{
return Err(Status::invalid_argument(format!(
"cap {cap:?} names a meter {} does not declare; it declares {:?}",
product.id(),
product.meters().iter().map(|m| m.name).collect::<Vec<_>>()
)));
}
Ok(())
}
/// **[`quote_order`] in the deployment's default market, now** — the quote a
/// shop window row and an order make, for an integration test to hold the
/// queue (round 5.3) to. Panics on a refusal the quote itself raises (a
/// downgrade, a no-op): a test that asks one has asked the wrong thing.
pub fn quote_order_for_test(
s: &AppState,
product: monetize_product::Served<'_>,
tenant: &str,
caps: std::collections::BTreeMap<String, u64>,
) -> Quote {
let market = s
.terms
.read()
.unwrap_or_else(|e| e.into_inner())
.default_market;
quote_order(s, product, tenant, caps, market, 0, None)
.unwrap_or_else(|e| panic!("the quote itself refused: {e}"))
}
pub(crate) fn quote_order(
s: &AppState,
product: monetize_product::Served<'_>,
tenant: &str,
caps: std::collections::BTreeMap<String, u64>,
market: Market,
starts_unix_ms: u64,
payer: Option<String>,
) -> Result<Quote, TxError> {
let order = Order {
product: product.id().into(),
tenant: TenantId(tenant.to_owned()),
caps,
};
let terms = s.terms.read().unwrap_or_else(|e| e.into_inner()).clone();
let host = terms.host(product.id());
let terms = terms.terms(product.id(), market, &host);
let starts = if starts_unix_ms == 0 {
s.now()
} else {
starts_unix_ms
};
// The quote needs no vendor: nothing is reserved. `NO_VENDOR_PROVIDER` is
// the honest stand-in — a transaction that could not pay if it tried.
//
// The `TxError` is handed back unmapped because the SHOP WINDOW needs it:
// `Purchase.Bundles` turns a `Refused` into `can_serve: false` with the
// customer's sentence rather than into a gRPC error, and it must be the
// same quote this function makes or the window and the quote would be two
// opinions. See [`bundles_of`].
// The product's room, with the QUEUE past it (round 5.3, UW-4): an order past the room
// of an estate that grows its own set when told is sold and queued there — see
// `crate::grow::can_absorb_queued`.
let absorb = |tenant: &monetize_product::TenantId,
caps: &std::collections::BTreeMap<String, u64>| {
crate::grow::can_absorb_queued(s, product, tenant, caps)
};
let mut quote = s
.transaction(product.with_room(&absorb), monetize::no_vendor())
.quote(&order, &terms, starts, payer)?;
// ★ The oversell ceiling (a-ceiling-s3): the sold-capacity wall the
// signup has, for the order too — the window, the quote and Start all
// come through here, so none of them can sell past it.
// Only an order served from the iron ON HAND is a new promise against it;
// one that BUYS its own iron (`Absorb::WithIron`) brings the room with it.
if quote.serve.is_ok()
&& matches!(
quote.purchase.absorb,
Some(monetize_product::Absorb::OnHand)
)
{
if let Err(over) = crate::oversell::admit_order(s, product, "e.current, &order.caps) {
// ★ Round 5.3 (owner 2026-10-08: "the customer must not know about
// a cap! it must be queued and resized later"): where the set is
// resized later, past the sold-capacity wall is SOLD and QUEUED —
// the estate grows its set for what the purchases queue. Said to
// the operator's log; the customer is offered every size.
// Quiet here: the window quotes every row on every load; the queue
// is said ONCE, when the order is placed (`Purchase.Start`,
// [`queued_past_the_wall`] + `oversell::queue_note`).
if crate::grow::queues_for_resize(s, product) {
return Ok(quote);
}
crate::oversell::say_order_refused(&over.why);
quote.serve = Err(monetize::transaction::Refusal::CannotAbsorb {
tenant: order.tenant.0.clone(),
reason: over.why,
// The room the ceiling still leaves — the wall that refused
// (cov-10): a measured room, never "we could not measure".
shortfall: monetize::transaction::Shortfall {
asked: order.caps.clone(),
servable_bytes: Some(over.room_left_bytes),
},
});
}
}
Ok(quote)
}
/// ★ **Why this order is QUEUED rather than served from the box** (round
/// 5.3): a served quote on the iron on hand that the oversell ceiling would
/// refuse, on a set that is resized later — the operator's reason; `None` for
/// an order the box serves now (or that buys its own iron).
pub(crate) fn queued_past_the_wall(
s: &AppState,
product: monetize_product::Served<'_>,
quote: &Quote,
caps: &std::collections::BTreeMap<String, u64>,
) -> Option<String> {
if quote.serve.is_err()
|| !matches!(
quote.purchase.absorb,
Some(monetize_product::Absorb::OnHand)
)
|| !crate::grow::queues_for_resize(s, product)
{
return None;
}
crate::oversell::admit_order(s, product, "e.current, caps)
.err()
.map(|over| over.why)
}
/// **The metadata key a refusal's CUSTOMER sentence travels under.**
///
/// A gRPC `Status` carries one message, and a refused order has two readers who
/// must not be given the same words: the operator, who needs the machine named
/// (`/data`, the set, `Admin.GrowStart`), and the customer, who must never read
/// any of it (`monetize::transaction::words::NEVER_SHOWN`, owner's ruling
/// 2026-09-14). Until 2026-09-20 `Purchase.Start` had only the message, so the
/// refusal a customer saw after pressing BUY was whichever of the two the
/// server happened to send — and a console that could not tell them apart
/// substituted a sentence of its own. gunnar-ui's was *"down for
/// maintenance"*, about a store that was simply too small.
///
/// So the message stays the operator's, byte for byte, and the customer's
/// sentence rides beside it. A `-bin` key because these sentences are UTF-8 and
/// an em dash is not ASCII.
///
/// **Absence is not a licence to invent.** A reader that does not find this
/// shows the message — see `gunnar_ui_server::grpc::Fail::from`.
pub const CUSTOMER_WORDS_KEY: &str = "monetize-customer-words-bin";
/// One refusal as a `Status`: the operator's sentence in the message, the
/// customer's in [`CUSTOMER_WORDS_KEY`]. The ONE writer, so no door can send
/// one without the other.
pub(crate) fn refused_status(r: &monetize::transaction::Refusal) -> Status {
let mut status = Status::failed_precondition(r.to_string());
status.metadata_mut().insert_bin(
CUSTOMER_WORDS_KEY,
tonic::metadata::MetadataValue::from_bytes(r.customer_words().as_bytes()),
);
status
}
/// The quote's refusals as a `Status`, for the doors that answer one order.
pub(crate) fn quote_status(e: TxError) -> Status {
match e {
TxError::Refused(r) => refused_status(&r),
TxError::WrongProduct { .. } => Status::invalid_argument(e.to_string()),
TxError::Ledger(l) => ledger_err(l),
other => Status::internal(other.to_string()),
}
}
/// **Nothing is sold of a product served free** — `[products.<id>] free = true`: refused by name before
/// anything is priced, reserved or written.
fn refuse_if_free(s: &AppState, product: &str) -> Result<(), Status> {
let free = s
.config
.read()
.map(|c| c.products.get(product).is_some_and(|p| p.free))
.unwrap_or(false);
if free {
return Err(Status::failed_precondition(format!(
"nothing is sold here: {product} is served free on this deployment ([products.{product}] free = true) — no order, no renewal and no users are sold; its owner buys at the shop that sold it"
)));
}
Ok(())
}
/// **Quote a RENEWAL — the one writer for `Quote` and `Start` with `renew`.**
///
/// The two guards are the anti-typo half, and they refuse rather than ignore.
/// A renewal names no caps (they come off the stored fact) and names no start
/// (the period starts where the paid one ends). A caller that sends either is
/// not asking for a renewal — it is an order-shaped request with a flag set,
/// and quietly dropping the fields it sent would charge for something other
/// than what it asked for.
fn quote_renewal(
s: &AppState,
product: monetize_product::Served<'_>,
r_tenant: &str,
caps_named: bool,
starts_named: bool,
market: Market,
payer: Option<String>,
) -> Result<Quote, Status> {
if r_tenant.is_empty() {
return Err(Status::invalid_argument("tenant is required"));
}
if caps_named {
return Err(Status::invalid_argument(
"a renewal names no caps: it sells another period of what the tenant already holds, and the caps come off their stored entitlement. Send caps to place an ORDER, or drop them to renew — a renewal that could be expressed as an order for the caps a tenant happens to hold is one a client sends by mistake",
));
}
if starts_named {
return Err(Status::invalid_argument(
"a renewal names no start: the period starts where the paid one ends (or today, when that is past), so that no day already bought is forfeited and no lapsed day is billed",
));
}
let terms = s.terms.read().unwrap_or_else(|e| e.into_inner()).clone();
let host = terms.host(product.id());
let terms = terms.terms(product.id(), market, &host);
s.transaction(product, monetize::no_vendor())
.renewal(&TenantId(r_tenant.to_owned()), &terms, payer)
.map_err(|e| match e {
// A renewal's refusals are about WHO is renewing, not about capacity,
// and none of them is a quote worth showing: there is no period to
// price. `failed_precondition` with the operator's reason; the
// customer's sentence is `Refusal::customer_words`.
TxError::Refused(r) => refused_status(&r),
TxError::WrongProduct { .. } => Status::invalid_argument(e.to_string()),
TxError::Ledger(l) => ledger_err(l),
other => Status::internal(other.to_string()),
})
}
/// **THE SHOP WINDOW: every bundle this deployment offers this tenant, priced
/// and asked about** — the read behind `Purchase.Bundles`.
///
/// Two halves, and they are deliberately different kinds of thing:
///
/// * **priced, purely** — `monetize::bundle::offer` over `[products.<id>.bundles]`
/// and the market's price list. No ledger, no control plane. A bundle this
/// deployment cannot price is not in the list at all and its refusal goes to
/// the operator's log, because an offer nobody priced is a sale at a price
/// nobody chose;
/// * **asked about, really** — the SAME `quote_order` a typed order goes
/// through, on the caps this tenant would end up with. That is what puts
/// `can_absorb`, the cloud's `can_serve` and the box's ceilings behind
/// `can_serve` here, and it is why a window row and the quote behind the
/// confirm button cannot disagree: there is one quote.
///
/// # It costs one control-plane call per bundle
///
/// MEASURED cost, stated rather than discovered: N bundles is N `can_absorb`
/// calls to ONE product (the request names one). `products.proto` warns that a
/// page asking each PRODUCT directly hangs on the slowest box in the estate;
/// this asks one box N times, with N a number an operator wrote in a config
/// file, and a shop window of four rows is four calls. A window with dozens of
/// rows is a different design problem and would need the answer cached.
///
/// # Silence is never a yes
///
/// A product that cannot be reached, or that does not measure, comes back as
/// `Refusal::CannotAbsorb` — `products/gunnar`'s `absorbs` turns
/// `Capacity::Unanswered` into exactly that — and lands here as
/// `can_serve: false` with `Refusal::customer_words`. The operator's reason
/// (which names the data set, PID 1, a fresh install) goes to the log and never
/// onto this wire.
fn bundles_of(
s: &AppState,
product: monetize_product::Served<'_>,
tenant: &str,
market: Market,
) -> Result<Vec<pb::Bundle>, Status> {
let Some(declared) = s
.bundles
.read()
.unwrap_or_else(|e| e.into_inner())
.get(product.id())
.cloned()
else {
return Ok(Vec::new());
};
let terms = s.terms.read().unwrap_or_else(|e| e.into_inner()).clone();
let host = terms.host(product.id());
let terms = terms.terms(product.id(), market, &host);
let (offers, refused) = monetize::bundle::offers(declared.iter(), product.meters(), &terms);
for r in &refused {
// The operator, every time the window could have shown it. A console is
// not always open and journald is where a deployment's own mistakes are
// read back; the customer sees nothing at all, which is the point.
eprintln!(
"monetize-server: bundle not offered for {}: {}",
product.id(),
r.why
);
}
let current = s
.ledger
.entitlement(product.id(), tenant)
.map_err(ledger_err)?
.map(|f| f.caps)
.unwrap_or_default();
// `offer` already refused every bundle naming a meter this product does not
// declare, so this is the tenant's name and nothing else. Asked anyway,
// once, so the two doors cannot drift apart on what a valid order is.
check_order_args(product, tenant, &std::collections::BTreeMap::new())?;
let mut out = Vec::with_capacity(offers.len());
for o in offers {
let caps = target_of(¤t, &o.delta);
// `starts_unix_ms = 0` is "now", the same default the console's quote
// uses. Nothing is reserved and nothing is written.
let (can_serve, reason) = match quote_order(s, product, tenant, caps, market, 0, None) {
Ok(q) => match &q.serve {
Ok(()) => (true, String::new()),
// The OFFER's words, not an order's refusal: nobody has
// ordered it (owner, test2 2026-10-05).
Err(r) => {
eprintln!(
"monetize-server: bundle {:?} cannot be served for {}/{}: {r}",
o.id,
product.id(),
tenant
);
(false, r.offer_words())
}
},
// A refusal the quote itself raised. It should not be reachable —
// the delta is non-empty and strictly a raise, so neither
// `NotAnIncrease` nor `Downgrade` applies — but a window row that
// turned an unexpected refusal into a *yes* would sell it, so the
// honest answer is the customer's sentence and a loud log line.
Err(TxError::Refused(r)) => {
eprintln!("monetize-server: bundle {:?} was refused while pricing the window for {}/{}: {r}", o.id, product.id(), tenant);
(false, r.offer_words())
}
Err(e) => return Err(quote_status(e)),
};
out.push(pb::Bundle {
id: o.id,
label: o.label,
// WORDS, never minor units: the console may not divide by 100 or
// map a currency enum. Same writer as `QuoteResponse.price_words`.
price: o.price.in_words(),
period: o.period.name().to_owned(),
delta: o.delta,
can_serve,
reason,
// A bundle of a deployment without plans: neither is said.
action: String::new(),
target: Default::default(),
});
}
Ok(out)
}
/// **The caps a bundle order names**: what the tenant holds, with each of the
/// bundle's meters raised by its delta.
///
/// A bundle is "10 GiB MORE", and an [`monetize::Order`] says what the tenant is
/// to be ALLOWED — so the addition happens here, once, and `delta_caps` then
/// computes exactly the bundle's delta back out of it. Saturating, because two
/// caps near `u64::MAX` must ask the product an absurd question and be told no
/// rather than wrap into a small one and be told yes.
fn target_of(
current: &std::collections::BTreeMap<String, u64>,
delta: &std::collections::BTreeMap<String, u64>,
) -> std::collections::BTreeMap<String, u64> {
let raised = delta
.iter()
.map(|(m, d)| {
(
m.clone(),
current.get(m).copied().unwrap_or(0).saturating_add(*d),
)
})
.collect();
monetize::order::target_caps(current, &raised)
}
/// **Resolve `bundle` into the caps a typed order would have named** — the ONE
/// place a bundle becomes an order, for `Quote` and for `Start`.
///
/// After this returns there is no such thing as a bundle: the rest of both
/// doors sees the caps and nothing else, which is why ordering a bundle and
/// ordering the same caps by hand reach the same transaction, the same
/// reference and the same ledger rows. There is no second way to spend money
/// in this server.
///
/// It runs BEFORE the actor ticket is checked, so the ticket is held against
/// the RESOLVED caps. A ticket covering the target caps works for either door
/// and neither can spend the other's — which would not be true if the ticket
/// were checked against an empty cap map on the bundle path.
fn caps_of_bundle(
s: &AppState,
product: monetize_product::Served<'_>,
tenant: &str,
bundle: &str,
caps: &std::collections::BTreeMap<String, u64>,
market: Market,
) -> Result<std::collections::BTreeMap<String, u64>, Status> {
if !caps.is_empty() {
return Err(Status::invalid_argument(
"a request names a bundle OR caps, never both. A bundle IS a set of caps, and a client that sends both is a client that disagrees with itself about what it is buying — one of them would have to win silently",
));
}
if tenant.is_empty() {
return Err(Status::invalid_argument("tenant is required"));
}
let terms = s.terms.read().unwrap_or_else(|e| e.into_inner()).clone();
let host = terms.host(product.id());
let terms = terms.terms(product.id(), market, &host);
let spec = s
.bundles
.read()
.unwrap_or_else(|e| e.into_inner())
.get(product.id())
.and_then(|b| b.get(bundle))
.cloned()
.ok_or_else(|| {
Status::failed_precondition(format!(
"{:?} is not a bundle {} offers; ask Purchase.Bundles for the ones it does",
bundle,
product.id()
))
})?;
// Declared is not offered. The same rule that keeps an unpriceable bundle
// out of the window keeps it from being SOLD through a stale console page,
// and the operator's sentence goes to the log rather than to the customer.
let offer = monetize::bundle::offer(bundle, &spec, product.meters(), &terms).map_err(|e| {
eprintln!(
"monetize-server: bundle {:?} was ordered and is not offered for {}: {}",
bundle,
product.id(),
e.why
);
Status::failed_precondition(format!("{:?} is not on sale here", bundle))
})?;
// An account made since the poller's last tick is adopted first: the raise is resolved
// from the fact its product already holds, as the site's ticket is (held + delta).
crate::poller::adopt_signup(s, product.id(), tenant).map_err(Status::unavailable)?;
let current = s
.ledger
.entitlement(product.id(), tenant)
.map_err(ledger_err)?
.map(|f| f.caps)
.unwrap_or_default();
Ok(target_of(¤t, &offer.delta))
}
/// **What a `Quote`/`Start` asks of a product that sells PLANS**: a row by
/// id, a renewal (prolong a 1-month plan, renew a 12-month one), or a typed
/// size (the 12-month field). A bundle beside caps, or a start date, is
/// refused as on the bundle path.
fn plan_ask<'a>(
renew: bool,
bundle: &'a str,
caps: &'a std::collections::BTreeMap<String, u64>,
starts: u64,
) -> Result<crate::plans::Ask<'a>, Status> {
if starts != 0 {
return Err(Status::invalid_argument(
"a plan names no start: it starts today, or where the paid one ends",
));
}
match (renew, bundle.is_empty(), caps.is_empty()) {
(true, true, true) => Ok(crate::plans::Ask::Renew),
(true, _, _) => Err(Status::invalid_argument(
"a renewal names no bundle and no caps",
)),
(false, false, true) => Ok(crate::plans::Ask::Row(bundle)),
(false, false, false) => Err(Status::invalid_argument(
"a request names a bundle OR caps, never both — one of them would have to win silently",
)),
(false, true, _) => Ok(crate::plans::Ask::Typed(caps)),
}
}
/// **No vendor takes this order's money**: the price in major units with its
/// currency (`110.00 SEK`), never the öre count.
fn no_vendor_words(
price: monetize_pay::Money,
market: Market,
said: &str,
asked: &str,
serving: &[&str],
) -> String {
format!("this order costs {} in {market:?}; {said} (asked: {asked:?}), and no vendor here takes that money; serving: {serving:?}", price.in_words())
}
#[cfg(test)]
mod no_vendor_words_tests {
#[test]
fn the_refusal_names_the_price_in_major_units() {
let w = super::no_vendor_words(
monetize_pay::Money {
minor: 11_000,
currency: monetize_pay::Currency::Sek,
},
monetize_pay::Market::Se,
"no route",
"stripe/x",
&[],
);
assert!(w.contains("costs 110.00 SEK in Se"), "{w}");
assert!(!w.contains("11000"), "{w}");
}
}
/// **Sell a quoted purchase**: the vendor for its price and market, the
/// transaction, and the answer `Start` gives — the tail every door shares.
fn sell(
s: &AppState,
product: monetize_product::Served<'_>,
purchase: &PurchaseReq,
market: Market,
vendor_name: &str,
) -> Result<pb::StartPurchaseResponse, Status> {
let vendor = s
.registry
.vendor_for_tenant(
&purchase.price,
market,
product.id(),
&purchase.tenant.0,
vendor_name,
)
.ok_or_else(|| {
Status::not_found(no_vendor_words(
purchase.price,
market,
&s.registry.route_said(product.id()),
vendor_name,
&s.registry.vendor_names(),
))
})?;
let reference = purchase.reference();
let v = run_and_report(s, product, vendor, purchase)?;
let handle = s
.ledger
.attempt(&reference)
.map_err(ledger_err)?
.map(|a| a.handle);
let redirect_url = match (&handle, v.state) {
(Some(h), PurchaseState::Pending) => redirect_url(s, vendor, h, &reference),
_ => String::new(),
};
let (handle_vendor, handle_id) = handle.map(|h| (h.vendor, h.id)).unwrap_or_default();
Ok(pb::StartPurchaseResponse {
reference: v.reference,
state: v.state as i32,
handle_vendor,
handle_id,
redirect_url,
reason: v.reason,
fact: v.fact.as_ref().map(fact_to_pb),
words: match v.state {
PurchaseState::Paid => purchase.granted_words().to_owned(),
_ => String::new(),
},
users: None,
})
}
/// **Who knocks**, as the door saw it: the calling site (its estate's bearer),
/// the actor ticket the request carried, and the door's own word for the caller.
/// The three travel together into [`check_actor`].
#[derive(Clone, Copy)]
struct Asker<'a> {
site: Option<&'a str>,
ticket: &'a [u8],
caller: Option<&'a Caller>,
}
/// **Who is asking — the ONE gate on the tenant path, for `Quote` and for
/// `Start`.**
///
/// monetize authenticates exactly one caller on its gRPC surface (a shared
/// bearer, and an empty one means open — `crate::auth`), so nothing else in
/// this file can tell the product's console from the product's operator, let
/// alone one browser session from another. An actor ticket is the product's own
/// appliance saying that the human driving this call holds `tenant`, and the
/// PLUGIN checks it: it is the half that holds the appliance's key, and
/// `monetize-embed` (which owns the ticket) depends on `monetize-product`, so
/// the seam carries opaque bytes.
///
/// Three rules, and every one of them is load-bearing:
///
/// * **An empty ticket is the caller the DOOR vouched for, and nobody else.**
/// Through the bearer (or the control signature) it is the operator, as it
/// always was. Through no door at all it is REFUSED: an empty ticket must never mean
/// "the operator" by its own say-so, or every in-process caller is one.
/// * **A ticket with `renew = false` is REFUSED**, not ignored. A ticket
/// authorises one verb; an order is new caps, new money and new iron, and the
/// appliance vouched for none of it. Silently dropping the field would work
/// today and is exactly how a widening sneaks in later.
/// * **It is called before anything else**, so a refusal has nothing to unwind:
/// nothing priced, nothing reserved, no row appended, no vendor dialled.
///
/// A `ProductError` is a refusal too — an appliance that cannot be asked has
/// not said yes — and the two are kept apart on the wire only so an operator
/// can tell a wrong ticket from a plugin that is down.
fn check_actor(
s: &AppState,
product: monetize_product::Served<'_>,
tenant: &str,
purpose: &str,
caps: &std::collections::BTreeMap<String, u64>,
asker: Asker<'_>,
) -> Result<(), Status> {
let Asker {
site,
ticket,
caller,
} = asker;
if ticket.is_empty() {
// No ticket means nobody is claiming to be the tenant, which is what an
// operator ordering on a customer's behalf is — when the bearer says it
// is the operator. The product is not asked, so a deployment with no
// appliance key is not broken by this gate existing.
// ★ S5 (prod audit 2026-10-12): a SITE's own bearer is that site's operator door, as
// the service bearer was the estate monetize's: its console orders on its own
// accounts' behalf (the tenant is already scoped to the site, `scope_tenant`).
if site.is_some_and(|e| !e.trim().is_empty()) {
return Ok(());
}
return match caller {
Some(Caller::Door) => Ok(()),
_ => Err(Status::permission_denied(
"no actor ticket, and this request did not come through the operator's door: an empty ticket is nobody",
)),
};
}
// **The verb the REQUEST is, named by the CALLER, not derived from a
// flag here.** It was a `renew: bool` until `PURPOSE_READ` existed, and a
// bool cannot say three things — the moment there was a third verb the
// mapping would have had to guess which one "not renew" meant. The
// ticket's own word is checked against this inside the product, where the
// signature is; naming it here is what stops any one verb's ticket being
// spent on another.
// A renewal names no caps on the wire, and `quote_renewal` has already
// refused one that did. Passing the order's caps straight through means a
// ticket that covered a different amount is refused by the comparison in
// `verify_ticket` rather than by anything written here.
// ★ ST-3: from a SITE, under that site's own key ([`crate::site_ticket::may_act`]).
match crate::site_ticket::may_act(s, product, site, tenant, purpose, caps, ticket) {
Ok(monetize_product::ActorVerdict::Allowed) => Ok(()),
Ok(monetize_product::ActorVerdict::Refused(why)) => Err(Status::permission_denied(why)),
// The product could not answer. NOT an allow, and not a
// `permission_denied` either: the caller may well be entitled and the
// honest answer is "ask again".
Err(monetize_product::ProductError::Unavailable(why)) => Err(Status::unavailable(format!(
"{} cannot be asked who you are right now, so this request cannot be authorised: {why}",
product.id()
))),
Err(monetize_product::ProductError::Refused(why)) => Err(Status::permission_denied(why)),
}
}
/// **The SITE a call comes from** — its estate's own bearer ([`crate::auth::CallerEstate`]).
fn site_of<T>(req: &Request<T>) -> Option<String> {
req.extensions()
.get::<crate::auth::CallerEstate>()
.map(|e| e.0.clone())
}
/// ★ ST-4: **the request's tenant, as the calling site's account** — `@<estate>/<name>`
/// away from the product's home ([`crate::site_ticket::tenant_on`]); another site's account
/// is refused before anything is read. No calling site: as given.
fn scope_tenant(
s: &AppState,
product: &str,
site: Option<&str>,
tenant: &mut String,
) -> Result<(), Status> {
if tenant.is_empty() {
return Ok(());
}
*tenant = crate::site_ticket::tenant_on(s, product, site, tenant)
.map_err(Status::permission_denied)?;
Ok(())
}
/// **The one sentence a scoped caller gets for a reference that is not its
/// tenant's — and for one that names nothing at all.** The same words both
/// ways, and no reference or owner in them: a refusal that told "exists, not
/// yours" from "does not exist" would be a read-only enumeration of every
/// order on the server, which is what the scope exists to prevent.
pub const NOT_THIS_TENANTS: &str = "this tenant has no purchase by that reference";
/// **Who is asking about ONE order** — `Status`, `Abandon` and `Cancel`.
///
/// Three paths, decided before the ledger is read (see
/// `PurchaseStatusRequest.tenant` in the proto):
///
/// * `tenant` empty — the operator, unscoped, and only through the bearer's
/// door ([`Caller::Door`]). A ticket here is a client that confused two
/// paths: a ticket names a tenant.
/// * `tenant` set, no ticket — the operator (it holds the bearer), scoped.
/// * `tenant` set, a ticket — the tenant, scoped, and the appliance's word for
/// it ([`check_actor`], the same gate `Quote`, `Bundles` and `History` pass).
enum OrderScope {
Operator,
Tenant { product: String, tenant: String },
}
fn order_scope(
s: &AppState,
site: Option<&str>,
product: &str,
tenant: &str,
ticket: &[u8],
purpose: &str,
caller: Option<&Caller>,
) -> Result<OrderScope, Status> {
if tenant.is_empty() {
if !ticket.is_empty() {
return Err(Status::invalid_argument(
"an actor ticket names a tenant, so name it in `tenant` (and `product`) beside the ticket",
));
}
if caller != Some(&Caller::Door) {
return Err(Status::permission_denied(
"an unscoped read of any order is the operator's, through the bearer's door",
));
}
return Ok(OrderScope::Operator);
}
if product.is_empty() {
return Err(Status::invalid_argument(
"`product` is required with `tenant`",
));
}
let p = product_or_not_found(s, product)?;
let mut tenant = tenant.to_owned();
scope_tenant(s, p.id(), site, &mut tenant)?;
// FIRST, before a single key is read out of the ledger: a refusal must cost
// nothing and learn nothing. No caps — none of these verbs buys anything.
check_actor(
s,
p,
&tenant,
purpose,
&std::collections::BTreeMap::new(),
Asker {
site,
ticket,
caller,
},
)?;
Ok(OrderScope::Tenant {
product: product.to_owned(),
tenant,
})
}
impl OrderScope {
/// May this caller see `reference` at all? The owner is read off the
/// ledger's own copy of the purchase (the `Reserved` row) — never off a
/// prefix of the reference, because `team` must not reach `team/sub`.
fn admits(&self, s: &AppState, reference: &str) -> Result<(), Status> {
let OrderScope::Tenant { product, tenant } = self else {
return Ok(());
};
match owner_of(s, reference)? {
Some((p, t)) if p == *product && t == *tenant => Ok(()),
_ => Err(Status::not_found(NOT_THIS_TENANTS)),
}
}
}
/// `(product, tenant)` of the order behind `reference`, or `None` when the
/// ledger has nothing under it.
///
/// An attempt carries its purchase, which is authoritative. A reference with
/// rows and NO attempt was declined at reserve, and its own name is then the
/// only record of whose it was: `<product>/<tenant>/<date>+<caps>`, where the
/// tenant is everything between the FIRST slash and the LAST — an exact
/// equality on that is not a prefix match, so `team/sub` still is not `team`.
fn owner_of(s: &AppState, reference: &str) -> Result<Option<(String, String)>, Status> {
if let Some(a) = s.ledger.attempt(reference).map_err(ledger_err)? {
return Ok(Some((
a.purchase.product.clone(),
a.purchase.tenant.0.clone(),
)));
}
if s.ledger.rows(reference).map_err(ledger_err)?.is_empty() {
return Ok(None);
}
Ok(owner_named_by(reference))
}
/// `(product, tenant)` as a reference spells them.
pub fn owner_named_by(reference: &str) -> Option<(String, String)> {
let (product, rest) = reference.split_once('/')?;
let (tenant, _when) = rest.rsplit_once('/')?;
(!product.is_empty() && !tenant.is_empty()).then(|| (product.to_owned(), tenant.to_owned()))
}
/// **Where an order is, in the customer's terms** — `PurchasePhase`, from the
/// ledger's own attempt. One writer, so `Status` and `Cancel` cannot answer two
/// different words for the same row (and the console derives none).
pub fn phase_of(
attempt: Option<&monetize::ledger::Attempt>,
rows: &[monetize::ledger::Row],
) -> pb::PurchasePhase {
match attempt {
None => match rows.last().map(|r| &r.step) {
Some(monetize::Step::Declined { .. }) => pb::PurchasePhase::Declined,
_ => pb::PurchasePhase::Unspecified,
},
Some(a) => match &a.closed {
None if a.captured.is_some() => pb::PurchasePhase::PaidAdding,
None => pb::PurchasePhase::WaitingForPayment,
Some(Closed::Pushed { .. }) => pb::PurchasePhase::Done,
Some(Closed::Released { .. }) if a.captured.is_some() => pb::PurchasePhase::Refunded,
Some(Closed::Released { .. }) => pb::PurchasePhase::Cancelled,
},
}
}
/// Would `Cancel` end this attempt now — open, and nothing captured as far as
/// the ledger knows (`Cancel` asks the vendor again before it acts).
pub fn cancellable(attempt: Option<&monetize::ledger::Attempt>) -> bool {
attempt.is_some_and(|a| a.is_open() && a.captured.is_none())
}
/// Would `Abandon` end this attempt now — open, and its money captured: the
/// two things `monetize::abandon_attempt` requires.
pub fn abandonable(attempt: Option<&monetize::ledger::Attempt>) -> bool {
attempt.is_some_and(|a| {
a.is_open()
&& matches!(
a.captured,
Some(monetize_pay::ChargeStatus::Captured { .. })
)
})
}
/// **The most attempts one `Purchase.History` answer may carry.**
///
/// A caller names a `limit` and the server obeys it only up to here. An
/// unbounded history is a disk read whoever holds a ticket gets to size, and a
/// page is a page: a hundred attempts is more than a tenant buying every month
/// accumulates in eight years.
pub const HISTORY_CEILING: u32 = 100;
/// **The most of a tenant's references one call will FOLD**, before the answer
/// is clamped. The bound on the read rather than on the reply — the clamp above
/// makes the response small, this makes the work small.
///
/// The tail is kept, not the head: references sort as
/// `<product>/<tenant>/<YYYY-MM-DD>+<caps>`, so lexicographic order is date
/// order, and the last ones are the recent ones. A tenant with more than this
/// many attempts sees their most recent thousand folded and never their first.
const HISTORY_SCAN_CEILING: usize = 1_000;
/// `limit` as the server will honour it: 0 means the ceiling, and anything
/// larger is the ceiling. Pure, so the clamp is provable without a ledger.
pub fn clamp_limit(limit: u32) -> usize {
if limit == 0 {
HISTORY_CEILING as usize
} else {
limit.min(HISTORY_CEILING) as usize
}
}
/// **One tenant's attempts, folded, newest first** — the read behind
/// `Purchase.History`, with the gate already passed.
///
/// # It reuses `Sale::fold`, and there is no second folder
///
/// `monetize::ledger::Sale::fold` is the fold `Ledger.Sales` uses, and it fits
/// here without a change: it is PURE — `(reference, at, rows) -> Option<Sale>`
/// — so the only thing `Sales` has that this does not is where the rows come
/// from. `Sales` discovers references off the time index (recent activity
/// across every tenant); this one takes them off the reference prefix (one
/// tenant's whole history, however old). Same fold, different door, and
/// `PurchaseAttempt` is then a SUBSET of the `Sale` — the customer's own half
/// of it, with `cost`, `fee`, `vendor`, `cloud`, `market` and the tax rate left
/// behind rather than nulled out.
///
/// # Why the tenant is compared again after the scan
///
/// `references_with_prefix("<product>/<tenant>/")` is a STRING prefix, and a
/// gunnar namespace may itself contain a slash (`team/sub`), so the scan for
/// `gunnar/team/` also returns every reference of `gunnar/team/sub`. A prefix is
/// not an equality. The fold reads the tenant off the `Reserved` row's own copy
/// of the purchase — which is the authoritative name, not a substring of a key
/// — and the result is filtered on it exactly.
fn history_of(
s: &AppState,
product: &str,
tenant: &str,
limit: u32,
) -> Result<Vec<pb::PurchaseAttempt>, Status> {
let mut references = s
.ledger
.references_with_prefix(&format!("{product}/{tenant}/"))
.map_err(ledger_err)?;
// The bound on the READ. The scan is ordered, and a reference sorts as
// `<product>/<tenant>/<YYYY-MM-DD>+<caps>`, so the tail is the recent end:
// a tenant with more attempts than this sees their newest and never their
// first, which is the same trade `Ledger::recent_sales` already makes.
if references.len() > HISTORY_SCAN_CEILING {
references.drain(..references.len() - HISTORY_SCAN_CEILING);
}
let mut folded: Vec<monetize::ledger::Sale> = Vec::with_capacity(references.len());
for reference in &references {
let rows = s.ledger.rows(reference).map_err(ledger_err)?;
// `at_unix_ms = 0`: the fold takes the attempt's own last row, which is
// the honest instant here. `Sales` passes the time index's key instead,
// because THERE the ordering key and the row must not disagree.
if let Some(sale) = monetize::ledger::Sale::fold(reference, 0, &rows) {
if sale.tenant == tenant {
folded.push(sale);
}
}
}
// Newest first, by the attempt's last row — the same order `Sales` answers
// in, and the same meaning: a give-back on a year-old purchase floats it up,
// because that is when that purchase last moved.
folded.sort_by_key(|a| std::cmp::Reverse(a.at_unix_ms));
folded.truncate(clamp_limit(limit));
Ok(folded.iter().map(attempt_to_pb).collect())
}
/// **The customer's half of a folded attempt.**
///
/// Field by field, never a blanket conversion from [`monetize::ledger::Sale`]:
/// `Sale` carries `cost`, `fee`, `vendor` and `cloud`, and the first version of
/// gunnar-ui's quote renderer passed a whole message through and put the
/// provider's cost in a browser. What is dropped is dropped HERE, where a new
/// field on `Sale` cannot arrive on the wire by default.
fn attempt_to_pb(s: &monetize::ledger::Sale) -> pb::PurchaseAttempt {
pb::PurchaseAttempt {
reference: s.reference.clone(),
outcome: s.outcome.name().into(),
// **The amount the customer can actually read.** `price` stays for the
// operator console, which formats its own; this is the string a
// customer-facing console renders verbatim because it may not format
// one. One writer, so this and the quote cannot disagree.
price_words: s.price.in_words(),
at_unix_ms: s.at_unix_ms,
started_unix_ms: s.started_unix_ms,
caps: s.caps.clone(),
delta: s.delta.clone(),
// **The customer's own money and nothing beside it.** `price` is what
// they were charged, which `QuoteResponse.price` already answers a
// ticket-gated call with. `cost` is the provider's and `fee` is the
// vendor's; together with `price` either one is a margin, and a margin
// is not the customer's business.
price: Some(money_to_pb(s.price)),
steps: s.steps as u32,
}
}
fn quote_to_pb(q: &Quote) -> pb::QuoteResponse {
let p = &q.purchase;
let (can_serve, reason, words) = match &q.serve {
Ok(()) => (true, String::new(), String::new()),
Err(r) => (false, r.to_string(), r.customer_words().to_owned()),
};
pb::QuoteResponse {
reference: p.reference(),
price: Some(money_to_pb(p.price)),
// The same string `attempt_to_pb` puts on a history row, from the same
// writer — so what a customer is quoted and what they later see they
// were charged are formatted once, not twice.
price_words: p.price.in_words(),
cost: Some(money_to_pb(p.cost)),
current: q.current.clone(),
caps: p.caps.clone(),
delta: p.delta.clone(),
resources: p.resources.iter().map(resource_to_pb).collect(),
can_serve,
reason,
period: p.policy.period.name().into(),
words,
}
}
#[tonic::async_trait]
impl Purchase for PurchaseService {
/// What the order would come to. Pure: nothing reserved, nothing written.
/// A downgrade or a no-op is `failed_precondition` — there is no quote for
/// nothing — while a refusal by the product, the cloud or a ceiling is a
/// quote with `can_serve = false` and the reason, because the price of what
/// was refused is still what the operator wants to see.
async fn quote(
&self,
req: Request<pb::QuoteRequest>,
) -> Result<Response<pb::QuoteResponse>, Status> {
let caller = req.extensions().get::<Caller>().cloned();
let site = site_of(&req);
let r = req.into_inner();
blocking(&self.0, move |s| {
let site = site.as_deref();
let mut r = r;
let product = product_or_not_found(s, &r.product)?;
scope_tenant(s, product.id(), site, &mut r.tenant)?;
let market = market_of(s, r.market);
// ★ A product that sells PLANS (`[pricing.<id>]`): the row, the
// ticket, then the rule's price off the cached basis.
if let Some(cfg) = s.plans.config(product.id()) {
let sel = crate::plans::select(s, &cfg, product.id(), &r.tenant, plan_ask(r.renew, &r.bundle, &r.caps, r.starts_unix_ms)?)?;
check_actor(s, product, &r.tenant, sel.purpose, &sel.ticket_caps, Asker { site, ticket: &r.actor_ticket, caller: caller.as_ref() })?;
let q = crate::plans::quote(s, product, &cfg, &sel, market, None, false)?;
return Ok(Response::new(quote_to_pb(&q)));
}
// **A BUNDLE BECOMES CAPS HERE, and then there is no bundle.**
// Everything below sees the caps a typed order would have named, so
// the two doors share the quote, the reference and the ledger rows.
//
// It reads the tenant's standing caps before the ticket is checked,
// which is deliberate and is not a widening: nothing is priced,
// written, reserved or dialled, and no byte of it leaves this
// function unless the ticket then passes. The alternative — check
// the ticket against an EMPTY cap map on this path — would let one
// bundle's ticket buy another's.
let caps = if r.bundle.is_empty() {
r.caps.clone()
} else {
if r.renew {
return Err(Status::invalid_argument(
"a renewal names no bundle: it sells another period of what the tenant already holds, and a \
bundle is a RAISE. Send the bundle to order more room, or drop it to renew",
));
}
caps_of_bundle(s, product, &r.tenant, &r.bundle, &r.caps, market)?
};
// Before the price is computed and before the product's control
// plane is touched: a quote names a tenant's standing caps and what
// they cost, which is not a thing a stranger may read.
check_actor(s, product, &r.tenant, if r.renew { monetize_embed::PURPOSE_RENEW } else { monetize_embed::PURPOSE_ORDER }, &caps, Asker { site, ticket: &r.actor_ticket, caller: caller.as_ref() })?;
let q = if r.renew {
quote_renewal(s, product, &r.tenant, !r.caps.is_empty(), r.starts_unix_ms != 0, market, None)?
} else {
{
check_order_args(product, &r.tenant, &caps)?;
quote_order(s, product, &r.tenant, caps, market, r.starts_unix_ms, None).map_err(quote_status)?
}
};
Ok(Response::new(quote_to_pb(&q)))
})
.await
}
/// **What this tenant can buy, priced.** Empty on a deployment that
/// declared no bundles — not an error: the console falls back to the typed
/// order, which is untouched by any of this.
async fn bundles(
&self,
req: Request<pb::ListBundlesRequest>,
) -> Result<Response<pb::ListBundlesResponse>, Status> {
let caller = req.extensions().get::<Caller>().cloned();
let site = site_of(&req);
let r = req.into_inner();
let me = Arc::clone(&self.0);
blocking(&self.0, move |s| {
let site = site.as_deref();
let mut r = r;
let product = product_or_not_found(s, &r.product)?;
scope_tenant(s, product.id(), site, &mut r.tenant)?;
if r.tenant.is_empty() {
return Err(Status::invalid_argument("tenant is required"));
}
// The same gate `Quote` has, and for the same reason: every price
// below is on the delta over what THIS tenant holds, so an ungated
// window would read a customer's standing caps back out of a price.
// `PURPOSE_READ` and no caps — this call buys nothing and must not
// be able to spend an order ticket.
check_actor(
s,
product,
&r.tenant,
monetize_embed::PURPOSE_READ,
&std::collections::BTreeMap::new(),
Asker {
site,
ticket: &r.actor_ticket,
caller: caller.as_ref(),
},
)?;
let market = market_of(s, r.market);
// ★ A product that sells PLANS: the rule's rows and the plan's gauge.
if let Some(cfg) = s.plans.config(product.id()) {
return crate::plans::window(&me, product, &cfg, &r.tenant, market)
.map(Response::new);
}
Ok(Response::new(pb::ListBundlesResponse {
bundles: bundles_of(s, product, &r.tenant, market)?,
reading: false,
plan: None,
}))
})
.await
}
/// **Sell an order.** The same quote `Quote` makes, then the refusals it
/// found, then the vendor for the price — nobody at price 0, the named one,
/// or the only one serving the market — then `buy`.
async fn start(
&self,
req: Request<pb::StartPurchaseRequest>,
) -> Result<Response<pb::StartPurchaseResponse>, Status> {
let caller = req.extensions().get::<Caller>().cloned();
let site = site_of(&req);
let r = req.into_inner();
blocking(&self.0, move |s| {
let site = site.as_deref();
let mut r = r;
let product = product_or_not_found(s, &r.product)?;
scope_tenant(s, product.id(), site, &mut r.tenant)?;
// ★ A product served FREE sells nothing (wiring tracer B 2026-10-08): a sold estate's own
// monetize (`[products.<id>] free = true`, fed as `plugin.<id>.free`) grants no order, no
// renewal and no users — its owner buys at the shop that sold it.
refuse_if_free(s, product.id())?;
let market = market_of(s, r.market);
// **"Add users"** is its own resolution into an order (or into no
// order at all, when the unused room covers it).
if let Some(users) = &r.users {
return start_users(s, product, &r, users, market, caller.as_ref(), site).map(Response::new);
}
// ★ A product that sells PLANS (`[pricing.<id>]`, owner 2026-10-05):
// the row is resolved, the ticket checked against it, and the
// provider's price read NOW — a price under the floor at it is
// refused before anything moves. Then the same transaction.
if let Some(cfg) = s.plans.config(product.id()) {
let sel = crate::plans::select(s, &cfg, product.id(), &r.tenant, plan_ask(r.renew, &r.bundle, &r.caps, r.starts_unix_ms)?)?;
check_actor(s, product, &r.tenant, sel.purpose, &sel.ticket_caps, Asker { site, ticket: &r.actor_ticket, caller: caller.as_ref() })?;
let payer = if r.payer.is_empty() { s.signups.email_of(product.id(), &r.tenant).ok().flatten() } else { Some(r.payer.clone()) };
let quote = crate::plans::quote(s, product, &cfg, &sel, market, payer, true)?;
return sell(s, product, "e.purchase, market, &r.vendor).map(Response::new);
}
// **A BUNDLE BECOMES CAPS HERE, and then there is no bundle.**
// Everything below sees the caps a typed order would have named, so
// the two doors share the quote, the reference and the ledger rows.
//
// It reads the tenant's standing caps before the ticket is checked,
// which is deliberate and is not a widening: nothing is priced,
// written, reserved or dialled, and no byte of it leaves this
// function unless the ticket then passes. The alternative — check
// the ticket against an EMPTY cap map on this path — would let one
// bundle's ticket buy another's.
let caps = if r.bundle.is_empty() {
r.caps.clone()
} else {
if r.renew {
return Err(Status::invalid_argument(
"a renewal names no bundle: it sells another period of what the tenant already holds, and a \
bundle is a RAISE. Send the bundle to order more room, or drop it to renew",
));
}
caps_of_bundle(s, product, &r.tenant, &r.bundle, &r.caps, market)?
};
// FIRST, before the idempotency read, before the quote, before the
// vendor: a refusal here must leave the ledger exactly as it found
// it. See `check_actor`.
check_actor(s, product, &r.tenant, if r.renew { monetize_embed::PURPOSE_RENEW } else { monetize_embed::PURPOSE_ORDER }, &caps, Asker { site, ticket: &r.actor_ticket, caller: caller.as_ref() })?;
// **Who pays, when the caller named nobody**: the address the
// tenant signed up with, so the vendor's page does not ask a
// logged-in customer for it again (owner 2026-09-28: Stripe asked
// for "E-postadress" on test2). A store error is not a refusal: the
// page then asks, as it always did.
let payer = if r.payer.is_empty() { s.signups.email_of(product.id(), &r.tenant).ok().flatten() } else { Some(r.payer.clone()) };
// A retry of an order already filled: the quote would compute the
// delta against caps the tenant now HOLDS and refuse it as a no-op,
// but the reference it names is the attempt that filled it, and
// that is the answer — the console that asks twice is the one
// whose first request timed out after the money moved.
//
// A RENEWAL needs none of this: its quote never refuses, so the
// request reaches `buy`, which short-circuits a filled reference on
// its own.
if !r.renew && !r.tenant.is_empty() {
let starts = if r.starts_unix_ms == 0 { s.now() } else { r.starts_unix_ms };
let order = Order { product: product.id().into(), tenant: TenantId(r.tenant.clone()), caps: caps.clone() };
if let Some(filled) = s.transaction(product, monetize::no_vendor()).already_filled(&order, starts).map_err(|e| Status::internal(e.to_string()))? {
let Outcome::Paid { reference, fact } = filled else { unreachable!("already_filled answers Paid or nothing") };
return Ok(Response::new(pb::StartPurchaseResponse {
reference,
state: PurchaseState::Paid as i32,
handle_vendor: String::new(),
handle_id: String::new(),
redirect_url: String::new(),
reason: String::new(),
fact: Some(fact_to_pb(&fact)),
// An order that was already filled is still a granted
// order, and the customer asking again is usually the
// one who did not see the first answer. This branch is
// guarded by `!r.renew`, so it is an order by
// construction and never a renewal.
words: monetize::transaction::words::ORDER_GRANTED.to_owned(),
users: None,
}));
}
}
let quote = if r.renew {
quote_renewal(s, product, &r.tenant, !r.caps.is_empty(), r.starts_unix_ms != 0, market, payer)?
} else {
{
check_order_args(product, &r.tenant, &caps)?;
quote_order(s, product, &r.tenant, caps.clone(), market, r.starts_unix_ms, payer).map_err(quote_status)?
}
};
if let Err(refusal) = "e.serve {
// The operator's reason to the log as well as to the caller: a
// capacity refusal is something an operator has to act on (enlarge
// a set, extend a filesystem), and the console is not always open.
eprintln!("monetize-server: purchase refused for {}/{}: {refusal}", product.id(), r.tenant);
// **THE REFUSAL A CUSTOMER MEETS AFTER PRESSING BUY.** The
// window said this could be served and by the time the button
// was pressed it could not — the store filled, a floor moved,
// the appliance answered differently. That person has just
// committed to a charge, so the one thing they must not be
// handed is a sentence with the reason taken out of it.
// `refused_status` sends the operator's words AND the
// customer's; the console picks the one for its reader.
return Err(refused_status(refusal));
}
let purchase = "e.purchase;
let vendor = s.registry.vendor_for_tenant(&purchase.price, market, product.id(), &purchase.tenant.0, &r.vendor).ok_or_else(|| {
Status::not_found(no_vendor_words(purchase.price, market, &s.registry.route_said(product.id()), &r.vendor, &s.registry.vendor_names()))
})?;
let reference = purchase.reference();
// ★ Round 5.3: an order past the room is QUEUED — said once, as INFO,
// with its reference and the reason (never a refusal's WARN).
if !r.renew {
if let Some(why) = queued_past_the_wall(s, product, "e, &caps) {
if let Some(line) = crate::oversell::queue_note(&reference, &format!("{}/{}", product.id(), r.tenant), &why) {
eprintln!("{line}");
}
}
// ★ UW-4: past the room of an estate that grows its own set when told.
if let Some(line) = crate::grow::queue_past_room(s, product, &purchase.tenant, &caps) {
eprintln!("{line}");
}
}
let v = run_and_report(s, product, vendor, purchase)?;
let handle = s.ledger.attempt(&reference).map_err(ledger_err)?.map(|a| a.handle);
// Only while the customer still has somewhere to go: a PAID or
// DECLINED attempt with a checkout link on it is an invitation to
// pay twice.
let redirect_url = match (&handle, v.state) {
(Some(h), PurchaseState::Pending) => redirect_url(s, vendor, h, &reference),
_ => String::new(),
};
let (handle_vendor, handle_id) = handle.map(|h| (h.vendor, h.id)).unwrap_or_default();
Ok(Response::new(pb::StartPurchaseResponse {
reference: v.reference,
state: v.state as i32,
handle_vendor,
handle_id,
redirect_url,
reason: v.reason,
fact: v.fact.as_ref().map(fact_to_pb),
// **Only when it was actually granted.** A PENDING attempt has
// not raised anything yet — the customer is still at the
// vendor — and a sentence saying the room is already theirs
// would be the one untruth this field exists to prevent.
words: match v.state {
PurchaseState::Paid => purchase.granted_words().to_owned(),
_ => String::new(),
},
users: None,
}))
})
.await
}
async fn status(
&self,
req: Request<pb::PurchaseStatusRequest>,
) -> Result<Response<pb::PurchaseStatusResponse>, Status> {
let caller = req.extensions().get::<Caller>().cloned();
let site = site_of(&req);
let r = req.into_inner();
blocking(&self.0, move |s| {
let site = site.as_deref();
// **Whose order, first** — before the vendor is asked on anybody's
// behalf and before a row is read. A `read` ticket: this buys
// nothing and must not be able to spend another verb's.
order_scope(s, site, &r.product, &r.tenant, &r.actor_ticket, monetize_embed::PURPOSE_READ, caller.as_ref())?.admits(s, &r.reference)?;
// **Somebody is watching this order: ask the vendor now**, at most
// every `ASK_VENDOR_EVERY` per reference, instead of leaving it to
// the webhook or the poller's next pass. MEASURED on the KVM mock
// 2026-09-28 (functest): a paid order whose webhook never came was
// captured by the poller alone, 300 s after the payment — and for
// those five minutes the customer back from the vendor's page was
// shown `pending`, with the pay button. The same reconcile the
// poller runs (`resume_reference`); a failure here is said and the
// read goes on with what the ledger holds.
if ask_vendor_due(&r.reference) {
match resume_reference(s, &r.reference) {
Ok(Some(v)) if v.state != PurchaseState::Pending => eprintln!("monetize-server: status {}: the vendor was asked while the customer watched: {:?}", r.reference, v.state),
Ok(_) => {}
Err(e) => eprintln!("monetize-server: status {}: asking the vendor: {}", r.reference, e.message()),
}
}
let rows = s.ledger.rows(&r.reference).map_err(ledger_err)?;
let attempt = s.ledger.attempt(&r.reference).map_err(ledger_err)?;
let (state, reason, fact) = match (&attempt, rows.last()) {
(None, None) => (PurchaseState::Unknown, String::new(), None),
(None, Some(last)) => match &last.step {
monetize::Step::Declined { reason, .. } => (PurchaseState::Declined, reason.clone(), None),
_ => (PurchaseState::Unknown, String::new(), None),
},
(Some(a), _) => match &a.closed {
None => (PurchaseState::Pending, if a.captured.is_some() { "captured, entitlement not yet pushed".into() } else { "reserved, awaiting the vendor".into() }, None),
Some(Closed::Released { reason }) => (PurchaseState::Released, reason.clone(), None),
Some(Closed::Pushed { fact }) => (PurchaseState::Paid, String::new(), Some(fact.clone())),
},
};
// The link, still live, for an attempt that is still open. Asked of
// the vendor on every read (see the proto): a session that has
// expired at the vendor must stop being offered, and a vendor that
// is no longer linked into this server has nothing to say.
let redirect = match (&attempt, state) {
(Some(a), PurchaseState::Pending) => s
.registry
.find_vendor(&a.handle.vendor)
.map(|v| redirect_url(s, v, &a.handle, &r.reference))
.unwrap_or_default(),
_ => String::new(),
};
// "Add users": the grant (retried here if the paid order's grant
// did not land), or the one waiting for its order; and, for a paid
// order, where the room it bought stands in the grow queue.
let product_id = r.reference.split('/').next().unwrap_or_default();
let free_grant = if attempt.is_none() { users_granted_row(s, &r.reference).map_err(ledger_err)? } else { None };
let (users, room) = if let Some(granted) = free_grant.clone() {
// Granted from unused room: there is nothing to enlarge.
(Some(granted), Some(pb::RoomProgress { state: "done".into(), words: monetize::transaction::progress::USERS_FROM_UNUSED.into() }))
} else {
let users = match (&attempt, state) {
(Some(a), PurchaseState::Paid) if a.purchase.users.is_some() => match s.registry.find_product(product_id) {
Some(p) => grant_paid_users(s, p, &r.reference)?,
None => None,
},
(Some(a), _) => a.purchase.users.as_ref().map(|ask| {
// A checkout that ended unpaid adds nobody: its names go.
if matches!(a.closed, Some(Closed::Released { .. })) {
forget_names(s, &r.reference);
}
pb::UsersGrant {
room_bytes: ask.room_bytes,
need_bytes: ask.room_bytes.saturating_mul(u64::from(ask.count.max(1))),
granted: false,
..Default::default()
}
}),
_ => None,
};
let room = (state == PurchaseState::Paid).then(|| room_progress(s, product_id, &r.reference));
(users, room)
};
let state = if free_grant.is_some() { PurchaseState::Paid } else { state };
Ok(Response::new(pb::PurchaseStatusResponse {
reference: r.reference,
state: state as i32,
reason,
fact: fact.as_ref().map(fact_to_pb),
rows: rows.iter().map(row_to_pb).collect(),
redirect_url: redirect,
users,
room,
cancellable: cancellable(attempt.as_ref()),
abandonable: abandonable(attempt.as_ref()),
phase: if free_grant.is_some() { pb::PurchasePhase::Done } else { phase_of(attempt.as_ref(), &rows) } as i32,
}))
})
.await
}
/// **One tenant's own purchase history, folded.** See the proto for why it
/// lives on `Purchase` and not on `Ledger`, and [`history_of`] for the read.
async fn history(
&self,
req: Request<pb::PurchaseHistoryRequest>,
) -> Result<Response<pb::PurchaseHistoryResponse>, Status> {
let caller = req.extensions().get::<Caller>().cloned();
let site = site_of(&req);
let r = req.into_inner();
blocking(&self.0, move |s| {
let site = site.as_deref();
let mut r = r;
let product = product_or_not_found(s, &r.product)?;
scope_tenant(s, product.id(), site, &mut r.tenant)?;
// FIRST, before a single key is read out of the ledger. A history
// names what a tenant bought and for how much; a refusal must cost
// nothing and must learn nothing.
//
// **`PURPOSE_READ`, and it names no caps.** A read buys nothing,
// so there is no amount for its ticket to cover.
//
// This was a RENEWAL ticket for one commit — strictly narrower than
// what that ticket authorises, so nothing could be done with it
// that could not have been done anyway. It was still wrong: a
// ticket is single-use, so listing a history burned the one the
// customer would have renewed with, and the appliance was vouching
// for a verb the caller was not performing. The same `check_actor`
// is reused verbatim, so the operator path, the refusal words and
// the "before anything" ordering are one piece of code on all four
// calls.
check_actor(
s,
product,
&r.tenant,
monetize_embed::PURPOSE_READ,
&std::collections::BTreeMap::new(),
Asker {
site,
ticket: &r.actor_ticket,
caller: caller.as_ref(),
},
)?;
if r.tenant.is_empty() {
return Err(Status::invalid_argument("tenant is required"));
}
Ok(Response::new(pb::PurchaseHistoryResponse {
attempts: history_of(s, product.id(), &r.tenant, r.limit)?,
}))
})
.await
}
/// Refund what was drawn, give back what was bought, close the attempt. See
/// the proto for why this exists and why it is not idempotent.
async fn abandon(
&self,
req: Request<pb::AbandonPurchaseRequest>,
) -> Result<Response<pb::AbandonPurchaseResponse>, Status> {
let caller = req.extensions().get::<Caller>().cloned();
let site = site_of(&req);
let r = req.into_inner();
blocking(&self.0, move |s| {
let site = site.as_deref();
if r.reason.trim().is_empty() {
return Err(Status::invalid_argument("abandon needs a reason: a refund nobody can account for is not one worth making"));
}
// **Whose order** — a `cancel` ticket on the tenant path: ending
// one's own open order, which for captured money is this refund.
order_scope(s, site, &r.product, &r.tenant, &r.actor_ticket, monetize_embed::PURPOSE_CANCEL, caller.as_ref())?.admits(s, &r.reference)?;
let attempt = s
.ledger
.attempt(&r.reference)
.map_err(ledger_err)?
.ok_or_else(|| Status::not_found(format!("no attempt was ever made for {}", r.reference)))?;
let vendor = s.registry.find_vendor(&attempt.handle.vendor).ok_or_else(|| {
Status::failed_precondition(format!(
"{} was reserved at {:?}, which is not linked into this server — the refund must be made where the money was taken",
r.reference, attempt.handle.vendor
))
})?;
let out = monetize::abandon_attempt(vendor.payment(), s.registry.cloud_for_transactions(), &s.ledger, s.now(), &r.reference, &r.reason)
.map_err(|e| Status::failed_precondition(e.to_string()))?;
skidbladnir::functional_status(
"monetize-server/purchase",
"abandon",
true,
&format!("{} refunded and closed, {} resource(s) released: {}", r.reference, out.destroyed.len(), r.reason),
);
Ok(Response::new(pb::AbandonPurchaseResponse {
reference: r.reference,
released: out.destroyed.iter().map(|id| format!("{}:{}", id.provider, id.id)).collect(),
}))
})
.await
}
/// **Cancel an order nobody has paid for** — see the proto, and
/// [`monetize::Transaction::cancel_reference`] for why the vendor is asked
/// first and a payment always wins.
async fn cancel(
&self,
req: Request<pb::CancelPurchaseRequest>,
) -> Result<Response<pb::CancelPurchaseResponse>, Status> {
let caller = req.extensions().get::<Caller>().cloned();
let site = site_of(&req);
let r = req.into_inner();
blocking(&self.0, move |s| {
let site = site.as_deref();
if r.reason.trim().is_empty() {
return Err(Status::invalid_argument("cancel needs a reason: it is written into the ledger's Released row"));
}
order_scope(s, site, &r.product, &r.tenant, &r.actor_ticket, monetize_embed::PURPOSE_CANCEL, caller.as_ref())?.admits(s, &r.reference)?;
let attempt = s
.ledger
.attempt(&r.reference)
.map_err(ledger_err)?
.ok_or_else(|| Status::not_found(format!("no order was ever placed under {}", r.reference)))?;
let product = product_or_not_found(s, &attempt.purchase.product)?;
let vendor = s.registry.find_vendor(&attempt.handle.vendor).ok_or_else(|| {
Status::failed_precondition(format!(
"{} was reserved at {:?}, which is not linked into this server — the hold must be let go where it was taken",
r.reference, attempt.handle.vendor
))
})?;
// **One closer per reference at a time**, the same flight a webhook,
// the poller and a watching `Status` take: a cancel racing the grant
// of the same order must not write a close beside it.
let flight = s
.resuming
.fly(&r.reference)
.ok_or_else(|| Status::unavailable(format!("{}; ask again in a moment", IN_FLIGHT)))?;
let done = s.transaction(product, vendor).cancel_reference(&r.reference, &r.reason);
drop(flight);
let (already_closed, state) = match done {
Ok(monetize::Cancelled::Closed(o)) if matches!(*o, Outcome::ReleasedWithOrphans { .. }) => (false, PurchaseState::ReleasedWithOrphans),
Ok(monetize::Cancelled::Closed(_)) => (false, PurchaseState::Released),
Ok(monetize::Cancelled::AlreadyClosed) => (true, PurchaseState::Released),
Err(monetize::CancelError::Paid { .. }) => {
// The capture is written down; finish the order as paid now
// rather than at the poller's next pass. A failure here is
// said and changes nothing about the answer.
if let Err(e) = resume_reference(s, &r.reference) {
eprintln!("monetize-server: cancel {}: finishing the paid order: {}", r.reference, e.message());
}
return Err(Status::failed_precondition(format!(
"{} has been paid, so it cannot be cancelled: it finishes as paid, and ending it now is a refund (Abandon)",
r.reference
)));
}
Err(e @ monetize::CancelError::Granted { .. }) => return Err(Status::failed_precondition(e.to_string())),
Err(e @ monetize::CancelError::NoAttempt { .. }) => return Err(Status::not_found(e.to_string())),
Err(monetize::CancelError::Tx(e)) => return Err(Status::unavailable(e.to_string())),
};
let rows = s.ledger.rows(&r.reference).map_err(ledger_err)?;
let attempt = s.ledger.attempt(&r.reference).map_err(ledger_err)?;
let reason = match attempt.as_ref().and_then(|a| a.closed.as_ref()) {
Some(Closed::Released { reason }) => reason.clone(),
_ => String::new(),
};
let released = rows
.iter()
.filter_map(|row| match &row.step {
monetize::Step::Destroyed { id, .. } => Some(format!("{}:{}", id.provider, id.id)),
_ => None,
})
.collect();
if !already_closed {
skidbladnir::functional_status("monetize-server/purchase", "cancel", true, &format!("{} cancelled: {}", r.reference, r.reason));
}
Ok(Response::new(pb::CancelPurchaseResponse {
reference: r.reference,
already_closed,
state: state as i32,
phase: phase_of(attempt.as_ref(), &rows) as i32,
reason,
released,
}))
})
.await
}
}
#[cfg(test)]
mod market_tests {
/// ★ An order that names DK is priced in DK (DKK, 25 %) — never silently in
/// the deployment's default market (owner 2026-09-28: holger sells in DK).
#[test]
fn an_order_naming_dk_is_a_danish_order() {
use monetize_proto as pb;
assert_eq!(
super::market_of_wire(pb::Market::Dk as i32),
Some(monetize_pay::Market::Dk)
);
assert_eq!(
super::market_of_wire(pb::Market::Fi as i32),
Some(monetize_pay::Market::Fi)
);
assert_eq!(
super::market_of_wire(pb::Market::Unspecified as i32),
None,
"unnamed: the default applies"
);
}
}
#[cfg(test)]
mod site_operator_tests {
//! ★ prod audit 2026-10-12 (S5): **a site's own console orders for its own account**.
//! Through the ONE box the console presents its estate's bearer, which marks the call
//! [`crate::auth::CallerEstate`] and never [`Caller::Door`]; the console's "more room"
//! Quote/Start/Bundles carry no actor ticket (an operator ordering on a tenant's behalf,
//! gunnar-ui `api::increase`), so every one was PERMISSION_DENIED live. The site's bearer is
//! that site's operator door — scoped to the site's own accounts by `scope_tenant` — while
//! an empty ticket through no door at all is still nobody.
use monetize_proto as pb;
use monetize_proto::purchase_server::Purchase as _;
use tonic::Request;
use super::*;
fn state() -> Arc<AppState> {
Arc::new(AppState::in_memory(
crate::registry::Registry::new().product(monetize::testing::MockProduct::gunnar()),
))
}
fn quote(tenant: &str) -> pb::QuoteRequest {
pb::QuoteRequest {
product: "gunnar".into(),
tenant: tenant.into(),
caps: [("pack_bytes".to_string(), 10u64 << 30)].into(),
..Default::default()
}
}
#[tokio::test]
async fn a_sites_console_quotes_for_its_own_account_with_no_ticket() {
let s = PurchaseService(state());
let mut req = Request::new(quote("alice"));
req.extensions_mut()
.insert(crate::auth::CallerEstate("test2-gunnar-rs".into()));
if let Err(e) = s.quote(req).await {
assert_ne!(
e.code(),
tonic::Code::PermissionDenied,
"the site's own console is its operator: {e:?}"
);
}
let nobody = s
.quote(Request::new(quote("alice")))
.await
.expect_err("an empty ticket through no door is nobody");
assert_eq!(nobody.code(), tonic::Code::PermissionDenied, "{nobody:?}");
let mut other = Request::new(quote("@other-site/alice"));
other
.extensions_mut()
.insert(crate::auth::CallerEstate("test2-gunnar-rs".into()));
assert_eq!(
s.quote(other)
.await
.expect_err("another site's account")
.code(),
tonic::Code::PermissionDenied
);
}
}