lila/app/controllers/Plan.scala

227 lines
7.0 KiB
Scala

package controllers
import play.api.libs.json._
import play.api.mvc._
import scala.concurrent.duration._
import lila.api.Context
import lila.app._
import lila.common.{ EmailAddress, HTTPRequest }
import lila.plan.StripeClient.StripeException
import lila.plan.{
Cents,
Checkout,
CreateStripeSession,
CustomerId,
Freq,
MonthlyCustomerInfo,
OneTimeCustomerInfo,
StripeCustomer
}
import lila.user.{ User => UserModel }
import views._
final class Plan(env: Env)(implicit system: akka.actor.ActorSystem) extends LilaController(env) {
private val logger = lila.log("plan")
def index =
Open { implicit ctx =>
pageHit
ctx.me.fold(indexAnon) { me =>
import lila.plan.PlanApi.SyncResult._
env.plan.api.sync(me) flatMap {
case ReloadUser => Redirect(routes.Plan.index()).fuccess
case Synced(Some(patron), None) =>
env.user.repo email me.id flatMap { email =>
renderIndex(email, patron.some)
}
case Synced(Some(patron), Some(customer)) => indexPatron(me, patron, customer)
case Synced(_, _) => indexFreeUser(me)
}
}
}
def list =
Open { implicit ctx =>
ctx.me.fold(Redirect(routes.Plan.index()).fuccess) { me =>
import lila.plan.PlanApi.SyncResult._
env.plan.api.sync(me) flatMap {
case ReloadUser => Redirect(routes.Plan.list()).fuccess
case Synced(Some(_), _) => indexFreeUser(me)
case _ => Redirect(routes.Plan.index()).fuccess
}
}
}
private def indexAnon(implicit ctx: Context) = renderIndex(email = none, patron = none)
private def indexFreeUser(me: UserModel)(implicit ctx: Context) =
env.user.repo email me.id flatMap { email =>
renderIndex(email, patron = none)
}
private def renderIndex(email: Option[EmailAddress], patron: Option[lila.plan.Patron])(implicit
ctx: Context
): Fu[Result] =
for {
recentIds <- env.plan.api.recentChargeUserIds
bestIds <- env.plan.api.topPatronUserIds
_ <- env.user.lightUserApi preloadMany { recentIds ::: bestIds }
} yield Ok(
html.plan.index(
stripePublicKey = env.plan.stripePublicKey,
email = email,
patron = patron,
recentIds = recentIds,
bestIds = bestIds
)
)
private def indexPatron(me: UserModel, patron: lila.plan.Patron, customer: StripeCustomer)(implicit
ctx: Context
) =
env.plan.api.customerInfo(me, customer) flatMap {
case Some(info: MonthlyCustomerInfo) => Ok(html.plan.indexStripe(me, patron, info)).fuccess
case Some(info: OneTimeCustomerInfo) =>
renderIndex(info.customer.email map EmailAddress.apply, patron.some)
case None =>
env.user.repo email me.id flatMap { email =>
renderIndex(email, patron.some)
}
}
def features =
Open { implicit ctx =>
pageHit
fuccess {
html.plan.features()
}
}
def switch =
AuthBody { implicit ctx => me =>
implicit val req = ctx.body
lila.plan.Switch.form
.bindFromRequest()
.fold(
_ => funit,
data => env.plan.api.switch(me, data.cents)
) inject Redirect(routes.Plan.index())
}
def cancel =
AuthBody { _ => me =>
env.plan.api.cancel(me) inject Redirect(routes.Plan.index())
}
def thanks =
Open { implicit ctx =>
// wait for the payment data from stripe or paypal
lila.common.Future.delay(2.seconds) {
ctx.me ?? env.plan.api.userPatron flatMap { patron =>
patron ?? env.plan.api.patronCustomer map { customer =>
Ok(html.plan.thanks(patron, customer))
}
}
}
}
def webhook =
Action.async(parse.json) { req =>
env.plan.webhook(req.body) map { _ =>
Ok("kthxbye")
}
}
def badStripeSession[A: Writes](err: A) = BadRequest(jsonError(err))
def badStripeApiCall: PartialFunction[Throwable, Result] = { case e: StripeException =>
logger.error("Plan.stripeCheckout", e)
badStripeSession("Stripe API call failed")
}
private def createStripeSession(checkout: Checkout, customerId: CustomerId) =
env.plan.api
.createSession(
CreateStripeSession(
s"${env.net.baseUrl}${routes.Plan.thanks()}",
s"${env.net.baseUrl}${routes.Plan.index()}",
customerId,
checkout
)
)
.map(session => Ok(Json.obj("session" -> Json.obj("id" -> session.id.value))) as JSON)
.recover(badStripeApiCall)
def switchStripePlan(user: UserModel, cents: Cents) = {
env.plan.api
.switch(user, cents)
.inject(Ok(Json.obj("switch" -> Json.obj("cents" -> cents.value))) as JSON)
.recover(badStripeApiCall)
}
private val StripeRateLimit = lila.memo.RateLimit.composite[lila.common.IpAddress](
key = "stripe.checkout.ip",
enforce = env.net.rateLimit.value
)(
("fast", 6, 10.minute),
("slow", 20, 1.day)
)
// update the stripe integration they said, it will be simple they said
// Actually they didn't, I was warned.
def stripeCheckout =
AuthBody { implicit ctx => me =>
implicit val req = ctx.body
StripeRateLimit(HTTPRequest lastRemoteAddress req) {
if (!HTTPRequest.isXhr(req)) BadRequest.fuccess
else
lila.plan.Checkout.form
.bindFromRequest()
.fold(
err => badStripeSession(err.toString).fuccess,
checkout =>
env.plan.api.userCustomer(me) flatMap {
case Some(customer) if checkout.freq == Freq.Onetime =>
createStripeSession(checkout, customer.id)
case Some(customer) if customer.firstSubscription.isDefined =>
switchStripePlan(me, checkout.amount)
case _ =>
env.plan.api
.makeCustomer(me, checkout)
.flatMap(customer => createStripeSession(checkout, customer.id))
}
)
}(rateLimitedFu)
}
def payPalIpn =
Action.async { implicit req =>
import lila.plan.Patron.PayPal
lila.plan.PlanForm.ipn
.bindFromRequest()
.fold(
err => {
if (err.errors("txn_type").nonEmpty) {
logger.debug(s"Plan.payPalIpn ignore txn_type = ${err.data get "txn_type"}")
fuccess(Ok)
} else {
logger.error(s"Plan.payPalIpn invalid data ${err.toString}")
fuccess(BadRequest)
}
},
ipn =>
env.plan.api.onPaypalCharge(
userId = ipn.userId,
email = ipn.email map PayPal.Email.apply,
subId = ipn.subId map PayPal.SubId.apply,
cents = lila.plan.Cents(ipn.grossCents),
name = ipn.name,
txnId = ipn.txnId,
ip = lila.common.HTTPRequest.lastRemoteAddress(req).value,
key = get("key", req) | "N/A"
) inject Ok
)
}
}