Skip to content

Commit 3450438

Browse files
authored
Merge pull request #19 from hmrc/DTR-896_v3
DTR-896:
2 parents fa42939 + 3bb1a9b commit 3450438

File tree

16 files changed

+995
-15
lines changed

16 files changed

+995
-15
lines changed

app/config/FrontendAppConfig.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,13 @@ class FrontendAppConfig @Inject() (configuration: Configuration) {
5454
val countdown: Int = configuration.get[Int]("timeout-dialog.countdown")
5555

5656
val cacheTtl: Long = configuration.get[Int]("mongodb.timeToLiveInSeconds")
57+
58+
val sessionTimeOut: Long = configuration.get[Long]("session.timeoutSeconds")
59+
60+
// AddressLookup configuration
61+
private val addressLookupPort: String = configuration.get[String]("address-lookup-frontend.port")
62+
private val addressLookupHost: String = configuration.get[String]("address-lookup-frontend.host")
63+
private val addressLookupProtocol: String = configuration.get[String]("address-lookup-frontend.protocol")
64+
val addressLookupBaseUrl: String = s"$addressLookupProtocol://$addressLookupHost:$addressLookupPort"
65+
val addressLookupTimeoutUrl: String = configuration.get[String]("address-lookup-frontend.timeoutUrl")
5766
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/*
2+
* Copyright 2025 HM Revenue & Customs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package connectors
18+
19+
import config.FrontendAppConfig
20+
import jakarta.inject.Singleton
21+
import models.NormalMode
22+
import models.responses.addresslookup.JourneyInitResponse.AddressLookupResponse
23+
import models.responses.addresslookup.JourneyOutcomeResponse.AddressLookupJourneyOutcome
24+
import play.api.i18n.{Lang, Messages, MessagesApi}
25+
import play.api.libs.json.*
26+
import uk.gov.hmrc.http.client.HttpClientV2
27+
import uk.gov.hmrc.http.{HeaderCarrier, StringContextOps}
28+
29+
import javax.inject.Inject
30+
import scala.concurrent.{ExecutionContext, Future}
31+
import play.api.Logger
32+
33+
@Singleton
34+
class AddressLookupConnector @Inject()(val appConfig: FrontendAppConfig,
35+
http: HttpClientV2,
36+
val messagesApi: MessagesApi)(implicit ec: ExecutionContext) {
37+
38+
private val baseUrl: String = appConfig.addressLookupBaseUrl
39+
val addressLookupInitializeUrl : String = s"$baseUrl/api/v2/init"
40+
val addressLookupOutcomeUrl: String => String = (id: String) => s"$baseUrl/api/v2/confirmed?id=$id"
41+
42+
private val sessionTimeout: Long = appConfig.sessionTimeOut
43+
private val addressLookupTimeoutUrl: String = appConfig.addressLookupTimeoutUrl
44+
45+
private val langResourcePrefix : String = "manageAgents.addressLookup"
46+
47+
private val continueUrl = appConfig.loginContinueUrl +
48+
controllers.manageAgents.routes.AddressLookupController.onSubmit(NormalMode).url
49+
50+
private def setJourneyOptions(): Seq[(String, JsValue)] = {
51+
Seq(
52+
"continueUrl" -> JsString(continueUrl),
53+
54+
"ukMode" -> JsBoolean(true),
55+
// TODO: we expect Welsh translation to be disabled / not working as expected
56+
"disableTranslations" -> JsBoolean(true),
57+
58+
"showPhaseBanner" -> JsBoolean(true),
59+
"alphaPhase" -> JsBoolean(true),
60+
61+
"includeHMRCBranding" -> JsBoolean(true),
62+
63+
"selectPageConfig" -> JsObject(
64+
Seq(
65+
"proposalListLimit" -> JsNumber(30),
66+
"showSearchLinkAgain" -> JsBoolean(true)
67+
)
68+
),
69+
"confirmPageConfig" -> JsObject(
70+
Seq(
71+
"showChangeLink" -> JsBoolean(true),
72+
"showSubHeadingAndInfo" -> JsBoolean(false),
73+
"showSearchAgainLink" -> JsBoolean(false),
74+
"showConfirmChangeText" -> JsBoolean(false),
75+
)
76+
),
77+
"manualAddressEntryConfig" -> JsObject(
78+
Seq(
79+
"line1MaxLength" -> JsNumber(255),
80+
"line2MaxLength" -> JsNumber(255),
81+
"line3MaxLength" -> JsNumber(255),
82+
"townMaxLength" -> JsNumber(255)
83+
)
84+
),
85+
"timeoutConfig" -> JsObject(
86+
Seq(
87+
"timeoutAmount" -> JsNumber(sessionTimeout),
88+
"timeoutUrl" -> JsString(addressLookupTimeoutUrl)
89+
)
90+
),
91+
"pageHeadingStyle" -> JsString("govuk-heading-l")
92+
)
93+
}
94+
95+
private def setLabels(agentName: Option[String], lang : Lang)
96+
(implicit messages: Messages): Seq[(String, JsObject)] = {
97+
Seq(
98+
"appLevelLabels" -> JsObject(
99+
Seq(
100+
"navTitle" -> JsString(messagesApi.preferred( Seq( lang ) )(s"$langResourcePrefix.header.title"))
101+
)
102+
),
103+
"selectPageLabels" -> JsObject(
104+
Seq(
105+
"heading" -> JsString(
106+
messages(
107+
s"$langResourcePrefix.select.heading", agentName.getOrElse("")
108+
)
109+
)
110+
)
111+
),
112+
"lookupPageLabels" -> JsObject(
113+
Seq(
114+
"heading" -> JsString(
115+
messages(
116+
s"$langResourcePrefix.lookup.heading", agentName.getOrElse("")
117+
)
118+
)
119+
)
120+
),
121+
"confirmPageLabels" -> JsObject(
122+
Seq(
123+
"heading" -> JsString(
124+
messages(
125+
s"$langResourcePrefix.confirm.heading", agentName.getOrElse("")
126+
)
127+
),
128+
"changeLinkText" -> JsString(
129+
messages(
130+
s"$langResourcePrefix.confirm.changeLinkText", agentName.getOrElse("")
131+
)
132+
),
133+
)
134+
),
135+
"editPageLabels" -> JsObject(
136+
Seq(
137+
"heading" ->
138+
JsString(
139+
messages(
140+
s"$langResourcePrefix.edit.heading", agentName.getOrElse("")
141+
)
142+
)
143+
)
144+
)
145+
)
146+
}
147+
148+
private def buildConfig(agentName: Option[String])
149+
(implicit messages: Messages): JsValue = {
150+
JsObject(
151+
Seq(
152+
"version" -> JsNumber(2),
153+
"options" -> JsObject(
154+
setJourneyOptions()
155+
),
156+
"labels" -> JsObject(
157+
Seq(
158+
"en" -> JsObject(
159+
setLabels(agentName, Lang("en") )
160+
)
161+
)
162+
)
163+
)
164+
)
165+
}
166+
167+
// Step 1: Journey start/init
168+
def initJourney(agentName: Option[String])
169+
(implicit hc: HeaderCarrier, messages: Messages): Future[AddressLookupResponse] = {
170+
import play.api.libs.ws.writeableOf_JsValue
171+
val payload: JsValue = buildConfig(agentName)
172+
Logger("application").info(s"[AddressLookupConnector] - body: ${Json.stringify(payload)}")
173+
http.post(url"$addressLookupInitializeUrl")
174+
.withBody(payload)
175+
.execute[AddressLookupResponse]
176+
}
177+
178+
// Step 2: Extract journey result/outcome
179+
def getJourneyOutcome(id: String)
180+
(implicit hc: HeaderCarrier): Future[AddressLookupJourneyOutcome] = {
181+
Logger("application").info(s"[AddressLookupConnector] - Extract address: ${addressLookupOutcomeUrl(id)}")
182+
http.get(url"${addressLookupOutcomeUrl(id)}").execute[AddressLookupJourneyOutcome]
183+
}
184+
185+
}

app/controllers/manageAgents/AddressLookupController.scala

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,67 @@
1616

1717
package controllers.manageAgents
1818

19-
import controllers.actions.IdentifierAction
20-
import models.Mode
21-
import javax.inject.{Inject, Singleton}
19+
import cats.data.EitherT
20+
import controllers.actions.{DataRequiredAction, DataRetrievalAction, IdentifierAction, StornRequiredAction}
21+
import controllers.routes.JourneyRecoveryController
22+
import jakarta.inject.Singleton
23+
import models.responses.addresslookup.JourneyInitResponse.JourneyInitSuccessResponse
24+
import models.{Mode, NormalMode, UserAnswers}
25+
import navigation.Navigator
26+
import pages.manageAgents.AgentContactDetailsPage
27+
import play.api.Logger
2228
import play.api.i18n.I18nSupport
2329
import play.api.mvc.{Action, AnyContent, MessagesControllerComponents}
30+
import services.AddressLookupService
2431
import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController
25-
import views.html.IndexView
32+
33+
import javax.inject.Inject
34+
import scala.concurrent.{ExecutionContext, Future}
35+
import scala.util.Try
2636

2737
@Singleton
2838
class AddressLookupController @Inject()(
2939
val controllerComponents: MessagesControllerComponents,
40+
val addressLookupService: AddressLookupService,
3041
identify: IdentifierAction,
31-
view: IndexView
32-
) extends FrontendBaseController with I18nSupport {
42+
getData: DataRetrievalAction,
43+
requireData: DataRequiredAction,
44+
stornRequiredAction: StornRequiredAction,
45+
navigator: Navigator
46+
)(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport {
3347

34-
def onPageLoad(mode: Mode): Action[AnyContent] = identify { implicit request =>
35-
Ok(view())
48+
def onPageLoad(mode: Mode): Action[AnyContent] = (identify andThen getData andThen requireData andThen stornRequiredAction).async { implicit request =>
49+
addressLookupService.initJourney(request.userAnswers, request.storn).map {
50+
case Right(JourneyInitSuccessResponse(Some(addressLookupLocation))) =>
51+
Logger("application").debug(s"[AddressLookupController] - Journey initiated: ${addressLookupLocation}")
52+
Redirect(addressLookupLocation)
53+
case Right(models.responses.addresslookup.JourneyInitResponse.JourneyInitSuccessResponse(None)) =>
54+
Logger("application").error("[AddressLookupController] - Failed::Location not provided")
55+
Redirect(JourneyRecoveryController.onPageLoad())
56+
case Left(ex) =>
57+
Logger("application").error(s"[AddressLookupController] - Failed to Init journey: $ex")
58+
Redirect(JourneyRecoveryController.onPageLoad())
59+
}
3660
}
3761

38-
def onSubmit(mode: Mode): Action[AnyContent] = identify { implicit request =>
39-
Ok(view())
62+
def onSubmit(mode: Mode): Action[AnyContent] = (identify andThen getData andThen requireData andThen stornRequiredAction).async { implicit request => {
63+
Logger("application").debug(s"[AddressLookupController] - UA: ${request.userAnswers}")
64+
for {
65+
id <- EitherT(Future.successful(Try {
66+
request.queryString.get("id").get(0)
67+
}.toEither))
68+
journeyOutcome <- EitherT(addressLookupService.getJourneyOutcome(id, request.userAnswers))
69+
} yield journeyOutcome
70+
}.value.map {
71+
case Right(updatedAnswer) =>
72+
Logger("application").info(s"[AddressLookupController] - address extracted and saved")
73+
Redirect(controllers.manageAgents.routes.AgentContactDetailsController.onPageLoad(NormalMode).url)
74+
// TODO: re-enable this when relevant screens will be merged
75+
// Redirect(navigator.nextPage(AgentContactDetailsPage, mode, updatedAnswer))
76+
case Left(ex) =>
77+
Logger("application").error(s"[AddressLookupController] - failed to extract address: ${ex}")
78+
Redirect(JourneyRecoveryController.onPageLoad())
79+
}
4080
}
41-
}
81+
82+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2025 HM Revenue & Customs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package models.responses.addresslookup
18+
19+
import play.api.http.Status.ACCEPTED
20+
import uk.gov.hmrc.http.{HttpReads, HttpResponse}
21+
import play.api.Logger
22+
23+
object JourneyInitResponse {
24+
25+
case class JourneyInitSuccessResponse(location: Option[String])
26+
27+
case class JourneyInitFailureResponse(status: Int) extends Throwable
28+
29+
type AddressLookupResponse = Either[JourneyInitFailureResponse, JourneyInitSuccessResponse]
30+
31+
implicit def postAddressLookupHttpReads: HttpReads[AddressLookupResponse] =
32+
new HttpReads[AddressLookupResponse] {
33+
34+
override def read(method: String, url: String, response: HttpResponse): AddressLookupResponse = {
35+
response.status match {
36+
case ACCEPTED => Right(
37+
if (response.header(key = "location").isEmpty) {
38+
JourneyInitSuccessResponse(response.header(key = "Location"))
39+
} else {
40+
JourneyInitSuccessResponse(response.header(key = "location"))
41+
}
42+
)
43+
case status =>
44+
Logger("application").error(s"[JourneyInitResponse] - ${response.body}")
45+
Left(JourneyInitFailureResponse(status))
46+
}
47+
}
48+
49+
}
50+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2025 HM Revenue & Customs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package models.responses.addresslookup
18+
19+
import play.api.http.Status.{NOT_FOUND, OK}
20+
import play.api.libs.json.JsSuccess
21+
import uk.gov.hmrc.http.HttpReads
22+
23+
24+
object JourneyOutcomeResponse {
25+
26+
type AddressLookupJourneyOutcome = Either[JourneyResultFailure, Option[JourneyResultAddressModel]]
27+
28+
implicit def getAddressLookupDetailsHttpReads: HttpReads[AddressLookupJourneyOutcome] = HttpReads { (_, _, response) =>
29+
response.status match {
30+
case OK =>
31+
response.json.validate[JourneyResultAddressModel] match {
32+
case JsSuccess(value, _) =>
33+
Right(Some(value))
34+
case _ =>
35+
Left(InvalidJson)
36+
}
37+
case NOT_FOUND =>
38+
Right(None)
39+
case status =>
40+
Left(UnexpectedGetStatusFailure(status))
41+
}
42+
}
43+
44+
sealed trait JourneyResultFailure extends Throwable
45+
46+
private case object InvalidJson extends JourneyResultFailure
47+
48+
case class UnexpectedGetStatusFailure(status: Int) extends JourneyResultFailure
49+
50+
}

0 commit comments

Comments
 (0)