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

package services.zuora.rest import com.gu.i18n.{Country, Currency, Title} import com.gu.memsub.Subscription.{AccountId, AccountNumber, SubscriptionNumber, RatePlanId, SubscriptionRatePlanChargeId} import com.gu.memsub.subsv2.reads.CommonReads._ import com.gu.monitoring.SafeLogger.LogPrefix import com.gu.salesforce.ContactId import com.gu.zuora.ZuoraLookup import com.gu.zuora.api.PaymentGateway import com.gu.zuora.rest.ZuoraResponse import org.joda.time.format.ISODateTimeFormat import org.joda.time.{DateTime, LocalDate} import scalaz.\/ import services.zuora.rest.ZuoraRestService.{ AccountSummary, AccountsByCrmIdResponse, AccountsByCrmIdResponseRecord, ContactData, GetAccountsQueryResponse, GiftSubscriptionsFromIdentityIdRecord, ObjectAccount, PaymentMethodResponse, } import scala.concurrent.{ExecutionContext, Future} import scala.language.higherKinds object ZuoraRestService { import com.gu.memsub.subsv2.reads.CommonReads.localWrites import play.api.libs.functional.syntax._ import play.api.libs.json.Reads._ import play.api.libs.json._ implicit class MapOps(in: Map[String, Option[String]]) { def flattenWithDefault(defaultValue: String) = in.collect { case (key, Some(value)) => key -> value case (key, None) => key -> defaultValue } } def jsStringOrNull(value: Option[String]) = value.map(JsString(_)).getOrElse(JsNull) def isoDateStringAsDateTime(dateString: String): DateTime = ISODateTimeFormat.dateTimeParser().parseDateTime(dateString) case class AddressData( address1: Option[String], address2: Option[String], city: Option[String], state: Option[String], zipCode: Option[String], country: String, ) { def asJsObject: JsObject = Json.obj( "address1" -> jsStringOrNull(address1), "city" -> jsStringOrNull(city), "country" -> JsString(country), "address2" -> jsStringOrNull(address2), "state" -> jsStringOrNull(state), "zipCode" -> jsStringOrNull(zipCode), ) } // these classes looks similar to what we use in RestQuery maybe we can remove that duplication case class ContactData( title: Option[String], firstName: String, lastName: String, specialDeliveryInstructions: Option[String], workEmail: Option[String], companyName: Option[String], address: AddressData, ) { def asJsObject: JsObject = Json.obj( "firstName" -> JsString(firstName), "lastName" -> JsString(lastName), "Title__c" -> jsStringOrNull(title), "SpecialDeliveryInstructions__c" -> jsStringOrNull(specialDeliveryInstructions), "workEmail" -> jsStringOrNull(workEmail), "Company_Name__c" -> jsStringOrNull(companyName), ) ++ address.asJsObject } case class UpdateContactsCommand(billTo: Option[ContactData], soldTo: Option[ContactData]) implicit val updateContactsWrites = new Writes[UpdateContactsCommand] { override def writes(command: UpdateContactsCommand): JsValue = { val billtoJson = command.billTo.map(billto => Json.obj("billToContact" -> billto.asJsObject)) val soldToJson = command.soldTo.map(soldto => Json.obj("soldToContact" -> soldto.asJsObject)) val maybeDisableEmailInvoices = command.billTo.flatMap { billTocontact => if (billTocontact.workEmail.isEmpty) { Some(Json.obj("invoiceDeliveryPrefsEmail" -> JsBoolean(false))) } else { None } } val jsonParts = List(billtoJson, soldToJson, maybeDisableEmailInvoices).flatten jsonParts.foldRight(Json.obj())(_ ++ _) } } case class UpdateAccountIdentityIdCommand(identityId: String) implicit val updateAccountWithIdentityIdWrites = new Writes[UpdateAccountIdentityIdCommand] { override def writes(c: UpdateAccountIdentityIdCommand): JsValue = Json.obj( "IdentityId__c" -> c.identityId, ) } case class CancelSubscriptionCommand(cancellationEffectiveDate: LocalDate) implicit val cancelSubscriptionCommandWrites = new Writes[CancelSubscriptionCommand] { override def writes(command: CancelSubscriptionCommand): JsValue = Json.obj( "cancellationPolicy" -> "SpecificDate", "cancellationEffectiveDate" -> command.cancellationEffectiveDate, "invoiceCollect" -> false, ) } case class RenewSubscriptionCommand() implicit val renewSubscriptionCommandWrites = new Writes[RenewSubscriptionCommand] { override def writes(command: RenewSubscriptionCommand): JsValue = Json.obj( "invoiceCollect" -> false, ) } case class UpdateCancellationSubscriptionCommand(cancellationReason: String, userCancellationReason: String) implicit val updateCancellationSubscriptionCommand = new Writes[UpdateCancellationSubscriptionCommand] { override def writes(command: UpdateCancellationSubscriptionCommand): JsValue = { Json.obj( "CancellationReason__c" -> command.cancellationReason, "UserCancellationReason__c" -> command.userCancellationReason, ) } } case class DisableAutoPayCommand() implicit val disableAutoPayCommand = new Writes[DisableAutoPayCommand] { override def writes(command: DisableAutoPayCommand): JsValue = { Json.obj( "autoPay" -> false, ) } } case class UpdateAccountCommand(email: String) implicit val updateAccountCommandWrites = new Writes[UpdateAccountCommand] { override def writes(command: UpdateAccountCommand): JsValue = { Json.obj( "billToContact" -> Json.obj( "workEmail" -> command.email, ), ) } } case class UpdateChargeCommand( price: Double, ratePlanChargeId: SubscriptionRatePlanChargeId, ratePlanId: RatePlanId, applyFromDate: LocalDate, note: String, ) implicit val updateChargeCommandWrites = new Writes[UpdateChargeCommand] { override def writes(command: UpdateChargeCommand): JsValue = { Json.obj( "notes" -> command.note, "update" -> Json.arr( Json.obj( "chargeUpdateDetails" -> Json.arr( Json.obj( "price" -> command.price, "ratePlanChargeId" -> command.ratePlanChargeId.get, ), ), "contractEffectiveDate" -> command.applyFromDate, "customerAcceptanceDate" -> command.applyFromDate, "serviceActivationDate" -> command.applyFromDate, "ratePlanId" -> command.ratePlanId.get, ), ), ) } } case class RestQuery(queryString: String) implicit val restQueryWrites = Json.writes[RestQuery] case class SalesforceContactId(get: String) extends AnyVal case class AccountSummary( id: AccountId, accountNumber: AccountNumber, identityId: Option[String], billToContact: BillToContact, soldToContact: SoldToContact, invoices: List[Invoice], payments: List[Payment], currency: Option[Currency], balance: Double, defaultPaymentMethod: Option[DefaultPaymentMethod], sfContactId: SalesforceContactId, ) case class ObjectAccount( id: AccountId, autoPay: Option[Boolean], defaultPaymentMethodId: Option[PaymentMethodId], currency: Option[Currency], ) case class BillToContact( email: Option[String], country: Option[Country], ) case class SoldToContact( title: Option[Title], firstName: Option[String], lastName: String, email: Option[String], address1: Option[String], address2: Option[String], city: Option[String], postCode: Option[String], state: Option[String], country: Option[Country], ) case class InvoiceId(get: String) extends AnyVal case class Invoice( id: InvoiceId, invoiceNumber: String, invoiceDate: DateTime, dueDate: DateTime, amount: Double, balance: Double, status: String, ) case class PaidInvoice(invoiceNumber: String, appliedPaymentAmount: Double) case class Payment( status: String, paidInvoices: List[PaidInvoice], ) case class PaymentMethodId(get: String) extends AnyVal case class DefaultPaymentMethod(id: PaymentMethodId) case class AccountObject( Id: AccountId, Balance: Double = 0, Currency: Option[Currency], DefaultPaymentMethodId: Option[PaymentMethodId] = None, PaymentGateway: Option[PaymentGateway] = None, LastInvoiceDate: Option[DateTime] = None, ) case class GetAccountsQueryResponse( records: List[AccountObject], size: Int, ) case class AccountsByCrmIdResponseRecord(Id: AccountId, SoldToId: Option[String], BillToId: Option[String], sfContactId__c: Option[String]) case class AccountsByCrmIdResponse( records: List[AccountsByCrmIdResponseRecord], size: Int, ) object AccountsByCrmIdResponseRecord { implicit val reads: Reads[AccountsByCrmIdResponseRecord] = Json.reads[AccountsByCrmIdResponseRecord] } object AccountsByCrmIdResponse { implicit val reads: Reads[AccountsByCrmIdResponse] = Json.reads[AccountsByCrmIdResponse] } case class GiftSubscriptionsFromIdentityIdRecord(Name: String, Id: String, TermEndDate: LocalDate) case class GiftSubscriptionsFromIdentityIdResponse( records: List[GiftSubscriptionsFromIdentityIdRecord], size: Int, ) object GiftSubscriptionsFromIdentityIdRecord { implicit val reads: Reads[GiftSubscriptionsFromIdentityIdRecord] = ( (JsPath \ "Name").read[String] and (JsPath \ "Id").read[String] and (JsPath \ "TermEndDate").read[LocalDate] )(GiftSubscriptionsFromIdentityIdRecord.apply _) } object GiftSubscriptionsFromIdentityIdResponse { implicit val reads: Reads[GiftSubscriptionsFromIdentityIdResponse] = Json.reads[GiftSubscriptionsFromIdentityIdResponse] } case class PaymentMethodResponse(numConsecutiveFailures: Int, paymentMethodType: String, lastTransactionDateTime: DateTime) implicit val paymentMethodReads: Reads[PaymentMethodResponse] = ( (JsPath \ "NumConsecutiveFailures").read[Int] and (JsPath \ "Type").read[String] and (JsPath \ "LastTransactionDateTime").read[String].map(isoDateStringAsDateTime) )(PaymentMethodResponse.apply _) implicit val paymentGatewayReads: Reads[Option[PaymentGateway]] = __.read[String].map(PaymentGateway.getByName) implicit val currencyReads: Reads[Option[Currency]] = __.read[String].map(Currency.fromString) implicit val billToContactReads: Reads[BillToContact] = ( (JsPath \ "workEmail").readNullable[String].filter(_ != "") and (JsPath \ "country").read[String].map(ZuoraLookup.country) )(BillToContact.apply _) implicit val soldToContactReads: Reads[SoldToContact] = ( (JsPath \ "Title__c").readNullable[String].map(_.flatMap(Title.fromString)) and (JsPath \ "firstName").readNullable[String] and (JsPath \ "lastName").read[String] and (JsPath \ "workEmail").readNullable[String] and (JsPath \ "address1").readNullable[String] and (JsPath \ "address2").readNullable[String] and (JsPath \ "city").readNullable[String] and (JsPath \ "zipCode").readNullable[String] and (JsPath \ "state").readNullable[String] and (JsPath \ "country").read[String].map(ZuoraLookup.country) )(SoldToContact.apply _) implicit val invoiceReads: Reads[Invoice] = ( (JsPath \ "id").read[String].map(InvoiceId.apply) and (JsPath \ "invoiceNumber").read[String] and (JsPath \ "invoiceDate").read[String].map(isoDateStringAsDateTime) and (JsPath \ "dueDate").read[String].map(isoDateStringAsDateTime) and (JsPath \ "amount").read[Double] and (JsPath \ "balance").read[Double] and (JsPath \ "status").read[String] )(Invoice.apply _) implicit val paidInvoiceReads: Reads[PaidInvoice] = ( (JsPath \ "invoiceNumber").read[String] and (JsPath \ "appliedPaymentAmount").read[Double] )(PaidInvoice.apply _) implicit val paymentReads: Reads[Payment] = ( (JsPath \ "status").read[String] and (JsPath \ "paidInvoices").read[List[PaidInvoice]] )(Payment.apply _) implicit val paymentMethodIdReads: Reads[PaymentMethodId] = JsPath.read[String].map(PaymentMethodId.apply) implicit val defaultPaymentMethodReads: Reads[DefaultPaymentMethod] = Json.reads[DefaultPaymentMethod] implicit val accountSummaryReads: Reads[AccountSummary] = ( (__ \ "basicInfo" \ "id").read[String].map(AccountId.apply) and (__ \ "basicInfo" \ "accountNumber").read[String].map(AccountNumber.apply) and (__ \ "basicInfo" \ "IdentityId__c").readNullable[String] and (__ \ "billToContact").read[BillToContact] and (__ \ "soldToContact").read[SoldToContact] and (__ \ "invoices").read[List[Invoice]] and (__ \ "payments").read[List[Payment]] and (__ \ "basicInfo" \ "currency").read[Option[Currency]] and (__ \ "basicInfo" \ "balance").read[Double] and (__ \ "basicInfo" \ "defaultPaymentMethod").readNullable[DefaultPaymentMethod] and (__ \ "basicInfo" \ "sfContactId__c").read[String].map(SalesforceContactId.apply) )(AccountSummary.apply _) implicit val objectAccountReads: Reads[ObjectAccount] = ( (__ \ "Id").read[String].map(AccountId.apply) and (__ \ "AutoPay").readNullable[Boolean] and (__ \ "DefaultPaymentMethodId").readNullable[PaymentMethodId] and (__ \ "Currency").read[Option[Currency]] )(ObjectAccount.apply _) implicit val nameReads: Reads[AccountId] = JsPath.read[String].map(AccountId.apply) implicit val accountObjectReads: Reads[AccountObject] = ( (JsPath \ "Id").read[AccountId] and (JsPath \ "Balance").read[Double] and (JsPath \ "Currency").read[Option[Currency]] and (JsPath \ "DefaultPaymentMethodId").readNullable[PaymentMethodId] and (JsPath \ "PaymentGateway").readWithDefault[Option[PaymentGateway]](None) and (JsPath \ "LastInvoiceDate").readNullable[String].map(_.map(isoDateStringAsDateTime)) )(AccountObject.apply _) implicit val queryResponseReads: Reads[GetAccountsQueryResponse] = Json.reads[GetAccountsQueryResponse] case class Amendment(effectiveDate: Option[String], `type`: Option[String]) implicit val amendment: Reads[Amendment] = Json.reads[Amendment] case class CancelledSubscription(subscriptionEndDate: String, status: String) implicit val cancelledSubscription: Reads[CancelledSubscription] = Json.reads[CancelledSubscription] } trait ZuoraRestService { def getAccount(accountId: AccountId)(implicit logPrefix: LogPrefix): Future[String \/ AccountSummary] def getObjectAccount(accountId: AccountId)(implicit logPrefix: LogPrefix): Future[String \/ ObjectAccount] def getGiftSubscriptionRecordsFromIdentityId(identityId: String)(implicit logPrefix: LogPrefix, ): Future[String \/ List[GiftSubscriptionsFromIdentityIdRecord]] def getPaymentMethod(paymentMethodId: String)(implicit logPrefix: LogPrefix): Future[String \/ PaymentMethodResponse] 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] def updateCancellationReason(subscriptionNumber: SubscriptionNumber, userCancellationReason: String)(implicit logPrefix: LogPrefix, ): Future[String \/ Unit] def disableAutoPay(accountId: AccountId)(implicit logPrefix: LogPrefix): Future[String \/ Unit] def updateChargeAmount( subscriptionNumber: SubscriptionNumber, ratePlanChargeId: SubscriptionRatePlanChargeId, ratePlanId: RatePlanId, amount: Double, reason: String, applyFromDate: LocalDate, )(implicit ex: ExecutionContext, logPrefix: LogPrefix): Future[\/[String, Unit]] def getCancellationEffectiveDate(subscriptionNumber: SubscriptionNumber)(implicit logPrefix: LogPrefix): Future[String \/ Option[String]] }