membership-attribute-service/app/controllers/PaymentUpdateController.scala (203 lines of code) (raw):

package controllers import actions.{CommonActions, Return401IfNotSignedInRecently} import com.gu.memsub import com.gu.memsub.subsv2.ProductType import com.gu.memsub.{CardUpdateFailure, CardUpdateSuccess, GoCardless, PaymentMethod} import com.gu.monitoring.SafeLogger.LogPrefix import com.gu.monitoring.SafeLogging import com.gu.salesforce.Contact import com.gu.zuora.api.GoCardlessGateway import com.gu.zuora.api.GoCardlessTortoiseMediaGateway import com.gu.zuora.soap.models.Commands.{BankTransfer, CreatePaymentMethod} import json.PaymentCardUpdateResultWriters._ import models.AccessScope.{readSelf, updateSelf} import monitoring.CreateMetrics import play.api.data.Form import play.api.data.Forms._ import play.api.libs.json.Json import play.api.mvc._ import scalaz.EitherT import scalaz.std.scalaFuture._ import services.mail.Emails.paymentMethodChangedEmail import services.mail.{Card, DirectDebit, PaymentType, SendEmail} import utils.SimpleEitherT import utils.SimpleEitherT.SimpleEitherT import models.GatewayOwner import scala.concurrent.{ExecutionContext, Future} class PaymentUpdateController( commonActions: CommonActions, override val controllerComponents: ControllerComponents, sendEmail: SendEmail, createMetrics: CreateMetrics, ) extends BaseController with SafeLogging { import AccountHelpers._ import commonActions._ implicit val executionContext: ExecutionContext = controllerComponents.executionContext val metrics = createMetrics.forService(classOf[PaymentUpdateController]) def updateCard(subscriptionName: String) = AuthorizeForRecentLoginAndScopes(Return401IfNotSignedInRecently, requiredScopes = List(readSelf, updateSelf)).async { implicit request => import request.logPrefix metrics.measureDuration("POST /user-attributes/me/update-card/:subscriptionName") { // TODO - refactor to use the Zuora-only based lookup, like in AttributeController.pickAttributes - https://trello.com/c/RlESb8jG val legacyForm = Form { tuple("stripeToken" -> nonEmptyText, "publicKey" -> nonEmptyText) }.bindFromRequest().value val updateForm = Form { tuple("stripePaymentMethodID" -> nonEmptyText, "stripePublicKey" -> nonEmptyText) }.bindFromRequest().value val services = request.touchpoint val useStripePaymentMethod = updateForm.isDefined val user = request.user val userId = user.identityId logger.info(s"Attempting to update card for $userId") (for { stripeDetails <- EitherT.fromEither( Future.successful(updateForm.orElse(legacyForm).toRight("no 'stripePaymentMethodID' and 'stripePublicKey' submitted with request")), ) contact <- EitherT.fromEither(services.contactRepository.get(userId).map(_.toEither).map(_.flatMap(_.toRight(s"no SF user $userId")))) subscription <- EitherT.fromEither( services.subscriptionService .current(contact) .map(subs => subscriptionSelector(memsub.Subscription.SubscriptionNumber(subscriptionName), s"the sfUser $contact", subs)), ) (stripeCardIdentifier, stripePublicKey) = stripeDetails updateResult <- services .setPaymentCard(stripePublicKey) .setPaymentCard(useStripePaymentMethod, subscription.accountId, stripeCardIdentifier) catalog <- SimpleEitherT.rightT(services.futureCatalog) productType = subscription.plan(catalog).productType(catalog) _ <- sendPaymentMethodChangedEmail(user.primaryEmailAddress, contact, Card, productType) } yield updateResult match { case success: CardUpdateSuccess => { logger.info(s"Successfully updated card for identity user: $user") Ok(Json.toJson(success)) } case failure: CardUpdateFailure => { logger.error(scrub"Failed to update card for identity user: $user due to $failure") Forbidden(Json.toJson(failure)) } }).run.map(_.toEither).map { case Left(message) => logger.warn(s"Failed to update card for user $userId, due to $message") InternalServerError(s"Failed to update card for user $userId") case Right(result) => result } } } private def sendPaymentMethodChangedEmail( emailAddress: String, contact: Contact, paymentMethod: PaymentType, productType: ProductType, )(implicit logPrefix: LogPrefix): SimpleEitherT[Unit] = SimpleEitherT.rightT(sendEmail.send(paymentMethodChangedEmail(emailAddress, contact, paymentMethod, productType))) private def checkDirectDebitUpdateResult( freshDefaultPaymentMethodOption: Option[PaymentMethod], bankAccountName: String, bankAccountNumber: String, bankSortCode: String, )(implicit logPrefix: LogPrefix): Result = freshDefaultPaymentMethodOption match { case Some(dd: GoCardless) if bankAccountName == dd.accountName && dd.accountNumber.length > 3 && bankAccountNumber.endsWith(dd.accountNumber.substring(dd.accountNumber.length - 3)) && bankSortCode == dd.sortCode => logger.info(s"Successfully updated direct debit") Ok( Json.obj( "accountName" -> dd.accountName, "accountNumber" -> dd.accountNumber, "sortCode" -> dd.sortCode, ), ) case Some(_) => logger.error( scrub"New payment method $freshDefaultPaymentMethodOption, does not match the posted Direct Debit details $bankSortCode $bankAccountNumber $bankAccountName", ) InternalServerError("") case None => logger.error( scrub"default-payment-method-lost: Default payment method was set to nothing, when attempting to update Direct Debit details", ) InternalServerError("") } def updateDirectDebit(subscriptionName: String): Action[AnyContent] = AuthorizeForRecentLoginAndScopes(Return401IfNotSignedInRecently, requiredScopes = List(readSelf, updateSelf)).async { implicit request => import request.logPrefix metrics.measureDuration("POST /user-attributes/me/update-direct-debit/:subscriptionName") { // TODO - refactor to use the Zuora-only based lookup, like in AttributeController.pickAttributes - https://trello.com/c/RlESb8jG val updateForm = Form { tuple( "accountName" -> nonEmptyText, "accountNumber" -> nonEmptyText, "sortCode" -> nonEmptyText, "gatewayOwner" -> optional(text).transform[GatewayOwner]( GatewayOwner.fromString, _.value, ), ) } val services = request.touchpoint val user = request.user val userId = user.identityId logger.info(s"Attempting to update direct debit") (for { directDebitDetails <- SimpleEitherT.fromEither(updateForm.bindFromRequest().value.toRight("no direct debit details submitted with request")) (bankAccountName, bankAccountNumber, bankSortCode, paymentGatewayOwner) = directDebitDetails contact <- SimpleEitherT(services.contactRepository.get(userId).map(_.toEither.flatMap(_.toRight(s"no SF user for $userId")))) subscription <- SimpleEitherT( services.subscriptionService .current(contact) .map(subs => subscriptionSelector(memsub.Subscription.SubscriptionNumber(subscriptionName), s"the sfUser $contact", subs)), ) account <- SimpleEitherT( annotateFailableFuture(services.zuoraSoapService.getAccount(subscription.accountId), s"get account with id ${subscription.accountId}"), ) billToContact <- SimpleEitherT( annotateFailableFuture(services.zuoraSoapService.getContact(account.billToId), s"get billTo contact with id ${account.billToId}"), ) bankTransferPaymentMethod = BankTransfer( accountHolderName = bankAccountName, accountNumber = bankAccountNumber, sortCode = bankSortCode, firstName = billToContact.firstName, lastName = billToContact.lastName, countryCode = "GB", ) paymentGatewayToUse = paymentGatewayOwner match { case GatewayOwner.TortoiseMedia => GoCardlessTortoiseMediaGateway case _ => GoCardlessGateway } createPaymentMethod = CreatePaymentMethod( accountId = subscription.accountId, paymentMethod = bankTransferPaymentMethod, paymentGateway = paymentGatewayToUse, billtoContact = billToContact, ) _ <- SimpleEitherT( annotateFailableFuture( services.zuoraSoapService.createPaymentMethod(createPaymentMethod), s"create direct debit payment method using ${paymentGatewayToUse.gatewayName}", ), ) freshAccount <- SimpleEitherT( annotateFailableFuture( services.zuoraSoapService.getAccount(subscription.accountId), s"get fresh account with id ${subscription.accountId}", ), ) freshDefaultPaymentMethodOption <- SimpleEitherT( annotateFailableFuture(services.paymentService.getPaymentMethod(freshAccount.defaultPaymentMethodId), "get fresh default payment method"), ) catalog <- SimpleEitherT.rightT(services.futureCatalog) productType = subscription.plan(catalog).productType(catalog) _ <- sendPaymentMethodChangedEmail(user.primaryEmailAddress, contact, DirectDebit, productType) } yield checkDirectDebitUpdateResult(freshDefaultPaymentMethodOption, bankAccountName, bankAccountNumber, bankSortCode)).run .map(_.toEither) .map { case Left(message) => logger.error(scrub"Failed to update direct debit due to $message") InternalServerError("") case Right(result) => result } } } }