in lambda/src/main/scala/pricemigrationengine/services/ZuoraLive.scala [61:278]
private def failureMessage(request: HttpRequest, t: Throwable) =
s"Request for ${request.method} ${request.url} returned error ${t.toString}"
val impl: ZLayer[ZuoraConfig with Logging, ConfigFailure, Zuora] =
ZLayer.fromZIO(
for {
logging <- ZIO.service[Logging]
config <- ZIO.service[ZuoraConfig]
accessToken <- ZIO
.fromEither(fetchedAccessToken(config))
.mapError(failure => ConfigFailure(failure.reason))
.tap(token => logging.info(s"Fetched Zuora access token: $token"))
} yield new Zuora {
private def retry[E, A](effect: => ZIO[Any, E, A]) =
effect.retry(exponential(1.second) && recurs(5))
private def get[A: Reader](path: String, params: Map[String, String] = Map.empty) = {
for {
a <- retry(
handleRequest[A](
Http(s"${config.apiHost}/$apiVersion/$path")
.params(params)
.header("Authorization", s"Bearer $accessToken")
).mapError(e => ZuoraFetchFailure(e.reason))
)
} yield a
}
private def post[A: Reader](path: String, body: String) =
for {
a <- handleRequest[A](
Http(s"${config.apiHost}/$apiVersion/$path")
.header("Authorization", s"Bearer $accessToken")
.header("Content-Type", "application/json")
.postData(body)
).mapError(e => ZuoraUpdateFailure(e.reason))
} yield a
private def put[A: Reader](path: String, body: String) =
for {
a <- handleRequest[A](
Http(s"${config.apiHost}/$apiVersion/$path")
.header("Authorization", s"Bearer $accessToken")
.header("Content-Type", "application/json")
.put(body)
).mapError(e => ZuoraUpdateFailure(e.reason))
} yield a
private def handleRequest[A: Reader](request: HttpRequest) =
for {
response <-
ZIO
.attempt(request.option(connTimeout).option(readTimeout).asString)
.mapError(e => ZuoraFailure(failureMessage(request, e)))
.filterOrElseWith(_.code == 200)(response => ZIO.fail(ZuoraFailure(failureMessage(request, response))))
a <-
ZIO
.attempt(read[A](response.body))
.orElseFail(ZuoraFailure(failureMessage(request, response)))
} yield a
override def fetchSubscription(subscriptionNumber: String): ZIO[Any, ZuoraFetchFailure, ZuoraSubscription] =
get[ZuoraSubscription](s"subscriptions/$subscriptionNumber")
.mapError(e => ZuoraFetchFailure(s"Subscription $subscriptionNumber: ${e.reason}"))
.tapBoth(
e => logging.error(s"Failed to fetch subscription $subscriptionNumber: $e"),
_ => logging.info(s"Fetched subscription $subscriptionNumber")
)
override def fetchAccount(
accountNumber: String,
subscriptionNumber: String
): ZIO[Any, ZuoraFetchFailure, ZuoraAccount] =
get[ZuoraAccount](s"accounts/$accountNumber")
.mapError(e =>
ZuoraFetchFailure(s"Account ${accountNumber} for subscription $subscriptionNumber: ${e.reason}")
)
.tapBoth(
e => logging.error(s"Failed to fetch account ${accountNumber} for subscription $subscriptionNumber: $e"),
_ => logging.info(s"Fetched account $accountNumber for subscription $subscriptionNumber")
)
// See https://www.zuora.com/developer/api-reference/#operation/POST_BillingPreviewRun
override def fetchInvoicePreview(
accountId: String,
targetDate: LocalDate
): ZIO[Any, ZuoraFetchFailure, ZuoraInvoiceList] = {
retry(
post[ZuoraInvoiceList](
path = "operations/billing-preview",
body = write(
InvoicePreviewRequest(
accountId,
targetDate,
assumeRenewal = "Autorenew",
chargeTypeToExclude = "OneTime"
)
)
).mapError(e => ZuoraFetchFailure(s"Invoice preview for account $accountId: ${e.reason}"))
)
.tapBoth(
e => logging.error(s"Failed to fetch invoice preview for account $accountId: $e"),
_ => logging.info(s"Fetched invoice preview for account $accountId")
)
}
override val fetchProductCatalogue: ZIO[Any, ZuoraFetchFailure, ZuoraProductCatalogue] = {
def fetchPage(idx: Int): ZIO[Any, ZuoraFetchFailure, ZuoraProductCatalogue] =
get[ZuoraProductCatalogue](path = "catalog/products", params = Map("page" -> idx.toString))
.mapError(e => ZuoraFetchFailure(s"Product catalogue: ${e.reason}"))
.tapBoth(
e => logging.error(s"Failed to fetch product catalogue page $idx: $e"),
_ => logging.info(s"Fetched product catalogue page $idx")
)
def hasNextPage(catalogue: ZuoraProductCatalogue) = catalogue.nextPage.isDefined
def combine(c1: ZuoraProductCatalogue, c2: ZuoraProductCatalogue) =
ZuoraProductCatalogue(products = c1.products ++ c2.products)
def fetchCatalogue(
acc: ZuoraProductCatalogue,
pageIdx: Int
): ZIO[Any, ZuoraFetchFailure, ZuoraProductCatalogue] =
for {
curr <- fetchPage(pageIdx)
soFar = combine(acc, curr)
catalogue <-
if (hasNextPage(curr)) {
fetchCatalogue(soFar, pageIdx + 1)
} else ZIO.succeed(soFar)
} yield catalogue
fetchCatalogue(ZuoraProductCatalogue.empty, pageIdx = 1)
}
override def updateSubscription(
subscription: ZuoraSubscription,
update: ZuoraSubscriptionUpdate
): ZIO[Any, ZuoraUpdateFailure, ZuoraSubscriptionId] = {
put[SubscriptionUpdateResponse](
path = s"subscriptions/${subscription.subscriptionNumber}",
body = write(update)
).mapBoth(
e => ZuoraUpdateFailure(s"Subscription ${subscription.subscriptionNumber} and update $update: ${e.reason}"),
response => response.subscriptionId
)
}
override def applyAmendmentOrder(
subscription: ZuoraSubscription,
payload: ZuoraAmendmentOrderPayload
): ZIO[Any, ZuoraOrderFailure, Unit] = {
// The existence of type_flush is explained in comment cada56ad.
// This is, for all intent and purpose a hack due to the way upickle deals with sealed traits
// and in a future change we will get rid of it, either by changing JSON library or coming up
// with the correct writers.
def type_flush(str: String): String = {
str
.replace(""""$type":"ZuoraAmendmentOrderPayloadOrderActionAdd",""", "")
.replace(""""$type":"ZuoraAmendmentOrderPayloadOrderActionRemove",""", "")
}
val body = type_flush(write(payload))
post[ZuoraAmendmentOrderResponse](
path = s"orders",
body = type_flush(write(payload))
).foldZIO(
failure = e =>
ZIO.fail(
ZuoraOrderFailure(
s"[f8569839] subscription number: ${subscription.subscriptionNumber}, payload: ${payload}, reason: ${e.reason}"
)
),
success = response =>
if (response.success) {
ZIO.succeed(())
} else {
ZIO.fail(
ZuoraOrderFailure(
s"[bb6f22ef] subscription number: ${subscription.subscriptionNumber}, payload: ${payload}, serialised payload: ${body}, with answer ${response}"
)
)
}
)
}
override def renewSubscription(
subscriptionNumber: String,
payload: ZuoraRenewOrderPayload
): ZIO[Any, ZuoraRenewalFailure, Unit] = {
post[ZuoraRenewOrderResponse](
path = s"orders",
body = write(payload)
).foldZIO(
failure = e =>
ZIO.fail(
ZuoraRenewalFailure(
s"[06f5bd6f] subscription number: ${subscriptionNumber}, payload: ${payload}, reason: ${e.reason}"
)
),
success = response =>
if (response.success) {
ZIO.succeed(())
} else {
ZIO.fail(
ZuoraRenewalFailure(
s"[bc532694] subscription number: ${subscriptionNumber}, payload: ${payload}, with answer ${response}"
)
)
}
)
}
}