membership-attribute-service/app/services/zuora/rest/SimpleClientZuoraRestService.scala (113 lines of code) (raw):

package services.zuora.rest import com.gu.memsub.Subscription._ import com.gu.monitoring.SafeLogger.LogPrefix import com.gu.monitoring.SafeLogging import com.gu.zuora.rest.{SimpleClient, ZuoraResponse} import org.joda.time.LocalDate import scalaz.{Name => avoidclash, _} import services.zuora.rest.ZuoraRestService._ import scala.concurrent.{ExecutionContext, Future} class SimpleClientZuoraRestService(private val simpleRest: SimpleClient[Future])(implicit val m: Monad[Future]) extends ZuoraRestService with SafeLogging { def getAccount(accountId: AccountId)(implicit logPrefix: LogPrefix): Future[String \/ AccountSummary] = { simpleRest.get[AccountSummary](s"accounts/${accountId.get}/summary") // TODO error handling } def getObjectAccount(accountId: AccountId)(implicit logPrefix: LogPrefix): Future[String \/ ObjectAccount] = { simpleRest.get[ObjectAccount](s"object/account/${accountId.get}") } def getGiftSubscriptionRecordsFromIdentityId( identityId: String, )(implicit logPrefix: LogPrefix): Future[String \/ List[GiftSubscriptionsFromIdentityIdRecord]] = { val today = LocalDate.now().toString("yyyy-MM-dd") val queryString = s"select name, id, termEndDate from subscription where GifteeIdentityId__c = '${identityId}' and status = 'Active' and termEndDate >= '$today'" val response = simpleRest.post[RestQuery, GiftSubscriptionsFromIdentityIdResponse]("action/query", RestQuery(queryString)) EitherT(response).map(_.records).run } def getPaymentMethod(paymentMethodId: String)(implicit logPrefix: LogPrefix): Future[String \/ PaymentMethodResponse] = simpleRest.get[PaymentMethodResponse](s"object/payment-method/$paymentMethodId") private def unsuccessfulResponseToLeft(restResponse: EitherT[String, Future, ZuoraResponse]): EitherT[String, Future, ZuoraResponse] = { val futureMonad = implicitly[Monad[Future]] val validated = futureMonad.map(restResponse.run) { case \/-(zuoraResponse) => if (zuoraResponse.success) \/.r[String](zuoraResponse) else \/.l[ZuoraResponse](zuoraResponse.error.getOrElse("Zuora returned with success = false")) case -\/(e) => \/.l[ZuoraResponse](e) } EitherT(validated) } def cancelSubscription( subscriptionNumber: SubscriptionNumber, termEndDate: LocalDate, maybeChargedThroughDate: Option[ LocalDate, ], // FIXME: Optionality should probably be removed and semantics changed to cancellationEffectiveDate (see comments bellow) )(implicit ex: ExecutionContext, logPrefix: LogPrefix): Future[String \/ Unit] = { // FIXME: Not always safe assumption. There are multiple scenarios to consider // 1. Free trial should be explicitly handled: val cancellationEffectiveDate = if(sub.startDate <= today && sub.acceptanceDate > today) LocalDate.now // 2. If outside trial, and invoiced, ChargedThroughDate should always exist: val cancellationEffectiveDate = ChargedThroughDate // 3. If outside trial, and invoiced, but ChargedThroughDate does not exist, then it is a likely logic error. Investigate ASAP!. Currently it happens after Contributions amount change. val cancellationEffectiveDate = maybeChargedThroughDate.getOrElse(LocalDate.now) // immediate cancellation for subs which aren't yet invoiced (e.g. during digipack trial) val extendTermIfNeeded = maybeChargedThroughDate .filter(_.isAfter(termEndDate)) // we need to extend the term if they've paid past their term end date, otherwise cancel call will fail .map(_ => EitherT( simpleRest.put[RenewSubscriptionCommand, ZuoraResponse](s"subscriptions/${subscriptionNumber.getNumber}/renew", RenewSubscriptionCommand()), ), ) .getOrElse(EitherT.right[String, Future, ZuoraResponse](ZuoraResponse(success = true))) val cancelCommand = CancelSubscriptionCommand(cancellationEffectiveDate) val restResponse = for { _ <- extendTermIfNeeded cancelResponse <- EitherT( simpleRest.put[CancelSubscriptionCommand, ZuoraResponse](s"subscriptions/${subscriptionNumber.getNumber}/cancel", cancelCommand), ) } yield cancelResponse unsuccessfulResponseToLeft(restResponse).map(_ => ()).run } def updateCancellationReason(subscriptionNumber: SubscriptionNumber, userCancellationReason: String)(implicit logPrefix: LogPrefix, ): Future[String \/ Unit] = { val future = implicitly[Monad[Future]] val restResponse = for { restResponse <- EitherT( simpleRest.put[UpdateCancellationSubscriptionCommand, ZuoraResponse]( s"subscriptions/${subscriptionNumber.getNumber}", UpdateCancellationSubscriptionCommand(cancellationReason = "Customer", userCancellationReason = userCancellationReason), ), ) } yield restResponse unsuccessfulResponseToLeft(restResponse).map(_ => ()).run } def disableAutoPay(accountId: AccountId)(implicit logPrefix: LogPrefix): Future[String \/ Unit] = { val future = implicitly[Monad[Future]] val restResponse = for { restResponse <- EitherT(simpleRest.put[DisableAutoPayCommand, ZuoraResponse](s"accounts/${accountId.get}", DisableAutoPayCommand())) } yield restResponse unsuccessfulResponseToLeft(restResponse).map(_ => ()).run } def updateChargeAmount( subscriptionNumber: SubscriptionNumber, ratePlanChargeId: SubscriptionRatePlanChargeId, ratePlanId: RatePlanId, amount: Double, reason: String, applyFromDate: LocalDate, )(implicit ex: ExecutionContext, logPrefix: LogPrefix): Future[\/[String, Unit]] = { val updateCommand = UpdateChargeCommand(price = amount, ratePlanChargeId = ratePlanChargeId, ratePlanId = ratePlanId, applyFromDate = applyFromDate, note = reason) val restResponse = for { restResponse <- EitherT(simpleRest.put[UpdateChargeCommand, ZuoraResponse](s"subscriptions/${subscriptionNumber.getNumber}", updateCommand)) } yield restResponse unsuccessfulResponseToLeft(restResponse).map(_ => ()).run } def getCancellationEffectiveDate(subscriptionNumber: SubscriptionNumber)(implicit logPrefix: LogPrefix): Future[String \/ Option[String]] = { (for { amendment <- EitherT(simpleRest.get[Amendment](s"amendments/subscriptions/${subscriptionNumber.getNumber}")) cancelledSub <- EitherT(simpleRest.get[CancelledSubscription](s"subscriptions/${subscriptionNumber.getNumber}")) } yield { if (amendment.`type`.contains("Cancellation") && cancelledSub.status == "Cancelled") Some(cancelledSub.subscriptionEndDate) else None }).run } }