Skip to content

Commit

Permalink
Merge pull request #164 from hmrc/CIR-619-final
Browse files Browse the repository at this point in the history
[PS] CIR-619 - Read welsh country name data from third party/object-store
  • Loading branch information
beyond-code-github authored Sep 12, 2024
2 parents a586307 + b3f2949 commit 6449992
Show file tree
Hide file tree
Showing 32 changed files with 1,001 additions and 192 deletions.
46 changes: 43 additions & 3 deletions app/Module.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,53 @@
* limitations under the License.
*/

import com.google.inject.AbstractModule
import com.google.inject.{AbstractModule, Provides}
import controllers.RemoteMessagesApiProvider
import org.apache.pekko.stream.Materializer
import play.api.libs.concurrent.PekkoGuiceSupport
import play.api.{Configuration, Environment, Logger}
import services._
import uk.gov.hmrc.objectstore.client.play.PlayObjectStoreClient

import javax.inject.Singleton
import scala.concurrent.ExecutionContext

class Module(env: Environment, playConfig: Configuration) extends AbstractModule with PekkoGuiceSupport {

class Module() extends AbstractModule
with PekkoGuiceSupport {
override def configure(): Unit = {
bind(classOf[RemoteMessagesApiProvider])
bind(classOf[GovWalesCacheUpdateScheduler]).asEagerSingleton()
}

@Provides
@Singleton
private def provideWelshCountryNamesDataSource(english: EnglishCountryNamesDataSource,
objectStore: PlayObjectStoreClient,
ec: ExecutionContext, mat: Materializer): WelshCountryNamesDataSource = {
val logger = Logger(this.getClass)

val useLocal =
playConfig.getOptional[Boolean]("microservice.services.gov-wales.useLocal").getOrElse(true)

if (useLocal) {
logger.info(s"Using local gov-wales country data")
new WelshCountryNamesDataSource(english)
} else {
logger.info(s"Using gov-wales country data from object-store")

val proxyHost: Option[String] = playConfig.getOptional[String]("proxy.host")
val proxyPort: Option[Int] = playConfig.getOptional[Int]("proxy.port")
val proxyUsername: Option[String] = playConfig.getOptional[String]("proxy.username")
val proxyPassword: Option[String] = playConfig.getOptional[String]("proxy.password")

val proxyConfig: Option[ExtendedProxyConfig] = for {
pHost <- proxyHost
pPort <- proxyPort
pUserName <- proxyUsername
pPassword <- proxyPassword
} yield new ExtendedProxyConfig(pHost, pPort, "https", pUserName, pPassword)

new WelshCountryNamesObjectStoreDataSource(english, objectStore, proxyConfig, ec, mat)
}
}
}
4 changes: 2 additions & 2 deletions app/controllers/AbpAddressLookupController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ class AbpAddressLookupController @Inject()(
(
Some(
journeyData.copy(
selectedAddress = Some(edit.toConfirmableAddress(id))
selectedAddress = Some(edit.toConfirmableAddress(id, c => countryService.find(isWelsh, c)))
)
),
requestWithWelshHeader(isWelsh) {
Expand Down Expand Up @@ -448,7 +448,7 @@ class AbpAddressLookupController @Inject()(
(
Some(
journeyData.copy(
selectedAddress = Some(edit.toConfirmableAddress(id))
selectedAddress = Some(edit.toConfirmableAddress(id, c => countryService.find(isWelsh, c)))
)
),
requestWithWelshHeader(isWelsh) {
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/InternationalAddressLookupController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ class InternationalAddressLookupController @Inject()(
(
Some(
journeyData.copy(
selectedAddress = Some(edit.toConfirmableAddress(id))
selectedAddress = Some(edit.toConfirmableAddress(id, c => countryService.find(isWelsh, c)))
)
),
requestWithWelshHeader(isWelsh) {
Expand Down
19 changes: 19 additions & 0 deletions app/controllers/PingController.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright 2024 HM Revenue & Customs
*
*/

package controllers

import play.api.mvc.{Action, AnyContent, MessagesControllerComponents}
import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendController

import javax.inject.Inject

class PingController @Inject()(controllerComponents: MessagesControllerComponents)
extends FrontendController(controllerComponents) {

def ping: Action[AnyContent] = Action {
Ok
}
}
24 changes: 24 additions & 0 deletions app/health/HealthController.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2024 HM Revenue & Customs
*
*/

package health

import play.api.mvc.{Action, AnyContent, DefaultControllerComponents}
import play.api.{Configuration, Environment}
import services.WelshCountryNamesDataSource
import uk.gov.hmrc.play

import javax.inject.{Inject, Singleton}

@Singleton
class HealthController @Inject()(configuration: Configuration, environment: Environment,
dataSource: WelshCountryNamesDataSource, controllerComponents: DefaultControllerComponents)
extends play.health.HealthController(configuration, environment, controllerComponents) {

override def ping: Action[AnyContent] = Action {
if (dataSource.isCacheReady) Ok
else ServiceUnavailable
}
}
10 changes: 4 additions & 6 deletions app/model/Model.scala
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ case class Edit(organisation: Option[String],
postcode: String,
countryCode: String = "GB") {

def toConfirmableAddress(auditRef: String): ConfirmableAddress =
def toConfirmableAddress(auditRef: String, findCountry: String => Option[Country]): ConfirmableAddress =
ConfirmableAddress(
auditRef,
None,
Expand All @@ -54,7 +54,7 @@ case class Edit(organisation: Option[String],
if (postcode.isEmpty) None
else if (countryCode == "GB") Postcode.cleanupPostcode(postcode).map(_.toString)
else Some(postcode),
ForeignOfficeCountryService.find(code = countryCode)
findCountry(countryCode)
)
)
}
Expand All @@ -67,9 +67,7 @@ case class ProposedAddress(addressId: String,
postcode: Option[String],
town: Option[String],
lines: List[String] = List.empty,
country: Country = ForeignOfficeCountryService
.find(code = "GB")
.getOrElse(Country("GB", "United Kingdom")),
country: Country = Country("GB", "United Kingdom"),
poBox: Option[String] = None) {

def toConfirmableAddress(auditRef: String): ConfirmableAddress =
Expand Down Expand Up @@ -107,7 +105,7 @@ case class ConfirmableAddressDetails(
lines: Seq[String] = Seq(),
town: Option[String] = None,
postcode: Option[String] = None,
country: Option[Country] = ForeignOfficeCountryService.find(code = "GB"),
country: Option[Country] = Some(Country("GB", "United Kingdom")),
poBox: Option[String] = None
) {

Expand Down
4 changes: 2 additions & 2 deletions app/services/AddressService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ trait AddressService {
}

@Singleton
class AddressLookupAddressService @Inject()(frontendAppConfig: FrontendAppConfig, http: HttpClient)(implicit val
class AddressLookupAddressService @Inject()(frontendAppConfig: FrontendAppConfig, http: HttpClient, countryService: CountryService)(implicit val
ec: ExecutionContext) extends AddressService {

val endpoint = frontendAppConfig.addressReputationEndpoint
Expand Down Expand Up @@ -100,7 +100,7 @@ ec: ExecutionContext) extends AddressService {
s"${addr.city.getOrElse("")}",
s"${addr.region.getOrElse("")}"
).filter(_.nonEmpty).toList,
ForeignOfficeCountryService.find(code = countryCode).get
countryService.find(code = countryCode).get
)
}

Expand Down
48 changes: 48 additions & 0 deletions app/services/CountryNamesDataSource.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2024 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 services

import address.v2.Country
import com.github.tototoshi.csv.CSVReader

import scala.io.Source

trait CountryNamesDataSource {
protected def allAliases(aliasFileClasspath: String) =
CSVReader.open(Source.fromInputStream(getClass.getResourceAsStream(aliasFileClasspath), "UTF-8"))
.allWithOrderedHeaders()._2.sortBy(x => x("countryCode"))
.map { x => x("countryCode") -> (x("aliases").split("\\|").map(_.trim).toList) }
.map { case (c, as) => c -> as.map(a => Country(c, a)) }
.toMap

protected def renameFields(m: Map[String, String]): Map[String, String] = {
mappings.flatMap { case (o, nm) => nm.map(n => n -> m(o)) }
}

protected val mappings = Map(
"independent" -> None,
"alpha_3_code" -> None,
"full_name_en" -> Some("Official Name"),
"status" -> None,
"short_name_uppercase_en" -> None,
"numeric_code" -> None,
"short_name_en" -> Some("Name"),
"alpha_2_code" -> Some("Country")
)

protected val utfSorter = java.text.Collator.getInstance()
}
93 changes: 7 additions & 86 deletions app/services/CountryService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import com.github.tototoshi.csv._
import com.google.inject.ImplementedBy
import play.api.libs.json.Json

import javax.inject.Singleton
import javax.inject.{Inject, Singleton}
import scala.collection.immutable.SortedMap
import scala.io.Source

Expand All @@ -32,105 +32,26 @@ trait CountryService {

// to match uk.gov.hmrc.address.v2.Countries and serve as a comprehensive replacement
def find(welshFlag: Boolean = false, code: String): Option[Country]

}

@Singleton
class ForeignOfficeCountryService extends CountryService {

class ForeignOfficeCountryService @Inject() (english: EnglishCountryNamesDataSource, welsh: WelshCountryNamesDataSource) extends CountryService {
implicit val fcoCountryFormat = Json.format[FcoCountry]

private val countriesENJson: Seq[Country] = Json.parse(getClass.getResourceAsStream("/countriesEN.json")).as[Map[String, FcoCountry]].map { country =>
Country(country._2.country, country._2.name)
}.toSeq.sortWith(_.name < _.name)

private val mappings = Map(
"independent" -> None,
"alpha_3_code" -> None,
"full_name_en" -> Some("Official Name"),
"status" -> None,
"short_name_uppercase_en" -> None,
"numeric_code" -> None,
"short_name_en" -> Some("Name"),
"alpha_2_code" -> Some("Country")
)

private def renameFields(m: Map[String, String]): Map[String, String] = {
mappings.flatMap { case (o, nm) => nm.map(n => n -> m(o)) }
}

private val utfSorter = java.text.Collator.getInstance()

private def allAliases(aliasFileClasspath: String) =
CSVReader.open(Source.fromInputStream(getClass.getResourceAsStream(aliasFileClasspath), "UTF-8"))
.allWithOrderedHeaders()._2.sortBy(x => x("countryCode"))
.map { x => x("countryCode") -> (x("aliases").split("\\|").map(_.trim).toList) }
.map { case (c, as) => c -> as.map(a => Country(c, a)) }
.toMap
private val allAliasesEN = allAliases("/countryAliasesEN.csv")
private val allAliasesCY = allAliases("/countryAliasesCY.csv")

private val allISORows = CSVReader.open(Source.fromInputStream(getClass.getResourceAsStream("/iso-countries.csv"), "UTF-8"))
.allWithOrderedHeaders._2.sortBy(x => x("alpha_2_code"))
.map(renameFields)
.groupBy(_ ("Country")).view
.mapValues(v => v.head)

private val allFCDORows = CSVReader.open(Source.fromInputStream(getClass.getResourceAsStream("/fcdo_countries.csv"), "UTF-8"))
.allWithOrderedHeaders._2.sortBy(x => x("Country"))
.groupBy(_ ("Country")).view
.mapValues(v => v.head)

private val allFCDOTRows = CSVReader.open(Source.fromInputStream(getClass.getResourceAsStream("/fcdo_territories.csv"), "UTF-8"))
.allWithOrderedHeaders._2.sortBy(x => x("Country"))
.filterNot(m => m("Country") == "Not applicable")
.groupBy(_ ("Country")).view
.mapValues(v => v.head)

private val allWCORows = CSVReader.open(Source.fromInputStream(getClass.getResourceAsStream("/wco-countries.csv"), "UTF-8"))
.allWithOrderedHeaders._2.sortBy(x => x("Country"))
.groupBy(_ ("Country"))
.view.mapValues(v => v.head)

private val countriesENFull: Seq[Country] = {
SortedMap.from(allISORows ++ allFCDORows ++ allFCDOTRows)
.map(Country.apply)
.toSeq.sortWith{ case (a, b) => utfSorter.compare(a.name, b.name) < 0 }
}

private val countriesEN = countriesENFull.flatMap { country =>
if(allAliasesEN.contains(country.code)) country +: allAliasesEN(country.code)
else Seq(country)
}

private val countriesCYFull: Seq[Country] = {
SortedMap.from(allISORows ++ allFCDORows ++ allFCDOTRows ++ allWCORows)
.map(Country.apply)
.toSeq.sortWith{ case (a, b) => utfSorter.compare(a.name, b.name) < 0 }
}

private val countriesCY = countriesCYFull.flatMap { country =>
if (allAliasesCY.contains(country.code)) country +: allAliasesCY(country.code)
else Seq(country)
}

override def findAll(welshFlag: Boolean = false): Seq[Country] =
if (!welshFlag) countriesEN
else countriesCY
if (!welshFlag) english.countriesEN
else welsh.countriesCY

override def find(welshFlag: Boolean = false, code: String): Option[Country] = {
if (!welshFlag) {
val filtered = countriesEN.filter(_.code == code)
val filtered = english.countriesEN.filter(_.code == code)
filtered.headOption
}
else {
val filtered = countriesCY.filter(_.code == code)
val filtered = welsh.countriesCY.filter(_.code == code)
filtered.headOption
}

}
}

case class FcoCountry(country: String, name: String)

object ForeignOfficeCountryService extends ForeignOfficeCountryService
case class FcoCountry(country: String, name: String)
Loading

0 comments on commit 6449992

Please sign in to comment.