formulate 1.2.0

formulate is a standalone server that listens for web form data submissions.
use chrono::{Local, TimeZone};
use either::Either;

mod forms;
use forms::FormSubmission;

mod mailer;
use mailer::{default_subject_line, send_email, OptionalFields};

mod mail_template;

use rocket::{
    form::Form,
    http::Status,
    response::{status::BadRequest, Redirect},
    serde::json::Json,
    {get, launch, post, routes},
};

mod spam;
mod strings;
use strings::{SUCCESS_MSG, VERSION, WELCOME_MSG};

mod stripe_orders;
use stripe_orders::find_order_info;

use validator::Validate;

mod webhooks;
use webhooks::{StripeData, StripeEvent};

type FormResponse = Either<(Status, &'static str), Redirect>;
type FormData<'b> = Either<&'b Form<FormSubmission<'b>>, &'b Json<FormSubmission<'b>>>;

fn process_form_data(form_data: FormData) -> Result<(), BadRequest<String>> {
    let (
        validated,
        is_spam,
        email,
        full_name,
        subject,
        message,
        from_site,
        last_name,
        company_name,
        phone_number,
    ) = match form_data {
        Either::Left(form_data) => (
            form_data.validate(),
            form_data.check_if_spam(),
            form_data.email.trim(),
            form_data.full_name,
            form_data.subject,
            form_data.message,
            form_data.from_site,
            form_data.last_name,
            form_data.company_name,
            form_data.phone_number,
        ),
        Either::Right(form_data) => (
            form_data.validate(),
            form_data.check_if_spam(),
            form_data.email.trim(),
            form_data.full_name,
            form_data.subject,
            form_data.message,
            form_data.from_site,
            form_data.last_name,
            form_data.company_name,
            form_data.phone_number,
        ),
    };

    if let Err(error) = validated {
        Err(BadRequest(error.to_string()))
    } else {
        is_spam?;

        let optional_fields = OptionalFields {
            last_name,
            company_name,
            phone_number,
            message_summary: None,
            cta_link: None,
            logo_img: None,
            ordered_items: None,
        };

        send_email(
            email,
            full_name,
            subject,
            message,
            from_site,
            optional_fields,
        )
        .map_err(|err| BadRequest(err.to_string()))
    }
}

fn process_webhook_data(webhook_data: Json<StripeEvent>) -> Result<(), BadRequest<String>> {
    let (
        order_total,
        order_created,
        order_currency,
        order_id,
        order_type,
        order_paid,
        order_receipt_url,
        order_status,
        customer_name,
        customer_email,
        statement_desc,
    ) = match &webhook_data.data {
        StripeData::StripeCharge { object } => (
            object.amount,
            object.created,
            object.currency,
            object.id,
            webhook_data.event_type,
            object.paid,
            object.receipt_url,
            object.status,
            object.billing_details.name,
            object.billing_details.email,
            object.calculated_statement_descriptor,
        ),
        StripeData::StripeCheckout { object } => (
            object.amount_total,
            object.created,
            object.currency,
            object.id,
            webhook_data.event_type,
            object.payment_status == "paid",
            None,
            object.status,
            object.customer_details.name,
            object.customer_details.email,
            None,
        ),
    };

    if order_paid {
        let items = if order_type == "checkout.session.completed" {
            find_order_info(order_id)?
        } else {
            // If the event type is not a checkout session, ignore it for now.
            // TODO: Decide if we will send emails to process other types.
            return Ok(());
        };

        let currency = if order_currency == "usd" { "$" } else { "" };
        let order_msg = if let Some(date) = Local.timestamp_opt(order_created, 0).single() {
            format!(
                "{customer_name} placed an order on {date} for {currency}{}. Order status was {order_status}.",
                (order_total as f32 / 100.0)
            )
        } else {
            format!(
                "{customer_name} placed an order for {currency}{}. Order status was {order_status}.",
                (order_total as f32 / 100.0)
            )
        };

        let optional_fields = OptionalFields {
            company_name: None,
            last_name: None,
            phone_number: None,
            message_summary: Some("A new Stripe order has been successfully created!"),
            cta_link: order_receipt_url,
            ordered_items: Some(&items),
            logo_img: if statement_desc.is_some() && statement_desc.unwrap() == "SIMPLY SOURDOUGH" {
                // Some("https://simplysourdough.org/assets/logo.png")
                None
            } else {
                None
            },
        };
        send_email(
            customer_email,
            customer_name,
            optional_fields.message_summary.unwrap_or("New message"),
            &order_msg,
            "Stripe",
            optional_fields,
        )
        .map_err(|err| BadRequest(err.to_string()))
    } else {
        Err(BadRequest("Payment did not go through".to_string()))
    }
}

#[get("/")]
fn index() -> String {
    format!("{WELCOME_MSG}\nv{VERSION}")
}

#[post("/", data = "<form>")]
fn submit(form: Form<FormSubmission>) -> Result<FormResponse, BadRequest<String>> {
    process_form_data(Either::Left(&form)).map(|_| {
        if let Some(redirect) = form.redirect {
            if redirect {
                return Either::Right(Redirect::to(form.from_site.to_string()));
            }
        }
        Either::Left((Status::Ok, SUCCESS_MSG))
    })
}

#[post("/", format = "json", data = "<form>", rank = 2)]
fn submit_json(form: Json<FormSubmission>) -> Result<(Status, &'static str), BadRequest<String>> {
    process_form_data(Either::Right(&form)).map(|_| (Status::Ok, SUCCESS_MSG))
}

#[post("/webhook", format = "json", data = "<hooks>")]
fn submit_webhook(hooks: Json<StripeEvent>) -> Result<(Status, &'static str), BadRequest<String>> {
    process_webhook_data(hooks).map(|_| (Status::Ok, "Order data processed successfully."))
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index, submit, submit_json, submit_webhook])
}

#[cfg(test)]
mod tests;