use rustlavel::prelude::*;
use crate::Caller;
pub const READ: &str = "orders.read";
pub const WRITE: &str = "orders.write";
pub fn routes(r: &mut Router) {
r.get("/api/orders", index);
r.post("/api/orders", store);
}
fn caller(req: &Request, scope: &str) -> std::result::Result<Caller, Response> {
let Some(caller) = req.extension::<Caller>() else {
return Err(Response::new(Status::UNAUTHORIZED)
.with_json(crate::problem("Unauthenticated.")));
};
if !caller.can(scope) {
return Err(Response::new(Status::FORBIDDEN)
.with_json(crate::problem(&format!("This token does not carry `{scope}`."))));
}
Ok(caller.clone())
}
async fn index(req: Request) -> Result<Response> {
let caller = match caller(&req, READ) {
Ok(caller) => caller,
Err(response) => return Ok(response),
};
let db = req.state::<Database>().expect("the database is registered in main.rs");
let rows = db
.table("orders")
.filter("subject", caller.owner())
.order_by("id", rustlavel::db::Direction::Desc)
.limit(50)
.get(db)
.await?;
Ok(Response::json(Json::object([(
"data",
Json::Array(rows.iter().map(as_json).collect()),
)])))
}
async fn store(mut req: Request) -> Result<Response> {
let caller = match caller(&req, WRITE) {
Ok(caller) => caller,
Err(response) => return Ok(response),
};
let body = req.json().cloned().unwrap_or(Json::Null);
let description = body.get("description").and_then(Json::as_str).unwrap_or("").trim().to_string();
let currency = body.get("currency").and_then(Json::as_str).unwrap_or("IDR").trim().to_uppercase();
let amount = match body.get("amount_minor").and_then(Json::as_f64) {
Some(amount) if amount.fract() == 0.0 && amount > 0.0 => amount as i64,
_ => {
return Ok(Response::new(Status::UNPROCESSABLE).with_json(crate::problem(
"`amount_minor` must be a whole number of minor units, above zero.",
)));
}
};
if description.is_empty() {
return Ok(Response::new(Status::UNPROCESSABLE)
.with_json(crate::problem("`description` is required.")));
}
let reference = match req.header("idempotency-key").map(str::trim).filter(|key| !key.is_empty()) {
Some(key) => format!("{}:{key}", caller.owner()),
None => format!("{}:{}", caller.owner(), rustlavel::auth::random::hex(12)),
};
let db = req.state::<Database>().expect("the database is registered in main.rs").clone();
if let Some(existing) =
db.table("orders").filter("reference", reference.as_str()).first(&db).await?
{
return Ok(Response::new(Status::OK).with_json(as_json(&existing)));
}
db.table("orders")
.insert(
&db,
&[
("subject", caller.owner().into()),
("reference", reference.as_str().into()),
("description", description.as_str().into()),
("amount_minor", amount.into()),
("currency", currency.as_str().into()),
("status", "recorded".into()),
],
)
.await?;
let stored = db
.table("orders")
.filter("reference", reference.as_str())
.first(&db)
.await?
.ok_or_else(|| Error::msg("the order was inserted and could not be read back"))?;
Ok(Response::new(Status::CREATED).with_json(as_json(&stored)))
}
fn as_json(row: &rustlavel::db::Row) -> Json {
let text = |column: &str| row.get::<String>(column).map_or(Json::Null, Json::from);
let number = |column: &str| row.get::<i64>(column).map_or(Json::Null, Json::from);
Json::object([
("id", number("id")),
("reference", text("reference")),
("description", text("description")),
("amount_minor", number("amount_minor")),
("currency", text("currency")),
("status", text("status")),
])
}