DTR-6349: CIS VR Business Function F26 - Retrieve Last Submitted Batch - #324
DTR-6349: CIS VR Business Function F26 - Retrieve Last Submitted Batch#324samraducan wants to merge 17 commits into
Conversation
|
There was a problem hiding this comment.
CTA selection compares the translated display field verificationStatus against the hardcoded English "Unmatched", so the "Review unmatched subcontractors" button never appears in Welsh locale.
The fix is to add an isUnmatched: Boolean flag to VerificationResultsViewModel and compare on that:
Update VerificationResultsViewModel:
package viewmodels.verify
import models.{Subcontractor, TypeOfSubcontractor, Verification}
import models.response.GetNewestVerificationBatchResponse
import play.api.i18n.Messages
case class VerificationResultsViewModel(
name: String,
verificationStatus: String,
taxTreatment: String,
verificationNumber: String,
isUnmatched: Boolean
)
object VerificationResultsViewModel {
def from(
response: GetNewestVerificationBatchResponse
)(implicit messages: Messages): Seq[VerificationResultsViewModel] = {
val subcontractorsById = response.subcontractors.map(s => s.subcontractorId -> s).toMap
response.verifications.flatMap { verification =>
verification.subcontractorId.flatMap { subId =>
subcontractorsById.get(subId).map { sub =>
VerificationResultsViewModel(
name = nameFor(sub),
verificationStatus = verificationStatusFor(verification),
taxTreatment = taxTreatmentFor(verification),
verificationNumber = verification.verificationNumber.getOrElse(messages("site.unknown")),
isUnmatched = verification.matched.contains("unmatched")
)
}
}
}
}
private def nameFor(sub: Subcontractor)(implicit messages: Messages): String = {
val first = sub.firstName.map(_.trim).filter(_.nonEmpty)
val sur = sub.surname.map(_.trim).filter(_.nonEmpty)
val trading = sub.tradingName.map(_.trim).filter(_.nonEmpty)
val partnershipTrading = sub.partnershipTradingName.map(_.trim).filter(_.nonEmpty)
val individualName: Option[String] =
sur.map(s => first.fold(s)(f => s"$s, $f"))
sub.subcontractorType
.flatMap(TypeOfSubcontractor.fromString)
.map {
case TypeOfSubcontractor.Partnership => partnershipTrading.orElse(trading)
case TypeOfSubcontractor.Limitedcompany | TypeOfSubcontractor.Trust => trading
case TypeOfSubcontractor.Individualorsoletrader => individualName.orElse(trading)
}
.getOrElse(individualName.orElse(trading))
.getOrElse(messages("verify.noName"))
}
// TODO: ALSO, HERE IS SOME OF THE CODE I HAVE REFERRED TO FROM PREVIOUS CODE YOU WROTE FOR A PR COMMENT.
// I was doing some research to understand the words: "net", "gross", "unmatched" and "matched"
// used in verificationStatusFor AND taxTreatmentFor as the verify journey is quite new to me
private def verificationStatusFor(verification: Verification)(implicit messages: Messages): String =
verification.matched match {
case Some("matched") => messages("verify.verificationResults.status.matched")
case Some("unmatched") => messages("verify.verificationResults.status.unmatched")
case _ => messages("site.unknown")
}
private def taxTreatmentFor(verification: Verification)(implicit messages: Messages): String =
verification.taxTreatment match {
case Some("net") => messages("verify.verificationResults.taxTreatment.net")
case Some("gross") => messages("verify.verificationResults.taxTreatment.gross")
case Some("unmatched") => messages("verify.verificationResults.taxTreatment.unmatched")
case _ => messages("site.unknown")
}
}
Update VerificationResultsView:
@import uk.gov.hmrc.govukfrontend.views.viewmodels.content._
@import uk.gov.hmrc.govukfrontend.views.viewmodels.table._
@import viewmodels.verify.VerificationResultsViewModel
@this(
layout: templates.Layout,
govukTable: GovukTable,
govukButton: GovukButton,
h1: components.H1,
paragraph: components.Paragraph,
link: components.Link
)
@(verificationResults: Seq[VerificationResultsViewModel], manageSubcontractorsUrl: String)(implicit request: Request[_], messages: Messages)
@layout(pageTitle = titleNoForm(messages("verify.verificationResults.title"))) {
@h1(messages("verify.verificationResults.heading"))
@paragraph(messages("verify.verificationResults.paragraph"))
@govukTable(
Table(
head = Some(
Seq(
"verify.verificationResults.name",
"verify.verificationResults.status",
"verify.verificationResults.taxTreatment",
"verify.verificationResults.verificationNumber"
).map(key => HeadCell(content = Text(messages(key))))
),
rows = verificationResults.map { result =>
Seq(
TableRow(
content = Text(result.name),
),
TableRow(
content = Text(result.verificationStatus)
),
TableRow(
content = Text(result.taxTreatment)
),
TableRow(
content = Text(result.verificationNumber)
)
)
}
)
)
@if(verificationResults.exists(_.isUnmatched)) {
<div>
@govukButton(
ButtonViewModel(messages("verify.verificationResults.reviewUnmatchedSubcontractors.button"))
)
</div>
} else {
@link(
linkTextKey = "verify.verificationResults.manageYourSubcontractors.link",
linkUrl = manageSubcontractorsUrl,
hasFullStop = true,
prefixTextKey = "verify.verificationResults.backTo"
)
}
}
Update VerificationResultsViewSpec:
package views.verify
import base.SpecBase
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.select.Elements
import play.api.i18n.{Lang, Messages, MessagesApi, MessagesImpl}
import play.api.mvc.Request
import play.api.test.FakeRequest
import play.twirl.api.HtmlFormat
import viewmodels.verify.VerificationResultsViewModel
import views.html.verify.VerificationResultsView
import java.util
class VerificationResultsViewSpec extends SpecBase {
"VerificationResultsView" - {
"must display the Back to Manage your subcontractors link when all subcontractors are verified" in new Setup {
val verificationResults = Seq(
VerificationResultsViewModel(
"Brody, Martin",
"Verified",
"Higher rate",
"V0004528765/A",
isUnmatched = false
),
VerificationResultsViewModel(
"Hooper and Associates",
"Verified",
"Standard rate",
"V0004528765",
isUnmatched = false
),
VerificationResultsViewModel(
"Quint Transportation",
"Verified",
"Higher rate",
"V0004528765/B",
isUnmatched = false
),
VerificationResultsViewModel(
"The Kintner Group",
"Verified",
"Higher rate",
"V0004528765/C",
isUnmatched = false
)
)
val manageSubcontractorsUrl = "/manage-subcontractors/1"
val html: HtmlFormat.Appendable = view(verificationResults, manageSubcontractorsUrl)
val doc: Document = Jsoup.parse(html.body)
doc.select("title").text() must include(messages("verify.verificationResults.title"))
doc.select("h1").text() must include(messages("verify.verificationResults.heading"))
doc.select("p").text() must include(messages("verify.verificationResults.paragraph"))
val headers: util.List[String] = doc.select("thead th").eachText()
headers mustBe util.Arrays.asList(
messages("verify.verificationResults.name"),
messages("verify.verificationResults.status"),
messages("verify.verificationResults.taxTreatment"),
messages("verify.verificationResults.verificationNumber")
)
val rows: Elements = doc.select("tbody tr")
rows.size() mustBe verificationResults.size
verificationResults.zipWithIndex.foreach { case (result, index) =>
val cells = rows.get(index).select("td").eachText()
cells mustBe util.Arrays.asList(
result.name,
result.verificationStatus,
result.taxTreatment,
result.verificationNumber
)
}
doc.select("p").text() must include(messages("verify.verificationResults.backTo"))
doc.select(".govuk-link").text must include(messages("verify.verificationResults.manageYourSubcontractors.link"))
}
"must display the Review unmatched subcontractors button when there is at least one unmatched subcontractor" in new Setup {
val verificationResults = Seq(
VerificationResultsViewModel(
"Brody, Martin",
"Unmatched",
"Higher rate",
"V0004528765/A",
isUnmatched = true
),
VerificationResultsViewModel(
"Hooper and Associates",
"Verified",
"Standard rate",
"V0004528765",
isUnmatched = false
),
VerificationResultsViewModel(
"Quint Transportation",
"Unmatched",
"Higher rate",
"V0004528765/B",
isUnmatched = true
),
VerificationResultsViewModel(
"The Kintner Group",
"Verified",
"Higher rate",
"V0004528765/C",
isUnmatched = false
)
)
val manageSubcontractorsUrl = "/manage-subcontractors/1"
val html: HtmlFormat.Appendable = view(verificationResults, manageSubcontractorsUrl)
val doc: Document = Jsoup.parse(html.body)
val headers: util.List[String] = doc.select("thead th").eachText()
headers mustBe util.Arrays.asList(
messages("verify.verificationResults.name"),
messages("verify.verificationResults.status"),
messages("verify.verificationResults.taxTreatment"),
messages("verify.verificationResults.verificationNumber")
)
val rows: Elements = doc.select("tbody tr")
rows.size() mustBe verificationResults.size
verificationResults.zipWithIndex.foreach { case (result, index) =>
val cells = rows.get(index).select("td").eachText()
cells mustBe util.Arrays.asList(
result.name,
result.verificationStatus,
result.taxTreatment,
result.verificationNumber
)
}
doc.select("button").text() mustBe
messages("verify.verificationResults.reviewUnmatchedSubcontractors.button")
}
}
trait Setup {
implicit val request: Request[_] =
FakeRequest()
implicit val messages: Messages =
MessagesImpl(
Lang.defaultLang,
app.injector.instanceOf[MessagesApi]
)
val view: VerificationResultsView =
app.injector.instanceOf[VerificationResultsView]
}
}
| verify.verificationResults.status.unmatched = Unmatched | ||
| verify.verificationResults.taxTreatment.net = Standard rate | ||
| verify.verificationResults.taxTreatment.gross = Gross | ||
| verify.verificationResults.taxTreatment.unmatched = Higher rate No newline at end of file |
| Ok(view(VerificationResultsViewModel.from(response), manageSubcontractorsUrl)) | ||
| case None => Redirect(controllers.routes.JourneyRecoveryController.onPageLoad()) | ||
| } | ||
| // TODO: Instead of redirecting to JourneyRecoveryController, AC says to navigate to CRR1 (Recovery Page) |
There was a problem hiding this comment.
CRR1 is the Journey Recovery page.
https://cis-prototype-68106471f73b.herokuapp.com/v1-v4/error-handling/crr1
Should AC3 refer to ERR1 which is the System Error page which loags and provides the user with a tracking id ?
https://cis-prototype-68106471f73b.herokuapp.com/v1-v4/error-handling/err1
There was a problem hiding this comment.
nameFor re-implements type-per-type subcontractor name resolution that already exists in SubcontractorViewModel.getSubcontractorName, with a divergent fallback for unknown subcontractortypes.
Update Subcontractor:
package models
import play.api.libs.json.{Json, OFormat}
import java.time.LocalDateTime
import models.TypeOfSubcontractor.*
case class Subcontractor(
subcontractorId: Long,
firstName: Option[String],
secondName: Option[String],
surname: Option[String],
tradingName: Option[String],
partnershipTradingName: Option[String],
verified: Option[String],
verificationNumber: Option[String],
taxTreatment: Option[String],
verificationDate: Option[LocalDateTime],
lastMonthlyReturnDate: Option[LocalDateTime],
createDate: Option[LocalDateTime],
subcontractorType: Option[String],
subbieResourceRef: Option[Long],
utr: Option[String],
partnerUtr: Option[String],
crn: Option[String],
nino: Option[String]
) {
def isVerified: Boolean =
verified.exists(_.equalsIgnoreCase("Y"))
}
object Subcontractor:
given format: OFormat[Subcontractor] = Json.format[Subcontractor]
/** Resolves a display name for a subcontractor using the type-specific field
* selection rules. Returns None when the subcontractor type is unrecognised
* or when all applicable name fields are blank; callers should fall back to a
* localised "no name" message.
*/
def resolveName(sub: Subcontractor): Option[String] = {
def nonBlank(field: Option[String]): Option[String] =
field.map(_.trim).filter(_.nonEmpty)
val trading = nonBlank(sub.tradingName)
val partnershipTrading = nonBlank(sub.partnershipTradingName)
val soleTraderName: Option[String] =
nonBlank(sub.surname).map { surname =>
nonBlank(sub.firstName).fold(surname)(firstName => s"$surname, $firstName")
}
sub.subcontractorType.flatMap(TypeOfSubcontractor.fromString).flatMap {
case Individualorsoletrader => soleTraderName.orElse(trading)
case Limitedcompany => trading
case Partnership => partnershipTrading.orElse(trading)
case Trust => trading
}
}
Update SubcontractorViewModel:
package models
import play.api.libs.json.{Json, OFormat}
import play.api.i18n.Messages
import uk.gov.hmrc.govukfrontend.views.viewmodels.checkboxes.CheckboxItem
import uk.gov.hmrc.govukfrontend.views.viewmodels.content.Text
import viewmodels.govuk.checkbox.CheckboxItemViewModel
case class SubcontractorViewModel(id: String, name: String)
object SubcontractorViewModel {
implicit val format: OFormat[SubcontractorViewModel] = Json.format[SubcontractorViewModel]
def checkboxItems(subcontractors: Seq[SubcontractorViewModel]): Seq[CheckboxItem] =
subcontractors.sortBy(_.name.toLowerCase).zipWithIndex.map { case (sub, index) =>
CheckboxItemViewModel(
content = Text(sub.name),
fieldId = "value",
index = index,
value = sub.id
)
}
def fromSubcontractors(
subcontractors: Seq[Subcontractor]
)(implicit messages: Messages): Seq[SubcontractorViewModel] =
subcontractors.map(fromSubcontractor)
private def fromSubcontractor(subcontractor: Subcontractor)(implicit messages: Messages): SubcontractorViewModel =
SubcontractorViewModel(
id = subcontractor.subcontractorId.toString,
name = Subcontractor.resolveName(subcontractor).getOrElse(messages("verify.noName"))
)
}
Update VerificationResultsViewModel:
package viewmodels.verify
import models.{Subcontractor, Verification}
import models.response.GetNewestVerificationBatchResponse
import play.api.i18n.Messages
case class VerificationResultsViewModel(
name: String,
verificationStatus: String,
taxTreatment: String,
verificationNumber: String,
isUnmatched: Boolean
)
object VerificationResultsViewModel {
def from(
response: GetNewestVerificationBatchResponse
)(implicit messages: Messages): Seq[VerificationResultsViewModel] = {
val subcontractorsById = response.subcontractors.map(s => s.subcontractorId -> s).toMap
response.verifications.flatMap { verification =>
verification.subcontractorId.flatMap { subId =>
subcontractorsById.get(subId).map { sub =>
VerificationResultsViewModel(
name = Subcontractor.resolveName(sub).getOrElse(messages("verify.noName")),
verificationStatus = verificationStatusFor(verification),
taxTreatment = taxTreatmentFor(verification),
verificationNumber = verification.verificationNumber.getOrElse(messages("site.unknown")),
isUnmatched = verification.matched.contains("unmatched")
)
}
}
}
}
private def verificationStatusFor(verification: Verification)(implicit messages: Messages): String =
verification.matched match {
case Some("matched") => messages("verify.verificationResults.status.matched")
case Some("unmatched") => messages("verify.verificationResults.status.unmatched")
case _ => messages("site.unknown")
}
private def taxTreatmentFor(verification: Verification)(implicit messages: Messages): String =
verification.taxTreatment match {
case Some("net") => messages("verify.verificationResults.taxTreatment.net")
case Some("gross") => messages("verify.verificationResults.taxTreatment.gross")
case Some("unmatched") => messages("verify.verificationResults.taxTreatment.unmatched")
case _ => messages("site.unknown")
}
}
# Conflicts: # app/controllers/verify/VerificationResultsController.scala # test/controllers/verify/VerificationResultsControllerSpec.scala
|
Hi @samraducan as discussed, will need to create a new endpoint in formp-proxy to call the VERIFICATION_PROCS.GET_LAST_VERIFICATION_BATCH stored procedure. |
# Conflicts: # conf/messages.en
|
|
No description provided.