diff --git a/build.sbt b/build.sbt index 21e2cf51a34..59637274212 100644 --- a/build.sbt +++ b/build.sbt @@ -66,6 +66,7 @@ val common = library("common") pekkoSlf4j, pekkoSerializationJackson, pekkoActorTyped, + supportInternationalisation ) ++ jackson, ) diff --git a/common/app/controllers/EmailSignupController.scala b/common/app/controllers/EmailSignupController.scala index f1d0554bf02..cfd6620f5c7 100644 --- a/common/app/controllers/EmailSignupController.scala +++ b/common/app/controllers/EmailSignupController.scala @@ -1,5 +1,6 @@ package controllers +import com.gu.i18n.{CountryGroup, Country} import com.typesafe.scalalogging.LazyLogging import common.EmailSubsciptionMetrics._ import common.{GuLogging, ImplicitControllerExecutionContext, LinkTo} @@ -12,6 +13,7 @@ import conf.switches.Switches.{ } import model.Cached.{RevalidatableResult, WithoutRevalidationResult} import model._ +import net.liftweb.json.JObject import play.api.data.Forms._ import play.api.data._ import play.api.data.format.Formats._ @@ -60,8 +62,30 @@ class EmailFormService(wsClient: WSClient, emailEmbedAgent: NewsletterSignupAgen extends LazyLogging with RemoteAddress { + private def getCountryAndRegion( + countryCode: String, + )(implicit request: Request[AnyContent]): (String, Option[String]) = { + val registrationLocation: String = CountryGroup.byFastlyCountryCode(countryCode).map(_.name).getOrElse("Other") + val registrationLocationState: Option[String] = + for { + countryCode <- Option(countryCode).filter(Set("US", "AU").contains) + country <- CountryGroup.countryByCode(countryCode) + stateCode <- request.headers.get("X-GU-GeoIP-Region") + stateName <- country.statesByCode.get(stateCode) + } yield stateName + (registrationLocation, registrationLocationState) + } + def submit(form: EmailForm)(implicit request: Request[AnyContent]): Future[WSResponse] = { val consentMailerUrl = serviceUrl(form, emailEmbedAgent) + val countryCode = request.headers.get("X-GU-GeoLocation") match { + case Some(country) => + country.replace("country:", "") + case None => "row" + } + + val (registrationLocation, registrationLocationState) = getCountryAndRegion(countryCode) + val consentMailerPayload = JsObject( Json .obj( @@ -70,6 +94,8 @@ class EmailFormService(wsClient: WSClient, emailEmbedAgent: NewsletterSignupAgen "set-consents" -> form.marketing.filter(_ == true).map(_ => List("similar_guardian_products")), "unset-consents" -> form.marketing.filter(_ == false).map(_ => List("similar_guardian_products")), "browser-id" -> form.browserId, + "registrationLocation" -> registrationLocation, + "registrationLocationState" -> registrationLocationState, ) .fields, ) @@ -87,6 +113,8 @@ class EmailFormService(wsClient: WSClient, emailEmbedAgent: NewsletterSignupAgen } def submitWithMany(form: EmailFormManyNewsletters)(implicit request: Request[AnyContent]): Future[WSResponse] = { + val countryCode = request.headers.get("X-GU-GeoLocation").getOrElse("country:row").replace("country:", "") + val (registrationLocation, registrationLocationState) = getCountryAndRegion(countryCode) val consentMailerPayload = JsObject( Json .obj( @@ -96,6 +124,8 @@ class EmailFormService(wsClient: WSClient, emailEmbedAgent: NewsletterSignupAgen "ref" -> form.ref, "set-consents" -> form.marketing.filter(_ == true).map(_ => List("similar_guardian_products")), "unset-consents" -> form.marketing.filter(_ == false).map(_ => List("similar_guardian_products")), + "registrationLocation" -> registrationLocation, + "registrationLocationState" -> registrationLocationState, ) .fields, ) diff --git a/common/test/controllers/EmailFormServiceTest.scala b/common/test/controllers/EmailFormServiceTest.scala new file mode 100644 index 00000000000..087ab2e8d8c --- /dev/null +++ b/common/test/controllers/EmailFormServiceTest.scala @@ -0,0 +1,234 @@ +package controllers + +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers._ +import org.mockito.Mockito._ +import org.scalatest.concurrent.ScalaFutures +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec +import org.scalatestplus.mockito.MockitoSugar +import play.api.libs.json._ +import play.api.libs.ws._ +import play.api.mvc.AnyContentAsEmpty +import play.api.test.FakeRequest +import services.newsletters.NewsletterSignupAgent +import test.WithTestExecutionContext + +import scala.concurrent.Future + +class EmailFormServiceTest + extends AnyWordSpec + with Matchers + with MockitoSugar + with ScalaFutures + with WithTestExecutionContext { + + trait Fixture { + val wsClient: WSClient = mock[WSClient] + val wsRequest: WSRequest = mock[WSRequest] + val wsResponse: WSResponse = mock[WSResponse] + val newsletterSignupAgent: NewsletterSignupAgent = mock[NewsletterSignupAgent] + + val service = new EmailFormService(wsClient, newsletterSignupAgent) + + val singleNewsletterBaseForm: EmailForm = EmailForm( + email = "test@example.com", + listName = Some("the-long-read"), + marketing = None, + referrer = None, + ref = None, + refViewId = None, + browserId = None, + campaignCode = None, + googleRecaptchaResponse = None, + name = None, + ) + + val multipleNewslettersBaseForm: EmailFormManyNewsletters = EmailFormManyNewsletters( + email = "test@example.com", + listNames = Seq("the-long-read", "morning-briefing"), + marketing = None, + referrer = None, + ref = None, + refViewId = None, + campaignCode = None, + googleRecaptchaResponse = None, + name = None, + ) + + when(newsletterSignupAgent.getV2NewsletterByName(any[String])) thenReturn Left("test") + when(wsClient.url(any[String])) thenReturn wsRequest + when(wsRequest.withQueryStringParameters(any())) thenReturn wsRequest + when(wsRequest.addHttpHeaders(any())) thenReturn wsRequest + when(wsRequest.post(any[JsValue])(any[BodyWritable[JsValue]])) thenReturn Future.successful(wsResponse) + when(wsResponse.status) thenReturn 200 + } + + def capturePostedBody(wsRequest: WSRequest): JsObject = { + val captor: ArgumentCaptor[JsValue] = ArgumentCaptor.forClass(classOf[JsValue]) + verify(wsRequest).post(captor.capture())(any()) + captor.getValue.as[JsObject] + } + + def registrationLocation(body: JsObject): String = + (body \ "registrationLocation").get.as[String] + + def registrationLocationState(body: JsObject): JsValue = + (body \ "registrationLocationState").get + + "EmailFormService.submit" when { + + "getting registrationLocation from X-GU-GeoLocation header" should { + + "use the country group name for a known country code" in new Fixture { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = + FakeRequest().withHeaders("X-GU-GeoLocation" -> "country:GB") + service.submit(singleNewsletterBaseForm).futureValue + registrationLocation(capturePostedBody(wsRequest)) shouldBe "United Kingdom" + } + + "use 'Other' for an unrecognised country code" in new Fixture { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = + FakeRequest().withHeaders("X-GU-GeoLocation" -> "country:XX") + service.submit(singleNewsletterBaseForm).futureValue + registrationLocation(capturePostedBody(wsRequest)) shouldBe "Other" + } + + "use 'Other' when the X-GU-GeoLocation header isn't present" in new Fixture { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + service.submit(singleNewsletterBaseForm).futureValue + registrationLocation(capturePostedBody(wsRequest)) shouldBe "Other" + } + } + + "getting registrationLocationState from X-GU-GeoIP-Region header" should { + + "get US state name from state code" in new Fixture { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders( + "X-GU-GeoLocation" -> "country:US", + "X-GU-GeoIP-Region" -> "CA", + ) + service.submit(singleNewsletterBaseForm).futureValue + registrationLocationState(capturePostedBody(wsRequest)) shouldBe JsString("California") + } + + "get AU state name from state code" in new Fixture { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders( + "X-GU-GeoLocation" -> "country:AU", + "X-GU-GeoIP-Region" -> "NSW", + ) + service.submit(singleNewsletterBaseForm).futureValue + registrationLocationState(capturePostedBody(wsRequest)) shouldBe JsString("New South Wales") + } + + "not be there (None) when the country is US but no state header is present" in new Fixture { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = + FakeRequest().withHeaders("X-GU-GeoLocation" -> "country:US") + service.submit(singleNewsletterBaseForm).futureValue + registrationLocationState(capturePostedBody(wsRequest)) shouldBe JsNull + } + + "not be there (None) when the country is US but the state code is unrecognised" in new Fixture { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders( + "X-GU-GeoLocation" -> "country:US", + "X-GU-GeoIP-Region" -> "ZZ", + ) + service.submit(singleNewsletterBaseForm).futureValue + registrationLocationState(capturePostedBody(wsRequest)) shouldBe JsNull + } + + "not be there (None) for a non-US/AU country even when a region header is present" in new Fixture { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders( + "X-GU-GeoLocation" -> "country:GB", + "X-GU-GeoIP-Region" -> "ENG", + ) + service.submit(singleNewsletterBaseForm).futureValue + registrationLocationState(capturePostedBody(wsRequest)) shouldBe JsNull + } + + "not be there (None) when the X-GU-GeoLocation header is missing" in new Fixture { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + service.submit(singleNewsletterBaseForm).futureValue + registrationLocationState(capturePostedBody(wsRequest)) shouldBe JsNull + } + } + } + + "EmailFormService.submitWithMany" when { + + "getting registrationLocation from X-GU-GeoLocation header" should { + + "use the country group name for a known country code" in new Fixture { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = + FakeRequest().withHeaders("X-GU-GeoLocation" -> "country:GB") + service.submitWithMany(multipleNewslettersBaseForm).futureValue + registrationLocation(capturePostedBody(wsRequest)) shouldBe "United Kingdom" + } + + "use 'Other' for an unrecognised country code" in new Fixture { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = + FakeRequest().withHeaders("X-GU-GeoLocation" -> "country:XX") + service.submitWithMany(multipleNewslettersBaseForm).futureValue + registrationLocation(capturePostedBody(wsRequest)) shouldBe "Other" + } + + "use 'Other' when the X-GU-GeoLocation header isn't present" in new Fixture { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + service.submitWithMany(multipleNewslettersBaseForm).futureValue + registrationLocation(capturePostedBody(wsRequest)) shouldBe "Other" + } + } + + "getting registrationLocationState from X-GU-GeoIP-Region header" should { + + "get US state name from state code" in new Fixture { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders( + "X-GU-GeoLocation" -> "country:US", + "X-GU-GeoIP-Region" -> "CA", + ) + service.submitWithMany(multipleNewslettersBaseForm).futureValue + registrationLocationState(capturePostedBody(wsRequest)) shouldBe JsString("California") + } + + "get AU state name from state code" in new Fixture { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders( + "X-GU-GeoLocation" -> "country:AU", + "X-GU-GeoIP-Region" -> "NSW", + ) + service.submitWithMany(multipleNewslettersBaseForm).futureValue + registrationLocationState(capturePostedBody(wsRequest)) shouldBe JsString("New South Wales") + } + + "not be there (None) when the country is US but no state header is present" in new Fixture { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = + FakeRequest().withHeaders("X-GU-GeoLocation" -> "country:US") + service.submitWithMany(multipleNewslettersBaseForm).futureValue + registrationLocationState(capturePostedBody(wsRequest)) shouldBe JsNull + } + + "not be there (None) when the country is US but the state code is unrecognised" in new Fixture { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders( + "X-GU-GeoLocation" -> "country:US", + "X-GU-GeoIP-Region" -> "ZZ", + ) + service.submitWithMany(multipleNewslettersBaseForm).futureValue + registrationLocationState(capturePostedBody(wsRequest)) shouldBe JsNull + } + + "not be there (None) for a non-US/AU country even when a region header is present" in new Fixture { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders( + "X-GU-GeoLocation" -> "country:GB", + "X-GU-GeoIP-Region" -> "ENG", + ) + service.submitWithMany(multipleNewslettersBaseForm).futureValue + registrationLocationState(capturePostedBody(wsRequest)) shouldBe JsNull + } + + "not be there (None) when the X-GU-GeoLocation header is missing" in new Fixture { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + service.submitWithMany(multipleNewslettersBaseForm).futureValue + registrationLocationState(capturePostedBody(wsRequest)) shouldBe JsNull + } + } + } +}