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

package controllers import _root_.services.salesforce.ContactRepository import _root_.services.zuora.rest.ZuoraRestService.ObjectAccount import actions.{CommonActions, ContinueRegardlessOfSignInRecency} import com.gu.i18n.Currency import com.gu.identity.SignedInRecently import com.gu.memsub.Subscription.AccountId import com.gu.memsub._ import com.gu.memsub.subsv2.Subscription import com.gu.memsub.subsv2.services.SubscriptionService import com.gu.monitoring.SafeLogger.LogPrefix import com.gu.monitoring.SafeLogging import components.TouchpointComponents import models.AccessScope.completeReadSelf import models.ExistingPaymentOption import monitoring.CreateMetrics import org.joda.time.LocalDate import org.joda.time.LocalDate.now import play.api.libs.json.Json import play.api.mvc.{Action, AnyContent, BaseController, ControllerComponents} import scalaz.-\/ import scalaz.std.scalaFuture._ import scalaz.syntax.monadPlus._ import utils.ListTEither import utils.SimpleEitherT.SimpleEitherT import scala.concurrent.{ExecutionContext, Future} class ExistingPaymentOptionsController( commonActions: CommonActions, override val controllerComponents: ControllerComponents, createMetrics: CreateMetrics, ) extends BaseController with SafeLogging { import commonActions._ implicit val executionContext: ExecutionContext = controllerComponents.executionContext val metrics = createMetrics.forService(classOf[ExistingPaymentOptionsController]) def allSubscriptionsSince( date: LocalDate, maybeUserId: Option[String], contactRepository: ContactRepository, subscriptionService: SubscriptionService[Future], )(implicit logPrefix: LogPrefix): SimpleEitherT[Map[AccountId, List[Subscription]]] = (for { user <- ListTEither.fromOption(maybeUserId) contact <- ListTEither.fromFutureOption(contactRepository.get(user)) subscription <- ListTEither.fromFutureList(subscriptionService.since(date)(contact)) } yield subscription).toList.map(_.groupBy(_.accountId)) def consolidatePaymentMethod(existingPaymentOptions: List[ExistingPaymentOption]): Iterable[ExistingPaymentOption] = { def extractConsolidationPart(existingPaymentOption: ExistingPaymentOption): Option[String] = existingPaymentOption.paymentMethodOption match { case Some(card: PaymentCard) => card.paymentCardDetails.map(_.lastFourDigits) case Some(dd: GoCardless) => Some(dd.accountNumber) case Some(payPal: PayPalMethod) => Some(payPal.email) case _ => None } def mapConsolidatedBackToSingle(consolidated: List[ExistingPaymentOption]): ExistingPaymentOption = { val theChosenOne = consolidated.head // TODO in future perhaps use custom fields on PaymentMethod to see which is safe to clone ExistingPaymentOption( freshlySignedIn = theChosenOne.freshlySignedIn, objectAccount = theChosenOne.objectAccount, paymentMethodOption = theChosenOne.paymentMethodOption, subscriptions = consolidated.flatMap(_.subscriptions), ) } existingPaymentOptions.groupBy(extractConsolidationPart).view.filterKeys(_.isDefined).values.map(mapConsolidatedBackToSingle) } // TODO should probably fetch upToDate details from Stripe to determine this (rather than relying on Zuora) - see getUpToDatePaymentDetailsFromStripe in AccountController def cardThatWontBeExpiredOnFirstTransaction(cardDetails: PaymentCardDetails): Boolean = new LocalDate(cardDetails.expiryYear, cardDetails.expiryMonth, 1).isAfter(now.plusMonths(1)) def existingPaymentOptions(currencyFilter: Option[String]): Action[AnyContent] = AuthorizeForRecentLogin(ContinueRegardlessOfSignInRecency, requiredScopes = List(completeReadSelf)).async { implicit request => import request.logPrefix metrics.measureDuration("GET /user-attributes/me/existing-payment-options") { implicit val tp: TouchpointComponents = request.touchpoint val maybeUserId = request.redirectAdvice.userId val isSignedInRecently = request.redirectAdvice.signInStatus == SignedInRecently val eligibilityDate = now.minusMonths(3) val defaultMandateIdIfApplicable = "CLEARED" def paymentMethodStillValid(paymentMethodOption: Option[PaymentMethod]) = paymentMethodOption match { case Some(card: PaymentCard) => card.isReferenceTransaction && card.paymentCardDetails.exists(cardThatWontBeExpiredOnFirstTransaction) case Some(dd: GoCardless) => dd.mandateId != defaultMandateIdIfApplicable // i.e. mandateId a real reference and hasn't been cleared in Zuora because of mandate failure case _ => false } def paymentMethodHasNoFailures(paymentMethodOption: Option[PaymentMethod]) = !paymentMethodOption.flatMap(_.numConsecutiveFailures).exists(_ > 0) def paymentMethodIsActive(paymentMethodOption: Option[PaymentMethod]) = !paymentMethodOption.flatMap(_.paymentMethodStatus).contains("Closed") def currencyMatchesFilter(accountCurrency: Option[Currency]) = (accountCurrency.map(_.iso), currencyFilter) match { case (Some(accountCurrencyISO), Some(currencyFilterValue)) => accountCurrencyISO == currencyFilterValue case (None, Some(_)) => false // if the account has no currency but there is filter the account is not eligible case _ => true } logger.info(s"Attempting to retrieve existing payment options for identity user: ${maybeUserId.mkString}") val futureEitherListExistingPaymentOption = (for { groupedSubsList <- ListTEither.fromEitherT( allSubscriptionsSince(eligibilityDate, maybeUserId, tp.contactRepository, tp.subscriptionService).map(_.toList), ) (accountId, subscriptions) = groupedSubsList objectAccount <- ListTEither.singleDisjunction(tp.zuoraRestService.getObjectAccount(accountId).recover { case x => -\/[String, ObjectAccount](s"error receiving OBJECT account with account id $accountId. Reason: $x") }) if currencyMatchesFilter(objectAccount.currency) && objectAccount.defaultPaymentMethodId.isDefined account <- ListTEither.singleRightT(tp.zuoraSoapService.getAccount(accountId)) paymentMethodOption <- ListTEither.single( tp.paymentService .getPaymentMethod(account.defaultPaymentMethodId, Some(defaultMandateIdIfApplicable)) .map(Right(_)) .recover { case x => Left(s"error retrieving payment method for account: $accountId. Reason: $x") }, ) if paymentMethodStillValid(paymentMethodOption) && paymentMethodHasNoFailures(paymentMethodOption) && paymentMethodIsActive(paymentMethodOption) } yield ExistingPaymentOption(isSignedInRecently, objectAccount, paymentMethodOption, subscriptions)).run.run.map(_.toEither) for { catalog <- tp.futureCatalog result <- futureEitherListExistingPaymentOption.map { case Right(existingPaymentOptions) => logger.info(s"Successfully retrieved eligible existing payment options for identity user: ${maybeUserId.mkString}") Ok(Json.toJson(consolidatePaymentMethod(existingPaymentOptions.toList).map(_.toJson(catalog)))) case Left(message) => logger.warn(s"Unable to retrieve eligible existing payment options for identity user ${maybeUserId.mkString} due to $message") InternalServerError("Failed to retrieve eligible existing payment options due to an internal error") } } yield result } } }