Skip to content

Commit

Permalink
Merge pull request #20 from hmrc/feature/YTA-2946/annual-statement
Browse files Browse the repository at this point in the history
Feature/yta 2946/annual statement
  • Loading branch information
flashingpumpkin authored Dec 4, 2017
2 parents e53b03b + 5c4ff47 commit 51e534f
Show file tree
Hide file tree
Showing 26 changed files with 1,361 additions and 185 deletions.
15 changes: 5 additions & 10 deletions app/uk/gov/hmrc/epayeapi/connectors/EpayeConnector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import javax.inject.{Inject, Singleton}

import uk.gov.hmrc.domain.EmpRef
import uk.gov.hmrc.epayeapi.models.Formats._
import uk.gov.hmrc.epayeapi.models.in.{AnnualSummaryResponse, ApiResponse, EpayeTotalsResponse, TaxYear}
import uk.gov.hmrc.epayeapi.models.in.{EpayeAnnualStatement, ApiResponse, EpayeTotalsResponse, TaxYear}
import uk.gov.hmrc.play.http.HeaderCarrier
import uk.gov.hmrc.play.http.ws.WSHttp

Expand All @@ -45,19 +45,14 @@ case class EpayeConnector @Inject() (
get[EpayeTotalsResponse](url, headers)
}

