crates/monetize-server/src/services/estates.rs
//! **`Estates`** — the console's Overview, classified here ([`crate::estate_order::standing`]),
//! and the New-estate slot ([`crate::estate_order::new_estate`]). Thin: the job decides; this
//! checks the operator, maps the wire, and waits for the box to take a site the slot added.
use std::sync::Arc;
use monetize_proto as pb;
use monetize_proto::estates_server::Estates;
use tonic::{Request, Response, Status};
use crate::estate_order::new_estate::{self, NewRefusal, Prepared};
use crate::estate_order::{self, standing};
use crate::services::blocking;
use crate::site_book::SiteBookRefusal;
use crate::state::AppState;
pub struct EstatesService(pub Arc<AppState>);
/// The operator's alone: the operator marker, never a site's own bearer.
fn operator_only<T>(req: &Request<T>) -> bool {
req.extensions().get::<crate::auth::Operator>().is_some()
&& req
.extensions()
.get::<crate::auth::CallerEstate>()
.is_none()
}
fn refused(r: NewRefusal) -> Status {
match r {
NewRefusal::Bad(w) => Status::invalid_argument(w),
NewRefusal::Taken(w) => Status::already_exists(w),
NewRefusal::NotHere(w) => Status::failed_precondition(w),
}
}
fn book_refused(r: SiteBookRefusal) -> Status {
let w = format!("the site book did not keep it: {r}");
match r {
SiteBookRefusal::Bad(_) => Status::invalid_argument(w),
SiteBookRefusal::Taken(_) => Status::already_exists(w),
SiteBookRefusal::NotFound(_) => Status::not_found(w),
SiteBookRefusal::Refused(_) => Status::failed_precondition(w),
SiteBookRefusal::Broken(_) => Status::internal(w),
}
}
/// Pure: one line on the wire.
pub fn line_pb(l: &standing::EstateLine) -> pb::EstateStanding {
pb::EstateStanding {
name: l.name.clone(),
product: l.product.clone(),
standing: l.standing.to_string(),
words: l.words.clone(),
in_fleet: l.in_fleet,
domain: l.domain.clone(),
}
}
/// Pure: the Overview on the wire.
pub fn overview_pb(o: &standing::Overview) -> pb::EstatesOverviewResponse {
pb::EstatesOverviewResponse {
estates: o.estates.iter().map(line_pb).collect(),
ended: o.ended.iter().map(line_pb).collect(),
slot: Some(pb::NewEstateSlot {
products: o.products.clone(),
declared: o.declared.clone(),
}),
}
}
/// How often the slot looks whether the box took a site it added.
const TAKEN_RELOOK: std::time::Duration = std::time::Duration::from_millis(250);
#[tonic::async_trait]
impl Estates for EstatesService {
async fn overview(
&self,
_req: Request<pb::EstatesOverviewRequest>,
) -> Result<Response<pb::EstatesOverviewResponse>, Status> {
blocking(&self.0, |s| {
Ok(Response::new(overview_pb(&standing::overview(s))))
})
.await
}
async fn new_estate(
&self,
req: Request<pb::NewEstateRequest>,
) -> Result<Response<pb::NewEstateResponse>, Status> {
if !operator_only(&req) {
return Err(Status::permission_denied("a new estate is the operator's: the operator bearer — never a site's own, never the service bearer"));
}
let r = req.into_inner();
let name = r.name.trim().to_string();
let ask = r.clone();
let prepared = blocking(&self.0, move |s| {
new_estate::prepare(s, &ask.name, &ask.product, &ask.email, &ask.actor).map_err(refused)
})
.await?;
let (created, domain, template) = match prepared {
Prepared::Declared { domain } => (false, domain, String::new()),
Prepared::Create { put, template } => {
let domain = put.domain.clone();
blocking(&self.0, move |s| {
let book = s.registry.linked().site_book.clone().ok_or_else(|| {
Status::failed_precondition(format!(
"REFUSED [new-estate-no-book] {}",
crate::site_book::NO_BOOK
))
})?;
book.0.put(&put).map_err(book_refused)
})
.await?;
eprintln!("monetize-server: new estate {name}: added to the site book as {domain} (copied from {template})");
// The box takes it on its next look (PID 1, then the config watcher): the order
// needs it declared. Past a short look a queued job owns the order (OVW-5): the
// call answers at once, and the job orders it once the box took it.
let t0 = std::time::Instant::now();
while !new_estate::taken(&self.0, &name) {
if t0.elapsed() >= new_estate::TAKEN_AT_ONCE {
let late = new_estate::LateOrder {
name: name.clone(),
email: r.email.trim().to_string(),
size: r.size.trim().to_string(),
return_url: r.return_url.trim().to_string(),
};
new_estate::queue_late_order(&self.0, &late)
.map_err(Status::unavailable)?;
eprintln!("monetize-server: new estate {name}: the box has not taken it yet; a queued job orders it once it does");
let words = new_estate::late_words(&name, &domain, &template);
return Ok(Response::new(pb::NewEstateResponse {
estate: name,
domain,
created: true,
reference: String::new(),
checkout_url: String::new(),
price_words: String::new(),
words,
}));
}
tokio::time::sleep(TAKEN_RELOOK).await;
}
(true, domain, template)
}
};
let (n, email, size, back) = (
name.clone(),
r.email.trim().to_string(),
r.size.trim().to_string(),
r.return_url.trim().to_string(),
);
let run = blocking(&self.0, move |s| {
let sizes = estate_order::site::sizes_of_on(s, Some(&n), &size)
.map_err(Status::invalid_argument)?;
estate_order::site::start_site_back_to(s, &n, &email, &sizes, "", &back)
.map_err(crate::services::estate_order::site_refused)
})
.await?;
estate_order::nudge(&self.0).await;
let made = if created {
format!("{name} is made as {domain} (its admin, account, terms and media as {template}'s) and ordered")
} else {
format!("{name} is ordered")
};
let words = format!(
"{made}: pay {} at the payment page, and this box lays it",
run.amount.in_words()
);
Ok(Response::new(pb::NewEstateResponse {
estate: name,
domain,
created,
reference: run.reference,
checkout_url: run.checkout_url,
price_words: run.amount.in_words(),
words,
}))
}
}