use std::io::Read; use net::State; use iron::prelude::*; use iron::response::HttpResponse; use iron::status; use persistent::Read as Readable; use uuid::Uuid; use postgres::transaction::Transaction; use failure::Error; use failure::err_msg; use stripe::{Event, EventObject, CheckoutSession, SubscriptionStatus}; use net::{MnmlHttpError}; use pg::{PgPool}; use account; pub fn subscription_account(tx: &mut Transaction, sub: String) -> Result { let query = " SELECT account FROM stripe_subscriptions WHERE subscription = $1; "; let result = tx .query(query, &[&sub])?; let row = result.iter().next() .ok_or(err_msg("user not subscribed"))?; Ok(row.get(0)) } // we use i64 because it is converted to BIGINT for pg // and we can losslessly pull it into u32 which is big // enough for the ballers const CREDITS_COST_CENTS: i64 = 10; const CREDITS_SUB_BONUS: i64 = 40; // Because the client_reference_id (account.id) is only included // in the stripe CheckoutSession object // we ensure that we store each object in pg with a link to the object // and to the account id in case of refunds #[derive(Debug,Clone,Serialize,Deserialize)] enum StripeData { Customer { account: Uuid, customer: String, checkout: String }, Subscription { account: Uuid, customer: String, checkout: String, subscription: String, }, // i64 used because it converts to psql BIGINT // expecting a similar system to be used for eth amounts Purchase { account: Uuid, amount: i64, customer: String, checkout: String }, } impl StripeData { fn insert(&self, tx: &mut Transaction) -> Result<&StripeData, Error> { match self { StripeData::Customer { account, customer, checkout } => { tx.execute(" INSERT into stripe_customers (account, customer, checkout) VALUES ($1, $2, $3); ", &[&account, &customer, &checkout])?; info!("new stripe customer {:?}", self); Ok(self) }, StripeData::Subscription { account, customer, checkout, subscription } => { tx.execute(" INSERT into stripe_subscriptions (account, customer, checkout, subscription) VALUES ($1, $2, $3, $4); ", &[&account, &customer, &checkout, &subscription])?; info!("new stripe subscription {:?}", self); Ok(self) }, StripeData::Purchase { account, amount, customer, checkout } => { tx.execute(" INSERT into stripe_purchases (account, customer, checkout, amount) VALUES ($1, $2, $3, $4); ", &[&account, &customer, &checkout, amount])?; info!("new stripe purchase {:?}", self); Ok(self) }, } } fn side_effects(&self, tx: &mut Transaction) -> Result<&StripeData, Error> { match self { // when we get a subscription we just immediately set the user to be subbed // so we don't have to deal with going to fetch all the details from // stripe just to double check // update webhooks will tell us when the subscription changes // see EventObject::Subscription handler below StripeData::Subscription { subscription: _, account, customer: _, checkout: _ } => { account::credit(tx, *account, CREDITS_SUB_BONUS)?; account::set_subscribed(tx, *account, true)?; Ok(self) }, StripeData::Purchase { account, customer: _, amount, checkout: _ } => { let credits = amount .checked_div(CREDITS_COST_CENTS) .expect("credits cost 0"); account::credit(tx, *account, credits)?; Ok(self) }, _ => Ok(self), } } } fn stripe_checkout_data(session: CheckoutSession) -> Result, Error> { let account = match session.client_reference_id { Some(ref a) => Uuid::parse_str(a)?, None => { error!("unknown user checkout {:?}", session); return Err(err_msg("NoUser")) }, }; let mut items = vec![]; let customer = session.customer.ok_or(err_msg("UnknownCustomer"))?; let checkout = session.id; items.push(StripeData::Customer { account, customer: customer.id().to_string(), checkout: checkout.to_string() }); if let Some(sub) = session.subscription { items.push(StripeData::Subscription { account, customer: customer.id().to_string(), checkout: checkout.to_string(), subscription: sub.id().to_string() }); } for item in session.display_items.into_iter() { let amount = item.amount.ok_or(err_msg("NoPricePurchase"))? as i64; items.push(StripeData::Purchase { account, amount, customer: customer.id().to_string(), checkout: checkout.to_string() }); }; return Ok(items); } fn process_stripe_event(event: Event, pool: &PgPool) -> Result { info!("stripe event {:?}", event); let connection = pool.get()?; let mut tx = connection.transaction()?; match event.data.object { EventObject::CheckoutSession(s) => { let data = match stripe_checkout_data(s) { Ok(data) => data, Err(e) => { error!("{:?}", e); return Err(e); } }; for item in data.iter() { item.insert(&mut tx)? .side_effects(&mut tx)?; } }, // we only receive the cancelled and updated events // because the checkout object is needed to link // a sub to an account initially and // stripe doesn't guarantee the order // so this just checks if the sub is still active EventObject::Subscription(s) => { let account = subscription_account(&mut tx, s.id.to_string())?; let subbed = match s.status { SubscriptionStatus::Active => true, _ => false, }; account::set_subscribed(&mut tx, account, subbed)?; } _ => { error!("unhandled stripe event {:?}", event); return Err(err_msg("UnhanldedEvent")); }, }; tx.commit()?; Ok(event.id.to_string()) } pub fn stripe(req: &mut Request) -> IronResult { let state = req.get::>().unwrap(); let event = match req.get::>() { Ok(Some(b)) => b, _ => return Err(IronError::from(MnmlHttpError::BadRequest)), }; match process_stripe_event(event, &state.pool) { Ok(id)=> { info!("event processed successfully {:?}", id); Ok(Response::with(status::Ok)) } Err(e) => { error!("{:?}", e); Err(IronError::from(MnmlHttpError::ServerError)) } } } #[cfg(test)] mod tests { use super::*; use std::fs::File; #[test] fn test_stripe_checkout_sku() { let mut f = File::open("./test/checkout.session.completed.purchase.json").expect("couldn't open file"); let mut checkout_str = String::new(); f.read_to_string(&mut checkout_str) .expect("unable to read file"); let event: Event = serde_json::from_str(&checkout_str) .expect("could not deserialize"); let data = match event.data.object { EventObject::CheckoutSession(s) => stripe_checkout_data(s).expect("purchase error"), _ => panic!("unknown event obj"), }; assert!(data.iter().any(|d| match d { StripeData::Customer { account: _, customer: _, checkout: _ } => true, _ => false, })); assert!(data.iter().any(|d| match d { StripeData::Purchase { account: _, amount: _, customer: _, checkout: _ } => true, _ => false, })); } #[test] fn test_stripe_checkout_subscription() { let mut f = File::open("./test/checkout.session.completed.subscription.json").expect("couldn't open file"); let mut checkout_str = String::new(); f.read_to_string(&mut checkout_str) .expect("unable to read file"); let event: Event = serde_json::from_str(&checkout_str) .expect("could not deserialize"); let data = match event.data.object { EventObject::CheckoutSession(s) => stripe_checkout_data(s).expect("subscription error"), _ => panic!("unknown event obj"), }; assert!(data.iter().any(|d| match d { StripeData::Customer { account: _, customer: _, checkout: _ } => true, _ => false, })); assert!(data.iter().any(|d| match d { StripeData::Purchase { account: _, amount: _, customer: _, checkout: _ } => true, _ => false, })); assert!(data.iter().any(|d| match d { StripeData::Subscription { account: _, customer: _, checkout: _, subscription: _, } => true, _ => false, })); } }