formulate 1.2.0

formulate is a standalone server that listens for web form data submissions.
use base64::{engine::general_purpose::STANDARD, Engine};
use rocket::{figment::providers::Env, response::status::BadRequest, serde::Deserialize, Config};
use std::sync::LazyLock;

const STRIPE_API: &str = "https://api.stripe.com";
const CHECKOUT_ENDPOINT: &str = "/v1/checkout/sessions";

// Does not pick up config changes.
static STRIPE_CONFIG: LazyLock<rocket::figment::Figment> = LazyLock::new(|| {
    Config::figment()
        .select("stripe")
        .merge(Env::prefixed("FORMULATE_STRIPE_"))
});

/// Configuration struct for Stripe API access
#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
struct StripeConfig {
    /// Stripe secret key.
    secret_key: Option<String>,
}

/// Line item info for a Stripe Checkout order
#[derive(Debug, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct StripeCheckoutLineItem {
    #[serde(alias = "id")]
    pub _id: String,
    pub currency: String,
    pub amount_total: u32,
    pub amount_subtotal: u32,
    pub description: String,
    pub quantity: u32,
}

/// List of line items details for a Stripe Checkout order
#[derive(Debug, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct StripeCheckoutLineItems {
    pub data: Vec<StripeCheckoutLineItem>,
}

impl StripeCheckoutLineItems {
    /// Gets a list of all the line items from a checkout order
    pub fn get_line_items(&self) -> Vec<(&String, u32, &String, u32, u32)> {
        self.data
            .iter()
            .map(|item| {
                (
                    &item.description,
                    item.quantity,
                    &item.currency,
                    item.amount_subtotal,
                    item.amount_total,
                )
            })
            .collect()
    }
}

fn get_basic_auth_header(user: &str, pass: &str) -> String {
    let creds = String::from(user) + ":" + pass;
    String::from("Basic ") + &STANDARD.encode(creds.as_bytes())
}

/// Find the product line items that correspong to a checkout session
pub fn find_order_info(line_item_id: &str) -> Result<StripeCheckoutLineItems, BadRequest<String>> {
    let config: StripeConfig = match STRIPE_CONFIG.extract::<StripeConfig>() {
        Ok(config) => config,
        Err(err) => return Err(BadRequest(err.to_string())),
    };

    if let Some(secret_key) = config.secret_key {
        let api_url = format!("{STRIPE_API}{CHECKOUT_ENDPOINT}/{line_item_id}/line_items");
        let result = match ureq::get(&api_url)
            .set("Authorization", &get_basic_auth_header(&secret_key, ""))
            .call()
        {
            Err(ureq::Error::Status(code, _)) => {
                return if code == 401 {
                    Err(BadRequest(
                        "Error fetching product info. Try setting the correct secret key."
                            .to_owned(),
                    ))
                } else {
                    Err(BadRequest("Error fetching product info.".to_owned()))
                }
            }
            Err(err) => return Err(BadRequest(format!("Error fetching product info: {}", err))),
            Ok(res) => res,
        };

        match result.into_json::<StripeCheckoutLineItems>() {
            Err(err) => Err(BadRequest(format!(
                "Encountered error while parsing product info from JSON: {}",
                err
            ))),
            Ok(res) => Ok(res),
        }
    } else {
        Err(BadRequest(
            "Stripe secret key needs to be configured in order to find product information."
                .to_owned(),
        ))
    }
}