diff --git a/app/uk/gov/hmrc/currencyconversion/controllers/ExchangeRateController.scala b/app/uk/gov/hmrc/currencyconversion/controllers/ExchangeRateController.scala index 48f1598..37a418a 100644 --- a/app/uk/gov/hmrc/currencyconversion/controllers/ExchangeRateController.scala +++ b/app/uk/gov/hmrc/currencyconversion/controllers/ExchangeRateController.scala @@ -50,4 +50,11 @@ class ExchangeRateController @Inject() ( Future.successful(Ok(Json.toJson(rates))) } } -} \ No newline at end of file + + def getCurrenciesByDate(date: LocalDate): Action[AnyContent] = Action.async { implicit request => + exchangeRatesService.getCurrencies(date) match { + case Some(cp) => Future.successful(Ok(Json.toJson(cp))) + case None => Future.successful(NotFound) + } + } +} diff --git a/app/uk/gov/hmrc/currencyconversion/models/Currency.scala b/app/uk/gov/hmrc/currencyconversion/models/Currency.scala new file mode 100644 index 0000000..7df8668 --- /dev/null +++ b/app/uk/gov/hmrc/currencyconversion/models/Currency.scala @@ -0,0 +1,33 @@ +/* + * Copyright 2020 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package uk.gov.hmrc.currencyconversion.models + +import java.time.LocalDate + +import play.api.libs.json.{Json, OFormat} + +case class Currency(countryName: String, currencyName: String, currencyCode: String) + +object Currency { + implicit val format: OFormat[Currency] = Json.format[Currency] +} + +case class CurrencyPeriod(start: LocalDate, end: LocalDate, currencies: Seq[Currency]) + +object CurrencyPeriod { + implicit val format: OFormat[CurrencyPeriod] = Json.format[CurrencyPeriod] +} diff --git a/app/uk/gov/hmrc/currencyconversion/repositories/ConversionRatePeriodRepository.scala b/app/uk/gov/hmrc/currencyconversion/repositories/ConversionRatePeriodRepository.scala index 3b38366..5723371 100644 --- a/app/uk/gov/hmrc/currencyconversion/repositories/ConversionRatePeriodRepository.scala +++ b/app/uk/gov/hmrc/currencyconversion/repositories/ConversionRatePeriodRepository.scala @@ -19,33 +19,32 @@ package uk.gov.hmrc.currencyconversion.repositories import java.io.InputStream import java.time.LocalDate -import uk.gov.hmrc.currencyconversion.models.{ConversionRatePeriod, ExchangeRateOldFileResult, ExchangeRateResult, ExchangeRateSuccessResult} +import uk.gov.hmrc.currencyconversion.models.{ConversionRatePeriod, CurrencyPeriod} +import uk.gov.hmrc.currencyconversion.utils.{CurrencyParsing, ExchangeRateParsing} -import uk.gov.hmrc.currencyconversion.utils.ExchangeRateParsing - -import scala.xml.{Elem, XML} +import scala.xml.XML class ConversionRatePeriodRepository { - - lazy val conversionRatePeriods: Seq[ConversionRatePeriod] = { - def xmlStreams(month: Int = 9, year: Int = 19): Stream[InputStream] = { - val nextMonth = if (month == 12) 1 else month + 1 - val nextYear = if (month == 12) year + 1 else year + private def xmlStreams(month: Int = 9, year: Int = 19): Stream[InputStream] = { + val nextMonth = if (month == 12) 1 else month + 1 + val nextYear = if (month == 12) year + 1 else year - val file = "/resources/xml/exrates-monthly-" + "%02d".format(month) + year + ".xml" + val file = "/resources/xml/exrates-monthly-" + "%02d".format(month) + year + ".xml" - val inputStream: Option[InputStream] = Option(getClass.getResourceAsStream(file)) + val inputStream: Option[InputStream] = Option(getClass.getResourceAsStream(file)) - inputStream match { - case Some(resource) => resource #:: xmlStreams(nextMonth, nextYear) - case None => Stream.empty - } + inputStream match { + case Some(resource) => resource #:: xmlStreams(nextMonth, nextYear) + case None => Stream.empty } + } + lazy val conversionRatePeriods: Seq[ConversionRatePeriod] = xmlStreams().map(XML.load).flatMap(ExchangeRateParsing.ratesFromXml).reverse - } + lazy val currencyPeriods: Seq[CurrencyPeriod] = + xmlStreams().map(XML.load).flatMap(CurrencyParsing.currenciesFromXml).reverse def getConversionRatePeriod(date: LocalDate): Option[ConversionRatePeriod] = conversionRatePeriods.find(crp => (crp.startDate.isBefore(date) || crp.startDate.isEqual(date)) && (crp.endDate.isAfter(date) || crp.endDate.isEqual(date))) @@ -53,4 +52,8 @@ class ConversionRatePeriodRepository { def getLatestConversionRatePeriod: ConversionRatePeriod = conversionRatePeriods.head + def getCurrencyPeriod(date: LocalDate): Option[CurrencyPeriod] = + currencyPeriods.find(cp => + (cp.start.isBefore(date) || cp.start.isEqual(date)) && (cp.end.isAfter(date) || cp.end.isEqual(date))) + } diff --git a/app/uk/gov/hmrc/currencyconversion/services/ExchangeRateService.scala b/app/uk/gov/hmrc/currencyconversion/services/ExchangeRateService.scala index d1d2531..2e3d069 100644 --- a/app/uk/gov/hmrc/currencyconversion/services/ExchangeRateService.scala +++ b/app/uk/gov/hmrc/currencyconversion/services/ExchangeRateService.scala @@ -46,4 +46,7 @@ class ExchangeRateService @Inject()(exchangeRateRepository: ConversionRatePeriod } } } + + def getCurrencies(date: LocalDate): Option[CurrencyPeriod] = + exchangeRateRepository.getCurrencyPeriod(date) } diff --git a/app/uk/gov/hmrc/currencyconversion/utils/CurrencyParsing.scala b/app/uk/gov/hmrc/currencyconversion/utils/CurrencyParsing.scala new file mode 100644 index 0000000..26a9790 --- /dev/null +++ b/app/uk/gov/hmrc/currencyconversion/utils/CurrencyParsing.scala @@ -0,0 +1,82 @@ +/* + * Copyright 2020 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package uk.gov.hmrc.currencyconversion.utils + +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +import play.api.Logger +import uk.gov.hmrc.currencyconversion.models.{Currency, CurrencyPeriod} + +import scala.xml.Elem + +object CurrencyParsing { + + private val formatter = DateTimeFormatter.ofPattern("dd/MMM/yyyy") + + def currenciesFromXml(exchangeRatesRoot: Elem): Option[CurrencyPeriod] = { + val periodAttr = (exchangeRatesRoot \ "@Period").text + + val currencies = (exchangeRatesRoot \ "_").map { node => + val countryName: String = (node \ "countryName").text + val currencyName: String = (node \ "currencyName").text + val currencyCode: String = (node \ "currencyCode").text + + Currency(countryName, currencyName, currencyCode) + } + + def parseDates(dateRange: String): Option[(LocalDate, LocalDate)] = dateRange.split("to").map(_.trim) match { + case Array(start, end) => Some((LocalDate.parse(start, formatter), LocalDate.parse(end, formatter))) + case _ => None + } + + val parsedDates = parseDates(periodAttr) + + parsedDates match { + case Some((s, e)) => Some(CurrencyPeriod(s, e, currencies)) + case None => + Logger.warn("Unable to parse dates from xml Element") + None + } + } + + def isValidXmlElem(elem: Elem): Boolean = { + val validatePeriodAttribute = { + val periodRegex = "\\d{2}\\/[a-zA-Z]{3}\\/\\d{4}\\s+to\\s+\\d{2}\\/[a-zA-Z]{3}\\/\\d{4}" + val periodAttr = (elem \ "@Period").toList.map(_.text) + periodAttr.forall(p => p.matches(periodRegex)) + } + + val validateEssentialElems = { + // get all the nodes and node labels that actually have values + val exchangeRateNodes = elem.child.toList.filterNot(_.isAtom) + val exchangeRateLabels = exchangeRateNodes.map(exchangeRate => exchangeRate.child.filterNot(_.isAtom).map(childNode => childNode.label).toList) + + val hasCorrectNodes = exchangeRateLabels.exists(x => x.contains("countryName") + && x.contains("currencyName") + && x.contains("currencyCode")) + + val nodesHaveContent = exchangeRateNodes.forall(x => (x \ "countryName").text.nonEmpty + && (x \ "currencyName").text.nonEmpty + && (x \ "currencyCode").text.nonEmpty) + + hasCorrectNodes && nodesHaveContent + } + + validatePeriodAttribute && validateEssentialElems + } +} diff --git a/conf/app.routes b/conf/app.routes index 9543b4c..11aa89d 100644 --- a/conf/app.routes +++ b/conf/app.routes @@ -1,3 +1,4 @@ # microservice specific routes GET /rates/:date uk.gov.hmrc.currencyconversion.controllers.ExchangeRateController.getRatesByCurrencyCode(cc: List[String], date: LocalDate) +GET /currencies/:date uk.gov.hmrc.currencyconversion.controllers.ExchangeRateController.getCurrenciesByDate(date: LocalDate) diff --git a/test/uk/gov/hmrc/currencyconversion/controllers/ExchangeRateControllerSpec.scala b/test/uk/gov/hmrc/currencyconversion/controllers/ExchangeRateControllerSpec.scala index bdb40be..bb1a18f 100644 --- a/test/uk/gov/hmrc/currencyconversion/controllers/ExchangeRateControllerSpec.scala +++ b/test/uk/gov/hmrc/currencyconversion/controllers/ExchangeRateControllerSpec.scala @@ -16,20 +16,16 @@ package uk.gov.hmrc.currencyconversion.controllers -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter - -import org.mockito.Mockito -import org.scalatest.mockito.MockitoSugar +import org.scalatestplus.mockito.MockitoSugar import org.scalatestplus.play.guice.GuiceOneAppPerSuite import play.api.http.Status +import play.api.inject.bind import play.api.inject.guice.GuiceApplicationBuilder import play.api.libs.json.{JsArray, JsObject, Json} import play.api.test.FakeRequest import play.api.test.Helpers._ -import uk.gov.hmrc.play.test.UnitSpec -import play.api.inject.bind import uk.gov.hmrc.play.audit.http.connector.AuditConnector +import uk.gov.hmrc.play.test.UnitSpec class ExchangeRateControllerSpec extends UnitSpec with GuiceOneAppPerSuite { @@ -158,4 +154,36 @@ class ExchangeRateControllerSpec extends UnitSpec with GuiceOneAppPerSuite { } } + + "Getting currencies for a valid date" should { + + "return 200 and the correct json" in { + + val result = route(app, FakeRequest("GET", "/currency-conversion/currencies/2019-09-01")).get + + status(result) shouldBe Status.OK + + contentAsJson(result).as[JsObject].keys shouldBe Set("start", "end", "currencies") + } + } + + "Getting currencies for an invalid date" should { + + "return 400" in { + + val result = route(app, FakeRequest("GET", "/currency-conversion/currencies/INVALID-DATE")).get + + status(result) shouldBe Status.BAD_REQUEST + } + } + + "Getting currencies for a date which does not exist" should { + + "return 404" in { + + val result = route(app, FakeRequest("GET", "/currency-conversion/currencies/2019-01-01")).get + + status(result) shouldBe Status.NOT_FOUND + } + } } diff --git a/test/uk/gov/hmrc/currencyconversion/repositories/ExchangeRateRepositorySpec.scala b/test/uk/gov/hmrc/currencyconversion/repositories/ExchangeRateRepositorySpec.scala index 688e3db..eee025e 100644 --- a/test/uk/gov/hmrc/currencyconversion/repositories/ExchangeRateRepositorySpec.scala +++ b/test/uk/gov/hmrc/currencyconversion/repositories/ExchangeRateRepositorySpec.scala @@ -17,7 +17,6 @@ package uk.gov.hmrc.currencyconversion.repositories import java.io.File -import java.time.LocalDate import org.scalatest.Inspectors._ import org.scalatest.{Matchers, WordSpec} diff --git a/test/uk/gov/hmrc/currencyconversion/utils/CurrencyParsingSpec.scala b/test/uk/gov/hmrc/currencyconversion/utils/CurrencyParsingSpec.scala new file mode 100644 index 0000000..f9db0b0 --- /dev/null +++ b/test/uk/gov/hmrc/currencyconversion/utils/CurrencyParsingSpec.scala @@ -0,0 +1,227 @@ +/* + * Copyright 2020 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package uk.gov.hmrc.currencyconversion.utils + +import java.time.LocalDate + +import org.scalatest.{Matchers, WordSpec} +import uk.gov.hmrc.currencyconversion.models.Currency + +class CurrencyParsingSpec extends WordSpec with Matchers { + + "currenciesFromXml" should { + + "parse xml representing currencies" in { + + val xml = + + + Argentina + AR + Peso + ARS + 28.67 + + + Australia + AU + Dollar + AUD + 1.782 + + + + val optionOfSample = CurrencyParsing.currenciesFromXml(xml) + + val resultOfSample = optionOfSample match { + case Some(x) => x + case None => fail("Result returned as None") + } + + resultOfSample.start shouldBe LocalDate.parse("2018-05-01") + resultOfSample.end shouldBe LocalDate.parse("2018-05-31") + resultOfSample.currencies shouldBe Seq(Currency("Argentina", "Peso", "ARS"), Currency("Australia", "Dollar", "AUD")) + } + + "isValidXml" should { + + "return true for valid xml" in { + + val xml = + + + Argentina + AR + Peso + ARS + 28.5 + + + Australia + AU + Dollar + AUD + 1.782 + + + Brazil + BR + Real + BRL + 4.5523 + + + + CurrencyParsing.isValidXmlElem(xml) shouldBe true + } + + "validate the period attribute of an xml element" in { + + val invalidPeriodXml = + + + Argentina + AR + Peso + ARS + + + + CurrencyParsing.isValidXmlElem(invalidPeriodXml) shouldBe false + } + + "validate the existence of essential child elements" in { + + val xmlMissingCurrencyCode = + + + Argentina + AR + Peso + 28.5 + + + Hong Kong + HK + Dollar + HKD + 10.93 + + + + val xmlMissingCountryName = + + + Argentina + AR + Peso + ARS + 28.5 + + + HK + Dollar + HKD + 10.93 + + + + val xmlMissingCurrencyName = + + + Argentina + AR + Peso + ARS + 28.5 + + + Hong Kong + HK + HKD + 10.93 + + + + CurrencyParsing.isValidXmlElem(xmlMissingCurrencyCode) shouldBe false + CurrencyParsing.isValidXmlElem(xmlMissingCountryName) shouldBe false + CurrencyParsing.isValidXmlElem(xmlMissingCurrencyName) shouldBe false + } + + "validate essential elements have content" in { + + val xmlEmptyCountryName = + + + + AR + Peso + 10.93 + HKD + + + Hong Kong + HK + Dollar + HKD + 10.93 + + + + val xmlEmptyCurrencyCode = + + + Argentina + AR + Peso + 28.25 + HKD + + + Hong Kong + HK + Dollar + + 10.93 + + + + val xmlEmptyCurrencyName = + + + Argentina + AR + + 10.93 + HKD + + + Hong Kong + HK + Dollar + HKD + 10.93 + + + + CurrencyParsing.isValidXmlElem(xmlEmptyCountryName) shouldBe false + CurrencyParsing.isValidXmlElem(xmlEmptyCurrencyCode) shouldBe false + CurrencyParsing.isValidXmlElem(xmlEmptyCurrencyName) shouldBe false + } + } + } +}