276 lines
9.1 KiB
Rust
276 lines
9.1 KiB
Rust
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<Uuid, Error> {
|
|
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<Vec<StripeData>, 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<String, Error> {
|
|
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<Response> {
|
|
let state = req.get::<Readable<State>>().unwrap();
|
|
let event = match req.get::<bodyparser::Struct<Event>>() {
|
|
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,
|
|
}));
|
|
}
|
|
} |