mod mail;
mod order_info;
mod products;
mod stripe;
#[cfg(feature = "database")]
mod db;
#[cfg(feature = "database")]
mod schema;
use mail::generate_email::{send_email, AdditionalFields};
use order_info::create::{CheckoutCustomerCreation, CheckoutLineItem, CreateCheckoutSession};
use order_info::retrieve::find_order_info;
use products::ProductsConfig;
use rocket::serde::{json::Json, Deserialize};
use rocket::{fairing::AdHoc, figment::providers::Env, Config, State};
use rocket::{http::Status, response::status::BadRequest};
use rocket::{get, launch, post, routes};
use stripe::checkout::{StripeCheckoutSession, StripeMode, StripeObjectType};
use stripe::customer::StripeCustomerDetails;
use stripe::event::StripeEventData;
use validator::Validate;
const VERSION: &str = env!("CARGO_PKG_VERSION");
fn process_webhook_data(
webhook_data: &StripeEventData,
app_config: &State<AppConfig>,
) -> Result<(), BadRequest<String>> {
let (
order_id,
order_type,
order_created,
order_currency,
order_paid,
order_status,
_order_mode,
order_total,
_order_subtotal,
customer_details,
receipt_url,
statement_desc,
) = match &webhook_data {
StripeEventData::StripeCheckoutObject { object } => (
&object.id,
&object.object_type,
&object.created,
&object.currency,
object.is_successful(),
&object.status,
&object.mode,
object.amount_total,
object.amount_subtotal,
&object.customer_details,
None::<&str>,
None::<&str>,
),
};
let customer = if let Some(customer) = customer_details.as_ref() {
customer
} else {
eprintln!("No customer information provided with checkout session.");
&StripeCustomerDetails {
email: "invalid@email.missing".to_string(),
name: "Unknown Customer".to_string(),
}
};
if order_paid {
let line_items = match order_type {
StripeObjectType::CheckoutSession => find_order_info(&order_id, &app_config.stripe)?,
};
let order_msg = format!(
"New order recieved.\nStatus: {order_status}.\n\n{} placed an order on {order_created} for {order_currency}{}.",
customer.name,
(order_total as f32 / 100.0)
);
let additional_fields = AdditionalFields {
message_summary: "A new Stripe order has been successfully created!",
cta_link: receipt_url,
ordered_items: Some(&line_items),
logo_img: if statement_desc.is_some() && statement_desc.unwrap() == "SIMPLY SOURDOUGH" {
None
} else {
None
},
};
let name = customer
.name
.split(',')
.collect::<Vec<&str>>()
.iter()
.rev()
.map(|s| s.trim())
.collect::<Vec<&str>>()
.join(" ");
send_email(
&customer.email,
&name,
additional_fields.message_summary,
&order_msg,
"Stripe",
additional_fields,
&app_config.email,
)
.map_err(|err| BadRequest(err.to_string()))
} else {
Err(BadRequest("Payment did not go through".to_string()))
}
}
#[get("/")]
fn index() -> String {
let desc = "cor·re·late, verb | ˈkôrəˌlāt | to have a mutual relationship or connection, in which one thing affects or depends on another.";
format!("{}\n{}", desc, VERSION)
}
#[post("/", format = "json", data = "<payload>")]
fn webhook_event(
payload: Json<stripe::event::StripeEvent>,
app_config: &State<AppConfig>,
) -> Result<(Status, &'static str), BadRequest<String>> {
if let Err(errors) = payload.validate() {
Err(BadRequest(errors.to_string()))
} else {
process_webhook_data(&payload.data, app_config)
.map(|_| (Status::Ok, "Order data processed successfully."))
}
}
#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
struct CheckoutOrderSubmission<'o> {
#[serde(alias = "coupon")]
pub _coupon: Option<&'o str>,
#[serde(alias = "successUrl")]
success_url: &'o str,
#[serde(alias = "submissionUrl")]
submission_url: &'o str,
items: Vec<CheckoutOrderItem<'o>>,
}
#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
struct CheckoutOrderItem<'o> {
name: &'o str,
sku: &'o str,
quantity: u32,
}
#[post("/checkout", data = "<payload>")]
fn create_checkout(
payload: Json<CheckoutOrderSubmission>,
app_config: &State<AppConfig>,
) -> Result<(Status, String), BadRequest<String>> {
let mut checkout_items = Vec::new();
for item in &payload.items {
let price = match app_config.products.get_price(&item.name, &item.sku) {
Some(price) => price.to_string(),
None => {
return Err(BadRequest("Failed to find price for item".to_owned()));
}
};
checkout_items.push(CheckoutLineItem::new(price, item.quantity));
}
let checkout_data = CreateCheckoutSession::new(
payload.success_url.to_string(),
payload.submission_url.to_string(),
CheckoutCustomerCreation::Always,
StripeMode::Payment,
checkout_items,
);
let config = app_config
.stripe
.as_ref()
.ok_or_else(|| BadRequest("Stripe configuration is missing".to_string()))?;
match checkout_data.send_to_stripe(&config.secret) {
Ok(mut response) => {
let checkout_session = response
.body_mut()
.read_json::<StripeCheckoutSession>()
.map_err(|err| BadRequest(err.to_string()))?;
println!("Created checkout session: {}", &checkout_session.id);
Ok((Status::Ok, checkout_session.url))
}
Err(err) => {
let error_message = format!("Failed to create checkout session: {}", err);
eprintln!("{}", error_message);
Err(BadRequest(error_message))
}
}
}
#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
struct AppConfig {
email: MailConfig,
stripe: Option<StripeConfig>,
#[serde(flatten)]
products: ProductsConfig,
}
#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
struct StripeConfig {
secret: String,
}
#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
struct MailConfig {
sender: String,
recipient: String,
#[serde(alias = "MAIL_HOST")]
smtp_relay_host: Option<String>,
#[serde(alias = "MAIL_PORT")]
#[serde(default = "default_smtp_port")]
smtp_relay_port: u16,
smtp_username: Option<String>,
smtp_password: Option<String>,
}
fn default_smtp_port() -> u16 {
25
}
#[launch]
fn rocket() -> _ {
let figment = Config::figment().merge(Env::prefixed("CORRELATE_").split("_"));
let rocket = rocket::custom(figment)
.mount("/", routes![index, create_checkout, webhook_event])
.attach(AdHoc::config::<AppConfig>());
#[cfg(feature = "database")]
let rocket = rocket.attach(db::stage());
rocket
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod integration_tests;