Skip to content

DTR-6349: CIS VR Business Function F26 - Retrieve Last Submitted Batch - #324

Open
samraducan wants to merge 17 commits into
mainfrom
DTR-6349
Open

DTR-6349: CIS VR Business Function F26 - Retrieve Last Submitted Batch#324
samraducan wants to merge 17 commits into
mainfrom
DTR-6349

Conversation

@samraducan

Copy link
Copy Markdown
Contributor

No description provided.

@samraducan
samraducan requested a review from jassalrichy August 7, 2026 09:38
@platops-pr-bot

Copy link
Copy Markdown

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]
  }
}

Comment thread conf/messages.en Outdated
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add new line at eof

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@jassalrichy

Copy link
Copy Markdown
Contributor

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.
Will need to create a new and a new response model and new endpoint within construction-industry-scheme (BE) to call the new formp-proxy endpoint.
Will also need to add the new endpoint to the stubs.

@platops-pr-bot

Copy link
Copy Markdown

@platops-pr-bot

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants