Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

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,
        }))
    }
}