def getAnnualSummary(empRef: EmpRef, headers: HeaderCarrier, taxYear: Option[String]): Future[ApiResponse[AnnualSummaryResponse]] = {
def getAnnualStatement(empRef: EmpRef, taxYear: TaxYear, headers: HeaderCarrier): Future[ApiResponse[EpayeAnnualStatement]] = {
val url =
s"${config.baseUrl}" +
s"/epaye" +
s"/${empRef.encodedValue}" +
s"/api/v1/annual-statement" +
taxYear
.flatMap(TaxYear.extractTaxYear)
.map(TaxYear.asString)
.map(q => s"/$q")
.getOrElse("")

get[AnnualSummaryResponse](url, headers)
s"/api/v1/annual-statement/${taxYear.asString}"

get[EpayeAnnualStatement](url, headers)
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2017 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 uk.gov.hmrc.epayeapi.controllers

import javax.inject.{Inject, Singleton}

import akka.stream.Materializer
import play.api.Logger
import play.api.libs.json.Json
import play.api.mvc.{Action, EssentialAction}
import uk.gov.hmrc.auth.core.AuthConnector
import uk.gov.hmrc.domain.EmpRef
import uk.gov.hmrc.epayeapi.connectors.EpayeConnector
import uk.gov.hmrc.epayeapi.models.Formats._
import uk.gov.hmrc.epayeapi.models.in._
import uk.gov.hmrc.epayeapi.models.out.ApiError.EmpRefNotFound
import uk.gov.hmrc.epayeapi.models.out.{AnnualStatementJson, ApiError}

import scala.concurrent.ExecutionContext

@Singleton
case class GetAnnualStatementController @Inject()(
authConnector: AuthConnector,
epayeConnector: EpayeConnector,
implicit val ec: ExecutionContext,
implicit val mat: Materializer
)
extends ApiController {

def getAnnualStatement(empRef: EmpRef, taxYear: TaxYear): EssentialAction = EmpRefAction(empRef) {
Action.async { request =>
epayeConnector.getAnnualStatement(empRef, taxYear, hc(request)).map {
case ApiSuccess(epayeAnnualStatement) =>
Ok(Json.toJson(AnnualStatementJson(empRef, taxYear, epayeAnnualStatement)))
case ApiJsonError(err) =>
Logger.error(s"Upstream returned invalid json: $err")
InternalServerError(Json.toJson(ApiError.InternalServerError))
case ApiNotFound() =>
NotFound(Json.toJson(EmpRefNotFound))
case error: ApiResponse[_] =>
Logger.error(s"Error while fetching totals: $error")
InternalServerError(Json.toJson(ApiError.InternalServerError))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ case class Cleared(
credit: BigDecimal = 0
)


case class TaxMonth(month: Int) {
def firstDay(year: TaxYear): LocalDate =
year.firstDay.plusMonths(month - 1)
Expand All @@ -45,7 +44,8 @@ case class LineItem(
balance: DebitAndCredit,
dueDate: LocalDate,
isSpecified: Boolean = false,
codeText: Option[String] = None
codeText: Option[String] = None,
itemType: Option[String] = None
)

case class AnnualTotal(
Expand All @@ -54,6 +54,6 @@ case class AnnualTotal(
balance: DebitAndCredit
)

case class AnnualSummary(lineItems: Seq[LineItem], totals: AnnualTotal)
case class AnnualStatementTable(lineItems: Seq[LineItem], totals: AnnualTotal)

case class AnnualSummaryResponse(rti: AnnualSummary, nonRti: AnnualSummary)
case class EpayeAnnualStatement(rti: AnnualStatementTable, nonRti: AnnualStatementTable, unallocated: Option[BigDecimal])
4 changes: 2 additions & 2 deletions app/uk/gov/hmrc/epayeapi/models/in/Formats.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ trait Formats {
implicit lazy val annualTotalFormat: Format[AnnualTotal] = format[AnnualTotal]
implicit lazy val taxMonthFormat: Format[TaxMonth] = format[TaxMonth]
implicit lazy val lineItemFormat: Format[LineItem] = format[LineItem]
implicit lazy val annualSummaryFormat: Format[AnnualSummary] = format[AnnualSummary]
implicit lazy val annualSummaryResponseFormat: Format[AnnualSummaryResponse] = format[AnnualSummaryResponse]
implicit lazy val annualSummaryFormat: Format[AnnualStatementTable] = format[AnnualStatementTable]
implicit lazy val annualSummaryResponseFormat: Format[EpayeAnnualStatement] = format[EpayeAnnualStatement]

implicit lazy val epayeTotals: Format[EpayeTotals] = format[EpayeTotals]
implicit lazy val epayeTotalsItems: Format[EpayeTotalsItem] = format[EpayeTotalsItem]
Expand Down
12 changes: 8 additions & 4 deletions app/uk/gov/hmrc/epayeapi/models/in/TaxYear.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,19 @@ case class TaxYear(yearFrom: Int) {
val asString: String = s"$yearFrom-${yearTo % 100}"
val firstDay: LocalDate = TaxYearResolver.startOfTaxYear(yearFrom)
val lastDay: LocalDate = firstDay.plusYears(1).minusDays(1)
def next: TaxYear = TaxYear(yearTo)
def previous = TaxYear(yearFrom - 1)
}

object TaxYear {
private lazy val pattern = """20(\d\d)-(\d\d)""".r

def asString(taxYear: TaxYear): String =
s"${taxYear.yearFrom}-${taxYear.yearTo % 100}"
}

def extractTaxYear(taxYear: String): Option[TaxYear] = {
object ExtractTaxYear {
private lazy val pattern = """20(\d\d)-(\d\d)""".r

def unapply(taxYear: String): Option[TaxYear] = {
taxYear match {
case pattern(fromYear, toYear) =>
Try(toYear.toInt - fromYear.toInt) match {
Expand All @@ -44,4 +48,4 @@ object TaxYear {
case _ => None
}
}
}
}
160 changes: 160 additions & 0 deletions app/uk/gov/hmrc/epayeapi/models/out/AnnualStatementJson.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* Copyright 2017 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 uk.gov.hmrc.epayeapi.models.out

import org.joda.time.LocalDate
import uk.gov.hmrc.domain.EmpRef
import uk.gov.hmrc.epayeapi.models.in.{EpayeAnnualStatement, LineItem, TaxYear}

case class TaxYearJson(year: String, firstDay: LocalDate, lastDay: LocalDate)

case class PeriodJson(firstDay: LocalDate, lastDay: LocalDate)

case class NonRtiChargesJson(
code: String,
amount: BigDecimal,
clearedByCredits: BigDecimal,
clearedByPayments: BigDecimal,
balance: BigDecimal,
dueDate: LocalDate
)

object NonRtiChargesJson {
def from(lineItem: LineItem, taxYear: TaxYear): Option[NonRtiChargesJson] = {
for {
code <- lineItem.codeText
} yield NonRtiChargesJson(
code = code,
amount = lineItem.charges.debit,
clearedByCredits = lineItem.cleared.credit,
clearedByPayments = lineItem.cleared.payment,
balance = lineItem.balance.debit,
dueDate = lineItem.dueDate)
}
}

case class PaymentsAndCreditsJson(
payments: BigDecimal,
credits: BigDecimal
)

case class EarlierYearUpdateJson(
amount: BigDecimal,
clearedByCredits: BigDecimal,
clearedByPayments: BigDecimal,
balance: BigDecimal,
dueDate: LocalDate
)

object EarlierYearUpdateJson {
def extractFrom(lineItems: Seq[LineItem]): Option[EarlierYearUpdateJson] = {
lineItems
.find(_.itemType.contains("eyu"))
.map { lineItem =>
EarlierYearUpdateJson(
lineItem.charges.debit,
lineItem.cleared.credit,
lineItem.cleared.payment,
lineItem.balance.debit,
lineItem.dueDate
)
}
}
}

case class EmbeddedRtiChargesJson(
earlierYearUpdate: Option[EarlierYearUpdateJson],
rtiCharges: Seq[MonthlyChargesJson]
)

case class MonthlyChargesJson(
taxMonth: TaxMonthJson,
amount: BigDecimal,
clearedByCredits: BigDecimal,
clearedByPayments: BigDecimal,
balance: BigDecimal,
dueDate: LocalDate,
isSpecified: Boolean,
_links: SelfLink
)

object MonthlyChargesJson {

def from(lineItem: LineItem, empRef: EmpRef, taxYear: TaxYear): Option[MonthlyChargesJson] = {
for {
taxMonth <- lineItem.taxMonth
} yield MonthlyChargesJson(
taxMonth = TaxMonthJson(taxMonth.month, taxMonth.firstDay(taxYear), taxMonth.lastDay(taxYear)),
amount = lineItem.charges.debit,
clearedByCredits = lineItem.cleared.credit,
clearedByPayments = lineItem.cleared.payment,
balance = lineItem.balance.debit,
dueDate = lineItem.dueDate,
isSpecified = lineItem.isSpecified,
_links = SelfLink(Link(s"${AnnualStatementJson.baseUrlFor(empRef)}/statements/${taxYear.asString}/${taxMonth.month}"))
)
}
}

case class TaxMonthJson(
number: Int,
firstDay: LocalDate,
lastDay: LocalDate
)
case class SelfLink(
self: Link
)

case class AnnualStatementLinksJson(
empRefs: Link,
statements: Link,
self: Link,
next: Link,
previous: Link
)

case class AnnualStatementJson(
taxYear: TaxYearJson,
nonRtiCharges: Seq[NonRtiChargesJson],
_embedded: EmbeddedRtiChargesJson,
_links: AnnualStatementLinksJson
)

object AnnualStatementJson {
val baseUrl = "/organisations/paye"

def baseUrlFor(empRef: EmpRef): String =
s"$baseUrl/${empRef.taxOfficeNumber}/${empRef.taxOfficeReference}"

def apply(empRef: EmpRef, taxYear: TaxYear, epayeAnnualStatement: EpayeAnnualStatement): AnnualStatementJson =
AnnualStatementJson(
taxYear = TaxYearJson(taxYear.asString, taxYear.firstDay, taxYear.lastDay),
_embedded = EmbeddedRtiChargesJson(
EarlierYearUpdateJson.extractFrom(epayeAnnualStatement.rti.lineItems),
epayeAnnualStatement.rti.lineItems.flatMap(MonthlyChargesJson.from(_, empRef, taxYear))
),
nonRtiCharges = epayeAnnualStatement.nonRti.lineItems.flatMap(NonRtiChargesJson.from(_, taxYear)),
_links = AnnualStatementLinksJson(
empRefs = Link(baseUrl),
statements = Link(s"${baseUrlFor(empRef)}/statements"),
self = Link(s"${baseUrlFor(empRef)}/statements/${taxYear.asString}"),
next = Link(s"${baseUrlFor(empRef)}/statements/${taxYear.next.asString}"),
previous = Link(s"${baseUrlFor(empRef)}/statements/${taxYear.previous.asString}")
)
)

}
14 changes: 14 additions & 0 deletions app/uk/gov/hmrc/epayeapi/models/out/Formats.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ trait Formats {
}
implicit lazy val linkFormat: Format[Link] = format[Link]

implicit lazy val taxYearJsonFormat: Format[TaxYearJson] = format[TaxYearJson]
implicit lazy val periodJsonFormat: Format[PeriodJson] = format[PeriodJson]
implicit lazy val earlierYearUpdateJsonFormat: Format[EarlierYearUpdateJson] = format[EarlierYearUpdateJson]
implicit lazy val nonRtiChargesJsonFormat: Format[NonRtiChargesJson] = format[NonRtiChargesJson]
implicit lazy val paymentsAndCreditsJsonFormat: Format[PaymentsAndCreditsJson] = format[PaymentsAndCreditsJson]
implicit lazy val embeddedRtiChargesJsonFormat: Format[EmbeddedRtiChargesJson] = format[EmbeddedRtiChargesJson]
implicit lazy val rtiChargesJsonFormat: Format[MonthlyChargesJson] = format[MonthlyChargesJson]
implicit lazy val taxMonthJsonFormat: Format[TaxMonthJson] = format[TaxMonthJson]
implicit lazy val annualStatementLinksJsonFormat: Format[AnnualStatementLinksJson] = format[AnnualStatementLinksJson]
implicit lazy val annualStatementJsonFormat: Format[AnnualStatementJson] = format[AnnualStatementJson]


implicit lazy val selfLinksFormat: Format[SelfLink] = format[SelfLink]

implicit lazy val empRefsLinksFormat: Format[EmpRefsLinks] = format[EmpRefsLinks]
implicit lazy val empRefLinksFormat: Format[EmpRefLinks] = format[EmpRefLinks]
implicit lazy val empRefItemFormat: Format[EmpRefItem] = format[EmpRefItem]
Expand Down
8 changes: 6 additions & 2 deletions app/uk/gov/hmrc/epayeapi/router/ApiRouter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,21 @@ import play.api.routing.Router.Routes
import play.api.routing.{Router, SimpleRouter}
import play.api.routing.sird._
import uk.gov.hmrc.domain.EmpRef
import uk.gov.hmrc.epayeapi.controllers.GetSummaryController
import uk.gov.hmrc.epayeapi.controllers.{GetAnnualStatementController, GetSummaryController}
import uk.gov.hmrc.epayeapi.models.in.{ExtractTaxYear, TaxYear}

@Singleton
case class ApiRouter @Inject() (
prodRoutes: prod.Routes,
getTotalsController: GetSummaryController
getTotalsController: GetSummaryController,
getAnnualStatementController: GetAnnualStatementController
) extends SimpleRouter {

val appRoutes = Router.from {
case GET(p"/$taxOfficeNumber/$taxOfficeReference/") =>
getTotalsController.getSummary(EmpRef(taxOfficeNumber, taxOfficeReference))
case GET(p"/$taxOfficeNumber/$taxOfficeReference/statements/${ExtractTaxYear(taxYear)}") =>
getAnnualStatementController.getAnnualStatement(EmpRef(taxOfficeNumber, taxOfficeReference), taxYear)
}

val routes: Routes = appRoutes.routes.orElse(prodRoutes.routes)
Expand Down
Loading

0 comments on commit 51e534f

Please sign in to comment.