Skip to content

Commit

Permalink
MMCA-4972 : Implement search by a specific payment and View transacti…
Browse files Browse the repository at this point in the history
…ons on the Cash Account dashboard (#180)

* MMCA-4972 : Implemented search by a specific payment

* MMCa-4972 : fixed test case 01

* MMCA-4972 : fixed PR comments 01

* MMCA-4972 : changed from valueDate to postingDate to match Acc31

* MMCA-4972 : PR Comment fixes 02
  • Loading branch information
HariHmrc authored Oct 22, 2024
1 parent 4a7a6be commit 1e2d743
Show file tree
Hide file tree
Showing 27 changed files with 1,674 additions and 83 deletions.
46 changes: 33 additions & 13 deletions app/connectors/CustomsFinancialsApiConnector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ import models.request.{
}
import org.slf4j.LoggerFactory
import play.api.http.Status.{
BAD_REQUEST, CREATED, INTERNAL_SERVER_ERROR, NOT_FOUND, OK, REQUEST_ENTITY_TOO_LARGE, SERVICE_UNAVAILABLE
BAD_REQUEST, CREATED, INTERNAL_SERVER_ERROR, NOT_FOUND, OK,
REQUEST_ENTITY_TOO_LARGE, SERVICE_UNAVAILABLE
}
import play.api.libs.ws.JsonBodyWritables.writeableOf_JsValue
import play.api.mvc.AnyContent
import repositories.CacheRepository
import repositories.{CacheRepository, CashAccountSearchRepository}
import services.MetricsReporterService
import uk.gov.hmrc.http.HttpReads.Implicits.*
import uk.gov.hmrc.http.client.HttpClientV2
Expand All @@ -44,13 +45,15 @@ import javax.inject.Inject
import scala.concurrent.{ExecutionContext, Future}
import models.request.CashAccountStatementRequestDetail.jsonBodyWritable
import models.response.CashAccountTransactionSearchResponseDetail
import play.api.libs.json.{JsResult, Json}
import play.api.libs.json.Json
import utils.EtmpErrorCode
import utils.Utils.buildCacheId

class CustomsFinancialsApiConnector @Inject()(httpClient: HttpClientV2,
appConfig: AppConfig,
metricsReporter: MetricsReporterService,
cacheRepository: CacheRepository)
cacheRepository: CacheRepository,
searchRepository: CashAccountSearchRepository)
(implicit executionContext: ExecutionContext) {

private val logger = LoggerFactory.getLogger("application." + getClass.getCanonicalName)
Expand Down Expand Up @@ -142,6 +145,7 @@ class CustomsFinancialsApiConnector @Inject()(httpClient: HttpClientV2,
def retrieveCashTransactionsBySearch(can: String,
ownerEORI: String,
searchType: SearchType.Value,
searchInput: String,
declarationDetails: Option[DeclarationDetailsSearch] = None,
cashAccountPaymentDetails: Option[CashAccountPaymentDetails] = None
)(implicit hc: HeaderCarrier
Expand All @@ -150,10 +154,17 @@ class CustomsFinancialsApiConnector @Inject()(httpClient: HttpClientV2,
val request = CashAccountTransactionSearchRequestDetails(
can, ownerEORI, searchType, declarationDetails, cashAccountPaymentDetails)

httpClient.post(url"$retrieveCashAccountStatementSearchUrl")
.withBody[CashAccountTransactionSearchRequestDetails](request)
.execute[HttpResponse]
.map(processResponseForTransactionsBySearch)
val cacheId = buildCacheId(can, searchInput)

searchRepository.get(cacheId).flatMap {
case Some(value) => Future.successful(Right(value))

case None =>
httpClient.post(url"$retrieveCashAccountStatementSearchUrl")
.withBody[CashAccountTransactionSearchRequestDetails](request)
.execute[HttpResponse]
.map { jsonResponse => processResponseForTransactionsBySearch(cacheId, jsonResponse) }
}
}.recover {
case UpstreamErrorResponse(_, BAD_REQUEST, _, _) =>
logger.error("BAD Request for retrieveCashTransactionsBySearch")
Expand Down Expand Up @@ -265,13 +276,10 @@ class CustomsFinancialsApiConnector @Inject()(httpClient: HttpClientV2,
}
}

private def processResponseForTransactionsBySearch(response: HttpResponse) = {
import CashAccountTransactionSearchResponseDetail.format
private def processResponseForTransactionsBySearch(cacheId: String, response: HttpResponse) = {

response.status match {
case OK =>
Json.fromJson[CashAccountTransactionSearchResponseDetail](response.json)
.asOpt.fold(Left(UnknownException))(Right(_))
case OK => processOKResponse(cacheId, response)

case CREATED => processETMPErrors(response)

Expand All @@ -289,6 +297,18 @@ class CustomsFinancialsApiConnector @Inject()(httpClient: HttpClientV2,
}
}

private def processOKResponse(cacheId: String, response: HttpResponse) = {
val responseDetail = Json.fromJson[CashAccountTransactionSearchResponseDetail](response.json)

searchRepository.set(cacheId, responseDetail.get).map { successfulWrite =>
if (!successfulWrite) {
logger.error("Failed to store data in the session cache defaulting to the api response")
}
}

responseDetail.asOpt.fold(Left(UnknownException))(Right(_))
}

private def processETMPErrors(res: HttpResponse): Either[ErrorResponse, CashAccountTransactionSearchResponseDetail] = {
val errorDetail: Option[ErrorDetail] = Json.fromJson[ErrorDetail](res.json).asOpt

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

import config.{AppConfig, ErrorHandler}
import connectors.CustomsFinancialsApiConnector
import controllers.actions.{EmailAction, IdentifierAction}
import models.response.CashAccountTransactionSearchResponseDetail
import models.CashAccount
import models.request.IdentifierRequest
import play.api.Logging
import play.api.i18n.I18nSupport
import play.api.mvc.{Action, AnyContent, MessagesControllerComponents}
import repositories.CashAccountSearchRepository
import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendController
import utils.Utils.{buildCacheId, extractNumericValue}
import viewmodels.PaymentSearchResultsViewModel
import views.html.{cash_account_declaration_details_search_no_result, cash_account_payment_search}

import javax.inject.Inject
import scala.concurrent.{ExecutionContext, Future}

class CashAccountPaymentSearchController @Inject()(authenticate: IdentifierAction,
verifyEmail: EmailAction,
apiConnector: CustomsFinancialsApiConnector,
errorHandler: ErrorHandler,
mcc: MessagesControllerComponents,
paymentSearchView: cash_account_payment_search,
searchRepository: CashAccountSearchRepository,
noSearchResultView: cash_account_declaration_details_search_no_result
)(implicit executionContext: ExecutionContext,
appConfig: AppConfig
) extends FrontendController(mcc) with I18nSupport with Logging {

def search(searchValue: String,
page: Option[Int]): Action[AnyContent] = (authenticate andThen verifyEmail).async { implicit request =>

apiConnector.getCashAccount(request.eori).flatMap {
case Some(account) => processCashAccountDetails(searchValue, account, page)
case None => Future.successful(NotFound(errorHandler.notFoundTemplate))
}
}

private def processCashAccountDetails(searchValue: String,
account: CashAccount,
page: Option[Int])(implicit request: IdentifierRequest[AnyContent]) = {

searchRepository.get(buildCacheId(account.number, extractNumericValue(searchValue))).flatMap {
case Some(paymentTransfers) => processPaymentTransfersAndDisplayView(searchValue, account, paymentTransfers, page)
case None => Future.successful(Ok(noSearchResultView(page, account.number, searchValue)))
}
}

private def processPaymentTransfersAndDisplayView(searchValue: String,
account: CashAccount,
paymentTransfers: CashAccountTransactionSearchResponseDetail,
page: Option[Int])
(implicit request: IdentifierRequest[AnyContent]) = {

paymentTransfers.paymentsWithdrawalsAndTransfers match {
case Some(paymentsWithdrawalsAndTransfers) =>
val paymentTransfersList = paymentsWithdrawalsAndTransfers.map(_.paymentsWithdrawalsAndTransfer)

Future.successful(
Ok(paymentSearchView(PaymentSearchResultsViewModel(searchValue, account, paymentTransfersList, page)))
)

case None => Future.successful(Ok(noSearchResultView(page, account.number, searchValue)))
}
}
}
57 changes: 39 additions & 18 deletions app/controllers/DeclarationDetailController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,24 @@ import connectors.{CustomsFinancialsApiConnector, ErrorResponse}
import controllers.actions.{EmailAction, IdentifierAction}
import helpers.CashAccountUtils
import models.{CashAccount, CashAccountViewModel, CashTransactions}
import models.request.{DeclarationDetailsSearch, IdentifierRequest, ParamName, SearchType}
import models.response.DeclarationWrapper
import models.request.{CashAccountPaymentDetails, DeclarationDetailsSearch, IdentifierRequest, ParamName, SearchType}
import models.response.{CashAccountTransactionSearchResponseDetail, DeclarationWrapper}
import play.api.i18n.I18nSupport
import play.api.mvc.{Action, AnyContent, MessagesControllerComponents, Result}
import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendController
import views.html.{
cash_account_declaration_details,
cash_account_declaration_details_search,
cash_account_transactions_not_available,
cash_account_declaration_details_search_no_result,
cash_account_declaration_details, cash_account_declaration_details_search,
cash_account_declaration_details_search_no_result, cash_account_transactions_not_available,
cash_transactions_no_result
}

import javax.inject.Inject
import scala.concurrent.{ExecutionContext, Future}
import play.api.Logging
import utils.RegexPatterns.{mrnRegex, paymentRegex}
import viewmodels.{DeclarationDetailSearchViewModel, DeclarationDetailViewModel, ResultsPageSummary}
import connectors.{InvalidCashAccount, InvalidDeclarationReference, DuplicateAckRef, NoAssociatedDataFound, InvalidEori}
import connectors.{DuplicateAckRef, InvalidCashAccount, InvalidDeclarationReference, InvalidEori, NoAssociatedDataFound}
import utils.Utils.extractNumericValue

import java.time.LocalDate

Expand Down Expand Up @@ -84,27 +84,47 @@ class DeclarationDetailController @Inject()(authenticate: IdentifierAction,
page: Option[Int],
searchInput: String)(implicit request: IdentifierRequest[_]): Future[Result] = {

val (paramName, searchType) = determineParamNameAndSearchType(searchInput)
val declarationDetails = Some(DeclarationDetailsSearch(paramName, searchInput))
val (paramName, searchType, sanitizedSearchValue) = determineParamNameAndTypeAndSearchValue(searchInput)

val (declarationDetails, cashAccountPaymentDetails) = searchType match {
case SearchType.D => (Some(DeclarationDetailsSearch(paramName, sanitizedSearchValue)), None)
case SearchType.P => (None, Some(CashAccountPaymentDetails(sanitizedSearchValue.toDouble)))
}

apiConnector.retrieveCashTransactionsBySearch(account.number, request.eori, searchType, declarationDetails).map {
case Right(transactions) => processTransactions(transactions.declarations, searchInput, account, page)
apiConnector.retrieveCashTransactionsBySearch(account.number, request.eori, searchType, sanitizedSearchValue,
declarationDetails, cashAccountPaymentDetails).map {
case Right(transactions) => processTransactions(transactions, searchInput, account, page)
case Left(res) if isBusinessErrorResponse(res) => Ok(noSearchResultView(page, account.number, searchInput))
case Left(_) =>
Ok(transactionsUnavailableView(CashAccountViewModel(request.eori, account), appConfig.transactionsTimeoutFlag))
}
}

private def processTransactions(declarationsOpt: Option[Seq[DeclarationWrapper]],
private def processTransactions(cashAccResDetail: CashAccountTransactionSearchResponseDetail,
searchValue: String,
account: CashAccount,
page: Option[Int])(implicit request: IdentifierRequest[_]): Result = {

declarationsOpt.flatMap(_.headOption.map(_.declaration)) match {
(cashAccResDetail.declarations, cashAccResDetail.paymentsWithdrawalsAndTransfers) match {

case (Some(_), None | Some(Nil)) => processDeclarations(cashAccResDetail, searchValue, account, page)
case (None | Some(Nil), Some(_)) => Redirect(routes.CashAccountPaymentSearchController.search(searchValue, page))
case _ => Ok(noSearchResultView(page, account.number, searchValue))
}
}

private def processDeclarations(cashAccResDetail: CashAccountTransactionSearchResponseDetail,
searchValue: String,
account: CashAccount,
page: Option[Int])(implicit request: IdentifierRequest[_]) = {

cashAccResDetail.declarations.flatMap(_.headOption.map(_.declaration)) match {

case Some(declarationSearch) =>
Ok(searchView(DeclarationDetailSearchViewModel(searchValue, account, declarationSearch), page))

case None => Ok(noSearchResultView(page, account.number, searchValue))
case None =>
Ok(noSearchResultView(page, account.number, searchValue))
}
}

Expand All @@ -115,11 +135,12 @@ class DeclarationDetailController @Inject()(authenticate: IdentifierAction,
businessErrorResponseList.contains(incomingErrorResponse)
}

private def determineParamNameAndSearchType(searchInput: String): (ParamName.Value, SearchType.Value) = {
private def determineParamNameAndTypeAndSearchValue(searchInput: String
): (ParamName.Value, SearchType.Value, String) = {
searchInput match {
case input if isValidMRN(input) => (ParamName.MRN, SearchType.D)
case input if isValidPayment(input) => (ParamName.MRN, SearchType.P)
case _ => (ParamName.UCR, SearchType.D)
case input if isValidMRN(input) => (ParamName.MRN, SearchType.D, searchInput)
case input if isValidPayment(input) => (ParamName.MRN, SearchType.P, extractNumericValue(searchInput))
case _ => (ParamName.UCR, SearchType.D, searchInput)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2023 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 crypto

import models.*
import models.response.CashAccountTransactionSearchResponseDetail
import play.api.libs.json.Json
import javax.inject.Inject

class CashAccountTransactionSearchResponseDetailEncrypter @Inject()(crypto: AesGCMCrypto) {

def encryptSearchResponseDetail(cashTransactions: CashAccountTransactionSearchResponseDetail,
key: String): EncryptedValue = {
val json = Json.toJson(cashTransactions).toString()
crypto.encrypt(json, key)
}

def decryptSearchResponseDetail(encryptedData: EncryptedValue,
key: String): CashAccountTransactionSearchResponseDetail = {
val decryptedJson = crypto.decrypt(encryptedData, key)
Json.parse(decryptedJson).as[CashAccountTransactionSearchResponseDetail]
}

}
Loading

0 comments on commit 1e2d743

Please sign in to comment.