lila/app/controllers/Plan.scala

291 lines
9.1 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
import lila.plan.StripeClient.StripeException
import lila.plan.{
Checkout,
CreateStripeSession,
CustomerId,
Freq,
Money,
MonthlyCustomerInfo,
NextUrls,
OneTimeCustomerInfo,
StripeCustomer
}
import lila.user.{ User => UserModel }
import views._
import java.util.Currency
import lila.mon
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)
pricing <- env.plan.priceApi.pricingOrDefault(myCurrency)
} yield Ok(
html.plan.index(
stripePublicKey = env.plan.stripePublicKey,
email = email,
patron = patron,
recentIds = recentIds,
bestIds = bestIds,
pricing = pricing,
methods = env.plan.stripePaymentMethods("payment", pricing.currency)
)
)
private def indexPatron(me: UserModel, patron: lila.plan.Patron, customer: StripeCustomer)(implicit
ctx: Context
) = for {
pricing <- env.plan.priceApi.pricingOrDefault(myCurrency)
info <- env.plan.api.customerInfo(me, customer)
gifts <- env.plan.api.giftsFrom(me)
res <- info match {
case Some(info: MonthlyCustomerInfo) =>
Ok(html.plan.indexStripe(me, patron, info, env.plan.stripePublicKey, pricing, gifts)).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)
}
}
} yield res
private def myCurrency(implicit ctx: Context): Currency =
get("currency") flatMap lila.plan.CurrencyApi.currencyOption getOrElse
env.plan.currencyApi.currencyByCountryCodeOrLang(
env.security.geoIP(ctx.ip).flatMap(_.countryCode),
ctx.lang
)
def features =
Open { implicit ctx =>
pageHit
fuccess {
html.plan.features()
}
}
def switch =
AuthBody { implicit ctx => me =>
implicit val req = ctx.body
env.plan.priceApi.pricingOrDefault(myCurrency) flatMap { pricing =>
lila.plan.Switch
.form(pricing)
.bindFromRequest()
.fold(
_ => funit,
data => env.plan.api.switch(me, data.money)
) 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) {
for {
patron <- ctx.me ?? env.plan.api.userPatron
customer <- patron ?? env.plan.api.patronCustomer
gift <- ctx.me ?? env.plan.api.recentGiftFrom
} yield Ok(html.plan.thanks(patron, customer, gift))
}
}
def webhook =
Action.async(parse.json) { req =>
env.plan.webhookHandler(req.body) map { _ =>
Ok("kthxbye")
}
}
def badStripeApiCall: PartialFunction[Throwable, Result] = { case e: StripeException =>
logger.error("Plan.stripeCheckout", e)
BadRequest(jsonError("Stripe API call failed"))
}
private def createStripeSession(
checkout: Checkout,
customerId: CustomerId,
giftTo: Option[lila.user.User]
)(implicit ctx: Context) = {
for {
isLifetime <- env.plan.priceApi.isLifetime(checkout.money)
session <- env.plan.api
.createSession(
CreateStripeSession(
customerId,
checkout,
NextUrls(
cancel = s"${env.net.baseUrl}${routes.Plan.index}",
success = s"${env.net.baseUrl}${routes.Plan.thanks}"
),
giftTo = giftTo,
isLifetime = isLifetime
)
)
} yield JsonOk(Json.obj("session" -> Json.obj("id" -> session.id.value)))
}.recover(badStripeApiCall)
def switchStripePlan(user: UserModel, money: Money) =
env.plan.api
.switch(user, money)
.inject(jsonOkResult)
.recover(badStripeApiCall)
private val StripeRateLimit = lila.memo.RateLimit.composite[lila.common.IpAddress](
key = "stripe.checkout.ip"
)(
("fast", 8, 10.minute),
("slow", 40, 1.day)
)
def stripeCheckout =
AuthBody { implicit ctx => me =>
implicit val req = ctx.body
StripeRateLimit(ctx.ip) {
env.plan.priceApi.pricingOrDefault(myCurrency) flatMap { pricing =>
env.plan.checkoutForm
.form(pricing)
.bindFromRequest()
.fold(
err => {
logger.info(s"Plan.stripeCheckout 400: $err")
BadRequest(jsonError(err.errors.map(_.message) mkString ", ")).fuccess
},
data => {
val checkout = data.fixFreq
for {
gifted <- checkout.giftTo.filterNot(ctx.userId.has).??(env.user.repo.enabledNamed)
customer <- env.plan.api.userCustomer(me)
session <- customer match {
case Some(customer) if checkout.freq == Freq.Onetime =>
createStripeSession(checkout, customer.id, gifted)
case Some(customer) if customer.firstSubscription.isDefined =>
switchStripePlan(me, checkout.money)
case _ =>
env.plan.api
.makeCustomer(me, checkout)
.flatMap(customer => createStripeSession(checkout, customer.id, gifted))
}
} yield session
}
)
}
}(rateLimitedFu)
}
def updatePayment =
AuthBody { implicit ctx => me =>
implicit val req = ctx.body
StripeRateLimit(ctx.ip) {
env.plan.api.userCustomer(me) flatMap {
_.flatMap(_.firstSubscription) ?? { sub =>
env.plan.api
.createPaymentUpdateSession(
sub,
NextUrls(
cancel = s"${env.net.baseUrl}${routes.Plan.index}",
success =
s"${env.net.baseUrl}${routes.Plan.updatePaymentCallback}?session={CHECKOUT_SESSION_ID}"
)
)
.map(session => JsonOk(Json.obj("session" -> Json.obj("id" -> session.id.value))))
.recover(badStripeApiCall)
}
}
}(rateLimitedFu)
}
def updatePaymentCallback =
AuthBody { implicit ctx => me =>
get("session") ?? { session =>
env.plan.api.userCustomer(me) flatMap {
_.flatMap(_.firstSubscription) ?? { sub =>
env.plan.api.updatePaymentMethod(sub, session) inject Redirect(routes.Plan.index)
}
}
}
}
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(
ipn,
ip = lila.common.HTTPRequest.ipAddress(req),
key = get("key", req) | "N/A"
) inject Ok
)
}
}