tovuk 0.1.114

Use Tovuk scraper APIs from a native CLI.
use super::super::{
    args::CliOptions,
    auth::read_or_login_token,
    constants::BILLING_CHECKOUT_ROUTE,
    errors::{Result, agent_error, print_json},
    utils::open_url,
};
use super::common::joined_args;
use super::http::api_request;
use reqwest::Method;
use serde_json::{Value, json};

pub(crate) fn billing_command(cli: &CliOptions) -> Result<()> {
    let token = read_or_login_token(cli)?;
    let action = cli.args.first().map_or("checkout", String::as_str);
    let route = match action {
        "" | "checkout" => BILLING_CHECKOUT_ROUTE,
        "portal" => "/v1/billing/portal",
        _ => {
            return Err(agent_error(
                "unknown_billing_command",
                "Unknown billing command.",
                "Use `tovuk billing checkout plus --json`, `tovuk billing checkout pro --json`, `tovuk billing checkout max --json`, or `tovuk billing portal`.",
                cli.output.json,
            ));
        }
    };
    let body = if route == BILLING_CHECKOUT_ROUTE {
        Some(billing_checkout_body(cli)?)
    } else {
        None
    };
    let response = api_request(cli, Method::POST, route, Some(&token), body)?;
    if cli.output.json {
        return print_json(&response);
    }
    let url = billing_session_url(&response, cli.output.json)?;
    println!("{url}");
    open_url(url);
    Ok(())
}

fn billing_checkout_body(cli: &CliOptions) -> Result<Value> {
    if !cli.top_up_usd_cents.is_empty() {
        return billing_top_up_checkout_body(cli);
    }
    let target_plan = billing_checkout_plan(cli)?;
    let reason = joined_args(cli, 2);
    Ok(json!({
        "target_plan": target_plan,
        "reason": if reason.is_empty() {
            format!("Open Tovuk {target_plan} checkout.")
        } else {
            reason
        },
    }))
}

fn billing_top_up_checkout_body(cli: &CliOptions) -> Result<Value> {
    if matches!(
        cli.args.get(1).map(String::as_str),
        Some("plus" | "pro" | "max")
    ) {
        return Err(agent_error(
            "billing_checkout_target_conflict",
            "Billing checkout cannot include both a plan and top-up amount.",
            "Use `tovuk billing checkout plus --json` for a plan or `tovuk billing checkout --top-up-usd-cents 2000 --json` for balance.",
            cli.output.json,
        ));
    }
    let top_up_usd_cents = cli.top_up_usd_cents.parse::<u32>().map_err(|_error| {
        agent_error(
            "billing_top_up_invalid",
            "Billing top-up amount must be an integer number of USD cents.",
            "Use `tovuk billing checkout --top-up-usd-cents 2000 --json` for the minimum $20 top-up.",
            cli.output.json,
        )
    })?;
    let reason = joined_args(cli, 1);
    Ok(json!({
        "top_up_usd_cents": top_up_usd_cents,
        "reason": if reason.is_empty() {
            format!("Open Tovuk ${:.2} balance top-up checkout.", f64::from(top_up_usd_cents) / 100.0)
        } else {
            reason
        },
    }))
}

fn billing_checkout_plan(cli: &CliOptions) -> Result<&str> {
    match cli.args.get(1).map(String::as_str) {
        Some(plan @ ("plus" | "pro" | "max")) => Ok(plan),
        Some(_) => Err(agent_error(
            "billing_plan_invalid",
            "Billing plan must be plus, pro, or max.",
            "Use `tovuk billing checkout plus --json`, `tovuk billing checkout pro --json`, or `tovuk billing checkout max --json`.",
            cli.output.json,
        )),
        None => Err(agent_error(
            "billing_plan_required",
            "Billing plan is required.",
            "Use `tovuk billing checkout plus --json`, `tovuk billing checkout pro --json`, or `tovuk billing checkout max --json`.",
            cli.output.json,
        )),
    }
}

fn billing_session_url(response: &Value, json_output: bool) -> Result<&str> {
    if let Some(url) = response
        .get("checkout")
        .and_then(|checkout| checkout.get("url"))
        .and_then(Value::as_str)
        .filter(|url| !url.trim().is_empty())
    {
        return Ok(url);
    }

    Err(agent_error(
        "billing_url_missing",
        "Tovuk billing did not return a URL.",
        "Retry `tovuk billing checkout plus --json`, `tovuk billing checkout pro --json`, `tovuk billing checkout max --json`, or `tovuk billing portal --json`. If it keeps failing, create a Tovuk support ticket with command output.",
        json_output,
    ))
}

#[cfg(test)]
#[path = "billing_tests.rs"]
mod tests;