lila/modules/plan/src/main/PlanApi.scala

261 lines
10 KiB
Scala
Raw Normal View History

2016-07-12 11:19:30 -06:00
package lila.plan
2016-06-06 03:36:21 -06:00
2016-06-06 08:41:04 -06:00
import lila.db.dsl._
import lila.user.{ User, UserRepo }
2016-06-06 03:36:21 -06:00
2016-06-06 08:41:04 -06:00
import org.joda.time.DateTime
2016-06-06 03:36:21 -06:00
2016-07-12 11:19:30 -06:00
final class PlanApi(
stripeClient: StripeClient,
patronColl: Coll,
2016-07-12 10:33:22 -06:00
chargeColl: Coll,
2016-07-10 14:12:22 -06:00
bus: lila.common.Bus) {
2016-06-06 08:41:04 -06:00
2016-07-12 10:58:39 -06:00
import BsonHandlers._
import PatronHandlers._
import ChargeHandlers._
2016-06-06 08:41:04 -06:00
def checkout(userOption: Option[User], data: Checkout): Funit =
2016-07-13 08:14:20 -06:00
getOrMakePlan(data.cents) flatMap { plan =>
userOption.fold(setAnonPlan(plan, data, renew = data.isMonthly)) { user =>
setUserPlan(user, plan, data, renew = data.isMonthly)
2016-07-13 08:46:25 -06:00
}
} void
2016-07-13 08:14:20 -06:00
def switch(user: User, cents: Cents): Fu[StripeSubscription] =
2016-06-06 08:41:04 -06:00
userCustomer(user) flatMap {
2016-06-06 12:18:40 -06:00
case None => fufail(s"Can't switch non-existent customer ${user.id}")
2016-06-06 08:41:04 -06:00
case Some(customer) =>
customer.firstSubscription match {
2016-07-13 08:14:20 -06:00
case None => fufail(s"Can't switch non-existent subscription of ${user.id}")
case Some(sub) if sub.plan.cents == cents => fuccess(sub)
case Some(sub) =>
getOrMakePlan(cents) flatMap { plan =>
stripeClient.updateSubscription(sub, plan, none)
}
2016-06-06 08:41:04 -06:00
}
}
2016-06-06 12:18:40 -06:00
def cancel(user: User): Funit =
userCustomer(user) flatMap {
case None => fufail(s"Can't cancel non-existent customer ${user.id}")
case Some(customer) =>
customer.firstSubscription match {
case None => fufail(s"Can't cancel non-existent subscription of ${user.id}")
2016-07-12 11:19:30 -06:00
case Some(sub) => stripeClient.cancelSubscription(sub) >>
2016-06-06 12:18:40 -06:00
UserRepo.setPlan(user, user.plan.disable)
}
}
2016-07-12 11:19:30 -06:00
def onStripeCharge(charge: StripeCharge): Funit =
2016-07-12 10:33:22 -06:00
customerIdPatron(charge.customer) flatMap { patronOption =>
chargeColl.insert(Charge.make(
2016-07-14 04:03:04 -06:00
userId = patronOption.map(_.userId),
2016-07-12 10:58:39 -06:00
stripe = Charge.Stripe(charge.id, charge.customer).some,
cents = charge.amount)) >> {
2016-07-12 10:33:22 -06:00
patronOption match {
2016-07-13 08:46:25 -06:00
case None =>
logger.info(s"Charged anon customer $charge")
lila.mon.plan.amount(charge.amount.value)
bus.publish(lila.hub.actorApi.plan.ChargeEvent(
username = "Anonymous",
amount = charge.amount.value), 'stripe)
funit
2016-07-12 10:33:22 -06:00
case Some(patron) if patron.canLevelUp =>
2016-07-14 04:03:04 -06:00
UserRepo byId patron.userId flatten s"Missing user for $patron" flatMap { user =>
patronColl.update($id(patron.id), patron.levelUpNow) >>
2016-07-12 10:33:22 -06:00
UserRepo.setPlan(user, user.plan.incMonths) >>- {
logger.info(s"Charged $charge $patron")
2016-07-12 11:19:30 -06:00
lila.mon.plan.amount(charge.amount.value)
bus.publish(lila.hub.actorApi.plan.ChargeEvent(
2016-07-12 10:33:22 -06:00
username = user.username,
2016-07-12 10:58:39 -06:00
amount = charge.amount.value), 'stripe)
2016-07-12 10:33:22 -06:00
}
2016-07-10 14:12:22 -06:00
}
2016-07-12 10:33:22 -06:00
case Some(patron) => fufail(s"Too early to level up $charge $patron")
2016-06-06 08:41:04 -06:00
}
2016-07-12 10:33:22 -06:00
}
2016-06-06 08:41:04 -06:00
}
2016-07-12 10:33:22 -06:00
def onPaypalCharge(userId: String, email: Option[Patron.PayPal.Email], subId: Option[Patron.PayPal.SubId], cents: Cents): Funit = (cents.value >= 500) ?? {
chargeColl.insert(Charge.make(
userId = userId.some,
payPal = Charge.PayPal(email = email.map(_.value), subId = subId.map(_.value)).some,
cents = cents)) >> {
UserRepo named userId flatMap {
_ ?? { user =>
userPatron(user).flatMap {
case None => patronColl.insert(Patron(
_id = Patron.UserId(user.id),
payPal = Patron.PayPal(email, subId, DateTime.now).some,
lastLevelUp = DateTime.now
).expireInOneMonth) >>
2016-07-12 10:33:22 -06:00
UserRepo.setPlan(user, lila.user.Plan.start)
case Some(patron) if patron.canLevelUp =>
patronColl.update($id(patron.id), patron.levelUpNow.expireInOneMonth) >>
2016-07-12 10:33:22 -06:00
UserRepo.setPlan(user, user.plan.incMonths)
case Some(patron) => fufail(s"Too early to level up with paypal $patron")
2016-07-12 11:19:30 -06:00
} >>- {
2016-07-14 04:03:04 -06:00
logger.info(s"Charged ${user.username} with paypal: $cents")
2016-07-12 11:19:30 -06:00
lila.mon.plan.amount(cents.value)
}
2016-07-12 10:33:22 -06:00
}
}
}
}
2016-06-06 08:41:04 -06:00
def onSubscriptionDeleted(sub: StripeSubscription): Funit =
customerIdPatron(sub.customer) flatMap {
2016-07-13 15:53:12 -06:00
case None => fufail(s"Deleted subscription of unknown patron $sub")
case Some(patron) =>
2016-07-14 04:03:04 -06:00
UserRepo byId patron.userId flatten s"Missing user for $patron" flatMap { user =>
2016-07-13 15:53:12 -06:00
UserRepo.setPlan(user, user.plan.disable) >>
patronColl.update($id(user.id), patron.removeStripe).void >>-
2016-07-14 04:03:04 -06:00
logger.info(s"Unsubed ${user.username} ${sub}")
2016-06-06 08:41:04 -06:00
}
}
2016-07-12 11:19:30 -06:00
def getEvent = stripeClient.getEvent _
2016-06-06 11:50:13 -06:00
def customerInfo(user: User): Fu[Option[CustomerInfo]] =
userCustomerId(user) flatMap {
_ ?? { customerId =>
2016-07-12 11:19:30 -06:00
stripeClient.getCustomer(customerId) zip
stripeClient.getNextInvoice(customerId) zip
stripeClient.getPastInvoices(customerId) map {
case ((Some(customer), Some(nextInvoice)), pastInvoices) =>
2016-07-13 08:14:20 -06:00
customer.plan match {
case Some(plan) => CustomerInfo(plan, nextInvoice, pastInvoices).some
case None =>
2016-07-14 04:03:04 -06:00
logger.warn(s"Can't identify ${user.username} plan $customer")
none
}
case fail =>
2016-07-14 04:03:04 -06:00
logger.warn(s"Can't fetch ${user.username} customer info $fail")
none
}
2016-06-06 11:50:13 -06:00
}
}
import PlanApi.SyncResult.{ ReloadUser, Synced }
def sync(user: User): Fu[PlanApi.SyncResult] = userPatron(user) flatMap {
2016-07-11 16:55:35 -06:00
2016-07-12 10:58:39 -06:00
case None if user.plan.active =>
2016-07-11 16:55:35 -06:00
logger.warn(s"sync: disable plan of non-patron")
UserRepo.setPlan(user, user.plan.disable) inject ReloadUser
2016-07-11 16:55:35 -06:00
case None => fuccess(Synced(none))
2016-07-11 16:55:35 -06:00
case Some(patron) => (patron.stripe, patron.payPal) match {
2016-07-12 11:19:30 -06:00
case (Some(stripe), _) => stripeClient.getCustomer(stripe.customerId) flatMap {
2016-07-11 16:55:35 -06:00
case None =>
logger.warn(s"sync: unset DB patron that's not in stripe")
patronColl.update($id(patron.id), patron.removeStripe) >> sync(user)
2016-07-11 16:55:35 -06:00
case Some(customer) if customer.firstSubscription.isEmpty =>
logger.warn(s"sync: unset DB patron of customer without a subscription")
patronColl.update($id(patron.id), patron.removeStripe) >> sync(user)
2016-07-11 16:55:35 -06:00
case Some(customer) if customer.firstSubscription.isDefined && !user.plan.active =>
logger.warn(s"sync: enable plan of customer with a subscription")
UserRepo.setPlan(user, user.plan.enable) inject ReloadUser
case customer => fuccess(Synced(patron.some))
2016-07-11 16:55:35 -06:00
}
case (_, Some(paypal)) =>
2016-07-12 10:58:39 -06:00
if (paypal.isExpired)
patronColl.update($id(user.id), patron.removePayPal) >> sync(user)
2016-07-11 16:55:35 -06:00
else if (!paypal.isExpired && !user.plan.active) {
logger.warn(s"sync: enable plan of customer with paypal")
UserRepo.setPlan(user, user.plan.enable) inject ReloadUser
2016-07-11 16:55:35 -06:00
}
else fuccess(Synced(patron.some))
2016-07-11 16:55:35 -06:00
case (None, None) if user.plan.active =>
logger.warn(s"sync: disable plan of patron with no paypal or stripe")
UserRepo.setPlan(user, user.plan.disable) inject ReloadUser
2016-07-11 16:55:35 -06:00
case _ => fuccess(Synced(patron.some))
2016-06-06 11:50:13 -06:00
}
}
2016-06-06 11:50:13 -06:00
2016-07-13 08:14:20 -06:00
def getOrMakePlan(cents: Cents): Fu[StripePlan] =
stripeClient.getPlan(cents) getOrElse stripeClient.makePlan(cents)
private def setAnonPlan(plan: StripePlan, data: Checkout, renew: Boolean): Fu[StripeSubscription] =
2016-07-13 08:46:25 -06:00
stripeClient.createAnonCustomer(plan, data) map { customer =>
logger.info(s"Subed anon $customer to ${plan} renew=$renew")
2016-07-13 08:46:25 -06:00
customer.firstSubscription err s"Can't create anon $customer subscription to $plan"
} flatMap { subscription =>
if (renew) fuccess(subscription)
else stripeClient dontRenewSubscription subscription
2016-07-13 08:46:25 -06:00
}
private def setUserPlan(user: User, plan: StripePlan, data: Checkout, renew: Boolean): Fu[StripeSubscription] =
2016-06-06 08:41:04 -06:00
userCustomer(user) flatMap {
case None => createCustomer(user, data, plan) map { customer =>
2016-07-14 04:03:04 -06:00
customer.firstSubscription err s"Can't create ${user.username} subscription for customer $customer"
2016-06-06 08:41:04 -06:00
}
2016-07-13 08:46:25 -06:00
case Some(customer) => setCustomerPlan(customer, plan, data.source)
} flatMap { subscription =>
logger.info(s"Subed user ${user.username} $subscription renew=$renew")
if (renew) fuccess(subscription)
else stripeClient dontRenewSubscription subscription
2016-06-06 08:41:04 -06:00
}
private def createCustomer(user: User, data: Checkout, plan: StripePlan): Fu[StripeCustomer] =
stripeClient.createCustomer(user, data, plan) flatMap { customer =>
saveStripePatron(user, customer.id, data.isMonthly) >>
2016-06-06 11:50:13 -06:00
UserRepo.setPlan(user, lila.user.Plan.start) >>-
logger.info(s"Create ${user.username} customer $customer") inject customer
2016-06-06 08:41:04 -06:00
}
private def saveStripePatron(user: User, customerId: CustomerId, renew: Boolean): Funit = userPatron(user) flatMap {
2016-07-12 12:25:58 -06:00
case None => patronColl.insert(Patron(
_id = Patron.UserId(user.id),
stripe = Patron.Stripe(customerId).some,
lastLevelUp = DateTime.now
).expireInOneMonth(!renew))
2016-07-12 12:25:58 -06:00
case Some(patron) => patronColl.update(
$id(patron.id),
patron.copy(stripe = Patron.Stripe(customerId).some).expireInOneMonth(renew))
2016-07-12 12:25:58 -06:00
} void
2016-06-06 08:41:04 -06:00
private def setCustomerPlan(customer: StripeCustomer, plan: StripePlan, source: Source): Fu[StripeSubscription] =
customer.subscriptions.data.find(_.plan == plan) match {
case Some(sub) => fuccess(sub)
case None => customer.firstSubscription match {
2016-07-12 11:19:30 -06:00
case None => stripeClient.createSubscription(customer, plan, source)
case Some(sub) => stripeClient.updateSubscription(sub, plan, source.some)
2016-06-06 08:41:04 -06:00
}
}
private def userCustomerId(user: User): Fu[Option[CustomerId]] =
2016-07-12 12:25:58 -06:00
userPatron(user) map {
_.flatMap { _.stripe.map(_.customerId) }
}
2016-06-06 08:41:04 -06:00
private def userCustomer(user: User): Fu[Option[StripeCustomer]] =
userCustomerId(user) flatMap {
2016-07-12 11:19:30 -06:00
_ ?? stripeClient.getCustomer
2016-06-06 08:41:04 -06:00
}
private def customerIdPatron(id: CustomerId): Fu[Option[Patron]] =
patronColl.uno[Patron](selectStripeCustomerId(id))
private def selectStripeCustomerId(id: CustomerId): Bdoc =
$doc("stripe.customerId" -> id)
private def userPatron(user: User): Fu[Option[Patron]] =
patronColl.uno[Patron]($id(user.id))
2016-06-06 03:36:21 -06:00
}
object PlanApi {
sealed trait SyncResult
object SyncResult {
case object ReloadUser extends SyncResult
case class Synced(patron: Option[Patron]) extends SyncResult
}
}